文章目录
- 搭建简易的webgl环境
- 绘制简单三角形(不带stencilTest)
- 绘制另一个三角形(不带模板测试)
- 加入模板测试
- 总结
- 调参练习
搭建简易的webgl环境
一直以来,我只是想通过搭建纯webgl环境,进行开发,来清楚地了解基础webgl功能,如果在C++环境里面使用opengl,还需要配置加载各种 lib,以及编译,容易错,而且里面有很多是其他各种库里面的函数,不能最大限度地接近原理。偶然间,发现了一个很简单的方法,基于最基本的webgl函数:WebGLRenderingContext里面的api。下面推荐一下,可以方便大家一起学习。
进入mdn在线文档:
https://developer.mozilla.org/zh-CN/
这里面不仅有详细地说明,还有一个平台给我们实践各种代码,如下图所示,点击其中的play:
则进入写代码平台:
https://developer.mozilla.org/zh-CN/play
平台就这么搭建好了,不用写任何东西,也不用安装任何软件,编译任何代码。否则,很多情况下,我的耐心都在各种无关的操作中被消磨掉了,到头来什么也没学到。
绘制简单三角形(不带stencilTest)
把下面这段js代码复制到上面写代码平台的最下面js框里面,然后右边会出现一个三角形:
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
document.body.append(canvas); // 创建和将 canvas 加入页面
const gl = canvas.getContext("webgl");
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 创建一个顶点着色器
gl.shaderSource(
vertexShader,
`
attribute vec4 a_position;
void main() {
gl_Position = a_position; // 设置顶点位置
}
`,
); // 编写顶点着色器代码
gl.compileShader(vertexShader); // 编译着色器代码
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 创建一个片元着色器
gl.shaderSource(
fragmentShader,
`
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color; // 设置片元颜色
}
`,
); // 编写片元着色器代码
gl.compileShader(fragmentShader); // 编译着色器代码
const program = gl.createProgram(); // 创建一个程序
gl.attachShader(program, vertexShader); // 添加顶点着色器
gl.attachShader(program, fragmentShader); // 添加片元着色器
gl.linkProgram(program); // 连接 program 中的着色器
gl.useProgram(program); // 告诉 webgl 用这个 program 进行渲染
const colorLocation = gl.getUniformLocation(program, "u_color");
// 获取 u_color 变量位置
gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1); // 设置它的值
const positionLocation = gl.getAttribLocation(program, "a_position");
// 获取 a_position 位置
const positionBuffer = gl.createBuffer();
// 创建一个顶点缓冲对象,返回其 ID,用来放三角形顶点数据,
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将这个顶点缓冲对象绑定到 gl.ARRAY_BUFFER
// 后续对 gl.ARRAY_BUFFER 的操作都会映射到这个缓存
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0, 0.5, 0.5, 0, -0.5, -0.5]), // 三角形的三个顶点
// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据
gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
// 将顶点数据加入的刚刚创建的缓存对象
gl.vertexAttribPointer(
// 告诉 OpenGL 如何从 Buffer 中获取数据
positionLocation, // 顶点属性的索引
2, // 组成数量,必须是1,2,3或4。我们只提供了 x 和 y
gl.FLOAT, // 每个元素的数据类型
false, // 是否归一化到特定的范围,对 FLOAT 类型数据设置无效
0, // stride 步长 数组中一行长度,0 表示数据是紧密的没有空隙,让OpenGL决定具体步长
0, // offset 字节偏移量,必须是类型的字节长度的倍数。
);
gl.enableVertexAttribArray(positionLocation);
// 开启 attribute 变量额,使顶点着色器能够访问缓冲区数据
gl.clearColor(0, 1, 1, 1); // 设置清空颜色缓冲时的颜色值
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区,也就是清空画布
gl.drawArrays(
// 从数组中绘制图元
gl.TRIANGLES, // 画三角形
0, // 从哪个点开始画
3, // 需要用到多少个点
);
上面是绘制简单三角形的webgl js代码,为了便于理解,这篇文章从这个最一般的helloworld讲起,通过加入一些变化,说明stenciltest的原理,因为我在网上看到很多有各种复杂的部分,什么贴纹理啊,绘制地板,box啊,甚至还加了动画,实在不方便对单个模板测试功能的理解。
绘制另一个三角形(不带模板测试)
把以下这一小段代码加入到刚才的js代码后面,可以绘制两个重叠的三角形:
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1.0, 0.0, 0.0, -1.0, 1., 0.0]), // 三角形的三个顶点
// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据
gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
gl.drawArrays(
// 从数组中绘制图元
gl.TRIANGLES, // 画三角形
0, // 从哪个点开始画
3, // 需要用到多少个点
);
加入模板测试
在刚才的基础上做如下修改:
- 修改初始化gl的代码 ,原来的
const gl = canvas.getContext("webgl");
改成:const gl = canvas.getContext("webgl",{stencil: true});
- 在绘制第一个三角形之前加入如下代码:
gl.enable(gl.STENCIL_TEST);
gl.colorMask(false, false, false, false);
gl.stencilFunc(gl.ALWAYS, 1, 255);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
其中第一句是开启模板测试,第二句设置color遮罩,目的是不让它真正显示在结果框中,但是还是影响了模板测试,并且可以修改了stencilbuffer里面的值,第三句是设置通过模板测试的条件,gl.ALWAYS也就是永远通过,1代表ref(参照值),255代表位掩码,这里没用,因为是always通过模板测试,第四句是设置模板测试不通过或者通过而深度测试不通过或者模板测试和深度测试都通过以后干什么,这里都设置为REPLACE,表示不管咋样,要把模板buffer里面的值全部替换成ref(参照值),第三句已设定参照值为1。这些都要在绘制第一个三角形时生效,还记得第一个三角形是什么吗?那是一个倾斜的三角形,可以回头看截图。
这样设置后在绘制完第一个三角形以后stencilbuffer里面是这样子的(示意图):
其中只有在中间三角形地方的片源都是1,其他都是0,因为模板测试缓冲区默认是0,其他没绘制的地方保持不变,绘制地方(也就是三角形区域)不论是深度测试和模板测试是否通过都用ref值1替换。
这样设置以后整体运行后是看不到任何三角形的,因为gl.colorMask(false, false, false, false)
这句话的原因。
- 在绘制第二个三角形之前加入以下代码:
gl.colorMask(true, true, true, true);
gl.stencilFunc(gl.NOTEQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
第一句是设置color遮罩,这里红绿蓝透明都是true,意思是都可以绘制出来了,所以第二个三角形就能看到了,下面一句话是设置模板测试通过的条件,不等于1,具体是:ref=1, mask=255,令:逐片源看stenbuffer里面当前存储值=buf,设置为NOTEQUAL则意思是:ref&mask!==buf&mask,翻译过来就是:ref按位与mask的值不等于逐片源stencilbuffer里面当前值按位与mask的值,其中ref按位与mask为1,恒定,逐片源stencilbuffer里面当前值按位与mask则是:非第一个三角形绘制区域为0,第一个三角形绘制区域为1,所以这里模板测试通过的条件就是:非第一个三角形绘制区域,将通过模板测试。
而第三句规定了不通过模板测试和通过模板测试以及通过模板测试和深度测试之后都不改变stencilbuffer的当前值。
在以上设置完成后绘制的第二个三角形如下所示:
从图中看,第二个三角形中在第一个三角形区域的片源被去掉了,因为那些片源没有通过模板测试。符合我们之前的设置结果。
修改后的整体js代码如下:
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 300;
document.body.append(canvas); // 创建和将 canvas 加入页面
const gl = canvas.getContext("webgl",{stencil:true});
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 创建一个顶点着色器
gl.shaderSource(
vertexShader,
`
attribute vec4 a_position;
void main() {
gl_Position = a_position; // 设置顶点位置
}
`,
); // 编写顶点着色器代码
gl.compileShader(vertexShader); // 编译着色器代码
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 创建一个片元着色器
gl.shaderSource(
fragmentShader,
`
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color; // 设置片元颜色
}
`,
); // 编写片元着色器代码
gl.compileShader(fragmentShader); // 编译着色器代码
const program = gl.createProgram(); // 创建一个程序
gl.attachShader(program, vertexShader); // 添加顶点着色器
gl.attachShader(program, fragmentShader); // 添加片元着色器
gl.linkProgram(program); // 连接 program 中的着色器
gl.useProgram(program); // 告诉 webgl 用这个 program 进行渲染
const colorLocation = gl.getUniformLocation(program, "u_color");
// 获取 u_color 变量位置
gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1); // 设置它的值
const positionLocation = gl.getAttribLocation(program, "a_position");
// 获取 a_position 位置
const positionBuffer = gl.createBuffer();
// 创建一个顶点缓冲对象,返回其 ID,用来放三角形顶点数据,
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将这个顶点缓冲对象绑定到 gl.ARRAY_BUFFER
// 后续对 gl.ARRAY_BUFFER 的操作都会映射到这个缓存
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0, 0.5, 0.5, 0, -0.5, -0.5]), // 三角形的三个顶点
// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据
gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
// 将顶点数据加入的刚刚创建的缓存对象
gl.vertexAttribPointer(
// 告诉 OpenGL 如何从 Buffer 中获取数据
positionLocation, // 顶点属性的索引
2, // 组成数量,必须是1,2,3或4。我们只提供了 x 和 y
gl.FLOAT, // 每个元素的数据类型
false, // 是否归一化到特定的范围,对 FLOAT 类型数据设置无效
0, // stride 步长 数组中一行长度,0 表示数据是紧密的没有空隙,让OpenGL决定具体步长
0, // offset 字节偏移量,必须是类型的字节长度的倍数。
);
gl.enableVertexAttribArray(positionLocation);
// 开启 attribute 变量额,使顶点着色器能够访问缓冲区数据
gl.clearColor(0, 1, 1, 1); // 设置清空颜色缓冲时的颜色值
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区,也就是清空画布
gl.enable(gl.STENCIL_TEST);
gl.colorMask(false, false, false, false);
gl.stencilFunc(gl.ALWAYS, 1, 255);
gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE);
gl.drawArrays(
// 从数组中绘制图元
gl.TRIANGLES, // 画三角形
0, // 从哪个点开始画
3, // 需要用到多少个点
);
gl.colorMask(true, true, true, true);
gl.stencilFunc(gl.NOTEQUAL, 1, 255);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1.0, 0.0, 0.0, -1.0, 1., 0.0]), // 三角形的三个顶点
// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据
gl.STATIC_DRAW, // 表示缓冲区的内容不会经常更改
);
gl.drawArrays(
// 从数组中绘制图元
gl.TRIANGLES, // 画三角形
0, // 从哪个点开始画
3, // 需要用到多少个点
);
总结
通过实践,我发现模板测试是这样的一个思路,总结如下:
在各种绘制语句之前加入一些配置项,这些配置项的目的是修改stencilbuffer以及决定这次的绘制是不是能通过模板测试,因为只有通过模板测试和深度测试的片源才能最终被绘制出来,而修改后的stencilbuffer里面的值会影响下次绘制是否能通过模板测试,所以模板测试并不会修改绘制出的东西的颜色啥的,他是一种高级裁剪。通过这种方式,经过多次不清空stencilbuffer的绘制过程,在stencilbuffer里面形成一幅独一无二的图像,这种图像是前面多次绘制的结果(它可能是复杂的),不是简单的行列排布,而是依赖于之前图像的一些特征,不断地影响着后面的绘制结果。
调参练习
为了便于理解我们可以修改其中的某些参数,来测试自己是不是真正理解,比如,我们把第二次绘制之前的模板测试条件设置语句中notequal改成equal:gl.stencilFunc(gl.EQUAL, 1, 255);
那么我们将得到另一个三角形:
最后留一道思考题,如何修改以上代码,绘制出以下图形呢?
快来尝试一下吧。
最近在学习英语,所以想用英语表达一下一点学习感悟:
Many engineers want to create the complex system which looks like cool and amazing at the beginning and ignore the easy and basic things, I know this feeling and experienced that, it is about anxiety, about unexpectable future, which tells me that we must work hard. After years of working, I found it is impossible, every skill need to practice from the easy, little, looks boring system, it may not be needed by our boss, by the society, we can not get any afford from that, but we need to face that, if we just copy any other code from the internet, and make out something that looks well, it doesn’t mean I have mastered that skill. Perhaps, it listened stupid, what? to make wheels by ourselves? What time is now? yeah, I confess it is stupid, but I just want to be the person, only in my way to understand the world.