//----------------------------------------------------------------------------- // 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); } } } }