Unity中使用后处理技术实现模型描边或自发光
前言
在3D游戏中描边或者说边缘发光、边缘是非常常见的技术,通常这种技术用来提醒玩家当前选中的目标、角色、建筑等
在unity中选中模型会有边缘发光
基本原理
在Unity的组件脚本中,给我们提供了OnRenderImage回调函数(该函数只能在有摄影机组件的对象上使用),这个回调函数给了我们开发者实现各种全屏幕效果的可能。同时呢还提供了CommandBuffer类用于自定义渲染模型。结合这两种功能我们通过CommandBuffer使用纯色渲染出想要描边的物体到一个RenderTexture,通过自定义的Shader将其边缘模糊,然后将模糊的部分再次赋予纯色从而扩展出边缘,将边缘再次模糊平滑后与原来的纯色RenderTexture做插值抠出边缘部分,再将这部分与OnRenderImage()提供的原图像混合从实现描边效果
实现过程
编写纯色Shader
一个简单的将模型渲染成纯色的Shader
Shader "Outline/SingleColor"
{
Properties
{
_outLineColor("OutLineColor",Color)= (0,0,0,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
float4 _MainTex_ST;
float4 _outLineColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _outLineColor;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
编写实现各种图像处理效果的Shader
整个Shader分为5Pass,分别对应横向和纵向的模糊、图像插值、图像混合与边缘实化
横向纵向模糊Pass
//pass 0 横向模糊
pass {
CGPROGRAM
#include"UnityCG.cginc"
#pragma vertex vert_heng
#pragma fragment frag
//横向扩展
v2f vert_heng(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float2 uv = v.uv;
o.uv[0] = uv;
o.uv[1] = uv + float2(1, 0) * _MainTex_TexelSize.xy;
o.uv[2] = uv + float2(-1, 0) * _MainTex_TexelSize.xy;
o.uv[3] = uv + float2(2, 0) * _MainTex_TexelSize.xy;
o.uv[4] = uv + float2(-2, 0) * _MainTex_TexelSize.xy;
return o;
}
ENDCG
}
//pass 1 竖直模糊
pass {
CGPROGRAM
#include"UnityCG.cginc"
#pragma vertex vert_shu
#pragma fragment frag
v2f vert_shu(a2v v) {//竖直扩展
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
float2 uv = v.uv;
o.uv[0] = uv;
o.uv[1] = uv + float2(0, 1) * _MainTex_TexelSize.xy;
o.uv[2] = uv + float2(0, -1) * _MainTex_TexelSize.xy;
o.uv[3] = uv + float2(0, 2) * _MainTex_TexelSize.xy;
o.uv[4] = uv + float2(0, -2) * _MainTex_TexelSize.xy;
return o;
}
ENDCG
}
公用的片源着色器与结构体
CGINCLUDE
sampler2D _MainTex;
float4 _MainTex_TexelSize;
struct a2v {
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
};
struct v2f {
float4 pos:SV_POSITION;
float2 uv[5]:TEXCOORD0;
};
fixed4 frag(v2f i) :SV_TARGET{
float3 col = tex2D(_MainTex,i.uv[0]).xyz * 0.4026;
float3 col1 = tex2D(_MainTex,i.uv[1]).xyz * 0.2442;
float3 col2 = tex2D(_MainTex,i.uv[2]).xyz * 0.2442;
float3 col3 = tex2D(_MainTex,i.uv[3]).xyz * 0.0545;
float3 col4 = tex2D(_MainTex,i.uv[4]).xyz * 0.0545;
float3 finalCol = col + col1 + col2 + col3 + col4;
return fixed4(finalCol,1.0);
}
ENDCG
图像差值Pass
该Pass将没有模糊的图像和模糊后的图像相减从而获得物体的轮廓图像
//pass 2 将没有模糊的图像和模糊后的图像相减获得轮廓图像
Pass{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f1
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f1 vert(appdata v)
{
v2f1 o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _RenderTex;
fixed4 frag(v2f1 i) : SV_Target
{
float3 col = tex2D(_MainTex,i.uv).xyz;
float3 commandCol = tex2D(_RenderTex,i.uv).xyz;
float3 finalCol = col - commandCol;
return fixed4(finalCol,1.0);
}
ENDCG
}
图像混合Pss
这个Pass将物体的轮廓图像与场景图像混合获得最终图像
//pass 3 将正常图像和轮廓图像混合到一起
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f2{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f2 vert(appdata v){
v2f2 o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _OutLineTex;
fixed4 _OutlineColor;
fixed4 frag(v2f2 i) : SV_Target{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 lineCol = tex2D(_OutLineTex,i.uv);
float a=(lineCol.r+lineCol.g+lineCol.b)/3;
col.rgb = col.rgb*(1-a)+_OutlineColor*a;
return col;
}
ENDCG
}
边缘实化Pass
这个Pass将第一阶段的模糊的边缘变成纯色从而扩展出边缘
//pass 4 边缘实体化
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f4{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f4 vert(appdata v){
v2f4 o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f4 i) : SV_Target{
fixed4 whiteCol=(1,1,1,1);
fixed4 col = tex2D(_MainTex, i.uv);
if(col.r>0 ||col.g>0 || col.b>0){
col.rgb = whiteCol;
}
return col;
}
ENDCG
}
脚本实现
主要的绘制相关的工作都是在OnRenderImage函数中实现的
首先我们用纯色Shader绘制出需要描边物体
然后将其边缘模糊
再将边缘部分实体化
一次这样的操作看不什么效果,我们多迭代几次,就变成了下面的效果。这是迭代了5次的效果,可以明显的看到比原物体大了一圈
然后再对边缘进行模糊使其变得更加柔和,下面是模糊迭代5次的效果
将原图与边缘扩展好的图做差值计算就可以得到轮廓图了
将轮廓图的RGB值当作alpha值,用自定义的颜色填充全图。再将这个图片与场景图片做混合就能的到最终的描边效果了
Unity脚本OutlineSystem.cs(该脚本必须挂在到Camera下)代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
public class OutlineSystem : MonoBehaviour
{
private List<GameObject> gameObjects = new List<GameObject>();
private Material effectMat = null;
private Material renderMat = null;
private CommandBuffer commandBuffer = null;
private RenderTexture renderTex = null;
private Renderer targetEBO;
private int meshcount = 0;
public Color outLineColor = Color.green; //renderMat
[Range(0, 10)]
public int outLineSize = 1;
[Range(0, 50)]
public int BlurSize = 5;
void Start()
{
commandBuffer = new CommandBuffer();
renderTex = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);
commandBuffer.SetRenderTarget(renderTex);
commandBuffer.ClearRenderTarget(true, true, Color.black);
//创建材质
effectMat = new Material(Shader.Find("Outline/OutlintEffect"));
renderMat= new Material(Shader.Find("Outline/SingleColor"));
}
public void AddOutline(GameObject gameObject)
{
if (gameObjects.IndexOf(gameObject) != -1)
return;
gameObjects.Add(gameObject);
targetEBO = gameObject.GetComponent<Renderer>();
if (targetEBO is MeshRenderer)
{
var f = targetEBO.GetComponent<MeshFilter>();
if (f != null && f.sharedMesh != null)
meshcount = f.sharedMesh.subMeshCount;
}
for (int i = 0; i < meshcount; i++)
{
commandBuffer.DrawRenderer(targetEBO, renderMat, i, 0);
}
}
public bool IsOutline(GameObject gameObject)
{
if (gameObjects.IndexOf(gameObject) != -1)
return true;
else
return false;
}
public void CancelOutline(GameObject gameObject) {
this.gameObjects.Remove(gameObject);
Outline();
}
/// <summary>
/// 取消所有的描边效果
/// </summary>
public void CancelOutlineAll() {
commandBuffer.Clear();
commandBuffer.SetRenderTarget(renderTex);
commandBuffer.ClearRenderTarget(true, true, Color.black);
this.gameObjects.Clear();
}
/// <summary>
/// 重新计算描边命令缓冲区
/// </summary>
private void Outline()
{
commandBuffer.Clear();
commandBuffer.SetRenderTarget(renderTex);
commandBuffer.ClearRenderTarget(true, true, Color.black);
foreach (GameObject gobj in gameObjects)
{
targetEBO = gobj.GetComponent<Renderer>();
if (targetEBO is MeshRenderer)
{
var f = targetEBO.GetComponent<MeshFilter>();
if (f != null && f.sharedMesh != null)
meshcount = f.sharedMesh.subMeshCount;
}
for (int i = 0; i < meshcount; i++)
{
commandBuffer.DrawRenderer(targetEBO, renderMat, i, 0);
}
}
}
private void OnEnable()
{
if (renderTex)
{
RenderTexture.ReleaseTemporary(renderTex);
renderTex = null;
}
if (commandBuffer != null)
{
commandBuffer.Release();
commandBuffer = null;
}
}
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (targetEBO == null) {
Graphics.Blit(src, dest);
return;
}
if (!targetEBO.gameObject.activeSelf) {
Graphics.Blit(src, dest);
return;
}
if (renderMat && renderTex && commandBuffer != null )
{
//用白色绘制出图像
renderMat.SetColor("_outLineColor", Color.white);
Graphics.ExecuteCommandBuffer(commandBuffer);
//声明用来模糊的RT
RenderTexture temp1 = RenderTexture.GetTemporary(src.width, src.width, 0);
RenderTexture temp2 = RenderTexture.GetTemporary(src.width, src.width, 0);
//先进行一次模糊,因为无法直接用循环叠加commandBuffer
Graphics.Blit(renderTex, temp1, effectMat, 0);
Graphics.Blit(temp1, temp2, effectMat, 1);
//边缘实体强化
Graphics.Blit(temp2, temp1, effectMat, 4);
Graphics.Blit(temp1, temp2, effectMat, 4);
//执行多次边缘扩展
for (int i = 0; i < outLineSize; i++)
{
Graphics.Blit(temp2, temp1, effectMat, 0);
Graphics.Blit(temp1, temp2, effectMat, 1);
Graphics.Blit(temp2, temp1, effectMat, 4);
Graphics.Blit(temp1, temp2, effectMat, 4);
}
//执行多次模糊
for (int i = 0; i < BlurSize; i++)
{
Graphics.Blit(temp2, temp1, effectMat, 0);
Graphics.Blit(temp1, temp2, effectMat, 1);
}
//将模糊后的图片减去commandBuffer中的实心剪影
effectMat.SetTexture("_RenderTex", renderTex);
Graphics.Blit(temp2, temp1, effectMat, 2);
//后期处理,叠入渲染成果
effectMat.SetTexture("_OutLineTex", temp1);
effectMat.SetColor("_OutlineColor", outLineColor);
Graphics.Blit(src, dest, effectMat, 3);
//释放RT
RenderTexture.ReleaseTemporary(temp1);
RenderTexture.ReleaseTemporary(temp2);
return;
}
}
}
总结
本文简单的介绍一种基于后处理技术的模型描边方法,该方法的优点在于可以对任何大小、形状的模型进行比较完美的描边。
相比于法向量扩展的描边方法,该方法不会出现边缘断裂的情况更加的美观。
当然,目前还是有些缺点,在要求实现多物体不同颜色描边时候需要用到多个CommandBuffer和多重的纹理叠加,性能会有显著的消耗。