我这里所要介绍的外轮廓是使用模糊后处理实现的,不涉及到边缘查找或是顶点扩展这些,简单的说这种方式渲染外轮廓总共分三步:
1.用单色渲染目标物体到RT1上
2.对该RT1进行模糊处理得到RT2
3.将RT2中与RT1重合的像素抠掉,形成的外轮廓与原始图叠加,最终在原图上绘制出了目标物体的外轮廓。
这样绘制出的外轮廓形状比较均匀,薄厚易于调整(通过调整模糊程度),不会被遮挡住也可以同时作为被遮挡人形的渲染效果,之前玩儿风暴英雄感觉他的那个角色轮廓就是这么搞的。另外如果说不希望外轮廓透过遮挡物的话可以在第一步的时候使用Camera目标缓冲区的深度缓存,添加一点深度位移就可以保证RT1得到的只是目标物体未被遮挡的部分。
第二步与第三步都是简单的图像处理,在OnRenderImage消息响应方法中使用Unity3D提供的非常简便的Graphic.Blit接口就好了,我主要讲讲使用Unity5新提供的CommandBuffer对第一步的实现方法。
原来我做第一步的时候就是创建一个Camera,调用主摄像机的CopyFrom,修改一些设置再使用replacementShader来对目标物体进行单独的额外渲染,这有两点要求,一是目标物体有单独的layer,这样复制相机可以通过layermask对其进行筛选,第二就是针对该物体原有shader的RenderType写一个replacementShader。Camera的脚本如下:
[DisallowMultipleComponent]
[RequireComponent(typeof(Camera))]
public class CopyCamera : MonoBehaviour {
public LayerMask cullingMask;
public Shader replacementShader;
public Camera mainCamera;
protected virtual void Start()
{
if (!mainCamera)
mainCamera = Camera.main;
if (!mainCamera)
return;
Camera camera = GetComponent<Camera>();
transform.parent = mainCamera.transform;
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
camera.CopyFrom(mainCamera);
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = Color.clear ;
camera.cullingMask = cullingMask.value;
camera.SetReplacementShader(replacementShader, null);
}
}
创建一个摄像机挂上这个脚步,运行时那个摄像机就会与原相机保持一致并使用replacementShader渲染指定layer的物体。可以直接在该摄像机下写后处理脚本,直接使用该摄像机的颜色缓存,或者为该摄像机指定一个renderTarget,将结果保存在一张RenderTexture上并在主摄像机的后处理逻辑中进行后面的步骤。
这种做法有几个问题,首先是需要额外创建出一个摄像机并对其进行管理,Camera本身属于Unity3D场景管理中比较重的对象,他的背后应该还涉及视锥切割,排序等一系列复杂的操作,对于仅需要绘制几个简单物体的操作来说太浪费计算资源了。另外需要绘制的对象需要有单独的层,如果本身已经由其他需要跟其他同类物体指定一个layer的话就不太方便操作了。最后渲染的第一步与后几步分开了,由于最终需要将结果输出到主摄像机上,这意味着两个摄像机上都有一些需要维护的脚本。
CommandBuffer算是一个渲染任务组,包含了完整的绘制指令,数据传输以及状态设定等,使用他可以让我们更灵活的控制渲染过程。直接上代码:
[RequireComponent(typeof(Camera))]
public class RenderBlurOutline : MonoBehaviour {
public int blurIterCount = 1;
public float blurScale = 1.0f;
public Shader outlineShader;
public Shader silhouetteShader;
public Renderer[] silhouettes;
public Color outlineColor = Color.red;
public Color OutlineColor
{
get { return outlineColor; }
set { outlineColor = value; }
}
Material outlineMaterial;
Material silhouetteMaterial;
Camera mCamera;
CommandBuffer renderCommand;
void Awake()
{
outlineMaterial = new Material(outlineShader);
silhouetteMaterial = new Material(silhouetteShader);
renderCommand = new CommandBuffer();
renderCommand.name = "Render Solid Color Silhouette";
mCamera = GetComponent<Camera>();
}
void OnEnable()
{
//顺序将渲染任务加入renderCommand中
renderCommand.ClearRenderTarget(true, true, Color.clear);
for (int i = 0; i < silhouettes.Length; ++i)
{
renderCommand.DrawRenderer(silhouettes[i], silhouetteMaterial);
}
}
void OnDisable()
{
renderCommand.Clear();
}
void OnDestroy()
{
renderCommand.Clear();
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
//1. Draw Solid Color Silhouette
silhouetteMaterial.SetColor("_Color", outlineColor);
RenderTexture mSolidSilhouette = RenderTexture.GetTemporary(Screen.width, Screen.height);
Graphics.SetRenderTarget(mSolidSilhouette);
Graphics.ExecuteCommandBuffer(renderCommand);
//2. Downscale 4x
RenderTexture mBlurSilhouette = RenderTexture.GetTemporary(Screen.width >> 2, Screen.height >> 2);
Graphics.Blit(mSolidSilhouette, mBlurSilhouette, outlineMaterial, 0);
//3. Blur
RenderTexture blurTemp = RenderTexture.GetTemporary(Screen.width >> 2, Screen.height >> 2);
Shader.SetGlobalFloat("g_BlurScale", blurScale);
for (int i = 0; i < blurIterCount; ++i)
{
Graphics.Blit(mBlurSilhouette, blurTemp, outlineMaterial, 1);//horizontal blur
Graphics.Blit(blurTemp, mBlurSilhouette, outlineMaterial, 2);//vertical blur
}
//4. Combine
Shader.SetGlobalTexture("g_SolidSilhouette", mSolidSilhouette);
Shader.SetGlobalTexture("g_BlurSilhouette", mBlurSilhouette);
Graphics.Blit(src, dest, outlineMaterial, 3);
//release RT
RenderTexture.ReleaseTemporary(mSolidSilhouette);
RenderTexture.ReleaseTemporary(mBlurSilhouette);
RenderTexture.ReleaseTemporary(blurTemp);
}
}
CommandBuffer提供了一系列高级渲染接口,包括参数设置SetXXX和绘制DrawRenderer/DrawMesh/Blit。在代码中,我在OnEnable中定义了CommandBuffer需要执行的渲染任务,在OnRenderImage中调用Graphic.ExcuteCommandBuffer来执行这一组定义好的渲染任务,将目标物体渲染到指定的RenderTarget上。查看一下FrameDebug(这个东西应该是新Unity里最好用的新功能了!尤其是对图像后处理这一块,定位渲染问题方便多了)
在RenderBlurOutline后处理渲染组中,首先执行了我定义的CommandBuffer“Render Solid Color Silhouette”,这个名字在CommandBuffer.name中定义。这组任务中执行了Clear,Draw Mesh,对应了我在创建CommandBuffer时的设置。然后执行了4步Draw GL分别对应代码OnRenderImage中执行的Graphic.Blit。
在定义渲染任务时,使用到的接口是DrawRenderer(Renderer, Material),这就相当于是使用replacementShader,使用一个新的材质去替代Renderer上原有的材质对其进行渲染,不过由于提供的是Material,这也就意味着可以通过Material去设置一些渲染参数,而无须使用全局的ShaderVariable。不过代码中我仍然是使用了RenderCommand.SetGlobalColor来进行颜色设置,这等同于正常的渲染中使用Shader.SetXX。
另外一个渲染接口是DrawMesh,与DrawRenderer不同的是你还要额外提供一个旋转矩阵,这个矩阵对应的是Unity shader中_Object2World变量,而DrawRenderer则无须设置,使用的就是该物体正常渲染时的转换矩阵。下面给出对应使用的shader代码:
Shader "Custom/SolidColor"
{
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
};
v2f vert( appdata_base v ) {
v2f o;
float4 vec = mul(_Object2World, v.vertex);
o.pos = mul(UNITY_MATRIX_VP, vec);
return o;
}
fixed4 g_SolidColor;
fixed4 frag(v2f i) : SV_Target {
return g_SolidColor;
}
ENDCG
}
}
}
这个shader很简单就是单色渲染,需要注意的就是vs中先乘了 _Object2World再乘以 UNITY_MATRIX_VP,我测试的时候发现MVP是不能直接用的,在OnPostRender或OnRenderImage中执行CommandBuffer均可保证UNITY_MATRIX_VP是有效的,但是在OnPreRender就不行了,Unity提供CommandBuffer接口主要目的应该是方便在Unity渲染流程的各个阶段方便的插入用户自定义的渲染任务,其渲染操作本身的执行在Graphic接口中均有对应的实现,除了OnPostRender、OnPreRender和OnRenderImage这几个摄像机渲染阶段外,还可以在OnWillRenderObject中调用。另外除了Graphics.ExcuteCommandBuffer外,Camera.AddCommandBuffer也可以对其进行执行。
最后给出Unity官方博客中对CommandBuffer的介绍,里面还给了示例工程,提供了另外几个CommandBuffer的使用方式。extending unity 5 render pipeline - command buffers