From 57660397aaca27bb756bf01be633fd42d8516808 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sat, 4 Jul 2026 12:16:34 -0400 Subject: [PATCH] wip faster texture loading --- assets.js | 58 +++++++++++++++++++++++++++++++----------------------- camera.js | 25 +++++++++++++++++++---- diorama.js | 39 ++++++++++++++++++++++++------------ engine.js | 8 +++++--- index.html | 16 ++++++++------- 5 files changed, 94 insertions(+), 52 deletions(-) diff --git a/assets.js b/assets.js index 56e30b73..52862f38 100644 --- a/assets.js +++ b/assets.js @@ -53,28 +53,18 @@ export class TextureAtlas { this.ctx.imageSmoothingEnabled = false; this.map = new Map(); + this.promises = new Map(); // Track loading promises separately! + this.cellSize = 16; - this.padding = 2; // Add empty space between textures + this.padding = 1; this.x = 0; this.y = 0; this.rowHeight = 0; } - async load(id) { - if (this.map.has(id)) return this.map.get(id); - + async fill(id, currentX, currentY) { const url = resolveResourceLocation(id, 'textures', 'png'); - // Include padding in the wrap calculation - if (this.x + this.cellSize > this.canvas.width) { - this.x = 0; - this.y += this.rowHeight + this.padding; - this.rowHeight = 0; - } - - const currentX = this.x; - const currentY = this.y; - try { const img = await new Promise((resolve, reject) => { const i = new Image(); @@ -90,21 +80,39 @@ export class TextureAtlas { this.ctx.fillStyle = '#ff00ff'; this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize); } + } - // Clean UV mapping (padding protects the edges natively) - const uvData = { - u: currentX / this.canvas.width, - v: currentY / this.canvas.height, - du: this.cellSize / this.canvas.width, - dv: this.cellSize / this.canvas.height - }; + load(id) { + // If already loading/loaded, just return the tracking promise + if (this.promises.has(id)) return this.promises.get(id); - this.map.set(id, uvData); + if (this.x + this.cellSize > this.canvas.width) { + this.x = 0; + this.y += this.rowHeight + this.padding; + this.rowHeight = 0; + } + + const currentX = this.x; + const currentY = this.y; - // Advance X by cell size AND padding this.x += this.cellSize + this.padding; this.rowHeight = Math.max(this.rowHeight, this.cellSize); + const epsX = 0.1 / this.canvas.width; + const epsY = 0.1 / this.canvas.height; - return uvData; + // 1. SYNCHRONOUSLY allocate the UV coordinates for buildGeometry + const uvData = { + u: currentX / this.canvas.width + epsX, + v: currentY / this.canvas.height + epsY, + du: this.cellSize / this.canvas.width - 2 * epsX, + dv: this.cellSize / this.canvas.height - 2 * epsY, + }; + this.map.set(id, uvData); + + // 2. ASYNCHRONOUSLY kick off the image fetch + const loadTask = this.fill(id, currentX, currentY); + this.promises.set(id, loadTask); + + return loadTask; } -} \ No newline at end of file +} diff --git a/camera.js b/camera.js index 96bbe3eb..6c401662 100644 --- a/camera.js +++ b/camera.js @@ -24,6 +24,24 @@ export class Camera { 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) { @@ -115,7 +133,7 @@ export class Camera { }; this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY); } - }, { passive: false }); + }, {passive: false}); this.element.addEventListener('touchmove', (e) => { if (!this.isDragging) return; @@ -128,8 +146,7 @@ export class Camera { this.orbit(dx, dy); this.onChange(); - } - else if (this.touchMode === 'pan-zoom' && e.touches.length >= 2) { + } 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; @@ -146,7 +163,7 @@ export class Camera { this.zoom(-dDist); // Pinching out zooms in this.onChange(); } - }, { passive: false }); + }, {passive: false}); const onTouchEnd = (e) => { if (e.touches.length === 0) { diff --git a/diorama.js b/diorama.js index 6573a9e4..0ee67ce0 100644 --- a/diorama.js +++ b/diorama.js @@ -1,6 +1,5 @@ -// diorama.js -import { Camera } from './camera.js'; -import { World } from './world.js'; +import {Camera} from './camera.js'; +import {World} from './world.js'; export class Diorama { constructor(elementId, engine) { @@ -8,8 +7,29 @@ export class Diorama { this.engine = engine; this.world = new World(); - // Pass the engine's render request function to the camera - this.camera = new Camera(this.element, () => this.engine.requestRender()); + this.element.style.position = "relative"; + this.reset = document.createElement('button') + this.reset.textContent = 'Reset View' + this.reset.style.cssText = ` + position: absolute; top: 1ch; right: 1ch; + padding: 0.5ch 1ch; background: rgba(0,0,0,0.7); + color: white; border: 1px solid gray; + 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, () => { + if (this.camera.checkpoint) this.reset.style.display = 'block'; + this.engine.requestRender() + }); this.camera.target = [0.5, 0.5, 0.5]; this.meshes = []; @@ -35,19 +55,12 @@ export class Diorama { maxZ = Math.max(maxZ, block.z + 1); } - this.camera.target = [ - (minX + maxX) / 2, - (minY + maxY) / 2, - (minZ + maxZ) / 2 - ]; - - // Wake up the renderer so the view snaps immediately + this.camera.target = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2]; this.engine.requestRender(); } async update() { this.meshes = await this.engine.buildMeshes(this.world); - // Ensure the scene draws immediately after meshes are built this.engine.requestRender(); } } \ No newline at end of file diff --git a/engine.js b/engine.js index 8b3e288c..66c86701 100644 --- a/engine.js +++ b/engine.js @@ -94,7 +94,7 @@ export class Engine { }; this.atlasTexture = gl.createTexture(); - this.atlas = new TextureAtlas(512); + this.atlas = new TextureAtlas(256); this.dioramas = []; this.renderRequested = false; @@ -160,6 +160,7 @@ export class Engine { } // 2. Build geometries & instantiate WebGL buffers + const load_tasks = []; const newMeshes = []; for (const [hash, pool] of instancePool.entries()) { const modelJSON = await loadModel(pool.partDef.model); @@ -167,7 +168,7 @@ export class Engine { for (const el of modelJSON.elements || []) { for (const face of Object.values(el.faces || {})) { const texPath = resolveTexture(modelJSON, face.texture); - if (texPath) await this.atlas.load(texPath); + if (texPath) load_tasks.push(this.atlas.load(texPath)); } } @@ -179,7 +180,8 @@ export class Engine { newMeshes.push(this.createInstancedMesh(geometry, instanceCount, matrixArray, colorArray)); } - this.updateAtlasTexture(); + await Promise.all(load_tasks) + this.updateAtlasTexture() return newMeshes; } diff --git a/index.html b/index.html index 1ac5cb97..520de1d8 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,7 @@ dis parturient montes, nascetur ridiculus mus.

- +