Fix : NiloToon 셀프 섀도우 카메라 밖 캐릭터 그림자 누락

Unity 6 RG path가 main camera cullResults만 사용하여 카메라가 캐릭터를
프러스텀 밖으로 두면 shadow map RT에 캐릭터가 안 그려져 바닥 그림자가
사라지던 문제 해결.

- Pass.cs ExecutePass(RG)/Execute(Legacy) 에 manual cmd.DrawRenderer
  추가하여 NiloToonAllInOneRendererFeature.characterList 의 모든
  활성 캐릭터를 cullResults 의존성 없이 직접 그림
- shader 별 NiloToonSelfShadowCaster pass index 캐싱
- validCharList의 frustum AABB 필터 제거하여 키워드/ortho box 항상 유지
- SkinnedMeshRenderer.updateWhenOffscreen 강제로 카메라 밖에서도
  본 매트릭스 갱신 (localBounds는 GetCharacterBoundCenter에 영향 주므로
  건드리지 않음)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
user 2026-05-05 22:32:58 +09:00
parent e1ed042365
commit ed764d7f83
2 changed files with 125 additions and 39 deletions

View File

@ -1114,6 +1114,16 @@ namespace NiloToon.NiloToonURP
AutoFillInMissingProperties(); AutoFillInMissingProperties();
// Streamingle: 이미 등록된 renderer에도 updateWhenOffscreen 적용 (씬에 미리 있던 캐릭터 대응).
// RefillAllRenderers는 새 등록 때만 호출되므로 OnEnable에서 한 번 더 강제.
for (int i = 0; i < allRenderers.Count; i++)
{
if (allRenderers[i] is SkinnedMeshRenderer existingSmr && !existingSmr.updateWhenOffscreen)
{
existingSmr.updateWhenOffscreen = true;
}
}
// To support VRMBlendShapeProxy, NiloToon now generates material instances on OnEnable(), which is before VRMBlendShapeProxy's Start() // To support VRMBlendShapeProxy, NiloToon now generates material instances on OnEnable(), which is before VRMBlendShapeProxy's Start()
// Note: // Note:
// - Don't call LateUpdate() in edit mode, since it is not required and may trigger a crash when building the game (if headBoneTransform is null) // - Don't call LateUpdate() in edit mode, since it is not required and may trigger a crash when building the game (if headBoneTransform is null)
@ -1538,6 +1548,14 @@ namespace NiloToon.NiloToonURP
// we don't want to add particle/vfx/trail....renderers // we don't want to add particle/vfx/trail....renderers
if(!(renderer is MeshRenderer or SkinnedMeshRenderer)) continue; if(!(renderer is MeshRenderer or SkinnedMeshRenderer)) continue;
// Streamingle: Pass.cs의 manual DrawRenderer로 cullResults 우회 시,
// SkinnedMeshRenderer가 화면 밖이면 Unity가 본 매트릭스 갱신을 skip하여 stale pose가 그려질 수 있음.
// updateWhenOffscreen=true 강제. (localBounds는 GetCharacterBoundCenter에 영향 주므로 건드리지 않음)
if (renderer is SkinnedMeshRenderer smr && !smr.updateWhenOffscreen)
{
smr.updateWhenOffscreen = true;
}
var NiloToonPerCharacterRenderControllerFound = renderer.transform.GetComponentInParent<NiloToonPerCharacterRenderController>(); var NiloToonPerCharacterRenderControllerFound = renderer.transform.GetComponentInParent<NiloToonPerCharacterRenderController>();
if(NiloToonPerCharacterRenderControllerFound) if(NiloToonPerCharacterRenderControllerFound)
{ {

View File

@ -174,6 +174,31 @@ namespace NiloToon.NiloToonURP
List<NiloToonPerCharacterRenderController> validCharList = new List<NiloToonPerCharacterRenderController>(); List<NiloToonPerCharacterRenderController> validCharList = new List<NiloToonPerCharacterRenderController>();
List<NiloToonPerCharacterRenderController> finalValidCharList = new List<NiloToonPerCharacterRenderController>(); List<NiloToonPerCharacterRenderController> finalValidCharList = new List<NiloToonPerCharacterRenderController>();
// Streamingle: shader → "NiloToonSelfShadowCaster" pass index 캐시.
// Unity 6 RG path는 main camera cullResults 만 쓰므로 카메라가 캐릭터 안 보면 shadow map 비어 그림자 사라짐.
// ExecutePass에서 manual DrawRenderer로 cullResults 우회. shader 별 pass index 조회는 비싸 캐시 필요.
static readonly Dictionary<Shader, int> s_shadowCasterPassIndexCache = new Dictionary<Shader, int>();
static readonly ShaderTagId s_lightModeTagId = new ShaderTagId("LightMode");
static readonly ShaderTagId s_shadowCasterTagId = new ShaderTagId("NiloToonSelfShadowCaster");
static int GetShadowCasterPassIndex(Shader shader)
{
if (shader == null) return -1;
if (s_shadowCasterPassIndexCache.TryGetValue(shader, out int cached)) return cached;
int found = -1;
int passCount = shader.passCount;
for (int i = 0; i < passCount; i++)
{
if (shader.FindPassTagValue(i, s_lightModeTagId) == s_shadowCasterTagId)
{
found = i;
break;
}
}
s_shadowCasterPassIndexCache[shader] = found;
return found;
}
// Constructor(will not call on every frame) // Constructor(will not call on every frame)
public NiloToonCharSelfShadowMapRTPass(NiloToonRendererFeatureSettings allSettings) public NiloToonCharSelfShadowMapRTPass(NiloToonRendererFeatureSettings allSettings)
@ -448,27 +473,15 @@ namespace NiloToon.NiloToonURP
// https://docs.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps // https://docs.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
GeometryUtility.CalculateFrustumPlanes(camera, cameraPlanes);
validCharList.Clear(); validCharList.Clear();
// [1] filter list // [1] Streamingle: 메인 카메라 frustum cull 제거. (controller bound 기반 필터)
// 캐릭터가 메인 카메라 frustum 밖이어도 ortho box / keyword 가 정상 동작해야 함.
// 실제 cullResults 우회는 ExecutePass의 manual DrawRenderer 가 담당.
foreach (var targetChar in NiloToonAllInOneRendererFeature.characterList) foreach (var targetChar in NiloToonAllInOneRendererFeature.characterList)
{ {
// if target is not valid, skip it
if (targetChar == null) continue; if (targetChar == null) continue;
if (!targetChar.isActiveAndEnabled) continue; // character GameObject not enabled(not rendering) but in list if (!targetChar.isActiveAndEnabled) continue;
// if character bounding sphere is completely not visible in game camera frustum, skip it
var boundRadius = targetChar.GetCharacterBoundRadius();
var centerPosWS = targetChar.GetCharacterBoundCenter();
// TODO: this section is not correct, which may incorrectly cull effective shadow caster that is OUTSIDE of main camera frustum
if (!GeometryUtility.TestPlanesAABB(cameraPlanes, new Bounds(centerPosWS, Vector3.one * boundRadius)))
{
continue;
}
// it is a valid visible char, add to list
validCharList.Add(targetChar); validCharList.Add(targetChar);
} }
@ -715,6 +728,13 @@ namespace NiloToon.NiloToonURP
var filterSetting = new FilteringSettings(RenderQueueRange.opaque); var filterSetting = new FilteringSettings(RenderQueueRange.opaque);
context.DrawRenderers(cullResults, ref drawSetting, ref filterSetting); // using custom cullResults from shadow camera's perspective, instead of main camera's cull result context.DrawRenderers(cullResults, ref drawSetting, ref filterSetting); // using custom cullResults from shadow camera's perspective, instead of main camera's cull result
// Streamingle: cullResults 우회용 manual draw.
// perfectCullingForShadowCasters=false 또는 terrain 존재 시 cullResults가 main camera cull로 fallback →
// 캐릭터가 frustum 밖이면 shadow map에 안 그려짐. 같은 renderer가 두 번 그려질 수 있으나 depth test가 처리.
DrawNiloToonCharsManuallyLegacy(cmd);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// Note: Since we are providing our own _NiloToonSelfShadowWorldToClip to shader for VP transform, // Note: Since we are providing our own _NiloToonSelfShadowWorldToClip to shader for VP transform,
// this section is not needed anymore, it will trigger a bug in multi pass mode XR // this section is not needed anymore, it will trigger a bug in multi pass mode XR
/* /*
@ -1036,29 +1056,15 @@ namespace NiloToon.NiloToonURP
// https://docs.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps // https://docs.microsoft.com/en-us/windows/win32/dxtecharts/common-techniques-to-improve-shadow-depth-maps
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
GeometryUtility.CalculateFrustumPlanes(camera, cameraPlanes);
validCharList.Clear(); validCharList.Clear();
// [1] filter list // [1] Streamingle: 메인 카메라 frustum cull 제거. (controller bound 기반 필터)
// 캐릭터가 메인 카메라 frustum 밖이어도 ortho box / keyword 가 정상 동작해야 함.
// 실제 cullResults 우회는 ExecutePass의 manual DrawRenderer 가 담당.
foreach (var targetChar in NiloToonAllInOneRendererFeature.characterList) foreach (var targetChar in NiloToonAllInOneRendererFeature.characterList)
{ {
// if target is not valid, skip it
if (targetChar == null) continue; if (targetChar == null) continue;
if (!targetChar.isActiveAndEnabled) if (!targetChar.isActiveAndEnabled) continue;
continue; // character GameObject not enabled(not rendering) but in list
// if character bounding sphere is completely not visible in game camera frustum, skip it
var boundRadius = targetChar.GetCharacterBoundRadius();
var centerPosWS = targetChar.GetCharacterBoundCenter();
// TODO: this section is not correct, which may incorrectly cull effective shadow caster that is OUTSIDE of main camera frustum
if (!GeometryUtility.TestPlanesAABB(cameraPlanes,
new Bounds(centerPosWS, Vector3.one * boundRadius)))
{
continue;
}
// it is a valid visible char, add to list
validCharList.Add(targetChar); validCharList.Add(targetChar);
} }
@ -1329,9 +1335,71 @@ namespace NiloToon.NiloToonURP
cmd.SetKeyword(GlobalKeyword.Create(_NILOTOON_RECEIVE_SELF_SHADOW_Keyword), true); cmd.SetKeyword(GlobalKeyword.Create(_NILOTOON_RECEIVE_SELF_SHADOW_Keyword), true);
// Draw the objects in the list // Draw the objects in the list (uses main camera cullResults — character가 frustum 안일 때만)
cmd.DrawRendererList(data.rendererListHandle); cmd.DrawRendererList(data.rendererListHandle);
// Streamingle: cullResults 우회용 manual draw.
// 카메라가 캐릭터 frustum 밖이면 rendererListHandle에 캐릭터가 없어 shadow map이 비어 그림자 사라짐.
// 모든 NiloToon 캐릭터의 모든 NiloToonSelfShadowCaster pass를 수동으로 한 번 더 그려 cullResults 의존성 제거.
// 같은 renderer가 두 번 그려질 수 있으나 depth test가 처리하므로 시각적 영향 없음.
DrawNiloToonCharsManually(cmd);
}
static void DrawNiloToonCharsManually(RasterCommandBuffer cmd)
{
var charList = NiloToonAllInOneRendererFeature.characterList;
if (charList == null) return;
for (int c = 0; c < charList.Count; c++)
{
var character = charList[c];
if (character == null || !character.isActiveAndEnabled) continue;
var renderers = character.allRenderers;
if (renderers == null) continue;
for (int r = 0; r < renderers.Count; r++)
{
var renderer = renderers[r];
if (renderer == null || !renderer.enabled || !renderer.gameObject.activeInHierarchy) continue;
var sharedMats = renderer.sharedMaterials;
if (sharedMats == null) continue;
for (int subIdx = 0; subIdx < sharedMats.Length; subIdx++)
{
var mat = sharedMats[subIdx];
if (mat == null) continue;
int passIdx = GetShadowCasterPassIndex(mat.shader);
if (passIdx < 0) continue;
cmd.DrawRenderer(renderer, mat, subIdx, passIdx);
}
}
}
} }
#endif #endif
static void DrawNiloToonCharsManuallyLegacy(CommandBuffer cmd)
{
var charList = NiloToonAllInOneRendererFeature.characterList;
if (charList == null) return;
for (int c = 0; c < charList.Count; c++)
{
var character = charList[c];
if (character == null || !character.isActiveAndEnabled) continue;
var renderers = character.allRenderers;
if (renderers == null) continue;
for (int r = 0; r < renderers.Count; r++)
{
var renderer = renderers[r];
if (renderer == null || !renderer.enabled || !renderer.gameObject.activeInHierarchy) continue;
var sharedMats = renderer.sharedMaterials;
if (sharedMats == null) continue;
for (int subIdx = 0; subIdx < sharedMats.Length; subIdx++)
{
var mat = sharedMats[subIdx];
if (mat == null) continue;
int passIdx = GetShadowCasterPassIndex(mat.shader);
if (passIdx < 0) continue;
cmd.DrawRenderer(renderer, mat, subIdx, passIdx);
}
}
}
}
} }
} }