Thursday, August 16, 2012

Tutorial: Baked Light Maps for XNA using 3DS Max 2013

Special Note: For those of you without access to 3D Studio max, an educational version is available through autodesk which does not require a license provided through a school.

In this tutorial I will introduce how to bake a light map with 3D Studio Max 2013 (will also work with version 8 and up) and then render your baked light map and level geometry in XNA 4.0 using a custom HLSL shader.

The first step is of course to make your level, or making a test level. Here's mine. Notice the very simple level geometry, simple geometry significantly improves the quality of the light bake and the ease of unwrapping.

For the tutorial I exaggerated the lighting  for the sake of visual demonstration. My lights are a standard Omni and Target Spotlight, their intensity is set to 5.0, Decay is set to Inverse with Decay Start set at 10, I'm using Far Attenuation set to start at 30 and end at 80. I Enabled shadows, using a shadow map. 3D Studio Max can sometimes produces unacceptable poor shadows, especially on physically small scenes like mine, scroll down under "Shadow Map Params" and set the "Bias" to 0.01 and the Size to 1024.

Now to prepare the level for baking. The best and most reliable exports are ones with as few modifiers as possible. Collapse all of your stacks to Editable Poly. Now select whatever model you consider to be the most appropriate main model, in my case it's the ground, now attach all of the other models using Editable Poly's Attach function. When prompted with attach options, select "Match Material IDs to Material" and check "Condense Material and IDs". Now your level should be a single model and each previously separate segment will be an Selection Element.

Now generate your second UV Channel. Select the level model, now under the Modifier List, select Unwrap UVW. Change the Map Channel to 2. Annoyingly a Channel Change Warning Dialog will come up, select Abandon. Now Open the UV Editor, at the lower left of the Edit UVW window, select Face, it's the pink square in 2013.­ Now select all of your geometry in the Edit UVW window, now go up to the top of the window and select Mapping and then Flatten Mapping. A dialog will appear with flatten configuration options which are handy but the default settings are appropriate for our uses in this tutorial. In case you have changed yours, I have attached a screenshot of mine.

Click Ok.

The more complex your level gets the more inefficient texture usage becomes. More complex levels will require adjusting the Flatten options and sometimes hand placement of some UV island. Here is what my flatten looks like.

This now the second UV channel which will be used to bake the light map. You can close the window, you won't be using it again in this tutorial. Now collapse the Modifier Stack Again. To recap, your first UV Channel is for your Textures, your second UV Channel is for your light map and your model is now ready for baking.

However, before baking, we are going to export the level geometry first. For this tutorial, we're going to be using the FBX format, it's awkward and hard to use. Normally I would use kwxport X exporter, which exports the model as a Microsoft X format, however the developer has not updated the exporter to function with 3D Studio Max 2012 or 2013 at the time of writing, however, if you're using an older 3D Studio, say versions 8 to 2011, I highly suggest substituting the FBX format for the X format, his exporter is available at this address,

Back to FBX instructions, which you can disregard if you're using kwxport. Honestly, from a game developer's perspective, the FBX exporter sucks, it sucks bad, it sucks so bad I wrote a custom exporter in MAXSCRIPT which is something in-itself worth avoiding. We won't be doing that for this tutorial because writing a MAXSCRIPT exporter and custom content importer for the XNA content pipeline is beyond the scope of this article.

Here is my export configuration, you must copy it exactly for the sake of producing usable content for this tutorial. Do not export Animation,  Cameras, or Lights. You must embed the media, otherwise you will need to edit texture file paths inside of the ASCII file, because for some reason unknown to me, the AUTODESK people saw fit to include a complete file path rather than a local file path, if you are going to be using multiple level segments AFTER completing this tutorial, you will need to replace the texture file path with the texture name and file extension to avoid embedding identical texture content. For example, "c:/users/steve/desktop/dualtextureturorial/metalfloor.png" with "metalfloor.png". But for now, if you Embed Media you can avoid editing the ASCII file. Click okay, and you will likely be presented with an error about turned edges or some related issue, ignore it.

Now to Render your light map. You may use whatever renderer you choose, but I will be using Mental Ray in order to generate an Ambient Occlusion Map.
I prefer that some optional ambient lighting be included in my light map, but of course this part is entirely up to you.

Now to actually bake the light map. Select your model and open the Render to Texture Dialog, this can be done from the Rendering drop down or by pressing the zero key. Don't forget to set your Output path. Set your Mapping Coordinates to "Use Existing Channel", set the Channel to "2" which corresponds to your previously covered UV unwrap.

Scroll down to output and press the "Add..." button. The Add Texture Element dialog screen will appear, select "LightingMap". If you are using Mental Ray you will also have a map option for "Ambient Occlusion (MR)", select that as well, and then click "Add Elements"

Under "Selected Element Common Settings", make sure both your maps are enabled and change both their width and height to 1024, this corresponds to the baked texture's dimensions. If you are also generating an Ambient Occlusion map, under "Selected Element Unique Settings", enter a Max Distance of 10.

Now under baked material, select "Save Source (Create Shell)" and "Duplicate Source to Baked", and slightly further down, also check "Render to Files Only" and "Keep Source Materials", this is essential, the render to texture can make a huge mess of your model and it can be very difficult and time consuming to recover the correct materials.

Now you're ready to render. Select Render at the bottom of the Render To Texture dialog. Here is what my Render window looks like, however, this is NOT what the final light map will look like.

Now, navigate to your output directory.

You will have two images. A lighting map which includes your baked lighting render and the second image will be your Ambient Occlusion render if you earlier chose to bake one. If you did not choose to make an Ambient Occlusion map, your lighting map is ready to go and you can skip this next step. If you did make an Ambient Occlusion map, then I will show you how to combine the two using Paint Dot Net.

Here are my two maps.

Open the lighting map in paint dot net, now drag the ambient occlusion map from windows explorer onto the light map. The Drag and Drop dialog will appear, select "Add Layer". Now open the layers window, double click the Ambient Occlusion layer and the Layer Properties dialog will open, change the blending mode to "multiply", click "OK", now duplicate the Ambient Occlusion layer if you want the Ambient Occlusion to really stand out, I duplicated mine three times to exaggerate the effect for this tutorial. Now save your image. Here is my Final Light Map.

Now Open Up Visual Studio 2010, create a new project XNA Game Studio 4.0 Windows Project.

First, add your level FBX and light map texture to the content project created by the Windows Project template.

XNA 4.0 includes "DualTextureEffect" for the model under Default Effect in the Content Processor, which works, which you must use to accomplish this effect on the windows 7 phone (because custom shaders are disabled), however this tutorial is for Desktop Windows and the XBOX 360 so we will be writing our own custom shader, so leave these settings alone.

Right Click on your Content Project and click add New Item, and then select "Effect File", name it "dualtextureeffect.fx" and then click add. The default shader only requires a few modifications to be acceptable.

Add two textures and two texture samplers. Copy and paste the following under the "TODO: Add Effect Parameters"

texture colorMapTexture;
texture lightMapTexture;

sampler2D lightMap = sampler_state
Texture = ;
MagFilter = Anisotropic;
MinFilter = Anisotropic;
MipFilter = Linear;
MaxAnisotropy = 16;

sampler2D colorMap = sampler_state
Texture = ;
MagFilter = Anisotropic;
MinFilter = Anisotropic;
MipFilter = Linear;
MaxAnisotropy = 16;

Now, scroll down to VertexShaderInput, add the UV channels to the vertex declarations, which correspond to your texture and light map channels you created in 3D Studio Max.

float2 TextureCoordinate : TEXCOORD0;
float2 LightMapCoordinate : TEXCOORD1;

Now, scroll down a bit further to VertexShaderOutput, and add the same two channels.

float2 TextureCoordinate : TEXCOORD0;
float2 LightMapCoordinate : TEXCOORD1;

Now, move a little further down to the VertexShaderOutput function. Add some code to copy the vertex channels from the VertexShaderInput to the VertexShaderOutput.

output.TextureCoordinate = input.TextureCoordinate;
output.LightMapCoordinate = input.LightMapCoordinate;

Now to update the PixelShaderFunction. Sample the sample and combine the regular textures with the light map.

Replace "return float4(1,0,0,1)" with the following.

return tex2D(colorMap, input.TextureCoordinate) * tex2D(lightMap, input.LightMapCoordinate);

Now the shader is nearly done, to avoid any mistakes we are also going to set some render states. Under technique Technique1 reads "Pass1" and then a todo about adding render states. Add the following before the vertexshader and pixelshader compile instructions:


Okay, our shader is done, and now all of our content is done. Now it's time to write some XNA code, big win time.

Now add a model, texture and effect to your Game1 object declarations.

//our content
Model levelGeometry;
Texture2D lightmap;
Effect myEffect;

//our camera
Matrix view;
Matrix projection;
float rotation;
Vector3 cameraPosition;

I know, I can hardly believe we are finally writing XNA C# code.

Go to the LoadContent function, and load your content. Your file names likely vary, substitute my filenames for your own.

// TODO: use this.Content to load your game content here
levelGeometry = Content.Load<Model>("levelgeo");
lightmap = Content.Load<Texture2D>("lightmap");
myEffect = Content.Load<Effect>("dualtextureeffect");

//setup our projection
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)graphics.PreferredBackBufferWidth / graphics.PreferredBackBufferHeight, .1f, 1000);

Move to the Update function. We are setting our camera up to orbit the level geometry. Add the following.

// TODO: Add your update logic here
rotation += gameTime.ElapsedGameTime.Milliseconds * 0.001f;

cameraPosition = new Vector3((float)Math.Sin(rotation) * 200, 175.0f, -    (float)Math.Cos(rotation) * 200);

view = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 0), Vector3.Up);

Time to render! Finally!

First make a new function with a void return type, call it "DrawDualTextureModel", put a xna model object in for its argument.

private void DrawDualTextureModel(Model m)

Now, to make sure you're objects are all rendering in the correct location, you need to process the transformations included in the export.

//set mesh transformations
Matrix[] transforms = new Matrix[m.Bones.Count];

Now loop through each meshpart contained within each mesh contained in the model object.

//Now, for the rendering code loop. Loop through each mesh contained within the model object.
foreach (ModelMesh mesh in m.Meshes)
     //loop through each meshpart contained within each mesh
     foreach (ModelMeshPart meshpart in mesh.MeshParts)
          //Rendering code goes here!

Now, before the super coders get on my case, I'm aware that there are a lot of better ways to do the following section, mostly with custom content processors, but like I said above, these types of solutions are outside of the scope of this tutorial. We are going to input shader parameters the regular way, but because we are using a custom shader with a dual texture effect we need to cast each meshpart effect created by the content manager into a basic effect in order to gain access to the textures which were imported with the model. Insert the following code into the innermost section of the loop.

//Set our shader parameters
myEffect.CurrentTechnique = myEffect.Techniques["Technique1"];
//really tacky, I know. Casting to a basic effect is essential for texture application without a custom processor
BasicEffect tmpBEffect = (BasicEffect)meshpart.Effect;
//now apply!

Okay, now we are finally ready to draw. First set the vertex and index buffers and then make a draw indexed primitives call. Inside of the innermost section of the loop immediately under the myEffect.CurrentTechnique.Passes[0].Apply();

//set our vertex and index buffers
GraphicsDevice.Indices = meshpart.IndexBuffer;

//render our geometry
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshpart.VertexOffset, 0, meshpart.NumVertices, meshpart.StartIndex, meshpart.PrimitiveCount);

Now move to the XNA template's draw function and insert our dual texture draw call.

// TODO: Add your drawing code here

Now hit run, yours should look like this. Here is the final image. If it doesn't, you did something wrong, feel free to comment.

No comments: