Linear Color Space, Same Color supplied once as Vertex Color, once as a shader property, different results

While working with the SteamVR plugin, it recommended me to change the color space to linear, which I did as I obviously wanted to do what was recommended to me…

I have a script that generates a simple plane with all vertices set to some color supplied in the inspector.
On top of that, I have two shaders (Source attached at the end), one for rendering vertex colors, one for rendering a mesh in a color that is supplied through a property.

When I place two meshes in a scene, one with the vertex color shader and the other one with the other shader, they look exactly the same - as expected - BUT only as long as the color space is set to gamma. When it is put to linear, the two meshes have a completely different color…
If anyone could explain to me why this is the case, I would be very thankful :slight_smile:
I am very sorry if this question has an obvious answer, but I’m not very good with shaders yet.

The left quad is rendered with the single color shader, the right side with the vertex color shader

Gamma Color Space:

Linear Color Space:

Here are the shaders:

Vertex Color Shader

Shader "Unlit/VertexColorTest"
{
	Properties
	{

	}
	SubShader
	{
		Tags{ "RenderType" = "Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
				float4 color : COLOR;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float4 color : COLOR;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.color = v.color;
				return o;
			}


			float4 frag(v2f i) : SV_Target
			{
				return i.color;
			}
			ENDCG
		}
	}
}

Single Color Shader:

Shader "Unlit/ColorTest"
{
	Properties
	{
		_Color("Color", Color) = (1,1,1,1)
	}
	SubShader
	{
		Tags{ "RenderType" = "Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
			};

			float4 _Color;

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				return o;
			}

			float4 frag(v2f i) : SV_Target
			{
				return _Color;
			}
			ENDCG
		}
	}
}

The vertex colors are probably in gamma space. Use your shader to convert them to linear space, first.

I just ran into this issue with my procedurally generated vertex colored mesh. For the built-in pipeline there is a function available in shaders called GammaToLinearSpace. Its source code is available here: Unity-Built-in-Shaders/CGIncludes/UnityCG.cginc at master · TwoTailsGames/Unity-Built-in-Shaders

This particular function didn’t seem to be available to URP shaders, but it’s an easy enough thing to port yourself. What I ended up doing was use GammaToLinearSpaceExact on the C#/CPU side before sending the mesh to the GPU:

// Pass each channel of the color through this function
private static float GammaToLinearExact(float value) {
    if (value <= 0.04045F) {
        return value / 12.92F;
    } else if (value < 1.0F) {
        return Mathf.Pow((value + 0.055F) / 1.055F, 2.4F);
    } else {
        return Mathf.Pow(value, 2.2F);
    }
}

For whatever reason the Mathf.GammaToLinearSpace function and Color.linear property did not give me the correct results.

Here’s the shader code from the above link for reference:

inline float GammaToLinearSpaceExact (float value)
{
    if (value <= 0.04045F)
        return value / 12.92F;
    else if (value < 1.0F)
        return pow((value + 0.055F)/1.055F, 2.4F);
    else
        return pow(value, 2.2F);
}

inline half3 GammaToLinearSpace (half3 sRGB)
{
    // Approximate version from http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
    return sRGB * (sRGB * (sRGB * 0.305306011h + 0.682171111h) + 0.012522878h);

    // Precise version, useful for debugging.
    //return half3(GammaToLinearSpaceExact(sRGB.r), GammaToLinearSpaceExact(sRGB.g), GammaToLinearSpaceExact(sRGB.b));
}