Skip to content

Instantly share code, notes, and snippets.

@Hotrian
Last active January 5, 2025 02:39
Show Gist options
  • Save Hotrian/a99c5f260134df71bb70f0b1a977509c to your computer and use it in GitHub Desktop.
Save Hotrian/a99c5f260134df71bb70f0b1a977509c to your computer and use it in GitHub Desktop.

Version 2 - now with LayerMask support

Automatic texture desaturation with Unity Light System based color reveal - Textures are converted in realtime to grayscale and Point/Directional/Spot lights reveal the original color of a texture. This is just a quick and dirty example.

Made for URP (3d) and tested in Unity 6000.0.30f1

img

img

img

Quick Example Vid on YouTube

More details in this comment thread probably

Shader "Custom/ColorByLight"
{
Properties
{
_PackedLayerMask ("Packed Layer Mask", Float) = 0
_BaseIsMultiplicative ("Base Color Is Multiplicative (1.0 == true)", Range(0, 1)) = 1.0
[HDR] _BaseColor ("Color", Color) = (1,1,1,1)
_BaseMap ("Texture", 2D) = "white" {}
_AmbientIsMultiplicative ("Ambient Is Multiplicative (1.0 == true)", Range(0, 1)) = 0.0
_AmbientMultLuminosity ("Ambient Light Mult (Luminosity)", Range(0, 2)) = 1.0
_AmbientMultColor ("Ambient Light Mult (Color)", Range(0, 2)) = 0.0
_SunIsMultiplicative ("Sun Is Multiplicative (1.0 == true)", Range(0, 1)) = 1.0
_SunMultReveal ("Sun Mult (Reveal)", Range(0, 2)) = 0
_SunMultColor ("Sun Mult (Color)", Range(0, 2)) = 1
_LightMultReveal ("Light Mult (Reveal)", Range(0, 200)) = 100
_LightMultColor ("Light Mult (Color)", Range(0, 2)) = 0
_ClampIntensityMax ("Clamp Intensity (Max)", Range(0, 2)) = 1
}
SubShader
{
Tags { "RenderPipeline" = "UniversalRenderPipeline" }
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float3 worldPosition : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float2 uv : TEXCOORD2;
};
int _PackedLayerMask; // Integer storing the layer mask as bits
bool IsLayerEnabled(int layerIndex)
{
return (_PackedLayerMask & (1 << layerIndex)) != 0;
}
float _LayerMask[32];
float4 _LightData[16];
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
float4 _BaseColor;
float _BaseIsMultiplicative;
float _AmbientIsMultiplicative;
float _AmbientMultLuminosity;
float _AmbientMultColor;
float _SunIsMultiplicative;
float _SunMultReveal;
float _SunMultColor;
float _LightMultReveal;
float _LightMultColor;
float _ClampIntensityMax;
Varyings vert(Attributes IN)
{
// Here we just convert World Space stuff to Object/Texture Space
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS);
OUT.worldPosition = TransformObjectToWorld(IN.positionOS);
OUT.worldNormal = TransformObjectToWorldNormal(IN.normalOS);
OUT.uv = IN.uv;
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
float3 normalWS = normalize(IN.worldNormal);
float4 sampledColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
// Get the Main Light (usually the Sun / first Directional Light which casts shadows)
Light mainLight = GetMainLight();
float mainLightNdotL = max(dot(normalWS, mainLight.direction), 0.0);
float3 mainLightDiffuse = mainLight.color * mainLightNdotL;
float mainLightIntensity = dot(mainLight.color, float3(0.299, 0.587, 0.114)) * mainLightNdotL;
// Calculate the effective Intensity
float intensity = (mainLightIntensity * _SunMultReveal);
float3 lightDiffuse = float3(0, 0, 0);
for (int i = 0; i < GetAdditionalLightsCount(); i++)
{
if (IsLayerEnabled((int)_LightData[i].x))
{
Light additionalLight = GetAdditionalPerObjectLight(i, IN.worldPosition);
float NdotL = max(dot(normalWS, additionalLight.direction), 0.0); // Calculate N·L (Lambertian shading)
lightDiffuse += additionalLight.color * NdotL * additionalLight.distanceAttenuation;
float lightIntensity = dot(additionalLight.color, float3(0.299, 0.587, 0.114)) * NdotL * additionalLight.distanceAttenuation;
if (lightIntensity <= 0.0) continue;
intensity += (lightIntensity * _LightMultReveal);
}
}
intensity = clamp(intensity, 0, _ClampIntensityMax);
// Calculate the Ambient Lighting, and use its Luminosity as the base brightness
float4 ambientLight = float4(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w, 1);
float ambientLuminosity = saturate(dot(ambientLight.rgb, float3(0.2126, 0.7152, 0.0722))) * _AmbientMultLuminosity;
// Calculate the Grayscale color (Luminosity method) and interpolate between that and the full color based on the effective light Intensity
float gsColor = dot(sampledColor.rgb, half3(0.299, 0.587, 0.114)) * ambientLuminosity;
float r = lerp(gsColor, sampledColor.r * ambientLuminosity, intensity);
float g = lerp(gsColor, sampledColor.g * ambientLuminosity, intensity);
float b = lerp(gsColor, sampledColor.b * ambientLuminosity, intensity);
// Multiplicative Sun looks better IMO, but depends on game style, lighting, etc
if (_BaseIsMultiplicative >= 1)
{
if (_SunIsMultiplicative >= 1)
{
if (_AmbientIsMultiplicative >= 1)
{
// Base Color Multiplicative, Ambient Multiplicative, Sun Multiplicative
return (float4(r, g, b, 1) * _BaseColor * float4(mainLightDiffuse, 1) * _SunMultColor * ambientLight * _AmbientMultColor) + (float4(lightDiffuse, 1) * _LightMultColor);
}
// Base Color Multiplicative, Ambient Additive, Sun Multiplicative
return (float4(r, g, b, 1) * _BaseColor * float4(mainLightDiffuse, 1) * _SunMultColor) + (float4(lightDiffuse, 1) * _LightMultColor) + (ambientLight * _AmbientMultColor);
}
// Base Color Multiplicative, Ambient Additive, Sun Additive
return (float4(r, g, b, 1) * _BaseColor) + (float4(mainLightDiffuse, 1) * _SunMultColor) + (float4(lightDiffuse, 1) * _LightMultColor);
}
if (_SunIsMultiplicative >= 1)
{
if (_AmbientIsMultiplicative >= 1)
{
// Base Color Additive, Ambient Multiplicative, Sun Multiplicative
return (float4(r, g, b, 1) * float4(mainLightDiffuse, 1) * _SunMultColor * ambientLight * _AmbientMultColor) + (float4(lightDiffuse, 1) * _LightMultColor) + _BaseColor;
}
// Base Color Additive, Ambient Additive, Sun Multiplicative
return (float4(r, g, b, 1) * float4(mainLightDiffuse, 1) * _SunMultColor) + (float4(lightDiffuse, 1) * _LightMultColor) + (ambientLight * _AmbientMultColor) + _BaseColor;
}
// Base Color Additive, Ambient Additive, Sun Additive
return (float4(r, g, b, 1)) + (float4(mainLightDiffuse, 1) * _SunMultColor) + (float4(lightDiffuse, 1) * _LightMultColor) + _BaseColor;
}
ENDHLSL
}
}
FallBack "Lit"
CustomEditor "ColorByLightInspector"
}
using UnityEditor;
using UnityEngine;
public class ColorByLightInspector : ShaderGUI
{
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
// Iterate over all properties
foreach (var property in properties)
{
switch (property.name)
{
case "_PackedLayerMask": // Do not render
break;
case "_BaseIsMultiplicative":
{
// Display as a toggle
EditorGUI.BeginChangeCheck();
var toggleValue = EditorGUILayout.Toggle("Base Color Is Multiplicative", property.floatValue > 0.5f);
if (EditorGUI.EndChangeCheck())
{
property.floatValue = toggleValue ? 1.0f : 0.0f; // Set the property value based on the toggle
}
break;
}
case "_AmbientIsMultiplicative":
{
// Display as a toggle
EditorGUI.BeginChangeCheck();
var toggleValue = EditorGUILayout.Toggle("Ambient Is Multiplicative", property.floatValue > 0.5f);
if (EditorGUI.EndChangeCheck())
{
property.floatValue = toggleValue ? 1.0f : 0.0f; // Set the property value based on the toggle
}
break;
}
case "_SunIsMultiplicative":
{
// Display as a toggle
EditorGUI.BeginChangeCheck();
var toggleValue = EditorGUILayout.Toggle("Sun Is Multiplicative", property.floatValue > 0.5f);
if (EditorGUI.EndChangeCheck())
{
property.floatValue = toggleValue ? 1.0f : 0.0f; // Set the property value based on the toggle
}
break;
}
default:
// Render other properties normally
materialEditor.ShaderProperty(property, property.displayName);
break;
}
}
// Add Material Property Block Array for Layer Mask
AddLayerMaskEditor(materialEditor);
}
private void AddLayerMaskEditor(MaterialEditor materialEditor)
{
var targetMaterial = materialEditor.target as Material;
if (targetMaterial == null) return;
const int totalLayers = 32; // Unity supports 32 layers
var packedLayerMask = 0;
// Ensure the packed layer mask is initialized
if (targetMaterial.HasProperty("_PackedLayerMask"))
{
float packedValue = targetMaterial.GetFloat("_PackedLayerMask");
packedLayerMask = Mathf.RoundToInt(packedValue);
if (Mathf.Approximately(packedValue, 0))
{
// If the value is uninitialized (defaulted to 0), set it to max
packedLayerMask = -1;
targetMaterial.SetFloat("_PackedLayerMask", packedLayerMask);
EditorUtility.SetDirty(targetMaterial); // Mark material as dirty
AssetDatabase.SaveAssets(); // Save changes to disk
}
else
{
packedLayerMask = Mathf.RoundToInt(packedValue);
}
}
var updated = false;
// Render the layer mask editor
EditorGUILayout.LabelField("Layer Mask Settings", EditorStyles.boldLabel);
for (var i = 0; i < totalLayers; i++)
{
var layerName = LayerMask.LayerToName(i); // Get the name of the layer
if (string.IsNullOrEmpty(layerName))
{
layerName = $"Layer {i} (Unnamed)"; // Placeholder for unnamed layers
}
// Update each toggle immediately in the packed mask
EditorGUI.BeginChangeCheck();
var isChecked = (packedLayerMask & (1 << i)) != 0; // Check if the layer is enabled
var newIsChecked = EditorGUILayout.Toggle(layerName, isChecked);
if (!EditorGUI.EndChangeCheck()) continue;
if (newIsChecked)
{
packedLayerMask |= (1 << i); // Enable the layer
}
else
{
packedLayerMask &= ~(1 << i); // Disable the layer
}
updated = true;
}
if (!updated) return;
targetMaterial.SetFloat("_PackedLayerMask", packedLayerMask); // Update the packed mask in the material
EditorUtility.SetDirty(targetMaterial); // Mark the material as dirty
EditorApplication.delayCall += AssetDatabase.SaveAssets; // Save the changes
}
}
using UnityEditor;
using UnityEngine;
// This is a really lazy bootstrapper that automatically grabs the first 16 lights and injects them into the global shader cache
// For production you should be injecting target lights more efficiently, but for experimenting this is fine
[ExecuteInEditMode]
public class ColorByLightUpdater : MonoBehaviour
{
// Don't look at me
public static ColorByLightUpdater Instance => _instance ?? (FindAnyObjectByType<ColorByLightUpdater>() ?? new GameObject("ColorByLightUpdater").AddComponent<ColorByLightUpdater>());
private static ColorByLightUpdater _instance;
[InitializeOnLoadMethod]
static void CreateInstance()
{
Instance.transform.position = Vector3.zero;
}
void Awake()
{
Singleton();
_instance = this;
EditorApplication.hierarchyChanged += UpdateLightData;
}
void OnValidate() => Singleton();
void Singleton()
{
if (_instance == null || _instance == this) return;
Debug.LogWarning("Duplicate ColorByLightUpdater");
if (Application.isPlaying)
Destroy(gameObject);
else
DestroyImmediate(gameObject);
}
void OnDestroy()
{
EditorApplication.hierarchyChanged -= UpdateLightData;
if (_instance == this)
_instance = null;
}
void Start()
{
Singleton();
_instance = this;
UpdateLightData();
}
// Okay you can look now
public void UpdateLightData()
{
var lights = FindObjectsByType<Light>(FindObjectsSortMode.InstanceID);
var lightData = new Vector4[16]; // Assuming 16 lights max
for (var i = 0; i < lights.Length; i++)
{
lightData[i] = new Vector4(lights[i].gameObject.layer, 0, 0, 0); // Store layer as float
if (i >= 15) break;
}
Shader.SetGlobalVectorArray("_LightData", lightData);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment