实现场景大纲

一个经典的简单的带坡度的小池塘,池塘岸边带有泡沫,池塘表面有起伏动画,池塘表面透明可以看到池塘中的物体。
toon water

实现思路

  • 根据深度设定不同坡度下水的颜色
  • 通过噪声和深度值获得池塘岸边表面的白色浪花
  • 通过 UV 动画获得波浪起飞
  • 使用法线缓冲区来区分平缓的表面和垂直的表面,然后在垂直的表面出增加泡沫。
  • 通过调整混合设置获得不透明效果

简单代码

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
74
75
76
77
78
79
80
81

struct appdata
{
float4 vertex : POSITION;
float4 uv : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float4 vertex : SV_POSITION;
float2 noiseUV : TEXCOORD0;
float2 distortUV : TEXCOORD1;
float4 screenPosition : TEXCOORD2;
float3 viewNormal : NORMAL;
};

//这个颜色混合用来上层的颜色和 alpha,并且在这个基础上用 (1-上层alpha) 叠加了底层的颜色和 alpha
float4 alphaBlend(float4 top, float4 bottom)
{
float3 color = (top.rgb * top.a) + (bottom.rgb * (1 - top.a));
float alpha = top.a + bottom.a * (1 - top.a);
return float4(color, alpha);
}

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPosition = ComputeScreenPos(o.vertex);
o.noiseUV = TRANSFORM_TEX(v.uv, _SurfaceNoise);
o.distortUV = TRANSFORM_TEX(v.uv, _SurfaceDistortion);
o.viewNormal = COMPUTE_VIEW_NORMAL;
return o;
}

float4 frag (v2f i) : SV_Target
{
//两个方法一个意思
//float existingDepth01 = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPosition)).r;
//获得该屏幕点的深度值
float existingDepth01 = tex2D(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPosition.xy / i.screenPosition.w)).r;
//将该屏幕点的深度值转换为线性的
float existingDepthLinear = LinearEyeDepth(existingDepth01);
// i.screenPosition.w 存储的是该点的深度信息
// 用该屏幕点的深度值减去 水平面 的深度值 得到水深的值
float depthDifference = existingDepthLinear - i.screenPosition.w;
//将水深收敛于 [0,1] 并且去处负数部分。
float waterDepthDifference01 = saturate(depthDifference / _DepthMaxDistance);
//lerp 水的颜色
float4 waterColor = lerp(_DepthGradientShallow, _DepthGradientDeep, waterDepthDifference01);


//float3 existingNormal = tex2Dproj(_CameraNormalsTexture, UNITY_PROJ_COORD(i.screenPosition));
//获取 view space 的法线
float3 existingNormal = tex2D(_CameraNormalsTexture, UNITY_PROJ_COORD(i.screenPosition.xyz / i.screenPosition.w));
//当前屏幕点的法线 和 水面的法线 的点乘,如果点乘的结果越小,说明越接近 90°,说明这个是水面物体的边缘。
float3 normalDot = saturate(dot(existingNormal, i.viewNormal));
//这个 lerp 会将 dot 特别小的值取大,这样,物体表面近乎于垂直的结果就会被认为和岸边一样。
float foamDistance = lerp(_FoamMaxDistance, _FoamMinDistance, normalDot);
// foamDepthDifference01 这个值越大,越可能显示泡沫
float foamDepthDifference01 = saturate(depthDifference / foamDistance);
//表面噪声阈值 通过深度值控制岸边的泡沫 通过法线乘积控制物体旁边的泡沫
float surfaceNoiseCutoff = foamDepthDifference01 * _SurfaceNoiseCutoff;
//扰动采样
float2 distortSample = (tex2D(_SurfaceDistortion, i.distortUV).xy * 2 - 1) * _SurfaceDistortionAmount;
// 用时间偏移, 用 scroll 控制速度,并且朝着 disort 采样结果偏移。
//float2 noiseUV = float2((i.noiseUV.x + _Time.y * _SurfaceNoiseScroll.x) + distortSample.x, (i.noiseUV.y + _Time.y * _SurfaceNoiseScroll.y) + distortSample.y);
float2 noiseUV = float2((i.noiseUV.x), (i.noiseUV.y));
//在当前点混合了速度和时间和扰动的情况下,采样的噪声的 alpha。
float surfaceNoiseSample = tex2D(_SurfaceNoise, noiseUV).r;
//确定该像素是否为泡沫
float surfaceNoise = surfaceNoiseSample > surfaceNoiseCutoff ? 1 : 0;
//float surfaceNoise = smoothstep(surfaceNoiseCutoff - SMOOTHSTEP_AA, surfaceNoiseCutoff + SMOOTHSTEP_AA, surfaceNoiseSample);
float4 surfaceNoiseColor = _FoamColor;
//这里如果判定为泡沫,就会在最终结果中混入泡沫的颜色,如果不是泡沫,就会直接显示水面的颜色。
surfaceNoiseColor.a *= surfaceNoise;


return alphaBlend(surfaceNoiseColor, waterColor);
}

案例总结

  • 获取深度信息可以直接修改摄像机的 depthTextureMode 来获得。
  • 获取法线贴图也可以通过修改摄像机的 depthTextureMode 来获得,但是如果同时获取深度和法线贴图,就会导致精度不足。原来是深度信息单独一张贴图,使用四个通道,如果同时获取就会变成一张贴图每个缓冲区两个通道。
  • Blend 对于渲染不透明物体很有用。
  • 波浪的扰动用法线贴图来的更方便,只需要采样一次,如果用其他程序性方法性能和重复性都无法得到保证。