现在我们已经创建了一个可以旋转的 3D 六面体,是时候使用纹理将每个面替换为单一颜色了。
首先添加加载纹理的代码。 现在我们仅使用单个纹理应用于六面体的所有 6 个面,但可以使用相同的方法加载任意数量的纹理贴图。
注意:值得注意的是,纹理的加载也需要遵守跨域访问规则; 也就是说,你只能从允许跨域访问的URL中加载你需要的纹理。 详细信息请参阅小节。
注意:将以下两个函数添加到“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纹理对象纹理。 接下来使用 texImage2D() 上传红色像素。 这样我们就可以在下载图像之前使用这个黑色纹理。
要从图像文件加载纹理,接下来创建一个 Image 对象并使用我们要用作纹理的图像的 URL 设置 src。 我们为 image.onload 设置的函数将在图像下载完成时被调用。 此时我们再次调用 texImage2D(),这次使用图像作为纹理的数据源。 之后,我们根据下载的图像是否是二维的2的幂来设置纹理的filter和wrap。
在WebGL1中,对于非2的幂纹理只能使用NEAREST和LINEAR过滤,并且不会生成任何纹理。 此外,平铺模式还必须设置为 CLAMP_TO_EDGE。 另一方面,如果纹理在两个维度上都是 2 的幂,则 WebGL 可以使用更高质量的过滤,可以使用贴图,并且可以将平铺模式设置为 REPEAT 或 MIRRORED_REPEAT。
多级渐进纹理和纹理坐标重复可以通过调用 texParameteri() 来禁用,当然你已经通过调用 bindTexture() 绑定了纹理。 这样纹理css,您已经可以使用非 2 的幂纹理,但是您将难以使用多级渐进纹理、纹理坐标打包、纹理坐标复制,并且无法控制设备如何处理您的纹理。
js
// 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 兼容设备将手动显示能够以任何比特率使用纹理(当然要考虑到像素限制)。 如果不使用这些参数,任何非 2 次幂纹理使用都将失败并返回纯黑色图像。
要加载图像,请在 main() 函数中添加对 loadTexture() 函数的调用。 这可以添加到 initBuffers(gl) 调用中。
但需要注意的是,浏览器将从左上角开始以自上而下的顺序从加载的图像中复制像素,而WebGL需要以自下而上的顺序 - 从左下角开始的像素顺序。 。 (有关详细信息,请参阅为什么我的 WebGL 纹理是颠倒的?)。
所以为了避免渲染时出现错误的图像纹理方向,我们还需要调用pixelStorei()并将gl.UNPACK_FLIP_Y_WEBGL参数设置为true来调整像素顺序,将其翻转为WebGL所需的自下而上的顺序。
注意:在 initBuffers() 调用之后立即将以下代码添加到 main() 函数中:
js
// 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”模块中:
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() 的调用替换为以下行:
js
const textureCoordBuffer = initTextureBuffer(gl);
注意:在“init-buffers.js”模块的 initBuffers() 函数中,将 return 语句替换为以下内容:
js
return {
position: positionBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
为了使用纹理而不是单一颜色,着色器程序和着色器程序的初始化代码都需要更改。
我们需要修改顶点着色器,使其不再获取颜色数据,而是获取纹理坐标数据。
注意:更新 main() 函数中的 vsSource 定义,如下所示:
js
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 声明纹理css,如下所示:
js
const fsSource = `
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
void main(void) {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}
`;
现在,代码不再使用简单的颜色值填充片段颜色,而是使用采样器使用纹理中每个像素的最佳映射方法来估计片段颜色。
因为我们更改了属性并添加了制服,所以我们需要找到它们的位置。
注意:在 main() 函数中,更新 programInfo 的定义,如下所示:
js
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()函数中添加以下函数:
js
// 告诉 WebGL 如何从缓冲区中提取纹理坐标
function setTextureAttribute(gl, buffers, programInfo) {
const num = 2; // 每个坐标由 2 个值组成
const type = gl.FLOAT; // 缓冲区中的数据为 32 位浮点数
const normalize = false; // 不做标准化处理
const stride = 0; // 从一个坐标到下一个坐标要获取多少字节
const offset = 0; // 从缓冲区内的第几个字节开始获取数据
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()的调用:
js
setTextureAttribute(gl, buffers, programInfo);
注意:在drawScene()函数中,在两次调用gl.uniformMatrix4fv()之后,添加以下代码:
js
// 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是第一个。 如果我们想改变第一个单元,我们需要调用bindTexture()将纹理绑定到纹理单元0的TEXTURE_2D绑定点。然后告诉着色器使用纹理单元0作为uSampler。
最后,在 drawScene() 函数中添加纹理作为参数,无论是在定义它的位置还是在调用它的位置。
注意:更新 drawScene() 函数的定义以添加新参数:
js
function drawScene(gl, programInfo, buffers, texture, cubeRotation) {
注意:更新 main() 函数中调用 drawScene() 的位置:
js
drawScene(gl, programInfo, buffers, texture, cubeRotation);
好的,现在我们的六面体将像这样向上旋转。
加载WebGL纹理也应该说是跨域访问控制中的一个话题。 为了在我们的显示内容中使用来自其他域的纹理图像,还应该考虑允许跨域访问。 您可以通过查看 HTTP 访问控制来获取更多详细信息。
本文还解释了将跨域纹理加载到 WebGL 中。 并且文章中还包含了一个使用的反例。
受污染的(只写)画布不能用作 WebGL 纹理。 举个反例,当跨域图片绘制到2D画布中时,画布就被“污染”了。
发现此页面存在内容问题?想要更多参与吗? 了解如何做出贡献。
本页面最后一次由 MDN 贡献者于 2023 年 7 月 30 日修改。