3.1 使用varying变量—绘制彩色三角形
3.1.1 使用varying变量
1、声明varying变量
声明varying变量的时候,需要在顶点着色器和片元着色器两边同时声明,而且类型和名称都需要一致。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
varying vec4 vColor;
void main() {
gl_Position = aPosition;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
varying vec4 vColor;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
2、给varying变量赋值
varying变量的作用是从顶点着色器向片元着色器传递数据。声明了vColor变量之后,就可以在顶点着色器的main函数里进行赋值,然后在片元着色器使用vColor变量。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
varying vec4 vColor;
void main() {
vColor = aPosition;
gl_Position = aPosition;
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`;
3.1.2 执行流程
3.1.3 代码示例
3.2 从顶点到图形—webgl渲染流程介绍
1、图元装配
将独立的顶点坐标装配成几何图形,图形的类别由gl.drawArrays()第一个参数确定。
2、光栅化
这一步是将装配好的图形转换为片元。
(1)剔除
对于不透明物体,背面对于观察者来说是不可见的。那么在渲染过程中,就会将不可见的部分剔除,不参与绘制。节省渲染开销。
(2)裁剪
在可视范围之外的事物是看不到的。图形生成后,有的部分可能位于可视范围之外,这一部分会被剪裁掉,不参与绘制。
3、渲染流程
图形所有的片元,它是逐个片元去经过片元着色器和颜色缓冲区的处理,然后绘制到浏览器里。
3.3 给图形添加背景图
3.3.1 纹理坐标
在canvas绘制图片的时候,首先new一个Image对象,然后设置Image实例的onload,再添加它的src属性,在onload函数里做一些绘制操作。
webgl相比canvas相对复杂一些。canvas的坐标跟图片的坐标是一致的,canvas以向下向右为正方向,图片也一样,它不需要特殊处理图片的坐标信息。在webgl的坐标系统跟canvas的不同,所以在使用之前需要做一定的转换,转换之后的图片坐标称为纹理坐标。
纹理坐标也称为st坐标,当前的原点位置在图片的左下角,往上为图片y轴的正方向,向右是图片x轴的正方向。将图片坐标转为纹理坐标之后,需要通过纹理的坐标和图形顶点坐标的映射关系来确定贴图,如下图所示:
3.3.2 添加背景图流程
1、实例化Image对象
const img = new Image();
img.onload = function() {
}
img.src = '../aasets/border.png';
2、创建纹理对象
通过gl.createTexture()方法创建纹理对象,主要是用于存储纹理图像数据。也可以通过gl.deleteTexture(texture)来删除纹理对象。
img.onload = function() {
// 创建纹理对象
const texture = gl.createTexture();
}
3、翻转图片y轴
图片原始的坐标是在左上角,而webgl的原点坐标是左下角,因此要让图片坐标跟webgl一致,就需要将它的y轴翻转。
可以通过gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)来实现y轴的翻转。
img.onload = function() {
// 创建纹理对象
const texture = gl.createTexture();
// 翻转图片Y轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
}
4、开启(激活)纹理单元
Webgl是通过纹理单元来管理纹理对象,每个纹理单元管理一张纹理图像。
可以通过gl.activeTexture(gl.TEXTURE0)方法来实现激活,WebGL默认至少支持8个纹理单元,分别对应TEXTURE0/1/2/3......,这里的gl.TEXTURE0指的就是纹理单元0,要使用它必须先激活它。
img.onload = function() {
// 创建纹理对象
const texture = gl.createTexture();
// 翻转图片Y轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启一个纹理单元
gl.activeTexture(gl.TEXTURE0);
}
5、绑定纹理对象
可以通过gl.bindTexture(type, texture)来实现绑定。texture是纹理对象,type参数有以下两种:
(1)gl.TEXTURE_2D:二维纹理
(2)gl.TEXTURE_CUBE_MAP:立方体纹理
img.onload = function() {
// 创建纹理对象
const texture = gl.createTexture();
// 翻转图片Y轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启一个纹理单元
gl.activeTexture(gl.TEXTURE0);
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
}
6、设置纹理参数
这一步是在图形放大缩小的时候,对应获取纹理的方式。可以通过gl.texParameteri(type, pname, param)来处理。
(1)type:参数同上
(2)pname:纹理参数
1.gl.TEXTURE_MAG_FILTER:放大
2.gl.TEXTURE_MIN_FILTER:缩小
3.gl.TEXTURE_WRAP_S:横向(水平填充)
4.gl.TEXTURE_WRAP_T:纵向(垂直填充)
(3)param
param参数要根据pname参数的设置而定。例如pname设置了gl.TEXTURE_MAG_FILTER或者gl.TEXTURE_MIN_FILTER(放大或缩小),那么param可以设置gl.NEAREST和gl.LINEAR。
img.onload = function() {
......
// 处理放大缩小的逻辑
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 横向 纵向 平铺的方式
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);
}
参考网站:blog.csdn.net/lufy_Legend…
7、将纹理图像分配给纹理对象
可以通过gl.texImage2D(type, level, internalformat, format, dataType, image)来配置纹理图像。
(1)type:参数同上
(2)level:为0即可
(3)internalformat:图像的内部格式
(4)format:纹理的内部格式,必须和internalformat相同
(5)dataType:纹理数据的数据类型
1.gl.UNSIGNED_BYTE:无符号整型,它所代表的是每个颜色分量占据一个字节
2.gl.UNSIGNED_SHORT_5_6_5:它代表rgb分量,分别占据5、6、5比特
3.gl.UNSIGNED_SHORT_4_4_4_4:整型:它代表rgba分量,分别占据4、4、4、4比特
4.gl.UNSIGNED_SHORT_5_5_5_1:整型:它代表rgba分量,分别占据5、5、5、1比特
(6)image:图片对象
img.onload = function() {
......
// 配置纹理图像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
}
8、片元着色器添加变量接收数据
声明一个uSampler变量来接收纹理,sampler2D类型代表的是接收的纹理是2D纹理,也可以通过samplerCube来接收立方体纹理。
声明好变量之后,就可以使用texture2D(uSampler, 纹理坐标)方法来从图像里读片元获取内容。
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, 纹理坐标);
}
`;
9、设置纹理坐标
这个texture2D里的纹理坐标,可以通过数据偏移量的方式来实现。
关于数据偏移量可参考:2.2 多缓冲区和数据偏移
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute vec4 aTex;
void main() {
// 要绘制的点的坐标
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
}
`;
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aTex = gl.getAttribLocation(program, 'aTex');
const points = new Float32Array([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0
]);
const BYTES = points.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 4, 0);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aTex, 2, gl.FLOAT, false, BYTES * 4, BYTES * 2);
gl.enableVertexAttribArray(aTex);
10、将纹理坐标传到片元着色器
由于纹理坐标目前是在顶点着色器接收,它想传到片元着色器,就需要使用varying变量。texture2D纹理坐标接收的数据类型是vec2,因此varying变量的类型也是vec2。
声明变量之后,还要将顶点着色器的纹理坐标赋值给这个varying变量。
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute vec4 aTex;
varying vec2 vTex;
void main() {
// 要绘制的点的坐标
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
vTex = vec2(aTex.x, aTex.y);
}
`;
// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
uniform sampler2D uSampler;
varying vec2 vTex;
void main() {
gl_FragColor = texture2D(uSampler, vTex);
}
`;
11、纹理单元传递给着色器
可以通过gl.uniform1i(uSampler, 纹理下标)方法进行传递,第一个参数对应片元着色器接收纹理的uSampler变量(参考第8步),第二个参数是激活纹理的下标,目前使用的是gl.TEXTURE0,即纹理下标为0(参考第4步)。
gl.uniform1i的i代表的是整型,如果uniform1f的f则代表浮点型。
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
uniform sampler2D uSampler;
varying vec2 vTex;
void main() {
gl_FragColor = texture2D(uSampler, vTex);
}
`;
const uSampler = gl.getUniformLocation(program, 'uSampler');
img.onload = function() {
......
gl.uniform1i(uSampler, 0);
}
注意:图片的大小尽量是2的整数幂,例如1024*1024。因为在光栅化的过程中,需要对纹理采样进行快速地取值,如果不是2的整数幂,它也可以显示出来,在贴图之前会将图片进行纹理的拉伸或者压缩到2的整数幂。
3.3.3 代码示例
3.4 使用多重纹理
多重纹理指的是将多个图片填充到同一个图形上。
1、声明多个sampler2D类型变量
图形是通过声明一个sampler2D类型的变量来接收纹理,如果有多个纹理,则需要声明多个变量来一一对应接收。
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
uniform sampler2D uSampler;
uniform sampler2D uSampler1;
varying vec2 vTex;
void main() {
gl_FragColor = texture2D(uSampler, vTex);
}
`;
2、接收多个纹理内容
图形是通过声明一个sampler2D类型的变量来接收纹理,如果有多个纹理,则需要声明多个变量来一一对应接收。
片元着色器本来是通过texture2D(uSampler, 纹理坐标)方法读取片元内容。现在声明了多个变量之后,则需要先执行多个texture2D方法进行一一读取。然后将它们的结果相乘再赋值给gl_FragColor。
const FRAGMENT_SHADER_SOURCE = `
precision lowp float;
uniform sampler2D uSampler;
uniform sampler2D uSampler1;
varying vec2 vTex;
void main() {
vec4 c1 = texture2D(uSampler, vTex);
vec4 c2 = texture2D(uSampler1, vTex);
gl_FragColor = c1 * c2;
}
`;
3、新增两个图片纹理方法
目前将两个纹理图片同时填充到一个图形,需要执行两遍图片纹理加载的流程。新增两个方法分别对应两个流程,然后使用Promise返回纹理加载结果。
const uSampler = gl.getUniformLocation(program, 'uSampler');
const uSampler1 = gl.getUniformLocation(program, 'uSampler1');
function getImage() {
return new Promise(resolve => {
const img = new Image();
img.onload = function() {
// 创建纹理对象
const 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_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 横向 纵向 平铺的方式
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.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
gl.uniform1i(uSampler, 0);
resolve();
}
img.src = '../assets/border.png';
});
}
function getImage1() {
return new Promise(resolve => {
const img = new Image();
img.onload = function () {
// 创建纹理对象
const texture = gl.createTexture();
// 翻转图片Y轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启一个纹理单元
gl.activeTexture(gl.TEXTURE1);
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 处理放大缩小的逻辑
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 横向 纵向 平铺的方式
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.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
gl.uniform1i(uSampler1, 1);
resolve();
}
img.src = '../assets/content.png';
});
}
4、监听所有纹理流程执行完成
可以通过Promise.all()方法来监听两张纹理图片流程是否全部执行完成,最后才执行绘制图形方法。
Promise.all([getImage(), getImage1()]).then(() => {
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
});
5、整合各个纹理加载流程方法
目前通过两个方法分别执行两段纹理加载流程,流程的内容只有激活纹理单元、传递给着色器的变量、图片路径3个不相同,可以通过传参的方式整合在一起。
/**
* 加载纹理图片流程
* @url: 图片路径
* @location: 着色器接收变量
* @index: 激活的纹理单元下标
* */
function getImage(url, location, index) {
return new Promise(resolve => {
const img = new Image();
img.onload = function() {
// 创建纹理对象
const texture = gl.createTexture();
// 翻转图片Y轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启一个纹理单元
gl.activeTexture(gl[`TEXTURE${index}`]);
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 处理放大缩小的逻辑
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 横向 纵向 平铺的方式
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.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
gl.uniform1i(location, index);
resolve();
}
img.src = url;
});
}
Promise.all([getImage('../assets/border.png', uSampler, 0), getImage('../assets/content.png', uSampler1, 1)]).then(() => {
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
});
6、代码示例