URP | 后处理-色调映射-ACES
那个人真狗
编辑于 2022年11月20日 17:48
收录于文集
共43篇

内容偏多这是目录

目的

  • 什么是Tone mapping ? 什么是ACEs?

  • 使用ACEs 有什么好处?为什么怎么使用?

效果

ACES后

色调映射

Tone mapping

Tone Mapping原是摄影学中的一个术语,因为打印相片所能表现的亮度范围不足以表现现实世界中的亮度域 , 而如果简单的将真实世界的整个亮度域线性压缩到照片所能表现的亮度域内,则会在明暗两端同时丢失很多细节,这显然不是所希望的效果,Tone Mapping就是为了克服这一情况而存在的.

既然相片所能呈现的亮度域有限则我们可以根据所拍摄场景内的整体亮度通过光圈与曝光时间的长短来控制一个合适的亮度域,这样既保证细节不丢失,也可以不使照片失真

这里有两个关键 ,

  • 光圈大小

  • 曝光时长短。

到了计算机图形学发展的时代,最早只有LDR,颜色就是0-255。这个情况一直持续了很长很长时间。 为了让图像更真实的显示在显示器上,同样需要Tone Mapping来辅助。

URP Tone mapping

URP是线性空间,游戏在多光源渲染过程,颜色信息有的超过 0-255的范围。 就称之为HDR模式,我们显示器默认显示的是LDR,LDR也就是 颜色范围是 (0-255)

使用游戏渲染过程中产生HDR的画面。需要映射到LDR的设备上,就需要转换的过程(tone mapping),就是Tone mapping。

  • URP默认Tone mapping

URP默认给我们提供了 2种默认的

  • Neutral

  • ACES

原理

过程就是首先要根据当前的场景计算出场景的平均亮度,再将整个场景映射到这个亮度域得到正确的结果。

Middle grey  :    整个场景的平均灰度,关系到场景所应处在亮度域Key:场景的Key将决定整个场景的亮度倾向,倾向偏亮亦或是偏暗。

[参考]Tone Mapping---色调映射算法_one_u_h的博客-CSDN博客

  • 下图是处理过程流程图

曝光

当人眼观察一个场景时,眼睛会自然扩张以适应到达它的光量。 上面我们说到摄影使用光圈大小和曝光来控制曝光获取场景光的大小。

曝光是以模拟实际光照射到传感器的光线多少,避免光线过多或过少。

  • 渲染画面我们也需要获取场景的曝光值,使用曝光值来控制场景

这里使用

近似的ACES拟合曲线由Krzysztof Narkowicz

[参考] ACES Filmic Tone Mapping Curve | Krzysztof Narkowicz (wordpress.com)

这里由我们来控制输入的曝光值的大小,默认 0.6

算法

  • 拟合曲线

代码块
JavaScript
自动换行
复制代码
float3 ACESFilm(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}
复制成功
  • 曲线

cut-off

URP实现过程

思路

  • 我们实现基础的后处理管线计算,

  • 制作后处理组件调节属性。

  • 核心计算使用Shader类计算,Shader在给到后处理。

Shader

代码块
JavaScript
自动换行
复制代码
const float ExposureMultiplier = _postExposure;                  // 曝光值
复制成功

色调映射矩阵

代码块
C#
自动换行
复制代码
const float3x3 PRE_TONEMAPPING_TRANSFORM =              // 色调映射矩阵            
    {
     0.575961650, 0.344143820, 0.079952030,
     0.070806820, 0.827392350, 0.101774690,
     0.028035252, 0.131523770, 0.840242300
    };
复制成功

整个场景的色调

代码块
C#
自动换行
复制代码
const float3x3 EXPOSED_PRE_TONEMAPPING_TRANSFORM = ExposureMultiplier * PRE_TONEMAPPING_TRANSFORM;    // key  场景颜色乘 曝光度 整个场景的色调。
复制成功

把我们输入线性颜色映射到 ACES

代码块
C#
自动换行
复制代码
/*
    float a; // 2.51
    float b; // 0.03
    float c; // 2.43
    float d; // 0.59
    float e; // 0.14
    */

    float3 Color = mul(EXPOSED_PRE_TONEMAPPING_TRANSFORM, LinearColor);                        // 线性颜色转换 
    Color = saturate((Color * (a * Color + b)) / (Color * (c * Color + d) + e));               // 应用到ACES 颜色校正
复制成功

在进行gamma校正

代码块
C#
自动换行
复制代码
const float3x3 POST_TONEMAPPING_TRANSFORM =
    {
        1.666954300, -0.601741150, -0.065202855,
    -0.106835220, 1.237778600, -0.130948950,
    -0.004142626, -0.087411870, 1.091555000
    };


return clamp(mul(POST_TONEMAPPING_TRANSFORM, Color), 0.0f, 1.0f);                          // 后置处理(颜色重构)
复制成功

全代码

代码块
C#
自动换行
复制代码
Shader "URP/ACEs"
{
    Properties
    {
        _MainTex ("_MainTex", 2D) = "white" {}
        _FilmSlope("_FilmSlope", float) = 2.51
        _FilmToe("_FilmToe", float) = 0.03
        _FilmShoulder("_FilmShoulder", float) = 2.43
        _FilmBlackClip("_FilmBlackClip", float) = 0.59
        _FilmWhiteClip("_FilmWhiteClip", float) = 0.14
    }
    SubShader
    {
        Tags { "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "ACES.hlsl"           //函数库


            ENDHLSL
        }
    }
}
复制成功

代码块
C#
自动换行
复制代码
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"



CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float _FilmSlope, _FilmToe, _FilmShoulder, _FilmBlackClip, _FilmWhiteClip;
float _postExposure;
CBUFFER_END


TEXTURE2D(_MainTex);            SAMPLER(sampler_MainTex);


struct appdata
{
    float4 positionOS : POSITION;
    float2 texcoord : TEXCOORD0;
};

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;

};




float3 ACESFilm(float3 LinearColor, float a, float b, float c, float d, float e)
{
    const float ExposureMultiplier = _postExposure;                  // 曝光值

    const float3x3 PRE_TONEMAPPING_TRANSFORM =              // 色调映射矩阵            
    {
     0.575961650, 0.344143820, 0.079952030,
     0.070806820, 0.827392350, 0.101774690,
     0.028035252, 0.131523770, 0.840242300
    };

    const float3x3 EXPOSED_PRE_TONEMAPPING_TRANSFORM = ExposureMultiplier * PRE_TONEMAPPING_TRANSFORM;    // key  场景颜色乘 曝光度 整个场景的色调。
   
    const float3x3 POST_TONEMAPPING_TRANSFORM =
    {
        1.666954300, -0.601741150, -0.065202855,
    -0.106835220, 1.237778600, -0.130948950,
    -0.004142626, -0.087411870, 1.091555000
    };
    
    /*
    float a; // 2.51
    float b; // 0.03
    float c; // 2.43
    float d; // 0.59
    float e; // 0.14
    */

    float3 Color = mul(EXPOSED_PRE_TONEMAPPING_TRANSFORM, LinearColor);                        // 线性颜色转换 
    Color = saturate((Color * (a * Color + b)) / (Color * (c * Color + d) + e));               // 应用到ACES 颜色校正

    return clamp(mul(POST_TONEMAPPING_TRANSFORM, Color), 0.0f, 1.0f);                          // 后置处理(颜色重构)

}



v2f vert(appdata v)
{
    v2f o;
    o.vertex = TransformObjectToHClip(v.positionOS.xyz);
    o.uv = v.texcoord;
    return o;
}

half4 frag(v2f i) : SV_Target
{
    
    float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
     
    col.xyz = ACESFilm(col.xyz, _FilmSlope, _FilmToe, _FilmShoulder, _FilmBlackClip, _FilmWhiteClip);
    
    return col;
}
复制成功

注意:ACESFilm是float3类型

Volume

后处理组件属性很简单

代码块
C#
自动换行
复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;




public class ACEsVolume : VolumeComponent, IPostProcessComponent
{

    public FloatParameter PostExposure = new FloatParameter(0.6f);

    [Tooltip("Film_Slope")]
    public ClampedFloatParameter slope = new ClampedFloatParameter(2.51f, 0f, 3f);
    /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
    [Tooltip("Film_Toe")]
    public ClampedFloatParameter toe = new ClampedFloatParameter(0.03f, 0.0f, 1.0f);
    /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
    [Tooltip("Film_Shoulder")]
    public ClampedFloatParameter shoulder = new ClampedFloatParameter(2.43f, 0.0f, 3.0f);
    /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
    [Tooltip("Film_BlackClip")]
    public ClampedFloatParameter blackClip = new ClampedFloatParameter(0.59f, 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.14f, 0.0f, 1.0f);


    public bool IsActive() => PostExposure.value > 0f;

    public bool IsTileCompatible() => false;


}
复制成功

  • 效果

注意:曝光值是0.6

RenderFeature

URP | 后处理-自定义后处理 - 哔哩哔哩 (bilibili.com)​

URP | 后处理-模糊算法总结 - 哔哩哔哩 (bilibili.com)​

主要是绑定 Shader属性,具体不清楚增加看前面的后处理

代码块
C#
自动换行
复制代码
// 属性绑定
        acesMaterial.SetFloat("_postExposure", acesVolume.PostExposure.value);                       // Shader变量  和 Volume 组件属性 绑定

        acesMaterial.SetFloat("_FilmSlope", acesVolume.slope.value);                       // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmToe", acesVolume.toe.value);                           // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmShoulder", acesVolume.shoulder.value);                 // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmBlackClip", acesVolume.blackClip.value);               // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmWhiteClip", acesVolume.whiteClip.value);               // Shader变量  和 Volume 组件属性 绑定
复制成功

设置RT

代码块
C#
自动换行
复制代码
cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去
复制成功

注意:设置可以储存HDR模式

代码

代码块
C#
自动换行
复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;



public class ACESRenderFeature : ScriptableRendererFeature
{
    [System.Serializable] 
    public class Settings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;   
        public Shader shader;
    }
    public Settings settings = new Settings();

    ACESPass  acesPass;           // 定义我们创建出Pass


    public override void Create()
    {
        this.name = "ACES";
        acesPass = new ACESPass(RenderPassEvent.BeforeRenderingPostProcessing, settings.shader);    // 初始化 我们的渲染层级和Shader
    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(acesPass);
    }
}



public class ACESPass : ScriptableRenderPass
{
    static readonly string RenderTag = "Post Effects";                         // 设置渲染标签

    ACESVolume acesVolume;                                                // 定义组件类型
    Material acesMaterial;                                                      // 后处理材质 

    public ACESPass(RenderPassEvent evt, Shader biltshader)
    {
        renderPassEvent = evt;
        var shader = biltshader;

        if (shader == null)
        {
            Debug.LogError("没有指定Shader");
            return;
        }
        acesMaterial = CoreUtils.CreateEngineMaterial(biltshader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (acesMaterial == null)
        {
            Debug.LogError("材质初始化失败");
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }

        var stack = VolumeManager.instance.stack;                                    // 传入 volume 
        acesVolume = stack.GetComponent<ACESVolume>();                     // 获取到后处理组件

        if (acesVolume == null)
        {
            Debug.LogError("获取组件失败");
            return;
        }

        var cmd = CommandBufferPool.Get(RenderTag);    // 渲染标签 

        Render(cmd, ref renderingData);                 // 调用渲染函数  

        context.ExecuteCommandBuffer(cmd);              // 执行函数,回收。
        CommandBufferPool.Release(cmd);

    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;                 // 定义RT
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;                                                                          // 清除深度


        // 属性绑定
        acesMaterial.SetFloat("_postExposure", acesVolume.PostExposure.value);                       // Shader变量  和 Volume 组件属性 绑定

        acesMaterial.SetFloat("_FilmSlope", acesVolume.slope.value);                       // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmToe", acesVolume.toe.value);                           // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmShoulder", acesVolume.shoulder.value);                 // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmBlackClip", acesVolume.blackClip.value);               // Shader变量  和 Volume 组件属性 绑定
        acesMaterial.SetFloat("_FilmWhiteClip", acesVolume.whiteClip.value);               // Shader变量  和 Volume 组件属性 绑定


        int destination = Shader.PropertyToID("Temp1");

        // 获取一张临时RT
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去                                                                 

        cmd.Blit(source, destination);                            // 设置后处理

        cmd.Blit(destination, source, acesMaterial, 0);           //  第二个Pass  
    }
}
复制成功

cut-off

效果对比

自定义和系统默认

  • 这是默认线性模式,没有进行Tone Mapping颜色爆,亮部看不到细节

默认

  • Unity 默认 ACES

  • 自定义ACE

增加饱和度

  • 如果我们感觉颜色饱和度还是不够,我们可以调整饱和度。

总结

  1. Tone Mapping 是色调映射的方式,需要把高动态颜色范围(HDR)映射到低动态颜色(LDR),我们渲染出来的颜色范围是大于 [0-1]的,我们需要映射到[ 0-1]的范围。

  2. ACES 是 映射方式其中比较好的算法,目前大部分是使用这个算法。

  3. 在Shader 计算不同颜色映射比较复杂,需要转换俩次,计算一次是线性颜色转换成ACES颜色,第二次是进行Gamma校正输出到屏幕。

    1. 色调映射 ----图像颜色进行变换的算法 [参考]Tone Mapping---色调映射算法_one_u_h的博客-CSDN博客

    2. 决定整个场景的亮度倾向,倾向偏亮亦或是偏暗。

  1. 应用ACEs 模式

  1. 后置处理,颜色重构