Upcoming Events
Southwest Gaming Expo
11/20 - 11/22 @ Dallas, TX

Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarães, Portugal

Global Game Jam
1/29 - 1/31  

More events...


Quick Stats
6830 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

Integrating Your XNA Engine With XSI ModTool


Implementation

An Effect-Based Material System

3D models have two important attributes we’re concerned with: geometry and materials. The geometry will determine the shape of the model, while the materials will determine what the surface of that geometry actually looks like. For our content authoring pipeline, we’re going to use Effects as primary building blocks for materials. Each material effect will determine the basic type of material we’re working with: some examples can include a basic texture-mapped surface, a normal-mapped surface, a reflective surface that uses an environment map, a surface with a fur shader, a metallic surface, a cel-shaded surface... whatever it is the actual game calls for.

Each material effect will have a set of artist-editable parameters, which can be tweaked in ModTool (in real-time) in order to further customize an effect. In the actual effect these parameters are implemented as textures or as shader constants.

A Consistent Effect Interface

One of the goals we laid out earlier was that we wanted our material effects to be interchangeable as far as our rendering code is concerned. This means we don’t want to have to treat any of our materials any differently: the code should be able to just set the shader constants it needs to set the same way for every effect. To facilitate this, we’re going to create a file containing the shader constants common to every effect and #include it in every material. We’ll call this file “mat_Common.fxh”, and it looks like this:

float4x4 g_matWorld;
float4x4 g_matWorldInverse;
float4x4 g_matView;
float4x4 g_matProj;

float3 g_vCameraPositionWS;
float3 g_vLightAmbient;
float3 g_vLightDirectionWS;
float3 g_vDirectionalLightColor;
We have a few basic constants here: transform matrices used to transform vertices to the various coordinate spaces, the camera position in world-space, and ambient lighting color, the direction of a directional light in world-space, and the color of the directional light. For now we’ll keep things simple and leave it at one directional light.

A Normal-Mapping Shader

As our first material type, we’re going to implement a basic normal-mapping shader. If you’re not familiar with normal-mapping, it works by sampling a per-pixel normal value from a texture and using that value for lighting calculations. This allows an otherwise flat surface to have the appearance of having much more geometry. These normal values we sample from the texture are in tangent-space, which means in the vertex shader we transform the light direction and the view direction to tangent-space so that we can perform the lighting calculations.

Before we write our vertex shader and pixel shader, let’s set up some parameters and textures. For parameters we’re going to need a specular color and power (glossiness), and for textures we’re going to need a diffuse map and a texture map.

float3 g_vSpecularAlbedo;
float g_fSpecularPower;

texture2D DiffuseMap;
sampler2D DiffuseSampler = sampler_state
{
    Texture = ;
    MinFilter = anisotropic;
    MagFilter = linear;
    MipFilter = linear;
    MaxAnisotropy = 16;
};

texture2D NormalMap;
sampler2D NormalSampler = sampler_state
{
    Texture = ;
    MinFilter = anisotropic;
    MagFilter = linear;
    MipFilter = linear;
    MaxAnisotropy = 16;
};
For our vertex shader, we first need to set up our vertex inputs. Models that are exported from XSI ModTool have a particular vertex format, which actually encodes the binormal and tangent in order to save space. The inputs for our vertex shader look like this:
in float4 in_vPositionOS	: POSITION0,
in float3 in_vNormalOS		: NORMAL0,  
in float4 in_vColor0		: COLOR0, 
in float4 in_vColor1		: COLOR1,
in float2 in_vTexCoord		: TEXCOORD0,
in float4 in_vTexCoord1	: TEXCOORD1,
in float4 in_vTexCoord2	: TEXCOORD2,
in float4 in_vTexCoord3	: TEXCOORD3,
in float4 in_vTexCoord4	: TEXCOORD4,
in float4 in_vTexCoord5	: TEXCOORD5,
in float4 in_vTexCoord6	: TEXCOORD6,
in float4 in_vTexCoord7	: TEXCOORD7
Now like I mentioned, we need to do some unpacking of our binormal and tangent. The code for that looks like this:
// Calculate the tangent and binormal
float3 vTangentOS = (in_vColor0 * 2) - 1; 
float fSign = (in_vColor0.a * 2) - 1; 
fSign  = (fSign > 0) ? 1 : -1; 
float3 vBinormalOS = in_vNormalOS.yzx * vTangentOS.zxy;
Okay now were’ all set up and ready to code our shaders. Here’s the final mat_NormalMapping.fx file:
float3 g_vSpecularAlbedo;
float g_fSpecularPower;

texture2D DiffuseMap;
sampler2D DiffuseSampler = sampler_state
{
    Texture = ;
    MinFilter = anisotropic;
    MagFilter = linear;
    MipFilter = linear;
    MaxAnisotropy = 16;
};

texture2D NormalMap;
sampler2D NormalSampler = sampler_state
{
    Texture = ;
    MinFilter = anisotropic;
    MagFilter = linear;
    MipFilter = linear;
    MaxAnisotropy = 16;
};

void NormalMappingVS(	in float4 in_vPositionOS	: POSITION0,					
			in float3 in_vNormalOS		: NORMAL0,  
			in float4 in_vColor0		: COLOR0, 
			in float4 in_vColor1		: COLOR1,
			in float2 in_vTexCoord          : TEXCOORD0,
			in float4 in_vTexCoord1         : TEXCOORD1,
			in float4 in_vTexCoord2         : TEXCOORD2,
			in float4 in_vTexCoord3         : TEXCOORD3,
			in float4 in_vTexCoord4         : TEXCOORD4,
			in float4 in_vTexCoord5         : TEXCOORD5,
			in float4 in_vTexCoord6         : TEXCOORD6,
			in float4 in_vTexCoord7         : TEXCOORD7,
			out float4 out_vPositionCS	: POSITION0,
			out float2 out_vTexCoord	: TEXCOORD0,
			out float3 out_vLightDirTS	: TEXCOORD1,
			out float3 out_vViewDirTS	: TEXCOORD2,
			out float3 out_vPositionWS	: TEXCOORD3 )
{
  // Figure out the position of the vertex in clip space
  out_vPositionWS = mul(in_vPositionOS, g_matWorld);
  float4x4 matViewProj = mul(g_matView, g_matProj);
  float4x4 matWorldViewProj = mul(g_matWorld, matViewProj);
  out_vPositionCS = mul(in_vPositionOS, matWorldViewProj);
  out_vTexCoord = in_vTexCoord;
    
  // We need these in object space before converting to tangent space
  float3 vLightDirectionOS = mul(-g_vLightDirectionWS, g_matWorldInverse);
  float3 vCameraPosOS = mul(float4(g_vCameraPositionWS, 1.0f), g_matWorldInverse);
    
  // Calculate the tangent and binormal
  float3 vTangentOS = (in_vColor0 * 2) - 1; 
  float fSign = (in_vColor0.a * 2) - 1; 
  fSign  = (fSign > 0) ? 1 : -1; 
  float3 vBinormalOS = in_vNormalOS.yzx * vTangentOS.zxy; 
    
  vBinormalOS = (-vTangentOS.yzx * in_vNormalOS.zxy) + vBinormalOS; 
  vBinormalOS = (vBinormalOS * fSign);

  // Build the TBN matrix
  float3x3 matTBN = float3x3(vTangentOS, vBinormalOS, in_vNormalOS);
    
  // Convert to tangent space
  out_vLightDirTS = mul(matTBN, vLightDirectionOS);
  out_vViewDirTS = mul(matTBN, vCameraPosOS - in_vPositionOS.xyz);   
}
 
float3 CalcLighting (	float3 vDiffuseAlbedo, 
			float3 vSpecularAlbedo, 
			float  fSpecularPower, 
			float3 vLightColor, 
			float3 vNormal, 
			float3 vLightDir, 
			float3 vViewDir	)
{
  float3 R = normalize(reflect(-vLightDir, vNormal));
    
  // Calculate the raw lighting terms
  float fDiffuseReflectance = saturate(dot(vNormal, vLightDir));
  float fSpecularReflectance = saturate(dot(R, vViewDir));
  if (fDiffuseReflectance == 0)
  fSpecularReflectance = 0;

  // Modulate the lighting terms based on the material colors, and the attenuation factor
  float3 vSpecular = vSpecularAlbedo * vLightColor;
  pow(fSpecularReflectance, fSpecularPower);
  float3 vDiffuse = vDiffuseAlbedo * vLightColor * fDiffuseReflectance;	

  // Lighting contribution is the sum of ambient, diffuse and specular terms
  return vDiffuse + vSpecular;
}


float4 NormalMappingPS( in float2 in_vTexCoord   : TEXCOORD0,
                        in float3 in_vLightDirTS : TEXCOORD1,
                        in float3 in_vViewDirTS  : TEXCOORD2,
                        in float3 in_vPositionWS : TEXCOORD3	) : COLOR0
{
  // Sample the texture maps
  float3 vDiffuseAlbedo = tex2D(DiffuseSampler, in_vTexCoord).rgb;
  float3 vNormalTS = tex2D(NormalSampler, in_vTexCoord).rgb;

  // Normalize after interpolation
  vNormalTS = vNormalTS = 2.0f * (vNormalTS.xyz - 0.5f);
  in_vLightDirTS = normalize(in_vLightDirTS);
  in_vViewDirTS = normalize(in_vViewDirTS);

  // Calculate the lighting term for the directional light
  float3 vColor = CalcLighting( vDiffuseAlbedo, 
                                g_vSpecularAlbedo, 
                                g_fSpecularPower,
                                g_vDirectionalLightColor, 
                                vNormalTS, 
                                in_vLightDirTS, 
                                in_vViewDirTS);
					
  // Add in ambient term	
  vColor += vDiffuseAlbedo * g_vLightAmbient;										
  return float4(vColor, 1.0f);
}

Technique Render
{
    Pass
    {
        VertexShader = compile vs_2_0 NormalMappingVS();
        PixelShader = compile ps_2_0 NormalMappingPS();
        ZEnable = true;
        ZWriteEnable = true;
        AlphaBlendEnable = false;       
    }
}

Setting Up SAS Annotations

Okay so we’ve got our fancy normal-mapping shader now, and if we want we could use it to render some stuff in our XNA application. But what about in ModTool? If we used it as-is, ModTool would have no idea what to do without effect. What parameters should be set by the user? Which ones should be set automatically? And to what values? To make sure ModTool can make heads or tails of everything, we need to add some SAS (“Standard Annotations and Semantics”) annotations.

We’ll start off with the shader constants in mat_Common.fxh. We said earlier that these are going to be the constants set by our rendering code, which means we don’t want the artist to be messing with these. Instead we’ll use annotations that tell ModTool what values to set there for us. First for the matrices, we can use standard HLSL semantics to bind them to certain transforms:

float4x4 g_matWorld : WORLD;
float4x4 g_matWorldInverse : WORLDINVERSE;
float4x4 g_matView : VIEW;
float4x4 g_matProj : PROJECTION;
For our lighting constants, we have to use some SAS annotations to specify what we want. Those annotations look like this:
float3 g_vCameraPositionWS
<
	string SasBindAddress = "SAS.CAMERA.POSITION"; 
>;

float3 g_vLightAmbient
<
	string SasBindAddress = "SAS.AMBIENTLIGHT[0].COLOR";
>;

float3 g_vLightDirectionWS
<
	string SasBindAddress = "SAS.DIRECTIONALLIGHT[0].DIRECTION";
> = {1, -1, 1};

float3 g_vDirectionalLightColor
<
	string SasBindAddress = "SAS.DIRECTIONALLIGHT[0].COLOR";
>;
We’re also going to add some SAS annotations to the material parameters to specify that they are artist-editable. We can also specify some other information: the name of the parameter to be displayed, the type of UI control to use, and minimum/maximum values.
float3 g_vSpecularAlbedo 
<
	string SasUiControl = "ColorPicker";
	string SasUiLabel =  "Specular Albedo";	
> =  {1.0f, 1.0f, 1.0f};

float g_fSpecularPower
<
	string SasUiControl = "Slider";
	string SasUiLabel = "Specular Power";
	float SasUiMin = 1;
	float SasUiMax = 200;	
> = 32.0f;
 
texture2D DiffuseMap
<
	string ResourceType = "2D";
>;
sampler2D DiffuseSampler = sampler_state
{
    Texture = ;
    MinFilter = anisotropic;
    MagFilter = linear;
    MipFilter = linear;
    MaxAnisotropy = 16;
};

texture2D NormalMap
<
	string ResourceType = "2D";
>;
sampler2D NormalSampler = sampler_state
{
    Texture = ;
    MinFilter = anisotropic;
    MagFilter = linear;
    MipFilter = linear;
    MaxAnisotropy = 16;
};

Setting Up Our Rendering Code

Now we’re ready to set up some code for rendering models in our game. As promised, thanks to our consistent material effect interface, this is easy.

protected void RenderModel(Model model, Matrix modelTransform)
{
    Matrix[] bones = new Matrix[model.Bones.Count];
    model.CopyAbsoluteBoneTransformsTo(bones);
    
    // Get camera matrices
    Matrix cameraTransform, viewMatrix, projMatrix;
    camera.GetWorldMatrix(out cameraTransform);
    camera.GetViewMatrix(out viewMatrix);
    camera.GetProjectionMatrix(out projMatrix);           

    for (int i = 0; i <: model.Meshes.Count; i++)
    {
        ModelMesh mesh = model.Meshes[i];
        Matrix worldMatrix = bones[mesh.ParentBone.Index];
        Matrix.Multiply(ref worldMatrix, ref modelTransform, out worldMatrix);
        Matrix worldInverseMatrix;
        Matrix.Invert(ref worldMatrix, out worldInverseMatrix);

        for (int j = 0; j < mesh.MeshParts.Count; j++)
        {
            ModelMeshPart meshPart = mesh.MeshParts[j];

            // If primitives to render
            if (meshPart.PrimitiveCount > 0)
            {
                // Setup vertices and indices
                GraphicsDevice.VertexDeclaration = meshPart.VertexDeclaration;
                GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer,
                                                     meshPart.StreamOffset,
                                                     meshPart.VertexStride);
                GraphicsDevice.Indices = mesh.IndexBuffer;

                // Setup the parameters for the sun
                Effect effect = meshPart.Effect;


                effect.Parameters["g_matWorld"].SetValue(worldMatrix);
                effect.Parameters["g_matWorldInverse"].SetValue(worldInverseMatrix);
                effect.Parameters["g_matView"].SetValue(viewMatrix);
                effect.Parameters["g_matProj"].SetValue(projMatrix);
                effect.Parameters["g_vCameraPositionWS"].SetValue(cameraTransform.Translation);
                effect.Parameters["g_vLightDirectionWS"].SetValue(sunLightDirection);
                effect.Parameters["g_vDirectionalLightColor"].SetValue(sunLightColor);
                effect.Parameters["g_vLightAmbient"].SetValue(ambientLight);

                // Begin effect
                effect.Begin(SaveStateMode.SaveState);
                effect.CurrentTechnique.Passes[0].Begin();

                // Draw primitives
                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                                                     meshPart.BaseVertex,
                                                     0,
                                                     meshPart.NumVertices,
                                                     meshPart.StartIndex,
                                                     meshPart.PrimitiveCount);

                effect.CurrentTechnique.Passes[0].End();
                effect.End();

                GraphicsDevice.Vertices[0].SetSource(null, 0, 0);
                GraphicsDevice.Indices = null;
                GraphicsDevice.VertexDeclaration = null;
            }
        }
    }
}




Working with XSI ModTool


Contents
  Introduction
  Implementation
  Working with XSI ModTool

  Source code
  Printable version
  Discuss this article