More About Textures

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

换张图试试看?

继上篇显示出绘制出图片後,不知道有没有读者好奇使用自己的图片试试?如果有,那麽有很高的机率图片是显示不出来的,假设换成这张好了:

another-image

也就是改这行:

-  const image = await loadImage('https://i.imgur.com/ISdY40yh.jpg');
+  const image = await loadImage('https://i.imgur.com/vryPVknh.jpg');

图片就显示不出来了,并且可以在 Console 看到 WebGL 的警告:

non-power-of-two

GL_INVALID_OPERATION: The texture is a non-power-of-two texture. 这张猫图的解析度是 1024x768,宽是 2 的次方,但是高不是,因此产生了错误,为什麽会这样呢?事实上 WebGL1 的 texture 的取样预设运作模式以及 gl.generateMipmap(gl.TEXTURE_2D) 制作缩图功能都只支援宽高皆为 2 次方的图,在上篇使用的图片解析度刚好是 1024x1024 所以可以动,这样的限制可能是效能考量吧,或是说 WebGL1 基於的 openGL 其实已经是蛮古老的了,当时的 GPU 硬体可能只能在 2 次方宽高的图片上做运算

所以图片就一定要事先把宽高调整成 2 的次方吗?其实也不用,无法进行的是『预设运作模式』的 texture 取样以及 gl.generateMipmap(gl.TEXTURE_2D),先把 gl.generateMipmap() 这行注解起来,接着使用 gl.texParameteri() 设定一些参数修改 texture 取样运作模式:

// gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

gl.texParameteri 的第二个输入值表示要设定的参数名称,第三的输入值表示要设定的参数值,在 mdn 文件中可以看到有这些可以设定:

  • gl.TEXTURE_MIN_FILTER: 显示大小比原图小的时候,显示的策略,可以填入的值:
    • gl.NEAREST: 从原图选择 1 个 pixel
    • gl.LINEAR: 从原图选择 4 个 pixel 平均
    • gl.NEAREST_MIPMAP_NEAREST: 从 mipmap 中选最接近的缩图,再选择 1 个 pixel
    • gl.LINEAR_MIPMAP_NEAREST: 从 mipmap 中选最接近的缩图,再选择 4 个 pixel 平均
    • gl.NEAREST_MIPMAP_LINEAR: 从 mipmap 中选最接近的 2 张缩图,分别选择 1 个 pixel 平均,此为预设模式
    • gl.LINEAR_MIPMAP_LINEAR: 从 mipmap 中选最接近的 2 张缩图,分别选择 4 个 pixel 平均
  • gl.TEXTURE_MAG_FILTER: 显示大小比原图大的时候,显示的策略,可以填入的值:
    • gl.NEAREST: 从原图选择 1 个 pixel
    • gl.LINEAR: 从原图选择 4 个 pixel 平均,此为预设运作模式
  • gl.TEXTURE_WRAP_S / gl.TEXTURE_WRAP_T: 对 texture 取样时,座标超出范围的行为,_S _T 分别对 x, y 轴方向进行设定,可以填入的值:
    • gl.REPEAT: 重复 pattern,此为预设运作模式
    • gl.CLAMP_TO_EDGE: 延伸边缘颜色
    • gl.MIRRORED_REPEAT: 重复并镜像 pattern

根据 WebGL 官方 wiki,非 2 次方宽高 texture 只支援:

  • gl.TEXTURE_MIN_FILTERgl.NEARESTgl.LINEAR,也因为没有产生 mipmap,使用这两个选项才不会用到 mipmap 功能
  • gl.TEXTURE_WRAP_S / gl.TEXTURE_WRAP_Tgl.CLAMP_TO_EDGE,为什麽是这样笔者也不清楚...

设定完成之後非 2 次方高的猫图可以显示了:

cat-is-shown

关於 texture 这边常常提到 WebGL1 只有支援什麽什麽,稍微看了一下 mdn 文件的话可以发现有 WebGL2,而且看起来很多支援会好很多,像是这边 2 平方宽高的限制就会直接消失,为什麽不改用 WebGL2 呢?因为相容性不够好,WebGL2 在 2017 年正式推出,现今在 Chrome, Firefox 上都没问题,但是在 Safari 上还是预设不支援,选用 WebGL2 就表示舍弃 iOS 装置,因此笔者在撰写本系列文章这个时间点还是使用 WebGL1 就好

经过笔者测试,iOS 15 beta 版的 Safari 有支援 WebGL2 了,看来不久正式推出後主流浏览器就都支援 WebGL2 了,耶!

笔者当时看到 mipmap 相关资料时有个疑惑,明明在 fragement shader 内只是使用 texture2D() 给予取样的位置,是怎麽知道缩放比例的?查到这篇 stackoverflow 回答,看起来因为 fragment shader 是平行运算的,所以各个邻近 pixel 运算会同时呼叫 texture2D() ,这样 GPU 就可以知道缩放的比例

gl.TEXTURE_MIN_FILTER 运作模式

gl.TEXTURE_MIN_FILTER 可以控制缩小显示时的方式,而这些显示方式显然跟显示品质、效能有关,这边先把图片改回来,并启用 gl.generateMipmap(),比较一下 gl.TEXTURE_MIN_FILTER 各个模式的视觉差异:

gl.NEAREST:

gl.NEAREST

gl.LINEAR:

gl.LINEAR

gl.NEAREST_MIPMAP_NEAREST:

gl.NEAREST_MIPMAP_NEAREST

gl.LINEAR_MIPMAP_NEAREST:

gl.LINEAR_MIPMAP_NEAREST

gl.NEAREST_MIPMAP_LINEAR:

gl.NEAREST_MIPMAP_LINEAR

gl.LINEAR_MIPMAP_LINEAR:

gl.LINEAR_MIPMAP_LINEAR

使用越多 pixel 做平均的显示出来的成像就越平滑,但是也比较消耗效能,最後这个 gl.LINEAR_MIPMAP_LINEAR 从 mipmap 中选最接近的 2 张缩图,分别选择 4 个 pixel 平均,意思就是一次 texture2D() 得读取 texture 中 2x4 = 8 个 pixel 出来平均,结果也最平滑

绘制赛车格纹

为了同时试玩 gl.TEXTURE_MAG_FILTER 以及重复 pattern 的 texture,接下来绘制看起来像这样的赛车格纹:

block-pattern

可以看得出来整张图就是重复这边红色框起来的区域:

repeated-area

也就是说,这张图只需要 2x2 的大小,左上、右下为白色,右上、左下为黑色。事实上,输入 texture 的 gl.texImage2D() 支援各式各样的输入来源,其中一个是 ArrayBufferView,也就是可以传(有型别的)阵列资料进去:

const whiteColor = [255, 255, 255, 255];
const blackColor = [0, 0, 0, 255];
gl.texImage2D(
  gl.TEXTURE_2D,
  0, // level
  gl.RGBA, // internalFormat
  2, // width
  2, // height
  0, // border
  gl.RGBA, // format
  gl.UNSIGNED_BYTE, // type
  new Uint8Array([
    ...whiteColor, ...blackColor,
    ...blackColor, ...whiteColor,
  ])
);

因为 type 使用 gl.gl.UNSIGNED_BYTE,也就是每个 pixel 的每个颜色 channel 为一个 Uint8Array 的元素,白色 RGBA 即为 [255, 255, 255, 255]、黑色 RGBA 即为 [0, 0, 0, 255];另外直接传阵列进去时,需要额外给的是 width, height, border,这张图为 2x2 且没有 border,给予的参数如上所示

为什麽要使用 gl.RGBA? 因为有个 gl.UNPACK_ALIGNMENT 的设定值,这个值预设为 4,表示每行的储存单位为 4 bytes,如果这样 2x2 的小 texture 要使用 RGB 就得透过 gl.pixelStorei() 改成 1,我们这边使用 gl.RGBA 符合预设值就好

把原本使用外部图片的 texImage2D() 注解起来,存档重整可以看到:

2x2-block-linear

现在显示大小显然比原图来的大,所以是 gl.TEXTURE_MAG_FILTER 预设的 gl.LINEAR 导致的平滑效果,但是现在的状况不想要平滑效果,因此加上这行:

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

2x2-block-nearest

这张图显然不会用到 mipmap,可以把 gl.generateMipmap() 注解起来并使用 gl.TEXTURE_MIN_FILTER => gl.LINEAR 以停用 mipmap:

// gl.generateMipmap(gl.TEXTURE_2D);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

接下来让图片重复,直接修改 texcoord gl.bufferData() 时传入的值:

// after gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);

gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array([
    0, 0, // A
    8, 0, // B
    8, 8, // C

    0, 0, // D
    8, 8, // E
    0, 8, // F
  ]),
  gl.STATIC_DRAW,
);

笔者把 1 改成 8,所以 x, y 轴皆将重复 8 次,我们确实还没改完,不过可以来看一下 gl.CLAMP_TO_EDGE 的结果:

8x8-clamp-to-edge

可以看到黑色的边缘被延伸到最後,要得到想要的结果得把 gl.TEXTURE_WRAP_S, TEXTURE_WRAP_T 改成 gl.REPEAT 重复图样:

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);

大功告成:

block-pattern

本篇各个阶段的完整程序码:

介绍 texture 功能至此,之後甚至可以让 GPU 渲染到 texture 上,有相关需求时再接续讨论。到目前为止我们使用 WebGL 网页读取完毕只会绘制一次,待下篇来加入控制项接收事件重新渲染画面,并制作成动画


<<:  [烧烤吃到饱-1] 爱烤爱对罗 - 台中店 #中秋节烤肉精选店家

>>:  Day7 SQL

[Day27] 沟通之术 - 开发工程师篇

撑过了双十连假啦~~这也是铁人赛接近尾声的倒数第 4 篇~今天就来讲讲跟开发工程师的沟通之术吧! 前...

【DAY 22】Algorithm - Insertion sort 插入排序法

前面我们提过了 Bubble sort,这次我们要来从题目来看另一种排序的演算法 —— Insert...

Day 21. Zabbix 自动化通知介绍

首先在介绍自动化通知之前,需要介绍是如何被触发的。 触发器的设定也是跟着套用样板时候被设定。 具体可...

【Day8】 将Function当成state传给子类别套用在事件上吧≖‿≖

相信各位看官们很熟悉各种Html的Events事件, 这篇呢~我们要透过上一篇所提到的State传入...

第7天

中秋连假结束~ 参考线完成开始放入标题~ 把textview跟参考线连在一起~ 使用TextView...