当前位置:   article > 正文

Unity 着色器训练营(2) - MVP转换和法线贴图_红色 法线贴图 unity

红色 法线贴图 unity

https://mp.weixin.qq.com/s/Qf4qT15s9bWjbVGh7H32lw

我们刚刚公布了Unity 2018.1中,Unity将会内置可视化编程工具Shader Graph,很多开发者留言给小编是否以后构建着色器编码就可以下岗了。实际上Unity希望开发能够越来越大众化,减轻技术上的壁垒,Shader Graph虽然能够更加轻松的构建着色器,但实际上开发者都应该掌握着色器的开发。

 

广受开发者欢迎的Unity着色器训练营已经开展三期了。本篇文章将由Unity技术经理鲍健运帮助大家温习第二期训练营的内容:MVP转换和法线贴图。

 

MVP转换

MVP转换全称:Model * View * Projection Matrix 模型视图投影矩阵转换。在图形学领域,这个是非常重要的内容。利用模型、观察和投影矩阵,可以将变换过程清晰地分解为三个阶段。虽然此法并非必需,但采用此法较为稳妥。我们将看到这种公认的方法对变换流程作了清晰的划分。

 

  

首先,在三维空间中,我们看到一个简单对象。这个对象假设为一个立方体,而通常的三维模型也是由一组顶点定义。就像下图中空间顶点的一些坐标。

 

 

顶点的XYZ坐标是相对于物体中心定义的。换而言之,若某顶点位于(0,0,0),则其位于物体的中心。而现在的坐标系,其实主要就是模型空间(Model Space)。

 

 

我们希望能够移动它,玩家有时也需要通过键盘鼠标控制这个模型。而这些操作无外乎,缩放旋转平移。在每一帧中,用算出的这个矩阵去乘(所有的顶点,物体就会移动。唯一不动的是世界空间(World Space)的中心。物体所有顶点都位于世界空间。图中黄色箭头的意思是:从模型空间(Model Space)(顶点都相对于模型的中心定义)变换到世界空间(顶点都相对于世界空间中心定义)。

 

  

仔细想想,摄像机的原理也是相通的。如果想换个角度观察一座山,你可以移动摄像机,当然也可以移动山……但是,后者在实际中不可行,在计算机图形学中却十分方便。起初摄像机位于世界坐标系的原点。移动世界只需乘一个矩阵。假如你想把摄像机向右(X轴正方向)移动3个单位,这和把整个世界(包括网格)向左(X轴负方向)移3个单位是等效的!

 

 

下图便展示了从世界空间(顶点都相对于世界空间中心定义)到摄像机空间(Camera Space,顶点都相对于摄像机定义)的变换。

 

 

现在,我们处于摄像机空间中。从下图中可以发现,摄像机所观察到的锥形空间(上图中以近似圆锥体的方式呈现),通过诸如近剪裁、远剪裁、视野这些重要的参数,剪裁平面是摄像机空间中的XY坐标系,摄像机空间中的“远”与“近”,即反映了Z的取值。

 

  

从摄像机空间(顶点都相对于摄像机定义)到齐次坐空间(Homogeneous Space)(顶点都在一个小立方体中定义。立方体内的物体都会在屏幕上显示)的变换。摄像机空间给定的两个端点(1,1),(-1,-1)就是屏幕投影空间的两端坐标。

 

 

再看下面的图,以便大家更好地理解投影变换。投影前,蓝色物体都位于摄像机空间中,红色的东西是摄像机的平截头体(frustum):这是摄像机实际能看见的区域。

 

 

用投影矩阵去乘前面的结果,得到如下效果:下图中平截头体变成了一个正方体(每条棱的范围都是-1到1),所有的蓝色物体都经过了相同的变形。因此离摄像机近的物体就显得大一些,远的显得小一些。这和现实生活一样!这样就完成了MVP转换。

 

法线贴图

还记得下图中的场景吗?第一期的训练营我们有具体的展示。第一个机器蜘蛛是使用顶点/片元着色器,仅以简单的贴图做显示,所以感觉很平面。第二个机器蜘蛛加入了光照通道来辅助运算,所以有光照影响的效果。前两个材质没有法线贴图,最后个standard有法线贴图,可以看到很多的细节,明显的凹凸感。这些凹凸感就是通过法线贴图(Normal Mapping)来实现的。

 

 

下图呈现了两张图片,左边的就是常规的贴图,展现了机器蜘蛛的纹理,而右边的图片颜色奇怪的图片就是法线贴图,实际上大家可以发现,它与左边的问题其实是吻合的,并且有明显的凹凸质感,在实际附着在材质上显示时,物体的表面也会由观察的角度和光照的关系产生凹凸的感觉。

 

那么到底什么是法线呢?

 

 

下图大家可能会想起一些学校里学习的数学概念。图中绿色的线垂直于AB两点的连线,而这条绿色的线就是法线。

 

 

但是在现实空间中,法线是有长有短的。为了便于之后对于颜色值的处理(值域0~1),需要对其进行归一化(Normalize)的处理(值域-1~1),将数值从绝对的量变为相对的量。这些具体的实现会在下文中代码部分解释。

 

 

法线的应用在现实世界中的表现有很多,就以下图二个图对比为例,法线就影响了AB线上的弹性值,给小球不同的反弹效果。但是,更为广泛的应用场合是在光照相关的场景。

 

 

如何计算法线呢?在欧几里得空间中,三点可以确定一个平面。我们就试着计算A、B、C这三个点所在平面的法线。连接AB点与AC点,构成二条线段。

 

 

在向量计算中,一般使用叉乘的方式来获得与两个向量都垂直的向量,在这里就是获得法线向量。在Unity中我们所遵循的是“左手定则”,正如所展示的,通过AB × AC可以得到蓝色的法线向量;通过AC × AB可以得到红色的法线向量。从归一化到计算方式,现在基本梳理了法线的原理。

 

 

一般外部导入的模型,本身就包含了法线与切线的一些相关设置。下图红框所确定的区域就是对应的设置选项。

 

 

这里的设置主要是用于定义是否以及如何计算法线,这个选项对优化游戏大小很有用。

  • Import(导入):这项是默认选项,就是从原文件中导出法线值。

  • Calculate(计算):基于Smoothing angle(平滑角度)计算法线值。

  • None(无):禁用法线。

 

 

  • Normals Mode(法线模式),即Unity如何计算法线,当Normal设置为Calculate才有效。

  • Unweighted Legacy(传统未加权):主要是针对从2017.1版本之前的版本迁移过来的项目,迁移过来之后这个就是默认选项。计算的结果与现有加权方式略有不同。

  • Unweighted(未加权):Unity 2017.1及以上版本的未加权方式计算。

  • Area Weighted(区域加权):根据表面的区域加权计算。

  • Angle Weighted(顶点角加权):根据每个表面上顶点的角度加权计算。

  • Area and Angle Weighted(区域与顶点角加权):综合区域与顶点角的加权,这个是新项目的默认选项。

     

 

那么为什么法线贴图能呈现出各种凹凸效果呢?现在我们先从下图网格与法线的关系展开。首先这是常规的网格,每个面上的法线值是一样的,因此在光照下这网格上所呈现的凹凸感与实际面的形状是一致的。

 

 

但是如果面上的各个法线呈圆周相关的值变化,这样在光照下就能呈现出平滑弧面的质感。

 

 

推而广之,这里还能使用更为丰富的法线值来表现凹凸感十足的效果,而这些在网格上的法线数值,平铺到法线贴图上就会表现为不同的颜色效果。

 

这里我先以最简单的法线相关的Shader来呈现效果:

  1. Shader "Custom/SimpleNormals"
  2. {
  3. SubShader
  4. {
  5. Pass
  6. {
  7. CGPROGRAM
  8. #pragma vertex vert
  9. #pragma fragment frag
  10. #include "UnityCG.cginc"
  11. struct appdata
  12. {
  13. float4 vertex : POSITION;
  14. float3 normal : NORMAL;
  15. };
  16. struct v2f
  17. {
  18. float4 vertex : SV_POSITION;
  19. float3 normal : NORMAL;
  20. };
  21. v2f vert (appdata v)
  22. {
  23. v2f o;
  24. o.vertex = UnityObjectToClipPos(v.vertex);
  25. o.normal = v.normal;
  26. return o;
  27. }
  28. fixed4 frag (v2f i) : SV_Target
  29. {
  30. float3 normal = normalize(i.normal);
  31. float3 color = (normal + 1) * 0.5;
  32. return fixed4(color.rgb, 0);
  33. }
  34. ENDCG
  35. }
  36. }
  37. }

这里值得注意的是,v2f在顶点函数部分获取了材质的法线值,对于这个法线值在片元函数中先进行了归一化处理,使其值域在-1到1之间。但是该处理后的数值还不便于进行直观的表现,因此通过加1在乘0.5的方式将值域变换到0到1之间,最后返回fixed4(color.rgb,0),这样法线值就能以颜色值rgb的方式呈现出来。

 

 

正如上图所呈现法线颜色均匀表现。二个不同多边形的球体,但是着色器是一样的。

 

 

除了叉乘之外,另外一个重要概念就是点乘,它的结果直接反应了两个向量直接的关系。常见的有三种,二向量同向,点乘值为1;二向量相垂直,点乘值为0;二向量互为反向,点乘值为-1。光对于物体照射后的效果,如何做出正确的呈现就必须用到点乘的方法。

 

 

举个更为直观的例子。在宇宙空间中,太阳发射出阳光,照射到地球。

 

 

但是观看者实际感受光的效果,是由“地球”给到我们的反射光所决定的。通过反射光与物体表面的法线进行一定的处理(主要是点乘),来获得凹凸感的效果。

 

 

比方说这个反射光与法线同向时,即在物体表面最为高亮的角度。法线与光照的点乘值就为1。

 

 

当这两者是相互垂直的时候,它们的点乘就为0,你可以认为该点就是暗的,因为没有有效的光照。

 

 

到了最背光面法线与反射光的点乘,结果就是为-1,但是-1这个数值在用于颜色计算没有意义。这时候就需要使用saturate(饱和)方法。saturate是CG语言的函数,功能是返回不小于标量或每个向量分量的最小整数。其参考实现如下:

  1. float saturate(float x)
  2. {
  3. return max(0, min(1, x));
  4. }

一旦,数值小于0之后,它将返回0。

既然有了这些,我们可以写个将光照与法线进行简单运算的着色器:

  1. Shader "Custom/SimpleLightingObjectSpace"
  2. {
  3. SubShader
  4. {
  5. Pass
  6. {
  7. Tags{ "LightMode" = "ForwardBase" }
  8. CGPROGRAM
  9. #pragma vertex vert
  10. #pragma fragment frag
  11. #include "UnityCG.cginc"
  12. struct appdata
  13. {
  14. float4 vertex : POSITION;
  15. float3 normal : NORMAL;
  16. };
  17. struct v2f
  18. {
  19. float4 vertex : SV_POSITION;
  20. float3 normal : NORMAL;
  21. };
  22. sampler2D _MainTex;
  23. float4 _MainTex_ST;
  24. v2f vert (appdata v)
  25. {
  26. v2f o;
  27. o.vertex = UnityObjectToClipPos(v.vertex);
  28. o.normal = v.normal;
  29. return o;
  30. }
  31. fixed4 frag (v2f i) : SV_Target
  32. {
  33. return saturate(dot(i.normal, _WorldSpaceLightPos0));
  34. }
  35. ENDCG
  36. }
  37. }
  38. }

接着查看其运行效果。

 

 

但是我们应该可以从旋转的圆球上发现,光影的显示有一定的问题。一开始光影还是正确的,但是光影在旋转过程中,好像只是附着在圆球上,不受环境光照的影响,这是为什么呢?因为在vert顶点函数中,“o.normal = v.normal;”这个语句只是赋予了对象本身的法线值,仅仅是模型空间自己的,而且只是第一次获取到光照时的法线值。静态物体没什么问题,但是如果动态的时候,就需要获取其在世界空间中对应法线的数值来进行运算了。世界空间中的法线值可以通过UnityCG.cginc的UnityObjectToWorldNormal方法来处理。

 

现在只需改写一句便可:

o.normal = UnityObjectToWorldNormal(v.normal);

 

 

现在我们再观察使用修正后着色器SimpleLightingObjectSpaceCorrect.shader,显示的圆球就有正常显示效果了。

 

 

上图中左边的是通过法线贴图来辅助显示光照效果的,右边的完全是通过圆球和面片的结合来显示光照效果,而这些对象就直接使用SimpleLightingObjectSpaceCorrect.shader。我们调整场景中光照的角度,对象上的阴影也会随之变化。但是两边阴影是对称的,而不是一致的,这肯定有问题了。因为法线是正确的,那么是哪里不对呢?实际上问题出在另外一个方面:切线。

 

 

这里一个球体作为参照,蓝色的就是某个点的法线。切线在哪里呢?因为在二维空间中,某点的切线一般就1条。但是在三维空间中就不一样了。

 

 

因为在三维空间中,法线相关的是一个切平面,这就比较难去选取所需的切线了。但是我们也有约定俗成的方式去选取所需的切线。

 

 

这边使用一张有数字和规整的区域作为参照,我们通常以纹理的方向找出一条切线(Tangent),而与之相垂直的(基于左手定则)选取另一条副切线,通过这个基准我们就可以得到世界法线值(WorldNormal)。引入副切线主要是便于进行转换的运算。

 

 

理解了相关世界法线的计算之后,现在我们可以来看看法线贴图的应用了。在上图中哪个是正确的显示,哪个是错误的显示呢?实际上左边是错误的,而右边的是正确的。

 

 

为了便于理解我们制作了这个场景,原理上而言还是需要遵守左手定则,原来的UV就对应反了。法线一般用蓝色的表示(RGB中的Blue),这个是朝向屏幕的,因此这里看不到。上边的就是对应绿色的副切线(RGB中的G),右边的就是对应红色的切线(RGB中的R)。

 

这样我们就可以得到正确的代码实现。

  1. SimpleNormalMappedLighting.shader:
  2. Shader "Custom/SimpleNormalMappedLighting"
  3. {
  4. Properties
  5. {
  6. _NormalTex("Normal Map", 2D) = "white"
  7. }
  8. SubShader
  9. {
  10. Pass
  11. {
  12. Tags{ "LightMode" = "ForwardBase" }
  13. CGPROGRAM
  14. #pragma vertex vert
  15. #pragma fragment frag
  16. #include "UnityCG.cginc"
  17. struct appdata
  18. {
  19. float4 vertex : POSITION;
  20. float3 normal : NORMAL;
  21. float4 tangent : TANGENT;
  22. float2 uv : TEXCOORD0;
  23. };
  24. struct v2f
  25. {
  26. float4 vertex : SV_POSITION;
  27. float2 uv : TEXCOORD0;
  28. float3 tbn[3] : TEXCOORD1; // TEXCOORD2; TEXCOORD3;
  29. };
  30. sampler2D _NormalTex;
  31. float4 _NormalTex_ST;
  32. v2f vert(appdata v)
  33. {
  34. v2f o;
  35. o.vertex = UnityObjectToClipPos(v.vertex);
  36. o.uv = TRANSFORM_TEX(v.uv, _NormalTex);
  37. float3 normal = UnityObjectToWorldNormal(v.normal);
  38. float3 tangent = UnityObjectToWorldNormal(v.tangent);
  39. float3 bitangent = cross(tangent, normal);
  40. o.tbn[0] = tangent;
  41. o.tbn[1] = bitangent;
  42. o.tbn[2] = normal;
  43. return o;
  44. }
  45. fixed4 frag(v2f i) : SV_Target
  46. {
  47. float3 tangentNormal = tex2D(_NormalTex, i.uv) * 2 - 1;
  48. float3 surfaceNormal = i.tbn[2];
  49. float3 worldNormal = float3(i.tbn[0] * tangentNormal.r + i.tbn[1] * tangentNormal.g + i.tbn[2] * tangentNormal.b);
  50. return dot(worldNormal, _WorldSpaceLightPos0);
  51. }
  52. ENDCG
  53. }
  54. }
  55. }

 

这些做完之后,我们就能看到最终的显示效果。法线贴图的相关知识点也就梳理至此了。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/116613
推荐阅读
相关标签
  

闽ICP备14008679号