Trying to find a way to use deferred decals on an unlit texture

Would it be possible to use a deferred decal (like this one Deferred Decals - Community Showcases - Unity Discussions) with unlit textures?

I’ve been working at this a bit and one thing I’ve tried is using a modified standard shader but outputting the texture colors to o.Emission instead of o.Albedo.

That’s pretty darn close but it’s still getting light on it from light sources and the colors aren’t precise. It was easy enough to turn off shadows but I’m hoping there’s a way to have it mimic a simple, unlit shader but still showing the decal on top of it.

It seems like there are several questions embedded in this:

Can you have a decal that’s unlit:
Yes, but you’ll need a decal shader that writes to the emission gbuffer in a non additive way, otherwise the color of the decal will just mix with the ambient & emissive color of what’s was rendered before.

Can you have a deferred shader that’s unlit:
Yes. Just using the built in Standard Specular shader with the albedo and specular set to black and an Emission texture should do what you want, otherwise you’ll always get a little bit of ambient reflections.

Can you have decals that work on unlit surfaces:
Yes, but see the answer to the first question.

And a question that was not asked but is relevant here; do you need to use the deferred rendering path to use deferred decals:
No! Especially if you’re just looking to have unlit decals, these could just as easily be applied with out having to be “deferred decals” in that they don’t need to write into the gbuffers and instead could just apply directly to depth texture and render directly to the screen during the CameraEvent.AfterForwardOpaque with a command buffer. This might be an easier path to get things working depending on what your needs are. This would work with deferred rendering, or forward rendering, or even forward rendered opaque objects rendered while using deferred rendering.

I believe this asset does what I described above, along with what you’re trying to do: Unity Asset Store - The Best Assets for Game Making

If this is something you’re trying to implement yourself, you’re going to have to give us more information about what exactly you’re doing, and what problems you’re seeing (preferably with screenshots showing the problem).

Also, the color shifts you’re seeing using Emissive might be a product of using a Standard “metallic” shader, or might be from using linear color space? Not entirely sure.

4 Likes

Thank you so much for taking the time to reply! I really appreciate the detailed response. Let me flesh out some detail to my question as you’ve asked for:

Part 1 [image being projected]
For the first question, in looking at the stock code from the popular deferred decal shader, the diffuse only shader doesn’t seem to multiply the color. I’m sure i’m misreading it, but in the frag function, it seems to directly output the color from the texture? I’m only seeing the color output from the tex2d function then returned. And this only shows if there’s a light-source in the scene.

            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); //Is this getting multiplied somehwere else?
                return col;
            }

Part 2 [object being projected on]
For still seeing color shifts on objects being projected on while only outputting to emissive:
I’m using a standard shader and only modified this portion, so I’ve got the texture going to emission and albedo set to black, and metallic and smoothness set to 0. Strangely though, if I set o.Emission to a single color, it appears to work correctly! (But I need it to use my texture, instead).

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex).rgba;
            o.Albedo = half4(0.0, 0.0, 0.0, 0.0);
            o.Emission = c.rgba;

            o.Metallic = 0.0;// _Metallic;
            o.Smoothness = 0.0;// _Glossiness;
        }

With a standard shader it looks like this:

And with my modified standard, like this:


Part 3
Lastly, for your point about not needing to use deferred rendering. It sounds like I may be coming at this from a fundamentally wrong angle? Would this be a much simpler problem to solve if if it weren’t deferred? Or just more efficient?

I may use an assett store decal solution but I was really hoping to get an understanding of the process for this. I’ve googled pretty hard for ‘unlit decal shader’ and varients without much luck. Do you know of a resource that might help me for creating an unlit, non-deferred rendering decal? (My entire game is going to use unlit textures)

Thank you again for your help, I really appreciate your time.
-falldeaf

Part 1

Is shader using Blend? When is this shader being run? You’re describing it as a diffuse shader, and if that is used from a command buffer using CameraEvent.AfterGBuffer then it’ll be writing to the diffuse buffer which subsequently gets lit by your real time lights and shadows. You need to be writing to SV_Target3 if you want it to remain unlit.

Part 2

No it shouldn’t. You’re using a Standard metallic shader, and “0” metallic does not mean no specular! You can read my longer explanations elsewhere, but the short version is the metallic value is essentially a lerp between “dark grey” and “albedo color”. That means o.Metallic = 0.0 will still have some amount of reflection which is causing the slight color shift.

The “correct” way to handle this is to use a #pragma surface surf StandardSpecular instead of #pragma surface surf Standard and set the o.Albedo and o.Specular to black. You can also kind of cheat by using a black o.Albedo and o.Metallic = 1.0 instead of = 0.0. Internally the Standard shader is basically a Standard Specular shader that calculates the specular color from the albedo and metallic values. This is why I mentioned Standard Specular in my previous post.

Part 3

The deferred pipeline has a lot of overhead as it makes a lot of assumptions about you wanting to use PBR lighting and reflections and the whole thing. It’ll end up doing a lot of work that you ultimately don’t need or want if your game is just unlit textures.

The fundamentals of deferred decals are you’re projecting onto a camera depth texture (and camera normals texture) rather than geometry, both of which you can get with forward rendering as well. This has it’s own overhead as the depth and normals are rendered as separate passes prior to the “main” forward pass, so using deferred may still make sense here to some degree. However because you don’t care about any of the lighting aspect of using deferred it may be easier and faster to draw the decals during CameraEvent.AfterForwardOpaque and draw directly to SV_Target (which at that point is just what’s going to render on screen, not a gbuffer).

It might all make more sense to you if you read up on deferred rendering, and try using the Frame Debugger in Unity to look through all of the rendering steps that are happening.

Again, I very much appreciate your help!

The background is now working perfectly. It’s exactly as you described it should work. I used a standard specular, the emission channel to the texture/uv and albedo and specular to 0.

[EDIT: Wait sorry, disregard this section below. I think you’ve already answered this in your response, when you said: “You need to be writing to SV_Target3 if you want it to remain unlit.”]

You were right also that there was blending going on. First, I tried turning blending off completely. That turned the sprite into a white box that was still accepting light. I tried different non additive blending modes and finally I tried blending mode off with a clip function inside CPROGRAM. This replicated the original blending for some reason.

Somehow, the color being put onto the surface is already based on light sources

// 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'

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

Shader "Decal/DecalShader Unlit"
{
    Properties
    {
        _MainTex ("Diffuse", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            Fog { Mode Off } // no fog in g-buffers pass
            ZWrite Off
            //Blend SrcAlpha OneMinusSrcAlpha

            //Tried many different blend modes
            Blend Off
            //AlphaTest Greater [0.]
            //BlendOp Max

            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 = mul (UNITY_MATRIX_MVP, 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)
            //)
            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);
                //fixed4 col = fixed4(1.0, 0., 0., 1.);

                //Ended up using clip to remove pixels that weren't high enough alhpa
                clip(col.a - .5);
                return col;
            }
            ENDCG
        }

    }

    Fallback Off
}

Finally, on your last point, it’s clear that I’ll need to read up on and understand deferred rendering to figure out if I need to change the overall method. Thank you for your guidance.

That commented out “//void frag(” section in your shader (which appears to be from a maybe Unity 5.1 era shader?) has some hints in there for you. Since at least 5.4 or so that should look like this:

    out half4 outGBuffer0 : SV_Target0, // diffuse (aka albedo) RGB, occlusion A
    out half4 outGBuffer1 : SV_Target1, // specular RGB, roughness A
    out half4 outGBuffer2 : SV_Target2, // world normal RGB, unused A
    out half4 outEmission : SV_Target3 // emission RGB, unused A

Note the names of those variables (outGBuffer1, outEmission, etc.) are essentially arbitrary, they could be john, paul, george, and ringo and still work, the semantic (SV_Target#) is important for informing the GPU on what to do with them. However the “emission” target, SV_Target3, is kind of different than the other 3 targets listed. The first 3 are data that get put into a texture that’s used for the deferred lighting that happens later, along with a depth texture that gets generated for “free” while rendering this so doesn’t need to be written out by the fragment shader. The “emission” does include the emission, but also the ambient lighting and lightmapping already multiplied with the albedo and in the past (before 5.6) any specular from the directional specular lightmaps. During the deferred lighting passes it also becomes the target that is being rendered to and is finally what appears on screen. Thats ignoring any forward renderer objects, or image effects, or reflections, that happen afterwards, but that’s the gist.

Lighting passes are all additive, so having the albedo and specular be black means the lighting (and perhaps more usefully for you, reflections) are calculated as black, and contribute nothing. It also means they’re basically wasted memory.

I would suggest if you continue to use deferred you should probably go into your Graphics settings and set Deferred Reflections to No Support. If you have no lights in your scene (and you should remove them if they’re not needed) the lighting and shadows will be skipped, but it’ll still do the work to calculate the reflections even though they’ll always be black.