抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

草地 Shader,根据噪声图控制摆动,支持风向和风力调整,可交互。有若干可以控制的参数调整整体的效果。

原理

风场

利用噪声图保存的向量信息使顶点进行偏移。噪声图的颜色信息的 rgb 分量即对应方向向量的 xyz 分量。

在顶点着色器中,我们使用 tex2Dlod 方法对噪声图进行采样,采样的 uv 使用顶点的世界坐标,这样可以在所有模型上保持连贯性。

利用内置 _Time 参数进行采样数据的变化:

1
2
3
4
5
6
//计算世界坐标
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
//计算uv
float2 offsetUV = (worldPos.xz * 1 / _sampleScale) * _Time.x;
//采样
float4 sample = tex2Dlod(_sample, float4(offsetUV, 0, 0));

其中 _sampleScale 用于缩放采样尺度,越小变化越快。

为了能够改变风向,我们设置一个风向参数(范围为 0 - 360 度),因绕 Y 轴的旋转矩阵为:

$$
\left[
\begin{matrix}
cos(θ) & 0 & sin(θ) & 0 \\
0 & 1 & 0 & 0 \\
-sin(θ) & 0 & cos(θ) & 0 \\
0 & 0 & 0 & 1
\end{matrix}
\right]
$$

把风向转为弧度并填入其中就能得到该方向的旋转矩阵。

最后把旋转矩阵乘以偏移量就能得到正确的方向了。

为了使草地底部顶点不偏移,我们计算顶点相对于底部的高度,为了适配不同中心点的模型,我们单独设置一个参数 _Bottom 来校准。

1
2
3
4
5
6
7
8
//计算模型顶点相对于底部顶点的高度
float bottom = v.vertex.y - _Bottom;
//计算xz方向偏移
float2 offset = sample.xz * bottom * _WindStrength;
//改变xz顶点坐标
v.vertex.xz += offset * 0.1;
//改变y顶点坐标
v.vertex.y -= sample.y * bottom * 0.1 * _WindStrength;

顶点偏移的规则可以根据自己的需要修改。

交互

此处交互只做了单个对象的交互,使用全局坐标。多个对象可以使用 RenderTexture 来实现。

通过获取随意一个草地材质的 sharedMaterial 并设置 _GlobalPos 参数,我们就可以在所有的草地 shader 上同步交互物体坐标。

然后通过自定义的规则进行顶点交互偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取改变后顶点坐标的世界空间
float3 newWoldPos = mul(unity_ObjectToWorld, v.vertex);
//计算交互方向
float2 interactiveVec = newWoldPos.xz - _GlobalPos.xz;
//计算顶点与交互物体坐标的距离
float dist = length(interactiveVec);
//计算交互距离比率
float distRatio = clamp(0, 1, dist / _InteractiveRadius);
//计算交互方向法线
float2 interactiveOffset = normalize(interactiveVec.xy);
//计算最终交互向量
float2 interactiveOffsetFinal = clamp(0,0.4,interactiveOffset * (1 - distRatio) * bottom);
//更新顶点坐标
v.vertex.xz += interactiveOffsetFinal;
v.vertex.y -= bottom * (1 - distRatio);

如果有不止一种草地 Shader 的话,还是使用 RenderTexture 来实现比较好,以后有空再更新。

投影

投影需要新增一个 Pass,顶点着色器也要进行顶点变化,这样才能显示正确的阴影。

代码

草地Shader
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
Shader "Custom/Plant"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Bottom("Bottom",Range(-1,0)) = 0.0
_WindStrength("WindStrength",Range(0,45)) = 0.0
_WindDirection("WindDirection",Range(0,360)) = 0.0
_sample("sample (RGB)",2D) = "black" {}
_sampleScale("sampleScale",Range(0,100)) = 1.0
_InteractiveRadius("InteractiveRadius",Float) = 1.0
_GlobalPos("GlobalPos",Vector) = (0,0,0)
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}

Cull Back

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

sampler2D _MainTex;
sampler2D _sample;

float _WindStrength;
float _WindDirection;
float _sampleScale;

float _InteractiveRadius;
float3 _GlobalPos;
float _Bottom;

struct v2f
{
float4 pos : SV_POSITION;
fixed4 uv : TEXCOORD0;
fixed4 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
SHADOW_COORDS(3)
};

v2f vert(appdata_base v)
{
v2f o;
//计算世界坐标
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
//计算uv
float2 offsetUV = (worldPos.xz * 1 / _sampleScale) * _Time.x;
//采样
float4 sample = tex2Dlod(_sample, float4(offsetUV, 0, 0));

//风向
float rad = _WindDirection * UNITY_PI / 180;
float4x4 rot = float4x4(
cos(rad),0,sin(rad),0,
0,1,0,0,
-sin(rad),0,cos(rad),0,
0,0,0,1
);
//旋转采样
sample = mul(rot,sample);
//计算模型顶点相对于底部顶点的高度
float bottom = v.vertex.y - _Bottom;
//计算xz方向偏移
float2 offset = sample.xz * bottom * _WindStrength;
//改变xz顶点坐标
v.vertex.xz += offset * 0.1;
//改变y顶点坐标
v.vertex.y -= sample.y * bottom * 0.1 * _WindStrength;
//获取改变后顶点坐标的世界空间
float3 newWoldPos = mul(unity_ObjectToWorld, v.vertex);
//计算交互方向
float2 interactiveVec = newWoldPos.xz - _GlobalPos.xz;
//计算顶点与交互物体坐标的距离
float dist = length(interactiveVec);
//计算交互距离比率
float distRatio = clamp(0, 1, dist / _InteractiveRadius);
//计算交互方向法线
float2 interactiveOffset = normalize(interactiveVec.xy);
//计算最终交互向量
float2 interactiveOffsetFinal = clamp(0,0.4,interactiveOffset * (1 - distRatio) * bottom);
//更新顶点坐标
v.vertex.xz += interactiveOffsetFinal;
v.vertex.y -= bottom * (1 - distRatio);

o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.worldPos = worldPos;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// o.worldNormal = UnityObjectToWorldNormal(mul(rotX, mul(rotZ, v.normal)));

TRANSFER_SHADOW(o);
return o;
}

float4 frag(v2f i) : SV_Target
{
//texture采样
fixed4 color = tex2D(_MainTex, i.uv);
// color *= _Diffuse;

fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

fixed atten = 1.0; //光照衰减

fixed shadow = SHADOW_ATTENUATION(i);

return fixed4(color.xyz * (ambient + (diffuse + specular) * atten * shadow), 1.0);
}
ENDCG
}

Pass
{
Tags
{
"LightMode" = "ShadowCaster"
}

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"

struct v2f
{
V2F_SHADOW_CASTER;
// float4 pos : SV_POSITION;
// float3 worldPos : TEXCOORD0;
};

sampler2D _sample;

float _WindStrength;
float _WindDirection;
float _sampleScale;

float _InteractiveRadius;
float3 _GlobalPos;
float _Bottom;

v2f vert(appdata_base v)
{
v2f o;
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);

float2 offsetUV = (worldPos.xz * 1 / _sampleScale) * _Time.x;
//采样
float4 sample = tex2Dlod(_sample, float4(offsetUV, 0, 0));

float rad = _WindDirection * UNITY_PI / 180;
//风向
float4x4 rot = float4x4(
cos(rad),0,sin(rad),0,
0,1,0,0,
-sin(rad),0,cos(rad),0,
0,0,0,1
);

sample = mul(rot,sample);

float bottom = v.vertex.y - _Bottom;

float2 offset = sample.xz * bottom * _WindStrength;

v.vertex.xz += offset * 0.1;

v.vertex.y -= sample.y * bottom * 0.1 * _WindStrength;

float3 newWoldPos = mul(unity_ObjectToWorld, v.vertex);
//计算交互方向
float2 interactiveVec = newWoldPos.xz - _GlobalPos.xz;

//计算顶点与交互物体坐标的距离
float dist = length(interactiveVec);
//计算交互距离比率
float distRatio = clamp(0, 1, dist / _InteractiveRadius);

float2 interactiveOffset = normalize(interactiveVec.xy);
float2 interactiveOffsetFinal = clamp(0,0.4,interactiveOffset * (1 - distRatio) * bottom);

v.vertex.xz += interactiveOffsetFinal;
v.vertex.y -= bottom * (1 - distRatio);

TRANSFER_SHADOW_CASTER(o);
return o;
}

float4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
FallBack "Diffuse"
}
交互坐标更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using UnityEngine;

public class PosUpdate : MonoBehaviour
{
private Material _grass;

private void Awake()
{
//自己获取草地的sharedMaterial,此处偷懒直接查找了
_grass = GameObject.Find("uploads_files_3639591_Grass").transform.GetChild(0).GetComponent<Renderer>().sharedMaterial;
}

private void Update()
{
_grass.SetVector("_GlobalPos",transform.position);
}
}

项目工程

更新日志

2024-04-17

  1. 更新基础内容。

评论