Adding emission parameter to Deferred Decal shader

So I’ve been trying to implement a fast decal system for bullet holes/blood etc:
I found this very useful decal system using deferred decals and it seems very fast and works well for my game. Got it here:

The problem is that the decal shaders that come with this system do not have any parameters for emission. I would like my bullet holes to have an emissive glowing effect for a brief moment when being placed on the environment (That part is simple, just adjusting the emission value during runtime.)

However, I can’t seem to get my emission value to actually increase the output color to the point where it looks emissive. I believe this is because the shader is applying the lighting of the object its being placed on after the output. Probably something to do with how gbuffer works but I’m not too good at shader code and can’t figure out where to go from here.

Here is my shader code:

// Upgrade NOTE: commented out 'float4x4 _CameraToWorld', a built-in variable
// Upgrade NOTE: replaced '_CameraToWorld' with 'unity_CameraToWorld'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// http://www.popekim.com/2012/10/siggraph-2012-screen-space-decals-in.html

Shader "Decal/DecalShaderEmission"
{
	Properties
	{
		_Color("Color", Color) = (1, 1, 1, 1)
		_MainTex ("Diffuse", 2D) = "white" {}
		[HDR]_EmissionColor ("EmissionColor", Color) = (1, 1, 1, 1)
	}
	SubShader
	{
		Pass
		{
			Fog { Mode Off } // no fog in g-buffers pass
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag
			#pragma exclude_renderers nomrt
			
			#include "UnityCG.cginc"

			struct v2f
			{
				float4 pos : SV_POSITION;
				half2 uv : TEXCOORD0;
				float4 screenUV : TEXCOORD1;
				float3 ray : TEXCOORD2;
				half3 orientation : TEXCOORD3;
			};

			v2f vert (float3 v : POSITION)
			{
				v2f o;
				o.pos = UnityObjectToClipPos (float4(v,1));
				o.uv = v.xz+0.5;
				o.screenUV = ComputeScreenPos (o.pos);
				o.ray = mul (UNITY_MATRIX_MV, float4(v,1)).xyz * float3(-1,-1,1);
				o.orientation = mul ((float3x3)unity_ObjectToWorld, float3(0,1,0));
				return o;
			}

			CBUFFER_START(UnityPerCamera2)
			// float4x4 _CameraToWorld;
			CBUFFER_END

			sampler2D _MainTex;
			sampler2D_float _CameraDepthTexture;
			sampler2D _NormalsCopy;

			//void frag(
			//	v2f i,
			//	out half4 outDiffuse : COLOR0,			// RT0: diffuse color (rgb), --unused-- (a)
			//	out half4 outSpecRoughness : COLOR1,	// RT1: spec color (rgb), roughness (a)
			//	out half4 outNormal : COLOR2,			// RT2: normal (rgb), --unused-- (a)
			//	out half4 outEmission : COLOR3			// RT3: emission (rgb), --unused-- (a)
			//)

			float4 _Color;
			float4 _EmissionColor;

			fixed4 frag(v2f i) : SV_Target
			{
				i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
				float2 uv = i.screenUV.xy / i.screenUV.w;
				// read depth and reconstruct world position
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
				depth = Linear01Depth (depth);
				float4 vpos = float4(i.ray * depth,1);
				float3 wpos = mul (unity_CameraToWorld, vpos).xyz;
				float3 opos = mul (unity_WorldToObject, float4(wpos,1)).xyz;

				clip (float3(0.5,0.5,0.5) - abs(opos.xyz));


				i.uv = opos.xz+0.5;

				half3 normal = tex2D(_NormalsCopy, uv).rgb;
				fixed3 wnormal = normal.rgb * 2.0 - 1.0;
				clip (dot(wnormal, i.orientation) - 0.3);

				fixed4 col = tex2D(_MainTex, i.uv) * _Color;
				fixed4 emission = (tex2D(_MainTex, i.uv) * _EmissionColor);
				
				col.rgb += emission.rgb;

				return col;
			}
			ENDCG
		}		

	}

	Fallback Off
}

You would be correct in assuming that this is to do with the GBuffer. In fact, the solution is actually already written there in the shader (from lines 61-67); it’s just commented out. For deferred rendering, you manually need to write to each GBuffer texture appropriately via MRT output, whereas here you are only writing to the Albedo texture that has lighting applied afterwards. The emission texture (as noted in the comments) is denoted by the COLOR3 semantic (which should really be SV_Target3). The following should work;

void frag
(
	v2f i,
	out half4 outDiffuse : SV_Target0, // RT0: diffuse color (rgb), --unused-- (a)
	//out half4 outSpecRoughness : SV_Target1, // RT1: spec color (rgb), roughness (a)
	//out half4 outNormal : SV_Target2, // RT2: normal (rgb), --unused-- (a)
	out half4 outEmission : SV_Target3 // RT3: emission (rgb), --unused-- (a)
)
{
	i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
	float2 uv = i.screenUV.xy / i.screenUV.w;
	// read depth and reconstruct world position
	float depth = SAMPLE_DEPTH_TEXTURE (_CameraDepthTexture, uv);
	depth = Linear01Depth (depth);
	float4 vpos = float4 (i.ray * depth,1);
	float3 wpos = mul (unity_CameraToWorld, vpos).xyz;
	float3 opos = mul (unity_WorldToObject, float4 (wpos,1)).xyz;

	clip (float3 (0.5,0.5,0.5) - abs (opos.xyz));

	i.uv = opos.xz + 0.5;

	half3 normal = tex2D (_NormalsCopy, uv).rgb;
	fixed3 wnormal = normal.rgb * 2.0 - 1.0;
	clip (dot (wnormal, i.orientation) - 0.3);

	half4 col = tex2D (_MainTex, i.uv) * _Color;
	half4 emission = (tex2D (_MainTex, i.uv) * _EmissionColor);

	outDiffuse = col;
	outEmission = emission;
}

I rewrote a chunk of the decal system to allow for emission decals, the files that have changed are in this zip;

[155513-deferreddecalrenderer.zip|155513]

It wasn’t too difficult to modify the system to work with emission because I’ve spent enough time working with command buffers in my own time, however the changes aren’t exactly straightforward, and the modifications are all over the place so it would be too confusing to post the individual changes here. The decal system now uses two command buffers - previously all decals could be injected before lighting, however we actually need the lighting buffer because that is where the emission is stored, so a second command buffer is injected after the GBuffer is created to copy and add to emission. To use emission decals, I’ve added an emission decal type, as well as a second material slot to make use of the emission decal shader I’ve provided. Emission is handled separately, so it needs two materials to render to both diffuse and emission portions of the GBuffer. I’ve also added a checkbox to the shader that allows you to just use the texture’s alpha to mask the emission rather than the whole colour. This wasn’t as easy a solution as I first thought, but I hope that it finally works (I’ve tested it and it works fine on my side, with HDR anyway).

@Namey5
Thanks for the help :slight_smile:
I tried your code and it doesn’t seem like the emission value is affecting anything when changed. Does this maybe have to do with the DeferredDecalRenderer class? It has different options for diffuse, diffuse+normal, normal only, etc so I’m not sure if i need to create a new section for handling emission values. There are 2 classes here I think are important. DeferredDecalRenderer which handles the CommandBuffer stuff of which I understand very little, and the DecalBetter class which is just placed on each decal and has an enum to switch between decal types. I saw somewhere that BuiltinRenderTextureType.GBuffer3 is used for emission so do I need to pass my emission value/decal there somehow? (In the DefferredDecalRenderer class?) Maybe there’s something else I’m missing here.

DeferredDecalSystem & DeferredDecalRenderer

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

// See _ReadMe.txt

public class DeferredDecalSystem
{
	static DeferredDecalSystem m_Instance;
	static public DeferredDecalSystem instance {
		get {
			if (m_Instance == null)
				m_Instance = new DeferredDecalSystem();
			return m_Instance;
		}
	}

	internal HashSet<DecalBetter> m_DecalsDiffuse = new HashSet<DecalBetter>();
	internal HashSet<DecalBetter> m_DecalsNormals = new HashSet<DecalBetter>();
	internal HashSet<DecalBetter> m_DecalsBoth = new HashSet<DecalBetter>();

	public void AddDecal (DecalBetter d)
	{
		RemoveDecal (d);
		if (d.m_Kind == DecalBetter.Kind.DiffuseOnly)
			m_DecalsDiffuse.Add (d);
		if (d.m_Kind == DecalBetter.Kind.NormalsOnly)
			m_DecalsNormals.Add (d);
		if (d.m_Kind == DecalBetter.Kind.Both)
			m_DecalsBoth.Add (d);
	}
	public void RemoveDecal (DecalBetter d)
	{
		m_DecalsDiffuse.Remove (d);
		m_DecalsNormals.Remove (d);
		m_DecalsBoth.Remove (d);
	}
}

[ExecuteInEditMode]
public class DeferredDecalRenderer : MonoBehaviour
{
	public Mesh m_CubeMesh;
	private Dictionary<Camera,CommandBuffer> m_Cameras = new Dictionary<Camera,CommandBuffer>();

	public void OnDisable()
	{
		foreach (var cam in m_Cameras)
		{
			if (cam.Key)
			{
				cam.Key.RemoveCommandBuffer (CameraEvent.BeforeLighting, cam.Value);
			}
		}
	}

	public void OnWillRenderObject()
	{
		var act = gameObject.activeInHierarchy && enabled;
		if (!act)
		{
			OnDisable();
			return;
		}

		var cam = Camera.current;
		if (!cam)
			return;

		CommandBuffer buf = null;
		if (m_Cameras.ContainsKey(cam))
		{
			buf = m_Cameras[cam];
			buf.Clear ();
		}
		else
		{
			buf = new CommandBuffer();
			buf.name = "Deferred decals";
			m_Cameras[cam] = buf;

			// set this command buffer to be executed just before deferred lighting pass
			// in the camera
			cam.AddCommandBuffer (CameraEvent.BeforeLighting, buf);
		}

		//@TODO: in a real system should cull decals, and possibly only
		// recreate the command buffer when something has changed.

		var system = DeferredDecalSystem.instance;

		// copy g-buffer normals into a temporary RT
		var normalsID = Shader.PropertyToID("_NormalsCopy");
		buf.GetTemporaryRT (normalsID, -1, -1);
		buf.Blit (BuiltinRenderTextureType.GBuffer2, normalsID);
		// render diffuse-only decals into diffuse channel
		buf.SetRenderTarget (BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.CameraTarget);
		foreach (var decal in system.m_DecalsDiffuse)
		{
			buf.DrawMesh (m_CubeMesh, decal.transform.localToWorldMatrix, decal.m_Material);
		}
		// render normals-only decals into normals channel
		buf.SetRenderTarget (BuiltinRenderTextureType.GBuffer2, BuiltinRenderTextureType.CameraTarget);
		foreach (var decal in system.m_DecalsNormals)
		{
			buf.DrawMesh (m_CubeMesh, decal.transform.localToWorldMatrix, decal.m_Material);
		}
		// render diffuse+normals decals into two MRTs
		RenderTargetIdentifier[] mrt = {BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer2};
		buf.SetRenderTarget (mrt, BuiltinRenderTextureType.CameraTarget);
		foreach (var decal in system.m_DecalsBoth)
		{
			buf.DrawMesh (m_CubeMesh, decal.transform.localToWorldMatrix, decal.m_Material);
		}
		// release temporary normals RT
		buf.ReleaseTemporaryRT (normalsID);
	}
}

DecalBetter

using UnityEngine;

[ExecuteInEditMode]
public class DecalBetter : MonoBehaviour
{
	public enum Kind
	{
		DiffuseOnly,
		NormalsOnly,
		Both
	}
	public Kind m_Kind;
	public Material m_Material;

	public void OnEnable()
	{
		DeferredDecalSystem.instance.AddDecal (this);
	}

	public void Start()
	{
		DeferredDecalSystem.instance.AddDecal (this);
	}

	public void OnDisable()
	{
		DeferredDecalSystem.instance.RemoveDecal (this);
	}

	private void DrawGizmo(bool selected)
	{
		var col = new Color(0.0f,0.7f,1f,1.0f);
		col.a = selected ? 0.3f : 0.1f;
		Gizmos.color = col;
		Gizmos.matrix = transform.localToWorldMatrix;
		Gizmos.DrawCube (Vector3.zero, Vector3.one);
		col.a = selected ? 0.5f : 0.2f;
		Gizmos.color = col;
		Gizmos.DrawWireCube (Vector3.zero, Vector3.one);		
	}

	public void OnDrawGizmos()
	{
		DrawGizmo(false);
	}
	public void OnDrawGizmosSelected()
	{
		DrawGizmo(true);
	}
}