大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 6 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容
有在玩游戏的读着们在讨论一款 3D 游戏的时候,可能有提到游戏内的『3D 贴图』,游戏 3D 物件表面常常不会是纯色或是渐层单调的样子,而是有一张图片贴在这个物件表面的感觉,所以才叫做『3D 贴图』吧,而且也可以用在图案重复的『材质』显示上,因此在英文叫做 texture。虽然现在还完全没有进入 3D 的部份,但是 3D 贴图/texture 追根究底得有个方法在 WebGL 里面取用、显示图片,本篇抽离 3D 的部份,来介绍在 WebGL 取用、显示图片的方式
02-texture-2d.html
/ 02-texture-2d.js
本篇开始将使用新的 .html
作为开始,起始点完整程序码可以在这边找到:github.com/pastleo/webgl-ironman/commit/75179fb,笔者将 createShader
, createProgram
移动到工具箱 lib/utils.js
,里面有 loadImage
用来下载并回传 Image
元素(注意,是 async function),并且 position
与 Day 5 相同使用 pixel 座标定位,看起来像是这样:
这一个灰色的正方形是由两个三角形组成的,读者可以在 02-texture-2d.js
的 gl.bufferData()
中看到每个顶点後面有一个注解字母,其对应了下面这张示意图:
接下来以此为起点,让灰色方形区域显示图片
建立之前,把来源图片下载好,直接呼叫 loadImage
并传入图片网址,因为牵扯到非同步,这边得用 await
(也是因此得写在 async function main()
内):
const image = await loadImage('https://i.imgur.com/ISdY40yh.jpg');
这张图片是笔者的大头贴:
接着建立、bind(对准)并设定 texture:
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0, // level
gl.RGB, // internalFormat
gl.RGB, // format
gl.UNSIGNED_BYTE, // type
image, // data
);
可以发现 gl.createTexture()
/ gl.bindTexture()
这个组合与 gl.createBuffer()
/ gl.bindBuffer()
这个组合神似,建立并且把 gl.TEXTURE_2D
目标对准 texture;接下来设定 texture 资料 gl.texImage2D()
:
level
: 对於一个 texture 其实有许多缩放等级,与接下来的 gl.generateMipmap
有一定的关系,不过这边通常是填 0
表示输入的是原始尺寸/最大张的图internalFormat
, format
, type
: 根据 mdn 文件,format
在 WebGL1 必须与 internalFormat
相同,从文件中 internalformat 下方的表格可以看到有哪些选项可以填,显然来源图片有 RGB 三个 channel,想当然尔 format 选 RGB
,而 type
没有特别的需求选择 UNSIGNED_BYTE
最後 data
直接把 image
元素给进去,图片就以 RGB 的格式输入到 GPU 内 texture 的 level: 0
位置上
读者可能对 level
的部份很纳闷,可以想像一下,在 3D 的世界中镜头可能距离贴图很遥远,显示时为了效率没办法当下做完整图片的缩放,因此会事先把各个尺寸的缩图做好放在记忆体里,这样的东西叫做 mipmap,而 level
表示缩放的等级,0
表示没有缩放的版本。所以开发者得自己把各个缩放尺寸做好分别输入吗?幸好 WebGL 有内建方法一行对着目前的 texture 产生所有尺寸:
gl.generateMipmap(gl.TEXTURE_2D);
回想 fragment shader 的运作方式:在每个 pixel 运算其颜色。那麽如果要显示 texture,就会变成『在每个 pixel 运算时从 texture 图片上的某个位置取出其颜色来输出』。在 GLSL 中可以透过 uniform 传输一种叫做 Sampler2D
的资料型别:
uniform sampler2D u_texture;
把这个 uniform 变数叫做 u_texture
,其实就是 texture。接着 GLSL 的内建 function texture2D()
可以进行上面所说的『从 texture 图片上的某个位置取出其颜色』:
gl_FragColor = texture2D(u_texture, v_texcoord);
v_texcoord
即为『某个位置』,既然是 sampler2D
,v_texcoord
型别必须是 vec2
表示在 texture 图片上的 (x, y)
座标,并且 (0.0, 0.0)
为图片左上角, (1.0, 1.0)
为图片右下角。脑筋快的应该已经想到,v_texcoord
是一个 varying,因为每个顶点之间所要取用的 texture 图片座标是连续、平滑的。最後完整的 fragment shader 长这样:
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texcoord);
}
v_texcoord
与 Day 5 类似,既然 fragment shader 需要 varying,因此得在 vertex shader 提供 varying,vertex shader 又需要从 attribute 取得 texture 各个顶点需要取用的座标,对 vertex shader 加上这几行:
attribute vec2 a_position;
+attribute vec2 a_texcoord;
uniform vec2 u_resolution;
+varying vec2 v_texcoord;
+
void main() {
gl_Position = vec4(
a_position / u_resolution * vec2(2, -2) + vec2(-1, 1),
0, 1
);
+ v_texcoord = a_texcoord;
}
取得 a_texcorrd
attribute 位置、并设定 buffer, vertex attribute array:
const texcoordAttributeLocation = gl.getAttribLocation(program, 'a_texcoord');
// ...
// a_texcoord
const texcoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.enableVertexAttribArray(texcoordAttributeLocation);
gl.vertexAttribPointer(
texcoordAttributeLocation,
2, // size
gl.FLOAT, // type
false, // normalize
0, // stride
0, // offset
);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, 0, // A
1, 0, // B
1, 1, // C
0, 0, // D
1, 1, // E
0, 1, // F
]),
gl.STATIC_DRAW,
);
因为 texture (0.0, 0.0) 为图片左上角, (1.0, 1.0) 为图片右下角,在 gl.bufferData()
对於每个顶点的填入的 texcoord
示意图如下:
整张 texture 应该是个巨大的阵列资料,但是与 array buffer 不同,texture 必须提供随机存取(random access),意思是说 fragment shader 不论在哪个 pixel 都可以取用 texture 任意位置的资料;texture 又是用 uniform 类似指标的方式提供给 shader 使用,可能 texture 在 GPU 上有特别的硬体做处理
首先一样取得 uniform 位置:
const textureUniformLocation = gl.getUniformLocation(program, 'u_texture');
然後设定把 texture 启用在一个『通道』,并把这个通道的编号传入 uniform:
// after gl.useProgram()...
const textureUnit = 0;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.uniform1i(textureUniformLocation, textureUnit);
textureUnit
为通道的编号,设定为 0
使用第一个通道gl.bindTexture
把目标指向建立好的 texture
,如果有其他 texture 导致目标更换时,这边要把目标设定正确,虽然本篇只有一个 texture 就是了gl.activeTexture()
启用通道并把目标 texture 设定到通道上,这边还有神奇的 gl.TEXTURE0 + textureUnit
写法;读者可以尝试在 Console 输入 gl.TEXTURE1 - gl.TEXTURE0
(1
),或是 gl.TEXTURE5 - gl.TEXTURE2
(3
),就可以知道为什麽可以用 +
共用 textureUnit
指定通道了gl.uniform1i
传的是 1 个整数,把通道的编号传入,在 fragment shader 中就会直接被反应成 sampler2D
一切顺利的话,就可以看到图片出现在 canvas 里头,fragment shader 成功地『在每个 pixel 运算时从 texture 图片上的某个位置取出其颜色来输出』:
读者如果有兴趣,可以修改 texcoord
的数字感受一下 texture2D()
,像是把 C 点 texcoord
改成 (0.8, 0.8) 就变成这样:
本篇的完整程序码可以在这边找到:
但是关於 texture 其实还有许多细节,待下篇再来继续讨论
<<: 30天轻松学会unity自制游戏-让Player动起来
>>: [13th-铁人赛]Day 1:Modern CSS 超详细新手攻略 - 简介
演算法在程序设计中扮演重要的角色,而演算法和时间复杂度有很大的关联, 时间复杂度本意为程序执行的时间...
以下内容皆参考 Backtrader 官网 昨天介绍了 backtrader 如何去执行一个策略,今...
物件内可以放function 学生考了几分 var exam = {} console.log(ex...
在React中加入CSS样式分爲3种方式: 1.External css (外部样式) 外部样式是使...
等待UI开发者的回覆之际,算是有空闲的时间可以展开实际的怪物调整。有监於Invenotry系统的复杂...