Dynamic Grass – 动态草地制作

Last modified date

制作随风摇摆的草地,并且草碰到障碍物会弯折。 

先看看Demo效果:
  • 自定义草皮尺寸。
  • 调整风向风速,对草皮的影响效果。

 Unity发布的webGL不支持复杂For循环语句的shader,所以上图demo里只有主角跟草有交互。实际上可以多个对象跟草交互的。而且webGL里也不支持ShadowCaster,所以影子也无法显示。其他主流平台都是可以正常运行的。

  分析一下要实现的内容:
  1. 每根草根据风速随机摇摆。
  2. 根据风的方向,草向一边倾倒。
  3. 碰到障碍物时草会弯曲。

  然后开工,一个个实现:

首先做个草的模型

为了方便后面测试,我搞了几种不同的草。

然后代码生成一块草皮
  • 自定义草皮尺寸
  • 用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;
        }
    }
}

Share

This site is protected by wp-copyrightpro.com