지난 Lit 커스텀 강좌에서는 URP 패키지 전체를 복사하여
URP 내부에서 Lit 쉐이더를 수정했습니다.
이번에는 관련 파일들만 가져와 Lit 쉐이더 클론에 기능을
추가하는 예시를 만들어 보도록 하겠습니다.
파일명이나 클래스명은 똑같이 따라 안하셔도 됩니다.
(하지만 일단 따라하시는걸 추천드립니다. ^^;)
1. 관련파일 가져오기
URP 환경이 갖추어진 프로젝트라고 가정합니다.
Assets 하위에 폴더를 하나 만듭니다.
저는 URP Core 프로젝트를 새로 만들고
'URPLit'이라는 폴더를 만들겠습니다.
그리고 하위에 'Editor', 'HLSL' 폴더도 만들어 두겠습니다.
탐색기를 열어 URP 프로젝트 폴더로 이동합니다.
.\Library\PackageCache\com.unity.render-pipelines.universal@x.x.x
'Editor/ShaderGUI/Shaders/LitShader.cs'
'Editor/ShaderGUI/ShadingModels/LitGUI.cs'
'Editor/ShaderGUI/ShadingModels/LitDetailGUI.cs'
'Editor/ShaderGraph/UniversalProperties.cs'
위 파일들을 만든 'URPLit/Editor'에 복사합니다.
파일이름 Lit다음에 '_Custom'을 붙여서 리네임 합니다.
'UniversalProperties.cs'은 그대로 두도록 합니다.
(internal static class라 그대로 두어도 됩니다.)
이제 쉐이더를 가져옵니다.
'Shaders/LitForwardPass.hlsl'
'Shaders/Lit.shader'
'Shaders/LitInput.hlsl'
'Shaders/ShadowCasterPass.hlsl.'
위 파일중 Lit.shader는 'URPLit'에 복사하고,
나머지 hlsl은 'HLSL' 폴더에 복사하도록 합니다.
마찬가지로 파일이름 Lit다음에 'Custom'을 붙입니다.
'ShadowCasterPass.hlsl'은 그대로 두도록 합니다.
(ShadowCaster는 그대로 사용하겠습니다)
2. 파일수정
먼저 클래스 이름을 파일이름과 동일하게 다 바꾸어 줍니다.
public static class Lit_GUI ->
public static class Lit_CustomGUI
public static class Lit_DetailGUI ->
public static class Lit_CustomDetailGUI
public static class Lit_Shader ->
public static class Lit_CustomShader
Lit_CustomShader 클래스의 파일을 열어
class 'Lit_GUI', 'Lit_DetailGUI'를 바꾼
'Lit_CustomGUI', 'Lit_CustomDetailGUI'로 바꿔 줍니다.
internal class Lit_CustomShader : BaseShaderGUI
{
// 아래 예시 처럼 찾아서 바꿔 주세요
static readonly string[] workflowModeNames = Enum.GetNames(typeof(Lit_CustomGUI.WorkflowMode));
private Lit_CustomGUI.LitProperties litProperties;
private Lit_CustomDetailGUI.LitProperties litDetailProperties;
....
...
..
이번에는 쉐이더를 수정합니다.
'Lit_Custom.shader'를 열어 먼저 쉐이더 명을 수정합니다.
Shader "Universal Render Pipeline/Lit_Custom"
{
....
...
ForwardLit Pass에 Include를 수정해 줍니다.
Pass
{
// Lightmode matches the ShaderPassName set in UniversalRenderPipeline.cs. SRPDefaultUnlit and passes with
// no LightMode tag are also rendered by Universal Render Pipeline
Name "ForwardLit"
Tags{"LightMode" = "UniversalForward"}
....
...
..
#include "HLSL/Lit_Custom_Input.hlsl"
#include "HLSL/Lit_Custom_ForwardPass.hlsl"
}
ShadowCaster Pass에 Include도 마찬가지로 수정해줍니다.
Pass
{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}
....
...
..
#include "HLSL/Lit_Custom_Input.hlsl"
#include "HLSL/ShadowCasterPass.hlsl"
ENDHLSL
}
GBuffer, DepthOnly, DepthNormals, Meta, Universal2D Pass는
삭제하셔도 되고 일단 두셔도 됩니다.
그리고 아래 쪽에 ShaderModel 2.0 SubShader에도 똑같이 수정해줍니다.
마지막으로 가장 아래 쪽에 CustomEditor를 Lit_CustomShader 로 변경해줍니다.
CustomEditor "UnityEditor.Rendering.Universal.ShaderGUI.Lit_CustomShader"
}
에디터로 돌아와 기본 도형을 만들고 머터리얼을 생성후
Lit_Custom 쉐이더를 적용해 봅니다.
기존 Lit과 동일하게 동작하면 기본작업이 끝난것입니다.
위 해당 작업이 끝난후 'URPLit' 폴더를 백업해두고 템플릿 처럼 사용하면
추후 Lit 베이스 쉐이더 작업하기가 수월합니다.
3. 프로퍼티 추가
아래 부터는 기존 LitCustom 강좌와 내용이 거의 동일합니다.
'Lit_Custom.shader'를 열고 프로퍼티를 추가해 줍니다.
Properties
{
....
...
..
_DiffPer("Diffuse Percent", Range(0.01, 1.0)) = 1
}
그리고 SRP Batcher를 위해 CBUFFER 에 등록해줍니다.
'Lit_Custom_Input.hlsl'을 열어 아래와 같이 추가합니다.
CBUFFER_START(UnityPerMaterial)
....
...
..
half _DiffPer;
CBUFFER_END
(그리고 아래쪽으로 내려보면 프로퍼티를 Dot Instancing과 Meta에 등록 하는 매크로가 있는데 여기서는 생략합니다.)
쉐이더에 추가한 _DiffPer를 표시하기 위해 'LitCustomGUI.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에 출력을 위해 'LitCustomShader.cs' 를 열어줍니다.
public override void DrawAdvancedOptions(Material material)
{
....
...
..
if (litProperties.diffPer != null)
{
materialEditor.ShaderProperty(litProperties.diffPer, Lit_CustomGUI.Styles.diffPercent);
}
}
에디터로 돌아가서 인스팩터를 보면 비로서 프로퍼티가 추가되어 있는걸 볼 수 있습니다.
4. 기능구현
프로퍼티를 추가하고 인스팩터에 나오게 했으니 실제로 동작하게 해야합니다.
하프램버트 기능을 추가하기 위해서 'LitCustomForwardPass.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);
}
LitPassFragment 함수 위쪽에 붙여 넣으셔야 합니다.
에디터로 돌아와서 기존 상태와 변함이 없는지 확인합니다.
이번에 다시 같은 방법으로 '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 함수 위쪽에 붙여넣으셔야 합니다.)
그리고 좀전에 추가한 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
이번 강좌의 작업한 소스도 같이 공유해드립니다.
참고하셔서 작업해보세요.
그럼 안녕~
