This commit is contained in:
2026-07-05 10:14:01 -04:00
parent 50fa0f5258
commit 53a4a5b9c5
8 changed files with 35 additions and 65 deletions

25
README.md Normal file
View File

@@ -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.

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
);
}
}

View File

@@ -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;

View File

@@ -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}`;
}

View File

@@ -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);

View File

@@ -1,21 +1,8 @@
export class Block {
/**
* @param {string} id
* @param {[number, number, number]} pos
* @param {Object} state
* @param {Array<World>} 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}`);
}