Wednesday, February 28, 2024

GPU Instanced Grass in Unity - Part 7

 

GPU Instanced Grass in Unity


For the original post, please go to https://www.yuque.com/qwerasdwww/dfy5xd


Part 7: Add Shadow

Now the grass doesn't cast or reveive shadow. First we will add shadow to the default grass then add shadow to the grass with command buffer.

In the grass shader, we need two pass. The first pass to receice shadow and the second pass to cast shadow. We will be using Unity's built in shadow parameters and functions.



Run the code and we can see the grass is casting shadow to the environment.



However we lost the grass in the camera because we add a new pass and DrawMeshInstancedIndirect() needs a new parameter. 

We need to fix this in the C# script by using material property block.

Material property block allows us to draw multiple objects with the same mesh but different material property.



Now we successfully add shadow to the grass.

 



Now let's add shadow with command buffer.

We need to create our own shadow map because the built-in shadowmap cannot get the instanced mesh. Thus we need to add a command buffer to cast shadow.

In the C# script, it is very similar to previous method. We declare the property block along with other parameters we need to get the shadow map and initialize them.

To get the shadow map, we add a camera to the directional light, set it to be orthogonal and disable it.

 


Create a function to prepare what we need to get the shadow map.



Now create a shader to generate the shadow map. We need two passes, one for the noninstanced objects and one for the instanced mesh grass. The fragment shader is the same. We need to process the vertex differently.




Now we get a shadow map.



Now we use a new material and a new shader to replace our previous grass material and shader.

In this shader, we add the shadow data we get from our custom shadow map. We need to write in our shadow map and get the matrix from world space to light space because the shadow coordinate we get is from the virtual camera we put on the directional light.

In the OnPreRender(), we need to draw the grass from the light position so that we can get where the grass should cast shadow. We then need to draw the grass from the main camera and it will receive shadow.






Now we successfully add shadow to our grass.



Also with the shadow infomation, we can see that our culling is working.









Monday, February 26, 2024

GPU Instanced Grass in Unity - Part 6

 

GPU Instanced Grass in Unity


For the original post, please go to https://www.yuque.com/qwerasdwww/dfy5xd


Part 6: Use Command Buffer to change the rendering order



Right now there is some glitching when we rotate the camera fast.

It is becasue we get the depth texture and generate hi-z inside OnPostRender(), which happens when the camera finishes render the scene. We can see the mesh is drawn before the depth mipmap is drawn, so we cannnot get the Hi-Z of current frame.

With command buffer, we can adjust the rendering order.

We can get the depth texture, generate Hi-Z mipmap, then draw the grass.




We duplicate the C# script grass.cs and rename the class to be GrassWithCommandBuffer.

We declare three command buffers, and add them.



We comment out the OnPostRender() because we want to get the depth texture before the grass is drawn. We also don't need to dispatch the compute shader inside Update() since we are gonna do that together in OnPostRender( ).



 

We add the command buffer in Start.



Now we need another pass to get the camera depth in our DepthTextureMipmap shader.



Now we are able to get the depth texture before the grass is been drawn. We get the correct result now and as we can see the FPS is not being affected .






Sunday, February 25, 2024

GPU Instanced Grass in Unity - Part 5

 

GPU Instanced Grass in Unity


For the original post, please go to https://www.yuque.com/qwerasdwww/dfy5xd


Part 5: More Optimization with Hi-Z Occlusion Culling


We want to cull off those grass which are on the opposite of terrain.

We can use hierarchical depth buffer to determine which parts of a scene are occluded.

We first want to get the depth map of current scene. We want the depth texture to be able to generate mipmap.




To get the current depth texture, we have to do it inside OnPostRender().





Now we need to generate the mipmap of the depth texture.



We create a new shader and calculate the max depth value of each pixel of the mipmap.

For each pixel on the current mipmap, we sample the surround four pixel and get the min depth value of them, the result is the depth value of the corrsponding pixel on next level mipmap.




Now for each AABB of the grass, we compare the depth value of the bounding box with the depth value of the mipmap, and decide whether to cull it.




In the C# script, we send the depth texture and texture size to the compute shader.



We need to know the vertex coordinate of the bounding box inside the NDC coordinate system, and compare the z component to the depth value stored in the depth texture.









Friday, February 23, 2024

GPU Instanced Grass in Unity - Part 4

 

GPU Instanced Grass in Unity


For the original post, please go to https://www.yuque.com/qwerasdwww/dfy5xd


Part 4: Optimization Grass with Frustum Culling




Now, the grass will be rendered in the whole plane, but we only need to render it inside the camera.

Since we are working on GPU, we can do the culling in the compute shader.


In the C# script, we declare a new boolean to determine whether to use furstum culling and two Vector3 values for the grass bounding box.




In previous part, we initialize our clumpsBuffer inside InitMaterialParameters().

We need to create another coumput buffer to store the unculled data, and let the previous compute buffer be the buffer that store the instance data after culling.

Now, we are having two compute buffers, so we can take that part out and create a new function called InitDefaultBuffer() to initialize it.

We use a macro "UseFrustumCulling" and disable it when not using frustum culling.



Now we create a function to initialize buffers we need to store the data after culling. The buffer type is AppendBuffer.

We write the grass bounding box data into the compute shader and enable the macro "UseFrustumCulling".




So in the Start() function, if we are not using fustum culling, then we initalize the clumpsBuffer as usual.

If we are using frustum culling, the we use clumpsRawBuffer to store the unculled data, and use clumpsBuffer to store the data after culling.




Now we finish the prepare work in C# script, we shall move on to the compute shader.

First we define the macro at the very beginning. 



Add we need to write the buffer and corresponding data into the shader.




If we are using furstum culling, then the buffer we use to do the calculation is clumpsRawBuffer, and use append to add clump instance into the buffer.

The count is to make sure the index is less than the size of the compute buffer.




Now the prepare work is done. Let's start the frustum culling.

We can do the culling in the world space or the clip space

Method 1: In the world space, for each grass instance, determine whether the vertices of grass bounding box is inside the frustum.

Let's first define the function for plane. There are two ways to define a plane. A point and a normal vector or three points.



Now let's get the four vertices of the camera far clipping plane. With camera FOV and aspect of ratio, it is easy to calcualte the position of the four vertices.



 

Every two adjenct vertices on the far clipping plane and the camera position will form a plane.

We then use point on the far plane and near plane and the camera forward to create the far and near plane.



Now we have the six planes of the frustum, we write them into the compute shader and declare the array inside the compute shader.



In the compute shader, we also create a method to determine whether a point is outside a plane.



Now for every grass instance, we determine whether its bounding box is inside each frustum plane.



Since this appendbuffer is dynamically changed, we need to update the buffer every frame inside the C# script.



Finally we need to release the buffer.






Method 2:

We do the frustum culling in clipping space.

For a point, after applying projection transformation into clip coordinates, if  -w <= (x,y,z) <= w

then the point is inside the screen. We only need to compare the homogeneous coordinate of each vertex of the grass AABB to its own w component.






GPU Instanced Grass in Unity - Part 7

  GPU Instanced Grass in Unity For the original post, please go to  https://www.yuque.com/qwerasdwww/dfy5xd Part 7: Add Shadow Now the grass...