Uniform - shader 之参数

大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 4 篇文章。本系列文章从 WebGL 之基础开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是 WebGL 基本的运作机制以及如何使用其提供的功能,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容

使用『画布中的 x/y pixel 位置』定位

在上一篇虽然把三角形画出来了,但是在传入 a_position 时要先算出顶点在 clip space 中 -1 ~ +1 的值,如果要画更多 2D 三角形,可以用 pixel 为单位直接在画布上定位会方便许多,本篇就以这个为目标进行修改

Uniform - 设定在 program 上的参数

Uniform 类似 attribute,可以把资料传到 shader 内,但是使用上比 attribute 简单许多,因为 uniform 是直接设定在 program 上的,因此不会有各个顶点读取 buffer 中哪个位置的问题,也是因为这样,在每个顶点计算的时候 uniform 的值都一样,所以才叫做 uniform 吧

使三角形的定位使用『画布中的 pixel 位置』,要先算出顶点在 clip space 中 -1 ~ +1 的值,要做到这件事情当然可以写一个简单的 function 在 gl.bufferData() 之前对座标做一些处理,但是这边是一个很适合使用 uniform 解决的问题;当传入 positionBuffer 的顶点座标是画布上 x/y 轴的 pixel 位置,而输出给 gl_Position 的值必须介於 -1 ~ +1,shader 需要知道的资讯就是画布宽高,画布的宽高不论在哪个顶点值都相同,故适合以 uniform 来处理

在 vertex / fragment shader 中可以以这样的方式宣告 uniform:

uniform vec2 u_resolution;

接着跟 attribute 一样,先取得变数位置:

const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution');

在呼叫 gl.useProgram() 设定好使用中的 program 之後,像这样就可以对着使用中的 program 设定 uniform:

gl.uniform2f(resolutionUniformLocation, canvas.width, canvas.height);

canvas 是第一篇 document.getElementById('canvas') 取得的元素,其身上就有 .width, .height 可用

在 Chrome 的 Console 上,输入 gl.uniform 可以看到有这麽多 function:

uniform-types

这些 uniformXXX 是针对不同型别所使用的,像是笔者上面使用的 uniform2f2f 表示 2 个元素的 float,也就是 vec2,这边可以看到一路从 1f 单个 float 到 Matrix4f 设定整个 4x4 矩阵都有。除此之外,对於每种资料型别分别还有一个结尾多了 v 的版本 (以 uniform2f 为例:gl.uniform2fv),其实功能没什麽不同,只是 function 接收参数的方式改变,从 gl.uniform2f(index, x, y) 变成 gl.uniform2fv(index, [x ,y])

当然也得来修改 vertex shader 使用 u_resolution 做转换:

void main() {
  gl_Position = vec4(
    a_position / u_resolution * vec2(2, -2) + vec2(-1, 1),
    0, 1
  );
}

讲解一下:假设宽高是 300x150,一组顶点位置 a_position 为 (150, 90),除以 u_resolution 得到 (0.5, 0.6) 0 ~ 1 之间的位置,最後分别对 x 座标 * 2 - 1、对 y 座标 * -2 + 1 得到 (0.0, -0.2) 给 gl_Position 在 clip space 中的位置。这边我想读者会有两个疑问:

  1. vec2 可以跟 vec2 直接做加减乘除运算?对,相当於每个元素分别做运算,以加法为例像是这样:vec2(x1, y1) + vec2(x2, y2) = vec2(x1+x2, y1+y2)。笔者看到这样的写法第一个瞬间也是『这样会动?』像 Javascript [1,2] * [3,4] 只会得到 NaN,毕竟一般常见的程序语言的用途比较通用 (general) 不像 GLSL 很常有这样的运算特化出 vec 之间加减乘除的写法
  2. 对 x 座标 * 2 - 1,而对 y 座标 * -2 + 1? 因为在 clip space /画布 中,上方为 y = 1、下方为 y = -1,因此 y 轴正向指着上方的,这个方向和我们在电脑中图片、网页的 y 轴方向是相反的,既然要做转换,那就把这个问题一起修正

传入的顶点位置 a_position 可以改用 pixel 座标了

笔者用上面的公式拿原本的值做反向运算可以得知在 300x150 的 pixel 座标:

gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([
    150, 60,
    180, 82.5,
    120, 82.5,
  ]),
  gl.STATIC_DRAW,
);

不过看起来没有任何改变就是了...

使画布填满整个画面

如果读者使用过 CSS,并且知道 <canvas /> 元素类似 <img />,那麽应该可以想到简单的这几行 CSS,笔者直接写在 HTML 上:

<style>
  html, body {
    margin: 0;
    height: 100%;
  }
  #canvas {
    width: 100%;
    height: 100%;
  }
</style>

但是重整之後看到的只是放大的样子,就像是把图片放大的感觉:

scaled-canvas

<canvas /> 元素上有自己的宽高资讯,类似於图片的原始大小,可以在 Console 上输入 gl.canvas.width 从 WebGL instance 找回 canvas 元素并取得『原始大小』的宽度:

canvas-width-height

显然还是原本预设的值,幸好 DOM API 有另外一组提供实际的宽高 .clientWidth, .clientHeight,我们可以直接把 .clientWidth / .clientHeight 设定回这个 canvas 图片的原始大小:

canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;

// before gl.clearColor(...)

canvas-resized

模糊的现象消失了,看起来实际大小的更动有效,但是那个三角形的位置显然不太对...

事实上,WebGL 还有一个内部的『绘制区域』设定,因此还需要:

// after canvas.height = canvas.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);

// before gl.clearColor(...)

参数分别为 x, y, width, height,这个 x, y 是指左下角在画布中的位置,这边我们要填满整张画布,给 0 即可,并把宽高给满。大家可能会想说,为什麽 WebGL 有内部的『绘制区域』的设定?不知道各位有没有玩过马力欧赛车的多人同乐模式,这种在同一个萤幕『分割画面』的状况,就可以用 gl.viewport 设定绘制区域为一个玩家绘制画面,绘制完再呼叫 gl.viewport 绘制另外一位玩家的画面,WebGL 没有帮开发者预设使用情境,因此需要自行呼叫 gl.viewport 来修正

result

若在网页载入渲染完成後调整视窗大小,一样会发生拉伸的状况,这时 canvas.width, canvas.height 跟 WebGL 绘制区域都得再进行调整并重新绘制

终於正确了,三角形的顶点位置符合 a_position 传入的 pixel 座标值,本篇的完整程序码可以在这边找到:

画面上只有一个三角形显然有点孤单,待下篇来画多个、颜色不同的三角形


<<:  AI ninja project [day 4] AI RPA系统--名片model建立篇

>>:  第04天 - 一些些的HTML

28 - 有效的使用 Observability 的资料 (2) - 使用 Kibana Alerts 主动通知异常状况

有效的使用 Observability 的资料 系列文章 (1/4) - 透过 Machine Le...

阅读.evtx文件--关於从16进位看事件纪录这回事

事情来自某天我在找资料的过程中,看到有些大大提供了事件纪录档的文本说明,所以今天要来试着阅读.evt...

Messenger 隐藏讯息功能|专页必学推播讯息

Messenger有不少的隐藏功能,立即看看如何使用这些功能来增加对话时的趣味性吧! 大家知道Mes...

[Day 13]Template应用

大家好,明明才第13天,我已经不知道要发什麽文了呜呜╯︿╰,但难产生出的文还是一样充满了知识!!希望...