US3S8L4——反射效果
US3S8L4——反射效果
本章代码关键字
1 | reflect() // 计算反射光线方法 |
反射效果
反射效果指光的反射,是一种光学现象。指光在两种物质分界面上改变传播方向又返回原来物质中的现象,就叫做光的反射。
比如:光遇到水面、玻璃、金属等许多物体的表面都会发生反射
在 Unity Shader 中,反射效果模拟了物体表面反射环境光的特性使得物体看起来像镜子或金属表面,能够反射周围环境的图像。
反射效果的原理
反射效果的原理就是利用立方体纹理(CubeMap 或 RenderTexture)进行环境映射
我们利用摄像机 看向物体表面顶点的方向向量 作为入射光,结合 顶点法线向量 可以算出反射向量
然后利用反射方向向量在立方体纹理中进行采样,得到最终反射的颜色。
简单来说,就是根据 视角方向 和 顶点法线,计算得到反射向量,使用反射向量从立方体纹理内取值
光路可逆原则:如果光在某一路径上从点A传播到点B,那么光在同一路径上可以从点B传播回点A。
换句话说,光的传播路径在没有任何变化的情况下可以被反向利用。
因此,根据光路可逆原则,我们就可以根据视角方向计算出光线方向
反射的基础实现
反射和立方体纹理采样
-
反射函数 reflect(),通过 反向视角所在方向 和 法线向量,得到反射向量
-
立方体纹理采样函数,CG 中提供了内置函数 texCUBE() 用于进行立方体纹理采样,
通过texCUBE(立方体纹理, 反射方向向量)
便可以得到在立方体纹理中的采样结果1
2
3
4
5
6
7samplerCUBE _Cube;
float _Reflectivity;
fixed4 frag(v2f i): SV_TARGET
{
fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
}
实现反射效果
-
属性声明
我们将声明 2 个关键属性
- 立方体纹理(此属性既可以使用 Cubemap,也可以使用 RenderTexture)
- 反射率(范围限定在 0~1 之间)
1
2
3
4
5Properties
{
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率
} -
顶点着色器
反射向量会在顶点着色器内完成计算,这样效率更高,相比片元着色器内计算效果差异不大
关键步骤:
- 顶点坐标转裁剪坐标
- 顶点法线转世界坐标
- 顶点坐标转世界坐标
- 世界空间下视角方向计算
- 通过逆向视角方向得到反射方向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float3 worldReflection: TEXCOORD0; // 世界空间下的反射向量
};
v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
float3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); // 计算世界空间下视角所在方向
data.worldReflection = reflect(-worldViewDir, worldNormal); // 通过反向视角方向得到反射方向
return data;
} -
片元着色器
关键步骤:
- 立方体纹理采样(利用
texCUBE
函数) - 结合 反射率 返回最终颜色
1
2
3
4
5
6
7
8samplerCUBE _Cube;
float _Reflectivity;
fixed4 frag(v2f i): SV_TARGET
{
fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
return cubemapColor * _Reflectivity;
} - 立方体纹理采样(利用
完整 Shader 代码如下:
1 | Shader "Unlit/Lesson75_ReflectBase" |
将 CubeMap
或者 RenderTexture
设置到使用反射 Shader 的材质的 Cube 属性上
(如果使用 RenderTexture
,该 RenderTexture
可能需要在合适的时机动态更新,因为 RenderTexture
不能持久化纹理数据,数据会在保存时丢失)
如果要实时的生成反射效果,需要在运行时实时为物体生成立方体贴图,代码可参考:在运行时动态生成立方体纹理
显示效果:
反射结合漫反射和阴影
插值计算
CG 中提供了内置函数 lerp 用于进行插值计算
1 | lerp(a, b, t) |
参数:
-
a
:插值起点值 -
b
:插值终点值 -
t
:插值因子,0~1 之间
内部计算公式:
- 当 时,插值结果为
- 当 时,插值结果为
- 当 在 之间变化时,返回一个 到 之间的一个线性插值结果
我们将利用该函数来决定反射效果的程度,用它在漫反射颜色和反射颜色之间进行插值控制反射程度
实现带漫反射的反射效果
-
复用上文的反射基础效果代码
-
属性声明
我们将加入 2 个关键属性
- 漫反射颜色
- 反射颜色
1
2
3
4
5
6
7Properties
{
_Color("Color", Color) = {1, 1, 1, 1} // 漫反射颜色
_ReflectColor("ReflectColor", Color) = {1, 1, 1, 1} // 反射颜色
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率
} -
v2f
结构体因为要在片元着色器中处理光和阴影,因此
v2f
结构体需要加入:- 世界空间法线
- 世界空间顶点位置
- 阴影坐标宏 SHADOW_COORDS()
此外,还需要添加
#pragma multi_complie_fwdbase
和#include "AutoLight.cginc"
,这部分知识回顾详见:US3S6——阴影1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fixed4 _Color;
fixed4 _ReflectColor;
samplerCUBE _Cube;
float _Reflectivity;
struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
fixed3 worldNormal: NORMAL; // 世界空间下法线
fixed3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
float3 worldReflection: TEXCOORD1; // 世界空间下的反射向量
SHADOW_COORDS(2) // 阴影坐标宏
}; -
顶点着色器
关键步骤:
- 顶点坐标转裁剪坐标
- 顶点法线转世界坐标
- 顶点坐标转世界坐标
- 世界空间下 视角方向计算
- 视角反向逆向得到反射方向
- 阴影相关计算 TRANSFER_SHADOW
1
2
3
4
5
6
7
8
9
10
11
12v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
data.worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
data.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
fixed3 worldViewDir = UnityWorldSpaceViewDir(data.worldPos); // 计算世界空间下视角所在方向
data.worldReflection = reflect(-worldViewDir, data.worldNormal); // 通过反向视角方向得到反射方向
TRANSFER_SHADOW(data) // 阴影转换计算
return data;
} -
片元着色器
关键步骤:
-
得到光的方向
UnityWorldSpaceDir()
-
兰伯特漫反射颜色计算
具体计算方法详见:US3S1L2——兰伯特光照模型
-
立方体纹理采样(利用 texCUBE 函数)
-
光照衰减和阴影相关的衰减值 UNITY_LIGNT_ATTENUATION()
-
最终颜色计算(利用 lerp 函数)
1
2
3
4
5
6
7
8
9
10
11
12
13fixed4 frag(v2f i): SV_TARGET
{
// 漫反射计算
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(normalize(i.worldNormal), worldLightDir));
// 立方体纹理计算
fixed3 cubemapColor = texCUBE(_Cube, i.worldReflection).rgb * _ReflectColor.rgb;
// 计算光照衰减和阴影相关的衰减值
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
// 利用插值计算,在 漫反射颜色 和 反射颜色 之间进行插值,0和1就是极限状态,0代表没有反射效果,1代表只要反射效果,0~1就是两者的叠加
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb + lerp(diffuse, cubemapColor, _Reflectivity) * atten;
return fixed4(color, 1);
} -
-
添加
FallBack "Reflective/VertexLit"
,实现投射阴影1
2
3
4
5
6
7Shader "Unlit/Lesson76_Reflection"
{
Properties {/*...*/}
SubShader {/*...*/}
Fallback "Reflective/VertexLit"
}
显示效果(左为结合了漫反射光照和阴影效果的反射Shader,反射率为0.5,右为纯反射Shader):
可见,加入漫反射和阴影计算后,物体可以受到光照影响,还可以投射和接收阴影
完整 Shader 代码如下:
1 | Shader "Unlit/Lesson76_Reflection" |