法线贴图的计算方式
创始人
2025-05-31 16:18:22

大家好,我是阿赵。
之前介绍了光照模型相关的一些知识,包括了MatCap、动态阴影,都是为了模拟模型的光照效果的。这次打算讲一下法线贴图的计算原理。
说到法线贴图,估计大家都听说过,都知道法线贴图其实就是用来模拟模型上的凹凸细节的。
至于怎样使用法线贴图,估计也都知道,找个支持法线贴图的shader,把贴图放上材质球就行了。既然都知道的东西,我为什么还单独拿出来说呢?那是因为,我们要写自己的效果,包括光照模型都是要自己去实现的,法线作为一个计算角度的重要组成部分,他的作用并不止是做个简单的凹凸那么简单。我们如果要做复合的效果,也要自己去实现法线贴图的计算。

法线贴图

一、完整的代码:

Shader "azhao/NormalShader"
{Properties{_MainTex ("Texture", 2D) = "white" {}_NormalTex("Normal Tex", 2D) = "black"{}_normalScale("normalScale", Range(-1 , 1)) = 0_specColor("SpecColor",Color) = (1,1,1,1)_shininess("shininess", Range(1 , 100)) = 1_specIntensity("specIntensity",Range(0,1)) = 1_ambientIntensity("ambientIntensity",Range(0,1)) = 1}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{cull offCGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;float3 normal:NORMAL;float3 tangent:TANGENT;};struct v2f{                float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float3 worldPos : TEXCOORD1;//为了构建TBN矩阵,所以要获取下面这三个值float3 worldNormal : TEXCOORD2;float3 worldTangent :TEXCOORD3;float3 worldBitangent : TEXCOORD4;};sampler2D _MainTex;float4 _MainTex_ST;sampler2D _NormalTex;float4 _NormalTex_ST;float _normalScale;float4 _specColor;float _shininess;float _specIntensity;float _ambientIntensity;//简化版的转换法线并缩放的方法half3 UnpackScaleNormal(half4 packednormal, half bumpScale){half3 normal;//由于法线贴图代表的颜色是0到1,而法线向量的范围是-1到1//所以通过*2-1,把色值范围转换到-1到1normal = packednormal * 2 - 1;//对法线进行缩放normal.xy *= bumpScale;//向量标准化normal = normalize(normal);return normal;}//获取HalfLambert漫反射值float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal){float3 lightDir = UnityWorldSpaceLightDir(worldPos);float NDotL = saturate(dot(worldNormal, lightDir));float halfVal = NDotL * 0.5 + 0.5;return halfVal;}//获取BlinnPhong高光float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal){float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));float specDir = max(dot(normalize(worldNormal), halfDir), 0);float specVal = pow(specDir, _shininess);return specVal;}v2f vert (appdata v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.worldPos = mul(unity_ObjectToWorld, v.vertex);o.worldNormal = UnityObjectToWorldNormal(v.normal);o.worldTangent = UnityObjectToWorldDir(v.tangent);o.worldBitangent = cross(o.worldNormal, o.worldTangent);return o;}half4 frag (v2f i) : SV_Target{//采样漫反射贴图的颜色half4 col = tex2D(_MainTex, i.uv);//计算法线贴图的UVhalf2 normalUV = i.uv * _NormalTex_ST.xy + _NormalTex_ST.zw;//采样法线贴图的颜色half4 normalCol = tex2D(_NormalTex, normalUV);//得到切线空间的法线方向half3 normalVal = UnpackScaleNormal(normalCol, _normalScale).rgb;//构建TBN矩阵float3 tanToWorld0 = float3(i.worldTangent.x, i.worldBitangent.x, i.worldNormal.x);float3 tanToWorld1 = float3(i.worldTangent.y, i.worldBitangent.y, i.worldNormal.y);float3 tanToWorld2 = float3(i.worldTangent.z, i.worldBitangent.z, i.worldNormal.z);//通过切线空间的法线方向和TBN矩阵,得出法线贴图代表的物体世界空间的法线方向float3 worldNormal = float3(dot(tanToWorld0, normalVal), dot(tanToWorld1, normalVal), dot(tanToWorld2, normalVal));//用法线贴图的世界空间法线,算漫反射half diffuseVal = GetHalfLambertDiffuse(i.worldPos, worldNormal);//用法线贴图的世界空间法线,算高光角度half3 specCol = _specColor * GetBlinnPhongSpec(i.worldPos, worldNormal)*_specIntensity;//最终颜色 = 环境色+漫反射颜色+高光颜色half3 finalCol = UNITY_LIGHTMODEL_AMBIENT* _ambientIntensity +col.rgb*diffuseVal + specCol;return half4(finalCol,1);}ENDCG}}
}

通过调节参数,可以做出不同凹凸方向和高光的效果。
在这里插入图片描述
在这里插入图片描述

二、说明

1、法线贴图是什么

在这里插入图片描述

一般来说,法线贴图就是这么一张偏蓝色的贴图。为什么法线贴图偏蓝色,简单来说,这是因为如果一个法线是垂直于物体表面的,那么他的法线向量是(0,0,1),转换到rgb颜色之后,b通道的颜色会比较大。
然后还有一个需要注意的地方,法线方向的取值范围是-1到1的,但如果转换成贴图,颜色的取值是0-255的,可以理解成是0-1。所以我们在保存法线贴图的时候,需要把法线向量加1,再除以2,从而把-1到1的范围变成0-1。所以在使用法线贴图的时候,有一个UnpackNormal的过程,其实就是把贴图颜色乘以2再减1,让法线贴图的颜色值从0到1转换回-1到1。

2、切线空间的法线贴图

看完上面的介绍后,可能大家会有个疑问了,既然向量是有方向的,那么如果模型本身发生了旋转,那么它的法线方向是不是就变了?答案是,真的会变的。
那么为什么从来没有出现过,当模型被旋转之后,需要重新生成法线贴图的情况呢?
这是因为,我们导出法线贴图时,使用的坐标系并不是世界坐标系,而是模型的切线坐标系。切线是平行于物体表面的,如果用切线作为坐标系来记录法线向量方向,比如某个法线是一直垂直于物体表面的,那么就算物体怎样旋转,它的世界坐标系的法线方向怎样改变,但相对于切线本身的方向是不会变的。其实模型的顶点法线向量,也都是切线坐标系下的法线向量。
所以,当我们使用法线方向的时候,需要先把模型局部的法线方向转换成世界坐标系的法线方向,才能用来参与计算。

3、TBN矩阵

TBN矩阵,之前也有介绍过,里面主要用到了一个binormal 的感念。binormal 是通过法线和切线叉乘得到的,通过叉乘的几何含义,我们可以知道,实际上binormal 是一条垂直法线和切线的向量,它和切线可以定义一个和顶点相切的面。binormal 和bitangent,其实都是同一个东西。
TBN(Tangent,Binormal,Normal)矩阵一般是用于把切线空间坐标转换到世界空间坐标。这里用TBN矩阵把法线贴图里面读取到的切线空间的法线向量,转换到了世界空间。

4、UnpackScaleNormal方法

之前介绍过,需要使用法线贴图,要把颜色值乘以2再减一。这就是UnpackNormal的过程。
UnpackScaleNormal是UnityStandardUtils.cginc自带有的方法,它的源码是这样的:

half3 UnpackScaleNormalRGorAG(half4 packednormal, half bumpScale)
{#if defined(UNITY_NO_DXT5nm)half3 normal = packednormal.xyz * 2 - 1;#if (SHADER_TARGET >= 30)// SM2.0: instruction count limitation// SM2.0: normal scaler is not supportednormal.xy *= bumpScale;#endifreturn normal;#else// This do the trickpackednormal.x *= packednormal.w;half3 normal;normal.xy = (packednormal.xy * 2 - 1);#if (SHADER_TARGET >= 30)// SM2.0: instruction count limitation// SM2.0: normal scaler is not supportednormal.xy *= bumpScale;#endifnormal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy)));return normal;#endif
}half3 UnpackScaleNormal(half4 packednormal, half bumpScale)
{return UnpackScaleNormalRGorAG(packednormal, bumpScale);
}

只要#include "UnityStandardUtils.cginc"就能使用这个方法。
但我个人的习惯是,为了减少shader变体的产生,一般是避免使用宏的,而且只是为了计算一个法线的缩放值,我觉得没有必要包含整个UnityStandardUtils.cginc,只需要自己实现一个把法线的xy轴乘以缩放,再归一化就行了。

5、套用各种光照模型

从代码可以看到,我直接复制了之前介绍光照模型时的HalfLambert漫反射模型和BlinnPhong高光模型,完全不需要改,直接套用就行了,最终还是通过环境色、漫反射颜色、高光颜色来组成了模型最终显示的颜色。
同理,我们也可以套用其他的漫反射模型、高光模型、甚至NdotV边缘光、MatCap模拟光照,等等。
到了这里,是不是稍微感觉到,自己实现光照模型的好处了?通过各种的组合,我们就能很自由的实现各种不同的效果,甚至我们还可以不按照标准的计算方式,自己提取一部分的值来做扭曲,做出很多奇怪的效果。

相关内容

热门资讯

头歌--第1关:Linux文件... 任务描述 假设系统中存在一个文件File,修改该文件的权限,根据实际需求...
【Spring从成神到升仙系列... 👏作者简介:大家好,我是爱敲代码的小黄,独...
梦见蜈蚣是什么意思,做梦梦见蜈... 梦见蜈蚣是什么意思目录梦见蜈蚣是什么意思做梦梦见蜈蚣什么意思梦见蜈蚣是什么意思,哪里有解释啊梦见蜈蚣...
小区车位比一般是多少,车库配比... 小区车位比一般是多少目录小区车位比一般是多少车库配比是什么小区总户数8200,总车位是1450个,配...
车锁上的lock什么意思,汽车... 车锁上的lock什么意思目录车锁上的lock什么意思汽车上lock是什么意思?车子上“lock标志”...
kirin710是什么处理器,... kirin710是什么处理器目录kirin710是什么处理器海思kirin710是高通多少?骁龙71...
程序的循环结构和random库...   第三个参数就是步长     引入文件时记得指明字符格式,否则读入不了 ...
跟着文档制作cocos第一个游... 背景 近期打算学习一下cocos creator,想着开发自己的游戏,是...
乌干达是什么梗,网络语乌干达什... 乌干达是什么梗目录乌干达是什么梗网络语乌干达什么意思?乌干达是什么梗乌干达是什么梗乌干达是什么梗 ...
车载电子狗怎么用,怎样使用电子... 车载电子狗怎么用目录车载电子狗怎么用怎样使用电子狗怎么使用电子狗求简答车载电子狗怎么使用车载电子狗怎...
梦见偷东西是什么意思,梦见自己... 梦见偷东西是什么意思目录梦见偷东西是什么意思梦见自己偷东西是什么意思?做梦梦见自己偷东西好不好梦见偷...
黄金瞳到底是什么,黄金瞳电视剧... 黄金瞳到底是什么目录黄金瞳到底是什么黄金瞳电视剧什么时候上映?《黄金瞳》的结局是什么?电视剧《黄金瞳...
前端-session、jwt 目录:   (1)session (2&#x...
企业即时通讯怎样为企业实现移动... 对于企业来说,在办公过程中少不了工作人员相互传递信息和数据传输,企业内部...
骑行选择什么自行车 极速百科网... 骑行选择什么自行车目录骑行选择什么自行车骑行选择什么自行车 1. 山地自行车:适合崎岖不平的路...
蓝色都有哪几种,蓝色都有什么颜... 蓝色都有哪几种目录蓝色都有哪几种蓝色都有什么颜色的蓝图片,蓝色都有什么颜色的蓝二年级蓝色有哪些种类蓝...
如何自学游泳要安全的,初学游泳... 如何自学游泳要安全的目录如何自学游泳要安全的初学游泳的人需要准备哪些东西,注意哪些事项?如何自学游泳...
一年级家长的话怎么写评语,一年... 一年级家长的话怎么写评语目录一年级家长的话怎么写评语一年级学生评价手册家长寄语怎么写一年级最佳家长评...
EEG微状态的功能意义 导读大脑的瞬时全局功能状态反映在其电场结构上。聚类分析方法一致地提取了四种头表面脑电场结构ÿ...
docker 镜像管理 查看本地镜像 docker images 可以查看本地下载的镜像 docker images [O...
k8s-1.22.15部署ng... 1.介绍 在前面文章中已经提到,Service对集群之外暴露服务的主要方式有两种&#x...
革命烈士寄语怎么写,清明节缅怀... 革命烈士寄语怎么写目录革命烈士寄语怎么写清明节缅怀先烈的寄语有哪些呢?革命烈士寄语怎么写 革命...
5万元以下新车推荐,5万以下买... 本篇文章极速百科给大家谈谈5万元以下新车推荐,5万以下买什么车好,以及5万以下的新车哪款最好对应的知...
真皮沙发翻新一般多少钱?(真皮... 本篇文章极速百科给大家谈谈真皮沙发翻新一般多少钱?,以及真皮沙发翻新一般多少钱一个对应的知识点,希望...
磨皮什么意思(磨皮是啥?) 磨... 本篇文章极速百科给大家谈谈磨皮什么意思,以及磨皮是啥?对应的知识点,希望对各位有所帮助,不要忘了收藏...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
从NVIDIA GTC大会,看... 从NVIDIA GTC 2023这场全球行业盛宴,我们可以解读出AI算力行业的哪些重要...
请问什么是童子,什么是童子 极... 请问什么是童子目录请问什么是童子什么是童子古代 童子是什么意思童子是什么意思?请问什么是童子 ...
中招考试考哪些科目,中招考试考... 中招考试考哪些科目目录中招考试考哪些科目中招考试考几门科目一共多少分?中考有哪些科目中考考几科,都什...
做电商如何做,电商怎样做才能赚... 做电商如何做目录做电商如何做电商怎样做才能赚钱?做的好的电商朋友可以教教我怎么做吗新手小白怎么做跨境...