在 fragment shader 中计算颜色虽然灵活但不好实现复杂的效果。更常用的方法从一张图片采样得到颜色,把一张图片贴到几何体的表面,通常叫做纹理贴图,而采样的图片就叫做 texture image (纹理图片) 或 texture (纹理) 。
我们来实现把一张图片贴到正方形上面
先来看看纹理坐标
这里用 (s, t) 表示, 有些书籍用的是 (u, v)
纹理坐标跟图片的大小无关,无论图片是长方形还是正方形,左下角都是 (0,0) 右上角是 (1,1)
纹理贴图要做的就是把图片的坐标映射到几何体坐标。这里我们采用下图的映射方式。
shader 中要加入纹理坐标
// vertex shader
var VERTEX_SHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec2 a_TexCoord;\n' +
'varying vec2 v_TexCoord;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' v_TexCoord = a_TexCoord;\n' +
'}\n';
// fragment shader
var FRAGMENT_SHADER_SOURCE =
'precision mediump float;\n' +
'varying vec2 v_TexCoord;\n' +
'uniform sampler2D u_Sampler;\n' +
'void main() {\n' +
' gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
'}\n';
var vertices = new Float32Array([
-0.5, 0.5, 0.0, 1.0, // 前 2 位是位置坐标, 后 2 位是纹理坐标
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0
]);
顶点中加入了纹理坐标
下面我们要加载图片
var image = new Image();
image.onload = function () {
loadTexture(image);
draw();
};
image.src = 'images/sky.jpg';
加载图片是一个异步的过程,对图片的处理要放在回调中
如果浏览器不允许加载本地图片,就自己用 node.js 等起个本地静态文件服务器
function loadTexture(image) {
var texture = gl.createTexture();
// 翻转 Y 轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
gl.uniform1i(u_Sampler, 0);
}
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
下面我们重点分析一下 loadTexture 方法
- gl.createTexture()
新建一个 texture object
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
翻转 Y 轴,因为图片的 Y 轴和纹理的 Y 轴正好是反的
- gl.activeTexture(texUnit)
激活一个 texUnit
webgl 通过 Texture Unit 来支持多个纹理图片,意思是你可以传多个纹理图片到 webgl 中使用。
- gl.bindTexture(target, texture)
把 texture 绑定到 target 上。
target 可取 gl.TEXTURE_2D 和 gl.TEXTURE_CUBE_MAP ,用来告诉 webgl 纹理图片的类型。
gl.TEXTURE_2D 表示一个二维的纹理图片
- gl.texParameteri(target, pname, param)
设置纹理参数
有 4 种纹理参数可以设置:
gl.TEXTURE_MAG_FILTER
gl.TEXTURE_MIN_FILTER
gl.TEXTURE_WRAP_S
gl.TEXTURE_WRAP_T
因为纹理图片的大小跟几何图形的大小不一定是相等的,如果大小相等很简单每个像素一一对应就行了。如果不相等就得通过参数指定映射的方法。
gl.TEXTURE_MAG_FILTER : 用于当纹理图片大小小于集合体大小的时候,这时纹理图片需要被放大,通过这个参数指定纹理图片放大后怎么映射。
gl.TEXTURE_MIN_FILTER : 用于当纹理图片大小大于几何体大小的时候,这时纹理图片需要被缩小,通过这个参数指定纹理图片缩小后怎么映射。
gl.TEXTURE_WRAP_S: 用于指定当把图片映射到几何体的部分子区域时,左右两边剩余的区域怎么填充。
gl.TEXTURE_WRAP_T : 用于指定当把图片映射到几何体的部分子区域时,上下两边剩余的区域怎么填充。
看看下图会清晰一点
参数可以设置的值看下表
参数默认值看下表
我对这几个参数的理解也不是特别清晰,读者可参考其它资料,有懂的可以留言分享一下
- gl.texImage2D(target, level, internalformat, format, type, image)
把图片拷贝到 GPU 中并和 texture object 绑定在一起
level:0 (设置为 0 ,本书不讲解这个参数)
internalformat: 指定图片的格式
format : 图片格式,跟 internalformat 值相同
type : 指定纹理的数据类型
图片格式可以取以下值:
gl.RGB
gl.RGBA
gl.ALPHA
gl.LUMINANCE
gl.LUMINANCE_ALPHA
图片数据类型 type 可以取以下值:
gl.UNSIGNED_BYTE (Each color component has 1 byte)
gl.UNSIGNED_SHORT_5_6_5 (RGB: Each component has 5, 6, and 5 bits)
gl.UNSIGNED_SHORT_4_4_4_4 (RGBA: Each component has 4, 4, 4, and 4 bits)
gl.UNSIGNED_SHORT_5_5_5_1 (RGBA: Each RGB component has 5 bits, and A has 1 bit)
- gl.uniform1i(u_Sampler, 0)
把 0 赋值给 u_Sampler, 因为我们用了 TEXTURE0
fragment shader 中 sampler2D 其实是一个整型
- vec4 texture2D(sampler2D sampler, vec2 coord)
fragment shader 中的 texture2D 是一个内置函数, 用纹理坐标 coord 从图片 sampler 中采样,返回的是采样的颜色
完整代码
// vertex shader
var VERTEX_SHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec2 a_TexCoord;\n' +
'varying vec2 v_TexCoord;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' v_TexCoord = a_TexCoord;\n' +
'}\n';
// fragment shader
var FRAGMENT_SHADER_SOURCE =
'precision mediump float;\n' +
'varying vec2 v_TexCoord;\n' +
'uniform sampler2D u_Sampler;\n' +
'void main() {\n' +
' gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
'}\n';
var canvas = document.getElementById("canvas");
var gl = canvas.getContext('webgl');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
if (!initShaders(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)) {
alert('Failed to init shaders');
}
var vertices = new Float32Array([
-0.5, 0.5, 0.0, 1.0, // 前 2 位是位置坐标, 后 2 位是纹理坐标
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0
]);
initVertexBuffers(gl, vertices);
var image = new Image();
image.onload = function () {
loadTexture(image);
draw();
};
image.src = 'images/sky.jpg';
function initVertexBuffers(gl, vertices) {
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create buffer object');
return -1;
}
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var FSIZE = vertices.BYTES_PER_ELEMENT;
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 4 * FSIZE, 0);
gl.enableVertexAttribArray(a_Position);
var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, 4 * FSIZE, 2 * FSIZE);
gl.enableVertexAttribArray(a_TexCoord);
}
function loadTexture(image) {
var texture = gl.createTexture();
// 翻转 Y 轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
gl.uniform1i(u_Sampler, 0);
}
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}