본문 바로가기
Shader

3주차, 함수 조작을 배워보자.

by JDCM 2020. 9. 17.

좀 더 깔끔하게 보고 싶으신 분들은, 구글 도큐먼트 링크를 추천합니다.

docs.google.com/document/d/1wJvxya6Wts9d4404O_js6wajwYbVPjv3l23wF6Hv_s4/edit?usp=sharing

 

3주차, 함수 조작을 배워보자.

3주차, 함수 조작을 배워보자. 조작이라고 하니까 마음에 드는데? 우선 저번 시간에 배웠던 것을 다시 보자 2주차 돌아보기 float, SubShader, Swizzling… 가볍게 떠올려보자. ( 귀여워 해주자. ) 텍스쳐

docs.google.com

 

3주차,

함수 조작을 배워보자.

조작이라고 하니까 마음에 드는데?

우선 저번 시간에 배웠던 것을 다시 보자

1. 2주차 돌아보기

float, SubShader, Swizzling… 가볍게 떠올려보자.

 

( 귀여워 해주자. )

 

텍스쳐 압축마다 다르던 범위를 프로그래머들은 친절하게 0~1 사이의 값으로 나타낼 수 있게 해주었고… 그런 소수점이 되어버린 RGB들을 불러올 수 있도록 float 단위를 쥐어주었다.

 

float = 부동 소수점 숫자(floating point numbers) (32bit) 

매우 큰 숫자나 소수점이 있는 숫자를 저장할 때 쓰인다.

소수점 앞과 뒤에 있는 자릿수(4320.0, -3.33, 0.01226과 같은 실수)를 지원한다.


즉, 소수점이 있는 일반적인 수를 가져오는 프로그래밍 계산 단위다.


R, G, B를 나타내려면 (float, float, float) = 즉 float3 이다.


색상을 표현하면 

float3(1.0, 1.0, 1.0)

 = 24bit (255, 255, 255) // 흰색이 나온다.

 

물론 다른 half나 fixed가 있지만 잘못 만졌다가 큰일이 날 수도 있으니 넘어가자.

 

이 뿐만이 아니라 쉐이더 코드에는 다양한 종류가 있었다고 했다.

대표적으로 HLSL, GLSL, Cg가 있었다.



  • HLSL

고급 쉐이더 언어(High Level Shader Language), 대중적으로 쓰이는 상위 레벨 셰이딩 언어이다.

Unity URP버전과 Unreal에서 주 쉐이더 언어로 쓰인다.

 

  • GLSL

OpenGL 쉐이더 언어(OpenGL Shading Language), C언어를 기초로 한 상위 레벨 셰이딩 언어이다. 하드웨어에 의존한 언어를 사용하지 않고 개발자가 그래픽스 파이프라인을 제어할 수 있도록 

OpenGL 전용으로 만들어졌다.

 

  • Cg

C for Graphics, Nvidia와 Microsoft 협력으로 만든 상위 레벨 셰이딩 언어이다.

HLSL과 비슷하지만 변수 유지에 있어 HLSL이 더 깔끔하다.

참고로 Unity의 주 쉐이더 언어기도 하다.

 

저번 시간에 Unity가 Cg를 쓰지 않았다고 했는데, 사실은 Cg가 들어가있다. 

자체 언어를 만들어 주기는 했어도 서로 섞어쓰는 모양이다. 덜 피곤해졌다.

 

 

쉐이더 레퍼런스는 너무 기니까 차라리 2주차 정리본을 읽고 넘어가자.

( 2주차 : https://docs.google.com/document/d/143upW_QCl_7Nw9c7-qGdYAjrJAFmKJap8THlDQRFBV8/edit?usp=sharing )

 

1-1. Surface Shader 또 돌아보기

 

(쉐이더의 진명만을 기억해주자.)

 

New Surface Shader를 생성할 때에 이름을 최대한 확정된 형태로 지어주거나, 파일의 이름과 함께 내부 코드 안에서 이름을 수정해야 한다는 것을 알게 되었다.

 

이름을 넣어주는 방법을 알게 되었다면 그 내부의 설정도 조작할 수 있다.

 

Properties Syntax, 프로퍼티의 블록을 정의는 중괄호{} 안에 여러 프로퍼티를 정의하는 예시가 있었다.

 

_Name(“display name”, Range(min, max)) = number

_Name(“display name”, Float) = number

_Name(“display name”, int) = number

_Name(“display name”, Color) = (number,number,number,number)

_Name(“display name”, Vector) = (number,number,number,number)


_Name(“display name”, 2D) = “name” { options } 

_Name(“display name”, Rect) = “name” { options }

_Name(“display name”, Cube) = “name” { options }

_Name(“display name”, 3D) = “name” { options }

 

상단이 주어진 틀이라고 한다면, 그 틀에 맞춰서 값을 넣은 결과도 볼 수 있었다.


 

  • “Color”라는 명시 이름(display name) 값에 1, 1, 1, 1이 들어갔으니 흰색? ( O )

  • “Albedo(RGB)”라는 이름이 있는 곳에 2D Texture를 넣을 수 있음! ( O )

  • “Smoothness”라는 이름이 있는 곳에 0~1을 넣을 수 있는데 0.5 값이 들어감! ( O )

  • “Metallic”이라는 이름이 있는 곳에 0~1을 넣을 수 있는데 0.0 값이 들어감! ( O )

 

이렇게 Properties를 만들 수 있었다.

 

 

설정을 만들었다면 그대로 값이 들어가는 것이 아닌, 값과 설정이 연동될 수 있도록 연결하는 과정이 필요했다. SubShader도 다시 보도록 하자.

 

1-2. SubShader도 다시 확인하기 

(물론 물 속의 Ambient Light 값은 제가 책임 안 져요!)

 

세미콜론 영역에 들어왔어도 놀라지 말자.

기본적으로 우리가 알 수 있었던 부분은 주석과 INSTANCING을 지운 void surf 영역이다.

 

(텅텅...)

 

상단의 Properties Syntax를 할 때와 동일하게 이 친구들도 주문서가 있었다, 구조체(Struct)이다. 대표적으로 저 inout 옆에 있는 SurfaceOutputStandard라는 친구가 구조체의 한 예시인데, 그 안에 있는 주문서(구조체)를 보자.

 

struct

SurfaceOutputStandard

{

fixed3 Albedo; // RGB 값을 가진 Albedo 메뉴

fixed3 Normal; // RGB 값을 가진 Normal 메뉴

fixed3 Emission; // RGB 값을 가진 Emission 메뉴

half Metallic; // float1을 가진 Metallic 메뉴

half Smoothness; // float1을 가진 Smoothness 메뉴

half Occlusion; // float1을 가진 Occlusion 메뉴

half Alpha; // float1을 가진 Alpha 메뉴

};

 

우리는 이 주문서를 보고서 o.Albedo = float3(1, 0, 0); 를 주문하게 되면 정확하게 빨간 Albedo가 나오는 것을 볼 수 있었다.

 

그리고 변수를 통해서 값을 미리 설정하고 그 값을 불러올 수 있었다.

변수의 예로는 float3 red = float3(1, 0, 0); 로, red 를 o.Albedo = red; 값에 넣으면 그대로 빨강이 나올 수 있는 방식이었다.

 

뿐만 아니라 red에서 float(1, 0, 0)은 사실 R, G, B(또는 X, Y, Z)값이었기 때문에 

float(1, 0, 0)을 float(r, g, b)로 볼 수 있기 때문에 R, G, B의 배치를 바꿀 수 있었다.

 

o.Emission = red.grb;

 

결과는 초록색인 것은 이제 눈으로 읽힐 수 있다.

 

1-3. Swizzling도 다시 다시 확인하기 

상단의 R, G, B의 배치를 바꿀 수 있었다 했지만 R, G, B 중 하나의 값만 가져올 수 있다는 것도 알았다.

 

 만약 R, G, B 배치에 R의 값만 넣으려고 한다면

o.Albedo = red.rrr 가 아니라 o.Albedo = red.r를 넣는 것도 편하다는 것도 이해했다.

 

그 개념을 이해했다면 하단의 예시들도 이해할 수 있다.

 

o.Emission = float3 (1, red.b, 0);


o.Emission = float3 (alphaBlue.ba, 0);

( alphaBlue는 float4 alphaBlue = float4 (0, 1, 0, 0.5)이다. )

(무슨 색인지는 맞춰보도록 하자! 모르겠다면 2~3강을 다시 보는게 좋다.)

 

2. Properties 적용하기

저번 시간의 내용이 중요하기 때문에 서론이 무척이나 길었다.

이제 Properties를 만들고 조정하는 법을 알았으니 응용해보자.

 

우선은 R, G, B를 다룰 줄 알게 되었으니 조정하는 툴도 필요한 법이다.

Range를 넣어서 조정할 수 있도록 만들어보자.

 

(예상은 되겠지만 이건 회색이다.)

 

Properties를 넣었다면 이제 o(SurfaceOutputStandard)에 연결해주자.








 

SubShader에 Properties에 만들어둔 _R, _G, _B 애들을 가져와두면

SurfaceOutputStandard(o.호출)에서 후다닥 float3에 _R, _G, _B를 가져가는 것이다.

 

상단의 코드로도 잘 모르겠다면 하단의 참고 일러스트를 확인해보자.

 

물론 택배라는 모습으로는 완벽하게 설명하기 어렵지만

얼추 Properties에서 생성하고, Subshader에 두면 SurfaceOutputStandard가 값을 받아서 출력한다고 생각하는 것으로 충분할 것 같다.






이제 나도 RGB값 조절할 수 있다!

원하는 색은 얼추 숫자로 맞출 수 있게 되었다.

 

(만약 진짜로 원하는 색을 정확히 가져올 수 있다면 마비노기를 시작해서 밀레시안이 되보자.)





자 이제 색깔놀이는 했으니 이번엔 사진을 가져올 차례다.

사진(텍스쳐)을 어떻게 가져오는지 알아보자!

2-1. Texture 가져와보기 

텍스처는 기왕이면 Alpha 값도 있는 텍스처가 좋다.

아니 근데 … 

(내가 설마 텍스처 픽셀 하나하나 찍어야 하는 것은 아니겠죠?!)

 

숫자가 아닌 텍스처는 어떻게 넣을까? 물론 텍스처도 R, G, B값으로 연결이 되어 있다고 하지만 그렇다고 그 숫자 하나하나를 불러 올 수도 없는 일이다.

 

걱정마세요. 결국 프로그래머들도 우리가 이런 고민을 할 것을 알고 있었기 때문에

우리를 위해서 외울 공식을 만들어주었습니다.

 

아니 근데… (또?) 

외전. 쉐이더 방 좀 치우기 

(으아악! 도망가! 이건 내 나와바리가 아니다!)

 

애초에 Surface Shader 코드를 생성하자마자 키면 뭔… 정신이 없다.

초록색 주석들은 나를 메트릭스로 인도하며 파란색 단위와 함수들은 어색하기만 하다.

 

이런 Surface Shader를 청소하는 법이 있다!

안 쓰는 것 다 치우고 빈 집만 남겨놓기를 해보자.

 

상단의 택배 이미지를 보고 알았겠지만 환불을 하려고 한다면 회사에서 물건을 없애기(...)보다 수령받은 수령자가 하나씩 돌려보내는 것이 알맞다.

 

그러므로 수령받은 사람(SurfaceOutputStandard)부터 배달 전까지, 하단에서 상단으로 지워가야 한다. void surf부터 보도록 하자.

외전-1. void surf 치우기 

 

내부에 있는 것 다 지우세요. 설명은 이것 뿐입니다. 

 

외전-2. SubShader 치우기 

 

이미지 안에 있는 붉은 부분을 오른쪽의 예수님의 마음으로 치워주세요.

sampler2D 값을 지우지 않은 것은 추후 설명이 나옵니다!

외전-3. Properties 치우기 

 

얘도 내부에 있는 것 다 지우세요. 설명은 여기까지 입니다.



아무튼 이렇게 잘 치우고 나니 나름(본래는 sampler2D도 지워줘야 합니다.) 초기화가 잘 된 것 같다. 이 초기화 과정은 중고 핸드폰을 받으면 기계 공장 초기화를 하는 것처럼 반복하는 습관이 필요하다. 그래야 내가 바라는대로 쓸 수 있기 때문이다.

 

(완벽하게 공장 초기화)

 

이제 이 상태에서 계속 더 진행해보자.




2-2. Texture 진짜 가져오기 

float3로 가져올 수 없는 Texture를 위해

프로그래머가 따로 무언가를 준비했다는게 사실이야?

 

Properties Syntax를 다시 돌이켜볼 때 이런 틀이 있었다.

 

_Name(“display name”, 2D) = “name” { options } 



상단에서 언급했던대로 “내가 텍스쳐 픽셀 하나하나 찍어야 하는 것 아니겠죠?!” 의 의문을 풀어주는 툴이다.

 

(고마워요, 코드!)

 

어차피 픽셀 하나하나가 R, G, B인 것은 사실이다. 하지만 그걸 내가 할 필요 없다.

그런 일은 컴퓨터가 나보다 잘 할 것이다.

코드가 나 대신 픽셀 하나씩 R, G, B를 읽고 출력한다.

 

근데 이 과정을 마치 스포이드로 하나씩 찍고 출력하고 있기 때문에…

비커에 담듯 “Sampling(샘플링)”이라고 명칭한다.

그리고 그 샘플링을 진행하고 있는 컴퓨터에 -er를 붙여서, 샘플러(Sampler)가 된다.



그럼 우선 Properties에다가 텍스쳐를 불러오는 코드를 먼저 만들어보고, SubShader에다가도 가져다가 둬보자!

 

 

이제 자연스럽게 Texture를 가져왔다.

근데 그래픽을 배운 사람이라면 안다. Texture를 모델링에 올리려면 UV가 필요하단 사실을.

UV는 어딨죠? 

모델을 불러오면 UV도 똑같이 가져오게 되는 것을 안다.

문제는 이 과정에서 UV가 불러와질 때 Mesh 그대로 불러와지는 것이 아니다. 

 

https://www.researchgate.net/figure/Mapping-between-UV-map-3D-mesh-and-texture-image_fig1_262287176

 

UV 배치에 따라서 우리는 Vertex의 정보가 불러와지는 것을 알 수 있다.

문제는 Vertex 정보를 만들고, 다시 엔진에 넣을 때 엔진은 UV 설정을 ‘재해석’하게 된다. 

 

UV도 결국 X, Y, Z … R, G, B 와 같은 float2의 좌표 값이라는 것이다!

 

Mesh는 export 되면 자연스럽게 UV를 가져오면서 Vertex 좌표 값도 가져와진다.

문제는 그 값을 엔진 내에서는 가지고 있지만, 우리 쉐이더 코드 내에서는 가져오지 못했다.

 

그러기 위해 엔진에서 UV를 강탈해오는 코드가 있다.

 

(당신은 이 코드로 토끼같은 U와 여우같은 V를 데려올 수 있었다.)

 

Properties에서 정의한 이름 앞에 uv만 가져와 struct Input에 uv만 붙여 float2로 선언해주면 자연스럽게 가져와진다.



잘 선언하고서 SubShader에게 전달했으면, 이젠 SurfaceOutputStandard가 사용만 하게 연결해주면 된다! 



엥?

float4가 들어가는 것은 Texture가 R, G, B, A가 들어가기 때문이라는 것은 알겠지만…

tex2D는 낯설고 누구세요? 싶어지는 것이다.

 

함수, 함수가 나옵니다.

 

 

어려울 것 없다. 함수란 결국 귀걸이, 반지 세트 포장인 셈이다.

박스 하나에 귀걸이, 반지를 포장해야 한다면 박스 안 소켓에 귀걸이를 넣는 자리와 반지 넣는 자리가 있듯이… tex2D도 그렇다.

 

tex2D는 텍스쳐를 정갈하게 불러오기 위해 만들어진 함수라는 박스이다.

총 두 소켓이 있고, 앞에는 텍스쳐를 뒤에는 UV를 넣도록 만들어진 것이다.

tex2D 함수 설정은 이렇다.

 

tex2D( 텍스처, 텍스처 UV);

 

문제는 … UV가 struct Input에 숨어버렸다.

SurfaceOutputStandard에게 가져가려 하면 SubShader안의  Input 방이 틀어막고 있는 셈이다. (가져올 수가 없다.) 

 

그래서 UV가 숨은 방의 위치를 폭로시켜야 한다. 그래서 IN.uv_MainTex가 필요한 것이다.

 

이제 Texture의 결과 값도 불러오는 친구를 만들었으니 그 Texture를 Albedo에 붙일지, Emission에 붙일지는 자유다. 

 

 

아무튼 예쁘게 Texture가 잘 나오기 시작했다!



2-3. Texture 가지고 놀아보기 

Texture도 알고보니 float4인 것을 알았겠다, 여러모로 갖고 놀 수 있겠다!

 

(색깔은 작성자의 사상과 관계가 없음을 알림.)

이미지의는 Inspector에서 값을 바꾼 것이므로 코드를 짜고 나서는 하얀색일 것이다.

 

Color 값을 곱해서 Texture 위에 색을 입힐 수도 있다.

만약 게임 도중에 색을 바꾸고 싶다고 한다면 프로그래머에게 Properties의 선언 명칭인 _Col 의 값을 바꿔줘! 라고 할 수 있는 것이다.

(예: _Col이 float4거든? 빨갛게 보이고 싶으니까 (1, 0, 0, 0)으로 값을 설정해주라.)

 

그럼 밝기도 조절할 수 있겠다!

 

 

이젠 좀 텍스처와 친해진 것 같다.

 

근데… 만약 기획이 “얘가 석화하면 흑백 되어야 하는데 어떡하실래요?” 한다.

흑백이요? 어? RGB가 반으로(0.5) 동일한 값이 되어야 하는데… 그걸 어떻게???

 

내가 석화되지 말고 잘 생각해보자. 

Texture도 똑같이 float4 취급을 받는다고 한다면 R, G, B, A를 따로 다룰 수 있다.

 

R, G, B 모두 동일한 상태에서 0.5의 값을 가지려고 한다면 …

 

o.Albedo = (texResult.r + texResult.g + texResult.b) / 3;

 

실제 공식은 ((R+G+B)/3, (R+G+B)/3, (R+G+B)/3)인데 귀찮으니까 간편하게 (R+G+B)/3을 해버리는 것이다. (상단과 동일)

 

이렇게 하면 최대값을 가진 R, G, B가 동일하게 3으로 나뉘어서 0.5 값으로 나뉘게 된다.

 

( 고마워요, 기획자! )

 

우리는 이제 창백해진 석화에다가 텍스쳐를 곱하고 밝기를 더해주면 되는 것이다!

 

2-4. Texture Lerp 해보기

Lerp가 뭐죠?

 

( 어딜 보십니까? 그건 제 잔상입니다만! )

 

Lerp를 사용하기 전에, 상단에서 우리는 Texture를 자연스럽게 넣을 수 있었다.

그럼 한 Material에 Texture가 두 개가 들어간다면? 두 개가 나오려나???

 

 

두 개가 들어가긴 하는데 그냥 찐해지기만 하도 아무 의미가 없는 듯 하다.

에휴 모르겠다. Lerp가 뭔지부터 알아봐야 하겠다.

 

https://ko.wikipedia.org/wiki/%EC%84%A0%ED%98%95_%EB%B3%B4%EA%B0%84%EB%B2%95

 

Linear Interpolation, 선형 보간이라고 뜻하는 Lerp는

시작점(A)과 끝 점(B)이 있을 때 A와 B의 사이에 위치한 값을 추정하기 위해 직선 거리에 따라 선형적으로 계산하는 방법이다.

 

간단하게, A랑 B 사이에 있는 값을 구하기 위해 직선을 사용하는 공식이라고 보면 좋다.

쉐이더에선 가장 흔한 방식이고 벡터와 컬러를 부드럽게 전환시킬 때 사용한다고 한다.



Lerp의 공식은 이렇다.

 

Lerp(x, y, s) 

x가 A, y가 B, s가 중간 값이라고 생각하면 된다.

여기서 중요한 점은 A와 B가 단위가 같아야 한다.

(예: A가 float3이면 B도 float3이어야 한다.)

 

솔직히 잘 모르겠다.

해봐야지 알 것 같으니 넣어보자.

 

좌측 Properties 상태에서 lerp에 두 텍스쳐를 넣어보고 0, 1 값에 따른 결과를 나타낸 상태다.

0은 첫번째 텍스처고, 1은 두번째 텍스처가 나왔다는 것을 알 수 있다.

그럼… 중간(0.5)의 값은?

 

( 대강 풀이 불에 타고 있다는 이미지 )

 

미묘해졌다. 어떻게든 반반 섞이기는 했다!

근데 이렇게 반반 섞을 것이었다면 Multiply가 낫지 않나…? 하지만 Lerp가 자주 쓰이게 되는 이유는 따로 있는 법이다.

 

우선 Lerp가 활약할 수 있도록 “Blending”이라고 불릴 _Blend라는 Range 값 부터 만들어주자.

그리고, 0과 1이 들어가던 사이에 _Blend를 넣어보는 것이다.

 

그럼 0과 1사이를 마구 오갈 수 있는 Range를 만들 수 있다! 

뿐만 아니라 같은 float4 상태라고 한다면 텍스쳐가 아닌, 흑백 상태도 만들 수 있는 것이다. (예 : (texResult.r + texResult.g + texResult.b) / 3을 texResult처럼 하나의 함수로 만들어 lerp x, y 둘 중 하나의 값에 넣는 것.)

 

근데 우리가 하나 잊은 것이 있다.

float4라면 R, G, B 만 있는 것이 아니다. Alpha가 자연스레 끼어있다.

 

Alpha 채널을 보면 …

 

( 너는 이미 알고 있다. )

 

우리는 사실 알고 있다.

저 검정은 그냥 검정이 아니라 0이고, 풀의 저 하얗게 밝은 부분은 1이라는 사실을... 

 

만약, 알파 채널을 Lerp의 마지막 값에 넣어본다면…? …

 

( 아니, 중간 값이 여기저기 중간 값도 되는거였어??? )

 

 

물론 반대의 경우도 잘 된다. 그리고 우리는 저번에 ‘반전’ 공식도 배웠다.

One Minus, 즉 1을 앞에서 빼면 반대가 된다는 것이다. 한번 해보자!

 

( 진짜 불타는 풀이 되어버렸네??? )

 

좋다. 텍스쳐를 두개 쓰고 동시에 Alpha를 통해 보여줄 수 있다는 점은 정말 이득이다!

문제는 더 이득이 있다. 우선 Sphere Object보다는 Quad Object가 필요하다!

 

주목해야 할 점은 사실 Inspector 창에 있는 Tiling이다.

...두 Texture 모두 다른 값으로 들어가고 있다는 것이다! 만약 저기서 불꽃 Texture만 Tiling 값을 올리게 된다면…

( 인덕션 파이야!!! ) 

 

지금은 텍스쳐를 저런 텍스쳐를 써서 그렇지만, 만약 바닥 광장에 일정하게 마크 또는 자국이 남아있어야 한다면 무척이나 유용한 기능이다! (마크, 자국의 Tiling 값만 올리면 되는 일!) 

 

( 다 쓰고 나니 콩콩 페이지라니… )

 

3주차를 통해서 가장 중요하게 알아두어야 할 점은 총 3가지다.

  • Surface Shader의 초기화 방법(쉐이더 방 좀 치우기)

  • 텍스쳐를 불러오는 함수 공식과 방법

  • Lerp 공식에 대한 이해

 

특히 Surface Shader의 청소를 할 때에는 하나씩 지워가면서 Save를 해가며 오류가 나는지 안 나는지를 확인하는 것이 가장 중요하고, 손에 익히는 것이 중요할 듯 하다.

 

'Shader' 카테고리의 다른 글

6주차, 빛을 알아보자.  (0) 2023.01.30
5주차, Vertex Color를 주자.  (0) 2020.10.08
4주차, UV를 다뤄보자.  (0) 2020.10.05
2주차, 쉐이더 코드를 배워보자.  (1) 2020.09.15
1주차, 쉐이더를 배워보자.  (0) 2020.09.14