How to write a uniform lit shader? Similar to unlit but with light sources affecting the color/brightness of the whole object?

So currently I have an unlit shader with shadows and a float value for shadow brightness. This works fine and all objects are uniformly shaded, so you don’t see any edges.

Now I would want to have that shader output be modified by light sources. Not turning into a real lit-shader where faces are shaded individually, but just have the overall object be brighter (and color tinted) when affected by a light-source.

I do have the fallback option to add a tint to the material and then manually at the start of the game go through all MeshRenderers and all Light Sources and apply the tint manually, but I’d rather do it with a shader.

My current shader looks like this:

Shader"UnlitWithShadows"
{
    Properties
    {
        _TextureMap ("Texture", 2D) = "" {}
        _TileAmount ("Scale Multiplier", float) = 1
        _ShadowBrightness ("Shadow Brightness", Range(0, 1)) = 0
    }
    SubShader
    {
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex VSMain
            #pragma fragment PSMain
            #pragma multi_compile_fwdbase
#include "AutoLight.cginc"
         
sampler2D _TextureMap;
float _TileAmount;
float _ShadowBrightness;
struct SHADERDATA
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    float4 _ShadowCoord : TEXCOORD1;
};
float4 ComputeScreenPos(float4 p)
{
    float4 o = p * 0.5;
    return float4(float2(o.x, o.y * _ProjectionParams.x) + o.w, p.zw);
}
SHADERDATA VSMain(float4 vertex : POSITION, float2 uv : TEXCOORD0)
{
    SHADERDATA vs;
    vs.position = UnityObjectToClipPos(vertex);
    vs.uv = uv;
    vs._ShadowCoord = ComputeScreenPos(vs.position);
    return vs;
}
float4 PSMain(SHADERDATA ps) : SV_TARGET
{
    float4 col = tex2D(_TextureMap, ps.uv * _TileAmount);
    return lerp(col * float4(_ShadowBrightness, _ShadowBrightness, _ShadowBrightness, 1), col, step(0.5, SHADOW_ATTENUATION(ps)));
}
         
            ENDCG
        }
     
        Pass
        {
            Tags{ "LightMode" = "ShadowCaster" }
            CGPROGRAM
            #pragma vertex VSMain
            #pragma fragment PSMain
float4 VSMain(float4 vertex : POSITION) : SV_POSITION
{
    return UnityObjectToClipPos(vertex);
}
float4 PSMain(float4 vertex : SV_POSITION) : SV_TARGET
{
    return 0;
}
         
            ENDCG
        }
    }
}

Okay, so you want your object to be equally influenced by lights on all pixels.

The basics in pseudocode:
Light is additive which means the light from two sources is added up, so if you have a directional light and additional lights its like

light = directional * distance * shadows
foreach(additional)
{
light + additional * distance * shadows
}

The lights distance attenuation and shadows and calculated for the position of the pixel that is calculated in the fragment shader. Here’s your first change: you want all pixels of an object to have the same brightness, then you need to sample the same point for all pixels, like the center of the object.

You still need to write a loop that sums up the value of all additional lights.

The shader looks like built in pipeline, which i know almost nothing about, so i can’t tell you more about how to build a custom lighting solution for that.

That’s pretty easy to achieve (but will have some draw backs), just remove the normal vector inside the lighting calculation. This has been used in soome anime-style game.
For example - Blue Protocol

So a normal lighting model usually looks like this

//basic lambet lighting model, light attenuation is a falloff based on the distance to current pixel in world space.
float3 result = saturate(dot(normalWS, lightDirectionWS)) * light.attenuation * lightColor * albedo.rgb;

By removing the parts involves normal, every pixel will now lit by the distance to the light source.

//basic lambet lighting model, light attenuation is a falloff based on the distance to current pixel in world space.
float3 result = light.attenuation * lightColor * albedo.rgb;

You can then apply these logic to additional light source only.
The main draw back of this approach is that it will be lit purely based on the distacance. Even a light source behind the object will make the object brighter as long as it’s within the lighting range.

That’s close but not exactly what I want to achieve. I don’t want to have different light-intensities on a face-basis, but on the whole object basis. So, as an example, for this shader I do have the “atten” value, but when I multiply that into the color, then individual faces of the object will be lit differently. If I remove the atten value then every face is lit the same, but now the distance to a light source is completely ignored.

Shader "Custom/UniformLit" {
        Properties {
            _MainTex ("Texture", 2D) = "white" {}
        }
        SubShader {
        Tags { "RenderType" = "Opaque" }
        CGPROGRAM
          #pragma surface surf SimpleLambert
  
          half4 LightingSimpleLambert (SurfaceOutput s, half3 lightDir, half atten) {
              half NdotL = lightDir;
              half4 c;
              c.rgb = s.Albedo;
              c.rgb +=  50 * _LightColor0 * s.Albedo * atten;
              c.a = s.Alpha;
              return c;
          }
  
        struct Input {
            float2 uv_MainTex;
        };
        
        sampler2D _MainTex;
        
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.02;
        }
        ENDCG
        }
        Fallback "Diffuse"
    }

ChatGPT proposed to have a C# script calculate the distances of the object to each light source and set it as a float matrix for the shader. That might work… but then I limit the amount of light I can use as I have to predefine them for the matrix size.

Ok I did get it to work, with inputing the object center position via a C# script and then calculating the distance of the GameObject to the light sources. Looks really bad though, I’ll have to try something else. Still thanks for the ideas. ^^

Shader "Custom/UniformLitWithCenter"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
        _ObjectCenter ("Object Center Position", Vector) = (0, 0, 0, 0)
        _ShadowBrightness ("Shadow Brightness", Range(0, 1)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            Name "FORWARD"
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                LIGHTING_COORDS(3, 4)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float3 _ObjectCenter;
            float _ShadowBrightness;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.worldNormal = normalize(mul((float3x3)unity_ObjectToWorld, v.normal));
                TRANSFER_VERTEX_TO_FRAGMENT(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // Sample the texture
                fixed4 texColor = tex2D(_MainTex, i.uv) * _Color;

                // Initialize the lighting accumulation
                fixed3 lightingAccum = fixed3(0, 0, 0);

                // Iterate over the first 4 lights
                for (int lightIndex = 0; lightIndex < 4; lightIndex++)
                {
                    float3 lightPos = float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x);
                    float distance = length(lightPos - _ObjectCenter);
                    float atten = 1.0 / (distance * distance);

                    lightingAccum += unity_LightColor[lightIndex].rgb * atten; //
                }

                // Final color calculation
                fixed4 finalColor = texColor * fixed4(lightingAccum, 1.0);
                finalColor = lerp(finalColor * float4(_ShadowBrightness, _ShadowBrightness, _ShadowBrightness, 1), finalColor, step(0.5, SHADOW_ATTENUATION(i)));
                return finalColor;
            }
            ENDCG
           
        }

        Pass
        {
            Tags { "LightMode" = "ShadowCaster" }
            CGPROGRAM
            #pragma vertex VSMain
            #pragma fragment PSMain
            float4 VSMain(float4 vertex : POSITION) : SV_POSITION
            {
                return UnityObjectToClipPos(vertex);
            }
            float4 PSMain(float4 vertex : SV_POSITION) : SV_TARGET
            {
                return 0;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}