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.
-
+