[Unity] [ComputeShader] use ComputeShader to calculate the camera field of view

preface:

        For a 1024 * 1024 plot, it is necessary to calculate which plots are within the camera range in real time. In order to improve the operation efficiency, ComputeShader is used here.

1. Preparatory work

        First, right-click Unity to create a ComputeShader, then create a Canvas and put a RawImage (an image for debugging).

        Then create a control class and add the following control components:

public class ComputeTest : MonoBehaviour
{
    #region external assignment attribute

    /// <summary>
    ///Debugging pictures for display;
    /// </summary>
    public RawImage ShowImage;

    /// <summary>
    ///Current calculated Shader
    /// </summary>
    public ComputeShader shader;

    /// <summary>
    ///Current master camera
    /// </summary>
    public Camera mainCamera;

    #endregion
}

2. Write ComputeShader Code:

        The principle is that the client transmits the VP matrix of the camera, and then calculates whether the current grid is displayed in the Shader. Then write it into the target image.

        (there are many specific introductions to ComputeShader on the Internet, so I won't repeat them here)

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel WolrdMapVisable

// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;

float4x4 WorldToCameraMatrix;

//Calculate visibility using VP matrix
bool IsVisableInCamera (float x,float z)
{
    float4 clipPos =mul(WorldToCameraMatrix, float4(x,0,z,1));
    float3 ndcPos = float3(clipPos.x / clipPos.w, clipPos.y / clipPos.w, clipPos.z / clipPos.w);

    float view_x = 0.5f + 0.5f * clipPos.x / clipPos.w;
    float view_y = 0.5f + 0.5f * clipPos.y / clipPos.w;

    return view_x>=0 && view_x<=1 && view_y>=0 && view_y<=1 && clipPos.w>0;
}

[numthreads(8,8,1)]
void WolrdMapVisable (uint3 id : SV_DispatchThreadID)
{
    float x=id.x;//0~1024
    float z=id.y;//0~1024

    bool IsVisialbe = IsVisableInCamera(x,z);

    float retValue = IsVisialbe?1:0;    
    //White if visible, black if invisible
    half4 col=half4(retValue,retValue,retValue,1);

    Result[id.xy] =col;
}

        The picture here is transmitted from the outside and has a fixed size (1024 * 1024);

3. Writing Unity C# control code

    private const int TextureSize = 1024;//Entire map size
    private const int ThreadGroup = 8;
    private const int ThreadGroupSize = TextureSize / ThreadGroup;

    //Various attribute names;
    private const string ComputeShaderName = "WolrdMapVisable";
    private const string ComputeTextureName = "Result";
    private const string ComputeMatrixName = "WorldToCameraMatrix";

    private int kID;
    private int matixNameID;
    private RenderTexture texture;

    // Start is called before the first frame update
    void Start() { RunComputeShader(); }

    private void RunComputeShader()
    {
        //Set the map for ComputeShader;
        texture = new RenderTexture(TextureSize, TextureSize, 24);
        texture.enableRandomWrite = true;
        texture.filterMode = FilterMode.Point;
        texture.Create();

        //Display the picture on the UI and try it out;
        ShowImage.texture = texture;

        //Get some attributes of Shader;
        kID = shader.FindKernel(ComputeShaderName);
        matixNameID = Shader.PropertyToID(ComputeMatrixName);

        //Start Shader:
        shader.SetTexture(kID, ComputeTextureName, texture);
        UpdateComputeShader();
    }

    void Update() { UpdateComputeShader(); }

    /// <summary>
    ///Call once per frame to set the camera matrix
    /// </summary>
    private void UpdateComputeShader()
    {
        shader.SetMatrix(matixNameID, mainCamera.projectionMatrix * mainCamera.worldToCameraMatrix);
        shader.Dispatch(kID, ThreadGroupSize, ThreadGroupSize, 1);
    }

        This C# code is relatively simple. Just take a look at it.

        Then run it to see:

          You can see that the visual range (white) changes with the camera.

4. Return the calculation result

        However, the above result calculated by ComputeShader is only a picture, and specific values cannot be obtained in C# inside. In some cases (such as displaying a small map), this is OK. However, if we want to know which coordinate points are within the camera range, we need to use ComputeBuffer.

        Make the following supplements in the C# Code:

......

    private const string ComputeBufferName = "VisableCellBuffer";
    private int ComputeBufferID;
    private ComputeBuffer AppendBuffer;

......

private void RunComputeShader()
{
......
        //Here, the theoretical maximum value must be passed in when initializing the AppendBuffer, otherwise subsequent reading will fail;
        AppendBuffer = new ComputeBuffer(TextureSize * TextureSize, sizeof(int), ComputeBufferType.Append);
        ComputeBufferID = Shader.PropertyToID(ComputeBufferName);
......
}

private void UpdateComputeShader()
{
......
        SetAppedBuffer();
......
}

private void SetAppedBuffer()
{
        AppendBuffer.SetCounterValue(0);
        shader.SetBuffer(kID, ComputeBufferID, AppendBuffer);
}

  The following supplements need to be made in ComputeShader:

......

AppendStructuredBuffer<int2> VisableCellBuffer;

......

[numthreads(8,8,1)]
void WolrdMapVisable (uint3 id : SV_DispatchThreadID)
{
......

    if(IsVisialbe)
    {
        //Convert the visible grid into int value and store it in VisableCellBuffer.
        int2 index=x*10000+z;
        VisableCellBuffer.Append(index);
    }

......
}

         In this way, the display grid can be converted into a coordinate ID and transmitted.

         Then read as follows:

    private int[] ArrRet = new int[ThreadGroupSize];

    private void ReadAppendBuffer()
    {
        var countBuffer = new ComputeBuffer(1, sizeof(int), ComputeBufferType.IndirectArguments);
        ComputeBuffer.CopyCount(AppendBuffer, countBuffer, 0);

        //The first data obtained through this method is the number of appendbuffers
        int[] counter = new int[1] { 0 };//ToDo, you can continue to optimize here;
        countBuffer.GetData(counter);

        int leftCount = counter[0];
        int startIndex = 0;

        while (leftCount > 0)
        {
            int leftSize = Mathf.Min(leftCount, ThreadGroupSize);
            AppendBuffer.GetData(ArrRet, 0, startIndex, leftSize);//Read it out several times;

            for (int i = 0; i < ThreadGroupSize; i++)
            {
                //Debug.Log($"[{startIndex}|{i}] {ArrRet[i]}");
            }

            leftCount = leftCount - ThreadGroupSize;
            startIndex = startIndex + ThreadGroupSize;
        }
    }

         In this way, you can read the calculation results of ComputeShader.

PS:

        Reading through ComputeBuffer actually consumes some performance. If the data is too large, you need to consider whether it is cost-effective.

 

 

Keywords: Unity Game Development Shader

Added by Yamakazi on Sun, 12 Sep 2021 02:10:26 +0300