Skip to main content
 首页 » 资源教程 » Unity3D教程

C#渲染教程4--第一束光(上)

2016年10月11日 19:19:405210蛮牛网

内容概要:

世界空间坐标物体的法线变换。

平行光下的工作情况。

计算漫反射和镜面反射。

执行能量转换。

使用金属工作流。

利用的PBS算法。

这是我们渲染教程的第四部分。前一部分我们介绍了混合贴图的相关内容。这次我们来看看如何计算光照。

本教材使用Unity 5.4.0,具体编译版本为5.4.0b17。

这个Shaders与之前的教程不兼容?

我已经修改之前教程中的一些内确保兼容性。我已经提前在第二章中介绍了Shaders的结构。

C#渲染教程4--第一束光(上) Unity3D教程 第1张

是时候为物体加上光照了

1 法线(Normals)

我们之所以能够看见东西,是因为我们的眼睛能够接收电磁辐射。光量子是光的个体。我们把我们能够看到的光谱的那部分称之为可见光。其余的部分我们是看不到的。

整个光谱包含了什么?

我们把光谱分割为不同的光谱带,他们由低到高依次为无线电波,微波,红外线,可见光,紫外线,x射线和伽马射线。

光源发出光线。一些光击中物体,其中的一部分被物体反弹。如果这些被反弹的光线最终再次击中我们的眼睛或者相机的透镜,我们就能看见它们。

要让着一切正常工作,我们必须要知道物体的表面情况。我们已经知道了位置,但是不知道方向。因此,我们需要表面的法向量。

1.1 使用网格法线

复制我们的第一个shader,修改为我们的第一个光照shader。使用shader新建一个材质并把它应用在场景中不同的立方体和球中。调整它们的大小,角度让它们各不相同,在场景中摆好。

C#渲染教程4--第一束光(上) Unity3D教程 第2张

一些立方体和球

Unity的cube和sphere的mash中包含了顶点法线。我们可以直接把它们传入fragment shader中。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
struct VertexData {
        float4 position : POSITION;
        float3 normal : NORMAL;
        float2 uv : TEXCOORD0;
};
struct Interpolators {
        float4 position : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : TEXCOORD1;
};
Interpolators MyVertexProgram (VertexData v) {
        Interpolators i;
        i.uv = TRANSFORM_TEX(v.uv, _MainTex);
        i.position = mul(UNITY_MATRIX_MVP, v.position);
        i.normal = v.normal;
        return i;
}

现在可以让我们的法线在shader中可视化。

[C#] 纯文本查看 复制代码

?

1
2
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        return float4(i.normal * 0.5 + 0.5, 1);
}

C#渲染教程4--第一束光(上) Unity3D教程 第3张

颜色法线向量

这些原始的法线直接来自网格。Cube是的每一面都是平的,因为每个面都是由4个顶点构成的单独平面。法线的所有顶点都在一个方向上。然而在球面中,顶点法线都不在一个平面上,所以表现出平滑的插值。

1.1 动态批处理

这里的立方体法线看起来有些奇怪。通常情况下我们希望立方体显示的是同一种颜色,但是这并没有太大的关系。更奇怪的是这些立方体可以改变颜色,当我们用不同多角度看的时候。

C#渲染教程4--第一束光(上) Unity3D教程 第4张

可变颜色的立方体

这是由动态批处理引起的。Unity动态的将小的网格合并在一起,这样可以减少draw calls的消耗。球面的网格比较大,所以对他们没有影响,但是立方体的表面是平的。

合并网格时,我们要将本地坐标转换为世界坐标。不论对象是如何成批处理的,关键在于他们是如何排序进行渲染的。正是因为这种转化影响到了法线的显示,让我们看到了不一样的颜色。

要解决这个问题,我们可以在Player Setting里面修改Dynamic Batching选项。

C#渲染教程4--第一束光(上) Unity3D教程 第5张

Batching 设置

除了动态批处理(Dynamic batching)以外,Unity还支持静态批处理(Static Batching)。这不同于静态几何学,但是涉及到转换世界坐标。它发生在创建的时候。

C#渲染教程4--第一束光(上) Unity3D教程 第6张

发现,没有Dynamic Batching

然而你需要意识到的是dynamic batching并没有什么问题。实际上我们需要对我们的法线做同样的事情。所以你可以让它处于enable状态。

1.1 世界坐标下的法线

除了动态批处理物件,我们所有的法线都处于物件空间(object space)。但是我们需要知道表面位于世界坐标的原始方向。所以我们需要将法线从物件控件转换到世界坐标空间。我们需要对这个对象进行矩阵转换。

Unity将整个转换层次结构放入了一个单一矩阵,就像我们第一章中做的一样。我们可以写成O=T1T2T3这样…这里的T代表个体变换,O代表组合变换。矩阵就知道从物件到世界坐标转换了。

Unity在Shader中使用类型为float4x4名为unity_objectToWolrld的变量,这个变量定义在UnityShaderVariables中。让法线乘上这个变量就可以转好到世界坐标。因为代表着方向,重定位需要忽略。第四个齐次坐标必须为0.

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
Interpolators MyVertexProgram (VertexData v) {
        Interpolators i;
        i.position = mul(UNITY_MATRIX_MVP, v.position);
        i.normal = mul(unity_ObjectToWorld, float4(v.normal, 0));
        i.uv = TRANSFORM_TEX(v.uv, _MainTex);
        return i;
}

另外,我们可以只乘以3和3的部分。编译器最终会编译相同的结果,因为编译器会消除乘法中所有乘以0的数。

[C#] 纯文本查看 复制代码

?


i.normal = mul((float3x3)unity_ObjectToWorld, v.normal);

C#渲染教程4--第一束光(上) Unity3D教程 第7张

现在发现已经转换为世界坐标了,但是有些部分看起来比其他的明亮。这是因为缩放的关系,所有我们需要将转换结果规范化。

[C#] 纯文本查看 复制代码

?

1
i.normal = mul(unity_ObjectToWorld, float4(v.normal, 0));
i.normal = normalize(i.normal);

C#渲染教程4--第一束光(上) Unity3D教程 第8张

规范化后的法线

然后我们必须规范化向量,他们看起来有写奇怪,因为没有进行统一的缩放。当表面只在一个维度上进行伸缩的时候,法线没有根据相同的方式进行伸缩。

C#渲染教程4--第一束光(上) Unity3D教程 第9张

X轴缩放,顶点和法线同时用1/2规范化

当不是进行统一缩放的时候,必须进行反向规范化。这种方式使得法线能匹配变形后的表面,之后再规范化回来。和统一缩放并不会产生不同。

C#渲染教程4--第一束光(上) Unity3D教程 第10张

X轴缩放,顶点缩放1/2并且法线缩放2

所以我们需要翻转缩放比例,但是角度依然有问题,我们要如何处理呢?

我们将物体的旋转矩阵表示为O=T1T2T3。。。但是我们可以更详细些。我们知道没一步都是由缩放,旋转和位移构成的。所以每一个T都可以分解为SRP。

这意味着O=S1R1P1S2R2P2S3R3P3。。。但是为了让他看起来简洁,我们表示为O=S1R1P1S2R2P2。

因为发现是方向向量,我们并不关心重定位。所以可以表示为O=S1R1S2R2,这就可以转换为3*3矩阵。

我们需要翻转缩放,但是方向不变,所以我们得到的新矩阵为

N=S−11R1S−12R2.

翻转矩阵(求逆)是如何工作的?

矩阵M的逆矩阵写作M-1,当我们需要取消一个矩阵的操作的时候可以乘上他的你矩阵。所以MM-1=M-1M=I。

取消一系列的步骤,需要采取逆矩阵相反的步骤。用助记符表示为(AB)-1=B-1A-1.

举个简单的X的例子,它的倒数为1/X,因为X/X=1。显而易见0没有倒数。同样并不是每个矩阵都有逆矩阵。

当我们在缩放,旋转,重定位矩阵的时候,只要我们不进行0倍缩放,所有的矩阵都可逆。

取反重定位矩阵只需要将XYZ在四行里取反即可

C#渲染教程4--第一束光(上) Unity3D教程 第11张

缩放矩阵取反需要将对角线取反。

C#渲染教程4--第一束光(上) Unity3D教程 第12张

旋转矩阵可以看做每次绕一个轴旋转一次,例如绕Z轴旋转。点A绕Z轴旋转的撤销操作可以看做旋转-Z。如果你学习过正弦和余弦波形,你会注意到sin(-z)=-sinz和cos(-z)=cos(z)。这样求逆矩阵就容易多了。

C#渲染教程4--第一束光(上) Unity3D教程 第13张

注意到旋转的逆矩阵与原矩阵相比沿着对角线镜像,只有正弦sin发生了变化。

相对于物品坐标转世界坐标,Unity同样支持世界坐标转物品坐标。他们是互逆的矩阵。所以表示为O-1=R2-1S2-1R1-1S1-1.

我们得到了缩放的逆矩阵,与此同时也让我们的矩阵有的逆旋转。幸运的是我们可以通过矩阵变换消除这些影响。然后我们得到了(O-1)T=N。

什么是置换矩阵?

矩阵M的置换矩阵用MT表示。置换矩阵根据轴镜像旋转,行变列,列变行,轴不变。

C#渲染教程4--第一束光(上) Unity3D教程 第14张

和逆矩阵一样,置换矩阵满足乘法交换律。

(AB)T=BTAT。感觉在处理处理对称矩阵时候是可行的,否则可能会无法相乘乘法。但这却是真的,你可以找到很多证据。

当然置换两次能够回到原来的矩阵(MT)T=M。

为什么置换后的矩阵是正确的?

首先,R-1=RT,正如上面所看到的。

这导致O-1=R2-1S2-1R1-1S1-1=RT2S2-1RT1S1-1

现在转换(O-1)T=(S1-1)T(RT1)T(S12)T(RT2)T=(S1-1)TR1(S2-1)TR2

接下来,我们注意到ST=S,因为除了主对角线以外处处为0.

所以(O-1)T = S1-1R1S2-1R2=N.

现在让我们将世界坐标转换为物品坐标,让顶点法线正常。

[C#] 纯文本查看 复制代码

?

1
2
3
4
i.normal = mul(
                                        transpose((float3x3)unity_WorldToObject),
                v.normal
        );
        i.normal = normalize(i.normal);

C#渲染教程4--第一束光(上) Unity3D教程 第15张

正确的世界坐标法线

实际上,UnityCG包含该了硬编码方法 UnityObjectToWorldNormal方法。所以我们可以用这个方法。他是乘法,而不是转置,编码后能有更好的结果。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
Interpolators MyVertexProgram (VertexData v) {
        Interpolators i;
        i.position = mul(UNITY_MATRIX_MVP, v.position);
        i.normal = UnityObjectToWorldNormal(v.normal);
        i.uv = TRANSFORM_TEX(v.uv, _MainTex);
        return i;
        }

UnityObjectToWOldNormal方法是什么样的?

这里的inline关键字不错任何事情,只是提示。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
7
8
9
// Transforms normal from object to world space
inline float3 UnityObjectToWorldNormal( in float3 norm ) {
// Multiply by transposed inverse matrix,
// actually using transpose() generates badly optimized code
return normalize(
        unity_WorldToObject[0].xyz * norm.x +
        unity_WorldToObject[1].xyz * norm.y +
        unity_WorldToObject[2].xyz * norm.z
        );
}

1.1 重整

当我们在顶点程序里得到正确的法线之后,将他们传输给插值器。不幸的是,线性插值器在处理不同长度的向量有不同的结果。不使其变得更短。

所以我们要在fragment shader中重新规整化向量。

[C#] 纯文本查看 复制代码

?

1
2
3
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        i.normal = normalize(i.normal);
        return float4(i.normal * 0.5 + 0.5, 1);
        }

C#渲染教程4--第一束光(上) Unity3D教程 第16张

重新规整化后法线

虽然这能有更好的效果,但是通常情况下这里的错误是非常少的。如果性能需要,我们可以不在fragment shader中重新规整化。这是移动设备中常见的优化方法。

C#渲染教程4--第一束光(上) Unity3D教程 第17张

夸张的错误

1 漫反射阴影

我们能开到不自身不发光的物体,是因为他们反射光线。反射光线有不同的方式。我们首先来看一下慢反射。

漫反射的发生是因为射线并不是直接从表面反弹回来。取而代之的是穿入表面,反射基础,分开一段时间,直到穿出表面。实际上,光子和原子直接的相互作用更为复杂,但是我们并不需要真实世界里的这么多细节。

有多少光线发射大部分取决于光线入射的角度。直接0度角照射能反射最多的光线。随着光线的入射角度的增大,反射光线递减,直到九十度,不再有光线被反射为止,看起来就是黑暗的。漫反射的光线与光线方向和表面法线的余弦值呈正相关。这就是我们所知的朗伯余弦定律(余弦辐射体)。

C#渲染教程4--第一束光(上) Unity3D教程 第18张

我们可以通过确定的朗伯反射率因子计算表面法线的点积和光线的方向。我们已经知道法线,但没有光的方向。让我们先用一个从上面照射的固定方向光开始。

[C#] 纯文本查看 复制代码

?

1
2
3
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        i.normal = normalize(i.normal);
        return dot(float3(0, 1, 0), i.normal);
        }

C#渲染教程4--第一束光(上) Unity3D教程 第19张

C#渲染教程4--第一束光(上) Unity3D教程 第20张

顶光源,在伽马空间和线性空间的情况

什么是点积?

点积在几何学中对两个向量进行如下定义:A·B=|A||B|cosΘ。这表示了两个夹角为余弦的向量,乘以他们的长度。那么两个单位向量表示为 A·B=cosΘ。点积的通项公式为A·B==A1B1 + A2B2+…+AnBn。这意味着你可以计算每一部分的值。

[C#] 纯文本查看 复制代码

?


float dotProduct = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;

如果把它画出来,就是沿着一边画垂线。像影子投射下来一样(投影)。这样我们就得到了一个底边为直角的三角形。底边的长度就是点积的值。如果两个向量都是单位向量,那么它的值就是夹角的余弦值。

1.1 固定照明

计算点积的时候制作向着光源的时候起作用,而在远离光源的时候无效。在这种情况下,表面逻辑上拥有阴影并且不接受到光线。当光线与法线夹角大于90°时,余弦值为负数,我们不需要一个负的光线,所以在这里需要一个clamp的操作。我们可以使用标注的max方法来做这件事情。

[C#] 纯文本查看 复制代码

?


return max(0, dot(float3(0, 1, 0), i.normal));

除了max方法,我们还在shader中经常看见saturate,这个标准方法clamps的值为0和1之间。

[C#] 纯文本查看 复制代码

?


return saturate(dot(float3(0, 1, 0), i.normal));

这通常情况下是非必须的,我们知道点积并不产生结果大于1的数。然而,一些情况下在依赖硬件的情况下,这样做会更加的高效。但是我们并不需要过多的关系这些细节。实际上,我们只要把他委托给Unity就可以了。

UnityStandardBRDF文件包含该了转换方法DotClamped。这个方法保证了点积不会为负数。这正是我们所需要的。实际上它还包含了其他处理光线的方法,我们将来也会用到。现在我们就来使用它。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
7
8
#include "UnityCG.cginc"
                        #include "UnityStandardBRDF.cginc"
                        
                        float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
                                i.normal = normalize(i.normal);
                                return DotClamped(float3(0, 1, 0), i.normal);
                        }

DotClamp方法的具体内容?

实际上,他决定了如何更好的使用saturate,他是针对低性能的shader硬件,或者PS3。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
inline half DotClamped (half3 a, half3 b) {
        #if (SHADER_TARGET < 30 || defined(SHADER_API_PS3))
                return saturate(dot(a, b));
        #else
                return max(0.0h, dot(a, b));
        #endif
}

这个shader半精度数值,但是我们不需要担心数字精度。这只对移动设备有影响。

因为UnityStandardBRDF包含了UnityCG等一系列文件,所以我们不必继续在头文中包含它们。虽然这没有错,但是为了保持简介,我们还是删掉它们。

C#渲染教程4--第一束光(上) Unity3D教程 第21张

头文件包含该关系

1.1 光源

现对于因编码,我们在场景中需要使用平行代替。默认情况,每一个Unity场景都有一盏灯表示太阳。它是一个平行光,这就意味着距离我们无限远。其结果就是,所以的光线都来自一个方向。当然这并不是真实生活中的情况,但是太阳也离我们很远,可以近似处理。

C#渲染教程4--第一束光(上) Unity3D教程 第22张

默认场景光,将其移除

UnityShaderVariables定义了float4 类型的 _WorldSpaceLightPos0变量,它包含了当前光的坐标。或者平行光的方向。它由4个部分组成,因为是齐次坐标,所以对于我们的平行光来说第四个值为0.

[C#] 纯文本查看 复制代码

?

1
float3 lightDir = _WorldSpaceLightPos0.xyz;
        return DotClamped(lightDir, i.normal);

1.1 光照模式

在计算出正确结果之前,我们需要告诉Unity那个光照数据是我们所需要的。所以我们需要在Shader中添加LightMode Tag。光照模式需要依赖我们场景中的渲染模式。我们可以使用forward或者deferred渲染路径。它们是两个比较老的渲染模式,但是我们不需要为此操心。你可以在Player rendering setting里设置它们。它们在色值空间设置的上面。这里我们使用默认的Forward模式。

C#渲染教程4--第一束光(上) Unity3D教程 第23张

渲染路径选择

我们使用ForwardBase通道。这是forward rendering路径里渲染某物体的第一个通道。这里允许我们访问场景中的平行光。它设置了一下其他的东西,我们稍后来处理。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
7
Pass {
        Tags {
                "LightMode" "ForwardBase"
        }
        CGPROGRAM
        
        ENDCG
}

C#渲染教程4--第一束光(上) Unity3D教程 第24张

漫反射光

1.1 光照颜色

当然,并不是所有的光都是白色的。每一个光源都有它们自己的颜色,我们可以通过fixed4类型的_LightColor0变量访问,它第一种UnityLightingCommon中。

什么是fixed4类型?

这是一个低精度的数,为了换取移动设备的高性能。在桌面环境下,fixed只是float的别名。性能优化又是另外一个主题。

这个变量包含了光的颜色,乘以光强。当然它包含了4个通道,但是我们只需要RGB部分。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        i.normal = normalize(i.normal);
        float3 lightDir = _WorldSpaceLightPos0.xyz;
        float3 lightColor = _LightColor0.rgb;
        float3 diffuse = lightColor * DotClamped(lightDir, i.normal);
                return float4(diffuse, 1);}

C#渲染教程4--第一束光(上) Unity3D教程 第25张

有颜色的光

1.1 反射率

许多材质会吸收电磁波。这赋予了它们颜色。例如,如果一种材料吸收了所有的红色波段频率,它就会显现出青色。

光线无法逃离的时候发生了什么?

光能被物体吸收了,通常情况下是热量。这也是为什么黑色的无比相比于白色物体更容易吸热一样。

我们用颜色的扩散反射率来定义材质的反射率。反射率在拉丁语里是白色程度的意思。所以它决定了红,绿,蓝通道的反射率。其余的部分被吸收。我们可以用材质和色彩来定义它。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
7
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        i.normal = normalize(i.normal);
        float3 lightDir = _WorldSpaceLightPos0.xyz;
        float3 lightColor = _LightColor0.rgb;
        float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
        float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
        return float4(diffuse, 1);
        }

我们将检视面板中的main texture标签改为Albedo。

C#渲染教程4--第一束光(上) Unity3D教程 第26张

C#渲染教程4--第一束光(上) Unity3D教程 第27张

C#渲染教程4--第一束光(上) Unity3D教程 第28张

评论列表暂无评论
发表评论