在 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(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
// 通过线程 id 来决定不同线程计算的内容
Result[id.x + id.y * 8] = float3(id.x / 8.0, id.y / 8.0, 0.0);
}
在 C# 脚本中,你会通过如下方式获取并调度这个 :
int kernelID = computeShader.FindKernel("CSMain");
computeShader.SetBuffer(kernelID, "Result", computeBuffer);
computeShader.Dispatch(kernelID, groupCountX, groupCountY, groupCountZ);
这段代码会让 GPU 按照指定的线程组数量并行执行,从而高效完成数据计算。
多个
下面给出一个简单的案例,展示如何在同一个 文件中定义多个 以及如何在 C# 脚本中依次调用它们。这个案例中,我们将使用两个 :
这样可以将不同的计算任务模块化,既便于维护又方便调试。
代码示例
创建一个后缀为.的文件,内容如下:
// 多 Kernel 示例:初始化数据并处理数据
// 定义第一个 Kernel:用于初始化数据
#pragma kernel CS_Init
// 定义第二个 Kernel:用于后处理(例如数据乘以2)
#pragma kernel CS_Process
// 声明一个可读写的结构化缓冲区,用于在 GPU 与 CPU 之间传递数据
RWStructuredBuffer<float> buffer;
// 每个线程组内部的线程数量为 256(x 轴上),根据任务需要可自行调整
[numthreads(256, 1, 1)]
void CS_Init(uint3 id : SV_DispatchThreadID)
{
uint index = id.x;
// 初始化数据:将缓冲区的每个元素赋值为其索引
buffer[index] = (float)index;
}
[numthreads(256, 1, 1)]
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(bufferSize, sizeof(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(kernelInit, threadGroups, 1, 1);
// 调用 CS_Process kernel 对数据进行进一步处理
computeShader.Dispatch(kernelProcess, threadGroups, 1, 1);
// 从 GPU 中读取最终数据
float[] results = new float[bufferSize];
computeBuffer.GetData(results);
// 打印前 10 项数据,验证结果是否为 (索引 * 2)
for (int i = 0; i < 10; i++)
{
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 - planePoint, planeNormal);
outputPoints[index] = inputPoint - distance * planeNormal;
}
C#调用
//...
public int ImagePointSize = 0;
public Texture2D ProjectionImage;
// 平面的变换组件,用于确定平面的位置和方向
public Transform planeTransform;
// 平面的法向量
public Vector3 PlaneNormal { get; private set; }
// 平面上的一个点
public Vector3 PlanePoint { get; private set; }
[ShowInInspector, ReadOnly]
public bool IsSelected { get; set; } = 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(pointCount, sizeof(float) * 3);
outputBuffer = new ComputeBuffer(pointCount, sizeof(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(kernel, threadGroupSize, 1, 1);
projectedPoints = new Vector3[pointCount];
outputBuffer.GetData(projectedPoints);
inputBuffer.Release();
outputBuffer.Release();
var projectedPointList = projectedPoints.ToList();
//...
}
}
//...
总结
Unity 中的 为开发者提供了一种高效利用 GPU 并行计算能力的方式,通过编写 HLSL 代码并在 C# 中调用,可以大幅优化计算密集型任务的执行效率。不过在使用时需特别注意平台兼容性、数据同步问题以及资源释放的管理。对于需要高性能数据处理和复杂计算的项目shader,掌握 的使用无疑能带来显著提升。
限时特惠:本站持续每日更新海量各大内部创业课程,一年会员仅需要98元,全站资源免费下载
点击查看详情
站长微信:Jiucxh