Unity3D使用RenderCommand渲染外轮廓

  • 外轮廓渲染方式
  • 原来的做法
  • 使用CommandBuffer

外轮廓渲染方式

我这里所要介绍的外轮廓是使用模糊后处理实现的,不涉及到边缘查找或是顶点扩展这些,简单的说这种方式渲染外轮廓总共分三步:
Unity3D使用RenderCommand渲染外轮廓_第1张图片
1.用单色渲染目标物体到RT1上
Unity3D使用RenderCommand渲染外轮廓_第2张图片
2.对该RT1进行模糊处理得到RT2
Unity3D使用RenderCommand渲染外轮廓_第3张图片
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

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里最好用的新功能了!尤其是对图像后处理这一块,定位渲染问题方便多了)
Unity3D使用RenderCommand渲染外轮廓_第4张图片
在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

你可能感兴趣的:(unity3d,渲染,轮廓)