在 Unity 中shader, 是一种运行在 GPU 上的着色器程序,不过它并不直接参与传统的图形渲染,而是用来执行大量并行计算任务。它能高效地处理数据密集型操作,例如物理模拟、粒子系统、图像处理或其他需要大规模并行计算的场景。

下面介绍几个核心内容,帮助你快速上手并掌握 的常用技巧:

1. 基本概念2. 开发流程

通常的使用步骤包括:

创建 文件:在 Unity 编辑器中,通过 “ → → ” 来生成一个基本的 模板。模板中你会看到类似如下代码:

// 文件名:MyComputeShader.compute
#pragma kernel CSMain

RWStructuredBuffer Result;

[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    // 这里可以根据 id,计算并写入数据到 Result 缓冲区
    Result[id.x] = float3(id.x / 255.0, 0, 0);
}

这里声明为一个可读写的结构化缓冲区,用来在 GPU 与 CPU 间传递数据。

在 C# 中调用:

数据同步与性能调优:提前规划好缓冲区大小、线程组数量,确保数据传递和同步没有瓶颈。由于 不依赖于渲染管线,因此需要你手动管理数据的传入和取回,避免额外的数据复制开销。

3. 注意事项

4.

在 Unity 的 中,“”指的是一个并行执行单元,也可以理解为 GPU 上的入口函数。每个 都包含了你希望在大量数据上并行执行的计算逻辑,你可以在一个 文件中定义多个 ,每个 都用# 标记,从而让 C# 代码能够通过.("")识别并调度它们。

核心要点举例说明

假设你在一个 中定义了如下代码:

// 这里定义了一个名字为 CSMain 的 kernel
#pragma kernel CSMain

// 声明一个可读写的结构化缓冲区
RWStructuredBuffer<float3> Result;

[numthreads(881)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    // 通过线程 id 来决定不同线程计算的内容
    Result[id.x + id.y * 8= float3(id.x / 8.0id.y / 8.00.0);
}

在 C# 脚本中,你会通过如下方式获取并调度这个 :

int kernelID = computeShader.FindKernel("CSMain");
computeShader.SetBuffer(kernelID"Result"computeBuffer);
computeShader.Dispatch(kernelIDgroupCountXgroupCountYgroupCountZ);

这段代码会让 GPU 按照指定的线程组数量并行执行,从而高效完成数据计算。

多个

下面给出一个简单的案例,展示如何在同一个 文件中定义多个 以及如何在 C# 脚本中依次调用它们。这个案例中,我们将使用两个 :

这样可以将不同的计算任务模块化,既便于维护又方便调试。

代码示例

创建一个后缀为.的文件,内容如下:

// 多 Kernel 示例:初始化数据并处理数据

// 定义第一个 Kernel:用于初始化数据
#pragma kernel CS_Init
// 定义第二个 Kernel:用于后处理(例如数据乘以2)
#pragma kernel CS_Process

// 声明一个可读写的结构化缓冲区,用于在 GPU 与 CPU 之间传递数据
RWStructuredBuffer<float> buffer;

// 每个线程组内部的线程数量为 256(x 轴上),根据任务需要可自行调整
[numthreads(25611)]
void CS_Init(uint3 id : SV_DispatchThreadID)
{
    uint index = id.x;
    // 初始化数据:将缓冲区的每个元素赋值为其索引
    buffer[index= (float)index;
}

[numthreads(25611)]
void CS_Process(uint3 id : SV_DispatchThreadID)
{
    uint index = id.x;
    // 对缓冲区内的数据进行后处理:例如将每个元素乘以 2
    buffer[index*= 2.0;
}

在这个示例中,两个 都共享同一个缓冲区。用于将缓冲区中每个元素初始化成相应的索引值,而则读取这些数据进行进一步的处理。注意,根据线程组中的线程数量(这里设置为 256), 时的组数必须能够覆盖整个缓冲区的数据。

C# 脚本调用示例

在 Unity 的 C# 脚本中,我们需要按以下步骤进行操作:

创建并初始化一个与缓冲区大小匹配的。

使用方法获取各个 的索引。

使用将同一个分别绑定到不同的 。

按顺序调用分发第一个 (进行初始化)后,再调用第二个 (后处理)。

从 GPU 读取数据,验证计算结果。

下面是一段示例代码:

using UnityEngine;

public class MultiKernelExample : MonoBehaviour
{
    // 引用对应的 Compute Shader 资源
    public ComputeShader computeShader;
    
    // 缓冲区大小:本例以 1024 项 float 数据作为示例
    private int bufferSize = 1024;
    private ComputeBuffer computeBuffer;

    void Start()
    {
        // 创建一个 ComputeBuffer,每个元素占 sizeof(float) 字节
        computeBuffer = new ComputeBuffer(bufferSizesizeof(float));
        
        // 获取两个 kernel 的索引
        int kernelInit = computeShader.FindKernel("CS_Init");
        int kernelProcess = computeShader.FindKernel("CS_Process");
        
        // 将 ComputeBuffer 绑定到 Compute Shader 的对应变量上
        computeShader.SetBuffer(kernelInit"buffer"computeBuffer);
        computeShader.SetBuffer(kernelProcess"buffer"computeBuffer);
        
        // 根据每个 kernel 的 [numthreads(256,1,1)],确定 Dispatch 的组数
        int threadGroups = Mathf.CeilToInt(bufferSize / 256.0f);
        
        // 调用 CS_Init kernel 初始化缓冲区数据
        computeShader.Dispatch(kernelInitthreadGroups11);
        
        // 调用 CS_Process kernel 对数据进行进一步处理
        computeShader.Dispatch(kernelProcessthreadGroups11);
        
        // 从 GPU 中读取最终数据
        float[] results = new float[bufferSize];
        computeBuffer.GetData(results);
        
        // 打印前 10 项数据,验证结果是否为 (索引 * 2)
        for (int i = 0i < 10i++)
        {
            Debug.Log($"Index {i}: {results[i]}");
        }
    }
    
    private void OnDestroy()
    {
        // 释放 ComputeBuffer 以防内存泄漏
        if (computeBuffer != null)
            computeBuffer.Release();
    }
}

注意事项4. 应用—投影算法

投影点计算

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

StructuredBuffer<float3> inputPoints;
RWStructuredBuffer<float3> outputPoints;

float3 planePoint;
float3 planeNormal;

[numthreads(256,1,1)]
void ProjectPointsMain (uint3 id : SV_DispatchThreadID)
{
    uint index= id.x;
    if(index >= outputPoints.Length)
        return;
    float3 inputPoint = inputPoints[index];
    float distance = dot(inputPoint - planePointplaneNormal);
    outputPoints[index= inputPoint - distance * planeNormal;
}

C#调用

//...

    public int ImagePointSize = 0;
    public Texture2D ProjectionImage;
    
    // 平面的变换组件,用于确定平面的位置和方向
    public Transform planeTransform;

    // 平面的法向量
    public Vector3 PlaneNormal { getprivate set; }

    // 平面上的一个点
    public Vector3 PlanePoint { getprivate set; }

    [ShowInInspectorReadOnly]
    public bool IsSelected { getset; } = false;
    
    public ComputeShader projectionShader;
    private ComputeBuffer inputBuffer;
    private ComputeBuffer outputBuffer;
    public Vector3[] projectedPoints;

//...

    private async UniTaskVoid Project()
    {
        await UniTask.Yield();
        
        var visiblePlyResult = FileDataManager.Instance.GetVisiblePlyResult();
        
        if (visiblePlyResult.Vertices is { Length> 0 })
        {
            int pointCount = visiblePlyResult.Vertices.Length;
            // 设置 compute buffer
            inputBuffer = new ComputeBuffer(pointCountsizeof(float* 3);
            outputBuffer = new ComputeBuffer(pointCountsizeof(float* 3);

            var inputPoints = visiblePlyResult.Vertices;
            
            inputBuffer.SetData(inputPoints);

            int kernel = projectionShader.FindKernel("ProjectPointsMain");

            projectionShader.SetBuffer(kernel"inputPoints"inputBuffer);
            projectionShader.SetBuffer(kernel"outputPoints"outputBuffer);
            projectionShader.SetVector("planePoint"PlanePoint);
            projectionShader.SetVector("planeNormal"PlaneNormal);

            int threadGroupSize = Mathf.CeilToInt(pointCount / 256.0f);
            projectionShader.Dispatch(kernelthreadGroupSize11);

            projectedPoints = new Vector3[pointCount];
            outputBuffer.GetData(projectedPoints);

            inputBuffer.Release();
            outputBuffer.Release();

            var projectedPointList = projectedPoints.ToList();
            //...
        }
    }
//...

总结

Unity 中的 为开发者提供了一种高效利用 GPU 并行计算能力的方式,通过编写 HLSL 代码并在 C# 中调用,可以大幅优化计算密集型任务的执行效率。不过在使用时需特别注意平台兼容性、数据同步问题以及资源释放的管理。对于需要高性能数据处理和复杂计算的项目shader,掌握 的使用无疑能带来显著提升。


限时特惠:
本站持续每日更新海量各大内部创业课程,一年会员仅需要98元,全站资源免费下载
点击查看详情

站长微信:Jiucxh

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注