0
\$\begingroup\$

I have a stone texture that looks great, except for one thing: it's very dark and I want white stone.

I don't want to mess with the texture, because it's used for other things in the same project. What I'd like to do is invert the brightness in a shader. Specifically, I want a custom shader that does everything exactly the same way as the Standard PBR shader, except that it inverts brightness.

By "inverting brightness", I mean:

  • take albedo.RGB, convert to HSV
  • V of HSV = 1 - V
  • Convert back to RGB

I tried making a trivial custom shader that does exactly this, but it has two problems:

  1. The result is completely flat, because the custom shader template didn't include the normal map and the height map.
  2. The brightness didn't actually get inverted!

Not sure what's going on with point #2, but when I tried adding the additional textures to the custom shader, I discovered that the SurfaceOutputStandard struct doesn't have a field for outputting height map data to!

Does anyone know how to make this work? Here's what I've got so far:

Shader "Custom/Bright Stone"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _NormalTex ("Normal Map", 2D) = "white" {}
        _HeightTex("Height Map", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _NormalTex;
        sampler2D _HeightTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

            float3 rgb_to_hsv_no_clip(float3 RGB)
        {
            float3 HSV;

            float minChannel, maxChannel;
            if (RGB.x > RGB.y) {
                maxChannel = RGB.x;
                minChannel = RGB.y;
            }
            else {
                maxChannel = RGB.y;
                minChannel = RGB.x;
            }

            if (RGB.z > maxChannel) maxChannel = RGB.z;
            if (RGB.z < minChannel) minChannel = RGB.z;

            HSV.xy = 0;
            HSV.z = maxChannel;
            float delta = maxChannel - minChannel;             //Delta RGB value
            if (delta != 0) {                    // If gray, leave H  S at zero
                HSV.y = delta / HSV.z;
                float3 delRGB;
                delRGB = (HSV.zzz - RGB + 3 * delta) / (6.0*delta);
                if (RGB.x == HSV.z) HSV.x = delRGB.z - delRGB.y;
                else if (RGB.y == HSV.z) HSV.x = (1.0 / 3.0) + delRGB.x - delRGB.z;
                else if (RGB.z == HSV.z) HSV.x = (2.0 / 3.0) + delRGB.y - delRGB.x;
            }
            return (HSV);
        }

        float3 hsv_to_rgb(float3 HSV)
        {
            float3 RGB = HSV.z;

            float var_h = HSV.x * 6;
            float var_i = floor(var_h);   // Or ... var_i = floor( var_h )
            float var_1 = HSV.z * (1.0 - HSV.y);
            float var_2 = HSV.z * (1.0 - HSV.y * (var_h - var_i));
            float var_3 = HSV.z * (1.0 - HSV.y * (1 - (var_h - var_i)));
            if (var_i == 0) { RGB = float3(HSV.z, var_3, var_1); }
            else if (var_i == 1) { RGB = float3(var_2, HSV.z, var_1); }
            else if (var_i == 2) { RGB = float3(var_1, HSV.z, var_3); }
            else if (var_i == 3) { RGB = float3(var_1, var_2, HSV.z); }
            else if (var_i == 4) { RGB = float3(var_3, var_1, HSV.z); }
            else { RGB = float3(HSV.z, var_1, var_2); }

            return (RGB);
        }

        float3 InvertBrightness(float3 albedo)
        {
            float3 hsv = rgb_to_hsv_no_clip(albedo);
            hsv.z = 1.0 - hsv.z;
            return hsv_to_rgb(hsv);
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = InvertBrightness(c.rgb);
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
            o.Normal = tex2D(_NormalTex, IN.uv_MainTex);
            // o.Height = tex2D(_HeightTex, IN.uv_MainTex);
        }
        ENDCG
    }
    FallBack "Diffuse"
}

If I'm going about this completely wrong and there's a better way to do it, feel free to point that out as well.

\$\endgroup\$
1
\$\begingroup\$

I think the conversion to HSL and back is wasteful if you have no intention of modifying H or S, only the L. ;) So instead I've used a simpler trick below to keep the chromaticity while flipping dark to light and vice versa.

I've used a simple & cheap parallax offset that's built into Unity to make use of the heightmap. You can also implement parallax occlusion mapping instead for a stronger sense of depth, at correspondingly greater expense.

Shader "Custom/LightnessInverter"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _BumpMap("Normal Map", 2D) = "bump" {}
        [MaterialToggle] _Invert("Invert Brightness", Float) = 0
        _ParallaxMap("Height Map", 2D) = "white" {}
        _Parallax("Height Scale", float) = 0.05
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _BumpMap;
        sampler2D _ParallaxMap;


        struct Input
        {
            float2 uv_MainTex;
            float3 viewDir;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        float _Invert;
        float _Parallax;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            IN.uv_MainTex += ParallaxOffset(tex2D(_ParallaxMap, IN.uv_MainTex).r, _Parallax, IN.viewDir);
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);             

            if (_Invert > 0.0f) {
                float brightest = max(c.r, max(c.g, c.b));
                if (brightest < 0.0001f) {
                    c.rgb = 1.0f;
                }
                else {
                    float3 peakVersion = c.rgb / brightest;
                    c.rgb = (1.0f - brightest) * peakVersion;
                }               
            }

            c *= _Color;
            o.Albedo = c.rgb;

            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;

            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
        }
        ENDCG
    }
    FallBack "Diffuse"
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks, that helps a lot! \$\endgroup\$ Apr 2 '20 at 19:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.