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