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

View File

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