Unity自定义SRP(七):LOD和反射

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity自定义SRP(七):LOD和反射相关的知识,希望对你有一定的参考价值。

参考技术A https://catlikecoding.com/unity/tutorials/custom-srp/lod-and-reflections/

​ 离远时,细节过多的物体会因太小而模糊不清,最好的方式就是不渲染它们,让CPU内存占用减少,让GPU渲染更为重要的东西。我们也可以提前剔除这些物体,不过突然剔除可能会造成视觉问题,我们可以添加一些中间过渡的状态,Unity使用LOD(level of detail)组来完成。

​ 我们可以创建一个空对象,然后添加一个 LODGroup 组件:

​ 百分比即物体在窗口中的占比。我们可以点击任一LOD级别,然后赋予一个要显示的物体。

​ LOD组的渐变模式有 Cross Fade 和 Speed Tree ,Corss Fade即交叉渐变,会提前显示下一LOD级别的物体,交叉过渡,Speed Tree用于 Speed Tree 的树,可在网格和公告板间过渡。

​ 切换为 Cross Fade 后,我们可以点击任一LOD级别,调节 Fade Transition Width 滑条,数值越低越早混合,0表示不混合,1表示立即切换。

​ 开启交叉渐变后,会同时渲染两个LOD级别,我们需要在对应的shader中添加相应关键字生成shader变体:

​ 物体的渐变程度可由 UnityPerDraw 缓冲中的 unity_LODFade 获得,X组件是渐变因数,比如可以这样观察LOD渐变因数:

​ 我们可以开启 Animate Cross-fading 选项,这样的渐变符合平滑动画曲线。默认的动画时间是0.5s,我们可以通过 LODGroup.corssFadeAnimationDuration 属性修改长度。

​ 向场景中添加细节的另一种方式是添加环境高光反射,这对于金属度高的表面来说非常重要。

​ 我们已经支持了漫反射全局光照,取决于BRDF的漫反射颜色,现在添加高光全局光照。添加一个 IndirectBRDF 方法,一开始只返回漫反射光:

​ 在一开始添加反射光颜色,包括GI高光和BRDF高光颜色:

​ 使用粗糙度散射这些反射,我们除以粗糙度的二次方,防止分母为0加1:

​ 在 GetLighting 中替换直接计算的间接光漫反射:

​ 最常见的环境就是天空盒,可通过 unity_SpecCube0 获得相应立方体贴图纹理:

​ 添加一个采样方法 SampleEnvironment ,方法中使用 SAMPLE_TEXTURECUBE_LOD 来采样,最后一个参数是mipmap级别,这里先设为最大mipmap级别:

​ 我们需要通过反射光方向来采样,而这个我们可看见的反射光方向也就是观察方向的反射:

​ 然后,在GI中添加高光属性,在 GetGI 中采样获得:

​ 之后在 GetLighting 中的 IndirectBRDF 中传入正确的GI高光颜色:

​ 想起作用的话,需要配置逐物体数据标志:

​ 粗糙表面不仅会散射高光的强度,还会为高光添加一定的混乱度。Unity通过使用较低级别的mipmap模糊环境贴图来模拟这些效果。为得到正确的mipmap等级,我们需要知道人所感知的粗糙度。将其添加到BRDF结构体中:

​ 我们使用 PerceptualSmoothnessToPerceptualRoughness 来获得感知粗糙度,然后获得我们想要的表面粗糙度,这些方法定义在 Core RP 的 CommonMaterial.hlsl 中:

​ 接着我们通过定义在 ImageBasedLighting.hlsl 中的 PerceptualRoughnessToMipmapLevel 来获得正确的mipmap级别:

​ PerceptualRoughnessToMipmapLevel :

​ 然后在 GetGI 和 LitPassFragment 中应用:

​ 表面的一种属性是当沿着掠射角观察时,会看起来完全是镜面,这种现象称之为菲涅尔反射。真实的菲涅尔非常复杂,我们用一种模拟边界的方式来模拟菲涅尔效应。

​ 这里使用Schlick的模拟的变体:

​ 在 IndirectBRDF 中,我们用1-法线与观察防线的点积来影响菲涅尔的强度,并进行4次幂运算:

​ 然后使用菲涅尔强度在菲涅尔颜色和高光间插值:

​ 在shader中添加相应的属性:

​ 在 LitInput 中声明相应的变量:

​ 在 Surface 结构体中添加菲涅尔强度的属性:

​ 在 IndirectBRDF 中应用:

​ 默认的环境立方体贴图只包含天空盒,不包含场景中的其它物体。为了反射所有东西,我们可以添加反射探针, GameObject/Light/Refelection Probe 。反射探针从它所在位置将场景渲染到一个立方体贴图中,因为与位置有关,往往需要在场景中防止多个反射探针:

​ 反射探针的类型默认设置为 Baked ,即只渲染一次,我们还可以设置为 Realtime 。

​ 场景中根据物体的摆放,我们设置多个反射探针,但这也意味着可能会破坏GPU批处理。

​ 物体的 MeshRenderer 中的 Anchor Override 可以用来调整物体使用哪个探针:

​ 反射探针也有多种混合模式可以选择,默认是 Blend Probes ,即可以在最佳的两个反射探针间混合,但该模式不支持SRP批处理。因此,目前暂时仅支持关闭 Off ,以及 Simple ,选择最重要的探针。

​ 立方体贴图数据可以是LDR或HDR的,要HDR的话,可以这么声明:

​ 然后使用 DecodeHDREnvironment 来解码:

可编程脚本渲染管线SRP

Unity 2018.1 beta中引入的Scriptable Render Pipeline可编程脚本渲染管线,简称SRP。是一种在Unity中通过C#脚本配置和执行渲染的方式。在编写自定义渲染管线之前,必须要先理解渲染管线的含义。本文将帮助你开始学习编写自定义SRP。

 

本文演示项目,请访问Github下载:

https://github.com/stramit/SRPBlog/tree/master/SRP-Demo

 

什么是渲染管线

渲染管线是将对象显示到屏幕上所需的一系列技术的总称。它包含: 剔除、渲染对象、后期处理等一系列高级概念。这些高级概念还可以分别根据你所希望的执行方式继续分解。

 

例如:渲染对象可以按照以下方式进行

  • 多通道渲染:每个光照每个对象一个通道

  • 单通道渲染:每个对象一个通道

  • 延迟渲染:渲染表面属性到一个G-Buffer,执行屏幕空间光照。

 

这些就是当你编写一个自定义SRP时需要作出的决定。每项技术都有一些需要考虑的性能成本。

 

渲染入口点

当使用SRP时,你需要定一个类,用于控制渲染;这就是你将要创建的渲染管线。入口点是一个对“Render”函数的调用,它需要两个参数,渲染上下文以及一个需要渲染的摄像机列表。

 

public class BasicPipeInstance : RenderPipeline
{
   public override void Render(ScriptableRenderContext context, Camera[] cameras){}
}

渲染管线上下文

SRP渲染采用的是延迟执行的方式。用户要设置好需要执行的命令列表,然后再执行。用来设置这些命令的对象叫做“ScriptableRenderContext”。当你向上下文填充完操作命令后,可以通过调用“Submit”提交队列中的所有绘制调用。

 

举例来说,使用一个由渲染上下文执行的命令缓冲区清除一个渲染目标:

 

//新建一个命令缓冲区
//用于向渲染上下文发送命令
var cmd = new CommandBuffer();
 
//发送一个清除渲染目标的命令
cmd.ClearRenderTarget(true, false, Color.green);
 
//执行命令缓冲区
context.ExecuteCommandBuffer(cmd);

 

技术分享图片

一个简单渲染管线示例 

 

下面有一个完整的渲染管线代码,仅仅用于清除屏幕。

 

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
 
[ExecuteInEditMode]
public class BasicAssetPipe : RenderPipelineAsset
{
    public Color clearColor = Color.green;
 
#if UNITY_EDITOR
    [UnityEditor.MenuItem("SRP-Demo/01 - Create Basic Asset Pipeline")]
    static void CreateBasicAssetPipeline()
    {
        var instance = ScriptableObject.CreateInstance<BasicAssetPipe>();
        UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/1-BasicAssetPipe/BasicAssetPipe.asset");
    }
#endif
 
    protected override IRenderPipeline InternalCreatePipeline()
    {
        return new BasicPipeInstance(clearColor);
    }
}
 
public class BasicPipeInstance : RenderPipeline
{
    private Color m_ClearColor = Color.black;

    public BasicPipeInstance(Color clearColor)
    {
        m_ClearColor = clearColor;
    }
 
    public override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        base.Render(context, cameras);
        var cmd = new CommandBuffer();
        cmd.ClearRenderTarget(true, true, m_ClearColor);
        context.ExecuteCommandBuffer(cmd);
        cmd.Release();
        context.Submit();
    }
}

 

剔除

剔除是确定要在屏幕上显示什么对象的过程。

 

在Unity中,剔除包括:

  • 视锥剔除:计算存在于摄像机远近视平面之间的对象。

  • 遮挡剔除:计算哪些对象被其它对象挡住,并将它们从渲染中排除。

 

当渲染开始时,首先要计算的是到底要渲染什么。这包括获取摄像机,并从摄像机的视角进行剔除操作。剔除操作会返回一个可为摄像机进行渲染的对象和光照的列表。这些对象随后将被用在渲染管线中。

 

SRP中的剔除操作

在SRP中,你通常会选择某个摄像机的视角执行对象渲染。这与Unity内置渲染所使用的摄像机对象是相同的。SRP提供了一系列API用于剔除操作。整个流程通常看起来像下面这样:

 

//新建一个结构体,用于存储剔除参数
ScriptableCullingParameters   cullingParams;
 
//填充来自摄像机的剔除参数
if   (!CullResults.GetCullingParameters(camera, stereoEnabled, out cullingParams))
      continue;
 
//如果你想修改剔除参数,可在这里进行
cullingParams.isOrthographic   = true;
 
//新建一个结构体,用于存储剔除结果
CullResults   cullResults = new CullResults();
 
//执行剔除操作
CullResults.Cull(ref   cullingParams, context, ref cullResults);

现在可以使用填充的剔除结果执行渲染了。

 

绘制

现在我们已有了一组剔除结果,可以将它们渲染到屏幕了。但还有很多东西需要配置,所以有一些选择需要事先确定。这些选择的驱动因素有:

  • 渲染管线的目标硬件

  • 希望获得的观感

  • 制作的项目的类型

 

例如一个移动2D滚轴游戏和一个PC端第一人称游戏,这些游戏所受的约束条件大不相同,因此它们的渲染管线自然也就大相径庭。以下是一些实际可能需要面对的选择:

  •  高动态范围 vs 低动态范围

  • 线性 vs 伽马

  • 多重采样抗锯齿(MSAA) vs 后期处理抗锯齿

  • PBR材质 vs 简单材质

  • 光照 vs 无光照

  • 光照技术

  • 阴影技术

 

在编写渲染管线时作好这些决定将有助于确定在创作时遇到的许多约束。

 

现在我们将演示一个无光照的简易渲染器,这个渲染器可以将一些对象渲染为不透明。

 

 过滤:渲染区域(Bucket)和图层(Layer)

一般来说,渲染对象会有某个特定分类,比如不透明、透明、次面,或其它的什么类别。Unity用一个称为队列(queue) 的概念表示需要进行渲染的对象,这些队列进而组成存放对象的区域(bucket)(源自对象上的材质)。当从SRP调用渲染时,需要制定所使用区域的范围。

 

除了区域之外,标准的Unity图层也可以被用于过滤。这为通过SRP绘制对象时提供了额外的过滤能力。

//获取不透明渲染过滤器设置
var   opaqueRange = new FilterRenderersSettings();
 
//设置不透明队列的范围
opaqueRange.renderQueueRange   = new RenderQueueRange()
{
      min = 0,
      max = (int)UnityEngine.Rendering.RenderQueue.GeometryLast,
};
 
//Include   all layers包括所有图层
opaqueRange.layerMask   = ~0;

绘制设置:应当如何绘制

过滤和剔除确定了渲染什么,但随后我们还需要确定如何渲染。SRP提供了一系列不同的选项,配置被过滤对象的渲染方式。用于进行这个数据的结构体叫做“DrawRenderSettings”。这个结构体可对许多方面进行配置:

  • 排序——对象渲染的顺序,例如自后向前和自前向后

  • 每渲染器标志 —— 应当从Unity传递给着色器的“内置”设置,这包括每个对象的光照探头,每个对象的光照贴图之类的东西。

  • 渲染标志 —— 用于进行批处理的算法,实例化 vs 非实例化

  • 着色器通道 —— 当前绘制调用应当使用哪个着色器通道   

//新建绘制渲染设置
//注意它需要输入一个着色器通道名
var drs =   new DrawRendererSettings(Camera.current, new ShaderPassName("Opaque"));
 
//启用绘制调用上的实例化
drs.flags =   DrawRendererFlags.EnableInstancing;
 
//传递光照探针和光照贴图数据给每个渲染器
drs.rendererConfiguration   = RendererConfiguration.PerObjectLightProbe |   RendererConfiguration.PerObjectLightmaps;
 
//像普通不透明对象一样排序对象
drs.sorting.flags   = SortFlags.CommonOpaque;

 

绘制

 现在我们已有了发送一个绘制调用所需的三样东西:

  • 剔除结果

  • 过滤规则

  • 绘制规则

 

我们可以发送一个绘制调用了。就像SRP中的所有东西一样,绘制调用也是以一个针对上下文发出的调用。在SRP中,你通常不会渲染单独的网格,而是发出一个调用,一次性渲染大批量的网格。这不仅减少了脚本执行上的开销,也使CPU上的执行得以快速作业化。

 

要发送一个绘制调用,需要将我们已有的东西进行合并。

 

//绘制所有渲染器
context.DrawRenderers(cullResults.visibleRenderers,   ref drs, opaqueRange);
//提交上下文,这将执行所有队列中的命令。
context.Submit();

这将会把对象绘制到当前绑定的渲染目标中。你可以使用一个命令缓冲区,按需切换渲染目标。

 

技术分享图片

 

这是一个可以渲染不透明对象的渲染器:

using System;    
using UnityEngine;    
using UnityEngine.Rendering;    
using UnityEngine.Experimental.Rendering;    
[ExecuteInEditMode]    
public class OpaqueAssetPipe : RenderPipelineAsset    
{    
#if UNITY_EDITOR    
[UnityEditor.MenuItem("SRP-Demo/02 - Create Opaque Asset Pipeline")]    
static void CreateBasicAssetPipeline()    
{    
var instance = ScriptableObject.CreateInstance<OpaqueAssetPipe>();    
UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/2-OpaqueAssetPipe/OpaqueAssetPipe.asset");    
}    
#endif    
protected override IRenderPipeline InternalCreatePipeline()    
{    
return new OpaqueAssetPipeInstance();    
}    
}    
public class OpaqueAssetPipeInstance : RenderPipeline    
{    
public override void Render(ScriptableRenderContext context, Camera[] cameras)    
{    
base.Render(context, cameras);    
foreach (var camera in cameras)    
{    
ScriptableCullingParameters cullingParams;    
if (!CullResults.GetCullingParameters(camera, out cullingParams))    
continue;    
CullResults cull = CullResults.Cull(ref cullingParams, context);    

context.SetupCameraProperties(camera);    
var cmd = new CommandBuffer();    
cmd.ClearRenderTarget(true, false, Color.black);    
context.ExecuteCommandBuffer(cmd);    
cmd.Release();    
var settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass"));    
settings.sorting.flags = SortFlags.CommonOpaque;    
var filterSettings = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque };    
context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);    
context.DrawSkybox(camera);    
context.Submit();    
}    
}    
}    

这个示例可以进一步扩展,添加透明渲染:

 

using System;    
using UnityEngine;    
using UnityEngine.Rendering;    
using UnityEngine.Experimental.Rendering;    
[ExecuteInEditMode]    
public class TransparentAssetPipe : RenderPipelineAsset    
{    
#if UNITY_EDITOR    
[UnityEditor.MenuItem("SRP-Demo/03 - Create Transparent Asset Pipeline")]    
static void CreateBasicAssetPipeline()    
{    
var instance = ScriptableObject.CreateInstance<TransparentAssetPipe>();    
UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/SRP-Demo/3-TransparentAssetPipe/TransparentAssetPipe.asset");    
}    
#endif    
protected override IRenderPipeline InternalCreatePipeline()    
{    
return new TransparentAssetPipeInstance();    
}    
}    
public class TransparentAssetPipeInstance : RenderPipeline    
{    
public override void Render(ScriptableRenderContext context, Camera[] cameras)    
{    
base.Render(context, cameras);    
foreach (var camera in cameras)    
{    
ScriptableCullingParameters cullingParams;    
if (!CullResults.GetCullingParameters(camera, out cullingParams))    
continue;    
CullResults cull = CullResults.Cull(ref cullingParams, context);    
context.SetupCameraProperties(camera);    
var cmd = new CommandBuffer();    
cmd.ClearRenderTarget(true, false, Color.black);    
context.ExecuteCommandBuffer(cmd);    
cmd.Release();    

var settings = new DrawRendererSettings(camera, new ShaderPassName("BasicPass"));    
settings.sorting.flags = SortFlags.CommonOpaque;    
var filterSettings = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque };    
context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);    
context.DrawSkybox(camera);    
settings.sorting.flags = SortFlags.CommonTransparent;    
filterSettings.renderQueueRange = RenderQueueRange.transparent;    
context.DrawRenderers(cull.visibleRenderers, ref settings, filterSettings);    
context.Submit();    
}    
}    
}    

这里要重点注意的是,渲染透明时,渲染顺序会变为自后向前。

 

技术分享图片

小结

我们希望本篇文章能帮你入门,开始编写你自己的自定义SRP。下载Unity 2018.1 beta 即刻开始创作自定义SRP的旅程吧。更多关于Unity 2018.1的信息请访问Unity Connect平台!

以上是关于Unity自定义SRP(七):LOD和反射的主要内容,如果未能解决你的问题,请参考以下文章

Unity自定义SRP(十五):SSAO

Unity自定义SRP(十三):颜色分级

Unity自定义SRP(十四):抗锯齿和缩放渲染

可编程脚本渲染管线SRP

Unity 自定义编辑器窗口输入控件,用于反射确定的类型

Unity Shaders学习笔记之为创建自定义慢反射光照模型