Files
sbox-public/engine/Sandbox.Engine/Scene/Components/Particles/Renderers/ParticleModelRenderer.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

229 lines
6.0 KiB
C#

using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Sandbox;
/// <summary>
/// Renders particles as models, using the particle's position, rotation, and size.
/// </summary>
[Expose]
[Title( "Particle Model Renderer" )]
[Category( "Particles" )]
[Icon( "category" )]
public sealed class ParticleModelRenderer : ParticleController, Component.ExecuteInEditor
{
/// <summary>
/// Render options for advanced rendering.
/// </summary>
[Property, Order( -100 ), InlineEditor( Label = false ), Group( "Advanced Rendering", StartFolded = true )]
public RenderOptions RenderOptions { get; } = new RenderOptions( null );
/// <summary>
/// Entry for a model, including its material group and body group settings.
/// </summary>
[Expose]
public sealed class ModelEntry
{
private Model _model;
/// <summary>
/// The model associated with this entry.
/// </summary>
[KeyProperty]
public Model Model
{
get => _model;
set
{
if ( _model == value )
return;
_model = value;
MaterialGroup = default;
BodyGroups = _model?.Parts.DefaultMask ?? default;
}
}
/// <summary>
/// Material group for the model.
/// </summary>
[Model.MaterialGroup, ShowIf( nameof( HasMaterialGroups ), true )]
public string MaterialGroup { get; set; }
/// <summary>
/// Body group mask for the model.
/// </summary>
[Model.BodyGroupMask, ShowIf( nameof( HasBodyGroups ), true )]
public ulong BodyGroups { get; set; }
/// <summary>
/// Indicates whether the model has material groups.
/// </summary>
[Hide, JsonIgnore]
public bool HasMaterialGroups => Model?.MaterialGroupCount > 0;
/// <summary>
/// Indicates whether the model has body groups.
/// </summary>
[Hide, JsonIgnore]
public bool HasBodyGroups => Model?.Parts.All.Sum( x => x.Choices.Count ) > 1;
/// <summary>
/// Converts a <see cref="Model"/> to a <see cref="ModelEntry"/>.
/// </summary>
/// <param name="model">The model to convert.</param>
/// <returns>A new <see cref="ModelEntry"/> instance.</returns>
public static implicit operator ModelEntry( Model model ) => new() { Model = model };
}
/// <summary>
/// List of models for rendering. This property is obsolete; use <see cref="Choices"/> instead.
/// </summary>
[Hide, Obsolete( "Use Choices" )]
public List<Model> Models { get; set; } = new();
/// <summary>
/// List of model entries available for rendering.
/// </summary>
[Property]
public List<ModelEntry> Choices { get; set; } = new List<ModelEntry> { Model.Cube };
/// <summary>
/// Material override for rendering.
/// </summary>
[Property]
public Material MaterialOverride { get; set; }
/// <summary>
/// If true, the models will rotate relative to the this GameObject
/// </summary>
[Property]
public bool RotateWithGameObject { get; set; }
/// <summary>
/// Scale factor for particle rendering.
/// </summary>
[Property]
public ParticleFloat Scale { get; set; } = 1;
/// <summary>
/// Indicates whether particles cast shadows.
/// </summary>
[Property]
public bool CastShadows { get; set; } = true;
/// <summary>
/// Called when a particle is created.
/// </summary>
/// <param name="p">The particle being created.</param>
protected override void OnParticleCreated( Particle p )
{
p.AddListener( new ParticleModel( this ), this );
}
/// <summary>
/// Version of the component.
/// </summary>
public override int ComponentVersion => 1;
/// <summary>
/// Upgrades the JSON representation of the particle model renderer to version 1.
/// </summary>
/// <param name="obj">The JSON object to upgrade.</param>
[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" );
}
}
}
/// <summary>
/// Represents a particle model listener that updates the scene object based on particle properties.
/// </summary>
file class ParticleModel : Particle.BaseListener
{
/// <summary>
/// Renderer associated with this particle model.
/// </summary>
public ParticleModelRenderer Renderer;
SceneObject so;
/// <summary>
/// Initializes a new instance of the <see cref="ParticleModel"/> class.
/// </summary>
/// <param name="renderer">The particle model renderer.</param>
public ParticleModel( ParticleModelRenderer renderer )
{
Renderer = renderer;
}
/// <summary>
/// Called when the particle is enabled.
/// </summary>
/// <param name="p">The particle being enabled.</param>
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 );
}
}
/// <summary>
/// Called when the particle is disabled.
/// </summary>
/// <param name="p">The particle being disabled.</param>
public override void OnDisabled( Particle p )
{
if ( !so.IsValid() ) return;
so.Delete();
}
/// <summary>
/// Updates the particle.
/// </summary>
/// <param name="p">The particle being updated.</param>
/// <param name="dt">The delta time since the last update.</param>
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 );
}
}