立方体纹理的应用

前言

立方体纹理是webGL纹理中的一种,实际上可以看做是六个2D纹理组成的,分别对应立方体的六个面;由于六个纹理图片组成了立方体,因此立方体纹理的取样和2D纹理的取样有点区别,因此作用也更加丰富;

立方体纹理的取样

由于立方体有六个纹理,因此不能像2D纹理那样只要指定一个二维纹理坐标就可以进行取样;

1
vec4 textureCube(samplerCube sampler, vec3 coord)

上面就是GLSL中对立方体纹理进行取样的函数,其中第二个参数就是用于进行取样的坐标;可以看到这个坐标是三维的,有的地方[1]说这个坐标是纹理的法向量;不过我觉得这里坐标更像是一个方向向量,即从立方体中心位置朝这个方向发射射线,射线与立方体相交的位置就是取样点;

立方体纹理的应用

以我目前的了解,立方体纹理大致有以下几种应用:

  • 正方体表面贴图
  • 用于环境贴图(反射贴图),模拟镜面反射结果
  • 天空盒

立方体贴图

这个应用就很好理解了,借助立方体纹理可以分别给立方体每个面贴上对应面的纹理图片,这样就省去一个一个声明6个不同的2D纹理然后去分别贴图了;而且这样还可以直接利用立方体的顶点坐标来获取每个片元上面的取样方向向量了,因为立方体物体的中心就是几何中心;

1
2
3
4
5
6
7
8
// 顶点着色器
attribute vec4 a_Pos;
varying vec4 v_Pos;

void main() {
gl_Position = a_Pos;
v_Pos = a_Pos;
}
1
2
3
4
5
6
7
// 片元着色器
varying vec4 v_Pos; // 立方体各点的插值坐标
uniform samplerCube u_Texture; // 立方体纹理

void main() {
gl_FragColor = textureCube(u_Texture, normalize(v_Pos);
}

事实上不用立方体纹理也可以很方便地进行立方体贴图,做法就是将六张纹理图片拼接成一张纹理图片,然后每个面对应的纹理区域可以利用公式计算得到;这种做法就跟前端使用的精灵图原理是一样的,不过专业术语叫做纹理图集[2]

立方体环境贴图

环境贴图,也叫反射贴图

反射映射(Reflection mapping)在计算机图形学领域是用预先计算的纹理图像模拟复杂镜面的一种高效方法。纹理用来储存被渲染物体周围环境的图像。


这种实现方法比传统的光线跟踪(光线跟踪通过射出一束光线并且跟踪光线的传输路径来计算反射)算法效率更高,但是需要注意的是这种方法是实际反射的一种近似,有时甚至是非常粗糙的近似。[3]

从上面这段描述可以看出,环境贴图就是对物体环境的一种近似模拟,目的就是想要高效地计算得到对于环境的镜面反射

立方体环境贴图就是其中的一种,其原理就是将环境映射到一个立方体纹理上,然后通过物体表面法向量以及视线方向向量得到反射方向向量,然后取样得到对应的纹理像素;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 顶点着色器
precision mediump float; // 中等精度
attribute vec3 a_Pos;
attribute vec3 a_Normal;
varying vec3 v_Normal;
varying vec3 v_Pos;
uniform mat4 u_Projection;
uniform mat4 u_View;
uniform mat4 u_Model;
void main() {
gl_Position = u_Projection * u_View * u_Model * vec4(a_Pos, 1.0);
v_Normal = mat3(u_Model) * a_Normal;
v_Pos = mat3(u_Model) * a_Pos;
}
1
2
3
4
5
6
7
8
9
10
11
12
// 片元着色器
precision mediump float; // 中等精度
varying vec3 v_Normal;
varying vec3 v_Pos;
uniform samplerCube u_Texture;
uniform vec3 u_EyePos;
void main() {
vec3 normal = normalize(v_Normal); // 法向量
vec3 eyeToPos = normalize(v_Pos - u_EyePos); // 视线方向
vec3 reflectDirection = reflect(eyeToPos, normal); // 计算反射方向
gl_FragColor = textureCube(u_Texture, reflectDirection);
}

img

天空盒

天空盒是应用于场景的背景以显示天空、空间或封闭结构的纹理。天空盒可以是环绕到球体上的单个纹理,或环绕到立方体上的六个纹理。[4]

天空盒通常充当着背景的角色,因此其一定是位于渲染区域的最底层;如果仅仅只是将立方体天空盒当做一个巨大的容器,其他所有的物体都位于这个容器内来渲染天空盒的话,就需要保证渲染物体不会超过容器的范围,否则就会被天空盒遮住[5]

img

所以另一个常用的做法就是通过屏幕坐标(NDC)然后反推出该点的世界坐标,于是根据该点的世界坐标与相机坐标就得到了该点对于天空盒的方向向量;

那么如何根据屏幕坐标然后反推得到该点的世界坐标呢?已知屏幕坐标计算过程为:

PNDC=MprojectionMviewMmodelPP_{NDC} = M_{projection} * M_{view} * M_{model} * P

根据矩阵变换的特点可知,逆矩阵即为矩阵变换的逆变换,因此屏幕坐标对应的世界坐标为:

Pworld=(MprojectionMview)1PNDCP_{world} = (M_{projection} * M_{view})^{-1} * P_{NDC}

1
2
3
4
5
6
7
8
// 顶点着色器
precision highp float;
attribute vec2 a_Pos;
varying vec4 v_Pos;
void main() {
gl_Position = vec4(a_Pos, 1.0, 1.0); // 绘制一个能够填满屏幕的矩形
v_Pos = gl_Position;
}
1
2
3
4
5
6
7
8
9
10
// 片元着色器
precision highp float;
varying vec4 v_Pos;
uniform samplerCube u_Texture;
uniform mat4 u_VPInverse; // 视图投影矩阵的逆矩阵
void main() {
// 获取屏幕坐标的世界坐标,这里相机坐标为原点,因此视线方向可以简化
vec4 direction = u_VPInverse * v_Pos;
gl_FragColor = textureCube(u_Texture, normalize(direction.xyz / direction.w));
}

img

这种渲染看起来跟VR的感觉很相似,不知道VR渲染原理是不是就是天空盒呢?

相关文档


  1. WebGL 立方体贴图 ↩︎

  2. WebGL 三维纹理 ↩︎

  3. https://zh.wikipedia.org/wiki/反射贴图 ↩︎

  4. 天空盒 - Amazon Sumerian天空盒 - Amazon Sumerian ↩︎

  5. WebGL 天空盒 ↩︎