Utility class for painting/material slot management for a PolyMesh. It computes which material slots faces occupy, chooses slots to bind an atlas to, detects if merging painted meshes would lose paint, finds the next free material slot, and reassigns all faces to a slot.
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// Which material slots a painted body's atlas should bind to.
///
/// THE BUG THIS EXISTS FOR. Compile used to write the authored .vmat onto
/// <c>MaterialNames[0]</c> unconditionally. Slot 0 is where faces start, so that is usually right,
/// and that is why it shipped. It is wrong when every face has been dropped onto another slot
/// (the atlas binds to a slot nothing samples) and wrong when slot 0 already carries a material
/// the user chose (that name is overwritten). Bind the slots the painted faces actually wear.
/// </summary>
public static class PaintBind
{
/// <summary>Every material slot a face of this mesh is on, sorted.</summary>
public static SortedSet<int> SlotsOn( PolyMesh mesh )
{
var slots = new SortedSet<int>();
if ( mesh?.Faces is null )
return slots;
foreach ( var face in mesh.Faces )
slots.Add( face.Material );
return slots;
}
/// <summary>
/// The slots this painted body should bind its atlas to: the slots its faces wear.
///
/// Does not invent a slot. Does not write slot 0 unless a face is actually on it. A second
/// painted body that shares a slot with this one is the caller's problem — isolate faces onto
/// a fresh slot before asking, or accept that two atlases cannot share a name.
/// </summary>
public static List<int> SlotsToBind( PolyMesh mesh )
{
var slots = new List<int>();
foreach ( var slot in SlotsOn( mesh ) )
slots.Add( slot );
return slots;
}
/// <summary>
/// Whether merging <paramref name="source"/> into <paramref name="target"/> would drop an
/// atlas. Paint is one canvas per body; a merge can show one, so the target keeps its own and
/// only adopts the source's when it has none. Two painted bodies is the case this names.
/// </summary>
public static bool MergeDropsPaint( PolyMesh target, PolyMesh source ) =>
target?.Paint is not null && source?.Paint is not null;
/// <summary>A free slot no face on <paramref name="used"/> wears and no name occupies.</summary>
public static int NextFreeSlot( IReadOnlyDictionary<int, string> names, IEnumerable<int> used )
{
var taken = new HashSet<int>();
if ( used is not null )
{
foreach ( var slot in used )
taken.Add( slot );
}
if ( names is not null )
{
foreach ( var slot in names.Keys )
taken.Add( slot );
}
// Slot 0 is the default every face starts on; MaterialDrop never allocates it either.
for ( var slot = 1; slot < 64; slot++ )
{
if ( !taken.Contains( slot ) )
return slot;
}
return -1;
}
/// <summary>Move every face of <paramref name="mesh"/> onto <paramref name="slot"/>.</summary>
public static void AssignAllFaces( PolyMesh mesh, int slot )
{
if ( mesh?.Faces is null )
return;
foreach ( var face in mesh.Faces )
face.Material = slot;
}
}