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

实现思路
- 根据深度设定不同坡度下水的颜色
- 通过噪声和深度值获得池塘岸边表面的白色浪花
- 通过 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 对于渲染不透明物体很有用。
- 波浪的扰动用法线贴图来的更方便,只需要采样一次,如果用其他程序性方法性能和重复性都无法得到保证。