Editor/InteriorLayoutBuilder/RoomLayoutGeometryBuilder.Mesh.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Sandbox;
namespace ReusableRoomLayout;
public sealed partial class RoomLayoutGeometryBuilder
{
private const string PluginFallbackMaterialPath = "materials/room_layout/fallback_neutral.vmat";
private const string InMemoryFallbackShaderPath = "shaders/complex.shader";
private static readonly Dictionary<string, Material> MaterialCache = new( StringComparer.OrdinalIgnoreCase );
private static readonly Dictionary<RoomLayoutSurface, Material> FallbackMaterialCache = new();
internal static void InvalidateMaterial( string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
{
return;
}
var normalizedPath = NormalizeAssetPath( path );
MaterialCache.Remove( normalizedPath );
if ( normalizedPath.EndsWith( ".vmat", StringComparison.OrdinalIgnoreCase ) )
{
MaterialCache.Remove( $"{normalizedPath}_c" );
}
}
private static GameObject CreateBox(
Scene scene,
GameObject parent,
string name,
Vector3 center,
Vector3 size,
RoomLayoutSettings settings,
RoomLayoutSurface surface,
string materialPath = null,
float textureWorldSize = 0.0f )
{
var gameObject = CreateVisualBox( scene, parent, name, center, size, settings, surface, materialPath, textureWorldSize );
var collider = gameObject.Components.Create<BoxCollider>();
collider.Scale = size;
collider.Static = true;
return gameObject;
}
private static GameObject CreateVisualBox(
Scene scene,
GameObject parent,
string name,
Vector3 center,
Vector3 size,
RoomLayoutSettings settings,
RoomLayoutSurface surface,
string materialPath = null,
float textureWorldSize = 0.0f )
{
var gameObject = scene.CreateObject( true );
gameObject.Name = name;
gameObject.SetParent( parent, false );
gameObject.LocalPosition = center;
var meshComponent = gameObject.Components.Create<MeshComponent>( false );
meshComponent.Mesh = BuildBoxMesh(
size,
center,
MaterialScaleOrFallback( textureWorldSize, MaterialScaleForSettings( settings, surface ) ),
MaterialFor( settings, surface, materialPath ) );
meshComponent.Color = Color.White;
meshComponent.SmoothingAngle = 0.0f;
meshComponent.Enabled = true;
meshComponent.RebuildMesh();
return gameObject;
}
private static void AddUniqueCoordinate( List<float> values, float value )
{
foreach ( var existing in values )
{
if ( existing.AlmostEqual( value ) )
{
return;
}
}
values.Add( value );
}
private static int QuantizeCoordinate( float value )
{
return (int)MathF.Round( value * 1000.0f );
}
private static PolygonMesh BuildBoxMesh(
Vector3 size,
Vector3 worldCenter,
float textureWorldSize,
Material material )
{
var half = size * 0.5f;
var mesh = new PolygonMesh();
var vertices = mesh.AddVertices( new[]
{
new Vector3( -half.x, -half.y, -half.z ),
new Vector3( half.x, -half.y, -half.z ),
new Vector3( half.x, half.y, -half.z ),
new Vector3( -half.x, half.y, -half.z ),
new Vector3( -half.x, -half.y, half.z ),
new Vector3( half.x, -half.y, half.z ),
new Vector3( half.x, half.y, half.z ),
new Vector3( -half.x, half.y, half.z )
} );
AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[4], vertices[5], vertices[6], vertices[7]], BoxUvPlane.XY );
AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[0], vertices[3], vertices[2], vertices[1]], BoxUvPlane.XY );
AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[3], vertices[7], vertices[6], vertices[2]], BoxUvPlane.XZ );
AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[0], vertices[1], vertices[5], vertices[4]], BoxUvPlane.XZ );
AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[1], vertices[2], vertices[6], vertices[5]], BoxUvPlane.YZ );
AddBoxFace( mesh, material, worldCenter, textureWorldSize, [vertices[0], vertices[4], vertices[7], vertices[3]], BoxUvPlane.YZ );
mesh.ComputeFaceTextureParametersFromCoordinates();
mesh.SetSmoothingAngle( 0.0f );
return mesh;
}
private static void AddBoxFace(
PolygonMesh mesh,
Material material,
Vector3 worldCenter,
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++ )
{
var worldPosition = worldCenter + mesh.GetVertexPosition( vertices[i] );
uvs[i] = ProjectBoxUv( worldPosition, textureSize, uvPlane );
}
mesh.SetFaceTextureCoords( face, uvs );
}
private static Vector2 ProjectBoxUv( Vector3 worldPosition, float textureWorldSize, BoxUvPlane uvPlane )
{
return uvPlane switch
{
BoxUvPlane.XY => new Vector2( worldPosition.x, worldPosition.y ) / textureWorldSize,
BoxUvPlane.XZ => new Vector2( worldPosition.x, worldPosition.z ) / textureWorldSize,
_ => new Vector2( worldPosition.y, worldPosition.z ) / textureWorldSize
};
}
private enum BoxUvPlane
{
XY,
XZ,
YZ
}
private static Material MaterialFor( RoomLayoutSettings settings, RoomLayoutSurface surface, string materialPath = null )
{
var path = !string.IsNullOrWhiteSpace( materialPath )
? materialPath
: MaterialPathForSettings( settings, surface );
path = NormalizeAssetPath( path );
if ( string.IsNullOrWhiteSpace( path ) || IsKnownMissingBuiltInMaterial( path ) )
{
return FallbackMaterialFor( surface );
}
if ( IsTexturePath( path ) )
{
path = TryCreateTextureMaterial( path );
if ( string.IsNullOrWhiteSpace( path ) )
{
return FallbackMaterialFor( surface );
}
}
if ( !MaterialCache.TryGetValue( path, out var material ) )
{
material = Material.Load( path );
MaterialCache[path] = material;
}
return material;
}
private static Material FallbackMaterialFor( RoomLayoutSurface surface )
{
if ( !FallbackMaterialCache.TryGetValue( surface, out var material ) )
{
material = LoadPluginFallbackMaterial() ??
Material.Create( $"roomlayout_fallback_{surface.ToString().ToLowerInvariant()}", InMemoryFallbackShaderPath );
FallbackMaterialCache[surface] = material;
}
return material;
}
private static Material LoadPluginFallbackMaterial()
{
if ( !PluginFallbackMaterialExists() )
{
return null;
}
if ( !MaterialCache.TryGetValue( PluginFallbackMaterialPath, out var material ) )
{
material = Material.Load( PluginFallbackMaterialPath );
MaterialCache[PluginFallbackMaterialPath] = material;
}
return material is not null && material.IsValid()
? material
: null;
}
private static bool PluginFallbackMaterialExists()
{
try
{
return global::Editor.AssetSystem.FindByPath( PluginFallbackMaterialPath ) is not null ||
Sandbox.FileSystem.Mounted.FileExists( PluginFallbackMaterialPath ) ||
Sandbox.FileSystem.Mounted.FileExists( $"{PluginFallbackMaterialPath}_c" );
}
catch
{
return false;
}
}
private static string MaterialPathForSettings( RoomLayoutSettings settings, RoomLayoutSurface surface )
{
return surface switch
{
RoomLayoutSurface.Floor => settings.FloorMaterialPath,
RoomLayoutSurface.Wall => FirstMaterialPath( settings.OuterWallMaterialPath, settings.WallMaterialPath ),
RoomLayoutSurface.Cap => settings.WallCapMaterialPath,
RoomLayoutSurface.Baseboard => settings.BaseboardMaterialPath,
RoomLayoutSurface.DoorFrame => settings.DoorFrameMaterialPath,
RoomLayoutSurface.WindowFrame => settings.WindowFrameMaterialPath,
RoomLayoutSurface.CorridorFloor => settings.CorridorFloorMaterialPath,
RoomLayoutSurface.Threshold => settings.ThresholdMaterialPath,
RoomLayoutSurface.Roof => settings.RoofMaterialPath,
_ => settings.WallMaterialPath
};
}
private static float MaterialScaleForSettings( RoomLayoutSettings settings, RoomLayoutSurface surface )
{
return surface switch
{
RoomLayoutSurface.Floor => MaterialScaleOrFallback( settings.FloorMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.Wall => MaterialScaleOrFallback( settings.OuterWallMaterialScale, MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ),
RoomLayoutSurface.Cap => MaterialScaleOrFallback( settings.WallCapMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.Baseboard => MaterialScaleOrFallback( settings.BaseboardMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.DoorFrame => MaterialScaleOrFallback( settings.DoorFrameMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.WindowFrame => MaterialScaleOrFallback( settings.WindowFrameMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.CorridorFloor => MaterialScaleOrFallback( settings.CorridorFloorMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.Threshold => MaterialScaleOrFallback( settings.ThresholdMaterialScale, settings.TextureWorldSize ),
RoomLayoutSurface.Roof => MaterialScaleOrFallback( settings.RoofMaterialScale, settings.TextureWorldSize ),
_ => MaterialScaleOrFallback( 0.0f, settings.TextureWorldSize )
};
}
private static string RoomMaterialPath( RoomLayoutSettings settings, RoomLayoutRoom room, RoomLayoutSurface surface )
{
return surface switch
{
RoomLayoutSurface.Floor => FirstMaterialPath( room.FloorMaterialPath, settings.FloorMaterialPath ),
RoomLayoutSurface.Wall => RoomOuterWallMaterialPath( settings, room ),
RoomLayoutSurface.Cap => FirstMaterialPath( room.WallCapMaterialPath, settings.WallCapMaterialPath ),
RoomLayoutSurface.Baseboard => FirstMaterialPath( room.BaseboardMaterialPath, settings.BaseboardMaterialPath ),
RoomLayoutSurface.Threshold => FirstMaterialPath( room.ThresholdMaterialPath, settings.ThresholdMaterialPath ),
RoomLayoutSurface.Roof => FirstMaterialPath( room.RoofMaterialPath, settings.RoofMaterialPath ),
_ => ""
};
}
private static float RoomMaterialScale( RoomLayoutSettings settings, RoomLayoutRoom room, RoomLayoutSurface surface )
{
return surface switch
{
RoomLayoutSurface.Floor => MaterialScaleOrFallback( room.FloorMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Floor ) ),
RoomLayoutSurface.Wall => RoomOuterWallMaterialScale( settings, room ),
RoomLayoutSurface.Cap => MaterialScaleOrFallback( room.WallCapMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Cap ) ),
RoomLayoutSurface.Baseboard => MaterialScaleOrFallback( room.BaseboardMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Baseboard ) ),
RoomLayoutSurface.Threshold => MaterialScaleOrFallback( room.ThresholdMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Threshold ) ),
RoomLayoutSurface.Roof => MaterialScaleOrFallback( room.RoofMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Roof ) ),
_ => MaterialScaleForSettings( settings, surface )
};
}
private static string CorridorMaterialPath( RoomLayoutSettings settings, RoomLayoutCorridor corridor, RoomLayoutSurface surface )
{
return surface switch
{
RoomLayoutSurface.Floor or RoomLayoutSurface.CorridorFloor => FirstMaterialPath( corridor.FloorMaterialPath, settings.CorridorFloorMaterialPath ),
RoomLayoutSurface.Wall => CorridorOuterWallMaterialPath( settings, corridor ),
RoomLayoutSurface.Cap => FirstMaterialPath( corridor.WallCapMaterialPath, settings.WallCapMaterialPath ),
RoomLayoutSurface.Baseboard => FirstMaterialPath( corridor.BaseboardMaterialPath, settings.BaseboardMaterialPath ),
RoomLayoutSurface.Threshold => FirstMaterialPath( corridor.ThresholdMaterialPath, settings.ThresholdMaterialPath ),
RoomLayoutSurface.Roof => FirstMaterialPath( corridor.RoofMaterialPath, settings.RoofMaterialPath ),
_ => ""
};
}
private static float CorridorMaterialScale( RoomLayoutSettings settings, RoomLayoutCorridor corridor, RoomLayoutSurface surface )
{
return surface switch
{
RoomLayoutSurface.Floor or RoomLayoutSurface.CorridorFloor => MaterialScaleOrFallback( corridor.FloorMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.CorridorFloor ) ),
RoomLayoutSurface.Wall => CorridorOuterWallMaterialScale( settings, corridor ),
RoomLayoutSurface.Cap => MaterialScaleOrFallback( corridor.WallCapMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Cap ) ),
RoomLayoutSurface.Baseboard => MaterialScaleOrFallback( corridor.BaseboardMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Baseboard ) ),
RoomLayoutSurface.Threshold => MaterialScaleOrFallback( corridor.ThresholdMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Threshold ) ),
RoomLayoutSurface.Roof => MaterialScaleOrFallback( corridor.RoofMaterialScale, MaterialScaleForSettings( settings, RoomLayoutSurface.Roof ) ),
_ => MaterialScaleForSettings( settings, surface )
};
}
private static string RoomOuterWallMaterialPath( RoomLayoutSettings settings, RoomLayoutRoom room )
{
return FirstMaterialPath(
room.OuterWallMaterialPath,
FirstMaterialPath( room.WallMaterialPath, FirstMaterialPath( settings.OuterWallMaterialPath, settings.WallMaterialPath ) ) );
}
private static float RoomOuterWallMaterialScale( RoomLayoutSettings settings, RoomLayoutRoom room )
{
return MaterialScaleOrFallback(
room.OuterWallMaterialScale,
MaterialScaleOrFallback(
room.WallMaterialScale,
MaterialScaleOrFallback(
settings.OuterWallMaterialScale,
MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) ) );
}
private static string RoomInnerWallMaterialPath( RoomLayoutSettings settings, RoomLayoutRoom room )
{
return FirstMaterialPath(
room.InnerWallMaterialPath,
FirstMaterialPath( settings.InnerWallMaterialPath, settings.WallMaterialPath ) );
}
private static float RoomInnerWallMaterialScale( RoomLayoutSettings settings, RoomLayoutRoom room )
{
return MaterialScaleOrFallback(
room.InnerWallMaterialScale,
MaterialScaleOrFallback(
settings.InnerWallMaterialScale,
MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) );
}
private static string CorridorOuterWallMaterialPath( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
{
return FirstMaterialPath(
corridor.OuterWallMaterialPath,
FirstMaterialPath( corridor.WallMaterialPath, FirstMaterialPath( settings.OuterWallMaterialPath, settings.WallMaterialPath ) ) );
}
private static float CorridorOuterWallMaterialScale( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
{
return MaterialScaleOrFallback(
corridor.OuterWallMaterialScale,
MaterialScaleOrFallback(
corridor.WallMaterialScale,
MaterialScaleOrFallback(
settings.OuterWallMaterialScale,
MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) ) );
}
private static string CorridorInnerWallMaterialPath( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
{
return FirstMaterialPath(
corridor.InnerWallMaterialPath,
FirstMaterialPath( settings.InnerWallMaterialPath, settings.WallMaterialPath ) );
}
private static float CorridorInnerWallMaterialScale( RoomLayoutSettings settings, RoomLayoutCorridor corridor )
{
return MaterialScaleOrFallback(
corridor.InnerWallMaterialScale,
MaterialScaleOrFallback(
settings.InnerWallMaterialScale,
MaterialScaleOrFallback( settings.WallMaterialScale, settings.TextureWorldSize ) ) );
}
private static string ThresholdMaterialPath( RoomLayoutDocument document, RoomLayoutCorridor corridor, DoorPoint door )
{
var roomMaterialPath = document.FindRoom( door.RoomId ) is { } room
? room.ThresholdMaterialPath
: "";
return FirstMaterialPath(
corridor.ThresholdMaterialPath,
FirstMaterialPath( roomMaterialPath, document.Settings.ThresholdMaterialPath ) );
}
private static float ThresholdMaterialScale( RoomLayoutDocument document, RoomLayoutCorridor corridor, DoorPoint door )
{
var settingsScale = MaterialScaleForSettings( document.Settings, RoomLayoutSurface.Threshold );
var roomScale = document.FindRoom( door.RoomId ) is { } room
? room.ThresholdMaterialScale
: 0.0f;
return MaterialScaleOrFallback(
corridor.ThresholdMaterialScale,
MaterialScaleOrFallback( roomScale, settingsScale ) );
}
private static string FirstMaterialPath( string preferred, string fallback )
{
if ( !string.IsNullOrWhiteSpace( preferred ) )
{
return preferred.Trim();
}
return fallback?.Trim() ?? "";
}
private static float MaterialScaleOrFallback( float scale, float fallback )
{
return Math.Clamp( scale > 0.0f ? scale : fallback, 1.0f, 8192.0f );
}
private static bool IsKnownMissingBuiltInMaterial( string path )
{
return path.Equals( "materials/dev/gray_50.vmat", StringComparison.OrdinalIgnoreCase ) ||
path.Equals( "materials/dev/gray_75.vmat", StringComparison.OrdinalIgnoreCase ) ||
path.Equals( "materials/dev/reflectivity_30.vmat", StringComparison.OrdinalIgnoreCase );
}
private static bool IsTexturePath( string path )
{
var extension = System.IO.Path.GetExtension( path ).ToLowerInvariant();
return extension is ".png" or ".jpg" or ".jpeg" or ".tga" or ".psd" or ".vtex";
}
private static string TryCreateTextureMaterial( string texturePath )
{
var textureAssetPath = NormalizeAssetPath( texturePath );
var materialPath = $"room-layouts/generated-materials/texture-{StablePathHash( textureAssetPath )}.vmat";
try
{
if ( global::Editor.FileSystem.Content.IsReadOnly )
{
return null;
}
global::Editor.FileSystem.Content.CreateDirectory( "room-layouts/generated-materials" );
global::Editor.FileSystem.Content.WriteAllText( materialPath, BuildTextureMaterialFile( textureAssetPath ) );
return materialPath;
}
catch ( Exception exception )
{
Log.Warning( $"Unable to create interior layout material for {textureAssetPath}: {exception.Message}" );
return null;
}
}
private static string NormalizeAssetPath( string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
{
return "";
}
path = path.Trim().Replace( '\\', '/' ).TrimStart( '/' );
if ( path.StartsWith( "assets/", StringComparison.OrdinalIgnoreCase ) )
{
path = path[7..];
}
return path.EndsWith( ".vmat_c", StringComparison.OrdinalIgnoreCase )
? path[..^2]
: path;
}
private static string BuildTextureMaterialFile( string texturePath )
{
texturePath = texturePath.Replace( "\"", "\\\"" );
return $$"""
"Layer0"
{
"shader" "shaders/complex.shader"
"g_flModelTintAmount" "1.000000"
"g_vColorTint" "[1.000000 1.000000 1.000000 0.000000]"
"TextureColor" "{{texturePath}}"
"g_flMetalness" "0.000000"
"g_flRoughness" "0.850000"
"g_bFogEnabled" "1"
"g_vTexCoordScale" "[1.000 1.000]"
}
""";
}
private static string StablePathHash( string path )
{
const uint fnvOffset = 2166136261;
const uint fnvPrime = 16777619;
var hash = fnvOffset;
foreach ( var value in Encoding.UTF8.GetBytes( path.ToLowerInvariant() ) )
{
hash ^= value;
hash *= fnvPrime;
}
return hash.ToString( "x8", CultureInfo.InvariantCulture );
}
private enum RoomLayoutSurface
{
Floor,
Wall,
Cap,
Baseboard,
DoorFrame,
WindowFrame,
CorridorFloor,
Threshold,
Roof
}
}