目录
- 理解纹理
- 理解材质
- 纹理映射
- 高级纹理技术
- 实现材质系统
- 纹理坐标和纹理坐标的生成
- 纹理映射模式
- 纹理过滤
- 纹理压缩
- 纹理单元和纹理数组
- 动态纹理和视频纹理
- 纹理映射的变形
- 材质属性的扩展
- 材质库
- 纹理混合
- 性能优化
- 总结
在WebGL中,纹理和材质是实现3D物体表面视觉效果的关键。纹理是图像数据,而材质则是如何应用这些纹理以模拟物体表面的属性。
理解纹理
纹理是二维图像,可以映射到3D物体的表面上,为物体增添细节和真实感。在WebGL中,我们通常使用TEXTURE_2D类型的纹理,它们可以是普通的RGB或RGBA图像,也可以是特殊的立方体贴图、深度贴图等。
创建纹理对象
javascript
const texture = gl.createTexture();
加载纹理图像
javascript
const image = new Image();
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
};
image.src = 'texture.jpg';
设置纹理参数
纹理参数会影响纹理的过滤方式(如何插值纹理坐标)和边缘处理:
javascript
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); // 水平方向重复
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); // 垂直方向重复
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); // 使用线性过滤和MIP贴图
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // 使用线性过滤
理解材质
材质是描述物体表面属性的参数集合,包括颜色、光泽、透明度、粗糙度等。在WebGL中,材质通常通过着色器程序实现,通过传入不同的 uniforms 来控制材质属性。
常见材质属性
- 颜色(Color):物体的基本颜色,可以通过vec3或vec4传递。
- 镜面反射系数(Shininess):影响镜面高光的强度。
- 环境光(Ambient Light):环境对物体的全局照明。
- 漫射光(Diffuse Light):模拟光源直接照射到物体的效果。
- 镜面光(Specular Light):模拟镜面反射的效果。
- 透明度(Opacity):物体的不透明程度,通常通过float值传递。
应用材质
在顶点着色器中,通常不需要材质属性,但在片段着色器中,你需要根据材质属性计算最终的颜色:
glsl
precision mediump float;
uniform vec3 u_color;
uniform float u_opacity;
uniform vec3 u_ambientLight;
uniform vec3 u_diffuseLight;
uniform vec3 u_specularLight;
uniform vec3 u_shininess;
uniform vec3 u_eyePosition;
// ...其他输入...
void main() {
vec3 ambient = u_ambientLight;
vec3 diffuse = u_diffuseLight * max(dot(normal, lightDirection), 0.0);
vec3 specular = u_specularLight * pow(max(dot(normal, reflect(-lightDirection, normal)), 0.0), u_shininess);
vec3 litColor = ambient + diffuse + specular;
vec4 finalColor = vec4(litColor * u_color, u_opacity);
gl_FragColor = finalColor;
}
纹理映射
将纹理应用到3D物体上,需要在顶点着色器中设置纹理坐标属性,并在片段着色器中采样纹理:
glsl
// 顶点着色器
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
// ...其他计算...
v_texCoord = a_texCoord;
}
// 片段着色器
uniform sampler2D u_texture;
void main() {
vec4 texColor = texture2D(u_texture, v_texCoord);
vec3 litColor = ... // 计算光照
gl_FragColor = vec4(litColor * texColor.rgb, texColor.a);
}
在主程序中,设置纹理坐标属性和纹理单元:
javascript
const texCoordAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_texCoord');
gl.enableVertexAttribArray(texCoordAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(shaderProgram.uniforms.u_texture, 0);
高级纹理技术
- 立方体贴图(Cubemaps):用于模拟环境反射。
- 法线贴图(Normal Maps):模拟物体表面的凹凸感。
- 环境光遮罩(Environment Masking):用于控制光照的边缘效果。
- 位移贴图(Displacement Maps):通过改变表面的高度来模拟物体的细节。
- 颜色空间转换:例如从sRGB转换到线性空间,以正确处理颜色表现。
实现材质系统
为了简化材质的管理和使用,可以创建一个材质类,包含材质属性和方法,用于设置和应用材质:
javascript
class Material {
constructor(color, shininess) {
this.color = color;
this.shininess = shininess;
// ...其他材质属性...
}
applyMaterial(shaderProgram) {
gl.uniform3fv(shaderProgram.uniforms.u_color, this.color);
gl.uniform1f(shaderProgram.uniforms.u_shininess, this.shininess);
// ...应用其他材质属性...
}
}
const myMaterial = new Material([1, 0, 0], 100);
在绘制物体时,先调用applyMaterial方法:
javascript
myMaterial.applyMaterial(shaderProgram);
drawObject();
纹理坐标和纹理坐标的生成
纹理坐标是将纹理映射到3D物体表面的关键。通常,每个顶点都有一个对应的纹理坐标,用于在片段着色器中采样纹理。纹理坐标范围通常在[0, 1]之间,(0, 0)对应纹理左下角,(1, 1)对应右上角。
手动设置纹理坐标
如果你知道每个顶点应该映射到纹理的哪个部分,可以直接在顶点数据中指定纹理坐标:
javascript
const vertices = [
// 顶点位置和纹理坐标
-1, -1, 0, 0, 0,
1, -1, 1, 0,
1, 1, 1, 1,
-1, 1, 0, 1
];
// ...创建顶点缓冲对象和纹理坐标缓冲对象...
自动生成纹理坐标
有时,你可能希望纹理按照特定方式拉伸或重复。在这种情况下,可以编写一个函数自动生成纹理坐标:
javascript
function generateTextureCoordinates(numVertices) {
const textureCoordinates = new Float32Array(numVertices * 2);
for (let i = 0; i < numVertices; i++) {
const u = i / (numVertices - 1);
const v = i % 2 === 0 ? 0 : 1;
textureCoordinates[i * 2] = u;
textureCoordinates[i * 2 + 1] = v;
}
return textureCoordinates;
}
纹理映射模式
WebGL提供了多种纹理映射模式,通过TEXTURE_WRAP_S和TEXTURE_WRAP_T
纹理参数控制水平和垂直方向的纹理重复:
- CLAMP_TO_EDGE:边缘处的纹理坐标被固定在0或1,不会重复。
- REPEAT:纹理会水平和垂直方向重复。
- MIRRORED_REPEAT:纹理会水平和垂直方向重复,每次翻转一次。
纹理过滤
纹理过滤决定了当纹理坐标在[0, 1]范围内之外时,如何插值纹理颜色。通过TEXTURE_MIN_FILTER和TEXTURE_MAG_FILTER
纹理参数控制:
NEAREST
:使用最近的颜色像素,结果可能会有明显的像素化。LINEAR
:线性插值,更平滑,但可能会模糊细节。NEAREST_MIPMAP_NEAREST
和LINEAR_MIPMAP_NEAREST
:在MIP贴图中选择最近的级别进行插值。NEAREST_MIPMAP_LINEAR
和LINEAR_MIPMAP_LINEAR
:在MIP贴图中线性插值。
纹理压缩
纹理压缩可以减少内存占用和带宽需求,特别是在移动设备上。WebGL支持多种纹理压缩格式,如S3TC (DXTn)、ETC1、PVRTC等。使用压缩纹理时,需要检查浏览器/设备是否支持,并使用相应的扩展。
javascript
const compressedTexImage2D = gl.compressedTexImage2D;
const COMPRESSED_RGB_S3TC_DXT1_EXT = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS)[0];
compressedTexImage2D(gl.TEXTURE_2D, 0, COMPRESSED_RGB_S3TC_DXT1_EXT, width, height, 0, data);
纹理单元和纹理数组
WebGL支持多个纹理单元,允许同时应用多个纹理。通过gl.activeTexture()切换纹理单元,gl.uniform1i()设置着色器中的纹理单元位置。
javascript
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture2);
gl.uniform1i(shader.uniforms.u_texture2, 1);
纹理数组允许在一个纹理对象中存储多个独立的2D纹理,通过纹理坐标的一个额外维度访问。
动态纹理和视频纹理
WebGL允许动态更新纹理内容,这对于实时渲染或使用视频作为纹理特别有用:
javascript
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoElement);
在视频播放时,定期更新纹理:
javascript
function updateTexture() {
if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, videoElement);
}
requestAnimationFrame(updateTexture);
}
updateTexture();
纹理映射的变形
除了简单的平面映射,WebGL还支持多种纹理映射技术,以实现更复杂的表面效果:
- 偏移和缩放(Offset and Scale):通过设置纹理坐标,可以改变纹理在物体表面的位置和大小。
javascript
vec2 offset = vec2(0.1, 0.2);
vec2 scale = vec2(1.5, 0.8);
v_texCoord = (a_texCoord - offset) * scale;
- 扭曲(Tiling and Wrapping):使用纹理坐标和数学函数(如sin、cos)来创建扭曲效果。
javascript
v_texCoord = vec2(
a_texCoord.x + sin(a_texCoord.y * 10.0) * 0.1,
a_texCoord.y + cos(a_texCoord.x * 10.0) * 0.1
);
- 偏移映射(Parallax Mapping):根据法线和视角,模拟物体表面的深度感。
- 立方体贴图环境映射(Cube Map Environment Mapping):使用6个面的立方体贴图,模拟物体对周围环境的反射。
材质属性的扩展
除了基本的颜色、光泽和透明度,材质还可以包含更多属性,以模拟更复杂的表面效果:
- 金属度(Metallic):表示物体是否像金属一样反射光线。
- 粗糙度(Roughness):影响镜面高光的扩散程度。
- AO(Ambient Occlusion):模拟物体表面的阴影,增加深度感。
- 法线(Normal):用于法线映射,改变物体表面的光照效果。
- 环境光遮罩(Environment Masking):控制光照在物体边缘的衰减。
在着色器中,可以根据这些属性计算最终的颜色:
glsl
// ...其他输入...
vec3 baseColor = u_baseColor.rgb;
float metallic = u_metallic;
float roughness = u_roughness;
vec3 ao = u_ao;
vec3 normal = normalize(u_normal);
vec3 reflectedColor = ... // 使用立方体贴图计算反射色
vec3 ambient = u_ambientLight * baseColor * ao;
vec3 diffuse = u_diffuseLight * max(dot(normal, lightDirection), 0.0);
vec3 specular = ... // 根据metallic、normal和viewDirection计算
vec3 litColor = ambient + diffuse + specular + reflectedColor;
vec4 finalColor = vec4(litColor, u_opacity);
gl_FragColor = finalColor;
材质库
为了更好地组织和管理材质,可以创建一个材质库,存储各种预设的材质,便于在场景中复用:
javascript
class MaterialLibrary {
constructor() {
this.materials = {};
}
add(name, material) {
this.materials[name] = material;
}
get(name) {
return this.materials[name];
}
}
const materialLibrary = new MaterialLibrary();
materialLibrary.add('red', new Material([1, 0, 0], 100));
在绘制物体时,从库中获取并应用材质:
javascript
const myMaterial = materialLibrary.get('red');
myMaterial.applyMaterial(shaderProgram);
drawObject();
纹理混合
通过在片段着色器中混合多个纹理,可以实现复杂的表面效果,如混合不同的纹理层、实现渐变和过渡效果:
glsl
uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
void main() {
vec4 texColor1 = texture2D(u_texture1, v_texCoord);
vec4 texColor2 = texture2D(u_texture2, v_texCoord);
vec4 finalColor = mix(texColor1, texColor2, u_mixFactor);
gl_FragColor = finalColor;
}
性能优化
- 纹理压缩:减少纹理的内存占用和加载时间。
- 纹理合并:将多个小纹理合并为一个大纹理,减少纹理切换的开销。
- MIP贴图:提高纹理采样速度,尤其是在物体远离相机时。
- 纹理过滤:选择合适的纹理过滤策略,平衡质量与性能。
- 纹理单元复用:尽量减少纹理单元的切换。
总结
WebGL中的纹理和材质是实现3D图形真实感的关键。通过理解它们的工作原理,以及如何创建和应用,你将能够创建出丰富的3D场景。结合高级纹理技术,如法线映射、环境光遮罩等,可以进一步提升视觉效果。在实践中不断学习和探索,你将能够掌握更多的3D图形编程技巧。