Unity基于自定义管线实现风格化水

一,简介

本文在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
        }
    }
}

参考链接:

【用Shader Graph创建风格化水体Shader | URP】

【Unity Shader】Plane实现风格化水

【Unity URP 风格化水】

【Unity URP】风格化水体渲染

【Unity , shader】原神の海を再現したい

相关推荐
WMX10122 小时前
Unity-登录界面UI制作
ui·unity·游戏引擎
Kurisu5755 小时前
深海迷航2修改器 2026.5.16最新破解版加修改器免费下载 一键转存 永久更新 (看到速转存 资源随时走丢)
游戏·游戏引擎·游戏程序·修改器·关卡设计
吾日吾身三摆烂7 小时前
Unity协程(Coroutine)底层原理全解析
unity·游戏引擎
LF男男7 小时前
StarBullect.cs
unity
UWA8 小时前
Unity小游戏优化简谱 | 吃透底层逻辑,告别掉帧与流失
unity·性能优化·游戏引擎·小游戏开发
Unity-Plane8 小时前
QClaw 的再一次的深度体验
unity
归真仙人10 小时前
【UE】Lightmass可执行文件已经过时
ue5·游戏引擎·ue4·虚幻·unreal engine
scott.cgi15 小时前
Unity直接编译Java文件作为插件,导致失败的两个打包设置
java·unity·unity调用java·unity的java文件·unity的android插件·unity调用android·unity加载java代码
WiChP1 天前
【V0.1B9】从零开始的2D游戏引擎开发之路
c++·游戏引擎