ModelMeshRenderer.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. using Microsoft.Xna.Framework.Graphics;
  2. using System.Numerics;
  3. using Shooter.Core.Plugins.Graphics;
  4. namespace Shooter.Core.Components;
  5. /// <summary>
  6. /// Component for rendering MonoGame Model files (FBX imported through Content Pipeline).
  7. ///
  8. /// UNITY COMPARISON:
  9. /// Similar to Unity's combination of:
  10. /// - MeshFilter (holds mesh data)
  11. /// - MeshRenderer (renders the mesh)
  12. /// - Model Importer (FBX import pipeline)
  13. ///
  14. /// Unity automatically handles FBX imports and creates GameObjects with MeshRenderer.
  15. /// In MonoGame, we:
  16. /// 1. Import FBX through Content.mgcb
  17. /// 2. Load Model with Content.Load&lt;Model&gt;()
  18. /// 3. Manually render each mesh in the model
  19. ///
  20. /// EDUCATIONAL NOTE - WHY SEPARATE FROM MeshRenderer?
  21. ///
  22. /// MeshRenderer uses our custom Mesh class (for procedural geometry like cubes/spheres).
  23. /// ModelMeshRenderer uses MonoGame's Model class (for imported FBX files).
  24. ///
  25. /// These are different systems:
  26. /// - Custom Mesh: We manually create vertices and indices
  27. /// - Model: MonoGame's Content Pipeline processes FBX and creates optimized data
  28. ///
  29. /// Unity hides this distinction. MonoGame exposes it for more control.
  30. /// </summary>
  31. public class ModelMeshRenderer : EntityComponent
  32. {
  33. private Model? _model;
  34. private Vector4 _tintColor = Vector4.One; // White = no tint
  35. private bool _visible = true;
  36. private float _scale = 1.0f;
  37. /// <summary>
  38. /// The MonoGame Model to render.
  39. /// Load this from the Content Pipeline, e.g.:
  40. /// renderer.Model = Content.Load&lt;Model&gt;("Models/Mesh_Weapon_Primary");
  41. /// </summary>
  42. public Model? Model
  43. {
  44. get => _model;
  45. set => _model = value;
  46. }
  47. /// <summary>
  48. /// Tint color applied to all meshes in the model.
  49. /// Use Vector4(1,1,1,1) for white (no tint).
  50. ///
  51. /// EDUCATIONAL NOTE - TINTING:
  52. /// Tinting multiplies the model's texture colors by this color.
  53. /// - Vector4(1, 0, 0, 1) = red tint
  54. /// - Vector4(0.5, 0.5, 0.5, 1) = darken 50%
  55. /// - Vector4(2, 2, 2, 1) = brighten 2x (emissive effect)
  56. ///
  57. /// Unity has similar functionality in the Material color property.
  58. /// </summary>
  59. public Vector4 TintColor
  60. {
  61. get => _tintColor;
  62. set => _tintColor = value;
  63. }
  64. /// <summary>
  65. /// Whether this model is currently visible.
  66. /// Similar to MeshRenderer.enabled in Unity.
  67. /// </summary>
  68. public bool Visible
  69. {
  70. get => _visible;
  71. set => _visible = value;
  72. }
  73. /// <summary>
  74. /// Uniform scale applied to the model.
  75. /// For non-uniform scaling, use the Transform3D component's scale property.
  76. ///
  77. /// EDUCATIONAL NOTE:
  78. /// Sometimes imported models are too large or small.
  79. /// Use this for quick adjustments without re-exporting from Unity.
  80. ///
  81. /// Example:
  82. /// - Unity model is 10 units tall
  83. /// - MonoGame needs 2 units
  84. /// - Set Scale = 0.2f
  85. /// </summary>
  86. public float Scale
  87. {
  88. get => _scale;
  89. set => _scale = value;
  90. }
  91. public override void Initialize()
  92. {
  93. base.Initialize();
  94. // Model is set by code or loaded from Content Manager
  95. }
  96. /// <summary>
  97. /// Render the model using MonoGame's BasicEffect.
  98. /// Called by the ForwardGraphicsProvider during the render pass.
  99. ///
  100. /// EDUCATIONAL NOTE - MODEL RENDERING LOOP:
  101. ///
  102. /// A Model contains multiple ModelMeshes (for complex objects).
  103. /// Each ModelMesh contains multiple ModelMeshParts (for different materials).
  104. ///
  105. /// Example FBX structure:
  106. /// Model "HoverBot"
  107. /// ├── ModelMesh "Body"
  108. /// │ ├── ModelMeshPart (Metal material)
  109. /// │ └── ModelMeshPart (Glass material)
  110. /// └── ModelMesh "Eyes"
  111. /// └── ModelMeshPart (Emissive material)
  112. ///
  113. /// We iterate through all parts and draw them with the correct transforms.
  114. ///
  115. /// COMMON UNITY-TO-MONOGAME PORTING ISSUE - TRANSFORM MATRICES:
  116. ///
  117. /// Unity automatically applies GameObject.transform to all renderers.
  118. /// In MonoGame, YOU must build the world matrix from your entity's transform.
  119. ///
  120. /// If models render at world origin (0,0,0) instead of their entity positions:
  121. /// → Check that you're building the SRT matrix (Scale-Rotation-Translation)
  122. /// → Matrix multiplication order matters: Scale * Rotation * Translation
  123. /// → Remember to actually USE the entity's Position and Rotation!
  124. ///
  125. /// This is one of the most common issues when porting from Unity.
  126. /// Unity hides this complexity; MonoGame exposes it for performance and control.
  127. /// </summary>
  128. public void Draw(GraphicsDevice graphicsDevice, Matrix4x4 world, Matrix4x4 view, Matrix4x4 projection)
  129. {
  130. if (!_visible || _model == null)
  131. return;
  132. // Get transform from owner entity
  133. var transform = Owner?.GetComponent<Transform3D>();
  134. if (transform == null)
  135. return;
  136. // Build world matrix: Scale -> Rotation -> Translation (SRT order)
  137. var scaleMatrix = Matrix4x4.CreateScale(_scale);
  138. var rotationMatrix = Matrix4x4.CreateFromQuaternion(transform.Rotation);
  139. var translationMatrix = Matrix4x4.CreateTranslation(transform.Position);
  140. // Combine: First scale, then rotate, then translate (right-to-left multiplication)
  141. var entityWorld = scaleMatrix * rotationMatrix * translationMatrix;
  142. var finalWorld = entityWorld * world;
  143. // Convert System.Numerics matrices to MonoGame matrices for BasicEffect
  144. var mgWorld = ToXnaMatrix(finalWorld);
  145. var mgView = ToXnaMatrix(view);
  146. var mgProjection = ToXnaMatrix(projection);
  147. // Draw each mesh in the model
  148. foreach (var mesh in _model.Meshes)
  149. {
  150. // Each mesh has its own transformation within the model
  151. var meshWorld = mesh.ParentBone.Transform * mgWorld;
  152. foreach (var meshPart in mesh.MeshParts)
  153. {
  154. var effect = meshPart.Effect as BasicEffect;
  155. if (effect != null)
  156. {
  157. // Set transformation matrices
  158. effect.World = meshWorld;
  159. effect.View = mgView;
  160. effect.Projection = mgProjection;
  161. // Apply tint color to diffuse
  162. effect.DiffuseColor = new Microsoft.Xna.Framework.Vector3(
  163. _tintColor.X,
  164. _tintColor.Y,
  165. _tintColor.Z
  166. );
  167. effect.Alpha = _tintColor.W;
  168. // Enable default lighting (MonoGame's BasicEffect has simple lighting)
  169. effect.EnableDefaultLighting();
  170. effect.PreferPerPixelLighting = true;
  171. // Apply the effect and draw
  172. foreach (var pass in effect.CurrentTechnique.Passes)
  173. {
  174. pass.Apply();
  175. graphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
  176. graphicsDevice.Indices = meshPart.IndexBuffer;
  177. graphicsDevice.DrawIndexedPrimitives(
  178. PrimitiveType.TriangleList,
  179. 0,
  180. meshPart.StartIndex,
  181. meshPart.PrimitiveCount
  182. );
  183. }
  184. }
  185. }
  186. }
  187. }
  188. /// <summary>
  189. /// Convert System.Numerics.Matrix4x4 to Microsoft.Xna.Framework.Matrix.
  190. ///
  191. /// EDUCATIONAL NOTE - WHY TWO MATRIX TYPES?
  192. ///
  193. /// As explained in UnityToMonoGame.md, we use System.Numerics throughout our codebase
  194. /// for SIMD performance and BepuPhysics compatibility.
  195. ///
  196. /// But MonoGame's BasicEffect requires Microsoft.Xna.Framework.Matrix.
  197. /// This conversion only happens at the rendering boundary.
  198. ///
  199. /// The overhead is minimal compared to the benefits of SIMD math everywhere else.
  200. /// </summary>
  201. private Microsoft.Xna.Framework.Matrix ToXnaMatrix(Matrix4x4 matrix)
  202. {
  203. return new Microsoft.Xna.Framework.Matrix(
  204. matrix.M11, matrix.M12, matrix.M13, matrix.M14,
  205. matrix.M21, matrix.M22, matrix.M23, matrix.M24,
  206. matrix.M31, matrix.M32, matrix.M33, matrix.M34,
  207. matrix.M41, matrix.M42, matrix.M43, matrix.M44
  208. );
  209. }
  210. /// <summary>
  211. /// Convenience method to load a model from the Content Manager.
  212. ///
  213. /// UNITY COMPARISON:
  214. /// In Unity, you drag-drop an FBX into a prefab and it's ready.
  215. /// In MonoGame, you:
  216. /// 1. Add FBX to Content.mgcb
  217. /// 2. Build content
  218. /// 3. Call this method to load at runtime
  219. ///
  220. /// Example usage:
  221. /// var renderer = entity.AddComponent&lt;ModelMeshRenderer&gt;();
  222. /// renderer.LoadModel(contentManager, "Models/Mesh_Weapon_Primary");
  223. /// </summary>
  224. public void LoadModel(Microsoft.Xna.Framework.Content.ContentManager content, string assetName)
  225. {
  226. try
  227. {
  228. _model = content.Load<Model>(assetName);
  229. Console.WriteLine($"[ModelMeshRenderer] Loaded model: {assetName}");
  230. }
  231. catch (Exception ex)
  232. {
  233. Console.WriteLine($"[ModelMeshRenderer] Failed to load model '{assetName}': {ex.Message}");
  234. _model = null;
  235. }
  236. }
  237. /// <summary>
  238. /// Get the bounding sphere for the entire model.
  239. /// Useful for culling and distance calculations.
  240. ///
  241. /// UNITY COMPARISON:
  242. /// Similar to MeshRenderer.bounds in Unity.
  243. /// Unity calculates bounds automatically.
  244. /// MonoGame stores it in the model data.
  245. /// </summary>
  246. public Microsoft.Xna.Framework.BoundingSphere? GetBoundingSphere()
  247. {
  248. if (_model == null || _model.Meshes.Count == 0)
  249. return null;
  250. // Get the first mesh's bounding sphere as approximation
  251. return _model.Meshes[0].BoundingSphere;
  252. }
  253. }