[toc]

# 效果拆解

上图是一个现实中积水的地面的效果。那么该如何在图形中实现这个效果呢 首先我们可以将雨水拆解成无数的面片,然后将面片随机的分布在地表面上,如果想要控制雨水博文的纹路,可以自己用 Shader 或者贴图来修改雨水的形状

# 效果实现

# 1. ComputeShader 中计算面片的矩阵和时间

首先需要写一个 ComputeShader, 在 ComputeShader 中计算每个雨滴面片的随机位置 / 缩放等,并将其赋值给 4x4 的齐次转换矩阵,以及计算每个面片的随即速率。
然后将这些数据回读到 CPU

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
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWStructuredBuffer<float4x4> matrixBuffer; //计算用的坐标矩阵
RWStructuredBuffer<float2> timeSliceBuffer; //计时器
float _DeltaFlashSpeed; //脚本传入的每帧的数据 = Time.DeltaTime*X

//用于生成一个随机的平面位置
inline float2 LocalRand(float2 seed) {
return frac(cos(sin(seed) * 547.54584533) * 354.19757984) * 2 - 1;
}

//用于生成一个随机的计时器速度
inline float LocalRand(float seed) {
return frac(cos(sin(seed) * 431.2543) * 1548.9824) * 3254;
}

[numthreads(1023,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
if (timeSliceBuffer[id.x].x > 1) {
timeSliceBuffer[id.x].x = 0;
timeSliceBuffer[id.x].y = 0.8 + LocalRand(timeSliceBuffer[id.x].y) * 0.4;
float4x4 mtx = matrixBuffer[id.x];
float2 seed = float2(mtx._m03, mtx._m13);
seed = LocalRand(seed);
mtx._m03 = seed.x;//m03和m13是二维齐次矩阵中的XY位置
mtx._m13 = seed.y;
matrixBuffer[id.x] = mtx;
}
timeSliceBuffer[id.x].x += _DeltaFlashSpeed * timeSliceBuffer[id.x].y;
}

# 2. 回读 CS 的数据,渲染到 RenderTexture 上

这一点其实很简单,就是把所有的面片绘制到 RenderTexture 上就行了。
- 先把数据从 GPU 回读到 CPU 的转换矩阵列表 Matrix 中 - 使用 GpuDrawMeshInstanced 接口将 Matrix 传入

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class RainWater : MonoBehaviour
{
private static Mesh m_mesh;
public static Mesh fullScreenMesh
{
get
{
if(m_mesh != null)
{
return m_mesh;
}
m_mesh = new Mesh();
m_mesh.vertices = new Vector3[]
{
new Vector3(-1,-1,0),
new Vector3(-1,1,0),
new Vector3(1,1,0),
new Vector3(1,-1,0),
};
m_mesh.uv = new Vector2[]
{
new Vector3(0,1),
new Vector3(0,0),
new Vector3(1,0),
new Vector3(1,1),
};

m_mesh.SetIndices(new int[] { 0, 1, 2, 3 }, MeshTopology.Quads, 0);
return m_mesh;
}
}


public RenderTexture targetTexture;
public ComputeShader shader;
public Material mat;
public float timeSpeed = 1.25f;
private CommandBuffer buffer;
private Matrix4x4[] matrix = new Matrix4x4[COUNT];
private Vector2[] times = new Vector2[COUNT];
private Material gausBlurMat;
private int kernal;
private ComputeBuffer matrixBuffers;
private ComputeBuffer timeSliceBuffers;


const int COUNT = 1023;
const float SCALE = 0.02f;

private void Awake()
{
buffer = new CommandBuffer();
for(int i = 0; i < COUNT; ++i)
{
times[i] = new Vector2(Random.Range(-1f, 1f), Random.Range(0.8f, 1.2f));
matrix[i] = Matrix4x4.identity;
matrix[i].m00 = SCALE; //缩放
matrix[i].m11 = SCALE;
matrix[i].m22 = SCALE;
matrix[i].m03 = Random.Range(-1f,1f); //位置
matrix[i].m13 = Random.Range(-1f, 1f);
}

matrixBuffers = new ComputeBuffer(COUNT,64);
matrixBuffers.SetData(matrix);
timeSliceBuffers = new ComputeBuffer(COUNT, 8);
timeSliceBuffers.SetData(times);
kernal = shader.FindKernel("CSMain");
shader.SetBuffer(kernal, ShaderIDs.matrixBuffer, matrixBuffers);
shader.SetBuffer(kernal, ShaderIDs.timeSliceBuffer, timeSliceBuffers);
gausBlurMat = new Material(Shader.Find("Hidden/GaussianBlur"));
mat.SetBuffer(ShaderIDs.timeSliceBuffer,timeSliceBuffers);
}


private void Update()
{
shader.SetFloat(ShaderIDs._DeltaFlashSpeed,Time.deltaTime * timeSpeed);
shader.Dispatch(kernal, COUNT, 1, 1);
matrixBuffers.GetData(matrix);
buffer.Clear();
buffer.SetRenderTarget(targetTexture);
buffer.ClearRenderTarget(true, true, new Color(0.5f, 0.5f, 1, 1));

buffer.DrawMeshInstanced(fullScreenMesh,0,mat,0,matrix);
buffer.GetTemporaryRT(ShaderIDs._TempTexture, targetTexture.descriptor);
buffer.Blit(targetTexture, ShaderIDs._TempTexture, gausBlurMat, 0);
buffer.Blit(ShaderIDs._TempTexture, targetTexture, gausBlurMat, 1);
Graphics.ExecuteCommandBuffer(buffer);
}
}

# 3. 渲染 Shader

将回读后的时间数据绑定到渲染 Shader 上就行了,转换矩阵不需要做处理,直接 mul (matrix,vertex) 就可以了

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
Shader "Unlit/Wave"
{
SubShader
{
Tags { "RenderType"="Opaque" }
ZWrite Off
ZTest Always
Cull Off
Blend oneMinusSrcAlpha srcAlpha

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
#pragma target 5.0
#define MAXCOUNT 1023
StructuredBuffer<float2> timeSliceBuffer;


struct appdata
{
float4 vertex : POSITION;
float4 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f {
float4 vertex : SV_POSITION;
float timeSlice : TEXCOORD0;
float2 uv : TEXCPPRD1;
};

v2f vert(appdata v, uint instanceID : SV_InstanceID)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
o.vertex = mul(unity_ObjectToWorld, v.vertex);
o.timeSlice = timeSliceBuffer[instanceID].x;
o.uv = v.uv;
return o;
}

#define PI 18.84955592153876
float4 frag(v2f i) : SV_Target
{
float4 c = 1;
float2 dir = i.uv - 0.5;
float len = length(dir);
bool ignore = len > 0.5;
dir /= max(len, 1e-5);
c.xy = (dir * sin(-i.timeSlice * PI + len * 20)) * 0.5 + 0.5;
c.a = ignore ? 1 : i.timeSlice;
return c;
}
ENDCG
}
}
}

# 最终效果

# 参考

MaxwellGeng - Unity3D 水特效之雨天模拟(一)