Files
sbox-public/engine/Sandbox.Engine/Resources/Model/Model.Builder.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

577 lines
16 KiB
C#

using NativeEngine;
using System.Runtime.InteropServices;
using System.Text;
namespace Sandbox
{
/// <summary>
/// Provides ability to generate <see cref="Model"/>s at runtime.
/// A static instance of this class is available at <see cref="Model.Builder"/>
/// </summary>
public sealed partial class ModelBuilder
{
private readonly List<Mesh> meshes = new();
private readonly List<Vector3> vertices = new();
private readonly List<int> indices = new();
private readonly List<BoxDesc> boxes = new();
private readonly List<SphereDesc> spheres = new();
private readonly List<CapsuleDesc> capsules = new();
private readonly List<HullDesc> hulls = new();
private readonly List<MeshDesc> meshShapes = new();
private readonly List<int> lods = new();
private readonly List<ulong> bodyGroups = new();
private readonly List<MeshGroupDesc> meshGroups = new();
private readonly List<BoneDesc> bones = new();
private readonly StringBuilder names = new();
private int startTraceVertex;
private int startTraceIndex;
private int numTraceVertices;
private int numTraceIndices;
private string modelName;
private float mass = 1000;
private string surfaceProperty = "";
private readonly float[] lodSwitchDistance = Enumerable.Range( 0, 8 )
.Select( i => i * 50.0f )
.ToArray();
private struct BoxDesc
{
public Transform transform;
public Vector3 extents;
}
private struct SphereDesc
{
public Vector3 center;
public float radius;
}
private struct CapsuleDesc
{
public Vector3 center0;
public Vector3 center1;
public float radius;
}
private struct HullDesc
{
public Transform transform;
public int startVertex;
public int numVertex;
}
private struct MeshDesc
{
public int startVertex;
public int numVertex;
public int startIndex;
public int numIndex;
}
private struct BoneDesc
{
public int nameOffset;
public int nameLength;
public int parentNameOffset;
public int parentNameLength;
public Vector3 position;
public Rotation rotation;
public float radius;
public bool attachment;
}
private struct MeshGroupDesc
{
public int nameOffset;
public int nameLength;
}
/// <summary>
/// Total mass of the physics body (Default is 1000)
/// </summary>
public ModelBuilder WithMass( float mass )
{
this.mass = mass;
return this;
}
/// <summary>
/// Surface property to use for collision
/// </summary>
public ModelBuilder WithSurface( string name )
{
surfaceProperty = name;
return this;
}
/// <summary>
/// LOD switch distance increment for each Level of Detail (LOD) level. (Default is 50)
/// </summary>
public ModelBuilder WithLodDistance( int lod, float distance )
{
if ( lod >= 8 )
throw new ArgumentException( "Max LOD count is 8" );
lodSwitchDistance[lod] = distance;
return this;
}
/// <summary>
/// Add box collision shape.
/// </summary>
public ModelBuilder AddCollisionBox( Vector3 extents, Vector3? center = default, Rotation? rotation = default )
{
boxes.Add( new()
{
extents = extents,
transform = new Transform( center ?? Vector3.Zero, rotation ?? Rotation.Identity )
} );
return this;
}
/// <summary>
/// Add sphere collision shape.
/// </summary>
public ModelBuilder AddCollisionSphere( float radius, Vector3 center = default )
{
spheres.Add( new()
{
center = center,
radius = radius
} );
return this;
}
/// <summary>
/// Add capsule collision shape.
/// </summary>
public ModelBuilder AddCollisionCapsule( Vector3 center0, Vector3 center1, float radius )
{
capsules.Add( new()
{
center0 = center0,
center1 = center1,
radius = radius
} );
return this;
}
/// <summary>
/// Add a CONVEX hull collision shape.
/// </summary>
public ModelBuilder AddCollisionHull( List<Vector3> vertices, Vector3? center = default, Rotation? rotation = default )
{
return AddCollisionHull( CollectionsMarshal.AsSpan( vertices ), center, rotation );
}
/// <summary>
/// Add a CONVEX hull collision shape.
/// </summary>
public ModelBuilder AddCollisionHull( Span<Vector3> vertices, Vector3? center = default, Rotation? rotation = default )
{
if ( vertices.IsEmpty ) return this;
var startVertex = this.vertices.Count;
hulls.Add( new()
{
startVertex = startVertex,
numVertex = vertices.Length,
transform = new Transform( center ?? Vector3.Zero, rotation ?? Rotation.Identity )
} );
this.vertices.AddRange( vertices );
return this;
}
/// <summary>
/// Add a CONCAVE mesh collision shape. (This shape can NOT be physically simulated)
/// </summary>
public ModelBuilder AddCollisionMesh( List<Vector3> vertices, List<int> indices )
{
return AddCollisionMesh( CollectionsMarshal.AsSpan( vertices ), CollectionsMarshal.AsSpan( indices ) );
}
/// <summary>
/// Add a CONCAVE mesh collision shape. (This shape can NOT be physically simulated)
/// </summary>
public ModelBuilder AddCollisionMesh( Span<Vector3> vertices, Span<int> indices )
{
if ( vertices.Length < 3 )
return this;
if ( indices.Length < 3 )
return this;
// Validate indices are in range, creating collision mesh is expensive anyway so no harm in being safe.
int numVertices = vertices.Length;
foreach ( var i in indices )
{
if ( i < 0 || i >= numVertices )
throw new ArgumentOutOfRangeException( nameof( indices ), $"Tried to access out of range vertex {i}, range is 0-{numVertices - 1}" );
}
var startVertex = this.vertices.Count;
var startIndex = this.indices.Count;
meshShapes.Add( new()
{
startVertex = startVertex,
numVertex = vertices.Length,
startIndex = startIndex,
numIndex = indices.Length
} );
this.vertices.AddRange( vertices );
this.indices.AddRange( indices );
return this;
}
/// <summary>
/// Add trace vertices for tracing against mesh
/// </summary>
public ModelBuilder AddTraceMesh( List<Vector3> vertices, List<int> indices )
{
return AddTraceMesh( CollectionsMarshal.AsSpan( vertices ), CollectionsMarshal.AsSpan( indices ) );
}
/// <summary>
/// Add trace vertices for tracing against mesh
/// </summary>
public ModelBuilder AddTraceMesh( Span<Vector3> vertices, Span<int> indices )
{
if ( vertices.Length < 3 )
return this;
if ( indices.Length < 3 )
return this;
// Validate indices are in range
int numVertices = vertices.Length;
foreach ( var i in indices )
{
if ( i < 0 || i >= numVertices )
throw new ArgumentOutOfRangeException( nameof( indices ), $"Tried to access out of range vertex {i}, range is 0-{numVertices - 1}" );
}
var startVertex = this.vertices.Count;
var startIndex = this.indices.Count;
startTraceVertex = startVertex;
startTraceIndex = startIndex;
numTraceVertices = vertices.Length;
numTraceIndices = indices.Length;
this.vertices.AddRange( vertices );
this.indices.AddRange( indices );
return this;
}
/// <summary>
/// Add a mesh.
/// </summary>
public ModelBuilder AddMesh( Mesh mesh )
{
AddMesh( mesh, 255, ulong.MaxValue );
return this;
}
/// <summary>
/// Add a bunch of meshes.
/// </summary>
public ModelBuilder AddMeshes( Mesh[] meshes )
{
AddMeshes( meshes, 255, ulong.MaxValue );
return this;
}
/// <summary>
/// Add a mesh to a Level of Detail (LOD) group.
/// </summary>
public ModelBuilder AddMesh( Mesh mesh, int lod )
{
if ( lod < 0 ) lod = 0;
var lodMask = 1 << lod;
AddMesh( mesh, lodMask, ulong.MaxValue );
return this;
}
/// <summary>
/// Add a bunch of meshes to a Level of Detail (LOD) group.
/// </summary>
public ModelBuilder AddMeshes( Mesh[] meshes, int lod )
{
if ( lod < 0 ) lod = 0;
var lodMask = 1 << lod;
AddMeshes( meshes, lodMask, ulong.MaxValue );
return this;
}
/// <summary>
/// Add a mesh to a body group choice.
/// </summary>
public ModelBuilder AddMesh( Mesh mesh, string groupName, int choiceIndex )
{
return AddMesh( mesh, 0, groupName, choiceIndex );
}
/// <summary>
/// Add a mesh to a Level of Detail (LOD) and a body group choice.
/// </summary>
public ModelBuilder AddMesh( Mesh mesh, int lod, string groupName, int choiceIndex )
{
var groupIndex = EnsureGroupChoice( groupName, choiceIndex );
var groupMask = 1UL << groupIndex;
if ( lod < 0 ) lod = 0;
var lodMask = 1 << lod;
return AddMesh( mesh, lodMask, groupMask );
}
private ModelBuilder AddMesh( Mesh mesh, int lodMask, ulong bodyGroupMask )
{
if ( mesh == null || !mesh.IsValid )
return this;
if ( !mesh.HasVertexBuffer )
throw new ArgumentException( "Mesh has invalid vertex buffer" );
meshes.Add( mesh );
lods.Add( lodMask );
bodyGroups.Add( bodyGroupMask );
return this;
}
private ModelBuilder AddMeshes( Mesh[] meshes, int lodMask, ulong bodyGroupMask )
{
if ( meshes == null || meshes.Length == 0 )
return this;
int numMeshes = 0;
foreach ( var mesh in meshes )
{
if ( mesh == null || !mesh.IsValid )
continue;
if ( !mesh.HasVertexBuffer )
throw new ArgumentException( "Mesh has invalid vertex buffer" );
this.meshes.Add( mesh );
numMeshes++;
}
if ( numMeshes == 0 )
return this;
lods.AddRange( Enumerable.Repeat( lodMask, numMeshes ) );
bodyGroups.AddRange( Enumerable.Repeat( bodyGroupMask, numMeshes ) );
return this;
}
private readonly Dictionary<(string, int), int> _groupChoiceToIndex = new();
private readonly HashSet<string> _groupNames = new( StringComparer.Ordinal );
private ulong _defaultMeshGroupMask;
private ulong DefaultMeshGroupMask => _groupNames.Count > 0 ? _defaultMeshGroupMask : ulong.MaxValue;
private int EnsureGroupChoice( string name, int choice )
{
if ( string.IsNullOrWhiteSpace( name ) ) throw new ArgumentException( null, nameof( name ) );
ArgumentOutOfRangeException.ThrowIfNegative( choice );
var key = (name, choice);
if ( _groupChoiceToIndex.TryGetValue( key, out var index ) ) return index;
if ( meshGroups.Count >= 64 )
throw new InvalidOperationException( "Total bodygroup choices exceed 64 bits." );
var groupName = $"{name}_@{choice}";
var nameOffset = names.Length;
index = meshGroups.Count;
meshGroups.Add( new MeshGroupDesc { nameLength = groupName.Length, nameOffset = nameOffset } );
names.Append( groupName );
_groupChoiceToIndex[key] = index;
if ( _groupNames.Add( name ) )
{
_defaultMeshGroupMask |= (1UL << index);
}
return index;
}
/// <summary>
/// A bone definition for use with <see cref="ModelBuilder"/>.
/// </summary>
/// <param name="Name">Name of the bone.</param>
/// <param name="ParentName">Name of the parent bone.</param>
/// <param name="Position">Position of the bone, relative to its parent.</param>
/// <param name="Rotation">Rotation of the bone, relative to its parent.</param>
public readonly record struct Bone( string Name, string ParentName, Vector3 Position, Rotation Rotation );
/// <summary>
/// Add a bone to the skeleton via a <see cref="Bone"/> struct.
/// </summary>
public void AddBone( Bone bone )
{
AddBone( bone.Name, bone.Position, bone.Rotation, bone.ParentName );
}
/// <summary>
/// Add multiple bones to the skeleton.
/// </summary>
public void AddBones( Bone[] bones )
{
if ( bones == null )
return;
foreach ( var bone in bones )
AddBone( bone.Name, bone.Position, bone.Rotation, bone.ParentName );
}
/// <summary>
/// Add a bone to the skeleton.
/// </summary>
public ModelBuilder AddBone( string name, Vector3 position, Rotation rotation, string parentName = null )
{
return AddBone( name, position, rotation, parentName, false );
}
/// <summary>
/// Add an attachment to the skeleton.
/// </summary>
public ModelBuilder AddAttachment( string name, Vector3 position, Rotation rotation, string parentName = null )
{
return AddBone( name, position, rotation, parentName, true );
}
internal ModelBuilder AddBone( string name, Vector3 position, Rotation rotation, string parentName, bool attachment )
{
var nameOffset = -1;
if ( !string.IsNullOrWhiteSpace( name ) )
{
nameOffset = names.Length;
names.Append( name );
}
var parentNameOffset = -1;
if ( !string.IsNullOrWhiteSpace( parentName ) )
{
parentNameOffset = names.Length;
names.Append( parentName );
}
bones.Add( new BoneDesc
{
nameOffset = nameOffset,
nameLength = name != null ? name.Length : 0,
parentNameOffset = parentNameOffset,
parentNameLength = parentName != null ? parentName.Length : 0,
position = position,
rotation = rotation,
radius = -1,
attachment = attachment
} );
return this;
}
/// <summary>
/// Provide a name to identify the model by
/// </summary>
/// <param name="name">Desired model name</param>
public ModelBuilder WithName( string name )
{
modelName = name;
return this;
}
/// <summary>
/// Finish creation of the model.
/// </summary>
public unsafe Model Create()
{
var renderMeshes = meshes
.Where( x => x != null && x.IsValid )
.Select( x => x.native )
.ToArray();
var vertices_span = CollectionsMarshal.AsSpan( vertices );
var indices_span = CollectionsMarshal.AsSpan( indices );
var spheres_span = CollectionsMarshal.AsSpan( spheres );
var capsules_span = CollectionsMarshal.AsSpan( capsules );
var boxes_span = CollectionsMarshal.AsSpan( boxes );
var hulls_span = CollectionsMarshal.AsSpan( hulls );
var meshes_span = CollectionsMarshal.AsSpan( meshShapes );
var lods_span = CollectionsMarshal.AsSpan( lods );
var bodygroups_span = CollectionsMarshal.AsSpan( bodyGroups );
var meshgroups_span = CollectionsMarshal.AsSpan( meshGroups );
var bones_span = CollectionsMarshal.AsSpan( bones );
fixed ( IMesh* meshes_ptr = renderMeshes )
fixed ( Vector3* vertices_ptr = vertices_span )
fixed ( int* indices_ptr = indices_span )
fixed ( SphereDesc* spheres_ptr = spheres_span )
fixed ( CapsuleDesc* capsule_ptr = capsules_span )
fixed ( BoxDesc* boxes_ptr = boxes_span )
fixed ( HullDesc* hulls_ptr = hulls_span )
fixed ( MeshDesc* meshShapes_ptr = meshes_span )
fixed ( int* lods_ptr = lods_span )
fixed ( ulong* bodygroups_ptr = bodygroups_span )
fixed ( MeshGroupDesc* meshgroups_ptr = meshgroups_span )
fixed ( BoneDesc* bones_ptr = bones_span )
fixed ( float* pLodSwitchDistance = &lodSwitchDistance[0] )
{
var anim = CreateAnimationGroup();
var bodies = CreatePhysBodyDesc();
var materialGroups = CreateMaterialGroups();
var model = MeshGlue.CreateModel(
anim,
bodies,
materialGroups,
mass,
surfaceProperty,
(IntPtr)pLodSwitchDistance,
(IntPtr)meshes_ptr, renderMeshes.Length,
(IntPtr)lods_ptr,
(IntPtr)bodygroups_ptr,
(IntPtr)meshgroups_ptr, meshGroups.Count,
(IntPtr)vertices_ptr, vertices.Count,
(IntPtr)indices_ptr, indices.Count,
(IntPtr)spheres_ptr, spheres.Count,
(IntPtr)capsule_ptr, capsules.Count,
(IntPtr)boxes_ptr, boxes.Count,
(IntPtr)hulls_ptr, hulls.Count,
(IntPtr)meshShapes_ptr, meshShapes.Count,
(IntPtr)bones_ptr, bones.Count,
names.Length > 0 ? names.ToString() : null,
startTraceVertex, startTraceIndex,
numTraceVertices, numTraceIndices,
DefaultMeshGroupMask );
if ( anim.IsValid ) anim.DeleteThis();
if ( bodies.IsValid ) bodies.DeleteThis();
if ( materialGroups.IsValid ) materialGroups.DeleteThis();
return Model.FromNative( model, true, modelName );
}
}
}
}