在 WebGL 中使用纹理
加载纹理
首先要添加加载纹理的代码。在本例中,我们将使用单个纹理,映射到旋转立方体的六个面上,但同样的技术可以用于任意数量的纹理。
注意:将这两个函数添加到您的“webgl-demo.js”脚本中
//
// Initialize a texture and load an image.
// When the image finished loading copy it into the texture.
//
function loadTexture(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Because images have to be downloaded over the internet
// they might take a moment until they are ready.
// Until then put a single pixel in the texture so we can
// use it immediately. When the image has finished downloading
// we'll update the texture with the contents of the image.
const level = 0;
const internalFormat = gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
gl.texImage2D(
gl.TEXTURE_2D,
level,
internalFormat,
width,
height,
border,
srcFormat,
srcType,
pixel,
);
const image = new Image();
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
level,
internalFormat,
srcFormat,
srcType,
image,
);
// WebGL1 has different requirements for power of 2 images
// vs. non power of 2 images so check if the image is a
// power of 2 in both dimensions.
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
// Yes, it's a power of 2. Generate mips.
gl.generateMipmap(gl.TEXTURE_2D);
} else {
// No, it's not a power of 2. Turn off mips and set
// wrapping to clamp to edge
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
};
image.src = url;
return texture;
}
function isPowerOf2(value) {
return (value & (value - 1)) === 0;
}
loadTexture()
例程首先通过调用 WebGL createTexture()
函数创建一个 WebGL 纹理对象 texture
。然后,它使用 texImage2D()
上传单个蓝色像素。这使得纹理可以立即用作纯蓝色,即使我们的图像可能需要一段时间才能下载。
要从图像文件加载纹理,它会创建一个 Image
对象并将 src
设置为我们希望用作纹理的图像的 URL。分配给 image.onload
的函数将在图像下载完成后被调用。此时,我们再次调用 texImage2D()
,这次使用图像作为纹理的源。之后,我们根据下载的图像在两个维度上是否为 2 的幂来设置纹理的过滤和环绕方式。
WebGL1 只能使用过滤设置为 NEAREST
或 LINEAR
的非 2 的幂纹理,并且不能为它们生成 mipmap。它们的环绕模式也必须设置为 CLAMP_TO_EDGE
。另一方面,如果纹理在两个维度上都是 2 的幂,则 WebGL 可以进行更高质量的过滤,可以使用 mipmap,并且可以将环绕模式设置为 REPEAT
或 MIRRORED_REPEAT
。
重复纹理的一个例子是用几块砖的图像平铺来覆盖砖墙。
可以使用 texParameteri()
禁用 Mipmapping 和 UV 重复。这将允许非 2 的幂 (NPOT) 纹理,但代价是 mipmap、UV 环绕、UV 平铺以及您对设备如何处理纹理的控制。
// gl.NEAREST is also allowed, instead of gl.LINEAR, as neither mipmap.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Prevents s-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// Prevents t-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
同样,使用这些参数,兼容的 WebGL 设备将自动接受该纹理的任何分辨率(直至其最大尺寸)。如果不执行上述配置,WebGL 将要求所有 NPOT 纹理的样本都失败,并返回透明黑色:rgb(0 0 0 / 0%)
。
要加载图像,请在我们的 main()
函数中添加对 loadTexture()
函数的调用。这可以在 initBuffers(gl)
调用之后添加。
但也要注意:浏览器从加载的图像中从上到下复制像素——从左上角开始;但 WebGL 想要从下到上复制像素——从左下角开始。(有关更多详细信息,请参阅为什么我的 WebGL 纹理上下颠倒?)。
因此,为了防止渲染时生成的图像纹理方向错误,我们还需要使用 gl.UNPACK_FLIP_Y_WEBGL
参数设置为 true
调用 pixelStorei()
——使像素翻转到 WebGL 预期的从下到上的顺序。
注意:将以下代码添加到您的 main()
函数中,紧接在 initBuffers()
的调用之后
// Load texture
const texture = loadTexture(gl, "cubetexture.png");
// Flip image pixels into the bottom-to-top order that WebGL expects.
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
注意:最后,将 cubetexture.png 文件下载到与您的 JavaScript 文件相同的本地目录中。
将纹理映射到面上
此时,纹理已加载并可以使用。但在我们使用它之前,我们需要建立纹理坐标到立方体面的顶点的映射。这将替换 initBuffers()
中之前所有为立方体每个面的颜色配置设置的代码。
注意:将此函数添加到您的“init-buffer.js”模块中
function initTextureBuffer(gl) {
const textureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
const textureCoordinates = [
// Front
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
// Back
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
// Top
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
// Bottom
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
// Right
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
// Left
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
];
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(textureCoordinates),
gl.STATIC_DRAW,
);
return textureCoordBuffer;
}
首先,此代码创建一个 WebGL 缓冲区,我们将把每个面的纹理坐标存储到其中,然后我们将该缓冲区绑定为我们将写入的数组。
textureCoordinates
数组定义了对应于每个面的每个顶点的纹理坐标。请注意,纹理坐标范围为 0.0 到 1.0;为了进行纹理映射,无论纹理的实际大小如何,纹理的尺寸都将标准化为 0.0 到 1.0 的范围。
设置完纹理映射数组后,我们将数组传递到缓冲区中,以便 WebGL 准备好该数据以供使用。
然后我们返回新的缓冲区。
接下来,我们需要更新 initBuffers()
以创建和返回纹理坐标缓冲区,而不是颜色缓冲区。
注意:在“init-buffers.js”模块的 initBuffers()
函数中,用以下行替换对 initColorBuffer()
的调用
const textureCoordBuffer = initTextureBuffer(gl);
注意:在“init-buffers.js”模块的 initBuffers()
函数中,用以下内容替换 return
语句
return {
position: positionBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
更新着色器
着色器程序也需要更新以使用纹理而不是纯色。
顶点着色器
我们需要替换顶点着色器,以便它不是获取颜色数据,而是获取纹理坐标数据。
注意:像这样更新 main()
函数中的 vsSource
声明
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec2 vTextureCoord;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vTextureCoord = aTextureCoord;
}
`;
这里的主要变化是,我们不是获取顶点颜色,而是获取纹理坐标并将其传递给顶点着色器;这将指示对应于顶点的纹理中的位置。
片段着色器
片段着色器也需要更新。
注意:像这样更新 main()
函数中的 fsSource
声明
const fsSource = `
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
out vec4 fragColor;
void main(void) {
fragColor = texture(uSampler, vTextureCoord);
}
`;
片段的颜色不是通过为片段的颜色分配颜色值来计算的,而是通过根据 vTextureCoord
的值获取纹理元素(即纹理中的像素)来计算的,该值与颜色一样在顶点之间进行插值。
属性和统一变量位置
因为我们更改了一个属性并添加了一个统一变量,所以我们需要查找它们的位置。
注意:像这样更新 main()
函数中的 programInfo
声明
const programInfo = {
program: shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
},
};
绘制带纹理的立方体
对 drawScene()
函数的更改很简单。
注意:在“draw-scene.js”模块的 drawScene()
函数中,添加以下函数
// tell webgl how to pull out the texture coordinates from buffer
function setTextureAttribute(gl, buffers, programInfo) {
const num = 2; // every coordinate composed of 2 values
const type = gl.FLOAT; // the data in the buffer is 32-bit float
const normalize = false; // don't normalize
const stride = 0; // how many bytes to get from one set to the next
const offset = 0; // how many bytes inside the buffer to start from
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
gl.vertexAttribPointer(
programInfo.attribLocations.textureCoord,
num,
type,
normalize,
stride,
offset,
);
gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
}
注意:在“draw-scene.js”模块的 drawScene()
函数中,用以下行替换对 setColorAttribute()
的调用
setTextureAttribute(gl, buffers, programInfo);
然后添加代码以指定要映射到面的纹理。
注意:在您的 drawScene()
函数中,紧接在对 gl.uniformMatrix4fv()
的两次调用之后,添加以下代码
// Tell WebGL we want to affect texture unit 0
gl.activeTexture(gl.TEXTURE0);
// Bind the texture to texture unit 0
gl.bindTexture(gl.TEXTURE_2D, texture);
// Tell the shader we bound the texture to texture unit 0
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
WebGL 最少提供 8 个纹理单元,其中第一个是 gl.TEXTURE0
。我们告诉 WebGL 我们想要影响单元 0。然后我们调用 bindTexture()
,它将纹理绑定到纹理单元 0 的 TEXTURE_2D
绑定点。然后我们告诉着色器,对于 uSampler
,使用纹理单元 0。
最后,将 texture
作为参数添加到 drawScene()
函数中,包括其定义和调用位置。
注意:更新 drawScene()
函数的声明以添加新的参数。
function drawScene(gl, programInfo, buffers, texture, cubeRotation) {
注意:更新在 main()
函数中调用 drawScene()
的位置。
drawScene(gl, programInfo, buffers, texture, cubeRotation);
此时,旋转立方体应该可以正常工作了。
跨域纹理
WebGL 纹理的加载受跨域访问控制的影响。为了使您的内容能够从其他域加载纹理,需要获得 CORS 许可。有关 CORS 的详细信息,请参阅 HTTP 访问控制。
由于 WebGL 现在要求纹理从安全上下文中加载,因此您无法在 WebGL 中使用从 file:///
URL 加载的纹理。这意味着您需要一个安全的 Web 服务器来测试和部署您的代码。对于本地测试,请参阅我们的指南 如何设置本地测试服务器? 以获取帮助。
请参阅此 hacks.mozilla.org 文章,了解如何使用 CORS 许可的图像作为 WebGL 纹理。
污染的(只写)2D 画布不能用作 WebGL 纹理。例如,当在 2D <canvas>
上绘制跨域图像时,它就会被污染。