사전적 의미론 일괄 이지만, 유니티에서는 단순 사전적 의미로 간주하지 않는다.

 

최적화 포스팅 에서도 다루었지만, CPU와 GPU간 통신을 줄일수록 시간과 자원이 절약된다.

 

통신이라 함은 CPU가 GPU에게 명령을 내리는 DrawCall 로 간주할 수 있는데 (엄밀히는 DrawCalls + SetPass Calls) 

 

예를 들어 큐브 10개를 그리는데 GPU한테 10번을 명령하는 것과, 10개를 하나로 묶어 1번을 명령하는 방식이 있다면,

일반적인 경우에선 후자가 더 좋다는 것이다.

 

그렇다고 너무 많은 큐브 (이 기준은 GPU마다 다르다) 를 한꺼번에 묶으면 GPU의 대역폭 (BandWidth) 에 따라 메모리가 터질 수도 있다.

 

어쨋든, 이렇게 여러 드로우콜을 하나로 묶는 일련의 행위를 유니티에서는 (배칭)batching 이라고 표현하며,

방법은 또 여러가지가 있다.

 

그 중 유니티에서 지원하는 배칭은 현재 4가지인데 그 종류와 특징에 대해 짚고 넘어가보자

 

Dynamic Batching

작은 메시들 몇개를 큰 메시 하나로 합쳐서 드로우콜을 줄이는 방법으로, 별도의 작업없이 자동으로 수행된다.

단 수행될 조건이 몇가지가 있는데, 버텍스 갯수가 180 ~ 300 개 미만인 메시여야 하고, 같은 메테리얼일 경우에만 배칭해준다.

유념해야 할 점은 큰 메시를 하나로 합치는 과정을 CPU 에서 하는데, 이 작업이 GPU한테 드로우콜을 수행하는 것보다 비용이 더 적을 경우에만 유리하다는 것이다.

풀어서 쓰자면,

 

[옛날]

CPU : 야, GPU! 너한테 맡길일을 CPU인 내가 대신 해줄께 너 이거 하는데 오래 걸리잖아.

GPU : 응 고마워!

 

[지금 (Metal, Vulkan)]

CPU : 야, GPU! 너한테 맡길일을 CPU인 내가 대신 해줄께 너 이거 하는데 오래 걸리잖아.

GPU : 너가 해주는게 시간 더 걸릴껄?

 

최신 API 일 경우 Dynamic Batching이 전혀 유리하지 않을 수 있다.

 

Static Batching

다이나믹 배칭과 달리 크기에 관계없이 하나로 합쳐주고 그 과정에서 CPU 자원이 소모되는 것도 아니어서 장점만 있는 것 같지만, 움직이지 않는 메시에만 적용되며, (인스펙터에서 static 체크) 또한 과도하게 메모리를 사용할 수 있는 것이 단점이다.

 

예를 들어 동일한 나무 메시를 다수 배치한 숲 같은 배경일 경우 스태틱 배칭을 켤 경우 자칫 합쳐진 메시가 너무 비대해져 메모리가 뻗어버릴 수 있다.

 

GPU Instancing

동일 메시, 동일 메테리얼 오브젝트들을 하나의 배열에 모아서 GPU에 제공하는 방법으로, Graphics.DrawXXX API를 통해 수동으로 전달할 수 도 있고, 지원가능한 쉐이더의 Inspector를 통해서도 설정이 가능하다.

또 하나의 장점은 MaterialPropertyBlock을 통해 인스턴스별 (오브젝트별) 외형 변화를 줄 수 있다는 것

단점은 메시랑 메테리얼이 모두 같을때만 동작한다는 것과 SkinnedMeshRenderer 미지원 이 있다.

 

SRP Batcher

비교적 최근에 지원하기 시작한 배칭 방법으로 동일 쉐이더를 사용하되 다른 메테리얼을 쓰는 오브젝트들도 모두 배칭해주기 때문에 범용적으로 쓸 수 있지만, 셰이더 배리언트 에 대해 일정한 규칙을 고수할 경우에만 작동하며, 그 외에도 배칭 조건이 다소 까다롭다.

예를 들어 GPU Instancing의 인스턴스별 외형 변화는 지원하지 않는 것 등..

 

작동방식은 이전 URP 포스팅에서 다루었으므로 참고하시기 바랍니다.

 

배칭 우선순위는 SRP Batcher > GPU Instancing > Dynamic Batching 이다.

 

'Unity > 최적화' 카테고리의 다른 글

병렬처리 기법 2  (0) 2021.02.05
병렬처리 기법 1  (0) 2021.02.04

이전 포스팅에서는 CPU의 작업을 GPU로 전환하는 과정을 설명했었다.

 

여기서 이제 좀 더 효율적으로 GPU 에게 작업을 맡겨보자

 

현재 CubeShader.shader 에서 회전 행렬과 이동 행렬을 연산하고 있는데,

GPU를 이용해 이를 병렬로 처리해볼 것이다.

 

어떻게 GPU에게 병렬처리를 맡기죠?

 

바로 컴퓨트 셰이더 를 이용하는 것이다.

 

https://docs.unity3d.com/kr/2018.4/Manual/class-ComputeShader.html

 

컴퓨트 셰이더 - Unity 매뉴얼

컴퓨트 셰이더는 일반 렌더링 파이프라인과 별도로 그래픽 카드에서 실행되는 프로그램입니다. 컴퓨트 셰이더는 대량 병렬 GPGPU 알고리즘 또는 게임 렌더링의 일부를 가속시키기 위해 사용할

docs.unity3d.com

메뉴얼에 설명되어 있지만 좀처럼 개념이 와닿지 않는다.

 

요약하자면, GPU는 대량의 병렬 알고리즘 처리에 특화되어 있는데, 우리는 컴퓨트 셰이더를 이용해 이를 이용할 수 있다는 것

 

쓰기 전에 GPU가 작업을 처리하는 방식과 구조에 대해 간단히 짚고 넘어가보자.

그래야 각각의 코드가 어떤 의미인지 이해할 수 있으니 말이다.

 

먼저 GPU에서는 하나의 작업을 쓰레드라고 명시한다.

위에 말한 병렬 이라는 것도 결국 여러 쓰레드를 병렬로 처리할 수 있다는 뜻이며, 우리는 이 스레드 하나가 어떤 일을 할지 명시해주면 되는 것이다.

 

단 이 스레드는 우리가 어떤 변수를 저장할때 다차원 배열을 선언해서 쓸 수 있듯, (arr[][] 같은 형태) 스레드도 3차원 배열까지 선언할 수 있다.

  • c++ 에서 3차원 배열 선언하는 형태 : float arr[][][]
  • 컴퓨트 셰이더에서 3차원 배열 선언하는 형태 : numthreads(int, int, int)

만약 numthreads(8,8,1) 이라면 8 * 8 * 1 = 64 개의 스레드를 병렬로 돌리겠다 라는 의미이다.

 

c++ 함수 처럼 GPU도 함수라는 개념을 가지고 있는데 이는 커널 이라는 용어를 사용한다.

요 하나의 커널이 여러개의 스레드를 한꺼번에 (병렬로) 돌릴 수 있으며 위에 명시한 numthreads 크기만큼의 스레드들을 가질 수 있다.

여기에 추가로 이 커널 하나를 실행하는 단위를 그룹이라고 명명하기도 하며, 이 그룹까지도 병렬로 처리가 가능하다는 것이다.

 

결국 여러 스레드를 가진 커널 하나가 있고, 커널을 실행하는 그룹들을 동시에 돌릴 수 있는게 바로 다이렉트 컴퓨트 이다.

CPU가 할일을 GPU에게 맡기고, GPU는 이를 다이렉트 컴퓨트를 통해 연산하는 이러한 기술을 GPGPU 라고 명명한다

 

CUDA 의 Direct Compute 모델. Block 은 그룹으로 간주할 수 있다

개념도 알았으니, 실제 스크립팅을 통해 구현해보자 

 

먼저 Unity 에서 다음 메뉴를 실행해 Compute Shader 파일을 생성하자.

하고 나면 .compute 확장자를 가진 파일이 생성될 것이다.

Assets / Create / Shader / Compute Shader

아래 코드를 파일에 붙여넣기 한 후 한줄 한줄 살펴보도록 하자

#pragma kernel MyKernel

uint _Resolution;

float _Time;

RWStructuredBuffer<float4x4> _Positions;

float4x4 RotateYMatrix(float r)
{
    float sina, cosa;
    sincos(r, sina, cosa);
                
    float4x4 m;

    m[0] = float4(cosa, 0, -sina, 0);
    m[1] = float4(0, 1, 0, 0);
    m[2] = float4(sina, 0, cosa, 0);
    m[3] = float4(0, 0, 0, 1);

    return m;
}

float4x4 PositionMatrix(float3 pos)
{
    float4x4 m;

    m[0] = float4(1,0,0,pos.x);
    m[1] = float4(0,1,0,pos.y);
    m[2] = float4(0,0,1,pos.z);
    m[3] = float4(0,0,0,1);

    return m;
}

[numthreads(8,8,1)]
void MyKernel(uint3 id : SV_DispatchThreadID)
{
    float x = (-(_Resolution * 0.5f) + id.x) * 2;
    float z = (-(_Resolution * 0.5f) + id.y) * 2;

    float4x4 tfM = mul(PositionMatrix(float3(x,0,z)), RotateYMatrix(_Time));
    
    if (id.x <_Resolution && id.y <_Resolution) {
        _Positions [id.x * _Resolution + id.y] = tfM;
    }
}

 

 

더보기

#pragma kernel 커널이름

컴퓨트 쉐이더는 최소 하나의 커널(함수)를 선언하고 정의해야 한다

쓰레드 하나가 실행할 작업을 이 함수 안에 구현해야 하는 것이다.

여기서는 MyKernel 이라는 이름으로 커널을 선언

 

더보기

uint _Resolution, float _Time

쉐이더에 변수를 전달하는 것과 같은 방식으로, 스크립트에서 해당 변수에 값을 설정할 수 있다.

 

더보기

RWStructuredBuffer<float4x4> _Positions;

우리는 지금 컴퓨트 쉐이더를 통해 큐브의 위치와 회전값을 처리해서 넘겨줘야 한다.

이를 위해 나는 4x4 행렬을 통째로 CubeShader.shader 에 전달할 것이고, vertex 셰이더에서 해당 행렬을 통해 정점 위치를 변환할 예정이다.

 

StructuredBuffer는 스크립트에서 썼던 ComputeBuffer를 전달받기 위한 자료형인데, 앞에 RW가 붙어있다.

이는 ComputeBuffer를 읽기만 하는게 아니라 쓰기도 하겠다는 의미로서, 밑에서 설명하겠지만, 커널 함수 내부에서 위치를 계산한 후 이 RWStructuredBuffer 에 값을 셋팅할 것이다.

 

만약 앞에 RW를 붙이지 않는다면, 읽기만 하겠다는 의미

 

더보기

RotateYMatrix, PositionMatrix 함수

CubeShader.shader 에서 썼던 함수를 그대로 들고 왔다.

다시 한번 짚고 넘어가지만, 우리는 렌더 쉐이더 (CubeShader.shader) 가 하던 일을 Compute Shader를 통해 병렬로 처리하기 위해 이런 일련의 작업을 하고 있다.

그렇기 때문에 함수를 그대로 가져온 것

 

더보기

[numthreads(8, 8, 1)]

중요한 한줄이다.

위에서 설명한 대로 커널 하나가 처리할 스레드의 갯수를 3차원으로 설정하는 곳으로 꼭 커널함수의 윗줄에 선언되어야 한다.

각각의 숫자를 어느정도까지 쓸 수 있는지는 GPU의 종류에 따라 다르다고 한다.

또한 스레드의 최소 갯수 또한 효율에 영향이 있다고 하는데, 여기서는 x, y 각각 8 총 64개의 스레드를 설정하겠다.

 

더보기

void MyKernel(uint3 id : SV_DispatchThreadID)

실질적인 쓰레드가 처리할 일을 선언하는 곳

매개변수로서 SV_DispatchThreadID 를 받고 있는데, 쓰레드 하나가 실행될때마다 고유한 아이디가 발급되고, 저 매개변수를 통해 우리는 쓰레드별 고유한 인덱스를 받아올 수 있는 것이다.

 

만약 내가 64개의 스레드를 실행하라고 명령하면, id 에 0~63까지의 값이 전달된다는 것

스레드별 고유한 아이디를 이용해 각 큐브의 위치를 미리 계산할수 있다.

나는 여기서 이동행렬, 회전행렬을 미리 다 계산해서 렌더 셰이더에 넘겨줄 것이므로 행렬을 생성해서 RWStructuredBuffer 에 셋팅하고 있는 걸 확인할 수 있다.

여기까지 Compute Shader 의 코드설명이 끝났다.

 

요약하자면, 쓰레드가 처리할 일을 커널 함수에 구현하되, 이를 위해 외부로부터 변수를 전달받아야 하기 때문에 몇가지의 변수와 버퍼를 설정했다는 것

 

남은건 이제 이 컴퓨트 셰이더에 변수를 전달하고, 실행하라는 명령을 내리는 것뿐

이를 위해 InstancedProcedural.cs 파일을 몇군데 수정하자.

 

public class InstancedProcedural : MonoBehaviour
{
    [SerializeField] private ComputeShader computeShader;
    [SerializeField] private Material material;

    [SerializeField] private Mesh mesh;

    [SerializeField] private int count;
    
    private ComputeBuffer positionBuffer;
    
    private static readonly int PositionID = Shader.PropertyToID("_Positions");
    private static readonly int ResolutionID = Shader.PropertyToID("_Resolution");
    private static readonly int TimeID = Shader.PropertyToID("_Time");
    

    private void Update()
    {
        positionBuffer?.Release();
        positionBuffer = new ComputeBuffer(count * count, 4 * 16);
        
        computeShader.SetInt(ResolutionID, count);
        computeShader.SetFloat(TimeID, Time.time);
        computeShader.SetBuffer(0, PositionID, positionBuffer);
        
        int groups = Mathf.CeilToInt(count / 8f);
        computeShader.Dispatch(0, groups, groups, 1);
        
        material.SetBuffer(PositionID, positionBuffer);
        Graphics.DrawMeshInstancedProcedural(
            mesh, 0, material, new Bounds(Vector3.zero, Vector3.one), count * count
        );
    }
    
    private void OnDisable()
    {
        positionBuffer?.Release();
        positionBuffer = null;
    }
}

일단 코드량이 더 줄었다. 왜냐하면 위치계산을 하던 부분이 컴퓨트 셰이더로 빠졌기 때문

 

변경된 부분만 살펴보자면,

[SerializeField] private ComputeShader computeShader;

//시리얼라이즈를 통해 위에서 만든 Compute Shader 를 연결해주고
positionBuffer = new ComputeBuffer(count * count, 4 * 16);

//기존에는 x,y,z 의 float 값 3개를 전달하기 위해 4 * 3 = 12 를 썼다면,
//지금은 4x4 행렬을 전달하기 위해 4 * 16 크기로 바꾸었다.
computeShader.SetInt(ResolutionID, count);
computeShader.SetFloat(TimeID, Time.time);
computeShader.SetBuffer(0, PositionID, positionBuffer);

//컴퓨트 셰이더에서 위치와 회전 계산을 위해 변수를 전달하고 있으며,
//계산한 결과를 받기 위해 ComputeBuffer 도 전달한다.
int groups = Mathf.CeilToInt(count / 8f);
computeShader.Dispatch(0, groups, groups, 1);



중요한 부분이다

컴퓨트 셰이더를 실행시키는 함수는 Dispatch인데
첫번째 인자에는 커널의 인덱스를 넣는다
미리 언질하지 않았지만, 하나의 컴퓨트 셰이더는 여러개의 커널을 가질 수 있다.
우리는 현재 커널을 하나만 선언했으므로 0을 넣는다.

이하 매개변수에는 그룹의 갯수를 설정해야 하는데,
아까 위에서 커널을 실행하는 단위는 그룹이라고 했었다.
이 그룹 또한 3차원으로 설정 가능한데, 저 세 매개변수를 모두 곱한게 총 그룹의 갯수가 된다.

자 현재 우리는 컴퓨트 셰이더에 총 64개의 스레드가 병렬 처리되도록 설정했었다 [numthreads(8,8,1)]
하지만 큐브를 count * count 만큼 그려야 하기 때문에 count * count 만큼의 스레드가 필요하단 얘기인데,
count / 8 을 하게 되면 1차원의 그룹 갯수가 도출되고, 이를 x, y 그룹 갯수에 설정하면, 총 돌려야 할 그룹 갯수를 산출할 수 있다.

예를 들어 설명해보자

만약 count 가 128 이라면, 큐브를 총 128 * 128 그려야 하므로 16,384 개의 스레드가 필요하다.
여기서 하나의 커널이 현재 8 * 8 * 1 = 64개의 스레드를 병렬처리 할 수 있으므로
16,384 / 64 하면 필요한 그룹의 갯수가 256개가 나온다.

여기서 Dispatch 함수에 256, 1, 1 로 전달할 수 있지만, 3차원으로도 그룹을 전달 할 수 있기 때문에
x와 y에다가 균일하게 분배하면 16, 16, 1 이렇게도 전달이 가능하다.
결국 요 16 이라는 숫자는 128 / 8 과 같으므로

int groups = count / 8 과 같다

추가적으로 count가 8의 배수가 아닐 경우도 있으므로 올림처리를 해서 그룹이 부족하지 않도록 넉넉히 잡아준 것
material.SetBuffer(PositionID, positionBuffer);

//이제 CubeShader.shader 에는 float3 형태가 아닌 float4x4 형태의 버퍼가 전달된다.

여기까지가 코드 변경부분이다.

 

렌더 셰이더 (CubeShader.shader) 에서는 전달받은 행렬을 통해 정점을 변환 시켜주면 되기 때문에 코드량도 확 줄게 된다.

CubeShader.shader 를 살펴보자

StructuredBuffer<float4x4> _Positions;

...

v2f vert (Input v, uint instanceID : SV_InstanceID)
{
    float4 worldPos = mul(_Positions[instanceID], v.positionOS);
    v2f o;
    o.pos = mul(unity_MatrixVP, worldPos);
    o.normal = mul(v.normalOS, (float3x3)Inverse(_Positions[instanceID]));
    return o;
}

StructuredBuffer는 이제 float4x4 를 받아야 하므로 자료형을 바꿔주었고,

 

버텍스 셰이더 내부에서 정점 위치에 월드 행렬을 곱한 게 전부

추가적으로 행렬을 전달받았기 때문에 노멀 연산도 문제 없다 :)

 

이제 다시 유니티를 실행해보면!! 더욱더 부드러워진 당신의 큐브세상을 볼 수 있을 것이다.

(큐브가 귀여워 보인다면 이미 당신은 프ㅊ...)

 

이로서 컴퓨트 셰이더를 어떤 상황에 어떻게 쓰는지 살펴보았다.

예제 코드는 https://github.com/SeonHwan/ComputeShaderSample/tree/9558675375a04b8e82e5b1498849db03fc51fd6c 를 통해 확인할 수 있다.

 

프로젝트의 퍼포먼스가 떨어져 병렬처리를 고려하고 계신 분에게 조금이나마 도움이 되었으면 하는 바램이다.

다음 포스팅에서는 Job System 을 통해 CPU 작업을 Worker Thread 에게 어떻게 분산시킬 수 있는지를 다뤄보겠다.

'Unity > 최적화' 카테고리의 다른 글

Batch 개념 및 종류  (0) 2021.02.16
병렬처리 기법 1  (0) 2021.02.04

이번 포스팅에서는 유니티 최적화 중 병렬처리 기법에 대해 다루고자 한다.

 

유니티의 약점 중 하나가 싱글 스레드 라는 점인데, (엄밀히 말하면 듀얼 스레드다)

 

씬 로딩화면을 구현할때 ResourceManager.LoadSceneAsync 같은 비동기 함수를 쓰더라도 로딩 중 화면이 끊기는 현상을 자주 접해봤을 것이다.

 

하지만 로딩화면이야 뭐 사용자가 조작도 할 수 없고 현상도 도드라져 보이지 않게끔 움직이는 UI를 최대한 안쓴다던지 하는 방법으로 타협이 가능한데, 조작 중 끊기는 건 그냥 넘어갈 수가 없다. 

 

FPS 나 액션 게임같은 경우 렉 한방에 파티가 전멸하거나 내 케릭터가 사망하는 경험을 한번이라도 해보았다면, 그 중요성을 실감할 수 있을 것이다.

 

그래서 우리는 멀티 스레딩을 대신할 방법을 동원해 이를 해결해야 하는데, 

 

첫번째가 Job System, 두번째는 Compute Shader 이다.

 

이 둘 중 어느 것을 써야 하는지는 병렬 처리 특성에 따라 다르다.

 

Job System 은 Main Thread 가 아닌 별도의 Job Thread ( 유니티에서의 명칭은 WorkerThread 이므로 이후 WorkerThread라고 명명 ) 를 이용해 CPU가 할일을 다른 CPU 코어한테 나눠주는 것이고, (최신 CPU는 코어 갯수가 최소 3~8개이다) Compute Shader 는 GPU한테 그 역할을 맡기는 것이다.

 

Profiler 상에서의 Worker Thread

만약 자신의 프로젝트가 CPU는 바쁜데 GPU는 비교적 널널할 경우 우선 Compute Shader 도입을 고려해야 한다.

GPU는 비용이 작은 스레드를 동시에 처리하는 방식에 특화되어 있기 때문에 스레드 간 데이터 종속성이 없고, 하나의 스레드에 무거운 연산을 하지 않는다면 Job System 보다 효과적인 결과를 얻을 수 있다.

 

 이에 반해 어? 우리는 CPU도 바쁘고 GPU도 바쁜데...이러면 선택의 여지없이 Job System 을 도입해야 한다.

또한 스레드 갯수는 상대적으로 적고 하나의 스레드에서 무거운 연산을 해야 한다면 이 또한 Job System을 도입하는게 유리하다.

 

개념 설명은 여기까지 하고 우리는 프로그래머니까 코드로 설명하는게 확 와닿을 것이다.

 

먼저 병목 현상을 인위적으로 만들어 낸 후 이를 Compute Shader 와 Job System 으로 해결하면서 최적화 된 결과를 도출해내는 과정을 설명하겠다. 

 

먼저 단순히 40000 개의 큐브를 월드에 뿌려보자

using UnityEngine;

public class GeneralInstancing : MonoBehaviour
{
    [SerializeField] private GameObject instancingTarget;

    [SerializeField] private int count = 200;
    
    private void Start()
    {
        for (int i = 0; i < count; ++i)
        {
            for (int j = 0; j < count; ++j)
            {
                GameObject instanced = Instantiate(instancingTarget);
                instanced.gameObject.SetActive(true);
                
                float x = -(count * 0.5f) + i;
                float z = -(count * 0.5f) + j;
                instanced.transform.position = new Vector3(x * 2, 0, z * 2);
            }
        }
    }
}

Scene View, Hierarchy View
큐브 생성 후 Collider는 제거해주자

실제 실행하면 다음과 같이 FPS가 확연히 떨어짐을 체감할 수 있다. (수치값은 PC마다 다를 수 있음)

40000만개의 큐브

프로파일러로 확인해보면 RenderLoop.ScheduleDraw() 에서 많은 시간을 할애하고 있는데, 이는 CPU가 GPU에게 40000 번의 드로우콜을 요청하는데 이만큼의 시간이 걸리고 있다는 것을 의미한다. ( = CPU 병목)

녹색의 RenderLoop.ScheduleDraw 가 확연히 길다.

 그렇다면, CPU를 거치지 않고 우리가 직접 GPU한테 그리라고 명령을 내리면 이 시간이 걸리지 않을테니, 병렬처리 기법에 대해 알아보기 전에 이 작업을 GPU 에게 넘겨보자.

 일단 GPU한테 작업을 넘겨야 그 뒤에 GPU한테 병렬처리를 맡길 수 있으니 일단 해보자

 

using UnityEngine;

public class InstancedProcedural : MonoBehaviour
{
    [SerializeField] private Material material;

    [SerializeField] private Mesh mesh;

    [SerializeField] private int count;
    
    private ComputeBuffer positionBuffer;
    private Vector3[] positions;
    private static readonly int Positions = Shader.PropertyToID("_Positions");

    private void Start()
    {
        positions = new Vector3[count * count];
    }

    private void Update()
    {
        positionBuffer?.Release();
        positionBuffer = new ComputeBuffer(count * count, 12);
        
        for (int i = 0; i < count; ++i)
        {
            for (int j = 0; j < count; ++j)
            {
                float x = (-(count * 0.5f) + i) * 2;
                float z = (-(count * 0.5f) + j) * 2;
                positions[(count * i) + j] = new Vector3(x, 0f, z);
            }
        }
        positionBuffer.SetData(positions);
        material.SetBuffer(Positions, positionBuffer);
        Graphics.DrawMeshInstancedProcedural(
            mesh, 0, material, new Bounds(Vector3.zero, Vector3.one), count * count
        );
    }
    
    private void OnDisable()
    {
        positionBuffer?.Release();
        positionBuffer = null;
    }
}

개념은 4만개의 큐브의 위치를 미리 설정해놓고, Graphics.DrawMeshInstancedProcedural 함수를 통해 GPU한테 직접 그리란 명령을 실행하는 것이다.

 

해당 함수의 매개변수를 보면 Mesh, SubMeshIndex, Material, Bounds, Count 로서 포지션을 셋팅할 수 있는 부분이 없다.

아니 그럼 어떻게 여기에 각각의 큐브의 포지션을 전달합니까?

 

이를 위해 ComputeBuffer가 필요하다.

ComputeBuffer 생성시 매개변수로, 생성할 갯수와 버퍼의 사이즈를 셋팅해줘야 하는데

매개변수에 count 와 stride

4만개의 큐브의 포지션 값을 저장할 것이므로 Count는 4만이고, 사이즈는 바이트 값으로서, float 값 3개 (x, y, z) 를 전달하면 되니까

float (= 4byte) * 3개 = 12 byte

즉 12 를 셋팅해 주면 된다.

 

이 버퍼를 통해 포지션값을 전달해주면, 쉐이더 내부에서 이 포지션 값을 참조해서 정점에다 셋팅해주면 끝

 

코드를 보면서 설명하자 (참고로 해당 쉐이더 코드는 URP 기준입니다)

쉐이더 파일을 하나 생성후 다음 코드를 붙여넣자

(CubeShader.shader)

Shader "Custom/CubeShader"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "UniversalMaterialType" = "SimpleLit" "IgnoreProjector" = "True" "ShaderModel"="4.5"}
        LOD 300

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
            
            Blend One Zero
            ZWrite On
            Cull Back

            HLSLPROGRAM

            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 4.5
            
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Input.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            #pragma vertex vert;
            #pragma fragment frag;
            
            StructuredBuffer<float3> _Positions;
            
            struct Input
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
            };
            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
            };

            v2f vert (Input v, uint instanceID : SV_InstanceID)
            {
                float3 data = _Positions[instanceID];
                
                float3 localPos = v.positionOS.xyz;
                float3 worldPos = data.xyz + localPos;
                
                v2f o;
                o.pos = mul(unity_MatrixVP, float4(worldPos, 1.0f));
                o.normal = TransformObjectToWorldNormal(v.normalOS);
                return o;
            }

            half4 frag(v2f i) : SV_TARGET
            {
                Light mainLight = GetMainLight();
                i.normal = normalize(i.normal);

                float3 lambert = LightingLambert(mainLight.color, mainLight.direction, i.normal);
                half3 ambient = SampleSH(i.normal);

                lambert *= mainLight.distanceAttenuation * mainLight.shadowAttenuation;
                lambert += ambient;
                
                return half4(lambert, 1);
            }
            
            ENDHLSL
        }
    }
}

fragment 쉐이더는 일반적인 램버트 라이팅과 앰비언트를 적용한 코드므로 중요치 않고, vertex 쉐이더 부분을 주의깊게 보자

 

중간쯤 StructuredBuffer<float3> 이 보인다. 스크립트의 material.SetBuffer() 를 통해 전달된 ComputeBuffer 가 여기에 저장되어 있고, 

vertex shader 함수의 SV_InstanceID 를 통해 나의 포지션을 받아 이를 정점위치에 적용시키고 있다.

 

StructuredBuffer는 일종의 배열이므로 _Positions[index] 문법을 사용할 수 있고, SV_InstanceID 는 각 메시별 고유한 인스턴싱 아이디로서 0 부터 시작하는 int 값이다.

 

코드 작업이 끝났다면, 유니티에서 메테리얼을 하나 생성 후 해당 쉐이더를 설정해준뒤, Serialize Field를 통해 메시와, 그 메테리얼을 설정해준다.

CubeMtrl 이란 메테리얼에 CubeShader를 설정하자
게임오브젝트에 InstancedProcedural 스크립트를 연결하고 Material 은 CubeMtrl, Mesh는 일반 Cube 연결

이제 다시 실행하고 통계창을 확인해보면 FPS가 확연히 높아짐을 확인할 수 있다.

65 FPS 확인

프로파일러 에서도 해당 작업이 시간이 거의 걸리지 않음을 확인할 수 있다.

RenderLoop.ScheduleDraw() 가 보이지 않는다.

여기까지만 해도 사실 병목현상은 해결했기 때문에 병렬처리와 무관한 부분이다.

하지만 이제 여기에 큐브가 회전하는 작업을 넣고 싶다면?

 

일반적인 상황이라면, Update() 함수에서 다음 한줄만 실행하면 된다.

transform.Rotate(Vector3.up, Time.deltaTime * 50f);

그런데 우린 정점 처리를 GPU에다 직접 넣고 있기 때문에 회전 정보도 직접 GPU에 전달해줘야 한다.

 

위 셰이더 코드 중 vert 함수를 아래 코드로 대체하자

 

float4x4 RotateYMatrix(float r)
{
    float sina, cosa;
    sincos(r, sina, cosa);
    
    float4x4 m;
    
    m[0] = float4(cosa, 0, -sina, 0);
    m[1] = float4(0, 1, 0, 0);
    m[2] = float4(sina, 0, cosa, 0);
    m[3] = float4(0, 0, 0, 1);
    
    return m;
}

float4x4 PositionMatrix(float3 pos)
{
    float4x4 m;
    m[0] = float4(1,0,0,pos.x);
    m[1] = float4(0,1,0,pos.y);
    m[2] = float4(0,0,1,pos.z);
    m[3] = float4(0,0,0,1);
    
    return m;
}

v2f vert (Input v, uint instanceID : SV_InstanceID)
{
    float4x4 tfM = mul(PositionMatrix(_Positions[instanceID]), RotateYMatrix(_Time.y));
    float4 worldPos = mul(tfM, v.positionOS);
    
    v2f o;
    o.pos = mul(unity_MatrixVP, worldPos);
    o.normal = mul(v.normalOS, (float3x3)Inverse(tfM));
    
    return o;
}

코드에 대해 설명하자면,

 

정점의 위치 행렬과 회전 행렬을 구한 후 이를 통해 Model 행렬 (로컬 공간에서 월드 공간으로 가기 위한 행렬) 을 도출해서 fragment 쉐이더에 전달하고 있다.

 

여기서 3D 그래픽에 대해 간단히 짚고 넘어가야 할 것 같다.

 

물체의 정점은 vertex shader 에서 여러 변환(MVP 행렬을 곱함) 을 거쳐 fragment shader 로 전달되고 fragment shader 에서 픽셀 연산을 거친 후 (주로 라이팅 연산) 최종적으로 모니터 화면에 뿌려진다.

 

우리는 현재 shader 내부에서 회전을 해줘야 하기 때문에 직접 회전행렬을 구해야 하고, 위치 행렬또한 직접 구해서 Model 행렬을 도출해내야 한다.

 

회전 행렬은 어떤 축을 기준으로 회전시키냐에 따라 다른데 우리는 Y축을 기준으로 회전하고 싶으니 아래 행렬을 써야 한다.

Y축 회전행렬

또한 위치 행렬은 아래 행렬을 쓴다

이를 이용해 RotateYMatrix 함수에서는

  • sincos(r, sina, cosa) 에서 해당 r값(라디안값) 을 사인코사인 값으로 변환
  • float4x4 로 4x4 행렬을 생성
  • 4개의 행에 회전행렬 요솟값들을 설정

PositionMatrix 함수에서는

  • 4x4 행렬 생성
  • 포지션 값을 설정

이 두 행렬을 구해서 동일하게 넘겨주면 shader 에서 회전을 연산하는 코드가 완성된다.

 

실행을 해보면 Update() 에서 실행한 것관 다르게 프레임 저하없이 부드럽게 회전하고 있는 큐브를 확인할 수 있다.

 

'아 이 사람 뭐지 병렬처리 설명해준담서 이상한 것만 하고 있네'

 

이제 기반환경이 모두 마련되었으니 Compute Shader 를 어떤때 쓰는지 확인할 시간이다.

이후는 다음 블로그에서 다루기로 하겠습니다.

 

여기까지의 코드는 아래 링크에서 다운받을 수 있으니 혹여 실행이 안된다면 참고용으로 확인해 보시기 바랍니다.

https://github.com/SeonHwan/ComputeShaderSample/tree/3f2dac345239c940db67b02fe6b7f97132c2ab4d

 

'Unity > 최적화' 카테고리의 다른 글

Batch 개념 및 종류  (0) 2021.02.16
병렬처리 기법 2  (0) 2021.02.05

이전 포스팅에서 RenderObjects 를 이용한 2Pass 방식의 알파처리를 다루었습니다.

 

동작과정에 대해 설명하기 전에 기본적인 파이프라인 개념이 있다고 가정하고 진행하도록 할게요.

 

먼저 URP 에 대한 개념부터 짚어보자

 

유니티에서 만든 스크립터블 렌더 파이프라인의 일종으로 가볍고 고성능을 지향해서 모바일 환경에서 주로 쓰는데,

이전의 빌트인 파이프라인에서는 그래픽스 API를 직접 접근할 수 없었고, OnRenderImage 함수 안에서 제한적인 기능만 쓸 수 있었다.

대표적인 함수가 Graphics.Blit

 

아래 이미지는 빌트인과 스크립터블 렌더 파이프라인 (이제부터 SRP 라고 명명하겠다) 의 차이를 설명한 것

빌트인 파이프라인
SRP

저기 빨간 네모를 통해 CPU 메인쓰레드에서 GPU 렌더쓰레드로 명령이 전달되는데 GPU 에게 그려라는 명령을 내리기 위한 전반적인 프로세스인 저 빨간박스 영역을 우리는 파이프라인이라고 한다.

 

차이가 보이는가?

빨간색 박스가 게임로직 박스 옆으로 이동되었다. 이게 아주 중요한 부분이다.

유니티께서 우리에게 그래픽스 API에 바로 명령을 전달할 수 있게 스크립트로 빼준 것

 

저렇게 바뀜으로서 우리는 어떻게 어떻게 그려라 라는 값들을 C# 스크립팅을 통해 잘 정리하고 패킹해서 SRP에 전달할 수 있게 된것.

다시 말하자면 예전엔 빨간색 상자에 전달할 수 있는 명령이 몇개 없었는데, 지금은 대부분의 것을 빨간색 상자에 전달하면 빨간색 상자는 그대로 그려준다는 것이다. (여기서 빨간색 상자는 SRP)

 

단! 저 빨간색 상자에 어떤 값을 전달해줘야 하는지 알기 위해선 그래픽스에 대한 이해가 필요하다.

비유하자면 21단 자전거를 사줬지만 기어변경을 할 줄 모르면 쓸모가 없다는 것

 

이제 기존 빌트인 파이프라인과의 차이에 대해 이해하겠는가?

 

그래도 모르겠다면, 유니티 코리아에서 자세한 개념설명 해주셧으니 해당 영상을 참고하기 바란다

https://www.youtube.com/watch?v=QRlz4-pAtpY&list=PL412Ym60h6utrWNnHpZYSlG_pwRafryMO&index=15

 

차이에 대해 이해했다고 가정하고, 자 그럼 어떠 어떤걸 SRP 에 넘겨줘야 하는거지?

 

이를 위해 SRP 는 Pass 개념을 도입했는데 SRP 는 여러개의 패스로 구성된 렌더러를 가지고 있다.

DOD 스타일로 다루어져서 이 렌더러를 바꿔끼기만 하면 다른 렌더링 방식으로 렌더링이 가능하다.

우리는 이 Pass, Pass, Pass 를 잘 구성하기만 하면 되는 것이다.

 

그래서 ForwardRenderer.cs 파일을 열어보면 이 패스들을 정해진 순서대로 그리고 있는것을 확인할 수 있다.

이 패스를 지지고 볶고 추가하고 삭제하고 하는 방식으로 SRP 와 상호작용 할 수 있다.

 

자 개념도 알겠고 내가 Pass 를 전달해줘야 하는 것도 알겠다.

그런데 어떻게 스크립팅 하라고?

그럴걸 대비해서 유니티는 친절하게도 예제 소스를 친히 제공해주는데, 그게 바로 RenderObjects 라는 클래스 되시겠다.

 

얘를 까보면 코드 몇줄 없고, 실질적으로 RenderObjectsPass 생성하고, Renderer에 패스 추가하는 역할이 끝

renderObjectsPass = new RenderObjectsPass(settings.passTag, settings.Event, filter.PassNames,
                filter.RenderQueueType, filter.LayerMask, settings.cameraSettings);
                

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
	renderer.EnqueuePass(renderObjectsPass);
}

요 Pass (위 코드에서는 RenderObjectsPass) 에다가 잘 셋팅하는 것. 이게 URP 를 다루기 위한 핵심이다.

 

이전 포스팅에서의 작동 방식에 대해 설명하기 위해 먼길을 돌아왔다.

 

저번에 ForwardRenderer 에다가 Add RenderFeature 를 한 것을 기억하는가?

바로 이 패스를 추가하기 위함이었던 것이다.

 

이제 이러한 개념을 바탕으로 2패스 알파블렌딩에 대해 고찰해보자

 

URP 는 기본적으로 멀티 패스 렌더링을 지원하지 않는다.

하지만, Universal Render Pipeline 관련 쉐이더를 까보면 아닌데? SubShader 에 쉐이더 Pass 여러갠데?

이건 오브젝트 하나 그릴때 여러개의 패스를 다 탄 다음에 그려라는 내용이 아니다.

 

URP 는 쉐이더 Pass 의 LightMode 태그를 통해 동일한 객체를 여러번 그리게 되는데,

 

뎁스쓰기용 태그를 가지고 있는 오브젝트를 다 갖고와서 뎁스만 설정하고 (이게 Depth 패스)

불투명 태그를 가지고 있는 오브젝트를 다 갖고와서 불투명 오브젝트만 그리고 (이게 Opaque 패스)

투명 태그를 가지고 있는 오브젝트를 다 갖고와서 투명 오브젝트 그리고 (Transparent 패스)

 

이런 식으로 하나의 쉐이더를 가지고 각 패스마다 여러번 참조하기 때문에 쉐이더 패스를 여러번 참조당한다 라는 개념이 맞을것이다.

 

다 알겠다. 그래서 2 Pass 어떻게 처리하는데요?

이제 결론을 얘기할 때가 된 것 같다.

 

우리는 이 Pass 사이사이마다 임의로 패스를 추가할수도 있기 때문에 중간 어디쯤에 패스를 밀어넣어주면 결과적으로 멀티 패스를 구현할 수 있는 것이다.

 

Event 항목이 이 패스를 어디에 끼워넣을건지 설정할 수 있는 부분이며,

이 패스를 어떤 오브젝트에 적용할건지 필터 설정도 할 수 있다.

 

Override 항목에는,

어떤 재질로 덧 그릴것인지,

뎁스버퍼를 어떻게 설정할 것인지, (ZTest, ZWrite 모두 저 항목으로 설정 가능)

스텐실버퍼를 어떻게 설정할 것인지

 

자주쓰는 기능들이 모두 마련되어 있다.

 

2 Pass 알파 처리의 핵심은 뎁스버퍼를 먼저 채우고 난 다음 반투명을 덧그리는 것이기 때문에,

반투명 오브젝트를 그리기 전에 뎁스버퍼를 채우는 Pass 를 추가하면 해결이 되는 것이다.

 

2 Pass 쉐이더를 자주 이용했던 프로젝트에서 URP로 넘어갈때 어떻게 해야 하는지 감이 안잡히셨던 분들에게 조금이나마 도움이 됬으면 하는 바램이다.

 

 

'Unity > Universal Render Pipeline' 카테고리의 다른 글

알파 블렌딩 (1)  (0) 2020.12.09

URP 를 공부하며, 기존 레거시 쉐이더에서 자주 쓰이는 2Pass 방식을 어떻게 적용할지 고민하고 있다.

 

URP 는 기본적으로 싱글패스만 지원하기 때문에 멀티패스를 쓰기 위해선 별도 처리가 필요한데, 이를 위해 유니티에서 마련해준 기능이 RenderFeature 중 RenderObjects 이다.

(물론 이 RenderFeature 또한 직접 구현이 가능하며, RenderObjects 는 유니티에서 편하게 쓰라고 만들어준 RenderFeature 의 일종이다)

 

어떻게 쓰느냐에 따라 활용방법이 무궁무진 한데, 멀티패스 구현 또한 가능하다.

 

여기서는 알파 블렌딩 시 가려지는 뒷쪽 메시들이 부분부분 보이는 문제를 해결하기 위한 RenderFeature 사용 방식을 설명할 것이다.

이런 Convex 한 반투명 메시들은 뒷 메시가 보여져버렷..

 

먼저 우리의 가장 중요한 ForwardRenderer 를 보자

 

이녀석은 ScriptableRenderer 를 상속받아 유니티에서 마련해놓은 포워드렌더링 방식의 파이프라인이다.

여기에 Add RenderFeature 가 보일것이다.

버튼을 눌러보면 Render Objects(Experimental) 이라는 항목이 하나 있을텐데 그걸 선택한 뒤 다음처럼 셋팅하자

아 참, 아래 ZWriteOnly 메테리얼은 새로 만든 셰이더로서 이렇게 셋팅하면 된다

ZWriteOnly 메테리얼의 Inspector

중요한 부분은 Base Map 의 알파를 0 으로 셋팅해서 안보이게 해야 한다는 것이다 (추후 설명하겠지만 Z 버퍼에 쓰기만 할 용도임)

 

아래 부분이 Render Object 에 대한 설정값

각 셋팅에 대한 설명은 아래에..

그 후 반투명 처리할 물체의 메시에 적용해줄 메테리얼을 하나 만들고 다음과 같이 셋팅한다

URP 기본 쉐이더로 셋팅 (원한다면 lit 을 해도 상관 없지만, Surface Type, Blending Mode 는 꼭 저대로 셋팅해야 반투명 오브젝트로 인식하고, 알파 블렌딩도 적용된다.)

Base Map 항목에 컬러값을 셋팅할 수 있는걸 볼 수 있는데 여기에서 알파를 조절하면, 씬뷰에서 반투명 처리되는 메시를 볼 수 있을 것이다.

이 메시의 Layer 도 위 값에 셋팅된 대로 ZTestTransparent 라는 걸로 만들어서 적용하면, 아래처럼 깔끔히 알파처리된 메시를 볼 수 있다.

왼쪽이 깔끔처리된 메시, 오른쪽이 알파 블렌딩 문제 메시

 

위 코드는 저의 깃헙에서도 확인할 수 있습니다. (Assets/Scenes/ConvexMeshAlphaScene.unity 를 열어서 확인가능함)

https://github.com/SeonHwan/ShaderExample

 

SeonHwan/ShaderExample

Contribute to SeonHwan/ShaderExample development by creating an account on GitHub.

github.com

 

자 이제 어떻게 이렇게 구현이 되는지 하나하나 따져볼 시간이지만 포스팅이 길어지므로 다음 포스팅에 자세히 설명하겠습니다.

 

Universal RP 패키지의 몇몇 버전에서 제대로 동작하지 않는 버그가 있습니다. (7.2.0 ~ 7.4.3 쯤에서 비정상 동작하는거 같음)

각 유니티 버전에 verified 된 버전을 사용하거나 최신 유니티 버전을 사용하세요

'Unity > Universal Render Pipeline' 카테고리의 다른 글

알파 블렌딩 (2)  (0) 2020.12.10

GameView의 Statistics 창을 보면 DrawCall 을 나타내는 지표로 Batches 와 SetPass Calls 가 있다.

아니, DrawCall 횟수가 없네?

 

사실 몇달전 면접질문 중 하나가 유니티의 DrawCall 을 Batch와 SetPass Call 개념을 통해 설명해보라 였는데 대답을 못했었다.

문득문득 이불킥 감이었는데, 얼마전 오지현님의 URP 관련 동영상 강의를 보면서 개념에 대해 정리하게 되었다.

 

먼저 DrawCall 에 대해 설명하자면,

CPU가 GPU한테 그려라! 명령을 내리는 작업이다.

보통 그래픽 최적화 시, 프레임당 드로우콜이 몇번 발생했느냐를 따질때 그 드로우콜인데, 유니티에서는

그려라! 라는 것(DrawCall) + SetPass Call 을 Batch 숫자로 환산한다.

 

자 그럼 이 Set Pass Call 이 무엇인지만 알면 Batch 가 무엇인지 확실히 알게 되는데, 이 개념이 사실 쉽게 와닿지 않았다(최소한 나에겐)

 

CPU가 GPU에게 내리는 명령은 사실 DrawCall 뿐만이 아니다.

CPU는 GPU에게 명령할때 Command Buffer 라는 곳에 여려 명령들을 모아놨다가, 이 버퍼 자체를 전달하는데 이때 여려 명령들 중에 하나가 드로우 콜이다.

 

유니티 코리아 유투브 채널의  [Dev Weeks : 성능을 고려한 파이프라인. Universal Render Pipeline]  세션 에서 캡쳐한 커맨드 버퍼의 개념

보다시피 드로우콜은 수많은 커맨드 중 하나일뿐이고, 특정 커맨드들을 묶어서 Set Pass Call 로 명명한다.

왜 얘네들만 따로 묶어서 명명하지?

그 이유는 저 커맨드들이 GPU에 부하를 주는 주범이기 때문

 

이 커맨드들의 공통점은 GPU에 상태값을 전달하는 명령이라는 것인데, 만약 같은 메테리얼을 가진 오브젝트 10개를 그린다고 가정했을때 첫번째 그리는 오브젝트에서 DrawCall + Set Pass Call 이 호출되고나면 두번째 오브젝트부터는 DrawCall 만 GPU에 전달하면 된다.

왜냐면 텍스쳐나, 쉐이더 상수, 쉐이더 코드, 블렌딩 공식등 (Set Pass Call 에 해당하는 명령들) 은 첫번째 오브젝트 그릴때 전달했던 값을 그대로 쓰면 되니까

 

하지만 만약 다른 메테리얼을 가진 오브젝트를 그려야 하면 이전에 전달한 값을 그대로 쓸수 없을 것이니 저 Set Pass Call 에 해당하는 명령들을 GPU에 다시 전달해야 할것이다. 문제는 저 명령들이 GPU 입장에서 비용이 많이 드는 작업이라는 것.

 

그럼 위 결과를 토대로 본다면, 저 Set Pass Call 을 줄일수록 GPU에 주는 부하가 적게 들것이고, 이는 곧 최적화와 연결된다.

Set Pass Call 을 최대한 적게 호출하면서 Draw Call을 수행하는 것.

유니티에서는 이를 배칭 이라고 표현한다.

 

자 이제 아래의 Statistics 창에서 왜 Batches 와 Set Pass Calls 이 표현되는지 이해가 가는가?

우리의 최적화의 최종 목표는 Batches 에 비해 Set Pass Calls 를 최대한 줄이는 것 (Batches - SetPass Calls 값이 적을수록 최적화가 잘 되었단 의미)

 

그럼 어떻게 하면 SetPass 를 줄일 수 있죠?

  • 일단 단순히 메테리얼을 적게 쓰면 된다. (= 메테리얼 베리언트 수를 적게 유지할것)
  • 메테리얼 교체 비용이 많이 발생하지 않도록 renderer 옵션의 sorting layer 를 통해 동일한 메테리얼들을 하나로 묶어서 그리게 한다
  • Dynamic 배칭과 Static 배칭을 적극 활용한다 (이 두가지 배칭에 대한 개념 및 차이는 본 블로그 주제와 다르므로 설명 생략)
    • 하지만 만약 Dynamic 배칭이 일어날 확률이 극히 적을 경우엔 아예 Dynamic 배칭 기능을 끄는게 좋다고 한다
      • Dynamic 배칭 대상인지 아닌지 확인하는 작업도 비용소모가 발생

 

위 내용을 기반으로 다른 문제를 하나 더 생각해보자

 

만약 서로 다른 메테리얼이지만 동일한 쉐이더를 쓰는 경우를 가정해보자 (A 메테리얼의 색깔은 빨간색, B 메테리얼의 색깔은 파란색으로 하고 싶다)

 

A 메테리얼 - Shader A

B 메테리얼 - Shader A

 

이 경우 Batch 와 SetPass Call 은 각각 2 개로서 동일한 쉐이더를 쓰지만 둘은 서로 메테리얼로 인식해서 배칭이 이루어지지 않을 것이다.

하.지.만 유니티 에서는 SRP Batcher 기능을 켜면 위와 같은 상황에서 배칭이 된다 (!!!)

 

원리는 간단한데, GPU 메모리에다가 저 SetPass Call을 통해 전달받은 값을 저장해놨다가, 나중에 동일한 값이 들어왔을때 그 값을 바로 참조해서 쓰는 것이다. 이에 대한 자세한 개념은 위에 소개한 동영상에서 확인 할 수 있다.

단점은 저 값을 저장해야 하니까 GPU 메모리를 먹는다는 것인데, 효과에 비한다면 엎드려 절이라고 하고싶은 기능이다.

 

 

 

 

'Unity' 카테고리의 다른 글

Quaternion 공간변환  (0) 2020.11.23
rigidbody, collider 개념 및 정리  (0) 2020.10.08
IL2CPP  (0) 2020.09.29
Addressables  (0) 2020.07.28
Mobile 최적화  (0) 2020.06.12

제목으로는 어떤 포스팅인지 궁금할 것이다.

 

우리는 유니티에서 회전값을 받아오거나 셋팅할때 Quaternion 자료형을 쓴다.

 

평소때는 부모의 회전값을 고려할 필요가 없고, 있더라도 localRotation을 씀으로써 별 어려움 없이 쓸 수 있다.

하지만, 근래 모바일의 자이로 센서 회전값을 카메라에 적용해야 할 필요가 있어, Input.gyro.attitude 값을 그대로 카메라에 넣었더니, 당연하게도 정상동작을 하지 않았다.

 

첫번째 난관으로는 오른손 좌표로 넘어오는 자이로 회전값을 왼손좌표를 쓰는 유니티 좌표로 변환하는 곳

두번째 난관으로는 변환 후 카메라에 해당 회전값을 적용하는 곳이었다.

여기까지는 단순히 자이로 공간 -> 유니티 카메라 공간 일 뿐이므로, 약간의 삽질은 있었지만, 샘플 프로젝트에서 정상동작 확인 후 실 프로젝트에 적용하게 되었다.

 

하지만 역시나 되지 않았다.

몇번의 구글링을 해보니 카메라의 부모 오브젝트를 하나 만들고 그녀석의 회전값을 (90f, 180f, 0f) 해놓고 자이로 값을 카메라의 localRotation에 넣으라신다.

 

하지만 나는 부모의 부모의 회전값도 실시간으로 건드려야 하기 때문에 로컬회전값 따윈 필요하지 않아서, '좋아 얘를 월드회전으로 바꿔야지' 하면서 호기롭게 부모의 월드 Quaternion * 자이로 회전값 을 카메라의 월드회전값에 적용했다.

결과는 비루했고, 원인을 찾아 해결은 햇지만 추후 다시 쿼터니언을 다룰 일이 있을것 같아 알게된 몇가지 명제에 대해 정리하고자 한다.

 

  • transform.rotation : Quaternion.Inverse(transform.parent.rotation) * transform.localRotation
  • 쿼터니언 A, B 간의 차이 : A * Quaternion.Inverse(B)  쿼터니언의 곱은 교환법칙이 성립하지 않는다. 순서 주의
    • A에서 B로 가기 위한 회전값으로도 간주할 수 있다.
  • A 쿼터니언에 회전값 B를 추가 : B * A
    • 주의하자. 회전값을 추가할때는 항상 추가하고자 하는 값을 먼저 곱한다.

 

이를 이용해 자이로 센서값을 카메라의 월드회전에 적용하기 위한 코드는 다음과 같다.

camera.rotation = Quaternion.Inverse(Quaternion.Euler(90f, 180f, 0f)) 
					* Input.gyro.attitude 
					* new Quaternion(0f, 0f, 1f, 0f);

 

 

포지션은 부모의 위치를 쭉 순서대로 더하면 나의 월드 위치를 알 수 있는 것관 다르게 회전은 부모의 회전 역행렬을 먼저 곱해야 나온다는 사실을 잊지 말아야겠다.

 

 

'Unity' 카테고리의 다른 글

Batch 및 SetPass Calls  (0) 2020.12.03
rigidbody, collider 개념 및 정리  (0) 2020.10.08
IL2CPP  (0) 2020.09.29
Addressables  (0) 2020.07.28
Mobile 최적화  (0) 2020.06.12

알파가 포함된 이미지의 실제 보이는 부분만 터치영역으로 설정하고 싶을때가 있다.

가령 아래와 같은 이미지

참고 이미지

UI 는 기본적으로 사각형 형태로 영역이 잡히기 때문에 몇가지 설정을 해주어야 하는데,

  1. Texture Import Inspector
    • Sprite MeshType 을 FullRect로 설정
    • Advanced -> Read/Write Enable 체크
  2. Image 의 AlphaHitTestMinimumThreshold 설정

GetComponent<Image>().alphaHitTestMinimumThreshold = 0~1값

터치의 raycast hit 테스트의 알파값을 설정할 수 있는 값으로, 설정 값 이상의 알파값을 갖는 영역만 터치로 간주하겠다는 의미

+ Recent posts