Unity中关于法线贴图的实现

自己的第一篇博客,记录一下在unity中处理法线贴图的一些技术点。

首先,什么是法线贴图(Normal Mapping)呢?维基百科上的解释是“是一种模拟凹凸处光照效果的技术,是凸凹贴图的一种实现(原文:it is a technique used for faking the lighting of bumps and dents – an implementation of bump mapping)。而我一直把它当作一种能表现物体凹凸感、真实感的技术,并且实现起来并不复杂(至少网上一搜一大堆,无脑复制黏贴都行)。但是,真正自己来实现一遍,还是有些坑踩了,所以在此记录下。

PS. 不知道凹凸贴图的小伙伴我把维基百科的解释贴在这儿,“凹凸贴图就是让每个待渲染的像素在计算照明之前都要加上一个从高度图中找到的扰动,这样得到的结果表面表现更加丰富、细致,更加接近物体在自然界本身的模样。”

既然要实现法线贴图,法线图是必不可少的,我们需要像这样的一张图


Normal Map.png

嗯,看着就和一般的贴图不一样,整体偏向蓝紫色。这是为什么呢?要解释这个问题也很简单,看官方文档就行了(https://docs.unity3d.com/Manual/StandardShaderMaterialParameterNormalMap.html),不过如果你比较懒(比如我>-<),不想去看文档的话,就看我在这里的解释吧(^ - ^)。

图中rgb通道的实际上储存着法线的xyz方向,z分量代表向上(unity通常让y代表向上,但这里不是),我们知道rgb值是0-1的范围,而方向的取值范围是-1到1之间,所以在制作这张法线图的时候会有一个转换,让-1到1的值变成0-1的值(映射函数为rgb =(normal+1)/2);大多数情况下我们不需要让法线偏很多,或者说法线根本没变,就是一个朝上的vector(0,0,1),那么变成rgb值就是(0.5,0.5,1),这个值看起来就是蓝紫色了。那些看起来不是蓝紫色的地方,说明法线偏的比较厉害,所以变成了其他颜色。

另外,如果美术给了法线图,扔进unity是需要改一下texture type的,default的话后面会有麻烦,最好改成normal map(如图所示)


截图.png

现在可以开始写代码来使用这张normal图了……
等等,再写代码使用之前还需要记录一件事,那就是其实这个图里所记录的法线方向是在tangent sapce下,我们要用的话需要转换,可以选择把tangent space下的法线转换到local space再一步步处理,或者干脆把涉及到的东西转换到tangent space下,两种都可以。
不过首先,什么是tangent space?我们知道local spaceworld space这些是因为定义的原点不同而有了各自的空间表达,那么tangent space的原点就是和他们这些空间的原点都不一样,这个空间的原点就是模型的顶点,z轴是该点的normal方向,如下图(unity让z代表向上,原来如此!)

NormalVector.png

TangentVectors.png

那么x轴,y轴就应该是和该点相切的两条线,这样的线本来在空间中是有无数条的,但模型里会定义好一个tangent,这个东西的方向一般就是x轴,而y轴就可以通过cross(x,z)来求得了。


TangentVectorFromUVs.png

NTBFromUVs.png

终于,可以开始写代码实现了:)
我们在vertex shader里的代码是这样的

        v2f vert (appdata_tan v)//一定要用appdata_tan否则取不到变量TANGENT_SPACE_ROTATION就用不了了
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            o.texNormal = TRANSFORM_TEX(v.texcoord, _NormalTex);

            TANGENT_SPACE_ROTATION;
            //unity自带命令,实际上就是在算下面两行东西
            //float3 binormal = cross(v.tan.xyz,v.normal) * v.tan.w;
            //float3x3 rotation = float3x3(v.tan.xyz,binormal,v.normal);
            float3 ld = mul(unity_WorldToObject, _WorldSpaceLightPos0.xyz);//将光向量从世界空间转到模型空间
            o.lightDirection = mul(rotation, ld);//再从模型空间转到切线空间

            UNITY_TRANSFER_FOG(o,o.vertex);

            return o;
        }

其中重要的有两点:

一是vertex shader传入的变量类型要是appdata_tan,否则TANGENT_SPACE_ROTATION用了会报错,因为这个东西要用模型自带的tangent计算东西,创建shader时自动生成的那个appdata就可以不用了,免得麻烦;

二是o.lightDirection = mul(rotation, ld);这句代码,它表达的意思把光向量从local space转到tangent space,记住它!记住它!记住它!!!

呃。。。死记硬背不是个好方法,我们还是来看看为什么吧。首先这个rotation是个float3x3类型的矩阵,而在这里这个矩阵是按行存储的(我没有找到确切的文档说明是按行存储,但根据结果来看一定是),而我们在计算一个向量从一个空间到另一个空间都是把转换矩阵按列存储再左乘(这里要安利一个视频合集https://www.bilibili.com/video/av6731067,对于线性代数,各种矩阵转换有很直观的解释),而这边的这个rotation矩阵是按行存储的,相当于是按列存储的rotation矩阵的转置,这里就很有意思了,因为这个rotation矩阵是个单位正交矩阵(这个不明白还是百度吧),而单位正交矩阵有个性质就是它的转置矩阵等于它的逆矩阵,所以这里相当于在左乘rotation矩阵的逆矩阵,原本按列存储的时候每个列向量代表的是tangent space下的向量,所以左乘了是把某个向量从tangent space转到local space,那么左乘它的逆矩阵就是把某个向量从local space转到tangent space了。

PS. 这里如果不太明白的话需要去熟悉一下线性代数的相关内容,上面那个链接是个很好的入门。

其实还有隐藏的第三点:),如果是自己算的rotation矩阵,这句float3 binormal = cross(v.tan.xyz,v.normal) * v.tan.w;为什么要乘个v.tan.w?这是因为这个w分量记录了方向,可正可负,在OpenGLDirectX下是不一样的,所以要乘上。

再来就是fragment shader

        fixed4 frag (v2f i) : SV_Target
        {
            // sample the texture
            fixed4 col = tex2D(_MainTex, i.uv);

            float3 normal = UnpackNormal(tex2D(_NormalTex, i.texNormal));//获取图中法线向量,把normal图的texture type设置正确会得到正确结果
            normal.xy *= _NormalScale;//控制凹凸程度,若是直接normal*_NormalScale的话若_NormalScale为0则下面的diffuseLight项为0,只用UNITY_LIGHTMODEL_AMBIENT项会导致cube黑乎乎的,不好看
            normal.z = 1 - saturate(sqrt(dot(normal.xy,normal.xy)));//因为xy拉伸了所以z要重新算,本来x*x + y*y + z*z = 1,所以z = 1 - sqrt(x*x + y*y),而dot(normal.xy,normal.xy)就是在表达x*x + y*y

            float3 ambientLight = UNITY_LIGHTMODEL_AMBIENT.rgb;
            
            float3 diffuseLight = _LightColor0.rgb * saturate(dot(i.lightDirection.xyz,normal));

            float4 finalCol = float4((ambientLight + diffuseLight) * col.rgb,col.a);
            // apply fog
            UNITY_APPLY_FOG(i.fogCoord, finalCol);
            return finalCol;
        }

我们利用unity的内置函数UnpackNormal来获取那张蓝紫色贴图里的normal,这个方法的源代码在UnityCG.cginc中有,是这样子的

// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
 // Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
{
  // This do the trick
  packednormal.x *= packednormal.w;

  fixed3 normal;
  normal.xy = packednormal.xy * 2 - 1;
  normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
  return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
  #if defined(UNITY_NO_DXT5nm)
    return packednormal.xyz * 2 - 1;
  #else
    return UnpackNormalmapRGorAG(packednormal);
  #endif
}

还记得之前说要把美术给的法线贴图在unity中设置好texture type么?设置了那张贴图就会以DXT5nm的压缩格式存储,这样就可以调用这个内置方法来得到正确的normal了,当然了不设也行,从源代码来看unity帮你做了这个公式rgb = (normal+1)/2的事情,也可以自己去做。不过设置了texture type在跨平台的时候我们就不用考虑其他事情,无脑调用方法就好,这个还是比较方便的,所以强烈建议要设置!

最后,一个应用了法线贴图的cube就这样诞生了!项目地址

截图.png

2020.05.11更新
上文中我把计算都放到了tangent space底下做,但我们也能把法线转到world space底下进行计算,所以这次更新来讲讲如何把法线从tangent space转到world space。
PS. 这么做会有一些性能上的损失,首先我们需要在片元着色器中获取法线,然后逐片元的进行坐标转换,而如果把计算都放在tangent space的话,我们可以在顶点着色器中把光源方向、视线方向都转换到tangent space,然后在片元着色器中进行颜色计算,这是个逐顶点的过程。我们都认同一件事,片元比顶点多,那么逐片元会比逐顶点性能开销大,所以说要不要这么做,还是要由开发者自己来判断了。

首先我们需要一个矩阵,这个矩阵可以把tangent space中的物体转换到world space,如何构建这样一个矩阵呢?我们先要确定x,y,z三根坐标轴的方向,这里需要tangent space底下x,y,z轴在world space底下的表达,是这样的

float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); //z轴方向
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); //x轴方向
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; //y轴方向

这一步不太清楚的小伙伴可以去看冯乐乐的《Unity Shader入门精要》的4.6.2小节,明确一下数学概念。确定好三根轴以后那么矩阵就可以构建出来了,注意这里是按列排序的矩阵,最后一列存放worldPos这个变量,不浪费空间。

o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

在vertex shader中做好了这些操作后,接下来我们去fragment shader中进行使用。

float3 normal = UnpackNormal(tex2D(_NormalTex, i.texNormal)); //取出贴图中的法线,此时法线在tangent space中
normal.xy *= _NormalScale; //缩放法线
normal.z = 1 - saturate(sqrt(dot(normal.xy,normal.xy))); //计算缩放后的z
float3 worldNormal = normalize(float3(dot(i.TtoW0.xyz, normal), dot(i.TtoW1.xyz, normal), dot(i.TtoW2.xyz, normal))); //将法线从tangent space转到world space

接下来,就是大家表演的时间了。有了world space下的法线,无论做Blinn Phong还是Lambert,或者PBR等等,都是可以的。

至于法线贴图中的法线到底放在tangent space底下好,还是local space、或者world space(可以这么做但比较少见)好,仁者见仁智者见智。

参考
Unity Shader - 表面凹凸技术汇总 https://www.jianshu.com/p/fea6c9fc610f

【Unity Shaders】法线纹理(Normal Mapping)的实现细节https://blog.csdn.net/candycat1992/article/details/41605257

【光能蜗牛的图形学之旅】Unity切线空间问题和推理思考https://www.jianshu.com/p/af800402f5db

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容