Varying - fragment shader 之资料

大家好,我是西瓜,你现在看到的是 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);

看起来像是这样:

multiple-triangles

这些只是笔者随便想到的图案,读者们可以自行发挥想像力调整顶点座标

笔者当时很好奇:如果 buffer 资料长度不足,或是 count 不是三的倍数,那麽会怎麽样呢?如果把最後一组顶点删除(220, 40 那组),count 保持为 9,不仅不会有什麽阵列超出的错误,感觉上 vertex attribute array 还给不足的部份填上预设值 0

buffer-not-full

或者把 count 改成 8,也不会有错误,只是最後一组三角形没有完整,两个点凑不出一个面,因此最後一个三角形就消失了:

draw-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

varying 这功能可以让 vertex shader 输出资料给 fragment shader 使用,但是两者执行的回合数显然是对不起来,假设回到一个低解析度三角形的状况如下图,vertex shader 执行三次得到三个顶点,灰色的方格每格执行一次 fragment shader 计算颜色:

vertex-fragment

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 平滑化的样子:

varying

这个特性不仅解决问题,也让笔者觉得相当有意思,有种当初玩 flash 移动补间动画的感觉

输入颜色资讯到 varying 给 fragment shader 使用

我们从 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 都有提过,比较需要注意几点:

  1. gl.vertexAttribPointer() 以及 gl.bufferData() 该行执行的当下要注意 bind 的 ARRAY_BUFFER 是哪个,要不然会对着错误的目标做事,当然最好的就是把对於一个 attribute 的操作清楚分好,日後也比较好看出该区域在操作的对象
  2. gl.vertexAttribPointer()size: 3,因为颜色有 3 个 channel: RGB,因此对於每个顶点 gl.bufferData() 要给 3 个值
  3. 笔者在 gl.vertexAttribPointer() 使用 gl.UNSIGNED_BYTE 配合 normalize: true 来使用,在 Day 3 有提到: normalize 配合整数型别时可以把资料除以该型别的最大值使 attribute 变成介於 <= 1 的浮点数,那麽在 gl.bufferData() 时传入 Uint8Array,并且可以在资料内容写熟悉的 rgb 值

总结来说资料流如下:

  1. 每个顶点有一组 (x, y) 座标值 a_position 以及颜色资料 a_color
  2. 在 vertex shader 除了计算 clip space 座标外,设定 varying v_color 成为 a_color
  3. 在各个顶点之间 v_color 会平滑化,约接近一个顶点的 pixel v_color 就越接近该顶点当初设定的 a_color
  4. fragment shader 拿到 v_color 并直接输出该颜色

对於颜色资料的部份,笔者在前三个顶点给一样的颜色,所以第一个三角形是纯色,第二、第三个三角形的第一个顶点为黑色,剩下两个顶点为白色,因为平滑化的缘故,会得到渐层的效果:

multiple-different-color-triangles

本篇的完整程序码可以在这边找到:

笔者认为 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系统--表单篇

JavaScript Prototype (原型)

原型 Prototype 与 原型链 Prototype Chain JavaScript 每一个物...

Day 2 - 输出

大家好,我是长风青云。 我发现了一件超级糟糕的事,3分钟真的有点痛苦QAQ 原本我想要今天讲完输入输...

食谱搜寻系统未来展望

小白食谱搜寻系统缺点 (要说缺点真的是一大堆) 属性不足 : 最大的缺点就是在食材、调味料和做法的栏...

[ Day7 ] General Skills 小暖身

今天我们再解 General Skills 2 题好了, 我看到和昨天同样概念的题型耶 一起解完好了...

[ Day 28 ] 实作一个 React.js 网站 4/5

接续上一篇的 Layout 相关元件实作後,今天要来开发 React.js 网站的功能是表单设计的...