multi-view support

This commit is contained in:
David Allemang
2026-07-04 13:13:09 -04:00
parent ffd48b2585
commit a92ac6d736
4 changed files with 313 additions and 278 deletions

203
camera.js
View File

@@ -1,203 +0,0 @@
// camera.js
import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
export class Camera {
constructor(element, onChangeCallback) {
this.element = element;
this.onChange = onChangeCallback;
this.target = [0.5, 0, 0.5];
this.radius = 4;
this.theta = 135;
this.phi = 30;
this.projMatrix = new Float32Array(16);
this.viewMatrix = new Float32Array(16);
this.viewProjMatrix = new Float32Array(16);
this.isDragging = false;
this.dragButton = 0;
this.touchMode = '';
this.lastMouse = {x: 0, y: 0};
this.lastPinchDist = 0;
this.attachEvents();
}
saveState() {
this.checkpoint = {
target: [...this.target],
radius: this.radius,
theta: this.theta,
phi: this.phi,
}
}
loadState() {
if (!this.checkpoint) return;
this.target = [...this.checkpoint.target];
this.radius = this.checkpoint.radius;
this.theta = this.checkpoint.theta;
this.phi = this.checkpoint.phi;
this.onChange();
}
// --- INTERACTION MATH ---
orbit(dx, dy) {
this.theta -= dx * 0.4;
this.phi += dy * 0.4;
this.phi = Math.max(-90, Math.min(90, this.phi));
}
pan(dx, dy) {
const t = this.theta * Math.PI / 180;
const p = this.phi * Math.PI / 180;
const rightX = Math.cos(t);
const rightZ = -Math.sin(t);
const upX = -Math.sin(p) * Math.sin(t);
const upY = Math.cos(p);
const upZ = -Math.sin(p) * Math.cos(t);
const panSpeed = this.radius * 0.0025;
this.target[0] += (-rightX * dx + upX * dy) * panSpeed;
this.target[1] += (upY * dy) * panSpeed;
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
}
zoom(delta) {
this.radius += delta * 0.05;
this.radius = Math.max(1, Math.min(50, this.radius));
}
// --- EVENT LISTENERS ---
attachEvents() {
this.element.addEventListener('contextmenu', e => e.preventDefault());
// --- MOUSE EVENTS ---
this.element.addEventListener('mousedown', (e) => {
e.preventDefault();
this.isDragging = true;
this.lastMouse = {x: e.clientX, y: e.clientY};
// Base button
this.dragButton = e.button;
// Modifier Overrides (Accessibility)
if (this.dragButton === 0) {
if (e.ctrlKey || e.metaKey) this.dragButton = 2; // Ctrl = Zoom
else if (e.shiftKey) this.dragButton = 1; // Shift = Pan
}
if (this.dragButton === 1) this.element.style.cursor = 'move';
else if (this.dragButton === 2) this.element.style.cursor = 'ns-resize';
else this.element.style.cursor = 'grabbing';
});
window.addEventListener('mouseup', () => {
this.isDragging = false;
this.element.style.cursor = 'grab';
});
window.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const dx = e.clientX - this.lastMouse.x;
const dy = e.clientY - this.lastMouse.y;
this.lastMouse = {x: e.clientX, y: e.clientY};
if (this.dragButton === 0) this.orbit(dx, dy);
else if (this.dragButton === 1) this.pan(dx, dy);
else if (this.dragButton === 2) this.zoom(dy);
this.onChange();
});
// --- TOUCH EVENTS ---
this.element.addEventListener('touchstart', (e) => {
e.preventDefault(); // Prevents mobile browser pull-to-refresh / page scrolling
this.isDragging = true;
if (e.touches.length === 1) {
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
} else if (e.touches.length >= 2) {
this.touchMode = 'pan-zoom';
const t1 = e.touches[0], t2 = e.touches[1];
this.lastMouse = {
x: (t1.clientX + t2.clientX) / 2,
y: (t1.clientY + t2.clientY) / 2
};
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
}
}, {passive: false});
this.element.addEventListener('touchmove', (e) => {
if (!this.isDragging) return;
e.preventDefault();
if (this.touchMode === 'orbit' && e.touches.length === 1) {
const dx = e.touches[0].clientX - this.lastMouse.x;
const dy = e.touches[0].clientY - this.lastMouse.y;
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
this.orbit(dx, dy);
this.onChange();
} else if (this.touchMode === 'pan-zoom' && e.touches.length >= 2) {
const t1 = e.touches[0], t2 = e.touches[1];
const cx = (t1.clientX + t2.clientX) / 2;
const cy = (t1.clientY + t2.clientY) / 2;
const dist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
const dx = cx - this.lastMouse.x;
const dy = cy - this.lastMouse.y;
const dDist = dist - this.lastPinchDist; // Positive if spreading fingers
this.lastMouse = {x: cx, y: cy};
this.lastPinchDist = dist;
this.pan(dx, dy);
this.zoom(-dDist); // Pinching out zooms in
this.onChange();
}
}, {passive: false});
const onTouchEnd = (e) => {
if (e.touches.length === 0) {
this.isDragging = false;
} else if (e.touches.length === 1) {
// Seamlessly fall back to orbit if one finger is lifted
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
}
};
this.element.addEventListener('touchend', onTouchEnd);
this.element.addEventListener('touchcancel', onTouchEnd);
}
updateMatrices(aspectRatio) {
// ... (Keep existing updateMatrices code exactly as it is) ...
const t = this.theta * Math.PI / 180;
const p = this.phi * Math.PI / 180;
const eyeX = this.target[0] + this.radius * Math.cos(p) * Math.sin(t);
const eyeY = this.target[1] + this.radius * Math.sin(p);
const eyeZ = this.target[2] + this.radius * Math.cos(p) * Math.cos(t);
const upX = -Math.sin(p) * Math.sin(t);
const upY = Math.cos(p);
const upZ = -Math.sin(p) * Math.cos(t);
const size = this.radius * 0.5;
mat4Ortho(this.projMatrix, -size * aspectRatio, size * aspectRatio, -size, size, -50, 50);
mat4LookAt(this.viewMatrix, eyeX, eyeY, eyeZ, this.target[0], this.target[1], this.target[2], upX, upY, upZ);
mat4Multiply(this.viewProjMatrix, this.projMatrix, this.viewMatrix);
return this.viewProjMatrix;
}
}

View File

@@ -1,40 +1,78 @@
import {Camera} from './camera.js'; // diorama.js
import {World} from './world.js'; import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
export class Diorama { export class Diorama {
constructor(elementId, requestRenderCallback) { constructor(elementId, world, requestRenderCallback) {
this.element = document.getElementById(elementId); this.element = document.getElementById(elementId);
this.world = new World(); this.world = world;
this.requestRender = requestRenderCallback; 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 = '';
this.lastMouse = {x: 0, y: 0};
this.lastPinchDist = 0;
this.checkpoint = null;
this.initUI();
this.attachEvents();
}
initUI() {
this.element.style.position = "relative"; this.element.style.position = "relative";
this.reset = document.createElement('button') this.resetBtn = document.createElement('button');
this.reset.textContent = 'Reset View' this.resetBtn.textContent = 'Reset View';
this.reset.style.cssText = ` this.resetBtn.style.cssText = `
position: absolute; top: 1ch; right: 1ch; position: absolute; top: 1ch; right: 1ch;
padding: 0.5ch 1ch; background: rgba(0,0,0,0.7); padding: 0.5ch 1ch; background: rgba(0,0,0,0.7);
color: white; border: 1px solid gray; color: white; border: 1px solid gray;
cursor: pointer; display: none; z-index: 10; cursor: pointer; display: none; z-index: 10;
`; `;
this.reset.addEventListener('mousedown', e => e.stopPropagation())
this.reset.addEventListener('touchstart', e => e.stopPropagation())
this.reset.addEventListener('click', () => {
this.camera.loadState();
this.reset.style.display = 'none';
});
this.element.appendChild(this.reset);
this.camera = new Camera(this.element, () => { this.resetBtn.addEventListener('mousedown', e => e.stopPropagation());
if (this.camera.checkpoint) this.reset.style.display = 'block'; this.resetBtn.addEventListener('touchstart', e => e.stopPropagation());
this.requestRender(); this.resetBtn.addEventListener('click', () => {
this.loadState();
this.resetBtn.style.display = 'none';
}); });
this.camera.target = [0.5, 0.5, 0.5]; this.element.appendChild(this.resetBtn);
}
saveState() {
this.checkpoint = {
target: [...this.target],
radius: this.radius,
theta: this.theta,
phi: this.phi,
};
}
loadState() {
if (!this.checkpoint) return;
this.target = [...this.checkpoint.target];
this.radius = this.checkpoint.radius;
this.theta = this.checkpoint.theta;
this.phi = this.checkpoint.phi;
this.requestRender();
} }
centerView() { centerView() {
if (this.world.blocks.size === 0) { if (this.world.blocks.size === 0) {
this.camera.target = [0.5, 0.5, 0.5]; this.target = [0.5, 0.5, 0.5];
return; return;
} }
@@ -50,7 +88,181 @@ export class Diorama {
maxZ = Math.max(maxZ, block.z + 1); maxZ = Math.max(maxZ, block.z + 1);
} }
this.camera.target = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2]; this.target = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2];
this.requestRender(); this.requestRender();
} }
updateMatrices(aspectRatio) {
const t = this.theta * Math.PI / 180;
const p = this.phi * Math.PI / 180;
this.viewDir = [
Math.cos(p) * Math.sin(t),
Math.sin(p),
Math.cos(p) * Math.cos(t)
];
this.upDir = [
-Math.sin(p) * Math.sin(t),
Math.cos(p),
-Math.sin(p) * Math.cos(t),
];
this.eyePos = [
this.target[0] + this.radius * this.viewDir[0],
this.target[1] + this.radius * this.viewDir[1],
this.target[2] + this.radius * this.viewDir[2],
];
const size = this.radius * 0.5;
mat4Ortho(this.projMatrix, -size * aspectRatio, size * aspectRatio, -size, size, -50, 50);
mat4LookAt(
this.viewMatrix,
this.eyePos[0], this.eyePos[1], this.eyePos[2],
this.target[0], this.target[1], this.target[2],
this.upDir[0], this.upDir[1], this.upDir[2],
);
mat4Multiply(this.viewProjMatrix, this.projMatrix, this.viewMatrix);
return this.viewProjMatrix;
}
// --- INTERACTION MATH ---
orbit(dx, dy) {
this.theta -= dx * 0.4;
this.phi += dy * 0.4;
this.phi = Math.max(-90, Math.min(90, this.phi));
}
pan(dx, dy) {
const t = this.theta * Math.PI / 180;
const p = this.phi * Math.PI / 180;
const rightX = Math.cos(t);
const rightZ = -Math.sin(t);
const upX = -Math.sin(p) * Math.sin(t);
const upY = Math.cos(p);
const upZ = -Math.sin(p) * Math.cos(t);
const panSpeed = this.radius * 0.0025;
this.target[0] += (-rightX * dx + upX * dy) * panSpeed;
this.target[1] += (upY * dy) * panSpeed;
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
}
zoom(delta) {
this.radius += delta * 0.05;
this.radius = Math.max(1, Math.min(50, this.radius));
}
// --- EVENT LISTENERS ---
attachEvents() {
this.element.addEventListener('contextmenu', e => e.preventDefault());
const notifyChange = () => {
if (this.checkpoint) this.resetBtn.style.display = 'block';
this.requestRender();
};
// --- MOUSE EVENTS ---
this.element.addEventListener('mousedown', (e) => {
e.preventDefault();
this.isDragging = true;
this.lastMouse = {x: e.clientX, y: e.clientY};
this.dragButton = e.button;
if (this.dragButton === 0) {
if (e.ctrlKey || e.metaKey) this.dragButton = 2;
else if (e.shiftKey) this.dragButton = 1;
}
if (this.dragButton === 1) this.element.style.cursor = 'move';
else if (this.dragButton === 2) this.element.style.cursor = 'ns-resize';
else this.element.style.cursor = 'grabbing';
});
window.addEventListener('mouseup', () => {
this.isDragging = false;
this.element.style.cursor = 'grab';
});
window.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const dx = e.clientX - this.lastMouse.x;
const dy = e.clientY - this.lastMouse.y;
this.lastMouse = {x: e.clientX, y: e.clientY};
if (this.dragButton === 0) this.orbit(dx, dy);
else if (this.dragButton === 1) this.pan(dx, dy);
else if (this.dragButton === 2) this.zoom(dy);
notifyChange();
});
// --- TOUCH EVENTS ---
this.element.addEventListener('touchstart', (e) => {
e.preventDefault();
this.isDragging = true;
if (e.touches.length === 1) {
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
} else if (e.touches.length >= 2) {
this.touchMode = 'pan-zoom';
const t1 = e.touches[0], t2 = e.touches[1];
this.lastMouse = {
x: (t1.clientX + t2.clientX) / 2,
y: (t1.clientY + t2.clientY) / 2
};
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
}
}, {passive: false});
this.element.addEventListener('touchmove', (e) => {
if (!this.isDragging) return;
e.preventDefault();
if (this.touchMode === 'orbit' && e.touches.length === 1) {
const dx = e.touches[0].clientX - this.lastMouse.x;
const dy = e.touches[0].clientY - this.lastMouse.y;
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
this.orbit(dx, dy);
notifyChange();
} else if (this.touchMode === 'pan-zoom' && e.touches.length >= 2) {
const t1 = e.touches[0], t2 = e.touches[1];
const cx = (t1.clientX + t2.clientX) / 2;
const cy = (t1.clientY + t2.clientY) / 2;
const dist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
const dx = cx - this.lastMouse.x;
const dy = cy - this.lastMouse.y;
const dDist = dist - this.lastPinchDist;
this.lastMouse = {x: cx, y: cy};
this.lastPinchDist = dist;
this.pan(dx, dy);
this.zoom(-dDist);
notifyChange();
}
}, {passive: false});
const onTouchEnd = (e) => {
if (e.touches.length === 0) {
this.isDragging = false;
} else if (e.touches.length === 1) {
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
}
};
this.element.addEventListener('touchend', onTouchEnd);
this.element.addEventListener('touchcancel', onTouchEnd);
}
} }

View File

@@ -16,6 +16,7 @@ layout(location=9) in vec3 i_color;
uniform mat4 u_viewProj; uniform mat4 u_viewProj;
uniform vec3 u_lightDir; uniform vec3 u_lightDir;
uniform vec3 u_viewDir;
out vec2 v_uv; out vec2 v_uv;
out float v_light; out float v_light;
@@ -24,8 +25,13 @@ out vec3 v_color;
void main() { void main() {
gl_Position = u_viewProj * i_matrix * vec4(a_position, 1.0); gl_Position = u_viewProj * i_matrix * vec4(a_position, 1.0);
v_uv = a_uv; v_uv = a_uv;
vec3 normal = normalize(mat3(i_matrix) * a_normal); vec3 normal = normalize(mat3(i_matrix) * a_normal);
float baseLight = max(dot(normal, normalize(u_lightDir)), 0.0) * 0.6 + 0.4;
float sun = max(dot(normal, normalize(u_lightDir)), 0.0);
float head = max(dot(normal, normalize(u_viewDir)), 0.0);
float baseLight = clamp(sun + head * 0.3, 0.0, 1.0) * 0.6 + 0.4;
v_light = mix(1.0, baseLight, a_shade); v_light = mix(1.0, baseLight, a_shade);
v_color = mix(vec3(1.0), i_color, a_tint); v_color = mix(vec3(1.0), i_color, a_tint);
} }
@@ -80,6 +86,7 @@ export class Engine {
this.uniforms = { this.uniforms = {
viewProj: gl.getUniformLocation(this.program, "u_viewProj"), viewProj: gl.getUniformLocation(this.program, "u_viewProj"),
lightDir: gl.getUniformLocation(this.program, "u_lightDir"), lightDir: gl.getUniformLocation(this.program, "u_lightDir"),
viewDir: gl.getUniformLocation(this.program, "u_viewDir"),
texture: gl.getUniformLocation(this.program, "u_texture") texture: gl.getUniformLocation(this.program, "u_texture")
}; };
@@ -87,10 +94,13 @@ export class Engine {
this.atlas = new TextureAtlas(256); this.atlas = new TextureAtlas(256);
this.dioramas = []; this.dioramas = [];
this.dioramaMeshes = new Map(); // Global Mesh Storage: Diorama -> Array of meshes this.worldMeshes = new Map();
this.renderRequested = false; this.renderRequested = false;
window.addEventListener('resize', () => { this.resize(); this.requestRender(); }); window.addEventListener('resize', () => {
this.resize();
this.requestRender();
});
window.addEventListener('scroll', () => this.requestRender(), {passive: true}); window.addEventListener('scroll', () => this.requestRender(), {passive: true});
this.resize(); this.resize();
} }
@@ -112,41 +122,42 @@ export class Engine {
addDiorama(diorama) { addDiorama(diorama) {
this.dioramas.push(diorama); this.dioramas.push(diorama);
this.dioramaMeshes.set(diorama, []);
} }
// O(1) Cascade: Fetch all resources breadth-first across ALL dioramas
async updateAll() { async updateAll() {
// Extract all unique worlds currently being viewed
const uniqueWorlds = new Set(this.dioramas.map(d => d.world));
// --- SWEEP 1: Collect & Fetch Blockstates --- // --- SWEEP 1: Collect & Fetch Blockstates ---
const uniqueBlockIds = new Set(); const uniqueBlockIds = new Set();
for (const d of this.dioramas) { for (const world of uniqueWorlds) {
for (const block of d.world.blocks.values()) uniqueBlockIds.add(block.id); for (const block of world.blocks.values()) uniqueBlockIds.add(block.id);
} }
await Promise.all(Array.from(uniqueBlockIds).map(id => loadBlockstate(id))); await Promise.all(Array.from(uniqueBlockIds).map(id => loadBlockstate(id)));
// --- SWEEP 2: Resolve Parts & Fetch Models --- // --- SWEEP 2: Resolve Parts & Fetch Models ---
const uniqueModelIds = new Set(); const uniqueModelIds = new Set();
const parsedDioramas = new Map(); // Diorama -> Array of {block, parts} const parsedWorlds = new Map(); // World -> Array of {block, parts}
for (const d of this.dioramas) { for (const world of uniqueWorlds) {
const parsedBlocks = []; const parsedBlocks = [];
for (const block of d.world.blocks.values()) { for (const block of world.blocks.values()) {
const stateJSON = await loadBlockstate(block.id); // Returns instantly from cache const stateJSON = await loadBlockstate(block.id);
const parts = resolveBlock(stateJSON, block.props); const parts = resolveBlock(stateJSON, block.props);
parsedBlocks.push({block, parts}); parsedBlocks.push({block, parts});
for (const p of parts) uniqueModelIds.add(p.model); for (const p of parts) uniqueModelIds.add(p.model);
} }
parsedDioramas.set(d, parsedBlocks); parsedWorlds.set(world, parsedBlocks);
} }
await Promise.all(Array.from(uniqueModelIds).map(id => loadModel(id))); await Promise.all(Array.from(uniqueModelIds).map(id => loadModel(id)));
// --- SWEEP 3: Pool Instances & Fetch Textures --- // --- SWEEP 3: Pool Instances & Fetch Textures ---
const uniqueTexturePaths = new Set(); const uniqueTexturePaths = new Set();
const dioramaPools = new Map(); // Diorama -> instancePool const worldPools = new Map(); // World -> instancePool
for (const d of this.dioramas) { for (const world of uniqueWorlds) {
const instancePool = new Map(); const instancePool = new Map();
for (const {block, parts} of parsedDioramas.get(d)) { for (const {block, parts} of parsedWorlds.get(world)) {
for (const part of parts) { for (const part of parts) {
const hash = getVariantHash(part); const hash = getVariantHash(part);
if (!instancePool.has(hash)) { if (!instancePool.has(hash)) {
@@ -167,9 +178,8 @@ export class Engine {
} }
} }
// Collect required textures from the resolved models
for (const pool of instancePool.values()) { for (const pool of instancePool.values()) {
const modelJSON = await loadModel(pool.partDef.model); // Instant const modelJSON = await loadModel(pool.partDef.model);
for (const el of modelJSON.elements || []) { for (const el of modelJSON.elements || []) {
for (const face of Object.values(el.faces || {})) { for (const face of Object.values(el.faces || {})) {
const texPath = resolveTexture(modelJSON, face.texture); const texPath = resolveTexture(modelJSON, face.texture);
@@ -177,25 +187,24 @@ export class Engine {
} }
} }
} }
dioramaPools.set(d, instancePool); worldPools.set(world, instancePool);
} }
// Fire all texture image requests concurrently
await Promise.all(Array.from(uniqueTexturePaths).map(path => this.atlas.load(path))); await Promise.all(Array.from(uniqueTexturePaths).map(path => this.atlas.load(path)));
this.updateAtlasTexture(); this.updateAtlasTexture();
// --- SWEEP 4: Bake Geometry --- // --- SWEEP 4: Bake Geometry ---
for (const d of this.dioramas) { for (const world of uniqueWorlds) {
const newMeshes = []; const newMeshes = [];
for (const pool of dioramaPools.get(d).values()) { for (const pool of worldPools.get(world).values()) {
const modelJSON = await loadModel(pool.partDef.model); // Instant const modelJSON = await loadModel(pool.partDef.model);
const geometry = buildGeometry(modelJSON, this.atlas, pool.partDef); const geometry = buildGeometry(modelJSON, this.atlas, pool.partDef);
const matrixArray = new Float32Array(pool.matrices); const matrixArray = new Float32Array(pool.matrices);
const colorArray = new Float32Array(pool.colors); const colorArray = new Float32Array(pool.colors);
const instanceCount = pool.matrices.length / 16; const instanceCount = pool.matrices.length / 16;
newMeshes.push(this.createInstancedMesh(geometry, instanceCount, matrixArray, colorArray)); newMeshes.push(this.createInstancedMesh(geometry, instanceCount, matrixArray, colorArray));
} }
this.dioramaMeshes.set(d, newMeshes); this.worldMeshes.set(world, newMeshes);
} }
this.requestRender(); this.requestRender();
@@ -275,11 +284,14 @@ export class Engine {
gl.scissor(rect.left, bottom, rect.width, rect.height); gl.scissor(rect.left, bottom, rect.width, rect.height);
const aspect = rect.width / rect.height; const aspect = rect.width / rect.height;
const viewProj = diorama.camera.updateMatrices(aspect); const viewProj = diorama.updateMatrices(aspect);
gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj); gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj);
gl.uniform3f(this.uniforms.lightDir, 1.0, 2.0, 0.5); gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, 2);
gl.uniform3f(this.uniforms.viewDir,
diorama.viewDir[0], diorama.viewDir[1], diorama.viewDir[2]
);
const meshes = this.dioramaMeshes.get(diorama) || []; const meshes = this.worldMeshes.get(diorama.world) || [];
for (const mesh of meshes) { for (const mesh of meshes) {
gl.bindVertexArray(mesh.vao); gl.bindVertexArray(mesh.vao);
gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount); gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount);

View File

@@ -38,7 +38,10 @@
Mauris ullamcorper accumsan dui vel pulvinar. Mauris ullamcorper accumsan dui vel pulvinar.
</p> </p>
<div class="figure" id="demo-2"></div> <div style="display: grid; grid-template-columns: 1fr 1fr;">
<div class="figure" id="demo-2"></div>
<div class="figure" id="demo-3"></div>
</div>
<p> <p>
Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque penatibus et magnis Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque penatibus et magnis
@@ -51,67 +54,78 @@
<script type="module"> <script type="module">
import {Engine} from './engine.js'; import {Engine} from './engine.js';
import {Diorama} from './diorama.js'; import {Diorama} from './diorama.js';
import {World} from './world.js';
async function initDocument() { async function initDocument() {
try { try {
const engine = new Engine(); const engine = new Engine();
document.getElementById('atlas-container')?.appendChild(engine.atlas.canvas); document.getElementById('atlas-container')?.appendChild(engine.atlas.canvas);
// Pass the render trigger directly const world1 = new World();
const demo1 = new Diorama('demo-1', () => engine.requestRender()); world1.setBlock('minecraft:redstone_wire', -1, 0, 0, {
demo1.camera.radius = 2;
demo1.camera.phi = 10;
demo1.camera.theta = -5;
demo1.world.setBlock('minecraft:redstone_wire', -1, 0, 0, {
east: 'none', east: 'none',
west: 'none', west: 'none',
north: 'none', north: 'none',
south: 'none', south: 'none',
power: '15' power: '15'
}); });
demo1.world.setBlock('minecraft:repeater', 0, 0, 0, { world1.setBlock('minecraft:repeater', 0, 0, 0, {
facing: 'east', facing: 'east',
delay: '1', delay: '1',
locked: 'false', locked: 'false',
powered: 'true' powered: 'true'
}); });
demo1.world.setBlock('minecraft:oak_trapdoor', 2, 0, 0, {facing: 'east', half: 'bottom', open: 'true'}); world1.setBlock('minecraft:oak_trapdoor', 2, 0, 0, {facing: 'east', half: 'bottom', open: 'true'});
demo1.world.setBlock('lodestone', 0, -1, 0); world1.setBlock('lodestone', 0, -1, 0);
demo1.world.setBlock('piston', 0, -2, 0, {facing: 'north', extended: 'false'}); world1.setBlock('piston', 0, -2, 0, {facing: 'north', extended: 'false'});
const world2 = new World();
const demo2 = new Diorama('demo-2', () => engine.requestRender()); world2.setBlock('minecraft:redstone_wire', 0, 0, 0, {
demo2.camera.radius = 3;
demo2.world.setBlock('minecraft:redstone_wire', 0, 0, 0, {
east: 'side', east: 'side',
west: 'none', west: 'none',
north: 'none', north: 'none',
south: 'none', south: 'none',
power: '0' power: '0'
}); });
demo2.world.setBlock('minecraft:repeater', 1, 0, 0, { world2.setBlock('minecraft:repeater', 1, 0, 0, {
facing: 'east', facing: 'east',
delay: '4', delay: '4',
locked: 'false', locked: 'false',
powered: 'false' powered: 'false'
}); });
demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 0, {facing: 'east', extended: 'false'}); world2.setBlock('minecraft:sticky_piston', 2, 0, 0, {facing: 'east', extended: 'false'});
demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 1, {facing: 'east', extended: 'true'}); world2.setBlock('minecraft:sticky_piston', 2, 0, 1, {facing: 'east', extended: 'true'});
demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'}); world2.setBlock('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'});
demo2.world.setBlock('minecraft:piston_head', 2.5, 0, 1, {facing: 'east', short: 'true', type: 'sticky'}); world2.setBlock('minecraft:piston_head', 2.5, 0, 1, {facing: 'east', short: 'true', type: 'sticky'});
demo2.world.setBlock('minecraft:piston_head', 3, 0, 2, {facing: 'east', short: 'false', type: 'sticky'}); world2.setBlock('minecraft:piston_head', 3, 0, 2, {facing: 'east', short: 'false', type: 'sticky'});
demo2.world.setBlock('minecraft:glass', 3, 0, 0); world2.setBlock('minecraft:glass', 3, 0, 0);
demo2.world.setBlock('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'}); world2.setBlock('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'});
demo2.world.setBlock('minecraft:redstone_block', 4, 0, 2); world2.setBlock('minecraft:redstone_block', 4, 0, 2);
demo1.centerView();
demo1.camera.saveState();
demo2.centerView();
demo2.camera.saveState();
const demo1 = new Diorama('demo-1', world1, () => engine.requestRender())
demo1.radius = 2;
demo1.phi = 10;
demo1.theta = -5;
demo1.target = [0.5, 0, 0.5]
demo1.saveState()
engine.addDiorama(demo1); engine.addDiorama(demo1);
const demo2 = new Diorama('demo-2', world2, () => engine.requestRender())
demo2.radius = 4;
demo2.theta = 180;
demo2.phi = 90;
demo2.centerView()
demo2.saveState()
engine.addDiorama(demo2); engine.addDiorama(demo2);
const demo3 = new Diorama('demo-3', world2, () => engine.requestRender())
demo3.radius = 4;
demo3.theta = 0;
demo3.phi = 0;
demo3.centerView()
demo3.saveState()
engine.addDiorama(demo3);
// Fetch and build everything concurrently! // Fetch and build everything concurrently!
await engine.updateAll(); await engine.updateAll();
} catch (err) { } catch (err) {