Code

Along the path to XNA zen I am encoutering many road blocks. As I face these challenges and mash out solutions I'll post the most difficult code here. Any links I found that help me to write this code I'll try to also post either on this page or my blog a few tabs to the left. I've already come to rely a great deal on the work of others who are kind enough to post their results on the web, and I will try to maintain this long and nerdy tradition.

Earth Shader

I devloped this shader for basically just one purpose. I wanted the clouds to cast shadows on the Earth. The clouds in my game is created by a sphere that is scaled just slightly larger than the earth model. This way the cloud model rotates randomly relative to the Earth giving the impression that theclouds are slipping and sliding free from the Earth's surface. To add shadows to the Earth I needed to derive the UV coordinates of a cooresponding point on the cloud model. This ended up being a bit tricker than I though, but I eventually hashed something out. If you look at the code you may see that the UV coordinates are not acquired directly from the model but are rather implicitly calculated base only on the cloud models transformation.

HLSL
uniform extern float4x4 fx_WVP : WORLDVIEWPROJ;  //world view projection matrix
uniform extern float4x4 fx_World;              //world transformation matrix
uniform extern float4x4 fx_InvCloudWorld; //inverse transform matrix for the cloud object

uniform extern texture fx_DiffuseTexture;       //stores diffuse texture for shader
uniform extern texture fx_CloudTexture;            //stores the diffuse cloud texture

//directional lights
uniform extern bool fx_Enabled_DL0;
uniform extern float3 fx_Direction_DL0;
uniform extern float3 fx_DiffuseColor_DL0;
uniform extern float3 fx_SpecularColor_DL0;

//PI
const float PI = 3.14159265358;
const float PI2 = 6.28318530717;
const float HALF_PI = 1.5707963267;

//ambient light
uniform extern float4 fx_AmbientColor;

// texture filter
sampler textureSampler = sampler_state
{
    Texture = <fx_DiffuseTexture>;
    magfilter = LINEAR; // magfilter, bigger than real size
    minfilter = LINEAR; // minfilter, smaller than real size
    mipfilter = LINEAR;
};

// texture filter
sampler cloudSampler = sampler_state
{
    Texture = <fx_CloudTexture>;
    magfilter = LINEAR; // magfilter, bigger than real size
    minfilter = LINEAR; // minfilter, smaller than real size
    mipfilter = LINEAR;
    AddressU =  Wrap;
    AddressV =  Wrap;
};

// input to vertex shader
struct VS_INPUT
{
    float4 f4Position                       : POSITION0;
    float4 f4Normal                         : NORMAL0;
    float2 textureCoordinate                : TEXCOORD0;
};

// vertex shader output
struct VS_OUTPUT
{
    float4 f4Position                       : POSITION0;
    float3 f3Normal                         : TEXCOORD0;
    float2 textureCoordinate                : TEXCOORD1;
    float2 cloudTexCoordinate                : TEXCOORD2;
    float cloudSpaceZCoordinate                : TEXCOORD3;
};

// alter vertex inputs
void vertex_shader(in VS_INPUT IN, out VS_OUTPUT OUT)
{
    //transformed vertex positions
    OUT.f4Position = mul(IN.f4Position, fx_WVP);
    
    float curY = sin((IN.textureCoordinate.y * PI) - HALF_PI);
    float curRad = sqrt(1 - curY * curY);
    float curTheta = IN.textureCoordinate.x * PI2;
    
    float4 trueSpherePoint = float4(-cos(curTheta) * curRad,
							curY, sin(curTheta) * curRad, 0.0);
    trueSpherePoint = mul(trueSpherePoint, fx_World);
    float4 inCloudSpace = normalize(mul(trueSpherePoint, fx_InvCloudWorld));

    float v = acos(-inCloudSpace.y) / PI;
    float u = acos(-inCloudSpace.x / sin(PI * v)) / PI2;
    
    OUT.cloudSpaceZCoordinate = inCloudSpace.z;
    OUT.cloudTexCoordinate = float2(u, v);
    
    //transformed vertex positions
    OUT.f3Normal = normalize(mul(IN.f4Normal, fx_World)).xyz;
    
    //forward texture coordinates
    OUT.textureCoordinate = IN.textureCoordinate;
}

// alter vs output and send to hardware one pixel at a time
// renders without the use of lighting calculations
float4 pixel_shader(in VS_OUTPUT IN) : COLOR
{
    if(IN.cloudSpaceZCoordinate < 0)
    {
        IN.cloudTexCoordinate.x = 1 - IN.cloudTexCoordinate.x;
    }
    
    float4 diffuseColor = tex2D(textureSampler, IN.textureCoordinate);
    float4 cloudColor = tex2D(cloudSampler, IN.cloudTexCoordinate);
    
    float3 diffuseLight = float3(0,0,0);
    float cloudShadow = cloudColor.a;
    
    if(fx_Enabled_DL0)
    {
        diffuseLight = diffuseLight + saturate(dot(IN.f3Normal, fx_Direction_DL0))
								* fx_DiffuseColor_DL0;
    }
    
    diffuseLight = saturate(diffuseLight + fx_AmbientColor.rgb);
    
    if(cloudShadow < 0.99)
    {
        diffuseLight = diffuseLight * (1 - cloudShadow) + float4(0.2,0.25,0.32,0)
									* cloudShadow;
    }
    return saturate(diffuseColor * float4(diffuseLight.rgb, 0));
}

// the shader starts here
technique mytechnique
{
    pass p0
    {
        //compile vs
        vertexshader = compile vs_1_1 vertex_shader();    

        //compile ps
        pixelshader = compile ps_2_0 pixel_shader();    
    }
}

Light Mask Shader

Here is my first real (vertex + pixel) shader in it's entirety. There is nothing really amazing about it other than it does a lot of really common tasks that I think are worthwhile to show. I've found that very rarely do XNA developers post there whole shader. I think they break it up and explain the chunks to make it easier to read but sometimes the information you need is in the sections they leave out :-\ I'm afraid my code isn't commented very thoroughly. Here is a list of tasks this shader accomplishes. Hopefully this will make it clear what the code is doing.

  • Diffuse texture mapping
  • Diffuse illumination from directional light(s)
  • Specular illumination from directional light(s)
  • Ambient illumination
  • Reflections using an environemnt map (texCUBE)
  • Attenuated reflection based on grazing angle
  • Illumination directly from an incandesence map

HLSL
//transformation matrix
uniform extern float4x4 fx_World;
uniform extern float4x4 fx_WVP;
uniform extern float3    fx_ViewPosition;
uniform extern float3    fx_LocalViewPosition;

//directional lights
uniform extern bool fx_Enabled_DL0;
uniform extern float3 fx_Direction_DL0;
uniform extern float3 fx_DiffuseColor_DL0;

//ambient light
uniform extern float4 fx_AmbientColor;

//specular
uniform extern bool fx_SpecularOn;
uniform extern float4 fx_SpecularColor;
uniform extern float fx_SpecularPower;

//incandesence
uniform extern bool fx_LightMaskEnabled;
//incandesence texture for shader
uniform extern texture fx_LightMaskTexture;        

//diffuse
uniform extern float4 fx_DiffuseColor;
uniform extern bool fx_DiffuseEnabled;
// diffuse texture for shader
uniform extern texture fx_DiffuseTexture;

//reflection
uniform extern bool fx_ReflectionEnabled;
uniform extern float fx_ReflectionIntensity;
// reflection texture for shader
uniform extern texture fx_ReflectionTexture;

// diffuse texture filter
sampler2D diffuseSampler = sampler_state
{
    Texture = <fx_DiffuseTexture>;
    magfilter = LINEAR; // magfilter, bigger than real size
    minfilter = LINEAR; // minfilter, smaller than real size
    mipfilter = LINEAR;
};

// incandesence texture filter
sampler2D lightMaskSampler = sampler_state
{
    Texture = <fx_LightMaskTexture>;
    magfilter = LINEAR; // magfilter, bigger than real size
    minfilter = LINEAR; // minfilter, smaller than real size
    mipfilter = LINEAR;
};

// reflection texture filter
samplerCUBE reflectionSampler = sampler_state
{
    Texture = <fx_ReflectionTexture>;
};

// input to vertex shader
struct VS_INPUT
{
    float4 f4Position            : POSITION0;
    float4 f4Normal            : NORMAL;
    float2 textureCoordinate        : TEXCOORD0;
};

// vertex shader output
struct VS_OUTPUT
{
    float4 f4Position                : POSITION0;
    float4 f4WorldPos                : TEXCOORD0;
    float3 f3Normal                : TEXCOORD1;
    float4 f4Incident                : TEXCOORD2;
    float3 f3Reflection                : TEXCOORD3;
    float3 f3LocalReflection            : TEXCOORD4;
    float2 textureCoordinate            : TEXCOORD5;
    float  grazingRatio                : TEXCOORD6;
};


// alter vertex inputs
void vertex_shader(in VS_INPUT IN, out VS_OUTPUT OUT)
{
    //raster output positions
    OUT.f4Position = mul(IN.f4Position, fx_WVP);
    //world vertex position
    OUT.f4WorldPos = mul(IN.f4Position, fx_World);
    //world normal
    OUT.f3Normal = normalize(mul(float4(IN.f4Normal.x, IN.f4Normal.y, IN.f4Normal.z, 0.0),
								fx_World ).xyz);
    OUT.f4Incident = normalize(OUT.f4WorldPos - float4(fx_ViewPosition.xyz, 0.0));
    
    if(fx_SpecularOn)
    {
        OUT.f3Reflection = normalize( reflect( OUT.f4Incident, OUT.f3Normal )).xyz;
    }
    if(fx_ReflectionEnabled)
    {
        float4 localIncident = normalize(IN.f4Position - float4(fx_LocalViewPosition,0.0));
        OUT.f3LocalReflection = normalize( reflect( -localIncident , IN.f4Normal)).xyz;
        
        OUT.grazingRatio = max(0.2, 1 - pow(saturate(dot( OUT.f3LocalReflection,
					normalize(IN.f4Normal.xyz)) ), 2));
    }
    
    //UV texture coordinates
    OUT.textureCoordinate = IN.textureCoordinate;
}

// alter vertex shader output and send to hardware one pixel at a time
float4 pixel_shader(in VS_OUTPUT IN) : COLOR
{
    float4 diffuseColor = fx_DiffuseColor;
    
    if(fx_DiffuseEnabled)
    {
        diffuseColor = tex2D(diffuseSampler, IN.textureCoordinate);
    }
    
    float3 diffuseLight = float3(0,0,0);
    float specularIntensity = 0;
    
    if(fx_Enabled_DL0)
    {
        diffuseLight = diffuseLight + (dot(IN.f3Normal, fx_Direction_DL0) 
                * fx_DiffuseColor_DL0);
				
        if(fx_SpecularOn)
            specularIntensity = specularIntensity
                + pow(saturate(dot(IN.f3Reflection, fx_Direction_DL0)), fx_SpecularPower);
    }
        
    //incandesence
    float4 lightMaskContribution = float4(0,0,0,0);
    if(fx_LightMaskEnabled)
    {
      lightMaskContribution = tex2D(lightMaskSampler, IN.textureCoordinate) *
            (1 - min(0.85, max(max(diffuseLight.r, diffuseLight.g),diffuseLight.b)));
    }
    
    //reflections
    float4 reflectionContribution = float4(0,0,0,0);
    if(fx_ReflectionEnabled)
    {
      reflectionContribution = fx_ReflectionIntensity * IN.grazingRatio *
            texCUBE(reflectionSampler, IN.f3LocalReflection) * fx_SpecularColor;
    }
    
    return (diffuseColor * (lightMaskContribution + float4(diffuseLight.xyz, 1.0)
           + fx_AmbientColor)) + reflectionContribution
           + (fx_SpecularColor * specularIntensity);
}

//technique, main entry
technique t0
{
    pass p0
    {
        // declare and initialize vs
        vertexshader = compile vs_1_1 vertex_shader();    

        // declare and initialize ps
        pixelshader = compile ps_3_0 pixel_shader();    
    }
}
								

Handling Transformed Billboarded Sprites

The problem at hand is that a sprite that needs to be billboarded needs to be rotated so that it is facing the camera. However if there are other rotations that happen to that billboard as well it becomes difficult to know how to construct the billboard matrix to account for this. My current solution is to traverse up the scene graph through all of the parent transforms of the sprite accumulating any rotations that are applied. By combing the inverse of those rotation matrices to the billboard matrix the sprite will rotated to always face the camera.

C#
if (mBillBoard && passNumber == 0)
{
    Matrix lookAt = Matrix.Identity;
    lookAt.Forward = Vector3.Normalize(curGame.ActiveCam.WorldPosition - 
							mParentMatrix.Translation);
    lookAt.Right = Vector3.Normalize(Vector3.Cross(lookAt.Forward,
							curGame.ActiveCam.WorldUp));
    lookAt.Up = Vector3.Cross(lookAt.Forward, lookAt.Right);
 
    //undo any rotations by parent transform nodes
    SceneItem curItem = this;
    Matrix reverseRotate = Matrix.Identity;
    while(curItem.HasParent)
    {
        curItem = curItem.Parent;
        if (curItem is TransformNode)
        {
            reverseRotate = Matrix.Invert((curItem as TransformNode).rotateMat) *
									reverseRotate;
        }
    }
 
    mWorldMatrix = lookAt * reverseRotate;
    mCumulativeMatrix = mWorldMatrix * mParentMatrix;
    base.WorldMatrix = mCumulativeMatrix;
}

Parsing Geometry data from the XNA Model class object

I found it frustrating that the geometry data is not particullarly accessible to the developer. As a result there is no way to test a ray intersection with the model. This test is important if you ever want the game to respond to the user mousing over or clicking on a 3D object. The developers at Microsoft recommend catching this data before it is loaded and written into the darkest dungeon of a memory buffer. I'm sure it is good advice; however, I wanted to learn more about the actual mechanics of the XNA data structures so I decided to parse it out of them by force. Like Robin Hood breaking Maid Marion out of jail... Is that how that story goes?

In terms of performance we only need to read this data once after the model has finished loading so it is not a big issue there. The difficulty comes in when you consider that the vertex data could be in several different formats. The code below shows how to access the vertex data if it is in a VertexPositionColorTexture data structure and if the vertex indicies are integer values. I'm afraid the only way I could find to handle the alternatives (i.e. short indicies and VertexPositionColor, VertexPositionNormalTexture, etc) I had to create copies of this function using the alternate types. For space I haven't included the code for the other format, but you can imagine it looks the same with "int" replaced by "short" and/or "VertexPositionColorTexture" replaced with one of the other possible Vertex memory structures, of which there are four that come with the XNA framework.

C#
//get data from vertex buffer
VertexPositionColorTexture[] vertexData = 
				new VertexPositionColorTexture[meshPart.NumVertices];
				
int numVerts = meshPart.PrimitiveCount * 3;
mesh.VertexBuffer.GetData<VertexPositionColorTexture>(meshPart.BaseVertex * 
							meshPart.VertexStride,
							vertexData,
							0,
							meshPart.NumVertices,
							meshPart.VertexStride);
Triangle newTri;

if (indexType == IndexType.Short)
{
    //get indices from index buffer
    short[] indices = new short[numVerts];
    mesh.IndexBuffer.GetData<short>(meshPart.StartIndex * sizeof(short),
								indices,
								0,
								numVerts);
										
    for (int x = 0; x < numVerts; x += 3)
    {
        newTri = new Triangle(vertexData[(int)indices[x]].Position,
				vertexData[(int)indices[x + 1]].Position,
				vertexData[(int)indices[x + 2]].Position);
        newTri.Parent = this;
        localShape[index].Add(newTri);
    }
}
else
{
    //get indices from index buffer
    int[] indices = new int[numVerts];
    mesh.IndexBuffer.GetData<int>(meshPart.StartIndex * sizeof(int),
								indices,
								0,
								numVerts);
								
    for (int x = 0; x < numVerts; x += 3)
    {
        newTri = new Triangle(vertexData[(int)indices[x]].Position, 
				vertexData[(int)indices[x + 1]].Position,
				vertexData[(int)indices[x + 2]].Position);
        newTri.Parent = this;
        localShape[index].Add(newTri);
    }
}

Before you can parse the geometry in the function above, you have to figure out what type of Vertex data structure you are using. To do that I wrote the code below. It is a bit ugly perhaps, but it get's the job done. Again, you should note that this only checks for the four vertex data structures that are included with XNA. There can exist many other formats, but they should exist only out of the developers specefic needs.

C#
for (int partCount = 0; partCount < mesh.MeshParts.Count; partCount++)
{
    VertexElement[] vElements =
			mesh.MeshParts[partCount].VertexDeclaration.GetVertexElements();
 
    //possible vertex element usages...
    Boolean bin = false; Boolean ble = false; Boolean blw = false;
    Boolean col = false; Boolean dep = false; Boolean fog = false;
    Boolean nor = false; Boolean poi = false; Boolean pos = false;
    Boolean sam = false; Boolean tan = false; Boolean tes = false;
    Boolean tex = false;
 
    foreach (VertexElement ve in vElements)
    {
        switch (ve.VertexElementUsage)
        {
            case VertexElementUsage.Binormal:
                bin = true;
                break;
            case VertexElementUsage.BlendIndices:
                ble = true;
                break;
            case VertexElementUsage.BlendWeight:
                blw = true;
                break;
            case VertexElementUsage.Color:
                col = true;
                break;
            case VertexElementUsage.Depth:
                dep = true;
                break;
            case VertexElementUsage.Fog:
                fog = true;
                break;
            case VertexElementUsage.Normal:
                nor = true;
                break;
            case VertexElementUsage.PointSize:
                poi = true;
                break;
            case VertexElementUsage.Position:
                pos = true;
                break;
            case VertexElementUsage.Sample:
                sam = true;
                break;
            case VertexElementUsage.Tangent:
                tan = true;
                break;
            case VertexElementUsage.TessellateFactor:
                tes = true;
                break;
            case VertexElementUsage.TextureCoordinate:
                tex = true;
                break;
            default:
                //Unknown Vertex Element type
                break;
        } //end switch
    }//end for each vertex element
 
    switch (vElements.Length)
    {
        case 1:
            //no single vertex element type
            break;
        case 2:
            if (pos && col)
                parsePositionColorData(mesh, mesh.MeshParts[partCount],
						indexType, index);
            else if (pos && tex)
                parsePositionTextureData(mesh, mesh.MeshParts[partCount],
						indexType, index);
            break;
        case 3:
            if (pos && col && tex)
                parsePositionColorTextureData(mesh, mesh.MeshParts[partCount],
						indexType, index);
            else if (pos && nor && tex)
                parsePositionNormalTextureData(mesh, mesh.MeshParts[partCount],
						indexType, index);
            break;
        case 4:
            //no 4 element vertex type
            break;
        default:
            break;
    }
}