一、引言
在 WebGL 中,纹理是创建逼真 3D 场景不可或缺的一部分。它可以为物体表面添加细节,使其看起来更加真实和生动。纹理可以是图像、图案或者其他形式的数据,通过将其映射到 3D 模型的表面,能够极大地增强场景的视觉效果。本文将从基础的纹理概念入手,逐步深入探讨 WebGL 中纹理的使用方法,包含大量的代码示例,帮助你更好地理解和掌握这一重要技术。
二、纹理基础概念
2.1 什么是纹理
纹理可以理解为一张图像,它被 “贴” 在 3D 模型的表面。例如,在创建一个虚拟的木质箱子时,我们可以使用一张木纹的图像作为纹理,这样箱子看起来就像由真实的木材制成。在 WebGL 中,纹理可以是 JPEG、PNG 等常见的图像格式。
2.2 纹理坐标
纹理坐标是用来指定纹理图像上的点如何映射到 3D 模型表面的坐标系统。它的范围是从 0 到 1,其中 (0, 0) 表示纹理图像的左下角,(1, 1) 表示右上角。例如,纹理坐标 (0.5, 0.5) 表示纹理图像的中心点。
2.3 纹理单元
WebGL 支持多个纹理单元,每个纹理单元可以存储一个纹理。在使用多个纹理时,需要指定使用哪个纹理单元。纹理单元通过编号来标识,从 0 开始,例如 gl.TEXTURE0
、gl.TEXTURE1
等。
三、基础纹理加载与使用
3.1 初始化 WebGL 上下文
首先,我们需要创建一个 WebGL 上下文,这是使用 WebGL 的基础。以下是创建 WebGL 上下文的代码:
function initWebGL(canvas) {
var gl = null;
try {
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
} catch (e) {}
if (!gl) {
alert("无法初始化 WebGL,你的浏览器可能不支持。");
return null;
}
return gl;
}
3.2 创建和绑定纹理
接下来,我们要创建一个纹理对象,并将其绑定到当前的纹理单元。代码如下:
function createAndBindTexture(gl) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
return texture;
}
3.3 加载纹理图像
我们使用 JavaScript 的 Image
对象来加载纹理图像。在图像加载完成后,将其上传到 WebGL 纹理对象中。示例代码如下:
function loadTexture(gl, url) {
var texture = createAndBindTexture(gl);
var image = new Image();
image.onload = function () {
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 = url;
return texture;
}
3.4 顶点着色器和片元着色器
顶点着色器用于处理顶点的位置和纹理坐标,片元着色器用于对每个像素进行颜色计算。以下是一个简单的顶点着色器和片元着色器的代码示例:
// 顶点着色器代码
var vertexShaderSource = `
attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = a_position;
v_texCoord = a_texCoord;
}
`;
// 片元着色器代码
var fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`;
3.5 完整示例代码
将上述步骤整合起来,形成一个完整的示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebGL 纹理示例</title>
</head>
<body>
<canvas id="glCanvas" width="640" height="480"></canvas>
<script>
function initWebGL(canvas) {
var gl = null;
try {
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
} catch (e) {}
if (!gl) {
alert("无法初始化 WebGL,你的浏览器可能不支持。");
return null;
}
return gl;
}
function createAndBindTexture(gl) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
return texture;
}
function loadTexture(gl, url) {
var texture = createAndBindTexture(gl);
var image = new Image();
image.onload = function () {
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 = url;
return texture;
}
function createShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
function createProgram(gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
function main() {
var canvas = document.getElementById("glCanvas");
var gl = initWebGL(canvas);
if (!gl) {
return;
}
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
var program = createProgram(gl, vertexShader, fragmentShader);
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
var textureUniformLocation = gl.getUniformLocation(program, "u_texture");
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
var texCoords = [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);
var texture = loadTexture(gl, "texture.jpg");
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
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(textureUniformLocation, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
var vertexShaderSource = `
attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = a_position;
v_texCoord = a_texCoord;
}
`;
var fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`;
main();
</script>
</body>
</html>
在这个示例中,我们创建了一个简单的 WebGL 场景,将一张纹理图像应用到一个矩形上。通过上述代码,我们可以看到纹理加载和使用的基本流程:初始化 WebGL 上下文、创建和绑定纹理、加载纹理图像、编写着色器代码、设置顶点和纹理坐标,最后进行渲染。
四、纹理参数设置
4.1 纹理过滤
纹理过滤决定了当纹理被放大或缩小时如何处理。WebGL 提供了几种不同的过滤模式,包括最近邻过滤(gl.NEAREST
)和线性过滤(gl.LINEAR
)。以下是设置纹理过滤参数的代码示例:
function loadTexture(gl, url) {
var texture = createAndBindTexture(gl);
var image = new Image();
image.onload = function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 设置纹理过滤参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.generateMipmap(gl.TEXTURE_2D);
};
image.src = url;
return texture;
}
在上述代码中,gl.TEXTURE_MIN_FILTER
用于设置纹理缩小过滤模式,gl.TEXTURE_MAG_FILTER
用于设置纹理放大过滤模式。gl.LINEAR
表示使用线性过滤,它会在多个纹素之间进行插值,使纹理看起来更加平滑。
4.2 纹理环绕
纹理环绕决定了当纹理坐标超出 [0, 1] 范围时如何处理。WebGL 提供了几种环绕模式,如 gl.REPEAT
(重复纹理)、gl.CLAMP_TO_EDGE
(边缘拉伸)和 gl.MIRRORED_REPEAT
(镜像重复)。以下是设置纹理环绕参数的代码示例:
function loadTexture(gl, url) {
var texture = createAndBindTexture(gl);
var image = new Image();
image.onload = function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 设置纹理环绕参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.generateMipmap(gl.TEXTURE_2D);
};
image.src = url;
return texture;
}
在上述代码中,gl.TEXTURE_WRAP_S
用于设置纹理在水平方向的环绕模式,gl.TEXTURE_WRAP_T
用于设置纹理在垂直方向的环绕模式。gl.REPEAT
表示纹理会重复显示。
五、多纹理使用
在某些情况下,我们可能需要同时使用多个纹理来创建更复杂的效果。以下是一个使用两个纹理的示例代码:
// 顶点着色器代码
var vertexShaderSource = `
attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = a_position;
v_texCoord = a_texCoord;
}
`;
// 片元着色器代码
var fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
varying vec2 v_texCoord;
void main() {
vec4 color1 = texture2D(u_texture1, v_texCoord);
vec4 color2 = texture2D(u_texture2, v_texCoord);
gl_FragColor = mix(color1, color2, 0.5);
}
`;
function main() {
var canvas = document.getElementById("glCanvas");
var gl = initWebGL(canvas);
if (!gl) {
return;
}
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
var program = createProgram(gl, vertexShader, fragmentShader);
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
var texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
var texture1UniformLocation = gl.getUniformLocation(program, "u_texture1");
var texture2UniformLocation = gl.getUniformLocation(program, "u_texture2");
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
var positions = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
var texCoords = [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);
var texture1 = loadTexture(gl, "texture1.jpg");
var texture2 = loadTexture(gl, "texture2.jpg");
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
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, texture1);
gl.uniform1i(texture1UniformLocation, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture2);
gl.uniform1i(texture2UniformLocation, 1);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
在这个示例中,我们在片元着色器中定义了两个 sampler2D
类型的统一变量 u_texture1
和 u_texture2
,分别对应两个纹理。在 JavaScript 代码中,我们使用 gl.activeTexture
激活不同的纹理单元,并将纹理绑定到相应的单元上,最后通过 gl.uniform1i
将纹理单元编号传递给着色器。
六、纹理压缩
纹理压缩可以减少纹理数据的存储空间,提高加载速度和渲染性能。WebGL 支持多种纹理压缩格式,如 DXT、ETC 等。以下是一个使用纹理压缩的示例代码:
function loadCompressedTexture(gl, url, format) {
var texture = createAndBindTexture(gl);
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var data = new Uint8Array(arrayBuffer);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, data);
gl.generateMipmap(gl.TEXTURE_2D);
}
};
xhr.send(null);
return texture;
}
在上述代码中,我们使用 XMLHttpRequest
加载压缩纹理数据,并将其传递给 gl.texImage2D
函数。需要注意的是,不同的纹理压缩格式需要使用不同的 format
参数。
七、结论
通过本文的介绍,我们了解了 WebGL 中纹理的基本概念、加载和使用方法,以及纹理参数设置、多纹理使用和纹理压缩等高级技术。纹理是 WebGL 中非常重要的一部分,它可以为 3D 场景带来更加真实和生动的效果。希望通过本文的代码示例和讲解,你能够更好地掌握 WebGL 纹理的使用,创造出更加精彩的 WebGL 应用。在实际开发中,还可以进一步探索纹理的更多应用,如纹理动画、纹理映射变形等,不断提升 WebGL 应用的质量和性能。