67 lines
3.2 KiB
JavaScript
67 lines
3.2 KiB
JavaScript
import { mkdir, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import sharp from "sharp";
|
|
import { createGameAssetIO } from "./lib/ktx2.mjs";
|
|
|
|
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const sourcePath = path.join(repositoryRoot, "game_assets/models/claudecraft/chars/players/mage.glb");
|
|
const outputPath = path.join(repositoryRoot, "game_assets/textures/claudecraft/chars/players/priest-vestments.webp");
|
|
|
|
function clamp01(value) {
|
|
return Math.max(0, Math.min(1, value));
|
|
}
|
|
|
|
function luminance(red, green, blue) {
|
|
return (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255;
|
|
}
|
|
|
|
function paintSwatch(pixels, width, channels, rectangle, shadow, highlight) {
|
|
const samples = [];
|
|
for (let y = rectangle.y; y < rectangle.y + rectangle.height; y += 1) {
|
|
for (let x = rectangle.x; x < rectangle.x + rectangle.width; x += 1) {
|
|
const offset = (y * width + x) * channels;
|
|
if (pixels[offset + 3] === 0) continue;
|
|
samples.push(luminance(pixels[offset], pixels[offset + 1], pixels[offset + 2]));
|
|
}
|
|
}
|
|
const minimum = Math.min(...samples);
|
|
const maximum = Math.max(...samples);
|
|
const range = Math.max(0.001, maximum - minimum);
|
|
|
|
for (let y = rectangle.y; y < rectangle.y + rectangle.height; y += 1) {
|
|
for (let x = rectangle.x; x < rectangle.x + rectangle.width; x += 1) {
|
|
const offset = (y * width + x) * channels;
|
|
if (pixels[offset + 3] === 0) continue;
|
|
const sourceLuminance = luminance(pixels[offset], pixels[offset + 1], pixels[offset + 2]);
|
|
const mix = clamp01((sourceLuminance - minimum) / range);
|
|
for (let channel = 0; channel < 3; channel += 1) {
|
|
pixels[offset + channel] = Math.round(shadow[channel] + (highlight[channel] - shadow[channel]) * mix);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const io = await createGameAssetIO();
|
|
const document = await io.read(sourcePath);
|
|
const sourceTexture = document.getRoot().listTextures().find((texture) => texture.getName() === "mage_texture");
|
|
if (!sourceTexture?.getImage()) throw new Error("Mage source is missing mage_texture.");
|
|
|
|
const decoded = await sharp(sourceTexture.getImage()).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
const { width, height, channels } = decoded.info;
|
|
if (width !== 512 || height !== 512 || channels !== 4) {
|
|
throw new Error(`Unexpected mage palette shape: ${width}x${height}x${channels}.`);
|
|
}
|
|
|
|
const pixels = new Uint8Array(decoded.data);
|
|
paintSwatch(pixels, width, channels, { x: 0, y: 128, width: 128, height: 128 }, [48, 58, 94], [244, 239, 211]);
|
|
paintSwatch(pixels, width, channels, { x: 128, y: 128, width: 64, height: 128 }, [105, 66, 18], [255, 218, 109]);
|
|
paintSwatch(pixels, width, channels, { x: 64, y: 256, width: 64, height: 128 }, [105, 66, 18], [255, 218, 109]);
|
|
paintSwatch(pixels, width, channels, { x: 128, y: 256, width: 64, height: 128 }, [37, 71, 101], [157, 215, 221]);
|
|
|
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
await writeFile(outputPath, await sharp(pixels, { raw: { width, height, channels } })
|
|
.webp({ quality: 92, smartSubsample: true })
|
|
.toBuffer());
|
|
console.log(`Built ${path.relative(repositoryRoot, outputPath)}`);
|