Editor-side partial of ArchMesh handling face creation and texture/geometry parameterization. It emits faces into a HalfEdgeMesh, computes normals with Newell, chooses texture axes, sets material/texture parameters, folds data into a content hash, handles authored UVs from weaves, and caches material sheet sizes and material keys.
using System;
using System.Collections.Generic;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
public sealed partial class ArchMesh
{
// Vertices() handed its handles to whichever canvas is open, so the face has to be built on the same one.
void Face( VertexHandle[] handles, ArchBrush brush, Vector3 tangent, ArchWeave? weave = null )
{
if ( open is { } into )
{
into.Face( handles, brush, tangent, weave );
return;
}
if ( handles is null || handles.Length < 3 )
{
return;
}
var corners = Collapsed( handles );
if ( corners is null )
{
return;
}
var face = mesh.AddFace( corners );
if ( !face.IsValid )
{
return;
}
mesh.SetFaceMaterial( face, brush.Material ?? ArchBrush.Missing );
// Axes from the normal in projection space - the local normal stretches rotated pieces.
var normal = projection.Rotation * Normal( corners );
var along = tangent.IsNearZeroLength ? tangent : projection.Rotation * tangent;
Axes( normal, along, out var axisU, out var axisV );
var scale = brush.TexelScale > 0f ? brush.TexelScale : TexelScale;
mesh.SetFaceTextureParameters( face, new Vector4( axisU, brush.Shift.x ), new Vector4( axisV, brush.Shift.y ), scale );
Woven( face, handles, corners, brush, weave, scale );
Folded( corners, brush, axisU, axisV, scale );
emitted++;
IsEmpty = false;
finished = false;
}
// Corners as INDICES, because their positions were folded where they were made.
void Folded( VertexHandle[] corners, ArchBrush brush, Vector3 axisU, Vector3 axisV, float scale )
{
content = ArchHash.Fold( content, corners.Length );
foreach ( var corner in corners )
{
content = ArchHash.Fold( content, corner.Index );
}
content = ArchHash.Fold( content, Keyed( brush.Material ) );
content = ArchHash.Fold( content, scale );
content = ArchHash.Fold( content, brush.Shift );
content = ArchHash.Fold( content, axisU );
content = ArchHash.Fold( content, axisV );
}
static readonly Dictionary<Material, ulong> brushKeys = new();
// By NAME, not by reference: reloading a kit rebuilds the material cache, and a canvas that came back
// identical has to key identically or every part in the plan reads as changed.
static ulong Keyed( Material material )
{
if ( material is null )
{
return 0ul;
}
if ( brushKeys.TryGetValue( material, out var held ) )
{
return held;
}
return brushKeys[material] = ArchHash.Of( material.Name );
}
// On a bend the outside outruns the centreline, so the strip AUTHORS texcoords from the developed surface.
// Projected parameters stay set - the mapping tool edits with them, the engine falls back on them.
void Woven( FaceHandle face, VertexHandle[] handles, VertexHandle[] corners, ArchBrush brush, ArchWeave? weave, float scale )
{
if ( weave is not { } woven || handles.Length is not (3 or 4) || !mesh.GetFaceVerticesConnectedToFace( face, out var edges ) )
{
return;
}
var sheet = Sheet( brush.Material );
var vertices = mesh.GetFaceVertices( face );
for ( var index = 0; index < edges.Length && index < vertices.Length; index++ )
{
var corner = System.Array.IndexOf( handles, vertices[index] );
if ( corner < 0 )
{
continue;
}
var developed = woven.Corner( corner ) / scale;
var coord = new Vector2( developed.x / sheet.x, developed.y / sheet.y );
mesh.SetTextureCoord( edges[index], coord );
content = ArchHash.Fold( content, coord );
}
// Claimed, because Finish recomputes coordinates from the projected parameters for everything that did NOT
// author its own - which is most of the generator. Left to run over the whole mesh it threw this weave away
// again the moment the canvas was finished, and no strip in the game ever tiled.
authored.Add( face );
}
// The size the engine measures brush-mapped faces against - density stays the texel scale's business.
static Vector2 Sheet( Material material )
{
if ( material is null )
{
return 512f;
}
if ( sheets.TryGetValue( material, out var size ) )
{
return size;
}
size = 512f;
if ( material.FirstTexture is { } texture )
{
size = texture.Size;
var width = material.Attributes.GetInt( "WorldMappingWidth" );
var height = material.Attributes.GetInt( "WorldMappingHeight" );
if ( width > 0 ) size.x = width / 0.25f;
if ( height > 0 ) size.y = height / 0.25f;
}
sheets[material] = size;
return size;
}
// Collapsed at ONE end is still a triangle and must be emitted - refusing it holes the strip.
// At both it is a line; a corner repeating out of sequence is a bowtie - neither has a face.
static VertexHandle[] Collapsed( VertexHandle[] handles )
{
var corners = new List<VertexHandle>( handles.Length );
for ( var index = 0; index < handles.Length; index++ )
{
if ( !handles[index].Equals( handles[(index + handles.Length - 1) % handles.Length] ) )
{
corners.Add( handles[index] );
}
}
return corners.Count >= 3 && !Repeats( corners ) ? corners.ToArray() : null;
}
static bool Repeats( IReadOnlyList<VertexHandle> handles )
{
for ( var index = 0; index < handles.Count; index++ )
{
for ( var other = index + 1; other < handles.Count; other++ )
{
if ( handles[index].Equals( handles[other] ) )
{
return true;
}
}
}
return false;
}
public static Vector3 Newell( IReadOnlyList<Vector3> points )
{
var normal = Vector3.Zero;
for ( var index = 0; index < points.Count; index++ )
{
var a = points[index];
var b = points[(index + 1) % points.Count];
normal += new Vector3(
(a.y - b.y) * (a.z + b.z),
(a.z - b.z) * (a.x + b.x),
(a.x - b.x) * (a.y + b.y) );
}
return normal.IsNearZeroLength ? Vector3.Up : normal.Normal;
}
static Vector3 Centre( IReadOnlyList<Vector3> points )
{
var total = Vector3.Zero;
foreach ( var point in points )
{
total += point;
}
return total / points.Count;
}
static List<Vector3> Flipped( IReadOnlyList<Vector3> points )
{
var reversed = new List<Vector3>( points );
reversed.Reverse();
return reversed;
}
// Newell over EVERY corner, not a cross product of the first three: a carved n-gon can open with three
// collinear corners - a clip crossing that lands on one, or a stranded corner conformed into an edge -
// and their cross product is direction noise the texture axes are then chosen from. That is a face whose
// tiling smears down the slope and whose texture parameters read back NaN.
Vector3 Normal( IReadOnlyList<VertexHandle> handles )
{
var corners = new List<Vector3>( handles.Count );
foreach ( var handle in handles )
{
corners.Add( mesh.GetVertexPosition( handle ) );
}
return Newell( corners );
}
static void Axes( Vector3 normal, Vector3 tangent, out Vector3 axisU, out Vector3 axisV )
{
if ( !tangent.IsNearZeroLength )
{
// Projected INTO the face's own plane, never taken raw. A tangent leaning out of that plane advances u
// barely at all across the face, so the courses stretch out to nothing in that direction - which is what a
// piece's run does on every face it stands EDGE ON to, and a frame carried round a bend or over a grade is
// a degree or two out of square with its own sides anyway.
axisU = (tangent - normal * Vector3.Dot( normal, tangent )).Normal;
axisV = Vector3.Cross( normal, axisU ).Normal;
if ( !axisU.IsNearZeroLength && !axisV.IsNearZeroLength )
{
return;
}
// Nothing of the run survived the projection - the face stands square across it, the end of a block whose
// other faces it maps. That face's own level line is still the piece's direction; the world's axes are not,
// and a piece that said which way it runs must never be mapped to them.
axisU = Vector3.Cross( normal, Vector3.Up ).Normal;
axisV = Vector3.Cross( normal, axisU ).Normal;
if ( !axisU.IsNearZeroLength && !axisV.IsNearZeroLength )
{
return;
}
}
var absolute = new Vector3( MathF.Abs( normal.x ), MathF.Abs( normal.y ), MathF.Abs( normal.z ) );
// Pitched axes: u along the eave, v up the slope - world x/y would skew every course.
if ( absolute.z > 0.05f && absolute.z < 0.995f )
{
var level = Vector3.Cross( normal, Vector3.Up ).Normal;
var pitch = Vector3.Cross( normal, level ).Normal;
if ( !level.IsNearZeroLength && !pitch.IsNearZeroLength )
{
axisU = level;
axisV = pitch;
return;
}
}
if ( absolute.z >= absolute.x && absolute.z >= absolute.y )
{
axisU = Vector3.Forward;
axisV = -Vector3.Left;
return;
}
if ( absolute.x >= absolute.y )
{
axisU = Vector3.Left;
axisV = -Vector3.Up;
return;
}
axisU = Vector3.Forward;
axisV = -Vector3.Up;
}
}