US3S8L5——折射效果
US3S8L5——折射效果
本章代码关键字
1 | refract() // 折射向量计算 |
折射效果
折射效果指光的折射,是一种光学现象。指光从一种透明介质斜射入另一种透明介质时,传播方向一般会发生变化,这种现象叫光的折射。
光的折射与光的反射一样,都是发生在两种介质的交界处,只是反射光返回原介质中,而折射光进入到另一种介质中,
由于光在两种不同的物质里传播速度不同,故在两种介质的交界处传播方向发生变化,这就是光的折射。
在 Unity Shader 中,折射效果模拟了光线通过透明或半透明材质时的弯曲行为。
一般用来模拟水面、透明玻璃球、眼镜、钻石、水晶球、空气扰动等等效果,它一般会配合其他表现效果一起使用
折射效果的原理
折射效果的原理还是利用立方体纹理(CubeMap
)进行环境映射
我们利用摄像机看向物体表面顶点的方向向量作为入射向量,结合顶点法线向量算出折射向量,
然后利用折射方向向量在立方体纹理中进行采样,得到最终反射的颜色。
简单来说,就是根据 视角方向 和 顶点法线,计算得到折射向量,使用折射向量从立方体纹理内取值
我们同样可以根据光路可逆原则,使用视角方向计算出光线方向
我们在计算折射方向时,会用到斯涅耳定律(Snell’s Law):
当光从介质 1 沿着表面法线夹角为 的方向斜射入介质 2 时,
我们可以利用数学公式 计算出折射光线和法线的夹角
其中 和 为两种介质的折射率
对于其中的折射率来说,不同物体的折射率各不相同,我们在制作时,可以在搜索引擎中搜索 常用折射率表 来获取对应物体的折射率
折射效果注意点
我们在 Unity 中处理 折射效果 的做法是:直接用得到的折射方向对立方体纹理进行采样,这样做其实不符合物理规律,
因为对于一个透明物体来说,更准确的模拟方式应该是进行两侧折射,一次是光线进入内部,一次是光线从物体内部射出。
但是,在实时渲染中模拟第二次折射方向较为复杂,而我们仅模拟一次折射得到的效果在视觉上看起来也是可以接受的!
因此,在实时渲染中,我们通常仅模拟第一次折射来得到最终的结果!
折射的基础实现
折射计算函数
CG 中提供了内置函数 refract
用于进行折射向量的计算
1 | refract(入射方向单位向量, 顶点法线单位向量, 入射介质折射率/射入介质折射率) |
传入 入射方向单位向量、顶点法线单位向量、入射介质与射入介质折射率比值 便可以得到在目标介质中的折射向量
该函数内部就是利用了斯涅耳定律进行的计算,我们无需再自己写逻辑计算了
折射基础效果实现
-
属性声明
我们将声明4个关键属性:
- 介质A折射率
- 介质B折射率
- 立方体纹理贴图
- 折射程度
指的一提的是,介质A折射率 和 介质B折射率 属性可以合并为 介质A与介质B的折射率比值 属性
我们在外部计算好这个比值,直接传入即可,这样即可减少计算步骤,提高效率,这部分因为是学习需要,所以还是使用两个属性1
2
3
4
5
6
7Properties
{
_RefractiveIndexA("RefractiveIndexA", Range(1, 2)) = 1 // 介质A折射率
_RefractiveIndexB("RefractiveIndexA", Range(1, 2)) = 1.3 // 介质B折射率
_Cube("Cubemap", Cube) = ""{} // 立方体纹理贴图
_RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度
} -
编译指令、内置文件、属性映射、结构体相关
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
28SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass
{
CGPROGRAM
samplerCUBE _Cube;
fixed _RefractiveIndexA;
fixed _RefractiveIndexB;
fixed _RefractAmount;
struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下顶点坐标
float3 worldRefr: TEXCOORD0; // 折射向量
};
// ...
ENDCG
}
} -
顶点着色器
关键步骤:
- 顶点坐标转裁剪坐标
- 顶点法线转世界坐标
- 顶点坐标转世界坐标
- 世界空间下视角方向计算
- 利用折射函数计算折射向量
1
2
3
4
5
6
7
8
9
10
11
12v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界法线
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标转世界坐标
fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); // 世界空间下视角方向的计算
// 根据逆向的视角方向,法线向量,两个介质的折射率比值计算折射向量
data.worldRefr = refract(-normalize(worldViewDir), normalize(worldNormal), _RefractiveIndexA / _RefractiveIndexB);
return data;
} -
片元着色器
关键步骤:
- 立方体纹理采样(利用
texCUBE
函数) - 结合折射程度返回最终颜色
1
2
3
4
5fixed4 frag(v2f i): SV_TARGET
{
fixed4 cubemapColor = texCUBE(_Cube, i.worldRefr); // 立方体纹理采样
return cubemapColor * _RefractAmount; // 结合折射程度计算最终颜色
} - 立方体纹理采样(利用
显示效果:
可见,物体呈现出了一种折射效果,它改变了物体后边的显示效果
完整 Shader 代码:
1 | Shader "Unlit/Lesson77_RefractionBase" |
实现带漫反射和阴影的折射效果
-
复用上文的基础折射 Shader 代码
-
折射效果结合漫反射和阴影
折射效果结合漫反射和阴影和之前 反射 几乎一模一样,我们可以直接复制之前的代码,避免书写重复内容
-
属性相关
添加漫反射颜色和折射颜色,将 折射率A 和 折射率B 合并为一个 折射率比值变量
折射率比值变量可以以后在外部算好了再传入,避免内部计算浪费性能1
2
3
4
5
6
7
8Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 漫反射颜色
_RefractColor("RefractColor", Color) = (1, 1, 1, 1) // 折射颜色
_RefractRatio("RefractRatio", Range(0.1, 1)) = 0.5 // 两个介质的折射率比值
_Cube("Cubemap", Cube) = ""{} // 立方体纹理贴图
_RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度
} -
渲染路径、编译指令、内置文件复制、属性映射复制修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
fixed4 _Color;
fixed4 _RefractColor;
samplerCUBE _Cube;
fixed _RefractRatio;
fixed _RefractAmount;
// ...
} -
结构体相关内容复制
1
2
3
4
5
6
7
8struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
fixed3 worldNormal: NORMAL; // 世界空间下法线
float3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
float3 worldRefr: TEXCOORD1; // 折射向量
SHADOW_COORDS(2) // 阴影坐标宏
}; -
顶点着色器相关内容修改
1
2
3
4
5
6
7
8
9
10
11
12
13v2f 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.worldRefr = refract(-normalize(worldViewDir), normalize(data.worldNormal), _RefractRatio);
TRANSFER_SHADOW(data) // 阴影转换计算
return data;
} -
片元着色器相关内容复制修改
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.worldRefr).rgb * _RefractColor.rgb;
// 计算光照衰减和阴影相关的衰减值
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
// 利用插值计算,在 漫反射颜色 和 反射颜色 之间进行插值,0和1就是极限状态,0代表没有反射效果,1代表只要反射效果,0~1就是两者的叠加
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb + lerp(diffuse, cubemapColor, _RefractAmount) * atten;
return fixed4(color, 1);
} -
添加
FallBack "Reflective/VertexLit"
,实现投射阴影1
2
3
4
5
6
7Shader "Unlit/Lesson78_Refraction"
{
Properties {/*...*/}
SubShader {/*...*/}
Fallback "Reflective/VertexLit"
}
-
显示效果(左为结合了漫反射光照和阴影效果的折射Shader,折射比值是0.76,右为纯折射Shader):
可见,加入漫反射和阴影计算后,物体可以受到光照影响,还可以投射和接收阴影
完整 Shader 代码如下:
1 | Shader "Unlit/Lesson78_Refraction" |