赞
踩
在3D游戏中描边或者说边缘发光、边缘是非常常见的技术,通常这种技术用来提醒玩家当前选中的目标、角色、建筑等
在unity中选中模型会有边缘发光
在Unity的组件脚本中,给我们提供了OnRenderImage回调函数(该函数只能在有摄影机组件的对象上使用),这个回调函数给了我们开发者实现各种全屏幕效果的可能。同时呢还提供了CommandBuffer类用于自定义渲染模型。结合这两种功能我们通过CommandBuffer使用纯色渲染出想要描边的物体到一个RenderTexture,通过自定义的Shader将其边缘模糊,然后将模糊的部分再次赋予纯色从而扩展出边缘,将边缘再次模糊平滑后与原来的纯色RenderTexture做插值抠出边缘部分,再将这部分与OnRenderImage()提供的原图像混合从实现描边效果
一个简单的将模型渲染成纯色的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分为5Pass,分别对应横向和纵向的模糊、图像插值、图像混合与边缘实化
//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 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 }
这个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 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和多重的纹理叠加,性能会有显著的消耗。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。