Need help with shader emission

Hello everyone, I tried to make a modified toon shader for glow-in-the-dark items. It shows emissions according to _Illum texture in the darkness, but with lights on it’s supposed to show only basic texture without any emission.

Here is the shader. In surf function it saves emission to Custom field, and in lighting function this Custom field is used for calculating emission for unlit object. Then lerp function chooses which one to use: lit with lights without emission (c.rgb), or unlit with emission (d.rgb). For weak lights it lerps somewhere in between.

Everything works fine as described above with directional light (images #1 & #2). But I can’t understand why it doesn’t work with point lights (image #3). No matter what I do, emission never disappears with point lights, even if they are at 2X intensity and placed on very item. The texture becomes almost pure white, but emission still doesn’t disappear…

I hope somebody can help me with this.

Shader "Custom/Illuminated" {
    Properties {
        _Color ("Main Color", Color) = (0.5,0.5,0.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Illum ("Illumin (A)", 2D) = "white" {}
        _IllumPower ("Illum Power", Float) = 1
    }

    SubShader {
        Tags {"RenderType"="Opaque"}
        LOD 200
        Lighting On
        //Cull off
    
CGPROGRAM
#pragma surface surf ToonRamp fullforwardshadows

#pragma lighting ToonRamp exclude_path:prepass

sampler2D _MainTex;
sampler2D _Illum;
float4 _Color;
half _IllumPower;
half4 Custom;

struct EditorSurfaceOutput {
         half3 Albedo;
         half3 Normal;
         half3 Emission;
         half3 Gloss;
         half Specular;
         half Alpha;
         half3 Custom;
       };
inline half4 LightingToonRamp (EditorSurfaceOutput s, half3 lightDir, half atten)
{
    #ifndef USING_DIRECTIONAL_LIGHT
    lightDir = normalize(lightDir);
    #endif

half4 d;
d.rgb = s.Custom*s.Albedo * _IllumPower;
half4 c;
c.rgb = s.Albedo * _LightColor0.rgb * (atten*2);
float intens = saturate(_LightColor0*5);
c.rgb = lerp(d.rgb, c.rgb, intens);
return c;
}

struct Input {
    float2 uv_MainTex : TEXCOORD0;
    float2 uv_Illum;
};

void surf (Input IN, inout EditorSurfaceOutput o) {
    half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    o.Custom = tex2D(_Illum, IN.uv_Illum).a;
    o.Emission = 0.0;;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
ENDCG

    }

    Fallback "Diffuse"
}

If I manually type a value between 0 and 1 instead of “intens” into the lerp function, it produces the effect I need, emission disappears for both directional and spot lights when it’s 1, shines for 0, and lerps in between. So the shader works fine, and all I need is getting a correct variable to produce number between 0 and 1 based on light intensity.

The solution is probably extremely easy…

_LightColor0 is a float4. Can you experiment with trying saturate(_LightColor0.r) or saturate((_LightColor0.r + _LightColor0.g + _LightColor0.b) / 3.0) and let us know the results?

The result is exactly the same as with using just _LightColor0, and can be seen in the images above. Works fine with directional light, but doesn’t work with point lights.
I tried using r, g, b colors separately, adding, multiplying, and subdividing them. Also experimented with values of s.Custom, s Albedo, atten, separately and together with _LightColor0 in all combinations I could think of.

Nothing results in that “1” value that will ignore the emission.

just manually ‘brute forcing’ some different shader settings, seems like adding this after the ‘Lighting On’ line helps a bit?

           Blend Zero Zero

Thanks for suggestion. Still doesn’t work, unfortunately. It helps to remove emission for point lights, but creates a whole array of other issues instead. Texture becomes completely black with directional light, and it’s also black (no emission) when there are no directional or point lights. Looks like something important is still missing.

Still looking for help… It’s not like I’m lazy and expect somebody to write a shader for me. The shader is written already and it works. Just one line of code is missing.

There is _LightColor0 variable that clearly changes its value depending on lighting. And there is lerp(d.rgb, c.rgb, intensity) function that produces the effect I need if intensity is a value between 0 and 1. The only thing required is writing one line of code to convert _LightColor0 into a variable between 0 and 1. That’s literally a few seconds of work for any person who is good with programming.:frowning:

The problem you’re having is with how Unity’s forward rendering path lighting system works. To render multiple lights on a single object Unity renders it multiple times. The first pass is the ForwardBase pass which renders the object’s ambient lighting, emission color the main directional lighting, and optionally reflections and lightmaps. Every subsequent pass uses the ForwardAdd pass which only renders the lighting from a single extra light and adds the results in top of the previous base pass.

For your case by the time the shader system knows if it’s lit by a point light it has already drawn the emissive and has no way of removing it.

edit: Some further thoughts. To do the effect you want will require some additional shader magic.

The easiest method I can think of is to use a grab pass and an additional emissive glow pass that uses the luminosity of the grab pass to modulate the emissive brightness.

Another option is to use destination alpha. You can render the brightness into the alpha channel and then use a Blend OneMinusDstAlpha One blend mode. The benefit of this technique is it doesn’t require a grab pass which can be expensive. It won’t work well in HDR though.

Thanks for your suggestions. I will try to experiment by writing a two pass shader.

Dumb idea: what about setting it to render in one pass (perhaps as one pixel light and the others vertex/SH lights)?

The object seems small and I doubt it’s going to be dramatically affected by having additional lights approximated

I think you’d be possibly better off making a simple vert frag shader for this, there are many tutorials out there, and pass any “lights” manually from code (x,y,z,r,g,b) perhaps. This manual approach will allow you to fine-tune exactly how you’d like things and might be simpler (!) if you have very modest lighting requirements.

Unity can do this with out any additional scripting. If your shader does not have a ForwardAdd pass it will send the first 4 brightest point lights as vertex lights. Directional lights will be pre-baked into the SH. You can disable the forward add pass by including noforwardadd to the #pragma surface line and not using Fallback "Diffuse" (which will otherwise re-add the forward add pass). You can use Fallback "Legacy Shaders/VertexLit" instead so your shader still has shadows, or include addshadow in the #pragma surface line.

However there’s a problem. In a surface shader there’s no good way to get access to the vertex lighting that’s been calculated. No user-facing function in a surface shader ever has access to the “vlight” data calculated in the vertex shader. You could recalculate it in your surf or lighting functions, but that would still be missing the ambient lighting if that matters to you.

half3 vlight = Shade4PointLights (
    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
    unity_4LightAtten0, worldPos, worldNormal );

An alternative would be to use the finalColor function. You can use the final color value calculated by the shader and modify it. This would actually be similar / cheaper than the grabpass method I suggested above as it’s being done in one pass.

That inout color will have fog already applied, so be mindful of that if you’re using a dark fog color. If you need more control than this, or don’t want to calculate the vertex lighting twice, I would suggest learning to write this as a vertex fragment shader.

1 Like

After months of experimenting I still wasn’t able to find any simple solution. A two pass vertex shader I wrote can correctly work with either directional or spot/point lights separately, but not together. Maybe it can be done with four/five pass shader which will store data into additional render textures between passes, but that probably would be an overkill already for that kind of effect. The code below explains how to set up this effect and make it fully functional with both directional and point/spot lights.

The whole issue was in this one line of code, which works correctly for directional lights:

float intens = clamp(_LightColor0,0,1)
c.rgb = lerp(d.rgb, c.rgb, intens)

For spot and point lights it should be:

float intens = clamp(_LightColor0,0,1)+0.5
c.rgb = lerp(d.rgb, c.rgb, intens)/1.5;

I don’t have any logical explanation why it works this way, but it does. To make the shader take this difference into account I had to add an additional C# script that dynamically changes _IllumPower float value between 2 and 0 for directional light intensity between 0 and 1. Instead of doing it in Update it can be done in a coroutine, or by some trigger, depending on what it will be used for.

This shader can be used for various night-time effects and decorations, like glow-in-the-dark sticks, bracelets, etc. The main difference from the regular self-illuminating shader is that it responds dynamically to lights, and glowing effects (set through glow textures) disappear showing regular item without glow effect (as can be seen in the screenshot above). For instance, if your character has a glowing bracelet and walks under a pole light, the “glow” effect on the bracelet will decrease and disappear, and appear again when you walk away.

If somebody can write a shader-only solution, or at least explain why that +0.5 thing should be added for point/spot lights (which have exactly the same “1” intensity as directional light), I would appreciate that.

“Glow in the dark” shader:

Shader "Cel/GITD" {
    Properties {
        _Color ("Main Color", Color) = (0.5,0.5,0.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Illum ("Illumin (A)", 2D) = "white" {}
        _IllumPower ("Illum Power", Float) = 2
    }

    SubShader {
        Tags {"RenderType"="Opaque"}
        LOD 200
        Lighting On
        //Cull off
     
CGPROGRAM
#pragma surface surf ToonRamp fullforwardshadows

#pragma lighting ToonRamp exclude_path:prepass

sampler2D _MainTex;
sampler2D _Illum;
float4 _Color;
half _IllumPower;
half4 Custom;

struct EditorSurfaceOutput {
         half3 Albedo;
         half3 Normal;
         half3 Emission;
         half Alpha;
         half3 Custom;
       };

inline half4 LightingToonRamp (EditorSurfaceOutput s, half3 lightDir, half atten)
{
half4 d;
d.rgb = s.Custom * s.Albedo * _IllumPower * (atten*2);
half4 c;
c.rgb = s.Albedo * _LightColor0.rgb * (atten*2);

float intens = clamp(_LightColor0,0,1)+0.5;
c.rgb = lerp(d.rgb, c.rgb, intens)/1.5;
return c;
}

struct Input {
    float2 uv_MainTex : TEXCOORD0;
    float2 uv_Illum;
};

void surf (Input IN, inout EditorSurfaceOutput o) {
    half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    o.Custom = tex2D(_Illum, IN.uv_Illum).a;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}

ENDCG

    }

    Fallback "Diffuse"
}

Script for directional light adjustment:

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

//[ExecuteInEditMode]
public class cs_gitd : MonoBehaviour {

public GameObject dir_light;
Light lk;
float lk_intens;
Renderer rend;

// Use this for initialization
void OnEnable () {
dir_light = GameObject.Find("dir_light");
lk = dir_light.GetComponent<Light>();
rend = GetComponent<Renderer>();
rend.sharedMaterial.shader = Shader.Find("Cel/GITD");
}
 
    // Update is called once per frame
    void Update () {
     
lk_intens = lk.intensity;
rend.sharedMaterial.SetFloat("_IllumPower", (1-lk_intens)*2);

    }
}