diff --git a/assets/witch.glb b/assets/witch.glb new file mode 100644 index 0000000..c2e49cf Binary files /dev/null and b/assets/witch.glb differ diff --git a/blockbench/renderang_preview.js b/blockbench/renderang_preview.js new file mode 100644 index 0000000..7aa030f --- /dev/null +++ b/blockbench/renderang_preview.js @@ -0,0 +1,120 @@ +// 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 ', + `#include + { + 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(); + }, + }); +})(); diff --git a/src/main.js b/src/main.js index e918aac..4747cd9 100644 --- a/src/main.js +++ b/src/main.js @@ -81,7 +81,8 @@ for (let i = 0; i < 14; i++) { let mixer = null; let actions = {}; let current = null; -new GLTFLoader().load('assets/suit.glb', (gltf) => { +const modelUrl = new URLSearchParams(location.search).get('model') || 'suit.glb'; +new GLTFLoader().load('assets/' + modelUrl, (gltf) => { const char = retrofyObject(gltf.scene); scene.add(char); mixer = new THREE.AnimationMixer(char); @@ -94,8 +95,8 @@ new GLTFLoader().load('assets/suit.glb', (gltf) => { o.value = o.textContent = clip.name; sel.appendChild(o); }); - play('Walk'); - sel.value = 'Walk'; + const first = actions['Walk'] ? 'Walk' : (gltf.animations[0] ? gltf.animations[0].name : null); + if (first) { play(first); sel.value = first; } sel.onchange = () => play(sel.value); }); function play(name) {