网上的描边实现大部分都是法线外扩、模板、后处理这种的,我想实现一个类似U3D编辑界面那种硬质描边,法线外扩的缺点大部分人都知道:遇到“硬边”,法线外扩就会造成描边断裂,非常影响美观。模板又要修改模型材质操作有点复杂。而后处理就挺简单修改模型的显示层单独渲染一遍就ok了,但是目前网上描边都做成了“自发光”,边不够“硬”,而且由于模型上需要绑碰撞,换层有点不“合理”;了解到CommandBuffer可以解决这个顾虑,需要操作的仅是把模型添加入CommandBuffer单独绘制即可,很满足我的需求,那就改一下吧。
首先需要软描边(类自发光)的大佬可以参考这篇文章,我也是参考这个大佬改的:传送门
具体操作是首先新建脚本OutLineCameraComponent.cs,拷贝上代码并挂载到主相机上。
/****************************************************
文件:OutLineCameraComponent.cs
名称:后处理描边相机组件
说明:使用时需要将脚本挂载到三维模型相机上。主要参考文章:https://blog.csdn.net/vikingsc2007_1/article/details/76570931
by--Sep
*****************************************************/
using System;
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Rendering;
// [ExecuteInEditMode]
public class OutLineCameraComponent : MonoBehaviour
{
private RenderTexture renderTexture = null;
private CommandBuffer commandBuffer = null;
private RenderTexture temp1 = null;
private RenderTexture temp2 = null;
[Header("单色shader")]
public Shader preoutlineShader = null;
[Header("描边shader")]
public Shader shader = null;
private Material _preoutlineMaterial = null;
private Material _material = null;
public Color outLineColor;
public Material mMaterial
{
get
{
if (_material == null)
{
_material = GenerateMaterial(shader);
_material.SetColor("_OutlineColor", outLineColor);
}
return _material;
}
}
[Header("降分辨率,与描边宽度对应")]
public int downSample = 2;
[Header("描边强度")]
[Range(0.0f, 10.0f)]
public float outLineStrength = 3.0f;
//目标对象
private BaseModel targetModel;
private void Awake()
{
_preoutlineMaterial = GenerateMaterial(preoutlineShader);
_preoutlineMaterial.SetColor("_MainColor",outLineColor);
}
//根据shader创建用于屏幕特效的材质
protected Material GenerateMaterial(Shader shader)
{
if (shader == null)
return null;
if (shader.isSupported == false)
return null;
Material material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
return null;
}
public void AddTarget(BaseModel target)
{
targetModel = target;
RefreshCommandBuff();
}
public void RemoveTarget(BaseModel target)
{
RefreshCommandBuff();
}
public void RefreshCommandBuff()
{
if (renderTexture)
RenderTexture.ReleaseTemporary(renderTexture);
int width = Screen.width / downSample;
int height = Screen.height / downSample;
renderTexture = RenderTexture.GetTemporary(width, height, 0);
temp1 = RenderTexture.GetTemporary(width, height, 0);
temp2 = RenderTexture.GetTemporary(width, height, 0);
commandBuffer = new CommandBuffer();
commandBuffer.SetRenderTarget(renderTexture);
commandBuffer.ClearRenderTarget(true, true, Color.black);
for (int i = 0; i < targetModel.meshRenders.Length; i++)
{
// Renderer[] renderers = targetObjects[i].GetComponentsInChildren<Renderer>();
// foreach (Renderer r in renderer)
// commandBuffer.DrawRenderer(r, targetObjects[i].material);
Renderer renderer = targetModel.meshRenders[i];
commandBuffer.DrawRenderer(renderer, _preoutlineMaterial);
}
}
/// <summary>
/// 绘制描边
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (mMaterial && renderTexture && commandBuffer != null)
{
Graphics.ExecuteCommandBuffer(commandBuffer);
//查找边界
Graphics.Blit(renderTexture, temp1, mMaterial, 0);
//用模糊图和原始图计算出轮廓图
mMaterial.SetTexture("_BlurTex", temp1);
Graphics.Blit(renderTexture, temp2, mMaterial, 1);
//轮廓图和场景图叠加
mMaterial.SetTexture("_BlurTex", temp2);
mMaterial.SetFloat("_OutlineStrength", outLineStrength);
Graphics.Blit(source, destination, mMaterial, 2);
RenderTexture.ReleaseTemporary(temp1);
RenderTexture.ReleaseTemporary(temp2);
}
else
{
Graphics.Blit(source, destination);
}
}
private void OnDestroy()
{
if (renderTexture)
{
RenderTexture.ReleaseTemporary(renderTexture);
renderTexture = null;
}
if (temp1)
{
RenderTexture.ReleaseTemporary(temp1);
temp1 = null;
}
if (temp2)
{
RenderTexture.ReleaseTemporary(temp2);
temp2 = null;
}
if (commandBuffer != null)
{
commandBuffer.Release();
commandBuffer = null;
}
}
}
然后新建OutLineEffect.shader,再把shader代码怼进去:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "SepShader/PostEffect/OutLineEffect" {
Properties{
_MainTex("Base (RGB)", 2D) = "white" {}
_BlurTex("Blur", 2D) = "white"{}
}
CGINCLUDE
#include "UnityCG.cginc"
//用于剔除中心留下轮廓
struct v2f_cull
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
//用于模糊
struct v2f_blur
{
float4 pos : SV_POSITION;
float2 uv[9] : TEXCOORD0;
};
//用于最后叠加
struct v2f_add
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float2 uv1 : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_TexelSize;
sampler2D _BlurTex;
float4 _BlurTex_TexelSize;
float4 _offsets;
float _OutlineStrength;
fixed4 _OutlineColor;
//获得轮廓 pass 1
v2f_cull vert_cull(appdata_img v)
{
v2f_cull o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord.xy;
//dx中纹理从左上角为初始坐标,需要反向
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv.y = 1 - o.uv.y;
#endif
return o;
}
fixed4 frag_cull(v2f_cull i) : SV_Target
{
fixed4 colorMain = tex2D(_MainTex, i.uv);
fixed4 colorBlur = tex2D(_BlurTex, i.uv);
return colorBlur - colorMain;
}
//高斯模糊 pass 0
v2f_blur vert_blur(appdata_img v)
{
v2f_blur o;
_offsets *= _MainTex_TexelSize.xyxy;
float2 width = _MainTex_TexelSize.xyxy;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.texcoord + half2(-1, -1) * width;
o.uv[1] = v.texcoord + half2(0, -1) * width;
o.uv[2] = v.texcoord + half2(1, -1) * width;
o.uv[3] = v.texcoord + half2(-1, 0) * width;
o.uv[4] = v.texcoord + half2(0, 0) * width;
o.uv[5] = v.texcoord + half2(1, 0) * width;
o.uv[6] = v.texcoord + half2(-1, 1) * width;
o.uv[7] = v.texcoord + half2(0, 1) * width;
o.uv[8] = v.texcoord + half2(1, 1) * width;
return o;
}
fixed4 frag_blur(v2f_blur i) : SV_Target
{
half outValue = 0;
outValue += tex2D(_MainTex, i.uv[0]).b;
outValue += tex2D(_MainTex, i.uv[1]).b;
outValue += tex2D(_MainTex, i.uv[2]).b;
outValue += tex2D(_MainTex, i.uv[3]).b;
outValue += tex2D(_MainTex, i.uv[4]).b;
outValue += tex2D(_MainTex, i.uv[5]).b;
outValue += tex2D(_MainTex, i.uv[6]).b;
outValue += tex2D(_MainTex, i.uv[7]).b;
outValue += tex2D(_MainTex, i.uv[8]).b;
outValue = saturate(outValue);
return outValue * _OutlineColor;
}
//最终叠加 pass 2
v2f_add vert_final(appdata_img v)
{
v2f_add o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy;
o.uv1.xy = o.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv.y = 1 - o.uv.y;
#endif
return o;
}
fixed4 frag_final(v2f_add i) : SV_Target
{
fixed4 ori = tex2D(_MainTex, i.uv1);
fixed4 blur = tex2D(_BlurTex, i.uv);
fixed4 final = ori + blur * _OutlineStrength;
return final;
}
ENDCG
SubShader
{
//pass 0: 高斯模糊
Pass
{
ZTest Off
Cull Off
ZWrite Off
Fog{ Mode Off }
CGPROGRAM
#pragma vertex vert_blur
#pragma fragment frag_blur
ENDCG
}
//pass 1: 剔除中心部分
Pass
{
ZTest Off
Cull Off
ZWrite Off
Fog{ Mode Off }
CGPROGRAM
#pragma vertex vert_cull
#pragma fragment frag_cull
ENDCG
}
//pass 2: 最终叠加
Pass
{
ZTest Off
Cull Off
ZWrite Off
Fog{ Mode Off }
CGPROGRAM
#pragma vertex vert_final
#pragma fragment frag_final
ENDCG
}
}
}
对了,还需要一个纯色的shader作减法抠出来显示本来的纹理:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "SepShader/S_SingleColor"
{
Properties
{
_MainColor ("Main Color", Color) = (1,0,0,1)
_Width ("width",int) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull Back
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
fixed4 _MainColor;
int _Width;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _MainColor;
}
ENDCG
}
}
}
然后你要做的就是把这两个shader拖到OutLineCameraComponent脚本中对应的shader位置,然后运行时就把模型的meshrenderer通过AddTarget(BaseModel target)的方法传进来就可以在这个模型周围产生描边了,BaseModel类是我自定义的类,存了一堆模型的参数,需要你们自己改一小点就可以用了。
注意一点我这个描边颜色是青色,若要其他颜色可以改脚本里的颜色,但是我描边的shader里判断的是颜色的b通道的值,你若改其他颜色比如红色就要改成R通道了。另外增加downSample可以增加描边的宽度。
在然后就是描边的效果图了: