From 53a4a5b9c5c4886bb4ff3dd7318309e35ce5c50b Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sun, 5 Jul 2026 10:14:01 -0400 Subject: [PATCH] readme --- README.md | 25 +++++++++++++++++++++++++ blockstate.js | 1 - diorama.js | 9 +-------- engine.js | 11 ++--------- math.js | 8 ++++++-- model.js | 1 - texture.js | 1 - world.js | 44 +------------------------------------------- 8 files changed, 35 insertions(+), 65 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..a917ea7c --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Sub-Tick Minecraft Diorama Renderer + +A lightweight, scriptable WebGL2 rendering engine designed to visualize and explain the sub-tick +execution of Minecraft blocks (specifically Redstone components) through interactive, 3D dioramas. + +### Project Goals + +* **Educational Visualization:** Provide clear, interactive 3D visualizations of Minecraft mechanics + for technical documentation. +* **Sub-Tick Accuracy:** Allow authors to script and step through intermediate, "invalid" block + states that occur between game ticks to explain complex redstone logic. +* **Resource Pack Compatibility:** Directly ingest and parse standard Minecraft vanilla resource + pack files (`.json` models, blockstates, and `.png` textures). +* **Lightweight & Embeddable:** Designed to be embedded natively into web-based technical articles + with minimal overhead. Multiple dioramas may be embedded in a document, and multiple dioramas may + represent the same block diagram from different viewpoints. The diagrams will stay synchronized. + +### Non-Goals (Scope Limits) + +* **Not a Clone:** We do not aim to support exact rendering parity. This is a techincal block + diagram viewer only. +* **Not a simulator:** This is a scriptable rendering engine only, suitable for technical authors to + create schematics and explain sub-tick redstone timings. It does not simulate anything, so every + block event must be scripted explicitly. In the future, we may build a simulation layer on top of + this which streams block events to the renderer, but no concrete plans exist yet. \ No newline at end of file diff --git a/blockstate.js b/blockstate.js index 4dfb135c..8f20bab4 100644 --- a/blockstate.js +++ b/blockstate.js @@ -84,7 +84,6 @@ export class Blockstate { } } - // Convert sets to arrays for readable console logging const summary = {}; for (const [k, v] of Object.entries(reqs)) { summary[k] = Array.from(v); diff --git a/diorama.js b/diorama.js index a288f034..362cca49 100644 --- a/diorama.js +++ b/diorama.js @@ -1,4 +1,3 @@ -// diorama.js import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js'; export class Diorama { @@ -6,22 +5,19 @@ export class Diorama { this.element = document.getElementById(elementId); this.canvas = document.createElement('canvas') this.element.appendChild(this.canvas) - this.ctx2d = this.canvas.getContext('2d'); // Lightweight 2D drawing surface + this.ctx2d = this.canvas.getContext('2d'); this.world = world; this.requestRender = requestRenderCallback; - // View State this.target = [0.5, 0, 0.5]; this.radius = 4; this.theta = 135; this.phi = 30; - // Matrices this.projMatrix = new Float32Array(16); this.viewMatrix = new Float32Array(16); this.viewProjMatrix = new Float32Array(16); - // Interaction State this.isDragging = false; this.dragButton = 0; this.touchMode = ''; @@ -35,7 +31,6 @@ export class Diorama { } initUI() { - // Because the element is a canvas itself, buttons are appended to its parent container this.element.style.position = "relative"; this.resetBtn = document.createElement('button'); @@ -178,7 +173,6 @@ export class Diorama { this.requestRender(); }; - // --- MOUSE EVENTS --- this.element.addEventListener('mousedown', (e) => { e.preventDefault(); this.isDragging = true; @@ -213,7 +207,6 @@ export class Diorama { notifyChange(); }); - // --- TOUCH EVENTS --- this.element.addEventListener('touchstart', (e) => { if (this.touchTimer) clearTimeout(this.touchTimer); this.touchTimedOut = false; diff --git a/engine.js b/engine.js index 2ba779ba..db2903f3 100644 --- a/engine.js +++ b/engine.js @@ -1,4 +1,3 @@ -// engine.js import {mat4Identity, mat4Translate} from './math.js'; import {Cache} from "./cache.js"; import {Blockstate, BlockstateHandler} from "./blockstate.js"; @@ -62,7 +61,6 @@ function compileShader(gl, type, src) { export class Engine { constructor() { - // One solitary hidden canvas to drive all WebGL multi-target logic this.canvas = document.createElement('canvas'); this.canvas.style.display = 'none'; document.body.appendChild(this.canvas); @@ -281,25 +279,20 @@ export class Engine { gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture); gl.uniform1i(this.uniforms.texture, 0); - // High-Performance Multi-Target Loop for (const diorama of this.dioramas) { const rect = diorama.canvas.getBoundingClientRect(); - // Fast Frustum Culling via native DOM properties if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) continue; - // Sync layout device-pixel ratios explicitly to prevent jagged texturing const dpr = window.devicePixelRatio || 1; const targetW = Math.floor(rect.width * dpr); const targetH = Math.floor(rect.height * dpr); - // Guard: Only update diorama canvas internal buffers if physical dimensions shifted if (diorama.canvas.width !== targetW || diorama.canvas.height !== targetH) { diorama.canvas.width = targetW; diorama.canvas.height = targetH; } - // Apply size changes to hidden canvas ONLY when growing if (this.canvas.width < targetW || this.canvas.height < targetH) { this.canvas.width = targetW; this.canvas.height = targetH; @@ -325,8 +318,8 @@ export class Engine { diorama.ctx2d.clearRect(0, 0, targetW, targetH); diorama.ctx2d.drawImage( this.canvas, - 0, 0, targetW, targetH, // Source sub-rect coordinates from WebGL - 0, 0, targetW, targetH // Destination coordinates on 2D surface + 0, 0, targetW, targetH, + 0, 0, targetW, targetH ); } } diff --git a/math.js b/math.js index 4009f6cd..c811a13f 100644 --- a/math.js +++ b/math.js @@ -1,4 +1,3 @@ -// math.js export function mat4Identity() { return new Float32Array([ 1, 0, 0, 0, @@ -46,6 +45,8 @@ export function mat4Multiply(out, a, b) { out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; + + // TODO We only use this in-place for this prototype. Update the signature to enforce this return out; } @@ -106,7 +107,7 @@ export function mat4Translate(out, a, v) { out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; } else { - // We only use this in-place for this prototype + // TODO We only use this in-place for this prototype. Update the signature to enforce this } return out; } @@ -125,6 +126,7 @@ export function mat4RotateY(out, a, rad) { out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; + // TODO We only use this in-place for this prototype. Update the signature to enforce this } out[0] = a00 * c - a20 * s; @@ -152,6 +154,8 @@ export function mat4RotateX(out, a, rad) { out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; + // TODO We only use this in-place for this prototype. + // update the signature to enforce this } out[4] = a10 * c + a20 * s; diff --git a/model.js b/model.js index 431bfe34..baab3aca 100644 --- a/model.js +++ b/model.js @@ -73,7 +73,6 @@ export class BlockModel { if (val && typeof val === 'object' && val.sprite) val = val.sprite; } - // BUG FIX: Ensure everything returned is namespace-normalized if (typeof val === 'string') { return val.includes(':') ? val : `minecraft:${val}`; } diff --git a/texture.js b/texture.js index 5523af63..3593fe9c 100644 --- a/texture.js +++ b/texture.js @@ -55,7 +55,6 @@ export class TextureHandler { this.ctx.fillRect(pos.x, pos.y + half, half, half); }; - // Short circuit to avoid unnecessary network requests for known missing ids if (id === ':missing') { drawMissing(); return this.uvmap.get(id); diff --git a/world.js b/world.js index af9a0dc5..6255d580 100644 --- a/world.js +++ b/world.js @@ -1,21 +1,8 @@ export class Block { - /** - * @param {string} id - * @param {[number, number, number]} pos - * @param {Object} state - * @param {Array} world - */ - constructor(id, pos, state, world = []) { + constructor(id, pos, state) { this.id = id; this.pos = pos; this.state = state; - this.worlds = world; - } - - delete() { - for (let world of this.worlds) { - world.blocks - } } } @@ -25,56 +12,27 @@ export class World { this.listeners = new Set(); } - /** - * Subscribe to modify events. - * @param {function()} callback. Called when this world is modified. - * @returns {function()} Unsubscribe function. - */ subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } - /** - * Notify all listeners of a modification. - */ notify() { for (const listener of this.listeners) listener(); } - /** - * Set a block and state at a position. - * @param {string} id - * @param {number} x - * @param {number} y - * @param {number} z - * @param {Object} state - */ set(id, x, y, z, state = {}) { const key = `${x},${y},${z}`; this.blocks.set(key, new Block(id, [x, y, z], state)); this.notify(); } - /** - * Remove a block at a position. - * @param {number} x - * @param {number} y - * @param {number} z - */ del(x, y, z) { if (this.blocks.delete(`${x},${y},${z}`)) { this.notify(); } } - /** - * Get a block at a position. - * @param {number} x - * @param {number} y - * @param {number} z - * @returns {any} - */ get(x, y, z) { return this.blocks.get(`${x},${y},${z}`); }