大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 4 篇文章。本系列文章从 WebGL 之基础开始介绍,最後建构出绘制 3D、光影效果之网页。本章节讲述的是 WebGL 基本的运作机制以及如何使用其提供的功能,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
在上一篇虽然把三角形画出来了,但是在传入 a_position
时要先算出顶点在 clip space 中 -1 ~ +1
的值,如果要画更多 2D 三角形,可以用 pixel 为单位直接在画布上定位会方便许多,本篇就以这个为目标进行修改
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:
这些 uniformXXX
是针对不同型别所使用的,像是笔者上面使用的 uniform2f
的 2f
表示 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 中的位置。这边我想读者会有两个疑问:
vec2
可以跟 vec2
直接做加减乘除运算?对,相当於每个元素分别做运算,以加法为例像是这样:vec2(x1, y1) + vec2(x2, y2) = vec2(x1+x2, y1+y2)
。笔者看到这样的写法第一个瞬间也是『这样会动?』像 Javascript [1,2] * [3,4]
只会得到 NaN
,毕竟一般常见的程序语言的用途比较通用 (general) 不像 GLSL 很常有这样的运算特化出 vec
之间加减乘除的写法* 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>
但是重整之後看到的只是放大的样子,就像是把图片放大的感觉:
在 <canvas />
元素上有自己的宽高资讯,类似於图片的原始大小,可以在 Console 上输入 gl.canvas.width
从 WebGL instance 找回 canvas
元素并取得『原始大小』的宽度:
显然还是原本预设的值,幸好 DOM API 有另外一组提供实际的宽高 .clientWidth
, .clientHeight
,我们可以直接把 .clientWidth
/ .clientHeight
设定回这个 canvas
图片的原始大小:
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// before gl.clearColor(...)
模糊的现象消失了,看起来实际大小的更动有效,但是那个三角形的位置显然不太对...
事实上,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
来修正
若在网页载入渲染完成後调整视窗大小,一样会发生拉伸的状况,这时
canvas.width
,canvas.height
跟 WebGL 绘制区域都得再进行调整并重新绘制
终於正确了,三角形的顶点位置符合 a_position
传入的 pixel 座标值,本篇的完整程序码可以在这边找到:
画面上只有一个三角形显然有点孤单,待下篇来画多个、颜色不同的三角形
<<: AI ninja project [day 4] AI RPA系统--名片model建立篇
=x= 🌵 建立 News Manager - Content Page 後台页面 - 日历功能。 ...
有效的使用 Observability 的资料 系列文章 (1/4) - 透过 Machine Le...
事情来自某天我在找资料的过程中,看到有些大大提供了事件纪录档的文本说明,所以今天要来试着阅读.evt...
Messenger有不少的隐藏功能,立即看看如何使用这些功能来增加对话时的趣味性吧! 大家知道Mes...
大家好,明明才第13天,我已经不知道要发什麽文了呜呜╯︿╰,但难产生出的文还是一样充满了知识!!希望...