内容简介:Buffer(数据缓冲区)与 Texture(纹理)是 WebGL 程序的两大数据来源。Buffer 可以通过 ArrayBuffer 或更语义化的 TypedArray 来构造;而 Texture 在大多数情况下,是通过 Image 对象来构造的。在构造和使用 Texture 的过程中,需要确定很多为此,我专门编写了一个 Demo,如下所示。Demo 页的右上角有一个使用
Buffer(数据缓冲区)与 Texture(纹理)是 WebGL 程序的两大数据来源。Buffer 可以通过 ArrayBuffer 或更语义化的 TypedArray 来构造;而 Texture 在大多数情况下,是通过 Image 对象来构造的。在构造和使用 Texture 的过程中,需要确定很多 选项 来以不同的方式构造 Texture;这些选项之间有着各种各样的关系,或互相依赖,或互相排斥,或互相影响。最近,我又重新梳理了一遍我所用到的 WebGL 纹理各种参数的影响,稍作整理,以防遗忘。
为此,我专门编写了一个 Demo,如下所示。Demo 页的右上角有一个使用 dat.GUI 生成的控件,其中列举了影响纹理的一些选项。这篇文章将逐个讨论这些选项的作用和相互关系。
Wrap
在 JavaScript 中,创建纹理的基本流程大约如下所示:
// 激活纹理单元 gl.activeTexture(...); // 创建和绑定 const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // 参数设置 gl.texParameteri(...); gl.texParameteri(...); // 填充纹理的内容 gl.texImage2D(..., image); // 通过纹理单元将纹理传送给着色器程序 gl.uniform1i(...);
然后,在着色器中,使用一个坐标 (x,y) 从纹理上取色。
vec4 color = texture2D(texture, vec2(x, y));
通常,从纹理上取色的坐标 x 和 y 的值均在 0~1 之间。Wrap 配置项规定了当取色坐标的坐标取值在 (0, 1) 之外(即试图从纹理图片之外取色)时,应该如何取色。Wrap 有两种:切割(CLAMP)和重复(REPEAT)。我们可以操作上面的 Demo,调整 scale 的值使得纹理图片缩小一些,然后切换 Wrap 配置项为 CLAMP 或 REPEAT,可以发现:当选项置为 CLAMP 时,从纹理外部取色会落到对应的纹理的边缘,比如 texture2D(texture, vec2(2.2, 0.5))
的值会等于 texture2D(texture, vec2(1.0, 0.5))
;而选项置为 REPEAT 时,纹理会「平铺」开来,从外部取色会把取色坐标按1取模,映射到纹理内部,比如 texture2D(texture, vec2(2.2, 0.5))
的值会等于 texture2D(texture, vec2(0.2, 0.5))
。
切换 Wrap 配置项的代码如下:
// CLAMP 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); // REPEAT gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
下图是 REPEAT 的情况:
下图是 CLAMP 的情况:
有一点需要注意的是,REPEAT 模式对纹理图片的尺寸有要求,宽度和高度必须为 2 的整数次幂,如 32x32 或 1024x256 的图片都是符合要求的,但 500x512 或 300x300 是不符合的。我们可以将控件中的 size 从 512 改成 300,这时 Demo 将加载这张图片的一个尺寸为 300x300 的替代品作为纹理。如果 Wrap 为 CLAMP,我们会发现稍微纹理模糊了一些,但是如果 Wrap 为 REPEAT,则会报警并黑屏。
FlipY
FlipY 是 WebGL 的一个全局配置项(准确地说,它的名称是 UNPACK_FLIP_Y_WEBGL),在本文的范畴中,它主要影响了 texImage2D()
方法的行为。
按照 OpenGL 的惯例,当着色器调用 texture2D(t, st)
方法从纹理中取色时,传入的取色坐标 st
的原点是左下角。换言之,如果传入的坐标是 (0, 0),那么 OpenGL 期望取到的是左下角的那个像素的颜色。但是,由于在 Web 上图片数据的存储是从左上角开始的,传入坐标 (0,0) 时,实际上会取到左上角坐标的值。如果我们设置了 FlipY
配置项,那么在向纹理中加载数据的时候,就会对数据作一次翻转,使纹理坐标原点变为左下角。
开启 FlipY 的代码是:
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
在 Demo 中,默认情况下 FlipY 配置项是勾选的,如果你勾选取消,会发现纹理图片被翻转了。下图是取消 FlipY 配置后的情况。
MIN_FILTER 和 MAG_FILTER
一个纹理是由离散的数据组成的,比如一个 2x2 的纹理是由 4 个像素组成的,使用 (0,0)、(0, 1) 等四个坐标去纹理上取样,自然可以取到对应的像素颜色;但是,如果使用非整数坐标到这个纹理上去取色。比如,当这个纹理被「拉近」之后,在屏幕上占据了 4x4 一共 16 个像素,那么就会使用 (0.33,0) 之类的坐标去取值,如何根据离散的 4 个像素颜色去计算 (0.33,0) 处的颜色,就取决于参数 MAG_FILTER。在 WebGL 中设置这两个项的代码如下所示:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR );
MAG_FILTER 有两个可选项,NEAREST 和 LINEAR。顾名思义,NEAREST 就是去取距离当前坐标最近的那个像素的颜色,而 LINEAR 则会根据距离当前坐标最近的 4 个点去内插计算出一个数值,如图所示。
显然 NEAREST 的运行速度更快,但 LINEAR 的效果更好。使用 NEAREST 时,当纹理被拉得比较近,颗粒感会比较明显,而使用 LINEAR 则会顺滑一些。你可以切换右上角空间上的 MAG_FILTER 选项,然后拖动 scale 滑块将纹理放大,体会一下两者的差别。
下图是 MAG_FILTER 为 NEAREST 的效果:
下图是 MAG_FILTER 为 LINEAR 的效果:
MAG_FILTER 作用于将纹理拉近/放大的情形,而当纹理远离/缩小的时候,起作用的是 MIN_FILTER。MIN_FILTER 有以下 6 个可选配置项:
- NEAREST
- LINEAR
- NEAREST_MIPMAP_NEAREST
- NEAREST_MIPMAP_LINEAR
- LINEAR_MIPMAP_NEAREST
- LINEAR_MIPMAP_LINEAR
前两个配置项和 MAG_FILTER 的含义和作用是完全一样的。但问题是,当纹理被缩小时,原纹理中并不是每一个像素周围都会落上采样点,这就导致了某些像素,完全没有参与纹理的计算,新纹理丢失了一些信息。假设一种极端的情况,就是一个纹理彻底缩小为了一个点,那么这个点的值应当是纹理上所有像素颜色的平均值,这才比较合理。但是 NEAREST 只会从纹理中取一个点,而 LINEAR 也只是从纹理中取了四个点计算了一下而已。这时候,就该用上 MIPMAP 了。
Mipmap 和 MIN_FILTER
为了在纹理缩小也获得比较好的效果,需要按照采样密度,选择一定数量(通常大于 LINEAR 的 4 个,极端情况下为原纹理上所有像素)的像素进行计算。实时进行计算的开销是很大的,所有有一种称为 MIPMAP(金字塔)的技术。在纹理创建之初,就为纹理创建好 MIPMAP,比如对 512x512 的纹理,依次建立 256x256(称为 1 级 Mipmap)、128x128(称为 2 级 Mipmap) 乃至 2x2、1x1 的纹理。实时渲染时,根据采样密度选择其中的某一级纹理,以此避免运行时的大量计算。
显而易见的是,使用 MIPMAP 同样需要纹理尺寸为 2 的整数次幂。
WebGL 中,可以通过调用以下方法来生成 mipmap。
gl.generateMipmap(gl.TEXTURE_2D);
我们将控制面板中的 generateMipmap
打开,并把 scale 调到一个较小的值(比如 0.5),然后切换 MIN_FILTER 的各种设置,就可以观察到 mipmap 的效果了。
下图是 MIN_FILTER 为 LINEAR 的效果(NEAREST 效果是类似的):
下图是 MIN_FILTER 为 LINEAR_MIPMAP_NEAREST 的效果(LINEAR_MIPMAP_LINEAR 效果是类似的):
可以看到当采用 MIPMAP 时,纹理平滑了很多(特别是头发部分)。
MIN_FILTER 的默认值是 LINEAR_MIPMAP_NEAREST。在 XXX_MIPMAP_XXX 的格式中,前一个 XXX 表示在单个 MIPMAP 中取色的方式,与单独的 LINEAR 或 NEAREST 类似,而后一个 XXX 表示,随着采样密度连续变化(通常是因为缩放因子连续变化)时,是否在多层 MIPMAP 之间内插。使用 MIPMAP 时,后一个 LINEAR 比较重要,只要后者是 LINEAR,前者的意义其实并不特别大,所以默认选项 NEAREST_MIPMAP_LINEAR 也是最常用的。
自定义 Mipmap
我们可以使用 gl.generateMipmap()
方法来根据原始纹理生成一套 mipmap,这种生成的方式是默认的。但是实际上,我们还有一种更灵活的方式来自定义 Mipmap,那就是直接传入另一张图片。比如,这里我们使用的是一张 512x512 的 lena 图,调用 gl.generateMipmap()
会生成 256x256、128x128 直至 1x1 等一系列图片。但是,如果我们手头正好有一张 128x128 尺寸的图片,我们就可以强制指定这张图片作为原始纹理的 1 级 Mipmap。
自定义 Mipmap 纹理的代码如下所示:
// 首先要调用此方法 // 如果在 texImage2D 后调用,则定制的 mipmap 会被覆盖 gl.generateTexture(gl.TEXTURE_2D); gl.texImage2D( gl.TEXTURE_2D, 1, // 就是这个参数指定几级 Mipmap gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image // 尺寸必须严格符合指定级别 Mipmap 应有的尺寸 );
当我们指定好自定义的 Mipmap 纹理后(即勾选 customMipmap),同时勾选 generateMipmap,并且保证 MIN_FILTER 在 XXX_MIPMAP_XXX 的配置上,此时拖动 scale 滑块,会发现在缩小过程中的某个过程,纹理会变成自定义图片。当 MIN_FILTER 在 XXX_MIPMAP_NEAREST 时,纹理是「突变」的;而当 MIN_FILTER 位于 XXX_MINMAP_LINEAR 时,纹理是「渐变」的,这也印证了前面关于 XXX_MIPMAP_XXX 的解释。
下图是 MIN_FILTER 为 LINEAR_MIPMAP_NEAREST 时缩放的效果:
下图是 MIN_FILTER 为 LINEAR_MIPMAP_LINEAR 时缩放的效果:
SRGB 扩展
RGB 颜色空间是为人眼设计的。但是人眼感光强度和真实光线的物理强度并不是线性关系,这是人眼的生理结构所决定的。比如,在人眼看来,屏幕上显示的白色 #FFFFFF 的亮度,是半灰色 #888888 亮度的 2 倍,但是两者真实的光线物理强度(直射下单位面积上受光照的功率)并不是 2 倍关系。所以,当着色器需要基于一些物理规律来计算颜色时,直接取纹理的颜色没有意义,需要作一次转换。
一个经验是,将人眼感知的颜色归一化到 (0,1) 之间,然后取 2.2 次方幂,得到的结果与光线物理强度是呈线性关系的。如下所示,横轴是 RGB 色彩空间,蓝色直线是人眼的感光曲线,红色曲线是光线物理强度曲线。
SRGB 扩展所做的就是这件事,在用户调用 texImage2D
向纹理中传输数据时,将纹理的每个像素颜色取了自己的 2.2 次方幂。开启 SRGB 扩展的代码如下所示:
const SRGBExtension = gl.getExtension("EXT_SRGB"); gl.texImage2D( gl.TEXTURE_2D, 0, // 接下来两个参数原本是 gl.RGBA SRGBExtension.SRGB_ALPHA_EXT, SRGBExtension.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE, image );
在控件中勾选 SRGB,会发现纹理变暗了很多,因为颜色作为 [0,1] 之间的数值,取了自己的 2.2 次方幂,自然变小了。其实,如果不使用 SRGB 扩展,在着色器中也可以模拟。我们可以取消勾选 SRGB 选项,然后将 postProcess(后处理)项置为 c^2.2,画面也同样变暗了。postProcess 是这个 Demo 埋在 Shader 中的一个普通 uniform 变量,为最后颜色输出增加一个可选的环节(取自己的 2.2 次方幂,或者取自己的 1/2.2 次方幂)。
着色器中有关 postProcess 的代码如下:
if(uPostProcess == 1){ gl_FragColor = vec4(pow(gl_FragColor.rgb, vec3(1.0/2.2)), 1.0); }else if(uPostProcess == 2){ gl_FragColor = vec4(pow(gl_FragColor.rgb, vec3(2.2)), 1.0); }
下图是 SRGB 模式的效果,postProcess 选择 c^2.2 能达到同样的效果。
当然,我们也可以勾选 SRGB,然后在 postProcess 中选择取 1/2.2 次方幂,这样输出的颜色和最初的几乎一样。SRGB 扩展将纹理颜色值压低,后处理又把输出的颜色值调亮,两者相互抵消。这正是 PBR 渲染的常用做法,先把纹理颜色从人眼颜色空间的纹理压低成物理空间,然后进行一系列物理规律运算,最后输出时再调亮成人眼颜色空间。
下图是 SRGB + postProcess c^1/2.2 的效果,和不使用 SRGB 和 postProcess 的效果基本一致:
Tex_Lod 扩展
之前说过,从 Mipmap 中的哪一级纹理取色,取决于在此纹理上采样的密度。但是有了 Tex_Lod 扩展,可以不受此限制,在着色器中直接编码指定从某一级纹理取色。
开启 Tex_Lod 扩展:
gl.getExtension("EXT_shader_texture_lod");
着色器中也需要加入开启 #extension 宏:
#extension GL_EXT_shader_texture_lod: enable
在着色器中,可调用 texture2DLodExt
函数来从指定级别的 Mipmap 上取色。
gl_FragColor = texture2DLodEXT(uTexture, st, uExtLodLevel);
在右上角的控制器中,确保 Mipmap 相关的选项都打开,然后打开 extLod,拖动 extLodLevel 滑块,可见虽然纹理本身没有在缩放,但是纹理似乎变模糊了,此时实际上就是取到级别较高的 Mipmap 中去了,因为 Mipmap 的级别越高,像素数量越少,在尺寸不变的情况下,就会变模糊了。
下图是 extLod 开启后,在不依赖纹理的缩放导致采样密度变化的情况下,直接手动编码来从不同级别的 Mipmap 上取色的情况:
小结
至此,完成了上述所有配置项的讨论。
(完)
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- OpenGL ES 入门之旅 -- GLSL纹理单元和纹理翻转解决策略
- OpenGL ES入门: 渲染金字塔 - 颜色、纹理、纹理与颜色混合填充以及GLKit实现
- WebGL 纹理颜色原理
- Unity中纹理格式探究
- 游戏制作之路(48)地形纹理工具
- OpenGL ES 高级进阶:纹理数组
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。