renderang/blockbench/renderang_preview.js
2026-08-30 11:29:09 +10:00

121 lines
4.3 KiB
JavaScript

// renderang_preview.js — Blockbench plugin: PS1-ify the viewport + one-click
// "walk my model in renderang".
// Install: Blockbench > File > Plugins > Load Plugin from File > pick this file.
(function () {
const RENDERANG_DIR = '/Users/jing/Documents/renderang';
const RENDERANG_URL = 'http://localhost:8930';
let ps1On = false;
const saved = { pixelRatios: new Map(), canvasCss: new Map() };
function eachPreview(fn) {
try { (Preview.all || []).forEach(fn); } catch (e) {}
}
function patchMaterials(on) {
// Best-effort: Blockbench cube shaders are custom, but mesh/gltf-style
// materials accept onBeforeCompile. Failures are silently fine — the
// pixelation + nearest textures carry most of the look regardless.
try {
scene.traverse((obj) => {
if (!obj.isMesh || !obj.material) return;
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
mats.forEach((m) => {
if (!m || typeof m.onBeforeCompile === 'undefined') return;
if (on && !m.userData.__renderang) {
m.userData.__renderang = true;
m.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
'#include <project_vertex>',
`#include <project_vertex>
{
vec2 grid = vec2(160.0, 120.0);
vec2 ndc = gl_Position.xy / gl_Position.w;
gl_Position.xy = (floor(ndc * grid + 0.5) / grid) * gl_Position.w;
}`);
};
m.needsUpdate = true;
} else if (!on && m.userData.__renderang) {
delete m.userData.__renderang;
m.onBeforeCompile = () => {};
m.needsUpdate = true;
}
});
});
} catch (e) { console.warn('renderang: material patch skipped', e); }
}
function setPS1(on) {
ps1On = on;
eachPreview((p) => {
try {
const r = p.renderer;
if (on) {
if (!saved.pixelRatios.has(p)) saved.pixelRatios.set(p, r.getPixelRatio());
r.setPixelRatio(0.28);
saved.canvasCss.set(p, p.canvas.style.imageRendering);
p.canvas.style.imageRendering = 'pixelated';
} else {
r.setPixelRatio(saved.pixelRatios.get(p) || window.devicePixelRatio);
p.canvas.style.imageRendering = saved.canvasCss.get(p) || '';
}
} catch (e) {}
});
patchMaterials(on);
try { Canvas.updateAll(); } catch (e) {}
Blockbench.showQuickMessage(on ? 'PS1 preview ON' : 'PS1 preview OFF', 1200);
}
async function openInRenderang() {
try {
const codec = Codecs.gltf;
if (!codec) throw new Error('gltf codec not found');
const content = await codec.compile({ binary: true });
const dest = RENDERANG_DIR + '/assets/preview.glb';
if (typeof require === 'function') {
require('fs').writeFileSync(dest, Buffer.from(content));
Blockbench.showQuickMessage('sent to renderang', 1500);
Blockbench.openLink(RENDERANG_URL + '/?model=preview.glb');
} else {
// web app fallback: download, user drops it into renderang/assets
Blockbench.export({
resource_id: 'gltf', type: 'GLB model', extensions: ['glb'],
name: 'preview', savetype: 'binary', content,
});
}
} catch (e) {
Blockbench.showQuickMessage('renderang export failed: ' + e.message, 3000);
console.error(e);
}
}
let togglePS1, sendAction;
Plugin.register('renderang_preview', {
title: 'renderang preview',
author: 'MonsterRobot',
description: 'PS1-ify the viewport, and open the current model walking in renderang.',
icon: 'videogame_asset',
version: '0.1.0',
variant: 'both',
onload() {
togglePS1 = new Action('renderang_ps1_toggle', {
name: 'PS1 Preview',
icon: 'tv',
click: () => setPS1(!ps1On),
});
sendAction = new Action('renderang_send', {
name: 'Open in renderang',
icon: 'videogame_asset',
click: openInRenderang,
});
MenuBar.addAction(togglePS1, 'view');
MenuBar.addAction(sendAction, 'file.export');
},
onunload() {
if (ps1On) setPS1(false);
togglePS1 && togglePS1.delete();
sendAction && sendAction.delete();
},
});
})();