CommandBuffer应用例子——模型局部发光

本文详细介绍了如何在Unity中利用CommandBuffer和自定义Shader实现模型的局部发光效果,包括渲染黑白遮罩图、模糊处理和叠加原图的步骤。通过两种不同的方法,一种是通过单独渲染发光物体并处理遮挡,另一种是使用额外摄像机配合Shader处理,展示了实现局部发光的技巧。文章还提供了C#代码示例和Shader代码,以及对每种方法的优缺点分析。
摘要由CSDN通过智能技术生成

Unity屏幕后处理之局部发光效果

  大家好,我是阿赵
  之前介绍了CommandBuffer的用法。这里来举一个实际应用的例子——模型局部发光

一、例子介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  上面是这个例子的实际效果。我可以根据需要,控制某个部位的模型发光,比如只是剑发光、只是头发发光,或者只是身体发光。
  注意最后一张图,发光的物体是受到遮挡关系的影响的,比如身体发光了,但腰带并没有发光,这是特意的,为了看出这个不是全屏后处理,而是可以有遮挡关系的。

二、原理分析

  要实现这样的效果,做法有很多,但核心的部分分为这么几步:
以只是剑发光为例,

1、渲染黑白遮罩图

在这里插入图片描述

2、模糊处理

在这里插入图片描述

3、叠加到原图

在这里插入图片描述

如果以身体为例
我们只看第一张遮罩贴图,可以看到没有指定发光的物体会挡住发光物体,所以那个部分是不会发光的。
在这里插入图片描述

  由于这个例子涉及到了卡通渲染的材质,具体的卡通渲染做法可以参考我之前的文章,这里为了简化问题,就用两个球和一个立方体来做这个演示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

三、实现步骤

  从上面的渲染过程可以看出来,这个效果的实现步骤分为三步

1、渲染出黑白遮罩图,这个过程的做法有很多,比如以下这些手段:

  1.先不渲染要发光的物体,单独用CommandBuffer渲染发光物体,然后使用摄像机深度图去裁剪单独渲染的物体。
  2.把需要渲染的物体和遮挡关系的物体全部用代码收集,然后通过CommandBuffer逐个渲染,发光的物体渲染成白色,不发光的物体渲染成黑色
  3.加多一个摄像机,使用指定的shader去渲染场景里面的物体,发光的物体渲染成白色,不发光的物体渲染成黑色
  我在网上看了几篇文章,发现都是使用第一种方式做的。但第一种方式我感觉是有问题的,深度图如果不包含发光物体,那么深度的信息就会有误,导致发光物体一直会被不发光物体遮盖,做不到发光物体穿插在两个不发光物体之间。
所以我并没有使用深度图的做法,而是使用了另外两种方法各自实现了一遍,具体的做法在下面会说。

2、将渲染得到的黑白遮罩贴图做高斯模糊,并且填充颜色

  这一步是比较简单的,高斯模糊的算法我只会会写单独的文章介绍,也不一定是用高斯模糊去做,模糊的算法有很多种。

3、将高斯模糊后的图片和原图做叠加,得到局部发光的效果

  这一步没什么好说的,只是把高斯模糊图和原图相加而已,当然也可以做混合,给一个权重值,控制高斯贴图叠加时的权重。
  第一步黑白遮罩图是关键,不但可以控制是否发光,还可以控制发光的强度。
  下面就重点来说一下黑白遮罩图的做法:

四、遮罩贴图第一种做法

1、C#代码

1.源代码

using UnityEngine;
using UnityEngine.Rendering;

public class DrawObjs : MonoBehaviour
{
    public Camera cam;
    public MeshRenderer[] renders;
    private RenderTexture rt1;
    private RenderTexture rt2;
    private RenderTexture rt3;

    public Material blurMat;
    public Material comMat;
    public Material whiteMat;
    public Material blackMat;
    private CommandBuffer cmd1;

    public int blurDownSample = 1;
    public int downSample = 1;
    public int iterations = 1;
    public float blurSpread = 1;
    // Start is called before the first frame update
    void Start()
    {

    }

    void OnEnable()
    {
        rt1 = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample, 24, RenderTextureFormat.Default);
        rt2 = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample, 24, RenderTextureFormat.Default);
        rt2.filterMode = FilterMode.Bilinear;
        rt3 = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample, 24, RenderTextureFormat.Default);
        rt3.filterMode = FilterMode.Bilinear;
        DrawObjects();
    }
    void OnDisable()
    {
        if(rt1)
        {
            RenderTexture.ReleaseTemporary(rt1);
            rt1 = null;
        }
        if(rt2)
        {
            RenderTexture.ReleaseTemporary(rt2);
            rt2 = null;
        }
        if (rt3)
        {
            RenderTexture.ReleaseTemporary(rt3);
            rt3 = null;
        }

        if (cmd1!=null)
        {
            cmd1.Dispose();
            cmd1 = null;
        }
    }
    // Update is called once per frame
    void Update()
    {
        
    }

    private void DrawObjects()
    {
        if (renders == null || renders.Length == 0)
        {
            return;
        }
        cmd1 = new CommandBuffer();
        cmd1.name = "AzhaoDrawLightObj";

        cmd1.SetRenderTarget(rt1);
        cmd1.ClearRenderTarget(true, true, Color.clear);
//渲染黑白物体
        for (int i = 0; i < renders.Length; i++)
        {
            Renderer item = renders[i];
            if (item.gameObject.activeInHierarchy == false || item.enabled == false)
            {
                continue;
            }
            if(item.gameObject.layer == 8)
            {
                cmd1.DrawRenderer(item, whiteMat);
            }
            else
            {
                cmd1.DrawRenderer(item, blackMat);
            }
            
        }
//高斯模糊
        cmd1.Blit(rt1, rt2);
        for (int i = 0; i < iterations; i++)
        {
            blurMat.SetFloat("_BlurSize", 1.0f + i * blurSpread);

            cmd1.Blit(rt2, rt3, blurMat, 0); 

            cmd1.Blit(rt3, rt2, blurMat, 1); 

        }
        cmd1.Blit(rt2, rt1);	
//把结果放到合成材质球的指定贴图
        comMat.SetTexture("_AddTex", rt1);
//添加到摄像机的CameraEvent.BeforeImageEffects事件渲染
        cam.AddCommandBuffer(CameraEvent.BeforeImageEffects, cmd1);
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, comMat);
    }


}

2.代码说明

  需要注意的几个点:
(1)RenderTexture和CommandBuffer都必须记得有创建就有回收
(2)在Enable的时候,创建CommandBuffer,并给他添加到摄像机的BeforeImageEffects。
(3)使用CommandBuffer.DrawRenderer方法,逐个渲染指定的物体
(4)我给发光物体指定了layer是8,所以如果layer是8的物体,就使用白色的材质球渲染,其他物体使用黑色的材质球渲染
(5)高斯模糊部分可以先忽略
(6)把高斯模糊后的图片存到合成用的材质球的_AddTex贴图
(7)在OnRenderImage生命周期,把原来屏幕的渲染图和刚才的_AddTex贴图做叠加,得到最后的发光效果。

2、shader

1.黑白渲染

在这里插入图片描述

正常的Unlit/Color就行,只是要一个颜色。创建一黑一白两个材质球,根据黑白的需要调整颜色

2.高斯模糊:

Shader "azhao/GaussianBlur" { 
	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {} 
		_BlurSize("Blur Size", Float) = 1.0 
		_Color("Color",Color) = (1,1,1,1)
	}

		SubShader{
			CGINCLUDE

			#include "UnityCG.cginc"

			sampler2D _MainTex;
			half4 _MainTex_TexelSize; 
			float _BlurSize;
			float4 _Color;
			struct v2f {
				float4 pos : SV_POSITION; 
				half2 uv[5]: TEXCOORD0;
			};

			v2f vertBlurVertical(appdata_img v) { 
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex); 
				half2 uv = v.texcoord;
				o.uv[0] = uv;
				o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
				o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
				o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
				o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
				return o;
			}

			v2f vertBlurHorizontal(appdata_img v) { 
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex); 
				half2 uv = v.texcoord;
				o.uv[0] = uv;
				o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
				o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
				o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
				o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
				return o;
			}

			fixed4 fragBlur(v2f i) : SV_Target {
				float weight[3] = {0.4026, 0.2442, 0.0545}; 
				fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
				for (int j = 1; j < 3; j++) {
					sum += tex2D(_MainTex, i.uv[j * 2 - 1]).rgb * weight[j]; 
					sum += tex2D(_MainTex, i.uv[j * 2]).rgb * weight[j]; 
				}
				sum *= _Color;
				return fixed4(sum, 1.0);
			}

			ENDCG

			ZTest Always Cull Off ZWrite Off

			Pass {
				NAME "GAUSSIAN_BLUR_VERTICAL"

				CGPROGRAM

				#pragma vertex vertBlurVertical  
				#pragma fragment fragBlur

				ENDCG
			}

			Pass {
				NAME "GAUSSIAN_BLUR_HORIZONTAL"

				CGPROGRAM

				#pragma vertex vertBlurHorizontal  
				#pragma fragment fragBlur

				ENDCG
			}
		}

			FallBack "Diffuse"
}

3.合成

Shader "azhao/Composite"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
		_AddTex("AddTex",2D) = "black"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
			Cull Off
			ZWrite Off
			ZTest Always
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;Q
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
			sampler2D _AddTex;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                // sample the texture
                half4 col = tex2D(_MainTex, i.uv);
				half4 addCol = tex2D(_AddTex, i.uv);
				half4 final = half4(saturate( col.rgb + addCol.rgb), 1);
                return final;
            }
            ENDCG
        }
    }
}

3、优缺点

1.优点

  实现过程简单,不需要对原物体的材质进行修改,只需要用代码收集需要渲染的物体到脚本就可以。

2.缺点

(1)对比起深度图的做法,物体渲染多了一遍,不顾深度图也有它本身的消耗
(2)由于物体不一定在摄像机的视锥范围内,所以需要自己去做剔除,不然会渲染多了很多摄像机看不见的物体。
(3)由于只有黑白两个材质球,所以变亮的物体的亮度是统一的,不能逐个调整。

五、遮罩贴图第二种做法

1、场景摄像机处理

在这里插入图片描述

1.复制一个摄像机在主摄像机下面,完全和主摄像机重叠,fov也和主摄像机一样,于是这
个摄像机看到的范围是和主摄像机一样的。
2.设置这个摄像机
在这里插入图片描述

把摄像机的Enable关掉,然后把摄像机的ClearFlags改成纯色,背景色改成黑色,去掉AudioListener

2、C#部分

1.源代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class DrawLightByCam : MonoBehaviour
{
    public Camera mainCam;
    public Camera subCam;
    public Shader blackShader;
    public RenderTexture rt1;
    public RenderTexture rt2;
    public RenderTexture rt3;
    private CommandBuffer cmd1;
    public Material blurMat;
    public Material comMat;
    public int downSample = 1;
    public int iterations = 1;
    public float blurSpread = 1;

    // Start is called before the first frame update
    void Start()
    {
        subCam.enabled = false;
    }
    private void OnEnable()
    {
        rt1 = RenderTexture.GetTemporary(Screen.width>>downSample, Screen.height >> downSample, 16, RenderTextureFormat.Default);
        rt2 = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 16, RenderTextureFormat.Default);
        rt3 = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 16, RenderTextureFormat.Default);
        subCam.targetTexture = rt1;
        DrawObjects();
    }

    private void OnDisable()
    {
        if(rt1)
        {
            RenderTexture.ReleaseTemporary(rt1);
            rt1 = null;
        }
        if (rt2)
        {
            RenderTexture.ReleaseTemporary(rt2);
            rt2 = null;
        }
        if (rt3)
        {
            RenderTexture.ReleaseTemporary(rt3);
            rt3 = null;
        }
        if (cmd1 != null)
        {
            cmd1.Dispose();
            cmd1 = null;
        }
    }
    // Update is called once per frame
    void Update()
    {
        subCam.RenderWithShader(blackShader, "RenderType");
    }

    private void DrawObjects()
    {
        cmd1 = new CommandBuffer();
        cmd1.name = "drawLightObj";

        cmd1.SetRenderTarget(rt2);
        cmd1.ClearRenderTarget(true, true, Color.clear);
//高斯模糊
        cmd1.Blit(rt1, rt2);
        for (int i = 0; i < iterations; i++)
        {
            blurMat.SetFloat("_BlurSize", 1.0f + i * blurSpread); 
            cmd1.Blit(rt2, rt3, blurMat, 0); 
            cmd1.Blit(rt3, rt2, blurMat, 1); 
        }
//把结果放到合成材质球的指定贴图
        comMat.SetTexture("_AddTex", rt2);
//添加到摄像机的CameraEvent.BeforeImageEffects事件渲染

        mainCam.AddCommandBuffer(CameraEvent.BeforeImageEffects, cmd1);
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, comMat);
    }
}

2.代码说明

(1)复制出来的摄像机要关闭Enable ,因为我们在Update的时候主动渲染它
(2)在Update的时候调用subCam.RenderWithShader(blackShader, “RenderType”);,指定一个shader来渲染场景里面的物体,把物体渲染成黑白色。
(3)其他步骤和上面第一种方法一样

3、Shader

1.RenderWithShader使用的shader

Shader "azhao/OnlyColor"
{
    Properties
    {
		_MaskColor("Color",Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;

            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

			float4 _MaskColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
				return _MaskColor;
            }
            ENDCG
        }
    }
}

2.修改发光的物体的原始Shader

只需要在原始Shader的属性里面加一个遮罩颜色就够了
_MaskColor(“MaskColor”,Color) = (1,1,1,1)

3.其他

模糊shader和合成shader参考第一种方法。

4、优缺点

1.优点

(1)不需要计算哪些物体需要渲染,不需要计算物体是否在摄像机的视锥内
(2)由于黑白遮罩颜色是记录在每个模型的_MaskColor上的,所以可以逐个模型指定发光的强度和变化。

2.缺点

(1)渲染的内容比第一种方法要多
(2)需要修改原模型的shader

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值