ADD: 레드 리허설용 스테이지 작업 및 이벤트 컨트롤러 컴포넌트 복제 기능 추가
This commit is contained in:
parent
1de279a021
commit
5d1b542820
BIN
Assets/ResourcesData/Background/[레드]레드 스테이지/Scenes/RedStageSceneProfile.asset
(Stored with Git LFS)
BIN
Assets/ResourcesData/Background/[레드]레드 스테이지/Scenes/RedStageSceneProfile.asset
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Character/@029_레드/Avatar/레드_260418_오리지널 데뷔/레드_260418_오리지널 데뷔_Biped Variant.prefab
(Stored with Git LFS)
BIN
Assets/ResourcesData/Character/@029_레드/Avatar/레드_260418_오리지널 데뷔/레드_260418_오리지널 데뷔_Biped Variant.prefab
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/ResourcesData/Character/@029_레드/Avatar/레드_260418_오리지널 데뷔/레드_260418_오리지널 데뷔_Biped.fbx
(Stored with Git LFS)
BIN
Assets/ResourcesData/Character/@029_레드/Avatar/레드_260418_오리지널 데뷔/레드_260418_오리지널 데뷔_Biped.fbx
(Stored with Git LFS)
Binary file not shown.
8
Assets/ResourcesData/Project/260414_레드리허설.meta
Normal file
8
Assets/ResourcesData/Project/260414_레드리허설.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83eec10194d09eb4c968599d5a154ffb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/ResourcesData/Project/260414_레드리허설/260414_레드리허설_레드스테이지.unity
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Project/260414_레드리허설/260414_레드리허설_레드스테이지.unity
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ee348a10351f7f41bc4416828bc4666
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/ResourcesData/Project/260414_레드리허설/260414_레드리허설_레드스테이지_Volume.asset
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Project/260414_레드리허설/260414_레드리허설_레드스테이지_Volume.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c4d7ba1f8af30a40a499ccc2210055b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/ResourcesData/Project/260414_레드리허설/음원.meta
Normal file
8
Assets/ResourcesData/Project/260414_레드리허설/음원.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61429f289c8fa5743b248c48b7fab962
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/AiScReam_Veilight_mix_master_2(Acappella).mp3
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/AiScReam_Veilight_mix_master_2(Acappella).mp3
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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:
|
||||
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/AiScReam_Veilight_mix_master_2.mp3
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/AiScReam_Veilight_mix_master_2.mp3
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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:
|
||||
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/GOLDEN_RED_Livecover(260410).mp3
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/GOLDEN_RED_Livecover(260410).mp3
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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:
|
||||
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/STREAM_ON_RED_Livecover(260410).mp3
(Stored with Git LFS)
Normal file
BIN
Assets/ResourcesData/Project/260414_레드리허설/음원/STREAM_ON_RED_Livecover(260410).mp3
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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:
|
||||
8
Assets/ResourcesData/Project/260414_레드리허설/타임라인.meta
Normal file
8
Assets/ResourcesData/Project/260414_레드리허설/타임라인.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6ef3a50bdbbce74a96224a72f6a0e88
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 709fedd9361df0c44ac24187cc866b98
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bc6c5421d4bcf742acb5312b6cd76dc
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a851b0ec32f68f24b8ab9955f9c07a89
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba6c4f43c9b32974fb7b3b70dea4b321
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7644bb67b9a95e142954a2ea19383d78
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42b19884e57192744859e9bcbbf9bfa8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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");
|
||||
|
||||
8
Assets/Scripts/YAMO_Scripts/NiloToonCharToneAdjust.meta
Normal file
8
Assets/Scripts/YAMO_Scripts/NiloToonCharToneAdjust.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e170c0ef61802649a846b7d9faeeba0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65f6d8f0e8059054cbf61425189a1f30
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e4332b2aba26f04e9eb62b08650f123
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ca6a61ffb470d04ebc7fa49bc65519d
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9b9e991f656a5e4c9c5eced1e8a2c53
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
preprocessorOverride: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 516077af451b5a148a61e3f3e8293873
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd725792ed0f6e144b37455fa5651a23
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Scripts/YAMO_Scripts/NiloToonCharToneAdjust/WORK_SUMMARY.md
(Stored with Git LFS)
Normal file
BIN
Assets/Scripts/YAMO_Scripts/NiloToonCharToneAdjust/WORK_SUMMARY.md
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef56d1a6ea7852e479f5fe5765240bd4
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
x
Reference in New Issue
Block a user