using System.Text.Json.Serialization; using static Sandbox.Component; using static Sandbox.ModelRenderer; namespace Sandbox; /// /// An editable polygon mesh with collision /// [Hide, Expose] public sealed class MeshComponent : Collider, ExecuteInEditor, ITintable, IMaterialSetter { [Expose] public enum CollisionType { None, Mesh, Hull } private PolygonMesh _mesh; [Property, Order( 0 )] public PolygonMesh Mesh { get => _mesh; set { if ( _mesh == value ) return; _mesh = value; Update(); } } [Property, Order( 1 )] public CollisionType Collision { get => _collisionType; set { if ( _collisionType == value ) return; _collisionType = value; RebuildImmediately(); } } [Property, Title( "Tint" ), Order( 2 )] public Color Color { get => _color; set { if ( _color == value ) return; _color = value; if ( SceneObject.IsValid() ) { SceneObject.ColorTint = Color; } } } [Property, Order( 3 )] public float SmoothingAngle { get => _smoothingAngle; set { if ( _smoothingAngle == value ) return; _smoothingAngle = value; Mesh?.SetSmoothingAngle( _smoothingAngle ); } } [Property, Order( 4 )] public bool HideInGame { get => _hideInGame; set { if ( _hideInGame == value ) return; _hideInGame = value; if ( Scene.IsEditor ) return; if ( HideInGame ) { DeleteSceneObject(); } else if ( !SceneObject.IsValid() && Model is not null ) { SceneObject = new SceneObject( Scene.SceneWorld, Model, WorldTransform ); UpdateSceneObject(); } } } private ShadowRenderType _renderType = ShadowRenderType.On; [Title( "Cast Shadows" ), Property, Category( "Lighting" )] public ShadowRenderType RenderType { get => _renderType; set { if ( _renderType == value ) return; _renderType = value; if ( SceneObject.IsValid() ) { SceneObject.Flags.CastShadows = RenderType == ShadowRenderType.On || RenderType == ShadowRenderType.ShadowsOnly; } } } [JsonIgnore, Hide] public Model Model { get; private set; } public override bool IsConcave => Collision == CollisionType.Mesh; private bool Hidden => !Scene.IsEditor && HideInGame; private SceneObject SceneObject; private Color _color = Color.White; private CollisionType _collisionType = CollisionType.Mesh; private float _smoothingAngle; private bool _hideInGame; public void SetMaterial( Material material, int triangle ) { if ( Mesh is null ) return; var face = Mesh.TriangleToFace( triangle ); if ( !face.IsValid ) return; Mesh.SetFaceMaterial( face, material ); } public Material GetMaterial( int triangle ) { if ( Mesh is null ) return default; var face = Mesh.TriangleToFace( triangle ); return Mesh.GetFaceMaterial( face ); } protected override void OnValidate() { WorldScale = 1; Update(); } protected override void OnEnabled() { base.OnEnabled(); RebuildMesh(); } protected override void OnDisabled() { base.OnDisabled(); DeleteSceneObject(); } private void DeleteSceneObject() { if ( !SceneObject.IsValid() ) return; SceneObject.RenderingEnabled = false; SceneObject.Delete(); SceneObject = null; } protected override void OnUpdate() { base.OnUpdate(); Update(); } private void Update() { if ( !Active ) return; if ( Mesh is null ) return; if ( Scene.IsEditor ) { Mesh.Transform = WorldTransform; } if ( !Mesh.IsDirty ) return; RebuildMesh(); } internal override void OnTagsUpdatedInternal() { if ( SceneObject.IsValid() ) { SceneObject.Tags.SetFrom( Tags ); } base.OnTagsUpdatedInternal(); } internal override void TransformChanged( GameTransform root ) { if ( WorldScale != 1 && Mesh is not null ) { Mesh.Scale( WorldScale ); WorldScale = 1; } if ( Mesh is not null && Scene.IsEditor ) { Mesh.Transform = WorldTransform; } if ( SceneObject.IsValid() ) { SceneObject.Transform = WorldTransform; } base.TransformChanged( root ); } protected override IEnumerable CreatePhysicsShapes( PhysicsBody targetBody, Transform local ) { if ( Collision == CollisionType.None ) yield break; if ( Model is null || Model.Physics is null ) yield break; foreach ( var part in Model.Physics.Parts ) { Assert.NotNull( part, "Physics part was null" ); var bx = local.ToWorld( part.Transform ); if ( Collision == CollisionType.Mesh ) { foreach ( var mesh in part.Meshes ) { var shape = targetBody.AddShape( mesh, bx, false, true ); Assert.NotNull( shape, "Mesh shape was null" ); shape.Surface = mesh.Surface; shape.Surfaces = mesh.Surfaces; yield return shape; } } else if ( Collision == CollisionType.Hull ) { foreach ( var hull in part.Hulls ) { var shape = targetBody.AddShape( hull, bx ); Assert.NotNull( shape, "Hull shape was null" ); shape.Surface = hull.Surface; yield return shape; } } } } public void RebuildMesh() { if ( !Active ) return; if ( Mesh is null ) return; Mesh.Transform = WorldTransform; Mesh.SetSmoothingAngle( SmoothingAngle ); Model = Mesh.Rebuild(); RebuildImmediately(); if ( Model is null || Model.MeshCount == 0 ) { if ( SceneObject.IsValid() ) { SceneObject.RenderingEnabled = false; SceneObject.Delete(); SceneObject = null; } return; } if ( Hidden ) return; if ( !SceneObject.IsValid() ) { SceneObject = new SceneObject( Scene.SceneWorld, Model, WorldTransform ); } else { SceneObject.Model = Model; SceneObject.Transform = WorldTransform; // We manually set the model, sceneobject needs to update based on any new materials in it SceneObject.UpdateFlagsBasedOnMaterial(); } UpdateSceneObject(); } private void UpdateSceneObject() { if ( !SceneObject.IsValid() ) return; SceneObject.Component = this; SceneObject.Tags.SetFrom( GameObject.Tags ); SceneObject.ColorTint = Color; SceneObject.Flags.CastShadows = RenderType == ShadowRenderType.On || RenderType == ShadowRenderType.ShadowsOnly; } }