1. 玻璃效果基本原理
要实现玻璃效果,可能会联想到使用透明相关知识来进行制作。透明固然可以制作出玻璃透明的效果,但是它在许多地方有所缺陷,比如:
- 透明无法表现出复杂的光学效果,玻璃不仅仅是透明的,它还具有反射、折射等光学效果,使用透明无法简单的实现这些效果
- 透明物体往往会遇到深度排序问题,渲染顺序不正确时,会导致视觉错误
使用 渲染纹理 来制作玻璃效果,基本原理是:
在渲染玻璃效果物体之前,先获取到当前屏幕图像,将当前屏幕图像存储在渲染纹理之中,之后在真正处理玻璃效果物体时,再利用该渲染纹理来实现 透明、折射 等等效果。
该过程中并不会使用混合,而是直接进行颜色相乘或相加来进行颜色叠加
总结:在渲染玻璃效果之前,先捕获当前屏幕内容并保存到一张渲染纹理当中,在之后的Shader处理中利用该渲染纹理进行采样,参与最终的颜色计算,实现各种玻璃效果
这里需要用到:特殊渲染通道 GrabPass、内置函数 ComputeGrabScreenPos、 模拟折射的自定义计算规则
特殊渲染通道 GrabPass
GrabPass 的作用是捕获当前屏幕上已经渲染的内容,并将其存储到一张纹理中
它需要包含在SubShader语句块中
它的用法有两种:
(1)大括号中什么都不写,默认会把屏幕内容写入一个叫做 _GrabTexture 的纹理变量中
直接在CG语句中声明_GrabTexture 纹理变量即可直接使用抓取的渲染纹理
(2)大括号中写入自定义变量名,会把对应屏幕内容写入该自定义纹理变量中
在CG语句中声明对应纹理变量即可使用抓取的渲染纹理
内置函数 ComputeGrabScreenPos
该内置函数可以用于计算屏幕空间位置,传入顶点的裁剪空间位置,返回一个 float4 结果,该float4中的内容分别代表:
- X:屏幕空间X坐标
- Y:屏幕空间Y坐标
- Z:裁剪空间深度值,一般表示顶点距离摄像机的相对深度
- W:裁剪空间的W分量,通常用于透视除法,即 X或Y/W 后 X或Y的范围将在 0~1之间
我们可以利用该函数得到顶点相对屏幕的坐标,从而从捕获的渲染纹理中进行采样
模拟折射的自定义计算规则
为了模拟出玻璃折射的效果,我们一般不会使用立方体纹理中采样,我们往往会自定义一些计算规则,来模拟计算出折射的效果。
总体的设计思路,就是在对捕获纹理进行采样时,进行一些偏移计算。
2、如何让玻璃效果对象滞后渲染
在实现玻璃效果之前,需要先捕获当前屏幕内容并保存到一张渲染纹理当中,那么要保证玻璃效果对象后面的内容正确渲染,我们必须保证玻璃对象能够滞后渲染,想要让一个对象滞后渲染,那么可以使用渲染标签Tags中的 渲染队列Queue
因此对于玻璃效果对象,虽然它本质上是一个不透明物体吗,但是我们完全可以将它的渲染队列设置为 Transparent(透明的),保证它晚于 背景队列、几何队列、透明测试队列 之后再进行渲染,这时我们捕获的屏幕内容,将包含这些更早渲染的内容信息,便可以利用GrabPass捕获到相对正确的内容了
Shader "ShaderProj/6/GlassBase"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Cube ("Cube", Cube) = "" {}
//折射程度 0~1 0代表完全反射(完全不折射)1代表完全折射(透明效果 相当于光全部进入了内部)
_RefractAmount ("RefractAmount", Range(0, 1)) = 1
}
SubShader
{
//将渲染队列改为透明的 目的是让玻璃对象 滞后渲染
//能够捕获到之前正确的屏幕图像
Tags { "RenderType"="Opaque" "Queue"="Transparent"}
//使用它来捕获当前屏幕内容 并存储到默认的渲染纹理变量中
GrabPass{}
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos:SV_POSITION;
float4 grabPos:TEXCOORD0; //用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
float2 uv:TEXCOORD1;
float3 worldRefl:TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cube;
float _RefractAmount;
//GrabPass默认存储的纹理变量 这个是规则
sampler2D _GrabTexture;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex);
float3 worldViewDir = UnityWorldSpaceViewDir(worldPos);
o.worldRefl = reflect(-worldViewDir, worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 mainTex = tex2D(_MainTex, i.uv);
//将反射颜色和主纹理颜色进行叠加
fixed4 reflColor = texCUBE(_Cube, i.worldRefl) * mainTex;
//折射相关的颜色
//其实就是从抓取的 屏幕渲染纹理中进行采样 参与计算
//抓取纹理中的颜色信息 相当于是这个玻璃对象后面的颜色
// 自定义折射规则
float2 offset = 1 - _RefractAmount;
i.grabPos.xy = i.grabPos.xy - offset / 10;
//利用透视除法 将屏幕坐标转换到 0~1范围内 然后再进行采样
fixed2 screenUV = i.grabPos.xy / i.grabPos.w;
fixed4 grabColor = tex2D(_GrabTexture, screenUV);
float4 color = lerp(reflColor, grabColor, _RefractAmount);
return color;
}
ENDCG
}
}
}
3、带法线纹理的玻璃效果
对 Cube 加上法线向量后,需要修改反射向量计算规则,由于法线需要从法线纹理中获取,因此需要将反射向量的计算放入到片元着色器中
利用切线空间法线来计算折射偏移
(1)加入一个控制折射扭曲程度的新属性_Distortion 取值范围可以大一些
(2)利用切线空间下法线来计算偏移值,加入两行关键代码
第一行:float2 offset = tangentNormal.xy * _Distortion;
使用切线空间下法线的xy* 扭曲值得到一个偏移量,代表光线经过法线方向扰动后的偏移程度,确定光线折射的方向和强度
第二行:屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;
用偏移量和屏幕空间深度值相乘,模拟出真实的折射效果,深度值越大(即距离相机越远),折射效果越明显。 这样可以实现近大远小的效果,使得物体在不同深度上的折射效果有所差异
Shader "ShaderProj/6/GlassRefraction"
{
Properties
{
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_Cube("Cubemap", Cube) = ""{}
_RefractAmount("RefractAmount", Range(0,1)) = 1
//控制折射扭曲程度的变量
_Distortion("Distortion", Range(0,10)) = 0
}
SubShader
{
Tags{"RenderType"="Opaque" "Queue"="Transparent"}
GrabPass{}
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cube;
float _RefractAmount;
sampler2D _GrabTexture;
float _Distortion;
struct v2f
{
float4 pos:SV_POSITION;
float4 grabPos:TEXCOORD0;
float4 uv:TEXCOORD1;
float4 TtoW0:TEXCOORD3;
float4 TtoW1:TEXCOORD4;
float4 TtoW2:TEXCOORD5;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
//计算反射光向量
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i):SV_TARGET
{
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
float3 worldNormal = float3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal));
fixed4 mainTex = tex2D(_MainTex, i.uv);
float3 refl = reflect(-viewDir, worldNormal);
fixed4 reflColor = texCUBE(_Cube, refl) * mainTex;
//折射相关的颜色
float2 offset = tangentNormal.xy * _Distortion;
i.grabPos.xy = offset*i.grabPos.z + i.grabPos.xy;
fixed2 screenUV = i.grabPos.xy / i.grabPos.w;
fixed4 grabColor = tex2D(_GrabTexture, screenUV);
float4 color = lerp(reflColor, grabColor, _RefractAmount);
return color;
}
ENDCG
}
}
}