Learn ComputeShader 15 Grass

1.Using Blender to create a single grass clump

首先blender与unity的坐标轴不同,z轴向上,不是y轴

通过小键盘的数字键可以快速切换视图,选中物体以后按下小键盘的点可以将物体聚焦于屏幕中心

首先我们创建一个平面,宽度为0.2m,然后切换到正交前视图,复制两个平面。shift+D可以复制面

接着将上下两个面旋转45°至中间面的中心。先按下R然后按下Y可以绕y轴旋转,然后按G键可以移动面

然后切换到正交顶视图(数字键7)

将两个复制的面分别向左向右旋转10.5°左右

最后加上材质和贴图以后的效果就是下面这样

然后就可以保存退出Blender了,后面我们在unity中批量产出这个grass

2. Using instancing to cover a surface with grass

首先就是定义草的类,包含了草的位置,摆动角度

 struct GrassClump
    {
        public Vector3 position;
        public float lean;
        public float noise;

        public GrassClump( Vector3 pos)
        {
            position.x = pos.x;
            position.y = pos.y;
            position.z = pos.z;
            lean = 0;
            noise = Random.Range(0.5f, 1);
            if (Random.value < 0.5f) noise = -noise;
        }
    }

接着还有全局的草的密度,大小,最大摆动角度

 [Range(0,1)]
    public float density = 0.8f;
    [Range(0.1f,3)]
    public float scale = 0.2f;
    [Range(10, 45)]
    public float maxLean = 25;

然后就是初始化草丛的位置信息,生成一个 ComputeBuffer 来存储这些位置数据,并通过计算着色器来模拟草丛的摆动效果。

获取附加的 MeshFilter 组件中的网格边界,bounds.extents 返回边界框的一半大小(每个轴的范围的一半)

MeshFilter mf = GetComponent<MeshFilter>();
Bounds bounds = mf.sharedMesh.bounds;
Vector3 clumps = bounds.extents;

使用对象的缩放值(transform.localScale)和一个密度因子来调整草丛的分布范围,主要是 x 和 z 轴 。并且计算草丛的总数量


        Vector3 vec = transform.localScale / 0.1f * density;
        clumps.x *= vec.x;
        clumps.z *= vec.z;

        int total = (int)clumps.x * (int)clumps.z;

获取计算着色器中的内核 LeanGrass,并计算每个线程组的大小。groupSize 是用于处理草丛的线程组数,而 count 则是实际生成的草丛总数 

kernelLeanGrass = shader.FindKernel("LeanGrass");
shader.GetKernelThreadGroupSizes(kernelLeanGrass, out threadGroupSize, out _, out _);
groupSize = Mathf.CeilToInt((float)total / (float)threadGroupSize);
int count = groupSize * (int)threadGroupSize;

随机生成 count 个草丛的 pos 位置。这个位置基于网格的边界和中心生成,使用 TransformPoint 将局部坐标转换为全局坐标 (世界坐标)

clumpsArray = new GrassClump[count];

for (int i = 0; i < count; i++)
{
    Vector3 pos = new Vector3(Random.value * bounds.extents.x * 2 - bounds.extents.x + bounds.center.x,
                              0,
                              Random.value * bounds.extents.z * 2 - bounds.extents.z + bounds.center.z);
    pos = transform.TransformPoint(pos);
    clumpsArray[i] = new GrassClump(pos);
}

创建了一个 ComputeBuffer 来存储所有的草丛位置信息,并将 clumpsArray 赋值到缓冲区中。 

clumpsBuffer = new ComputeBuffer(count, SIZE_GRASS_CLUMP);
clumpsBuffer.SetData(clumpsArray);

将缓冲区 clumpsBuffer 绑定到计算着色器的 clumpsBuffer 参数,并将草丛最大倾斜角度 maxLean 传递给着色器。 

shader.SetBuffer(kernelLeanGrass, "clumpsBuffer", clumpsBuffer);
shader.SetFloat("maxLean", maxLean * Mathf.PI / 180);
timeID = Shader.PropertyToID("time");

 通过 argsArray 设置绘制调用的参数(索引数量和实例数量),并使用 ComputeBuffer 类型为 IndirectArguments 创建一个缓冲区,用于 DrawMeshInstancedIndirect 函数的调用

  • argsArray[0] = mesh.GetIndexCount(0);

    • 这行代码获取的是 mesh 的索引数量,也就是用来渲染的几何体有多少个顶点索引。每个网格都有其顶点、法线、UV 等信息,而索引决定了如何连接这些顶点来形成三角形。
    • IndirectArguments 绘制时,第一个参数就是表示绘制网格时使用的顶点索引数量。
  • argsArray[1] = (uint)count;

    • count 是实例化对象的数量。通过 Graphics.DrawMeshInstancedIndirect 方法可以在一次绘制调用中实例化多个对象。
    • 这里第二个参数表示要绘制的实例化网格的数量
argsArray[0] = mesh.GetIndexCount(0);
argsArray[1] = (uint)count;
argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(argsArray);

然后看一下我们的计算着色器

很简短,就是设置了个倾斜角度,方便后续在表面shader中进行旋转

[numthreads(THREADGROUPSIZE,1,1)]
void LeanGrass (uint3 id : SV_DispatchThreadID)
{
    GrassClump clump = clumpsBuffer[id.x];

    clump.lean = sin(time + clump.noise) * maxLean * clump.noise;

    clumpsBuffer[id.x] = clump;
}

接着继续编写表面着色器

首先是设置每个草丛的位置以及旋转平移矩阵

        void setup()
        {
            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                GrassClump clump = clumpsBuffer[unity_InstanceID];
                _Position = clump.position;
                _Matrix = create_matrix(clump.position, clump.lean);
            #endif
        }

然后是创建矩阵函数,这是一个绕z轴旋转的矩阵

  float4x4 create_matrix(float3 pos, float theta){
            float c = cos(theta);
            float s = sin(theta);
            return float4x4(
                c,-s, 0, pos.x,
                s, c, 0, pos.y,
                0, 0, 1, pos.z,
                0, 0, 0, 1
            );
        }

最后就是顶点函数的设置

首先乘上缩放系数,然后计算经过旋转和平移的顶点位置,接着计算只经过平移的位置,最后根据uv的y值来插值坐标,也就是高度越高,弯曲幅度越大

 void vert(inout appdata_full v, out Input data)
        {
            UNITY_INITIALIZE_OUTPUT(Input, data);

            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                v.vertex.xyz *= _Scale;
                float4 rotatedVertex = mul(_Matrix, v.vertex);
                v.vertex.xyz += _Position;
                v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
            #endif
        }

最终效果:

3. Bending blades of grass

这次我们要自己通关代码创建一个草的mesh,并且设置顶点位置,法线,uv,索引。

下面几个图分别是这几个属性的图解。

这里面设置顶点索引要按照逆时针的顺序是因为unity会按照逆时针顺序的三角形认定为正面,防止它被背面剔除

代码:

 Mesh Blade
    {
        get
        {
            Mesh mesh;

            if (blade != null)
            {
                mesh = blade;
            }
            else
            {
                mesh = new Mesh();

                float height = 0.2f;
                float rowHeight = height / 4;
                float halfWidth = height / 10;

                //1. Use the above variables to define the vertices array
                Vector3[] vertices =
               {
                    new Vector3(-halfWidth, 0, 0),
                    new Vector3( halfWidth, 0, 0),
                    new Vector3(-halfWidth, rowHeight, 0),
                    new Vector3( halfWidth, rowHeight, 0),
                    new Vector3(-halfWidth*0.9f, rowHeight*2, 0),
                    new Vector3( halfWidth*0.9f, rowHeight*2, 0),
                    new Vector3(-halfWidth*0.8f, rowHeight*3, 0),
                    new Vector3( halfWidth*0.8f, rowHeight*3, 0),
                    new Vector3( 0, rowHeight*4, 0)
                };

                //2. Define the normals array, hint: each vertex uses the same normal
                Vector3 normal = new Vector3(0, 0, -1);

                Vector3[] normals =
               {
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal
                };

                //3. Define the uvs array
                Vector2[] uvs =
                {
                    new Vector2(0,0),
                    new Vector2(1,0),
                    new Vector2(0,0.25f),
                    new Vector2(1,0.25f),
                    new Vector2(0,0.5f),
                    new Vector2(1,0.5f),
                    new Vector2(0,0.75f),
                    new Vector2(1,0.75f),
                    new Vector2(0.5f,1)
                };

                //4. Define the indices array
                int[] indices =
{
                    0,1,2,1,3,2,//row 1
                    2,3,4,3,5,4,//row 2
                    4,5,6,5,7,6,//row 3
                    6,7,8//row 4
                };

                //5. Assign the mesh properties using the arrays
                //   for indices use
                //   mesh.SetIndices( indices, MeshTopology.Triangles, 0 );
                mesh.vertices = vertices;
                mesh.normals = normals;
                mesh.uv = uvs;
                mesh.SetIndices(indices, MeshTopology.Triangles, 0);

            }

            return mesh;
        }
    }

 接下来加入风向的影响

风向相关的属性主要是风速,风向以及风的强度。

        float theta = windDirection * Mathf.PI / 180;
        Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
        shader.SetVector("wind", wind);

之后在每一帧调用        Graphics.DrawMeshInstancedIndirect(mesh, 0, material, bounds, argsBuffer);来进行大规模绘制。

之后是计算着色器。

主要是计算bend属性,noise的取值范围是-1到1,这样草既可以向左也可也向右弯曲。

[numthreads(THREADGROUPSIZE,1,1)]
void BendGrass (uint3 id : SV_DispatchThreadID)
{
    GrassBlade blade = bladesBuffer[id.x];

    float2 offset = (blade.position.xz + wind.xy * time * wind.z) * wind.w;
    float noise = perlin(offset.x, offset.y) * 2 - 1;
    blade.bend = noise * maxBend * blade.noise;
    
    bladesBuffer[id.x] = blade;
}

至于表面着色器则是和之前的差不多。

最终效果:

完整代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GrassBlades : MonoBehaviour
{
    struct GrassBlade
    {
        public Vector3 position;
        public float bend;
        public float noise;
        public float fade;

        public GrassBlade( Vector3 pos)
        {
            position.x = pos.x;
            position.y = pos.y;
            position.z = pos.z;
            bend = 0;
            noise = Random.Range(0.5f, 1) * 2 - 1;
            fade = Random.Range(0.5f, 1);
        }
    }
    int SIZE_GRASS_BLADE = 6 * sizeof(float);

    public Material material;
    public ComputeShader shader;
    public Material visualizeNoise;
    public bool viewNoise = false;
    [Range(0,1)]
    public float density;
    [Range(0.1f,3)]
    public float scale;
    [Range(10, 45)]
    public float maxBend;
    [Range(0, 2)]
    public float windSpeed;
    [Range(0, 360)]
    public float windDirection;
    [Range(10, 1000)]
    public float windScale;

    ComputeBuffer bladesBuffer;
    ComputeBuffer argsBuffer;
    GrassBlade[] bladesArray;
    uint[] argsArray = new uint[] { 0, 0, 0, 0, 0 };
    Bounds bounds;
    int timeID;
    int groupSize;
    int kernelBendGrass;
    Mesh blade;
    Material groundMaterial;

    Mesh Blade
    {
        get
        {
            Mesh mesh;

            if (blade != null)
            {
                mesh = blade;
            }
            else
            {
                mesh = new Mesh();

                float height = 0.2f;
                float rowHeight = height / 4;
                float halfWidth = height / 10;

                //1. Use the above variables to define the vertices array
                Vector3[] vertices =
               {
                    new Vector3(-halfWidth, 0, 0),
                    new Vector3( halfWidth, 0, 0),
                    new Vector3(-halfWidth, rowHeight, 0),
                    new Vector3( halfWidth, rowHeight, 0),
                    new Vector3(-halfWidth*0.9f, rowHeight*2, 0),
                    new Vector3( halfWidth*0.9f, rowHeight*2, 0),
                    new Vector3(-halfWidth*0.8f, rowHeight*3, 0),
                    new Vector3( halfWidth*0.8f, rowHeight*3, 0),
                    new Vector3( 0, rowHeight*4, 0)
                };

                //2. Define the normals array, hint: each vertex uses the same normal
                Vector3 normal = new Vector3(0, 0, -1);

                Vector3[] normals =
               {
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal,
                    normal
                };

                //3. Define the uvs array
                Vector2[] uvs =
                {
                    new Vector2(0,0),
                    new Vector2(1,0),
                    new Vector2(0,0.25f),
                    new Vector2(1,0.25f),
                    new Vector2(0,0.5f),
                    new Vector2(1,0.5f),
                    new Vector2(0,0.75f),
                    new Vector2(1,0.75f),
                    new Vector2(0.5f,1)
                };

                //4. Define the indices array
                int[] indices =
{
                    0,1,2,1,3,2,//row 1
                    2,3,4,3,5,4,//row 2
                    4,5,6,5,7,6,//row 3
                    6,7,8//row 4
                };

                //5. Assign the mesh properties using the arrays
                //   for indices use
                //   mesh.SetIndices( indices, MeshTopology.Triangles, 0 );
                mesh.vertices = vertices;
                mesh.normals = normals;
                mesh.uv = uvs;
                mesh.SetIndices(indices, MeshTopology.Triangles, 0);

            }

            return mesh;
        }
    }
    // Start is called before the first frame update
    void Start()
    {
        bounds = new Bounds(Vector3.zero, new Vector3(30, 30, 30));
        blade = Blade;

        MeshRenderer renderer = GetComponent<MeshRenderer>();
        groundMaterial = renderer.material;


        InitShader();
    }

    private void OnValidate()
    {
        if (groundMaterial != null)
        {
            MeshRenderer renderer = GetComponent<MeshRenderer>();

            renderer.material = (viewNoise) ? visualizeNoise : groundMaterial;

            //TO DO: set wind using wind direction, speed and noise scale
            float theta = windDirection * Mathf.PI / 180;
            Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
            shader.SetVector("wind", wind);
            visualizeNoise.SetVector("wind", wind);
        }
    }

    void InitShader()
    {
        MeshFilter mf = GetComponent<MeshFilter>();
        Bounds bounds = mf.sharedMesh.bounds;

        Vector3 blades = bounds.extents;
        Vector3 vec = transform.localScale / 0.1f * density;
        blades.x *= vec.x;
        blades.z *= vec.z;

        int total = (int)blades.x * (int)blades.z * 20;

        kernelBendGrass = shader.FindKernel("BendGrass");

        uint threadGroupSize;
        shader.GetKernelThreadGroupSizes(kernelBendGrass, out threadGroupSize, out _, out _);
        groupSize = Mathf.CeilToInt((float)total / (float)threadGroupSize);
        int count = groupSize * (int)threadGroupSize;

        bladesArray = new GrassBlade[count];

        for(int i=0; i<count; i++)
        {
            Vector3 pos = new Vector3( Random.value * bounds.extents.x * 2 - bounds.extents.x + bounds.center.x,
                                       0,
                                       Random.value * bounds.extents.z * 2 - bounds.extents.z + bounds.center.z);
            pos = transform.TransformPoint(pos);
            bladesArray[i] = new GrassBlade(pos);
        }

        bladesBuffer = new ComputeBuffer(count, SIZE_GRASS_BLADE);
        bladesBuffer.SetData(bladesArray);

        shader.SetBuffer(kernelBendGrass, "bladesBuffer", bladesBuffer);
        shader.SetFloat("maxBend", maxBend * Mathf.PI / 180);
        //TO DO: set wind using wind direction, speed and noise scale
        float theta = windDirection * Mathf.PI / 180;
        Vector4 wind = new Vector4(Mathf.Cos(theta), Mathf.Sin(theta), windSpeed, windScale);
        shader.SetVector("wind", wind);


        timeID = Shader.PropertyToID("time");

        argsArray[0] = blade.GetIndexCount(0);
        argsArray[1] = (uint)count;
        argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
        argsBuffer.SetData(argsArray);

        material.SetBuffer("bladesBuffer", bladesBuffer);
        material.SetFloat("_Scale", scale);
    }

    // Update is called once per frame
    void Update()
    {
        shader.SetFloat(timeID, Time.time);
        shader.Dispatch(kernelBendGrass, groupSize, 1, 1);

        if (!viewNoise)
        {
            Graphics.DrawMeshInstancedIndirect(blade, 0, material, bounds, argsBuffer);
        }
    }

    private void OnDestroy()
    {
        bladesBuffer.Release();
        argsBuffer.Release();
    }
}
// Each #kernel tells which function to compile; you can have many kernels
#define THREADGROUPSIZE 128 
#pragma kernel BendGrass

#include "noiseSimplex.cginc"

struct GrassBlade
{
    float3 position;
    float bend;
    float noise;
    float fade;
};
RWStructuredBuffer<GrassBlade> bladesBuffer;
float time;
float maxBend;
float4 wind;

[numthreads(THREADGROUPSIZE,1,1)]
void BendGrass (uint3 id : SV_DispatchThreadID)
{
    GrassBlade blade = bladesBuffer[id.x];

    float2 offset = (blade.position.xz + wind.xy * time * wind.z) * wind.w;
    float noise = perlin(offset.x, offset.y) * 2 - 1;
    blade.bend = noise * maxBend * blade.noise;
    
    bladesBuffer[id.x] = blade;
}
Shader "Custom/GrassBlades"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags{ "RenderType"="Opaque" }
        
		LOD 200
		Cull Off
		
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types   
        #pragma surface surf Standard vertex:vert addshadow fullforwardshadows
        #pragma instancing_options procedural:setup

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        float _Scale;
        float _Fade;
        float4x4 _Matrix;
        float3 _Position;

        float4x4 create_matrix(float3 pos, float theta){
            float c = cos(theta);
            float s = sin(theta);
            return float4x4(
                c,-s, 0, pos.x,
                s, c, 0, pos.y,
                0, 0, 1, pos.z,
                0, 0, 0, 1
            );
        }
        
        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            struct GrassBlade
            {
                float3 position;
                float lean;
                float noise;
                float fade;
            };
            StructuredBuffer<GrassBlade> bladesBuffer; 
        #endif

        void vert(inout appdata_full v, out Input data)
        {
            UNITY_INITIALIZE_OUTPUT(Input, data);

            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                v.vertex.xyz *= _Scale;
                float4 rotatedVertex = mul(_Matrix, v.vertex);
                v.vertex.xyz += _Position;
                v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
            #endif
        }

        void setup()
        {
            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                GrassBlade blade = bladesBuffer[unity_InstanceID];
                _Matrix = create_matrix(blade.position, blade.lean);
                _Position = blade.position;
                _Fade = blade.fade;
            #endif
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color * _Fade;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

4. Grass on an uneven surface

之前都是在平面上生成草地,这次我们在不平坦的土地上面生成草地。

主要修改了随机生成草丛位置的函数。

初始化一个长度为 countGrassClump 数组,用于存储草丛的位置数据

clumpsArray = new GrassClump[count];

给当前游戏对象添加 MeshCollider,方便后续使用 Physics.Raycast 做物理碰撞检测,以确定草丛的具体位置 

    gameObject.AddComponent<MeshCollider>();
  • v 是用于存储坐标的临时 Vector3 变量。
  • bounds.center.y + bounds.extents.y 表示边界框的顶点高度,并将该高度变换到世界空间 transform.TransformPoint(v) 中,得到射线起点的 y 值(castY)。
  • minY 表示边界框的最低点,经过同样的变换后获得其在世界坐标系中的 y 值。
  • rangecastYminY 之间的高度差,用于之后的高度比例计算。
  • 最后,castY += 10 将射线的起始位置在边界框上方偏移 10 个单位,确保射线从足够高的地方开始射向地面。
    RaycastHit hit;
    Vector3 v = new Vector3();
    v.y = (bounds.center.y + bounds.extents.y);
    v = transform.TransformPoint(v);
    float castY = v.y;
    v.Set(0, 0, 0);
    v.y = (bounds.center.y - bounds.extents.y);
    v = transform.TransformPoint(v);
    float minY = v.y;
    float range = castY - minY;
    castY += 10;

开始一个 while 循环,直到生成的草丛数达到 count 或循环次数达到 count * 10。这个限制防止循环无休止进行(在某些情况下可能无法找到适合的位置)

  • 生成随机的 xz 坐标,使得草丛分布在 bounds 的范围内。
    • Random.value * bounds.extents.x * 2 - bounds.extents.x 随机生成一个在 bounds 范围内的 x 坐标。
    • bounds.center.xbounds.center.z 用于确保随机位置是围绕 bounds 中心生成的。
  • 使用 transform.TransformPoint(pos) 将位置变换到世界坐标系中。
  • 将生成的位置 y 赋值为 castY,即射线从该高度开始向下检测
 int loopCount = 0;
        int index = 0;

        while (index < count && loopCount < (count * 10))
        {
            loopCount++;

            Vector3 pos = new Vector3(
                (Random.value * bounds.extents.x * 2 - bounds.extents.x) + bounds.center.x,
                 0,
                (Random.value * bounds.extents.z * 2 - bounds.extents.z) + bounds.center.z);
            pos = transform.TransformPoint(pos);
            pos.y = castY;


        }
  • 发射一条从 pos 向下(Vector3.down)的射线,检测地形表面。如果射线击中了地形,hit 将包含碰撞信息。
  • pos.y 设为射线击中地面的 y 值,即草丛的实际高度
  • 计算 deltaHeight,这是草丛的高度比例,heightAffect 是影响草丛生成的高度因子。
  • 随机数与 deltaHeight 相比较,用于控制草丛的分布概率,位置越高(deltaHeight 越大),草丛生成的概率可能越低,反之越高。
  • 如果条件符合,将生成的 pos 位置赋给一个新的 GrassClump 对象,并将其存入 clumpsArray
  while (index < count && loopCount < (count * 10))
        {
            if (Physics.Raycast(pos, Vector3.down, out hit))
            {
                pos.y = hit.point.y;
                float deltaHeight = ((pos.y - minY) / range) * heightAffect;

                if (Random.value > deltaHeight)
                {
                    GrassClump clump = new GrassClump(pos);
                    clumpsArray[index++] = clump;
                }
            }
        }

最终效果:

完整代码:

 void InitPositionsArray(int count, Bounds bounds)
    {
        clumpsArray = new GrassClump[count];

        gameObject.AddComponent<MeshCollider>();

        RaycastHit hit;
        Vector3 v = new Vector3();
        v.y = (bounds.center.y + bounds.extents.y);
        v = transform.TransformPoint(v);
        float castY = v.y;
        v.Set(0, 0, 0);
        v.y = (bounds.center.y - bounds.extents.y);
        v = transform.TransformPoint(v);
        float minY = v.y;
        float range = castY - minY;
        castY += 10;

        int loopCount = 0;
        int index = 0;

        while (index < count && loopCount < (count * 10))
        {
            loopCount++;

            Vector3 pos = new Vector3(
                (Random.value * bounds.extents.x * 2 - bounds.extents.x) + bounds.center.x,
                 0,
                (Random.value * bounds.extents.z * 2 - bounds.extents.z) + bounds.center.z);
            pos = transform.TransformPoint(pos);
            pos.y = castY;

            if (Physics.Raycast(pos, Vector3.down, out hit))
            {
                pos.y = hit.point.y;
                float deltaHeight = ((pos.y - minY) / range) * heightAffect;

                if (Random.value > deltaHeight)
                {
                    GrassClump clump = new GrassClump(pos);
                    clumpsArray[index++] = clump;
                }
            }
        }

        Debug.Log("GrassTerrain:InitPositionArray count:" + count + " index:" + index);
    }

5. Trampling the grass

这次要完成的是角色与草地的交互。

主要是判断根据角色位置与草丛位置的距离是否小于踩踏半径,是的话就说明正在被踩踏,对草地进行旋转,当角色与草地逐渐远离的时候,再逐渐将草丛恢复。

这里还有一个返回四元数的函数。

四元数是一种数学表示方法,常用于描述三维空间中的旋转。四元数由四个分量组成,一个实部和三个虚部,通常表示为 q=w+xi+yj+zk,可以写成一个四元组 q=(w,x,y,z)

w(实部)

  • 代表旋转角度的余弦值(通常对应旋转角的一半)

x,y,z(虚部)

  • 表示旋转轴的方向。

因此首先通过两个向量相加取得中间向量,夹角也就变为了一半,接着直接点乘计算出余弦值,最后通过叉乘计算出旋转轴的方向。

float4 MapVector(float3 v1, float3 v2){
    v1 = normalize(v1);
    v2 = normalize(v2);
    float3 v = v1+v2;
    v = normalize(v);
    float4 q = 0;
    q.w = dot(v, v2);
    q.xyz = cross(v, v2);
    return q;
}

好,接下来就是在compute shader中根据距离计算踩踏幅度以及四元数。如果不在踩踏范围了,那么就逐渐减小踩踏幅度。

[numthreads(THREADGROUPSIZE,1,1)]
void UpdateGrass (uint3 id : SV_DispatchThreadID)
{
    GrassClump clump = clumpsBuffer[id.x];

    float3 relativePosition = clump.position - tramplePos.xyz;
    float dist = length(relativePosition);

    if (dist<trampleRadius){
        clump.trample = ((trampleRadius - dist)/trampleRadius) * 0.9;
        clump.quaternion = MapVector(float3(0,1,0), normalize(relativePosition));
    }else{
        clump.trample *= 0.98;
    }

    clump.lean = sin(time*speed) * maxLean * clump.noise;

    clumpsBuffer[id.x] = clump;
}

接着在表面着色器利用踩踏幅度对草进行旋转

这个函数将四元数变化成一个旋转矩阵。

 float4x4 quaternion_to_matrix(float4 quat)
        {
            float4x4 m = float4x4(float4(0, 0, 0, 0), float4(0, 0, 0, 0), float4(0, 0, 0, 0), float4(0, 0, 0, 0));

            float x = quat.x, y = quat.y, z = quat.z, w = quat.w;
            float x2 = x + x, y2 = y + y, z2 = z + z;
            float xx = x * x2, xy = x * y2, xz = x * z2;
            float yy = y * y2, yz = y * z2, zz = z * z2;
            float wx = w * x2, wy = w * y2, wz = w * z2;

            m[0][0] = 1.0 - (yy + zz);
            m[0][1] = xy - wz;
            m[0][2] = xz + wy;

            m[1][0] = xy + wz;
            m[1][1] = 1.0 - (xx + zz);
            m[1][2] = yz - wx;

            m[2][0] = xz - wy;
            m[2][1] = yz + wx;
            m[2][2] = 1.0 - (xx + yy);

            m[0][3] = _Position.x;
            m[1][3] = _Position.y;
            m[2][3] = _Position.z;
            m[3][3] = 1.0;

            return m;
        }

然后就是在顶点着色器中根据踩踏幅度来决定顶点的位置。

  void vert(inout appdata_full v, out Input data)
        {
            UNITY_INITIALIZE_OUTPUT(Input, data);

            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                v.vertex.xyz *= _Scale;
                float4 rotatedVertex = mul(_Matrix, v.vertex);
                float4 trampledVertex = mul(_TrampleMatrix, v.vertex);
                v.vertex.xyz += _Position;
                trampledVertex = lerp(v.vertex, trampledVertex, v.texcoord.y);
                v.vertex = lerp(v.vertex, rotatedVertex, v.texcoord.y);
                v.vertex = lerp(v.vertex, trampledVertex, _Trample);
            #endif
        }

最终效果:

URP(Universal Render Pipeline)是Unity引擎中的一种渲染管线,而Compute Shader是URP中的一种功能,用于在GPU上进行并行计算。Compute Shader可以用来执行各种计算任务,例如图像处理、物理模拟、粒子系统等。 URP Compute Shader提供了一种在GPU上进行高性能计算的方式,它可以利用GPU的并行计算能力来加速复杂的计算任务。与传统的图形渲染不同,Compute Shader不需要与图形渲染管线交互,它可以独立于渲染过程进行计算。 使用URP Compute Shader可以带来以下优势: 1. 并行计算:Compute Shader可以同时在多个线程上执行计算任务,充分利用GPU的并行计算能力,提高计算效率。 2. 高性能:由于在GPU上执行,Compute Shader可以利用硬件加速,提供更高的计算性能。 3. 灵活性:Compute Shader可以执行各种类型的计算任务,不仅限于图形渲染,可以用于各种领域的并行计算需求。 使用URP Compute Shader的基本步骤如下: 1. 创建Compute Shader:在Unity中创建一个Compute Shader文件,并编写需要执行的计算任务代码。 2. 创建Compute Buffer:创建一个Compute Buffer对象,用于在CPU和GPU之间传递数据。 3. 设置Compute Shader参数:将需要的参数传递给Compute Shader,例如输入数据、输出数据等。 4. 调度Compute Shader:使用Graphics类的Dispatch方法来调度Compute Shader的执行。 5. 获取计算结果:在计算完成后,可以从Compute Buffer中获取计算结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值