일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- URP
- Rim Light
- Windows Build
- 가상 바이트
- 게임 수학
- C언어
- 프로그래밍 기초
- Toon Shader
- Virtual Byte
- Specular
- ColorGradingLutPass
- 벡터
- Cell Shader
- Cartoon Rendering
- AppSW
- URP로 변경
- Three(Two) Tone Shading
- 개인 바이트
- VR
- Private Bytes
- OculusMotionVectorPass
- 메모리 누수
- Cell Look
- 작업 집합
- ASW(Application SpaceWarp)
- working set
- 3d
- Today
- Total
WinCNT
URP에서 Additional Lights(Point Light, Spot Light) 적용해보기! 본문
URP에서 Additional Lights(Point Light, Spot Light) 적용해보기!
WinCNT_SSS 2024. 3. 6. 10:15서론
현재 프로젝트는 배경에 대해서는 Only Light Map이기 때문에, 광원도 Directional Light만 존재하고 Additional Lights에 대해서 딱히 고려하고 있지 않았다
하지만 슬슬 PV를 만들 때가 됐을 때, Directional Light만 있으면 작업이 힘들어지는데 퀄리티도 떨어진다는 이야기를 들었다
그렇다면? 아예 PV용 리얼 타임 배경 셰이더를 새롭게 하나 만들면 되지 않을까?!
퍼포먼스를 신경 안 써도 된다니 최고다!!!!
그렇게 Additional Lights(Point Light, Spot Light)가 적용되는 PV용 리얼 타임 배경 셰이더를 만들게 되었다
이런 건 우선 디폴트 Lit를 확인해야…어? 왜 안 됨?
예전에 URP의 Lit에서 Additional Lights 관한 처리를 본 적이 있었기 때문에 우선 적용해봤다
근데 매우 놀랍게도! 이게 안 되네…
역시 외부의 설정이 원인
이런 기본적인 게 유니티에서 안 될 리가 없으니 URP의 더미 프로젝트를 만들어서 디폴트 셰이더로 확인했는데 문제 없이 잘 작동했다
왜긴 왜야! 이런 경우에는 대부분 뭔가 설정이 원인이지!
이리저리 확인해보니 역시 Project Setting…의 설정이 문제였다
정확히는 Project Setting > Quality에 설정된 URP Asset이 원인이었다
Quality에 설정된 URP Asset를 살펴보니 Addtional Lights가 Disabled로 되어 있었다
이러니 안 됐지…
💡 Graphics의 URP Asset에도 Addtional Lights를 설정할 수는 있지만 결국Quality에 있는 URP Asset의 설정으로 오버라이드되기 때문에 주의하자!
예를 들어 Graphics에 설정된 URP Asset의 Addtional Lights가 Disabled가 아니여도, Quality에 설정된 URP Asset의 Addtional Lights가 Disabled라면 작동하지 않는다
URP Asset의 Lighting의 Additional Lights 부분을 적당히 변경해줬다
(이번엔 PV용이므로 퍼포먼스 따위는 신경 안 써도 된다! 오예!)
이러면 디폴트 Lit은 잘 작동하는 걸 확인할 수 있었다
커스텀 셰이더에 Additional Lights 관련 코드 작성하기
URP의 디폴트 Lit 셰이더에서 동작하는 걸 확인했으니 이제 거기서 필요한 부분을 가져와서 커스텀 셰이더에 적용해보자!
가장 먼저 Additional Lights 관련 키워드를 추가해준다
HLSLPROGRAM
// 생략
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
// 생략
ENDHLSL
이 키워드들은 URP Asset의 Addtional Lights에 연동하는 키워드들이다
사실 Addtional Lighting 처리는 커스텀 셰이더의 프래그먼트 셰이더에 구현할 거라 직접적인 연관은 없지만, 어떤 라이브러리에 영향을 줄 지 모르니 얌전히 추가해줬다
다음은 프래그먼트 셰이더에서 Additional Lighting에 관한 처리를 추가해야 한다
이번에는 다음과 같이 URP의 Lit의 방식을 최대한 차용하기로 했다
uint pixelLightCount = GetAdditionalLightsCount();
LIGHT_LOOP_BEGIN(pixelLightCount)
// Additional Lighting 관련 처리
LIGHT_LOOP_END
참고로 LIGHT_LOOP_BEGIN하고 LIGHT_LOOP_END는 ShaderLibrary/Lighting.hlsl > ShaderLibrary/RealtimeLights.hlsl에 다음과 같이 정의되어 있다
#define LIGHT_LOOP_BEGIN(lightCount) \\
for (uint lightIndex = 0u; lightIndex < lightCount; ++lightIndex) {
#define LIGHT_LOOP_END }
즉 GetAdditionalLightsCount()로 Additional Lights의 개수를 구하고, LIGHT_LOOP_BEGIN와 LIGHT_LOOP_END로 그만큼 루프하면서 Additional Lighting를 하는 방식이다
역시나 루프가 답인가?
아래는 그 샘플 코드이다
실제 Additional Lighting는 #if defined(_ADDITIONAL_LIGHTS)부터 #endif까지이다
float3 light_col = SAMPLE_GI(i.lightmapUV, i.vertexSH, normal);
#if !defined(LIGHTMAP_ON)
float ld = dot(_MainLightPosition.xyz, normal);
Light mainLight = GetMainLight();
light_col.rgb += mainLight.color * mainLight.distanceAttenuation * max(0, ld);
#if defined(_ADDITIONAL_LIGHTS)
uint pixelLightCount = GetAdditionalLightsCount();
float3 additionalLightsColor = float4(0,0,0,0);
float3 light_dir = float3(0,0,0);
float3 lCol;
LIGHT_LOOP_BEGIN(pixelLightCount)
const Light light = GetAdditionalLight(lightIndex, i.worldPos);
half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
half3 lightDiffuseColor = LightingLambert(attenuatedLightColor, light.direction, normal);
additionalLightsColor += color * lightDiffuseColor;
LIGHT_LOOP_END
#endif
#endif
color = color * light_col;
#if defined(_ADDITIONAL_LIGHTS)
color += additionalLightsColor;
#endif
GetAdditionalLight()로 Additional Light의 데이터를 얻어와서 감쇠가 적용된 Light를 구하고 오브젝트의 Albedo(코드에선 color)와 곱한 값들을 누적한다
그리고 Main Light * Alebo의 값에 누적한 Additional Light의 값을 더하면 완성!
어? 그러고보니 그림자가 없네…
처음에는 이걸로 끝인 줄 알았으나…중요한 부분이 빠져있었다…
그건 바로 그림자!
PV에는 라이트 맵을 안 쓸 예정이다보니, 그림자가 없어서 씬이 너무 붕 떠보이는 느낌이 들었다
게다가 더 큰 문제가 있었으니…
그림자 관련 처리가 없으면 위와 같이 Lighting이 오브젝트를 관통해버리는 현상이 발생한다!!
그런고로 이어서 그림자도 추가해보자!
그림자를 구현에 앞서 사전 지식 정리하기
…라고 말하긴 했지만 필자도 그림자에 대해서 잘 모른단 말이지…
그래서 조사해봤고 대충 알게 된 내용들을 정리해봤다
우선 그림자 구현은 다음의 2가지 측면으로 접근해야 한다
- 그림자를 드리우는 것(Cast Shadows)
- 그림자를 받는 것(Receive Shadows)
Cast Shadows를 위해서는 광원에서 바라본 Depth 맵인 Shadow Map이란 것이 필요하다
Shadow Map 생성은 유니티가 알아서 잘 만들어주니, 필자가 할 것은 머티리얼이 Shadow Map에 반영되도록 하는 일이 필요할 것이다
Receive Shadows를 위해서는 Shadow Map을 이용해서 얻은 Light의 그림자 감쇠율이 필요할 것이다
이 부분도 대부분은 유니티에서 알아서 해주고 있을 테니, Lit 셰이더 등을 보고 적절한 API를 그대로 사용하면 되리라 예상된다
그림자를 위한 설정들
하지만 코드 쓰는 것보다 더 중요한 것이 있으니, 그건 바로 설정이다!
아무리 코드를 작성해봐야 설정에서 Off로 되어 있으면 그림자는 안 나온다
기본적으로 다음의 URP Asset, Light 컴포넌트, Renderer 컴포넌트에서 그림자 관련 설정을 해줘야 한다
URP Asset
Light 컴포넌트
(Mesh) Renderer 컴포넌트
그 외에도 Shadows의 Max Distance나 Near Plane 등등이 영향을 미칠 순 있는데 여기선 생략하도록 하자
자세한 건 아래의 참고 사이트를 참고!
【Unity】URPでの影の問題解決と設定方法について! | CGbox
그림자를 위한 소스 코드
Cast Shadows
위에서 Cast Shadows를 위해서는 Shadow Map이란 것이 필요하고 설명했었다
그럼 오브젝트를 Shadow Map에 반영시키려면 어떻게 해면 될까?
URP의 스탠다드 셰이더인 Lit 셰이더를 살펴보니 ShadowCaster라는 패스를 추가해서 대응하고 있었다
그럼 이제 카피 닌자가 날뛸 시간!
Lit 셰이더에서는 깔끔하게 Shaders/ShadowCasterPass.hlsl를 #include 하고 끝이다
하지만 필자의 커스텀 셰이더는 아쉽게도 Lit 셰이더와 CBUFFER가 너무 다르기 때문에, SRP Batcher를 위해서 ShadowCasterPass.hlsl의 내용들을 복사해서 Pass를 만들었다
아래는 대략적인 샘플 코드!
좀 첨삭한 부분이 있지만 대략적인 파악에는 큰 문제가 없을 것이다
Pass
{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}
HLSLPROGRAM
#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment
// Cast Shadow
#pragma multi_compile_fragment _ LOD_FADE_CROSSFADE
#pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
CBUFFER_START(UnityPerMaterial)
// ...생략...
// SRP Batcher를 위해 다른 Pass와 동일한 CBUFFER를 선언!
// ...생략...
CBUFFER_END
float3 _LightDirection;
float3 _LightPosition;
#ifdef LOD_FADE_CROSSFADE
float _DitheringTextureInvSize;
TEXTURE2D(_DitheringTexture);
SAMPLER(sampler_DitheringTexture);
#endif
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
};
float4 GetShadowPositionHClip(Attributes input)
{
float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
#if _CASTING_PUNCTUAL_LIGHT_SHADOW
float3 lightDirectionWS = normalize(_LightPosition - positionWS);
#else
float3 lightDirectionWS = _LightDirection;
#endif
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
#if UNITY_REVERSED_Z
positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#endif
return positionCS;
}
Varyings ShadowPassVertex(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
output.positionCS = GetShadowPositionHClip(input);
return output;
}
half4 ShadowPassFragment(Varyings input) : SV_TARGET
{
// Alpha(SampleAlbedoAlpha(input.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)).a, _BaseColor, _Cutoff);
#ifdef LOD_FADE_CROSSFADE
// LODFadeCrossFade(input.positionCS);
#endif
return 0;
}
ENDHLSL
}
세부적인 원리나 코멘트한 부분 등은 둘째치고(필자의 현 프로젝트에 맞춰 수정하거나 했다)
여기서 가장 중요한 것은 LightMode 태그를 ShadowCaster로 해야 한다는 점이다
Tags{"LightMode" = "ShadowCaster"}
URP는 LightMode 태그를 보고 이 패스를 ShadowCasterPass에 넣을지 말지 판단하기 때문에, 제대로 설정해야 한다
잘 추가했으면 커스텀 셰이더의 오브젝트가 다른 오브젝트에 그림자를 드리우는 것을 볼 수 있다!
(아래의 Plane만 Lit 셰이더)
Receive Shadows
이제는 그림자를 받도록 해보자
우선 필요한 키워드들을 가져와주자
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile_fragment _ _LIGHT_COOKIES
참고로 특정 키워드가 선언된 경우, Shadows.hlsl에서 추가로 키워드가 선언되는 구조였다
Unity는 키워드가 딱 보면 추측하기 쉬워서 좋다
아무튼 위의 키워드가 있으면 MAIN_LIGHT_CALCULATE_SHADOWS와 같은 키워드는 커스텀 셰이더에 추가할 필요 없다!
// ShaderLibrary/Lighting.hlsl > ShaderLibrary/RealtimeLights.hlsl > ShaderLibrary/Shadows.hlsl
#if !defined(_RECEIVE_SHADOWS_OFF)
#if defined(_MAIN_LIGHT_SHADOWS) || defined(_MAIN_LIGHT_SHADOWS_CASCADE) || defined(_MAIN_LIGHT_SHADOWS_SCREEN)
#define MAIN_LIGHT_CALCULATE_SHADOWS
#if defined(_MAIN_LIGHT_SHADOWS) || (defined(_MAIN_LIGHT_SHADOWS_SCREEN) && !defined(_SURFACE_TYPE_TRANSPARENT))
#define REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
#endif
#endif
#if defined(_ADDITIONAL_LIGHT_SHADOWS)
#define ADDITIONAL_LIGHT_CALCULATE_SHADOWS
#endif
#endif
커스텀 셰이더의 프래그먼트 셰이더에는 Shadow Map에서의 좌표를 가져오는 처리가 필요하다
이미 다 API가 있으니 그대로 이용해주자
우선 월드 좌표와 TransformWorldToShadowCoord로 Shadow Map에서의 좌표를 가져온다
그 다음은 GetMainLight에 Shadow Map에서의 좌표를 인수로 넣어주면 끝!
(이미 함수 오버로딩이 다 되어있더라)
그렇게 Shadow 감쇠도 포함된 Light 정보를 가져왔으면 마지막으로 Light의 shadowAttenuation를 Light Color에 곱해주면 끝!
float4 shadowCoord = TransformWorldToShadowCoord(i.worldPos);
float ld = dot(_MainLightPosition.xyz, normal);
// Light mainLight = GetMainLight();
Light mainLight = GetMainLight(shadowCoord);
// light_col.rgb += mainLight.color * mainLight.distanceAttenuation * max(0, ld);
light_col.rgb += mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation) * max(0, ld);
#if defined(_ADDITIONAL_LIGHTS)
uint pixelLightCount = GetAdditionalLightsCount();
float3 additionalLightsColor = float4(0,0,0,0);
float3 light_dir = float3(0,0,0);
float3 lCol;
LIGHT_LOOP_BEGIN(pixelLightCount)
// const Light light = GetAdditionalLight(lightIndex, i.worldPos);
const Light light = GetAdditionalLight(lightIndex, i.worldPos, shadowCoord);
half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
half3 lightDiffuseColor = LightingLambert(attenuatedLightColor, light.direction, normal);
additionalLightsColor += color* lightDiffuseColor;
LIGHT_LOOP_END
#endif
참고로 내부 처리를 좀 봤는데 Shadow Map를 샘플링할 때 용책 샘플에서 본 SampleCmpLevelZero가 쓰이더라(D3D11)
#define SAMPLE_TEXTURE2D_SHADOW(textureName, samplerName, coord3) textureName.SampleCmpLevelZero(samplerName, (coord3).xy, (coord3).z)
아무튼 이걸로 커스텀 셰이더의 오브젝트도 그림자를 받을 수 있게 되었다!
아! 갑자기 섬광탄 터뜨리지 말라고!ㅋㅋ
이걸로 끝나나 싶었는데 특정 시점에서 섬광탄이 터진 것마냥 빛이 터지는 버그가 발생했다!
정확히는 시점에 따라 아래와 같이 그림자가 찢어지는(?) 부분이 보이고 엄청난 Bloom이 발생하는 현상이었다
Unity의 Soft Shadows를 사용할 때 카메라의 특정 각도에서 해당 이슈가 재현되는 걸 확인했다
URP의 디폴트 Lit 셰이더에서도 해당 문제가 재현되므로 커스텀 셰이더의 문제는 아닐 것!
(제발 아니길…!)
그리고 해당 이슈는 Spot Light에서만 재현 가능했고, Spot Light와 Additional Lights의 Per Object Limit에 따라 해소되기도 하였다
그럼 Soft Shadow가 문제인가?
실제로 값이 이상해지는 부분은 그곳이 아니었다…
일단 실제로 이상한 값이 되는 것은 Light의 shadowAttenuation란 변수였다
이 변수의 값만 출력하니 해당 이슈가 발생했다
원인의 정확한 이유는 모르겠지만 내부 코드들을 살펴보니 shadowAttenuation는 0~1 사이의 값이 되는 게 맞는 것 같았다
그래서 잘못된 값이 뭐가 됐든 shadowAttenuation에 saturate를 해주니 해당 이슈가 해결됐다!
const Light light = GetAdditionalLight(lightIndex, i.worldPos, shadowCoord);
half3 attenuatedLightColor = light.color * (light.distanceAttenuation * saturate(light.shadowAttenuation));
마무리
사실 그림자에 대해서 잘 모르고 있었는데 이번 태스크로 어느 정도 알게 된 것 같다
사실 Shadow Map을 만들 거나 거기서 값을 가져오는 걸 직접한 건 아니지만 뭐 그런 걸 해주는 게 게임 엔진 아날까?
그러니 우선은 이 정도도 충분히 만족스럽다! 아무튼 그럼!
참고 사이트
【Unity】URPでの影の問題解決と設定方法について! | CGbox
'Unity > URP or Shader 관련' 카테고리의 다른 글
Vignette를 불투명 Mesh와 반투명 Mesh로 나눠서 구현해보자! (0) | 2024.04.08 |
---|---|
URP에서 간단하게 Stencil Buffer를 사용해서 Magic Card 만들어보기! (0) | 2024.03.13 |
Fog가 적용되는 Skybox의 셰이더 만들어보자! (0) | 2024.02.28 |
버텍스 ID 3개로 풀 스크린 쿼드를 대체할 수 있다고!! (0) | 2024.02.27 |
URP에서 팔레트 스왑(Palette Swap) 기능 구현하기 (0) | 2024.02.19 |