Getting started with Unity Compute Shader (a large number of object random assignment color experiments)

foreword

have not seen you for a long time! Today, I use Unity Compute Shader to implement random color assignment of a large number of cubes, as an introductory exercise for my Unity ComputeShader. This exercise can be said to be very classic.

A brief introduction to Compute Shader

Using Compute Shader allows the GPU to participate in the operation of any data type, thereby reducing the computing load of the CPU. It is well known that GPUs are very good at executing a large number of parallel simple algorithms, so handing such operations to GPUs will effectively improve the efficiency of projects. Unity's Compute Shader is written in HLSL.
You can think of the Compute Shader as an extension of the C# script. The Compute Shader needs to be told when and how to execute it through the script.

Compute Shader in Editor

You can create a new Compute Shader in Create -> Shader -> Compute Shader.
insert image description here

Introduction to Unity Compute Shader Code Principle

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

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

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    
    
    // TODO: insert actual code here!

    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

This is the default code of the Compute Shader. You first declare the function (#pragma kernel CSMain) to be called when the Compute Shader is running (when Dispatch from C#). There can be many kernels. Parameters used to pass data with C# scripts (RWTexture2D here, StructuredBuffer type is generally used to process arbitrary data), and the definition of the function itself (here it generates a bunch of float4 data, which is actually a picture), function parameters The thing inside the brackets is the thread ID.
In addition, you also need to adjust the parameters in numthreads. This thing specifies the number of threads that the Compute Shader will generate. It needs to be set according to the situation. I don't know the specifics, so keep the default.
The operation inside the function should be as simple as possible, and the GPU cannot perform branch operations very well, so do not do operations such as if in it.
If you are interested in the details, link this article written by a foreigner to explain more.

Getting Started With Compute Shaders In Unity
Zhihu Chinese translation version:
Unity3d | Talking about Compute Shader
Brief Introduction to
Unity Compute Shader Getting Started

CPU control group

In order to reflect the role of Compute Shader, we first implement a control group method that only uses CPU.

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

struct Cube
{
    
    
    public Vector3 position;
    public Color color;
}

public class ComputeShaderDemo : MonoBehaviour
{
    
    
    public int repetions;
    public List<GameObject> objects;

    public int count = 50;

    private void Start()
    {
    
    
        CreateCubes();
    }

    private void CreateCubes()
    {
    
    
        for (int i = 0; i < count; i++)
        {
    
    
            for (int j = 0; j < count; j++)
            {
    
    
                var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
                go.transform.position = new Vector3(i, j, 0);
                Color color = UnityEngine.Random.ColorHSV();
                go.GetComponent<MeshRenderer>().material.SetColor("_Color", color);
                objects.Add(go);
                go.transform.SetParent(this.transform);
            }
        }
    }

    [ContextMenu("OnRandomize CPU")]
    public void OnRandomizeCPU()
    {
    
    
        for (int i = 0; i < repetions; i++)
        {
    
    
            for (int c = 0; c < objects.Count; c++)
            {
    
    
                GameObject obj = objects[c];
                obj.transform.position = new Vector3(obj.transform.position.x, obj.transform.position.y, UnityEngine.Random.Range(-0.1f, 0.1f));
                obj.GetComponent<MeshRenderer>().material.SetColor("_Color", UnityEngine.Random.ColorHSV());
            }
        }
    }

The above is a C# script. What it does is to randomly assign colors and y-axis coordinates to a bunch of cube objects, and then repeat it several times. The purpose is to simulate simple operations that require a lot of running.
insert image description here
At 50 * 50 cubes, when executed 1000 times, my CPU is already quite stuck, and 10000 repetitions directly freezes.

Solve problems with Compute Shader

Now we will use the Compute Shader to call the GPU for computation.

#pragma kernel CSMain

struct Cube {
    
    
    float3 position;
    float4 color;
};
// StructuredBuffer
RWStructuredBuffer<Cube> cubes;
float repetions;
float resolution;

// GPU将用到的随机算法
float rand(float2 co)
{
    
    
    return(frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453)) * 1;
}

[numthreads(10,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    
    
    float xPos = id.x / resolution;
    Cube cube = cubes[id.x];
    for (int i = 0; i < repetions; i++)
    {
    
    
        float zPos = rand(float2(xPos, cube.position.z));
        cube.position.z = zPos;
        float r = rand(float2(cube.color.r, cube.color.g));
        float g = rand(float2(cube.color.g, cube.color.b));
        float b = rand(float2(cube.color.b, cube.color.r));
        cube.color = float4(r, g, b, 1.0);
    }
    cubes[id.x] = cube;
}

This is the Compute Shader code used to do this. It basically does the OnRandomizeCPU thing. Of course, the data must be passed to the Compute Shader from the C# script. The script, and the script finally uses the results.

    [ContextMenu("OnRandomize GPU")]
    public void OnRandomizeGPU()
    {
    
    
        int colorSize = sizeof(float) * 4;
        int vector3Size = sizeof(float) * 3;
        int totalSize = colorSize + vector3Size;
		// 这玩意将与Compute Shader中的StructuredBuffer绑定
        ComputeBuffer cubeBuffer = new ComputeBuffer(data.Length, totalSize);
		// 将需要处理的数据放到buffer里面
        cubeBuffer.SetData(data);
		// 将cubeBuffer与Compute Shader中的StructuredBuffer绑定(可以这么说吧)
		// 现在cubeBuffer相当于CPU与GPU之间的输入输出缓冲区
        computeShader.SetBuffer(0, "cubes", cubeBuffer);
        computeShader.SetFloat("resolution", data.Length);
        computeShader.SetFloat("repetions", repetions);
        // 执行Compute Shader
        computeShader.Dispatch(0, data.Length / 10, 1, 1);
		// 从缓冲区获取数据
        cubeBuffer.GetData(data);
        // 运用数据
        for (int i = 0; i < objects.Count; i++)
        {
    
    
            GameObject obj = objects[i];
            Cube cube = data[i];
            obj.transform.position = cube.position;
            obj.GetComponent<MeshRenderer>().material.SetColor("_Color", cube.color);
        }
        cubeBuffer.Dispose();
    }
}

This is a C# script. It does what I just said, passing data to the Compute Shader, running the Shader, getting the result and assigning the result to the object.

Effect

The effect can be said to be very good. 10,000 repetitions are completed in seconds. When the number of repetitions reaches 1 million, there is a little delay. The performance bottleneck of Compute Shader often comes from the data transmission process.

Guess you like

Origin blog.csdn.net/qq_37856544/article/details/120418041