当前位置: 首页 > news >正文

基于Babylon.js的Shader入门之五:让Shader支持法线贴图

        如果一个比较平坦的物体表面要添加更多的凹凸细节,但是我们又不想通过建模实现,这时候法线贴图就派上用场了。法线贴图是通过与灯光的交互来让一个平坦表面造成凹凸效果假象的,在基于Babylon.js的Shader入门之四:让Shader支持基础光照这一节的内容中,我们已经让Shader能够支持简单的灯光,这里我们让Shader来支持法线贴图。

        这里我们使用了这样一张法线贴图:

        最终呈现的效果参考如下:

顶点着色器

attribute vec3 position;
attribute vec3 normal; // 顶点法线
attribute vec2 uv; // 纹理坐标
attribute vec3 tangent; // 切线

uniform mat4 worldViewProjection;
uniform mat4 world; // 世界矩阵
uniform vec3 lightPosition; // 光源位置

varying vec3 vLightDir; // 传递光照方向到片元着色器
varying vec2 vUV; // 传递纹理坐标到片元着色器
varying mat3 vTBN; // 传递TBN矩阵到片元着色器

void main() {
    gl_Position = worldViewProjection * vec4(position, 1.0);
    
    // 计算世界矩阵的3x3部分
    mat3 worldMat3 = mat3(world);

    // 计算切线空间矩阵 (TBN)
    vec3 T = normalize(worldMat3 * tangent);
    vec3 N = normalize(worldMat3 * normal);
    vec3 B = cross(T, N);
    vTBN = mat3(T, B, N);

    // 计算光源方向(从顶点指向光源)
    vec3 worldPosition = (world * vec4(position, 1.0)).xyz;
    vLightDir = normalize(lightPosition - worldPosition);

    // 传递纹理坐标
    vUV = uv;
}

        这里我们对新增的内容做一些解释:

1.使用顶点切线数据

        在attribute类型的数据中,我们除了添加了uv数据之外,还添加了tangent顶点切线数据。

attribute vec2 uv; // 纹理坐标
attribute vec3 tangent; // 切线

         这里需要注意的是,在Babylon.js中,一个mesh的顶点数据中不一定包含切线数据,如果一个mesh缺少了顶点切线数据,可能导致整个材质变黑等问题,这个我们在后面使用案例里面会说明解决办法。

2. 计算世界矩阵的 3x3 部分

mat3 worldMat3 = mat3(world);
  • world 是一个 4x4 的世界矩阵,用于将顶点从模型空间转换到世界空间。
  • mat3(world) 提取了 world 矩阵的左上角 3x3 部分。这个 3x3 矩阵只保留了旋转和缩放信息,去除了平移部分。

为什么需要 3x3 矩阵?

  • 对于法线(normal)、切线(tangent)等方向向量,平移是没有意义的,因为它们只表示方向,而不是位置。
  • 使用 3x3 矩阵可以避免平移对方向向量的影响。

3. 计算切线空间矩阵 (TBN)

vec3 T = normalize(worldMat3 * tangent);
vec3 N = normalize(worldMat3 * normal);
vec3 B = cross(T, N);
vTBN = mat3(T, B, N);

 3.1 计算切线 (T) 和法线 (N)

T(切线)
  • tangent 是顶点的切线向量,表示纹理坐标的 U 方向。
  • worldMat3 * tangent 将切线从模型空间转换到世界空间。
  • normalize() 将向量归一化,确保其长度为 1。
N(法线)
  • normal 是顶点的法线向量,表示表面的垂直方向。
  • worldMat3 * normal 将法线从模型空间转换到世界空间。
  • normalize() 将向量归一化。

3.2 计算副切线 (B)

B(副切线,也称为双切线或副法线)
  • B = cross(T, N) 使用叉积计算副切线。
  • 叉积的结果是一个垂直于 T 和 N 的向量,表示纹理坐标的 V 方向。
  • 副切线、切线和法线共同构成了切线空间的基础。

3.3 构建 TBN 矩阵

vTBN = mat3(T, B, N)
  • TBN 矩阵是一个 3x3 矩阵,由切线 (T)、副切线 (B) 和法线 (N) 组成。
  • 这个矩阵用于将向量从切线空间转换到世界空间,或者从世界空间转换到切线空间。

3. TBN 矩阵的作用

        TBN 矩阵的主要作用是将向量从世界空间转换到切线空间,或者从切线空间转换到世界空间。在法线贴图(Normal Mapping)中,TBN 矩阵尤其重要:

  • 法线贴图中的法线向量 是定义在切线空间中的。切线空间参考如下:

  • 为了在片元着色器中使用这些法线向量,需要将光照方向、视线方向等从世界空间转换到切线空间。
  • TBN 矩阵就是用来完成这种空间转换的。

4. 代码的整体作用

  • worldMat3:提取世界矩阵的 3x3 部分,用于方向向量的变换。
  • TNB:计算切线、法线和副切线,并构建 TBN 矩阵。
  • vTBN:将 TBN 矩阵传递给片元着色器,用于后续的光照计算。

片元着色器

precision mediump float;

uniform vec3 diffuseColor; // 基础颜色
uniform sampler2D normalMap; // 法线贴图

varying vec3 vLightDir; // 接收从顶点着色器传来的光照方向
varying vec2 vUV; // 接收从顶点着色器传来的纹理坐标
varying mat3 vTBN; // 接收从顶点着色器传来的TBN矩阵

void main() {
    // 从法线贴图中获取法线
    vec3 normalMapValue = texture2D(normalMap, vUV).xyz * 2.0 - 1.0; // 将法线从[0,1]映射到[-1,1]
    vec3 normal = normalize(vTBN * normalMapValue); // 将法线从切线空间转换到世界空间

    // 计算光照强度(点积),确保不小于0
    float lightIntensity = max(dot(normal, normalize(vLightDir)), 0.0);

    // 将光照强度应用于基础颜色
    gl_FragColor = vec4(diffuseColor * lightIntensity, 1.0);
}

获取法向量

        代码行vec3 normalMapValue = texture2D(normalMap, vUV).xyz * 2.0 - 1.0;用于通过贴图来获取当前片元对应的法向量。

texture2D(normalMap, vUV)

  • texture2D:这是一个 GLSL 内置函数,用于从 2D 纹理(这里是法线贴图)中采样颜色值。
  • normalMap:是传入的法线贴图纹理。
  • vUV 是从顶点着色器传递过来的纹理坐标,范围通常是 [0, 1]

.xyz

        提取纹理采样的 RGB 分量。

  • 法线贴图的每个像素通常存储了一个法线向量,其分量(R, G, B)分别对应法线的 X, Y, Z 分量。
  • 例如,(R, G, B) = (0.5, 0.5, 1.0) 表示法线向量 (0, 0, 1)(即垂直于表面)。

* 2.0 - 1.0

        * 2.0 - 1.0:将法线向量从 [0, 1] 的范围映射到 [-1, 1]

        例如:

  • 如果 R = 0.5,则 R * 2.0 - 1.0 = 0.0
  • 如果 G = 0.0,则 G * 2.0 - 1.0 = -1.0
  • 如果 B = 1.0,则 B * 2.0 - 1.0 = 1.0

将法线向量从切线空间转换到世界空间

        代码行 vec3 normal = normalize(vTBN * normalMapValue);用于将法线向量从切线空间转换到世界空间,并进行归一化。其中vTBN 是从顶点着色器传递过来的 TBN 矩阵(Tangent-Bitangent-Normal 矩阵)。

使用示例

// 创建 ShaderMaterial
const material = new ShaderMaterial("BaseLight", scene, 
    "./src/assets/Shaders/NormalTexture/NormalTexture",
    {
        attributes: ["position", "normal", "uv", "tangent"], // 包括法线属性
        uniforms: ["world", "worldViewProjection", "lightPosition", "diffuseColor", "normalMap"]
    });
// 设置光源位置
const lightPosition = new Vector3(10, 10, 10); // 示例光源位置
material.setVector3("lightPosition", lightPosition);
material.setColor3("diffuseColor", new Color3(0.85, 0.9, 1)); // 设置基础颜色
const normalTex = new Texture("./src/assets/Textures/Metal06.jpg", scene);
material.setTexture("normalMap", normalTex);

if (!box.isVerticesDataPresent(VertexBuffer.TangentKind)) {
    const tangentsArray = ComputeTangents(box);
    if(tangentsArray && tangentsArray.length > 2){
        box.setVerticesData(VertexBuffer.TangentKind, tangentsArray, false);
    }
}
box.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;
}

        这里需要强调的是,在Babylon.js中,一个mesh不一定包含顶点切线数据,比如你使用MeshBuilder.CreateBox来创建一个Box,在Babylon.js当前的版本下,该Box是不带有顶点切线数据的。如果你通过Babylon.js的导出插件从三维建模软件中导出模型,你只要将导出切线选项勾选上就可以带有顶点切线数据导出了。如果遇到创建或者加载得到的模型本身没有顶点切线数据的情况,我们需要通过当前Mesh现有的顶点数据计算生成,具体方法可以参考上面代码中的ComputeTangents方法。

相关文章:

  • Hyperlane:Rust 生态中的轻量级高性能 HTTP 服务器库,助力现代 Web 开发
  • SQL Server 触发器
  • Python中的列表:全面解析与应用指南
  • uniapp配置代理解决跨域问题
  • PyTorch入门指南:环境配置与张量初探
  • 您对下列文件的本地修改将被合并操作覆盖XXXXX请 在 合 并前 提 交 或贮 藏 您 的 修 改
  • Mac:Ant 下载+安装+环境配置(详细讲解)
  • 2025年渗透测试面试题总结-某四字大厂实习面试复盘 二面(题目+回答)
  • 多种语言请求API接口方法
  • Python、MATLAB和PPT完成数学建模竞赛中的地图绘制
  • 【AI大模型】提示词(Prompt)工程完全指南:从理论到产业级实践
  • Linux上的`i2c-tools`工具集的编译构建和安装
  • 适合安卓开发工程师在 Android Studio 上使用的 AI 产品
  • A SURVEY ON POST-TRAINING OF LARGE LANGUAGE MODELS——大型语言模型的训练后优化综述——第一部分
  • 1.FastAPI简介与安装
  • Prometheus 和 Grafana科普介绍
  • 有emacs org babel, 还要什么数据分析软件
  • Git版本管理 | 基础指令汇总
  • 极空间NAS部署gitea教程
  • 初始OpenCV
  • 特朗普签署行政命令推动深海采矿,被指无视国际规则,引发环境担忧
  • 一周文化讲座|“不一样的社会观察”
  • 中央空管办组织加强无人机“黑飞”“扰航”查处力度
  • 现场观察·国防部记者会|美将举行大演习“应对中国”,备战太平洋引发关注
  • 海关总署牵头部署开展跨境贸易便利化专项行动
  • 凯撒旅业:2024年营业收入约6.53亿元,同比增长12.25%