1. Unity中的基本光照

光线是如何产生的,光源发射光,一些物体与光交互,吸收一些光,散射一些光,而摄像机吸收一些光,形成了图像。在光学中,使用辐照度(irradiance)来量化光。

那么如何计算辐照度呢?

辐照度与照射到物体表面光线之间的距离d/cosx成反比。

当光垂直下落cosx=1,那么距离为d

当光与竖直方向呈x度角,距离为d/cosx

其中x可以由表面法线n和光源方向l的点积得到。

1.1 光与物体相交一般两种结果:散射与吸收

  • 散射(scatering)只改变光线方向,不改变光线的密度和颜色。

散射的光线一般有两种方向,一种是向物体内部散射,叫做折射(refraction)或透射(transmission);

另一种是反射(reflection)到外部。

  • 吸收(absorption) 则相反,只改变光线密度和颜色。对于不透明物体向物体内部折射时,一部分被吸收,一部分与内部粒子相交并反射出去

为了区分两种散射,用高光反射(specular)和漫反射(diffuse)分别表示物体如何反射以及计算多少光线折射、吸收(absorption)、反射出表面。

根据光线的入射方向和入射量可以得到出射方向和出射量,称之为出射度(exitance)

辐照与出射成线性关系,比值为漫反射和高光。

着色

根据一系列的信息得到计算出射度的等式,也即光照模型。(lighting model )

标准光照模型
  • 自发光(emissive) 光线直接由光源进入摄像机,直接用材质的自发光颜色。

  • 高光反射(specular)

  • 漫反射(diffuse)

    漫反射光照符合兰伯特定律 (Lambert's law): 反射光线的强度与表面法线和光源方向之间角的余弦值 成正比。因此, 漫反射部分的计算如下

    c light 和 m diffuse 分别表示光源颜色和漫反射颜色

  • 环境光(ambient) 一般是全局变量

1.2 基本光照模型

逐顶点计算漫反射

漫反射光照符合兰伯特定律 (Lambert's law): 反射光线的强度与表面法线和光源方向之间

角的余弦值 。因此, 漫反射部分的计算如下

c_diffuse=( light * m_diffuse) max( n . I )

中, n是表面法线,I 是指向光源的单位矢量 ,m_diffuse 是材质的漫反射颜色 ,light 是光源颜色。

需要注意的是 我们需要防止法线和光源方向点乘的结果为负值,为此 我们使用取最大值的函数来将其截取到 o, 这可以防止物体被从后面来的光源照亮。

以下为具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//顶点着色器代码
v2f vert(a2v v){
v2f o; //用于输出

//将顶点信息从模型空间传递到投影空间
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);

//获取环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

//将法线从模型空间传递到世界空间,
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));

//获取世界空间的光照方向
fixed3 worldLight = normalizze(_WorldSpaceLightPos0.xyz); //只有一个光源且为平行光才有用

//计算漫反射 saturate:饱和,用于将数据截取到【0,1】之间
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));

o.color = ambient + diffuse;

return o;
}

详情见P128.

矩阵变换见P86

要注意,非统一缩放,经计算得知会导致,法线不再垂直,所以不能用M(A-B)矩阵

需要用变换矩阵的逆转置矩阵

结论

1.进行漫反射计算,需要法线,光照,但是需要两者在同一空间下,这里是世界空间,并且法线变换不同于顶点变换

法线变换mv方法

1
2
3
4
//右乘mv逆矩阵
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
//因为左乘逆转置矩阵,
//it为逆转置,其实对于正交来说,等于原来变换矩阵,但不是正交矩阵,会出现上述法线问题

2.一个矩阵可以左乘达成mv变换,那么它的逆矩阵就可以达成vm变换;逆=换方向,转=换位置

逐像素计算漫反射

类似于顶点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Shader "Unity Shaders Books/Chapter6/Pixel_Level"{
Properties{
//初始化Color属性,得到并控制漫反射颜色,设置为白色
_Diffuse("Diffuse",Color)=(1,1,1,1)

}
SubShader{
// Tags { "RenderType"="Opaque" }
//顶点片元着色器代码要写在pass通道中
Pass{
//正确定义lightmode才可以得到unity内置光照变量,该Tag用于定义pass的角色
Tags{"LightMode" = "ForwardBase"}

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//获取properties中的变量
fixed4 _Diffuse;

//定义顶点着色器的输入输出结构体,同时输出也是片元的输入
struct a2v{
float4 vertex : POSITION;
float3 normal :NORMAL; //将模型顶点法线信息保存
};

struct v2f{
float4 pos : SV_POSITION;
fixed3 worldNormal : TEXCOORD0; //将光照颜色传递给片元着色器,也可以用 XCOORDO 语义。
};



//顶点着色器代码,由于顶点着色器不参加计算,只要获取到坐标和世界法线
v2f vert(a2v v){
v2f o; //用于输出

//将顶点信息从模型空间传递到投影空间
o.pos = UnityObjectToClipPos(v.vertex);
// //获取环境光
// fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//将法线从模型空间传递到世界空间
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
// //获取世界空间的光照方向
// fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //只有一个光源且为平行光才有用
// //计算漫反射 saturate:饱和
// fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
// o.color = ambient + diffuse;

return o;
}

//逐像素计算
fixed4 frag(v2f i) :SV_TARGET{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

//法线
fixed3 worldNormal = normalize(i.worldNormal);
//获取光照方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

//计算漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
fixed3 color = ambient + diffuse;

return fixed4(color,1.0);
}
ENDCG
}

}
Fallback "Diffuse"
}

但是结果全黑

在片元着色器计算结果更加平滑,但是光照不到的地方全黑;

全黑可以通过添加环境光

但是背光区却和向光区明暗一致,这时可以用半兰伯特模型

2. Unity Complex Light(复杂光照)

2.1 渲染路径(Rendering Path)

额外参考:

  1. 前向渲染(Forward Rendering)和延迟渲染(Deferred Rendering)-CSDN博客

分为前向渲染路径,延迟渲染路径(更新),顶点渲染路径(弃用不介绍)

大多数情况下一个项目只使用一种渲染路径。也可以为不同相机设置不同路径。

在shaderlab中Pass中设置对应路径,lightMode告诉引擎程序员所需的光照属性,正确设置,才能适配不同流程获得的光照信息

2.1.1 前向渲染路径

  • 每进行一次前向渲染,需要渲染图元,并计算颜色缓冲深度缓冲的信息

  • 如果物体受到多个光源照射,就要写多个Pass,然后在帧缓冲中将这些光照结果混合。

  • 前向渲染中,光照类型(平行光parallel light或其他)+光照渲染模式决定了处理光照(照亮物体)的方式。

前向渲染光照处理方式分为逐像素,逐顶点、sh球谐函数计算 三种方法。在Light组件的RenderMode设置。

RenderMode 设置为important为逐像素,后二者则是Not important

2.1.2 延迟渲染路径

前向渲染的问题是 当场景中包含大量实时光源时 ,前向渲染的性能会急速下降。

延迟渲染主要包含了两个 Pass 。在第一 Pass 中,我们不进行任何光照计算,而是仅仅计算

哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它

的相关信息存储到 缓冲区中。然后,在第二个 Pass 中,我们利用 缓冲区的各个片元信息,

例如表面法线 、视角方向、漫反射系数 ,进行真正的光照计算

其它

Unity光源类型

1.平行光、点光源、面光源聚光灯,面光源只在烘焙时才会产生作用

2.当RenderMode是Auto,Unity自动判断哪些光源逐像素,哪些顶点、sh计算。

3.最亮的平行光是按照逐像素计算,而Auto状态下最多除平行光源外4个逐像素计算,这些在addtional pass中计算

最常用的光源属性有:位置、方向、颜色、强度、衰减

  • 光照衰弱
1
2
3
4
5
6
//使用纹理来计算衰弱值
fixed atten = tex2D(_ ghtTextureO, dot(lightCoord, lightCoord) .rr) .UNITY_ATTEN_CHANNEL;

//使用数学公式进行线性衰弱值计算
float distance = length (_WorldSpaceLightPosO . xyz - i. worldPosi on.xyz);
atten = 1.0 // distance; // linear attenuation

Unity阴影

1.如果最重要的平行光开启了阴影,那么Unity会为这个光源生成阴影纹理图,一种由光源出发的深度图。

2.另取一个LightMode为ShadowCaster的Pass,Unity会将摄像头放到光源的地方

3.一个物体想要接收阴影,那么在shader中要对纹理采样;相同地,一个物体想要投射阴影,就要参与纹理计算

见P200

  • 如何让正方体接收阴影

SHADOWCOORD,TRANSFER_SHADOW,SHADOW_ATTENUATION分别在v2f,vert,frag中调用

实现接收阴影

1
2
3
SHADOW_COORDS(2)
TRANSFER_SHADOW(o);
fixed shadow = SHADOW_ATTENUATION(i);

光照衰减和阴影的效果是一样的,于是就有了统一光照衰弱和阴影

unity提供一个方法同时管理这两者

UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);

  • 透明物体的阴影

    在AlphaTest的基础上使用上述方法