본문으로 바로가기

Unity Compute Shader

category Technical Report/Unity Shader 2018. 11. 17. 14:52
반응형



원문링크 : https://en.wikibooks.org/wiki/Cg_Programming/Unity/Computing_Image_Effects


Compute shader 기본 구조


#pragma kernel TintMain


float4 Color;

Texture2D<float4> Source;

RWTexture2D<float4> Destination;


[numthreads(8,8,1)]


void TintMain (uint3 groupID : SV_GroupID,

   uint3 groupThreadID : SV_GroupThreadID,

  // ID of thread in a thread group; range depends on numthreads

   uint groupIndex : SV_GroupIndex,

      // flattened/linearized GroupThreadID between 0 and
      // numthreads.x * numthreads.y * numthreadz.z - 1

   uint3 id : SV_DispatchThreadID)
      // = GroupID * numthreads + GroupThreadID


{

   Destination[id.xy] = Source[id.xy] * Color;

}



각 코드의 의미는 아래와 같다.


#pragma kernel TintMain

//Compute shader를 정의


float4 Color;

Texture2D<float4> Source;

// compute shader가 보간 없이 읽을 수 있도록 4 개의 부동 소수점 구성 요소가 있는 2D 텍스처를 정의. 프래그먼트 셰이더에서는 sampler2D Source를 사용. 2D 텍스처를 샘플링 (보간 포함). (HLSL은 별도의 텍스처 객체와 샘플러 객체를 사용. 이 함수는 SampleLevel 함수를 사용하여 compute shader에서 2D 텍스쳐를 샘플링하려는 경우에 필요)


RWTexture2D<float4> Destination;

// compute shader가 읽고 쓸 수 있는 읽기 / 쓰기 2D 텍스처를 지정. Unity의 render texture에 해당. compute shader는 RWTexture2D의 모든 위치에 쓸 수 있지만 fragmentn shader는 보통 정해진의 위치에만 쓸 수 있다. compute shader의 여러 스레드(즉, compute shader 함수 호출)는 정의되지 않은 순서로 같은 위치에 쓸 수 있으므로 이러한 문제를 피하기 위해 특별히 주의하지 않는 한 정의되지 않은 결과가 발생하므로 각 스레드가 RWTexture2D의 고유 한 위치에만 기록하도록함으로써 이러한 문제를 방지.


[numthreads(8,8,1)]

// 쓰레드 그룹을 정의. GPU에서 실행되는 커널을 실행하는 단위로써 3차원으로 구성된다.(x, y, z). 여러 쓰레드에서 동일 커널을 실행할 수 있다. 일반적으로 2D 이미지의 경우는 8x8의 64 쓰레드를 가지며 위와 같이 z 값이 1을 가지게 된다. 쓰레드의 수는 각 플랫폼별로 제한이 있다. DX11의 경우 x, y의 곱은 1024 미만, z는 64까지 값을 가지며 각 3차원 배열의 곱은 1024 미만이 되어야 한다. 쓰레드 그룹의 곱은 최소 32는 되어야 최대 효율을 가진다(플랫폼 별로 역시 다름)

// compute shader는 ComputeShader.Dispatch (int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ) 함수를 사용하여 스크립트에서 호출된다. 여기서 kernelIndex는 compute shader 함수를 지정하고 다른 인수는 3D array의 크기를 지정한다. 스레드 그룹 수 [numthreads (8,8,1)]의 예에서는 각 그룹에 64 개의 스레드가 있으므로 총 스레드 수는 64 * threadGroupsX * threadGroupsY * threadGroupsZ가 된다.


void TintMain (uint3 groupID : SV_GroupID,

// Compute shader 함수 void TintMain ()을 지정. 일반적으로 compute shader 함수는 호출 된 3D 배열의 위치를 ​​알아야 한다. 스레드 그룹의 3D 배열에서 스레드 그룹의 위치와 스레드 그룹 내의 스레드 위치를 아는 것도 중요한데, HLSL은 이 정보에 대해 다음과 같이 정의되어 있다.


   uint3 groupThreadID : SV_GroupThreadID,
// SV_GroupID : 스레드 그룹의 3D ID를 지정하는 uint3 벡터. ID의 각 좌표는 0에서 시작하여 ComputeShader.Dispatch () 호출에 지정된 차원까지 올라간다.


   uint groupIndex : SV_GroupIndex,
//SV_GroupIndex : 0과 numthreads.x * numthreads.y * numthreadz.z - 1 사이의 병합 / 선형화 된 SV_GroupThreadID를 지정하는 단위


   uint3 id : SV_DispatchThreadID)
   

// SV_DispatchThreadID : 모든 스레드 그룹의 전체 배열에서 스레드의 3D ID를 지정하는 uint3 벡터. SV_GroupID * numthreads + SV_GroupThreadID와 같다.


compute shader 함수는 이러한 값을 받을 수 있다. void TintMain (uint3 groupID : SV_GroupID, uint3 groupThreadID : SV_GroupThreadID, uint groupIndex : SV_GroupIndex, uint3 id : SV_DispatchThreadID)

특정된 함수인 TintMain은 실제로 의미 있는 SV_DispatchThreadID를 가진 변수 id만을 사용한다. 함수 호출은 대상 및 소스 텍스처의 차원(적어도)의 2D 배열로 구성된다. 따라서 id.x와 id.y는 이러한 텍셀 [Destination [id.xy]와 Source [id.xy]에 접근하는 데 사용될 수 있다. 기본 작업은 Source 텍스처의 색상에 Color를 곱하여 Destination 렌더링 텍스처에 쓴다.


{

   Destination[id.xy] = Source[id.xy] * Color;

}



Compute Shader를 카메라 뷰에 적용

컴퓨터 뷰의 모든 픽셀에 계산 쉐이더를 적용하려면 OnRenderImage (RenderTexture 소스, RenderTexture 대상) 함수를 정의하고 계산 쉐이더에서 이러한 렌더링 텍스처를 사용해야 한다.


그러나 두 가지 문제가 있다.


Unity가 프레임 버퍼에 직접 렌더링하는 경우 대상이 null로 설정되고 compute shader에 사용할 렌더링 텍스처가 없어야 한다. 또한 생성하기 전에 랜덤 쓰기 액세스를 위해 렌더링 텍스처를 활성화해야 한다.

OnRenderImage ()에서 렌더링 텍스처를 사용할 수는 없다. 소스 렌더 텍스처와 동일한 크기의 임시 렌더 텍스처를 생성하고 compute shader가 해당 임시 렌더링 텍스처에 쓸 수 있게 함으로써 이러한 경우(소스와 대상 렌더링 텍스처의 크기가 다른 경우)를 처리 할 수 ​​있다. 그런 다음 결과를 대상 렌더 텍스처에 복사 할 수 있다.(함수의 정의 >> 구현 >> 연산 결과를 저장해 CPU 와 주고 받을 버퍼 생성)


using System;
using UnityEngine;

[RequireComponent(typeof(Camera))]
[ExecuteInEditMode]

public class tintComputeScript : MonoBehaviour {

   public ComputeShader shader;
   public Color color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
  
   private RenderTexture tempDestination = null; 
      // we need this intermediate render texture for two reasons:
      // 1. destination of OnRenderImage might be null
      // 2. we cannot set enableRandomWrite on destination


   private int handleTintMain;

   void Start()
   {
      if (null == shader)
      {
         Debug.Log("Shader missing.");
         enabled = false;
         return;
      }
     
      handleTintMain = shader.FindKernel("TintMain");
     
      if (handleTintMain < 0)
      {
         Debug.Log("Initialization failed.");
         enabled = false;
         return;
      } 
   }


// handleTintMain = shader.FindKernel("TintMain");

// 스크립트의 Start () 함수는 오류 검사 만 수행하고 shader.FindKernel ( "TintMain")을 사용해 compute shader 함수의 번호를 가져 와서 Update () 함수에서 사용할 handleTintMain에 쓰게된다..


void OnDestroy()
   {
      if (null != tempDestination) {
         tempDestination.Release();
         tempDestination = null;
      }
   }


// GC가 렌더링 텍스처에 필요한 하드웨어 리소스를 자동으로 해제하지 않기 때문에 OnDestroy () 함수는 임시 렌더링 텍스처를 해제.


   void OnRenderImage(RenderTexture source, RenderTexture destination)
   {     
      if (null == shader || handleTintMain < 0 || null == source)
      {
         Graphics.Blit(source, destination); // just copy
         return;
      }
     
      // do we need to create a new temporary destination render texture?


      if (null == tempDestination || source.width != tempDestination.width
         || source.height != tempDestination.height)
      {
         if (null != tempDestination)
         {
            tempDestination.Release();
         }
         tempDestination = new RenderTexture(source.width, source.height,
            source.depth);
         tempDestination.enableRandomWrite = true;
         tempDestination.Create();
      }



      // call the compute shader
      shader.SetTexture(handleTintMain, "Source", source);
      shader.SetTexture(handleTintMain, "Destination", tempDestination);
      shader.SetVector("Color", (Vector4)color);
      shader.Dispatch(handleTintMain, (tempDestination.width + 7) / 8, (tempDestination.height + 7) / 8, 1);
     

      // copy the result

      Graphics.Blit(tempDestination, destination);
   }
}


// Update () 함수는 몇 가지 오류 검사를 수행 한 다음 - 필요하다면- tempDestination에 새로운 렌더 텍스처를 만든 다음 SetTexture (), SetVector () 및 SetInt () 함수를 사용하여 compute shader의 모든 균일 변수를 설정한 후 Dispatch ()를 호출하여 compute shader 함수를 호출하게 된다.

이 경우에는 (tempDestination.width + 7) / 8 번 (tempDestination.height + 7) / 8 스레드 그룹 (암시 적으로 절사한 숫자)을 사용하게 된다. 스레드 그룹의 수를 지정하고 연산 그룹의 [numthreads (8,8,1)]에 지정된 크기의 8x8을 가지고 있기 때문에 양쪽으로 모두 8로 나누게 된다. 렌더링 텍스처의 크기가 8로 나눌 수 없는 경우 짧지 않은지 확인하기 위해 7을 더하게 된다. compute shader를 Dispatch 한 후 결과는 tempDestination에서 OnRenderImage ()의 실제 대상으로 Graphics.Blit ()에 대한 호출의 도움으로 복사된다.


반응형

'Technical Report > Unity Shader' 카테고리의 다른 글

Fragment Lambert Lit  (0) 2019.02.09
Material Property Drawer Example  (0) 2019.02.07
Fake FixedLight Environment  (0) 2018.08.14
360 Equirectangular Projection in Unity  (0) 2018.08.07
Unity surface shader Billboard  (0) 2018.05.28