基于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); // 将法线从切线空间转换到世界空间// 计算光照强度(点积),确保不小于0float 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方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/36778.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

活码在实际操作中的具体场景有哪些?怎么应用?

当传统二维码因“内容固定、无法追踪、流量拥堵”等问题逐渐失效时&#xff0c;活码正在成为企业破解运营痛点的关键工具。 无论是需要实时更新内容的线下物料&#xff0c;还是面临用户分流压力的线上客服&#xff0c;动态二维码都能通过“一码多用、灵活配置”的特性&#xf…

极空间NAS部署gitea教程

极空间NAS部署gitea步骤教程 背景1. 准备镜像1.1 极空间官方1.2 Win系统docker再上传1.3 镜像转录 2. MySql配置2.1 容器配置2.2 命令行配置 3. gitea配置3.1 容器配置3.2 打开网页3.3 网页配置安装 参考资料 背景 极空间Nas和别的Nas不同的地方就在于&#xff0c;他不是那种标…

Wireshark:在 显示过滤器中“加入条件”过滤后,出现其他类型的数据包,为什么?

一、 在Wireshark中使用“tcp协议”过滤后&#xff0c;仍出现TLSv1.2协议的数据包&#xff0c;原因如下&#xff1a; 1. ‌协议层次关系‌ ‌TCP是传输层协议‌&#xff0c;而‌TLS属于应用层协议‌&#xff0c;后者直接运行于TCP之上‌28。因此&#xff0c;所有TLS流量&…

【医学影像 AI】大型语言模型生成 ROP 患者信息材料的能力

【医学影像 AI】大型语言模型生成 ROP 患者信息材料的能力 0. 论文简介0.1 基本信息0.2 摘要 1. 引言2. 材料与方法2.1 大语言模型的使用2.2 可读性标准2.3 统计分析 3. 结果3.1 Bezirci-Yılmaz可读性评分3.2 Ateşman可读性评分3.3 全面性评分3.4 准确性评分 4. 讨论4.1 可读…

设计模式(行为型)-策略模式

目录 定义 类图 角色 角色详解 Strategy&#xff08;抽象策略类&#xff09;​ Context&#xff08;环境类 / 上下文类&#xff09;​ ConcreteStrategy&#xff08;具体策略类&#xff09;​ 优缺点 优点​ 缺点​ 使用场景 类行为差异场景​ 动态算法选…

服装零售行业数字化时代的业务与IT转型规划P111(111页PPT)(文末有下载方式)

服装零售行业数字化时代的业务与IT转型规划P111 详细资料请看本解读文章的最后内容。 随着数字化技术的迅猛发展&#xff0c;服装零售行业正经历着前所未有的变革。本文将对《服装零售行业数字化时代的业务与IT转型规划P111》进行详细解读&#xff0c;探讨未来几年内该行业的…

【大语言模型_6】mindie启动模型错误整理

一、启动报 [hccl_runner.cpp:141] AllGatherHcclRunner:0 HcclCommInitRootInfo fa il, error:2, rank:0, rankSize:2 背景&#xff1a;运行DeepSeek-R1-Distill-Qwen-14B模型&#xff0c;在2张300 P卡可以运行&#xff0c;单独一张启动报以上错误。 问题分析&…

STM32F429单片机FMC接口驱动TFT LCD和SDRAM

1、FMC接口介绍 FMC 接口&#xff08;即可变存储控制器&#xff09;是一种用于管理外部存储器的外设接口&#xff0c;支持多种类型的存储器&#xff0c;主要分为三大类&#xff1a;NOR/SRAM/PSRAM设备&#xff08;TFTLCD相当于SRAM&#xff09;、NOR FLASH/NAND FLASH/PC卡设备…

ollama不安装到c盘,安装到其他盘

ollama 安装包默认安装到c盘&#xff0c;安装程序并没有提供选择文件夹安装功能&#xff0c;本来c盘就快满了&#xff0c;下几个模型c盘都快爆了&#xff0c;如何将ollma安装到其他盘呢&#xff1f; ollama 默认安装位置 C:\Users\Admin\.ollama 是 Ollama 用来放大模型的文件夹…

java项目之基于ssm的少儿编程在线培训系统(源码+文档)

项目简介 少儿编程在线培训系统实现了以下功能&#xff1a; 用户信息管理&#xff1a; 用户信息新增 用户信息修改 教师信息管理&#xff1a; 教师信息添加 教师信息删除 教师信息修改 课程信息管理&#xff1a; 课程信息添加 课程信息修改 课程信息删除 课程类型管理&…

Cinema4D安装及基本操作

一、简介 Cinema 4D&#xff08;C4D&#xff09;是德国 Maxon Computer 开发的 3D 软件&#xff0c;具备强大的建模、动画、材质、渲染功能&#xff0c;以易用高效著称&#xff0c;广泛应用于影视、游戏、设计等领域&#xff0c;是行业内主流 3D 创作工具。 二、安装 1.下载安…

为什么TCP需要三次握手?一次不行吗?

文章目录 1. 三次握手的过程2. 为什么需要三次握手&#xff1f;3. 握手过程中每一步的具体作用4. 简单比喻5. 为什么是三次握手&#xff0c;而不是两次或四次&#xff1f;6. 三次握手中的序列号有什么作用&#xff1f;7. 总结 1. 三次握手的过程 三次握手是建立 TCP 连接的过程…

大数据在人力资源管理中的洞察与决策

hello宝子们...我们是艾斯视觉擅长ui设计和前端数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩! 在数字化转型浪潮中&#xff0c;人力资源管理&#xff08;HRM&#xff09;正经历着前所未有的变革。…

让vscode远程开发也可以图形显示

目录 0. 摘要1. 保存查看2. jupyter内置inline渲染3. jupyter浏览器4. matplot修改后端5. SSH X11转发[※]6. 参考 0. 摘要 vscode登录远程服务器进行开发遇到图形显示需求时&#xff0c;该怎么处理&#xff1f;一般有几种方式&#xff1a; 保存下来查看jupyter内置的inline图…

Blender制作次表面材质

效果: 主要用沃罗诺伊纹理做出云絮感 然后EV开启次表面设置

服务器数据恢复—服务器raid故障导致上层分区不可用的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台服务器中有一组由三块SAS硬盘组建的raid阵列。服务器上部署的数据库存储在D分区&#xff0c;数据库备份存储在E分区。 服务器上一块硬盘指示灯显示红色。D分区不可识别。E分区虽然可以识别&#xff0c;但是E分区拷贝文件报错。 管…

PyTorch PINN实战:用深度学习求解微分方程

神经网络技术已在计算机视觉与自然语言处理等多个领域实现了突破性进展。然而在微分方程求解领域&#xff0c;传统神经网络因其依赖大规模标记数据集的特性而表现出明显局限性。物理信息神经网络(Physics-Informed Neural Networks, PINN)通过将物理定律直接整合到学习过程中&a…

关于“碰一碰发视频”系统的技术开发文档框架

以下是关于“碰一碰发视频”系统的技术开发文档框架&#xff0c;涵盖核心功能、技术选型、开发流程和关键模块设计&#xff0c;帮助您快速搭建一站式解决方案 --- 随着短视频平台的兴起&#xff0c;用户的创作与分享需求日益增长。而如何让视频分享更加便捷、有趣&#xff0c…

【VUE】day05-ref引用

这里写目录标题 1. ref引用1.1 使用ref引用组件 2. this.$nextTick(cb)方法3. 购物车案例3.1 数组中的方法 - some循环3.2 数组中的方法 - every循环3.3 数组中的方法 - reduce 4. 购物车案例 1. ref引用 ref用来辅助开发者在不依赖于jQuery的情况下&#xff0c;获取DOM元素或…

docker安装milvus向量数据库Attu可视化界面

Docker 部署 Milvus 及 Attu 可视化工具完整指南 一、环境准备 安装 Docker 及 Docker Compose Docker 版本需 ≥20.10.12Docker Compose 版本需 ≥2.20.0&#xff08;推荐 V2&#xff09; 验证 Docker 环境 docker --version && docker-compose --version若出现&…