Lightmaps + directional pixel light + vertex lights, all in one pass?

Hi,

I’m fairly new to Unity, and currently investigating lighting options for (higher-end) mobile devices. It’s clear that lightmaps and lightprobes work really nicely, but I’m looking for a way to add some normal-mapped specular, from a single directional light, in a single pass.

I’ve started looking into what’s possible with surface shaders. I took the ‘Bumped Specular (1 Directional Light)’ shader, with the intention of making it work with lightmapped materials, but it seems that this isn’t straightforward. Removing the ‘nolightmap’ pragma makes it work, but only if I also remove the ‘noforwardadd’, too - It seems to be doing the lightmaps and directional light in two separate passes.

Is there a quick/simple solution for that? - maybe using ‘nolightmap’ but then sampling the lightmap myself in the shader?

Or do I need to move to completely custom shaders, rather than just surface shaders? If I do that though, am I able to interact enough with Unity’s lighting system to get the light parameters?

(Longer-term, I’d like to support per-vertex dynamic lights, if it’s possible. So far I’ve not been able to get Unity to do any vertex lighting, if I turn Pixel Light Count down to 0, I just get no dynamic lighting. Is vertex lighting still supported?)

What you’ve described is exactly what the first pass of a regular surface shader does in the forward renderer: Unity - Manual: Surface Shaders and rendering paths

If you set your pixel light limit to zero, then you should get what you want from the built-in Bumped Specular shader. It’s zero rather than one because you get the first directional light for “free” as part of the base pass.

From reading the docs, I was expecting to get the directional light in the base pass. But that doesn’t seem to actually be happening if lightmaps are enabled - as soon as there’s lightmaps, it appears to become two passes :frowning:

I’m still experimenting with the ‘Mobile/Bumped Specular (1 Directional Light)’ shader.

Without lightmaps:

  • If I set the pixel light count to 0, the directional light is still there
  • If I add ‘#pragma noforwardadd’ to the the shader, the directional light is still there

With lightmaps (by removing ‘nolightmap’ from the shader):

  • If I set the pixel light count to 0, the directional light disappears
  • If I add ‘#pragma noforwardadd’ to the the shader, the directional light disappears

Also, vertex lights don’t appear to illuminate anything which is lightmapped, at all (testing that with the regular ‘Diffuse’ shader)

So it seems that everything works just great… until you use lightmaps. So if we can manually apply the lightmap in a surface shader, we might get one-pass goodness?

Spent a bit longer investigating this… it’s looking like when lightmaps are enabled, there’s no way at all to use vertex lights, even in a completely custom shader - as Unity doesn’t set up the lighting parameters for the shader :frowning:

If my current findings are correct, it’s rather disappointing, having this great lightmapping system, but if you turn it on it blocks you from mixing it with dynamic lights? (ignoring pass-per-light pixel lights, as they’re not really viable on mobile)

Haven’t yet figured out if I can access directional light parameters and do a single dynamic light along with lightmaps. That one may still be possible, even if it means getting a light direction+colour from elsewhere rather than from the Unity lighting system…

Well, I’ve managed to get lightmaps working with a directional light.

The shader is looking like this so far. It’s only calculating the directional specular term, not the diffuse, as it’s an attempt to bring out some normal-mapped shine in a mostly-lightmapped scene:

Shader "Custom/DirSpec_Lightmap" 
{
	Properties 
	{
		_Shininess ("Shininess", Range (1.0, 256)) = 64
		_MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}            
	}
    
	SubShader 
	{
		Tags { "Queue"="Geometry" }
		
        Pass 
		{           					
			Tags { "Lightmode"="ForwardBase" }
			
            CGPROGRAM			
            #pragma vertex   vertProgram
            #pragma fragment fragProgram
            #include "UnityCG.cginc"		
           
		    fixed4 		 _LightColor0;
			half		 _Shininess;
			
            sampler2D 	 _MainTex;
            float4		 _MainTex_ST; 				// Scale  position of _MainTex			           
            sampler2D 	 unity_Lightmap;			// Beast lightmap			
            float4		 unity_LightmapST; 			// Scale  position of Beast lightmap          
			sampler2D 	 _BumpMap;
           
            // vertex input: position, UV0, UV1
            struct vpIn 
			{
                float4 vertex   : POSITION;
    			float3 normal 	: NORMAL;
				float4 tangent 	: TANGENT;
				float2 texcoord : TEXCOORD0;
                float3 texcoord1: TEXCOORD1;	
				fixed4 col   	: COLOR;
            };
           
            struct vpOut
			{
                float4 pos  : SV_POSITION;
                float2 txuv : TEXCOORD0;
                float2 lmuv : TEXCOORD1;
				fixed4 col  : COLOR0;
				half3  halfDir : TEXCOORD2;
            };
           
            vpOut vertProgram (vpIn v) 
			{
				// Scale/Offset UVs
                vpOut o;
                o.pos   	= mul( UNITY_MATRIX_MVP, v.vertex );
                o.txuv  	= TRANSFORM_TEX(v.texcoord.xy,_MainTex);
                o.lmuv  	= v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
												
				// Todo, if possible - Vertex lighting (point lights)												
				o.col.rgb 	= 0;										
				o.col.a   	= v.col.a;
				
				// Compute tangent space half vector				
				TANGENT_SPACE_ROTATION; 
				half3 lightDirT = mul( rotation, ObjSpaceLightDir( v.vertex ) );
				half3 viewDirT  = mul( rotation, ObjSpaceViewDir( v.vertex ) );
				o.halfDir 		= normalize( normalize(lightDirT) + normalize(viewDirT) );   
				
				// Todo, maybe:
				// - VERTEX LIGHTS if possible...
				// - Attenuate specular with distance from camera
				// - Specular colour * LightColor
				
                return o;
            }
           
		   
            half4 fragProgram( vpOut i ) : COLOR 
			{
				fixed4 tex  = tex2D(_MainTex, i.txuv.xy);                
                fixed3 lm   = DecodeLightmap(tex2D(unity_Lightmap, i.lmuv.xy));
				fixed3 nrm  = UnpackNormal(tex2D(_BumpMap, i.txuv.xy));
				
				// Note: This is only calculating specular (not diffuse), but the diffuse term could easily be added, too				
				// This normalize() isn't strictly necessary, but does improve the quality noticably
				fixed  nh   = max (0, dot (nrm, normalize(i.halfDir)));
				fixed  spec = pow (nh, _Shininess) * tex.a;
				
				fixed4 col  = float4( (tex.rgb * lm) + ( _LightColor0.rgb * spec ), tex.a ); 							
												
                return col;
            }
            ENDCG
        }
    }
	
	FallBack Off
}

Now if only I could get the 4 per-vertex point lights working with this, too…

If you stick this in your vertex shader
o.vertexLights = ShadeVertexLights (v.vertex, v.normal);

And put the relevant output in your vpOut struct you should be able to get the vertex lights in there by adding that value to the final result in the frag shader.

Attenuation is done via a bunch of macros throughout the shader.
You’ll need to include AutoLight.cginc into the shader along with the UnityCG.cginc.

In your vertex out struct, add LIGHTING_COORDS(X,Y) with X and Y being the next two available TEXCOORD numbers.

In your vertex shader, add TRANSFER_VERTEX_TO_FRAGMENT(o)

In your frag shader, fixed atten = LIGHT_ATTENUATION(i); will give you the light’s attenuation (and shadow value if it’s there) which you can multiply your specular value by.

I’ve not managed to get this to work - I was getting 0,0,0 out of it if the object was lightmapped. I suspect Unity isn’t setting up the lights for objects with lightmaps, but I’ll experiment with this further tonight…

If you are new to unity but familar with shader,you could use #pragma debug in a surface shader to see what unity generates,
and #pragma only_renders XXX to restrict what unity may generates

Hmm, yeah, it can be strange.

Oh, btw, you don’t need any of those normalize functions in the vertex shader for halfDir if you’re normalizing it in the fragment shader anyway.

Good point, have been experimenting with it, will need to tidy up and see if there’s anything that I can optimize away. It’s still looking pretty expensive, but at least it’ll be a single pass now.

Still had no success at all with vertex lighting, though - tried the function mentioned above as well as the version that does 4 point lights. Seems like Unity might be hard-coded to not set up vertex lights when lightmapping is enabled… although maybe there’s a magic #pragma or similar which I’ve not discovered yet…

After a bit of exploration for this thread, I think you’re right: the forward renderer provides either lightmap data or vertex/SH data, but never both.

This seems a real shame - being unable to add dynamic point lights to a lightmapped scene (without resorting to multiple passes, which isn’t at all desirable on iOS/Android)

Would have been nice to have for special effects - e.g making explosions light up the scene a bit.

Can’t think of a good technical reason for the limitation, either - as SH lights + 4 point lights + 1 directional light all seem to work fine together in a single pass, and there shouldn’t really be anything special about the lightmaps, they’re just another texture to blend in?

I’m guessing that setting up the vertex lights has just been optimized away when lightmaps are in use - it’d be great if there was a way to force that back on.

(Hmmm… wondering if we can somehow turn off lightmaps but keep the lightmap textures and UVs, so Unity thinks it’s not lightmapped but the custom shader still renders it as if it is…)

Maybe see what gets set up when LightMode = VertexLM. The docs say you can use ShadeVertexLights() there, and your light maps should work as usual on mobile.

I wonder if there exists a solution to the problem yet? So point lights, a light mapped scene and these two together in a single pass…I’m thinking for days now about this and can not find a solution either.

Plan B would be to give us deferred lightning for mobile devices. :p:face_with_spiral_eyes:

The only solution that I’ve come up with (but not tried, yet) is to bake lightmaps outside of Unity (using some other tool). Then Unity won’t know that objects are lightmapped, and will still allow dynamic vertex lights. But that option could be somewhat painful in practice…

@bluescrn did you ever find a decent solution here? Trying to do something similar

Custom shaders on everything, and our own implementation of point lights in these shaders.

But this meant we had N global point lights, driven by global shader properties - rather than having lights set up per-mesh.

But that was back in the Unity 4.x days. There’s probably nicer solutions now with scriptable render pipelines, but there’s also less need for vertex lighting, with per-pixel now being more viable on modern mobile devices