560 lines
28 KiB
C#

using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
namespace YAMO
{
public class NiloToonCharToneAdjustFeature : ScriptableRendererFeature
{
public enum DebugMode
{
[InspectorName("Character Only (스텐실 마스킹)")]
Normal,
[InspectorName("Full Screen (전체 화면, 디버그)")]
FullScreen,
[InspectorName("Stencil View (마스크 시각화, 디버그)")]
StencilView,
}
[System.Serializable]
public class Settings
{
[Tooltip("YAMO/NiloToonCharToneAdjust 셰이더를 할당해주세요.")]
public Shader shader;
[Space]
[Tooltip("Normal: 캐릭터 픽셀에만 적용 (Pass 1 + StencilFill)\n" +
"FullScreen: 전체 화면에 적용 (Pass 2, 디버그)\n" +
"StencilView: 스텐실 마스크를 빨간 오버레이로 시각화 (디버그)")]
public DebugMode debugMode = DebugMode.Normal;
}
public Settings settings = new Settings();
CharToneAdjustPass _pass;
// ── Auto Match 원샷 요청 (에디터에서 설정, Pass에서 처리) ────
public static bool autoMatchRequested;
public static NiloToonCharToneAdjustVolume autoMatchTarget;
public static float autoMatchBrightnessStrength = 0.5f;
public static float autoMatchTintStrength = 0.5f;
public static float autoMatchSaturationStrength = 0.5f;
public override void Create()
{
if (settings.shader == null)
{
Debug.LogWarning("[NiloToonCharToneAdjust] Shader이 할당되지 않았습니다.");
return;
}
_pass = new CharToneAdjustPass(settings.shader);
_pass.renderPassEvent = (RenderPassEvent)((int)RenderPassEvent.AfterRenderingTransparents + 1);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (_pass == null) return;
var cameraType = renderingData.cameraData.cameraType;
if (cameraType == CameraType.Preview || cameraType == CameraType.Reflection) return;
var volume = VolumeManager.instance.stack.GetComponent<NiloToonCharToneAdjustVolume>();
if (volume == null || !volume.IsActive()) return;
_pass.debugMode = settings.debugMode;
renderer.EnqueuePass(_pass);
}
protected override void Dispose(bool disposing) => _pass?.Dispose();
// ══════════════════════════════════════════════════════════════
class CharToneAdjustPass : ScriptableRenderPass
{
static readonly ShaderTagId _stencilFillTagId =
new ShaderTagId("NiloToonCharacterAreaStencilBufferFill");
readonly Material _material;
// ── Pass 인덱스 ─────────────────────────────────────────
const int k_PassCopy = 0;
const int k_PassToneAdjust = 1;
const int k_PassToneAdjustFull = 2;
const int k_PassStencilView = 3;
const int k_PassDebugStencilFill = 4;
const int k_PassMeshStencilFill = 5;
const int k_PassMaskedDownsample = 7;
const int k_PassWeightedDownsample = 8;
const int k_PassStencilToMask = 9;
public DebugMode debugMode = DebugMode.Normal;
// ── 셰이더 프로퍼티 ID ──────────────────────────────────
static readonly int _MainTexId = Shader.PropertyToID("_MainTex");
static readonly int _MainTex_TexelSizeId = Shader.PropertyToID("_MainTex_TexelSize");
static readonly int _MaskTexId = Shader.PropertyToID("_MaskTex");
static readonly int _InvertMaskId = Shader.PropertyToID("_InvertMask");
static readonly int _LiftId = Shader.PropertyToID("_CharToneAdjust_Lift");
static readonly int _GammaId = Shader.PropertyToID("_CharToneAdjust_Gamma");
static readonly int _GainId = Shader.PropertyToID("_CharToneAdjust_Gain");
static readonly int _ShadowsId = Shader.PropertyToID("_CharToneAdjust_Shadows");
static readonly int _MidtonesId = Shader.PropertyToID("_CharToneAdjust_Midtones");
static readonly int _HighlightsId = Shader.PropertyToID("_CharToneAdjust_Highlights");
static readonly int _SMHRangeId = Shader.PropertyToID("_CharToneAdjust_SMHRange");
static readonly int _SaturationId = Shader.PropertyToID("_CharToneAdjust_Saturation");
static readonly int _PostExposureId = Shader.PropertyToID("_CharToneAdjust_PostExposure");
static readonly int _BlendAmountId = Shader.PropertyToID("_CharToneAdjust_BlendAmount");
static readonly Vector3 LumaW = new Vector3(0.2126729f, 0.7151522f, 0.0721750f);
// ── 원샷 분석 상태 ──────────────────────────────────────
enum AnalysisPhase { Idle, Running, ReadbackPending }
AnalysisPhase _analysisPhase = AnalysisPhase.Idle;
int _analysisStartFrame;
RenderTexture _charResult1x1;
RenderTexture _bgResult1x1;
Color _capturedCharAvg;
Color _capturedBgAvg;
int _readbackCount;
bool _charValid, _bgValid;
// ── PassData ────────────────────────────────────────────
class PassData
{
public TextureHandle colorHandle;
public TextureHandle depthHandle;
public TextureHandle tempHandle;
public RendererListHandle stencilList;
public Material material;
public NiloToonCharToneAdjustVolume volume;
public DebugMode debugMode;
// 원샷 분석 (Match 버튼)
public bool runAnalysis;
public TextureHandle maskHandle;
public TextureHandle down64, down16, down4;
public RenderTexture charResult1x1, bgResult1x1;
public int cameraWidth, cameraHeight;
}
// ── 생성/파기 ───────────────────────────────────────────
public CharToneAdjustPass(Shader shader)
{
_material = CoreUtils.CreateEngineMaterial(shader);
}
public void Dispose()
{
CoreUtils.Destroy(_material);
if (_charResult1x1 != null) { _charResult1x1.Release(); Object.DestroyImmediate(_charResult1x1); }
if (_bgResult1x1 != null) { _bgResult1x1.Release(); Object.DestroyImmediate(_bgResult1x1); }
}
void EnsureAnalysisRTs()
{
if (_charResult1x1 != null) return;
var desc = new RenderTextureDescriptor(1, 1, RenderTextureFormat.ARGBFloat, 0);
_charResult1x1 = new RenderTexture(desc) { name = "_CharResult1x1", filterMode = FilterMode.Point };
_bgResult1x1 = new RenderTexture(desc) { name = "_BgResult1x1", filterMode = FilterMode.Point };
_charResult1x1.Create();
_bgResult1x1.Create();
}
// ── 원샷 리드백 요청 ───────────────────────────────────
void RequestOneShotReadback()
{
_readbackCount = 0;
_charValid = false;
_bgValid = false;
_capturedCharAvg = Color.black;
_capturedBgAvg = Color.black;
AsyncGPUReadback.Request(_charResult1x1, 0, req =>
{
if (!req.hasError)
{
var data = req.GetData<Color>();
if (data.Length > 0 && data[0].a > 0.001f)
{
_capturedCharAvg = data[0];
_charValid = true;
}
}
_readbackCount++;
if (_readbackCount >= 2) OnReadbackComplete();
});
AsyncGPUReadback.Request(_bgResult1x1, 0, req =>
{
if (!req.hasError)
{
var data = req.GetData<Color>();
if (data.Length > 0 && data[0].a > 0.001f)
{
_capturedBgAvg = data[0];
_bgValid = true;
}
}
_readbackCount++;
if (_readbackCount >= 2) OnReadbackComplete();
});
}
// ── 리드백 완료 → 값 적용 ─────────────────────────────
void OnReadbackComplete()
{
_analysisPhase = AnalysisPhase.Idle;
if (!_charValid || !_bgValid)
{
Debug.LogWarning("[CharToneAdjust] Auto Match: 리드백 실패 (유효 픽셀 부족)");
autoMatchTarget = null;
return;
}
var volume = autoMatchTarget;
autoMatchTarget = null;
if (volume == null) return;
float sBright = autoMatchBrightnessStrength;
float sTint = autoMatchTintStrength;
float sSat = autoMatchSaturationStrength;
Vector3 cRGB = new Vector3(_capturedCharAvg.r, _capturedCharAvg.g, _capturedCharAvg.b);
Vector3 bRGB = new Vector3(_capturedBgAvg.r, _capturedBgAvg.g, _capturedBgAvg.b);
float cLum = Vector3.Dot(cRGB, LumaW);
float bLum = Vector3.Dot(bRGB, LumaW);
Debug.Log($"[CharToneAdjust AutoMatch] char=({cRGB.x:F3},{cRGB.y:F3},{cRGB.z:F3}) lum={cLum:F3}" +
$" | bg=({bRGB.x:F3},{bRGB.y:F3},{bRGB.z:F3}) lum={bLum:F3}" +
$" | strength B={sBright:F2} T={sTint:F2} S={sSat:F2}");
if (cLum < 1e-4f || bLum < 1e-4f)
{
Debug.LogWarning("[CharToneAdjust] Auto Match: 휘도가 너무 낮아 보정 불가");
return;
}
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(volume, "Auto Match Tone Adjust");
#endif
// 밝기 보정 → PostExposure (EV)
if (sBright > 0.001f)
{
float ev = Mathf.Log(bLum / cLum, 2f) * sBright;
volume.postExposure.value = Mathf.Clamp(ev, -5f, 5f);
volume.postExposure.overrideState = true;
}
// 색조 보정 → Midtones
if (sTint > 0.001f)
{
Vector3 cTint = cRGB / cLum;
Vector3 bTint = bRGB / bLum;
Vector3 diff = (bTint - cTint) * sTint;
volume.midtones.value = new Vector4(
1f + Mathf.Clamp(diff.x, -0.5f, 0.5f),
1f + Mathf.Clamp(diff.y, -0.5f, 0.5f),
1f + Mathf.Clamp(diff.z, -0.5f, 0.5f), 0f);
volume.midtones.overrideState = true;
}
// 채도 보정 → Saturation
if (sSat > 0.001f)
{
float cChroma = (Mathf.Max(cRGB.x, cRGB.y, cRGB.z) -
Mathf.Min(cRGB.x, cRGB.y, cRGB.z)) / Mathf.Max(cLum, 1e-4f);
float bChroma = (Mathf.Max(bRGB.x, bRGB.y, bRGB.z) -
Mathf.Min(bRGB.x, bRGB.y, bRGB.z)) / Mathf.Max(bLum, 1e-4f);
float satRatio = bChroma / Mathf.Max(cChroma, 1e-4f);
volume.saturation.value = Mathf.Clamp(Mathf.Lerp(1f, satRatio, sSat), 0f, 2f);
volume.saturation.overrideState = true;
}
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(volume);
#endif
Debug.Log($"[CharToneAdjust AutoMatch] 적용 완료 — " +
$"PostExposure={volume.postExposure.value:F3}, " +
$"Midtones={volume.midtones.value}, " +
$"Saturation={volume.saturation.value:F3}");
}
// ── RecordRenderGraph ───────────────────────────────────
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
if (_material == null) return;
var resourceData = frameData.Get<UniversalResourceData>();
var renderingData = frameData.Get<UniversalRenderingData>();
var cameraData = frameData.Get<UniversalCameraData>();
if (cameraData.camera.cameraType == CameraType.Preview) return;
var volume = VolumeManager.instance.stack.GetComponent<NiloToonCharToneAdjustVolume>();
if (volume == null || !volume.IsActive()) return;
// ── 원샷 분석: 대상 카메라 판별 ────────────────────
// Play 모드 → Game 카메라, Edit 모드 → SceneView 카메라
var camType = cameraData.camera.cameraType;
bool isTargetCamera = Application.isPlaying
? camType == CameraType.Game
: camType == CameraType.SceneView;
// ── 원샷 분석 상태 머신 ────────────────────────────
if (_analysisPhase == AnalysisPhase.Running && isTargetCamera)
{
if (Time.frameCount >= _analysisStartFrame + 2)
{
RequestOneShotReadback();
_analysisPhase = AnalysisPhase.ReadbackPending;
}
#if UNITY_EDITOR
else
{
UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
}
#endif
}
// 새 요청 수락 (대상 카메라 + Idle 상태 + FullScreen이 아닐 때)
bool runAnalysis = false;
if (autoMatchRequested && _analysisPhase == AnalysisPhase.Idle
&& debugMode != DebugMode.FullScreen && isTargetCamera)
{
autoMatchRequested = false;
runAnalysis = true;
_analysisPhase = AnalysisPhase.Running;
_analysisStartFrame = Time.frameCount;
EnsureAnalysisRTs();
}
var colorHandle = resourceData.activeColorTexture;
var depthHandle = resourceData.activeDepthTexture;
var camDesc = cameraData.cameraTargetDescriptor;
// 임시 컬러 텍스처
var tempHandle = renderGraph.CreateTexture(new TextureDesc(camDesc.width, camDesc.height)
{
format = camDesc.graphicsFormat,
filterMode = FilterMode.Bilinear,
name = "_CharToneAdjustTemp",
});
// 스텐실 재마킹용 RendererList (항상 Pass 5: stencil only)
var sortSettings = new SortingSettings(cameraData.camera) { criteria = SortingCriteria.CommonOpaque };
var drawSettings = new DrawingSettings(_stencilFillTagId, sortSettings)
{
overrideMaterial = _material,
overrideMaterialPassIndex = k_PassMeshStencilFill,
};
var filterSettings = new FilteringSettings(RenderQueueRange.opaque);
var rlParams = new RendererListParams(renderingData.cullResults, drawSettings, filterSettings);
var stencilList = renderGraph.CreateRendererList(rlParams);
// 분석용 텍스처 (Match 요청 시에만 생성)
TextureHandle maskHandle = TextureHandle.nullHandle;
TextureHandle down64 = TextureHandle.nullHandle;
TextureHandle down16 = TextureHandle.nullHandle;
TextureHandle down4 = TextureHandle.nullHandle;
if (runAnalysis)
{
maskHandle = renderGraph.CreateTexture(new TextureDesc(camDesc.width, camDesc.height)
{
format = GraphicsFormat.R8_UNorm,
filterMode = FilterMode.Bilinear,
name = "_CharMask",
});
var fmt = GraphicsFormat.R16G16B16A16_SFloat;
down64 = renderGraph.CreateTexture(new TextureDesc(64, 64) { format = fmt, filterMode = FilterMode.Bilinear, name = "_Down64" });
down16 = renderGraph.CreateTexture(new TextureDesc(16, 16) { format = fmt, filterMode = FilterMode.Bilinear, name = "_Down16" });
down4 = renderGraph.CreateTexture(new TextureDesc(4, 4) { format = fmt, filterMode = FilterMode.Bilinear, name = "_Down4" });
}
// ── UnsafePass 등록 ─────────────────────────────────
using (var builder = renderGraph.AddUnsafePass<PassData>(
"NiloToonCharToneAdjust", out var pd))
{
pd.colorHandle = colorHandle;
pd.depthHandle = depthHandle;
pd.tempHandle = tempHandle;
pd.stencilList = stencilList;
pd.material = _material;
pd.volume = volume;
pd.debugMode = debugMode;
pd.runAnalysis = runAnalysis;
pd.maskHandle = maskHandle;
pd.down64 = down64;
pd.down16 = down16;
pd.down4 = down4;
pd.charResult1x1 = _charResult1x1;
pd.bgResult1x1 = _bgResult1x1;
pd.cameraWidth = camDesc.width;
pd.cameraHeight = camDesc.height;
builder.UseTexture(colorHandle, AccessFlags.ReadWrite);
builder.UseTexture(depthHandle, AccessFlags.ReadWrite);
builder.UseTexture(tempHandle, AccessFlags.ReadWrite);
builder.UseRendererList(stencilList);
if (runAnalysis)
{
builder.UseTexture(maskHandle, AccessFlags.ReadWrite);
builder.UseTexture(down64, AccessFlags.ReadWrite);
builder.UseTexture(down16, AccessFlags.ReadWrite);
builder.UseTexture(down4, AccessFlags.ReadWrite);
}
builder.AllowPassCulling(false);
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext ctx) =>
{
var cmd = ctx.cmd;
SetMaterialProperties(data.volume, data.material);
if (data.debugMode == DebugMode.FullScreen)
ExecuteFullScreen(data, cmd);
else
ExecuteStencilMode(data, cmd);
});
}
}
// ── FullScreen 모드 렌더링 ──────────────────────────────
static void ExecuteFullScreen(PassData data, UnsafeCommandBuffer cmd)
{
cmd.SetRenderTarget(data.tempHandle,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.colorHandle);
cmd.DrawProcedural(Matrix4x4.identity, data.material,
k_PassCopy, MeshTopology.Triangles, 3);
cmd.SetRenderTarget(data.colorHandle,
RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.tempHandle);
cmd.DrawProcedural(Matrix4x4.identity, data.material,
k_PassToneAdjustFull, MeshTopology.Triangles, 3);
}
// ── Stencil(Normal/StencilView) 모드 렌더링 ─────────────
static void ExecuteStencilMode(PassData data, UnsafeCommandBuffer cmd)
{
// ── Step 1: 스텐실 재마킹 (Pass 5, ColorMask 0) ────
cmd.SetRenderTarget(
data.colorHandle,
RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,
data.depthHandle,
RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
cmd.DrawRendererList(data.stencilList);
// ── Step 1b+2: 원샷 분석 (Match 버튼 요청 시) ──────
if (data.runAnalysis)
{
// 스텐실 → 마스크 변환 (Pass 9)
cmd.SetRenderTarget(
data.maskHandle,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store,
data.depthHandle,
RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
cmd.ClearRenderTarget(false, true, Color.clear);
cmd.DrawProcedural(Matrix4x4.identity, data.material,
k_PassStencilToMask, MeshTopology.Triangles, 3);
// 캐릭터 분석: full-res → 64 → 16 → 4 → 1
RunAnalysisChain(data, cmd, invertMask: 0f, resultRT: data.charResult1x1);
// 배경 분석: full-res → 64 → 16 → 4 → 1
RunAnalysisChain(data, cmd, invertMask: 1f, resultRT: data.bgResult1x1);
}
// ── Step 3: color → temp 복사 ───────────────────────
cmd.SetRenderTarget(data.tempHandle,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.colorHandle);
cmd.DrawProcedural(Matrix4x4.identity, data.material,
k_PassCopy, MeshTopology.Triangles, 3);
// ── Step 4: temp → color 색조 보정 (스텐실) ─────────
int passIdx = data.debugMode == DebugMode.StencilView
? k_PassStencilView : k_PassToneAdjust;
cmd.SetRenderTarget(
data.colorHandle,
RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,
data.depthHandle,
RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.tempHandle);
cmd.DrawProcedural(Matrix4x4.identity, data.material,
passIdx, MeshTopology.Triangles, 3);
}
// ── 분석 다운샘플 체인 ──────────────────────────────────
static void RunAnalysisChain(PassData data, UnsafeCommandBuffer cmd,
float invertMask, RenderTexture resultRT)
{
var mat = data.material;
// 첫 단계: MaskedDownsample (full-res → 64x64)
cmd.SetGlobalVector(_MainTex_TexelSizeId,
new Vector4(1f / data.cameraWidth, 1f / data.cameraHeight,
data.cameraWidth, data.cameraHeight));
cmd.SetRenderTarget(data.down64,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.colorHandle);
cmd.SetGlobalTexture(_MaskTexId, data.maskHandle);
cmd.SetGlobalFloat(_InvertMaskId, invertMask);
cmd.DrawProcedural(Matrix4x4.identity, mat,
k_PassMaskedDownsample, MeshTopology.Triangles, 3);
// 64 → 16
cmd.SetGlobalVector(_MainTex_TexelSizeId, new Vector4(1f/64, 1f/64, 64, 64));
cmd.SetRenderTarget(data.down16,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.down64);
cmd.DrawProcedural(Matrix4x4.identity, mat,
k_PassWeightedDownsample, MeshTopology.Triangles, 3);
// 16 → 4
cmd.SetGlobalVector(_MainTex_TexelSizeId, new Vector4(1f/16, 1f/16, 16, 16));
cmd.SetRenderTarget(data.down4,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.down16);
cmd.DrawProcedural(Matrix4x4.identity, mat,
k_PassWeightedDownsample, MeshTopology.Triangles, 3);
// 4 → 1 (persistent RT)
cmd.SetGlobalVector(_MainTex_TexelSizeId, new Vector4(1f/4, 1f/4, 4, 4));
cmd.SetRenderTarget(resultRT,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
cmd.SetGlobalTexture(_MainTexId, data.down4);
cmd.DrawProcedural(Matrix4x4.identity, mat,
k_PassWeightedDownsample, MeshTopology.Triangles, 3);
}
// ── 셰이더 프로퍼티 설정 ───────────────────────────────
static void SetMaterialProperties(NiloToonCharToneAdjustVolume v, Material mat)
{
Vector4 lv = v.lift.value;
mat.SetVector(_LiftId, new Vector4(lv.x-1f+lv.w, lv.y-1f+lv.w, lv.z-1f+lv.w, 0f));
Vector4 gv = v.gamma.value;
mat.SetVector(_GammaId, new Vector4(gv.x+gv.w, gv.y+gv.w, gv.z+gv.w, 0f));
Vector4 gn = v.gain.value;
mat.SetVector(_GainId, new Vector4(gn.x+gn.w, gn.y+gn.w, gn.z+gn.w, 0f));
Vector4 sv = v.shadows.value;
mat.SetVector(_ShadowsId, new Vector4(sv.x-1f+sv.w, sv.y-1f+sv.w, sv.z-1f+sv.w, 0f));
Vector4 mv = v.midtones.value;
mat.SetVector(_MidtonesId, new Vector4(mv.x-1f+mv.w, mv.y-1f+mv.w, mv.z-1f+mv.w, 0f));
Vector4 hv = v.highlights.value;
mat.SetVector(_HighlightsId, new Vector4(hv.x-1f+hv.w, hv.y-1f+hv.w, hv.z-1f+hv.w, 0f));
mat.SetVector(_SMHRangeId, new Vector4(
v.shadowsStart.value, v.shadowsEnd.value,
v.highlightsStart.value, v.highlightsEnd.value));
mat.SetFloat(_SaturationId, v.saturation.value);
mat.SetFloat(_PostExposureId, Mathf.Pow(2f, v.postExposure.value));
mat.SetFloat(_BlendAmountId, v.blendAmount.value);
}
}
}
}