From 476da0dd903d24d400703740c166e31259469e8a Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 7 Jul 2026 15:52:45 -0400 Subject: [PATCH] fix rotations for torches. sr latch demo. --- index.html | 123 +++++++++++++++++++++++++++++++++++++++------ src/components.css | 4 +- src/components.js | 26 ++++++---- src/controller.js | 2 +- src/engine.js | 100 ++++++++++++++++++++---------------- src/math.js | 87 ++++++++++++++------------------ src/model.js | 51 +++++++++++++++---- 7 files changed, 262 insertions(+), 131 deletions(-) diff --git a/index.html b/index.html index a2ff927d..e7093956 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,9 @@ +
@@ -109,7 +112,7 @@ -

Static multi-view scene

+

Multi-view scene

Phasellus tortor turpis, commodo ut tortor scelerisque, fermentum tempus nisi. Pellentesque consectetur commodo rhoncus. Sed ullamcorper turpis dapibus, luctus nibh eu, dapibus risus. Fusce finibus ante eleifend, venenatis @@ -117,25 +120,117 @@ eros. Etiam porttitor libero a facilisis vulputate. Duis facilisis eget arcu quis ultricies. Etiam dui erat, luctus ut tristique vitae, fermentum quis magna.

- - + - p -1 0 0 redstone_wire east=side west=none north=none south=none power=0 - p -1 -1 0 smooth_stone_slab type=top - p 0 0 0 repeater facing=east delay=1 locked=false powered=false - p 2 0 0 oak_trapdoor facing=east half=bottom open=true - p 0 -1 0 lodestone - p 0 -2 0 piston facing=north extended=false + p -1 0 0 smooth_stone_slab type=top + p 0 0 0 smooth_stone_slab type=top + p 1 0 0 smooth_stone_slab type=top + p 2 0 0 smooth_stone_slab type=top + p 3 0 0 smooth_stone_slab type=top + p 4 0 0 smooth_stone_slab type=top + p -1 0 -1 smooth_stone_slab type=top + p 0 0 -1 smooth_stone_slab type=top + p 1 0 -1 smooth_stone_slab type=top + p 2 0 -1 smooth_stone_slab type=top + p 3 0 -1 smooth_stone_slab type=top + p 4 0 -1 smooth_stone_slab type=top + p -1 0 1 smooth_stone_slab type=top + p 0 0 1 smooth_stone_slab type=top + p 1 0 1 smooth_stone_slab type=top + p 2 0 1 smooth_stone_slab type=top + p 3 0 1 smooth_stone_slab type=top + p 4 0 1 smooth_stone_slab type=top + + p 2 1 1 light_blue_terracotta + p 1 1 -1 lime_terracotta + + p 4 1 1 light_gray_terracotta + p 4 1 -1 light_gray_terracotta + + p 1 1 0 redstone_wall_torch lit=true facing=south + p 2 1 0 redstone_wall_torch lit=false facing=north + + p -1 1 1 redstone_lamp lit=true + p 0 1 1 redstone_wire east=side north=none south=none west=side power=14 + p 1 1 1 redstone_wire east=up north=side south=none west=side power=15 + p 2 2 1 redstone_wire east=side north=none south=none west=side power=14 + p 3 1 1 redstone_wire east=side north=none south=none west=up power=13 + p 5 1 1 stone_button face=wall powered=false facing=east + + p -1 1 -1 redstone_lamp lit=false + p 0 1 -1 redstone_wire east=up north=none south=none west=side power=0 + p 1 2 -1 redstone_wire east=side north=none south=none west=side power=0 + p 2 1 -1 redstone_wire east=side north=none south=side west=up power=0 + p 3 1 -1 redstone_wire east=side north=none south=none west=side power=0 + p 5 1 -1 stone_button face=wall powered=false facing=east + + t 2 + p 5 1 -1 powered=true + p 0 1 -1 power=15 + p 1 2 -1 power=15 + p 2 1 -1 power=15 + p 3 1 -1 power=15 + + t 4 + p 1 1 0 lit=false + p 0 1 1 power=0 + p 1 1 1 power=0 + p 2 2 1 power=0 + p 3 1 1 power=0 + + t 6 + p -1 1 -1 lit=true + p 2 1 0 lit=true + p 0 1 -1 power=15 + p 1 2 -1 power=15 + p 2 1 -1 power=15 + p 3 1 -1 power=15 + + t 8 + p -1 1 1 lit=false + + t 22 + p 5 1 -1 powered=false + p 0 1 -1 power=15 + p 1 2 -1 power=15 + p 2 1 -1 power=15 + p 3 1 -1 power=15 + + t 32 + p 5 1 1 powered=true + p 0 1 1 power=15 + p 1 1 1 power=15 + p 2 2 1 power=15 + p 3 1 1 power=15 + + t 34 + p 2 1 0 lit=false + p 0 1 -1 power=0 + p 1 2 -1 power=0 + p 2 1 -1 power=0 + p 3 1 -1 power=0 + + t 36 + p -1 1 1 lit=true + p 1 1 0 lit=true + + t 38 + p -1 1 -1 lit=false + + t 52 + p 5 1 1 powered=false + + t 60 + p 0 0 0
- - + + - - +

Sed urna ante, aliquet et ex nec, posuere laoreet sem. Aenean non sem a massa vehicula faucibus. Donec varius @@ -217,8 +312,6 @@ Ut id massa eget dui vestibulum pretium. Nunc tempus odio quis nisi ultrices, id volutpat dui tristique. Suspendisse lacus metus, pulvinar ac urna vitae, pellentesque volutpat eros. In cursus ipsum in enim ornare, sed tempus mi tincidunt.

- -
\ No newline at end of file diff --git a/src/components.css b/src/components.css index 53dba934..eb7d479e 100644 --- a/src/components.css +++ b/src/components.css @@ -1,7 +1,5 @@ /* --- Component Structural Styles --- */ -mc-world, mc-timeline, mc-camera, mc-frame { - display: none; -} +mc-world, mc-timeline, mc-camera, mc-frame { display: none; } mc-diorama { display: block; diff --git a/src/components.js b/src/components.js index 428b4532..973ecaae 100644 --- a/src/components.js +++ b/src/components.js @@ -5,15 +5,8 @@ import {Diorama} from './diorama.js'; import {SpacetimeController} from './controller.js'; // Global Engine Singleton -let engineInstance = null; - -function getEngine() { - if (!engineInstance) { - engineInstance = new Engine(); - document.body.appendChild(engineInstance.canvas) - } - return engineInstance; -} +window.mcEngine = new Engine(); +document.body.appendChild(window.mcEngine.canvas) export class MCWorldElement extends HTMLElement { connectedCallback() { @@ -55,7 +48,7 @@ export class MCDioramaElement extends HTMLElement { } init() { - const engine = getEngine(); + const engine = window.mcEngine; let frame; // 1. Resolve the Frame context @@ -90,7 +83,18 @@ export class MCDioramaElement extends HTMLElement { if (this.hasAttribute('theta')) diorama.theta = parseFloat(this.getAttribute('theta')); if (this.hasAttribute('phi')) diorama.phi = parseFloat(this.getAttribute('phi')); - diorama.centerView(); + if (this.hasAttribute('target')) { + console.log('target:', this.getAttribute('target')); + const [x, y, z] = this.getAttribute('target').trim().split(/\s+/); + diorama.target = [ + parseFloat(x), + parseFloat(y), + parseFloat(z), + ]; + console.log('target:', diorama.target); + } else { + diorama.centerView(); + } diorama.saveState(); // Lock this in for the Reset button // 3. Resolve the Controllers diff --git a/src/controller.js b/src/controller.js index b3446e49..3156e953 100644 --- a/src/controller.js +++ b/src/controller.js @@ -15,7 +15,7 @@ export class SpacetimeController { subticks: true, autoplay: false, loop: false, - speed: 0.5, ...options + speed: 1.0, ...options }; this.hasSpatial = this.opts.orbit || this.opts.pan || this.opts.zoom; diff --git a/src/engine.js b/src/engine.js index a7571137..5449fef7 100644 --- a/src/engine.js +++ b/src/engine.js @@ -74,7 +74,7 @@ export class Engine { this.canvas.style.display = 'none'; document.body.appendChild(this.canvas); - this.gl = this.canvas.getContext('webgl2', {antialias: true, alpha: true}); + this.gl = this.canvas.getContext('webgl2', {antialias: false, alpha: true}); const gl = this.gl; gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); @@ -115,6 +115,8 @@ export class Engine { this.observedFrames = new Set(); this.dirtyFrames = new Set(); // Tracks specific timelines that need mesh updates + this.debug_redraw = false; + window.addEventListener('resize', () => this.requestRender()); window.addEventListener('scroll', () => this.requestRender()); } @@ -367,50 +369,60 @@ export class Engine { // Lazy Render: Only draw if the diorama actually changed if (!diorama.dirty) continue; - if (!setupProgram) { - gl.useProgram(this.program); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture); - gl.uniform1i(this.uniforms.texture, 0); - setupProgram = true; + const draw = () => { + if (!setupProgram) { + gl.useProgram(this.program); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture); + gl.uniform1i(this.uniforms.texture, 0); + setupProgram = true; + } + + if (this.canvas.width < targetW || this.canvas.height < targetH) { + this.canvas.width = Math.max(this.canvas.width, targetW); + this.canvas.height = Math.max(this.canvas.height, targetH); + } + + const viewportY = this.canvas.height - targetH; + gl.viewport(0, viewportY, targetW, targetH); + gl.enable(gl.SCISSOR_TEST); + gl.scissor(0, viewportY, targetW, targetH); + gl.clearColor(0.0, 0.0, 0.0, 0.0); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.disable(gl.SCISSOR_TEST); + + const aspect = rect.width / rect.height; + const viewProj = diorama.updateMatrices(aspect); + + gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj); + gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, 2.0); + gl.uniform3f(this.uniforms.viewDir, diorama.viewDir[0], diorama.viewDir[1], diorama.viewDir[2]); + gl.uniform3f(this.uniforms.upDir, diorama.upDir[0], diorama.upDir[1], diorama.upDir[2]); + + const meshes = this.frameMeshes.get(diorama.frame) || []; + for (const mesh of meshes) { + gl.bindVertexArray(mesh.vao); + gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount); + } + + diorama.ctx2d.clearRect(0, 0, targetW, targetH); + diorama.ctx2d.drawImage( + this.canvas, + 0, 0, targetW, targetH, + 0, 0, targetW, targetH + ); + + // Clean state! + diorama.dirty = false; + }; + + if (this.debug_redraw) { + diorama.ctx2d.fillStyle = '#FF00FF33'; + diorama.ctx2d.fillRect(0, 0, targetW, targetH); + requestAnimationFrame(draw); + } else { + draw(); } - - if (this.canvas.width < targetW || this.canvas.height < targetH) { - this.canvas.width = Math.max(this.canvas.width, targetW); - this.canvas.height = Math.max(this.canvas.height, targetH); - } - - const viewportY = this.canvas.height - targetH; - gl.viewport(0, viewportY, targetW, targetH); - gl.enable(gl.SCISSOR_TEST); - gl.scissor(0, viewportY, targetW, targetH); - gl.clearColor(0.0, 0.0, 0.0, 0.0); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - gl.disable(gl.SCISSOR_TEST); - - const aspect = rect.width / rect.height; - const viewProj = diorama.updateMatrices(aspect); - - gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj); - gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, 2.0); - gl.uniform3f(this.uniforms.viewDir, diorama.viewDir[0], diorama.viewDir[1], diorama.viewDir[2]); - gl.uniform3f(this.uniforms.upDir, diorama.upDir[0], diorama.upDir[1], diorama.upDir[2]); - - const meshes = this.frameMeshes.get(diorama.frame) || []; - for (const mesh of meshes) { - gl.bindVertexArray(mesh.vao); - gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount); - } - - diorama.ctx2d.clearRect(0, 0, targetW, targetH); - diorama.ctx2d.drawImage( - this.canvas, - 0, 0, targetW, targetH, - 0, 0, targetW, targetH - ); - - // Clean state! - diorama.dirty = false; } } } \ No newline at end of file diff --git a/src/math.js b/src/math.js index c811a13f..855fdf86 100644 --- a/src/math.js +++ b/src/math.js @@ -112,59 +112,50 @@ export function mat4Translate(out, a, v) { return out; } -export function mat4RotateY(out, a, rad) { +export function mat4RotateX(m, rad) { let s = Math.sin(rad), c = Math.cos(rad); - let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; - let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; + let m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; + let m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; - if (a !== out) { - out[4] = a[4]; - out[5] = a[5]; - out[6] = a[6]; - out[7] = a[7]; - out[12] = a[12]; - 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; - out[1] = a01 * c - a21 * s; - out[2] = a02 * c - a22 * s; - out[3] = a03 * c - a23 * s; - out[8] = a00 * s + a20 * c; - out[9] = a01 * s + a21 * c; - out[10] = a02 * s + a22 * c; - out[11] = a03 * s + a23 * c; - return out; + m[4] = m10 * c + m20 * s; + m[5] = m11 * c + m21 * s; + m[6] = m12 * c + m22 * s; + m[7] = m13 * c + m23 * s; + m[8] = m20 * c - m10 * s; + m[9] = m21 * c - m11 * s; + m[10] = m22 * c - m12 * s; + m[11] = m23 * c - m13 * s; + return m; } -export function mat4RotateX(out, a, rad) { +export function mat4RotateY(m, rad) { let s = Math.sin(rad), c = Math.cos(rad); - let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; - let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; + let m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; + let m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; - if (a !== out) { - out[0] = a[0]; - out[1] = a[1]; - out[2] = a[2]; - out[3] = a[3]; - out[12] = a[12]; - 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 - } + m[0] = m00 * c - m20 * s; + m[1] = m01 * c - m21 * s; + m[2] = m02 * c - m22 * s; + m[3] = m03 * c - m23 * s; + m[8] = m00 * s + m20 * c; + m[9] = m01 * s + m21 * c; + m[10] = m02 * s + m22 * c; + m[11] = m03 * s + m23 * c; + return m; +} - out[4] = a10 * c + a20 * s; - out[5] = a11 * c + a21 * s; - out[6] = a12 * c + a22 * s; - out[7] = a13 * c + a23 * s; - out[8] = a20 * c - a10 * s; - out[9] = a21 * c - a11 * s; - out[10] = a22 * c - a12 * s; - out[11] = a23 * c - a13 * s; - return out; +export function mat4RotateZ(m, rad) { + let s = Math.sin(rad), c = Math.cos(rad); + let m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; + let m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; + + m[0] = m00 * c + m10 * s; + m[1] = m01 * c + m11 * s; + m[2] = m02 * c + m12 * s; + m[3] = m03 * c + m13 * s; + m[4] = m10 * c - m00 * s; + m[5] = m11 * c - m01 * s; + m[6] = m12 * c - m02 * s; + m[7] = m13 * c - m03 * s; + return m; } \ No newline at end of file diff --git a/src/model.js b/src/model.js index 126e6446..38ae6b27 100644 --- a/src/model.js +++ b/src/model.js @@ -1,4 +1,4 @@ -import {mat4Identity, mat4RotateX, mat4RotateY, mat4Translate} from "./math.js"; +import {mat4Identity, mat4RotateX, mat4RotateY, mat4RotateZ, mat4Translate} from "./math.js"; const MISSING_MODEL_JSON = { "textures": {"missing": ":missing"}, @@ -113,10 +113,11 @@ export class BlockModel { const pos = [], norm = [], uv = [], tint = [], shade = [], idx = []; let vOffset = 0; + // --- GLOBAL BLOCKSTATE MATRIX --- const blockMatrix = mat4Identity(new Float32Array(16)); mat4Translate(blockMatrix, blockMatrix, [0.5, 0.5, 0.5]); - if (variant.x) mat4RotateX(blockMatrix, blockMatrix, -variant.x * Math.PI / 180); - if (variant.y) mat4RotateY(blockMatrix, blockMatrix, -variant.y * Math.PI / 180); + if (variant.y) mat4RotateY(blockMatrix, -variant.y * Math.PI / 180); + if (variant.x) mat4RotateX(blockMatrix, -variant.x * Math.PI / 180); mat4Translate(blockMatrix, blockMatrix, [-0.5, -0.5, -0.5]); for (const el of this.elements) { @@ -125,6 +126,22 @@ export class BlockModel { const size = [tx - fx, ty - fy, tz - fz]; const elementShade = el.shade === false ? 0.0 : 1.0; + // --- LOCAL ELEMENT MATRIX --- + const elMatrix = mat4Identity(new Float32Array(16)); + if (el.rotation) { + const [ox, oy, oz] = el.rotation.origin.map(x => x / 16); + const angle = el.rotation.angle * Math.PI / 180; + + mat4Translate(elMatrix, elMatrix, [ox, oy, oz]); + if (el.rotation.axis === 'x') mat4RotateX(elMatrix, angle); + else if (el.rotation.axis === 'y') mat4RotateY(elMatrix, angle); + else if (el.rotation.axis === 'z') mat4RotateZ(elMatrix, angle); + mat4Translate(elMatrix, elMatrix, [-ox, -oy, -oz]); + + // Note: Minecraft specifies a "rescale" boolean on some rotations to fit faces to block bounds. + // It is rarely used (the torch wall JSON doesn't use it), so it is omitted here to save math overhead. + } + for (const [name, face] of Object.entries(el.faces || {})) { const tmpl = CUBOID_FACES[name]; @@ -154,16 +171,32 @@ export class BlockModel { for (let i = 0; i < 4; i++) { const [cx, cy, cz] = tmpl.c[i]; + + // 1. Raw local position let vx = fx + cx * size[0], vy = fy + cy * size[1], vz = fz + cz * size[2]; - let wx = blockMatrix[0] * vx + blockMatrix[4] * vy + blockMatrix[8] * vz + blockMatrix[12]; - let wy = blockMatrix[1] * vx + blockMatrix[5] * vy + blockMatrix[9] * vz + blockMatrix[13]; - let wz = blockMatrix[2] * vx + blockMatrix[6] * vy + blockMatrix[10] * vz + blockMatrix[14]; + // 2. Apply element rotation + let evx = elMatrix[0] * vx + elMatrix[4] * vy + elMatrix[8] * vz + elMatrix[12]; + let evy = elMatrix[1] * vx + elMatrix[5] * vy + elMatrix[9] * vz + elMatrix[13]; + let evz = elMatrix[2] * vx + elMatrix[6] * vy + elMatrix[10] * vz + elMatrix[14]; + + // 3. Apply global blockstate rotation + let wx = blockMatrix[0] * evx + blockMatrix[4] * evy + blockMatrix[8] * evz + blockMatrix[12]; + let wy = blockMatrix[1] * evx + blockMatrix[5] * evy + blockMatrix[9] * evz + blockMatrix[13]; + let wz = blockMatrix[2] * evx + blockMatrix[6] * evy + blockMatrix[10] * evz + blockMatrix[14]; pos.push(wx, wy, wz); - let nx = blockMatrix[0] * tmpl.n[0] + blockMatrix[4] * tmpl.n[1] + blockMatrix[8] * tmpl.n[2]; - let ny = blockMatrix[1] * tmpl.n[0] + blockMatrix[5] * tmpl.n[1] + blockMatrix[9] * tmpl.n[2]; - let nz = blockMatrix[2] * tmpl.n[0] + blockMatrix[6] * tmpl.n[1] + blockMatrix[10] * tmpl.n[2]; + // --- Normals (Apply Rotations Only, No Translation) --- + + // 1. Apply element rotation to normal + let enx = elMatrix[0] * tmpl.n[0] + elMatrix[4] * tmpl.n[1] + elMatrix[8] * tmpl.n[2]; + let eny = elMatrix[1] * tmpl.n[0] + elMatrix[5] * tmpl.n[1] + elMatrix[9] * tmpl.n[2]; + let enz = elMatrix[2] * tmpl.n[0] + elMatrix[6] * tmpl.n[1] + elMatrix[10] * tmpl.n[2]; + + // 2. Apply global blockstate rotation to normal + let nx = blockMatrix[0] * enx + blockMatrix[4] * eny + blockMatrix[8] * enz; + let ny = blockMatrix[1] * enx + blockMatrix[5] * eny + blockMatrix[9] * enz; + let nz = blockMatrix[2] * enx + blockMatrix[6] * eny + blockMatrix[10] * enz; norm.push(nx, ny, nz); tint.push(tintable);