作者:Andre McGrail,2020年2月10日
本文中,我们一起来看看使用通用渲染管线制作《Boat Attack》Demo的过程 。
通用渲染管线是一款强大的,包含一系列艺术创作工具的产品级图像制作解决方案。若希望制作出具有顶尖质量的图像,将项目发布到所有Unity支持平台上,通用渲染管线是一个不错的选择。我们在本博文中已经介绍过通用渲染管线的优势了。
我们最初制作《Boat Attack》演示项目是为了验证和测试通用渲染管线的性能(当时还叫轻量渲染管线)。在开发时制作的这个垂直切片式演示,也是一次生产流程实地操作的练习。
自第一版本的Demo发布以来,我们多次升级了《Boat Attack》演示项目。如今,项目中使用了许多新出的通用渲染管线图像功能,其中包括C# Job System、Burst compiler、Shader Graph、Input System等等。
《Boat Attack》Demo 介绍
《Boat Attack》演示项目展示了一款用Unity制作的赛艇游戏。游戏现可供游玩,并且还在不断优化,我们尽量让游戏能完全利用起最新的Unity功能。
Demo能够在大部分平台中顺畅运行,包括中高端移动设备,现世代主机和单独的游戏平台App。在2019年的哥本哈根Unite大会上,我们使用了一系列的设备来演示《Boat Attack》,包括iPhone 7、PlayStation 4等等。
您可从GitHub上下载该Demo,并在最新版本Unity 2019.3中运行。
Shader Graph
Shader Graph是一款对艺术家十分友好的着色器创作界面,在技术美术的手中,它是一个强大的原型构建工具。在《Boat Attack》中,我们使用了Shader Graph制作出了一些独特的着色器效果。
Shader Graph可以制作出超棒的着色器效果,还能方便开发者在各版本的轻量渲染管线和通用渲染管线中维护所有的着色器。
《Boat Attack》中展示的峭壁上的着色器通过采集网格元数据制作,Shader Graph可以轻易从网格中提取出各类数据。在峭壁较为平坦的位置、以及朝上的平面上,我们在网格的法线方向上绘制了杂草,同时限定了绘制时的场景空间高度,确保杂草不会被绘制到靠近水面的峭壁和岩块上。
自左向右:Y轴高度遮罩、Y轴法线遮罩、高度和法线遮罩重新映射后的效果、最终的着色器
植被着色
《Boat Attack》中的植被最初是使用特制的顶点/片元着色器(Vertex/Fragment shader)制作,但由于管线当时处于早期开发阶段,代码时常会更改,维护植被着色器让人非常头疼。在用Shader Graph重新制作着色器后,更新就变得轻松多了。
植被的Shader Graph效果是基于Crytek的Tiago Sousa介绍的技术制作的,该技术很好地利用了顶点色彩的通道,用顶点错位来形成植被的风吹动画。在《Boat Attack》里,我们创建了一个次级图表来囊括进所有风吹动画运算所需的节点。在次级图表中包含了许多嵌套的次级图表,都是用于执行重复性数学运算的图表。
单个通道上的顶点动画和遮罩。自左向右:自远到近的主要混合模式,顶点色彩红色通道上的叶片边缘,顶点色彩蓝色通道和绿色通道一起组成植被的相位偏移。
在制作逼真植被时,另一个重要的部分是次表面散射(subsurfaceScattering,SSS),遗憾的是通用渲染管线当前并不支持SSS。然而,我们可以使用Shader Graph的自定义功能节点从通用渲染管线中取得光照信息,制作出一种近似SSS的效果。
以上是节点布局。SSS Mask部分使用了顶点颜色绿色通道(叶相)和反射率(Albedo)纹理贴图制作。
自定义功能节点可让我们有更多的创作自由。在《Unity通用渲染管线Universal RenderPipeline》中详细了解如何自定义渲染,或者直接从《Boat Attack》的代码库中迁出节点代码,尝试自己的光照设想。
自左向右:无SSS,加入SSS,最终着色
自定义船体
船体有许许多多的色彩供选择。我们使用SubstancePainter绘制了两种船体遮罩,打包储存在了纹理中,它们由Metallic(红色通道)、Smoothness(绿色通道)、Livery 1(蓝色通道)和Livery 2(不透明度)组成。我们可以在Shader Graph中选择性地在遮罩位置应用上不同的颜色。
这是船体上色方式的概览。使用覆盖混合可以让较不明显的色彩穿透反射率贴图。
这是Shader Graph中的节点布局,全部包括在了一个次级图表中,便于在RaceBoats父图表中使用。
房屋
《Boat Attack》中有完整的日夜循环。为了增加景色的实感,我们为关卡中所有建筑的窗口制作了一个Shader Graph。该图表会在日暮时为窗口添加些许光照,在黎明时去除光照。
我们使用了一个简单的发光纹理,根据返回的日夜数值来确定是否要添加贴图。此外,还根据对象的位置加入了打乱纹理排序的效果,让房屋能以不同的顺序被点亮。
实现随机发光的节点图
云朵
既然在《Boat Attack》中有了光线的变换,简单的高动态范围成像(HDRI)天空盒似乎已不能满足需求。云朵应该动态地受场景光照影响。
但大块云朵的实时渲染性能要求太高,对移动端来说尤其如此。因为我们不需要从多个角度观看云朵,所以可以使用带纹理的卡片状云朵来节省性能。
当前渲染云朵的图表
通过API渲染来实现无缝的平面反射
可编程渲染管线的目标是允许用户自定义渲染代码,将代码部分摆到用户的面前。我们并不会简单地打开使用现有的渲染代码,而是不停地添加新的API和硬件支持,将渲染技术带上新的台阶。
用户可以使用自己的C#脚本拓展通用渲染管线开箱即用的渲染功能。管线有4个与之相关的API:
- RenderPipelineManager.beginFrameRendering
- RenderPipelineManager.beginCameraRendering
- RenderPipelineManager.endCameraRendering
- RenderPipelineManager.endFrameRendering
你可以使用这些挂钩,在系统渲染场景和特定镜头之前,运行自己的代码。在《BoatAttack》中,我们使用了这些API,在主要帧被渲染之前,将场景渲染进纹理,实现了平面反射的效果。
private void OnEnable() {
RenderPipelineManager.beginCameraRendering += ExecutePlanarReflections;
}
由于我们订阅了该API的回调,需要用OnDisable
来取消订阅。
我们可以在平面反射脚本中看到入口点。这段代码可在每次通用渲染管线渲染镜头时调用一个自定义方法。这里调用的方法为ExecutePlanarReflections
。
public void ExecutePlanarReflections(ScriptableRenderContext context, Camera camera) {
//rendering code....
}
由于使用的是beginCameraRendering
回调,必须以一个ScriptableRenderContext
和一个Camera
作为参数。这些数据会随着回调一起被导入,方法还会告知渲染的是哪个镜头。
大部分情况下,这里的代码就是应用平面反射的范例了,它处理的是镜头和矩阵。唯一的区别在于,通用渲染管线有用于渲染镜头的新API。
应用平面反射的完整方法如下:
private void ExecutePlanarReflections(ScriptableRenderContext context, Camera camera)
{
// we dont want to render planar reflections in reflections or previews
if (camera.cameraType == CameraType.Reflection || camera.cameraType == CameraType.Preview)
return;
UpdateReflectionCamera(camera); // create reflected camera
PlanarReflectionTexture(camera); // create and assign RenderTexture
var data = new PlanarReflectionSettingData(); // save quality settings and lower them for the planar reflections
beginPlanarReflections?.Invoke(context, m_ReflectionCamera); // callback Action for PlanarReflection
UniversalRenderPipeline.RenderSingleCamera(context, m_ReflectionCamera); // render planar reflections
data.Restore(); // restore the quality settings
Shader.SetGlobalTexture(planarReflectionTextureID, m_ReflectionTexture); // Assign texture to water shader
}
这里我们使用了下述新的方法来渲染了平面反射镜头。
这里我们使用了新的UniversalRenderPipeline.RenderSingleCamera()
方法来渲染了平面反射镜头。
由于使用了一个纹理(通过Camera.targetTexture
设定)来渲染镜头,我们还取得了可用于之后水体渲染的渲染纹理RenderTexture。你可以在Github页面上查看完整的PlanarReflection脚本。
平面反射的构成。自左到右:原始平面反射镜头的输出,菲涅尔镜头调暗和法线偏移后的效果,最终水体着色,无平面反射的水体着色。
这里的回调主要是用于触发渲染,但它们也能有其他用处。比如,我们还能借其禁用平面反射镜头上的阴影。使用API可以让我们处理更加复杂的需求,有更全面的控制,若将行为硬编码进场景或预制件中,这是做不到的。
插入自定义渲染通道来实现特殊效果
在通用渲染管线中,渲染是基于ScriptableRenderPasses(可编程渲染通道)完成的,后者是设定渲染对象和方法的各种指令。许多的ScriptableRenderPasses排列起来后,便成为了ScriptableRenderer(可编程渲染器)。
另一部分是ScriptableRendererFeatures(可编程渲染器功能)。
这部分是用于储存自定义ScriptableRenderPasses数据的容器,可储存的数量不限,且支持任何类型的数据。
目前有两种ScriptableRenderer可开箱即用,ForwardRenderer(前向渲染器)和2DRenderer(2D渲染器)。ForwardRenderer支持插入不同的ScriptableRendererFeatures。
为了让ScriptableRendererFeatures的创建更加简便,我们加入了模板供用户使用,模板和C# MonoBehaviour脚本中的模板类似。你可以在项目视图中右击选择“Create→Rendering→Universal Pipeline→Renderer Feature”来创建模板。创建完成后,就可以在ForwardRendererData(前向渲染器数据)资源的Render Feature(渲染功能)列表中添加自己的ScriptableRendererFeature了。
在《Boat Attack》演示项目中,我们用ScriptableRendererFeatures为水体渲染添加了两种额外的渲染通道:一种用于焦散效果,另一种则是WaterEffects(水体效果)。
焦散效果
用于焦散的ScriptableRendererFeature为场景添加的渲染通道,可以在Opaque(不透明)和Transparent(透明)通道之间渲染一种自定义的焦散着色效果。通道会渲染一片与水面平行的大四边形,防止渲染到空中的像素。四边形随镜头移动,被固定在水面的高度上,然后着色器再叠加渲染屏幕中不透明通道的数据。
焦散渲染通道的构成。从左到右:深度纹理,基于深度信息重建的场景空间位置,根据场景空间位置贴上的焦散纹理,与不透明通道混合起来的最终效果。
你可以使用CommandBuffer.DrawMesh
来绘制四边形,组成矩阵用于放置网格(位置由水体和镜头的坐标决定),然后设置起焦散材质。代码如下:
<pre class="wp-block-code"><code>public class WaterCausticsPass : ScriptableRenderPass {
const string k_RenderWaterCausticsTag = "Render Water Caustics";
public Material m_WaterCausticMaterial;
public Mesh m_mesh;
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
var cam = renderingData.cameraData.camera;
if(cam.cameraType == CameraType.Preview) // Stop the pass rendering in the preview
return;
// Create the matrix to position the caustics mesh.
Vector3 position = cam.transform.position;
position.y = 0; // TODO should read a global 'water height' variable.
Matrix4x4 matrix = Matrix4x4.TRS(position, Quaternion.identity, Vector3.one);
// Setup the CommandBuffer and draw the mesh with the caustic material and matrix
CommandBuffer cmd = CommandBufferPool.Get(k_RenderWaterCausticsTag);
cmd.DrawMesh(m_mesh, matrix , m_WaterCausticMaterial, 0, 0);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
}
水体效果
WaterFXPass实际效果的分屏演示。左边,最终渲染效果;右边,调试视图,水体上只显示了该通道的效果。
WaterFXPass要稍微复杂一点。我们制作该效果的目的是让对象能影响到水体,制造出波浪和浮沫。为此,我们将部分对象渲染到了一个看不到的RenderTexture上,使用了一个自定义着色器将不同的渲染信息写入纹理的通道上:在红色通道中将浮沫遮到水面上,X和Z轴的法线偏移分别在绿色和蓝色通道上,最后将水体错位效果放在了不透明度通道上。
首先,我们以一半的分辨率制作了渲染纹理。接着,创建一个过滤器,来过滤出含有WaterFX着色通道的透明对象。
然后,使用ScriptableRenderContext.DrawRenderers
将对象渲染进场景。最终代码如下:
pre class="wp-block-code"><code>class WaterFXPass : ScriptableRenderPass
{
const string k_RenderWaterFXTag = "Render Water FX";
private readonly ShaderTagId m_WaterFXShaderTag = new ShaderTagId("WaterFX");
private readonly Color m_ClearColor = new Color(0.0f, 0.5f, 0.5f, 0.5f); //r = foam mask, g = normal.x, b = normal.z, a = displacement
private FilteringSettings m_FilteringSettings;
RenderTargetHandle m_WaterFX = RenderTargetHandle.CameraTarget;
public WaterFXPass()
{
m_WaterFX.Init("_WaterFXMap");
// only wanting to render transparent objects
m_FilteringSettings = new FilteringSettings(RenderQueueRange.transparent);
}
// Calling Configure since we are wanting to render into a RenderTexture and control cleat
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
// no need for a depth buffer
cameraTextureDescriptor.depthBufferBits = 0;
// Half resolution
cameraTextureDescriptor.width /= 2;
cameraTextureDescriptor.height /= 2;
// default format TODO research usefulness of HDR format
cameraTextureDescriptor.colorFormat = RenderTextureFormat.Default;
// get a temp RT for rendering into
cmd.GetTemporaryRT(m_WaterFX.id, cameraTextureDescriptor, FilterMode.Bilinear);
ConfigureTarget(m_WaterFX.Identifier());
// clear the screen with a specific color for the packed data
ConfigureClear(ClearFlag.Color, m_ClearColor);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(k_RenderWaterFXTag);
using (new ProfilingSample(cmd, k_RenderWaterFXTag)) // makes sure we have profiling ability
{
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// here we choose renderers based off the "WaterFX" shader pass and also sort back to front
var drawSettings = CreateDrawingSettings(m_WaterFXShaderTag, ref renderingData,
SortingCriteria.CommonTransparent);
// draw all the renderers matching the rules we setup
context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_FilteringSettings);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
public override void FrameCleanup(CommandBuffer cmd)
{
// since the texture is used within the single cameras use we need to cleanup the RT afterwards
cmd.ReleaseTemporaryRT(m_WaterFX.id);
}
}
这两个ScriptableRenderPasses通道全放在了一个ScriptableRendererFeature中。该功能包含了一个<code>Create()</code>
函数,可用于设置资源,从UI传入设置信息。在渲染水体时,两个通道一般都一起使用,所以我们加入了将两个通道同时加进ForwardRendererData的功能。完整的代码在Github页面上。
未来计划
在整个Unity 2019版本周期中,包括19.4LTS版,我们将持续更新项目。而自Unity 2020.1起,我们将以维护项目为主,确保其能正常运行,但不会再添加新的内容。
计划包括:
- 进一步完善日/夜循环(在通用渲染管线中整合进更多的功能,减少自定义的需要)。
- 打磨水体部分的UX/UI
- 应用“Imposter”假体
- 修整代码、调试性能
常用链接
《Boat Attack》Github代码库
完整的2019.3项目链接(不使用GitHub)
通用渲染管线手册
通用渲染管线并不会取代或包揽高清渲染管线。
通用渲染管线未来将成为Unity默认的渲染管线,“一次开发,到处部署”。它更具灵活性和可拓展性,产出比内置渲染管线性能更高,且图像质量非常出色。
HDRP则专为高端平台产出最顶级的图像。如果项目的目标是希望在高端硬件上突破图像的极限,展示性能强劲、高度清晰的图像,HDRP是最佳的选择。
请根据项目的功能特点和平台要求来选择使用两种渲染管线。
快来尝试通用渲染管线吧
快来体验管线所有成品级的功能和性能增益吧。你可以升级项目到通用渲染管线,或用Unity Hub中通用渲染管线的模板创建新项目。
版权所有:Unity官方社区
原文地址:Achieve beautiful, scalable, and performant graphics with the Universal Render Pipeline
Comments NOTHING