新闻详情

新闻详情

首页 / 资讯中心 / 详情

法线贴图与切线空间:从顶点法线到TBN矩阵的完整解析

发布时间:2026/9/30 4:47:02来源:尧图网络
法线贴图与切线空间:从顶点法线到TBN矩阵的完整解析
做图形渲染的人早晚会在法线问题上栽一次跟头。要么是模型光照看起来阴阴阳阳不知哪里出了问题要么是把法线贴图这张“假凹凸”用错了地方要么是想用法线贴图换位移贴图结果怎么转都是错的。我当年第一次给一个圆柱体加非等比缩放后打光整个高光直接糊成一片诡异的条纹排查了大半天才想起来是法线没经过逆转置矩阵处理。从那以后我就意识到法线这东西看起来只是一个小小的三维向量但背后牵扯到顶点插值、坐标空间变换、贴图压缩、UV拓扑一连串的东西。今天这篇就把法线和法线贴图的原理从头到尾掰开揉碎讲一遍希望后来人少走点弯路。这篇文章的内容可以把“法线是什么”和“法线贴图怎么用”串成一条完整的链路先讲顶点法线以及它为什么是所有光照计算的基础再讲法线贴图的诞生逻辑和那个标志性的蓝紫色通道编码接着把法线贴图最核心也最绕的切线空间TBN矩阵拆清楚最后聊一个我实际踩过坑的进阶话题——法线贴图转换成位移贴图的思路。无论你是刚学图形学的初学者还是已经在Shader里折腾了一阵子的半熟手这篇都值得花十分钟看完。1. 顶点法线一切光照计算的起点1.1 渲染为何离不开法线只要谈到实时渲染的光照几乎所有的经验公式里都少不了一个核心运算dot(N, L)也就是法线方向与光线方向的点积。这个点的意义很直观——当光垂直打在表面上时夹角为0点积是1表面最亮光沿着表面滑过去时夹角接近90度点积趋近于0表面变暗。漫反射的Lambert模型大致就是这句话的数学化表达而Blinn-Phong高光里的半程向量计算也离不开法线反射向量、环境光遮蔽、视差映射同样需要法线提供方向基准。所以可以这么说法线就是渲染光照的“地平线”。没有法线再高级的渲染模型也只是一堆平面色块。这一点对初学者尤其重要很多人先在Shader里把颜色、纹理、阴影一通调最后发现画面要么过曝要么死黑绕了一圈才发现是法线输错了方向。法线的方向错一格光照效果就可以从“正常”直接跳到“荒诞”。图形学里的法线分为两类面法线和顶点法线。网格数据里每个三角形都有一个几何法线垂直于这个三角形所在的平面而在实时渲染中我们真正喂给Shader的通常不是面法线而是顶点法线。顶点法线可以理解为这个顶点周围所有相邻面法线的“平均方向”它能让人造模型在光栅化后呈现出平滑过渡的明暗而不是每个三角形面片一块颜色、棱角分明。1.2 顶点法线是怎么算的以及为何要归一化顶点法线的计算方式对渲染结果有直接影响。最简单的做法是把共享该顶点的所有三角面法线做等权平均但实践中更常用的是面积加权平均或者角度加权平均。面积加权就是让面积大的三角形在顶点法线里占更高的权重这样法线不容易被一小片极小的碎面“带偏”。角度加权则按顶点在三角形里所夹的角来分配权重实现起来稍复杂一些但结果更稳。大部分建模软件和引擎默认的做法已经足够好除非你要做特殊风格化渲染否则不用在这上面过度纠结。这里有个关键细节在光栅化阶段GPU是对三角形内部的像素做顶点属性插值来得到每个像素的法线的。图形学管线里顶点法线不会直接喂给片段着色器而是在顶点着色器输出后被硬件在三角形内部线性插值。问题是线性插值之后法线向量的长度往往会小于1如果你在片元着色器里直接拿这个非单位向量去做normalize之外的计算比如直接点积L结果就会偏暗或偏亮。所以绝大多数渲染代码的第一步就是normalize()这一点怎么强调都不算过分。除此之外还有一个更隐蔽的坑模型矩阵中如果包含了非等比缩放法线不能直接用模型矩阵变换否则法线就会和真实表面偏离。正确的做法是用模型矩阵的逆转置矩阵来变换法线。这个过程可以用一个直观的例子理解你把一块木板沿X方向拉长木板上斜着钉的钉子它的“表面法线”并不是简单跟着X轴拉伸的法线会往Y方向“扭”过去。逆转置矩阵就是用来修正这个扭曲的也就是所谓的normal matrix。这个问题在我自己写代码的时候踩过也见过不少引擎论坛上的人问值得在这里特别提醒一句。2. 法线贴图用一张图骗过光照2.1 高模烘焙法线贴图是怎么产生的顶点法线说到底只能在顶点之间做平滑插值真正的细节比如皮肤上的毛孔、砖墙上的纹理、金属表面的划痕靠顶点法线是完全表达不出来的除非你给模型加上几十万甚至几百万个顶点。这对实时渲染来说不可接受。于是就有了法线贴图。法线贴图的本质是把一个高精度模型表面的细节方向信息“烘焙”到一张普通纹理上再给低精度模型使用。美术在ZBrush或者Maya里雕刻一个百万面的高模然后把它和拓扑好的低模对齐通过烘焙工具从低模表面沿法线方向发射射线去采样高模表面记录下高模法线和低模法线之间的偏差写入贴图的RGBA通道里。常用的工具有xNormal、Marmoset Toolbag、Substance Painter甚至Houdini、Blender也能干这个活。烘焙过程里有个很容易出问题的地方cage烘焙笼。如果低模和高模之间在凹陷处偏差太大射线可能会穿到高模的另一侧采出错误的方向最终表现就是局部出现奇怪的明暗条纹。烘焙时把cage的包裹范围设置得合理一点能大幅减少这种错误。此外低模的UV排布也会影响烘焙质量UV拉伸太厉害的区域采样的密度不够出来的法线贴图容易糊。2.2 蓝紫色通道编码RGB如何变成XYZ打开任意一张通用的法线贴图你会看到它以大片蓝紫色为主。这个颜色不是美术随手调的而是数学编码的必然结果。法线贴图里每个像素存的是一个三维方向向量但纹理通道只能存0到1的值而向量的分量范围通常是-1到1因此需要做一个简单的映射rgb normal * 0.5 0.5。默认情况下法线贴图采样得到的法线在切线空间里基本指向Z轴正方向也就是(0, 0, 1)。经过映射后变成R 0 * 0.5 0.5 0.5对应约128G 0 * 0.5 0.5 0.5对应约128B 1 * 0.5 0.5 1.0对应255于是平坦区域像素就是(128, 128, 255)最终呈现为熟悉的蓝紫色。一旦表面有凹凸起伏法线在X或Y方向偏了R和G通道就不再是128颜色也会跟着偏。这就是为什么法线贴图看起来“偏蓝”但彩色分布并不均匀。有一点需要注意不同图形API对法线贴图Z分量的压缩取值范围并不一致。OpenGL里Z分量映射到0到1而DirectX里某些格式会使用另一种编码导致同一张贴图在两个API下表现相反出现“凹凸反向”的问题。使用社区通用的工具时通常已经处理好了方向约定但你自己写引擎时一定要在工程层面约定好坐标系否则贴图跨平台复用会莫名奇妙地凹变凸、凸变凹。2.3 法线贴图能骗过什么骗不过什么法线贴图能骗过的是光照计算。它让一个平坦表面在像素级别拥有变化的方向因此受光后不同像素明暗不同看起来就像有凹凸细节。这是性能与视觉效果之间极好的折中方案一张1024x1024的贴图只占几MB显存却能让几万面的角色看起来像高模。但法线贴图骗不过轮廓。模型的边缘依旧是低模的边缘不会因为贴图而变得有锯齿状的真实凸起。它也不参与阴影计算一个由法线贴图伪造出的凸台不会在平面上投射出真的影子也不会被别的物体真实遮挡。这个局限在硬表面和特写镜头下尤其明显。因此项目里如果有近景特写的石头、墙面仅仅靠法线贴图是撑不住的通常得加上视差贴图或者真正的位移贴图才能过关。3. 切线空间与TBN矩阵法线贴图绕不开的坐标系3.1 为什么法线贴图不直接存世界坐标也许有人会问既然法线贴图存的是方向为什么不直接存世界空间下的方向向量省去一堆矩阵变换这个问题我当年也想过。答案是如果贴图里存的是世界空间法线那么这个物体就不能动、不能转、不能缩放否则贴图方向和物体表面就对不上了。UV一变、骨骼动画一拉伸、模型被镜像复制世界空间法线立刻全部错乱。于是图形学里引入了切线空间Tangent Space。这是一个依附在模型表面的局部坐标系每个像素都有自己的一组基向量法线N与表面垂直、切线T沿着UV的U方向、副切线B沿着UV的V方向。法线贴图里存储的向量就是相对于这组局部坐标系的偏移方向。这样好处很明显无论物体在世界里怎么旋转、移动、形变只要把切线空间里的法线通过TBN矩阵变换到世界空间光照就能在任意姿态下保持正确。这也让法线贴图可以被平铺、被重复使用一张砖墙法线贴图可以铺满整个大平面这在世界空间法线贴图里是不可想象的。3.2 TBN矩阵的推导与计算TBN矩阵的“TBN”由Tangent、Bitangent、Normal三个向量组成它的作用就是把切线空间向量变换到世界空间。问题变成了T和B怎么算出来思路来自三角形的UV坐标与顶点坐标之间的对应关系。假设一个三角形有三个顶点P0、P1、P2对应的UV是(U0, V0)、(U1, V1)、(U2, V2)。我们想找到两个向量T和B使得三角形在UV平面里的U方向对应世界空间里的T方向V方向对应世界空间里的B方向。写成方程就是E1 P1 - P0 ΔU1 * T ΔV1 * B E2 P2 - P0 ΔU2 * T ΔV2 * B这是一个二元线性方程组解出来就是T (E1 * ΔV2 - E2 * ΔV1) / (ΔU1 * ΔV2 - ΔU2 * ΔV1) B (E2 * ΔU1 - E1 * ΔU2) / (ΔU1 * ΔV2 - ΔU2 * ΔV1)这里的核心思想是UV的变化通过线性关系映射到世界空间的坐标变化UV的梯度方向就是切线方向的来源。拿到T和B之后再以顶点法线N为基准做Gram-Schmidt正交化让T、B、N尽量相互垂直就得到了该顶点的TBN基。现代引擎一般在模型导入阶段就把切线算好并烘焙进顶点数据里引擎里通常有一个叫tangent的四分量顶点属性其中第四分量w存的是副切线方向的正负标记用于处理UV镜像的情况。在实际Shader里用起来是这个样子的以GLSL为例// vertex shader vec3 T normalize(mat3(model) * tangent.xyz); vec3 N normalize(mat3(model) * normal); T normalize(T - dot(T, N) * N); // Gram-Schmidt正交化 vec3 B cross(N, T) * tangent.w; mat3 TBN mat3(T, B, N); // fragment shader vec3 n texture(normalMap, uv).xyz * 2.0 - 1.0; vec3 worldNormal normalize(TBN * n);这段代码里有一个关键细节TBN矩阵的三个列向量分别是T、B、N如果你用的是行向量左乘则需要转置成mat3(T, B, N)并按相反顺序乘。很多初学Shader的人在这上面浪费过时间渲染出来的法线完全不对但就是找不到原因。3.3 镜像UV与副切线方向TBN里最隐蔽的坑切线空间有一个很常见的工程坑UV镜像。美术在展UV时经常为了省贴图空间把左右对称的模型共用一块UV区域然后把其中一半镜像。这会导致UV的V方向在镜像后反转B向量的方向随之需要取反。如果没有正确处理模型镜像区域的法线贴图光照会“阴阳颠倒”——左边凹进去右边凸出来。解决的方案就是前面提到的tangent.w它记录了这个副切线是否需要翻转。三角形条带在网格导入阶段会检查UV的绕序如果发现镜像就把这个值设为-1。Shader里计算B时乘以这个值光照就一致了。你可能觉得这是个小问题但在角色模型、车辆这类大量使用对称UV的项目里没处理好它贴图调得再细致也白搭。4. 从法线贴图到位移贴图一次踩坑经历与离线转换思路4.1 需求来源与为什么不能直接拿法线贴图当位移贴图聊完法线贴图本身我想专门讲一个进阶话题把法线贴图转换成位移贴图。这个需求我自己遇到过。当时项目里只有一套现成的法线贴图材质但目标渲染器需要位移贴图做真实的曲面细分和轮廓换高模重烘焙时间来不及于是就想能不能从法线贴图里把高度信息恢复出来。一开始我的想法很简单法线贴图里不就有Z分量嘛Z越大不就代表越朝外吗直接拿它当高度图不就行了一试就翻车——法线贴图里存的是“方向”的编码Z分量代表的是归一化法线在Z轴上的投影它跟真实高度之间的关系并不是绝对值。一片平坦区域的Z可能是1一片高度差不多的斜面Z也可能是0.7直接用Z做高度会导致整个表面乱成地震波。实际上法线贴图里真正和高度有关的信息是法线的X、Y分量也就是法线相对表面的倾斜程度或者说梯度。换句话说法线贴图携带的是高度场的一阶导数信息重建高度等于对梯度做积分。4.2 从梯度重建高度的原理与操作既然法线贴图给了梯度信息那问题就变成了数值积分。设高度场为h(u, v)法线为(Nx, Ny, Nz)在切线空间中高度场的梯度与法线的关系可以写成∂h/∂u ≈ -Nx / Nz ∂h/∂v ≈ -Ny / Nz为什么是负号因为当表面向右上坡时法线会向左倾斜。这一点想清楚之后实现就有思路了对每个像素从图像的一角开始按U方向累积梯度得到一行高度值再按V方向累积行与行之间的差值。最简单粗暴的就是扫描线逐行积分但这样误差会一路累积图像末端的亮度容易跑到离谱。我在实际验证过程中试了几种提高精度的方法其中比较实用的是“多方向积分”加“均值融合”。做法是对每个像素分别从左到右、从右到左、从上到下、从下到上做四次积分。将四次结果按像素位置加权平均权重可以用离对应扫描起点的距离函数。对最终灰度图做一次高斯模糊把积分累积出来的高频噪声压下去。将灰度范围归一化到0到1必要时做对比度调整。但老实讲这种空域积分方法对小尺寸贴图还凑合一旦分辨率到2048以上累积误差和伪影就会冒出来。更稳的做法是走频率域把梯度变换到傅里叶域高度可以通过频域中的除法一步求解。这个思路的本质是因为频域中微分算子可以用乘法表示那积分自然就是除法。写起来代码量不小但鲁棒性比空域积分强很多。如果只是临时项目救急常用开源库里的displacement map from normal map工具已经能完成这一步如果追求高质量最好的选择仍然是回到高模去重新烘焙一张真正的位移贴图因为法线贴图本身在低模烘焙过程中已经损失了绝对高度信息转换本质上是在做病态反演只能“尽量接近”无法完全还原。4.3 转换结果的判断标准与常见问题怎么判断转换出来能不能用我的经验是做两个检查第一把转换得到的高度图贴回去做正常高度图渲染看看明暗变化是否和原法线贴图方向一致尤其注意凹凸方向会不会整体反转第二开启细分位移后观察轮廓边缘如果轮廓有那种“阶梯状”的突变说明高频区域噪声没有被压干净需要加大平滑或者降低高频增益。这里还有一个细节如果原法线贴图是DirectX风格编码转换前要先把Y分量翻转成OpenGL风格否则高度场会整体镜像。这类坐标系约定问题听着基础但真到了项目里最磨人的往往就是这种小约定。我在做转换的时候一开始没有注意原贴图的编码是哪个API体系的结果两套视觉方向全部反了白折腾了一个晚上。所以拿到任何外部贴图先确认坐标系约定再谈处理。5. 实战中的常见问题与经验总结5.1 法线贴图压缩格式DXT5与BC5怎么选法线贴图在物理存储上有一个和普通漫反射贴图完全不同的注意点它的通道数据是带符号的方向信息对压缩误差极其敏感。很多项目默认用DXT5压缩而DXT5会把R和G通道放在同一个64位块里压缩后精度损失比较明显。业界更推荐用BC5或者BC7它们的压缩算法对法线贴图这种RG分量重要、B分量可由公式重建的数据更友好。这里说一个值得记住的小经验如果只能用DXT5很多引擎会采用DXT5nm格式。这种格式把原本存在A通道里的X分量挪到G通道而RGB三个通道则不再存真正颜色再在Shader里通过Z sqrt(1 - X*X - Y*Y)重建Z分量。这样做的原因在于人眼对绿色通道的敏感度更高DXT5的G通道压缩精度通常最好把重要的法线X分量放到G通道里有实际收益。这个设计听上去有点绕但了解之后你会对贴图通道安排有更深的理解。另外千万别把法线贴图的采样开启sRGB。法线贴图是数据贴图不是颜色贴图它里面的值是需要直接参与向量运算的。如果管线默认给纹理开了sRGBShader里采样出来后再做一次线性化解码法线方向就被扭曲了光照会变得灰灰蒙蒙。这个问题在一些半成品引擎里反复出现排查的方式也很简单先关掉sRGB采样试一下。5.2 UV接缝与法线不连续法线贴图在UV接缝处经常会出现一条肉眼可见的“棱线”。原因在于法线贴图采样的方向依赖于顶点的切线方向而UV接缝两侧的切线方向不连续导致切线空间里的法线无法平滑衔接。解决思路有几个一是让美术在展UV时尽量把接缝藏在模型背光或者不起眼的位置二是在引擎里对切线做平滑处理或者导入时使用normalize后插值三是沿接缝两侧生成连续切线数据这在高端引擎里存在具体要看使用的渲染管线能力。我在做角色模型时还踩过一个与接缝相关的坑颈部和头部的UV如果被拆分到不同象限而法线贴图用的是同一张在接缝处光照方向不一致会形成明显的“贴图撕裂”。后来统一让美术重新整理UV把接缝放到头发下面视觉问题直接消失。所以说法线贴图的好效果不只是Shader调参调出来的一半功劳要归给之前UV布局和烘焙质量。5.3 法线贴图和位移贴图到底怎么选最后聊一个非常实际的取舍问题。法线贴图胜在性价比它不增加任何几何复杂度只需要一张贴图和一个矩阵变换是移动端和大量物体场景的最优选择。位移贴图胜在真实它能改变模型轮廓产生真正的阴影和高差但需要细分曲面Tessellation或大量额外几何体配合开销明显高。从项目实践的角度我建议按“距离”来决策角色脸部特写、硬表面产品展示优先考虑位移贴图角色全身、中远景环境法线贴图已经足够地形、水面这类大范围且需要细节的可以选择位移贴图配LOD方案移动端尽可能用视差贴图作为法线贴图的增强替代品它在高度图上做采样偏移成本可控且立体感比纯法线贴图强不少视差贴图在我的经验里是一个被低估的中间选项它只需在片元着色器里对UV做几次偏移和采样不需要细分几何体就能达到近似位移的视觉效果。如果你的项目里法线贴图轮廓感不足又暂时上不了位移贴图不妨优先试试视差贴图。最后再分享一个很基础但极容易被人忽略的点法线贴图里的向量是归一化的但在反复插值和坐标变换之后很多引擎里最终得到的向量长度已经不再是1。不论你怎么采样、怎么变换也不论你用的是TBN还是世界空间最后再normalize一次永远不亏。渲染里很多莫名其妙的光照错误最终都是栽在这个小到不能再小的细节上。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Dify本地部署全攻略:从环境准备到知识库与工作流实战 2026/9/30 5:43:34

Dify本地部署全攻略:从环境准备到知识库与工作流实战

1. Dify 是什么:拆开揉碎讲清楚过去一年我折腾了不少 AI 应用开发的东西,从最早的手搓 Prompt 调 OpenAI API,到后来用 LangChain 拼链条,再到各种低代码平台,走了挺多弯路。直到用了 Dify,我才有一种“终于…

阅读更多 →
Jev 决策型 Agent 实战:绕过 Token 生成直接输出动作 2026/9/30 5:43:34

Jev 决策型 Agent 实战:绕过 Token 生成直接输出动作

1. 从 Token 到决策:Jev 到底在解决什么问题第一次看到“Jev:当 AI 不再生成 Token,而是直接做决策”这个标题,我脑子里蹦出来的第一个画面是:过去两年我们跟大模型打交道,本质上都在做一件事——猜下一个 …

阅读更多 →
【优化求解】遗传算法创建蜘蛛姿态步态运动【含Matlab源码 15992期】 2026/9/30 5:43:33

【优化求解】遗传算法创建蜘蛛姿态步态运动【含Matlab源码 15992期】

💥💥💥💥💥💥💥💥💞💞💞💞💞💞💞💞💞Matlab武动乾坤博客之家💞…

阅读更多 →
FCE1100:国产EtherCAT从站控制器芯片替代LAN9252的实战要点 2026/9/30 5:43:27

FCE1100:国产EtherCAT从站控制器芯片替代LAN9252的实战要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
第一次小测复盘 2026/9/30 5:43:27

第一次小测复盘

R7-1 查找整数 本题要求从输入的N个整数中查找给定的X。如果找到,输出X的位置(从0开始数);如果没有找到,输出“Not Found”。 输入格式: 输入在第一行中给出两个正整数N(≤20)和X&am…

阅读更多 →
ADC 量化:连续电压变成一格格数字,背后的采样、参考电压与分辨率是什么? 2026/9/30 5:43:27

ADC 量化:连续电压变成一格格数字,背后的采样、参考电压与分辨率是什么?

ADC 量化:连续电压变成一格格数字,背后的采样、参考电压与分辨率是什么? 这篇面向第一次接触 ADC 量化 的读者,只解释一个核心因果。下方视频和图示是功能流程示意,不是逐引脚接线图,不能拿来直接施工。 先…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉