Unity 3 + Support for specular textures

Hi,

I have been doing a lot of shader work in unity3 beta, and am really enjoying it but some of the artists I have been working with are getting a bit annoyed. The reason is that in unity 3 (even with custom shaders) there is no way to output a specular color to the surface descriptor. The output structure only takes a specular power and a specular brightness (specular / gloss respectively).

I believe the reason for this is that the output structure is basically 3 half4’s and these go into the G-Buffer for deferred rendering.

struct SurfaceOutput {
	half3 Albedo;
	half3 Normal;
	half3 Emission;
	half Specular;
	half Gloss;
	half Alpha;
};

Because of G-Buffer not being fat enough there does not seem to be an easy way to get per pixel specular color into the final render output. And looking at the shaders that come with Unity3 it seems that per material specular color is kind of a hack at the moment (hard coded as float4 _SpecColor).

There are two ways that this could be fixed:

  1. Add an extra half4 channel added to the surface output structure. This could be a custom channel, that can be used in the lighting shader at the discretion of the shader programmer.
  2. Allow the emission channel to be used in this way for custom lighting, and provide a flag on the shader that tells unity internals to not perform the emissive pass when rending the material, then in the lighting passes the emissive value can be used by the custom lighting shader.

I can understand why you wouldn’t want to make the g-buffer fatter, but having a bit more customization on what the output channels are used for would be very nice :smile:

Unity 3 uses deferred lighting, not deferred shading. Specular colour is not stored in a G buffer. What happens is:

  1. Pre-pass G buffer(s) are filled with per-pixel depth, normal, layer and specular power. I believe MRTs are not used, so it’s just the standard depth buffer and something like Normal (RG) Power (B) Layer (A).

  2. Irradiance is computed and stored in a lighting buffer, Diffuse (RGB) Specular (A). Note that the one channel for specular is a limitation of light colour, not surface colour: lights can’t have different diffuse and specular colours, which they couldn’t have in Unity 2 either.

  3. Shading pass takes incoming light values and uses them with the remaining surface properties. Among other things, diffuse surface colour is applied. You could also apply a specular surface colour here.

The following shader works with both forward and deferred rendering in Unity 3:

Shader "Specular Colored" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_GlossTex ("Specular (RGB) Shininess (A)", 2D) = "gray" {}
	}
	SubShader {
		Tags { "RenderType" = "Opaque" }
		CGPROGRAM
			//use custom lighting functions
			#pragma surface surf BlinnPhongColor
			
			//custom surface output structure
			struct SurfaceOutputSpecColor {
				half3 Albedo;
				half3 Normal;
				half3 Emission;
				half Specular;
				half3 GlossColor; //Gloss is now three-channel
				half Alpha;
			};
			
			//forward lighting function
			inline half4 LightingBlinnPhongColor (SurfaceOutputSpecColor s, half3 lightDir, half3 viewDir, half atten) {
				#ifndef USING_DIRECTIONAL_LIGHT
				lightDir = normalize(lightDir);
				#endif
				viewDir = normalize(viewDir);
				half3 h = normalize (lightDir + viewDir);
				
				half diff = max (0, dot (s.Normal, lightDir));
				
				float nh = max (0, dot (s.Normal, h));
				float spec = pow (nh, s.Specular*128.0);
				
				half4 c;
				//Use gloss colour instead of gloss
				c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * s.GlossColor * spec) * (atten * 2);
				//We use gloss luminance to determine its overbright contribution
				c.a = s.Alpha + _LightColor0.a * Luminance(s.GlossColor) * spec * atten;
				return c;
			}
			
			//deferred lighting function
			inline half4 LightingBlinnPhongColor_PrePass (SurfaceOutputSpecColor s, half4 light) {
				//Use gloss colour instead of gloss
				half3 spec = light.a * s.GlossColor;
				
				half4 c;
				c.rgb = (s.Albedo * light.rgb + light.rgb * spec.rgb);
				//We use gloss luminance to determine its overbright contribution
				c.a = s.Alpha + Luminance(spec);
				return c;
			}
			
			sampler2D _MainTex;
			sampler2D _GlossTex;
			float4 _Color;
			
			struct Input {
				float2 uv_MainTex;
				float2 uv_GlossTex;
			};
			
			void surf (Input IN, inout SurfaceOutputSpecColor o) {
				half4 tex = tex2D(_MainTex, IN.uv_MainTex);
				half4 gloss = tex2D(_GlossTex, IN.uv_GlossTex);
				o.Albedo = tex.rgb * _Color.rgb;
				//Gloss colour come from RGB
				o.GlossColor = gloss.rgb;
				o.Alpha = tex.a * _Color.a;
				//Specular is mapped 
				o.Specular = gloss.a;
			}
		ENDCG
	}
	Fallback "Diffuse"
}

Deferred rendering and shading are the same thing :stuck_out_tongue:

This is different to how i have seen g-buffers done before, normally they describe all the surface properties. ie. surface color, surface specular color, spec power, normal emission ect.

I am nor referring to the specular color of the lights, but the specular color of the surface. It is possible for a surface’s specular reflection to be different from it’s diffuse color.

Looking at your example it is possible to add additional outputs to the surface output structure. After looking through the documentation I could not find any references to this. I had assumed that this was a hardcoded g-buffer description.

Actually, I just did some testing, it seems that the maximum size of the output structure is 6 half4’s. To me this implies that this structure is packed as the G-Buffer, as it needs to be used by the lighting passes.

I believe this is happening:

Pass 1: Perform the surface function. Pack all the values into the output structure as defined by the user. This is put into a G-Buffer that is packed / configured by unity based on how you define with a maximum size of 6 half4’s.

Pass 2: For each light sample the G-Buffer and render into the framebuffer using the defined lighting algorithm. The surface structure will have been sampled back from the g-buffer into a usable surface structure.

My issue was that I did not know it was possible to define a custom output structure! It still looks like there is some special case handling in there for emissive though. It would be nice if this could be exposed in a Lighting_NoLight pass so you could define how you wanted to do the ambient / base pass.

Disregard my last post, you are talking about a Light Prepass renderer. And now it all makes sense.

I’m silly and wrong. Thank you for taking the time to help me out.

I’m glad it all makes sense. Deferred lighting vs. shading is a subtle name difference for two fairly different types of deferred rendering. One advantage deferred lighting has is that lets you have more complex material definitions, as long as they don’t change the lighting calculations. If Unity 3 used deferred shading and didn’t support specular colour, you’d be out of luck.

Not if the materials and their output were properly abstracted. Then the engine could plug in the proper inputs and outputs to the shaders for surface creation and lighting. For instance, most would prioritize emission lower than specular, and push emission to a unique pass when needed (since very few surfaces emit light, in normal content), but if doing a tron like game, they might be happier dropping unique specular in favor of faster emission color. With a proper framework, you’d be hard pressed to come up with notable limitations of a deferred shader.

That would be cool, but I asked Aras and it’s not happening for Unity 3. I don’t know enough about graphics engine creation to tell if it’s a ridiculous request or not. Completely custom lighting models (not just deciding how to interpret incoming Blinn-Phong irradiance values) would be a lot of fun.

Oh, it’s not ridiculous at all, unless you’re hand coding freaking everything :evil:. I’m actually pretty disappointed looking at these shaders, I was all excited about Unity 3 having a deferred shader, now I realize it’ll be pointless to try and move my terrain shader I was planning on from my engine, as it’d be too expensive in Unity. And it’s looking like to create unique lighting we’re still stuck replacing every shader in use. This honestly just seems like an excuse to have the deferred keyword in their list of features. Hopes dashed, sad and disappointed, I am. Sorry for thread hijack though, I’ll go cry alone now :(.

Works great, I have extended it with a normal map and shininess setting:

Shader "Specular Colored" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_GlossTex ("Specular (RGB) Shininess (A)", 2D) = "gray" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}
		_Shininess ("Shininess", Range (0, 10)) = 1
	}
	SubShader {
		Tags { "RenderType" = "Opaque" }
		CGPROGRAM
			//use custom lighting functions
			#pragma surface surf BlinnPhongColor
			
			
			//custom surface output structure
			struct SurfaceOutputSpecColor {
				half3 Albedo;
				half3 Normal;
				half3 Emission;
				half Specular;
				half3 GlossColor; //Gloss is now three-channel
				half Alpha;
				
				
			};
			
			//forward lighting function
			inline half4 LightingBlinnPhongColor (SurfaceOutputSpecColor s, half3 lightDir, half3 viewDir, half atten) {
				#ifndef USING_DIRECTIONAL_LIGHT
				lightDir = normalize(lightDir);
				#endif
				viewDir = normalize(viewDir);
				half3 h = normalize (lightDir + viewDir);
				
				half diff = max (0, dot (s.Normal, lightDir));
				
				float nh = max (0, dot (s.Normal, h));
				float spec = pow (nh, s.Specular*128.0);
				
				half4 c;
				//Use gloss colour instead of gloss
				c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * s.GlossColor * spec) * (atten * 2);
				//We use gloss luminance to determine its overbright contribution
				c.a = s.Alpha + _LightColor0.a * Luminance(s.GlossColor) * spec * atten;
				return c;
			}
			
			//deferred lighting function
			inline half4 LightingBlinnPhongColor_PrePass (SurfaceOutputSpecColor s, half4 light) {
				//Use gloss colour instead of gloss
				half3 spec = light.a * s.GlossColor;
				
				half4 c;
				c.rgb = (s.Albedo * light.rgb + light.rgb * spec.rgb);
				//We use gloss luminance to determine its overbright contribution
				c.a = s.Alpha + Luminance(spec);
				return c;
			}
			
			sampler2D _MainTex;
			sampler2D _GlossTex;
			sampler2D _BumpMap;
			float4 _Color;
			float _Shininess;
			
			struct Input {
				float2 uv_MainTex;
				float2 uv_GlossTex;
				float2 uv_BumpMap;

			};
			
			void surf (Input IN, inout SurfaceOutputSpecColor o) {
				half4 tex = tex2D(_MainTex, IN.uv_MainTex);
				half4 gloss = tex2D(_GlossTex, IN.uv_GlossTex);
				o.Albedo = tex.rgb * _Color.rgb;
				//Gloss colour come from RGB
				o.GlossColor = _Shininess * gloss.rgb;
				o.Alpha = tex.a * _Color.a;
				//Specular is mapped 
				o.Specular = gloss.a;
				o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
			}
		ENDCG
	}
	Fallback "Diffuse"
}

I have produced a streamlined much faster version that seems to look just as good as the original:

Shader "Specular Colored fast" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_GlossTex ("Specular (RGB) Shininess (A)", 2D) = "gray" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}
		_Shininess ("Shininess", Range (0, 10)) = 1
	}
	SubShader {
		Tags { "RenderType" = "Opaque" }
		CGPROGRAM
			//use custom lighting functions
			#pragma surface surf BlinnPhongColor
			
			
			//custom surface output structure
			struct SurfaceOutputSpecColor {
				half3 Albedo;
				half3 Normal;
				half3 Emission;
				half Specular;
				half3 GlossColor; //Gloss is now three-channel
				half Alpha;
				
				
			};
			
			//forward lighting function
			inline half4 LightingBlinnPhongColor (SurfaceOutputSpecColor s, half3 lightDir, half3 viewDir, half atten) {
				
				half3 h = normalize (lightDir + viewDir);
				
				half diff = max (0, dot (s.Normal, lightDir));
				
				float nh = max (0, dot (s.Normal, h));
				float spec = pow (nh, s.Specular*128.0);
				
				half4 c;
				//Use gloss colour instead of gloss
				c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * s.GlossColor * spec) * (atten * 2);
				//We use gloss luminance to determine its overbright contribution
				return c;
			}
			
		
			sampler2D _MainTex;
			sampler2D _GlossTex;
			sampler2D _BumpMap;
			float _Shininess;
			
			struct Input {
				float2 uv_MainTex;
			};
			
			void surf (Input IN, inout SurfaceOutputSpecColor o) {
				half4 tex = tex2D(_MainTex, IN.uv_MainTex);
				half4 gloss = tex2D(_GlossTex, IN.uv_MainTex);
				o.Albedo = tex.rgb;
				//Gloss colour come from RGB
				o.GlossColor = _Shininess * gloss.rgb;
				//Specular is mapped 
				o.Specular = gloss.a;
				o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
			}
		ENDCG
	}
	Fallback "Diffuse"
}

michaelvoigt, the shader isn’t working properly. I get a bunch of errors such as:

Shader error in 'Specular Colored fast': D3D shader assembly failed with: (49): error X5204: Read of uninitialized component(*) in r0: r/x/0 g/y/1 b/z/2 *a/w/3

Shader Assembly: ps_2_0
; 34 ALU, 4 TEX
dcl_2d s0
dcl_2d s1
dcl_2d s2
dcl_2d s3
def c2, 2.00000000, -1.00000000, 1.00000000, 0.00000000
def c3, 128.00000000, 0, 0, 0
dcl t0.xy
dcl t1.xyz
dcl t2.xyz
dcl t3.xyz
dcl t4
texld r1, t0, s2
texld r3, t0, s0
texldp r6, t4, s3
texld r2, t0, s1
dp3_pp r1.x, t1, t1
mul r2.xyz, r2, c1.x
mov r0.y, r1
mov r0.x, r1.w
mad_pp r4.xy, r0, c2.x, c2.y
mul_pp r0.x, r4.y, r4.y
mad_pp r0.x, -r4, r4, -r0
add_pp r0.x, r0, c2.z
rsq_pp r0.x, r0.x
rcp_pp r4.z, r0.x
rsq_pp r1.x, r1.x
mov_pp r5.xyz, t2
mad_pp r5.xyz, r1.x, t1, r5
dp3_pp r1.x, r5, r5
rsq_pp r1.x, r1.x
mul_pp r0.xyz, r1.x, r5
dp3_pp r1.x, r4, r0
mul_pp r0.x, r2.w, c3
max_pp r1.x, r1, c2.w
pow r5, r1.x, r0.x
dp3_pp r1.x, r4, t2
mov r0.x, r5
mul r2.xyz, r2, c0
mul r2.xyz, r2, r0.x
max_pp r1.x, r1, c2.w
mul r4.xyz, r3, c0
mad r1.xyz, r4, r1.x, r2
mul_pp r0.x, r6, c2
mul r0.xyz, r1, r0.x
mul r1.xyz, r3, t3
add_pp r0.xyz, r0, r1
mov_pp oC0, r0

This should come bundled in the default shaders

You could define your own lighting function without the prepass part, give it a different SurfaceOutput struct and put the specular color inside. However this would force unity to render these materials in forward rendering, causing them to be slow and sometimes also more ugly. but ist possible…

Edit: Im sorry, didn’t realize this topic is quite old.