Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions tools/spine-renderer/render.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ const OUTPUT_DIR = path.resolve(
"../../backend/static/images/monsters"
);

const OUTPUT_SIZE = 512; // final output image size
const OUTPUT_WIDTH = 512; // final output image size
const OUTPUT_HEIGHT = 512; // final output image size
const SUPERSAMPLE = 2; // render at Nx and downscale to hide triangle seams
const RENDER_SIZE = OUTPUT_SIZE * SUPERSAMPLE;
const RENDER_WIDTH = OUTPUT_WIDTH * SUPERSAMPLE;
const RENDER_HEIGHT = OUTPUT_HEIGHT * SUPERSAMPLE;
const PADDING = 20 * SUPERSAMPLE;

/** Minimal Texture wrapper for node-canvas Image */
Expand Down Expand Up @@ -190,12 +192,12 @@ async function renderMonster(monsterDir, monsterName) {
const skelHeight = maxY - minY;

// Calculate canvas size to fit with padding, maintaining aspect ratio
const availableSize = RENDER_SIZE - PADDING * 2;
const availableSize = RENDER_WIDTH - PADDING * 2;
const scale = Math.min(availableSize / skelWidth, availableSize / skelHeight);

// Render skeleton (with automatic slot-by-slot fallback for complex meshes)
const imgData = renderSkeleton(skeleton, RENDER_SIZE, scale, minX, minY, maxX, maxY);
const buffer = imageDataToPng(imgData, RENDER_SIZE, OUTPUT_SIZE);
const imgData = renderSkeleton(skeleton, RENDER_WIDTH, RENDER_HEIGHT, scale, minX, minY, maxX, maxY);
const buffer = imageDataToPng(imgData, RENDER_WIDTH, RENDER_HEIGHT, OUTPUT_WIDTH, OUTPUT_HEIGHT);

// Save to PNG
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
Expand Down
13 changes: 8 additions & 5 deletions tools/spine-renderer/render_all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ const BASE = path.resolve(import.meta.dirname, "../..");
const ANIM_ROOT = path.join(BASE, "extraction/raw/animations");
const OUTPUT_ROOT = path.join(BASE, "backend/static/images/renders");

const OUTPUT_SIZE = 512;
const OUTPUT_WIDTH = 512;
const OUTPUT_HEIGHT = 512;
const SUPERSAMPLE = 3;
const RENDER_SIZE = OUTPUT_SIZE * SUPERSAMPLE;
const RENDER_WIDTH = OUTPUT_WIDTH * SUPERSAMPLE;
const RENDER_HEIGHT = OUTPUT_HEIGHT * SUPERSAMPLE;
const PADDING = 20 * SUPERSAMPLE;
const SHADOW_NAMES = new Set(["shadow", "shadow2", "ground", "ground_shadow"]);
const IDLE_NAMES = ["idle_loop", "idle", "Idle_loop", "Idle", "rest_idle", "rest_loop", "loop", "animation"];
Expand Down Expand Up @@ -125,12 +127,13 @@ async function renderSkel(skelPath, outPath) {
}

const sw = maxX - minX, sh = maxY - minY;
const avail = RENDER_SIZE - PADDING * 2;
// TODO: Generalize this.
const avail = RENDER_WIDTH - PADDING * 2;
const scale = Math.min(avail / sw, avail / sh);

// Render skeleton (with automatic slot-by-slot fallback for complex meshes)
const imgData = renderSkeleton(skeleton, RENDER_SIZE, scale, minX, minY, maxX, maxY);
const buffer = imageDataToPng(imgData, RENDER_SIZE, OUTPUT_SIZE);
const imgData = renderSkeleton(skeleton, RENDER_WIDTH, RENDER_HEIGHT, scale, minX, minY, maxX, maxY);
const buffer = imageDataToPng(imgData, RENDER_WIDTH, RENDER_HEIGHT, OUTPUT_WIDTH, OUTPUT_HEIGHT);

fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, buffer);
Expand Down
43 changes: 22 additions & 21 deletions tools/spine-renderer/render_all_webgl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const BASE = path.resolve(__dirname, "../..");
const ANIM_ROOT = path.join(BASE, "extraction/raw/animations");
const OUTPUT_ROOT = path.join(BASE, "backend/static/images/renders");

const OUTPUT_SIZE = 512;
const OUTPUT_WIDTH = 512;
const OUTPUT_HEIGHT = 512;
const IDLE_NAMES = ["idle_loop", "idle", "Idle_loop", "Idle", "rest_idle", "rest_loop", "loop", "animation"];
const SHADOW_NAMES = ["shadow", "shadow2", "shadow_v2", "ground", "ground_shadow"];
const HIDDEN_SLOTS = ["smoketex", "smoke_tex", "smokeplacholder", "smoke_placeholder", "megatail", "megablade"];
Expand All @@ -36,7 +37,7 @@ function findAllSkels(dir) {
const spineCorePath = path.join(__dirname, "node_modules/@esotericsoftware/spine-webgl/dist/iife/spine-webgl.js");
const spineCoreCode = fs.readFileSync(spineCorePath, "utf-8");

async function renderSkel(page, skelPath, outPath, outputSize) {
async function renderSkel(page, skelPath, outPath, outputWidth, outputHeight) {
const dir = path.dirname(skelPath);
const skelName = path.basename(skelPath, ".skel");
const atlasPath = path.join(dir, skelName + ".atlas");
Expand Down Expand Up @@ -71,16 +72,16 @@ async function renderSkel(page, skelPath, outPath, outputSize) {
});

const result = await page.evaluate(async (params) => {
const { skelB64, atlasB64, textureData, outputSize, idleNames, shadowNames, hiddenSlots, spineCoreCode } = params;
const { skelB64, atlasB64, textureData, outputWidth, outputHeight, idleNames, shadowNames, hiddenSlots, spineCoreCode } = params;

if (!window.spine) {
eval(spineCoreCode.replace(/^"use strict";\s*var spine\s*=/, "window.spine ="));
}
const spine = window.spine;

const canvas = document.createElement("canvas");
canvas.width = outputSize;
canvas.height = outputSize;
canvas.width = outputWidth;
canvas.height = outputHeight;
document.body.appendChild(canvas);

const gl = canvas.getContext("webgl2", { alpha: true, premultipliedAlpha: false, preserveDrawingBuffer: true })
Expand Down Expand Up @@ -175,17 +176,17 @@ async function renderSkel(page, skelPath, outPath, outputSize) {
if (!isFinite(minX)) return { error: "no bounds" };

const sw = maxX - minX, sh = maxY - minY;
const padding = outputSize * 0.04;
const avail = outputSize - padding * 2;
const padding = outputWidth * 0.04;
const avail = outputWidth - padding * 2;
const scale = Math.min(avail / sw, avail / sh);
const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;

mvp.ortho2d(
cx - outputSize / (2 * scale), cy - outputSize / (2 * scale),
outputSize / scale, outputSize / scale
cx - outputWidth / (2 * scale), cy - outputHeight / (2 * scale),
outputWidth / scale, outputHeight / scale
);

gl.viewport(0, 0, outputSize, outputSize);
gl.viewport(0, 0, outputWidth, outputWidth);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be a copy/paste error, second arg should be outputHeight

gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.BLEND);
Expand All @@ -210,22 +211,22 @@ async function renderSkel(page, skelPath, outPath, outputSize) {
batcher.end();
shader.unbind();

const pixels = new Uint8Array(outputSize * outputSize * 4);
gl.readPixels(0, 0, outputSize, outputSize, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const pixels = new Uint8Array(outputWidth * outputHeight * 4);
gl.readPixels(0, 0, outputWidth, outputHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

// Flip vertically
const flipped = new Uint8Array(pixels.length);
const rowSize = outputSize * 4;
for (let y = 0; y < outputSize; y++) {
flipped.set(pixels.subarray((outputSize - 1 - y) * rowSize, (outputSize - y) * rowSize), y * rowSize);
const rowSize = outputWidth * 4;
for (let y = 0; y < outputWidth; y++) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and line R221 should be outputHeight, I think you're flipping y loop here?

flipped.set(pixels.subarray((outputWidth - 1 - y) * rowSize, (outputWidth - y) * rowSize), y * rowSize);
}

// Check if anything was actually rendered
let nonTransparent = 0;
for (let i = 3; i < flipped.length; i += 4) {
if (flipped[i] > 0) nonTransparent++;
}
if (nonTransparent < outputSize * outputSize * 0.001) {
if (nonTransparent < outputWidth * outputWidth * 0.001) {
return { error: "no bounds (blank render)" };
}

Expand All @@ -235,7 +236,7 @@ async function renderSkel(page, skelPath, outPath, outputSize) {
size: `${sw.toFixed(0)}x${sh.toFixed(0)}`,
};
}, {
skelB64, atlasB64, textureData, outputSize,
skelB64, atlasB64, textureData, outputWidth, outputHeight,
idleNames: IDLE_NAMES, shadowNames: SHADOW_NAMES, hiddenSlots: HIDDEN_SLOTS, spineCoreCode,
});

Expand All @@ -245,17 +246,17 @@ async function renderSkel(page, skelPath, outPath, outputSize) {
fs.mkdirSync(path.dirname(outPath), { recursive: true });

// Write PNG via node-canvas
const pngCanvas = createCanvas(outputSize, outputSize);
const pngCanvas = createCanvas(outputWidth, outputHeight);
const pngCtx = pngCanvas.getContext("2d");
const imgData = pngCtx.createImageData(outputSize, outputSize);
const imgData = pngCtx.createImageData(outputWidth, outputHeight);
imgData.data.set(rawBuffer);
pngCtx.putImageData(imgData, 0, 0);
fs.writeFileSync(outPath, pngCanvas.toBuffer("image/png"));

// Write WebP via sharp
const webpPath = outPath.replace(/\.png$/, ".webp");
const webpBuffer = await sharp(rawBuffer, {
raw: { width: outputSize, height: outputSize, channels: 4 },
raw: { width: outputWidth, height: outputHeight, channels: 4 },
}).webp({ quality: 90 }).toBuffer();
fs.writeFileSync(webpPath, webpBuffer);

Expand Down Expand Up @@ -291,7 +292,7 @@ async function main() {
const outPath = path.join(OUTPUT_ROOT, relDir, skelName + ".png");
const label = path.join(relDir, skelName);

const result = await renderSkel(page, skelPath, outPath, OUTPUT_SIZE);
const result = await renderSkel(page, skelPath, outPath, OUTPUT_WIDTH, OUTPUT_HEIGHT);
if (result.status === "ok") {
console.log(` OK ${label} (${result.size})`);
ok++;
Expand Down
21 changes: 13 additions & 8 deletions tools/spine-renderer/render_extras.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ class NodeTexture extends Texture {
const BASE = path.resolve("/Users/peterlord/Documents/Projects/spire-codex");
const ANIM = path.join(BASE, "extraction/raw/animations");
const IMG = path.join(BASE, "backend/static/images");
const OUTPUT_SIZE = 512, SS = 2, RS = OUTPUT_SIZE * SS, PAD = 20 * SS;
const OUTPUT_WIDTH = 512;
const OUTPUT_HEIGHT = 512;
const SUPERSAMPLE = 2;
const RENDER_WIDTH = OUTPUT_WIDTH * SUPERSAMPLE;
const RENDER_HEIGHT = OUTPUT_HEIGHT * SUPERSAMPLE;
const PADDING = 20 * SUPERSAMPLE;

async function render(dir, skelName, outPath) {
const skelPath = path.join(dir, skelName + ".skel");
const atlasPath = path.join(dir, skelName + ".atlas");

if (!fs.existsSync(skelPath) || !fs.existsSync(atlasPath)) {
console.log(" SKIP " + skelName + ": missing files");
return false;
Expand Down Expand Up @@ -85,24 +90,24 @@ async function render(dir, skelName, outPath) {
if (!isFinite(minX)) { console.log(" SKIP " + skelName + ": no bounds"); return false; }

const sw = maxX - minX, sh = maxY - minY;
const avail = RS - PAD * 2;
const avail = RENDER_WIDTH - PADDING * 2;
const scale = Math.min(avail / sw, avail / sh);

const canvas = createCanvas(RS, RS);
const canvas = createCanvas(RENDER_WIDTH, RENDER_HEIGHT);
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, RS, RS);
ctx.clearRect(0, 0, RENDER_WIDTH, RENDER_HEIGHT);
ctx.save();
ctx.translate(RS/2, RS/2);
ctx.translate(RENDER_WIDTH/2, RENDER_HEIGHT/2);
ctx.scale(scale, -scale);
ctx.translate(-(minX+maxX)/2, -(minY+maxY)/2);
const renderer = new SkeletonRenderer(ctx);
renderer.triangleRendering = true;
renderer.draw(skeleton);
ctx.restore();

const out = createCanvas(OUTPUT_SIZE, OUTPUT_SIZE);
const out = createCanvas(OUTPUT_WIDTH, OUTPUT_HEIGHT);
const oc = out.getContext("2d");
oc.drawImage(canvas, 0, 0, OUTPUT_SIZE, OUTPUT_SIZE);
oc.drawImage(canvas, 0, 0, OUTPUT_WIDTH, OUTPUT_HEIGHT);

fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, out.toBuffer("image/png"));
Expand Down
35 changes: 18 additions & 17 deletions tools/spine-renderer/render_gif.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ const HIDDEN_SLOTS = [
async function main() {
const skelDir = path.resolve(process.argv[2] || "");
const outputPath = path.resolve(process.argv[3] || "output.gif");
const outputSize = parseInt(process.argv[4] || "256");
const outputWidth = parseInt(process.argv[4] || "256");
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This halves it, try *2ing it? same with r57

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if people do node render_gif.mjs

512 they'll get width 512 and height 256 i think

maybe try parseInt(process.argv[5] ||
process.argv[4] || "256")

const outputHeight = parseInt(process.argv[5] || "256");
const fpsArg = process.argv.find(a => a.startsWith("--fps="));
const fps = fpsArg ? parseInt(fpsArg.split("=")[1]) : 20;
const whiteMode = process.argv.includes("--white");
Expand Down Expand Up @@ -83,7 +84,7 @@ async function main() {
textureData[tf] = fs.readFileSync(path.join(skelDir, tf)).toString("base64");
}

console.log(`Rendering ${skelName} as GIF at ${outputSize}x${outputSize}, ${fps}fps...`);
console.log(`Rendering ${skelName} as GIF at ${outputWidth}x${outputHeight}, ${fps}fps...`);
console.log(` Textures: ${textureFiles.join(", ")}`);

const browser = await chromium.launch({ headless: true, channel: "chrome" });
Expand All @@ -95,9 +96,9 @@ async function main() {
if (isStreamFormat) {
fs.mkdirSync(framesDir, { recursive: true });
await page.exposeFunction("__saveFrame", (idx, pixels) => {
const pngCanvas = createCanvas(outputSize, outputSize);
const pngCanvas = createCanvas(outputWidth, outputHeight);
const pCtx = pngCanvas.getContext("2d");
const imgData = pCtx.createImageData(outputSize, outputSize);
const imgData = pCtx.createImageData(outputWidth, outputHeight);
imgData.data.set(new Uint8ClampedArray(pixels));
pCtx.putImageData(imgData, 0, 0);
fs.writeFileSync(path.join(framesDir, `frame_${String(idx).padStart(4, "0")}.png`), pngCanvas.toBuffer("image/png"));
Expand All @@ -108,7 +109,7 @@ async function main() {
const spineCoreCode = fs.readFileSync(spineCorePath, "utf-8");

const result = await page.evaluate(async (params) => {
const { skelB64, atlasB64, textureData, outputSize, fps, streamFrames, idleNames, shadowNames, hiddenSlots, whiteMode, skinName, animOverride, spineCoreCode } = params;
const { skelB64, atlasB64, textureData, outputWidth, outputHeight, fps, streamFrames, idleNames, shadowNames, hiddenSlots, whiteMode, skinName, animOverride, spineCoreCode } = params;

eval(spineCoreCode.replace(/^"use strict";\s*var spine\s*=/, "window.spine ="));
const spine = window.spine;
Expand Down Expand Up @@ -249,14 +250,14 @@ async function main() {
renderer.draw(batcher, skeleton);
batcher.end();

const pixels = new Uint8Array(outputSize * outputSize * 4);
gl.readPixels(0, 0, outputSize, outputSize, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const pixels = new Uint8Array(outputWidth * outputHeight * 4);
gl.readPixels(0, 0, outputSize, outputHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

// Flip vertically
const flipped = new Uint8Array(outputSize * outputSize * 4);
const rowSize = outputSize * 4;
for (let row = 0; row < outputSize; row++) {
flipped.set(pixels.subarray((outputSize - 1 - row) * rowSize, (outputSize - row) * rowSize), row * rowSize);
const flipped = new Uint8Array(outputWidth * outputHeight * 4);
const rowSize = outputWidth * 4;
for (let row = 0; row < outputWidth; row++) {
flipped.set(pixels.subarray((outputWidth - 1 - row) * rowSize, (outputWidth - row) * rowSize), row * rowSize);
}

// White mode
Expand All @@ -280,7 +281,7 @@ async function main() {

return { frames: streamFrames ? [] : frames, frameCount, duration };
}, {
skelB64, atlasB64, textureData, outputSize, fps,
skelB64, atlasB64, textureData, outputWidth, outputHeight, fps,
streamFrames: outputPath.endsWith(".webp") || outputPath.endsWith(".apng"),
idleNames: IDLE_NAMES, shadowNames: SHADOW_NAMES, hiddenSlots: HIDDEN_SLOTS,
whiteMode, skinName, animOverride, spineCoreCode,
Expand All @@ -298,10 +299,10 @@ async function main() {
if (!isStreamFormat) {
// Fallback: save frames from memory
fs.mkdirSync(tmpDir, { recursive: true });
const pngCanvas2 = createCanvas(outputSize, outputSize);
const pngCanvas2 = createCanvas(outputWidth, outputHeight);
const pCtx2 = pngCanvas2.getContext("2d");
for (let f = 0; f < result.frameCount; f++) {
const imgData = pCtx2.createImageData(outputSize, outputSize);
const imgData = pCtx2.createImageData(outputWidth, outputHeight);
imgData.data.set(new Uint8ClampedArray(result.frames[f]));
pCtx2.putImageData(imgData, 0, 0);
fs.writeFileSync(path.join(tmpDir, `frame_${String(f).padStart(4, "0")}.png`), pngCanvas2.toBuffer("image/png"));
Expand All @@ -323,17 +324,17 @@ imgs[0].save('${outputPath}', save_all=True, append_images=imgs[1:], duration=${
fs.rmdirSync(tmpDir);
} else {
// Encode GIF
const encoder = new GIFEncoder(outputSize, outputSize, "neuquant", true);
const encoder = new GIFEncoder(outputWidth, outputHeight, "neuquant", true);
encoder.setDelay(Math.round(1000 / fps));
encoder.setRepeat(0);
encoder.setTransparent(0x000000);
encoder.start();

const gifCanvas = createCanvas(outputSize, outputSize);
const gifCanvas = createCanvas(outputWidth, outputHeight);
const ctx = gifCanvas.getContext("2d");

for (let f = 0; f < result.frameCount; f++) {
const imgData = ctx.createImageData(outputSize, outputSize);
const imgData = ctx.createImageData(outputWidth, outputHeight);
imgData.data.set(new Uint8ClampedArray(result.frames[f]));
ctx.putImageData(imgData, 0, 0);
encoder.addFrame(ctx);
Expand Down
14 changes: 8 additions & 6 deletions tools/spine-renderer/render_hires.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import fs from "node:fs";
import path from "node:path";
import { renderSkeleton, imageDataToPng } from "./render_utils.mjs";

const OUTPUT_SIZE = 2048;
const OUTPUT_WIDTH = 2048;
const OUTPUT_HEIGHT = 2048;
const SUPERSAMPLE = 3;
const RENDER_SIZE = OUTPUT_SIZE * SUPERSAMPLE;
const RENDER_WIDTH = OUTPUT_WIDTH * SUPERSAMPLE;
const RENDER_HEIGHT = OUTPUT_HEIGHT * SUPERSAMPLE;
const PADDING = 40 * SUPERSAMPLE;
const SHADOW_NAMES = new Set(["shadow", "shadow2", "ground", "ground_shadow"]);
const IDLE_NAMES = ["idle_loop", "idle", "Idle_loop", "Idle", "rest_idle", "rest_loop", "loop", "animation"];
Expand Down Expand Up @@ -50,7 +52,7 @@ async function main() {
const skelName = path.basename(skelFile, ".skel");
const atlasPath = path.join(resolvedDir, skelName + ".atlas");

console.log(`Rendering ${skelName} at ${OUTPUT_SIZE}x${OUTPUT_SIZE}...`);
console.log(`Rendering ${skelName} at ${OUTPUT_WIDTH}x${OUTPUT_HEIGHT}...`);

const atlasText = fs.readFileSync(atlasPath, "utf-8");
const atlas = new TextureAtlas(atlasText);
Expand Down Expand Up @@ -116,13 +118,13 @@ async function main() {
}

const sw = maxX - minX, sh = maxY - minY;
const avail = RENDER_SIZE - PADDING * 2;
const avail = RENDER_WIDTH - PADDING * 2;
const scale = Math.min(avail / sw, avail / sh);
console.log(` Bounds: ${sw.toFixed(0)}x${sh.toFixed(0)}, scale: ${scale.toFixed(2)}`);

const imgData = renderSkeleton(skeleton, RENDER_SIZE, scale, minX, minY, maxX, maxY);
const imgData = renderSkeleton(skeleton, RENDER_WIDTH, RENDER_HEIGHT, scale, minX, minY, maxX, maxY);
const resolvedOutput = path.resolve(outputPath);
const buffer = imageDataToPng(imgData, RENDER_SIZE, OUTPUT_SIZE);
const buffer = imageDataToPng(imgData, RENDER_WIDTH, RENDER_HEIGHT, OUTPUT_WIDTH, OUTPUT_HEIGHT);

fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
fs.writeFileSync(resolvedOutput, buffer);
Expand Down
Loading
Loading