Home 7. 코드로 만드는 쉐이더
Post
Cancel

7. 코드로 만드는 쉐이더

HLSL


변수와 상수의 선언

Untitled

변수란 변화할 수 있는, 값을 담을 수 있는 메모리 공간을 의미합니다.

선언(declare)하는 순간 메모리의 어떤 영역에 자료형의 크기만큼 예약이 되며

해당 주소의 값을 수정할 수 있습니다.

상수는 조금 다르게, 처음 선언한 값을 사용할 수 있습니다.

간단히 말해서 수정불가라는 뜻이죠.

Untitled

그리고 저기서 1,2,3,4,5,6 같은 숫자로 적은 값들이 있습니다.

이는 리터럴(literal)이라 부르며, 데이터 그 자체를 의미합니다.

자, 조합의 시간입니다.

Untitled

대충 이런 표가 나옵니다.

리터럴은 실행시의 읽기전용 데이터고,

상수는 일단 명명할 수 있는 데이터,

변수는 씹고뜯고 맛보고즐길 수 있는 데이터입니다.

Untitled

대충 이런 느낌이죠.

자료형

그렇다면 자료형이란 무엇일까요?

자료형(資料型) 또는 데이터 타입(영어: data type)은 컴퓨터 과학과 프로그래밍 언어에서 실수치, 정수, 불린 자료형 따위의 여러 종류의 데이터를 식별하는 분류로서, 더 나아가 해당 자료형에 대한 가능한 값, 해당 자료형에서 수행을 마칠 수 있는 명령들, 데이터의 의미, 해당 자료형의 값을 저장하는 방식을 결정한다.

즉 영역크기랑 읽는방법 저장한다는 뜻입니다.

한번 메모리를 볼까요?

간단하게 태초의 C에서

읽기 편하게 메모리는 일단 깨-끗하게 채우고,

변수를 하나씩 선언해 봅시다.

변수는 왜 이렇게 썼냐고요? 같이 짜주는 친구(copilot)가 이렇게 짜주던데요 변수는 왜 이렇게 썼냐고요? 같이 짜주는 친구(copilot)가 이렇게 짜주던데요

뭔가 재미있는게 있습니다.

bool과 char는 1칸(1byte, 256)를,

int는 4칸(4,294,967,296)을,

long long 은 8칸(…)을 점령하되, 01이라는 데이터가 들어가는걸 볼 수 있습니다.

그런데 float과 double은 4칸,8칸을 차지하지만 들어가는 값이 조금 다릅니다.

왜 그럴까요?바로 부동소숫점(floating point)이라는 점입니다.

Untitled

부동소숫점은 데이터가 지수부와 가수부로 나눠져서 처리되는데, 실수를 처리할려면 이게 좋겠죠?

그런데 여기서 정확한 값을 계산할려면 fraction이 촘촘해야하는데,

이게 줄어들면 정확도가 줄어들지만 하지만 사용 메모리를 줄여 전력과 성능의 차이를 꾀할 수 있으니 잘 선택해야합니다.

정확도의 개념은 조금 아래에서 다시 설명하고

그러면 이제 HLSL, 특히 유니티에서 사용하는 쉐이더의 자료형에 대해 알아봅시다.

float (1.2E-38 to 3.4E+38, x.1234567)

가장 정밀한 부동 소수점 값입니다.

일반적으로 32비트를 사용하며(일반 프로그래밍 언어의 float과 동일합니다)

삼각법 또는 거듭제곱/지수와 같은 복잡한 기능을 포함하는

세계 공간 위치, 텍스처 좌표 또는 스칼라 계산에 사용됩니다.

half (-60000 ~ +60000, x.123)

중간 정밀도 부동 소수점 값입니다.

짧은 벡터, 방향, 객체 공간 위치, 높은 동적 범위 색상에 유용합니다.

fixed (-2.0 ~ +2.0, 1/256)

가장 낮은 정밀도 고정 소수점 값입니다.

11비트 밖에 사용하지 않는 고정 정밀도는

HDR이 아닌 색상 및 이에 대한 간단한 작업을 수행하는 데 유용합니다.

int?

정수는 종종 루프 카운터 또는 배열 인덱스에 사용되며, 이를 위해 다양한 플랫폼에서 잘 작동합니다.

플랫폼에 따라 GPU에서 정수 유형을 지원하지 않을 수 있습니다. 예를 들어, Direct3D 9 및 OpenGL ES 2.0 GPU는 부동 소수점 데이터에서만 작동하고 단순하게 보이는 정수 표현식(비트 또는 논리 연산 포함)은 상당히 복잡한 부동 소수점 수학 명령어를 사용하여 에뮬레이트될 수 있습니다.

Direct3D 11, OpenGL ES 3, Metal 및 기타 최신 플랫폼은 정수 데이터 유형을 적절하게 지원하므로 비트 이동 및 비트 마스킹을 사용하면 예상대로 작동합니다.

(만약 float으로 비트이동을 하면 … 으악! 나는 숫자가 아니야!(NaN, Not a Number) 를 뱉습니다.)

정확도 문제

부동 소수점으로 표현한 수가 실수를 정확히 표현하지 못하며,

부동 소수점 연산 역시 실제 수학적 연산을 정확히 표현하지 못하는 것은

여러가지 문제를 야기합니다.

그리고 가끔 분배법칙이 성립하지 않습니다..

반올림의 타이밍에 따라 변화하는 세부수치 반올림의 타이밍에 따라 변화하는 세부수치

또한 다음과 같은 현상이 발생할 수 있습니다.

  • 소거: 거의 동일한 피연산자를 빼면 정확도가 크게 손실될 수 있습니다. 거의 같은 두 숫자를 뺄 때 가장 중요한 숫자를 0으로 설정하고 중요하지 않고 가장 잘못된 숫자만 남깁니다.

  • 정수로의 변환 문제: (63.0/9.0)을 정수로 변환하면 7이 생성되지만 (0.63/0.09) 변환하면 6이 생성될 수 있습니다. 이는 변환이 일반적으로 반올림되지 않고 잘리기 때문입니다. 버림과 올림은 직관적으로 예상되는 값에서 하나씩 벗어난 답을 생성할 수 있습니다.

  • 제한된 지수 범위: 결과가 오버플로되어 무한대를 생성하거나 언더플로로 인해 비정규 숫자(NaN) 또는 0이 생성될 수 있습니다. 이러한 경우 정밀도가 손실됩니다.

  • 나눗셈이 안전한지 검사하는데 문제가 생김: 제수가 0이 아닌지 확인한다고 해서 나눗셈이 오버플로되지 않는다는 보장은 없습니다.

  • 같음을 검사하는데 문제가 생김: 수학적으로 같은 계산결과가 나오는 두 계산 순서가 다른 부동소수점 값을 만들어낼 수 있습니다. 어느정도의 허용 오차를 가지고 비교를 수행하지만, 그렇다고 해서 문제가 완전히 없어지지는 않죠.

자 그러면 이런 부동소숫점의 정밀도가 뭘 얘기하는걸까요?

한번 유니티 샘플 씬에 존재하는 모델링을 빛의속도보다 100배 느리게 한번 이동해볼까요?

2997924의 속도로 달려가는 카메라와 모델링 2997924의 속도로 달려가는 카메라와 모델링

위 오브젝트처럼 정밀도를 보장하지 못할 값을 갖게 된다면

정밀도를 잃어 몇몇 값으로 수렴하는 것 처럼 이상해지는걸 볼 수 있습니다.

이유는 간단합니다. 소숫점이 어느지점으로 이동하여 그 밑으로 7자리까지의 안정성을 보인다 할 때, 값이 너무나도 커지게 된다면 소수부가 아닌 정수부 역시 정밀도문제가 발생하기 때문입니다. (그래서 time이 너무 커지지 않도록 frac을 사용하죠)

쉐이더 또한 마찬가지입니다.

GIF 2022-04-26 오후 11-29-22.gif

GIF 2022-04-26 오후 11-27-45 2.gif

그러면 이제 정밀도를 반토막을 내서 확인해 볼까요??

GIF 2022-04-26 오후 11-31-16.gif

엥 float과 half가 둘 다 같은데?

심호흡하십시오. 다시 들어갈 시간입니다.

너무 깊게들어가서 유니티가 터졌습니다. 너무 깊게들어가서 유니티가 터졌습니다.

Untitled

생성된 쉐이더 코드를 뜯어보면 float4하나, half 4개로 구성되어 있음을 볼 수 있지만,

렌더 프로파일링 도구에서 상수버퍼를 직접 뜯어보면

float4 (4*4), half * 4 (2 * 4)가 float4 + float4

총 32바이트로 들어가는것을 알 수 있습니다.

대조군을 만들어 테스트할려는 찰나!!!!!

4번정도 터짐 4번정도 터짐

그렇게 더이상 렌더독을 사용할 수 없게 되었습니다

(바이바이 렌더독)

왜 그런지 GPU의 연산구조에 대해 알아봅시다.

GPU의 연산유닛은 대부분이 FPU(float processing unit)입니다.

이제 데이터를 저장하는 레지스터 구조에서,

4바이트 4개(128비트)가 들어가는 메인이 레지스터가 다수 존재하여

데이터는 Float4로 들어가게 됩니다.

여기서부터는 이제 컴퓨터 아키텍처에 대해 들어갑니다.

SIMD(single instruction multiple data)SIMT(single instruction multiple Thread)에 대해 소개해보겠습니다.

SIMD의 경우 하나의 명령어로 ‘다수의 데이터’를 처리합니다.

즉 여러 데이터를 묶어서 처리하므로,

Untitled

Untitled

다음과 같은 기능을 하게됩니다.

SIMT의 경우는 버텍스쉐이더나 픽셀쉐이더가 동작하는 것과 같이

하나의 명령어로 작업쓰레드 풀을 돌리게 됩니다.

이걸 이제 묶은것이 amd의 웨이브프론트, nVidia의 워프입니다.

Untitled

SIMD의 연산수는 제한되므로, 한번에 돌릴꺼 다 돌리는 형식으로 동작하는것이 이제 웨이브프론트, 워프입니다.

참조 : AMD 연산 유닛 아키텍처 참조 : AMD 연산 유닛 아키텍처

Untitled

…아무튼 그래서 과거 Dx9시절에는 fp16을 사용하였지만, dx10시절부터 사장되었고,

데이터를 쪼개는것도 귀찮아서 float3도 float4로 사용할정도로 컴퓨터가 발전하면서 fp16은 사장이 되었습니다. (물론 머신러닝에 의해 다시 부활하였지만요…)

결론적으로 PC GPU는 항상 고정밀도라는 것입니다.

즉, 모든 PC(Windows/Mac/Linux) GPU의 경우 셰이더에 부동 데이터 유형, 절반 데이터 유형 또는 고정 데이터 유형을 작성하는지 여부는 중요하지 않습니다.

컴퓨터에선 항상 완전한 32비트 부동 소수점 정밀도로 모든 것을 계산합니다.

half 및 fixed 유형은 모바일 GPU를 대상으로 할 때만 관련이 있으며, 이러한 유형은 주로 전력(때로는 성능) 제약 조건에 대해 존재합니다. 정밀도/숫자 문제가 있는지 여부를 확인하려면 모바일에서 셰이더를 테스트해야 합니다.

그런데 이거 유니티 에뮬레이션이 분명 있었는데 사라졌습니다…

(Graphics Hardware Capabilities and Emulation)

아무튼 모바일 GPU에서도 GPU 제품군마다 정밀도 지원이 다릅니다.

Untitled

대부분의 최신 모바일 GPU는 실제로 32비트 숫자(float 유형에 사용) 또는 16비트 숫자(half 및 fixed 유형 모두에 사용)만 지원합니다. 일부 구형 GPU는 정점 셰이더 및 프래그먼트 셰이더 계산에 대해 정밀도가 다릅니다.

GPU 레지스터 할당이 개선되었거나 특정 낮은 정밀도 수학 연산을 위한 특수 “빠른 경로” 실행 단위로 인해 낮은 정밀도를 사용하는 것이 더 빠를 수 있습니다.

이점이 없더라도 낮은 정밀도를 사용하면 종종 GPU에서 더 적은 전력을 사용하므로 배터리 수명이 향상됩니다.

일반적으론 위치와 텍스처 좌표를 제외한 모든 것에 대해 반-정밀도로 시작한다고 합니다. 이때, 계산의 일부에 대해 절반 정밀도가 충분하지 않은 경우에만 정밀도를 높이면 됩니다.

real

플랫폼에 따라서 알아서 잘 넣어줍니다.

알아서 잘 넣어준다 한다면 C#의 var, 혹은 C++의 auto와 유사하다 생각할 수 있지만,

플랫폼에 따라 결정된다는 부분이 약간 다릅니다.

float4

Untitled

그냥 vector4입니다.

float 4개를 사용한 데이터입니다.

자 그러면 해당 float4는 어떻게 접근할 수 있을까요?

다음과 같은 접근은 허용됩니다.

var.x ((float) x의 값에 접근)

var.xy ((vector2) x,y의 값에 접근)

var.xyz ((vector3) x,y,z의 값에 접근)

var.xyzw ((vector4) x,y,z,w의 값에 접근)

var.rgb ((vector3) x, y, z의 값에 접근)

Untitled

var.zxyw (스위즐, swizzle)

Untitled

아래와 같은 접근은 불가능합니다

var.xygb (xyzw 및 rgb의 혼합 접근)

var.xx (말도안되는 접근)

matrix

행렬이란…

수를 1차원적으로 나열한 체계를 벡터라고 하였다면, 수, 문자, 함수등을 네모꼴 괄호 안에 배열하여 2차원 놓은 것이 행렬입니다.

선형변환(Linear Transformation)를 좀 더 편하게 하기 위하여 단순화 시킨 도구라고합니다.

행(row)과 열(column)을 가지고 있으며 다음과 같이 표기합니다.

Untitled

또한 행이 하나이거나, 열이 하나인 경우를 볼 수 있는데, 모습이 마치 벡터와 같습니다.

Untitled

그리고 이걸 수학적으로 행이 하나인 행렬은 행벡터(row vector)

Untitled

열이 하나인 행렬은 열벡터(column vector)

라고 합니다.

벡터의 곱셈

float4 a = float4(1, 2, 3, 4);

float4 b = float4(5, 6, 7, 8);

float4 v = a*b;

→ 이 곱셈의 의미는

v.x = 1*5;

v.y = 2*6;

v.z = 3*7;

v.w = 4*8;

와 같은 각 채널별의 곱이며, 내적이 아닙니다.

벡터 내적은 dot(a, b)입니다.

행렬의 곱셈도 비슷합니다.

m1 * m2 를 하게 된다면, 각 구성 성분들을 곱하게 됩니다.

행렬곱은 mul(m1, m2) 으로 계산합니다.

GIF 2022-04-27 오전 3-25-01.gif

GIF 2022-04-27 오전 3-26-43.gif

행렬곱은 연산순서에 큰 영향을 받으며,

Untitled

비가환적입니다.

분배법칙은

Untitled

에 따라 성립함을 알 수 있습니다.

Unity Shader의 구성

네이밍

Untitled

Untitled

git에서 branch를 표현하는법, 혹은 유니티에서 MenuItem을 설정하는 법과 같다.

코드 영역 해부

hlsl Anatomy.png

코드는 크게

ShaderLab 영역과

HLSL영역(구 CGPROGRAM 영역)으로 구분지을 수 있습니다.

URP로 넘어오며 HLSLPROGRAM - ENDHLSL로 연결되었으며,

내부는 이제 HLSL처럼 작성하면 됩니다.

HLSL영역에서는 크게

어떤처리를 어떤함수이름으로 할 것이냐(vertex vert),

기타 설정,

입력 형식 설정,

전역변수,

함수 정의

로 구성됩니다.

hlsl Anatomy 2.png

어떤게 어떻게 연결되는가는 위와 같습니다.

프로퍼티를 가져와야하므로 전역변수로 선언하고,

vertex 및 fragment shader를 어떤 함수가 담당할 것인지 정의하고,

입력과 출력 데이터를 맞춰줍니다.

몇몇 명명 제약이 있습니다.

Untitled

UV좌표의 Scale, Bias에 대한 정보는 다음과 같이 들어갑니다.

Untitled

바로 이 tiling과 offset에 대한 값이죠.

정상적인 값을 넣으면 다음과 같이 나타나지만,

Untitled

Untitled

이름을 바꾼다면

Untitled

마젠타가 되어버립니다.

Untitled

해당 _ST를 사용하는 TransformTex를 사용하지 않고 평범하게 uv를 넣는다면

Untitled

인스펙터의 변경되는 부분을 건드려도 반응이 없습니다.

프로퍼티

쉐이더 그래프의 다음 부분에 해당하는 영역입니다

Untitled

상수버퍼(CBUFFER)

정점 및 픽셀 셰이더에서 사용될 상수를 모아 놓은 버퍼입니다

기본 유니티 렌더링 워크플로 기본 유니티 렌더링 워크플로

SRP 렌더링 워크플로 SRP 렌더링 워크플로

상수 버퍼는 대기 시간이 낮고 CPU에서 더 자주 업데이트하는 것이 특징인 상수 변수 사용에 최적화되어 있습니다.

프로퍼티 추가해서 재질 변경

일단 화면좌표를 받아오기 위해 UnityObjectToClipPos을 한 vertex 정보를 가져옵니다.

그리고 화면에 맞추기 위해 i.vertex.xy (SV_POSITION) / _ScreenParams.xy 를 진행합니다.

GIF 2022-04-27 오후 6-26-52.gif

간단하게 vertex.w(SV_POSITION)를 곱해봅니다

GIF 2022-04-27 오후 6-30-21.gif

간단하게 nDotL을 해봅니다

HLSL도 모르는채로 그냥 있는 함수 아무렇게나 쓴거라 정상적인 식이 아닐 수 있습니다.

GIF 2022-04-27 오후 6-43-08.gif

(제작에 의미는 없습니다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Shader "Unlit/PropShader"
{
    Properties
    {
        [MainTexture] _MainTex ("Texture", 2D) = "white" {}
        _SubTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        //왜인지 모르겠지만 현재 Transparent, 3000번이 아니면 씬과 게임에서 렌더링이 되지 않습니다.
        Tags { "RenderType" = "Opaque" "Queue" = "Transparent" "RenderPipeline" = "UniversalPipeline" }
        LOD 100

        Pass
        {
            HLSLPROGRAM

            //Setup function name
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            //vertex input
            struct vi
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            //vertex to fragment
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
                float3 wPos : TEXCOORD2;
            };

            //samplers
            sampler2D _MainTex;
            sampler2D _SubTex;
            
            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                //float4 _SubTex_ST; //NOT use
            CBUFFER_END

            v2f vert(vi v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = TransformObjectToWorldNormal(v.normal);
                o.wPos = mul(UNITY_MATRIX_M, v.vertex.xyz);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                half4 col = tex2D(_MainTex, i.uv);

                half4 additional = tex2D(_SubTex, i.vertex.xy / _ScreenParams.xy * i.vertex.w);

                float nDotl = saturate(dot(i.normal, _MainLightPosition));
                
                col = lerp(additional, col, nDotl);

                return col;
            }
            ENDHLSL
        }
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
    }
}

대충 그런겁니다 끝!

마치며

공부할게 더 있는데 조금 부족해서 아쉽습니다

갑자기 알레르기성 결막염에 감기기운이 있는지 건강이 안좋아져서 더 팔 수 있는데 아쉽습니다.

참조

https://m.blog.naver.com/fs0608/221650925743

https://ec.europa.eu/programmes/erasmus-plus/project-result-content/52dfac24-28e9-4379-8f28-f8ed05e225e0/lec03_gpu_architectures.pdf

http://developer.amd.com/wordpress/media/2013/06/2620_final.pdf

This post is licensed under CC BY 4.0 by the author.
Contents