基于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;
            });
        }
    );
相关推荐
米芝鱼2 天前
LearnOpenGL(九)自定义转换类
开发语言·c++·算法·游戏·图形渲染·shader·opengl
龚子亦3 天前
Unity学习之Shader(Phong与Blinn-Phong)
学习·unity·游戏引擎·shader
red_redemption3 天前
自由学习记录(49)
学习·shader
心之所向,自强不息4 天前
【Unity Shader编程】之透明物体渲染
unity·游戏引擎·shader
心之所向,自强不息9 天前
Unity Shader编程】之透明物体渲染
unity·游戏引擎·shader
米芝鱼12 天前
Unity URPShader:实现和PS一样的色相/饱和度调整参数效果
游戏·unity·游戏引擎·shader·urp·ps·hlsl
little_fat_sheep15 天前
【Android】RuntimeShader 应用
android·shader·runtimeshader·agsl·rendereffect
太妃糖耶17 天前
Shader中着色器的编译目标级别
unity·shader·着色器
Thomas_YXQ22 天前
Unity3D 图形渲染(Graphics & Rendering)详解
开发语言·unity·图形渲染·unity3d·shader