请选择 进入手机版 | 继续访问电脑版

查看: 1488|回复: 4

[原创] Unity 着色器训练营(2) - MVP转换和法线贴图

[复制链接]

1040

主题

1724

帖子

2万

贡献

管理员

Rank: 9Rank: 9Rank: 9

积分
22405
QQ
发表于 2018-2-8 03:39:31 | 显示全部楼层 |阅读模式
广受开发者欢迎的Unity着色器训练营已经开展三期了。今天 本篇文章将由Unity技术经理帮助大家温习第二期训练营的内容:MVP转换和法线贴图。

MVP转换

图01.png
图01


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

图02.png
图02


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

图03.png
图03


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

图04.png
图04


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

图05.png
图05


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

图06.png
图06


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

图07.png
图07


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

图08.png
图08


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

图09.png
图09


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

图10.png
图10


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

法线贴图

图11.png
图11

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

   图12.png
图12


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

那么到底什么是法线呢?

   图13.png
图13


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

   图14.png
图14


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

   图15.gif
图15

   图16.gif
图16


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

   图17.png
图17


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

   图18.png
图18


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

   图19.png
图19


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

   图20.png
图20


这里的设置主要是用于定义是否以及如何计算法线,这个选项对优化游戏大小很有用。
Import(导入):这项是默认选项,就是从原文件中导出法线值。
Calculate(计算):基于Smoothing angle(平滑角度)计算法线值。
None(无):禁用法线。

    图21.png
图21


Normals Mode(法线模式),即Unity如何计算法线,当Normal设置为Calculate才有效。
Unweighted Legacy(传统未加权):主要是针对从2017.1版本之前的版本迁移过来的项目,迁移过来之后这个就是默认选项。计算的结果与现有加权方式略有不同。
Unweighted(未加权):2017.1及以上版本的未加权方式计算。
Area Weighted(区域加权):根据表面的区域加权计算。
Angle Weighted(顶点角加权):根据每个表面上顶点的角度加权计算。
Area and Angle Weighted(区域与顶点角加权):综合区域与顶点角的加权,这个是新项目的默认选项。

图22.png
图22


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

图23.png
图23


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

图24.png
图24


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

这里我先以最简单的法线相关的Shader来呈现效果:
[C#] 纯文本查看 复制代码
Shader "Custom/SimpleNormals"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float3 normal = normalize(i.normal);
                float3 color = (normal + 1) * 0.5;
                return fixed4(color.rgb, 0);
            }
            
            ENDCG
        }
    }
}


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

图25.png
图25


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

图26.png
图26


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

图27.png
图27


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

图28.png
图28

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

图29.png
图29

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

   图30.png
图30


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

    图31.png
图31


到了最背光面法线与反射光的点乘,结果就是为-1,但是-1这个数值在用于颜色计算没有意义。这时候就需要使用saturate(饱和)方法。saturate是CG语言的函数,功能是返回不小于标量或每个向量分量的最小整数。其参考实现如下:
[C#] 纯文本查看 复制代码
           
    float saturate(float x)
    {
    	return max(0, min(1, x));
    }

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

既然有了这些,我们可以写个将光照与法线进行简单运算的着色器:
[C#] 纯文本查看 复制代码
Shader "Custom/SimpleLightingObjectSpace"
{
    SubShader
    {
        Pass
        {
            Tags{ "LightMode" = "ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return saturate(dot(i.normal, _WorldSpaceLightPos0));
            }
            ENDCG
        }
    }
}

接着查看其运行效果。

    图32.png
图32


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

现在只需改写一句便可:
o.normal = UnityObjectToWorldNormal(v.normal);

    图33.png
图33


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

    图34.png
图34

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

    图35.png
图35


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

   图36.png
图36


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

    图37.png
图37


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

    图38.png
图38


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

    图39.png
图39

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

这样我们就可以得到正确的代码实现SimpleNormalMappedLighting.shader:
[C#] 纯文本查看 复制代码
Shader "Custom/SimpleNormalMappedLighting"
{
    Properties
    {
        _NormalTex("Normal Map", 2D) = "white"
    }

    SubShader
    {
        Pass
        {
            Tags{ "LightMode" = "ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 tbn[3] : TEXCOORD1; // TEXCOORD2; TEXCOORD3;
            };

            sampler2D _NormalTex;
            float4 _NormalTex_ST;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _NormalTex);

                float3 normal = UnityObjectToWorldNormal(v.normal);
                float3 tangent = UnityObjectToWorldNormal(v.tangent);
                float3 bitangent = cross(tangent, normal);

                o.tbn[0] = tangent;
                o.tbn[1] = bitangent;
                o.tbn[2] = normal;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float3 tangentNormal = tex2D(_NormalTex, i.uv) * 2 - 1;
                float3 surfaceNormal = i.tbn[2];
                float3 worldNormal = float3(i.tbn[0] * tangentNormal.r + i.tbn[1] * tangentNormal.g + i.tbn[2] * tangentNormal.b);

                return dot(worldNormal, _WorldSpaceLightPos0);
            }
            ENDCG
        }
    }
}


   图40.png
图40


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

0

主题

20

帖子

215

贡献

初级UU族—3级

Rank: 3Rank: 3

积分
215
发表于 2018-2-11 02:29:56 | 显示全部楼层
哇,感谢,学习了

0

主题

3

帖子

55

贡献

初级UU族—2级

Rank: 2

积分
55
发表于 2018-2-11 03:56:03 | 显示全部楼层
非常感谢!讲的不错!

0

主题

5

帖子

40

贡献

初级UU族—1级

Rank: 1

积分
40
发表于 2018-2-28 01:42:56 | 显示全部楼层
非常感谢

1

主题

40

帖子

385

贡献

初级UU族—3级

Rank: 3Rank: 3

积分
385
发表于 昨天 13:36 | 显示全部楼层
占楼也收藏
个人作品(国外):https://jianpingwang.artstation.com
个人GitHub:https://github.com/MasterWangdaoyo
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表