第一种是冯乐乐版的
多附上了阴影和光照
基本原理:渲染队列为Transparent,也就是在所有不透明物体渲染完成之后再渲染毛玻璃,但不用开启混合,因为我们用GrabPass直接获取到渲染前的画面了,使用GrabPass保存玻璃渲染前的屏幕画面。
在shader中,首先在像素着色器中计算出当前像素在屏幕中的位置(范围[0,1]),然后将该位置沿着切线空间的(x,y)偏移,将偏移的结果作为在GrabPass中采样的位置,以实现画面扭曲的效果,得到扭曲后的像素,保存为一个颜色值。为什么不沿z偏移呢,因为世界空间下的高模中大部分顶点的法线不需要扰动,所以将其降为低模并映射到切线空间下的法线纹理后,大部分法线纹理的像素都趋近于(0, 0, 1),所以这种纹理大多偏向蓝色。使用z偏移屏幕坐标的话,效果不好。
此外,保存当前位置的立方体贴图,然后使用立方体映射得到另一个颜色值,然后用参数对以上两个颜色值做插值得到最后的扭曲结果。
最后可以再加上相应的光照和阴影,光照做加法即可。
- 计算屏幕坐标
(我自己理解的二者关系)
// VS
o.pos = UnityObjectToClipPos(v.vertex);
float4 srcPos = o.pos * 0.5f;
srcPos.xy = float2(srcPos.x, srcPos.y * _ProjectionParams.x) + srcPos.w;
srcPos.zw = o.pos.zw;
o.srcPos = srcPos;
// PS
当前像素的实际uv是o.srcPos.xy / o.srcPos.w
_ProjectionParams.x表示directx和opengl的uv坐标系差异,两者的y轴正好相反,可以认为是投影矩阵反转的结果,这个x取值为1或者-1,对应着投影矩阵是否反转。
顶点着色器里实际上是做了①
为什么要这样呢?这个式子①代表什么呢?实际上,对于透视投影来说,将顶点做MVP变换的结果,就是将视锥挤压(缩放)成一个立方体,连带着所有顶点坐标一起缩放,坐标中心是摄像机,结果的每一个分量就是顶点在这个裁剪空间中的坐标。(裁剪是硬件做的,不用管)
但是这个值的w分量不再是1,所以要做齐次除法将w变为1,也就意味着这个坐标的xyz三个分量都要除以w。齐次除法之后,xyz的范围会变成[-1,1](unity),也就是NDC空间下的坐标,接下来只需要将这个范围进行视口变换放到屏幕空间下即可。也就是除以2加0.5。写出齐次除法和视口变换公式即:
那么再看式子①,在着色器中我们可以忽略屏幕宽度和长度,也就是不考虑乘pixelWidth和pixelHeight,但是问题来了,在顶点着色器中进行齐次除法的话,会破坏在像素着色器中的插值结果,因为裁剪空间是非线性空间,而插值是基于三角形重心坐标的线性插值,因此,如果在VS中不做齐次除法的话,插值的对象就是x,而做了的话,插值的对象就是x/w,引入了w这个新的变量,破坏了线性插值的条件,因此等在PS中插值完了再做齐次除法。
因此,①式在像素着色器中除以w,即屏幕坐标(uv)。
Shader "Custom/MyGlassShader"
{
Properties
{
_MainTex ("Main Tex", 2D) = "white" {}
_BumpTex ("Bump Tex", 2D) = "bump" {}
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {}
_Distortion ("Distortion", Range(0, 100)) = 10
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
_LightAmount ("Light Amount", Range(0.0, 1.0)) = 1.0
}
SubShader
{
Tags { "LightMode" = "ForwardBase" "Queue" = "Transparent" "RenderType"="Opaque" }
GrabPass { "_RefractionTex" }
Pass
{
Cull Off
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex VS
#pragma fragment PS
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
samplerCUBE _Cubemap;
float _Distortion;
float _RefractAmount;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
float _LightAmount;
struct Input
{
float3 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord : TEXCOORD0;
};
struct Output
{
float4 pos : SV_POSITION;
float4 srcPos : TEXCOORD0;
float4 uv : TEXCOORD1;
float3 posW : TEXCOORD2;
float3 tanW : TEXCOORD3;
float3 bitanW : TEXCOORD4;
float3 normalW : TEXCOORD5;
SHADOW_COORDS(6)
};
Output VS(Input v)
{
Output o;
o.pos = UnityObjectToClipPos(v.vertex);
o.posW = mul((float3x3)UNITY_MATRIX_M, v.vertex);
//o.srcPos = ComputeGrabScreenPos(o.pos); // 得到该顶点在屏幕上的采样坐标
float4 srcPos = o.pos * 0.5f;
srcPos.xy = float2(srcPos.x, srcPos.y * _ProjectionParams.x) + srcPos.w;
srcPos.zw = o.pos.zw;
o.srcPos = srcPos;
o.uv.xy = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord * _BumpTex_ST.xy + _BumpTex_ST.zw;
o.normalW = normalize(mul((float3x3)unity_WorldToObject, v.normal));
o.tanW = normalize(mul((float3x3)UNITY_MATRIX_M, v.tangent));
o.bitanW = cross(o.normalW, o.tanW) * v.tangent.w;
TRANSFER_SHADOW(o)
return o;
}
fixed4 PS(Output o) : SV_TARGET
{
fixed3 bump = UnpackNormal(tex2D(_BumpTex, o.uv.zw));
// Compute the offset in tangent space
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
o.srcPos.xy = offset + o.srcPos.xy; // 对屏幕采样坐标进行偏移
fixed3 refrCol = tex2D(_RefractionTex, o.srcPos.xy / o.srcPos.w).rgb; //_RefractionTex就是屏幕画面
o.normalW = normalize(o.normalW);
o.tanW = normalize(o.tanW - dot(o.normalW, o.tanW) * o.normalW);
o.bitanW = normalize(o.bitanW);
float3x3 t2w = float3x3(o.tanW, o.bitanW, o.normalW);
bump = normalize(mul(bump, t2w));
fixed3 f2Cam = normalize(_WorldSpaceCameraPos - o.posW);
fixed3 reflDir = reflect(-f2Cam, bump);
fixed4 texColor = tex2D(_MainTex, o.uv.xy);
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;