Shader 海面/水面
首先用Terrain在场景中随便做个地形,当作海底
上面加个Plane作为海面
实现海水效果要考虑海水深度对颜色的影响,法线移动形成波浪,菲涅尔,高光等效果
深度
海水深的地方颜色深,浅的地方颜色浅,所以海边和礁石附近的颜色应该比较浅。在shader中申明_CameraDepthTexture即可获得相机看到的深度图
因为海面会用Transparent来渲染,不会写入深度,所以这张深度图就是相机到海底的距离,在相机空间下,用海底的深度(红色箭头)减去海面的深度(蓝色箭头)就得到海面到海底的深度,关键代码
// 非线性深度
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos).r;
// 转为线性深度,这个深度是相机看到的深度,也就是海底不透明物体深度,海面透明不会写入深度
depth = LinearEyeDepth(depth);
// 海面的深度
float seaSurfaceDepth = LinearEyeDepth(i.pos.z);
// 海面距离海底的深度
float biasDepth = depth - seaSurfaceDepth;
根据这个深度就可以在了两个颜色之间做插值
法线移动,菲涅尔
根据时间和X,Y两个方向的速度计算法线纹理的偏移量,关键代码
float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
// 对法线纹理进行两次采样(这是为了模拟两层交叉的水面波动的效果)
float3 bumpOffset1 = UnpackNormal(tex2D(_WaterNormalMap, i.uv.zw + speed)).rgb;
float3 bumpOffset2 = UnpackNormal(tex2D(_WaterNormalMap, i.uv.zw - speed)).rgb;
// 两次结果相加并归一化后得到切线空间下的法线方向
float3 tangentNormal = normalize(bumpOffset1 + bumpOffset2);
把法线从切线空间变换到世间空间,就可以计算反射和折射,然后用菲涅尔系数混合,这部分代码和玻璃效果的实现类似,参考 玻璃效果
添加法线移动和菲涅尔后的效果
高光
使用Blinn-Phong计算高光
float3 halfDir = normalize(worldLightDir + worldViewDir);
float hdotn = max(0, dot(halfDir, worldNormal));
float3 specular = pow(hdotn, _SpecularPower) * _SpecularIntensity;
specular *= _LightColor0.rgb * _SpecularColor;
此外,可以在场景中添加一个反射探针ReflectionProbe,Bake下环境信息
添加高光后的效果
完整的shader
Shader "MyCustom/Sea"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_ShallowColor ("[浅水区颜色]ShallowColor", Color) = (1, 1, 1, 1)
_DeepColor ("[深水区颜色]DeepColor", Color) = (1, 1, 1, 1)
_DepthRange ("[调节深度范围]DepthRange", Range(0,1)) = 0.5
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {}
// 一个由噪声纹理生成的法线纹理
_WaterNormalMap ("WaterNormalMap", 2D) = "bump" {}
_WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01
_WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01
_Distortion ("[折射时图像的扭曲程度]Distortion", Range(0, 100)) = 10
_NormalScale ("NormalScale", Float) = 4
_SpecularColor ("SpecularColor", Color) = (1, 1, 1, 1)
_SpecularPower ("SpecularPower", Range(0, 150)) = 100
_SpecularIntensity ("SpecularIntensity", Range(0, 10)) = 0.6
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent"}
ZWrite Off
GrabPass
{
"_GrabTexture"
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 scrPos : TEXCOORD1;
float4 TtoW0 : TEXCOORD2;
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
// sampler2D_float 精度更高
sampler2D_float _CameraDepthTexture;
float3 _ShallowColor;
float3 _DeepColor;
float _DepthRange;
samplerCUBE _Cubemap;
sampler2D _WaterNormalMap;
float4 _WaterNormalMap_ST;
float _WaveXSpeed;
float _WaveYSpeed;
float _Distortion;
float _NormalScale;
float3 _SpecularColor;
float _SpecularPower;
float _SpecularIntensity;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.uv, _WaterNormalMap);
o.scrPos = ComputeScreenPos(o.pos);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal, worldTangent) * 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
{
fixed4 albedo = tex2D(_MainTex, i.uv.xy);
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
// 非线性深度
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos).r;
// 等价于下面写法
// float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.scrPos.xy / i.scrPos.w).r;
// 转为线性深度,这个深度是相机看到的深度,也就是海底不透明物体深度,海面透明不会写入深度
depth = LinearEyeDepth(depth);
// 海面的深度
float seaSurfaceDepth = LinearEyeDepth(i.pos.z);
// 海面距离海底的深度
float biasDepth = depth - seaSurfaceDepth;
// 调节深度范围,指数部分为负数,所以是一条倾斜向下的曲线,深度越大越接近0
biasDepth = exp(-_DepthRange * biasDepth);
fixed3 baseColor = lerp(_DeepColor, _ShallowColor, biasDepth);
float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
// // 对法线纹理进行两次采样(这是为了模拟两层交叉的水面波动的效果)
float3 bumpOffset1 = UnpackNormal(tex2D(_WaterNormalMap, i.uv.zw * _NormalScale + speed)).rgb;
float3 bumpOffset2 = UnpackNormal(tex2D(_WaterNormalMap, i.uv.zw * _NormalScale - speed)).rgb;
// 两次结果相加并归一化后得到切线空间下的法线方向
float3 tangentNormal = normalize(bumpOffset1 + bumpOffset2);
// 将法线转换为世界空间
float3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));
// 反射
fixed3 reflectDir = reflect(-worldViewDir, worldNormal);
fixed3 reflectCol = texCUBE(_Cubemap, reflectDir).rgb * albedo.rgb;
// 对切线空间下的法线进行偏移
float2 offset = tangentNormal.xy * _Distortion * _GrabTexture_TexelSize.xy;
// 把偏移量和屏幕坐标的z分量相乘,这是为了模拟深度越大、折射程度越大的效果
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
// 对scrPos进行了透视除法,使用该坐标对抓取的屏幕图像进行采样,得到模拟的折射颜色
fixed3 refractCol = tex2D(_GrabTexture, i.scrPos.xy / i.scrPos.w).rgb;
// 计算菲涅耳系数,混合折射和反射
fixed fresnel = pow(1 - saturate(dot(worldViewDir, worldNormal)), 4);
fixed3 fresnelColor = reflectCol * fresnel + refractCol * (1 - fresnel);
// 使用Blinn-Phong计算高光
float3 halfDir = normalize(worldLightDir + worldViewDir);
float hdotn = max(0, dot(halfDir, worldNormal));
float3 specular = pow(hdotn, _SpecularPower) * _SpecularIntensity;
specular *= _LightColor0.rgb * _SpecularColor;
fixed4 finalCol = 1;
finalCol.rgb = baseColor + fresnelColor + specular;
return finalCol;
}
ENDCG
}
}
}
调节的参数
参考
《Shader 入门精要》