Editor/Effigy/PaintMaterial.cs
using System;
namespace Effigy;
/// <summary>
/// The authoring half of the texture path: turn a painted canvas into the assets a compiled model
/// can bind.
///
/// WHY THIS IS IN THE KERNEL AND NOT THE EDITOR. The editor is where the files get written, but the
/// TEXT — the PNG bytes and the .vmat — is exactly the kind of thing the headless suite can judge:
/// do the braces balance, does the material name the image it was given. Same reason VmdlMaterials
/// and VmdlPhysics live here.
///
/// The chain is canvas → opaque RGBA → PNG → a .vmat naming the PNG, then the .vmat is bound to the
/// painted body's material slot through the existing MaterialNames mechanism. The canvas is baked
/// opaque over white first, because the canvas's straight alpha (0 where unpainted) has no sensible
/// meaning on an ordinary material's colour map — a transparent texel reads as a hole or black — and
/// white is the blank the paint covers, the same trick the old vertex-colour Replace mode used.
///
/// THE .VMAT NAMES THE PNG, NOT A .VTEX. The first cut wrote a .vtex in the chain and pointed the
/// material at it, and the engine's material compiler refused it outright: ".vtex files not yet
/// supported as an input texture type." A material's colour texture is an IMAGE in this engine — the
/// engine's own template binds `TextureColor "materials/default/default_color.tga"` — so the material
/// points straight at the PNG and the asset system compiles the PNG to a texture on its own.
/// </summary>
public static class PaintMaterial
{
/// <summary>
/// The canvas as an opaque RGBA buffer, composited over a base colour. Unpainted texels fall back
/// to the base, painted ones are source-over the base at their coverage, and the alpha is 255
/// everywhere.
/// </summary>
public static byte[] OpaqueRgba( PaintCanvas canvas,
byte baseR = DefaultBaseR, byte baseG = DefaultBaseG, byte baseB = DefaultBaseB )
{
if ( canvas is null )
throw new ArgumentNullException( nameof( canvas ) );
var opaque = new byte[canvas.Width * canvas.Height * 4];
canvas.BakeOpaque( opaque, baseR, baseG, baseB );
return opaque;
}
/// <summary>
/// What an unpainted texel becomes when nobody has given a better answer.
///
/// WHITE WAS A GUESS, and a bad one: paint one dot on a part and the compiled model is a white
/// part with a dot on it. The live preview used the same white, so they agreed with each other
/// and with nothing the part actually looked like. This grey is the placeholder the rest of the
/// preview already falls back to — closer to an unbound slot than 255,255,255 is. A bound
/// material should pass its own colour in; this is the default when it cannot.
/// </summary>
public const byte DefaultBaseR = 180;
public const byte DefaultBaseG = 180;
public const byte DefaultBaseB = 180;
/// <summary>
/// The .vmat source that binds <paramref name="imagePath"/> as an ordinary lit material's colour.
///
/// The Layer0 form rather than the kv3 one, deliberately: it is the shape the engine's own
/// `templates/default.vmat` still ships, so a material written this way compiles today where a
/// hand-rolled kv3 material is a guess at a format this kernel has never emitted. The colour
/// texture is the paint atlas, named by its SOURCE image path — the engine compiles the PNG to a
/// texture itself, and a .vtex is not accepted as a texture input (see the class comment). The
/// rest of the attributes are the same defaults the template carries, so the paint lights like
/// any other surface.
/// </summary>
public static string VmatSource( string imagePath )
{
return
"// THIS FILE IS AUTO-GENERATED\n" +
"\n" +
"Layer0\n" +
"{\n" +
"\tshader \"complex.vfx\"\n" +
"\n" +
"\t//---- Color ----\n" +
"\tg_flModelTintAmount \"1.000\"\n" +
"\tg_vColorTint \"[1.000000 1.000000 1.000000 0.000000]\"\n" +
$"\tTextureColor \"{imagePath}\"\n" +
"\n" +
"\t//---- Lighting ----\n" +
"\tg_flDirectionalLightmapMinZ \"0.050\"\n" +
"\tg_flDirectionalLightmapStrength \"1.000\"\n" +
"\n" +
"\t//---- Metalness ----\n" +
"\tg_flMetalness \"0.000\"\n" +
"\n" +
"\t//---- Normal ----\n" +
"\tTextureNormal \"materials/default/default_normal.tga\"\n" +
"\n" +
"\t//---- Roughness ----\n" +
"\tTextureRoughness \"materials/default/default_rough.tga\"\n" +
"\n" +
"\t//---- Texture Coordinates ----\n" +
"\tg_nScaleTexCoordUByModelScaleAxis \"0\"\n" +
"\tg_nScaleTexCoordVByModelScaleAxis \"0\"\n" +
"\tg_vTexCoordOffset \"[0.000 0.000]\"\n" +
"\tg_vTexCoordScale \"[1.000 1.000]\"\n" +
"\tg_vTexCoordScrollSpeed \"[0.000 0.000]\"\n" +
"}\n";
}
}