基于Babylon.js的Shader入门之六:让Shader反射环境贴图

顶点着色器:

复制代码
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变量,分别是vPositionWvNormalW

复制代码
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)positionvec3 扩展为 vec4,其中 w 分量设为 1.0,表示这是一个 点(Point) (如果是 0.0,则表示 方向(Direction))。
  • world * vec4(position, 1.0) 执行 矩阵乘法,将顶点从模型空间变换到世界空间。
  • .xyz 提取变换后的 x, y, z 分量(因为 worldmat4,乘法结果是一个 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 矩阵变换法线并归一化也能得到正确结果。


这样,vPositionWvNormalW 就可以在片段着色器中正确计算 光照、反射、折射 等效果了。

片元着色器:

复制代码
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) 的核心计算部分,它们的作用是:

  1. 计算视角方向(从表面指向相机的方向)。
  2. 计算反射向量(基于视角方向和表面法线)。
  3. 从环境贴图(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 的方向决定了从哪一张纹理的哪个位置采样颜色。

完整流程

  1. 顶点着色器

    • 计算世界空间位置 vPositionW 和法线 vNormalW
  2. 片段着色器

    1. 计算 viewDirectionW(表面 → 相机)。

    2. 计算 reflectVectorW(反射方向)。

    3. reflectVectorW 从 Cubemap 采样 reflectionColor

  3. 最终颜色

    • 可以简单使用 reflectionColor 作为反射颜色,或者与 diffuseColor 混合(如 diffuseColor * reflectionColor)。

常见问题

1. 为什么 reflect 的第一个参数是 -viewDirectionW

因为 viewDirectionW表面 → 相机 ,而 reflect(I, N)I 需要是 入射方向(光源 → 表面),所以取反。

2. 为什么 vNormalWnormalize

因为:

  • **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;
            });
        }
    );
相关推荐
mxwin6 天前
Unity Shader 逐像素光照 vs 逐顶点光照性能与画质的权衡策略
unity·游戏引擎·shader·着色器
mxwin7 天前
Unity URP 全局光照 (GI) 完全指南 Lightmap 采样与实时 GI(光照探针、反射探针)的 Shader 集成
unity·游戏引擎·shader·着色器
mxwin7 天前
Unity URP 溶解效果基于噪声纹理与 clip 函数实现物体渐隐渐显
unity·游戏引擎·shader
mxwin7 天前
Unity Shader 顶点色:利用模型顶点颜色传递渲染数据
unity·游戏引擎·shader
mxwin7 天前
Unity URP 下的 GPU Instancing减少 DrawCall 的关键技术
unity·游戏引擎·shader
mxwin7 天前
Unity URP SRP Batcher 完全指南 URP/HDRP 下的核心批处理机制,大幅降低 CPU 开销
unity·游戏引擎·shader·单一职责原则
mxwin8 天前
Unity Shader UV 坐标与纹理平铺Tiling & Offset 深度解析
unity·游戏引擎·shader·uv
mxwin9 天前
Unity Shader Blinn-Phong vs PBR传统经验模型与现代物理基础渲染
unity·游戏引擎·shader
mxwin10 天前
Unity URP 阴影映射 深度纹理、阴影采样与分辨率控制的深度解析
unity·游戏引擎·shader·着色器
mxwin11 天前
Unity Shader 顶点动画:在顶点着色器中实现风吹草动、河流波动、布料模拟
unity·游戏引擎·shader·着色器