
Universal Render Pipeline에 내장된 Lit 쉐이더를 커스텀 해봅시다.
Lit 관련 쉐이더 파일 구조를 알아볼겸 URP 패키지 안에서 기존 Lit 쉐이더를 수정하겠습니다.
예시로 하프램버트 기능을 추가해보겠습니다.
1. URP 폴더 옮기기
우선 'Library\PackageCache' 경로에서 아래와 같은
'com.unity.render-pipelines.universal@x.x.x'
폴더를 찾아 'Packages' 폴더에 옮겨줍니다.
그리고 프로젝트로 돌아와 패키지매니저를 열어보면 'Custom' 이라는 마크가 떠있을겁니다.
이제 URP 폴더를 수정해도 원래대로 돌아오지 않을것입니다.
2.관련 파일들
Lit 쉐이더를 수정하기 위한 관련 파일 리스트는 아래와 같습니다.
(URP 패키지 폴더 기준입니다.)
외부 (프로퍼티, Pass List, GUI)
'Shaders/Lit.shader'
'Shaders/LitInput.hlsl'
'Editor/ShaderGUI/Shaders/LitShader.cs'
'Editor/ShaderGUI/ShadingModels/LitGUI.cs'
내부 (조명계산 및 실제 구현)
'Shaders/LitForwardPass.hlsl'
'ShaderLibrary/Lighting.hlsl'
3.프로퍼티 추가
'Lit.shader'를 열고 프로퍼티를 추가해 줍니다.
Properties
{
....
...
..
_DiffPer("Diffuse Percent", Range(0.01, 1.0)) = 1
}
그리고 SRP Batcher를 위해 CBUFFER 에 등록해줍니다.
'LitInput.hlsl'을 열어 아래와 같이 추가합니다.
CBUFFER_START(UnityPerMaterial)
....
...
..
half _DiffPer;
CBUFFER_END
(그리고 아래쪽으로 내려보면 프로퍼티를 Dot Instancing과 Meta에 등록 하는 매크로가 있는데 여기서는 생략합니다.)
4.GUI 수정
일반적으로 작성된 쉐이더라면 프로퍼티를 추가하는것 만으로 인스팩터에 표시가 되지만
URP에 내장된 Lit 쉐이더는 ShaderGUI 와 연결되어 있습니다.
'Lit.shader' 파일을 열어 가장 아래쪽을 보면
CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.LitShader"
이렇게 LitShader 라는 ShaderGUI와 연결되어 있는걸 볼 수 있습니다.
'LitShader.cs'를 열어 보면 BaseShaderGUI를 상속 받은걸 볼 수 있고,
DrawSurfaceOptions, DrawSurfaceInputs, DrawAdvancedOptions 이라는
함수가 오버라이드 되어 있는걸 볼 수 있습니다.
아래 Universal Render Pipeline/Lit 머터리얼 Inspector를 보면
함수명과 머터리얼에 표시된 그룹명이 매칭되어 있는걸 짐작 할 수 있습니다.
쉐이더에 추가한 _DiffPer를 표시하기 위해 'LitGUI.cs'를 열어 줍니다.
그리고 아래와 같이 스타일 GUIContent와 쉐이더 프로퍼티와 연결할 MaterialProperty를 추가해줍니다.
public static class Styles
{
....
...
..
public static GUIContent diffPercent = EditorGUIUtility.TrTextContent("Diffuse Percent",
"If you want to adjust Diffuse Percent, adjust it.");
}
public struct LitProperties
{
// Advanced Props
....
...
..
public MaterialProperty diffPer;
public LitProperties(MaterialProperty[] properties)
{
....
...
..
diffPer = BaseShaderGUI.FindProperty("_DiffPer", properties, false);
}
}
Syles는 프로퍼티 스트링 캡션과 툴팁을 표시하기 위해 추가한것입니다.
실제 쉐이더 프로퍼티와 연결할 프로퍼티는 MaterialProperty 입니다.
Styles 아래부분에 LitProperties에 추가해줍니다.
이렇게 하면 일단 GUI의 머터리얼 프로퍼티와 쉐이더의 프로퍼티가 연결 되었습니다.
이제 인스펙터에서 AdvanceOption에 출력을 위해 'LitShader.cs' 를 열어줍니다.
public override void DrawAdvancedOptions(Material material)
{
....
...
..
if (litProperties.diffPer != null)
{
materialEditor.ShaderProperty(litProperties.diffPer, LitGUI.Styles.diffPercent);
}
}
에디터로 돌아가서 인스팩터를 보면 비로서 프로퍼티가 추가되어 있는걸 볼 수 있습니다.
5. 기능 구현
프로퍼티를 추가하고 인스팩터에 나오게 했으니 실제로 동작하게 해야합니다.
하프램버트 기능을 추가하기 위해서 'LitForwardPass.hlsl' 파일을 열어줍니다.
Lit 쉐이더에서 Fragment 함수로 연결되어 있는 LitPassFragment 를 찾습니다.
그리고 UniversalFragmentPBR를 호출한 부분에 _Custom을 붙여줍니다.
half4 LitPassFragment(Varyings input) : SV_Target
{
....
...
..
// half4 color = UniversalFragmentPBR(inputData, surfaceData);
half4 color = UniversalFragmentPBR_Custom(inputData, surfaceData);
....
...
}
UniversalFragmentPBR 는
'Lighting.cs'에 구현되어 있는걸 볼 수 있습니다.
UniversalFragmentPBR 함수를 Lighting.cs에서 긁어와
LitForwardPass.hlsl에 붙여 넣기합니다. (새로운 파일은 다음 강좌에서..)
그리고 함수 이름에 _Custom 만 붙여줍니다.
아래는 UniversalFragmentPBR 함수를 긁어와서 이름만 바꾼 상태입니다.
////////////////////////////////////////////////////////////////////////////////
/// PBR lighting...
////////////////////////////////////////////////////////////////////////////////
half4 UniversalFragmentPBR_Custom(InputData inputData, SurfaceData surfaceData)
{
#if defined(_SPECULARHIGHLIGHTS_OFF)
bool specularHighlightsOff = true;
#else
bool specularHighlightsOff = false;
#endif
BRDFData brdfData;
// NOTE: can modify "surfaceData"...
InitializeBRDFData(surfaceData, brdfData);
#if defined(DEBUG_DISPLAY)
half4 debugColor;
if (CanDebugOverrideOutputColor(inputData, surfaceData, brdfData, debugColor))
{
return debugColor;
}
#endif
// Clear-coat calculation...
BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
half4 shadowMask = CalculateShadowMask(inputData);
AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
uint meshRenderingLayers = GetMeshRenderingLightLayer();
Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);
// NOTE: We don't apply AO to the GI here because it's done in the lighting calculation below...
MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);
LightingData lightingData = CreateLightingData(inputData, surfaceData);
lightingData.giColor = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
inputData.normalWS, inputData.viewDirectionWS);
if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
{
lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat,
mainLight,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
#if defined(_ADDITIONAL_LIGHTS)
uint pixelLightCount = GetAdditionalLightsCount();
#if USE_CLUSTERED_LIGHTING
for (uint lightIndex = 0; lightIndex < min(_AdditionalLightsDirectionalCount, MAX_VISIBLE_LIGHTS); lightIndex++)
{
Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
{
lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
}
#endif
LIGHT_LOOP_BEGIN(pixelLightCount)
Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
{
lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
LIGHT_LOOP_END
#endif
#if defined(_ADDITIONAL_LIGHTS_VERTEX)
lightingData.vertexLightingColor += inputData.vertexLighting * brdfData.diffuse;
#endif
return CalculateFinalColor(lightingData, surfaceData.alpha);
}
에디터로 돌아와서 기존 상태와 변함이 없는지 확인합니다.
이번에 다시 같은 방법으로 'Lighting.cs'에서 LightingPhysicallyBased 함수를 긁어 옵니다.
그리고 마찬가지로 함수이름 뒤에 _Custom을 붙입니다.
half3 LightingPhysicallyBased_Custom(BRDFData brdfData, BRDFData brdfDataClearCoat,
half3 lightColor, half3 lightDirectionWS, half lightAttenuation,
half3 normalWS, half3 viewDirectionWS,
half clearCoatMask, bool specularHighlightsOff)
{
half NdotL = saturate(dot(normalWS, lightDirectionWS));
half3 radiance = lightColor * (lightAttenuation * NdotL);
half3 brdf = brdfData.diffuse;
#ifndef _SPECULARHIGHLIGHTS_OFF
[branch] if (!specularHighlightsOff)
{
brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);
#if defined(_CLEARCOAT) || defined(_CLEARCOATMAP)
// Clear coat evaluates the specular a second timw and has some common terms with the base specular.
// We rely on the compiler to merge these and compute them only once.
half brdfCoat = kDielectricSpec.r * DirectBRDFSpecular(brdfDataClearCoat, normalWS, lightDirectionWS, viewDirectionWS);
// Mix clear coat and base layer using khronos glTF recommended formula
// https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md
// Use NoV for direct too instead of LoH as an optimization (NoV is light invariant).
half NoV = saturate(dot(normalWS, viewDirectionWS));
// Use slightly simpler fresnelTerm (Pow4 vs Pow5) as a small optimization.
// It is matching fresnel used in the GI/Env, so should produce a consistent clear coat blend (env vs. direct)
half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * Pow4(1.0 - NoV);
brdf = brdf * (1.0 - clearCoatMask * coatFresnel) + brdfCoat * clearCoatMask;
#endif // _CLEARCOAT
}
#endif // _SPECULARHIGHLIGHTS_OFF
return brdf * radiance;
}
half3 LightingPhysicallyBased_Custom(BRDFData brdfData, BRDFData brdfDataClearCoat, Light light, half3 normalWS, half3 viewDirectionWS, half clearCoatMask, bool specularHighlightsOff)
{
return LightingPhysicallyBased_Custom(brdfData, brdfDataClearCoat,
light.color, light.direction, light.distanceAttenuation * light.shadowAttenuation,
normalWS, viewDirectionWS, clearCoatMask, specularHighlightsOff);
}
그리고 좀전에 추가한 UniversalFragmentPBR_Custom 함수 내에 LightingPhysicallyBased를 호출하는 부분을
LightingPhysicallyBased_Custom으로 변경해 줍니다.
if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
{
lightingData.mainLightColor = LightingPhysicallyBased_Custom(brdfData, brdfDataClearCoat,
mainLight,
inputData.normalWS, inputData.viewDirectionWS,
surfaceData.clearCoatMask, specularHighlightsOff);
}
에디터로 돌아와 이상이 없는지 확인합니다.
그럼 이제 준비과정이 끝났습니다.
LightingPhysicallyBased_Custom에 하프램버트 기능을 추가해줍니다.
NdotL을 구하는 부분을 아래와 같이 수정합니다.
//half NdotL = saturate(dot(normalWS, lightDirectionWS));
half diffPer2 = 1.0 - _DiffPer;
half NdotL = saturate(dot(normalWS, lightDirectionWS)) * _DiffPer + diffPer2;
에디터로 돌아와 _DiffPer 값을 조절하여 하프램버트 기능이 작동되는지 확인해봅니다.
'0.5'일때 하프가 되겠지만 프로퍼티로 빼면 음영을 조절할 수 있게 되는것입니다.
이런 하프램버트는 음영을 아티스트 손맵에 의존하는 오브젝트 표현에 도움이 될것입니다.
혹시 NdotL에 대해 궁금하시면 '그래픽스 - NDotL' 강좌를 봐주세요.
https://goldryul.tistory.com/11
그럼 안녕~
