当前位置: 首页 > news >正文

Unity-ComputeShader

        ComputerShader是Unity中一个很实用且高性能的功能,本期分享一下ComputerShader的学习记录。最后会分享一个完全由computershader画出来的心形纹理,效果如下。

  

一.什么是ComputeShader?

        Compute Shader(计算着色器)是一种在图形处理器(GPU)上执行通用计算任务的程序。它不直接用于渲染图形(即不负责将三角形绘制到屏幕上),而是专注于利用 GPU 强大的并行处理能力来高效地执行大量数据操作和数学计算。

        一般CPU与GPU之间的信息传递容易出现性能瓶颈。ComputeShader(cs)的核心优势在于其高吞吐量低延迟(对于并行任务)。它能将 CPU 上原本需要串行执行或效率低下的任务,卸载到 GPU 上进行并行加速。

        Compute Shader 是 GPGPU(General-Purpose computing on Graphics Processing Units,利用图形处理器进行通用计算)的典型实现。

二.创建ComputeShader

        创建一个新的ComputeShader(本质也是资源文件),顺便了解一下ComputeShader中的一些核心概念。

默认的ComputeShader

// 每个 #kernel 告诉要编译哪个函数;你可以有多个内核
#pragma kernel CSMain// 创建一个带有 enableRandomWrite 标志的 RenderTexture 并设置它
// 使用 cs.SetTexture
RWTexture2D<float4> Result;[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{// TODO:在此处插入实际代码!Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

1.pragma kernel CSMain

        通过预编译指令定义了一个名称为CSMain的内核函数。当该内核被调度(Dispatch)时,核函数可以在 GPU 上被大量线程并行执行。

2.默认的RWTexture2D<float4> result是什么?

        RWTexture2D是 HLSL中用于 Compute Shader 的一种可读写纹理类型

(1)RW(Read-Write):表示这个纹理不仅可以像普通纹理一样被读取,还可以被 Compute Shader 的线程写入数据。这是它与Texture2D最核心的区别;

(2)Texture2D:表示它是一个二维纹理。

(3)<float4>:表示纹理中每个像素的颜色格式是float4。你可以根据需求使用自定义格式。

(4)Result:变量的名称。可以叫它任何你觉得有意义的名字。

RWTexture2D<float4> result不是必须的。 Compute Shader 不一定非要输出到RWTexture2D 它的输出可以是RWStructuredBuffer,AppendStructuredBuffer或没有显式输出。

三.ComputeShader的核心概念

1.kernel (内核 )

1.什么是kernel?

        在Compute Shader 的语境中,内核 (Kernel) 可以理解为 GPU 上执行的单个计算任务或程序。它是 Compute Shader 文件的基本执行单元。一个 Compute Shader 文件可以包含一个或多个内核函数(用#pragma kernel kernelName来声明)。每个内核函数都封装了 GPU 上并行执行的特定计算逻辑。kernel定义的核函数被设计成可以在 GPU 上被大量线程并行执行。 当从 C# 脚本调度 Compute Shader 时,需要指定要运行哪个内核函数(如computeShader.Dispatch(computerShader.FindKernel("CSMain")))。 

2.属性[numthreads(X,Y,Z)]

1.[numthreads(X,Y,Z)]是什么?

        [numthreads(X,Y,Z)]是必须的,每个核函数前面都需要定义numthreads。这个属性告诉 GPU 每个线程组的“大小”。GPU 在执行 Compute Shader 时,会把你的计算任务分解成很多个线程组来处理。而每个线程组内部,又会进一步分解成更小的单位——线程

每个线程组中可以执行的线程总数量=X*Y*Z;

        [numthreads(X,Y,Z)]定义了这些线程是如何在线程组内部组织起来的。如果缺少这个属性,GPU 就不知道该如何分配和管理线程来执行你的内核代码,因此编译会失败。

关于线程组

在同一个线程组中的线程可以共享变量(内存),并能设置同一个线程组内的线程同步。

2.[numthreads(X,Y,Z)]怎么理解?

        这里的 X, Y, Z 分别代表了在一个线程组 (Thread Group) 内部,线程 (Thread) 在各自维度上的数量。

X: 线程组在 X 维度上的线程数量。

Y: 线程组在 Y 维度上的线程数量。

Z: 线程组在 Z 维度上的线程数量。

        这三个值都是正整数。例如[numthreads(8,8,1)]意味着这个线程组有 8 个线程宽 (X)、8 个线程高 (Y),以及 1 个线程深 (Z)。那么一个线程组总共有 8times8times1=64 个线程。

示意图来源:Unity3D Shader系列之Compute Shader基础及图像灰度化-CSDN博客

       

3.Compute Shader 内线程相关语义

        SV代表 "System Value"(系统值)。(比较图形Shader里的SV_Target,这些带有sv前缀的变量是由 GPU 硬件或图形 API自动生成和填充的特殊值。它们提供了关于当前正在执行的着色器调用的一些上下文信息。)

 示意图来源:Unity3D Shader系列之Compute Shader基础及图像灰度化-CSDN博客

(1)SV_DispatchThreadID

含义:当前线程在整个调度任务中全局ID

用途:这是最常用的 ID,通常直接用来索引到你的输入/输出缓冲区或纹理中的数据元素。

(2)SV_GroupID

含义:当前线程所属的线程组的ID

用途:如果你需要对每个线程组进行一些独立的操作,或者根据线程组的 ID 来访问一些数据,就会用到它。

(3)SV_GroupThreadID

含义:当前线程在其所属线程组内部局部ID

范围:它的范围是从(0,0,0)到(X-1,Y-1,Z-1) 。

用途:常用于线程组内部的协作,例如访问groupshared内存,或者在线程组内部进行某种模式化的计算。

(4)SV_GroupIndex

含义:当前线程在其所属线程组内部一维平面索引

计算方式SV_GroupIndex=

SV_GroupThreadID.z*  (X*Y)

+SV_GroupThreadID.y*X

+SV_GroupThreadID.x 

用途:当你将线程组内部的线程视为一个平铺的数组时,它很有用,比如在 groupshared 内存中进行一维索引。

(*-*)举个例子:

假如我们用 Compute Shader 来处理一张 16x16 像素的图片,比如上色。

// 每个线程组会处理 8x8 的像素块
[numthreads(8,8,1)] 
void ProcessImage(uint3 globalID : SV_DispatchThreadID,      // (1) 全局唯一 IDuint3 groupID : SV_GroupID,               // (2) 线程组的全局 IDuint3 groupThreadID : SV_GroupThreadID,   // (3) 线程组内的局部 IDuint groupIndex : SV_GroupIndex)         // (4) 线程组内的一维局部索引

这张 16x16 的图片,因为每个线程组处理 8x8 的像素块,所以它被分成了 4 个线程组(一个 2x2 的网格):

现在,我们选择一个具体的像素,比如图片上的像素 (X=9, Y=5),看看处理这个像素的线程,它的四个 ID 分别是什么:

1.SV_DispatchThreadID (全局唯一 ID)

含义:是当前线程在整个 Compute Shader 调度任务中的绝对坐标。

例子:处理像素 (X=9, Y=5) 的线程:

它的SV_DispatchThreadID=(9,5,0) 

2.SV_GroupID(线程组的全局 ID)

含义:这个 ID 告诉当前线程,它属于哪个线程组。

例子:处理像素 (X=9, Y=5) 的线程:

像素 X=9 在 8-15 范围内,Y=5 在 0-7 范围内。所以,这个像素属于 线程组 (1,0)。那么,这个线程的SV_GroupID就是(1,0,0) 

3.SV_GroupThreadID (线程组内的局部 ID)

含义:这个 ID 告诉当前线程,它在自己所属的线程组内部相对位置。可以理解为在自己的 8x8 小方块里的坐标。

范围:它总是从(0,0,0)开始,到(X-1,Y-1,Z-1) 结束。在我们例子中,范围是(0,0,0)到(7,7,0)。

例子:处理像素 (X=9, Y=5) 的线程:

SV_GroupID是(1,0,0) 。

全局 X 是 9,这个线程组的起始 X 是 8。所以,局部 X = 9 - 8 = 1。

全局 Y 是 5,这个线程组的起始 Y 是 0。所以,局部 Y = 5 - 0 = 5。

那么这个线程的SV_GroupThreadID就是(1,5,0) 

4.SV_GroupIndex(线程组内的一维局部索引)

含义:这是SV_GroupThreadID的一个一维扁平化版本。它把线程组内部的三维坐标转换成一个从 0 开始的单一数字。

计算方式

SV_GroupIndex=

SV_GroupThreadID.z*  (X*Y)

+SV_GroupThreadID.y*X

+SV_GroupThreadID.x 

在[numthreads(8,8,1)]的例子中,Z维度是1。

例子:处理像素 (X=9, Y=5) 的线程:

其SV_GroupThreadID是(1,5,0) 。则SV_GroupIndex=5*8+1=41

四.ComputeShader中常用的输出类型

        Compute Shader 的目标是“计算并生成数据”。 调度 Compute Shader 是为了让它执行一些复杂的并行计算,并将结果写入到某个地方。这个“写入”的目标通常就是RWTexture2D或RWStructuredBuffer。下面总结ComputeShader中常用的输出类型:

1.RWStructuredBuffer<T>

        RWStructuredBuffer<T>可以理解为 GPU 显存中的一张可读写的表格,或者说是一个结构体数组。每个“行”都是自定义数据结构T,每个“列”是该结构体中的成员变量。

核心特点

结构化数据: 你可以定义任意复杂的C#struct,并在 HLSL 中用struct映射它。

随机读写: Compute Shader 的每个线程都可以通过索引(指线程的相关语义)直接读取或写入缓冲区中的任何一个元素,就像访问普通数组一样。

双向通信:RW前缀表示它既可以读(Read)也可以写(Write)。这意味着 Compute Shader 既能从缓冲区中读取旧数据进行处理,也能将新数据写回同一个缓冲区,实现数据在 GPU 上的原地更新。

与 CPU 和其他 Shader 共享:

CPU ↔ GPU:在C#脚本中可以使用ComputeBuffer 从 CPU(SetData) 上传数据到 GPU,也可以从 GPU(GetData) 下载数据回 CPU(但GetData通常很慢,应尽量避免)。

GPU ↔ GPU:RWStructuredBuffer的数据可以被其他的着色器(如顶点着色器、片段着色器)读取,用于渲染。比如,Compute Shader 更新了粒子的位置,顶点着色器就能读取这些新位置来绘制粒子。

2.RWTexture2D<T>

        RWTexture2D<T>可以理解为 GPU 显存中一张可读写的二维图像。它的每个“像素”(或称为“纹素”)都能存储你指定类型的数据。

核心特点

网格状数据: 纹理天生就是为二维网格数据设计的,非常适合图像、高度图、法线图等有明确 X、Y 坐标对应关系的数据。

随机读写: Compute Shader 的每个线程可以根据其全局线程ID直接读取或写入纹理中的任何一个纹素。

与 C# RenderTexture 对应: 在 C# 端RWTexture2D对应的是RenderTexture类型。你需要将 RenderTexture的enableRandomWrite设置为true,才能让 Compute Shader 写入它。

  RenderTexture rt = new RenderTexture(size, size, 0);rt.enableRandomWrite = true;  //启用写入功能rt.Create();   

高性能图像操作: GPU 的硬件本身就是为纹理处理而优化的,所以对RWTexture2D的读写效率非常高。

结果可直接渲染: Compute Shader 处理完的RWTexture2D可以直接赋值给任何材质的纹理槽,然后被渲染到屏幕上。

一个画心形小例子

C#控制脚本

using System;
using UnityEngine;[ExecuteAlways]
public class HeartCSTex : MonoBehaviour
{public ComputeShader cs;public Material mat;public int size=512;public float heartScale_x=1;public float heartScale_y=1;public float yScaleBound=0.3f;public float animSpeed=1f;public float heartRim=1f;[Range(-1,1)]public float heartRimThread=1;int kernel;private RenderTexture rt;void Start(){kernel = cs.FindKernel("CSMain");rt = new RenderTexture(size, size, 0);rt.enableRandomWrite = true;  rt.Create();   //cs对rt进行计算cs.SetTexture(kernel, "Result", rt); mat.SetTexture("_MainTex", rt);}private void Update(){rt = new RenderTexture(size, size, 0);cs.SetFloat("_TextureWidth",size);cs.SetFloat("_TextureHeight",size);cs.SetFloat("_HeartScale_X",heartScale_x);cs.SetFloat("_HeartScale_Y",heartScale_y);cs.SetFloat("_Time",Time.time*animSpeed);cs.SetFloat("_YScaleBound",yScaleBound);cs.SetFloat("_HeartRimThread",heartRimThread);cs.SetFloat("_HeartRim",heartRim);rt.enableRandomWrite = true;  rt.Create();cs.SetTexture(kernel, "Result", rt); mat.SetTexture("_MainTex", rt);cs.Dispatch(kernel,   size/8, size/8,1);}
}

ComputeShader

#pragma kernel CSMainRWTexture2D<float4> Result;float _TextureWidth;
float _TextureHeight;
float _HeartScale_X;
float _HeartScale_Y;
float _HeartRimThread;
float _HeartRim;
float _Time;
float _YScaleBound;[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{// 结果在 -1 到 1 之间才能和心形公式的坐标范围匹配float x = (id.x / _TextureWidth - 0.5f) * 2.0f;float y = (id.y / _TextureHeight - 0.5f) * 2.0f;// 调整Y轴,因为在屏幕坐标系中Y通常是朝下的,而数学公式中Y是朝上的y = y * -(_HeartScale_Y + _YScaleBound*sin(_Time)); // 反转Y轴方向,并稍微拉伸一点,让心形更饱满x=x*_HeartScale_X;// 代入心形公式:(x^2 + y^2 - 1)^3 - x^2 y^3 = 0float heartEquation = pow(x*x + y*y - 1.0f, 3.0f) - x*x * pow(y, 3.0f);// 设置一个阈值,判断像素是否在心形内部float4 finalColor;if (heartEquation < _HeartRimThread) // 如果计算结果小于阈值,说明是心形的一部分{// 还可以根据 heartEquation 的值做一些边缘柔化效果// 例如,让边缘颜色渐变:lerp(_HeartColor, _BackgroundColor, saturate(heartEquation / threshold));finalColor = float4(1,0,0,1);}else // 否则就是背景{finalColor = float4(1,1,1,1);//_BackgroundColor;}// 将最终颜色写入结果纹理Result[id.xy] = finalColor;
}

创建一个材质,Shader选择默认有的Unlit/Texture(选哪个Shader不重要,主要是要有属性_MainTex不然C#脚本里找不到),后面computerShader会把心形写入一张RT,进而再将RT作为这个材质的主纹理。

准备的差不多了,挂载控制脚本,拖入ComputerShader和材质引用就应该有效果了。

最终效果

本篇完

http://www.lryc.cn/news/578626.html

相关文章:

  • UE5.6 官方文档笔记 [1]——虚幻编辑器界面
  • C#.Net筑基-优雅LINQ的查询艺术
  • 6.2 实现文档加载和切分和简易向量数据库的功能
  • 图像处理专业书籍以及网络资源总结
  • beego打包发布到Centos系统及国产麒麟系统完整教程
  • 前端第二节(Vue)
  • 微信小程序实现table表格
  • 微信小程序21~30
  • CppCon 2018 学习:EFFECTIVE REPLACEMENT OF DYNAMIC POLYMORPHISM WITH std::variant
  • Linux->进程控制(精讲)
  • 《P5522 [yLOI2019] 棠梨煎雪》
  • 如何分析大语言模型(LLM)的内部表征来评估文本的“诚实性”
  • 在 Docker 容器中使用内网穿透
  • 大语言模型推理系统综述
  • NLP——RNN变体LSTM和GRU
  • 关于vue2使用elform的rules校验
  • 深度学习进阶:自然语言处理的推荐点评
  • (LeetCode 面试经典 150 题) 42. 接雨水 (单调栈)
  • Gartner《Choosing Event Brokers to Support Event-DrivenArchitecture》心得
  • 振荡电路Multisim电路仿真实验汇总——硬件工程师笔记
  • .NET跨平台开发工具Rider v2025.1——支持.NET 10、C# 14
  • K8s Pod调度基础——2
  • Langgraph 学习教程
  • 位运算经典题解
  • python+uniapp基于微信小程序的流浪动物救助领养系统nodejs+java
  • 用 YOLOv8 + DeepSORT 实现目标检测、追踪与速度估算
  • SeaTunnel 社区 2 项目中选“开源之夏 2025”,探索高阶数据集成能力!
  • 华为设备 QoS 流分类与流标记深度解析及实验脚本
  • flv.js视频/直播流测试demo
  • 欢乐熊大话蓝牙知识24:LE Secure Connections 是 BLE 的安全升级术