您现在的位置是:首页 >技术杂谈 >unity 实现水的波纹效果网站首页技术杂谈
unity 实现水的波纹效果
之前的实现过这个效果,可惜没有记笔记,所以现在有点遗忘,连多个波纹一起在水面上实现的效果都忘记了,所以,查看了下之前实现的代码,现在再记一下笔记。
基础的波纹效果
要实现波纹,首先要知道波纹的中心点,这个位置应该是世界空间坐标系的位置,然后还要知道扩散到了哪里,也需要传入,知道了这两个值以后,就可以实现一个最简易的效果,只需要通过脚本更新这两个值,就可以实现最简单的波纹扩散效果。
波纹中心点我命名为了_HitPos,扩散到了尺寸就命名为HitSize,在片元里面求出世界空间坐标位置和中心点的位置,然后再减去_HitSize,就可以得出一个扩散的圆,调节_HitSize的大小,就可以发现圆会变大变小。
half dis = distance(input.positionWS, _HitPos.xyz);
dis -= _HitSize;
里面黑外面白,用一下反值
里面白外面黑,这样我们就找到了散的边界,我们会返现有一片纯黑和纯白的区域,这一片区域就是大于1和小于0的区域,而边缘渐变的部分就是从0-1的渐变。我们做效果,其实就只需要这个0-1的范围即可,将其定义为波纹的宽度。
然后,我们再命名一个_HitSpread作为扩散范围的调整,用边界值去除以这个值,就去修改,但是我为了保证0-1的范围中心处于边缘,我这里使用smoothstep方法,获取了0到-1的区域,然后使用_HitSpread去控制拾取的范围值。0就是刚好对应尺寸的起点。
half disMask = distance(input.positionWS, _HitPos.xyz);
half range = -clamp((disMask - _HitSize) / _HitSpread, -1, 0);
这样保证了区域的设置。
有了这个值以后,美术可以准确的去设置具体的显示范围,然后我们再给一个贴图,可以去拾取一张渐变图,美术也就可以简单的调节具体如何去渐变。
这里我使用了这张Ramp图片,然后使用上面的值作为采样的x轴,y轴默认0.5,就可以准确拾取过渡值。
half disMask = distance(input.positionWS, _HitPos.xyz);
half range = -clamp((disMask - _HitSize) / _HitSpread, -1, 0);
half2 rampUV = half2(range, 0.5);
half ramp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).r;
采样出来的效果就是:
这种方法_HitSpread能准确的控制范围,而且_HitSize也能准确的控制移动的位置
half noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).r;
half disMask = distance(input.positionWS, _HitPos.xyz);
half range = -clamp((disMask - _HitSize + noise) / _HitSpread, -1, 0);
half2 rampUV = half2(range, 0.5);
half ramp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).r;
再加上一张扰动图进行偏移,让渐变没有这么的平滑
接下来实现了基本的效果,然后我们还需要设置一个终点,到终点位置以后,就渐渐的消失的效果,这个效果的实现,首先就是我们需要设置一个渐变消失位置_HitFadeDistance,然后用之前求出的范围值除以它,然后再设置_HitFadePower和这个值相乘,主要是为了防止它的效果太早的变淡。
half hitFade = saturate((1.0 - disMask / _HitFadeDistance) * _HitFadePower);
这个效果就是这样
然后用这个求出的值,再乘以之前的ramp,就实现了这个效果限制的显示范围。
就实现了我们所需要的效果。
实现脚本控制
既然实现了shader,但是位置和渐变的值需要我们手动传入啊。
为了实现这个效果,我们还需要设置全局变量,方便从脚本向shader传入变量。将_HitPos和_HitSize注销,这两个值我们需要从一个全局变量传入。这个全局变量前三个值设置为_HitPos的值,w通道则设置_HitSize的值,这样用一个四维向量就存储了两个值。
half4 HitParams;
声明一个HitParams,不要写在CBUFFER_START(UnityPerMaterial)里面,因为我们不需要合批,它们只是一个全局变量,然后还需要将Properties里面的值也注销掉,如果哪里设置了值,将会直接传入。
然后在运算前面重新声明它们
half3 _HitPos = HitParams.xyz;
half _HitSize = HitParams.w;
half noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).r;
half disMask = distance(input.positionWS, _HitPos.xyz);
half range = -clamp((disMask - _HitSize + noise) / _HitSpread, -1, 0);
half2 rampUV = half2(range, 0.5);
half ramp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).r;
half hitFade = saturate((1.0 - disMask / _HitFadeDistance) * _HitFadePower);
half hitResult = hitFade * ramp;
这样,就不用动原来代码,就实现。
接着是脚本,我声明了一个WaterRipple的脚本,
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode()] //在编辑模式下也可以运行
public class WaterRipple : MonoBehaviour
{
public string triggerTag = "ForceField";
private Vector4 HitPos;
private float HitSize;
private float clickTimer = 0.0f; //防止频繁触发间隔
void Start()
{
}
void RayCast()
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //从屏幕点击位置,在相机3D空间中发射一条射线
if (Physics.Raycast(ray, out hitInfo, 1000)) //通过射线拾取物体,拾取到返回true
{
if (hitInfo.transform.CompareTag(triggerTag)) //判断当前拾取到的物体的tag是否正确
{
Vector3 pos = hitInfo.point;
HitPos.x = pos.x;
HitPos.y = pos.y;
HitPos.z = pos.z;
}
}
}
// Update is called once per frame
void Update()
{
clickTimer += Time.deltaTime;
if (Input.GetMouseButtonDown(0))
{
if (clickTimer > 0.5f)
{
clickTimer = 0.0f;
RayCast();
}
}
//设置shader的全局变量
HitPos.w = clickTimer;
Shader.SetGlobalVector("HitParams", HitPos);
}
}
先附上代码,然后讲解里面主要的部分:
在update里面,首先对鼠标是否按下进行了判断,然后用一个数值进行放误触处理,这个值后面也可以设置一个可以修改的变量
clickTimer += Time.deltaTime;
if (Input.GetMouseButtonDown(0))
{
if (clickTimer > 0.5f)
{
clickTimer = 0.0f;
RayCast();
}
}
如果可以触发RayCast函数,那么,将调用此函数
void RayCast()
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //从屏幕点击位置,在相机3D空间中发射一条射线
if (Physics.Raycast(ray, out hitInfo, 1000)) //通过射线拾取物体,拾取到返回true
{
if (hitInfo.transform.CompareTag(triggerTag)) //判断当前拾取到的物体的tag是否正确
{
Vector3 pos = hitInfo.point;
HitPos.x = pos.x;
HitPos.y = pos.y;
HitPos.z = pos.z;
}
}
}
这个函数里面主要是获取了点击在场景中拾取到的位置,而且设置了一个tag判断,主要是防止不需要触发的物体也会触发此事件问题。
//设置shader的全局变量
HitPos.w = clickTimer;
Shader.SetGlobalVector("HitParams", HitPos);
最后则是在update函数里面设置全局变量,所有的shader只要声明了HitParams,都可以获取到此变量的值。
只要在平面上面设置一个ForceField的Tag,然后设置好材质,点击即可看到效果。但是这种设置,我们只能实现一个效果的点击,如果点击第二个,那么,第一个的效果将直接消失,接下来,我们将实现多个效果一起播放。
多个波纹的实现
在实现多个波纹的需求下,我们还需要继续修改Shader,将之前的参数作为数组传入,这样可以处理多个,然后循环计算得到结果。
int HitAmount;
half4 HitParams[20]; //xyz:pos w:size
首先声明三个新的变量,HitAmount代表循环次数,HitParams直接作为数组传入,就是可以实现当前的操作,然后将循环修改成一个函数:
half Hit(half hitNoise, half3 positionWS)
{
float hitResult;
//循环处理多个点的波纹
for (int i = 0; i < HitAmount; i++)
{
half disMask = distance(positionWS, HitParams[i].xyz);
half range = -clamp((disMask - HitParams[i].w + hitNoise) / _HitSpread, -1, 0);
half2 rampUV = half2(range, 0.5);
half ramp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).r;
half hitFade = saturate((1.0 - disMask / _HitFadeDistance) * _HitFadePower);
hitResult += hitFade * ramp;
}
return saturate(hitResult);
}
用来可以实现多个点的波纹,只是将HitPos和HitSize修改成了数组里面的值,这样就可以实现多个相加,然后最后限制到0-1的范围内即可。
然后是脚本修改,这次使用了粒子系统来模拟,渐变,然后获取到渐变的时间,然后乘以设置的大小,传入即可。
粒子系统的设置。
脚本的设置
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// [ExecuteInEditMode()] //在编辑模式下也可以运行
public class WaterRipple : MonoBehaviour
{
public ParticleSystem PS;
public string triggerTag = "ForceField";
public float HitSize = 1;
public int HitAmount; //当前粒子可以生成的粒子的个数
private Vector4[] HitParams;
private float clickTimer = 0.0f; //防止频繁触发间隔
private ParticleSystem.Particle[] particles; //用于获取粒子系统里面的粒子
void Start()
{
}
void RayCast()
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //从屏幕点击位置,在相机3D空间中发射一条射线
if (Physics.Raycast(ray, out hitInfo, 1000)) //通过射线拾取物体,拾取到返回true
{
if (hitInfo.transform.CompareTag(triggerTag)) //判断当前拾取到的物体的tag是否正确
{
PS.transform.position = hitInfo.point;
PS.Emit(1);
}
}
}
// Update is called once per frame
void Update()
{
clickTimer += Time.deltaTime;
if (Input.GetMouseButtonDown(0))
{
if (clickTimer > 0.2f)
{
clickTimer = 0.0f;
RayCast();
}
}
ParticleSystem.MainModule PSmain = PS.main;
PSmain.maxParticles = HitAmount; //设置最大粒子数
HitParams = new Vector4[HitAmount];
particles = new ParticleSystem.Particle[HitAmount];
PS.GetParticles(particles);
for (int i = 0; i < HitAmount; i++)
{
Vector3 pos = particles[i].position; //位置
float size = particles[i].GetCurrentSize(PS) * HitSize; //尺寸
HitParams[i].Set(pos.x, pos.y, pos.z, size);
}
Shader.SetGlobalVectorArray("HitParams", HitParams);
Shader.SetGlobalFloat("HitAmount", HitAmount);
}
}
脚本代码,会发现,我增加了一个HitAmount用来设置最多显示波浪的个数,然后从粒子系统拿到粒子的信息生成数组信息,传递给Shader的全局变量,其实我们也可以单独的传给Shader,这里我就不修改了。
下面,我列一下Shader的完整代码,基于URP的Unlit修改的:
Shader "Unlit/WaterRipple"
{
Properties
{
[MainTexture] _BaseMap("Texture", 2D) = "white" {}
[MainColor] _BaseColor("Color", Color) = (1, 1, 1, 1)
_Cutoff("AlphaCutout", Range(0.0, 1.0)) = 0.5
_NoiseTex("扰动图", 2D) = "white" {}
_RampTex("渐变图", 2D) = "white" {}
// _HitPos("点击位置", Vector) = (0, 0, 0, 0)
// _HitSize("波纹尺寸", Float) = 0
_HitSpread("扩散范围", Float) = 1
_HitFadeDistance("范围渐变", Float) = 6
_HitFadePower("范围渐变", Float) = 1
// BlendMode
_Surface("__surface", Float) = 0.0
_Blend("__mode", Float) = 0.0
_Cull("__cull", Float) = 2.0
[ToggleUI] _AlphaClip("__clip", Float) = 0.0
[HideInInspector] _BlendOp("__blendop", Float) = 0.0
[HideInInspector] _SrcBlend("__src", Float) = 1.0
[HideInInspector] _DstBlend("__dst", Float) = 0.0
[HideInInspector] _ZWrite("__zw", Float) = 1.0
}
SubShader
{
Tags {"RenderType" = "Opaque" "IgnoreProjector" = "True" "RenderPipeline" = "UniversalPipeline" "ShaderModel"="3.5"}
LOD 100
Blend [_SrcBlend][_DstBlend]
ZWrite [_ZWrite]
Cull [_Cull]
Pass
{
Name "Unlit"
HLSLPROGRAM
#pragma target 3.5
#pragma shader_feature_local_fragment _SURFACE_TYPE_TRANSPARENT
#pragma shader_feature_local_fragment _ALPHATEST_ON
#pragma shader_feature_local_fragment _ALPHAPREMULTIPLY_ON
// -------------------------------------
// Unity defined keywords
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3
#pragma multi_compile _ DEBUG_DISPLAY
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#include "./WaterRippleInput.hlsl"
#include "./WaterRippleForwardPass.hlsl"
ENDHLSL
}
Pass
{
Name "DepthOnly"
Tags{"LightMode" = "DepthOnly"}
ZWrite On
ColorMask 0
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment
// -------------------------------------
// Material Keywords
#pragma shader_feature_local_fragment _ALPHATEST_ON
//--------------------------------------
// GPU Instancing
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
ENDHLSL
}
Pass
{
Name "DepthNormalsOnly"
Tags{"LightMode" = "DepthNormalsOnly"}
ZWrite On
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex DepthNormalsVertex
#pragma fragment DepthNormalsFragment
// -------------------------------------
// Material Keywords
#pragma shader_feature_local_fragment _ALPHATEST_ON
// -------------------------------------
// Unity defined keywords
#pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT // forward-only variant
//--------------------------------------
// GPU Instancing
#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON
#include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitDepthNormalsPass.hlsl"
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
// CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.UnlitShader"
}
WaterRippleInput.hlsl
#ifndef UNIVERSAL_WATERRIPPLE_INPUT_INCLUDED
#define UNIVERSAL_WATERRIPPLE_INPUT_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _NoiseTex_ST;
half4 _BaseColor;
half _Cutoff;
half _Surface;
// half4 _HitPos;
// half _HitSize;
half _HitSpread;
half _HitFadeDistance;
half _HitFadePower;
CBUFFER_END
TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex);
TEXTURE2D(_RampTex); SAMPLER(sampler_RampTex);
int HitAmount;
half4 HitParams[20]; //xyz:pos w:size
#endif
WaterRippleForwardPass.hlsl
#ifndef URP_WATERRIPPLE_FORWARD_PASS_INCLUDED
#define URP_WATERRIPPLE_FORWARD_PASS_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Unlit.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 uv : TEXCOORD0;
float fogCoord : TEXCOORD1;
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD2;
float3 normalWS : TEXCOORD3;
float3 viewDirWS : TEXCOORD4;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
void InitializeInputData(Varyings input, out InputData inputData)
{
inputData = (InputData)0;
inputData.positionWS = input.positionWS;
inputData.normalWS = input.normalWS;
inputData.viewDirectionWS = input.viewDirWS;
inputData.shadowCoord = 0;
inputData.fogCoord = 0;
inputData.vertexLighting = half3(0, 0, 0);
inputData.bakedGI = half3(0, 0, 0);
inputData.normalizedScreenSpaceUV = 0;
inputData.shadowMask = half4(1, 1, 1, 1);
}
Varyings UnlitPassVertex(Attributes input)
{
Varyings output = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.positionCS = vertexInput.positionCS;
output.uv.xy = TRANSFORM_TEX(input.uv, _BaseMap);
output.uv.zw = TRANSFORM_TEX(input.uv, _NoiseTex);
#if defined(_FOG_FRAGMENT)
output.fogCoord = vertexInput.positionVS.z;
#else
output.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
#endif
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
half3 viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS);
// already normalized from normal transform to WS.
output.positionWS = vertexInput.positionWS;
output.normalWS = normalInput.normalWS;
output.viewDirWS = viewDirWS;
return output;
}
half Hit(half hitNoise, half3 positionWS)
{
float hitResult;
//循环处理多个点的波纹
for (int i = 0; i < HitAmount; i++)
{
half disMask = distance(positionWS, HitParams[i].xyz);
half range = -clamp((disMask - HitParams[i].w + hitNoise) / _HitSpread, -1, 0);
half2 rampUV = half2(range, 0.5);
half ramp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).r;
half hitFade = saturate((1.0 - disMask / _HitFadeDistance) * _HitFadePower);
hitResult += hitFade * ramp;
}
return saturate(hitResult);
}
half4 UnlitPassFragment(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
half2 uv = input.uv.xy;
half2 noiseUV = input.uv.zw;
half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
half3 color = texColor.rgb * _BaseColor.rgb;
half alpha = texColor.a * _BaseColor.a;
AlphaDiscard(alpha, _Cutoff);
#if defined(_ALPHAPREMULTIPLY_ON)
color *= alpha;
#endif
InputData inputData;
InitializeInputData(input, inputData);
SETUP_DEBUG_TEXTURE_DATA(inputData, input.uv, _BaseMap);
#ifdef _DBUFFER
ApplyDecalToBaseColor(input.positionCS, color);
#endif
#if defined(_FOG_FRAGMENT)
#if (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2))
float viewZ = -input.fogCoord;
float nearToFarZ = max(viewZ - _ProjectionParams.y, 0);
half fogFactor = ComputeFogFactorZ0ToFar(nearToFarZ);
#else
half fogFactor = 0;
#endif
#else
half fogFactor = input.fogCoord;
#endif
half4 finalColor = UniversalFragmentUnlit(inputData, color, alpha);
half noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).r;
// half disMask = distance(input.positionWS, _HitPos.xyz);
// half range = -clamp((disMask - _HitSize + noise) / _HitSpread, -1, 0);
// half2 rampUV = half2(range, 0.5);
// half ramp = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).r;
// half hitFade = saturate((1.0 - disMask / _HitFadeDistance) * _HitFadePower);
// half hitResult = hitFade * ramp;
half hitResult = Hit(noise, input.positionWS);
finalColor.rgb += hitResult;
finalColor.rgb = MixFog(finalColor.rgb, fogFactor);
// finalColor.rgb = hitFade;
return finalColor;
}
#endif
如果有问题,请及时联系我。