27 C
Seoul
Saturday, August 15, 2020
Home Blog 커스텀 라이팅 셰이더 포팅

[URP 포팅] 커스텀 라이팅 셰이더 포팅

세 번째 URP 포팅 기록입니다. 이전 편에서는 NGUI 포팅과 셰이더 포팅에 대한 고민을 적어보았습니다. 이번 편은 커스텀 라이팅을 사용하는 셰이더를 HLSL로 포팅한 기록입니다. 작업 메모를 가져온 것이라 내용이 두서없고 설명이 부족할 수 있습니다. 좌충우돌하면서 꾸역꾸역 포팅해 나가는 얘기입니다.


이제는 커스텀 라이팅 모델을 사용하는 헤어 셰이더를 포팅해보자. 예전 셰이더 매크로를 사용하는 것이 많아서 걱정 되지만 최선을 다해서 포팅해보자. HLSL로 바꾸고 컴파일 에러를 하나하나 잡아보자.

_BaseMap, _BumpMap, _EmissionMap 같은 경우에는 SurfaceInput.hlsl에 정의되어 있어서 다시 정의하면 컴파일 에러가 난다. 커스텀 라이팅 모델을 사용한다면 SurfaceInput.hlsl을 안 쓰는 게 맞는 것 같다.
버텍스 셰이더에서  position, normal, bitangent, tangent 등을 넘길 때는 아래 유틸리티 함수를 사용하자.

VertexPositionInputs vertexInput = GetVertexPositionInputs(v.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(v.normalOS, v.tangentOS);

vertexInput과 normalInput 안에 필요한 값들이 다 계산되어 있으니 꺼내서 쓰면 된다. 잘 모르겠으면 Lit 셰이더를 열어서 보면 됨.

LIGHTING_COORDS() 매크로를 모른다는 에러가 난다. LIGHTING_COORDS는 AutoLight.cgin에 정의된 매크로로 라이트 쿠키맵과 셰도우맵의 좌표를 vertex -> fragment 셰이더로 옮길 때 쓰는 매크로 같다. 유니티 4.x 대에서 쓰던 것 같은데. 관련된 매크로로 TRANSFER_VERTEX_TO_FRAGMENT와 LIGHT_ATTENUATION 매크로가 있다. URP의 대체품은 무엇인지 Lit 셰이더를 참고해보자.

URP 셰이더를 검색해보니 Lighting.hlsl에 DistanceAttenuation(), AngleAttenuation() 같은 함수도 보이고 Light 구조체에 shadowAttenuation도 있다. 이걸 어떻게 쓰면 될까.

Lit 셰이더를 보니 위에 얘기한 VertexPositionInputs을 얻어서 GetShadowCoord() 함수에 넣으면 필요한 버텍스 포지션을 가져와서 그림자 좌표를 바꿔준다.  ( GetShadowCoords는 Shadows.hlsl에 있음) 버텍스 셰이더에서 프래그먼트 셰이더로 넘길때는 그냥 float4 shadowCoord를 정의해서 쓰고 있고,  이 값은 UniversalFragmentPBR()이라는 PBR 라이팅을 계산하는 함수로 넘겨진다. (InputData 구조체에 넣어서)
UniversalFragmentPBR()은 Lighting.hlsl에 정의되어 있고, 이 안에서 메인 라이트와 추가 라이트에 대해서 루프를 돌면서 라이팅을 계산한다.

셰도우 좌표는 GetMainLight() 라는 함수로 넘어가고, MainLightRealtimeShadow() 함수를 통해서 실제 셰도우 값으로 변경. Light 구조체의 shadowAttenuation으로 저장된다.

정리해보면, 그림자를 처리하기 위해서 그냥 float4 하나 넘기면 되는 거고, GetShadowCoord()로 좌표를 구하고, MainLightRealtimeShadow() 함수를 통해서 실제 attenuation을 구할 수 있다. 커스텀 라이팅 모델을 구현할 때는 이렇게 하면 되고, 만약 내장된 PBR 라이팅을 쓸거라면 InputData 구조체에 좌표를 넣어서 UniversalFragmentPBR()을 호출하면 된다.

라이트 쿠키 처리는 어떻게 하는지 좀 더 조사해봐야 한다. 일단 그림자부터 구현해보자. 아.. 에러가 너무 많이 난다.. 그만두고 집에 가고 싶다. ㅠ

기존 셰이더와 Lit 셰이더를 같이 보면서 짜깁기를 하다 보니 뇌용량을 넘어서기 시작한다. 쉽게 따라할 수 있는 규칙 같은 것이 있어야 될 것 같다. 가능하면 Lit 셰이더에서 사용하는 InputData나 SurfaceData 구조체를 활용해서 구조화하고 싶은데 지금 포팅하려는 셰이더는 너무 복잡해서 맞지 않는다. 일단은 기존 셰이더 구조에서 열심히 옛날 함수 들을 대체해보자.

RoughnessToSpecPower() 가 없단다.  예전에 UnityDeprecated.cgin에 있었고, 결국에는 PerceptualRoughnessToSpecPower()를 호출한다. 얘는 UnityStandardBRDF.cginc에 있다.  이 함수는 URP에는 없어서 복사해서 쓰자. 이 함수 안에서 사용하는 PerceptualRoughnessToRoughness() 같은 함수는 URP에도 CommonMaterial.hlsl안에 포함되어 있다.
( 참고로, 완전히 동일한 함수는 없지만 CommonMaterial.hlsl에 있는 RoughnessToVariance() 함수가 Roughness와 Specular 간 변환에 사용하는 함수인 것 같다.)

_SpecColor가 없단다. 예전에는 UnityLightingCommon.cginc에 정의되어 있어서 셰이더에서 따라 정의 안 하고 썼나보다. 정의해주는 걸로.

HLSL에는 fixed 타입이 없나 보다. 컴파일 에러가 난다. 우선 half로 변경.

FresnelTerm()이 없단다. 예전에는 UnityStandardBRDF.cginc에 있었다. URP를 보니 따로 함수로 빼놓은 것은 없고 여기저기서 직접 다양하게 구해서 쓰는 것 같다. 예전 것을 가져오자.

_DetailNormal_ST를 모른단다. _ST가 붙는 것은 기존에 UV offset과 tile을 정의하는 방식인데, URP에서는 어떻게 하나. Lit 셰이더를 보니 그냥 float4 _XXXXX_ST를 정의하고, vertex 셰이더에서 보니 TRANSFORM_TEX 매크로로 uv를 전달한다. TRANSFROM_TEX 매크로는 Macros.hlsl에 정의되어 있다.

변수를 정의해 주었더니 이번에는 Texture2D<float4>를 sampler2D로 변환할 수 없다는 에러가 난다. 텍스쳐 변수를 정의할때 sampler2D를 써도 되지만 샘플러를 직접 설정할 수 있도록 TEXTURE2D와 SAMPLER 매크로를 썼더니 sampler2D가 아닌 Texture2D 타입으로 정의된다.

TEXTURE2D 매크로는 render-pipeline.core 패키지의 API 폴더 안에 렌더 플랫폼별로 정의되어 있다. 예를 들어, D3D11.hlsl에는 Texture2D로 정의되어 있다. D3D9.hlsl과 GLES2.hlsl에만 sampler2D로 정의되어 있다.

SAMPLER 매크로도 비슷한 식이다. 지원하는 플랫폼은 SamplerState로 정의되고, 나머지는 빈 매크로다. 우선은 GLES2와 D3D9을 지원할 예정이 없으므로 함수의 인자로 넘길때 Texture2D와 SamplerState 타입을 써보자.

샘플링할 때는 SAMPLE_TEXTURE2D 매크로를 쓰면 된다. 아래 링크 참조. (예전에 LWRP 공식 도움말에서 읽은 내용인 것 같은데 공식 도움말은 못찾겠다.)

TEXTURE2D, SAMPLER, xxx 매크로를 사용할 때는 Core.hlsl을 #include 하면 된다. ( Core.hlsl -> Common.hlsl  -> 각 플랫폼별 hlsl 순으로 #include 된다)

이번엔 BlendNormals() 가 없단다. 예전에 UnityStandardUtils.cginc에 있었다. 노멀을 섞는 방법은 사실상 표준이 없어서 엔진마다 조금씩 달라서 유니티도 조금 다른 버전을 썼는데, 어디엔가 있지 않을까. 역시 CommonMaterial.hlsl에 3가지 버전의 BlendNormal 함수가 있다. 그런데, 구현이 살짝 다르다. 렌더링 결과물에 영향을 줄 수 있으니 일단 이전 버전을 카피해서 써보자.

_WorldSpaceLightPos0이 없단다. 드디어 올 것이 왔다. 라이팅은 URP에서 가장 많이 바뀐 부분. 기존에는 여러 라이트를 여러 패스로 나누어 그렸지만 URP에서는 한 패스에 모든 라이트를 처리해야 한다. Lit 셰이더에서 라이트 정보 얻어오는 부분을 확인하자. 실제 라이팅 계산은 Lighting.hlsl의 UniversalFragmentPBR() 함수에서 한다. 메인 라이트는 GetMainLight() 함수로 얻어온다. (같은 파일에 있다) 나머지 라이트는 GetAdditionalLight()로 얻어온다. 반환은 Light 구조체인데, direction, color, distanceAttenuation, shadowAttenuation이 들어있다.

좀 더 안쪽으로 들어가보면 메인 라이트 위치는 _MainLightPosition, 색상은 _MainLightColor, distanceAttenuation은 unity_LightData.z 혹은 unity_ProbesOcclusion.x에서 얻어온다. 프레임 디버거로 보면 다 전달되고 있는 변수들이다. 나머지 라이트 정보는 _AddtionalLightBuffer에 구조체로 저장되거나 _AdditionalLightsPosition 같이 각각의 배열에 저장된다. 프레임 디버거를 보니 내 경우에는 각각의 배열로 전달되고 있다.

GetAdditionalLight() 함수를 보면 이 배열을 사용해서 여러 가지 조건에 따라 계산을 해준다. 다 파악하는 것은 복잡하니 반환값을 가져다 쓰는 것이 무난해보인다.

기존의 서피스 셰이더는 하나의 라이트에 대해서만 코딩하는 방식이었기 때문에, 셰이더의 구조도 한 패스에 여러 라이트를 계산하도록 개편을 해야 한다.

기존 코드는 텍스쳐 샘플링과 라이팅 계산이 뒤죽박죽이다. 이 코드를 라이트 별로 돌릴 걸 생각하면 정말 비효율적이다. URP로 포팅하면서 어쩔 수 없이 리펙토링 해야하면서 최적화가 된다. 기존 코드에서 표면의 정보를 구하는 부분과 라이팅 부분을 분리하는 리펙토링부터 실시.

표면의 정보를 구하는 함수는 SurfaceInput.hlsl에 정의된 SufaceData 구조체를 반환하게 하자. 기존의 서피스 셰이더에서 채우던 albedo, specular, metallic 등등의 필드를 가지고 있다.

라이팅 계산에 필요한 것은 표면 정보, 버텍스나 뷰 디렉션 안개등의 메시와 환경정보, 라이트 정보 이렇게 3가지. 메시와 환경 정보는 Input.hsls의 InputData 구조체를 사용해보려고 했는데 커스텀 라이팅이라 좀 안 맞는다.  

DotClamped()가 없다. UnityDeprecated.cginc에서 복사해오자.

unity_LightGammaCorrectionConsts_PIDiv4 가 없단다. 예전에는 UnityDeprecated.cginc에 정의되어 있었다. URP는 Color.hlsl에 다양한 SRGB – Linear 변환 함수가 있는데, 정확히 매칭되는 것은 못 찾겠다. 렌더링 결과가 달라질 수 있으니 일단은 복사해오자.

아.. 이제 기존의 UnityGlobalIllumiation()을 호출하던 코드를 포팅해야 한다. 예전에는 GI 정보를 얻기 위해서 UnityGIInput에 필요한 데이터를 채우고 저 함수를 호출하면 되었다. URP에서는 GetMainLight()로 메인 라이트 정보를 구한 다음에 MixRealtimeAndBakedGI()를 호출해서 실시간 그림자와 구워진 그림자 정보를 섞는다. 이 함수에서 입력으로 필요한 값은 Light 구조체, 월드스페이스 노멀, baked GI(라이트맵이나 라이트프로브 정보).

Lit 셰이더를 보면 baked GI는 SAMPLE_GI 매크로를 통해서 Fragment 셰이더에서 얻고 있다.
다음으로는 GlobalIllumination() 함수를 호출해서 GI로부터 받는 색상을 계산한다. 예전 유니티의 UnityGlobalIllumination() 함수는 UnityGI를 반환하는데, 여기에는 diffuse, specular 값이 포함된다. 반면, URP 버전의 구현 내부를 보면 diffuse, specular를 각각 계산하기는 하는데 EnvironmentBRDF()를 한 번 더 거치면서 fresnel 텀을 고려해서 BRDF 데이터에 맞게 최종값을 합쳐준다.

어쨌든 URP의 GlobalIllumination() 함수에는 BRDFData, baked GI, occlusion, 월드스페이스 노멀, 월드스페이스 뷰 벡터가 입력으로 들어간다. BRDFData 말고는 맞출 수 있을 것 같다. BRDFData는 어디서 오나.
BRDFData는 SurfaceData에 있던 albedo, metallic, specular 등등을 조합해서 diffuse, specular를 에너지 보존되게 조절하고 grazing 텀을 계산하는 등등의 가공된 데이터를 만든다.

일단 분석은 완료. 이 정보를 잘 활용해서 기존 코드를 포팅해보자.

우선 URP에서 Light Probe를 받기 위해 해야할 일들을 체크해보면, 버텍스 셰이더에서 프래그먼트 셰이더로 Light Probe 정보를 넘길 변수를 정의. lighting.hlsl에 있는 DECLARE_LIGHTMAP_OR_SH 매크로를 쓰면 된다.

버텍스 셰이더에서 이 변수를 채우기 위해서 OUTPUT_LIGHTMAP_UV와 OUTPUT_SH 매크로를 쓴다.
프래그먼트 셰이더에서 half3 bakedGI 값을 만들기 위해서 SAMPLE_GI 매크로를 쓴다. 내 경우에는 indirectDiffuse와 indirectSpecular를 각각 얻어서 별도의 라이팅을 하고 싶기 때문에 GlobalIllumination을 호출하는 대신에, 그 함수 안쪽에 indirectDiffuse와 indirectSpecular를 구하는 코드만 참조하자.

indirectDiffuse는 bakeGI와 occlusion을 곱해주면 되고 indirectSpecular는 reflection vector, perceptualRoughness, occlusion을 GlossyEnvironmentReflection()에 넘기면 된다. 이 함수에서는 환경맵등을 참조해서 계산하고 occlusion을 곱해준다.

내장 렌더 파이프라인의 UnityGloballIllumination()에서는 인자로 전달한 라이트 정보를 제법 많이 수정한다. 기본적으로 attenuation을 곱해주는 부분도 있고, 라이트맵을 쓰는 경우에는 아예 초기화 해버리기도 한다.

Luminance() 함수가 없단다. 이전에는 UnityCG.cginc에 있었는데, 비슷한 함수를 찾아보자. Color.hlsl에 같은 이름의 Luminance() 함수가 있다. 다만 이전 버전은 칼라 스페이스를 고려해서 계산해준 반면 URP 버전은 Linear Colorspace 전용이다. 우리 프로젝트는 Linear 기반이라 크게 고민하지 않고 그냥 color.hlsl만 #include 해주는 걸로.

FresnelLerp() 가 없단다. UnityStandardBRDF.cginc에서 복사해오자.

와우! 동작한다! 분명히 뭔가 빼먹은 것이 있을 것 같은데 일단 셰이더 프로퍼티들을 만져보면 동작하는 느낌이다. 제일 확실한 것은 내장 렌더 파이프라인을 사용한 버전과 1:1로 비교해보는 것. 지금은 전체적으로 돌아가는 수준으로 URP 포팅을 하는 단계라 나중에 세밀하게 비교를 해보자.

할 일 추가 : 내장 렌더러와 URP 헤어 셰이더의 렌더링 결과 비교

이 셰이더는 Alphatest와 Alphablending 두 개의 패스로 구현된 셰이더인데, URP에서는 멀티 패스 셰이더를 지원하지 않는다. 멀티 패스 셰이더는 URP 포팅의 큰 산중에 하나다. 일단 오늘은 여기까지.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay connected

58FansLike
56FollowersFollow
156FollowersFollow
128FollowersFollow
- Advertisment -

Recipe of the day