一,简介
本文在Catlike Coding 实现的【Custom SRP 2.5.0】基础上参考多方文章编写的风格化水的渲染
二,环境
Unity :2022.3.18f1
CRP Library :14.0.10
URP基本结构 :Custom SRP 2.5.0
三,实现
3.1 准备基础环境
准备基本shader脚本如下:
Shader "Custom/Water"
{
Properties
{
}
SubShader
{
LOD 100
Pass
{
Tags { "LightMode" = "CustomLit" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "../Custom RP/ShaderLibrary/Common.hlsl"
#include "../Custom RP/ShaderLibrary/Surface.hlsl"
#include "../Custom RP/ShaderLibrary/Shadows.hlsl"
#include "../Custom RP/ShaderLibrary/Light.hlsl"
// 顶点着色器输入结构
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
};
// 片元着色器输入结构
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : VAR_NORMAL;
float4 tangentWS : VAR_TANGENT;
};
Varyings vert (Attributes IN)
{
Varyings OUT;
OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = IN.uv;
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
OUT.tangentWS = float4(TransformObjectToWorldDir(IN.tangentOS.xyz), IN.tangentOS.w);
return OUT;
}
half4 frag (Varyings i) : SV_Target
{
return 1;
}
ENDHLSL
}
}
}
创建好Panel挂好材质
大概这个效果:

3.2 基本颜色
游戏里风格化的水基本是渲染一张有颜色的深度图,大概思路是用当前渲染点的深度与深度度里的深度做差值。
代码如下:
Shader "Custom/Water"
{
Properties
{
_Distance ("Distance", Float) = 1.0
_ShallowColor ("Shallow Color", Color) = (0.0, 0.5, 1.0, 1.0)
_DeepColor ("Deep Color", Color) = (0.0, 0.0, 0.5, 1.0)
}
SubShader
{
LOD 100
Pass
{
...................
#include "../Custom RP/ShaderLibrary/Common.hlsl"
#include "../Custom RP/ShaderLibrary/Surface.hlsl"
#include "../Custom RP/ShaderLibrary/Shadows.hlsl"
#include "../Custom RP/ShaderLibrary/Light.hlsl"
CBUFFER_START(UnityPerMaterial)
float _Distance;
float4 _ShallowColor;
float4 _DeepColor;
CBUFFER_END
.................
.................
float CustomSampleSceneDepth(float2 uv)
{
float depth = SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, uv, 0);
return depth;
}
float GetDepthFade(float3 WorldPos, float Distance)
{
float4 ScreenPosition = ComputeScreenPos(TransformWorldToHClip(WorldPos), _ProjectionParams.x);
float EyeDepth = LinearEyeDepth(CustomSampleSceneDepth(ScreenPosition.xy / ScreenPosition.w), _ZBufferParams);
return saturate((EyeDepth - ScreenPosition.a) / Distance);
}
//------------------Depth
.........................
half4 frag (Varyings i) : SV_Target
{
float depthFade = GetDepthFade(i.positionWS, _Distance);
float4 WaterColor = lerp(_ShallowColor, _DeepColor, depthFade);
return WaterColor ;
}
ENDHLSL
}
}
}
GetDepthFade 里会将深度差值简单限制在0~1这个范围,得到的效果如下:

直输深度图:

注意,渲染队列要在半透明队列上
3.3 水面顶点运动
通过在顶点着色器对顶点做周期偏移,让水面有运动的感觉。
代码如下:
Shader "Custom/Water"
{
Properties
{
.............
_WaterNoise ("WaterNoise", 2D) = "white" {}
_WaterSpeed ("WaterSpeed", Float) = 1.0
_WaterStrength ("WaterStrength", Float) = 1.0
_SceneMoveDepth ("SceneMoveDepth", Float) = 1.0
}
SubShader
{
Pass
{
CBUFFER_START(UnityPerMaterial)
............
float _WaterSpeed;
float _WaterStrength;
float _SceneMoveDepth;
CBUFFER_END
............
TEXTURE2D(_WaterNoise);
float4 _WaterNoise_ST;
SAMPLER(sampler_WaterNoise);
Varyings vert (Attributes IN)
{
Varyings OUT;
float2 NoiseUV = IN.uv * _WaterNoise_ST.xy + (_WaterSpeed * _TimeDelta).rr;//噪音图基础UV
OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);
float ReflactionSceneMoveDepth = GetDepthFade(OUT.positionWS, _SceneMoveDepth);//获得边缘深度图
float WaterNoise = SAMPLE_TEXTURE2D_LOD(_WaterNoise, sampler_WaterNoise,NoiseUV,0).r;
float noise = (WaterNoise - 0.5f )* saturate(1.0 - saturate(ReflactionSceneMoveDepth));//获得造影
IN.positionOS.y += noise * _WaterStrength;//对顶点的Y坐标进行位移
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = IN.uv;
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
OUT.tangentWS = float4(TransformObjectToWorldDir(IN.tangentOS.xyz), IN.tangentOS.w);
return OUT;
}
............
}
}
}
水面噪声贴图:

大概效果:

_TimeDelta 参数是外部传入,我在渲染管线的开始流程中加入了一个Pass用来设置一些初始化参数。
public class SetupBeForeLightPass
{
static readonly ProfilingSampler sampler = new("SetupBeForeLight");
static int
TimeDelta = Shader.PropertyToID("_TimeDelta");
void Render(RenderGraphContext context)
{
CommandBuffer buffer = context.cmd;
buffer.SetGlobalFloat(TimeDelta, Time.time);
context.renderContext.ExecuteCommandBuffer(context.cmd);
context.cmd.Clear();
}
public static void Record(
RenderGraph renderGraph)
{
using RenderGraphBuilder builder =
renderGraph.AddRenderPass(sampler.name, out SetupBeForeLightPass pass, sampler);
builder.AllowPassCulling(false);
builder.SetRenderFunc<SetupBeForeLightPass>(
static (pass, context) => pass.Render(context));
}
}
修改渲染管线流程:
public partial class CameraRenderer
............
using (renderGraph.RecordAndExecute(renderGraphParameters))
{
// Add passes here.
using var _ = new RenderGraphProfilingScope(renderGraph, cameraSampler);
SetupBeForeLightPass.Record(renderGraph);
LightResources lightResources = LightingPass.Record(renderGraph, cullingResults, shadowSettings, useLightsPerObject, cameraSettings.maskLights ? cameraSettings.renderingLayerMask : -1);
3.4 岸边边缘泡沫
水岸边缘的泡沫依旧可以用噪声贴图生成,代码如下:
Shader "Custom/Water"
{
Properties
{
............
_FoamSpeed ("FoamSpeed", Float) = 1.0
_FoamScale ("FoamScale", Float) = 1.0
_FoamAmount ("FoamAmount", Float) = 1.0
_FoamCutoff ("FoamCutoff", Float) = 1.0
_FoamAlpha ("FoamAlpha", Float) = 1.0
_FoamColor ("Foam Color", Color) = (0.0, 0.5, 1.0, 1.0)
}
SubShader
{
Pass
{
............
CBUFFER_START(UnityPerMaterial)
............
float _FoamSpeed;
float _FoamScale;
float _FoamAmount;
float _FoamCutoff;
float _FoamAlpha;
float4 _FoamColor;
CBUFFER_END
............
float2 UVMovement(float2 uv,float speed,float scale)
{
return uv * scale + speed * _TimeDelta;
}
............
half4 frag (Varyings i) : SV_Target
{
float depthFade = GetDepthFade(i.positionWS, _Distance);
float4 WaterColor = lerp(_ShallowColor, _DeepColor, depthFade);
//Water Filter
float2 FilterMoveUV = UVMovement(i.uv, _FoamSpeed, _FoamScale);
float FilterNoise = SAMPLE_TEXTURE2D(_WaterNoise, sampler_WaterNoise, FilterMoveUV).r;
float FilterSceneMoveDepth = GetDepthFade(i.positionWS, _FoamAmount) * _FoamCutoff;
float WaterFilter = step(FilterSceneMoveDepth, FilterNoise) * _FoamAlpha;
//Water Filter
float4 final_color = lerp(WaterColor, _FoamColor, WaterFilter);
return final_color;
}
ENDHLSL
}
}
}
效果:

3.5 水表面的波纹
水的表面主要靠一张法线贴图交替采样形成波纹效果,法线贴图可以让AI生成一张。

添加如下代码,主要是将采样点的法线从切线空间转换到对应的世界空间:
Properties
{
........
_WaveNormal ("WaveNormal", 2D) = "white" {}
_WaveParams ("WaveParams", Vector) = (1.0, 1.0, 1.0, 1.0)
_NormalScale ("NormalScale", Float) = 1.0
}
........
CBUFFER_START(UnityPerMaterial)
........
float4 _WaveParams;
float _NormalScale;
CBUFFER_END
........
TEXTURE2D(_WaveNormal);
float4 _WaveNormal_ST;
SAMPLER(sampler_WaveNormal);
........
float4 GetWaveNormal(float2 uv)
{
float4 color = SAMPLE_TEXTURE2D_LOD(_WaveNormal, sampler_WaveNormal, uv, 0);
return color;
}
half3 BlendNormals(half3 n1, half3 n2)
{
float3x3 nBasis = float3x3(
float3(n1.z, n1.y, -n1.x), // +90 degree rotation around y axis
float3(n1.x, n1.z, -n1.y), // -90 degree rotation around x axis
float3(n1.x, n1.y, n1.z));
float3 r = normalize(n2.x*nBasis[0] + n2.y*nBasis[1] + n2.z*nBasis[2]);
return r;
}
half4 frag (Varyings i) : SV_Target
{
........
//WaterNormal
half2 panner1 = ( _TimeDelta * _WaveParams.xy + i.uv);
half2 panner2 = ( _TimeDelta * _WaveParams.zw + i.uv);
half3 worldNormal = BlendNormals(UnpackNormal(GetWaveNormal(panner1)) , UnpackNormal(GetWaveNormal(panner2)));
worldNormal.xy *= _NormalScale;
worldNormal.z = sqrt(1.0 - saturate(dot(worldNormal.xy, worldNormal.xy)));
worldNormal = NormalTangentToWorld(worldNormal,i.normalWS, i.tangentWS);
//WaterNormal
float4 final_color = lerp(WaterColor, _FoamColor, WaterFilter);
return final_color ;
}
光照模型这边,采用的常见的Blinn_Phong 模型:
添加如下代码:
Properties
{
.......
_WaveStrength ("WaveStrength", Range(0,1)) = 1.0
_Shininess ("Shininess", Float) = 1.0
_SpecIntensity ("SpecIntensity", Float) = 1.0
}
CBUFFER_START(UnityPerMaterial)
.......
float _WaveStrength;
float _Shininess;
float _SpecIntensity;
CBUFFER_END
.......
half4 frag (Varyings i) : SV_Target
{
.......
//WaterNormal
float3 viewDir = normalize(_WorldSpaceCameraPos - i.positionWS);
float3 diffuse = 0;
float3 specular = 0;
for (int i = 0; i < GetDirectionalLightCount(); i++) {
DirectionalLightData data = _DirectionalLightData[i];
diffuse += data.color.rgb * max(_WaveStrength, dot(worldNormal, data.directionAndMask.xyz));
float3 halfDir = normalize(data.directionAndMask + viewDir);
float spec = pow(max(0, dot(worldNormal, halfDir)), _Shininess);
specular = spec * data.color.rgb * _SpecIntensity;
}
WaterColor.rgb = WaterColor.rgb * diffuse + specular;
float4 final_color = lerp(WaterColor, _FoamColor, WaterFilter);
final_color.rgb = lerp(SceneColor, final_color.rgb, final_color.a);
return final_color;
}
漫反射diffuse中我用_WaveStrength 提了一下暗部的亮度
漫反射:

高光:

最后效果:

参考参数:

不同法线贴图效果:

参考参数:

本文只是简单实现一个水体的渲染,还有不少渲染效果没有实现,比如SSR,SSS,焦散,近岸的折射,吃水线,水下渲染等
四,完整代码参考
Shader "Custom/Water_Test"
{
Properties
{
_Distance ("Distance", Float) = 1.0
_ShallowColor ("Shallow Color", Color) = (0.0, 0.5, 1.0, 1.0)
_DeepColor ("Deep Color", Color) = (0.0, 0.0, 0.5, 1.0)
_WaterNoise ("WaterNoise", 2D) = "white" {}
_WaterSpeed ("WaterSpeed", Float) = 1.0
_WaterStrength ("WaterStrength", Float) = 1.0
_SceneMoveDepth ("SceneMoveDepth", Float) = 1.0
_FoamSpeed ("FoamSpeed", Float) = 1.0
_FoamScale ("FoamScale", Float) = 1.0
_FoamAmount ("FoamAmount", Float) = 1.0
_FoamCutoff ("FoamCutoff", Float) = 1.0
_FoamAlpha ("FoamAlpha", Float) = 1.0
_FoamColor ("Foam Color", Color) = (0.0, 0.5, 1.0, 1.0)
_WaveNormal ("WaveNormal", 2D) = "white" {}
_WaveParams ("WaveParams", Vector) = (1.0, 1.0, 1.0, 1.0)
_NormalScale ("NormalScale", Float) = 1.0
_WaveStrength ("WaveStrength", Range(0,1)) = 1.0
_Shininess ("Shininess", Float) = 1.0
_SpecIntensity ("SpecIntensity", Float) = 1.0
}
SubShader
{
LOD 100
Pass
{
Tags { "LightMode" = "CustomLit" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "../Custom RP/ShaderLibrary/Common.hlsl"
#include "../Custom RP/ShaderLibrary/Surface.hlsl"
#include "../Custom RP/ShaderLibrary/Shadows.hlsl"
#include "../Custom RP/ShaderLibrary/Light.hlsl"
CBUFFER_START(UnityPerMaterial)
float _Distance;
float4 _ShallowColor;
float4 _DeepColor;
float _WaterSpeed;
float _WaterStrength;
float _SceneMoveDepth;
float _FoamSpeed;
float _FoamScale;
float _FoamAmount;
float _FoamCutoff;
float _FoamAlpha;
float4 _FoamColor;
float4 _WaveParams;
float _NormalScale;
float _WaveStrength;
float _Shininess;
float _SpecIntensity;
CBUFFER_END
TEXTURE2D(_WaterNoise);
float4 _WaterNoise_ST;
SAMPLER(sampler_WaterNoise);
TEXTURE2D(_WaveNormal);
float4 _WaveNormal_ST;
SAMPLER(sampler_WaveNormal);
// 顶点着色器输入结构
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
};
// 片元着色器输入结构
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : VAR_NORMAL;
float4 tangentWS : VAR_TANGENT;
};
//------------------Depth
float4 ComputeScreenPos(float4 pos, float projectionSign)
{
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y * projectionSign) + o.w;
o.zw = pos.zw;
return o;
}
float CustomSampleSceneDepth(float2 uv)
{
float depth = SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, uv, 0);
return depth;
}
float GetDepthFade(float3 WorldPos, float Distance)
{
float4 ScreenPosition = ComputeScreenPos(TransformWorldToHClip(WorldPos), _ProjectionParams.x);
float EyeDepth = LinearEyeDepth(CustomSampleSceneDepth(ScreenPosition.xy / ScreenPosition.w), _ZBufferParams);
return saturate((EyeDepth - ScreenPosition.a) / Distance);
}
//------------------Depth
float2 UVMovement(float2 uv,float speed,float scale)
{
return uv * scale + speed * _TimeDelta;
}
float4 GetWaveNormal(float2 uv)
{
float4 color = SAMPLE_TEXTURE2D_LOD(_WaveNormal, sampler_WaveNormal, uv, 0);
return color;
}
half3 BlendNormals(half3 n1, half3 n2)
{
float3x3 nBasis = float3x3(
float3(n1.z, n1.y, -n1.x), // +90 degree rotation around y axis
float3(n1.x, n1.z, -n1.y), // -90 degree rotation around x axis
float3(n1.x, n1.y, n1.z));
float3 r = normalize(n2.x*nBasis[0] + n2.y*nBasis[1] + n2.z*nBasis[2]);
return r;
}
Varyings vert (Attributes IN)
{
Varyings OUT;
float2 NoiseUV = IN.uv * _WaterNoise_ST.xy + (_WaterSpeed * _TimeDelta).rr;//噪音图基础UV
OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);
float ReflactionSceneMoveDepth = GetDepthFade(OUT.positionWS, _SceneMoveDepth);//获得边缘深度图
float WaterNoise = SAMPLE_TEXTURE2D_LOD(_WaterNoise, sampler_WaterNoise,NoiseUV,0).r;
float noise = (WaterNoise - 0.5f )* saturate(1.0 - saturate(ReflactionSceneMoveDepth));//获得造影
IN.positionOS.y += noise * _WaterStrength;//对顶点的Y坐标进行位移
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = IN.uv;
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
OUT.tangentWS = float4(TransformObjectToWorldDir(IN.tangentOS.xyz), IN.tangentOS.w);
return OUT;
}
half4 frag (Varyings i) : SV_Target
{
float depthFade = GetDepthFade(i.positionWS, _Distance);
float4 WaterColor = lerp(_ShallowColor, _DeepColor, depthFade);
//Water Filter
float2 FilterMoveUV = UVMovement(i.uv, _FoamSpeed, _FoamScale);
float FilterNoise = SAMPLE_TEXTURE2D(_WaterNoise, sampler_WaterNoise, FilterMoveUV).r;
float FilterSceneMoveDepth = GetDepthFade(i.positionWS, _FoamAmount) * _FoamCutoff;
float WaterFilter = step(FilterSceneMoveDepth, FilterNoise) * _FoamAlpha;
//Water Filter
//WaterNormal
half2 panner1 = ( _TimeDelta * _WaveParams.xy + i.uv);
half2 panner2 = ( _TimeDelta * _WaveParams.zw + i.uv);
half3 worldNormal = BlendNormals(UnpackNormal(GetWaveNormal(panner1)) , UnpackNormal(GetWaveNormal(panner2)));
worldNormal.xy *= _NormalScale;
worldNormal.z = sqrt(1.0 - saturate(dot(worldNormal.xy, worldNormal.xy)));
worldNormal = NormalTangentToWorld(worldNormal,i.normalWS, i.tangentWS);
//WaterNormal
float3 viewDir = normalize(_WorldSpaceCameraPos - i.positionWS);
float3 diffuse = 0;
float3 specular = 0;
for (int i = 0; i < GetDirectionalLightCount(); i++) {
DirectionalLightData data = _DirectionalLightData[i];
diffuse += data.color.rgb * max(_WaveStrength, dot(worldNormal, data.directionAndMask.xyz));
float3 halfDir = normalize(data.directionAndMask + viewDir);
float spec = pow(max(0, dot(worldNormal, halfDir)), _Shininess);
specular = spec * data.color.rgb * _SpecIntensity;
}
WaterColor.rgb = WaterColor.rgb * diffuse + specular;
float4 final_color = lerp(WaterColor, _FoamColor, WaterFilter);
return final_color;
}
ENDHLSL
}
}
}
参考链接: