Effigy/WeightRamp.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// A heat-map atlas of one bone's influence, rasterised through the mesh's UVs.
///
/// NOT VERTEX COLOURS. Paint taught this the expensive way: resolution equal to mesh density is
/// unusable on a CAD cage (a box paints six faces), and the preview material that read a COLOR
/// stream was a workaround the atlas path existed to delete. Weight painting's *data* is still per
/// vertex — that is what the compiler skins — but the *display* is a texture, the same answer
/// colour painting landed on. Rasterising barycentric-interpolated vertex weights onto the atlas
/// is what the GPU will interpolate at deform time, so the ramp is the bind, drawn at texel
/// resolution rather than as eight coloured corners.
///
/// Nothing here writes <see cref="PolyMesh.VertexColors"/>.
/// </summary>
public static class WeightRamp
{
/// <summary>
/// Rasterise <paramref name="influence"/> (one value per vertex, 0..1) onto a fresh canvas.
/// Faces without UVs are skipped; overlapping islands overwrite, which is why a box wants an
/// unwrap before this is useful — the same door colour painting already has.
/// </summary>
public static PaintCanvas Bake( PolyMesh mesh, float[] influence, int resolution )
{
if ( mesh is null )
throw new ArgumentNullException( nameof( mesh ) );
if ( influence is null )
throw new ArgumentNullException( nameof( influence ) );
if ( influence.Length != mesh.VertexCount )
throw new ArgumentException( $"influence ({influence.Length}) and mesh ({mesh.VertexCount}) disagree" );
if ( resolution < 1 )
throw new ArgumentOutOfRangeException( nameof( resolution ) );
var canvas = new PaintCanvas( resolution, resolution );
foreach ( var face in mesh.Faces )
{
if ( face.Count < 3 || face.UVs is null || face.UVs.Length != face.Count )
continue;
var corners = new List<Vec3>( face.Count );
for ( var c = 0; c < face.Count; c++ )
corners.Add( mesh.Positions[face.Indices[c]] );
foreach ( var (ia, ib, ic) in Triangulate.Face( corners ) )
{
var u0 = face.UVs[ia];
var u1 = face.UVs[ib];
var u2 = face.UVs[ic];
var w0 = influence[face.Indices[ia]];
var w1 = influence[face.Indices[ib]];
var w2 = influence[face.Indices[ic]];
NormalBake.Rasterise( u0, u1, u2, resolution, resolution, ( x, y, wa, wb, wc ) =>
{
var t = Math.Clamp( w0 * wa + w1 * wb + w2 * wc, 0f, 1f );
Color( t, out var r, out var g, out var b );
canvas.Blend( x, y, r, g, b, 1f );
} );
}
}
return canvas;
}
/// <summary>
/// Classic weight ramp: blue at 0, through cyan and green and yellow, to red at 1. Zero is
/// "this bone does not own you" and has to read as cold, not as missing.
/// </summary>
public static void Color( float t, out byte r, out byte g, out byte b )
{
t = Math.Clamp( t, 0f, 1f );
float rf, gf, bf;
if ( t < 0.25f )
{
var u = t / 0.25f;
rf = 0f;
gf = u;
bf = 1f;
}
else if ( t < 0.5f )
{
var u = ( t - 0.25f ) / 0.25f;
rf = 0f;
gf = 1f;
bf = 1f - u;
}
else if ( t < 0.75f )
{
var u = ( t - 0.5f ) / 0.25f;
rf = u;
gf = 1f;
bf = 0f;
}
else
{
var u = ( t - 0.75f ) / 0.25f;
rf = 1f;
gf = 1f - u;
bf = 0f;
}
r = ToByte( rf );
g = ToByte( gf );
b = ToByte( bf );
}
static byte ToByte( float v ) => (byte)Math.Clamp( MathF.Round( v * 255f ), 0f, 255f );
}