- Home /
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:
https://docs.unity3d.com/Manual/GraphicsCommandBuffers.html
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 assu$$anonymous$$g 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 $$anonymous$$RT 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 = SA$$anonymous$$PLE_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 (_$$anonymous$$ainTex, i.uv) * _Color;
half4 emission = (tex2D (_$$anonymous$$ainTex, i.uv) * _EmissionColor);
outDiffuse = col;
outEmission = emission;
}
Answer by Namey5 · Mar 30, 2020 at 04:13 AM
I rewrote a chunk of the decal system to allow for emission decals, the files that have changed are in this zip;
Updated Deferred Decals
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).
One thing to note is that it does flicker in the editor (I would guess the injection point is inconsistent on the scene view camera), but it's fine in play mode.
Thank you so much. I finally got it working using your code :D
There are still some issues I'll need resolving but for now this will work perfectly thanks :) $$anonymous$$y remaining issues:
Depending on where I am looking sometimes the decals don't appear until looking away and looking back, I realized this had to do with the world position of the GameObject with the DeferredDecalRenderer script attached to it. I solved the problem by just parenting the GameObject to the player's Camera and putting it forward so its always in the view of the player. I'm sure there's got to be a better solution to this, but it works.
The decals are completely messed up in the scene view for me. Not sure the solution to this but I can work around it for now.
Walking very close to the decals to the point where the camera is within the bounds of the decal makes it invisible. I assume this is just how deferred decals work and I can't fix this, but I'm not sure. I can live with this.
These deferred decals apply to all layers. I assume this might be unavoidable with how commandBuffers work, but it would be great if I could choose which layers are affected by the decals. Not that important of an issue for my game though.
Again thanks a ton for the help :) this will work great for my game and looking at your code I even learned a bit about how some command buffer stuff works
After doing some more testing, turns out I overthought things and the lighting buffer is the target texture before lighting (makes sense now), so all the decals can be drawn with one command buffer. Writing into emission at this point is much more consistent and now also works in scene view. I also moved the diffuse pass into the emission shader so now you only need one material with the emission shader to draw both diffuse and emission. For point number 1, I moved the decal rendering code from OnWillRenderObject to OnPostRender, so decals will always be drawn rather than only when the renderer's parent object is on screen. As for number 3, I made it so that the decals are drawn back-first and over the top of other objects such that you can't clip through them anymore. I thought that this was how they were doing it because I see no reason they shouldn't, but I guess not. Your last point however, can't really be handled in the renderer. If this was rendered in forward, you could mask the decals out with the stencil buffer, but we're in deferred. If you want the decals to only apply to a layermask, you'll need to do that per decal (with a raycast, etc). I can't post the updated files here, so I've updated the link on the answer above.
Answer by OfficialDingbat · Mar 29, 2020 at 01:08 PM
@Namey5 Thanks for the help ^_^ 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);
}
}
Ah right. I assumed the system would be using the full GBuffer $$anonymous$$RT but it makes sense they would only need the normals and diffuse. In that case, you would need to change line 110 in the first script;
...
// render diffuse+normals decals into two $$anonymous$$RTs
RenderTargetIdentifier[] mrt = {BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer2, BuiltinRenderTextureType.GBuffer3};
buf.SetRenderTarget (mrt, BuiltinRenderTextureType.CameraTarget);
foreach (var decal in system.m_DecalsBoth)
{
buf.Draw$$anonymous$$esh (m_Cube$$anonymous$$esh, decal.transform.localToWorld$$anonymous$$atrix, decal.m_$$anonymous$$aterial);
}
...
Also, because this is writing to diffuse, normals and emission now, we also need to change the render targets in the shader (I'm going to assume you don't need normal decals, so I'll comment out the normals target);
out half4 outDiffuse : SV_Target0, // RT0: diffuse color (rgb), --unused-- (a)
//out half4 outNormal : SV_Target1, // RT1: normal (rgb), --unused-- (a)
out half4 outEmission : SV_Target2 // RT2: emission (rgb), --unused-- (a)
Now when you need an emission decal, mark the decal type as 'Both'.
Hmm still still not working but different results. The diffuse color now seems like its affecting something differently, almost like specularity? And the emission color still does nothing. Replaced the shader code, tried with the commented line and without, replaced the DeferredDecalRenderer class lines after 110, and set the decal to Both. I guess I'll finally have to look a bit more into how CommandBuffers and all that works.
I was being a bit lazy here and wanted to avoid fully rewriting things where possible, however after looking into it you will need to make some fundamental changes to the system to work with emission. Emission and lighting are bundled together in deferred rendering, however the GBuffer textures are all normalised formats to $$anonymous$$imise memory usage. This doesn't work in HDR rendering, so ins$$anonymous$$d emission isn't rendered to a GBuffer texture - it's rendered straight to the camera's render target. Also, I didn't realise the system used a different material/shader per decal type, so that would need to be done too. I'm currently doing some tests and rewriting it to add emission support, so I'll get back to you soon.
Hmmm so I had HDR enabled on my player camera. Disabling that makes my diffuse and texture work perfectly. The emission value also sort of works except it seems almost inverted? full white emission turns the texture black, full black makes it glow bright white, bright red makes it glow neon cyan. Not sure why that's like that. The bigger problem here is removing HDR completely changes my lighting (for the worse.) is this system compatible with HDR cameras whatsoever? Otherwise I guess I'll need a different system.
Your answer
Follow this Question
Related Questions
Does the Web Player support PBR? 0 Answers
Material is not visible in android build but works fine in editor? 0 Answers
Is there an any way to use batching with UV tiling? 1 Answer
Lit, Simple Lit and Complex Lit Shaders are not Avaiable 1 Answer
Emissive material not working despite setting objects to "Static" 10 Answers