WebGL入门:贴图
一、基础概念
贴图(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.TEXTURE0
、gl.TEXTURE1
、gl.TEXTURE2
等表示纹理单元(texture units)。纹理单元是GPU中用于管理和处理纹理的硬件资源。每个纹理单元可以绑定一个纹理对象,以便在渲染过程中使用。通过使用多个纹理单元,你可以在同一个渲染过程中组合多个纹理,实现更复杂的视觉效果。
// 创建两个纹理对象
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();// 激活纹理单元0
gl.activeTexture(gl.TEXTURE0);
// 绑定纹理对象到纹理单元0
gl.bindTexture(gl.TEXTURE_2D, texture0);// 激活纹理单元1
gl.activeTexture(gl.TEXTURE1);
// 绑定纹理对象到纹理单元1
gl.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 = sizecanvas.height = sizeconst 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();
// 激活纹理单元0
gl.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_Sampler
gl.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缺点
使用的全局的坐标系,物体本身变化(例如旋转)时仍然会根据全局的方向显示贴图,右侧的就只显示右贴图,当最开始右侧的面旋转到了左面,它的贴图就变化了,这不符合常理,所以这种贴图方式一般应用于天空盒的场景,用于显示那种基本上不会变化的场景。
五、源码
传送门 欢迎点亮小星星