Doom3 lighting vs. Deluxel based lighting


(Tr3B) #1

I think this is interesting for people who are working on their own Q3A/Doom3 compatible engine and looking for some performance tricks.

I can render a Doom3 level using a Doom3 style lighting system with light shaders and everything but shadows. On the other hand I can use per pixel lighting using deluxemaps and light vectors (deluxels) that are stored in every BSP vertex. The lighting is generated by a modified q3map2 compiler. The new way does not need to render world surfaces multiple times for every static light. There is only 1 lighting pass needed for every world surface using deluxemaps and deluxel vectors. It allows all N dot L based lighting techniques even Specular Parallax Bumpmapping.

The advantage is clear. You need less lighting passes and this will result in more speed. The disadvantage is that you can use this technique only for static lights. But think about that Doom3 renders a surface 1 time more for really every simple static and non-exiting light just because to have a unified lighting model. I think it is better to use the GPU power for something else. Use deluxe mapping for all static lights in your map and Doom3 style dynamic lighting for all lights that require it like flickering or rotating lights.

I made some comparison shots to demonstrate how deluxel based lighting looks compared to the Doom3 style lighting system. Keep in mind that q3map2 uses a different light attenuation model than Doom3 and I haven’t written light shader support for q3map2 so it will look a bit different.

http://xreal.sourceforge.net/screenshots/qrazor-fxIII050.jpg <-- dynamic Doom3 style lighting
http://xreal.sourceforge.net/screenshots/qrazor-fxIII051.jpg <-- static Deluxel based lighting precomputed by q3map2
There are more shots till http://xreal.sourceforge.net/screenshots/qrazor-fxIII065.jpg

This technique works with Doom3 materials which allow to add lightmap and deluxemap shader stages to the shader without any special blend modes.

Material sample shader


textures/ind/tig_bluholes
{
   qer_editorimage  textures/ind_ed/tig_bluholes.tga

   {
      stage   diffusemap
      map     textures/ind/tig_bluholes.png
   }
   {
      // heightmap for parallax mapping can be saved in the alpha channel
      stage   bumpmap
      map     textures/ind_x/tig_bluholes_n8.png
      bumpScale 1.1
      heightBias -0.02
      heightScale 0.04
   }
   {
      stage   specularmap
      map     textures/ind_x/tig_bluholes_s.png
      specularExponent 24
   }
   {
      // you don't need to specify this stage
      stage   lightmap
     // apply tcMod commands if needed like scale
   }
   {
      // you don't need to specify this stage
      stage   deluxemap
      // apply tcMod commands if needed
   }
   //  add addional color stages if needed like this one
// {
//    stage   colormap
//    map     textures/ind_x/tr3b_blueholes_decal.png
// }
}

OpenGL GLSL code for vertex deluxel based lighting


attribute vec4  attr_TexCoord0;
attribute vec3  attr_Tangent;
attribute vec3  attr_Binormal;
attribute vec3  attr_Light;

varying vec3  var_vertex;
varying vec4  var_tex_diffuse_bump;
varying vec2  var_tex_specular;
varying mat3  var_mat_os2ts;
varying vec3  var_light;
varying vec3  var_color;

void main()
{ 
 // transform vertex position into homogenous clip-space
 gl_Position = ftransform();
 
 // assign position in object space
 var_vertex = gl_Vertex.xyz;
  
 // transform texcoords into diffusemap texture space
 var_tex_diffuse_bump.st = (gl_TextureMatrix[0] * attr_TexCoord0).st;
 
 // transform texcoords into bumpmap texture space
 var_tex_diffuse_bump.pq = (gl_TextureMatrix[1] * attr_TexCoord0).st;
 
 // transform texcoords into specularmap texture space
 var_tex_specular = (gl_TextureMatrix[2] * attr_TexCoord0).st;
 
 // construct object-space-to-tangent-space 3x3 matrix
 var_mat_os2ts = mat3( attr_Tangent.x, attr_Binormal.x, gl_Normal.x,
    attr_Tangent.y, attr_Binormal.y, gl_Normal.y,
    attr_Tangent.z, attr_Binormal.z, gl_Normal.z );
    
 // assign vertex to light vector in object space
 var_light = attr_Light;
 
 // assign color
 var_color = gl_Color.rgb;
}

uniform sampler2D u_diffusemap;
uniform sampler2D u_bumpmap;
uniform sampler2D u_specularmap;
uniform vec3  u_view_origin;
uniform float  u_bump_scale;
uniform float  u_height_scale;
uniform float  u_height_bias;
uniform float  u_specular_exponent;

void main()
{ 
 // compute view direction in tangent space
 vec3 V = normalize(var_mat_os2ts * (u_view_origin - var_vertex));
 
 // compute height
 float height = texture2D(u_bumpmap, var_tex_diffuse_bump.pq).a;
 
 // compute texcoords offset
 vec2 tex_offset = (height * u_height_scale + u_height_bias) * V.xy;
  
 // compute light direction in tangent space
 vec3 L = normalize(var_mat_os2ts * var_light);
 
 // compute half angle in tangent space
 vec3 H = normalize(L + V);
 
 // compute normal in tangent space from bumpmap
 vec3 N = 2.0 * (texture2D(u_bumpmap, var_tex_diffuse_bump.pq + tex_offset).xyz - 0.5);
 N.z *= u_bump_scale;
 N = normalize(N);
 
 // compute the diffuse term
 vec4 diffuse = texture2D(u_diffusemap, var_tex_diffuse_bump.st + tex_offset);
 diffuse.rgb *= var_color * clamp(dot(N, L), 0.0, 1.0);
 
 // compute the specular term
 vec3 specular = texture2D(u_specularmap, var_tex_specular + tex_offset).rgb * var_color * pow(clamp(dot(N, H), 0.0, 1.0), u_specular_exponent);
     
 // compute final color
 gl_FragColor.rgba = diffuse;
 gl_FragColor.rgb += specular;
}

OpenGL GLSL code for lightmap and deluxmap based lighting


attribute vec4  attr_TexCoord0;
attribute vec4  attr_TexCoord1;
attribute vec3  attr_Tangent;
attribute vec3  attr_Binormal;

varying vec3  var_vertex;
varying vec4  var_tex_diffuse_bump;
varying vec2  var_tex_specular;
varying vec4  var_tex_light_deluxe;
varying mat3  var_mat_os2ts;

void main()
{
 // transform vertex position into homogenous clip-space
 gl_Position = ftransform();
 
 // assign vertex in object space
 var_vertex = gl_Vertex.xyz;
 
 // transform texcoords into diffusemap texture space
 var_tex_diffuse_bump.st = (gl_TextureMatrix[0] * attr_TexCoord0).st;
 
 // transform texcoords into bumpmap texture space
 var_tex_diffuse_bump.pq = (gl_TextureMatrix[1] * attr_TexCoord0).st;
 
 // transform texcoords into specularmap texture space
 var_tex_specular = (gl_TextureMatrix[2] * attr_TexCoord0).st;
 
 // transform texcoords_lm into lightmap texture space
 var_tex_light_deluxe.st = (gl_TextureMatrix[3] * attr_TexCoord1).st;
 
 // transform texcoords_lm into deluxemap texture space
 var_tex_light_deluxe.pq = (gl_TextureMatrix[4] * attr_TexCoord1).st;
  
 // construct object-space-to-tangent-space 3x3 matrix
 var_mat_os2ts = mat3( attr_Tangent.x, attr_Binormal.x, gl_Normal.x,
    attr_Tangent.y, attr_Binormal.y, gl_Normal.y,
    attr_Tangent.z, attr_Binormal.z, gl_Normal.z );
}

uniform sampler2D u_diffusemap;
uniform sampler2D u_bumpmap;
uniform sampler2D u_specularmap;
uniform sampler2D u_lightmap;
uniform sampler2D u_deluxemap;
uniform vec3  u_view_origin;
uniform float  u_bump_scale;
uniform float  u_height_scale;
uniform float  u_height_bias;
uniform float  u_specular_exponent;

void main()
{
 // compute view direction in tangent space
 vec3 V = normalize(var_mat_os2ts * (u_view_origin - var_vertex));
 
 // compute height
 float height = texture2D(u_bumpmap, var_tex_diffuse_bump.pq).a;
 
 // compute texcoords offset
 vec2 tex_offset = (height * u_height_scale + u_height_bias) * V.xy;
 
 // compute light direction in tangent space from deluxemap
 vec3 L = normalize(var_mat_os2ts * (2.0 * (texture2D(u_deluxemap, var_tex_light_deluxe.pq).xyz - 0.5)));
 
 // compute half angle in tangent space
 vec3 H = normalize(L + V);
 
 // compute normal in tangent space from bumpmap
 vec3 N = 2.0 * (texture2D(u_bumpmap, var_tex_diffuse_bump.pq + tex_offset).xyz - 0.5);
 N.z *= u_bump_scale;
 N = normalize(N);
 
 // compute light color from object space lightmap
 vec3 C = texture2D(u_lightmap, var_tex_light_deluxe.st).xyz;
 
 // compute the diffuse term
 vec4 diffuse = texture2D(u_diffusemap, var_tex_diffuse_bump.st + tex_offset);
 diffuse.rgb *= C * clamp(dot(N, L), 0.0, 1.0);
 
 // compute the specular term
 vec3 specular = texture2D(u_specularmap, var_tex_specular + tex_offset).rgb * C * pow(clamp(dot(N, H), 0.0, 1.0), u_specular_exponent);
     
 // compute final color
 gl_FragColor = diffuse;
 gl_FragColor.rgb += specular;
}

I hope this helps to illustrate how things work in the GPU.


(Jaquboss) #2

Well this look a lot ambient , but maybe it is faster…


(Tr3B) #3

It depends on how many static lights you have in a room. Everybody who converts a Q1-Q3A .map to Doom3 knows the problem how dynamic lights decrease the performance.
I have more than 5x speed using deluxel based lighting.


(obsidian) #4

Interesting… A few questions since some of these terms are new to me…

What is this exactly, I’m guessing lightmaps for D3?

What exactly is a deluxel?

How is it different than a lightmap luxel?


(Tr3B) #5

Yes it is improved lightmapping in a certain way. A deluxel is a normalized 3d vector in the direction from the surface to light. A lightmap luxel stores the color in RGB values.
The Quake engine series just blended lightmaps together with the textures to achieve lighting. The new deluxemaps provide also light direction information so that most modern per pixel lighting techniques like bump mapping can be applied.
The following picture illustrates the difference between the lightmap and the deluxemap in q3dm17.


(obsidian) #6

Thanks for the info… this is very interesting indeed.

As I understand it, the HL2 engine uses multiple precomputed lightmaps (1 for each vector) for use with per-pixel lighting methods. So since normal maps can encode all that information into a single image, deluxemaps does the same thing without the need for multiple lightmaps. Does that sound right?

So I suppose since light angle is taken into consideration here, specular maps will behave as they should in D3 since we can calculate the angle at which the light is reflected from the surface (though not dynamic - so the flashlight in D3 wouldn’t work on these surfaces). But I suppose for per-pixel bumpmapping a separate normalmap is still required?


(kat) #7

Yeah, interesting stuff. The thing I noticed was that although you create a much ‘better’ lighting solution that in many respects is similar to lightmaps, the thing that seems to be missing are object shadows. That wierd looking object in the 1st two screenies should really be casting shadows (I can’t tell if it is doing this in the D3 shot as the screenie is so dark) but in shot two there should really be some sort of shadow presence on the floor if there’s a light directly behind the object like that. Perhaps I’m missing something??

Basically it looks kind of odd and ‘incomplete’ when you campare the two engines like that.

Also got a question, the ‘clarity’ or ‘definition’ of the shadow is determined by what? If you create denser polygons (more brushsplits for instance) does that increase the ‘quality’ of the shadows?


(carnage) #8

i no little about doom3 source so forgive me if ime worng or stupid

if you are not using per pixes lighting for all lights does that mean that a light grid is used to calculate light on moddles?

would it be poible to make staitic lights use lightmaps for the worlspawn and staitic moddles then use per pixel lighting on everything else? so greater lighting effects on moddles are achevable

also (getting way over my head now)

could each (or groups of lights that do not uses the same space on the lightmaps) have there own lightmap image. so by using the per pixel lighting on the moddle owrk that to the surface the light would hit, then working out the part of the surface the shdow would hit and re draw without the effect of the lightmap

pretty poorliy explined but hey


(Tr3B) #9

One thing to mention: The above screenshots were not taken in Doom3. They were taken in my ultra heavily modified Quake2 engine called QRazor-FX. It has no support for shadow volumes as in Doom3 so casting shadows should lack on all dynamic lighting images. The engine does not use the lightgrid although it would be possible to generate a second lightgrid with deluxels instead of luxels. I use the static lighting only on world models and the Doom3 similar lighting on everything else. I think having a lightmap per light would be some kind of memory overkill.


(Jaquboss) #10

oh , so :slight_smile:
Hmm , this cant be used with ET :-/

BTW: quake 3 screenshot is looking like it is something similar to normal map …
Hmm , it will be nice if you can convert this for ET , but I guess it will be a lot of work , maybe if someone will release ET client sources …
And i hope you will add shadows :slight_smile:
I cant try it cos i dont have cvs , but i like screenshots , nice job!


(carnage) #11

lightmap per light? could just use separate lightmaps for lights that overlaped on surfaces, and would the lightmaps be smaller or do all lightmaps have to be the same size?


(obsidian) #12

Maybe you should check out the Q2:Evolved project too. Would be cool if you guys combined your efforts since it supports shadow volumes but not deluxel lighting. See here:
http://quake3world.com/forum/viewtopic.php?t=2695


(Typhontaur) #13

I am implementing the deluxemapping in qfusion-fx, but I have this problem:

that is 3 walls are correctly drawn, while the others 3 are wrong.
The shaders that I use are those of Tr3B.

vertex program


!!ARBvp1.0
#================================================================
ATTRIB attr_TexCoord0 = vertex.attrib[8];
ATTRIB attr_TexCoord1 = vertex.attrib[9];
ATTRIB attr_VertexPos = vertex.position;
ATTRIB attr_Tangent = vertex.attrib[6];
ATTRIB attr_Binormal = vertex.attrib[7];
ATTRIB attr_Normal = vertex.normal;

#================================================================
OUTPUT var_TexCoord0 = result.texcoord[1];
OUTPUT var_TexCoord1 = result.texcoord[2];
OUTPUT var_tangent = result.texcoord[3];
OUTPUT var_binormal = result.texcoord[4];
OUTPUT var_normal = result.texcoord[5];
OUTPUT var_viewVec = result.texcoord[6];
OUTPUT var_vertexPos = result.texcoord[7];

#================================================================
PARAM texMatrix0[4] = { state.matrix.texture[0] };
PARAM mvpMatrix[4] = { state.matrix.mvp };

#================================================================
	MOV var_vertexPos, attr_VertexPos;
	MOV var_tangent, attr_Tangent;
	MOV var_binormal, attr_Binormal;
	MOV var_normal, attr_Normal;
	
	DP4 result.position.x, mvpMatrix[0], attr_VertexPos;
	DP4 result.position.y, mvpMatrix[1], attr_VertexPos;
	DP4 result.position.z, mvpMatrix[2], attr_VertexPos;
	DP4 result.position.w, mvpMatrix[3], attr_VertexPos;

	DP4 var_TexCoord0.x, texMatrix0[0], attr_TexCoord0;
	DP4 var_TexCoord0.y, texMatrix0[1], attr_TexCoord0;
	DP4 var_TexCoord0.z, texMatrix0[2], attr_TexCoord0;
	DP4 var_TexCoord0.w, texMatrix0[3], attr_TexCoord0;
	
	DP4 var_TexCoord1.x, texMatrix0[0], attr_TexCoord1;
	DP4 var_TexCoord1.y, texMatrix0[1], attr_TexCoord1;
	DP4 var_TexCoord1.z, texMatrix0[2], attr_TexCoord1;
	DP4 var_TexCoord1.w, texMatrix0[3], attr_TexCoord1;
#================================================================
END
#================================================================

fragment program


!!ARBfp1.0
#================================================================
ATTRIB var_TexCoord0 = fragment.texcoord[1];
ATTRIB var_TexCoord1 = fragment.texcoord[2];
ATTRIB var_tangent = fragment.texcoord[3];
ATTRIB var_binormal = fragment.texcoord[4];
ATTRIB var_normal = fragment.texcoord[5];
ATTRIB var_viewVec = fragment.texcoord[6];
ATTRIB var_vertexPos = fragment.texcoord[7];

#================================================================
PARAM u_view_origin = program.local[2];
PARAM u_bump_scale = program.local[1];
PARAM u_specular_exponent = program.local[0];
PARAM c0 = {2, 0.5, 0, 0};

#================================================================
TEMP R0;
TEMP R1;
TEMP R2;
TEMP R3;

#================================================================
TEX R0.xyz, var_TexCoord1, texture[4], 2D;# deluxemap
TEX R1.xyz, var_TexCoord0, texture[1], 2D;# bumpmap
ADD R0.xyz, R0, -c0.y;
MUL R0.xyz, R0, c0.x;
ADD R1.xyz, R1, -c0.y;
MUL R1.xyz, R1, c0.x;
ADD R2.xyz, u_view_origin, -var_vertexPos;
DP3 R0.w, var_tangent, R2;
MOV R3.x, R0.w;
DP3 R0.w, var_binormal, R2;
MOV R3.y, R0.w;
DP3 R0.w, var_normal, R2;
MOV R3.z, R0.w;
DP3 R0.w, var_tangent, R0;
MOV R2.x, R0.w;
DP3 R0.w, var_binormal, R0;
MOV R2.y, R0.w;
DP3 R0.x, var_normal, R0;
MOV R2.z, R0.x;
DP3 R0.x, R3, R3;
RSQ R0.x, R0.x;
MUL R3.xyz, R0.x, R3;
MOV R0.xy, R1;
MUL R0.w, R1.z, u_bump_scale.x;
MOV R0.z, R0.w;
DP3 R0.w, R2, R2;
RSQ R0.w, R0.w;
MAD R3.xyz, R0.w, R2, R3;
MUL R2.xyz, R0.w, R2;
DP3 R0.w, R0, R0;
RSQ R0.w, R0.w;
MUL R0.xyz, R0.w, R0;
DP3_SAT R0.w, R0, R2;
DP3 R1.x, R3, R3;
RSQ R1.x, R1.x;
MUL R3.xyz, R1.x, R3;
DP3_SAT R0.x, R0, R3;
POW R0.x, R0.x, u_specular_exponent.x;
TEX R1.xyz, var_TexCoord1, texture[3], 2D;# lightmap
TEX R2, var_TexCoord0, texture[0], 2D;# diffusemap
MUL R3.xyz, R1, R0.w;
MUL R3.xyz, R2, R3;
MOV R2.xyz, R3;
TEX R3.xyz, var_TexCoord0, texture[2], 2D;# specularmap
MUL R1.xyz, R3, R1;
MAD R0.xyz, R1, R0.x, R2;
MOV R2.xyz, R0;
MOV result.color, R2;
#================================================================
END
#================================================================

Does someone know in thing I am being wrong?
Tangents and Binormals and Normals and all the other parameters are correct, as use them with dynamic lights.

Sorry for my bad english!

Thanks


(Typhontaur) #14

I have resolved! :clap: