基于Babylon.js的Shader入门之七:综合案例(凹凸反射表面)

        通过前面的学习,我们了解了Babylon.js中Shader的很多用法,这里将前面的内容综合一下,编写一个稍微复杂一点儿的案例。

        该Shader将能够使用漫反射颜色Diffuse Color和漫反射纹理Diffuse Texture,同时支持法线纹理并可以反射环境纹理,能够接受环境光照和一个平行光照射,能够产生高光效果。效果参考如下:

顶点着色器:

attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
attribute vec3 tangent;

uniform mat4 world;
uniform mat4 worldViewProjection;
uniform vec3 cameraPosition;

varying vec3 vPositionW;
varying vec2 vUV;
varying mat3 vTBN;

void main() {
    // 基础变换
    vec4 worldPos = world * vec4(position, 1.0);
    vPositionW = worldPos.xyz;
    gl_Position = worldViewProjection * vec4(position, 1.0);
    
    // 构建 TBN 矩阵(切线空间 -> 世界空间)
    vec3 T = normalize((world * vec4(tangent, 0.0)).xyz);
    vec3 N = normalize((world * vec4(normal, 0.0)).xyz);
    vec3 B = cross(N, T);
    vTBN = mat3(T, B, N);
    
    vUV = uv;
}

         这部分内容不做解释,如果看不太明白可以参考基于Babylon.js的Shader入门之五:让Shader支持法线贴图这篇博客的顶点着色器的讲解内容,总体内容差不多。

片元着色器:

precision highp float;

// 贴图
uniform sampler2D diffuseTexture;    // 漫反射贴图
uniform sampler2D normalTexture;     // 法线贴图
uniform samplerCube environmentTexture; // 环境贴图

// 颜色参数
uniform vec3 diffuseColor;           // 漫反射颜色
uniform float ambientIntensity;      // 环境光强度
uniform vec3 ambientColor;           // 环境光颜色

// 平行光参数
uniform vec3 lightDirection;         // 平行光方向(世界空间)
uniform vec3 lightColor;             // 平行光颜色
uniform float lightIntensity;        // 平行光强度

// 高光参数
uniform float specularPower;         // 高光范围(指数)
uniform float specularIntensity;     // 高光强度

// 菲涅尔参数
uniform float fresnelPower;          // 菲涅尔边缘强度
uniform float fresnelBias;           // 菲涅尔基础值

// 其他
uniform vec3 cameraPosition;         // 相机位置
uniform vec2 scaleDiffuseTex;
uniform vec2 scaleNormalTex;

varying vec3 vPositionW;
varying vec2 vUV;
varying mat3 vTBN;

void main() {
    // 采样漫反射贴图并与颜色混合
    vec2 uvDiffuse = vUV * scaleDiffuseTex;
    vec4 diffuseTextureColor = texture2D(diffuseTexture, uvDiffuse);
    vec3 baseColor = diffuseColor * diffuseTextureColor.rgb;
    
    // 采样法线贴图并转换到世界空间
    vec2 uvNormal = vUV * scaleNormalTex;
    // vec2 uvNormal = vec2(3.0,3.0);
    vec3 normalColor = texture2D(normalTexture, uvNormal).xyz * 2.0 - 1.0;
    vec3 normalW = normalize(vTBN * normalColor);
    
    // ================= 光照计算 =================
    // 环境光
    vec3 ambient = ambientColor * ambientIntensity * baseColor;
    
    // 平行光漫反射
    vec3 lightDir = normalize(-lightDirection);
    float NdotL = max(dot(normalW, lightDir), 0.0);
    vec3 diffuse = lightColor * lightIntensity * NdotL * baseColor;
    
    // 平行光高光(Blinn-Phong模型)
    vec3 viewDir = normalize(cameraPosition - vPositionW);
    vec3 halfDir = normalize(lightDir + viewDir);
    float specTerm = pow(max(dot(normalW, halfDir), 0.0), specularPower);
    vec3 specular = lightColor * specularIntensity * specTerm;
    
    // ================= 环境反射 =================
    vec3 reflectDir = reflect(-viewDir, normalW);
    vec3 reflectionColor = textureCube(environmentTexture, reflectDir).rgb;
    
    // 菲涅尔混合
    float fresnel = fresnelBias + (1.0 - fresnelBias) * pow(1.0 - max(dot(viewDir, normalW), 0.0), fresnelPower);
    vec3 finalColor = mix(baseColor, reflectionColor, fresnel);
    
    // ================= 最终合成 =================
    finalColor = (ambient + diffuse + specular) * finalColor;
    
    gl_FragColor = vec4(finalColor, 1.0);
}

        大部分内容在前面的博客中都讲解过,只有高光处理的部分前面没有涉及,这里单独说一下。高光计算的部分代码如下:

    // 平行光高光(Blinn-Phong模型)
    vec3 viewDir = normalize(cameraPosition - vPositionW);
    vec3 halfDir = normalize(lightDir + viewDir);
    float specTerm = pow(max(dot(normalW, halfDir), 0.0), specularPower);
    vec3 specular = lightColor * specularIntensity * specTerm;

        这四行代码是 Blinn-Phong 光照模型 中计算高光(镜面反射)的核心部分,用于模拟光线在物体表面的镜面反射效果。以下是逐行详细解释:


1. vec3 viewDir = normalize(cameraPosition - vPositionW);

作用

计算从表面点到相机的 视角方向(View Direction)。

  • cameraPosition:相机在世界空间中的位置(Uniform 变量)。
  • vPositionW:当前片段(像素)在世界空间中的位置(从顶点着色器传递)。
  • normalize(...):将方向向量归一化为单位长度(避免长度影响后续计算)。
物理意义

        表示人眼(或相机)观察物体表面的方向,用于后续计算反射效果。


2. vec3 halfDir = normalize(lightDir + viewDir);

作用

计算 半角向量(Halfway Vector),即光线方向与视角方向的中间向量。

  • lightDir:平行光的方向(已归一化,通常为 normalize(-lightDirection))。
  • viewDir:上一步计算的视角方向。
  • normalize(...):确保半角向量是单位向量。
为什么需要半角向量?

        Blinn-Phong 模型通过 半角向量与法线的夹角 计算高光,比传统的 Phong 模型(直接计算反射向量)更高效。


3. float specTerm = pow(max(dot(normalW, halfDir), 0.0), specularPower);

作用

计算高光强度项(Specular Term),决定高光的亮度和范围。

  • normalW:世界空间中的法线方向(来自法线贴图或顶点法线)。
  • dot(normalW, halfDir):法线与半角向量的点积,表示它们的夹角余弦值(范围 [-1, 1])。
  • max(..., 0.0):将点积结果限制在 [0, 1],避免负值影响。
  • pow(..., specularPower)specularPower(高光指数)控制高光的集中程度。值越大,高光越锐利(如金属);值越小,高光越扩散(如塑料)。
数学意义       

其中:

  • N 是法线,H 是半角向量。


4. vec3 specular = lightColor * specularIntensity * specTerm;

作用

合成最终的高光颜色。

  • lightColor:平行光的颜色(如 vec3(1.0, 1.0, 1.0) 表示白光)。
  • specularIntensity:高光强度系数(Uniform 变量),用于艺术调节。
  • specTerm:上一步计算的高光强度项。
物理意义

高光颜色 = 光源颜色 × 高光强度 × 高光衰减项。

  • 当法线与半角向量对齐时(dot(normalW, halfDir) ≈ 1),高光最亮。
  • 当夹角增大时,高光强度按指数衰减。

完整流程图示

graph LR
    A[相机位置] --> B[viewDir = normalize(cameraPosition - vPositionW)]
    C[光源方向] --> D[halfDir = normalize(lightDir + viewDir)]
    B --> D
    D --> E[specTerm = pow(max(dot(normalW, halfDir), 0.0), specularPower)]
    E --> F[specular = lightColor * specularIntensity * specTerm]

关键参数的影响

参数作用典型值
specularPower控制高光范围(值越大,高光越集中)32.0(金属) / 8.0(塑料)
specularIntensity控制高光亮度0.5 ~ 1.0
lightColor高光的基础颜色与光源一致

与其他光照模型的对比

  • Phong 模型:直接计算反射向量 R 与视角向量的夹角,计算量较大。

  • Blinn-Phong 模型:改用半角向量,效率更高,适合实时渲染。

  • PBR 模型:基于物理的镜面反射(如 BRDF),更复杂但更真实。

使用示例:

export function SetMaterial(scene:Scene):void{

    SceneLoader.Append("Models/","Plane.gltf", scene, (scene)=>{
            // 创建 ShaderMaterial
            const material = new ShaderMaterial("Water", scene, 
            "./src/assets/Shaders/Water/Water",
            {
                attributes: ["position", "normal", "uv", "tangent", "bitangent"],
                uniforms: [
                    "world", "worldViewProjection", "cameraPosition",
                    "diffuseColor", "ambientIntensity", "ambientColor",
                    "lightDirection", "lightColor", "lightIntensity",
                    "specularPower", "specularIntensity",
                    "fresnelPower", "fresnelBias",
                    "scaleDiffuseTex","scaleNormalTex"
                ],
                samplers: ["diffuseTexture", "normalTexture", "environmentTexture"]
            });

            const diffuseTexture = new Texture("./src/assets/Textures/DaLiShi.jpg", scene);
            const normalTexture = new Texture("./src/assets/Textures/Metal06.jpg", scene);
            const environmentTexture = new CubeTexture("./src/assets/Textures/Environment/Environment", scene);

            // 设置贴图
            material.setTexture("diffuseTexture", diffuseTexture );
            material.setVector2("scaleDiffuseTex", new Vector2(5, 5));

            material.setTexture("normalTexture", normalTexture);
            material.setVector2("scaleNormalTex", new Vector2(5, 5));

            material.setTexture("environmentTexture", environmentTexture);

            // 设置颜色参数
            material.setVector3("diffuseColor", new Vector3(1, 1, 1));
            material.setFloat("ambientIntensity", 0.3);
            material.setVector3("ambientColor", new Vector3(1, 1, 1));

            // 设置平行光参数
            material.setVector3("lightDirection", new Vector3(-1, -1, -1));
            material.setVector3("lightColor", new Vector3(1, 1, 1));
            material.setFloat("lightIntensity", 1.0);

            // 设置高光参数
            material.setFloat("specularPower", 128.0);
            material.setFloat("specularIntensity", 1.0);

            // 设置菲涅尔参数
            material.setFloat("fresnelPower", 5.0);
            material.setFloat("fresnelBias", 0.6);

            // 绑定相机位置(每帧更新)
            scene.onBeforeRenderObservable.add(() => { 
                if(scene.activeCamera){
                    material.setVector3("cameraPosition", scene.activeCamera.position); // 设置相机位置
                }
            });

            scene.meshes.forEach((mesh)=>{
                // 获取网格的顶点数据
                if (mesh.isVerticesDataPresent(VertexBuffer.TangentKind)) {
                    // 应用材质到网格
                    console.log("mesh contain tangent = " + mesh.name);
                } 
                else{
                    const tangentsArray = ComputeTangents(mesh);
                    if(tangentsArray && tangentsArray.length > 2){
                        console.log("mesh don't contain tangent = " + mesh.name);
                        mesh.setVerticesData(VertexBuffer.TangentKind, tangentsArray, false);
                    }
                }
                mesh.material = material;
            });
        }
    );
}


function ComputeTangents(mesh:AbstractMesh){
    const positions = mesh.getVerticesData( VertexBuffer.PositionKind);
    const uvs = mesh.getVerticesData( VertexBuffer.UVKind);
    const indices = mesh.getIndices();

    if(positions && uvs && indices){
        let tangents = new Array(positions.length).fill(0);
        for (let i = 0; i < indices.length; i += 3) {
            let i0 = indices[i];
            let i1 = indices[i + 1];
            let i2 = indices[i + 2];

            let p0 = new Vector3(positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]);
            let p1 = new Vector3(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]);
            let p2 = new Vector3(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]);

            let uv0 = new Vector2(uvs[i0 * 2], uvs[i0 * 2 + 1]);
            let uv1 = new Vector2(uvs[i1 * 2], uvs[i1 * 2 + 1]);
            let uv2 = new Vector2(uvs[i2 * 2], uvs[i2 * 2 + 1]);

            let deltaPos1 = p1.subtract(p0);
            let deltaPos2 = p2.subtract(p0);

            let deltaUV1 = uv1.subtract(uv0);
            let deltaUV2 = uv2.subtract(uv0);

            let r = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
            let tangent = deltaPos1.scale(deltaUV2.y).subtract(deltaPos2.scale(deltaUV1.y)).scale(r);

            tangents[i0 * 4] = tangent.x;
            tangents[i0 * 4 + 1] = tangent.y;
            tangents[i0 * 4 + 2] = tangent.z;
            tangents[i0 * 4 + 3] = 1;

            tangents[i1 * 4] = tangent.x;
            tangents[i1 * 4 + 1] = tangent.y;
            tangents[i1 * 4 + 2] = tangent.z;
            tangents[i1 * 4 + 3] = 1;

            tangents[i2 * 4] = tangent.x;
            tangents[i2 * 4 + 1] = tangent.y;
            tangents[i2 * 4 + 2] = tangent.z;
            tangents[i2 * 4 + 3] = 1;
        }
        return tangents;
    }
    return null;
}

补充说明:        

        由于这里使用了法线贴图,如果使用Shader的物体表面完全是黑色的,很可能是顶点切线的问题,具体解决方法参考基于Babylon.js的Shader入门之五:让Shader支持法线贴图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值