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 = {};
|
||||
for (const [k, v] of Object.entries(reqs)) {
|
||||
summary[k] = Array.from(v);
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
engine.js
11
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
8
math.js
8
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;
|
||||
|
||||
1
model.js
1
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}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
44
world.js
44
world.js
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user