이 과정에 사용된 Unity 버전은 2021.1.18로, URP 버전 11을 기반으로 작성되었습니다.
다만 아래의 내용을 참조 하여 거의 모든 URP 버전에 통합 할 수 있을 것입니다.
UE4에서 사용되는 ACES 톤 매핑을 유니티 엔진 URP로 포팅하려고 합니다.
이 과정을 통해 향후 다양한 톤 매핑 로직을 유연하게 추가할 수 있습니다.
위와 동일한 형식으로 추가합니다.
Adding tone mapping mode.
Tonemapping.cs
public enum TonemappingMode
{
None,
Neutral, // Neutral tonemapper
ACES, // ACES Filmic reference tonemapper (custom approximation)
Custom,
/////////////////UE4_ACES_BEGIN///////////////
UE4_ACES,
/////////////////UE4_ACES_END/////////////////
}
UE4 ACES를 추가했습니다.
이제 몇 가지 옵션을 추가해 봅시다.
톤 매핑 클래스에 변수를 추가하겠습니다.
Tonemapping.cs
public sealed class Tonemapping : VolumeComponent, IPostProcessComponent
{
[Tooltip("Select a tonemapping algorithm to use for the color grading process.")]
public TonemappingModeParameter mode = new TonemappingModeParameter(TonemappingMode.None);
/////////////////UE4_ACES_BEGIN/////////////////
//create floatParameter
/// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
[Tooltip("Film_Slope")]
public ClampedFloatParameter slope = new ClampedFloatParameter(0.88f, 0f, 1f);
/// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
[Tooltip("Film_Toe")]
public ClampedFloatParameter toe = new ClampedFloatParameter(0.55f, 0.0f, 1.0f);
/// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
[Tooltip("Film_Shoulder")]
public ClampedFloatParameter shoulder = new ClampedFloatParameter(0.26f, 0.0f, 1.0f);
/// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
[Tooltip("Film_BlackClip")]
public ClampedFloatParameter blackClip = new ClampedFloatParameter(0.0f, 0.0f, 1.0f);
/// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
[Tooltip("Film_WhiteClip")]
public ClampedFloatParameter whiteClip = new ClampedFloatParameter(0.04f, 0.0f, 1.0f);
/////////////////UE4_ACES_END/////////////////
public bool IsActive() => mode.value != TonemappingMode.None;
public bool IsTileCompatible() => true;
}
이제 추가된 변수에서 얻은 값을 편집기에 전달해야 합니다.
먼저 에디터에 SerializedDataParameter 5개를 추가해 보겠습니다.
TonemappingEditor.cs
/////////////////UE4_ACES_BEGIN/////////////////
SerializedDataParameter m_Slope;
SerializedDataParameter m_Toe;
SerializedDataParameter m_Shoulder;
SerializedDataParameter m_BlackClip;
SerializedDataParameter m_WhiteClip;
/////////////////UE4_ACES_END/////////////////
TonemappingEditor.cs
public override void OnEnable()
{
var o = new PropertyFetcher<Tonemapping>(serializedObject);
m_Mode = Unpack(o.Find(x => x.mode));
/////////////////UE4_ACES_BEGIN/////////////////
m_Slope = Unpack(o.Find(x => x.slope));
m_Toe = Unpack(o.Find(x => x.toe));
m_Shoulder = Unpack(o.Find(x => x.shoulder));
m_BlackClip = Unpack(o.Find(x => x.blackClip));
m_WhiteClip = Unpack(o.Find(x => x.whiteClip));
/////////////////UE4_ACES_END///////////////////
}
재설정 버튼 UI 추가
TonemappingEditor.cs
OnInspectorGUI() 함수에서 GUILayout.Button() API를 사용하여 재설정 버튼을 추가합니다.
Unity - Scripting API: GUILayout (unity3d.com)
public override void OnInspectorGUI()
{
PropertyField(m_Mode);
/////////////////UE4_ACES_BEGIN/////////////////
if ( m_Mode.value.intValue == (int)TonemappingMode.UE4_ACES)
{
UnityEngine.GUILayout.BeginVertical("box");
UnityEngine.GUILayout.BeginHorizontal();
PropertyField(m_Slope);
if (UnityEngine.GUILayout.Button("Reset"))
{
m_Slope.value.floatValue = 0.88f;
}
UnityEngine.GUILayout.EndHorizontal();
UnityEngine.GUILayout.BeginHorizontal();
PropertyField(m_Toe);
if (UnityEngine.GUILayout.Button("Reset"))
{
m_Toe.value.floatValue = 0.55f;
}
UnityEngine.GUILayout.EndHorizontal();
UnityEngine.GUILayout.BeginHorizontal();
PropertyField(m_Shoulder);
if (UnityEngine.GUILayout.Button("Reset"))
{
m_Shoulder.value.floatValue = 0.26f;
}
UnityEngine.GUILayout.EndHorizontal();
UnityEngine.GUILayout.BeginHorizontal();
PropertyField(m_BlackClip);
if (UnityEngine.GUILayout.Button("Reset"))
{
m_BlackClip.value.floatValue = 0.0f;
}
UnityEngine.GUILayout.EndHorizontal();
UnityEngine.GUILayout.BeginHorizontal();
PropertyField(m_WhiteClip);
if (UnityEngine.GUILayout.Button("Reset"))
{
m_WhiteClip.value.floatValue = 0.04f;
}
UnityEngine.GUILayout.EndHorizontal();
UnityEngine.GUILayout.EndVertical();
}
/////////////////UE4_ACES_END////////////////////
// Display a warning if the user is trying to use a tonemap while rendering in LDR
var asset = UniversalRenderPipeline.asset;
if (asset != null && !asset.supportsHDR)
{
EditorGUILayout.HelpBox("Tonemapping should only be used when working in HDR.", MessageType.Warning);
return;
}
}
Using 을 사용하여 UnityEngine 네임스페이스를 임포트할 수도 있습니다.
재설정 버튼을 클릭하면 직렬화된 각 프로퍼티가 기본값인 초기값으로 다시 가져옵니다.
좋습니다. 기능은 작동하지 않지만 UI는 의도한 대로 잘 갖춰져 있습니다.
Properties and shader integration.
UE4 엔진의 ACES 구조를 참조하여 작성한 셰이더 코드를 추가해 보겠습니다.
Packages/com.unity.render-pipelines.universal@11.0.0/ShaderLibrary/UE4_ACES.hlsl
UE4_ACES.hlsl 형식으로 위 경로에 추가합니다.
#ifndef UNITY_UE4ACES_INCLUDED
#define UNITY_UE4ACES_INCLUDED
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ACES.hlsl"
float FilmSlope;// = 0.91;
float FilmToe;// = 0.53;
float FilmShoulder;// = 0.23;
float FilmBlackClip;// = 0;
float FilmWhiteClip;// = 0.035;
float3 UE4ACES(float3 aces)
{
// "Glow" module constants
const float RRT_GLOW_GAIN = 0.05;
const float RRT_GLOW_MID = 0.08;
float saturation = rgb_2_saturation(aces);
float ycIn = rgb_2_yc(aces);
float s = sigmoid_shaper((saturation - 0.4) / 0.2);
float addedGlow = 1.0 + glow_fwd(ycIn, RRT_GLOW_GAIN * s, RRT_GLOW_MID);
aces *= addedGlow;
const float RRT_RED_SCALE = 0.82;
const float RRT_RED_PIVOT = 0.03;
const float RRT_RED_HUE = 0.0;
const float RRT_RED_WIDTH = 135.0;
// --- Red modifier --- //
float hue = rgb_2_hue(aces);
float centeredHue = center_hue(hue, RRT_RED_HUE);
float hueWeight;
{
hueWeight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centeredHue / RRT_RED_WIDTH));
hueWeight *= hueWeight;
}
//float hueWeight = Square( smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centeredHue / RRT_RED_WIDTH)) );
aces.r += hueWeight * saturation * (RRT_RED_PIVOT - aces.r) * (1.0 - RRT_RED_SCALE);
// Use ACEScg primaries as working space
float3 acescg = max(0.0, ACES_to_ACEScg(aces));
// Pre desaturate
acescg = lerp(dot(acescg, AP1_RGB2Y).xxx, acescg, 0.96);
const half ToeScale = 1 + FilmBlackClip - FilmToe;
const half ShoulderScale = 1 + FilmWhiteClip - FilmShoulder;
const float InMatch = 0.18;
const float OutMatch = 0.18;
float ToeMatch;
if (FilmToe > 0.8)
{
// 0.18 will be on straight segment
ToeMatch = (1 - FilmToe - OutMatch) / FilmSlope + log10(InMatch);
}
else
{
// 0.18 will be on toe segment
// Solve for ToeMatch such that input of InMatch gives output of OutMatch.
const float bt = (OutMatch + FilmBlackClip) / ToeScale - 1;
ToeMatch = log10(InMatch) - 0.5 * log((1 + bt) / (1 - bt)) * (ToeScale / FilmSlope);
}
float StraightMatch = (1 - FilmToe) / FilmSlope - ToeMatch;
float ShoulderMatch = FilmShoulder / FilmSlope - StraightMatch;
half3 LogColor = log10(acescg);
half3 StraightColor = FilmSlope * (LogColor + StraightMatch);
half3 ToeColor = (-FilmBlackClip) + (2 * ToeScale) / (1 + exp((-2 * FilmSlope / ToeScale) * (LogColor - ToeMatch)));
half3 ShoulderColor = (1 + FilmWhiteClip) - (2 * ShoulderScale) / (1 + exp((2 * FilmSlope / ShoulderScale) * (LogColor - ShoulderMatch)));
ToeColor = LogColor < ToeMatch ? ToeColor : StraightColor;
ShoulderColor = LogColor > ShoulderMatch ? ShoulderColor : StraightColor;
half3 t = saturate((LogColor - ToeMatch) / (ShoulderMatch - ToeMatch));
t = ShoulderMatch < ToeMatch ? 1 - t : t;
t = (3 - 2 * t)*t*t;
half3 linearCV = lerp(ToeColor, ShoulderColor, t);
// Post desaturate
linearCV = lerp(dot(float3(linearCV), AP1_RGB2Y), linearCV, 0.93);
// Returning positive AP1 values
//return max(0, linearCV);
// Convert to display primary encoding
// Rendering space RGB to XYZ
float3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);
// Apply CAT from ACES white point to assumed observer adapted white point
XYZ = mul(D60_2_D65_CAT, XYZ);
// CIE XYZ to display primaries
linearCV = mul(XYZ_2_REC709_MAT, XYZ);
linearCV = saturate(linearCV); //Protection to make negative return out.
return linearCV;
}
/////////////////SWS_UE4_ACES_END/////////////////
#endif
UE4의 unity3D용 ACES 코드
구현할 때 종종 씬 뷰에서 확인하는데, 채도는 씬 뷰에서 내부적으로 작동하는 것 같습니다.
linearCV = saturate(linearCV); 추가하지 않으면 게임 뷰에 블랙 클립에 의한 음수 값이 반환됩니다.
UE4 ACES 톤 매핑은 기본적으로 ODT_Rec709_100nits_dim을 사용합니다.
유니티에서 사용되는 ACES의 ODT_Rec709_100nits_dim( ) 함수만 살펴봅시다.
//Packages/com.unity.render-pipelines.core@11.0.0/ShaderLibrary/ACES.hlsl
// CIE XYZ to display primaries
linearCV = mul(XYZ_2_REC709_MAT, XYZ);
// Handle out-of-gamut values
// Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
linearCV = saturate(linearCV);
ODT_Rec709_100nits_dim
ACES 코드 브리핑은 일단 생략하겠습니다. 아주 길고 긴 설명이 필요하니 다음 기회에...
하지만 관심이 있으시다면 아래 링크를 참조하시기 바랍니다.
ampas/aces-dev: AMPAS Academy Color Encoding System Developer Resources (github.com)
개인적으로 이 ACES를 완전히 이해하려면 거의 반년 동안 관련 주제를 다양하게 살펴봐야 할 것 같습니다.
Added Shader Keyword.
Packages/com.unity.render-pipelines.universal@11.0.0/Runtime/UniversalRenderPipelineCore.cs
public static class ShaderKeywordStrings
{
...
public static readonly string TonemapACES = "_TONEMAP_ACES";
public static readonly string TonemapNeutral = "_TONEMAP_NEUTRAL";
/////////////////UE4_ACES_END/////////////////
public static readonly string TonemapUE4ACES = "_TONEMAP_UE4ACES";
/////////////////UE4_ACES_END/////////////////
...
}
public static readonly string TonemapUE4ACES = "_TONEMAP_UE4ACES"; 를 추가합니다.
Packages/com.unity.render-pipelines.universal@11.0.0/Runtime/Passes/PostProcessPass.cs
void SetupColorGrading(CommandBuffer cmd, ref RenderingData renderingData, Material material)
{
ref var postProcessingData = ref renderingData.postProcessingData;
bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
int lutHeight = postProcessingData.lutSize;
int lutWidth = lutHeight * lutHeight;
// Source material setup
float postExposureLinear = Mathf.Pow(2f, m_ColorAdjustments.postExposure.value);
cmd.SetGlobalTexture(ShaderConstants._InternalLut, m_InternalLut.Identifier());
material.SetVector(ShaderConstants._Lut_Params, new Vector4(1f / lutWidth, 1f / lutHeight, lutHeight - 1f, postExposureLinear));
material.SetTexture(ShaderConstants._UserLut, m_ColorLookup.texture.value);
material.SetVector(ShaderConstants._UserLut_Params, !m_ColorLookup.IsActive()
? Vector4.zero
: new Vector4(1f / m_ColorLookup.texture.value.width,
1f / m_ColorLookup.texture.value.height,
m_ColorLookup.texture.value.height - 1f,
m_ColorLookup.contribution.value)
);
if (hdr)
{
material.EnableKeyword(ShaderKeywordStrings.HDRGrading);
}
else
{
switch (m_Tonemapping.mode.value)
{
case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
case TonemappingMode.ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapACES); break;
/////////////////UE4_ACES_BEGIN/////////////////
case TonemappingMode.UE4_ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapUE4ACES); break;
default: break; // None
}
//////LDR Bind UI
material.SetFloat("FilmSlope", (float)m_Tonemapping.slope);
material.SetFloat("FilmToe", (float)m_Tonemapping.toe);
material.SetFloat("FilmShoulder", (float)m_Tonemapping.shoulder);
material.SetFloat("FilmBlackClip", (float)m_Tonemapping.blackClip);
material.SetFloat("FilmWhiteClip", (float)m_Tonemapping.whiteClip);
}
}
case TonemappingMode.UE4_ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapUE4ACES); break;
셰이더에 전송할 인수를 설정합니다.
material.SetFloat("FilmSlope", (float)m_Tonemapping.slope);
material.SetFloat("FilmToe", (float)m_Tonemapping.toe);
material.SetFloat("FilmShoulder", (float)m_Tonemapping.shoulder);
material.SetFloat("FilmBlackClip", (float)m_Tonemapping.blackClip);
material.SetFloat("FilmWhiteClip", (float)m_Tonemapping.whiteClip);
Tone mapping applied.
Packages/src/com.unity.render-pipelines.universal@7.2.1/Shaders/PostProcessing/Common.hlsl
#ifndef UNIVERSAL_POSTPROCESSING_COMMON_INCLUDED
#define UNIVERSAL_POSTPROCESSING_COMMON_INCLUDED
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/Utils/Fullscreen.hlsl"
/////////////////UE4_ACES_BEGIN///////////////
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UE4_ACES.hlsl"
/////////////////UE4_ACES_END/////////////////
// ----------------------------------------------------------------------------------
// Render fullscreen mesh by using a matrix set directly by the pipeline instead of
// relying on the matrix set by the C++ engine to avoid issues with XR
half3 ApplyTonemap(half3 input)
{
#if _TONEMAP_ACES
float3 aces = unity_to_ACES(input);
input = AcesTonemap(aces);
#elif _TONEMAP_NEUTRAL
input = NeutralTonemap(input);
///////////////////UE4_ACES_BEGIN/////////////////
#elif _TONEMAP_UE4ACES
float3 aces = unity_to_ACES(input);
input = UE4ACES(aces);
///////////////////UE4_ACES_END/////////////////
#endif
return saturate(input);
}
Added multi-compilation keyword to UberPost.
UberPost에 멀티컴파일 키워드를 추가했습니다.
Assets/BundleRes/Shader/PostEffect/UberPost.shader
Shader "Hidden/Universal Render Pipeline/UberPost"
{
Properties
{
[HideInInspector] _StencilRef("_StencilRef", Int) = 0
}
HLSLINCLUDE
#pragma multi_compile_local _ _DISTORTION
#pragma multi_compile_local _ _CHROMATIC_ABERRATION
#pragma multi_compile_local _ _BLOOM_LQ _BLOOM_HQ _BLOOM_LQ_DIRT _BLOOM_HQ_DIRT
#pragma multi_compile_local _ _HDR_GRADING _TONEMAP_ACES _TONEMAP_NEUTRAL _TONEMAP_UE4ACES /////////////////UE4_ACES_BEGIN/////////////////
#pragma multi_compile_local _ _FILM_GRAIN
#pragma multi_compile_local _ _DITHERING
#pragma multi_compile_local _ _LINEAR_TO_SRGB_CONVERSION
Add operation to lut builder.
Assets/BundleRes/Shader/PostEffect/LutBuilderHdr.shader
// Note: when the ACES tonemapper is selected the grading steps will be done using ACES spaces
float3 ColorGrade(float3 colorLutSpace)
{
// Switch back to linear
float3 colorLinear = LogCToLinear(colorLutSpace);
// White balance in LMS space
float3 colorLMS = LinearToLMS(colorLinear);
colorLMS *= _ColorBalance.xyz;
colorLinear = LMSToLinear(colorLMS);
// Do contrast in log after white balance
/////////////////UE4_ACES_BEGIN///////////////
#if _TONEMAP_ACES || _TONEMAP_UE4ACES
float3 colorLog = ACES_to_ACEScc(unity_to_ACES(colorLinear));
/////////////////UE4_ACES_END///////////////
#else
float3 colorLog = LinearToLogC(colorLinear);
#endif
colorLog = (colorLog - ACEScc_MIDGRAY) * _HueSatCon.z + ACEScc_MIDGRAY;
/////////////////UE4_ACES_BEGIN///////////////
#if _TONEMAP_ACES || _TONEMAP_UE4ACES
colorLinear = ACES_to_ACEScg(ACEScc_to_ACES(colorLog));
/////////////////UE4_ACES_END///////////////
#else
colorLinear = LogCToLinear(colorLog);
#endif
// Color filter is just an unclipped multiplier
colorLinear *= _ColorFilter.xyz;
// Do NOT feed negative values to the following color ops
colorLinear = max(0.0, colorLinear);
...
)
ColorGrade()에 _TONEMAP_UE4ACES 전처리 키워드를 추가합니다.
float3 Tonemap(float3 colorLinear)
{
#if _TONEMAP_NEUTRAL
{
colorLinear = NeutralTonemap(colorLinear);
}
#elif _TONEMAP_ACES
{
// Note: input is actually ACEScg (AP1 w/ linear encoding)
float3 aces = ACEScg_to_ACES(colorLinear);
colorLinear = AcesTonemap(aces);
}
/////////////////UE4_ACES_BEGIN/////////////////
#elif _TONEMAP_UE4ACES
{
float3 aces = ACEScg_to_ACES(colorLinear);
colorLinear = UE4ACES(aces);
}
/////////////////UE4_ACES_END/////////////////
#endif
return colorLinear;
}
Packages/com.unity.render-pipelines.universal@11.0.0/Runtime/Passes/ColorGradingLutPass.cs
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.ColorGradingLUT)))
{
...
// Secondary curves
material.SetTexture(ShaderConstants._CurveHueVsHue, curves.hueVsHue.value.GetTexture());
material.SetTexture(ShaderConstants._CurveHueVsSat, curves.hueVsSat.value.GetTexture());
material.SetTexture(ShaderConstants._CurveLumVsSat, curves.lumVsSat.value.GetTexture());
material.SetTexture(ShaderConstants._CurveSatVsSat, curves.satVsSat.value.GetTexture());
/////////////////UE4_ACES_BEGIN/////////////////
//HDR Bind UI
material.SetFloat("FilmSlope", (float)tonemapping.slope);
material.SetFloat("FilmToe", (float)tonemapping.toe);
material.SetFloat("FilmShoulder", (float)tonemapping.shoulder);
material.SetFloat("FilmBlackClip", (float)tonemapping.blackClip);
material.SetFloat("FilmWhiteClip", (float)tonemapping.whiteClip);
/////////////////UE4_ACES_END/////////////////
// Tonemapping (baked into the lut for HDR)
if (hdr)
{
material.shaderKeywords = null;
switch (tonemapping.mode.value)
{
case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
case TonemappingMode.ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapACES); break;
/////////////////UE4_ACES_BEGIN/////////////////
case TonemappingMode.UE4_ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapUE4ACES); break;
/////////////////UE4_ACES_END/////////////////
default: break; // None
}
}
...
}
ColorGradingLutPass.cs 에 UE4 Aces 인수를 추가하고 키워드를 추가합니다.
부록.
중립적 톤맵핑.
racoon-artworks.de/cgbasics/tonemapping.php
float3 NeutralTonemap(float3 x, float A, float B, float C, float D, float E,
float F, float WL, float WC)
{
// Tonemap
float a = A;
float b = B;
float c = C;
float d = D;
float e = E;
float f = F;
float whiteLevel = WL;
float whiteClip = WC;
float3 whiteScale = (1.0).xxx / NeutralCurve(whiteLevel, a, b, c, d, e, f);
x = NeutralCurve(x * whiteScale, a, b, c, d, e, f);
x *= whiteScale;
// Post-curve white point adjustment
x /= whiteClip.xxx;
return x;
}
'UNITY3D' 카테고리의 다른 글
드레곤헤어 침묵의 신 작업 했을 때 히어로 렌더링 중에서... (0) | 2023.12.15 |
---|---|
Tone-mapping assessment guide (1) | 2023.09.01 |
[번역]Firewatch multi-coloured fog (0) | 2023.08.30 |
[번역]The Fog of Random! Losf of Random 개발 사례. (0) | 2023.08.30 |
Light-Probe Data manager implementation (0) | 2023.05.16 |