Dynamic Grass – 动态草地制作
制作随风摇摆的草地,并且草碰到障碍物会弯折。
先看看Demo效果:
- 自定义草皮尺寸。
- 调整风向风速,对草皮的影响效果。
Unity发布的webGL不支持复杂For循环语句的shader,所以上图demo里只有主角跟草有交互。实际上可以多个对象跟草交互的。而且webGL里也不支持ShadowCaster,所以影子也无法显示。其他主流平台都是可以正常运行的。
分析一下要实现的内容:
- 每根草根据风速随机摇摆。
- 根据风的方向,草向一边倾倒。
- 碰到障碍物时草会弯曲。
然后开工,一个个实现:
首先做个草的模型
为了方便后面测试,我搞了几种不同的草。
然后代码生成一块草皮
- 自定义草皮尺寸
- 用Instantiate的方式创建一堆草,用二维矩阵创建草,加一点随机偏移,并且随机设置草的随机大小,方向。
- 然后通过Mesh.CombineMeshes把草合并成一个Mesh。
开始写shader
从草根到草头,通过一张上下渐变的贴图获得Y轴向的梯度值,用来控制偏移率,让草根不动,草身摇摆。 每根草根据风速随机摇摆
- 随机摇摆的核心是利用了每根草的xz坐标作为随机因数导入sin和cos的函数里。
- 这段shader还用了泰勒展开式的一些技巧(这部分抄自网络),让草浪有此起彼伏的效果,这段我也搞不太懂。我删减了一些代码,搞简单了一点。
- 草的摇摆除了X轴和Z轴偏移,也需要Y轴的偏移才能体现出草以根部为圆心摇摆的效果。Y的偏移值根据XZ轴的偏移计算获得。
根据风的方向,草向一边倾倒
- 风的方向由外部倒入,换算成normalized的Vecter2值。
- 根据风速大小,偏移ZX和Y三个轴向。
碰到障碍物时草会弯曲
- 障碍物的坐标通过“Mesh.SetGlobalVector”由外部输入。多个物体的坐标通过“SetGlobalVectorArray”的方式输入,并且shader里也做循环处理。
- 通过障碍物与草顶点坐标的距离差,计算得到草顶点偏移的值,并且带上Y轴偏移。
- 用一张上下渐变的贴图获得Y轴向的梯度值,偏移值乘以这个梯度值,即可得到草逐渐弯曲的效果。(这个梯度值直接共用上面那个)
- 设置障碍物的最高点和最低点,让角色跳跃的时候草也有交互效果。
这样shader中草的动态基本就完成了。以上用了不少参数,要调到合适的效果也要花点时间。
最后附上shader代码。
Shader "WalkingFat/DynamicGrass"
{
Properties
{
// _MainTex ("Texture", 2D) = "white" {}
_GrassColorTop ("Grass Color Top", Color) = (1 ,1 ,1 ,1)
_GrassColorBottom ("Grass Color Bottom", Color) = (1 ,1 ,1 ,1)
_ShadowColor ("Shadow Color", Color) = (1 ,1 ,1 ,1)
_GradientTex ("Gradient Tex", 2D) = "white" {}
// grass hit obstacle
_EffectTopOffset ("Effect Top Offset", float) = 2
_EffectBottomOffset ("Effect Bottom Offseth", float) = -1
_OffsetGradientStrength ("Offset Gradient Strength", range (0,1)) = 0.7
_OffsetFixedRoots ("Offset Fixed Roots", range (0,1)) = 1
_OffsetMultiplier ("Offset Multiplier", range (0.1,2)) = 2
_GravityGradientStrength ("Gravity Gradient Strength", range (0,1)) = 0.7
_GravityFixedRoots ("Gravity Fixed Roots", range (0,1)) = 0.7
_GravityMultiplier ("Gravity Multiplier", range (0,1)) = 0.7
// shake with wind
_ShakeWindspeed ("Shake Wind speed", float) = 0
// _ShakeBending ("Shake Bending", float) = 0
// _WindDirectionX ("Wind Direction X", range (-1,1)) = 0
// _WindDirectionZ ("Wind Direction Z", range (-1,1)) = 0
// _WindStrength ("Wind Strength", range (0,2)) = 0.5
_WindDirRate ("Wind Direction Rate", float) = 0.5
}
SubShader
{
Tags
{
"RenderType"="Opaque"
}
LOD 100
Pass
{
Tags { "LightMode" = "ForwardBase" }
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
// make light work
#pragma multi_compile_fwdbase
#include "AutoLight.cginc"
// make shadow work
#pragma multi_compile_fwdbase_fullshadows
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 posWorld : TEXCOORD1;
float4 pos : SV_POSITION;
UNITY_FOG_COORDS(2)
LIGHTING_COORDS(3,4)
};
// basic
float4 _GrassColorTop, _GrassColorBottom, _ShadowColor;
sampler2D _MainTex, _GradientTex;
float4 _MainTex_ST, _GradientTex_ST;
// obstacles
float _PositionArray;
float3 _ObstaclePositions[100];
// grass bend
float _EffectRadius, _BendAmount, _EffectTopOffset, _EffectBottomOffset, _OffsetGradientStrength;
float _OffsetFixedRoots, _OffsetMultiplier, _GravityGradientStrength, _GravityFixedRoots, _GravityMultiplier;
float _ShakeDisplacement, _ShakeWindspeed, _ShakeBending, _WindDirRate;
float _WindDirectionX, _WindDirectionZ, _WindStrength;
void FastSinCos (float4 val, out float4 s, out float4 c)
{
val = val * 6.408849 - 3.1415927;
// powers for taylor series
float4 r5 = val * val;
float4 r6 = r5 * r5;
float4 r7 = r6 * r5;
float4 r8 = r6 * r5;
float4 r1 = r5 * val;
float4 r2 = r1 * r5;
float4 r3 = r2 * r5;
//Vectors for taylor's series expansion of sin and cos
float4 sin7 = {1, -0.16161616, 0.0083333, -0.00019841};
float4 cos8 = {-0.5, 0.041666666, -0.0013888889, 0.000024801587};
// sin
s = val + r1 * sin7.y + r2 * sin7.z + r3 * sin7.w;
// cos
c = 1 + r5 * cos8.x + r6 * cos8.y + r7 * cos8.z + r8 * cos8.w;
}
v2f vert (appdata v)
{
v2f o;
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _GradientTex);
// get bend rate --------------------------
fixed4 grandientCol = tex2Dlod (_GradientTex, float4 (TRANSFORM_TEX(v.uv, _GradientTex), 0.0, 0.0));
float grandient = lerp (grandientCol.g, 1, 1 - _OffsetGradientStrength);
float xzOffset = lerp (o.uv.y * grandient, 1, 1 - _OffsetFixedRoots); // holding the roots sticking at their position
// get gravity rate with grass height --------------------------
float gravityCurvature = lerp (grandientCol.g, 1, 1 - _GravityGradientStrength);
float yOffset = lerp (o.uv.y * gravityCurvature, 1, 1 - _GravityFixedRoots);
float3 yMultiplier = float3 (0,-1,0) * _GravityMultiplier;
float2 gravityDistRate = float2 (0,0);
// waving force by wind ==========================================================
const float _WindSpeed = _ShakeWindspeed;
const float4 _waveXSize = float4 (0.048, 0.06, 0.24, 0.096);
const float4 _waveZSize = float4 (0.024, 0.08, 0.08, 0.2);
const float4 waveSpeed = float4 (1.2, 2, 1.6, 4.8);
float4 _waveXmove = float4 (0.024, 0.04, -0.12, 0.096);
float4 _waveZmove = float4 (0.006, 0.02, -0.02, 0.1);
float4 waves;
waves = v.vertex.x * _waveXSize;
waves += v.vertex.z * _waveZSize;
waves += _Time.x * waveSpeed * _WindSpeed + v.vertex.x + v.vertex.z;
float4 s, c;
waves = frac (waves);
FastSinCos (waves, s, c);
float waveAmount = v.uv.y * _ShakeBending;
s *= waveAmount;
s *= normalize(waveSpeed);
float fade = dot (s, 1.3);
float3 waveMove = float3 (0, 0, 0);
float windDirX = _WindDirectionX * _WindStrength;
float windDirZ = _WindDirectionZ * _WindStrength;
waveMove.x = dot (s, _waveXmove * windDirX);
waveMove.z = dot (s, _waveZmove * windDirZ);
float3 windDirOffset = float3 (windDirX * _WindDirRate, 0, windDirZ * _WindDirRate) * xzOffset;
gravityDistRate += float2 (windDirX, windDirZ);
float3 waveForce = -mul ((float3x3)unity_WorldToObject, waveMove).xyz * xzOffset + windDirOffset;
v.vertex.xyz += waveForce;
// ===========================================================================
for (int n = 0; n < _PositionArray; n++){
// get char top and bottom pos as effect range in Y.
float charTopY = _ObstaclePositions[n].y + _EffectTopOffset;
float charBottomY = _ObstaclePositions[n].y + _EffectBottomOffset;
float charY = clamp (o.posWorld.y, charBottomY, charTopY); // when Y is within the Range (charTopY to charBottomY)
// get bend force by distance --------------------------
float dist = distance (float3(_ObstaclePositions[n].x, charY, _ObstaclePositions[n].z), o.posWorld.xyz); // get distance of char and vertices
float effectRate = clamp (_EffectRadius - dist, 0, _EffectRadius); // get bend rate within 0 ~ max
float3 bendDir = normalize (o.posWorld.xyz - float3 (_ObstaclePositions[n].x, o.posWorld.y, _ObstaclePositions[n].z)); // get bend dir
float3 bendForce = bendDir * effectRate;
gravityDistRate += float2 (o.posWorld.x - _ObstaclePositions[n].x, o.posWorld.z - _ObstaclePositions[n].z) * effectRate;
// get final bend force
float3 finalBendForce = xzOffset * bendForce * _OffsetMultiplier;
// set bend force to vertices offset ================================================
v.vertex.xyz += finalBendForce;
}
float gravityForce = length (gravityDistRate);
v.vertex.xyz += gravityForce * yMultiplier * yOffset;
o.pos = UnityObjectToClipPos(v.vertex);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
// using fog
UNITY_TRANSFER_FOG (o, o.pos);
TRANSFER_VERTEX_TO_FRAGMENT (o); // make light work
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 Gradient = tex2D(_GradientTex, i.uv);
fixed4 col = lerp (_GrassColorBottom, _GrassColorTop, Gradient.g);
// apply light and shadow
float attenuation = LIGHT_ATTENUATION(i);
fixed4 shadowCol = lerp (_ShadowColor, fixed4 (1,1,1,1), attenuation);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col * shadowCol;
}
ENDCG
}
Pass {
Name "ShadowCaster"
Tags {
"LightMode"="ShadowCaster"
}
Offset 1, 1
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_SHADOWCASTER
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma multi_compile_shadowcaster
#pragma multi_compile_fog
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 posWorld : TEXCOORD1;
float4 pos : SV_POSITION;
};
// basic
sampler2D _GradientTex;
float4 _GradientTex_ST;
// obstacles
float _PositionArray;
float3 _ObstaclePositions[100];
// grass bend
float _EffectRadius, _BendAmount, _EffectTopOffset, _EffectBottomOffset, _OffsetGradientStrength;
float _OffsetFixedRoots, _OffsetMultiplier, _GravityGradientStrength, _GravityFixedRoots, _GravityMultiplier;
float _ShakeDisplacement, _ShakeWindspeed, _ShakeBending, _WindDirRate;
float _WindDirectionX, _WindDirectionZ, _WindStrength;
void FastSinCos (float4 val, out float4 s, out float4 c)
{
val = val * 6.408849 - 3.1415927;
// powers for taylor series
float4 r5 = val * val;
float4 r6 = r5 * r5;
float4 r7 = r6 * r5;
float4 r8 = r6 * r5;
float4 r1 = r5 * val;
float4 r2 = r1 * r5;
float4 r3 = r2 * r5;
//Vectors for taylor's series expansion of sin and cos
float4 sin7 = {1, -0.16161616, 0.0083333, -0.00019841};
float4 cos8 = {-0.5, 0.041666666, -0.0013888889, 0.000024801587};
// sin
s = val + r1 * sin7.y + r2 * sin7.z + r3 * sin7.w;
// cos
c = 1 + r5 * cos8.x + r6 * cos8.y + r7 * cos8.z + r8 * cos8.w;
}
v2f vert (appdata v)
{
v2f o;
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _GradientTex);
// get bend rate --------------------------
fixed4 grandientCol = tex2Dlod (_GradientTex, float4 (TRANSFORM_TEX(v.uv, _GradientTex), 0.0, 0.0));
float grandient = lerp (grandientCol.g, 1, 1 - _OffsetGradientStrength);
float xzOffset = lerp (o.uv.y * grandient, 1, 1 - _OffsetFixedRoots); // holding the roots sticking at their position
// get gravity rate with grass height --------------------------
float gravityCurvature = lerp (grandientCol.g, 1, 1 - _GravityGradientStrength);
float yOffset = lerp (o.uv.y * gravityCurvature, 1, 1 - _GravityFixedRoots);
float3 yMultiplier = float3 (0,-1,0) * _GravityMultiplier;
float2 gravityDistRate = float2 (0,0);
// waving force by wind ==========================================================
const float _WindSpeed = _ShakeWindspeed;
const float4 _waveXSize = float4 (0.048, 0.06, 0.24, 0.096);
const float4 _waveZSize = float4 (0.024, 0.08, 0.08, 0.2);
const float4 waveSpeed = float4 (1.2, 2, 1.6, 4.8);
float4 _waveXmove = float4 (0.024, 0.04, -0.12, 0.096);
float4 _waveZmove = float4 (0.006, 0.02, -0.02, 0.1);
float4 waves;
waves = v.vertex.x * _waveXSize;
waves += v.vertex.z * _waveZSize;
waves += _Time.x * waveSpeed * _WindSpeed + v.vertex.x + v.vertex.z;
float4 s, c;
waves = frac (waves);
FastSinCos (waves, s, c);
float waveAmount = v.uv.y * _ShakeBending;
s *= waveAmount;
s *= normalize(waveSpeed);
float fade = dot (s, 1.3);
float3 waveMove = float3 (0, 0, 0);
float windDirX = _WindDirectionX * _WindStrength;
float windDirZ = _WindDirectionZ * _WindStrength;
waveMove.x = dot (s, _waveXmove * windDirX);
waveMove.z = dot (s, _waveZmove * windDirZ);
float3 windDirOffset = float3 (windDirX * _WindDirRate, 0, windDirZ * _WindDirRate) * xzOffset;
gravityDistRate += float2 (windDirX, windDirZ);
float3 waveForce = -mul ((float3x3)unity_WorldToObject, waveMove).xyz * xzOffset + windDirOffset;
v.vertex.xyz += waveForce;
// ==================================================================================
for (int n = 0; n < _PositionArray; n++){
// get char top and bottom pos as effect range in Y.
float charTopY = _ObstaclePositions[n].y + _EffectTopOffset;
float charBottomY = _ObstaclePositions[n].y + _EffectBottomOffset;
float charY = clamp (o.posWorld.y, charBottomY, charTopY); // when Y is within the Range (charTopY to charBottomY)
// get bend force by distance --------------------------
float dist = distance (float3(_ObstaclePositions[n].x, charY, _ObstaclePositions[n].z), o.posWorld.xyz); // get distance of char and vertices
float effectRate = clamp (_EffectRadius - dist, 0, _EffectRadius); // get bend rate within 0 ~ max
float3 bendDir = normalize (o.posWorld.xyz - float3 (_ObstaclePositions[n].x, o.posWorld.y, _ObstaclePositions[n].z)); // get bend dir
float3 bendForce = bendDir * effectRate;
gravityDistRate += float2 (o.posWorld.x - _ObstaclePositions[n].x, o.posWorld.z - _ObstaclePositions[n].z) * effectRate;
// get final bend force
float3 finalBendForce = xzOffset * bendForce * _OffsetMultiplier;
// set bend force to vertices offset ================================================
v.vertex.xyz += finalBendForce;
}
float gravityForce = length (gravityDistRate);
v.vertex.xyz += gravityForce * yMultiplier * yOffset;
o.pos = UnityObjectToClipPos(v.vertex);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
TRANSFER_SHADOW_CASTER (o); // make light work
return o;
}
float4 frag(v2f i) : COLOR {
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
Fallback "VertexLit"
}
C#代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WalkingFat;
namespace WalkingFat {
// [ExecuteInEditMode]
public class DynamicGrass : MonoBehaviour {
public float effectRadius = 2f;
public GameObject grassModel;
public Material grassMat;
public float grassAreaWidth = 8f;
public float grassAreaLength = 6f;
public float grassDensity = 5f;
public float grassSize = 1f;
public float grassHeight = 1f;
public float windAngle = 0f;
public float windStrength = 1f;
private Vector2 windDir = new Vector2 (0,0);
public Transform[] obstacles;
private Vector4[] obstaclePositions = new Vector4[100];
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
// check grass size change
private float prevGrassAreaWidth;
private float prevGrassAreaLength;
private float prevGrassSize;
private float prevGrassHeight;
private GameObject prevGrassModel;
// Use this for initialization
void Start () {
meshFilter = gameObject.AddComponent<MeshFilter> () as MeshFilter;
meshRenderer = gameObject.AddComponent<MeshRenderer> () as MeshRenderer;
meshRenderer.material = grassMat;
InitializeGrass ();
prevGrassAreaWidth = grassAreaWidth;
prevGrassAreaLength = grassAreaLength;
prevGrassSize = grassSize;
prevGrassHeight = grassHeight;
prevGrassModel = grassModel;
}
// Update is called once per frame
void Update () {
// send data to grass shader
for (int n = 0; n < obstacles.Length; n++) {
obstaclePositions [n] = obstacles [n].position;
}
Shader.SetGlobalFloat("_PositionArray", obstacles.Length);
Shader.SetGlobalVectorArray("_ObstaclePositions", obstaclePositions);
Shader.SetGlobalVector("_ObstaclePosition", obstaclePositions[0]); // for H5
Shader.SetGlobalFloat("_EffectRadius", effectRadius);
windDir = GetWindDir (windAngle);
Shader.SetGlobalFloat ("_WindDirectionX", windDir.x);
Shader.SetGlobalFloat ("_WindDirectionZ", windDir.y);
Shader.SetGlobalFloat ("_WindStrength", windStrength);
float shakeBending = Mathf.Lerp (0.5f, 2f, windStrength);
Shader.SetGlobalFloat ("_ShakeBending", shakeBending);
// check grass size change
if (prevGrassAreaWidth != grassAreaWidth || prevGrassAreaLength != grassAreaLength
|| prevGrassSize != grassSize || prevGrassHeight != grassHeight || prevGrassModel != grassModel) {
InitializeGrass ();
prevGrassAreaWidth = grassAreaWidth;
prevGrassAreaLength = grassAreaLength;
prevGrassSize = grassSize;
prevGrassHeight = grassHeight;
prevGrassModel = grassModel;
}
}
void InitializeGrass () {
int tempGrassNumW = Mathf.FloorToInt (grassAreaWidth * grassDensity);
int tempGrassNumL = Mathf.FloorToInt (grassAreaLength * grassDensity);
float tempPosInterval = 1f / grassDensity;
float tempPosStartX = grassAreaWidth * -0.5f;
float tempPosStartZ = grassAreaLength * -0.5f;
Vector3[] tempGrassPosList = new Vector3[tempGrassNumW * tempGrassNumL];
GameObject[] temgGrassObjList = new GameObject[tempGrassNumW * tempGrassNumL];
CombineInstance[] combine = new CombineInstance[tempGrassNumW * tempGrassNumL];
float tempPosOffset = tempPosInterval * 0.3f;
for (int w = 0; w < tempGrassNumW; w++) {
for (int l = 0; l < tempGrassNumL; l++) {
tempGrassPosList [w + l * tempGrassNumW] = new Vector3 (
tempPosStartX + w * tempPosInterval,
transform.position.y,
tempPosStartZ + l * tempPosInterval
) + new Vector3 (
Random.Range (-tempPosOffset, tempPosOffset),
0,
Random.Range (-tempPosOffset, tempPosOffset)
);
temgGrassObjList [w + l * tempGrassNumW] = Tool_Spawn.SpawnObj (
true, grassModel, gameObject, "Grass_" + (w + l * tempGrassNumW).ToString(),
tempGrassPosList [w + l * tempGrassNumW], new Vector3 (0, Random.Range(0,360), 0), false
);
temgGrassObjList [w + l * tempGrassNumW].transform.localScale = new Vector3 (grassSize, Random.Range(0.7f, 1.3f) * grassHeight, grassSize);
MeshFilter mf = temgGrassObjList [w + l * tempGrassNumW].GetComponent<MeshFilter> () as MeshFilter;
combine [w + l * tempGrassNumW].mesh = mf.sharedMesh;
combine[w + l * tempGrassNumW].transform = temgGrassObjList [w + l * tempGrassNumW].transform.localToWorldMatrix;
temgGrassObjList[w + l * tempGrassNumW].SetActive (false);
}
}
meshFilter.mesh = new Mesh();
meshFilter.mesh.CombineMeshes(combine);
transform.gameObject.active = true;
for (int n = 0; n < tempGrassPosList.Length; n++) {
Destroy (temgGrassObjList [n]);
}
}
Vector2 GetWindDir (float degree) {
Vector2 dir = new Vector2 (0,0);
float radian = degree * Mathf.Deg2Rad;
dir = new Vector2 (Mathf.Cos(radian), Mathf.Sin(radian));
return dir.normalized;
}
}
}