//-----------------------------------------------------------------------------
// MeshRenderManager.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Text;
using RacingGame.GameLogic;
using RacingGame.Helpers;
using RacingGame.Shaders;
using RacingGame.Tracks;
namespace RacingGame.Graphics
{
///
/// Mesh render manager, a little helper class which allows us to render
/// all our models with much faster performance through sorting by
/// material and shader techniques.
///
/// We keep a list of lists of lists for rendering our RenderableMeshes
/// sorted by techniques first and materials second.
/// The most outer list contains all techniques (see
/// MeshesPerMaterialPerTechniques).
/// Then the first inner list contains all materials (see MeshesPerMaterial).
/// And finally the most inner list contains all meshes that use the
/// technique and material we got.
/// Additionally we could also sort by shaders, but since all models
/// use the same shader (normalMapping), that is the only thing we support
/// here. Improving it to support more shaders is easily possible.
///
/// All this is created in the Model constructor. At runtime we just
/// go through these lists and render everything down as quickly as
/// possible.
///
public class MeshRenderManager
{
///
/// Don't set vertex and index buffers again if they are already set this
/// frame.
///
static VertexBuffer lastVertexBufferSet = null;
static IndexBuffer lastIndexBufferSet = null;
///
/// Renderable mesh, created in the Model constructor and is rendered
/// when we render all the models at once at the end of each frame!
///
public class RenderableMesh
{
///
/// Vertex buffer
///
public VertexBuffer vertexBuffer;
///
/// Index buffer
///
public IndexBuffer indexBuffer;
///
/// Material
///
public Material material;
///
/// Used technique
///
public EffectTechnique usedTechnique;
///
/// World parameter
///
public EffectParameter worldParameter;
///
/// Stream offset, vertex stride, etc.
/// All parameters we need for rendering.
///
public int baseVertex, numVertices, startIndex, primitiveCount;
///
/// List of render matrices we use every frame. At creation time
/// this list is unused and empty, but for each frame we use this
/// list to remember which objects we want to render.
/// Of course rendering happens only if this list is not empty.
/// After each frame this list is cleared again.
///
public List renderMatrices = new List();
///
/// Create renderable mesh
///
/// Set vertex buffer
/// Set index buffer
/// Set material
/// Set used technique
/// Set world parameter
/// Set base vertex
/// Set number vertices
/// Set start index
/// Set primitive count
public RenderableMesh(VertexBuffer setVertexBuffer,
IndexBuffer setIndexBuffer, Material setMaterial,
EffectTechnique setUsedTechnique, EffectParameter setWorldParameter,
int setBaseVertex,
int setNumVertices, int setStartIndex, int setPrimitiveCount)
{
vertexBuffer = setVertexBuffer;
indexBuffer = setIndexBuffer;
material = setMaterial;
usedTechnique = setUsedTechnique;
worldParameter = setWorldParameter;
baseVertex = setBaseVertex;
numVertices = setNumVertices;
startIndex = setStartIndex;
primitiveCount = setPrimitiveCount;
}
///
/// Render this renderable mesh, MUST be called inside of the
/// render method of ShaderEffect.normalMapping!
///
/// World matrix
public void RenderMesh(Matrix worldMatrix)
{
// Update world matrix
ShaderEffect.normalMapping.WorldMatrix = worldMatrix;
ShaderEffect.normalMapping.Effect.CurrentTechnique.Passes[0].Apply();//.Update();
// Set vertex buffer and index buffer
if (lastVertexBufferSet != vertexBuffer ||
lastIndexBufferSet != indexBuffer)
{
lastVertexBufferSet = vertexBuffer;
lastIndexBufferSet = indexBuffer;
BaseGame.Device.SetVertexBuffer(vertexBuffer);
BaseGame.Device.Indices = indexBuffer;
}
// And render (this call takes the longest, we can't optimize
// it any further because the vertexBuffer and indexBuffer are
// WriteOnly, we can't combine it or optimize it any more).
BaseGame.Device.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
baseVertex, startIndex, primitiveCount);
}
///
/// Render
///
public void Render()
{
// Render all meshes we have requested this frame.
for (int matrixNum = 0; matrixNum < renderMatrices.Count; matrixNum++)
RenderMesh(renderMatrices[matrixNum]);
// Clear all meshes, don't render them again.
// Next frame everything will be created again.
renderMatrices.Clear();
}
}
///
/// Meshes per material
///
public class MeshesPerMaterial
{
///
/// Material
///
public Material material;
///
/// Meshes
///
public List meshes = new List();
///
/// Number of render matrices this material uses this frame.
///
/// Int
public int NumberOfRenderMatrices
{
get
{
int ret = 0;
for (int meshNum = 0; meshNum < meshes.Count; meshNum++)
ret += meshes[meshNum].renderMatrices.Count;
return ret;
}
}
///
/// Create meshes per material for the setMaterial.
///
/// Set material
public MeshesPerMaterial(Material setMaterial)
{
material = setMaterial;
}
///
/// Adds a renderable mesh using this material.
///
/// Add mesh
public void Add(RenderableMesh addMesh)
{
// Make sure this mesh uses the correct material
if (addMesh.material != material)
throw new ArgumentException("Invalid material, to add a mesh to " +
"MeshesPerMaterial it must use the specified material=" +
material);
meshes.Add(addMesh);
}
///
/// Render all meshes that use this material.
/// This method is only called if we got any meshes to render,
/// which is determinated if NumberOfRenderMeshes is greater 0.
///
public void Render()
{
// Set material settings. We don't have to update the shader here,
// it will be done in RenderableMesh.Render anyway because of
// updating the world matrix!
ShaderEffect.normalMapping.SetParametersOptimized(material);
// Set vertex declaration
//always true: if (meshes.Count > 0)
// Enable alpha if this material uses alpha
if (material.HasAlpha)
{
//TODO: AlphaTestEffect
//BaseGame.Device.RenderState.AlphaTestEnable = true;
//BaseGame.Device.RenderState.ReferenceAlpha = 128;
// Make 2sided, we use alpha mainly for our palms.
BaseGame.Device.RasterizerState = RasterizerState.CullNone;
}
// Render all meshes that use this material.
for (int meshNum = 0; meshNum < meshes.Count; meshNum++)
{
RenderableMesh mesh = meshes[meshNum];
if (mesh.renderMatrices.Count > 0)
mesh.Render();
}
// Disable alpha testing again and restore culling
if (material.HasAlpha)
{
//TODO: AlphaTestEffect
//BaseGame.Device.RenderState.AlphaTestEnable = false;
BaseGame.Device.RasterizerState = RasterizerState.CullCounterClockwise;
}
}
}
///
/// Meshes per material per techniques
///
public class MeshesPerMaterialPerTechniques
{
///
/// Technique
///
public EffectTechnique technique;
///
/// Meshes per materials
///
public List meshesPerMaterials =
new List();
///
/// Number of render matrices this technique uses this frame.
///
/// Int
public int NumberOfRenderMatrices
{
get
{
int ret = 0;
try
{
for (int listNum = 0; listNum < meshesPerMaterials.Count; listNum++)
ret += meshesPerMaterials[listNum].NumberOfRenderMatrices;
}
catch
{
}
return ret;
}
}
///
/// Create meshes per material per techniques
///
/// Set technique
public MeshesPerMaterialPerTechniques(EffectTechnique setTechnique)
{
technique = setTechnique;
}
///
/// Adds a renderable mesh using this technique.
///
/// Add mesh
public void Add(RenderableMesh addMesh)
{
// Make sure this mesh uses the correct material
if (addMesh.usedTechnique != technique)
throw new ArgumentException("Invalid technique, to add a mesh to " +
"MeshesPerMaterialPerTechniques it must use the specified " +
"technique=" + technique.Name);
// Search for the used material, maybe we have it already in list.
for (int listNum = 0; listNum < meshesPerMaterials.Count; listNum++)
{
MeshesPerMaterial existingList = meshesPerMaterials[listNum];
if (existingList.material == addMesh.material)
{
// Just add
existingList.Add(addMesh);
return;
}
}
// Not found, create new list and add mesh there.
MeshesPerMaterial newList = new MeshesPerMaterial(addMesh.material);
newList.Add(addMesh);
meshesPerMaterials.Add(newList);
}
///
/// Render all meshes that use this technique sorted by the materials.
/// This method is only called if we got any meshes to render,
/// which is determinated if NumberOfRenderMeshes is greater 0.
///
/// Effect
public void Render(Effect effect)
{
// Start effect for this technique
effect.CurrentTechnique = technique;
// Render all pass (we always just have one)
EffectPass pass = effect.CurrentTechnique.Passes[0];
pass.Apply();
// Render all meshes sorted by all materials.
for (int listNum = 0; listNum < meshesPerMaterials.Count; listNum++)
{
MeshesPerMaterial list = meshesPerMaterials[listNum];
if (list.NumberOfRenderMatrices > 0)
list.Render();
}
}
}
///
/// Sorted meshes we got. Everything is sorted by techniques and then
/// sorted by materials. This all happens at construction time.
/// For rendering use renderMatrices list, which is directly in the
/// most inner list of sortedMeshes (the RenderableMesh objects).
///
List sortedMeshes =
new List();
///
/// Add model mesh part with the used effect to our sortedMeshes list.
/// Neither the model mesh part nor the effect is directly used,
/// we will extract all data from the model and only render the
/// index and vertex buffers later.
/// The model mesh part must use the TangentVertex format.
///
/// Vertex buffer
/// Index buffer
/// Part
/// Effect
/// Renderable mesh
public RenderableMesh Add(VertexBuffer vertexBuffer,
IndexBuffer indexBuffer, ModelMeshPart part, Effect effect)
{
string techniqueName = effect.CurrentTechnique.Name;
// Does this technique already exist?
MeshesPerMaterialPerTechniques foundList = null;
for (int listNum = 0; listNum < sortedMeshes.Count; listNum++)
{
MeshesPerMaterialPerTechniques list = sortedMeshes[listNum];
if (list.technique.Name == techniqueName)
{
foundList = list;
break;
}
}
// Did not found list? Create new one
if (foundList == null)
{
EffectTechnique technique = ShaderEffect.normalMapping.GetTechnique(techniqueName);
if (technique == null)
{
// techniqueName came from a BasicEffect (MonoGame XImporter doesn't preserve
// EffectMaterialContent from .x files), so fall back to a valid NormalMapping technique.
technique = ShaderEffect.normalMapping.GetTechnique("DiffuseSpecular20");
}
foundList = new MeshesPerMaterialPerTechniques(technique);
sortedMeshes.Add(foundList);
}
// Create new material from the current effect parameters.
// This will create duplicate materials if the same material is used
// multiple times, we check this later.
Material material = new Material(effect);
// If the effect is a BasicEffect (MonoGame XImporter fallback), extract
// the texture from it since Material(Effect) only reads NormalMapping params.
if (material.diffuseTexture == null && effect is BasicEffect basicEffect &&
basicEffect.Texture != null)
{
material.diffuseTexture = new Texture(basicEffect.Texture);
}
// Search for material inside foundList.
for (int innerListNum = 0; innerListNum <
foundList.meshesPerMaterials.Count; innerListNum++)
{
MeshesPerMaterial innerList =
foundList.meshesPerMaterials[innerListNum];
// Check if this is the same material and we can use it instead.
// For our purposes it is sufficiant if we check textures and colors.
if (innerList.material.diffuseTexture == material.diffuseTexture &&
innerList.material.normalTexture == material.normalTexture &&
innerList.material.ambientColor == material.ambientColor &&
innerList.material.diffuseColor == material.diffuseColor &&
innerList.material.specularColor == material.specularColor &&
innerList.material.specularPower == material.specularPower)
{
// Reuse this material and quit this search
material = innerList.material;
break;
}
}
// Build new RenderableMesh object
RenderableMesh mesh = new RenderableMesh(
vertexBuffer, indexBuffer, material, foundList.technique,
ShaderEffect.normalMapping.WorldParameter, part.VertexOffset,
part.NumVertices, part.StartIndex, part.PrimitiveCount);
foundList.Add(mesh);
return mesh;
}
///
/// Render all meshes we collected this frame sorted by techniques
/// and materials. This method is about 3-5 times faster than just
/// using Model's Mesh.Draw method (see commented out code there).
/// The reason for that is that we require only very few state changes
/// and render everthing down as fast as we can. The only optimization
/// left would be to put vertices of several meshes together if they
/// are static and use the same technique and material. But since
/// meshes have WriteOnly vertex and index buffers, we can't do that
/// without using a custom model format.
///
public void Render()
{
// Make sure z buffer is on
BaseGame.Device.DepthStencilState = DepthStencilState.Default;
// We always use the normalMapping shader here.
Effect effect = ShaderEffect.normalMapping.Effect;
// Set general parameters for the shader
ShaderEffect.normalMapping.SetParametersOptimizedGeneral();
// Don't set vertex buffer again if it does not change this frame.
// Clear these remember settings.
lastVertexBufferSet = null;
lastIndexBufferSet = null;
for (int listNum = 0; listNum < sortedMeshes.Count; listNum++)
{
MeshesPerMaterialPerTechniques list = sortedMeshes[listNum];
if (list != null && list.NumberOfRenderMatrices > 0)
list.Render(effect);
}
}
}
}