ADD: 레드 리허설용 스테이지 작업 및 이벤트 컨트롤러 컴포넌트 복제 기능 추가

This commit is contained in:
DESKTOP-S4BOTN2\user 2026-04-13 19:26:35 +09:00
parent 1de279a021
commit 5d1b542820
45 changed files with 33813 additions and 6 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 83eec10194d09eb4c968599d5a154ffb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7ee348a10351f7f41bc4416828bc4666
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5c4d7ba1f8af30a40a499ccc2210055b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 61429f289c8fa5743b248c48b7fab962
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 4c30841c76708424585a1354c9f1842f
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: f922f0a6116f7704297341c45731aba5
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: a1b0e1b400773c944bfdc0f2db94d30f
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: c6273981a72f963458cbe705349f209e
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c6ef3a50bdbbce74a96224a72f6a0e88
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 709fedd9361df0c44ac24187cc866b98
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2bc6c5421d4bcf742acb5312b6cd76dc
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a851b0ec32f68f24b8ab9955f9c07a89
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba6c4f43c9b32974fb7b3b70dea4b321
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7644bb67b9a95e142954a2ea19383d78
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 42b19884e57192744859e9bcbbf9bfa8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -127,6 +127,11 @@ public class EventControllerEditor : Editor
downBtn.SetEnabled(index < controller.eventGroups.Count - 1);
headerRow.Add(downBtn);
var duplicateBtn = new Button(() => DuplicateEvent(idx)) { text = "\u2398" };
duplicateBtn.tooltip = "이벤트 복제";
duplicateBtn.AddToClassList("list-reorder-btn");
headerRow.Add(duplicateBtn);
var deleteBtn = new Button(() => DeleteEvent(idx)) { text = "X" };
deleteBtn.AddToClassList("list-delete-btn");
headerRow.Add(deleteBtn);
@ -165,6 +170,76 @@ public class EventControllerEditor : Editor
RebuildEventList();
}
private void DuplicateEvent(int index)
{
serializedObject.Update();
var listProp = serializedObject.FindProperty("eventGroups");
// 배열 끝에 새 요소 추가 후, 원본의 SerializedProperty 값을 복사
int newIndex = listProp.arraySize;
listProp.InsertArrayElementAtIndex(newIndex);
var srcElement = listProp.GetArrayElementAtIndex(index);
var dstElement = listProp.GetArrayElementAtIndex(newIndex);
// groupName 복사 (접미사 추가)
var srcName = srcElement.FindPropertyRelative("groupName");
var dstName = dstElement.FindPropertyRelative("groupName");
dstName.stringValue = srcName.stringValue + " (복사)";
// UnityEvent persistent calls 복사
var srcEvent = srcElement.FindPropertyRelative("unityEvent");
var dstEvent = dstElement.FindPropertyRelative("unityEvent");
CopySerializedProperty(srcEvent, dstEvent);
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
RebuildEventList();
}
private void CopySerializedProperty(SerializedProperty src, SerializedProperty dst)
{
var srcCalls = src.FindPropertyRelative("m_PersistentCalls.m_Calls");
var dstCalls = dst.FindPropertyRelative("m_PersistentCalls.m_Calls");
if (srcCalls == null || dstCalls == null) return;
dstCalls.ClearArray();
for (int i = 0; i < srcCalls.arraySize; i++)
{
dstCalls.InsertArrayElementAtIndex(i);
var srcCall = srcCalls.GetArrayElementAtIndex(i);
var dstCall = dstCalls.GetArrayElementAtIndex(i);
dstCall.FindPropertyRelative("m_Target").objectReferenceValue =
srcCall.FindPropertyRelative("m_Target").objectReferenceValue;
dstCall.FindPropertyRelative("m_MethodName").stringValue =
srcCall.FindPropertyRelative("m_MethodName").stringValue;
dstCall.FindPropertyRelative("m_Mode").intValue =
srcCall.FindPropertyRelative("m_Mode").intValue;
dstCall.FindPropertyRelative("m_CallState").intValue =
srcCall.FindPropertyRelative("m_CallState").intValue;
// Arguments
var srcArgs = srcCall.FindPropertyRelative("m_Arguments");
var dstArgs = dstCall.FindPropertyRelative("m_Arguments");
if (srcArgs != null && dstArgs != null)
{
dstArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue =
srcArgs.FindPropertyRelative("m_ObjectArgument").objectReferenceValue;
dstArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue =
srcArgs.FindPropertyRelative("m_ObjectArgumentAssemblyTypeName").stringValue;
dstArgs.FindPropertyRelative("m_IntArgument").intValue =
srcArgs.FindPropertyRelative("m_IntArgument").intValue;
dstArgs.FindPropertyRelative("m_FloatArgument").floatValue =
srcArgs.FindPropertyRelative("m_FloatArgument").floatValue;
dstArgs.FindPropertyRelative("m_StringArgument").stringValue =
srcArgs.FindPropertyRelative("m_StringArgument").stringValue;
dstArgs.FindPropertyRelative("m_BoolArgument").boolValue =
srcArgs.FindPropertyRelative("m_BoolArgument").boolValue;
}
}
}
private void SwapEvents(int a, int b)
{
Undo.RecordObject(target, "Reorder Event Groups");

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e170c0ef61802649a846b7d9faeeba0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 65f6d8f0e8059054cbf61425189a1f30
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,192 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEngine.Rendering;
namespace YAMO
{
[CustomEditor(typeof(NiloToonCharToneAdjustVolume))]
sealed class NiloToonCharToneAdjustVolumeEditor : VolumeComponentEditor
{
// Lift Gamma Gain
SerializedDataParameter _lift;
SerializedDataParameter _gamma;
SerializedDataParameter _gain;
// Shadows Midtones Highlights
SerializedDataParameter _shadows;
SerializedDataParameter _midtones;
SerializedDataParameter _highlights;
SerializedDataParameter _shadowsStart;
SerializedDataParameter _shadowsEnd;
SerializedDataParameter _highlightsStart;
SerializedDataParameter _highlightsEnd;
// Color Adjustments
SerializedDataParameter _saturation;
SerializedDataParameter _postExposure;
// Blend
SerializedDataParameter _blendAmount;
// Auto Match (에디터 전용 설정)
float _brightnessStrength = 0.5f;
float _tintStrength = 0.5f;
float _saturationStrength = 0.5f;
bool _matchFoldout = true;
readonly YAMOTrackballUIDrawer _trackball = new YAMOTrackballUIDrawer();
static class Styles
{
public static readonly GUIContent liftLabel = EditorGUIUtility.TrTextContent("Lift");
public static readonly GUIContent gammaLabel = EditorGUIUtility.TrTextContent("Gamma");
public static readonly GUIContent gainLabel = EditorGUIUtility.TrTextContent("Gain");
public static readonly GUIContent shadowsLabel = EditorGUIUtility.TrTextContent("Shadows");
public static readonly GUIContent midtonesLabel = EditorGUIUtility.TrTextContent("Midtones");
public static readonly GUIContent highlightsLabel = EditorGUIUtility.TrTextContent("Highlights");
}
public override void OnEnable()
{
var o = new PropertyFetcher<NiloToonCharToneAdjustVolume>(serializedObject);
_lift = Unpack(o.Find(x => x.lift));
_gamma = Unpack(o.Find(x => x.gamma));
_gain = Unpack(o.Find(x => x.gain));
_shadows = Unpack(o.Find(x => x.shadows));
_midtones = Unpack(o.Find(x => x.midtones));
_highlights = Unpack(o.Find(x => x.highlights));
_shadowsStart = Unpack(o.Find(x => x.shadowsStart));
_shadowsEnd = Unpack(o.Find(x => x.shadowsEnd));
_highlightsStart = Unpack(o.Find(x => x.highlightsStart));
_highlightsEnd = Unpack(o.Find(x => x.highlightsEnd));
_saturation = Unpack(o.Find(x => x.saturation));
_postExposure = Unpack(o.Find(x => x.postExposure));
_blendAmount = Unpack(o.Find(x => x.blendAmount));
}
public override void OnInspectorGUI()
{
// ── Lift Gamma Gain ──────────────────────────────────────
EditorGUILayout.LabelField("Lift Gamma Gain", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
_trackball.OnGUI(_lift.value, _lift.overrideState, Styles.liftLabel, GetLiftValue);
GUILayout.Space(4f);
_trackball.OnGUI(_gamma.value, _gamma.overrideState, Styles.gammaLabel, GetGammaValue);
GUILayout.Space(4f);
_trackball.OnGUI(_gain.value, _gain.overrideState, Styles.gainLabel, GetGainValue);
}
EditorGUILayout.Space(4f);
// ── Shadows Midtones Highlights ──────────────────────────
EditorGUILayout.LabelField("Shadows Midtones Highlights", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
_trackball.OnGUI(_shadows.value, _shadows.overrideState, Styles.shadowsLabel, GetSMHValue);
GUILayout.Space(4f);
_trackball.OnGUI(_midtones.value, _midtones.overrideState, Styles.midtonesLabel, GetSMHValue);
GUILayout.Space(4f);
_trackball.OnGUI(_highlights.value, _highlights.overrideState, Styles.highlightsLabel, GetSMHValue);
}
EditorGUILayout.Space(2f);
EditorGUILayout.LabelField("SMH Range", EditorStyles.boldLabel);
PropertyField(_shadowsStart);
PropertyField(_shadowsEnd);
PropertyField(_highlightsStart);
PropertyField(_highlightsEnd);
EditorGUILayout.Space(4f);
// ── Color Adjustments ────────────────────────────────────
EditorGUILayout.LabelField("Color Adjustments", EditorStyles.boldLabel);
PropertyField(_saturation);
PropertyField(_postExposure);
EditorGUILayout.Space(4f);
// ── Blend ────────────────────────────────────────────────
EditorGUILayout.LabelField("Blend", EditorStyles.boldLabel);
PropertyField(_blendAmount);
EditorGUILayout.Space(8f);
// ── Reset ───────────────────────────────────────────────
if (GUILayout.Button("Reset All Parameters"))
{
var vol = (NiloToonCharToneAdjustVolume)target;
Undo.RecordObject(vol, "Reset Char Tone Adjust");
vol.lift.value = new Vector4(1f, 1f, 1f, 0f);
vol.gamma.value = new Vector4(1f, 1f, 1f, 0f);
vol.gain.value = new Vector4(1f, 1f, 1f, 0f);
vol.shadows.value = new Vector4(1f, 1f, 1f, 0f);
vol.midtones.value = new Vector4(1f, 1f, 1f, 0f);
vol.highlights.value = new Vector4(1f, 1f, 1f, 0f);
vol.shadowsStart.value = 0f;
vol.shadowsEnd.value = 0.3f;
vol.highlightsStart.value = 0.55f;
vol.highlightsEnd.value = 1f;
vol.saturation.value = 1f;
vol.postExposure.value = 0f;
vol.blendAmount.value = 1f;
EditorUtility.SetDirty(vol);
}
EditorGUILayout.Space(8f);
// ── Auto Match ──────────────────────────────────────────
_matchFoldout = EditorGUILayout.Foldout(_matchFoldout, "Auto Match", true, EditorStyles.foldoutHeader);
if (_matchFoldout)
{
EditorGUI.indentLevel++;
_brightnessStrength = EditorGUILayout.Slider("Brightness", _brightnessStrength, 0f, 1f);
_tintStrength = EditorGUILayout.Slider("Tint", _tintStrength, 0f, 1f);
_saturationStrength = EditorGUILayout.Slider("Saturation", _saturationStrength, 0f, 1f);
EditorGUILayout.Space(4f);
EditorGUILayout.HelpBox(
"현재 프레임의 캐릭터/배경 색상을 분석하여\n" +
"PostExposure, Midtones, Saturation 값을 자동 설정합니다.\n" +
"Blend Amount로 적용 강도를 조절하세요.",
MessageType.Info);
if (GUILayout.Button("Match", GUILayout.Height(30f)))
{
NiloToonCharToneAdjustFeature.autoMatchRequested = true;
NiloToonCharToneAdjustFeature.autoMatchTarget =
(NiloToonCharToneAdjustVolume)target;
NiloToonCharToneAdjustFeature.autoMatchBrightnessStrength = _brightnessStrength;
NiloToonCharToneAdjustFeature.autoMatchTintStrength = _tintStrength;
NiloToonCharToneAdjustFeature.autoMatchSaturationStrength = _saturationStrength;
// 에디터 모드에서도 렌더링이 실행되도록 리페인트 요청
SceneView.RepaintAll();
}
EditorGUI.indentLevel--;
}
}
// TrackballUIDrawer에 전달되는 표시값 계산 함수
static Vector3 GetLiftValue(Vector4 v) =>
new Vector3(v.x - 1f + v.w, v.y - 1f + v.w, v.z - 1f + v.w);
static Vector3 GetGammaValue(Vector4 v) =>
new Vector3(v.x + v.w, v.y + v.w, v.z + v.w);
static Vector3 GetGainValue(Vector4 v) =>
new Vector3(v.x + v.w, v.y + v.w, v.z + v.w);
static Vector3 GetSMHValue(Vector4 v) =>
new Vector3(v.x - 1f + v.w, v.y - 1f + v.w, v.z - 1f + v.w);
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3e4332b2aba26f04e9eb62b08650f123

View File

@ -0,0 +1,224 @@
// URP 내장 TrackballUIDrawer(internal)의 복사본.
// Hidden/Universal Render Pipeline/Editor/Trackball 셰이더를 재사용합니다.
using System;
using UnityEngine;
using UnityEditor;
using UnityEditor.Rendering;
namespace YAMO
{
sealed class YAMOTrackballUIDrawer
{
static readonly int s_ThumbHash = "yamoColorWheelThumb".GetHashCode();
static GUIStyle s_WheelThumb;
static Vector2 s_WheelThumbSize;
static Material s_Material;
Func<Vector4, Vector3> m_ComputeFunc;
bool m_ResetState;
Vector2 m_CursorPos;
const string k_ShaderName = "Hidden/Universal Render Pipeline/Editor/Trackball";
public void OnGUI(SerializedProperty property, SerializedProperty overrideState,
GUIContent title, Func<Vector4, Vector3> computeFunc)
{
if (!CheckMaterialAndShader()) return;
if (property.propertyType != SerializedPropertyType.Vector4)
{
EditorGUILayout.HelpBox("TrackballUIDrawer requires Vector4 property.", MessageType.Warning);
return;
}
m_ComputeFunc = computeFunc;
var value = property.vector4Value;
using (new EditorGUILayout.VerticalScope())
{
bool isOverridden = overrideState?.boolValue ?? true;
using (new EditorGUI.DisabledScope(!isOverridden))
DrawWheel(ref value, isOverridden);
DrawLabelAndOverride(title, overrideState);
}
if (m_ResetState)
{
value = new Vector4(1f, 1f, 1f, 0f);
m_ResetState = false;
}
property.vector4Value = value;
}
void DrawWheel(ref Vector4 value, bool overrideState)
{
var wheelRect = GUILayoutUtility.GetAspectRect(1f);
float size = wheelRect.width;
float hsize = size / 2f;
float radius = 0.38f * size;
Color.RGBToHSV(value, out float h, out float s, out float _);
float offset = value.w;
var thumbPos = Vector2.zero;
float theta = h * (Mathf.PI * 2f);
thumbPos.x = Mathf.Cos(theta + (Mathf.PI / 2f));
thumbPos.y = Mathf.Sin(theta - (Mathf.PI / 2f));
thumbPos *= s * radius;
if (Event.current.type == EventType.Repaint)
{
if (s_WheelThumb == null)
{
s_WheelThumb = new GUIStyle("ColorPicker2DThumb");
s_WheelThumbSize = new Vector2(
!Mathf.Approximately(s_WheelThumb.fixedWidth, 0f) ? s_WheelThumb.fixedWidth : s_WheelThumb.padding.horizontal,
!Mathf.Approximately(s_WheelThumb.fixedHeight, 0f) ? s_WheelThumb.fixedHeight : s_WheelThumb.padding.vertical);
}
float scale = EditorGUIUtility.pixelsPerPoint;
var oldRT = RenderTexture.active;
var rt = RenderTexture.GetTemporary(
(int)(size * scale), (int)(size * scale), 0,
RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
s_Material.SetFloat("_Offset", offset);
s_Material.SetFloat("_DisabledState", overrideState && GUI.enabled ? 1f : 0.5f);
s_Material.SetVector("_Resolution", new Vector2(size * scale, size * scale / 2f));
Graphics.Blit(null, rt, s_Material, EditorGUIUtility.isProSkin ? 0 : 1);
RenderTexture.active = oldRT;
GUI.DrawTexture(wheelRect, rt);
RenderTexture.ReleaseTemporary(rt);
var thumbSizeH = s_WheelThumbSize / 2f;
s_WheelThumb.Draw(
new Rect(wheelRect.x + hsize + thumbPos.x - thumbSizeH.x,
wheelRect.y + hsize + thumbPos.y - thumbSizeH.y,
s_WheelThumbSize.x, s_WheelThumbSize.y),
false, false, false, false);
}
// 마우스 입력
var bounds = wheelRect;
bounds.x += hsize - radius;
bounds.y += hsize - radius;
bounds.width = bounds.height = radius * 2f;
var hsv = GetInput(bounds, new Vector3(h, s, 1f), thumbPos, radius);
value = Color.HSVToRGB(hsv.x, hsv.y, 1f);
value.w = offset;
// W 슬라이더 (밝기 오프셋)
var sliderRect = GUILayoutUtility.GetRect(1f, 17f);
float padding = sliderRect.width * 0.05f;
sliderRect.xMin += padding;
sliderRect.xMax -= padding;
value.w = GUI.HorizontalSlider(sliderRect, value.w, -1f, 1f);
// R/G/B 수치 표시
if (m_ComputeFunc != null)
{
var display = m_ComputeFunc(value);
using (new EditorGUI.DisabledGroupScope(true))
{
var vr = GUILayoutUtility.GetRect(1f, 17f);
vr.width /= 3f;
GUI.Label(vr, display.x.ToString("F2"), EditorStyles.centeredGreyMiniLabel);
vr.x += vr.width;
GUI.Label(vr, display.y.ToString("F2"), EditorStyles.centeredGreyMiniLabel);
vr.x += vr.width;
GUI.Label(vr, display.z.ToString("F2"), EditorStyles.centeredGreyMiniLabel);
}
}
}
void DrawLabelAndOverride(GUIContent title, SerializedProperty overrideState)
{
var areaRect = GUILayoutUtility.GetRect(1f, 17f);
var labelSize = EditorStyles.miniLabel.CalcSize(title);
var labelRect = new Rect(
areaRect.x + areaRect.width / 2f - labelSize.x / 2f,
areaRect.y, labelSize.x, labelSize.y);
GUI.Label(labelRect, title, EditorStyles.miniLabel);
if (overrideState != null)
{
var overrideRect = new Rect(labelRect.x - 17f, labelRect.y + 3f, 17f, 17f);
overrideState.boolValue = GUI.Toggle(
overrideRect, overrideState.boolValue,
EditorGUIUtility.TrTextContent("", "Override this setting for this volume."),
CoreEditorStyles.smallTickbox);
}
}
Vector3 GetInput(Rect bounds, Vector3 hsv, Vector2 thumbPos, float radius)
{
var e = Event.current;
int id = GUIUtility.GetControlID(s_ThumbHash, FocusType.Passive, bounds);
var mousePos = e.mousePosition;
if (e.type == EventType.MouseDown && GUIUtility.hotControl == 0 && bounds.Contains(mousePos))
{
if (e.button == 0)
{
var center = new Vector2(bounds.x + radius, bounds.y + radius);
float dist = Vector2.Distance(center, mousePos);
if (dist <= radius)
{
e.Use();
m_CursorPos = new Vector2(thumbPos.x + radius, thumbPos.y + radius);
GUIUtility.hotControl = id;
GUI.changed = true;
}
}
else if (e.button == 1)
{
e.Use();
GUI.changed = true;
m_ResetState = true;
}
}
else if (e.type == EventType.MouseDrag && e.button == 0 && GUIUtility.hotControl == id)
{
e.Use();
GUI.changed = true;
m_CursorPos += e.delta * 0.2f;
GetWheelHueSaturation(m_CursorPos.x, m_CursorPos.y, radius, out hsv.x, out hsv.y);
}
else if (e.rawType == EventType.MouseUp && e.button == 0 && GUIUtility.hotControl == id)
{
e.Use();
GUIUtility.hotControl = 0;
}
return hsv;
}
void GetWheelHueSaturation(float x, float y, float radius,
out float hue, out float saturation)
{
float dx = (x - radius) / radius;
float dy = (y - radius) / radius;
float d = Mathf.Sqrt(dx * dx + dy * dy);
hue = Mathf.Atan2(dx, -dy);
hue = 1f - ((hue > 0) ? hue : (Mathf.PI * 2f) + hue) / (Mathf.PI * 2f);
saturation = Mathf.Clamp01(d);
}
bool CheckMaterialAndShader()
{
if (s_Material != null) return true;
var shader = Shader.Find(k_ShaderName);
if (shader == null)
{
Debug.LogError($"[YAMOTrackballUIDrawer] 셰이더를 찾을 수 없습니다: {k_ShaderName}");
return false;
}
s_Material = new Material(shader);
return true;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1ca6a61ffb470d04ebc7fa49bc65519d

View File

@ -0,0 +1,361 @@
// NiloToon 캐릭터 전용 색조 보정 셰이더 (YAMO)
//
// Pass 구성:
// Pass 0 "Copy" : 스텐실 없음 — 카메라 컬러를 임시 RT로 단순 복사
// Pass 1 "ToneAdjust" : Stencil Equal(128) — 캐릭터 픽셀에만 색조 보정
// Pass 2 "ToneAdjust_Full" : 스텐실 없음 — 전체 화면에 색조 보정 (디버그용)
// Pass 3 "StencilView" : Stencil Equal(128) — 마스크 시각화 (디버그용)
// Pass 4 "DebugStencilFill" : 전체 화면 stencil=128 기록 (진단용)
// Pass 5 "MeshStencilFill" : 메시 기반 stencil=128 기록 (ColorMask 0)
// Pass 6 "MeshMaskFill" : 메시 → 마스크=1 + stencil=128 (Auto Match용)
// Pass 7 "MaskedDownsample" : 마스크 기반 가중 다운샘플 (첫 단계)
// Pass 8 "WeightedDownsample": 알파 기반 가중 다운샘플 (후속 단계)
Shader "YAMO/NiloToonCharToneAdjust"
{
Properties { }
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// _MainTex는 Properties에 선언하지 않습니다.
// Properties에 선언하면 Material 기본값이 Global 값보다 우선합니다.
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
float4 _CharToneAdjust_Lift;
float4 _CharToneAdjust_Gamma;
float4 _CharToneAdjust_Gain;
float4 _CharToneAdjust_Shadows;
float4 _CharToneAdjust_Midtones;
float4 _CharToneAdjust_Highlights;
float4 _CharToneAdjust_SMHRange;
float _CharToneAdjust_Saturation;
float _CharToneAdjust_PostExposure;
float _CharToneAdjust_BlendAmount;
#define LUMA_WEIGHTS float3(0.2126729, 0.7151522, 0.0721750)
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
Varyings Vert(uint vertexID : SV_VertexID)
{
Varyings output;
float2 uv = float2((vertexID << 1u) & 2u, vertexID & 2u);
output.positionCS = float4(uv * 2.0 - 1.0, 0.0, 1.0);
#if UNITY_UV_STARTS_AT_TOP
uv.y = 1.0 - uv.y;
#endif
output.uv = uv;
return output;
}
float3 ApplyLiftGammaGain(float3 color, float3 lift, float3 gamma, float3 gain)
{
color = color * gain + lift;
gamma = max(gamma, 1e-4);
color = sign(color) * pow(abs(color) + 1e-5, 1.0 / gamma);
return max(color, 0.0);
}
float3 ApplySMH(float3 color,
float3 shadows, float3 midtones, float3 highlights,
float shadowStart, float shadowEnd,
float highlightStart, float highlightEnd)
{
float lum = dot(color, LUMA_WEIGHTS);
float shadowW = 1.0 - smoothstep(shadowStart, shadowEnd, lum);
float highlightW = smoothstep(highlightStart, highlightEnd, lum);
float midtoneW = saturate(1.0 - shadowW - highlightW);
color += shadows * shadowW;
color += midtones * midtoneW;
color += highlights * highlightW;
return max(color, 0.0);
}
float3 ApplySaturation(float3 color, float sat)
{
float lum = dot(color, LUMA_WEIGHTS);
return max(lerp(lum.xxx, color, sat), 0.0);
}
half4 ToneAdjustFrag(Varyings input) : SV_Target
{
half4 original = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
float3 color = original.rgb;
color *= _CharToneAdjust_PostExposure;
color = ApplyLiftGammaGain(color,
_CharToneAdjust_Lift.rgb, _CharToneAdjust_Gamma.rgb, _CharToneAdjust_Gain.rgb);
color = ApplySMH(color,
_CharToneAdjust_Shadows.rgb, _CharToneAdjust_Midtones.rgb, _CharToneAdjust_Highlights.rgb,
_CharToneAdjust_SMHRange.x, _CharToneAdjust_SMHRange.y,
_CharToneAdjust_SMHRange.z, _CharToneAdjust_SMHRange.w);
color = ApplySaturation(color, _CharToneAdjust_Saturation);
color = lerp(original.rgb, color, _CharToneAdjust_BlendAmount);
return half4(color, original.a);
}
ENDHLSL
// ── Pass 0: Copy ──────────────────────────────────────────────
Pass
{
Name "NiloToonCharToneAdjust_Copy"
ZTest Always ZWrite Off Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
half4 Frag(Varyings input) : SV_Target
{
return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
}
ENDHLSL
}
// ── Pass 1: ToneAdjust (Stencil Equal 128) ───────────────────
Pass
{
Name "NiloToonCharToneAdjust_ToneAdjust"
ZTest Always ZWrite Off Cull Off
Stencil
{
Ref 128 ReadMask 128 WriteMask 0
Comp Equal Pass Keep Fail Keep
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment ToneAdjustFrag
ENDHLSL
}
// ── Pass 2: ToneAdjust_Full (스텐실 없음, 전체 화면) ─────────
// 디버그 및 스텐실 마킹 우회용.
// Feature의 FullScreen 모드에서 사용됩니다.
Pass
{
Name "NiloToonCharToneAdjust_ToneAdjustFull"
ZTest Always ZWrite Off Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment ToneAdjustFrag
ENDHLSL
}
// ── Pass 3: StencilView (스텐실 마스크 시각화) ───────────────
// 스텐실 재마킹이 정상 동작하는지 확인하는 디버그용 패스.
// stencil=128 인 픽셀을 마젠타 오버레이로 표시합니다.
Pass
{
Name "NiloToonCharToneAdjust_StencilView"
ZTest Always ZWrite Off Cull Off
Stencil
{
Ref 128 ReadMask 128 WriteMask 0
Comp Equal Pass Keep Fail Keep
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment StencilViewFrag
half4 StencilViewFrag(Varyings input) : SV_Target
{
half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
return half4(lerp(c.rgb, half3(1.0, 0.0, 0.3), 0.6), 1.0);
}
ENDHLSL
}
// ── Pass 4: DebugStencilFill (전체 화면 stencil=128 기록) ────
// 진단용 풀스크린 패스. DrawProcedural로 전체 화면에 stencil=128 강제 기록.
Pass
{
Name "NiloToonCharToneAdjust_DebugStencilFill"
ZTest Always ZWrite Off Cull Off ColorMask 0
Stencil
{
Ref 128 WriteMask 128
Comp Always Pass Replace
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragEmpty
half4 FragEmpty(Varyings input) : SV_Target { return 0; }
ENDHLSL
}
// ── Pass 5: MeshStencilFill (메시 기반 stencil=128 기록) ─────
// DrawRendererList + overrideMaterial로 캐릭터 메시 픽셀에 stencil=128 기록.
// NiloToon 자체 스텐실 패스 대신 이 패스를 사용합니다.
Pass
{
Name "NiloToonCharToneAdjust_MeshStencilFill"
ZTest Always ZWrite Off Cull Off ColorMask 0
Stencil
{
Ref 128 WriteMask 128
Comp Always Pass Replace
}
HLSLPROGRAM
#pragma vertex MeshVert
#pragma fragment MeshFrag
struct MeshAttributes
{
float4 positionOS : POSITION;
};
struct MeshVaryings
{
float4 positionCS : SV_POSITION;
};
MeshVaryings MeshVert(MeshAttributes input)
{
MeshVaryings o;
o.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return o;
}
half4 MeshFrag(MeshVaryings input) : SV_Target { return 0; }
ENDHLSL
}
// ── Pass 6: MeshMaskFill (메시 → 마스크 + 스텐실) ──────────
// DrawRendererList + overrideMaterial로 캐릭터 메시 픽셀에
// color=1.0(마스크) + stencil=128 동시 기록. Auto Match 분석용.
Pass
{
Name "NiloToonCharToneAdjust_MeshMaskFill"
ZTest Always ZWrite Off Cull Off
Stencil
{
Ref 128 WriteMask 128
Comp Always Pass Replace
}
HLSLPROGRAM
#pragma vertex MeshVert2
#pragma fragment MaskFrag
struct MeshAttr2 { float4 positionOS : POSITION; };
struct MeshVary2 { float4 positionCS : SV_POSITION; };
MeshVary2 MeshVert2(MeshAttr2 input)
{
MeshVary2 o;
o.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return o;
}
half4 MaskFrag(MeshVary2 input) : SV_Target { return 1; }
ENDHLSL
}
// ── Pass 7: MaskedDownsample (마스크 기반 가중 다운샘플) ─────
// _MainTex(카메라 컬러) + _MaskTex(캐릭터 마스크)를 읽고
// 마스크 조건에 맞는 픽셀만 4x4 블록 평균. 첫 단계 축소용.
// _InvertMask: 0 = 캐릭터(mask>0.5), 1 = 배경(mask<0.5)
Pass
{
Name "NiloToonCharToneAdjust_MaskedDownsample"
ZTest Always ZWrite Off Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment MaskedDownsampleFrag
TEXTURE2D(_MaskTex);
SAMPLER(sampler_MaskTex);
float _InvertMask;
float4 _MainTex_TexelSize;
half4 MaskedDownsampleFrag(Varyings input) : SV_Target
{
float3 colorSum = 0;
float validCount = 0;
float2 texelSize = _MainTex_TexelSize.xy;
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
float2 offset = (float2(x, y) - 1.5) * texelSize;
float2 uv = input.uv + offset;
half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
half mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, uv).r;
float valid = _InvertMask > 0.5 ? step(mask, 0.499) : step(0.5, mask);
colorSum += color.rgb * valid;
validCount += valid;
}
}
if (validCount < 0.5)
return half4(0, 0, 0, 0);
return half4(colorSum / validCount, validCount / 16.0);
}
ENDHLSL
}
// ── Pass 8: WeightedDownsample (가중 다운샘플) ──────────────
// 4x4 블록의 유효 픽셀(alpha>0)만 평균하여 1/4 크기로 축소.
// alpha 채널에 유효 비율을 저장합니다.
Pass
{
Name "NiloToonCharToneAdjust_WeightedDownsample"
ZTest Always ZWrite Off Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment WeightedDownsampleFrag
float4 _MainTex_TexelSize; // (1/w, 1/h, w, h)
half4 WeightedDownsampleFrag(Varyings input) : SV_Target
{
float3 colorSum = 0;
float validCount = 0;
float2 texelSize = _MainTex_TexelSize.xy;
// 4x4 블록 샘플링 (현재 UV 중심으로)
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
float2 offset = (float2(x, y) - 1.5) * texelSize;
half4 s = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv + offset);
float valid = step(0.5, s.a);
colorSum += s.rgb * valid;
validCount += valid;
}
}
if (validCount < 0.5)
return half4(0, 0, 0, 0);
return half4(colorSum / validCount, validCount / 16.0);
}
ENDHLSL
}
// ── Pass 9: StencilToMask (스텐실 → 마스크 변환) ────────────
// stencil=128 픽셀에 1.0 기록. Auto Match 분석용 마스크 생성.
Pass
{
Name "NiloToonCharToneAdjust_StencilToMask"
ZTest Always ZWrite Off Cull Off
Stencil
{
Ref 128 ReadMask 128 WriteMask 0
Comp Equal Pass Keep Fail Keep
}
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment StencilToMaskFrag
half4 StencilToMaskFrag(Varyings input) : SV_Target { return 1; }
ENDHLSL
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: c9b9e991f656a5e4c9c5eced1e8a2c53
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
preprocessorOverride: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,559 @@
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);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 516077af451b5a148a61e3f3e8293873
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,89 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace YAMO
{
/// <summary>
/// NiloToon 캐릭터 셰이더를 사용하는 오브젝트에만 색조 보정을 적용하는 Volume 컴포넌트.
/// Unity의 Lift Gamma Gain / Shadows Midtones Highlights와 동일한 파라미터 구조를 사용합니다.
///
/// 적용 원리:
/// NiloToon의 캐릭터 영역 스텐실(bit7 = 128)을 마스크로 사용하여
/// 캐릭터 픽셀에만 색조 보정을 적용합니다.
/// 배경, 소품 등 NiloToon 캐릭터 셰이더를 사용하지 않는 오브젝트에는 영향 없음.
///
/// 주의:
/// 반투명(Transparent queue) 캐릭터 파츠는 스텐실 마스킹에서 제외됩니다.
/// (NiloToon의 CharacterAreaStencilBufferFill pass가 Opaque queue만 처리하기 때문)
/// </summary>
[System.Serializable]
[VolumeComponentMenu("NiloToon/Char Tone Adjust (YAMO)")]
public class NiloToonCharToneAdjustVolume : VolumeComponent, IPostProcessComponent
{
// ─────────────────────────────────────────────────────────────
// Lift Gamma Gain
// 파라미터 형식: Vector4 (R, G, B, W)
// 기본값 (1, 1, 1, 0) = 변화 없음
// W 채널 = 전체 채널에 균등 적용되는 글로벌 조절값
// ─────────────────────────────────────────────────────────────
[Header("Lift Gamma Gain")]
[Tooltip("어두운 영역(Lift) 색조 보정. 기본값 (1,1,1,0) = 변화 없음. W = 전체 밝기 오프셋.")]
public Vector4Parameter lift = new Vector4Parameter(new Vector4(1f, 1f, 1f, 0f));
[Tooltip("중간 영역(Gamma) 색조 보정. 기본값 (1,1,1,0) = 변화 없음. 1보다 크면 밝아짐, 작으면 어두워짐.")]
public Vector4Parameter gamma = new Vector4Parameter(new Vector4(1f, 1f, 1f, 0f));
[Tooltip("밝은 영역(Gain) 색조 보정. 기본값 (1,1,1,0) = 변화 없음. W = 전체 밝기 배율.")]
public Vector4Parameter gain = new Vector4Parameter(new Vector4(1f, 1f, 1f, 0f));
// ─────────────────────────────────────────────────────────────
// Shadows Midtones Highlights
// ─────────────────────────────────────────────────────────────
[Header("Shadows Midtones Highlights")]
[Tooltip("어두운 영역(Shadows) 색조 보정. 기본값 (1,1,1,0) = 변화 없음.")]
public Vector4Parameter shadows = new Vector4Parameter(new Vector4(1f, 1f, 1f, 0f));
[Tooltip("중간 영역(Midtones) 색조 보정. 기본값 (1,1,1,0) = 변화 없음.")]
public Vector4Parameter midtones = new Vector4Parameter(new Vector4(1f, 1f, 1f, 0f));
[Tooltip("밝은 영역(Highlights) 색조 보정. 기본값 (1,1,1,0) = 변화 없음.")]
public Vector4Parameter highlights = new Vector4Parameter(new Vector4(1f, 1f, 1f, 0f));
[Header("SMH Range")]
[Tooltip("그림자 영역이 시작되는 휘도 값.")]
public ClampedFloatParameter shadowsStart = new ClampedFloatParameter(0f, 0f, 1f);
[Tooltip("그림자 영역이 끝나는 휘도 값.")]
public ClampedFloatParameter shadowsEnd = new ClampedFloatParameter(0.3f, 0f, 1f);
[Tooltip("하이라이트 영역이 시작되는 휘도 값.")]
public ClampedFloatParameter highlightsStart = new ClampedFloatParameter(0.55f, 0f, 1f);
[Tooltip("하이라이트 영역이 끝나는 휘도 값.")]
public ClampedFloatParameter highlightsEnd = new ClampedFloatParameter(1f, 0f, 1f);
// ─────────────────────────────────────────────────────────────
// Color Adjustments
// ─────────────────────────────────────────────────────────────
[Header("Color Adjustments")]
[Tooltip("채도 배율. 0 = 흑백, 1 = 원본, 2 = 과채화.")]
public ClampedFloatParameter saturation = new ClampedFloatParameter(1f, 0f, 2f);
[Tooltip("노출 보정 (EV 단위). 0 = 변화 없음, 양수 = 밝게, 음수 = 어둡게.")]
public ClampedFloatParameter postExposure = new ClampedFloatParameter(0f, -5f, 5f);
// ─────────────────────────────────────────────────────────────
// Blend
// ─────────────────────────────────────────────────────────────
[Header("Blend")]
[Tooltip("원본과 보정된 색상 사이의 블렌드 강도. 0 = 원본, 1 = 완전 적용.")]
public ClampedFloatParameter blendAmount = new ClampedFloatParameter(1f, 0f, 1f);
// VolumeComponent 베이스 클래스의 `bool active` 필드를 그대로 사용합니다.
// (URP 17: VolumeComponent.active는 bool 타입, Inspector의 컴포넌트 ON/OFF 토글과 연동)
public bool IsActive() => active;
public bool IsTileCompatible() => false;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd725792ed0f6e144b37455fa5651a23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ef56d1a6ea7852e479f5fe5765240bd4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: