日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

计算着色器(Compute Shaders)

發布時間:2024/1/1 编程问答 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 计算着色器(Compute Shaders) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文 :?https://catlikecoding.com/unity/tutorials/basics/compute-shaders/

1 將工作轉移到GPU(Moving Work to the GPU)

我們圖形的分辨率越高,CPU和GPU需要做的工作就越多,即計算位置和渲染方塊。點的數量等于分辨率的平方,所以雙倍的分辨率會顯著的增加工作負載。我們也許在分辨率100的時候能達到60FPS,但是我們又能推進多遠?并且如果我們抵達了一個瓶頸我們能否使用不同的方法越過瓶頸?

1.1 分辨率200(Resolution 200)

讓我們從將Graph?的最大分辨率從100提升到200開始,并且看一下我們能得到什么樣的性能。

[SerializeField, Range(10, 200)]int resolution = 10;

?

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Graph with resolution set to 200.

?我們現在渲染了40 000 個點。我這里的平均分辨率,DRP降到了10FPS,URP降到了15FPS。

?

?Profiling a build at resolution 200, without VSync, DRP and URP.

?1.2 GPU 圖像(GPU Graph)

?排序、批處理,然后將40 000個點的變換矩陣發送給GPU消耗很多時間。單個矩陣包含16個float?數字,每個是4 字節,一個矩陣就是總共64 字節。40 000 個點就是2.56?百萬字節--差不多2.44M--每次花這些點時都要復制給GPU。這些事URP每幀要做兩次,一次是陰影,一次是常規幾何圖形。DRP至少要做三次,因為它的額外的深度通道,除此之外,除了主平行光之外的每盞燈都要再加一次。

通常情況下,最好將CPU和GPU間的通信和數據量降到最低。由于我們只需要點的位置來顯示他們,最好是這些數據只存在于GPU。這會消除許多數據轉換。但是CPU將不再計算位置,轉而交由CPU計算。幸運的是它很適合這個任務。

讓CPU計算位置需要一個不同的方法。我們將創建一個新的圖像,并保留目前的圖像作為對比。復制Graph?腳本并重命名為GPUGraph。移除pointPrefab?和?points?字段,并移除?Awake,?UpdateFunction, and?UpdateFunctionTransition?方法。

using UnityEngine;public class GPUGraph : MonoBehaviour {//[SerializeField]//Transform pointPrefab;[SerializeField, Range(10, 200)]int resolution = 10;[SerializeField]FunctionLibrary.FunctionName function;public enum TransitionMode { Cycle, Random }[SerializeField]TransitionMode transitionMode = TransitionMode.Cycle;[SerializeField, Min(0f)]float functionDuration = 1f, transitionDuration = 1f;//Transform[] points;float duration;bool transitioning;FunctionLibrary.FunctionName transitionFunction;//void Awake () { … }void Update () { … }void PickNextFunction () { … }//void UpdateFunction () { … }//void UpdateFunctionTransition () { … } }

然后在Update 中移除這些函數的調用

void Update () {…//if (transitioning) {// UpdateFunctionTransition();//}//else {// UpdateFunction();//}}

我們的GPUGraph?組件與Graph?內部不同,但有著相同的配置選項,除了預制件。它包含了功能之間轉換的邏輯,但除此之外不做任何事。創建一個游戲對象并添加此組件,resolution為200,Transition Mode 為 Cycle。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?GPU graph component, set to instantaneous transitions.

?1.3 計算緩沖區(Compute Buffer)

為了保存GPU上的位置,我們需要為他們申請空間。為此我們創建一個ComputeBuffer?對象。在GPUGraph?中添加一個位置緩沖區字段,并在Awake?函數中通過調用new?ComputeBuffer()創建對象。

ComputeBuffer positionsBuffer;void Awake () {positionsBuffer = new ComputeBuffer();}

?我們需要傳遞給緩沖區元素的數量作為參數,也就是分辨率的平方,就像Graph.的位置數組一樣。

positionsBuffer = new ComputeBuffer(resolution * resolution);

compute buffer 包含任意無類型數據。通過第二個參數,我們需要指定每個元素精確的尺寸。我們需要存儲3D位置向量,即包含3個float?數字,所以每個元素是3倍的4?bytes。

positionsBuffer = new ComputeBuffer(resolution * resolution, 3 * 4);

現在我們得到了一個compute buffer,但是這些對象不會再熱重載時生存(not survive hot reloads),也就是說如果我們在play模式下修改代碼它就會消失。我們可以將其從Awake 函數替換到 OnEnable 函數,每次組件被激活時它都會被調用。

void OnEnable () {positionsBuffer = new ComputeBuffer(resolution * resolution, 3 * 4);}

除此之外我們還需要添加?OnDisable?函數,當組件禁用時會被調用,同時在被銷毀和熱重載之前被調用。通過在其中調用Release?函數來釋放緩沖區。這樣聲明的GPU內存可以被立即釋放。

void OnDisable () {positionsBuffer.Release();}

由于這之后我們不在使用這個對象實例,最好將這個字段設為null 。這使得Unity的垃圾回收機制下次運行時可以回收這個對象,如果我們的圖像在play模式時被禁用或銷毀。

void OnDisable () {positionsBuffer.Release();positionsBuffer = null;}

1.4 計算著色器(Compute Shader)

為了在GPU上計算位置,我們必須為其寫一個腳本,也就是一個計算著色器,通過?Assets / Create / Shader / Compute Shader?創建。它將成為我們FunctionLibrary?類的GPU等價物,所以也將其命名為FunctionLibrary?。雖然它是作為一個著色器并使用HLSL 語法,但它作為通用程序運行,而不是常規的用作渲染東西的著色器。因此我將其放置在Scripts?文件夾。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Function library compute shader asset.

?打開這個文件并移除它默認的內容。一個計算著色器需要包含一個主函數作為核心,通過#pragma kernel?指令后面跟著一個名字來指定,就像我們表面著色器的#pragma surface。這條指令作為第一行也是當前唯一一行,使用名字FunctionKernel

#pragma kernel FunctionKernel

在指令下面來定義函數。

#pragma kernel FunctionKernelvoid FunctionKernel () {}

1.5 計算線程(Compute Threads)

當GPU被安排執行計算著色器時,它將它的任務劃分為多個組然后獨立的或平行的調度他們。每個組由若干線程組成,這些線程執行相同的計算但是有不同的輸入。通過為我們的核心函數添加numthreads 屬性,我們必須詳細說明每個組有多少個線程。最簡單的選項是所有三個參數都使用1,這使每個組只運行一個線程。

[numthreads(1, 1, 1)] void FunctionKernel () {}

GPU 硬件包含始終執行特定數量的lockstep里的線程的計算單元。它們被稱為warps 或?wavefronts。如果一個組的線程數比warp 大小要小,一些線程就會空運轉,浪費時間。如果線程數量超出了,CPU會給每個組使用更多warps。通常,默認使用64個線程比較好,因為這和ADM GPU的warp大小匹配,如果是NVidia GPU則是32,所以后者每個組將使用兩個warp。在現實中硬件會更復雜并且會對線程組做更多,但是這和我們簡單的圖形無關。

numthreads?的三個參數可以被用來組織線程是1、 2 還是3維度的。例如,(64, 1, 1)是一維的,而(8, 8, 1) 數量相同但是提供了一個2D 的8X8 方格。由于我們基于2D uv坐標定義我們的點,我們使用后者。

[numthreads(8, 8, 1)]

這里的線程是包含三個無符號整型的向量,我們可以通過給我們的函數添加uint3?參數來實現。

void FunctionKernel (uint3 id) {}

我們必須明確的指明這個參數是作為線程標識。為此我們在參數名字后面添加SV_DispatchThreadID?關鍵字。

void FunctionKernel (uint3 id: SV_DispatchThreadID) {}

1.6 UV坐標(UV Coordinates)

如果我們知道圖像的步長,我們可以將線程標識轉換為坐標。給計算著色器添加一個屬性命名為_Step?,就像我們給我們的表面著色器添加_Smoothness?

float _Step;[numthreads(8, 8, 1)] void FunctionKernel (uint3 id: SV_DispatchThreadID) {}

然后創建一個GetUV?函數,將線程標識作為它的參數并返回float2型的UV坐標。我們可以使用Graph?中循環點時同樣的邏輯。

float _Step;float2 GetUV (uint3 id) {return (id.xy + 0.5) * _Step - 1.0; }

1.7 設置位置(Setting Positions)

?為了存儲位置我們需要訪問位置緩沖區。在HLSL 中,一個計算緩沖區被看作是一個結構體緩沖區。因為我們需要對其進行讀寫,所以添加一個RWStructuredBuffer. 字段,命名為_Positions

RWStructuredBuffer _Positions;float _Step;

在這個例子中我們需要列出緩沖區元素的類型。位置是float3?值,我們直接寫在RWStructuredBuffer?后面,并用尖括號括起來。

RWStructuredBuffer<float3> _Positions;

為了存儲點的位置,我們需要基于線程標識為其分配一個索引。我們需要知道圖像的分辨率。所以添加一個_Resolution?屬性,類型是uint?以匹配標識的類型。

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

然后創建一個SetPosition?函數以設置點,傳遞它一個標識和要設置的點。我們將使用標識的x 部分加上y部分乘以分辨率作為索引。這樣我們就將2D數據按順序存儲到了1D數組中。

float2 GetUV (uint3 id) {return (id.xy + 0.5) * _Step - 1.0; }void SetPosition (uint3 id, float3 position) {_Positions[id.x + id.y * _Resolution] = position; }

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Position indices for 3×3 grid.

?需要注意的是我們組的每個計算都是8*8個點的網格。如果圖形的分辨率不是8的倍數,我們最后將得到一行和一列的一些點的計算是越界的。這表明那些點會在緩沖區之外或與有效的索引沖突,而這會破壞我們的數據。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Going out of bounds.

可以通過限制標識的X 和 Y 小于分辨率來避免非法的位置

void SetPosition (uint3 id, float3 position) {if (id.x < _Resolution && id.y < _Resolution) {_Positions[id.x + id.y * _Resolution] = position;} }

1.8 Wave 功能(Wave Function)

我們現在可以通過FunctionKernel?獲得UV坐標,并通過我們創建的函數設置位置。先從設置位置為0開始

[numthreads(8, 8, 1)] void FunctionKernel (uint3 id: SV_DispatchThreadID) {float2 uv = GetUV(id);SetPosition(id, 0.0); }

我們首先只支持Wave 功能,也就是我們庫里最簡單的功能。為了讓其動起來我們需要知道時間,所以添加一個_Time?屬性。

float _Step, _Time;

然后從FunctionLibrary?類復制?Wave?函數,將其插到FunctionKernel 之上。為了將其轉換為 HLSL 函數,移除public?static?修飾符,用float3?替換Vector3?,用sin. 替換 Sin。

float3 Wave (float u, float v, float t) {float3 p;p.x = u;p.y = sin(PI * (u + v + t));p.z = v;return p; }

最后還缺少定義是PI。我們將添加一個宏來定義它。添加#define PI?然后在其后面加上數字,我們使用3.14159265358979323846. 這比一個float?值要精確很多。

#define PI 3.14159265358979323846float3 Wave (float u, float v, float t) { … }

現在我們用 Wave 函數來計算位置。

void FunctionKernel (uint3 id: SV_DispatchThreadID) {float2 uv = GetUV(id);SetPosition(id, Wave(uv.x, uv.y, _Time)); }

1.9 分發一個計算著色器核心(Dispatching a Compute Shader Kernel)

我們現在有一個計算并存儲我們圖形點位置的核心函數,下一步將其在GPU上運行。為此GPUGraph?需要訪問計算著色器,添加一個序列化字段ComputeShader?然后和我們的資源關聯起來

[SerializeField]ComputeShader computeShader;

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Compute shader assigned.

?我們需要設置一些計算著色器的屬性。為此我們需要知道他們在Unity中的標識。它們都是整型,可以通過調用Shader.PropertyToID?并傳入一個字符串獲得。這些標識是按需聲明的,并在app或編輯器運行時保持不變,所以我們可以直接用靜態字段存儲它們。先從_Positions 屬性開始。

static int positionsId = Shader.PropertyToID("_Positions");

我們永不會修改這些字段,所以我們可以為其添加readonly?修飾符。

static readonly int positionsId = Shader.PropertyToID("_Positions");

存儲?_Resolution,?_Step, 和_Time? 的標識符

static readonly intpositionsId = Shader.PropertyToID("_Positions"),resolutionId = Shader.PropertyToID("_Resolution"),stepId = Shader.PropertyToID("_Step"),timeId = Shader.PropertyToID("_Time");

接下來,創建一個UpdateFunctionOnGPU?函數計算步長并設置分辨率、步長和時間屬性。通過調用SetInt?和?SetFloat?來實現。

void UpdateFunctionOnGPU () {float step = 2f / resolution;computeShader.SetInt(resolutionId, resolution);computeShader.SetFloat(stepId, step);computeShader.SetFloat(timeId, Time.time);}

我們還需要設置位置緩沖區,它并不復制任何數據而是將緩沖區鏈接到核心(kernel)。通過調用?SetBuffer實現,它和其他函數很像只是多了一個參數。它的第一個參數是核心函數的索引,因為一個計算著色器可以包含多個核心,緩沖區可以被鏈接到特定的一個。我們可以通過調用計算著色器上的FindKernel?來獲得核心索引,但我們只有一個核心,它的索引會總是0,所以我們可以直接使用這個值。

computeShader.SetFloat(timeId, Time.time);computeShader.SetBuffer(0, positionsId, positionsBuffer);

設置完緩沖區后我們可以運行我們的核心,調用計算著色器上的有四個參數的Dispatch?函數。第一個是核心的索引,另外三個是運行的組的數量。所有維度都使用1,意思就是只有第一個有8x8位置的組被計算。

computeShader.SetBuffer(0, positionsId, positionsBuffer);computeShader.Dispatch(0, 1, 1, 1);

因為我們的組是8X8的尺寸,我們需要X和Y等于分辨率除以8,向上取整。

int groups = Mathf.CeilToInt(resolution / 8f);computeShader.Dispatch(0, groups, groups, 1);

最后在Update中調用UpdateFunctionOnGPU?來運行我們的核心。

void Update () {…UpdateFunctionOnGPU();}

現在,在play模式下,我們已經每幀在計算所有圖像的位置,即使我們沒有注意到這點并且沒有對數據做任何事。?

2 程序化繪制(Procedural Drawing)

下一步是繪制這些點,且不必從CPU傳遞任何變換矩陣到GPU。因此著色器需要從緩沖區獲得正確的位置而不是標準矩陣。

2.1 繪制許多網格(Drawing Many Meshes)

因為那些點已經在GPU上存在,我們不需要在CPU端記錄它們。我們甚至不需要為他們建游戲對象。相反,我們需要通過一個命令通知GPU用特定的材質繪制特定的網格。為了能配置繪制些什么,在GPUGraph. 中添加序列化字段?Material?和?Mesh?。我們先用我們已有的Point Surface?材質來繪制DRP點。網格我們用默認的方塊。

[SerializeField]Material material;[SerializeField]Mesh mesh;

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Material and mesh configured.

?我們通過調用?Graphics.DrawMeshInstancedProcedural?來實現程序化繪制,并傳入網格,子網格索引和材質作為參數。子網格索引是為一個網格包含多個部分時準備的,我們例子沒有這種情況所以為0,。

void UpdateFunctionOnGPU () {…Graphics.DrawMeshInstancedProcedural(mesh, 0, material);}

由于這種方式不使用游戲對象,Unity不知道在場景中哪里繪制。我們需要添加一個參數指定邊界。這是一個軸對其框,只是我們正在繪制內容的邊界。Unity使用這個來決定是否跳過這個繪制,因為它可能在攝像機視域之外。這被稱為視錐剔除。現在是評估一次整個圖形邊界而不是單個點。這對我們的圖形沒有影響,因為我們想要完整的看到它。

我們的圖形坐落于遠點,它的點仍應該在2以內。我們可以用Vector3.zero?和??Vector3.one乘以2來調用Bounds?構造函數以創建一個邊界值。

var bounds = new Bounds(Vector3.zero, Vector3.one * 2f);Graphics.DrawMeshInstancedProcedural(mesh, 0, material, bounds);

但是點也有尺寸,它的一半會在邊界之外。所以我們同樣需要增加邊界。

var bounds = new Bounds(Vector3.zero, Vector3.one * (2f + 2f / resolution));

最后一個要傳遞給DrawMeshInstancedProcedural?的參數是要繪制多少實例。這需要和位置緩沖區的元素個數匹配,我們可以通過它的count?屬性獲得。

Graphics.DrawMeshInstancedProcedural(mesh, 0, material, bounds, positionsBuffer.count);

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Overlapping unit cubes.

?進入play模式后,我們可以看到一個彩色的單位方塊坐落于原點。每個點渲染一次相同的方塊,但都具有單位變換矩陣,所以他們會重疊。現在性能比之前好很多,因為幾乎沒有數據要被拷貝到GPU,并且所有的點只用一個draw call 繪制。Unity也不需做任何裁剪。也不必根據它們的視空間深度來為他們排序,通常離攝像機最近的點最先被繪制。深度排序對不透明物體的渲染非常有效,因為這可以避免overdraw,但是我們的程序化繪制命令僅僅一個接一個的繪制那些點。然而,消除的CPU工作和數據傳輸,加上GPU全速渲染所有立方體的能力足以彌補這一點。

2.2 獲取位置(Retrieving the Positions)

為了獲取我們存在GPU的點的位置,我們需要創建一個新著色器,先重DRP開始。復制Point Surface?著色器并重命名為Point Surface GPU。同樣修改其菜單標簽。由于我們現在基于一個由計算著色器填充的結構緩沖區,提升著色器的目標級別為4.5。

Shader "Graph/Point Surface GPU" {Properties {_Smoothness ("Smoothness", Range(0,1)) = 0.5}SubShader {CGPROGRAM#pragma surface ConfigureSurface Standard fullforwardshadows#pragma target 4.5…ENDCG}FallBack "Diffuse" }

程序化渲染的工作類似于GPU instancing,但是我們需要通過#pragma instancing_options?指令指定一個額外的選項。

#pragma surface ConfigureSurface Standard fullforwardshadows#pragma instancing_options procedural:ConfigureProcedural

這表明表面著色器需要為每個點調用ConfigureProcedural?函數。它是一個沒有任何參數,返回空的函數。

void ConfigureProcedural () {}void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {surface.Albedo = saturate(input.worldPos * 0.5 + 0.5);surface.Smoothness = _Smoothness;}

默認情況下,只有常規繪制通道(regular draw pass)會調用這個函數。為了在渲染陰影時也調用,我們需要指定一個陰影通道,通過在#pragma surface?指令后添加addshadow?

#pragma surface ConfigureSurface Standard fullforwardshadows addshadow

現在添加一個和計算著色器一樣的位置緩沖區。因為這次我們只需要讀取它所以用StructuredBuffer替換RWStructuredBuffer

StructuredBuffer<float3> _Positions;void ConfigureProcedural () {}

但是我們應該只對為程序化繪制專門編譯的著色器變體執行此操作。這是定義了UNITY_PROCEDURAL_INSTANCING_ENABLED?宏時的情況。我們可以通過#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)檢查是否定義。這是一個預處理指令,它使編譯器只在標簽被定義時才包含后面的代碼,直到遇到#endif?指令。

#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)StructuredBuffer<float3> _Positions;#endif

我們還要為ConfigureProcedural?做同樣的事。

void ConfigureProcedural () {#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)#endif}

現在我們可以通過位置緩沖區的索引,也就是當前正在被繪制的實例的標識符,來得到點的位置。我們通過unity_InstanceID 得到標識符,他可以被全局訪問。

void ConfigureProcedural () {#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)float3 position = _Positions[unity_InstanceID];#endif}

2.3 創建變換矩陣(Creating a Transformation Matrix)

我們有了位置之后,下一步就是創建一個object-to-world 變換矩陣。為簡單起見,我們將圖形放在世界坐標原點,沒有旋轉和縮放。調整GPU Graph?游戲對象的Transform?不會有任何效果,所以我們不會對他做任何操作。

我們只操作點的位置和縮放。位置被存儲在4 x 4 變換矩陣的最后一列,縮放存儲在矩陣的對角線。矩陣的最后一位是1,其他都是0。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Transformation matrix with position and scale.

?變換矩陣是為了將頂點從對象空間轉換到世界空間。它由unity_ObjectToWorld 提供。因為我們是程序化繪制,它是一個單位矩陣,所以我們需要重置他。先把它設為0

float3 position = _Positions[unity_InstanceID];unity_ObjectToWorld = 0.0;

我們可以通過float4(position,?1.0). 構建一個列向量。我們可以通過unity_ObjectToWorld._m03_m13_m23_m33.將其設為矩陣的第四列。

unity_ObjectToWorld = 0.0;unity_ObjectToWorld._m03_m13_m23_m33 = float4(position, 1.0);

然后添加一個float?_Step?屬性并將其賦值給unity_ObjectToWorld._m00_m11_m22. 這是用來縮放的。

float _Step;void ConfigureProcedural () {#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)float3 position = _Positions[unity_InstanceID];unity_ObjectToWorld = 0.0;unity_ObjectToWorld._m03_m13_m23_m33 = float4(position, 1.0);unity_ObjectToWorld._m00_m11_m22 = _Step;#endif}

還有一個unity_WorldToObject?矩陣,用來變換法線向量。它是用來矯正方向向量的轉換,但是我們不需要所以我們可以忽略它。我們通過添加assumeuniformscaling 來通知我們的著色器。

#pragma instancing_options assumeuniformscaling procedural:ConfigureProcedural

現在為我們的著色器創建一個材質,勾選GPU instancing,并賦值給我們的GPU graph

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Using GPU material.

?我們還需要設置一下材質的屬性,就像我們之前設置計算著色器一樣。在UpdateFunctionOnGPU 中,繪制之前調用材質的SetBuffer?和?SetFloat?函數。此時我們不需要提供核心的索引。

material.SetBuffer(positionsId, positionsBuffer);material.SetFloat(stepId, step);var bounds = new Bounds(Vector3.zero, new Vector3(2f + 2f / resolution));Graphics.DrawMeshInstancedProcedural(mesh, 0, material, bounds, positionsBuffer.count);

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?40,000 shadowed cubes, drawn with DRP.

?我們進入play模式時,我們再次看見了我們的圖像,但是現在有40 000個點被渲染并保持60FPS。如果我關閉VSync 它可以達到245FPS。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Profiling a DRP build with VSync.

?2.4 百萬(Going for a Million)

既然40 000個點表現的這么好,我們看一下是否可以處理百萬個點。但在這之前我們需要知道異步著色器編譯。這是Unity 編輯器的特色,而非構建(builds)的。編輯器只在需要時編譯著色器,這可以節省很多編譯時間,但也意味著著色器并不總是立即有效。當這種情發生時,會臨時使用一個統一的藍綠色著色器直到著色器被編譯完成。這通常還好,但是這個臨時著色器不支持程序化繪制。這會顯著的減緩繪制過程,如果要渲染百萬個點很可能會使Unity崩潰,甚至整個機器也可能崩潰。

我們可以通過設置關閉異步著色器編譯,但只有我們的Point Surface GPU?才有這個問題。幸運的人是我們可以通過添加#pragma editor_sync_compilation?指令通知Unity為某個著色器使用同步編譯。

#pragma surface ConfigureSurface Standard fullforwardshadows addshadow#pragma instancing_options assumeuniformscaling procedural:ConfigureProcedural#pragma editor_sync_compilation#pragma target 4.5

現在我們可以將分辨率限制增加到1000了

[SerializeField, Range(10, 1000)]int resolution = 10;

?

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Resolution set to 1,000.

?在小窗口中它看起來并不漂亮,因為點太小會出現摩爾紋,但它確實運行了。我這里渲染百萬個點是24FPS。在編輯器和構建中性能是一樣的。此時編輯器開支是無關緊要的,GPU才是瓶頸。并且,是否打開VSync 也并沒有明顯的不同。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Profiling a build rendering a million points, no VSync.

?當VSync 被關閉時,可以看出多數player loop時間花費在等待GPU完工。

注意我們現在是渲染一百萬個有陰影的點,對于DPR需要每幀渲染他們三次。關閉陰影和VSync 后幀率會上升到65FPS。

2.5 URP

為了看下URP的表現,我們需要復制我們的?Point URP?shader graph,重命名為Point URP GPU?。Shader graph 并不直接支持程序化繪制,但我們可以一些自定義代碼使其支持。為了簡單化和重用性我們創建一個HLSL文件資源。Unity沒有這個選項,所以復制一個表面著色器然后重命名為PointGPU。然后修改擴展名為hlsl.

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?PointGPU HLSL script asset.

?清空文件內容,讓后復制一下內容

#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)StructuredBuffer<float3> _Positions; #endiffloat _Step;void ConfigureProcedural () {#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)float3 position = _Positions[unity_InstanceID];unity_ObjectToWorld = 0.0;unity_ObjectToWorld._m03_m13_m23_m33 = float4(position, 1.0);unity_ObjectToWorld._m00_m11_m22 = _Step;#endif }

?我們現在可以通過?#include "PointGPU.hlsl"?指令在Point Surface GPU? 著色器中包含這個文件,原來的代碼可以被移除。

#include "PointGPU.hlsl"struct Input {float3 worldPos;};float _Smoothness;//#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)// StructuredBuffer<float3> _Positions;//#endif//float2 _Scale;//void ConfigureProcedural () { … }void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) { … }

?我們將在shader graph 中使用一個Custom Function?節點來包含HLSL文件。那個節點會調用一個文件里的函數。我們給PointGPU?添加一個簡單的函數,僅僅傳入一個float3?值并將其返回。

給PointGPU?添加一個有兩個float3?參數的void?ShaderGraphFunction_float?函數,僅僅將輸入賦值給輸出。

void ShaderGraphFunction_float (float3 In, float3 Out) {Out = In; }

這里假定了Out?參數是一個輸出參數,我們需要在其前面添加?out?

void ShaderGraphFunction_float (float3 In, out float3 Out) {Out = In; }

函數名后面的_float?后綴是必要的的,因為它指定了函數的精度。Shader graph 提供了兩種精度,float?和?half. 后者是前者的一半。節點的精度可以被明確的選擇或設為繼承,繼承也是默認值。為了支持兩種精度,使用half 添加一個變體函數。

void ShaderGraphFunction_float (float3 In, out float3 Out) {Out = In; }void ShaderGraphFunction_half (half3 In, out half3 Out) {Out = In; }

現在給Point URP GPU?添加一個Custom Function?節點。它的Type?默認是File?。將PointGPU?賦值給它的Source?屬性。Name?設為ShaderGraphFunction?,不要后綴。然后在Inputs?中添加?In?,Outputs?中添加Out?,都是Vector3. 類型

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Custom function via file.

添加一個Position?節點,設為對象空間,并將其與我們的自定義節點的輸入鏈接起來。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Object-space vertex position passed through our function.

現在對象空間的頂點位置就傳入了我們的函數,并且我們的代碼也被包含進了著色器。但是為了程序化渲染,我們還需要包含#pragma instancing_options?和#pragma editor_sync_compilation?。它們必須被直接注入生成的著色器源代碼,不能通過文件引用。所以添加一個?Custom Function?節點,輸入和輸出與之前一樣,但是Type?設為String. 。Name?設一個適當的值,比如InjectPragmas,然后將指令寫入Body?文本框。body就像一個函數代碼塊一樣,所以這里我們還需要將輸入賦值給輸出。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Custom function via string injecting pragmas.

?為了看得更清晰,下面是body的代碼

#pragma instancing_options assumeuniformscaling procedural:ConfigureProcedural #pragma editor_sync_compilationOut = In;

將頂點位置傳遞給這個節點。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Shader graph with pragmas

?創建一個材質,啟用instancing,使用Point URP GPU?著色器,將其賦值給我們的graph,然后進入play模式。我這里達到了36FPS,在開啟陰影的情況下,比DRP快了50%

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Profiling URP build.

2.6 可變的分辨率(Variable Resolution)

因為我們總是為緩沖區的每個位置繪制點,降低分辨率會使一些點固定在那里。這是因為計算著色器值更新符合圖形的點。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Stuck points after lowering resolution.

?計算著色器不能調整大小。我們可以每次改變分辨率時創建一個新的,但簡單的方法是總是申請一個最大分辨率的的緩沖區。這樣天生的就可以改變分辨率。

我們將最大分辨率設為一個常量,然后在resolution 字段的?Range?屬性里使用它

const int maxResolution = 1000;…[SerializeField, Range(10, maxResolution)]int resolution = 10;

接下來,使用最大分辨率創建緩沖區。

void OnEnable () {positionsBuffer = new ComputeBuffer(maxResolution * maxResolution, 3 * 4);}

最后,使用當前分辨率的平方替換緩沖區元素數

void UpdateFunctionOnGPU () {…Graphics.DrawMeshInstancedProcedural(mesh, 0, material, bounds, resolution * resolution);}

3 GPU函數庫(GPU Function Library)

現在我們基于GPU的方式是功能化的,讓我們將我們整個函數庫轉到我們的計算著色器。

3.1 所有函數(ll Functions)

我們可以像拷貝修改Wave 一樣拷貝其他函數。第二個是MultiWave。它與Wave唯一較大的不同是它包含了float?值。在HLSL中沒有 f 后綴,所以將其全部移除。為了指明他們是浮點數,我給他們加了一個點,比如2f 變成了 2.0

float3 MultiWave (float u, float v, float t) {float3 p;p.x = u;p.y = sin(PI * (u + 0.5 * t));p.y += 0.5 * sin(2.0 * PI * (v + t));p.y += sin(PI * (u + v + 0.25 * t));p.y *= 1.0 / 2.5;p.z = v;return p; }

同樣修改其他函數,Sqrt?改為sqrt?,Cos?改為cos.

float3 Ripple (float u, float v, float t) {float d = sqrt(u * u + v * v);float3 p;p.x = u;p.y = sin(PI * (4.0 * d - t));p.y /= 1.0 + 10.0 * d;p.z = v;return p; }float3 Sphere (float u, float v, float t) {float r = 0.9 + 0.1 * sin(PI * (6.0 * u + 4.0 * v + t));float s = r * cos(0.5 * PI * v);float3 p;p.x = s * sin(PI * u);p.y = r * sin(0.5 * PI * v);p.z = s * cos(PI * u);return p; }float3 Torus (float u, float v, float t) {float r1 = 0.7 + 0.1 * sin(PI * (6.0 * u + 0.5 * t));float r2 = 0.15 + 0.05 * sin(PI * (8.0 * u + 4.0 * v + 2.0 * t));float s = r2 * cos(PI * v) + r1;float3 p;p.x = s * sin(PI * u);p.y = r2 * sin(PI * v);p.z = s * cos(PI * u);return p; }

3.2 宏(Macros)

我們現在要為每個圖形函數創建一個獨立的核心函數,但那會有非常多的重復代碼。我們可以通過創建一個宏避免這些。在FunctionKernel?函數上面寫上#define KERNEL_FUNCTION?

#define KERNEL_FUNCTION[numthreads(8, 8, 1)]void FunctionKernel (uint3 id: SV_DispatchThreadID) { … }

這些定義通常只適用于寫在它后面的,同一行的東西,但是我們可以通過給除了最后一行之外的每一行后面添加 \ 反斜杠來擴展到多行。

#define KERNEL_FUNCTION \[numthreads(8, 8, 1)] \void FunctionKernel (uint3 id: SV_DispatchThreadID) { \float2 uv = GetUV(id); \SetPosition(id, Wave(uv.x, uv.y, _Time)); \}

現在,當我們寫KERNEL_FUNCTION?時,編譯器會用FunctionKernel函數代碼將其替換。為了讓其有函數功能,我們給宏添加一個參數。這就像函數的參數一樣,但是沒有類型并且圓括號緊跟著宏名字。給它一個?function?參數并用其代替Wave.

#define KERNEL_FUNCTION(function) \[numthreads(8, 8, 1)] \void FunctionKernel (uint3 id: SV_DispatchThreadID) { \float2 uv = GetUV(id); \SetPosition(id, function(uv.x, uv.y, _Time)); \}

我們還需要修改核心函數的名字。我們使用function?作為前綴,后面跟著Kernel 。我們需要將function?標簽分離出,否則它不能被識別為著色器參數。為此我們使用?##?宏鏈接操作符將兩個字組合起來。

void function##Kernel (uint3 id: SV_DispatchThreadID) { \

現在所有五個函數都可以用KERNEL_FUNCTION?來定義了。

#define KERNEL_FUNCTION(function) \…KERNEL_FUNCTION(Wave) KERNEL_FUNCTION(MultiWave) KERNEL_FUNCTION(Ripple) KERNEL_FUNCTION(Sphere) KERNEL_FUNCTION(Torus)

我們還需要為每個函數替換我們的kernel指令

#pragma kernel WaveKernel #pragma kernel MultiWaveKernel #pragma kernel RippleKernel #pragma kernel SphereKernel #pragma kernel TorusKernel

最后一步是在?GPUGraph.UpdateFunctionOnGPU?中使用當前函數作為kernel索引來替換之前的0

var kernelIndex = (int)function;computeShader.SetBuffer(kernelIndex, positionsId, positionsBuffer);int groups = Mathf.CeilToInt(resolution / 8f);computeShader.Dispatch(kernelIndex, groups, groups, 1);

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?All functions at resolution 1,000, with plane to show shadows.

計算著色器非常快所以每個函數的幀率都差不多。

3.3 變形(Morphing Functions)

支持從一個函數變形到另一個函數有點復雜,因為每個變形都需要有一個單獨的核心函數。先添加一個轉換進程屬性,后面的融合函數會用到。

float _Step, _Time, _TransitionProgress;

復制那個核心宏,重命名為KERNEL_MOPH_FUNCTION, 給它添加兩個參數:functionA?和functionB.將函數名改為?functionA##To##functionB##Kernel?,然后使用lerp??來對計算的點線性插值。

#define KERNEL_MOPH_FUNCTION(functionA, functionB) \[numthreads(8, 8, 1)] \void functionA##To##functionB##Kernel (uint3 id: SV_DispatchThreadID) { \float2 uv = GetUV(id); \float3 position = lerp( \functionA(uv.x, uv.y, _Time), functionB(uv.x, uv.y, _Time), \_TransitionProgress \); \SetPosition(id, position); \}

每個函數都可以轉換為其他,所以每個函數有四個轉換。為他們添加核心函數

KERNEL_FUNCTION(Wave) KERNEL_FUNCTION(MultiWave) KERNEL_FUNCTION(Ripple) KERNEL_FUNCTION(Sphere) KERNEL_FUNCTION(Torus)KERNEL_MOPH_FUNCTION(Wave, MultiWave); KERNEL_MOPH_FUNCTION(Wave, Ripple); KERNEL_MOPH_FUNCTION(Wave, Sphere); KERNEL_MOPH_FUNCTION(Wave, Torus);KERNEL_MOPH_FUNCTION(MultiWave, Wave); KERNEL_MOPH_FUNCTION(MultiWave, Ripple); KERNEL_MOPH_FUNCTION(MultiWave, Sphere); KERNEL_MOPH_FUNCTION(MultiWave, Torus);KERNEL_MOPH_FUNCTION(Ripple, Wave); KERNEL_MOPH_FUNCTION(Ripple, MultiWave); KERNEL_MOPH_FUNCTION(Ripple, Sphere); KERNEL_MOPH_FUNCTION(Ripple, Torus);KERNEL_MOPH_FUNCTION(Sphere, Wave); KERNEL_MOPH_FUNCTION(Sphere, MultiWave); KERNEL_MOPH_FUNCTION(Sphere, Ripple); KERNEL_MOPH_FUNCTION(Sphere, Torus);KERNEL_MOPH_FUNCTION(Torus, Wave); KERNEL_MOPH_FUNCTION(Torus, MultiWave); KERNEL_MOPH_FUNCTION(Torus, Ripple); KERNEL_MOPH_FUNCTION(Torus, Sphere);

我們添加核心以使他們的索引等于functionB + functionA *?5,?

#pragma kernel WaveKernel #pragma kernel WaveToMultiWaveKernel #pragma kernel WaveToRippleKernel #pragma kernel WaveToSphereKernel #pragma kernel WaveToTorusKernel#pragma kernel MultiWaveToWaveKernel #pragma kernel MultiWaveKernel #pragma kernel MultiWaveToRippleKernel #pragma kernel MultiWaveToSphereKernel #pragma kernel MultiWaveToTorusKernel#pragma kernel RippleToWaveKernel #pragma kernel RippleToMultiWaveKernel #pragma kernel RippleKernel #pragma kernel RippleToSphereKernel #pragma kernel RippleToTorusKernel#pragma kernel SphereToWaveKernel #pragma kernel SphereToMultiWaveKernel #pragma kernel SphereToRippleKernel #pragma kernel SphereKernel #pragma kernel SphereToTorusKernel#pragma kernel TorusToWaveKernel #pragma kernel TorusToMultiWaveKernel #pragma kernel TorusToRippleKernel #pragma kernel TorusToSphereKernel #pragma kernel TorusKernel

?回到GPUGraph, 添加轉換進程屬性的標識

static readonly int…timeId = Shader.PropertyToID("_Time"),transitionProgressId = Shader.PropertyToID("_TransitionProgress");

如果正在轉換,在UpdateFunctionOnGPU 中使用它。

computeShader.SetFloat(timeId, Time.time);if (transitioning) {computeShader.SetFloat(transitionProgressId,Mathf.SmoothStep(0f, 1f, duration / transitionDuration));}

為了選擇正確的索引,如果在轉換增加?transition function 乘以5,否則增加自身乘以5

var kernelIndex =(int)function + (int)(transitioning ? transitionFunction : function) * 5;

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??Continuous random morphing.

?添加的轉換對幀率沒有影響。很明顯渲染才是瓶頸,計算不是。

3.4 函數數量屬性(Function Count Property)

為了計算核心索引,GPUGraph?需要知道有多少個函數。我們可以給FunctionLibrary?添加GetFunctionCount?函數來得到個數。這樣做的好處是,如果我們添加或移除函數,只需要修改哪兩個FunctionLibrary?文件。

public static int GetFunctionCount () {return 5;}

我們甚至可以移除常量,返回functions?數組的長度,更加減少了我們需要修改的代碼。

public static int GetFunctionCount () {return functions.Length;}

將函數數量改為屬性也是一個好方法。

public static int FunctionCount {get {return functions.Length;}}

這就定義了一個getter屬性。由于它唯一要做的就是返回一個值,我們可以將其簡化為?get?=> functions.Length;.

public static int FunctionCount {get => functions.Length;}

因為沒有set?塊,我們可以將其更簡化為省略get. 這樣它就減為只有一行。

public static int FunctionCount => functions.Length;

?GetFunction?和?GetNextFunctionName.也使用這種方法

public static Function GetFunction (FunctionName name) => functions[(int)name];public static FunctionName GetNextFunctionName (FunctionName name) =>(int)name < functions.Length - 1 ? name + 1 : 0;

GPUGraph.UpdateFunctionOnGPU.中使用新屬性替換常量

var kernelIndex =(int)function +(int)(transitioning ? transitionFunction : function) *FunctionLibrary.FunctionCount;

3.5 更多細節 (More Details)

由于分辨率的增加,我們的圖像可以更加詳細。比如,我們可以雙倍Sphere.扭曲的頻率。

float3 Sphere (float u, float v, float t) {float r = 0.9 + 0.1 * sin(PI * (12.0 * u + 8.0 * v + t));… }

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??More detailed sphere.

? 同樣還有Torus. 的星星樣式。

float3 Torus (float u, float v, float t) {float r1 = 0.7 + 0.1 * sin(PI * (8.0 * u + 0.5 * t));float r2 = 0.15 + 0.05 * sin(PI * (16.0 * u + 8.0 * v + 3.0 * t));… }

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??More detailed torus.

總結

以上是生活随笔為你收集整理的计算着色器(Compute Shaders)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。