Custom SRP - 16 Render Scale

Scaling Up and Down这篇教程介绍如何解耦屏幕分辨率与渲染分辨率

  • 支持缩放

  • 支持每个摄像机不同的缩放

  • 在 post fx 之后恢复缩放,避免失真

1 Variable Resolution

程序运行在固定的分辨率下,一些程序允许在运行时更改分辨率,但这需要重新初始化图形设备。一种更灵活的方式是不改变程序分辨率,而是改变摄像机渲染的缓冲区的分辨率。这会影响除了最后绘制到 frame buffer 的整个渲染过程,最后的绘制会执行缩放后匹配程序的分辨率。

缩放 buffer 尺寸可以降低渲染的片段数量,从而提升性能。比如可以以全分辨率渲染UI,而以较低的分辨率渲染3D场景。可以动态的调整缩放,以获得一个能接受的帧率。最后,可以通过增大缩放,来实现超采样,以降低锯齿,该技术就是 SSAA 抗锯齿。

1.1 Buffer Settings

首先向 CameraBufferSettings 中增加一个滑动条来调整 scale,最小值是 0.1,最大值是 2,因为如果用 bilinear 插值缩放回屏幕分辨率,缩放超过2对于提升图像质量就没什么帮助了,甚至由于会跳过很多像素,下采样到屏幕分辨率时,图像质量还会降低。同时默认值设置为 1。

cs 复制代码
// CameraBufferSettings.cs
public class CameraBufferSettings
{
    ...
    [Range(0.1f,2f)]
    public float renderScale;
}

// CustomRenderPipelineAsset.cs
public partial class CustomRenderPipelineAsset : RenderPipelineAsset 
{
    ...
    [SerializeField] CameraBufferSettings cameraBuffer = new CameraBufferSettings() { 
        allowHDR = true,
        renderScale = 1.0f,
    };
    ...
}

1.2 Scaled Rendering

在 CameraRenderer 中跟踪是否使用缩放渲染,根据配置的值是否等于 1 来确定。实际上缩放太小的话(比如<1%)基本上没什么效果,因此当差异大于 1% 时才使用缩放渲染。

缩放后,buffer size 和摄像机组件上的 size 不同,因此用 Vector2Int 类型记录下 buffer size。

判定是否缩放渲染后,在 PrepareForSceneWindow 中,如果是 Scene Camera,就关闭缩放渲染。因为 Scene 窗口是用来编辑场景的,不需要缩放。

缩放渲染同样需要我们渲染到中间缓冲区,因此需要设置相关状态。

cs 复制代码
public void Render(...)
{
    ...
    useHDR = cameraBufferSettings.allowHDR && camera.allowHDR;
    // 启用缩放渲染,缩放至少要达到 1%
    float renderScale = cameraBufferSettings.renderScale;
    useScaledRendering = renderScale <= 0.99f || renderScale >= 1.01f;
    if (useScaledRendering)
    {
        bufferSize.x = (int)(camera.pixelWidth * renderScale);
        bufferSize.y = (int)(camera.pixelHeight * renderScale);
    }
    else
    {
        bufferSize.x = camera.pixelWidth;
        bufferSize.y = camera.pixelHeight;
    }


    PrepareForSceneWindow();
    ...
}

partial void PrepareForSceneWindow()
{
    if (camera.cameraType == CameraType.SceneView)
    {
        ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
        useScaledRendering = false;
    }
}

void Setup()
{
    ...
    useIntermediateBuffer = useScaledRendering || useDepthTexture 
                        || useColorTexture || postFXStack.IsActive;
    ...
}

1.3 BufferSize

创建 color/depth attachment buffer,以及 color/depth texture 时,使用记录的尺寸创建。

cs 复制代码
...
buffer.GetTemporaryRT(colorAttachmentId,
    bufferSize.x, bufferSize.y,
    32, FilterMode.Bilinear, 
    useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);
buffer.GetTemporaryRT(depthAttachmentId, bufferSize.x, bufferSize.y,
    32, FilterMode.Point, RenderTextureFormat.Depth);
...

void CopyAttachments()
{
    if(useColorTexture)
    {
        buffer.GetTemporaryRT(colorTextureId, bufferSize.x, bufferSize.y,
            0, FilterMode.Bilinear, useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);
     ...

    if(useDepthTexture)
    {
        buffer.GetTemporaryRT(depthTextureId, bufferSize.x, bufferSize.y,
            32, FilterMode.Point, RenderTextureFormat.Depth);
    ...
}

现在,可以在关闭 postFX 的情况下,设置 render scale,并在 Game 视口放大,可以清楚的看到像素的变化。

降低 render scale 可以提升渲染速度,降低图像质量。增大 render scale 则相反。需要注意的是,当关闭 postFX 时,render scale 需要专门的中间 buffer,以及额外的绘制,因此需要额外的工作量。

重新缩放到屏幕分辨率是在 final draw 时自动完成的,只是简单的双线性上采样或下采样。唯一奇怪的结果涉及 HDR 值,这些值似乎破坏了插值结果。可以在高亮的黄色球上看到这样的结果,稍后会加以处理。

1.4 Fragment Screen UV

缩放后引入了一个BUG:采样颜色和深度贴图时出错了。通过粒子的扰动可以看到这些错误,这是因为使用了错误的屏幕空间 UV 坐标。

这是因为 Unity 向 _ScreenParams 中写入的是屏幕的分辨率,而不是我们的 buffer 的尺寸。我们定义一个新的 _CameraBufferSize 来传递正确的值,来解决该问题

cs 复制代码
static int bufferSizeId = Shader.PropertyToID("_CameraBufferSize");
...

public void Render(...)
{
    ...
    // 让 lighting 作为 SampleName 的子项目
    buffer.BeginSample(SampleName);
    buffer.SetGlobalVector(bufferSizeId, 
        new Vector4(1f/bufferSize.x, 1f/bufferSize.y, bufferSize.x, bufferSize.y));
    ExecuteBuffer();
    ...
}

在 Fragment.hlsl 声明该常量,并在计算屏幕UV时使用我们传入的值。因为我们已经在CPU端计算了 buff size 的倒数,因此在 shader 中改为乘法。

cs 复制代码
float4 _CameraBufferSize;

Fragment GetFragment(float4 positionSS)
{
    ...
    f.screenUV = positionSS.xy * _CameraBufferSize.xy;
    ...
}

_ScreenParams 的后2个分量,存储了分辨率的倒数+1,这个 1 在其它一些用途下可以节省一个加法运算,我们如果用该值,则需要减 1。

1.5 Scaled Post FX

调整缩放也需要调整 post FX,否则会得到意料之外的结果,因此 post FX 也需要引用 buffer size,将参数传递给 PostFXStack.setup 接口,然后 PostFXStack 将该参数缓存下来。然后在 DoBloom 中使用 buffer size。

Bloom 的效果依赖分辨率,调整 render scale 会改变其结果。在迭代次数少时可以很容易的看到这种结果:降低 render scale 会导致 bloom 效果范围变大,增大 render scale 则范围变小。将 bloom 迭代次数设置到最大,这种差异就不那么明显了,但是调整缩放时导致的分辨率变化,会产生闪烁的效果。

特別是如果 render scale 是逐步调整的,我们希望尽量保证 bloom 效果的稳定性。通过让 bloom 金字塔的底层从摄像机分辨率开始,就是一个可行的办法。因此我们在 BloomSettings 中加入一个选项,来忽略 render scale,以避免这种不希望的效果,在 DoBloom 时根据选项确定 bloom 工作的尺寸

cs 复制代码
public struct BloomSettings
{
    ...
    public bool ignoreRenderScale;
}

...

Vector2Int bufferSize;
...
public void Setup(ScriptableRenderContext context, 
    Camera camera, PostFXSettings settings, bool useHDR, Vector2Int bufferSize,
    CameraSettings.FinalBlendMode finalBlendMode)
{
    this.bufferSize = bufferSize;
    ...
}
...
bool DoBloom(int sourceId)
{
    buffer.BeginSample("Bloom");
    var bloom = settings.Bloom;

    int width, height;
    if(bloom.ignoreRenderScale)
    {
        width = camera.pixelWidth;
        height = camera.pixelHeight;
    }
    else
    {
        width = bufferSize.x / 2;
        height = bufferSize.y / 2;
    }
    ...
    // 绘制到结果图像
    buffer.GetTemporaryRT(bloomResultId, bufferSize.x, bufferSize.y, 0, FilterMode.Bilinear, format);
    Draw(fromId, bloomResultId, finalPass);
    ...
}

对比下两张图,上图 ingore render scale = false,不同 render scale 效果不同,下图 ignore render scale = true,不同 render scale 效果基本一致。

1.6 Render Scale per Camera

接下来我们让每个摄像机可以设置自己的 render scale,或者使用 CameraBufferSettings 中的管线全局配置。我们将相关的配置参数定义到 CameraSettings 中:

首先定义摄像机 render scale 配置模式,包括:

  • inherit 使用全局配置

  • multiply 摄像机参数与全局参数相乘作为 render scale

  • override 使用摄像机配置

然后定义 render scale 的值。

同时定义 GetRenderScale 函数,以外部(全局)的 render scale 作为参数,根据 render scale mode 返回相应的 render scale。

cs 复制代码
public class CameraSettings
{
    ...
    public enum RenderScaleMode { Inherit, Multiply, Override}
    public RenderScaleMode renderScaleMode = RenderScaleMode.Inherit;
    [Range(0.1f,2f)]
    public float renderScale = 1.0f;

    public float GetRenderScale(float rpRenderScale)
    {
        return renderScaleMode == RenderScaleMode.Inherit ? rpRenderScale :
            renderScaleMode == RenderScaleMode.Override ? renderScale :
            rpRenderScale * renderScale;
    }
}

在 CameraRenderer.Render 中,调用上面定义的函数获得 render scale。因为函数内部可能会执行乘法,使结果不在 0.1 - 2 之间,因此调用 clamp 函数确保值的范围:

cs 复制代码
...
float renderScale = cameraSettings.GetRenderScale(cameraBufferSettings.renderScale);
useScaledRendering = renderScale <= 0.99f || renderScale >= 1.01f;
if (useScaledRendering)
{
    renderScale = Mathf.Clamp(renderScale, 0.1f, 2f);
    bufferSize.x = (int)(camera.pixelWidth * renderScale);
    bufferSize.y = (int)(camera.pixelHeight * renderScale);
}
else
...

如下图,底下的摄像机和上面中间的摄像机,render scale 分别为 0.8 和 1

2 Rescaling

如果设置 render scale 为 1 以外的值,那么所有内容都会被缩放,除了最后绘制到 camera target buffer。

如果没有开启 post FX,则需要一个简单的 copy 来缩放回最终尺寸。如果开启了 post FX,则由其 final draw 来完成回复缩放的操作。然而,通过 final draw 来完成恢复缩放的操作也有一些缺点。

2.1 Current Approach

目前的方法有些缺点:

  • 首先,无论是 upscaling 或 downscaling,对于亮度超过 1 的HDR颜色,总是会走样。插值仅在 LDR 时是平滑的。HDR 插值的结果依然大于1,看起来像是完全没有混合。例如 0 和 10 的平均值是 5,在LDR中 0 和 1 的平均值是 1,而不是我们期望的 0.5。
  • 通过 final pass 恢复缩放的第二个问题是 color correction 被应用在了插值过的颜色上,而不是原本的颜色。这会引入异常的颜色条带。最明显的是在阴影和高光之间插值的中间色调,由于将很强的颜色调整到了中间色调导致非常显眼,比如下图会导致其呈红色。

2.2 Rescaling in LDR

HDR 粗糙的边缘,以及 color correction 走样,都是由于在 color correction 和 tone mapping 前进行HDR颜色插值导致的(导致处理的是插值后的颜色,而不是原本的颜色)。因此解决方案是先执行 color correction 和 tone mapping,再在 LDR 中通过一个 copy pass 恢复缩放。在 PostFXStack.shader 中增加一个 rescale pass 来完成这个步骤。该 pass 也是一个简单的 copy pass,允许配置 blend mode,同时增加 pass 对应的枚举值。现在有两个 final pass,因此需要给 DrawFinal 增加一个参数,指定用哪个 pass。

cs 复制代码
Pass
{
    Name "Final Rescale"

    Blend [_FinalSrcBlend] [_FinalDstBlend]

    HLSLPROGRAM
    #pragma target 3.5
    #pragma vertex DefaultPassVertex
    #pragma fragment CopyPassFragment
    ENDHLSL
}

检测当没有 scale 时,直接 DrawFinal。

否则需要绘制两次:首先获取一个匹配缩放过的分辨率的 RT 来执行 color grading and tone mapping,也就是 Final Pass,同时混合模式为 One Zero。然后再执行 rescale pass。

cs 复制代码
void DoColorGradingAndToneMapping(int sourceId)
{
    ...
    // 没有缩放,直接 draw final
    if(bufferSize.x == camera.pixelWidth)
    {
        DrawFinal(sourceId, Pass.Final);
    }
    // 有缩放,现在缩放过的尺寸上进行 color grading and tone mapping,然后在通过 final rescale pass 赋值到 camera target
    else
    {
        buffer.SetGlobalFloat(finalSrcBlendId, 1f);
        buffer.SetGlobalFloat(finalDstBlendId, 0f);
        buffer.GetTemporaryRT(finalResultId, bufferSize.x, bufferSize.y, 0, FilterMode.Bilinear, RenderTextureFormat.Default);
        Draw(sourceId, finalResultId, Pass.Final);
        DrawFinal(finalResultId, Pass.FinalRescale);
        buffer.ReleaseTemporaryRT(finalResultId);
    }
    buffer.ReleaseTemporaryRT(colorGradingLUTId);
}

现在有 render scale 时 HDR 颜色也对了

color grading 也不会导致颜色条带了

我们的方法仅在启用了 post FX 时有效,未启用时我们假定没有 color grading ,也没有 HDR,因此没有上面的问题。

2.3 Bicubic Sampling

当 render scale 过小时,图像会有色块的感觉。我们之前为 bloom 添加 bicubic 上采样选项来改善图像质量。在 rescale 时也可以用该方法来提升质量,添加该 bicubicRescaling 选项到 CameraBufferSettings 中。

在 PostFXStackPasses.hlsl 中,添加对应的shader常量,并根据该常量决定采样方式。修改 shader 中的 Rescale pass 使用该 fragment

cs 复制代码
bool _CopyBicubic;
float4 FinalRescalePassFragment(Varyings intput) : SV_Target
{
    if (_CopyBicubic)
        return GetSourceBicubic(input.screenUV);
    else
        return GetSource(input.screenUV);
}

在 PostFXStack.cs 中,用参数传入 CameraBufferSettings 中的参数,并同步给 shader 常量参数

cs 复制代码
...
buffer.SetGlobalFloat(bicubicRescalingId, bicubicRescaling ? 1f : 0f);
DrawFinal(finalResultId, Pass.FinalRescale);
...

2.4 Only Bicubic Upscaling

Bicubic rescaling 在上采样时可以提升质量,但是在下采样时,效果就不明显了,因此将 bicubic rescaling 改为三种状态:off,up only,up and down,然后对相关逻辑做调整就可以了

cs 复制代码
bool isBicubicRescaling = 
    bicubicRescaling == CameraBufferSettings.BicubicRescalingMode.UpAndDown
    || (bicubicRescaling == CameraBufferSettings.BicubicRescalingMode.UpOnly && bufferSize.x < camera.pixelWidth);
buffer.SetGlobalFloat(bicubicRescalingId, isBicubicRescaling ? 1f : 0f);
相关推荐
毕安格 - BimAngle7 小时前
Revit 模型一键输出 3D Tiles (for Cesium) 和 glTF/glb
3d·cesium·gltf·glb·3d tiles
map_vis_3d8 小时前
JSAPIThree 加载 3D Tiles 学习笔记:大规模三维场景渲染
笔记·学习·3d
da_vinci_x9 小时前
Substance 3D Painter 进阶:手绘“掉漆”太累?用 Anchor Point 让材质“活”过来
游戏·3d·aigc·材质·设计师·技术美术·游戏美术
ellis19709 小时前
Unity出安卓包知识点汇总
android·unity
攻城狮7号10 小时前
微软开源 TRELLIS.2:单图 3 秒变 3D?
人工智能·3d·trellis.2·o-voxel·sc-vae·微软开源模型
樱桃园园长12 小时前
【Three.js 实战】手势控制 3D 奢华圣诞树 —— 从粒子系统到交互实现
javascript·3d·交互
二狗哈12 小时前
Cesium快速入门30:CMZL动画
javascript·3d·webgl·cesium·地图可视化
二狗哈13 小时前
Cesium快速入门29:CMZL数据格式加载
3d·状态模式·webgl·cesium·着色器·地图可视化
Robot侠13 小时前
ROS1从入门到精通 2:ROS1核心概念详解(节点、话题、服务一网打尽)
unity·游戏引擎·ros·机器人操作系统