顶点着色器:
attribute vec3 position;
attribute vec3 normal;
uniform mat4 worldViewProjection;
uniform mat4 world;
varying vec3 vPositionW;
varying vec3 vNormalW;
void main() {
// 将顶点位置转换到世界空间
vPositionW = (world * vec4(position, 1.0)).xyz;
// 将法线转换到世界空间
vNormalW = normalize((world * vec4(normal, 0.0)).xyz);
// 输出顶点位置
gl_Position = worldViewProjection * vec4(position, 1.0);
}
顶点着色器中添加了两个varying变量,分别是vPositionW 和vNormalW。
varying vec3 vPositionW;
varying vec3 vNormalW;
它们是基于世界坐标的顶点位置和基于世界坐标的顶点法线,在顶点着色器中计算这两个值以备后面片元着色器中插值使用。计算这两个值使用了一下两行代码:
vPositionW = (world * vec4(position, 1.0)).xyz;
vNormalW = normalize((world * vec4(normal, 0.0)).xyz);
这两行代码的作用是将 顶点位置 和 法线 从 模型空间(局部空间) 转换到 世界空间,以便在片段着色器中进行光照或反射计算。下面分别详细解释:
1. vPositionW = (world * vec4(position, 1.0)).xyz;
作用:
将顶点的位置从 模型空间(Model Space) 转换到 世界空间(World Space)。
详细解释:
position
是顶点的 模型空间坐标 (attribute vec3 position
)。world
是 世界变换矩阵 (uniform mat4 world
),它包含了模型的 平移(Translation)、旋转(Rotation)、缩放(Scale) 信息。vec4(position, 1.0)
将position
从vec3
扩展为vec4
,其中w
分量设为1.0
,表示这是一个 点(Point) (如果是0.0
,则表示 方向(Direction))。world * vec4(position, 1.0)
执行 矩阵乘法,将顶点从模型空间变换到世界空间。.xyz
提取变换后的x, y, z
分量(因为world
是mat4
,乘法结果是一个vec4
,但我们只需要前三个分量)。vPositionW
是 世界空间中的顶点位置 ,后续在片段着色器中用于计算 视角方向 或 光照方向。
为什么 w = 1.0
?
w = 1.0
表示这是一个 点(Point) ,会受到 平移(Translation) 影响。- 如果
w = 0.0
,则表示这是一个 方向(Direction)(如法线),不会受平移影响。
2. vNormalW = normalize((world * vec4(normal, 0.0)).xyz);
作用:
将 法线(Normal) 从 模型空间 转换到 世界空间 ,并 归一化(Normalize) 以保持单位长度。
详细解释:
normal
是顶点的 模型空间法线 (attribute vec3 normal
)。vec4(normal, 0.0)
将normal
扩展为vec4
,其中w = 0.0
,表示这是一个 方向(Direction) ,不受 平移(Translation) 影响。world * vec4(normal, 0.0)
执行 矩阵乘法,将法线从模型空间变换到世界空间。.xyz
提取变换后的x, y, z
分量。normalize(...)
确保法线仍然是 单位向量(长度为 1) ,因为 旋转和缩放可能会改变法线的长度。
为什么 w = 0.0
?
- 法线是 方向向量 ,不应该受 平移(Translation) 影响,所以
w = 0.0
可以避免平移变换。 - 如果
w = 1.0
,法线会被错误地平移,导致光照或反射计算错误。
为什么需要 normalize
?
- 非均匀缩放(Non-Uniform Scaling) (如
scaleX=2, scaleY=1, scaleZ=1
)会 扭曲法线方向,导致它不再垂直于表面。 - 即使使用
world
矩阵变换后,法线可能不再是单位长度,所以需要重新归一化。
补充:法线变换的正确方式
如果模型发生了 非均匀缩放(Non-Uniform Scaling) ,直接使用 world
矩阵变换法线可能会导致错误。更精确的方法是使用 法线矩阵(Normal Matrix) ,即 world
矩阵的 逆转置(Inverse Transpose):
mat3 normalMatrix = transpose(inverse(mat3(world)));
vNormalW = normalize(normalMatrix * normal);
但在大多数情况下(如果只有 旋转+均匀缩放 ),直接用 world
矩阵变换法线并归一化也能得到正确结果。
这样,vPositionW
和 vNormalW
就可以在片段着色器中正确计算 光照、反射、折射 等效果了。
片元着色器:
precision highp float;
uniform vec3 diffuseColor;
uniform samplerCube environmentTexture; // 环境贴图
uniform vec3 cameraPosition; // 相机位置
varying vec3 vPositionW;
varying vec3 vNormalW;
void main() {
// 计算视角方向
vec3 viewDirectionW = normalize(cameraPosition - vPositionW);
// 计算反射向量
vec3 reflectVectorW = reflect(-viewDirectionW, normalize(vNormalW));
// 从环境贴图中采样反射颜色
vec3 reflectionColor = textureCube(environmentTexture, reflectVectorW).rgb;
// 将反射颜色与漫反射颜色混合
vec3 finalColor = diffuseColor * reflectionColor;
// 输出最终颜色
gl_FragColor = vec4(finalColor, 1.0);
}
以下三行代码通过计算来获取环境颜色:
// 计算视角方向
vec3 viewDirectionW = normalize(cameraPosition - vPositionW);
// 计算反射向量
vec3 reflectVectorW = reflect(-viewDirectionW, normalize(vNormalW));
// 从环境贴图中采样反射颜色
vec3 reflectionColor = textureCube(environmentTexture, reflectVectorW).rgb;
这三行代码是 环境反射(Environment Reflection) 的核心计算部分,它们的作用是:
- 计算视角方向(从表面指向相机的方向)。
- 计算反射向量(基于视角方向和表面法线)。
- 从环境贴图(Cubemap)中采样反射颜色。
下面我们逐行详细解释:
1. vec3 viewDirectionW = normalize(cameraPosition - vPositionW);
作用:
计算 从表面点指向相机的方向 (即 视角方向)。
详细解释:
cameraPosition
是相机在世界空间中的位置(uniform vec3
)。vPositionW
是当前片段(像素)在世界空间中的位置(从顶点着色器传递并插值计算而来)。cameraPosition - vPositionW
计算的是 从表面点到相机的向量。normalize(...)
将这个向量归一化(变为单位长度),因为方向向量通常需要标准化,避免长度影响后续计算。
为什么是 cameraPosition - vPositionW
而不是 vPositionW - cameraPosition
?
cameraPosition - vPositionW
表示 从表面指向相机(这是光照计算的标准做法)。- 如果是
vPositionW - cameraPosition
,则方向相反(从相机指向表面),在反射计算中会导致错误。
2. vec3 reflectVectorW = reflect(-viewDirectionW, normalize(vNormalW));
作用:
计算 反射向量,即光线从表面反射出去的方向。
详细解释:
-
reflect(I, N)
是 GLSL 内置函数,用于计算反射向量:-
I
是 入射方向(从光源指向表面)。 -
N
是 表面法线(单位向量)。 -
返回的是 反射方向(从表面反射出去的方向)。
-
-
在这里:
-
-viewDirectionW
是 入射方向 (因为viewDirectionW
是从表面指向相机,取反后变成从相机指向表面,模拟光线入射)。 -
normalize(vNormalW)
是 表面法线(确保单位长度)。
-
-
最终
reflectVectorW
就是 反射方向,用于从 Cubemap 中采样环境颜色。
反射公式(物理原理)
反射向量的计算公式为:

其中:
-
R = 反射方向
-
I = 入射方向(指向表面)
-
N = 表面法线
GLSL 的 reflect(I, N)
已经实现了这个计算。
3. vec3 reflectionColor = textureCube(environmentTexture, reflectVectorW).rgb;
作用:
使用 反射向量 从 环境贴图(Cubemap) 中采样颜色,得到反射的环境光颜色。
详细解释:
-
textureCube(environmentTexture, reflectVectorW)
:-
environmentTexture
是一个 立方体贴图(Cubemap) ,存储了 6 个方向的环境光信息。在链接关于Babylon.js的天空盒里面提供了一个天空盒贴图,可以供大家测试使用。 -
reflectVectorW
是反射方向,用于查找 Cubemap 中对应的颜色。
-
-
.rgb
提取颜色的 RGB 分量(忽略 Alpha 通道)。 -
reflectionColor
就是最终反射的环境颜色。
Cubemap 的工作原理
Cubemap 是一个由 6 张 2D 纹理组成的立方体 ,分别代表 +X, -X, +Y, -Y, +Z, -Z 六个方向。reflectVectorW
的方向决定了从哪一张纹理的哪个位置采样颜色。
完整流程
-
顶点着色器:
- 计算世界空间位置
vPositionW
和法线vNormalW
。
- 计算世界空间位置
-
片段着色器:
-
计算
viewDirectionW
(表面 → 相机)。 -
计算
reflectVectorW
(反射方向)。 -
用
reflectVectorW
从 Cubemap 采样reflectionColor
。
-
-
最终颜色:
- 可以简单使用
reflectionColor
作为反射颜色,或者与diffuseColor
混合(如diffuseColor * reflectionColor
)。
- 可以简单使用
常见问题
1. 为什么 reflect
的第一个参数是 -viewDirectionW
?
因为 viewDirectionW
是 表面 → 相机 ,而 reflect(I, N)
的 I
需要是 入射方向(光源 → 表面),所以取反。
2. 为什么 vNormalW
要 normalize
?
因为:
- **
vNormalW来源于顶点着色器vNormalW中的插值结果,
**插值后的法线可能不再是单位长度。 - 非均匀缩放会导致法线方向不正确,归一化可以修正。
3. 如果环境贴图不是 Cubemap 怎么办?
- 如果是 2D 环境贴图(Equirectangular),需要先转换成 Cubemap。
- Babylon.js 提供了
BABYLON.CubeTexture
支持自动加载 Cubemap。
这样,你的 Shader 就能正确计算环境反射了!
颜色混合:
在片元着色器中漫反射和反射颜色的混合方式是:
vec3 finalColor = diffuseColor * reflectionColor;
这种乘法混合方式会让颜色变暗(因为两个颜色分量相乘会降低亮度),这在物理上是正确的(类似于光线被表面吸收一部分),但如果你想要更明亮、更"闪耀"的效果,可以使用其他混合方式。以下是几种常见的改进方法:
1. 加法混合(Additive Blending)
方式:
vec3 finalColor = diffuseColor + reflectionColor;
效果:
- 颜色会 变亮 ,但可能 过曝(特别是高光部分)。
- 适合 发光材质(如霓虹灯、火焰等),但不太适合普通反射。
2. 线性插值(Lerp / Mix)
方式:
float reflectionStrength = 0.5; // 控制反射强度(0.0~1.0)
vec3 finalColor = mix(diffuseColor, reflectionColor, reflectionStrength);
效果:
- 通过
reflectionStrength
控制反射颜色的占比。 - 比纯乘法更灵活,但仍然可能偏暗。
3. 屏幕混合(Screen Blending)
方式:
vec3 finalColor = 1.0 - (1.0 - diffuseColor) * (1.0 - reflectionColor);
效果:
- 类似 Photoshop 的 "屏幕"混合模式,会使颜色变亮,但不会过曝。
- 计算公式:
1 - (1 - A) * (1 - B)
,避免完全丢失暗部细节。 - 适合让反射更明亮但自然。
4. 叠加混合(Overlay Blending)
方式:
vec3 finalColor = mix(
2.0 * diffuseColor * reflectionColor, // 暗部增强
1.0 - 2.0 * (1.0 - diffuseColor) * (1.0 - reflectionColor), // 亮部增强
step(0.5, diffuseColor) // 根据亮度选择混合方式
);
效果:
- 类似 Photoshop 的 "叠加"混合模式,暗部加深,亮部提亮。
- 计算稍复杂,但能保持对比度。
5. 菲涅尔增强(Fresnel Boost)
方式:
// 计算菲涅尔系数(视角与法线夹角越大,反射越强)
float fresnel = pow(1.0 - max(dot(normalize(vNormalW), viewDirectionW), 0.0), 2.0);
vec3 finalColor = diffuseColor + reflectionColor * fresnel;
效果:
- 表面法线越垂直于摄像机方向的表面反射越强,漫反射越弱。
- 表面法线越平行(方向相对或者相同)于摄像机方向的表面反射越弱,漫反射越强。
6. 可控混合(Custom Blending)
方式:
float reflectionIntensity = 0.8; // 反射强度(0.0~1.0)
vec3 finalColor = diffuseColor * (1.0 - reflectionIntensity) + reflectionColor * reflectionIntensity;
效果:
- 类似 lerp,但可以单独控制反射的贡献。
- 适合调整艺术风格。
使用示例:
TypeScript
SceneLoader.Append("Models/","Plane.gltf", scene, (scene)=>{
// 创建 ShaderMaterial
const material = new ShaderMaterial("skybox", scene,
"./src/assets/Shaders/EnvirenmentReflect/EnvirenmentReflect",
{
attributes: ["position", "normal"], // 包括法线属性
uniforms: ["worldViewProjection", "world", "diffuseColor", "environmentTexture", "cameraPosition"]
});
const environmentTexture = new CubeTexture("./src/assets/Textures/Environment/Environment", scene);
if(!environmentTexture){console.log("envirenment failed!");}
// 设置材质参数
material.setTexture("environmentTexture", environmentTexture);
material.setVector3("diffuseColor", new Vector3(0.5, 0.5, 0.5)); // 设置漫反射颜色
if(scene.activeCamera){
material.setVector3("cameraPosition", scene.activeCamera.position); // 设置相机位置
}
scene.meshes.forEach((mesh)=>{
mesh.material = material;
});
}
);