Integrating Your XNA Engine With XSI ModTool
ImplementationAn Effect-Based Material System3D 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 InterfaceOne 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 ShaderAs 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 =
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 : TEXCOORD7Now 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 =
Setting Up SAS AnnotationsOkay 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 =
Setting Up Our Rendering CodeNow 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;
}
}
}
}
|
|