Computer Shader是什么?

  Computer shader是一段运行在GPU上的一段程序。

什么时候用Computer shader?

  假如我们把一个cube当作单独的点,用许多个(cube)点来组成一个变换矩阵。

  每帧cpu都需要对矩阵的点进行排序,批处理,将每个点位置复制给GPU,URP每帧需要执行两次,DRP必须执行至少三遍。

  当100*100个点时,也许我们的cpu可以轻松应对,但如果我们想组成分辨率更高的图形,1000 * 1000,一百万个点时,CPU和GPU的工作量会大大的增加,从而失去流畅的体验。

  而CS就是通过将工作转移到GPU上,最大程度的减少CPU和GPU之前的通讯和数据传输量,从而提升渲染性能。总的来说,在需要高频的重复计算时,我们使用CS;

创建一个计算着色器

Assets/Creats/Shader/Computer Shader,创建一个CS文件

打开文件可以看到如下

//第一个红框中,声明了一个kernel,相当于main函数。在一个cs文件里可以定义多个不同的kernel方法
#pragma kernel CSMain //第二个红框,定义前面声明的CSMain函数
void CSMain(uint3 id:SV DispatchThreadID){};

在CSMain函数上面的numthreads(8,8,1)]是什么?我们需要了解一下线程组和线程的概念

线程组、线程

   当GPU执行CS时,会将其分成几个组(线程组),安排它们独立和并行运行。每个小组由多个线程组成。

最左边的是一个dispatch,由它决定分成几个线程组并行。如图所示,图中有3x2x3个thread groups(线程组)

中间的是一个thread group,由一个个线程组成,每个线程有自己的相对位置。图中有4x4x2个线程,在我们上文提到的numthreads(8,8,1)],表示设置每个线程组的线程数8x8x1个;

最右边的是单个线程。

需要注意的是,一个线程组中最大只支持1024个线程数

更近一步,看下图



上半张图是一个5x3x2的Dispatch,每个格子都代表着一个Thread Group

把坐标(2,1,0)的Thread Group打开,是一个10x8x3的Thread Group,每个格子里都是一个线程。

其中的几个概念:

  SV_GroupThreadID:该线程在当前线程组中的坐标,如下半图中箭头指向坐标(7,5,0)

  SV_GroupID:该线程所在线程组在Dispatch的坐标(2,1,0);

  SV_DispatchThreadID:这是该线程全局唯一的ID,相当于在所有线程中该线程的坐标位置,算法为线程组大小*线程数大小+该线程坐标

  SV_GroupIndex:该线程在该线程组中的索引,即线程在这个线程组中排在第几个位置;

  我们可以利用这些ID,定位我们的结构化缓冲区。

了解了这些概念,接下来我们可以做一个案例。通过计算着色器做一个动态的波浪矩阵;

1.首先创建一个C#文件,我们需要先创建组成矩阵的点,我们用Cube代替。

点的位置信息我们先不管,因为我们要交给计算着色器来计算。

 void Awake()
{
for (int i = 0; i < points.Length; i++) {
points[i]= Instantiate(prefab);
points[i].SetParent(transform);
}
}

2.接下来,我们需要一个缓冲区,用于给GPU计算的区域。通过new ComputeBuffer构造函数,第一个参数是我们要创建的缓冲区的长度,我们有一个矩阵的点 边长*边长的点的位置需要计算,所以我们第一个是resolution * resolution,第二个参数是每个点信息的内存大小,一个position是共有三个浮点数,所以是3 * 4个字节的大小;

  positions = new ComputeBuffer(resolution*resolution,12);

分配了缓冲区,我们还需要在disable的时候将缓冲区释放

  private void OnDisable()
{
positions.Release();
positions = null;
}

3.还需要定义一个数组,用于存储从GPU返回的位置信息。长度与我们的点数量是一样的

   pointsArr = new Vector3[resolution * resolution];

Awake的代码就是这些

   void Awake()
{
//位置缓冲区 在这里第一个参数是我们存放的矩阵点的数量
positions = new ComputeBuffer(resolution * resolution, 12);
//从GPU返回的位置信息
pointsArr = new Vector3[resolution * resolution];
//点的实例数组
points = new Transform[resolution * resolution];
//创建点;
for (int i = 0; i < points.Length; i++) {
points[i]= Instantiate(prefab);
points[i].SetParent(transform);
}
}

1.我们要GPU帮我们算出一个波浪矩阵的信息,那么总得给它传递一些信息数据才行。

要想要一个动态波浪的矩阵,随着Time时间变化,Time这个信息我们需要传过去。边长,只有知道了边长,GPU才知道我们的矩阵是什么构造,怎么波动。还需要给它把位置缓冲区传过去,毕竟它需要靠这个给我们返回计算结果。我们通过它们的标识符进行传递。

   //获得着色器属性的存储标识符
static readonly int positionsId = Shader.PropertyToID("_Positions"),
resolutionId = Shader.PropertyToID("_Resolution"),
timeId = Shader.PropertyToID("_Time");
void Update()
{
float time = Time.time;
//给着色器传递当前时间
_ComputeShader.SetFloat(timeId, time);
//给着色器传递当前边长
_ComputeShader.SetInt(resolutionId, resolution);
//给着色器传递位置缓冲区
_ComputeShader.SetBuffer(kernel, positionsId, positions);
}

2.万事具备,开始分派线程组,执行内核函数。线程组的分派也有些门道,比如我们现在是8080的矩阵,6400个点。而我们的一个线程组设置的是[8,8,1],那就是88*1=64点;那么怎么说也得把让这些点有足够的线程数用。那就是6400/64=100个组。如果多了几个点,6500个点呢,那只能再把组数加上去。总之总组数,需要让点够用。但是也不能分配太多,否则会造成性能浪费。至于分配的组的形式,不管是[2,50,1],还是[100,1,1],怎么方便怎么分配;

	//获取内核函数的索引
kernel = _ComputeShader.FindKernel("CSMain");
//分派线程组,执行内核函数
_ComputeShader.Dispatch(kernel, resolution/8, resolution/8, 1);

3.现在GPU并发执行了它的内核函数,但是我们怎么获取它计算的结果呢;我们通过GetData获取缓冲区的数据,并将它复制给你传进去的参数PointArr,我们开头定义的用来存储从GPU返回的位置信息的数组,最后根据返回的信息,将点位置进行更新即可

	  //从位置缓冲区获取结果 将结果复制给pointsArr
positions.GetData(pointsArr);
for (int i = 0; i < pointsArr.Length; i++)
{
//将各个点的位置更新
points[i].localPosition = pointsArr[i];
}

再看看计算着色器是怎么运作的

1.刚刚从C#,也就是CPU段传过来了哪些信息呢。时间_Time,边长_Resolution,位置缓冲区_Positions。我们需要用对应的变量存储起来。变量命名是和前面的标识符获取的属性名对应的;

    RWStructuredBuffer <float3> _Positions;
float _Time;
uint _Resolution;

2.有了这些数据我们可以开始在内核函数内计算 需要的位置信息;[numthreads(8, 8, 1)],根据前面的概念解释,我们知道这是一个线程组的规格,也就是88的一个二维矩形为一个线程组。

我们通过一个id参数,后面加我们需要获取的类型SV_DispatchThreadID,获取到当前线程在所有线程中的三维坐标,因为我们是单个线程组和dispatch设置的都是二维坐标,所以呈现在我们面前的总线程应该是一个(线程组.x
dispatch.x)(线程组.ydispath.y)的二维矩形。而我们的点矩阵被总线程二维的

包含。下图,我们假设线程组我设为[2,2,1],我们的边长是5,所以把dispath设为[3,3,1],即9个线程组,这样才可以完整覆盖我们所有需要计算的点。但是有一行和一列是我们矩阵不需要的点,所以我们把这一行一列除外。即做了一个判断,仅在id.x < _Resolution && id.y < _Resolution作为有效的点位置。



[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
//获取当前索引
uint index=id.x + id.y * _Resolution;
float3 position;
//获取的id.x
position.x = id.x;
position.z = id.y;
//根据x的位置和时间的变化,让y的位置变化起伏
position.y = sin(PI * (position.x/10 + _Time));
if (id.x < _Resolution && id.y < _Resolution) {
_Positions[index] = position;
}
}

效果图

C#完整代码


using UnityEngine;
public class WaveRect : MonoBehaviour
{
//绑定一个计算着色器
[SerializeField]
ComputeShader _ComputeShader = default;
//我们的cube实例
[SerializeField]
Transform prefab = default;
//定义矩阵边长 配置成可控制的范围10-100;
[SerializeField,Range(10,100)]
int resolution = 10;
//储存我们实例的数组
Transform[] points;
//定义结构化缓冲区 用于给计算着色器 计算我们需要的点 的位置
ComputeBuffer positions;
//获得着色器属性的存储标识符
static readonly int positionsId = Shader.PropertyToID("_Positions"),
resolutionId = Shader.PropertyToID("_Resolution"),
timeId = Shader.PropertyToID("_Time");
//存放由计算着色器也就是Gpu返回的点位置信息
private Vector3[] pointsArr;
void Awake()
{
//位置缓冲区 在这里第一个参数是我们存放的矩阵点的数量
positions = new ComputeBuffer(resolution * resolution, 12);
//从GPU返回的位置信息
pointsArr = new Vector3[resolution * resolution];
//点的实例数组
points = new Transform[resolution * resolution];
//创建点;
for (int i = 0; i < points.Length; i++) {
points[i]= Instantiate(prefab);
points[i].SetParent(transform);
}
}
int kernel;
void Update()
{
float time = Time.time;
//给着色器传递当前时间
_ComputeShader.SetFloat(timeId, time);
//给着色器传递当前边长
_ComputeShader.SetInt(resolutionId, resolution);
//给着色器传递位置缓冲区
_ComputeShader.SetBuffer(kernel, positionsId, positions);
kernel = _ComputeShader.FindKernel("CSMain");
//分派线程组,执行内核函数
int count = Mathf.CeilToInt(resolution / 8);
_ComputeShader.Dispatch(kernel, count, count, 1);
//从位置缓冲区获取结果 将结果复制给pointsArr
positions.GetData(pointsArr);
for (int i = 0; i < pointsArr.Length; i++)
{
//将各个点的位置更新
Debug.Log(i +"====="+ pointsArr[i]);
points[i].localPosition = pointsArr[i];
}
} private void OnDisable()
{
positions.Release();
positions = null;
}
}

CS完整代码

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain;
#define PI 3.14159265358979323846 // Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWStructuredBuffer <float3> _Positions;
float _Time;
uint _Resolution;
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
//获取当前索引
float3 position;
position.x = id.x;
position.z = id.y;
position.y = sin(PI * (position.x/10 + _Time));
if (id.x < _Resolution && id.y < _Resolution) {
_Positions[id.x + id.y * _Resolution] = position;
}
}

欢迎批评指正。原文博客http://xmxw.top/index.php/2021/04/19/unitybasic-computer-shader/;

Unity基础—Computer Shader的更多相关文章

  1. 【浅墨Unity3D Shader编程】之二 雪山飞狐篇:Unity的基本Shader框架写法&amp;颜色、光照与材质

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/40955607 作者:毛星云(浅墨)  ...

  2. Unity基础6 Shadow Map 阴影实现

    这篇实现来的有点墨迹,前前后后折腾零碎的时间折腾了半个月才才实现一个基本的shadow map流程,只能说是对原理理解更深刻一些,但离实际应用估计还需要做很多优化.这篇文章大致分析下shadow ma ...

  3. [译]Vulkan教程(13)图形管道基础之Shader模块

    [译]Vulkan教程(13)图形管道基础之Shader模块 Shader modules Unlike earlier APIs, shader code in Vulkan has to be s ...

  4. unity之初识shader

    自己做个总结先.当然文中很多内容都是从各位大神的文档当中看的.我只是站在巨人的肩膀上.       首先什么是shader?其实就是一个在显示屏当中的显示程序,俗称着色器.它可以定义物体在硬件显示屏当 ...

  5. 【unity shaders】:Unity中的Shader及其基本框架

    shader和Material的基本关系 Shader(着色器)实际上就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出.绘图单元可以依据这个输出来将图 ...

  6. unity 基础之InputManager

    unity  基础之InputManager 说一下unity中的InputManager,先截个图 其中Axes指的是有几个轴向!Size指的是有几个轴,改变Size可以添加或者减少轴! Name指 ...

  7. unity 基础学习 transform

    unity  基础学习   transform 1.unity采用的是右手坐标系,X轴右手为+,Y轴向上为+,Z轴朝里为+; 但是我们从3D MAX中导入模型之后,发现轴向并没有遵从这个原理, 其实是 ...

  8. 【Unity Shaders】Shader学习资源和Surface Shader概述

    写在前面 写这篇文章的时候,我断断续续学习Unity Shader半年了,其实还是个门外汉.我也能体会很多童鞋那种想要学好Shader却无从下手的感觉.在这个期间,我找到一些学习Shader的教程以及 ...

  9. Unity 基础

    Unity 基础是unity入门的关键.他将讲解Unity的界面, 菜单项,使用资源,创设场景,并发布版本. 当你读完这段,你将理解unity是怎么工作的,如何有效地使用它,并且完成一个基本的游戏. ...

随机推荐

  1. 5. vue常用高阶函数及综合案例

    一. 常用的数组的高阶函数 假设, 现在有一个数组, 我们要对数组做如下一些列操作 1. 找出小于100的数字: 2. 将小于100的数字, 全部乘以2: 3. 在2的基础上, 对所有数求和: 通常我 ...

  2. Vulhun-y0usef靶机通关

    Vulhub-y0sef靶机通关 安装靶机环境,下载地址:https://www.vulnhub.com/entry/y0usef-1,624/ 网络模式:桥接 目标:user.txt和root.tx ...

  3. vue之provide和inject跨组件传递属性值失败(父组件向子组件传值的两种方式)

    简单介绍:当一个子组件需要用到父组件的父组件的某些参数.那么这个时候为了避免组件重复传参,使用vue的依赖注入是个不错的方法,直接在最外层组件设置一个provide,内部不管多少嵌套都可以直接取到最外 ...

  4. web前端学习笔记(python)(一)

    瞎JB搞]感觉自己全栈了,又要把数据库里面的内容,以web形式展示出来,并支持数据操作.占了好多坑.....慢慢填(主要参考廖雪峰的官网,不懂的再百度) 一.web概念 Client/Server模式 ...

  5. Kubernetes-3.安装

    docker version:19.03.14 kubernetes version:1.19.4 本文介绍使用kubeadm安装Kubernetes集群的简单过程. 目录 使用kubeadm安装k8 ...

  6. R语言barplot ,掌握本篇的内容,基本的条形图都可以画了

    本篇主要想复现文章中的一张图,原图来源(Antibiotic resistome and its association with bacterial communities during sewag ...

  7. 测试成长记录:python调adb无法获取设备信息bug记录

    背景介绍: 一直在负责公司Android自动化的编写工作,采用的是uiautomator2,需要获取设备id来连接设备,就是 adb devices 问题描述: 之前一直用 subprocess.ch ...

  8. 解决appium点击软键盘上的搜索按钮

    在执行appium自动化测试的时候,需要点击软件盘上的搜索按钮. 具体操作步骤如下: 前提:需要事先安装搜狗输入法 1.唤醒软件盘,可以封装到一个类里,用到的时候随时调用. import os#调起s ...

  9. java内存区域的划分

    前言 之前我们探讨过一个.class文件是如何被加载到jvm中的.但是jvm内又是如何划分内存的呢?这个内被加载到了那一块内存中?jvm内存划分也是面试当中必被问到的一个面试题. 什么是JVM内存区域 ...

  10. 盘点Excel中的那些有趣的“bug”

    本文由葡萄城技术团队原创并首发 转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. Excel 1.0早在1985年正式进入市场,距今已经有36年了,虽然在推出 ...