2025-04-25 21:14:54 +09:00

242 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace NiloToon.NiloToonURP
{
public class NiloToonAverageShadowTestRTPass : ScriptableRenderPass
{
// singleton
public static NiloToonAverageShadowTestRTPass Instance => _instance;
static NiloToonAverageShadowTestRTPass _instance;
[Serializable]
public class Settings
{
[Tooltip( "If you want NiloToon character to receive URP shadow map in an extremely soft and blurry way(a special URP shadow sampling that is blurry across the whole character, darken the character uniformly), turn this on.\n" +
"When turned on, character won't receive main directional light's direct lighting when occluded by URP's shadow casters (e.g. character completely under a bridge, where the bridge is casting URP shadow).\n\n" +
"Default is OFF, since some users don't want this kind of shadow ON by default when character is completely indoor")]
[OverrideDisplayName("Enable?")]
public bool enableAverageShadow = false;
}
public Settings settings;
// for most game type, 128 is a big enough number, but still not affecting performance
// maximum 127 active nilotoon characters can be inside the same scene(including any active character in scene, no camera culling), the last slot is reserved for camera's anime postprocess
const int MAX_SHADOW_SLOT_COUNT = 128;
Material material;
float[] shaderDataArray;
#if UNITY_2022_2_OR_NEWER
RTHandle shadowTestResultRTH;
#else
RenderTargetHandle shadowTestResultRTH;
#endif
static readonly int _SphereShadowMapRT_SID = Shader.PropertyToID("_SphereShadowMapRT");
public NiloToonAverageShadowTestRTPass(NiloToonRendererFeatureSettings allSettings)
{
this.settings = allSettings.sphereShadowTestSettings;
#if !UNITY_2022_2_OR_NEWER
shadowTestResultRTH.Init("_NiloToonAverageShadowMapRT");
#endif
shaderDataArray = new float[MAX_SHADOW_SLOT_COUNT * 4]; // each slot has 4 data, total of 128 slots. (default with (0,0,0,0))
_instance = this;
base.profilingSampler = new ProfilingSampler(nameof(NiloToonAverageShadowTestRTPass));
}
// This method is called before executing the render pass.
// It can be used to configure render targets and their clear state. Also to create temporary render target textures.
// When empty this render pass will render to the active camera render target.
// You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
// The render pipeline will ensure target setup and clearing happens in a performant manner.
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
// [possible optimization note]
// in URP12(Unity2021.3), even there is no shadow caster to draw, URP will still allocate and clear a small 1x1 RT, used as shadowmap
// see URP12 -> MainLightShadowCasterPass.cs's SetupForEmptyRendering().
// here we can't do this optimization, since character shader will LOAD texture using uv = (index,0),
// this texture must be 128x1
int RTwidth = MAX_SHADOW_SLOT_COUNT;
// RT width: use MAX_SHADOW_SLOT_COUNT as RT width, each pixel represent 1 NiloToon character's average shadow(the last pixel is camera's average shadow)
// RT height: RT height is 1
// RTFormat: RT format is RFloat, because we want to store a 0~1 average shadowAttenuation value
// don't need depthbuffer/stencil/mipmap
RenderTextureDescriptor renderTextureDescriptor = new RenderTextureDescriptor(RTwidth, 1, RenderTextureFormat.RFloat, 0, 1);
// it is linear(non color) data
renderTextureDescriptor.sRGB = false;
renderTextureDescriptor.depthBufferBits = 0;
renderTextureDescriptor.depthStencilFormat = GraphicsFormat.None;
// Some Samsung phones didn't support GraphicsFormat.R16_UNorm, so we need to do a full fallback chain
// Devices that can't support R16_UNorm = Galaxy S8, S7, and S21
if(SystemInfo.IsFormatSupported(GraphicsFormat.R16_UNorm,FormatUsage.Render))
{
renderTextureDescriptor.graphicsFormat = GraphicsFormat.R16_UNorm;
}
else if (SystemInfo.IsFormatSupported(GraphicsFormat.R8_UNorm, FormatUsage.Render))
{
renderTextureDescriptor.graphicsFormat = GraphicsFormat.R8_UNorm;
}
else if (SystemInfo.IsFormatSupported(GraphicsFormat.R8G8B8A8_UNorm, FormatUsage.Render))
{
renderTextureDescriptor.graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm;
}
else
{
Debug.LogError($"NiloToon: The creation of R8G8B8A8_UNorm RenderTexture of {nameof(NiloToonAverageShadowTestRTPass)} failed, contact email/discord for help.");
return;
}
// we need each pixel providing a per character average shadow attenuation value when sampling, so FilterMode is Point
#if UNITY_2022_2_OR_NEWER
RenderingUtils.ReAllocateIfNeeded(ref shadowTestResultRTH, renderTextureDescriptor, FilterMode.Point, TextureWrapMode.Clamp, name: "_NiloToonAverageShadowMapRT");
cmd.SetGlobalTexture(shadowTestResultRTH.name, shadowTestResultRTH.nameID);
#else
cmd.GetTemporaryRT(shadowTestResultRTH.id, renderTextureDescriptor, FilterMode.Point);
cmd.SetGlobalTexture(_SphereShadowMapRT_SID, shadowTestResultRTH.Identifier());
#endif
// still need to clear RT to white even average shadow is not enabled
// (in character shader we removed average shadow's multi_compile to save memory & build time/size, so character shader is always sampling this RT)
#if UNITY_2022_2_OR_NEWER
ConfigureTarget(shadowTestResultRTH);
#else
ConfigureTarget(shadowTestResultRTH.Identifier());
#endif
ConfigureClear(ClearFlag.Color, Color.white); // default white = default no shadow
}
// Here you can implement the rendering logic.
// Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
// https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
// You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (!shouldRenderRT(renderingData)) return;
renderPerCharacterAverageShadowAtlaRT(context, renderingData);
}
// Cleanup any allocated resources that were created during the execution of this render pass.
public override void OnCameraCleanup(CommandBuffer cmd)
{
#if !UNITY_2022_2_OR_NEWER
cmd.ReleaseTemporaryRT(shadowTestResultRTH.id);
#endif
}
#if UNITY_2022_2_OR_NEWER
public void Dispose()
{
shadowTestResultRTH?.Release();
}
#endif
bool shouldRenderRT(RenderingData renderingData)
{
// TODO:
// WebGL is not safe to run big foploop in shader
// remove this when the problem is solved
#if UNITY_WEBGL
return false;
#endif
if (renderingData.cameraData.cameraType == CameraType.Preview)
return false;
var shadowControlVolumeEffect = VolumeManager.instance.stack.GetComponent<NiloToonShadowControlVolume>();
// in NiloToon 0.11.1, we changed the merge method from simple override to a "&&" merge, so renderer feature can force disable all average shadow even if volume has overridden and enabled it.
bool enableAverageShadow = shadowControlVolumeEffect.enableCharAverageShadow.value && settings.enableAverageShadow;
return enableAverageShadow;
}
private void renderPerCharacterAverageShadowAtlaRT(ScriptableRenderContext context, RenderingData renderingData)
{
// delay CreateEngineMaterial to as late as possible, to make it safe when ReimportAll is running
if (!material)
material = CoreUtils.CreateEngineMaterial("Hidden/NiloToon/AverageShadowTestRT");
// sometimes the shader is not yet compile when first time opening the project or opening the project after deleting Library folder,
// if material is not ready, cmd.DrawMesh will produce "Invalid pass" error log, so we need to skip it.
if (!material)
return;
// NOTE: Do NOT mix ProfilingScope with named CommandBuffers i.e. CommandBufferPool.Get("name").
// Currently there's an issue which results in mismatched markers.
CommandBuffer cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, base.profilingSampler))
{
/*
Note : should always ExecuteCommandBuffer at least once before using
ScriptableRenderContext functions (e.g. DrawRenderers) even if you
don't queue any commands! This makes sure the frame debugger displays
everything under the correct title.
*/
// https://www.cyanilux.com/tutorials/custom-renderer-features/?fbclid=IwAR27j2f3VVo0IIYDa32Dh76G9KPYzwb8j1J5LllpSnLXJiGf_UHrQ_lDtKg
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// TODO: shadow will be wrong if number of active character in scene is more than MAX_SHADOW_SLOT_COUNT-1
// reserve the right most slot for camera, other slots for each active character
for (int i = 0; i < Mathf.Min(MAX_SHADOW_SLOT_COUNT - 1, NiloToonAllInOneRendererFeature.characterList.Count); i++)
{
NiloToonPerCharacterRenderController controller = NiloToonAllInOneRendererFeature.characterList[i];
if (controller)
{
Vector3 centerPosWS = controller.GetCharacterBoundCenter();
float radiusWS = controller.GetCharacterBoundRadius();
shaderDataArray[i * 4 + 0] = centerPosWS.x;
shaderDataArray[i * 4 + 1] = centerPosWS.y;
shaderDataArray[i * 4 + 2] = centerPosWS.z;
shaderDataArray[i * 4 + 3] = radiusWS;
}
else
{
shaderDataArray[i * 4 + 3] = 0;
}
}
// RT's right most slot(pixel) for camera only
Camera cam = renderingData.cameraData.camera;
Vector3 cameraPosForTesting = cam.transform.position + cam.transform.forward * (cam.nearClipPlane + 1f); // add 1 unit offset to not use cam pos as testing center directly, to avoid wrong cascade shadowmap index(avoid testing outside of camera frustrum)
shaderDataArray[(MAX_SHADOW_SLOT_COUNT - 1) * 4 + 0] = cameraPosForTesting.x;
shaderDataArray[(MAX_SHADOW_SLOT_COUNT - 1) * 4 + 1] = cameraPosForTesting.y;
shaderDataArray[(MAX_SHADOW_SLOT_COUNT - 1) * 4 + 2] = cameraPosForTesting.z;
shaderDataArray[(MAX_SHADOW_SLOT_COUNT - 1) * 4 + 3] = 0.5f; // width is 0.5 unit, to avoid testing outside of camera frustrum
var shadowControlVolumeEffect = VolumeManager.instance.stack.GetComponent<NiloToonShadowControlVolume>();
cmd.SetGlobalFloatArray("_GlobalAverageShadowTestBoundingSphereDataArray", shaderDataArray); // once set, we can't change the size of array in GPU anymore, it is not Unity's fault but graphics API's design.
cmd.SetGlobalFloat("_GlobalAverageShadowStrength", shadowControlVolumeEffect.charAverageShadowStrength.value);
#if UNITY_2022_2_OR_NEWER
Blitter.BlitTexture(cmd, shadowTestResultRTH, shadowTestResultRTH, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0);
#else
cmd.Blit(null, shadowTestResultRTH.Identifier(), material);
#endif
}
// must write these line after using{} finished, to ensure profiler and frame debugger display correctness
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
CommandBufferPool.Release(cmd);
}
}
}