US3S8L4——反射效果

本章代码关键字

1
2
3
reflect()    // 计算反射光线方法
texCUBE() // 立方体纹理采样方法
lerp() // 插值,在这里用于将光照效果和反射效果进行插值结合处理

反射效果

反射效果指光的反射,是一种光学现象。指光在两种物质分界面上改变传播方向又返回原来物质中的现象,就叫做光的反射。

比如:光遇到水面、玻璃、金属等许多物体的表面都会发生反射

image

在 Unity Shader 中,反射效果模拟了物体表面反射环境光的特性使得物体看起来像镜子或金属表面,能够反射周围环境的图像。

imageimage

反射效果的原理

反射效果的原理就是利用立方体纹理(CubeMap 或 RenderTexture)进行环境映射
我们利用摄像机 看向物体表面顶点的方向向量 作为入射光,结合 顶点法线向量 可以算出反射向量
然后利用反射方向向量在立方体纹理中进行采样,得到最终反射的颜色。

简单来说,就是根据 视角方向 和 顶点法线,计算得到反射向量,使用反射向量从立方体纹理内取值

imageimage

光路可逆原则:如果光在某一路径上从点A传播到点B,那么光在同一路径上可以从点B传播回点A。
换句话说,光的传播路径在没有任何变化的情况下可以被反向利用。

因此,根据光路可逆原则,我们就可以根据视角方向计算出光线方向

反射的基础实现

反射和立方体纹理采样

  • 反射函数 reflect()​,通过 反向视角所在方向 和 法线向量,得到反射向量

  • 立方体纹理采样函数,CG 中提供了内置函数 texCUBE()​ 用于进行立方体纹理采样,
    通过 texCUBE(立方体纹理, 反射方向向量)​ 便可以得到在立方体纹理中的采样结果

    1
    2
    3
    4
    5
    6
    7
    samplerCUBE _Cube;
    float _Reflectivity;

    fixed4 frag(v2f i): SV_TARGET
    {
    fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
    }

实现反射效果

  1. 属性声明

    我们将声明 2 个关键属性

    1. 立方体纹理(此属性既可以使用 Cubemap​,也可以使用 RenderTexture​)
    2. 反射率(范围限定在 0~1 之间)
    1
    2
    3
    4
    5
    Properties
    {
    _Cube("Cubemap", Cube) = ""{} // 立方体纹理
    _Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率
    }
  2. 顶点着色器

    反射向量会在顶点着色器内完成计算,这样效率更高,相比片元着色器内计算效果差异不大

    关键步骤:

    1. 顶点坐标转裁剪坐标
    2. 顶点法线转世界坐标
    3. 顶点坐标转世界坐标
    4. 世界空间下视角方向计算
    5. 通过逆向视角方向得到反射方向
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct 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;
    }
  3. 片元着色器

    关键步骤:

    1. 立方体纹理采样(利用 texCUBE​ 函数)
    2. 结合 反射率 返回最终颜色
    1
    2
    3
    4
    5
    6
    7
    8
    samplerCUBE _Cube;
    float _Reflectivity;

    fixed4 frag(v2f i): SV_TARGET
    {
    fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
    return cubemapColor * _Reflectivity;
    }

完整 Shader 代码如下:

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
Shader "Unlit/Lesson75_ReflectBase"
{
Properties
{
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }

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

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

samplerCUBE _Cube;
float _Reflectivity;

struct 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;
}

fixed4 frag(v2f i): SV_TARGET
{
fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
return cubemapColor * _Reflectivity;
}
ENDCG
}
}
}

CubeMap​ 或者 RenderTexture​ 设置到使用反射 Shader 的材质的 Cube 属性上
(如果使用 RenderTexture​,该 RenderTexture​ 可能需要在合适的时机动态更新,因为 RenderTexture​ 不能持久化纹理数据,数据会在保存时丢失)

如果要实时的生成反射效果,需要在运行时实时为物体生成立方体贴图,代码可参考:在运行时动态生成立方体纹理

显示效果:

image

反射结合漫反射和阴影

插值计算

CG 中提供了内置函数 lerp​ 用于进行插值计算

1
lerp(a, b, t)

参数:

  • a​:插值起点值
  • b​:插值终点值
  • t​:插值因子,0~1 之间

内部计算公式:a+t×(ba)a + t \times (b - a)

  • t=0t = 0 时,插值结果为 aa
  • t=1t = 1 时,插值结果为 bb
  • tt010 \sim 1 之间变化时,返回一个 aabb 之间的一个线性插值结果

我们将利用该函数来决定反射效果的程度,用它在漫反射颜色和反射颜色之间进行插值控制反射程度

实现带漫反射的反射效果

  1. 复用上文的反射基础效果代码

  2. 属性声明

    我们将加入 2 个关键属性

    1. 漫反射颜色
    2. 反射颜色
    1
    2
    3
    4
    5
    6
    7
    Properties
    {
    _Color("Color", Color) = {1, 1, 1, 1} // 漫反射颜色
    _ReflectColor("ReflectColor", Color) = {1, 1, 1, 1} // 反射颜色
    _Cube("Cubemap", Cube) = ""{} // 立方体纹理
    _Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率
    }
  3. v2f​ 结构体

    因为要在片元着色器中处理光和阴影,因此 v2f​ 结构体需要加入:

    1. 世界空间法线
    2. 世界空间顶点位置
    3. 阴影坐标宏 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
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_fwdbase

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"

    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) // 阴影坐标宏
    };
  4. 顶点着色器

    关键步骤:

    1. 顶点坐标转裁剪坐标
    2. 顶点法线转世界坐标
    3. 顶点坐标转世界坐标
    4. 世界空间下 视角方向计算
    5. 视角反向逆向得到反射方向
    6. 阴影相关计算 TRANSFER_SHADOW​
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    v2f 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;
    }
  5. 片元着色器

    关键步骤:

    1. 得到光的方向 UnityWorldSpaceDir()

    2. 兰伯特漫反射颜色计算

      具体计算方法详见:US3S1L2——兰伯特光照模型

    3. 立方体纹理采样(利用 texCUBE​ 函数)

    4. 光照衰减和阴影相关的衰减值 UNITY_LIGNT_ATTENUATION()​

    5. 最终颜色计算(利用 lerp​ 函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fixed4 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);
    }
  6. 添加 FallBack "Reflective/VertexLit"​,实现投射阴影

    1
    2
    3
    4
    5
    6
    7
    Shader "Unlit/Lesson76_Reflection"
    {
    Properties {/*...*/}
    SubShader {/*...*/}

    Fallback "Reflective/VertexLit"
    }

显示效果(左为结合了漫反射光照和阴影效果的反射Shader,反射率为0.5,右为纯反射Shader):

image

可见,加入漫反射和阴影计算后,物体可以受到光照影响,还可以投射和接收阴影

完整 Shader 代码如下:

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 "Unlit/Lesson76_Reflection"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 漫反射颜色
_ReflectColor("ReflectColor", Color) = (1, 1, 1, 1) // 反射颜色
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }

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

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
fixed4 _ReflectColor;
samplerCUBE _Cube;
float _Reflectivity;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
fixed3 worldNormal: NORMAL; // 世界空间下法线
float3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
float3 worldReflection: TEXCOORD1; // 世界空间下的反射向量
SHADOW_COORDS(2) // 阴影坐标宏
};

v2f 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;
}

fixed4 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);
}
ENDCG
}
}

Fallback "Reflective/VertexLit"
}