Editor/InteriorLayoutBuilder/RoomLayoutGeometryBuilder.Solids.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace ReusableRoomLayout;
public sealed partial class RoomLayoutGeometryBuilder
{
private static void BuildSolidGeometry(
Scene scene,
GameObject parent,
RoomLayoutSettings settings,
IReadOnlyCollection<RoomLayoutSolidSlab> solidSlabs )
{
if ( solidSlabs.Count == 0 )
{
return;
}
foreach ( var layer in solidSlabs.GroupBy( slab => slab.Surface ) )
{
CreateSolidSurface( scene, parent, settings, layer.Key, layer.ToArray() );
}
foreach ( var slab in solidSlabs )
{
if ( slab.HasCollider )
{
CreateSolidCollider( scene, parent, slab );
}
}
}
private static void CreateSolidSurface(
Scene scene,
GameObject parent,
RoomLayoutSettings settings,
RoomLayoutSurface surface,
IReadOnlyCollection<RoomLayoutSolidSlab> slabs )
{
var gameObject = scene.CreateObject( true );
gameObject.Name = surface switch
{
RoomLayoutSurface.Cap => "Combined Wall Caps",
RoomLayoutSurface.Wall => "Combined Wall Bodies",
_ => $"Combined {surface}"
};
gameObject.SetParent( parent, false );
var meshComponent = gameObject.Components.Create<MeshComponent>( false );
meshComponent.Mesh = BuildSolidSurfaceMesh(
slabs,
MaterialScaleForSettings( settings, surface ),
settings,
surface );
meshComponent.Color = Color.White;
meshComponent.SmoothingAngle = 0.0f;
meshComponent.Enabled = true;
meshComponent.RebuildMesh();
}
private static PolygonMesh BuildSolidSurfaceMesh(
IReadOnlyCollection<RoomLayoutSolidSlab> slabs,
float textureWorldSize,
RoomLayoutSettings settings,
RoomLayoutSurface surface )
{
var xs = new List<float>();
var ys = new List<float>();
var zs = new List<float>();
foreach ( var slab in slabs )
{
AddUniqueCoordinate( xs, slab.Rect.X );
AddUniqueCoordinate( xs, slab.Rect.X + slab.Rect.Width );
AddUniqueCoordinate( ys, slab.Rect.Y );
AddUniqueCoordinate( ys, slab.Rect.Y + slab.Rect.Height );
AddUniqueCoordinate( zs, slab.ZMin );
AddUniqueCoordinate( zs, slab.ZMax );
}
xs.Sort();
ys.Sort();
zs.Sort();
var mesh = new PolygonMesh();
var vertices = new Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle>();
var cells = SolidCells( slabs, xs, ys, zs );
for ( var x = 0; x < xs.Count - 1; x++ )
{
for ( var y = 0; y < ys.Count - 1; y++ )
{
for ( var z = 0; z < zs.Count - 1; z++ )
{
var cell = cells[x, y, z];
if ( !cell.Covered )
{
continue;
}
var min = new Vector2( xs[x], ys[y] );
var max = new Vector2( xs[x + 1], ys[y + 1] );
var zMin = zs[z];
var zMax = zs[z + 1];
if ( z == zs.Count - 2 || !cells[x, y, z + 1].Covered )
{
var material = MaterialFor( settings, surface, cell.MaterialPath );
AddSolidTopFace( mesh, vertices, material, CellTextureWorldSize( cell, textureWorldSize ), min, max, zMax );
}
if ( ShouldBuildSolidBottomFace( surface ) && (z == 0 || !cells[x, y, z - 1].Covered) )
{
var material = MaterialFor( settings, surface, cell.MaterialPath );
AddSolidBottomFace( mesh, vertices, material, CellTextureWorldSize( cell, textureWorldSize ), min, max, zMin );
}
if ( x == 0 || !cells[x - 1, y, z].Covered )
{
var material = MaterialFor( settings, surface, FaceMaterialPath( cell, cell.WestMaterialPath ) );
AddSolidWestFace( mesh, vertices, material, FaceTextureWorldSize( cell, cell.WestTextureWorldSize, textureWorldSize ), min, max, zMin, zMax );
}
if ( x == xs.Count - 2 || !cells[x + 1, y, z].Covered )
{
var material = MaterialFor( settings, surface, FaceMaterialPath( cell, cell.EastMaterialPath ) );
AddSolidEastFace( mesh, vertices, material, FaceTextureWorldSize( cell, cell.EastTextureWorldSize, textureWorldSize ), min, max, zMin, zMax );
}
if ( y == 0 || !cells[x, y - 1, z].Covered )
{
var material = MaterialFor( settings, surface, FaceMaterialPath( cell, cell.SouthMaterialPath ) );
AddSolidSouthFace( mesh, vertices, material, FaceTextureWorldSize( cell, cell.SouthTextureWorldSize, textureWorldSize ), min, max, zMin, zMax );
}
if ( y == ys.Count - 2 || !cells[x, y + 1, z].Covered )
{
var material = MaterialFor( settings, surface, FaceMaterialPath( cell, cell.NorthMaterialPath ) );
AddSolidNorthFace( mesh, vertices, material, FaceTextureWorldSize( cell, cell.NorthTextureWorldSize, textureWorldSize ), min, max, zMin, zMax );
}
}
}
}
mesh.ComputeFaceTextureParametersFromCoordinates();
mesh.SetSmoothingAngle( 0.0f );
return mesh;
}
private static SolidCell[,,] SolidCells(
IReadOnlyCollection<RoomLayoutSolidSlab> slabs,
IReadOnlyList<float> xs,
IReadOnlyList<float> ys,
IReadOnlyList<float> zs )
{
var cells = new SolidCell[xs.Count - 1, ys.Count - 1, zs.Count - 1];
for ( var x = 0; x < xs.Count - 1; x++ )
{
for ( var y = 0; y < ys.Count - 1; y++ )
{
for ( var z = 0; z < zs.Count - 1; z++ )
{
var center = new Vector3(
(xs[x] + xs[x + 1]) * 0.5f,
(ys[y] + ys[y + 1]) * 0.5f,
(zs[z] + zs[z + 1]) * 0.5f );
cells[x, y, z] = SolidCellAt( slabs, center );
}
}
}
return cells;
}
private static SolidCell SolidCellAt( IReadOnlyCollection<RoomLayoutSolidSlab> slabs, Vector3 point )
{
foreach ( var slab in slabs )
{
var rect = slab.Rect;
if ( point.x >= rect.X && point.x <= rect.X + rect.Width &&
point.y >= rect.Y && point.y <= rect.Y + rect.Height &&
point.z >= slab.ZMin && point.z <= slab.ZMax )
{
return new SolidCell(
true,
slab.MaterialPath ?? "",
slab.NorthMaterialPath ?? "",
slab.SouthMaterialPath ?? "",
slab.EastMaterialPath ?? "",
slab.WestMaterialPath ?? "",
slab.TextureWorldSize,
slab.NorthTextureWorldSize,
slab.SouthTextureWorldSize,
slab.EastTextureWorldSize,
slab.WestTextureWorldSize );
}
}
return new SolidCell( false, "", "", "", "", "", 0.0f, 0.0f, 0.0f, 0.0f, 0.0f );
}
private static string FaceMaterialPath( SolidCell cell, string faceMaterialPath )
{
return string.IsNullOrWhiteSpace( faceMaterialPath )
? cell.MaterialPath
: faceMaterialPath;
}
private static float CellTextureWorldSize( SolidCell cell, float fallback )
{
return MaterialScaleOrFallback( cell.TextureWorldSize, fallback );
}
private static float FaceTextureWorldSize( SolidCell cell, float faceTextureWorldSize, float fallback )
{
return MaterialScaleOrFallback( faceTextureWorldSize, CellTextureWorldSize( cell, fallback ) );
}
private static bool ShouldBuildSolidBottomFace( RoomLayoutSurface surface )
{
return surface is RoomLayoutSurface.Wall or RoomLayoutSurface.Cap or RoomLayoutSurface.Baseboard;
}
private static void AddSolidTopFace(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
Material material,
float textureWorldSize,
Vector2 min,
Vector2 max,
float z )
{
AddSolidFace( mesh, material, textureWorldSize, new[]
{
SolidVertex( mesh, vertices, min.x, min.y, z ),
SolidVertex( mesh, vertices, max.x, min.y, z ),
SolidVertex( mesh, vertices, max.x, max.y, z ),
SolidVertex( mesh, vertices, min.x, max.y, z )
}, BoxUvPlane.XY );
}
private static void AddSolidBottomFace(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
Material material,
float textureWorldSize,
Vector2 min,
Vector2 max,
float z )
{
AddSolidFace( mesh, material, textureWorldSize, new[]
{
SolidVertex( mesh, vertices, min.x, min.y, z ),
SolidVertex( mesh, vertices, min.x, max.y, z ),
SolidVertex( mesh, vertices, max.x, max.y, z ),
SolidVertex( mesh, vertices, max.x, min.y, z )
}, BoxUvPlane.XY );
}
private static void AddSolidWestFace(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
Material material,
float textureWorldSize,
Vector2 min,
Vector2 max,
float zMin,
float zMax )
{
AddSolidFace( mesh, material, textureWorldSize, new[]
{
SolidVertex( mesh, vertices, min.x, min.y, zMin ),
SolidVertex( mesh, vertices, min.x, min.y, zMax ),
SolidVertex( mesh, vertices, min.x, max.y, zMax ),
SolidVertex( mesh, vertices, min.x, max.y, zMin )
}, BoxUvPlane.YZ );
}
private static void AddSolidEastFace(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
Material material,
float textureWorldSize,
Vector2 min,
Vector2 max,
float zMin,
float zMax )
{
AddSolidFace( mesh, material, textureWorldSize, new[]
{
SolidVertex( mesh, vertices, max.x, min.y, zMin ),
SolidVertex( mesh, vertices, max.x, max.y, zMin ),
SolidVertex( mesh, vertices, max.x, max.y, zMax ),
SolidVertex( mesh, vertices, max.x, min.y, zMax )
}, BoxUvPlane.YZ );
}
private static void AddSolidSouthFace(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
Material material,
float textureWorldSize,
Vector2 min,
Vector2 max,
float zMin,
float zMax )
{
AddSolidFace( mesh, material, textureWorldSize, new[]
{
SolidVertex( mesh, vertices, min.x, min.y, zMin ),
SolidVertex( mesh, vertices, max.x, min.y, zMin ),
SolidVertex( mesh, vertices, max.x, min.y, zMax ),
SolidVertex( mesh, vertices, min.x, min.y, zMax )
}, BoxUvPlane.XZ );
}
private static void AddSolidNorthFace(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
Material material,
float textureWorldSize,
Vector2 min,
Vector2 max,
float zMin,
float zMax )
{
AddSolidFace( mesh, material, textureWorldSize, new[]
{
SolidVertex( mesh, vertices, min.x, max.y, zMin ),
SolidVertex( mesh, vertices, min.x, max.y, zMax ),
SolidVertex( mesh, vertices, max.x, max.y, zMax ),
SolidVertex( mesh, vertices, max.x, max.y, zMin )
}, BoxUvPlane.XZ );
}
private static void AddSolidFace(
PolygonMesh mesh,
Material material,
float textureWorldSize,
HalfEdgeMesh.VertexHandle[] vertices,
BoxUvPlane uvPlane )
{
var face = mesh.AddFace( vertices );
mesh.SetFaceMaterial( face, material );
var textureSize = MathF.Max( 1.0f, textureWorldSize );
var uvs = new Vector2[vertices.Length];
for ( var i = 0; i < vertices.Length; i++ )
{
uvs[i] = ProjectBoxUv( mesh.GetVertexPosition( vertices[i] ), textureSize, uvPlane );
}
mesh.SetFaceTextureCoords( face, uvs );
}
private static HalfEdgeMesh.VertexHandle SolidVertex(
PolygonMesh mesh,
Dictionary<SolidVertexKey, HalfEdgeMesh.VertexHandle> vertices,
float x,
float y,
float z )
{
var key = new SolidVertexKey( QuantizeCoordinate( x ), QuantizeCoordinate( y ), QuantizeCoordinate( z ) );
if ( vertices.TryGetValue( key, out var vertex ) )
{
return vertex;
}
vertex = mesh.AddVertex( new Vector3( x, y, z ) );
vertices[key] = vertex;
return vertex;
}
private static void AddSolidSlab(
ICollection<RoomLayoutSolidSlab> solidSlabs,
string name,
Vector3 center,
Vector3 size,
RoomLayoutSurface surface,
bool hasCollider = true,
string materialPath = null,
string northMaterialPath = null,
string southMaterialPath = null,
string eastMaterialPath = null,
string westMaterialPath = null,
float textureWorldSize = 0.0f,
float northTextureWorldSize = 0.0f,
float southTextureWorldSize = 0.0f,
float eastTextureWorldSize = 0.0f,
float westTextureWorldSize = 0.0f )
{
if ( size.x < 0.5f || size.y < 0.5f || size.z < 0.5f )
{
return;
}
var rect = new RoomLayoutRect(
center.x - size.x * 0.5f,
center.y - size.y * 0.5f,
size.x,
size.y );
solidSlabs.Add( new RoomLayoutSolidSlab(
name,
rect,
center.z - size.z * 0.5f,
center.z + size.z * 0.5f,
surface,
hasCollider,
materialPath ?? "",
northMaterialPath ?? materialPath ?? "",
southMaterialPath ?? materialPath ?? "",
eastMaterialPath ?? materialPath ?? "",
westMaterialPath ?? materialPath ?? "",
textureWorldSize,
northTextureWorldSize > 0.0f ? northTextureWorldSize : textureWorldSize,
southTextureWorldSize > 0.0f ? southTextureWorldSize : textureWorldSize,
eastTextureWorldSize > 0.0f ? eastTextureWorldSize : textureWorldSize,
westTextureWorldSize > 0.0f ? westTextureWorldSize : textureWorldSize ) );
}
private static void CreateSolidCollider( Scene scene, GameObject parent, RoomLayoutSolidSlab slab )
{
var colliderObject = scene.CreateObject( true );
colliderObject.Name = $"{slab.Name} Collider";
colliderObject.SetParent( parent, false );
colliderObject.LocalPosition = new Vector3(
slab.Rect.Center.x,
slab.Rect.Center.y,
(slab.ZMin + slab.ZMax) * 0.5f );
var collider = colliderObject.Components.Create<BoxCollider>();
collider.Scale = new Vector3( slab.Rect.Width, slab.Rect.Height, slab.ZMax - slab.ZMin );
collider.Static = true;
}
private readonly record struct RoomLayoutSolidSlab(
string Name,
RoomLayoutRect Rect,
float ZMin,
float ZMax,
RoomLayoutSurface Surface,
bool HasCollider,
string MaterialPath,
string NorthMaterialPath,
string SouthMaterialPath,
string EastMaterialPath,
string WestMaterialPath,
float TextureWorldSize,
float NorthTextureWorldSize,
float SouthTextureWorldSize,
float EastTextureWorldSize,
float WestTextureWorldSize );
private readonly record struct SolidVertexKey( int X, int Y, int Z );
private readonly record struct SolidCell(
bool Covered,
string MaterialPath,
string NorthMaterialPath,
string SouthMaterialPath,
string EastMaterialPath,
string WestMaterialPath,
float TextureWorldSize,
float NorthTextureWorldSize,
float SouthTextureWorldSize,
float EastTextureWorldSize,
float WestTextureWorldSize );
}