using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace Sandbox; /// /// Renders particles as models, using the particle's position, rotation, and size. /// [Expose] [Title( "Particle Model Renderer" )] [Category( "Particles" )] [Icon( "category" )] public sealed class ParticleModelRenderer : ParticleController, Component.ExecuteInEditor { /// /// Render options for advanced rendering. /// [Property, Order( -100 ), InlineEditor( Label = false ), Group( "Advanced Rendering", StartFolded = true )] public RenderOptions RenderOptions { get; } = new RenderOptions( null ); /// /// Entry for a model, including its material group and body group settings. /// [Expose] public sealed class ModelEntry { private Model _model; /// /// The model associated with this entry. /// [KeyProperty] public Model Model { get => _model; set { if ( _model == value ) return; _model = value; MaterialGroup = default; BodyGroups = _model?.Parts.DefaultMask ?? default; } } /// /// Material group for the model. /// [Model.MaterialGroup, ShowIf( nameof( HasMaterialGroups ), true )] public string MaterialGroup { get; set; } /// /// Body group mask for the model. /// [Model.BodyGroupMask, ShowIf( nameof( HasBodyGroups ), true )] public ulong BodyGroups { get; set; } /// /// Indicates whether the model has material groups. /// [Hide, JsonIgnore] public bool HasMaterialGroups => Model?.MaterialGroupCount > 0; /// /// Indicates whether the model has body groups. /// [Hide, JsonIgnore] public bool HasBodyGroups => Model?.Parts.All.Sum( x => x.Choices.Count ) > 1; /// /// Converts a to a . /// /// The model to convert. /// A new instance. public static implicit operator ModelEntry( Model model ) => new() { Model = model }; } /// /// List of models for rendering. This property is obsolete; use instead. /// [Hide, Obsolete( "Use Choices" )] public List Models { get; set; } = new(); /// /// List of model entries available for rendering. /// [Property] public List Choices { get; set; } = new List { Model.Cube }; /// /// Material override for rendering. /// [Property] public Material MaterialOverride { get; set; } /// /// If true, the models will rotate relative to the this GameObject /// [Property] public bool RotateWithGameObject { get; set; } /// /// Scale factor for particle rendering. /// [Property] public ParticleFloat Scale { get; set; } = 1; /// /// Indicates whether particles cast shadows. /// [Property] public bool CastShadows { get; set; } = true; /// /// Called when a particle is created. /// /// The particle being created. protected override void OnParticleCreated( Particle p ) { p.AddListener( new ParticleModel( this ), this ); } /// /// Version of the component. /// public override int ComponentVersion => 1; /// /// Upgrades the JSON representation of the particle model renderer to version 1. /// /// The JSON object to upgrade. [Expose, JsonUpgrader( typeof( ParticleModelRenderer ), 1 )] static void Upgrader_v1( JsonObject obj ) { if ( obj.TryGetPropertyValue( "Models", out var node ) ) { var choices = new JsonArray(); foreach ( var model in node.AsArray() ) { if ( model is null ) continue; choices.Add( new JsonObject { ["Model"] = model.ToString() } ); } obj["Choices"] = choices; obj.Remove( "Models" ); } } } /// /// Represents a particle model listener that updates the scene object based on particle properties. /// file class ParticleModel : Particle.BaseListener { /// /// Renderer associated with this particle model. /// public ParticleModelRenderer Renderer; SceneObject so; /// /// Initializes a new instance of the class. /// /// The particle model renderer. public ParticleModel( ParticleModelRenderer renderer ) { Renderer = renderer; } /// /// Called when the particle is enabled. /// /// The particle being enabled. public override void OnEnabled( Particle p ) { var entry = Random.Shared.FromList( Renderer.Choices ); var model = entry?.Model; so = new SceneObject( Renderer.Scene.SceneWorld, model ?? Model.Cube ); if ( model is not null ) { so.MeshGroupMask = entry.BodyGroups; so.SetMaterialGroup( entry.MaterialGroup ); } } /// /// Called when the particle is disabled. /// /// The particle being disabled. public override void OnDisabled( Particle p ) { if ( !so.IsValid() ) return; so.Delete(); } /// /// Updates the particle. /// /// The particle being updated. /// The delta time since the last update. public override void OnUpdate( Particle p, float dt ) { if ( !so.IsValid() ) return; var rot = p.Angles.ToRotation(); if ( Renderer.RotateWithGameObject ) { // Rotate the particle with the object rot = Renderer.WorldRotation * rot; } so.Transform = new Transform( p.Position, rot, p.Size * Renderer.Scale.Evaluate( p, 2356 ) ); so.ColorTint = p.Color.WithAlphaMultiplied( p.Alpha ); so.Flags.CastShadows = Renderer.CastShadows; so.SetMaterialOverride( Renderer.MaterialOverride ); Renderer.RenderOptions.Apply( so ); } }