
집에 고이 모셔져있는 PBR 책
진입장벽이 너무 커서 포기한 기억이..
Physically Based Rendering 이란?
표면 ( surface ) 의 물리적 성질에 기반하여 오브젝트를 렌더링 함을 의미한다.
여기서 물리적 성질은 Albedo Color, Roughness, Smoothness, Metallicity 를 말한다.
차근차근 하나씩 정복해보자.
Smoothness
Diffuse light 와 Specular light 는 표면에서 빛이 반사되면서 발생한다.
Diffuse 라이트는 빛과 표면 사이에서 복잡한 상호작용으로 빛을 반사시킨다.
완전한 Diffuse Surface는 빛이 모두 동일한 방향으로 반사된다. 이를 Lambertian이라고 부른다.
사실 Diffuse Light HLSL 코드에서 사용하는 방식이 바로 Lambertian이다.
당연히, 실제 세계에서는 Lambertian은 불가능하다. 그럼에도 Diffuse Relfection을 잘 근사하는 기법이다.
반면에 Specular Light 에서는, 평평한 표면일수록 빛이 같은 방향으로 반사된다.
표면의 노말 벡터 지점에서 빛이 반사 된다면, 반사된 빛들을 받을 것이다.
거친 표면이 놓여있다면, Specular Light는 거의 찾기 힘들다. 빛이 Scattered 되버리기 때문이다.
거울들은 Specular 반사의 특별한 케이스이다.
빛은 거의 반사되며 Diffuse 라이트는 없는 물질이기 때문이다.
이는 빛나는 오브젝트들이 거울처럼 느껴지는 이유다.
Diffuse Light와 Specular Light는 표면의 Smoothness를 변경함으로써 영향을 줄 수 있다.
Full Smoth는 매우 밝게 바꾸고 (빛을 잘 반사 시키고), 다시 말해서 Speuclar 반사의 각도가 크게 되고.
Smoothness가 적을수록 원초적으로 Specular Light는 줄어들고, Diffuse Light를 사용한다.
Smoothness는 PBR 라이팅의 일부일 뿐이다.
PBR에서는 2개의 렌더링을 추가로 제어 할 수 있는 모드를 포함한다.
(1) Metalic Mode
금속성과 비금속성 사이의 크기로 모델링하도록 한다.
(2) Specular Mode
오브젝트의 다른 물리적 속성을 맡기지 않고, 반사되는 색상을 직접 제어 할 수 있게 해준다.
둘다 설명이 애매모호하니 집중적으로 다루어보자.
Metaliic Mode
Metallic 모드는 오브젝트의 반사성이 금속성의 정도에 따라 모델링하는걸 말한다.
비금속성 (Fully Non-Metallic)은 Diffuse Surface나 다름없다.
이런 금속성에 Smoothness에 기반한 Specular Light를 함께해서 오브젝트가 렌더링된다.
완전한 금속성 (Fully Metallic)은 Diffuse Surface를 갖고 있지 않다.
이때는 오브젝트의 색상은 완전히 환경 반사에 이루어진다. 그래도 Specular Light는 여전히 존재한다.
금속성 모드에서 매트리얼은 Specular 반사를 가지는데
들어오는 빛의 색상과 오브젝트의 Albedo에 기반하여 Specular Reflection 색상이 결정된다.
*Albedo : 0부터 1사이로 빛이 반사하는 정도, 표면의 베이스 컬러를 조절
Specular Mode
메탈릭 모드보다 Specular Highlights의 색을 잘 조절 할 수 있다.
Specular 모드에서 매트리얼은 Smoothness, Specularity가 강할수록 거울처럼 보인다.
(메탈릭에서는 Smoothness와 Metallicity가 강할수록..)
하지만, 메탈릭과 달리 Specularity가 커질수록 Albedo 컬러를 잃어버린다.
만약 Specular 모드에서 금속성 느낌을 내고 싶다면, Albedo는 검정으로 두고, Specular 컬러를 조절해야 한다.
Normal Mapping
노말 맵핑은 우리가 좀 더 디테일한 표면을 표현하기 위해 텍스처를 이용한 기법이다.
이 텍스처는 normal map 이라 부른다.
노말 맵핑은 표면에 불완전함을 주고, 폴리곤 추가 없이도 다른 표면 요소를 제공한다.
폴리곤 렌더링 비용 대신에 약간의 텍스처 처리 비용을 소요할 뿐이다.
그리고, 노말 맵을 수정하는 걸로도 같은 모델에 다른 표면의 느낌을 줄 수도 있다.
노말 맵핑은 오브젝트 표면의 각 포인트에서 노말 벡터를 변조함으로써 이루어진다.
그리고, 빛 계산들은 결과가 다르게 바뀌게 된다.
엄밀히 말하면 Normal Mapping은 PBR에만 속한 기법은 아니다.
그래도 빛과 관련되어 있기 때문에 표면의 물리적 성질을 흉내내기 때문이다.
Ambient Occlusion
메쉬가 굴곡이 많고, 모양이 다채로우면 빛을 덜 받는 부분이 있을 수 있다.
가려진 부분들은 빛이 도달하지도 못한다.
Ambient Occlusion은 위의 상황을 표현하는 말이다.
Occlusion Map은 Occlusion의 정도를 표현한다. 1에 가까울수록 Ambient Light를 크게 받고, 0에 가까울수록 Occlusion된다.
Emission
어떤 오브젝트들은 스스로 빛을 뿜어낸다. 스크린이나 네온 형광을 생각해보자.
PBR에서는 Emission을 조절하여 표현할 수 있다.
Emission은 2개의 파트로 구분한다.
(1) Emission Map
텍스처링을 통해 오브젝트의 Emissive 파트를 적용할 수 있다.
(2) Emissive Color
매우 밝은 표면을 표현하기 위해 0-255 색상 밸류값을 넘어서는 HDR 컬러를 이용한다.
Emission Map을 적용하지 않으면, 오브젝트 전체가 Emission한 색상을 띄게 된다.
PBR In HLSL
통곡의 벽의 시간이 왔다..

딱히 가지고 있는 노말맵이나 오클루전 맵같은게 없어서.. 대충 나온 결과긴하다.
Shader "Lucid-Boundary/PBR_Shader"
{
Properties
{
_MainTex ("Maint Texture", 2D) = "white" {}
_BaseColor("Base Color", Color) = (1, 1, 1, 1)
_MetallicTex ("Metallic Texture", 2D) = "white" {}
_MetallicStrength ("Metallic Strength", Range(0, 1)) = 0
_Smoothness("Smoothness", Range(0, 1)) = 0.5
_NormalTex("Normal Map", 2D) = "bump" {}
_NormalStrength("Normal Strength", float) = 1
[Toggle(USE_EMISSION_ON)] _EmissionOn("Use Emission Toggle", float) = 0
_EmissionTex("Emission Map", 2D) = "white" {}
[HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 0)
_AOTex("Ambient Occulsion Map", 2D) = "white" {}
}
SubShader
{
Tags
{
"RenderType"="Opaque"
"Queue" = "Geometry"
"RenderPipeline" = "UniversalPipeline"
}
LOD 200
Pass
{
Tags
{
"LightMode" = "UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert;
#pragma fragment frag;
#pragma multi_compile_local USE_EMISSION_ON __
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DYNAMICLIGHTMAP_ON
#pragma multi_compile _ DECLARE_LIGHTMAP_OR_SH
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
texture2D _MainTex;
texture2D _MetallicTex;
texture2D _NormalTex;
texture2D _EmissionTex;
texture2D _AOTex;
SamplerState sampler_MainTex;
SamplerState sampler_MetallicTex;
SamplerState sampler_NormalTex;
SamplerState sampler_EmissionTex;
SamplerState sampler_AOTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _BaseColor;
float _MetallicsStrength;
float _Smoothness;
float _NormalStrength;
float4 _EmissionColor;
CBUFFER_END
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 staticLightmapUV : TEXCOORD1;
float2 dynamicLightmapUV : TEXCOORD2;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float4 tangentWS : TEXCOORD3;
float3 viewDirWS : TEXCOORD4;
float4 shadowCoord : TEXCOORD5;
DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 6);
#ifdef DYNAMICLIGHTMAP_ON
float2 dynamicLightmapUV : TEXCOORD7;
#endif
};
// struct SurfaceData
// {
// half3 albedo;
// half3 specular;
// half3 metallic;
// half3 smoothness;
// half3 normalTS;
// half3 emission;
// half3 occlusion;
// half3 alpha;
// half3 clearCoatMask;
// half3 clearCoatSmoothness;
// };
// struct InputData
// {
// float3 positionWS;
// float4 positionCS;
// half3 normalWS;
// half3 viewDirectionWS;
// float4 shadowCoord;
// half fogCoord;
// half3 vertexLighting;
// half3 bakedGI;
// float2 normalizedScreenSpaceUV;
// half4 shadowMask;
// half3x3 tangentToWorld;
// };
v2f vert(appdata v)
{
v2f o;
VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(v.normalOS, v.tangentOS);
o.positionWS = vertexInput.positionWS;
o.vertex = vertexInput.positionCS;
// o.vertex = TransformObjectToHClip(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.normalWS = normalInput.normalWS;
float sign = v.tangentOS.w;
o.tangentWS = float4(normalInput.tangentWS.xyz, sign);
o.viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
o.shadowCoord = GetShadowCoord(vertexInput);
OUTPUT_LIGHTMAP_UV(v.staticLightmapUV, unity_LightmapST, o.staticLightmapUV);
#ifdef DYNAMICLIGHTMAP_ON
v.dynamicLightmapUV = v.dynamicLightmapUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
OUTPUT_SH(o.normalWS.xyz, o.vertexSH);
return o;
}
SurfaceData createSurfaceData(v2f i)
{
SurfaceData surfaceData = (SurfaceData)0;
// Albedo ouput
float4 albedoSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
surfaceData.albedo = albedoSample.rgb * _BaseColor.rgb;
// Metallic output
float4 metallicSample = SAMPLE_TEXTURE2D(_MetallicTex, sampler_MetallicTex, i.uv);
surfaceData.metallic = metallicSample * _MetallicsStrength;
// smoothness output
surfaceData.smoothness = _Smoothness;
// normal output
float3 normalSample = UnpackNormal(SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, i.uv));
normalSample.rg *= _NormalStrength;
surfaceData.normalTS = normalSample;
// emission output
#if USE_EMISSION_ON
surfaceData.emission = SAMPLE_TEXTURE2D(_EmissionTex, sampler_EmissionTex, i.uv);
#endif
// ambient occlusion output
float4 aoSample = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex, i.uv);
surfaceData.occlusion = aoSample.r;
// alpha output
surfaceData.alpha = albedoSample.a * _BaseColor.a;
return surfaceData;
}
InputData createInputData(v2f i, float3 normalTS)
{
InputData inputData = (InputData)0;
// Position input.
inputData.positionWS = i.positionWS;
// Normal Input
float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz);
inputData.tangentToWorld = float3x3(i.tangentWS.xyz, bitangent, i.normalWS);
inputData.normalWS = TransformTangentToWorld(normalTS, inputData.tangentToWorld);
inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
// View Direction Input.
inputData.viewDirectionWS = SafeNormalize(i.viewDirWS);
// Shadow Coords.
inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
//Baked lightmaps.
#if defined(DYNAMICLIGHTMAP_ON)
inputData.bakedGI = SAMPLE_GI(i.staticLightmapUV, i. dynamicLightmapUV, i.vertexSH, inputData.normalWS);
#else
inputData.bakedGI = SAMPLE_GI(i.staticLightmapUV, i.vertexSH, inputData.normalWS);
#endif
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(i.vertex);
inputData.shadowMask = SAMPLE_SHADOWMASK(i.staticLightmapUV);
return inputData;
}
float4 frag(v2f i) : SV_TARGET
{
SurfaceData surfaceData = createSurfaceData(i);
InputData inputData = createInputData(i, surfaceData.normalTS);
return UniversalFragmentPBR(inputData, surfaceData);
}
ENDHLSL
}
}
FallBack "Diffuse"
}
PBR HLSL 코드 리뷰
무지막지하게 많은 양은 그만큼 배울게 많다는 뜻이니
심호흡부터 하자.. 이번엔 천천히 Properties부터 살펴볼 예정이다.
Properties
Properties
{
_MainTex ("Maint Texture", 2D) = "white" {}
_BaseColor("Base Color", Color) = (1, 1, 1, 1)
_MetallicTex ("Metallic Texture", 2D) = "white" {}
_MetallicStrength ("Metallic Strength", Range(0, 1)) = 0
_Smoothness("Smoothness", Range(0, 1)) = 0.5
_NormalTex("Normal Map", 2D) = "bump" {}
_NormalStrength("Normal Strength", float) = 1
[Toggle(USE_EMISSION_ON)] _EmissionOn("Use Emission Toggle", float) = 0
_EmissionTex("Emission Map", 2D) = "white" {}
[HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 0)
_AOTex("Ambient Occulsion Map", 2D) = "white" {}
}
사실 이 부분은 크게 설명할 건 없는거 같다.
용도에 맞게 이해하자.
Metallic / Smoothness / Normal Mapping / Emission / Ambient Occlusion 같은 PBR 요소들의 파라미터 값들이라 생각하자.
Emission 같은 경우에는 On/OFF 토글로 구현되어 있다.
지난 포스팅 중에서 Keywords에 다루었으니 이해가 가지 않으면, 다시 한번 살펴보자.
나는 이걸로 Shader Variants가 분기 되겠구나 ㅎㅎ 하고 생각했다.
https://lucid-boundary.tistory.com/133
13. Shader Keywords - 다니엘 릿 쉐이더 프로젝트
때로는 2개 이상의 서로 다른 모드(분기) 동작이 필요한 쉐이더도 있다.예를 들면, 다른 플랫폼에서 동작할 때는 다른 쉐이더 피쳐를 사용하게 하는 방식으로 말이다.경우에 따라서는 서로 다른
lucid-boundary.tistory.com
Precompiler, Keywords 선언부
#pragma vertex vert;
#pragma fragment frag;
#pragma multi_compile_local USE_EMISSION_ON __
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DYNAMICLIGHTMAP_ON
#pragma multi_compile _ DECLARE_LIGHTMAP_OR_SH
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
그 동안 안쓰던 친구들이 갑자기 늘어나서 당황스럽다..
- USE_EMISSION_ON
위에서 Emission (빛을 방출하는 오브젝트로 설정) 할 지 토글을 선언 했었다.
그에 대응하는 키워드이다.
- _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
오브젝트가 main light 로부터 상호작용하는 그림자들을 컨트롤하기 위한 키워드들이다.
- _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
오브젝트에 적용하는 Primary 라이트를 제외한 모든 빛들을 제어하기 위한 키워드들이다.
- _ADDITIONAL_LIGHT_SHADOWS
오브젝트에 작용하는 추가 빛들로부터 그림자를 제어하는 키워드이다.
*multi_compile_fragment의 뜻은 local보다 더 나아가서 Fragment Shader 단계에서만 해당 키워드를 사용하겠다는 의미이다.
- _REFLECTION_PROBE_BLENDING, _REFLECTION_PROBE_BOX_PROJECTION
2개의 probes 사이에서 오브젝트의 반사를 혼합하기 위한 키워드이다.
*Reflection Probe
Reflection Probe 는 주변의 스피리컬 뷰를 모든 방향에서 캡처하는 카메라와 유사하다. 캡처된 이미지는 반사 머티리얼이 있는 오브젝트에서 사용할 수 있는 Cubemap으로 저장된다. 특정 씬에는 여러 반사 프로브를 사용할 수 있고, 가장 가까운 프로브가 생성한 큐브맵을 사용하도록 오브젝트를 설정할 수 있다. 이렇게 하면 오브젝트의 반사가 주변 환경에 따라 매우 사실적으로 변할 수 있다.
- _SHADOWS_SOFT
그림자의 Edge를 부드럽게 하기 위해 사용한다. 그러면서 빛을 받는 부분과 그림자가 나타나는 부분을 좀 더 경계를 가른다.
- _SCREEN_SPACE_OCCLUSION
Screen Space에서 Ambient Occlusion을 사용 할 수 있게 한다. occlusion texture와는 별개다.
- LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK, DIRLIGHTMAP_COMBINED, LIGHTMAP_ON, DYNAMICLIGHTMAP_ON
라이트 맵핑과 관련있는 키워드들이다.
이런 인터넷에 검색해도 용도를 알기 힘든 키워드들은..
왠만하면, 인내심을 가지고 하나씩 테스트 해가면서 쉐이더 변화를 확인하는게 좋다..
appdata 구조체
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 staticLightmapUV : TEXCOORD1;
float2 dynamicLightmapUV : TEXCOORD2;
};
기본적으로 쓰일 Vertex, UV 정보외에
각 vertex의 Normal , Tangent 정보가 필요하다.
staticLightmapUV, dynamicLightmapUV 은 유니티에 의해 자동으로 생성되는 라이트맵핑 UV들이다.
v2f 구조체
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
float4 tangentWS : TEXCOORD3;
float3 viewDirWS : TEXCOORD4;
float4 shadowCoord : TEXCOORD5;
DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 6);
#ifdef DYNAMICLIGHTMAP_ON
float2 dynamicLightmapUV : TEXCOORD7;
#endif
};
Clip Space로 변환된 vertex, 그리고 UV외에 추가적인 정보들이 좀 많다.
position, normal, tangent, viewDirection 벡터들은 World Space 벡터들이다.
ShadowCoord는 그림자를 맵핑하기 위한 것이고,
dynamicLightmapUV, staticLightmapUV, vertexSH은 추가적인 라이트 맵핑 UV들이다.
DECLARE_LIGHTMAP_OR_SH은 Light.hlsl 에서 제공되는 매크로이다.
어떤 인터폴레이터로 사용할 지 결정하도록 도와준다.
안에 파라미터로 staticLightmapUV, vertexSH, 6이 들어가있다.
인터넷에 검색해도 지독히도 안나오는데..
의미는 다음과 같다.
Lightmap 변수와 SH (구체 조화 함수) 변수를 선언하여 현재 쉐이더에 맞는 정보를 TEXCOORD6 UV 채널로 할당한다는 뜻이다.
이제 빌드 환경에 따라 Lightmap 변수에 TEXCOORD6 UV가 할당될 지, SH 변수에 할당 될지를 자동으로 분기한다.
이 새끼 때문에 컴파일 에러도 나고, 문제 해결 하느라 죽썼다.
Vert 함수
v2f vert(appdata v)
{
v2f o;
VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(v.normalOS, v.tangentOS);
o.positionWS = vertexInput.positionWS;
o.vertex = vertexInput.positionCS;
// o.vertex = TransformObjectToHClip(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.normalWS = normalInput.normalWS;
float sign = v.tangentOS.w;
o.tangentWS = float4(normalInput.tangentWS.xyz, sign);
o.viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
o.shadowCoord = GetShadowCoord(vertexInput);
OUTPUT_LIGHTMAP_UV(v.staticLightmapUV, unity_LightmapST, o.staticLightmapUV);
#ifdef DYNAMICLIGHTMAP_ON
v.dynamicLightmapUV = v.dynamicLightmapUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
OUTPUT_SH(o.normalWS.xyz, o.vertexSH);
return o;
}
VertexPositionInputs, VertexNormalInputs 생소한 이름의 타입들이 있다.
VertexPositionInputs GetVertexPositionInputs(float3 positionOS)
{
VertexPositionInputs input;
input.positionWS = TransformObjectToWorld(positionOS);
input.positionVS = TransformWorldToView(input.positionWS);
input.positionCS = TransformWorldToHClip(input.positionWS);
float4 ndc = input.positionCS * 0.5f;
input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
input.positionNDC.zw = input.positionCS.zw;
return input;
}
VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS)
{
VertexNormalInputs tbn;
// mikkts space compliant. only normalize when extracting normal at frag.
real sign = real(tangentOS.w) * GetOddNegativeScale();
tbn.normalWS = TransformObjectToWorldNormal(normalOS);
tbn.tangentWS = real3(TransformObjectToWorldDir(tangentOS.xyz));
tbn.bitangentWS = real3(cross(tbn.normalWS, float3(tbn.tangentWS))) * sign;
return tbn;
}
일반적인 Shader에서 쓰던 함수를 해당 structure로 변환하여 반환하는 간단한 함수들이다.
GetVertexNormalInputs는 변수 명칭과 구조체 변수들을 보아하니 Tangent Space와 관련된 함수임을 알 수 있다.
Normal Mapping에서 사용될 값들임을 알 수 있다.
다시 Vert로 돌아와서..
월드 스페이스의 포지션, 클립 스페이스의 포지션, UV까지 가져오고
normalWS에 월드 스페이스의 노말 벡터를 넣어주고 있다.
Normal Mapping에 쓰일 tangentWS
관찰자 벡터 viewDirWS도 구해주고 있다.
OUTPUT_LIGHTMAP_UV(v.staticLightmapUV, unity_LightmapST, o.staticLightmapUV);
OUTPUT_LIGHTMAP_UV는 Lightmap UV 좌표를 vertex shader output으로 전달하는 역할을 한다. 이 매크로는 라이트맵을 사용하는 환경에서 올바른 조명 정보를 픽셀 셰이더로 전달하기 위해 필요하다.
일종의 라이트맵을 위한 UV 맵핑으로 생각해보자. 라이트맵을 텍스처링 하기 위한 준비라고 생각하면 된다.
unity_LightmapST는 유니티에서 제공하는 스케일 및 오프셋 정보이다.
OUTPUT_SH 매크로는 말그대로 구체 조화로 변환하는 매크로이다.
입력 파라미터도 마침 o.normalWS.xyz, o.vertexSH 를 입력으로 받고 있다.
*이런 부분은 Phong Shading 할 때와는 달리 vertex에서 처리하는거 같은데..
큰 의미가 없는걸까..?
Frag 함수
복잡한 SurfaceData, InputData 생성문은 결국 Fragment Shader에서 사용할 값들을 계산하는 과정이다.
빠르게 Frag를 훑어보고 다루겠다.
float4 frag(v2f i) : SV_TARGET
{
SurfaceData surfaceData = createSurfaceData(i);
InputData inputData = createInputData(i, surfaceData.normalTS);
return UniversalFragmentPBR(inputData, surfaceData);
}
SurfaceData 와 InputData는 유니티 내부 쉐이더에서 정의된 구조체들이다.
UniversalFragmentPBR의 파라미터들이 정확히 위의 두 데이터를 요구한다.
최종 결과를 위해서는 두 구조체를 만드는 과정이 필요하다는 의미이다.
함수 이름이 createSurfaceData, createInputData 이다.
어떤식으로 두 구조체를 생성하는 걸까?
두 구조체는 일단은 다음과 같이 정의되어 있다.
익숙한 변수명들로 선언되어 있다.
struct SurfaceData
{
half3 albedo;
half3 specular;
half3 metallic;
half3 smoothness;
half3 normalTS;
half3 emission;
half3 occlusion;
half3 alpha;
half3 clearCoatMask;
half3 clearCoatSmoothness;
};
struct InputData
{
float3 positionWS;
float4 positionCS;
half3 normalWS;
half3 viewDirectionWS;
float4 shadowCoord;
half fogCoord;
half3 vertexLighting;
half3 bakedGI;
float2 normalizedScreenSpaceUV;
half4 shadowMask;
half3x3 tangentToWorld;
};
createSurfaceData 함수
SurfaceData createSurfaceData(v2f i)
{
SurfaceData surfaceData = (SurfaceData)0;
// Albedo ouput
float4 albedoSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
surfaceData.albedo = albedoSample.rgb * _BaseColor.rgb;
// Metallic output
float4 metallicSample = SAMPLE_TEXTURE2D(_MetallicTex, sampler_MetallicTex, i.uv);
surfaceData.metallic = metallicSample * _MetallicsStrength;
// smoothness output
surfaceData.smoothness = _Smoothness;
// normal output
float3 normalSample = UnpackNormal(SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, i.uv));
normalSample.rg *= _NormalStrength;
surfaceData.normalTS = normalSample;
// emission output
#if USE_EMISSION_ON
surfaceData.emission = SAMPLE_TEXTURE2D(_EmissionTex, sampler_EmissionTex, i.uv);
#endif
// ambient occlusion output
float4 aoSample = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex, i.uv);
surfaceData.occlusion = aoSample.r;
// alpha output
surfaceData.alpha = albedoSample.a * _BaseColor.a;
return surfaceData;
}
리턴 값을 SurfaceData로 하고 있고, 첫줄부터 비어있는 구조체를 선언한 것으로 보아
함수의 프로세스는 이 SurfaceData를 채우는걸 목표로 하고 있음을 알 수 있다.
SurfaceData는 무언가 처리된 데이터라기 보다는 프로세스 전에 미리 값들을 준비하는 단계이다.
(1) Albedo Output 부분
메인 텍스처와 UV를 이용해 텍스처를 하나 샘플링해서 abledoSample로 초기화 했다.
여기에 단순하게 _BaseColor를 곱해줘서
라이트맵의 순수 색상을 결정한다.
Base Color가 적용된 Albedo는 SurfaceData에 넣는다.
(2) Metallic Output 부분
이번에는 Metallic Texture를 샘플링한 다음
금속성 정도를 곱해줘서 SurfaceData에 넣어준다.
(3)Smoothness Output 부분
이 값은 0 에서 1 사이의 값을 SurfaceData에 넣어준다.
(4) Normal Output 부분
UnpackNormal 함수는 float4로 샘플링된 노말 텍스처를 노말 벡터화 시켜준다.
나중에 노말 맵핑때 사용할 값이다.
(5) Emission Output 부분
Emission이 키워드 토글에 의해 켜져있다면,
Emission 텍스처를 샘플링하여 SurfaceData에 넣어준다.
(6) Ambient Occlusion Output 부분
마찬가지로 샘플링된 값을 Surface Data로 넣어준다.
(7) Alpha Output 부분
Surface의 알파값을 채워준다. 이 때의 값은
Albedo 샘플 결과의 알파값과 베이스 컬러의 알파값의 곱이다.
createInputData 함수
InputData createInputData(v2f i, float3 normalTS)
{
InputData inputData = (InputData)0;
// Position input.
inputData.positionWS = i.positionWS;
// Normal Input
float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz);
inputData.tangentToWorld = float3x3(i.tangentWS.xyz, bitangent, i.normalWS);
inputData.normalWS = TransformTangentToWorld(normalTS, inputData.tangentToWorld);
inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
// View Direction Input.
inputData.viewDirectionWS = SafeNormalize(i.viewDirWS);
// Shadow Coords.
inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
//Baked lightmaps.
#if defined(DYNAMICLIGHTMAP_ON)
inputData.bakedGI = SAMPLE_GI(i.staticLightmapUV, i. dynamicLightmapUV, i.vertexSH, inputData.normalWS);
#else
inputData.bakedGI = SAMPLE_GI(i.staticLightmapUV, i.vertexSH, inputData.normalWS);
#endif
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(i.vertex);
inputData.shadowMask = SAMPLE_SHADOWMASK(i.staticLightmapUV);
return inputData;
}
createSurfaceData와 마찬가지로, InputData를 채우는게 목표인 함수이다.
코드구조를 살펴보자.
(1) Position Input
오브젝트 Vertics의 월드 스페이스 좌표를 InputData에 넣어준다.
(2) Normal Input
bitangent는 Tangent 값과 Normal 값의 외적을 통해 구할 수 있다.
float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz); 에서
tangentWS의 w 값은 좌표계의 방향을 표현한 것이다.
유니티에서 일반적인 바이탄젠트는 오른손 좌표계를 기준으로 하기 때문이다.
Tangent To World 를 만족하기 위해선, 결국은 Tangent Sapce를 기반으로 만들어야 한다.
월드 스페이스의 탄젠트 값, 바이탄젠트, 노말값을 InputData에 넣어준다.
PBR에서 normalWS는 노말 맵핑을 위한 값이다.
그렇기 때문에 TangentSapce의 노말 벡터를 가져와서 TransformTangentToWorld 함수에 넣어준다.
*PBR 이전 포스팅에서 Normal 벡터가 라이팅 계산에서 쓰이는걸 감안하면... 혼동 가능성이 있겠다..
(3) View Direction Input
SafeNormalize는 스케일에 따라 값이 바뀔 수 있음을 감안해서 노말라이즈 하는 함수이다.
그대로 InputData로 넣어준다.
(4) Shadow Coords
월드 스페이스의 좌표를 ShadowCoord로 변환하는 함수로 Input Data에 넣어준다.
(5) Baked Lightmaps
GI는 Global Illumination의 약자이다.
키워드 설명 부분에서 DECLARE_LIGHTMAP_OR_SH이 기억나는가?
똑같은 원리의 매크로이다.
SAMPLE_GI는 빌드 설정에 따라 파라미터를 받고, 전역 조명 정보를 샘플링하는 매크로인데
입력 값으로 staticLightMapUV, vertexSH를 받아서 둘 중 하나를 빌드 설정에 따라 TEXCOORD6 값을 가져와서 normalWS에 따라 값을 반환한다.
SAMPLE_SHADOWMASK는 실시간 조명과 베이크된 값을 혼합할 때, Shadow mask를 세팅한다.
마치며
PBR에 대한 설명은 이걸로 끝이다.
기존의 Lighting 부분과 설명이 혼동되어 더욱 어렵게 느껴지는 것 같다.
그래도 파트가 완전히 끝나진 않았다.
Shadow Casting 설명으로 찾아뵙겠다.
'Game Dev > Unity Shader' 카테고리의 다른 글
| 19. Image Effects, Post-Processing Effects - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.24 |
|---|---|
| 18. Shadow Casting - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.21 |
| 16. Flat Shading, Gouraud Shading, Phong Shading - 다니엘 릿 쉐이더 프로젝트 (1) | 2025.10.19 |
| 15. Lighting And Shadow - 다니엘 릿 쉐이더 프로젝트 (1) | 2025.10.18 |
| 14. UsePass, GrapPass - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.18 |