//----------------------------------------------------------------------------- // Model.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 RacingGame.GameLogic; using RacingGame.Helpers; using RacingGame.Shaders; using RacingGame.Tracks; using XnaModel = Microsoft.Xna.Framework.Graphics.Model; using System.IO; namespace RacingGame.Graphics { /// /// Model class for loading and displaying x files including all materials /// and textures. Provides load and render functions to render static /// non animated models (mostly 1 mesh), for animated models we need a more /// advanced class, which is not required for this game yet. /// public class Model : IDisposable { /// /// Name of this model, also used to load it from the content system. /// string name = ""; /// /// Underlying xna model object. Loaded with the content system. /// XnaModel xnaModel = null; /*not longer required /// /// Scaling factor from 3ds max to our engine (1 unit = 1 meter) /// const float MaxModelScaling = 1.0f; */ /// /// Default object matrix to fix models from 3ds max to our engine! /// static readonly Matrix objectMatrix = //right handed models: Matrix.CreateRotationX(MathHelper.Pi);// * //Matrix.CreateScale(MaxModelScaling); // left handed models (else everything is mirrored with x files) Matrix.CreateRotationX(MathHelper.Pi / 2.0f); /// /// Transform matrices for this model, used in all Render methods, /// build once in the constructor, it never changes and none of our /// models are animated. /// Matrix[] transforms = null; /// /// Scaling for this object, used for distance comparisons. /// float realScaling = 1.0f, scaling = 1.0f; /// /// Does this model has alpha textures? Then render with alpha blending /// turned on. This is usually false and rendering is faster without /// alpha blending. Also used to skip shadow receiving, which looks /// strange on palms. /// bool hasAlpha = false; /// /// Is this the car model? Set in constructor and used in the render /// methods, this way we can compare much faster when rendering! /// bool isCar = false; /// /// If we want to animated some mesh in the model, just set the /// modelmesh here. Used for the windmill, which is rotated in /// Render! /// ModelMesh animatedMesh = null; /// /// Cached effect parameters to improve performance. /// For around 100 000 objects we save 1 second per effect parameters /// call. We only save the world matrix, worldViewProj matrix, /// viewInverse matrix and the lightDir vector effect parameters. /// Update: We also save diffuseTexture, ambientColor and diffuseColor now /// List cachedEffectParameters = new List(); /// /// Another helper to check if the effect technique is /// "ReflectionSpecular". Checking this each frame takes a lot of time, /// this helper does the check only once in the constuctor. /// Used in RenderCar! /// List cachedIsReflectionSpecularTechnique = new List(); /// /// Renderable meshes dictionary. Used to render every RenderableMesh /// in our render method. /// Dictionary renderableMeshes = new Dictionary(); /// /// Name for this model, this is the content name. /// /// String public string Name { get { return name; } } /// /// Size /// /// Float public float Size { get { return realScaling; } } /// /// Number of mesh parts /// /// Int public int NumOfMeshParts { get { int ret = 0; for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) ret += xnaModel.Meshes[meshNum].MeshParts.Count; return ret; } } /// /// Create model /// /// Set model name public Model(string setModelName) { name = setModelName; xnaModel = BaseGame.Content.Load( Path.Combine(Directories.ContentDirectory, "Models", name)); // Get matrix transformations of the model // Has to be done only once because we don't use animations in our game. if (xnaModel != null) { transforms = new Matrix[xnaModel.Bones.Count]; xnaModel.CopyAbsoluteBoneTransformsTo(transforms); // Calculate scaling for this object, used for distance comparisons. if (xnaModel.Meshes.Count > 0) realScaling = scaling = xnaModel.Meshes[0].BoundingSphere.Radius * transforms[0].Right.Length(); // For palms, laterns, holders and column holders reduce scaling // to reduce the number of objects we have to render. if (name.ToLower() == "alphapalm" || name.ToLower() == "alphapalm2" || name.ToLower() == "alphapalm3" || name.ToLower() == "roadcolumnsegment") scaling *= 0.75f; // Hotels and windmills should always be visible (they are big) if (name.ToLower() == "hotel01" || name.ToLower() == "hotel02" || name.ToLower() == "casino01" || name.ToLower() == "windmill") scaling *= 5.0f; else // Don't use more than 3m for scaling and checking smaller objects if (scaling > 3) scaling = 3; } hasAlpha = name.ToLower().StartsWith("alpha"); // Is this a sign or banner? Then make sure ambient is pretty high! bool isSign = name.ToLower().StartsWith("sign") || name.ToLower().StartsWith("banner") || // Also include windmills, they are too dark name.ToLower().StartsWith("windmill"); //name.StartsWith("StartLight"); isCar = (name.ToLower() == "car"); // Go through all meshes in the model for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) { ModelMesh mesh = xnaModel.Meshes[meshNum]; int meshPartNum = 0; string meshName = mesh.Name; // Remember this mesh for animations done in Render! if (name.ToLower() == "windmill" && meshName.ToLower().StartsWith("windmill_wings")) animatedMesh = mesh; // And for each effect this mesh uses (usually just 1, multimaterials // are nice in 3ds max, but not efficiently for rendering stuff). for (int effectNum = 0; effectNum < mesh.Effects.Count; effectNum++) { Effect effect = mesh.Effects[effectNum]; // MonoGame's XImporter produces BasicEffect for .x files; in that case // cache parameters from the NormalMapping shader that actually renders them. Effect paramSource = (effect is BasicEffect) ? ShaderEffect.normalMapping.Effect : effect; // Store our effect parameters cachedEffectParameters.Add(paramSource.Parameters["diffuseTexture"]); cachedEffectParameters.Add(paramSource.Parameters["ambientColor"]); cachedEffectParameters.Add(paramSource.Parameters["diffuseColor"]); cachedEffectParameters.Add(paramSource.Parameters["world"]); cachedEffectParameters.Add(paramSource.Parameters["viewProj"]); cachedEffectParameters.Add(paramSource.Parameters["viewInverse"]); cachedEffectParameters.Add(paramSource.Parameters["lightDir"]); // Store if this is a "ReflectionSpecular" technique. // For BasicEffect fallback, identify glass by mesh name. cachedIsReflectionSpecularTechnique.Add( (effect is BasicEffect) ? mesh.Name.ToLower().StartsWith("glass") : effect.CurrentTechnique.Name.Contains("ReflectionSpecular")); // Increase ambient value to 0.5, 0.5, 0.5 for signs and banners! if (isSign && effect.Parameters["ambientColor"] != null) effect.Parameters["ambientColor"].SetValue( new Color(128, 128, 128).ToVector4()); // Car only uses alpha on the glass if (isCar && !mesh.Name.StartsWith("glass") && effect.Parameters["UseAlpha"] != null) effect.Parameters["UseAlpha"].SetValue(false); // Get technique from meshName int techniqueIndex = -1; if (meshName.Length > meshPartNum) { string techniqueNumberString = meshName.Substring( meshName.Length - (1 + meshPartNum), 1); #if !XBOX360 // Faster and does not throw an exception! int.TryParse(techniqueNumberString, out techniqueIndex); #else try { techniqueIndex = Convert.ToInt32(techniqueNumberString); } catch { } #endif } // No technique found or invalid? if (techniqueIndex < 0 || techniqueIndex >= effect.Techniques.Count) { // Try to use last technique techniqueIndex = effect.Techniques.Count - 1; // If this is NormalMapping, use DiffuseSpecular20 instead // of the last technique (which is SpecularWithReflection20) /*if (effect.Techniques[techniqueIndex].Name.Contains( "SpecularWithReflection")) techniqueIndex -= 2; // Update: We have now 2 more techniques (ReflectionSpecular) if (effect.Techniques[techniqueIndex].Name.Contains( "ReflectionSpecular")) techniqueIndex -= 4;*/ } // Set current technique for rendering below effect.CurrentTechnique = effect.Techniques[techniqueIndex]; // Next mesh part meshPartNum++; } // Add all mesh parts! for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++) { ModelMeshPart part = mesh.MeshParts[partNum]; // The model mesh part is not really used, we just extract the // index and vertex buffers and all the render data. // Material settings are build from the effect settings. // Also add this to our own dictionary for rendering. renderableMeshes.Add(part, BaseGame.MeshRenderManager.Add( part.VertexBuffer, part.IndexBuffer, part, part.Effect)); } } #if DEBUG // Check if there are no meshes to render if (xnaModel.Meshes.Count == 0) throw new ArgumentException("Invalid model "+name+ ". It does not contain any meshes"); #endif } /// /// Dispose /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Dispose /// /// Disposing protected virtual void Dispose(bool disposing) { if (disposing) { // Just set everything to null so we stop using this! name = ""; xnaModel = null; transforms = null; animatedMesh = null; } } /// /// Default view distance optimizer is at 250m, then skip stuff. /// This will be reduced as our framerate runs low to improve performance /// on low end systems. /// static int maxViewDistance = 200; /// /// Maximum view distance /// /// Int public static int MaxViewDistance { get { return maxViewDistance; } set { // Only set if we reduce, don't increase again if it is running // a little faster for a short time. if (value < maxViewDistance) maxViewDistance = value; } } /// /// Render /// /// Render matrix public void Render(Matrix renderMatrix) { // Optimization to skip smaller objects, which are very far away! // Display 1 meter big objects only if in a distance of 250 meters! // Scaling is guessed by the length of the first vector in our matrix, // because we always use the same scaling for x, y, z this should be // correct! float maxDistance = maxViewDistance * scaling; float distanceSquared = Vector3.DistanceSquared( BaseGame.CameraPos, renderMatrix.Translation); if (distanceSquared > maxDistance * maxDistance) // Don't render, too far away! return; // Check out if object is behind us or not visible, then we can skip // rendering. This is the GREATEST performance gain in the whole game! // Object must be away at least 20 units! if (distanceSquared > 20 * 20 && // And the object size must be small distanceSquared > (10 * scaling) * (10 * scaling)) { Vector3 objectDirection = Vector3.Normalize(BaseGame.CameraPos - renderMatrix.Translation); // Half field of view should be fov / 2, but because of // the aspect ratio (1.33) and an additional offset we need // to include to see objects at the borders. float objAngle = Vector3Helper.GetAngleBetweenVectors( BaseGame.CameraRotation, objectDirection); if (objAngle > BaseGame.ViewableFieldOfView) // Skip. return; } // Multiply object matrix by render matrix, result is used multiple // times here. renderMatrix = objectMatrix * renderMatrix; // Go through all meshes in the model for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) { ModelMesh mesh = xnaModel.Meshes[meshNum]; // Assign world matrix Matrix worldMatrix = transforms[mesh.ParentBone.Index] * renderMatrix; // Got animation? if (animatedMesh == mesh) { worldMatrix = Matrix.CreateRotationZ( // Use pseudo number for this object for different rotations renderMatrix.Translation.Length() * 3 + renderMatrix.Determinant() * 5 + (1.0f + ((int)(renderMatrix.M42 * 33.3f) % 100) * 0.00123f) * BaseGame.TotalTime / 0.654f) * transforms[mesh.ParentBone.Index] * renderMatrix; } // Just add this world matrix to our render matrices for each part. for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++) { // Find mesh part in the renderableMeshes dictionary and add the // new render matrix to be picked up in the mesh rendering later. renderableMeshes[mesh.MeshParts[partNum]].renderMatrices.Add( worldMatrix); } } } /// /// Render /// /// Render position public void Render(Vector3 renderPos) { Render(Matrix.CreateTranslation(renderPos)); } /// /// Render car model with this seperate method because we /// render it in 2 steps, first the solid stuff, then the alpha glass. /// We also rotate the wheels around :) /// /// Car type number (0, 1 or 2) for the car /// texture /// Car color we are currently using. /// In the shadow car mode we render /// everything (including wheels and glass) with a special ShadowCar /// shader, that is very transparent. Used for the shadow car when /// playing that shows how we drove the last time. /// Render matrix for the car public void RenderCar(int carNumber, Color carColor, bool shadowCarMode, Matrix renderMatrix) { // Multiply object matrix by render matrix, result is used multiple // times here. renderMatrix = objectMatrix * renderMatrix; // Do we just want to render the shadow car? Then do this in a // simpified way here instead of messing with the already complicated // code below. if (shadowCarMode) { // Start shadow car shader ShaderEffect simpleShader = ShaderEffect.lighting; simpleShader.Render( "ShadowCar20", delegate { int wheelNumber = 0; // And just render all meshes with it! for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) { ModelMesh mesh = xnaModel.Meshes[meshNum]; Matrix meshMatrix = transforms[mesh.ParentBone.Index]; // Only the wheels have 2 mesh parts (gummi and chrome) if (mesh.MeshParts.Count == 2) { wheelNumber++; meshMatrix = Matrix.CreateRotationX( // Rotate left 2 wheels forward, the other 2 backward (wheelNumber == 2 || wheelNumber == 4 ? 1 : -1) * RacingGameManager.Player.CarWheelPos) * meshMatrix; } // Assign world matrix BaseGame.WorldMatrix = meshMatrix * renderMatrix; // Set all matrices simpleShader.SetParameters(); simpleShader.Update(); // And render (must be done without mesh.Draw, which would // just use the original shaders for the model) for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++) { ModelMeshPart part = mesh.MeshParts[partNum]; // Make sure vertex declaration is correct // Set vertex buffer and index buffer BaseGame.Device.SetVertexBuffer(part.VertexBuffer); BaseGame.Device.Indices = part.IndexBuffer; // And render all primitives BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, part.VertexOffset, part.StartIndex, part.PrimitiveCount); } } }); // And get outta here return; } // Usually use default color values Color ambientColor = Material.DefaultAmbientColor; Color diffuseColor = Material.DefaultDiffuseColor; EffectTechnique remCurrentTechnique = null; for (int alphaPass = 0; alphaPass < 2; alphaPass++) { int wheelNumber = 0; int effectParameterIndex = 0; int effectTechniqueIndex = 0; for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) { ModelMesh mesh = xnaModel.Meshes[meshNum]; bool dontRender = false; for (int effectNum = 0; effectNum < mesh.Effects.Count; effectNum++) { Effect effect = mesh.Effects[effectNum]; if (effectNum == 0) { // For BasicEffect fallback, remember the normalMapping technique remCurrentTechnique = (effect is BasicEffect) ? ShaderEffect.normalMapping.Effect.CurrentTechnique : effect.CurrentTechnique; } // Find out if this is ReflectionSimpleGlass.fx, // NormalMapping.fx will also use reflection, but the techniques // are named in another way (SpecularWithReflection, etc.) if (cachedIsReflectionSpecularTechnique[effectTechniqueIndex++]) { if (alphaPass == 0) { dontRender = true; effectParameterIndex += 7; break; } // Skip the first 3 effect parameters effectParameterIndex += 3; } else { if (alphaPass == 1) { dontRender = true; effectParameterIndex += 7; break; } // To improve performance we only have to set this when it // changes! Doesn't do much, because this eats only 10% // performance, 5-10% are the matrices below and most of the // performance is just rendering the car with Draw! // Overwrite car diffuse textures depending on the car number // we want to render. cachedEffectParameters[effectParameterIndex++].SetValue( RacingGameManager.CarTexture(carNumber).XnaTexture); // Also set color cachedEffectParameters[effectParameterIndex++].SetValue( ambientColor.ToVector4()); cachedEffectParameters[effectParameterIndex++].SetValue( diffuseColor.ToVector4()); // Change shader to SpecularWithReflectionForCar20 if we changed the color. if (RacingGameManager.currentCarColor != 0 && effectNum == 0) { // Route through normalMapping when effect is BasicEffect fallback Effect carEffect = (effect is BasicEffect) ? ShaderEffect.normalMapping.Effect : effect; carEffect.CurrentTechnique = carEffect.Techniques["SpecularWithReflectionForCar20"]; carEffect.Parameters["carHueColor"]?.SetValue( carColor.ToVector3()); } } Matrix meshMatrix = transforms[mesh.ParentBone.Index]; // Only the wheels have 2 mesh parts (gummi and chrome) if (mesh.MeshParts.Count == 2) { wheelNumber++; meshMatrix = Matrix.CreateRotationX( // Rotate left 2 wheels forward, the other 2 backward! (wheelNumber == 2 || wheelNumber == 4 ? 1 : -1) * RacingGameManager.Player.CarWheelPos) * meshMatrix; } // Assign world matrix BaseGame.WorldMatrix = meshMatrix * renderMatrix; // Set matrices cachedEffectParameters[effectParameterIndex++].SetValue( BaseGame.WorldMatrix); // These values should only be set once every frame (see above)! // to improve performance again, also we should access them // with EffectParameter and not via name! // But since we got only 1 car it doesn't matter so much .. cachedEffectParameters[effectParameterIndex++].SetValue( BaseGame.ViewProjectionMatrix); cachedEffectParameters[effectParameterIndex++].SetValue( BaseGame.InverseViewMatrix.Translation); // Set light direction if (cachedEffectParameters [effectParameterIndex] != null) { cachedEffectParameters [effectParameterIndex++].SetValue ( BaseGame.LightDirection); } else { effectParameterIndex++; } } // Render if (dontRender == false) { // For BasicEffect fallback, render manually through normalMapping shader if (mesh.Effects.Count > 0 && mesh.Effects[0] is BasicEffect) { ShaderEffect.normalMapping.Effect.CurrentTechnique.Passes[0].Apply(); for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++) { ModelMeshPart part = mesh.MeshParts[partNum]; BaseGame.Device.SetVertexBuffer(part.VertexBuffer); BaseGame.Device.Indices = part.IndexBuffer; BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, part.VertexOffset, part.StartIndex, part.PrimitiveCount); } } else { mesh.Draw(); } } // Change shader back to default render technique. // We only have to do this if the color was changed if (RacingGameManager.currentCarColor != 0 && remCurrentTechnique != null) { Effect restoreEffect = (mesh.Effects.Count > 0 && mesh.Effects[0] is BasicEffect) ? ShaderEffect.normalMapping.Effect : mesh.Effects[0]; restoreEffect.CurrentTechnique = remCurrentTechnique; } } } } /// /// Generate shadow for this model in the generate shadow pass /// of our shadow mapping shader. All objects rendered here will /// cast shadows to our scene (if they are in range of the light) /// /// Render matrix public void GenerateShadow(Matrix renderMatrix) { // Find out how far the object is away from the shadow, // we can ignore it if it is outside of the shadow generation range. // Everything smaller than 0.5 meter can be ignored. float maxDistance = //nice, but not good for shadow mapping, have to use fixed value! scaling / 2.5f + 1.015f * ShaderEffect.shadowMapping.ShadowDistance; if (Vector3.DistanceSquared( ShaderEffect.shadowMapping.ShadowLightPos, renderMatrix.Translation) > maxDistance * maxDistance) // Don't render, too far away! return; // Multiply object matrix by render matrix. renderMatrix = objectMatrix * renderMatrix; for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) { ModelMesh mesh = xnaModel.Meshes[meshNum]; // Use the ShadowMapShader helper method to set the world matrices ShaderEffect.shadowMapping.UpdateGenerateShadowWorldMatrix( transforms[mesh.ParentBone.Index] * renderMatrix); // Got animation? if (animatedMesh == mesh) { ShaderEffect.shadowMapping.UpdateGenerateShadowWorldMatrix( Matrix.CreateRotationZ( // Use pseudo number for this object for different rotations renderMatrix.Translation.Length() * 3 + renderMatrix.Determinant() * 5 + (1.0f + ((int)(renderMatrix.M42 * 33.3f) % 100) * 0.00123f) * BaseGame.TotalTime / 0.654f) * transforms[mesh.ParentBone.Index] * renderMatrix); } for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++) { ModelMeshPart part = mesh.MeshParts[partNum]; // Render just the vertices, do not use the shaders of our model. // This is the same code as ModelMeshPart.Draw() uses, but // this method is internal and can't be used by us :( BaseGame.Device.SetVertexBuffer(part.VertexBuffer); BaseGame.Device.Indices = part.IndexBuffer; BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, part.VertexOffset, part.StartIndex, part.PrimitiveCount); } } } /// /// Use shadow for our scene. We render all objects that should receive /// shadows here. Called from the ShadowMappingShader.UseShadow method. /// /// Render matrix public void UseShadow(Matrix renderMatrix) { // If this is an object that uses alpha textures, never receive // shadows, this only causes troubes and needs a much more complex // shader. This affects usually only palms anyway, which look better // without shadowing on. if (hasAlpha) return; // Find out how far the object is away from the shadow, // we can ignore it if it is outside of the shadow generation range. // Everything smaller than 0.25 meter can be ignored. // Note: For receiving we usually use more objects than for generating // shadows. float maxDistance = //nice, but not good for shadow mapping, have to use fixed value! 1.015f * ShaderEffect.shadowMapping.ShadowDistance; if (Vector3.DistanceSquared( ShaderEffect.shadowMapping.ShadowLightPos, renderMatrix.Translation) > maxDistance * maxDistance) // Don't render, too far away! return; // Multiply object matrix by render matrix. renderMatrix = objectMatrix * renderMatrix; for (int meshNum = 0; meshNum < xnaModel.Meshes.Count; meshNum++) { ModelMesh mesh = xnaModel.Meshes[meshNum]; // Use the ShadowMapShader helper method to set the world matrices ShaderEffect.shadowMapping.UpdateCalcShadowWorldMatrix( transforms[mesh.ParentBone.Index] * renderMatrix); // Got animation? if (animatedMesh == mesh) { ShaderEffect.shadowMapping.UpdateCalcShadowWorldMatrix( Matrix.CreateRotationZ( // Use pseudo number for this object for different rotations renderMatrix.Translation.Length() * 3 + renderMatrix.Determinant() * 5 + (1.0f + ((int)(renderMatrix.M42 * 33.3f) % 100) * 0.00123f) * BaseGame.TotalTime / 0.654f) * transforms[mesh.ParentBone.Index] * renderMatrix); } for (int partNum = 0; partNum < mesh.MeshParts.Count; partNum++) { ModelMeshPart part = mesh.MeshParts[partNum]; // Render just the vertices, do not use the shaders of our model. // This is the same code as ModelMeshPart.Draw() uses, but // this method is internal and can't be used by us :( BaseGame.Device.SetVertexBuffer(part.VertexBuffer); BaseGame.Device.Indices = part.IndexBuffer; BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, part.VertexOffset, part.StartIndex, part.PrimitiveCount); } } } } }