Render Scale Scaling Up and Down
通过滑块调整渲染缩放比例。
支持每个摄像机使用不同的渲染缩放比例。
在所有后期特效处理后重新缩放到最终目标。
这是关于创建自定义可编程渲染管线的教程系列的第16部分,主要内容是将渲染分辨率与目标缓冲区大小解耦。
本教程使用 Unity 2019.4.16f1 制作,并升级到 2022.3.5f1
Comparing different render scales, zoomed in
比较不同渲染缩放比例(放大视图)
1. Variable Resolution 可变分辨率
应用程序通常以固定分辨率运行。部分应用允许通过设置菜单调整分辨率,但这需要图形系统的完全重新初始化。一种更灵活的方法是保持应用程序分辨率不变,但改变相机用于渲染的缓冲区尺寸。这会影响除帧缓冲最终绘制之外的所有渲染环节,在最终绘制阶段,渲染结果会被重新缩放以匹配应用程序分辨率。
通过缩放缓冲区尺寸可以减少需要处理的片段数量,从而提升性能。例如,可以对所有3D渲染采用此技术,同时保持用户界面以全分辨率清晰显示。还可以动态调整缩放比例以维持可接受的帧率。此外,我们也可以增大缓冲区尺寸进行超采样,从而减轻因分辨率有限导致的锯齿瑕疵。这种最终技术方案也被称为SSAA(超采样抗锯齿)
1.1 Buffer Settings 缓冲区设置
调整渲染缩放比例会影响缓冲区大小,因此我们将在 CameraBufferSettings 中添加一个可配置的渲染比例滑块。我们需要设置一个最小缩放比例,这里我们使用 0.1。同时将最大值设为 2,因为如果我们仅通过单次双线性插值进行重缩放,超过这个值将无法进一步提升图像质量。超过 2 反而会降低质量,因为在降采样到最终目标分辨率时会导致许多像素被完全跳过
using UnityEngine;[System.Serializable]
public struct CameraBufferSettings {…[Range(0.1f, 2f)]public float renderScale;
}
默认渲染缩放比例应在 CustomRenderPipelineAsset 中设置为 1
CameraBufferSettings cameraBuffer = new CameraBufferSettings {allowHDR = true,renderScale = 1f};
Render scale slider
渲染缩放比例滑块
1.2 Scaled Rendering 缩放渲染
从现在开始,我们还将在CameraRenderer中跟踪是否正在使用缩放渲染
bool useHDR, useScaledRendering;
我们不希望配置的渲染缩放比例影响场景窗口,因为场景窗口是用于编辑的。在适当的时候,通过在 PrepareForSceneWindow 方法中关闭缩放渲染来强制执行此规则。
partial void PrepareForSceneWindow () {if (camera.cameraType == CameraType.SceneView) {ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);useScaledRendering = false;}}
我们会在Render方法中调用PrepareForSceneWindow之前,确定是否应使用缩放渲染。在一个变量中跟踪当前的渲染缩放比例,并检查该比例是否为1
float renderScale = bufferSettings.renderScale;useScaledRendering = renderScale != 1f;PrepareBuffer();PrepareForSceneWindow();
不过我们应该考虑得更细致一些,因为与1有非常微小偏差时,视觉和性能上都不会产生有实际意义的差异。因此,我们仅在缩放比例与1存在至少1%的差异时才使用缩放渲染
useScaledRendering = renderScale < 0.99f || renderScale > 1.01f;
从现在开始,当使用缩放渲染时,我们还必须使用一个中间缓冲区。因此,请在Setup方法中对此进行检查。
useIntermediateBuffer = useScaledRendering ||useColorTexture || useDepthTexture || postFXStack.IsActive;
1.3 Buffer Size 缓冲区尺寸
由于我们相机的缓冲区尺寸现在可能与Camera组件指示的尺寸不同,我们必须跟踪最终使用的缓冲区尺寸。为此,我们可以使用单个Vector2Int字段。
Vector2Int bufferSize;
在剔除成功后,在Render方法中设置适当的缓冲区尺寸。如果应用了缩放渲染,则缩放相机的像素宽度和高度,并将结果转换为整数(向下取整)。
if (!Cull(shadowSettings.maxDistance)) {return;}useHDR = bufferSettings.allowHDR && camera.allowHDR;if (useScaledRendering) {bufferSize.x = (int)(camera.pixelWidth * renderScale);bufferSize.y = (int)(camera.pixelHeight * renderScale);}else {bufferSize.x = camera.pixelWidth;bufferSize.y = camera.pixelHeight;}
在Setup方法中获取相机附件的渲染纹理时,请使用此缓冲区尺寸。
buffer.GetTemporaryRT(colorAttachmentId, bufferSize.x, bufferSize.y,0, 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);…}…}
最初可以尝试在不使用任何后期特效的情况下进行测试。你可以放大游戏窗口以便更清楚地看到单个像素,这样调整渲染缩放比例的效果会更加明显
No post FX; render scale 1; game window zoomed in
无后期特效;渲染缩放比例为1;游戏窗口已放大
降低渲染缩放比例会加快渲染速度,但会降低图像质量。提高渲染缩放比例则会产生相反效果。请注意,在不使用后期特效时,调整渲染缩放比例需要使用中间缓冲区和额外的绘制操作,因此会增加一些额外的工作量
Render scale 0.25, 0.5, 1.5, and 2
渲染缩放比例 0.25、0.5、1.5 和 2
缩放到目标缓冲区尺寸的操作由最终绘制自动完成。最终我们得到的是简单的双线性上采样或下采样操作。唯一异常的结果涉及HDR值,这些值似乎破坏了插值效果。你可以在上面截图中黄色球体的高光部分看到这种情况。我们稍后会处理这个问题
1.4 Fragment Screen UV 片段屏幕UV
调整渲染缩放比例会引入一个错误:对颜色和深度纹理的采样会出现问题。这可以通过粒子扭曲效果观察到,明显可以看到最终使用了不正确的屏幕空间UV坐标。
Incorrect distortion, render scale 1.5
不正确的扭曲,渲染缩放比例为1.5
出现这个问题的原因是Unity填入_ScreenParams的值匹配的是相机的像素尺寸,而非我们目标缓冲区的实际尺寸。我们通过引入一个替代的_CameraBufferSize向量来解决这个问题,该向量包含相机调整后的尺寸数据
static intbufferSizeId = Shader.PropertyToID("_CameraBufferSize"),colorAttachmentId = Shader.PropertyToID("_CameraColorAttachment"),
在确定缓冲区尺寸后,我们在Render方法中将这些值发送到GPU。我们将采用与Unity的_TexelSize向量相同的格式,即宽度和高度的倒数,后跟宽度和高度
if (useScaledRendering) {bufferSize.x = (int)(camera.pixelWidth * renderScale);bufferSize.y = (int)(camera.pixelHeight * renderScale);}else {bufferSize.x = camera.pixelWidth;bufferSize.y = camera.pixelHeight;}buffer.BeginSample(SampleName);buffer.SetGlobalVector(bufferSizeId, new Vector4(1f / bufferSize.x, 1f / bufferSize.y,bufferSize.x, bufferSize.y));ExecuteBuffer();
将向量添加到Fragment中
TEXTURE2D(_CameraColorTexture);
TEXTURE2D(_CameraDepthTexture);float4 _CameraBufferSize;
然后在GetFragment中使用它来代替_ScreenParams。现在我们还可以使用乘法来代替除法
f.screenUV = f.positionSS * _CameraBufferSize.xy;
Correct distortion, render scale 1.5
正确的扭曲效果,渲染缩放比例为1.5
_ScreenParams不是也包含倒数尺寸吗?
差不多。它的后两个分量包含的是倒数加1。这个额外的1为某些特定用途节省了一次加法运算,但在我们的使用场景中反而需要一次额外的减法运算,所以我没有采用它
1.5 Scaled Post FX 缩放后期特效
调整渲染缩放比例也应该影响后期特效,否则会导致意外的缩放效果。最可靠的方法是始终使用相同的缓冲区尺寸,因此我们将其作为新的第三个参数传递给CameraRenderer.Render中的PostFXStack.Setup
postFXStack.Setup(context, camera, bufferSize, postFXSettings, useHDR, colorLUTResolution,cameraSettings.finalBlendMode);
PostFXStack现在必须跟踪缓冲区尺寸
Vector2Int bufferSize;…public void Setup (ScriptableRenderContext context, Camera camera, Vector2Int bufferSize,PostFXSettings settings, bool useHDR, int colorLUTResolution,CameraSettings.FinalBlendMode finalBlendMode) {this.bufferSize = bufferSize;…}
这必须在DoBloom中使用,而不是直接使用相机的像素尺寸
bool DoBloom (int sourceId) {BloomSettings bloom = settings.Bloom;int width = bufferSize.x / 2, height = bufferSize.y / 2;…buffer.GetTemporaryRT(bloomResultId, bufferSize.x, bufferSize.y, 0,FilterMode.Bilinear, format);…}
由于泛光是一种与分辨率相关的特效,调整渲染缩放比例会改变其外观效果。这在仅使用少量泛光迭代时最为明显。降低渲染缩放比例会使特效范围变大,而提高渲染缩放比例则会使其变小。使用最大迭代次数的泛光看起来变化不大,但在调整渲染缩放比例时由于分辨率变化可能会呈现脉动效果
Two iterations of additive bloom; render scale 0.5, 1, and 2
两次叠加泛光迭代;渲染缩放比例分别为0.5、1和2
特别是当渲染缩放比例被逐渐调整时,可能希望尽可能保持泛光效果的一致性。这可以通过将泛光金字塔的起始尺寸基于相机分辨率而非缓冲区尺寸来实现。让我们通过在BloomSettings中添加一个切换选项来配置是否忽略渲染缩放比例。
public struct BloomSettings {public bool ignoreRenderScale;…}
如果应该忽略渲染缩放比例,PostFXStack.DoBloom将像之前一样从相机像素尺寸的一半开始。这意味着它不再执行默认的降采样到半分辨率,而是依赖于渲染缩放比例。最终的泛光结果仍应与缩放后的缓冲区尺寸匹配,因此这将在最后引入另一个自动的降采样或上采样步骤
bool DoBloom (int sourceId) {BloomSettings bloom = settings.Bloom;int width, height;if (bloom.ignoreRenderScale) {width = camera.pixelWidth / 2;height = camera.pixelHeight / 2;}else {width = bufferSize.x / 2;height = bufferSize.y / 2;}…}
当忽略渲染缩放比例时,泛光效果现在更加一致,尽管在非常低的缩放比例下,由于可用的数据量过少,效果仍然会有所不同
Bloom ignoring render scale; render scale 0.5, 1, and 2
泛光效果忽略渲染缩放比例;渲染缩放比例分别为0.5、1和2
1.6 Render Scale per Camera 每相机渲染缩放比例
让我们也实现每个相机使用不同渲染缩放比例的功能。例如,某个相机可以始终以半分辨率或双分辨率进行渲染。这可以是固定的(覆盖RP的全局渲染缩放比例),也可以是叠加应用的(相对于全局渲染缩放比例)。
在CameraSettings中添加一个渲染缩放比例滑块,其范围与RP资源相同。同时添加一个渲染缩放模式,可以通过新的内部RenderScaleMode枚举类型设置为继承、相乘或覆盖
public enum RenderScaleMode { Inherit, Multiply, Override }public RenderScaleMode renderScaleMode = RenderScaleMode.Inherit;[Range(0.1f, 2f)]public float renderScale = 1f;
Render scale mode
渲染缩放模式
为了应用每相机的渲染缩放比例,还需要给CameraSettings添加一个公共的GetRenderScale方法,该方法接收一个渲染缩放参数并返回最终缩放比例。因此,根据模式的不同,它会返回相同的缩放比例、相机的缩放比例或两者的乘积
public float GetRenderScale (float scale) {returnrenderScaleMode == RenderScaleMode.Inherit ? scale :renderScaleMode == RenderScaleMode.Override ? renderScale :scale * renderScale;}
在CameraRenderer.Render中调用该方法以获取最终渲染缩放比例,并将缓冲区设置中的缩放比例传递给它
float renderScale = cameraSettings.GetRenderScale(bufferSettings.renderScale);
我们还需要对最终渲染缩放比例进行限制,使其保持在0.1-2的范围内(如果需要的话)。这样可以防止因相乘而得到过小或过大的缩放比例
if (useScaledRendering) {renderScale = Mathf.Clamp(renderScale, 0.1f, 2f);bufferSize.x = (int)(camera.pixelWidth * renderScale);bufferSize.y = (int)(camera.pixelHeight * renderScale);}
由于我们对所有渲染缩放比例使用相同的最小值和最大值,让我们将它们定义为CameraRenderer的公共常量。这里我只展示常量的定义,不展示CameraRenderer、CameraBufferSettings和CameraSettings中0.1f和2f值的替换过程
public const float renderScaleMin = 0.1f, renderScaleMax = 2f;
Different render scale per camera
每相机不同渲染缩放比例
2. Rescaling 重新缩放
当使用非1的渲染缩放比例时,除了最终绘制到相机目标缓冲区外,所有操作都在该缩放比例下进行。如果不使用后期特效,这只是一个简单的复制操作,会将图像重新缩放到最终尺寸。当启用后期特效时,最终绘制操作也会隐式执行重新缩放。然而,在最终绘制期间进行重新缩放存在一些缺点
2.1 Current Approach 当前方案
我们当前的重新缩放方法会产生不希望的副作用。首先,正如我们之前已经注意到的,无论是上采样还是下采样,亮度超过1的HDR颜色总是会出现锯齿。只有在LDR模式下执行插值才能产生平滑的结果。HDR插值可能产生仍然大于1的结果,这些结果完全不会呈现混合效果。例如,0和10的平均值是5。在LDR模式下,0和1的平均值会显示为1,而我们期望的结果应该是0.5
Color interpolation with and without HDR; render scale 0.5 and 2
颜色插值(启用与未启用HDR);渲染缩放比例0.5和2
在最终通道中进行重新缩放的第二个问题是,颜色校正应用于插值后的颜色而非原始颜色。这可能会引入不期望的色带。最明显的是在阴影和高光之间插值时出现中间调。通过对中间调应用非常强烈的颜色调整(例如使其变红),可以使这种现象变得极其明显
Strong red midtones; render scale 0.5, 1, and 2
强烈的红色中间调;渲染缩放比例0.5、1和2
2.2 Rescaling in LDR LDR模式下的重新缩放
尖锐的HDR边缘和色彩校正伪影都是由在色彩校正和色调映射之前对HDR颜色进行插值引起的。因此,解决方案是在调整后的渲染缩放比例下执行这两个步骤,然后执行另一个重新缩放LDR颜色的复制通道。向PostFXStack着色器添加一个新的最终重新缩放通道来处理这最后一步。这只是一个具有可配置混合模式的复制通道。像往常一样,也将其添加到PostFXStack.Pass枚举中
Pass {Name "Final Rescale"Blend [_FinalSrcBlend] [_FinalDstBlend]HLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment CopyPassFragmentENDHLSL}
现在我们有两个最终通道,这需要我们向DrawFinal添加一个通道参数
void DrawFinal (RenderTargetIdentifier from, Pass pass) {…buffer.DrawProcedural(Matrix4x4.identity, settings.Material,(int)pass, MeshTopology.Triangles, 3);}
我们现在在DoColorGradingAndToneMapping中必须使用哪种方法,取决于我们是否正在使用调整后的渲染缩放比例。我们可以通过比较缓冲区尺寸与相机的像素尺寸来检查这一点。检查宽度就足够了。如果它们相等,我们就像以前一样绘制最终通道,现在明确使用Pass.Final作为参数
void DoColorGradingAndToneMapping (int sourceId) {…if (bufferSize.x == camera.pixelWidth) {DrawFinal(sourceId, Pass.Final);}else {}buffer.ReleaseTemporaryRT(colorGradingLUTId);}
但如果我们需要重新缩放,就必须进行两次绘制。首先获取一个与当前缓冲区尺寸匹配的新临时渲染纹理。由于我们在其中存储LDR颜色,使用默认的渲染纹理格式即可。然后使用最终通道执行常规绘制,并将最终混合模式设置为One Zero。接着使用最终重新缩放通道执行最终绘制,随后释放中间缓冲区
if (bufferSize.x == camera.pixelWidth) {DrawFinal(sourceId, Pass.Final);}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);}
经过这些调整后,HDR颜色也能正确显示插值效果了
Rescaling in LDR; render scale 0.5 and 2
LDR模式下的重新缩放;渲染缩放比例0.5和2
而且色彩分级不再引入在渲染缩放比例为1时不存在的色带
色彩校正后重新缩放;强烈的红色中间调;渲染缩放比例0.5和2
请注意,这仅在使用后期特效时修复了问题。否则就没有色彩分级,我们假设也没有HDR
2.3 Bicubic Sampling 双三次采样
当渲染缩放比例降低时,图像会变得块状化。我们之前为泛光效果添加了使用双三次上采样的选项以提升其质量,同样地,在重新缩放到最终渲染目标时也可以采用这种方法。在CameraBufferSettings中添加一个切换选项来实现此功能
public bool bicubicRescaling;
Bicubic rescaling toggle
双三次重新缩放切换选项
在PostFXStackPasses中添加一个新的FinalPassFragmentRescale函数,同时添加一个_CopyBicubic属性来控制使用双三次采样还是常规采样
bool _CopyBicubic;float4 FinalPassFragmentRescale (Varyings input) : SV_TARGET {if (_CopyBicubic) {return GetSourceBicubic(input.screenUV);}else {return GetSource(input.screenUV);}
}
将最终重新缩放通道改为使用此函数而非复制函数
#pragma fragment FinalPassFragmentRescale
将属性标识符添加到PostFXStack中,并使其跟踪是否启用了双三次重新缩放,这是通过Setup的新参数配置的
intcopyBicubicId = Shader.PropertyToID("_CopyBicubic"),finalResultId = Shader.PropertyToID("_FinalResult"),…bool bicubicRescaling;…public void Setup (ScriptableRenderContext context, Camera camera, Vector2Int bufferSize,PostFXSettings settings, bool useHDR, int colorLUTResolution,CameraSettings.FinalBlendMode finalBlendMode, bool bicubicRescaling) {this.bicubicRescaling = bicubicRescaling;…}
在CameraRenderer.Render中传递缓冲区设置
postFXStack.Setup(context, camera, bufferSize, postFXSettings, useHDR, colorLUTResolution,cameraSettings.finalBlendMode, bufferSettings.bicubicRescaling);
在执行最终重新缩放之前,在PostFXStack.DoColorGradingAndToneMapping中适当设置着色器属性
buffer.SetGlobalFloat(copyBicubicId, bicubicRescaling ? 1f : 0f);DrawFinal(finalResultId, Pass.FinalRescale);
双线性和双三次重新缩放;渲染缩放比例0.25
2.4 Only Bicubic Upscaling 仅双三次上采样
双三次重新缩放在进行上采样时总能提升画质,但在下采样时差异就不那么明显了。对于渲染缩放比例为2的情况,它总是无效的,因为每个最终像素都是四个像素的平均值,这与双线性插值完全相同。因此,让我们将BufferSettings中的开关替换为三种模式的选择:关闭、仅上采样、以及上下采样都使用
public enum BicubicRescalingMode { Off, UpOnly, UpAndDown }public BicubicRescalingMode bicubicRescaling;
将PostFXStack中的类型更改为匹配
CameraBufferSettings.BicubicRescalingMode bicubicRescaling;…public void Setup (…CameraBufferSettings.BicubicRescalingMode bicubicRescaling) { … }
最后修改DoColorGradingAndToneMapping,使得双三次采样仅用于上下采样模式,或者在我们处理降低的渲染缩放比例时用于仅上采样模式
bool bicubicSampling =bicubicRescaling == CameraBufferSettings.BicubicRescalingMode.UpAndDown ||bicubicRescaling == CameraBufferSettings.BicubicRescalingMode.UpOnly &&bufferSize.x < camera.pixelWidth;buffer.SetGlobalFloat(copyBicubicId, bicubicSampling ? 1f : 0f);
仅对上采样使用双三次重新缩放