fix rotations for torches. sr latch demo.

This commit is contained in:
David Allemang
2026-07-07 15:52:45 -04:00
parent e4aa6dbe3e
commit 476da0dd90
7 changed files with 262 additions and 131 deletions

View File

@@ -8,6 +8,9 @@
<link rel="stylesheet" href="./src/components.css">
<script type="module" src="./src/components.js"></script>
<style> mc-world, mc-timeline, mc-camera, mc-frame {
display: none;
} </style>
</head>
<body>
<main>
@@ -109,7 +112,7 @@
</mc-world>
</mc-diorama>
<h2>Static multi-view scene</h2>
<h2>Multi-view scene</h2>
<p> 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>
<mc-frame id="nonsense">
<mc-camera orbit></mc-camera>
<mc-frame id="d-latch">
<mc-world>
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
</mc-world>
</mc-frame>
<div class="grid-2col">
<mc-diorama frameid="nonsense" theta="180" phi="90"></mc-diorama>
<mc-diorama frameid="nonsense">
<mc-diorama frameid="d-latch" radius="6" theta="140" phi="25" target="2 1 0.5" style="border: 1px solid white">
<mc-timeline autoplay controls="true" loop></mc-timeline>
<mc-camera pan="false" zoom="false"></mc-camera>
</mc-diorama>
<mc-diorama frameid="nonsense" theta="180" phi="0"></mc-diorama>
<mc-diorama frameid="nonsense" theta="270" phi="0"></mc-diorama>
<mc-diorama frameid="d-latch" radius="6" theta="180" phi="90" target="2 1 0.5" style="border: 1px solid white"></mc-diorama>
</div>
<p> 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. </p>
<div id="atlas-container"></div>
</main>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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