Grass Render

Unity interactive and chopable octree grass sea rendering – geometry, compute shader (BIRP/URP)

Project (BIRP) on Github:

https://github.com/Remyuu/Unity-Interactive-Grass

First, here is a screenshot of 10,0500 grasses running on Compute Shader on my M1 pro without any optimization. It can run more than 200 frames.

After adding octree frustum culling, distance fading and other operations, the frame rate is not so stable (I want to die). I guess it is because the CPU has too much pressure to operate each frame and needs to maintain such a large amount of grass information. But as long as enough culling is done, running 700+ frames is no problem (comfort). In addition, the depth of the octree also needs to be optimized according to the actual situation. In the figure below, I set the depth of the octree to 5.

Preface

This article is getting longer and longer. I mainly use it to review my knowledge. When you read it, you may feel that there are a lot of basic contents. I am a complete novice, and I beg for discussion and correction from you.

This article mainly has two stages:

  • The GS + TS method achieves the most basic effect of grass rendering
  • Then I used CS to re-render the sea of grass, adding various optimization methods

The rendering method of geometry shader + tessellation shader should be relatively simple, but the performance ceiling is relatively low and the platform compatibility is poor.

The method of combining compute shaders with GPU Instancing should be the mainstream method in the current industry, and it can also run well on mobile terminals.

The CS rendering of the sea of grass in this article mainly refers to the implementation of Colin and Minions Art, which is more like a hybrid of the two (the former has been analyzed by a big guy on ZhihuGrass rendering study notes based on GPU Instance). Use three sets of ComputeBuffer, one is the buffer containing all the grass, one is the buffer that is appended into the Material, and the other is a visible buffer (obtained in real time based on frustum culling). Implemented the use of a quad-octree (odd-even depth) for space division, plus the frustum culling to get the index of all the grass in the current frustum, pass it to the Compute Shader for further processing (such as Mesh generation, quaternion calculation rotation, LoD, etc.), and then use a variable-length ComputeBuffer (ComputeBufferType.Append) to pass the grass to be rendered to the Material through Instancing for final rendering.

You can also use the Hi-Z solution to eliminate it. I'm digging a hole and working hard to learn.

In addition, I referred to the article by Minions Art and copied a set of editor grass brushing tools (incomplete version), which stores the positions of all grass vertices by maintaining a vertex list.

Furthermore, by maintaining another set of Cut Buffer, if the grass is marked with a -1 value, it will not be processed. If it is marked with a non--1 value of the chopper height, it will be passed to the Material, and through the WorldPos + Split.y plus the lerp operation, the upper half of the grass will be made invisible, and the color of the grass will be modified, and finally some grass clippings will be added to achieve a grass-cutting effect.

Previous articleI have introduced in detail what a tessellation shader is and various optimization methods. Next, I will integrate tessellation into actual development. In addition, I combined the compute shader I learned in a few days to create a grass field based on the compute shader. You can find more details in the following article.This noteThe following is the small effect that this article will achieve, with complete code attached:

  • Grass Rendering
  • 草地渲染 – 几何着色器 (BIRP/URP)
  • Define grass width, height, orientation, pour, curvature, gradient, color, band, normal
  • INTEGER tessellation
  • URP adds Visibility Map
  • 草地渲染 – Compute Shader(BIRP/URP)work on MacOS
  • Octree frustum culling
  • Distance fades
  • Grass Interaction
  • Interactive Geometry Shaders (BIRP/URP)
  • Interactive Compute Shader (BIRP) work on MacOS
  • Unity custom grass generation tool
  • Grass cutting system

Main references(plagiarism)article:

There are many ways to render grass, two of which are shown in this article:

  • Geometry Shader + Tessellation Shader
  • Compute Shaders + GPU Instancing

First of all, the first solution has great limitations. Many mobile devices and Metal do not support GS, and GS will recalculate the Mesh every frame, which is quite expensive.

Secondly, can MacOS no longer run geometry shaders? Not really. If you want to use GS, you must use OpenGL, not Metal. But it should be noted that Apple supports OpenGL up to OpenGL 4.1. In other words, this version does not support Compute Shader. Of course, MacOS in the Intel era can support OpenGL 4.3 and can run CS and GS at the same time. The M series chips do not have this fate. Either use 4.1 or use Metal. On my M1p mbp, even if you choose a virtual machine (Parallels 18+ provides DX11 and Vulkan), the Vulkan running on macOS is translated and is essentially Metal, so there is still no GS. Therefore, there is no native GS after macOS M1.

Furthermore, Metal doesn't even support Tessellation shaders directly. Apple doesn't want to support these two things on the chip at all. Why? Because the efficiency is too low. On the M chip, TS is even simulated by CS!

To sum up, geometry shaders are a dead-end technology, especially after the advent of Mesh Shader. Although GS is very popular in Unity, any similar effect can be instanced on CS, and it is more efficient. Although new graphics cards will still support GS, there are still quite a few games on the market that use GS. It's just that Apple didn't consider compatibility and directly cut it off.

This article explains in detail why GS is so slow:http://www.joshbarczak.com/blog/?p=667. Simply put, Intel optimized GS by blocking threads, etc., while other chips do not have this optimization.

This article is a study note and is likely to contain errors.

1. Overview of Geometry Shader Rendering Grass (BIRP)

This chapter isRoystanA concise summary of the . If you need the project file or the final code, you can download it from the original article. Or readSocrates has no bottom article.

1.1 Overview

After the Domain Stage, you can choose to use a geometry shader.

A geometry shader takes a whole primitive as input and is able to generate vertices on output. The input to a geometry shader is the vertices of a complete primitive (three vertices for a triangle, two vertices for a line or a single vertex for a point). The geometry shader is called once for each primitive.

fromWeb DownloadInitial engineering.

1.2 Drawing a triangle

Draw a triangle.

// Add inside the CGINCLUDE block.
struct geometryOutput
{
    float4 pos : SV_POSITION;
};

...
    //Vertex shader
return vertex;
...

[maxvertexcount(3)]
void geo(triangle float4 IN[3] : SV_POSITION, inout TriangleStreamtriStream)
{
    geometryOutput o;

    o.POS = UnityObjectToClipPos(float4(0.5, 0, 0, 1));
    triStream.Append(o);

    o.POS = UnityObjectToClipPos(float4(-0.5, 0, 0, 1));
    triStream.Append(o);

    o.POS = UnityObjectToClipPos(float4(0, 1, 0, 1));
    triStream.Append(o);
}



// Add inside the SubShader Pass, just below the #pragma fragment frag line.
#pragma geometry geo

實際上,我們為網格中的每個頂點繪製了一個三角形,但我們分配給三角形頂點的位置是恆定的 – 它們不會針對每個輸入頂點而改變 – 將所有三角形放置在彼此之上了。

1.3 Vertex Offset

Therefore, we can just make an offset according to the position of each vertex.

C#
// Add to the top of the geometry shader.
float3 POS = IN[0];



// Update each assignment of o.pos.
o.POS = UnityObjectToClipPos(POS + float3(0.5, 0, 0));



o.POS = UnityObjectToClipPos(POS + float3(-0.5, 0, 0));



o.POS = UnityObjectToClipPos(POS + float3(0, 1, 0));

1.4 Rotating blades

However, it should be noted that currently all triangles are emitted in one direction, so normal correction is added. TBN matrix is constructed and multiplied with the current direction. And the code is organized.

float3 vNormal = IN[0].normal;
float4 vTangent = IN[0].tangent;
float3 vBinormal = cross(vNormal, vTangent) * vTangent.w;

float3x3 tangentToLocal = float3x3(
    vTangent.x, vBinormal.x, vNormal.x,
    vTangent.y, vBinormal.y, vNormal.y,
    vTangent.z, vBinormal.z, vNormal.z
    );

triStream.Append(VertexOutput(POS + mul(tangentToLocal, float3(0.5, 0, 0))));
triStream.Append(VertexOutput(POS + mul(tangentToLocal, float3(-0.5, 0, 0))));
triStream.Append(VertexOutput(POS + mul(tangentToLocal, float3(0, 0, 1))));

1.5 Coloring

Then define the upper and lower colors of the grass, and use UV to make a lerp gradient.

return lerp(_BottomColor, _TopColor, i.uv.y);
C#

1.6 Rotation Matrix Principle

Make a random orientation. Here a rotation matrix is constructed. The principle is also mentioned in GAMES101. There is also aVideo of formula derivation, and it is very clear! The simple derivation idea is, assuming that the vector $a$ rotates around the n-axis to $b$, then decompose $a$​ into the component parallel to the n-axis (found to be constant) plus the component perpendicular to the n-axis.

float3x3 AngleAxis3x3(float angle, float3 axis)
{
    float c, s;
    sincos(angle, s, c);

    float t = 1 - c;
    float x = axis.x;
    float y = axis.y;
    float z = axis.z;

    return float3x3(
        t * x * x + c, t * x * y - s * z, t * x * z + s * y,
        t * x * y + s * z, t * y * y + c, t * y * z - s * x,
        t * x * z - s * y, t * y * z + s * x, t * z * z + c
        );
}

旋转矩阵 $R$ 这里用罗德里格旋转公式(Rodrigues’ rotation formula)来计算: $$R=I+sin⁡(θ)⋅[k]×+(1−cos⁡(θ))⋅[k]×2$$

Among them, $\theta$ is the rotation angle. $k$ is the unit rotation axis. $I$ is the identity matrix. $[k]_{\times}$ is the antisymmetric matrix corresponding to the axis $k$.

For a unit vector $k=(x,y,z)$ , the antisymmetric matrix $[k]_{\times}=\left[\begin{array}{ccc} 0 & -z & y \\ z & 0 & -x \\ -y & x & 0 \end{array}\right]$ finally obtains the matrix elements:

$$ \begin{array}{ccc} tx^2 + c & txy – sz & txz + sy \\ txy + sz & ty^2 + c & tyz – sx \\ txz – sy & tyz + sx & tz^2 + c \\ \end{array} $$

float3x3 facingRotationMatrix = AngleAxis3x3(rand(POS) * UNITY_TWO_PI, float3(0, 0, 1));

1.7 Blade tipping

Get the grass in a random direction, and then pour it in any random direction on the x or y axis.

float3x3 bendRotationMatrix = AngleAxis3x3(rand(POS.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0));

1.8 Leaf size

Adjust the width and height of the grass. Originally, we set the height and width to be one unit. To make the grass more natural, we add rand to this step to make it look more natural.

_BladeWidth("Blade Width", Float) = 0.05
_BladeWidthRandom("Blade Width Random", Float) = 0.02
_BladeHeight("Blade Height", Float) = 0.5
_BladeHeightRandom("Blade Height Random", Float) = 0.3


float height = (rand(POS.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight;
float width = (rand(POS.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth;


triStream.Append(VertexOutput(POS + mul(transformationMatrix, float3(width, 0, 0)), float2(0, 0)));
triStream.Append(VertexOutput(POS + mul(transformationMatrix, float3(-width, 0, 0)), float2(1, 0)));
triStream.Append(VertexOutput(POS + mul(transformationMatrix, float3(0, 0, height)), float2(0.5, 1)));

1.9 Tessellation

Since the number is too small, the upper surface is subdivided here.

1.10 Perturbations

To animate the grass, add the normals to the _Time perturbation. Sample the texture, then calculate the wind rotation matrix and apply it to the grass.

float2 uv = POS.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.z + _WindFrequency * _Time.y;

float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength;

float3 wind = normalize(float3(windSample.x, windSample.y, 0));

float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind);

float3x3 transformationMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix), bendRotationMatrix);

1.11 Fixed blade rotation issue

At this time, the wind may rotate along the x and y axes, which is specifically manifested as:

Write a matrix for the two points under your feet that rotates only along z.

float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix);



triStream.Append(VertexOutput(POS + mul(transformationMatrixFacing, float3(width, 0, 0)), float2(0, 0)));
triStream.Append(VertexOutput(POS + mul(transformationMatrixFacing, float3(-width, 0, 0)), float2(1, 0)));

1.12 Blade curvature

In order to make the leaves have curvature, we have to add vertices. In addition, since double-sided rendering is currently enabled, the order of vertices does not matter. Here, a manual interpolation for loop is used to construct triangles. A forward is calculated to bend the leaves.

float forward = rand(POS.yyz) * _BladeForward;


for (int i = 0; i < BLADE_SEGMENTS; i++)
{
    float t = i / (float)BLADE_SEGMENTS;
    // Add below the line declaring float t.
    float segmentHeight = height * t;
    float segmentWidth = width * (1 - t);
    float segmentForward = pow(t, _BladeCurve) * forward;
    float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix;
    triStream.Append(GenerateGrassVertex(POS, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
    triStream.Append(GenerateGrassVertex(POS, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));
}

triStream.Append(GenerateGrassVertex(POS, 0, height, forward, float2(0.5, 1), transformationMatrix));

1.13 Creating Shadows

Create shadows in another Pass and output.

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

    CGPROGRAM
    #Pragmas vertex vert
    #Pragmas geometry geo
    #Pragmas fragment frag
    #Pragmas hull hull
    #Pragmas domain domain
    #Pragmas target 4.6
    #Pragmas multi_compile_shadowcaster

    float4 frag(geometryOutput i) : SV_Target{
        SHADOW_CASTER_FRAGMENT(i)
    }

    ENDCG
}

1.14 Receiving Shadows

Use SHADOW_ATTENUATION directly in Frag to determine the shadow.

// geometryOutput struct.
unityShadowCoord4 _ShadowCoord : TEXCOORD1;
...
o._ShadowCoord = ComputeScreenPos(o.POS);
...
#Pragmas multi_compile_fwdbase
...
return SHADOW_ATTENUATION(i);

1.15 Removing shadow acne

Removes surface acne.

#if UNITY_PASS_SHADOWCASTER
    o.POS = UnityApplyLinearShadowBias(o.POS);
#endif

1.16 Adding Normals

Add normal information to vertices generated by the geometry shader.

struct geometryOutput
{
    float4 POS : SV_POSITION;
    float2 uv : TEXCOORD0;
    unityShadowCoord4 _ShadowCoord : TEXCOORD1;
    float3 normal : NORMAL;
};
...
o.normal = UnityObjectToWorldNormal(normal);

1.17 Full code‼️ (BIRP)

The final effect.

Code:

https://pastebin.com/8u1ytGgU

Complete: https://pastebin.com/U14m1Nu0

2. Geometry Shader Rendering Grass (URP)

2.1 References

I have already written the BIRP version, and now I just need to port it.

  • URP code specification reference: https://www.cyanilux.com/tutorials/urp-shader-code/
  • BIRP->URP quick reference table: https://cuihongzhi1991.github.io/blog/2020/05/27/builtinttourp/

You can followThis article by DanielYou can also follow me to modify the code. It should be noted that the space transformation code in the original repo has problems.Pull requestsThe solution was found in

Now put the above BIRP tessellation shader together.

  • Tags changed to URP
  • The header file is introduced and replaced with the URP version
  • Variables are surrounded by CBuffer
  • Shadow casting, receiving code

2.2 Start to change

Declare the URP pipeline.

LOD 100
Cull Off
Pass{
    Tags{
        "RenderType" = "Opaque"
        "Queue" = "Geometry"
        "RenderPipeline" = "UniversalPipeline"
    }

Import the URP library.

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderVariablesFunctions.hlsl"

o._ShadowCoord = ComputeScreenPos(o.POS);

Change the function.

// o.normal = UnityObjectToWorldNormal(normal);
o.normal = TransformObjectToWorldNormal(normal);

URP receives the shadow. It is best to calculate this in the vertex shader, but for convenience, it is all calculated in the geometry shader.

Then generate the shadows. ShadowCaster Pass.

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

    ZWrite On
    ZTest LEqual

    HLSLPROGRAM

        half4 frag(geometryOutput input) : SV_TARGET{
            return 1;
        }

    ENDHLSL
}

2.3 Full code‼️(URP)

https://pastebin.com/6KveEKMZ

3. Optimize tessellation logic (BIRP/URP)

3.1 Organize the code

Above we just use a fixed number of subdivision levels, which I cannot accept. If you don't understand the principle of surface subdivision, you can seeMy Tessellation Articles, which details several solutions for optimizing segmentation.

I use the BIRP version of the code that I completed in Section 1 as an example. The current version only has the Uniform subdivision.

_TessellationUniform("Tessellation Uniform", Range(1, 64)) = 1

The output structures of each stage are quite confusing, so let's reorganize them.

3.1 Partitioning Mode

[KeywordEnum(INTEGER, FRAC_EVEN, FRAC_ODD, POW2)] _PARTITIONING("Partition algorithm", Float) = 0

#Pragmas shader_feature_local _PARTITIONING_INTEGER _PARTITIONING_FRAC_EVEN _PARTITIONING_FRAC_ODD _PARTITIONING_POW2

#if defined(_PARTITIONING_INTEGER)
    [partitioning("integer")]
#elif defined(_PARTITIONING_FRAC_EVEN)
    [partitioning("fractional_even")]
#elif defined(_PARTITIONING_FRAC_ODD)
    [partitioning("fractional_odd")]
#elif defined(_PARTITIONING_POW2)
    [partitioning("pow2")]
#else 
    [partitioning("integer")]
#endif

3.2 Subdivided Frustum Culling

In BIRP, use _ProjectionParams.z to represent the far plane, and in URP use UNITY_RAW_FAR_CLIP_VALUE.

bool IsOutOfBounds(float3 p, float3 lower, float3 higher) { //Given rectangle judgment
    return p.x < lower.x || p.x > higher.x || p.y < lower.y || p.y > higher.y || p.z < lower.z || p.z > higher.z;
}
bool IsPointOutOfFrustum(float4 positionCS) { //View cone judgment
    float3 culling = positionCS.xyz;
    float w = positionCS.w;
    float3 lowerBounds = float3(-w, -w, -w * _ProjectionParams.z);
    float3 higherBounds = float3(w, w, w);
    return IsOutOfBounds(culling, lowerBounds, higherBounds);
}
bool ShouldClipPatch(float4 p0PositionCS, float4 p1PositionCS, float4 p2PositionCS) {
    bool allOutside = IsPointOutOfFrustum(p0PositionCS) &&
        IsPointOutOfFrustum(p1PositionCS) &&
        IsPointOutOfFrustum(p2PositionCS);
    return allOutside;
}

TessellationControlPoint vert(Attributes v)
{
    ...
    o.positionCS = UnityObjectToClipPos(v.vertex);
    ...
}

TessellationFactors patchConstantFunction (InputPatch<TessellationControlPoint, 3> patch)
{
    TessellationFactors f;
    if(ShouldClipPatch(patch[0].positionCS, patch[1].positionCS, patch[2].positionCS)){
        f.edge[0] = f.edge[1] = f.edge[2] = f.inside = 0;
    }else{
        f.edge[0] = _TessellationFactor;
        f.edge[1] = _TessellationFactor;
        f.edge[2] = _TessellationFactor;
        f.inside = _TessellationFactor;
    }
    return f;
}

However, it should be noted that the judgment input here is the CS coordinates of the grass. If the triangular grass completely leaves the screen, but the grass grows high and may still be on the screen, it will cause a screen bug where the grass suddenly disappears. This depends on the needs of the project. If it is a project with an upward viewing angle and the grass is relatively short, this operation can be used.

The viewing angle is not a big problem.

If viewed from Voldemort's perspective, the grass is incomplete and over-culled.

3.3 Fine-grained control of screen distance

The grass is dense near and sparse far, but based on the screen distance (CS space). This method is affected by the resolution.

float EdgeTessellationFactor(float scale, float4 p0PositionCS, float4 p1PositionCS) {
    float factor = distance(p0PositionCS.xyz / p0PositionCS.w, p1PositionCS.xyz / p1PositionCS.w) / scale;
    return max(1, factor);
}

TessellationFactors patchConstantFunction (InputPatch<TessellationControlPoint, 3> patch)
{
    TessellationFactors f;

    f.edge[0] = EdgeTessellationFactor(_TessellationFactor, 
        patch[1].positionCS, patch[2].positionCS);
    f.edge[1] = EdgeTessellationFactor(_TessellationFactor, 
        patch[2].positionCS, patch[0].positionCS);
    f.edge[2] = EdgeTessellationFactor(_TessellationFactor, 
        patch[0].positionCS, patch[1].positionCS);
    f.inside = (f.edge[0] + f.edge[1] + f.edge[2]) / 3.0;


    #if defined(_CUTTESS_TRUE)
        if(ShouldClipPatch(patch[0].positionCS, patch[1].positionCS, patch[2].positionCS))
            f.edge[0] = f.edge[1] = f.edge[2] = f.inside = 0;
    #endif

    return f;
}

Tessellation Factor = 0.08

It is not recommended to select Frac as the segmentation mode, otherwise there will be strong shaking, which is very eye-catching. I don't like this method very much.

3.4 Camera distance classification

Calculate the ratio of "the distance between two points" to "the distance between the midpoint of the two vertices and the camera position". The larger the ratio, the larger the space occupied on the screen, and the more subdivision is required.

float EdgeTessellationFactor_WorldBase(float scale, float3 p0PositionWS, float3 p1PositionWS) {
    float length = distance(p0PositionWS, p1PositionWS);
    float distanceToCamera = distance(_WorldSpaceCameraPos, (p0PositionWS + p1PositionWS) * 0.5);
    float factor = length / (scale * distanceToCamera * distanceToCamera);
    return max(1, factor);
}
...
f.edge[0] = EdgeTessellationFactor_WorldBase(_TessellationFactor_WORLD_BASE, 
    patch[1].vertex, patch[2].vertex);
f.edge[1] = EdgeTessellationFactor_WorldBase(_TessellationFactor_WORLD_BASE, 
    patch[2].vertex, patch[0].vertex);
f.edge[2] = EdgeTessellationFactor_WorldBase(_TessellationFactor_WORLD_BASE, 
    patch[0].vertex, patch[1].vertex);
f.inside = (f.edge[0] + f.edge[1] + f.edge[2]) / 3.0;

There is still room for improvement. Adjust the density of the grass so that the grass at close distance is not too dense, and the grass curve at medium distance is smoother, and introduce a nonlinear factor to control the relationship between distance and tessellation factor.

float EdgeTessellationFactor_WorldBase(float scale, float3 p0PositionWS, float3 p1PositionWS) {
    float length = distance(p0PositionWS, p1PositionWS);
    float distanceToCamera = distance(_WorldSpaceCameraPos, (p0PositionWS + p1PositionWS) * 0.5);
    // Use the square root function to adjust the effect of distance to make the tessellation factor change more smoothly at medium distances
    float adjustedDistance = sqrt(distanceToCamera);
    // Adjust the impact of scale. You may need to further fine-tune the coefficient here based on the actual effect.
    float factor = length / (scale * adjustedDistance);
    return max(1, factor);
}

This is more appropriate.

3.5 Visibility Map Controls Grass Subdivision

The vertex shader reads the texture and passes it to the tessellation shader, which calculates the tessellation logic in PCF.

Take FIXED mode as an example:

_VisibilityMap("Visibility Map", 2D) = "white" {}
TEXTURE2D (_VisibilityMap);SAMPLER(sampler_VisibilityMap);
struct Attributes
{
    ...
    float2 uv : TEXCOORD0;
};
struct TessellationControlPoint
{
    ...
    float visibility : TEXCOORD1;
};
TessellationControlPoint vert(Attributes v){
    ...
    float visibility = SAMPLE_TEXTURE2D_LOD(_VisibilityMap, sampler_VisibilityMap, v.uv, 0).r; 
    o.visibility    = visibility;
    ...
}
TessellationFactors patchConstantFunction (InputPatch<TessellationControlPoint, 3> patch){
    ...
    float averageVisibility = (patch[0].visibility + patch[1].visibility + patch[2].visibility) / 3; // Calculate the average grayscale value of the three vertices
    float baseTessellationFactor = _TessellationFactor_FIXED; 
    float tessellationMultiplier = lerp(0.1, 1.0, averageVisibility); // Adjust the factor based on the average gray value
    #if defined(_DYNAMIC_FIXED)
        f.edge[0] = _TessellationFactor_FIXED * tessellationMultiplier;
        f.edge[1] = _TessellationFactor_FIXED * tessellationMultiplier;
        f.edge[2] = _TessellationFactor_FIXED * tessellationMultiplier;
        f.inside  = _TessellationFactor_FIXED * tessellationMultiplier;
    ...

3.6 Complete code‼️ (BIRP)

Grass Shader:

https://pastebin.com/TD0AupGz

3.7 Full code ‼ ️ (URP)

There are some differences in URP. For example, to calculate ShadowBias, you need to do the following. I won’t expand on it. Just look at the code yourself.

#if UNITY_PASS_SHADOWCASTER
    // o.pos = UnityApplyLinearShadowBias(o.pos);
    o.shadowCoord = TransformWorldToShadowCoord(ApplyShadowBias(posWS, norWS, 0));
#endif

Grass Shader:

https://pastebin.com/2ZX2aVm9

4. Interactive Grassland

URP and BIRP are exactly the same.

4.1 Implementation steps

The principle is very simple. The script transmits the character's world coordinates, and then bends the grass according to the set radius and interaction strength.

uniform float3 _PositionMoving; // Object position float _Radius; // Object interaction radius float _Strength; // Interaction strength

In the grass generation loop, calculate the distance between each grass fragment and the object and adjust the grass position according to this distance.

float dis = distance(_PositionMoving, posWS); // Calculate distance
float radiusEffect = 1 - saturate(dis / _Radius); // Calculate effect attenuation based on distance
float3 sphereDisp = POS - _PositionMoving; // Calculate the position difference
sphereDisp *= radiusEffect * _Strength; // Apply falloff and intensity
sphereDisp = clamp(sphereDisp, -0.8, 0.8); // Limit the maximum displacement

The new positions are then calculated within each blade of grass.

// Apply interactive effects
float3 newPos = i == 0 ? POS : POS + (sphereDisp * t);
triStream.Append(GenerateGrassVertex(newPos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
triStream.Append(GenerateGrassVertex(newPos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));

Don't forget the outside of the for loop, which is the top vertex.

// Final grass fragment
float3 newPosTop = POS + sphereDisp;
triStream.Append(GenerateGrassVertex(newPosTop, 0, height, forward, float2(0.5, 1), transformationMatrix));
triStream.RestartStrip();

In URP, using uniform float3 _PositionMoving may cause SRP Batcher to fail.

4.2 Script Code

Bind the object that needs interaction.

using UnityEngine;

public class ShaderInteractor : MonoBehaviour
{
    // Update is called once per frame
    void Update()
    {
        Shader.SetGlobalVector("_PositionMoving", transform.position);
    }
}

4.3 Full code ‼ ️ (URP)

Grass shader:

https://pastebin.com/Zs77EQgy

5. Compute Shader Rendering Grass v1.0

Why v1.0? Because I think it is quite difficult to render the sea of grass with this compute shader. Many of the things that are not available now can be improved slowly in the future. I also wrote some notes about Compute Shader.

  1. Compute Shader Study Notes (I)
  2. Compute Shader Learning Notes (II) Post-processing Effects
  3. Compute Shader Learning Notes (II) Particle Effects and Cluster Behavior Simulation
  4. Compute Shader Learning Notes (Part 3) Grass Rendering

5.1 Review/Organization

The Compute Shader notes above fully describe how to write a stylized grass sea from scratch in CS. If you forgot, review it here.

There are still many things that the CPU needs to do in the initialization stage. First, define the grass Mesh and Buffer transfer (the width and height of the grass, the position of each grass generation, the random orientation of the grass, and the random color depth of the grass). It also needs to specifically pass the maximum curvature value and grass interaction radius to the Compute Shader.

For each frame, the CPU also passes the time variable, wind direction, wind force/speed, and wind field scaling factor to the Compute Shader.

Compute Shader uses the information passed by the CPU to calculate how the grass should turn, using quaternions as output.

Finally, the shader instantiates the ID and all calculation results, first calculating the vertex offset, then applying the quaternion rotation, and finally modifying the normal information.

This demo can actually be further optimized, such as putting more calculations in the Compute Shader, such as the process of generating Mesh, the width and height of the grass, random tilting, etc. More real-time parameter adjustment variables can also be optimized. Various optimization culling can also be performed, such as culling the incoming camera position by distance, or culling with the view frustum, etc. This culling process requires the use of some atomic operations. There is also multi-object interaction. The logic of interactive grass deformation can also be optimized, such as the degree of interaction is proportional to the power of the distance of the interactive object, etc. The engine function can also be increased, and the function of brushing grass can be developed, which may require a quadtree storage system, etc.

And in Compute Shader, use vectors instead of scalars when possible.

First, organize the code. Put all variables that do not need to be sent to the Compute Shader every frame into a function for unified initialization. Organize the Inspector panel. (There are many code changes)

First, basically all calculations are run on the GPU, except that the world coordinates of each grass are calculated in the CPU and passed to the GPU through a Buffer.

The size of the buffer transmission depends entirely on the size of the ground mesh and the set density. In other words, if it is a super large open world, the buffer will become super large. For a 5*5 grass field, with the Density set to 0.5, approximately 312576 grass data will be sent, and the actual data will reach 4*312576*4=5001216 bytes. Based on the CPU->GPU transmission speed of 8 GB/s, it takes about 10 milliseconds to transmit.

Fortunately, this buffer does not need to be transmitted every frame, but it is enough to attract our attention. If the current grass size increases to 100*100, the time required will increase several times, which is scary. Moreover, we may not use many of the vertices, which causes a great waste of performance.

I added a function to generate perlin noise in the Compute Shader, as well as the xorshift128 random number generation algorithm.

// Perlin random number algorithm
float hash(float x, float y) {
    return frac(abs(sin(sin(123.321 + x) * (y + 321.123)) * 456.654));
}
float perlin(float x, float y){
    float col = 0.0;
    for (int i = 0; i < 8; i++) {
        float fx = floor(x); float fy = floor(y);
        float xx = ceil(x); float cy = ceil(y);
        float a = hash(fx, fy); float b = hash(fx, cy);
        float c = hash(xx, fy); float d = hash(xx, cy);
        col += lerp(lerp(a, b, frac(y)), lerp(c, d, frac(y)), frac(x));
        col /= 2.0; x /= 2.0; y /= 2.0;
    }
    return col;
}
// XorShift128 random number algorithm -- Edited Directly output normalized data
uint state[4];
void xorshift_init(uint s) {
    state[0] = s; state[1] = s | 0xffff0000u;
    state[2] = s < 16; state[3] = s >> 16;
}
float xorshift128() {
    uint t = state[3]; uint s = state[0];
    state[3] = state[2]; state[2] = state[1]; state[1] = s;
    t ^= t < 11u; t ^= t >> 8u;
    state[0] = t ^ s ^ (s >> 19u);
    return (float)state[0] / float(0xffffffffu);
}

[numthreads(THREADGROUPSIZE,1,1)]
void BendGrass (uint3 id : SV_DispatchThreadID)
{
    xorshift_init(id.x * 73856093u ^ id.y * 19349663u ^ id.z * 83492791u);
    ...
}

To review, at present, the CPU uses an AABB average grass paving logic to generate all possible grass vertices, which are then passed to the GPU to perform some culling, LoD and other operations in the Compute Shader.

So far I have three Buffers.

m_InputBuffer is the structure on the left of the above picture that sends all the grass to the GPU without any culling.

m_OutputBuffer is a variable length buffer that increases slowly in the Compute Shader. If the grass of the current thread ID is suitable, it will be added to this buffer for instanced rendering later. The structure on the right of the above picture.

m_argsBuffer is a parameterized Buffer, which is different from other Buffers. It is used to pass parameters to Draw, and its specific content is to specify the number of vertices to be rendered in batches, the number of rendering instances, etc. Let's take a look at it in detail:

First parameter, my grass mesh has seven triangles, so there are 21 vertices to render.

The second parameter is temporarily set to 0, indicating that nothing needs to be rendered. This number will be dynamically set according to the length of m_OutputBuffer after the Compute Shader calculation is completed. In other words, the number here will be the same as the number of grasses appended in the Compute Shader.

The third and fourth parameters represent respectively: the index of the first rendered vertex and the index of the first instantiation.

I haven't used the fifth parameter, so I don't know what it is used for.

The last step looks like this, passing in the Mesh, material, AABB and parameter Buffer.

5.2 Customizing Unity Tools

Create a new C# script and save it in the Editor directory of the project (if it doesn't exist, create one). The script inherits from Editor, and then write [CustomEditor(typeof(XXX))] . It means you work for XXX. I work for GrassControl, and then you can attach what you wrote now to XXX. Of course, you can also have a separate window, which should inherit from EditorWindow.

Write tools in the OnInspectorGUI() function, for example, write a Label.

GUILayout.Label("== Remo Grass Generator ==");

To center the Inspector, add a parameter.

GUILayout.Label("== Remo Grass Generator ==", new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter });

Too crowded? Just add a line of space.

EditorGUILayout.Space();

If you want to attach tools above XXX, then all the logic should be written above OnInspectorGUI.

... // Write here
// The default Inspector interface of GrassControl
base.OnInspectorGUI();

Create a button and press the code:

if (GUILayout.Button("xxx"))
{
    ...//Code after pressing

Anyway, these are the ones I use now.

5.3 Editor selects the object to generate grass

It is also very simple to get the Object of the script of the current service and display it in the Inspector.

[SerializeField] private GameObject grassObject;
...
grassObject = (GameObject)EditorGUILayout.ObjectField("Write any name", grassObject, typeof(GameObject), true);
if (grassObject == null)
{
    grassObject = FindObjectOfType<GrassControl>()?.gameObject;
}

After obtaining it, you can access the contents of the current script through GameObject.

How to get the object selected in the Editor window? It can be done with one line of code.

foreach (GameObject obj in Selection.gameObjects)

Display the selected objects in the Inspector panel. Note that you need to handle the case of multiple selections, otherwise a Warning will be issued.

// Display the current Editor selected object in real time and control the availability of the button
EditorGUILayout.LabelField("Selection Info:", EditorStyles.boldLabel);
bool hasSelection = Selection.activeGameObject != null;
GUI.enabled = hasSelection;
if (hasSelection)
    foreach (GameObject obj in Selection.gameObjects)
        EditorGUILayout.LabelField(obj.name);
else
    EditorGUILayout.LabelField("No active object selected.");

Next, get the MeshFilter and Renderer of the selected object. Since Raycast detection is required, get a Collider. If it does not exist, create one.

Then I will not talk about the code of sketching grass here.

5.4 Processing AABBs

After generating a bunch of grass, add each grass to the AABB and finally pass it to Instancing.

I assume that each grass is the size of a unit cube, so it is Vector3.one. If the grass is particularly tall, this should need to be modified.

Stuff each blade of grass into the big AABB and pass the new AABB back to the script's m_LocalBounds for Instancing.

Graphics.DrawMeshInstancedIndirect(blade, 0, m_Material, m_LocalBounds, m_argsBuffer);

5.5 Surface Shader – 踩坑

There is a small problem here. Since the current Material is a Surface Shader, the Vertex of the Surface Shader has calculated the center of the AABB by default to do the vertex offset, so the world coordinates passed in before cannot be used directly. You also need to pass the center of the AABB in and subtract it. It's so strange. I wonder if there is any elegant way.

5.6 Simple Camera Distance Culling + Fade

Currently, all generated grass is passed to the Compute Shader on the CPU, and then all grass is added to the AppendBuffer, which means there is no culling logic.

The simplest culling solution is to cull grass based on the distance between the camera and the grass. In the Inspector panel, open a value to represent the culling distance. Calculate the distance between the camera and the current grass instance. If it is greater than the set value, it will not be added to the AppendBuffer.

First, pass the world coordinates of the camera into C#. Here is the semi-pseudo code:

// Get the camera
private Camera m_MainCamera;

m_MainCamera = Camera.main;

if (m_MainCamera != null)
    m_ComputeShader.SetVector(ID_camreaPos, m_MainCamera.transform.position);

In CS, calculate the distance between the grass and the camera:

float distanceFromCamera = distance(input.position, _CameraPositionWS);

The distance function code is as follows:

float distanceFade = 1 - saturate((distanceFromCamera - _MinFadeDist) / (_MaxFadeDist - _MinFadeDist));

If the value is less than 0, return directly.

// skip if out of fading range too
if (distanceFade < 0.001f)
{
    return;
}

In the part between culling and not culling, set the grass width + Fade value to achieve a fading effect.

Result.height = (bladeHeight + bladeHeightOffset * (xorshift128()*2-1)) * distanceFade;
Result.width = (bladeWeight + bladeWeightOffset * (xorshift128()*2-1)) * distanceFade;
...
Result.fade = xorshift128() * distanceFade;

In the figure below, both are set to be relatively small for the convenience of demonstration.

I think the actual effect is quite good and smooth. If the width and height of the grass are not modified, the effect will be greatly reduced.

Of course, you can also modify the logic: do not completely remove the grass that exceeds the maximum drawing range, but reduce the number of drawings; or selectively draw the grass in the transition area.

Both logics are acceptable, and if it were me I would choose the latter.

5.7 Maintaining a set of visible ID buffers

The so-called frustum culling is to reduce the redundant calculations of GPU through various methods at the CPU stage.

So how do I let the Compute Shader know which grass needs to be rendered and which needs to be culled? My approach is to maintain a set of ID Lists. The length is the number of all grasses. If the current grass needs to be culled, otherwise the index value of the grass that needs to be rendered is recorded.

List<uint> grassVisibleIDList = new List<uint>();

// buffer that contains the ids of all visible instances
private ComputeBuffer m_VisibleIDBuffer;

private const int VISIBLE_ID_STRIDE        =  1 * sizeof(uint);

m_VisibleIDBuffer = new ComputeBuffer(grassData.Count, VISIBLE_ID_STRIDE,
    ComputeBufferType.Structured); //uint only, per visible grass
m_ComputeShader.SetBuffer(m_ID_GrassKernel, "_VisibleIDBuffer", m_VisibleIDBuffer);

m_VisibleIDBuffer?.Release();

Since some grass has been removed before being passed to the Compute Shader, the number of Dispatches is no longer the number of all grasses, but the number of the current List.

// m_ComputeShader.Dispatch(m_ID_GrassKernel, m_DispatchSize, 1, 1);

m_DispatchSize = Mathf.CeilToInt(grassVisibleIDList.Count / threadGroupSize);

Generates a fully visible ID sequence.

void GrassFastList(int count)
{
    grassVisibleIDList = Enumerable.Range(0, count).ToArray().ToList();
}

And each frame should be uploaded to GPU. The preparation is complete, and then use Quad tree to operate this array.

5.8 Quad/Octtree Storing Grass Index

You can consider dividing an AABB into multiple sub-AABBs and then use a quadtree to store and manage them.

Currently, all grass is in one AABB. Next, we build an octree and put all the grass in this AABB into branches. This makes it easy to do frustum culling in the early stages of the CPU.

How to store it? If the current grass has a small vertical drop, then a quadtree is enough. If it is an open world with undulating mountains, then use an octree. However, considering that the grass has a relatively high horizontal density, I use a quadtree + octree structure here. The parity of the depth determines whether the current depth is divided into four nodes or eight nodes. If there is no need for strong height division, it is OK to use an octree, but I feel that the efficiency may be a little lower. Here, it is directly evenly distributed. Later optimization can consider the AABB division method based on variable length dynamic changes.

if (depth % 2 == 0)
{
    ...
    m_children.Add(new CullingTreeNode(topLeftSingle, depth - 1));
    m_children.Add(new CullingTreeNode(bottomRightSingle, depth - 1));
    m_children.Add(new CullingTreeNode(topRightSingle, depth - 1));
    m_children.Add(new CullingTreeNode(bottomLeftSingle, depth - 1));
}
else
{
    ...
    m_children.Add(new CullingTreeNode(topLeft, depth - 1));
    m_children.Add(new CullingTreeNode(bottomRight, depth - 1));
    m_children.Add(new CullingTreeNode(topRight, depth - 1));
    m_children.Add(new CullingTreeNode(bottomLeft, depth - 1));

    m_children.Add(new CullingTreeNode(topLeft2, depth - 1));
    m_children.Add(new CullingTreeNode(bottomRight2, depth - 1));
    m_children.Add(new CullingTreeNode(topRight2, depth - 1));
    m_children.Add(new CullingTreeNode(bottomLeft2, depth - 1));
}

The detection of the view frustum and AABB can be done with GeometryUtility.TestPlanesAABB.

public void RetrieveLeaves(Plane[] frustum, List<Bounds> list, List<int> visibleIDList)
{
    if (GeometryUtility.TestPlanesAABB(frustum, m_bounds))
    {
        if (m_children.Count == 0)
        {
            if (grassIDHeld.Count > 0)
            {
                list.Add(m_bounds);
                visibleIDList.AddRange(grassIDHeld);
            }
        }
        else
        {
            foreach (CullingTreeNode child in m_children)
            {
                child.RetrieveLeaves(frustum, list, visibleIDList);
            }
        }
    }
}

This code is the key part, passing in:

  • The six planes of the camera frustum Plane[]
  • A list of Bounds objects storing all nodes within the frustum
  • Stores a list of all grass indices contained in the node within the frustum

By calling the method of this quad/octree, you can get the list of all bounding boxes and grass within the frustum.

Then all the grass indexes can be made into a Buffer and passed to the Compute Shader.

m_VisibleIDBuffer.SetData(grassVisibleIDList);

To get a visual AABB, use the OnDrawGizmos() method.

Pass all the AABBs obtained by culling the view frustum into this function. This way you can see the AABBs intuitively.

Also write everything inside the view frustum to the visible grass.

5.9 草叶闪烁问题 – 踩坑

Here I hit a small pit. I completed the octree and successfully divided many sub-AABBs as shown above. But when I moved the camera, the grass flickered wildly. I was a little lazy and didn't want to make GIF videos. Observe the two pictures below. I just moved the view slightly and changed the current Visibility List. The position of the grass jumped a lot, and it looked like the grass flickered continuously.

I can't figure it out, there is no problem with Compute Shader culling.

The number of dispatches is also calculated based on the length of the visibility list, so there must be enough threads to compute the shader.

And there is no problem with DrawMeshInstancedIndirect.

What's the problem?

After a long debugging, I found that the problem lies in the process of taking random numbers by Xorshift of Compute Shader.

Before using _VisibleIDBuffer, one grass corresponds to one thread ID, which is determined from the moment the grass is born. Now that this group of indexes has been added, and the ID of the incoming random value is not changed to a Visible ID, the random numbers will appear very discrete.

That is to say, all previous IDs are replaced with index values taken from _VisibleIDBuffer!

5.10 Multi-object Interaction

Currently there is only one trampler passed in. If it is not passed in, an error will be reported, which is unbearable.

There are three parameters about interaction:

  • pos – Vector3
  • trampleStrength – Float
  • trampleRadius – Float

Now put trampleRadius into pos (Vector4) (or another one, depending on your needs), and pass the position array into it using SetVectorArray. This way each interactive object can have a dedicated interactive radius. For fat interactive objects, make the radius larger, and for skinny ones, make it smaller. That is, remove the following line:

// In SetGrassDataBase, no need to upload every frame
// m_ComputeShader.SetFloat("trampleRadius", trampleRadius);

become:

// In SetGrassDataUpdate, each frame must be uploaded
// Set up multiple interactive objects
if (trampler.Length > 0)
{
    Vector4[] positions = new Vector4[trampler.Length];
    for (int i = 0; i < trampler.Length; i++)
    {
        positions[i] = new Vector4(trampler[i].transform.position.x, trampler[i].transform.position.y, trampler[i].transform.position.z,
            trampleRadius);
    }
    m_ComputeShader.SetVectorArray(ID_tramplePos, positions);
}

Then you have to pass the number of interactive objects so that the Compute Shader knows how many interactive objects need to be processed. This also needs to be updated every frame. I am used to storing an ID index for objects that are updated every frame, which is more efficient.

// Initializing
ID_trampleLength = Shader.PropertyToID("_trampleLength");
// In each frame
m_ComputeShader.SetFloat(ID_trampleLength, trampler.Length);

I repackaged it:

By modifying the corresponding code, you can adjust the radius of each interactive object on the panel. If you want to enrich this adjustment function, you can consider passing a separate Buffer into it.

In the Compute Shader, it is relatively simple to combine multiple rotations.

// Trampler
float4 qt = float4(0, 0, 0, 1); // 1 in quaternion is like this, the imaginary part is 0
for (int trampleIndex = 0; trampleIndex < trampleLength; trampleIndex++)
{
    float trampleRadius = tramplePos[trampleIndex].a;
    float3 relativePosition = input.position - tramplePos[trampleIndex].xyz;
    float dist = length(relativePosition);
    if (dist < trampleRadius) {
        // Use the power to enhance the effect at close range
        float eff = pow((trampleRadius - dist) / trampleRadius, 2) * trampleStrength;
        float3 direction = normalize(relativePosition);
        float3 newTargetDirection = float3(direction.x * eff, 1, direction.z * eff);
        qt = quatMultiply(MapVector(float3(0, 1, 0), newTargetDirection), qt);
    }
}

5.11 Editor real-time preview

The camera currently passed to the Compute Shader is the main camera, which is the one in the game window. Now you want to temporarily get the main camera's lens in the editor (Scene window) and restore it after starting the game. You can use the Scene View GUI to draw events.

Here is an example of remodeling my current code:

#if UNITY_EDITOR
    SceneView view;

    void OnDestroy()
    {
        // When the window is destroyed, remove the delegate
        // so that it will no longer do any drawing.
        SceneView.duringSceneGui -= this.OnScene;
    }

    void OnScene(SceneView scene)
    {
        view = scene;
        if (!Application.isPlaying)
        {
            if (view.camera != null)
            {
                m_MainCamera = view.camera;
            }
        }
        else
        {
            m_MainCamera = Camera.main;
        }
    }
    private void OnValidate()
    {
        // Set up components
        if (!Application.isPlaying)
        {
            if (view != null)
            {
                m_MainCamera = view.camera;
            }
        }
        else
        {
            m_MainCamera = Camera.main;
        }
    }
#endif

When initializing the shader, subscribe to the event at the beginning, and then determine whether the current state is game, and then pass a camera. If it is in edit mode, then m_MainCamera is still NULL.

void InitShader()
{
#if UNITY_EDITOR
    SceneView.duringSceneGui += this.OnScene;
    if (!Application.isPlaying)
    {
        if (view != null && view.camera != null)
        {
            m_MainCamera = view.camera;
        }
    }
#endif
    if (Application.isPlaying)
    {
        m_MainCamera = Camera.main;
    }
    ...

In the frame-by-frame Update function, if it is detected that m_MainCamera is NULL, it is determined that the current mode is edit mode:

// Pass in the camera coordinates
        if (m_MainCamera != null)
            m_ComputeShader.SetVector(ID_camreaPos, m_MainCamera.transform.position);
#if UNITY_EDITOR
        else if (view != null && view.camera != null)
        {
            m_ComputeShader.SetVector(ID_camreaPos, view.camera.transform.position);
        }

#endif

6. Cutting Grass

Maintain a set of Cut Buffers

// added for cutting
private ComputeBuffer m_CutBuffer;
float[] cutIDs;

Initializing Buffer

private const int CUT_ID_STRIDE            =  1 * sizeof(float);
// added for cutting
m_CutBuffer = new ComputeBuffer(grassData.Count, CUT_ID_STRIDE, ComputeBufferType.Structured);
// added for cutting
m_ComputeShader.SetBuffer(m_ID_GrassKernel, "_CutBuffer", m_CutBuffer);
m_CutBuffer.SetData(cutIDs);

Don't forget to release it when you disable it.

// added for cutting
m_CutBuffer?.Release();

Define a method to pass in the current position and radius to calculate the position of the grass. Set the corresponding cutID to -1.

// newly added for cutting
public void UpdateCutBuffer(Vector3 hitPoint, float radius)
{
    // can't cut grass if there is no grass in the scene
    if (grassData.Count > 0)
    {
        List<int> grasslist = new List<int>();
        // Get the list of IDS that are near the hitpoint within the radius
        cullingTree.ReturnLeafList(hitPoint, grasslist, radius);
        Vector3 brushPosition = this.transform.position;
        // Compute the squared radius to avoid square root calculations
        float squaredRadius = radius * radius;

        for (int i = 0; i < grasslist.Count; i++)
        {
            int currentIndex = grasslist[i];
            Vector3 grassPosition = grassData[currentIndex].position + brushPosition;

            // Calculate the squared distance
            float squaredDistance = (hitPoint - grassPosition).sqrMagnitude;

            // Check if the squared distance is within the squared radius
            // Check if there is grass to cut, or of the grass is uncut(-1)
            if (squaredDistance <= squaredRadius && (cutIDs[currentIndex] > hitPoint.y || cutIDs[currentIndex] == -1))
            {
                // store cutting point
                cutIDs[currentIndex] = hitPoint.y;
            }

        }
    }
    m_CutBuffer.SetData(cutIDs);
}

Then bind a script to the object that needs to be cut:

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


public class Cutgrass : MonoBehaviour
{
    [SerializeField]
    GrassControl grassComputeScript;

    [SerializeField]
    float radius = 1f;

    public bool updateCuts;

    Vector3 cachedPos;
    // Start is called before the first frame update


    // Update is called once per frame
    void Update()
    {
        if (updateCuts && transform.position != cachedPos)
        {
            Debug.Log("Cutting");
            grassComputeScript.UpdateCutBuffer(transform.position, radius);
            cachedPos = transform.position;

        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = new Color(1, 0, 0, 0.3f);
        Gizmos.DrawWireSphere(transform.position, radius);
    }
}

In the Compute Shader, just modify the grass height. (Very straightforward...) You can change the effect to whatever you want.

StructuredBuffer<float> _CutBuffer;// added for cutting

    float cut = _CutBuffer[usableID];
    Result.height = (bladeHeight + bladeHeightOffset * (xorshift128()*2-1)) * distanceFade;
    if(cut != -1){
        Result.height *= 0.1f;
    }

Done!

References

  1. https://learn.microsoft.com/zh-cn/windows/uwp/graphics-concepts/geometry-shader-stage–gs-
  2. https://roystan.net/articles/grass-shader/
  3. https://danielilett.com/2021-08-24-tut5-17-stylised-grass/
  4. https://catlikecoding.com/unity/tutorials/basics/compute-shaders/
  5. Notes - A preliminary exploration of compute-shader
  6. https://www.patreon.com/posts/53587750
  7. https://www.youtube.com/watch?v=xKJHL8nQiuM
  8. https://www.patreon.com/posts/40090373
  9. https://www.patreon.com/posts/47447321
  10. https://www.patreon.com/posts/wip-patron-only-83683483
  11. https://www.youtube.com/watch?v=DeATXF4Szqo
  12. https://catlikecoding.com/unity/tutorials/basics/compute-shaders/
  13. https://docs.unity3d.com/Manual/class-ComputeShader.html
  14. https://docs.unity3d.com/ScriptReference/ComputeShader.html
  15. https://learn.microsoft.com/en-us/windows/win32/api/D3D11/nf-d3d11-id3d11devicecontext-dispatch
  16. https://zhuanlan.zhihu.com/p/102104374
  17. Unity-compute-shader-Basic knowledge
  18. https://kylehalladay.com/blog/tutorial/2014/06/27/Compute-Shaders-Are-Nifty.html
  19. https://cuihongzhi1991.github.io/blog/2020/05/27/builtinttourp/
  20. https://jadkhoury.github.io/files/MasterThesisFinal.pdf

Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

en_USEN