大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 5 篇文章。本系列文章从 WebGL 之基础开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是 WebGL 基本的运作机制以及如何使用其提供的功能,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
Day 3 画出三角形时,在 positionBuffer
中传入了 3 个顶点,每个顶点分别有两个值 (x, y) 表示座标乘下来共 6 个值,并且在 gl.drawArrays()
的最後一个参数 count
参数传入 3 表示画三个顶点;若要画更多三角形,我想读者也已经想到,分别在 positionBuffer
传入更多『组』三角形的每个顶点座标,接着修改 gl.drawArrays()
的 count
即可,笔者直接画三个三角形:
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
150, 60,
180, 82.5,
120, 82.5,
100, 60,
80, 40,
120, 40,
200, 60,
180, 40,
220, 40,
]),
gl.STATIC_DRAW,
);
// ...
gl.drawArrays(gl.TRIANGLES, 0, 9);
看起来像是这样:
这些只是笔者随便想到的图案,读者们可以自行发挥想像力调整顶点座标
笔者当时很好奇:如果 buffer 资料长度不足,或是 count
不是三的倍数,那麽会怎麽样呢?如果把最後一组顶点删除(220, 40
那组),count
保持为 9,不仅不会有什麽阵列超出的错误,感觉上 vertex attribute array 还给不足的部份填上预设值 0
:
或者把 count
改成 8,也不会有错误,只是最後一组三角形没有完整,两个点凑不出一个面,因此最後一个三角形就消失了:
在 Day 2 我们实做的 fragment shader 只是纯粹把颜色指定上去,所以现在不论画几个三角形,颜色都是当初写死在 fragment shader 中的颜色:
gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
要让不同三角形有不同的颜色,要思考的是输入资料/参数给 fragment shader 的方式,在 fragment shader 中可以使用 uniform,但是那样的话所有三角形的颜色依然会是一样,得用类似 attribute / buffer 『每次 shader 呼叫不同』的东西,不过 fragment shader 中是不能使用 attribute 的功能的,回想 Day 2 fragment shader 的运作方式:fragment shader 是每个 pixel 执行一次,不像是 vertex shader 以顶点为单位,取用 array buffer 的方式显然对不起来,因此需要另外一种传输工具 -- varying
varying 这功能可以让 vertex shader 输出资料给 fragment shader 使用,但是两者执行的回合数显然是对不起来,假设回到一个低解析度三角形的状况如下图,vertex shader 执行三次得到三个顶点,灰色的方格每格执行一次 fragment shader 计算颜色:
vertex #1 输出一组资料、vertex #2 输出一组资料、vertex #3 输出一组资料,那麽 fragment #2, fragment #3, fragment #4 这些介於中间 pixel 执行的 fragment shader 会拿到什麽资料?答案是:WebGL 会把顶点与顶点之间输出的 varying 做平滑化!
假设 vertex #1 输出 v_number = 0.2
、vertex #2 输出 v_number = 1.1
,那麽介於 vertex #1, #2 之间的 fragment #2 将拿到两个点输出的中间值,并且越接近某个顶点的 pixel 就会得到越接近该顶点输出的 varying,笔者画了一张简易的示意图举例 varying 平滑化的样子:
这个特性不仅解决问题,也让笔者觉得相当有意思,有种当初玩 flash 移动补间动画的感觉
我们从 fragment shader 开始修改,varying 宣告方式、使用上跟 attribute 差不多,只是把 attribute
改成 varying
:
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1);
}
输出颜色从原本写死的改用一个叫做 v_color
的 varying vec3
这边还多了一行
precision mediump float;
,这是用来设定 shader 要使用多精准的浮点数,如果没有特别需求使用中等mediump
就行了
那麽在 vertex shader 得负责输出这个值,虽然在 vertex shader 这边 varying 是要输出,但是写法一样是 varying vec3 v_color;
:
attribute vec2 a_position;
+attribute vec3 a_color;
uniform vec2 u_resolution;
+
+varying vec3 v_color;
void main() {
gl_Position = vec4(
a_position / u_resolution * vec2(2, -2) + vec2(-1, 1),
0, 1
);
+ v_color = a_color;
}
可以看到笔者加了一个 attribute vec3 a_color
,并且直接把 v_color
指定成 a_color
的值,接下来就是重复 Day 3 『画什麽』的资料输入、vertex attribute array 等设定:
const colorAttributeLocation = gl.getAttribLocation(program, 'a_color');
// a_position
// ...
// a_color
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.enableVertexAttribArray(colorAttributeLocation);
gl.vertexAttribPointer(
colorAttributeLocation,
3, // size
gl.UNSIGNED_BYTE, // type
true, // normalize
0, // stride
0, // offset
);
gl.bufferData(
gl.ARRAY_BUFFER,
new Uint8Array([
121, 85, 72,
121, 85, 72,
121, 85, 72,
0, 0, 0,
255, 255, 255,
255, 255, 255,
0, 0, 0,
255, 255, 255,
255, 255, 255,
]),
gl.STATIC_DRAW,
);
这段程序码原理在 Day 3 都有提过,比较需要注意几点:
gl.vertexAttribPointer()
以及 gl.bufferData()
该行执行的当下要注意 bind 的 ARRAY_BUFFER
是哪个,要不然会对着错误的目标做事,当然最好的就是把对於一个 attribute 的操作清楚分好,日後也比较好看出该区域在操作的对象gl.vertexAttribPointer()
的 size: 3
,因为颜色有 3 个 channel: RGB,因此对於每个顶点 gl.bufferData()
要给 3 个值gl.vertexAttribPointer()
使用 gl.UNSIGNED_BYTE
配合 normalize: true
来使用,在 Day 3 有提到: normalize 配合整数型别时可以把资料除以该型别的最大值使 attribute 变成介於 <= 1 的浮点数,那麽在 gl.bufferData()
时传入 Uint8Array
,并且可以在资料内容写熟悉的 rgb 值总结来说资料流如下:
a_position
以及颜色资料 a_color
v_color
成为 a_color
v_color
会平滑化,约接近一个顶点的 pixel v_color
就越接近该顶点当初设定的 a_color
v_color
并直接输出该颜色对於颜色资料的部份,笔者在前三个顶点给一样的颜色,所以第一个三角形是纯色,第二、第三个三角形的第一个顶点为黑色,剩下两个顶点为白色,因为平滑化的缘故,会得到渐层的效果:
本篇的完整程序码可以在这边找到:
笔者认为 WebGL API 最基本的 building block 其实就是 Day 1 到 Day 5 的内容,接下来除了 texture, skybox 之外,几乎可以说是用这些 building block(搭配线性代数)建构出 3D、光影等效果。下一章就来介绍 texture 并让 shader 真的开始做一些运算
如果读者好奇去修改传入
gl.bufferData()
的资料玩玩的话,应该很快就会发现要自己去对a_position
的第几组资料跟a_color
的第几组资料是属於同一个顶点的,他们在程序码上有点距离,没有那种{position: [1,2], color: '#abcdef'}
清楚的感觉,真的要做些应用程序,很快就得自己对这部份做点抽象开始包装,要不然程序码一转眼就会让人难以摸着头绪
<<: Day 05 「乖,听话给你吃糖果!」测试与依赖:测资料 之 用资料控制依赖
>>: AI ninja project [day 5] AI RPA系统--表单篇
原型 Prototype 与 原型链 Prototype Chain JavaScript 每一个物...
大家好,我是长风青云。 我发现了一件超级糟糕的事,3分钟真的有点痛苦QAQ 原本我想要今天讲完输入输...
小白食谱搜寻系统缺点 (要说缺点真的是一大堆) 属性不足 : 最大的缺点就是在食材、调味料和做法的栏...
今天我们再解 General Skills 2 题好了, 我看到和昨天同样概念的题型耶 一起解完好了...
接续上一篇的 Layout 相关元件实作後,今天要来开发 React.js 网站的功能是表单设计的...