WebGL 纹理详解

栏目: 编程工具 · 发布时间: 5年前

内容简介:Buffer(数据缓冲区)与 Texture(纹理)是 WebGL 程序的两大数据来源。Buffer 可以通过 ArrayBuffer 或更语义化的 TypedArray 来构造;而 Texture 在大多数情况下,是通过 Image 对象来构造的。在构造和使用 Texture 的过程中,需要确定很多为此,我专门编写了一个 Demo,如下所示。Demo 页的右上角有一个使用

WebGL 纹理详解

Buffer(数据缓冲区)与 Texture(纹理)是 WebGL 程序的两大数据来源。Buffer 可以通过 ArrayBuffer 或更语义化的 TypedArray 来构造;而 Texture 在大多数情况下,是通过 Image 对象来构造的。在构造和使用 Texture 的过程中,需要确定很多 选项 来以不同的方式构造 Texture;这些选项之间有着各种各样的关系,或互相依赖,或互相排斥,或互相影响。最近,我又重新梳理了一遍我所用到的 WebGL 纹理各种参数的影响,稍作整理,以防遗忘。

为此,我专门编写了一个 Demo,如下所示。Demo 页的右上角有一个使用 dat.GUI 生成的控件,其中列举了影响纹理的一些选项。这篇文章将逐个讨论这些选项的作用和相互关系。

JS Bin on jsbin.com

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 的情况:

WebGL 纹理详解

下图是 CLAMP 的情况:

WebGL 纹理详解

有一点需要注意的是,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 配置后的情况。

WebGL 纹理详解

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 个点去内插计算出一个数值,如图所示。

WebGL 纹理详解

显然 NEAREST 的运行速度更快,但 LINEAR 的效果更好。使用 NEAREST 时,当纹理被拉得比较近,颗粒感会比较明显,而使用 LINEAR 则会顺滑一些。你可以切换右上角空间上的 MAG_FILTER 选项,然后拖动 scale 滑块将纹理放大,体会一下两者的差别。

下图是 MAG_FILTER 为 NEAREST 的效果:

WebGL 纹理详解

下图是 MAG_FILTER 为 LINEAR 的效果:

WebGL 纹理详解

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 的纹理。实时渲染时,根据采样密度选择其中的某一级纹理,以此避免运行时的大量计算。

WebGL 纹理详解

显而易见的是,使用 MIPMAP 同样需要纹理尺寸为 2 的整数次幂。

WebGL 中,可以通过调用以下方法来生成 mipmap。

gl.generateMipmap(gl.TEXTURE_2D);

我们将控制面板中的 generateMipmap 打开,并把 scale 调到一个较小的值(比如 0.5),然后切换 MIN_FILTER 的各种设置,就可以观察到 mipmap 的效果了。

下图是 MIN_FILTER 为 LINEAR 的效果(NEAREST 效果是类似的):

WebGL 纹理详解

下图是 MIN_FILTER 为 LINEAR_MIPMAP_NEAREST 的效果(LINEAR_MIPMAP_LINEAR 效果是类似的):

WebGL 纹理详解

可以看到当采用 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。

WebGL 纹理详解

自定义 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 时缩放的效果:

WebGL 纹理详解

下图是 MIN_FILTER 为 LINEAR_MIPMAP_LINEAR 时缩放的效果:

WebGL 纹理详解

SRGB 扩展

RGB 颜色空间是为人眼设计的。但是人眼感光强度和真实光线的物理强度并不是线性关系,这是人眼的生理结构所决定的。比如,在人眼看来,屏幕上显示的白色 #FFFFFF 的亮度,是半灰色 #888888 亮度的 2 倍,但是两者真实的光线物理强度(直射下单位面积上受光照的功率)并不是 2 倍关系。所以,当着色器需要基于一些物理规律来计算颜色时,直接取纹理的颜色没有意义,需要作一次转换。

一个经验是,将人眼感知的颜色归一化到 (0,1) 之间,然后取 2.2 次方幂,得到的结果与光线物理强度是呈线性关系的。如下所示,横轴是 RGB 色彩空间,蓝色直线是人眼的感光曲线,红色曲线是光线物理强度曲线。

WebGL 纹理详解

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 能达到同样的效果。

WebGL 纹理详解

当然,我们也可以勾选 SRGB,然后在 postProcess 中选择取 1/2.2 次方幂,这样输出的颜色和最初的几乎一样。SRGB 扩展将纹理颜色值压低,后处理又把输出的颜色值调亮,两者相互抵消。这正是 PBR 渲染的常用做法,先把纹理颜色从人眼颜色空间的纹理压低成物理空间,然后进行一系列物理规律运算,最后输出时再调亮成人眼颜色空间。

下图是 SRGB + postProcess c^1/2.2 的效果,和不使用 SRGB 和 postProcess 的效果基本一致:

WebGL 纹理详解

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 上取色的情况:

WebGL 纹理详解

小结

至此,完成了上述所有配置项的讨论。

(完)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Writing Windows VxDs and Device Drivers, Second Edition

Writing Windows VxDs and Device Drivers, Second Edition

Karen Hazzah / CMP / 1996-01-12 / USD 54.95

Software developer and author Karen Hazzah expands her original treatise on device drivers in the second edition of "Writing Windows VxDs and Device Drivers." The book and companion disk include the a......一起来看看 《Writing Windows VxDs and Device Drivers, Second Edition》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具