using Microsoft.Xna.Framework.Graphics;
using System.Numerics;
using Shooter.Core.Plugins.Graphics;
namespace Shooter.Core.Components;
///
/// Component for rendering MonoGame Model files (FBX imported through Content Pipeline).
///
/// UNITY COMPARISON:
/// Similar to Unity's combination of:
/// - MeshFilter (holds mesh data)
/// - MeshRenderer (renders the mesh)
/// - Model Importer (FBX import pipeline)
///
/// Unity automatically handles FBX imports and creates GameObjects with MeshRenderer.
/// In MonoGame, we:
/// 1. Import FBX through Content.mgcb
/// 2. Load Model with Content.Load<Model>()
/// 3. Manually render each mesh in the model
///
/// EDUCATIONAL NOTE - WHY SEPARATE FROM MeshRenderer?
///
/// MeshRenderer uses our custom Mesh class (for procedural geometry like cubes/spheres).
/// ModelMeshRenderer uses MonoGame's Model class (for imported FBX files).
///
/// These are different systems:
/// - Custom Mesh: We manually create vertices and indices
/// - Model: MonoGame's Content Pipeline processes FBX and creates optimized data
///
/// Unity hides this distinction. MonoGame exposes it for more control.
///
public class ModelMeshRenderer : EntityComponent
{
private Model? _model;
private Vector4 _tintColor = Vector4.One; // White = no tint
private bool _visible = true;
private float _scale = 1.0f;
///
/// The MonoGame Model to render.
/// Load this from the Content Pipeline, e.g.:
/// renderer.Model = Content.Load<Model>("Models/Mesh_Weapon_Primary");
///
public Model? Model
{
get => _model;
set => _model = value;
}
///
/// Tint color applied to all meshes in the model.
/// Use Vector4(1,1,1,1) for white (no tint).
///
/// EDUCATIONAL NOTE - TINTING:
/// Tinting multiplies the model's texture colors by this color.
/// - Vector4(1, 0, 0, 1) = red tint
/// - Vector4(0.5, 0.5, 0.5, 1) = darken 50%
/// - Vector4(2, 2, 2, 1) = brighten 2x (emissive effect)
///
/// Unity has similar functionality in the Material color property.
///
public Vector4 TintColor
{
get => _tintColor;
set => _tintColor = value;
}
///
/// Whether this model is currently visible.
/// Similar to MeshRenderer.enabled in Unity.
///
public bool Visible
{
get => _visible;
set => _visible = value;
}
///
/// Uniform scale applied to the model.
/// For non-uniform scaling, use the Transform3D component's scale property.
///
/// EDUCATIONAL NOTE:
/// Sometimes imported models are too large or small.
/// Use this for quick adjustments without re-exporting from Unity.
///
/// Example:
/// - Unity model is 10 units tall
/// - MonoGame needs 2 units
/// - Set Scale = 0.2f
///
public float Scale
{
get => _scale;
set => _scale = value;
}
public override void Initialize()
{
base.Initialize();
// Model is set by code or loaded from Content Manager
}
///
/// Render the model using MonoGame's BasicEffect.
/// Called by the ForwardGraphicsProvider during the render pass.
///
/// EDUCATIONAL NOTE - MODEL RENDERING LOOP:
///
/// A Model contains multiple ModelMeshes (for complex objects).
/// Each ModelMesh contains multiple ModelMeshParts (for different materials).
///
/// Example FBX structure:
/// Model "HoverBot"
/// ├── ModelMesh "Body"
/// │ ├── ModelMeshPart (Metal material)
/// │ └── ModelMeshPart (Glass material)
/// └── ModelMesh "Eyes"
/// └── ModelMeshPart (Emissive material)
///
/// We iterate through all parts and draw them with the correct transforms.
///
/// COMMON UNITY-TO-MONOGAME PORTING ISSUE - TRANSFORM MATRICES:
///
/// Unity automatically applies GameObject.transform to all renderers.
/// In MonoGame, YOU must build the world matrix from your entity's transform.
///
/// If models render at world origin (0,0,0) instead of their entity positions:
/// → Check that you're building the SRT matrix (Scale-Rotation-Translation)
/// → Matrix multiplication order matters: Scale * Rotation * Translation
/// → Remember to actually USE the entity's Position and Rotation!
///
/// This is one of the most common issues when porting from Unity.
/// Unity hides this complexity; MonoGame exposes it for performance and control.
///
public void Draw(GraphicsDevice graphicsDevice, Matrix4x4 world, Matrix4x4 view, Matrix4x4 projection)
{
if (!_visible || _model == null)
return;
// Get transform from owner entity
var transform = Owner?.GetComponent();
if (transform == null)
return;
// Build world matrix: Scale -> Rotation -> Translation (SRT order)
var scaleMatrix = Matrix4x4.CreateScale(_scale);
var rotationMatrix = Matrix4x4.CreateFromQuaternion(transform.Rotation);
var translationMatrix = Matrix4x4.CreateTranslation(transform.Position);
// Combine: First scale, then rotate, then translate (right-to-left multiplication)
var entityWorld = scaleMatrix * rotationMatrix * translationMatrix;
var finalWorld = entityWorld * world;
// Convert System.Numerics matrices to MonoGame matrices for BasicEffect
var mgWorld = ToXnaMatrix(finalWorld);
var mgView = ToXnaMatrix(view);
var mgProjection = ToXnaMatrix(projection);
// Draw each mesh in the model
foreach (var mesh in _model.Meshes)
{
// Each mesh has its own transformation within the model
var meshWorld = mesh.ParentBone.Transform * mgWorld;
foreach (var meshPart in mesh.MeshParts)
{
var effect = meshPart.Effect as BasicEffect;
if (effect != null)
{
// Set transformation matrices
effect.World = meshWorld;
effect.View = mgView;
effect.Projection = mgProjection;
// Apply tint color to diffuse
effect.DiffuseColor = new Microsoft.Xna.Framework.Vector3(
_tintColor.X,
_tintColor.Y,
_tintColor.Z
);
effect.Alpha = _tintColor.W;
// Enable default lighting (MonoGame's BasicEffect has simple lighting)
effect.EnableDefaultLighting();
effect.PreferPerPixelLighting = true;
// Apply the effect and draw
foreach (var pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
graphicsDevice.Indices = meshPart.IndexBuffer;
graphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
0,
meshPart.StartIndex,
meshPart.PrimitiveCount
);
}
}
}
}
}
///
/// Convert System.Numerics.Matrix4x4 to Microsoft.Xna.Framework.Matrix.
///
/// EDUCATIONAL NOTE - WHY TWO MATRIX TYPES?
///
/// As explained in UnityToMonoGame.md, we use System.Numerics throughout our codebase
/// for SIMD performance and BepuPhysics compatibility.
///
/// But MonoGame's BasicEffect requires Microsoft.Xna.Framework.Matrix.
/// This conversion only happens at the rendering boundary.
///
/// The overhead is minimal compared to the benefits of SIMD math everywhere else.
///
private Microsoft.Xna.Framework.Matrix ToXnaMatrix(Matrix4x4 matrix)
{
return new Microsoft.Xna.Framework.Matrix(
matrix.M11, matrix.M12, matrix.M13, matrix.M14,
matrix.M21, matrix.M22, matrix.M23, matrix.M24,
matrix.M31, matrix.M32, matrix.M33, matrix.M34,
matrix.M41, matrix.M42, matrix.M43, matrix.M44
);
}
///
/// Convenience method to load a model from the Content Manager.
///
/// UNITY COMPARISON:
/// In Unity, you drag-drop an FBX into a prefab and it's ready.
/// In MonoGame, you:
/// 1. Add FBX to Content.mgcb
/// 2. Build content
/// 3. Call this method to load at runtime
///
/// Example usage:
/// var renderer = entity.AddComponent<ModelMeshRenderer>();
/// renderer.LoadModel(contentManager, "Models/Mesh_Weapon_Primary");
///
public void LoadModel(Microsoft.Xna.Framework.Content.ContentManager content, string assetName)
{
try
{
_model = content.Load(assetName);
Console.WriteLine($"[ModelMeshRenderer] Loaded model: {assetName}");
}
catch (Exception ex)
{
Console.WriteLine($"[ModelMeshRenderer] Failed to load model '{assetName}': {ex.Message}");
_model = null;
}
}
///
/// Get the bounding sphere for the entire model.
/// Useful for culling and distance calculations.
///
/// UNITY COMPARISON:
/// Similar to MeshRenderer.bounds in Unity.
/// Unity calculates bounds automatically.
/// MonoGame stores it in the model data.
///
public Microsoft.Xna.Framework.BoundingSphere? GetBoundingSphere()
{
if (_model == null || _model.Meshes.Count == 0)
return null;
// Get the first mesh's bounding sphere as approximation
return _model.Meshes[0].BoundingSphere;
}
}