user 010beaea75 Chore: NiloToonURP 업데이트 및 배경 썸네일 갱신
- NiloToonURP 외부 에셋 업데이트
- 배경 씬 썸네일 16:9 해상도로 갱신
- 렌더 파이프라인 설정 업데이트
- 외부 셰이더 그래프 업데이트 (LEDScreen, PIDI Planar Reflections)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 01:28:02 +09:00

496 lines
22 KiB
C#

// Already existing Unity Motion Blur solutions (NiloToon's motion blur develop based on KinoMotion):
// https://github.com/keijiro/KinoMotion/tree/master
// https://github.com/keijiro/MotionBlurTest
// Important Reference:
// https://casual-effects.com/research/McGuire2012Blur/McGuire12Blur.pdf
// Other reference:
// https://github.com/raysjoshua/UnrealEngine/blob/master/Engine/Shaders/PostProcessMotionBlur.usf
// https://github.com/EpicGames/UnrealEngine/tree/release/Engine/Shaders/Private/MotionBlur
// https://john-chapman-graphics.blogspot.com/2013/01/per-object-motion-blur.html
// https://youtu.be/b0S6WMAfi0o?si=8KSxKDHz9Z95VbVt (A Reconstruction Filter for Plausible Motion Blur (I3D 12))
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
#if UNITY_6000_0_OR_NEWER
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Experimental.Rendering;
#endif
namespace NiloToon.NiloToonURP
{
public class NiloToonMotionBlurPass : ScriptableRenderPass
{
[System.Serializable]
public class Settings
{
[Revertible]
public bool allowRender = true;
}
#if UNITY_2022_3_OR_NEWER
private Material blurMaterial;
private RTHandle source;
private RTHandle tempRT0;
private RTHandle tempRT1;
private RTHandle copyRT;
static class Uniforms
{
internal static readonly int _VelocityScale = Shader.PropertyToID("_VelocityScale");
internal static readonly int _MaxBlurRadius = Shader.PropertyToID("_MaxBlurRadius");
internal static readonly int _RcpMaxBlurRadius = Shader.PropertyToID("_RcpMaxBlurRadius");
internal static readonly int _VelocityTex = Shader.PropertyToID("_VelocityTex");
internal static readonly int _MainTex = Shader.PropertyToID("_MainTex");
internal static readonly int _Tile2RT = Shader.PropertyToID("_Tile2RT");
internal static readonly int _Tile4RT = Shader.PropertyToID("_Tile4RT");
internal static readonly int _Tile8RT = Shader.PropertyToID("_Tile8RT");
internal static readonly int _TileMaxOffs = Shader.PropertyToID("_TileMaxOffs");
internal static readonly int _TileMaxLoop = Shader.PropertyToID("_TileMaxLoop");
internal static readonly int _TileVRT = Shader.PropertyToID("_TileVRT");
internal static readonly int _NeighborMaxTex = Shader.PropertyToID("_NeighborMaxTex");
internal static readonly int _LoopCount = Shader.PropertyToID("_LoopCount");
}
enum Pass
{
VelocitySetup,
TileMax1,
TileMax2,
TileMaxV,
NeighborMax,
Reconstruction,
}
private Settings settings;
private string profilerTag;
private float lastValidDeltaTime;
private Vector3 lastValidCameraPosWS;
private Matrix4x4 lastValidCameraViewProjMatrix;
private bool lastFrameIsSafeToApplyMotionBlur;
public NiloToonMotionBlurPass(NiloToonRendererFeatureSettings settings)
{
this.settings = settings.motionBlurSettings;
renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
profilerTag = "NiloToon Motion Blur";
if (blurMaterial == null)
{
string shaderPath = "Hidden/NiloToon/NiloToonKinoMotionBlur";
Shader shader = Shader.Find(shaderPath);
if (shader != null)
{
blurMaterial = CoreUtils.CreateEngineMaterial(shader);
}
}
}
// please always and only call this in ScriptableRendererFeature.AddRenderPasses()
public void ConfigureInputs(Camera camera)
{
ScriptableRenderPassInput input = ScriptableRenderPassInput.None;
if (ShouldRender(camera))
{
input = ScriptableRenderPassInput.Motion | ScriptableRenderPassInput.Depth;
}
ConfigureInput(input);
}
#if !UNITY_6000_4_OR_NEWER
#if UNITY_6000_0_OR_NEWER
[Obsolete]
#endif
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
if(!ShouldRender(renderingData.cameraData.camera)) return;
source = renderingData.cameraData.renderer.cameraColorTargetHandle;
// Allocate temporary RTHandles
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
descriptor.colorFormat = RenderTextureFormat.RGHalf; // Use RGHalf for higher precision if needed
float rt_TargetLength = GetRTTargetLength(descriptor);
float tileSize = GetTileSize(rt_TargetLength);
int tileWidth = Mathf.CeilToInt(descriptor.width / tileSize);
int tileHeight = Mathf.CeilToInt(descriptor.height / tileSize);
var tileDesc = descriptor;
tileDesc.width = tileWidth;
tileDesc.height = tileHeight;
RenderingUtils.ReAllocateIfNeeded(ref tempRT0, tileDesc, name: "_TempRT0");
RenderingUtils.ReAllocateIfNeeded(ref tempRT1, tileDesc, name: "_TempRT1");
var copyRTDesc = renderingData.cameraData.cameraTargetDescriptor;
copyRTDesc.depthBufferBits = 0;
RenderingUtils.ReAllocateIfNeeded(ref copyRT, copyRTDesc, name: "_CopyTex");
}
#endif
public float GetRTTargetLength(RenderTextureDescriptor descriptor)
{
return Mathf.Min(descriptor.width, descriptor.height);
}
public float GetIntensity()
{
float deltaTime = Time.deltaTime;
// handle editor pause, where Time.deltaTime is 0
if (deltaTime <= 0f)
{
deltaTime = lastValidDeltaTime;
}
else
{
lastValidDeltaTime = deltaTime;
}
if (deltaTime <= 0) return 0; // should not happen except the first frame
var motionBlurPP = VolumeManager.instance.stack.GetComponent<NiloToonMotionBlurVolume>();
float intensity = motionBlurPP.intensity.value; // user custom multiplier control
// make it fps independent (24fps 180 shutter angle as base, it is the perfect cinematic motion blur)
intensity /= deltaTime / (1f/24f);
// * 0.5, so that when intensity in volume is 1, the result is matching ground truth 24fps 180 shutter angle from NiloToonMotionBlurVideoBaker)
intensity *= 0.5f;
return intensity;
}
public int GetTileSize(float rt_TargetLength)
{
// the ideal 1/48s shutter speed tileSize for:
// 4320p = 30
// 2160p = 15
// 1080p = 7.5 (=8)
int tileSize = Mathf.CeilToInt(rt_TargetLength / 4320f * 30f);
// allow more blur bleed out
tileSize *= 4;
return tileSize;
}
bool IsCameraSafeToApplyMotionBlur(
Matrix4x4 prevViewProj,
Matrix4x4 currentViewProj,
Vector3 prevCamPos,
Vector3 currentCamPos)
{
// Default thresholds
const float maxPositionDiff = 2.0f; // units in world space
const float maxAngleDegrees = 15.0f; // degrees
const float maxSizeRatio = 2.0f; // maximum 2x screen size change (100% change)
// 1. Position Check
float positionDiff = Vector3.Distance(currentCamPos, prevCamPos);
if (positionDiff > maxPositionDiff)
return false;
// 2. Camera Direction Check
Vector3 prevForward = -new Vector3(prevViewProj.m20, prevViewProj.m21, prevViewProj.m22).normalized;
Vector3 currentForward = -new Vector3(currentViewProj.m20, currentViewProj.m21, currentViewProj.m22).normalized;
float dotProduct = Vector3.Dot(prevForward, currentForward);
float angleDiff = Mathf.Acos(Mathf.Clamp(dotProduct, -1.0f, 1.0f)) * Mathf.Rad2Deg;
if (angleDiff > maxAngleDegrees)
return false;
// 3. FOV Check - using screen size ratio directly
float prevTanHalfFOV = 1.0f / prevViewProj.m11;
float currentTanHalfFOV = 1.0f / currentViewProj.m11;
float screenSizeRatio = currentTanHalfFOV / prevTanHalfFOV;
if (screenSizeRatio > maxSizeRatio || screenSizeRatio < 1.0f/maxSizeRatio)
return false;
return true;
}
public bool ShouldRender(Camera camera)
{
// if PIDI planar reflection enabled postFX, it will make the motion blur disable,
// so we need to disable motion blur for all PIDI planar reflection
if (NiloToonPlanarReflectionHelper.IsPlanarReflectionCamera(camera))
return false;
var motionBlurPP = VolumeManager.instance.stack.GetComponent<NiloToonMotionBlurVolume>();
return motionBlurPP.IsActive() && settings.allowRender;
}
/////////////////////////////////////////////////////////////////////
// SHARED KINO MOTION BLUR ALGORITHM CORE
/////////////////////////////////////////////////////////////////////
private void ExecuteKinoMotionBlurCore(CommandBuffer cmd, RenderTextureDescriptor descriptor,
float intensity, float rt_TargetLength)
{
const float kMaxBlurRadius = 5f; // max blur is 5% of screen
Material material = blurMaterial;
// Texture format for storing 2D vectors.
RenderTextureFormat m_VectorRTFormat = RenderTextureFormat.RGHalf;
// Texture format for storing packed velocity/depth.
RenderTextureFormat m_PackedRTFormat = RenderTextureFormat.ARGB2101010;
float shutterAngle = 180f * intensity;
int sampleCount = 32;
// Calculate the maximum blur radius in pixels.
int maxBlurPixels = (int)(kMaxBlurRadius * rt_TargetLength / 100);
// Calculate the TileMax size.
// It should be a multiple of 8 and larger than maxBlur.
int tileSize = ((maxBlurPixels - 1) / 8 + 1) * 8;
// Pass 1 - Velocity/depth packing
var velocityScale = shutterAngle / 360f;
cmd.SetGlobalFloat(Uniforms._VelocityScale, velocityScale);
cmd.SetGlobalFloat(Uniforms._MaxBlurRadius, maxBlurPixels);
cmd.SetGlobalFloat(Uniforms._RcpMaxBlurRadius, 1f / maxBlurPixels);
int vbuffer = Uniforms._VelocityTex;
cmd.GetTemporaryRT(vbuffer, descriptor.width, descriptor.height, 0, FilterMode.Point, m_PackedRTFormat, RenderTextureReadWrite.Linear);
cmd.Blit((Texture)null, vbuffer, material, (int)Pass.VelocitySetup);
// Pass 2 - First TileMax filter (1/2 downsize)
int tile2 = Uniforms._Tile2RT;
cmd.GetTemporaryRT(tile2, descriptor.width / 2, descriptor.height / 2, 0, FilterMode.Point, m_VectorRTFormat, RenderTextureReadWrite.Linear);
cmd.SetGlobalTexture(Uniforms._MainTex, vbuffer);
cmd.Blit(vbuffer, tile2, material, (int)Pass.TileMax1);
// Pass 3 - Second TileMax filter (1/2 downsize)
int tile4 = Uniforms._Tile4RT;
cmd.GetTemporaryRT(tile4, descriptor.width / 4, descriptor.height / 4, 0, FilterMode.Point, m_VectorRTFormat, RenderTextureReadWrite.Linear);
cmd.SetGlobalTexture(Uniforms._MainTex, tile2);
cmd.Blit(tile2, tile4, material, (int)Pass.TileMax2);
cmd.ReleaseTemporaryRT(tile2);
// Pass 4 - Third TileMax filter (1/2 downsize)
int tile8 = Uniforms._Tile8RT;
cmd.GetTemporaryRT(tile8, descriptor.width / 8, descriptor.height / 8, 0, FilterMode.Point, m_VectorRTFormat, RenderTextureReadWrite.Linear);
cmd.SetGlobalTexture(Uniforms._MainTex, tile4);
cmd.Blit(tile4, tile8, material, (int)Pass.TileMax2);
cmd.ReleaseTemporaryRT(tile4);
// Pass 5 - Fourth TileMax filter (reduce to tileSize)
var tileMaxOffs = Vector2.one * (tileSize / 8f - 1f) * -0.5f;
cmd.SetGlobalVector(Uniforms._TileMaxOffs, tileMaxOffs);
cmd.SetGlobalFloat(Uniforms._TileMaxLoop, (int)(tileSize / 8f));
int tile = Uniforms._TileVRT;
cmd.GetTemporaryRT(tile, descriptor.width / tileSize, descriptor.height / tileSize, 0, FilterMode.Point, m_VectorRTFormat, RenderTextureReadWrite.Linear);
cmd.SetGlobalTexture(Uniforms._MainTex, tile8);
cmd.Blit(tile8, tile, material, (int)Pass.TileMaxV);
cmd.ReleaseTemporaryRT(tile8);
// Pass 6 - NeighborMax filter
int neighborMax = Uniforms._NeighborMaxTex;
int neighborMaxWidth = descriptor.width / tileSize;
int neighborMaxHeight = descriptor.height / tileSize;
cmd.GetTemporaryRT(neighborMax, neighborMaxWidth, neighborMaxHeight, 0, FilterMode.Point, m_VectorRTFormat, RenderTextureReadWrite.Linear);
cmd.SetGlobalTexture(Uniforms._MainTex, tile);
cmd.Blit(tile, neighborMax, material, (int)Pass.NeighborMax);
cmd.ReleaseTemporaryRT(tile);
// Set up for reconstruction (loop count)
cmd.SetGlobalFloat(Uniforms._LoopCount, Mathf.Clamp(sampleCount / 2, 1, 64));
cmd.ReleaseTemporaryRT(vbuffer);
cmd.ReleaseTemporaryRT(neighborMax);
}
// Non-RG version with RTHandle parameters
private void ExecuteKinoMotionBlur(CommandBuffer cmd, RenderTextureDescriptor descriptor,
float intensity, float rt_TargetLength, RTHandle sourceRT, RTHandle destinationRT, RTHandle copyBuffer)
{
// Execute core algorithm
ExecuteKinoMotionBlurCore(cmd, descriptor, intensity, rt_TargetLength);
// Copy the color buffer to use for sampling during the blur pass
cmd.Blit(sourceRT, copyBuffer);
// Pass 7 - Reconstruction pass
cmd.SetGlobalTexture(Uniforms._MainTex, copyBuffer);
cmd.Blit(copyBuffer, destinationRT, blurMaterial, (int)Pass.Reconstruction);
}
#if UNITY_6000_0_OR_NEWER
// RG version with TextureHandle parameters
private void ExecuteKinoMotionBlurRG(CommandBuffer cmd, RenderTextureDescriptor descriptor,
float intensity, float rt_TargetLength, TextureHandle sourceTexture, TextureHandle destinationTexture)
{
// Execute core algorithm
ExecuteKinoMotionBlurCore(cmd, descriptor, intensity, rt_TargetLength);
// Copy the color buffer to use for sampling during the blur pass
int copyRT = Shader.PropertyToID("_CopyTexture_RG");
cmd.GetTemporaryRT(copyRT, descriptor.width, descriptor.height, 0, FilterMode.Bilinear, descriptor.colorFormat);
cmd.Blit(sourceTexture, copyRT);
// Pass 7 - Reconstruction pass
cmd.SetGlobalTexture(Uniforms._MainTex, copyRT);
cmd.Blit(copyRT, destinationTexture, blurMaterial, (int)Pass.Reconstruction);
cmd.ReleaseTemporaryRT(copyRT);
}
#endif
/////////////////////////////////////////////////////////////////////
// NON-RG EXECUTE METHOD - Uses shared algorithm
/////////////////////////////////////////////////////////////////////
#if !UNITY_6000_4_OR_NEWER
#if UNITY_6000_0_OR_NEWER
[Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if(!ShouldRender(renderingData.cameraData.camera)) return;
if (blurMaterial == null || source == null)
{
Debug.LogError("Motion Blur: Missing required resources");
return;
}
CommandBuffer cmd = CommandBufferPool.Get(profilerTag);
using (new ProfilingScope(cmd, new ProfilingSampler(profilerTag)))
{
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
float rt_TargetLength = GetRTTargetLength(descriptor);
float intensity = GetIntensity();
Camera currentCam = renderingData.cameraData.camera;
bool thisFrameIsSafeToApplyMotionBlur = IsCameraSafeToApplyMotionBlur(
lastValidCameraViewProjMatrix,
currentCam.projectionMatrix * currentCam.worldToCameraMatrix,
lastValidCameraPosWS,
currentCam.transform.position);
lastValidCameraPosWS = currentCam.transform.position;
lastValidCameraViewProjMatrix = currentCam.projectionMatrix * currentCam.worldToCameraMatrix;
// debug
/*
if (lastFrameIsSafeToApplyMotionBlur == false && thisFrameIsSafeToApplyMotionBlur == false)
{
Debug.LogWarning("NiloToonMotionBlur 2 frames not safe to draw, is the auto detection too harsh?");
}
*/
lastFrameIsSafeToApplyMotionBlur = thisFrameIsSafeToApplyMotionBlur;
if (intensity > 0 && thisFrameIsSafeToApplyMotionBlur)
{
RTHandle destination = renderingData.cameraData.renderer.cameraColorTargetHandle;
// Call shared algorithm
ExecuteKinoMotionBlur(cmd, descriptor, intensity, rt_TargetLength, source, destination, copyRT);
}
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
#endif
public override void OnCameraCleanup(CommandBuffer cmd)
{
// do not write anything here
}
public void Dispose()
{
tempRT0?.Release();
tempRT1?.Release();
copyRT?.Release();
CoreUtils.Destroy(blurMaterial);
}
/////////////////////////////////////////////////////////////////////
// RG support
/////////////////////////////////////////////////////////////////////
#if UNITY_6000_0_OR_NEWER
// Render Graph data structures
private class MotionBlurPassData
{
public Material material;
public TextureHandle source;
public TextureHandle destination;
public float intensity;
public float rt_TargetLength;
public RenderTextureDescriptor descriptor;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameContext)
{
var cameraData = frameContext.Get<UniversalCameraData>();
var resourceData = frameContext.Get<UniversalResourceData>();
if (!ShouldRender(cameraData.camera))
return;
float intensity = GetIntensity();
var descriptor = cameraData.cameraTargetDescriptor;
float rt_TargetLength = GetRTTargetLength(descriptor);
Camera currentCam = cameraData.camera;
bool thisFrameIsSafeToApplyMotionBlur = IsCameraSafeToApplyMotionBlur(
lastValidCameraViewProjMatrix,
currentCam.projectionMatrix * currentCam.worldToCameraMatrix,
lastValidCameraPosWS,
currentCam.transform.position);
lastValidCameraPosWS = currentCam.transform.position;
lastValidCameraViewProjMatrix = currentCam.projectionMatrix * currentCam.worldToCameraMatrix;
lastFrameIsSafeToApplyMotionBlur = thisFrameIsSafeToApplyMotionBlur;
if (intensity <= 0 || !thisFrameIsSafeToApplyMotionBlur)
return;
using (var builder = renderGraph.AddUnsafePass<MotionBlurPassData>(profilerTag, out var passData))
{
passData.material = blurMaterial;
passData.source = resourceData.activeColorTexture;
passData.destination = resourceData.activeColorTexture;
passData.intensity = intensity;
passData.rt_TargetLength = rt_TargetLength;
passData.descriptor = descriptor;
builder.UseTexture(resourceData.activeColorTexture, AccessFlags.Read);
builder.UseTexture(resourceData.activeColorTexture, AccessFlags.Write);
builder.SetRenderFunc((MotionBlurPassData data, UnsafeGraphContext context) =>
{
ExecuteMotionBlurRenderGraph(data, context);
});
}
}
private void ExecuteMotionBlurRenderGraph(MotionBlurPassData data, UnsafeGraphContext context)
{
var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
// Call shared algorithm - RG version
ExecuteKinoMotionBlurRG(cmd, data.descriptor, data.intensity, data.rt_TargetLength,
data.source, data.destination);
}
#endif
#else
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// do nothing in Unity2021.3 (NiloToonMotionBlur requires Unity2022.3)
}
#endif
}
}