readme
This commit is contained in:
25
README.md
Normal file
25
README.md
Normal 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.
|
||||||
@@ -84,7 +84,6 @@ export class Blockstate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert sets to arrays for readable console logging
|
|
||||||
const summary = {};
|
const summary = {};
|
||||||
for (const [k, v] of Object.entries(reqs)) {
|
for (const [k, v] of Object.entries(reqs)) {
|
||||||
summary[k] = Array.from(v);
|
summary[k] = Array.from(v);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// diorama.js
|
|
||||||
import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
|
import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
|
||||||
|
|
||||||
export class Diorama {
|
export class Diorama {
|
||||||
@@ -6,22 +5,19 @@ export class Diorama {
|
|||||||
this.element = document.getElementById(elementId);
|
this.element = document.getElementById(elementId);
|
||||||
this.canvas = document.createElement('canvas')
|
this.canvas = document.createElement('canvas')
|
||||||
this.element.appendChild(this.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.world = world;
|
||||||
this.requestRender = requestRenderCallback;
|
this.requestRender = requestRenderCallback;
|
||||||
|
|
||||||
// View State
|
|
||||||
this.target = [0.5, 0, 0.5];
|
this.target = [0.5, 0, 0.5];
|
||||||
this.radius = 4;
|
this.radius = 4;
|
||||||
this.theta = 135;
|
this.theta = 135;
|
||||||
this.phi = 30;
|
this.phi = 30;
|
||||||
|
|
||||||
// Matrices
|
|
||||||
this.projMatrix = new Float32Array(16);
|
this.projMatrix = new Float32Array(16);
|
||||||
this.viewMatrix = new Float32Array(16);
|
this.viewMatrix = new Float32Array(16);
|
||||||
this.viewProjMatrix = new Float32Array(16);
|
this.viewProjMatrix = new Float32Array(16);
|
||||||
|
|
||||||
// Interaction State
|
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.dragButton = 0;
|
this.dragButton = 0;
|
||||||
this.touchMode = '';
|
this.touchMode = '';
|
||||||
@@ -35,7 +31,6 @@ export class Diorama {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initUI() {
|
initUI() {
|
||||||
// Because the element is a canvas itself, buttons are appended to its parent container
|
|
||||||
this.element.style.position = "relative";
|
this.element.style.position = "relative";
|
||||||
|
|
||||||
this.resetBtn = document.createElement('button');
|
this.resetBtn = document.createElement('button');
|
||||||
@@ -178,7 +173,6 @@ export class Diorama {
|
|||||||
this.requestRender();
|
this.requestRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- MOUSE EVENTS ---
|
|
||||||
this.element.addEventListener('mousedown', (e) => {
|
this.element.addEventListener('mousedown', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.isDragging = true;
|
this.isDragging = true;
|
||||||
@@ -213,7 +207,6 @@ export class Diorama {
|
|||||||
notifyChange();
|
notifyChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- TOUCH EVENTS ---
|
|
||||||
this.element.addEventListener('touchstart', (e) => {
|
this.element.addEventListener('touchstart', (e) => {
|
||||||
if (this.touchTimer) clearTimeout(this.touchTimer);
|
if (this.touchTimer) clearTimeout(this.touchTimer);
|
||||||
this.touchTimedOut = false;
|
this.touchTimedOut = false;
|
||||||
|
|||||||
11
engine.js
11
engine.js
@@ -1,4 +1,3 @@
|
|||||||
// engine.js
|
|
||||||
import {mat4Identity, mat4Translate} from './math.js';
|
import {mat4Identity, mat4Translate} from './math.js';
|
||||||
import {Cache} from "./cache.js";
|
import {Cache} from "./cache.js";
|
||||||
import {Blockstate, BlockstateHandler} from "./blockstate.js";
|
import {Blockstate, BlockstateHandler} from "./blockstate.js";
|
||||||
@@ -62,7 +61,6 @@ function compileShader(gl, type, src) {
|
|||||||
|
|
||||||
export class Engine {
|
export class Engine {
|
||||||
constructor() {
|
constructor() {
|
||||||
// One solitary hidden canvas to drive all WebGL multi-target logic
|
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
this.canvas.style.display = 'none';
|
this.canvas.style.display = 'none';
|
||||||
document.body.appendChild(this.canvas);
|
document.body.appendChild(this.canvas);
|
||||||
@@ -281,25 +279,20 @@ export class Engine {
|
|||||||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
|
||||||
gl.uniform1i(this.uniforms.texture, 0);
|
gl.uniform1i(this.uniforms.texture, 0);
|
||||||
|
|
||||||
// High-Performance Multi-Target Loop
|
|
||||||
for (const diorama of this.dioramas) {
|
for (const diorama of this.dioramas) {
|
||||||
const rect = diorama.canvas.getBoundingClientRect();
|
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;
|
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 dpr = window.devicePixelRatio || 1;
|
||||||
const targetW = Math.floor(rect.width * dpr);
|
const targetW = Math.floor(rect.width * dpr);
|
||||||
const targetH = Math.floor(rect.height * 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) {
|
if (diorama.canvas.width !== targetW || diorama.canvas.height !== targetH) {
|
||||||
diorama.canvas.width = targetW;
|
diorama.canvas.width = targetW;
|
||||||
diorama.canvas.height = targetH;
|
diorama.canvas.height = targetH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply size changes to hidden canvas ONLY when growing
|
|
||||||
if (this.canvas.width < targetW || this.canvas.height < targetH) {
|
if (this.canvas.width < targetW || this.canvas.height < targetH) {
|
||||||
this.canvas.width = targetW;
|
this.canvas.width = targetW;
|
||||||
this.canvas.height = targetH;
|
this.canvas.height = targetH;
|
||||||
@@ -325,8 +318,8 @@ export class Engine {
|
|||||||
diorama.ctx2d.clearRect(0, 0, targetW, targetH);
|
diorama.ctx2d.clearRect(0, 0, targetW, targetH);
|
||||||
diorama.ctx2d.drawImage(
|
diorama.ctx2d.drawImage(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
0, 0, targetW, targetH, // Source sub-rect coordinates from WebGL
|
0, 0, targetW, targetH,
|
||||||
0, 0, targetW, targetH // Destination coordinates on 2D surface
|
0, 0, targetW, targetH
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
math.js
8
math.js
@@ -1,4 +1,3 @@
|
|||||||
// math.js
|
|
||||||
export function mat4Identity() {
|
export function mat4Identity() {
|
||||||
return new Float32Array([
|
return new Float32Array([
|
||||||
1, 0, 0, 0,
|
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[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
|
||||||
out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
|
||||||
out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
|
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;
|
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[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];
|
out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
|
||||||
} else {
|
} 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;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -125,6 +126,7 @@ export function mat4RotateY(out, a, rad) {
|
|||||||
out[13] = a[13];
|
out[13] = a[13];
|
||||||
out[14] = a[14];
|
out[14] = a[14];
|
||||||
out[15] = a[15];
|
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;
|
out[0] = a00 * c - a20 * s;
|
||||||
@@ -152,6 +154,8 @@ export function mat4RotateX(out, a, rad) {
|
|||||||
out[13] = a[13];
|
out[13] = a[13];
|
||||||
out[14] = a[14];
|
out[14] = a[14];
|
||||||
out[15] = a[15];
|
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;
|
out[4] = a10 * c + a20 * s;
|
||||||
|
|||||||
1
model.js
1
model.js
@@ -73,7 +73,6 @@ export class BlockModel {
|
|||||||
if (val && typeof val === 'object' && val.sprite) val = val.sprite;
|
if (val && typeof val === 'object' && val.sprite) val = val.sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG FIX: Ensure everything returned is namespace-normalized
|
|
||||||
if (typeof val === 'string') {
|
if (typeof val === 'string') {
|
||||||
return val.includes(':') ? val : `minecraft:${val}`;
|
return val.includes(':') ? val : `minecraft:${val}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export class TextureHandler {
|
|||||||
this.ctx.fillRect(pos.x, pos.y + half, half, half);
|
this.ctx.fillRect(pos.x, pos.y + half, half, half);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Short circuit to avoid unnecessary network requests for known missing ids
|
|
||||||
if (id === ':missing') {
|
if (id === ':missing') {
|
||||||
drawMissing();
|
drawMissing();
|
||||||
return this.uvmap.get(id);
|
return this.uvmap.get(id);
|
||||||
|
|||||||
44
world.js
44
world.js
@@ -1,21 +1,8 @@
|
|||||||
export class Block {
|
export class Block {
|
||||||
/**
|
constructor(id, pos, state) {
|
||||||
* @param {string} id
|
|
||||||
* @param {[number, number, number]} pos
|
|
||||||
* @param {Object} state
|
|
||||||
* @param {Array<World>} world
|
|
||||||
*/
|
|
||||||
constructor(id, pos, state, world = []) {
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.pos = pos;
|
this.pos = pos;
|
||||||
this.state = state;
|
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();
|
this.listeners = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to modify events.
|
|
||||||
* @param {function()} callback. Called when this world is modified.
|
|
||||||
* @returns {function()} Unsubscribe function.
|
|
||||||
*/
|
|
||||||
subscribe(callback) {
|
subscribe(callback) {
|
||||||
this.listeners.add(callback);
|
this.listeners.add(callback);
|
||||||
return () => this.listeners.delete(callback);
|
return () => this.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify all listeners of a modification.
|
|
||||||
*/
|
|
||||||
notify() {
|
notify() {
|
||||||
for (const listener of this.listeners) listener();
|
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 = {}) {
|
set(id, x, y, z, state = {}) {
|
||||||
const key = `${x},${y},${z}`;
|
const key = `${x},${y},${z}`;
|
||||||
this.blocks.set(key, new Block(id, [x, y, z], state));
|
this.blocks.set(key, new Block(id, [x, y, z], state));
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a block at a position.
|
|
||||||
* @param {number} x
|
|
||||||
* @param {number} y
|
|
||||||
* @param {number} z
|
|
||||||
*/
|
|
||||||
del(x, y, z) {
|
del(x, y, z) {
|
||||||
if (this.blocks.delete(`${x},${y},${z}`)) {
|
if (this.blocks.delete(`${x},${y},${z}`)) {
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a block at a position.
|
|
||||||
* @param {number} x
|
|
||||||
* @param {number} y
|
|
||||||
* @param {number} z
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
get(x, y, z) {
|
get(x, y, z) {
|
||||||
return this.blocks.get(`${x},${y},${z}`);
|
return this.blocks.get(`${x},${y},${z}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user