> 技术文档 > WebGL入门:贴图_webgl texture

WebGL入门:贴图_webgl texture


 一、基础概念

贴图(Texture)本质上就是一张图片,在三维物体中,大多数时候我们很难给每个片元定义一个颜色,这时候就需要从图片中读取像素的颜色值,显示在三维物体上,看起来像是贴在上面的图片一样。

先回顾下原来片元着色器的代码

void main() { gl_FragColor = vec4(1, 0, 0.5, 1);}

或者

precision mediump float;uniform vec4 u_FragColor;void main() { gl_FragColor = u_FragColor;}

颜色值是定死的或者从外部传递过来的,如果这个颜色值从某个图片中获取,那么这个图片就叫做贴图。

1.纹理坐标

 纹理坐标,和传统的xy坐标系比较像,一般也称为uv坐标系(或者st),他们的范围都是0到1

2.纹理单元

在编程中经常看到有这么一行代码

gl.activeTexture(gl.TEXTURE0)

 大概意思就是激活纹理单元,但是后面的gl.TEXTURE0是什么意思呢,为什么还带编号,这个编号最大是多少呢?

2.1概念

在WebGL中,gl.TEXTURE0gl.TEXTURE1gl.TEXTURE2等表示纹理单元(texture units)。纹理单元是GPU中用于管理和处理纹理的硬件资源。每个纹理单元可以绑定一个纹理对象,以便在渲染过程中使用。通过使用多个纹理单元,你可以在同一个渲染过程中组合多个纹理,实现更复杂的视觉效果。

// 创建两个纹理对象var texture0 = gl.createTexture();var texture1 = gl.createTexture();// 激活纹理单元0gl.activeTexture(gl.TEXTURE0);// 绑定纹理对象到纹理单元0gl.bindTexture(gl.TEXTURE_2D, texture0);// 激活纹理单元1gl.activeTexture(gl.TEXTURE1);// 绑定纹理对象到纹理单元1gl.bindTexture(gl.TEXTURE_2D, texture1);

 2.2 数量

纹理单元的数量是由硬件和WebGL实现决定的。不同的设备和浏览器可能会有不同的纹理单元数量。WebGL规范要求至少支持8个纹理单元,但大多数现代设备都支持更多的纹理单元。

2.3 理解

一个纹理单元可以理解一个线程,多纹理单元一起使用可以理解成多线程,同时对多个纹理的处理,肯定比对单个的处理要更厉害

二、 贴图数据加载读取

1、读取方式

图片的读取是经典的前端的读取方式

const image = new Image()image.crossOrigin = \'anonymous\'image.onload = function() { console.log(\'纹理图片加载成功\') if (!gl || !texture) return // 创建临时画布来调整图片尺寸 const canvas = document.createElement(\'canvas\') // 将尺寸调整为2的幂次方 const size = Math.pow(2, Math.ceil(Math.log2(Math.max(image.width, image.height)))) canvas.width = size canvas.height = size const ctx = canvas.getContext(\'2d\') if (!ctx) return // 在画布上绘制图片,这会自动调整尺寸 ctx.drawImage(image, 0, 0, size, size)}image.onerror = function(err) { console.error(\'纹理图片加载失败:\', url, err)}image.src = url

2、图片要求

从上面代码看,贴图数据读取的时候,并没有直接使用图片,而是对图片的大小做了裁剪,把高宽都整成了 2的幂次方,为什么要对大小做个调整呢?WebGL 对纹理图片大小是有要求的,图片的宽度和高度必须是2的N次幂,比如 16 x 16,32 x 32,32 x 64 等。实际上,不是这个尺寸的图片也能进行贴图,但是这样不仅会增加更多的处理,还会影响性能。而且当使用纹理mipmap纹理压缩时也必须把高宽都整成了 2的幂次方,不然的话会报错

三、贴图绑定

 1.图片转换为 WebGL 贴图

//创建贴图const texture = gl.createTexture();// 激活纹理单元0gl.activeTexture(gl.TEXTURE0);// 绑定纹理对象到纹理单元0(类似gl.bindBuffer())gl.bindTexture(gl.TEXTURE_2D, texture);//Y轴反转gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)//把图片的值传给texture (类似gl.bufferData())gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);//将0号纹理单元传给着色器的u_Samplergl.uniform1i(gl.getUniformLocation(program, \'u_Sampler\'), 0)

上述代码中有一点需要注意,因为普通图片和画布一样,坐标轴是以坐上为原点的,但是贴图坐标是以左下为原点的,为了保持一致,通常对贴图的Y轴做下反转 

2. 设置贴图参数

2.1环绕方式

在 WebGL 中,纹理环绕方式(Texture Wrapping)主要有三种

1. gl.REPEAT(重复)
        特点:纹理会在边界处重复出现
        效果:当纹理坐标超出 [0,1] 范围时,会对纹理进行重复平铺
        适用场景:创建重复的图案,如墙壁、地板等
2. gl.MIRRORED_REPEAT(镜像重复)
        特点:纹理在每次重复时会进行镜像翻转
        效果:纹理坐标超出范围时,纹理会以镜像方式重复
        适用场景:需要无缝连接且避免明显重复痕迹的场景
3. gl.CLAMP_TO_EDGE(边缘拉伸)
        特点:超出范围的纹理坐标会被限制在边缘
        效果:使用纹理边缘的颜色值来填充超出范围的区域
        适用场景:单次图像显示,避免重复,如照片等

 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, \'REPEAT\') gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, \'REPEAT\')

2.2 过滤模式

在WebGL中,纹理过滤模式用于确定如何从纹理图像中获取像素值,尤其是在纹理被放大或缩小时。主要的纹理过滤模式有两种:放大过滤(magnification filter)和缩小过滤(minification filter)。每种过滤模式都有两种基本选项:最近邻过滤(nearest filtering)和线性过滤(linear filtering)。

过滤模式 描述 默认值 gl.TEXTURE_MAG_FILTER 放大过滤:当纹理的绘制范围比纹理本身更大时,如何获取纹素颜色。如将16 * 16的纹理图像映射到32 * 32的图形上,需要填充不足的纹理图像的像素 gl.LINEAR gl.TEXTURE_MIN_FILTER 缩小过滤:当纹理的绘制范围比纹理本身更小时,如何获取纹素颜色。如将32 * 32的纹理图像映射到16 * 16的图形上,需要剔除多余的纹理图像的像素 gl.LINEAR 基本选项值 描述 gl.NEAREST 临近过滤:使用原纹理上距离映射后的像素(新像素)中心最近的那个像素的颜色值作为新像素的值 gl.LINEAR 线性过滤:使用距离新像素最近的四个像素的颜色值的加权平均作为新像素的值 MIPMAP选项值 描述 gl.NEAREST_MIPMAP_NEAREST 选择最接近的 mipmap 级别,在该级别上使用最近点采样 gl.LINEAR_MIPMAP_NEAREST 选择最接近的 mipmap 级别,在该级别上使用线性过滤 gl.NEAREST_MIPMAP_LINEAR

在两个最接近的 mipmap 级别之间线性插值,对每个级别使用最近点采样

gl.LINEAR_MIPMAP_LINEAR 在两个最接近的 mipmap 级别之间线性插值,对每个级别使用线性过滤

对比下使用基础临近过滤和 mipmap的线性过滤之间的差别

很明显使用线性过滤显示效果更好 

 2.21 MIPMAP

MIPMAP(贴图金字塔)是一种纹理过滤技术,大体上可以理解为把一张比较清晰的图片复制出多分,并且分辨率逐渐降低,当距离比较远时,界面上不需要显示分辨率特别高的图片,这时候如果根据距离降低远处物体贴图的分辨率就能大大得节省性能。

//......gl.bindTexture(gl.TEXTURE_2D, texture)gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas)//生成贴图金字塔gl.generateMipmap(gl.TEXTURE_2D)

在执行generateMipmap后会把原来的图片创建log2(max(width, height)) + 1个层级 ,其中基本层级(Level 0):原始尺寸 width × height,每个后续层级的尺寸都是前一级的一半,直到 1×1。

曾经我也是因为这个问题不了解而错失了一个大厂的offer

四、贴图进阶

1.立方体贴图

❗ 立方体纹理通常不是给立方体设置纹理的

 ❗ 立方体纹理通常不是给立方体设置纹理的

❗ 立方体纹理通常不是给立方体设置纹理的 

 重要的事情说三遍,我以前也一直认为立方体贴图就是给立方体设置贴图的,但实际上并不是,它通常来说只是[法线立方体贴图]的简称,一般来说给三维物体贴图时,只要把三维坐标和贴图的uv坐标一一对上就能把图给贴上去,但是法线立方体贴图是从另一种方式进行贴图的,它根据三维坐标的矢量值(作为法向量存在)朝向哪个方向就显示立方体贴图的哪个图。

attribute vec4 a_Position;varying vec3 v_Normal;uniform mat4 u_ModelMatrix;uniform mat4 u_ViewMatrix;uniform mat4 u_ProjMatrix;void main() { gl_Position = u_ProjMatrix * u_ViewMatrix * u_ModelMatrix * a_Position; // 使用顶点位置作为法线方向(需要归一化) v_Normal = normalize(a_Position.xyz);}
precision mediump float;varying vec3 v_Normal;uniform samplerCube u_Sampler;void main() { // 使用归一化的法线方向采样立方体贴图 gl_FragColor = textureCube(u_Sampler, normalize(v_Normal));}

1.1 优点 

 根据法向量判断应该显示立方体6个贴图中的哪一个,简化了贴图坐标与顶点坐标一一对应的过程

1.2缺点

使用的全局的坐标系,物体本身变化(例如旋转)时仍然会根据全局的方向显示贴图,右侧的就只显示右贴图,当最开始右侧的面旋转到了左面,它的贴图就变化了,这不符合常理,所以这种贴图方式一般应用于天空盒的场景,用于显示那种基本上不会变化的场景。

2.法线贴图

2.1概念

法线贴图是一种特殊的纹理,它存储的不是颜色信息,而是 表面法线向量 的信息。通过这种技术,我们可以在不增加模型几何复杂度的情况下,模拟出复杂的表面细节和光照效果。

2.2效果预览

贴图
法线贴图
不带法线贴图效果
带法线贴图效果

从上面图片可以看出来,法线贴图能够明显的增加物体纹理的凹凸感,增强物体的逼真的感觉 

2.3法线贴图偏蓝色

 看过好多法线贴图,基本上都是这种蓝色为主色调的图片,实际上法线贴图是法线坐标转换为rgb得来的,因为贴图一般是一个平面,而平面的法线一般都是(0,0,1),值基本上都在z轴上,z值的大小就表示物体的凹凸,转换成颜色值来说就是蓝色的深浅,所以颜色一般是以蓝色为主。

2.4TBN矩阵

说到TBN矩阵,就必须说法线贴图的原理,首先贴图是一个二维的图片,它很难实现三维的效果,纵使某个图片它本身有阴影效果,看起来像是有三维凹凸感的,但是它毕竟在三维世界中展示。当旋转位移时,它的阴影效果就应该变化,但实际上并没有,所以法线贴图就出现了,它直接影响的是光照,也就是说它通过法线纹理中的信息,控制光照方向,从而从光线和阴影角度实现场景的凹凸感。

原始贴图
法线贴图效果

 假设在法线贴图(切线空间)中某个像素点中有一个法线向量 normalMap = (0.5, 0.5, 1.0),这表示一个稍微偏向右上方(相对于贴图)的法线,它其实就表示在此处在贴图坐标系中U方向上偏移了0.5个单位,在V方向上偏移了0.5个单位,所以得出一个结论,法线贴图是需要U方向和V方向的,这时候就出现了TBN矩阵这个概念

TBN 是一个 3x3 正交矩阵,由切线(Tangent,也可以理解成贴图的U方向)、副切线(Bitangent,可以理解成贴图的V方向)和法线(Normal)三个向量组成

下面从代码角度解释下这个逻辑

// 假设我们有以下向量和矩阵:// 法线贴图中采样的法线向量 (在切线空间中)normalMap = vec3(0.5, 0.5, 1.0) // 这表示一个稍微偏向右上方的法线// TBN矩阵的三个基向量 (在世界空间中)T = vec3(1.0, 0.0, 0.0) // 切线,指向纹理的\"右\"方向B = vec3(0.0, 1.0, 0.0) // 副切线,指向纹理的\"上\"方向N = vec3(0.0, 0.0, 1.0) // 法线,指向表面外部// TBN矩阵 = [T B N]TBN = mat3( 1.0, 0.0, 0.0, // 第一列 (T) 0.0, 1.0, 0.0, // 第二列 (B) 0.0, 0.0, 1.0 // 第三列 (N))// 矩阵乘法展开:normal = TBN * normalMap = [T B N] * [0.5]  [0.5]  [1.0] = T * 0.5 + B * 0.5 + N * 1.0 = vec3(1.0, 0.0, 0.0) * 0.5 + vec3(0.0, 1.0, 0.0) * 0.5 + vec3(0.0, 0.0, 1.0) * 1.0  = vec3(0.5, 0.0, 0.0) + vec3(0.0, 0.5, 0.0) + vec3(0.0, 0.0, 1.0)  = vec3(0.5, 0.5, 1.0)

上面代码的几何意义是

  1. T(切线)的贡献:向右倾斜0.5个单位
  2. B(副切线)的贡献:向上倾斜0.5个单位
  3. N(法线)的贡献:保持原始法线方向的1.0个单位

 上面的大概理解后还得再了解一个概念,那就是基底变换矩阵(两者都需要理解,从不同的角度来解释了TBN矩阵存在的意义)

2.4.1基底变换矩阵

基底变换矩阵只是两两垂直的方向轴矩阵,其几何意义是将该向量从一个坐标系(或基底)变换到另一个坐标系(或基底)

比如我们常见的XY轴坐标系\\begin{vmatrix} 0&1 \\\\ 1& 0 \\end{vmatrix}就是一个基底矩阵,假设在这个坐标系下有一个点(3,2)

向量表示为 v = (3, 2),

 这时候我们不想在XY轴坐标系下去展示它了,想要在\\begin{vmatrix} 1&1 \\\\ -1& 1 \\end{vmatrix}坐标轴下去展示它,那么我们此时用公式\\begin{vmatrix} 1&1 \\\\ -1& 1 \\end{vmatrix} * (3,2)=(5,-1)得出的结果刚好就是它的坐标,当然点的位置没有变化,变化的只是坐标系,最终我们得出一个结论基底矩阵乘以向量的几何意义就是坐标系变换

2.4.2 最终结果

 从法线贴图中获取的法线值转换到世界坐标系下就是下面的代码,实际上就表示把贴图准确的贴到了它应该显示的地方,不偏不倚刚刚好,没有褶皱

 // 将切线空间中的方向向量转换到世界空间 vec3 normal = normalize(TBN * normalMap);

3.HDR贴图

 3.1概念

HDR贴图(High Dynamic Range Texture)是一种包含高动态范围(HDR)亮度信息的图像文件,它本身的概念比较难理解,实际上它就是一个色彩亮度比较充实的图片,经常用于环境光贴图,也就是说把它当成一个光源(发光的贴图)去使用,例如天空盒。

3.2使用

因为它内部结构比较复杂,并不是简单的存储rgb,所以也不能用普通加载图片的方式去解析

// 加载贴图const image = new Image()image.crossOrigin = \'anonymous\'image.onload = () => {}image.src = \'/texture/raw_plank_wall_nor_gl_2k.jpg\'

据说原生的js解析很复杂,所以我在示例中选择了一个现成的库(Threejs)中的加载器(RGBELoader) ,解析后把它作为场景的贴图(天空盒)

// 加载HDR贴图const loader = new RGBELoader()loader.load(\'/texture/brown_photostudio_02_2k.hdr\', (texture) => { texture.mapping = THREE.EquirectangularReflectionMapping scene.background = texture})

最终的展示效果如下

贴图本身就相当于一个光源,所以代码中也不需要添加光照,显示的明暗效果也比较逼真 

HDR贴图下载地址推荐:传送门

五、源码

 

传送门 欢迎点亮小星星