wip faster texture loading

This commit is contained in:
David Allemang
2026-07-04 12:16:34 -04:00
parent a00d8c52d7
commit 57660397aa
5 changed files with 94 additions and 52 deletions

View File

@@ -53,28 +53,18 @@ export class TextureAtlas {
this.ctx.imageSmoothingEnabled = false; this.ctx.imageSmoothingEnabled = false;
this.map = new Map(); this.map = new Map();
this.promises = new Map(); // Track loading promises separately!
this.cellSize = 16; this.cellSize = 16;
this.padding = 2; // Add empty space between textures this.padding = 1;
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
this.rowHeight = 0; this.rowHeight = 0;
} }
async load(id) { async fill(id, currentX, currentY) {
if (this.map.has(id)) return this.map.get(id);
const url = resolveResourceLocation(id, 'textures', 'png'); 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 { try {
const img = await new Promise((resolve, reject) => { const img = await new Promise((resolve, reject) => {
const i = new Image(); const i = new Image();
@@ -90,21 +80,39 @@ export class TextureAtlas {
this.ctx.fillStyle = '#ff00ff'; this.ctx.fillStyle = '#ff00ff';
this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize); this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize);
} }
}
// Clean UV mapping (padding protects the edges natively) load(id) {
const uvData = { // If already loading/loaded, just return the tracking promise
u: currentX / this.canvas.width, if (this.promises.has(id)) return this.promises.get(id);
v: currentY / this.canvas.height,
du: this.cellSize / this.canvas.width,
dv: this.cellSize / this.canvas.height
};
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.x += this.cellSize + this.padding;
this.rowHeight = Math.max(this.rowHeight, this.cellSize); 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;
} }
} }

View File

@@ -24,6 +24,24 @@ export class Camera {
this.attachEvents(); 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 --- // --- INTERACTION MATH ---
orbit(dx, dy) { orbit(dx, dy) {
@@ -115,7 +133,7 @@ export class Camera {
}; };
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY); this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
} }
}, { passive: false }); }, {passive: false});
this.element.addEventListener('touchmove', (e) => { this.element.addEventListener('touchmove', (e) => {
if (!this.isDragging) return; if (!this.isDragging) return;
@@ -128,8 +146,7 @@ export class Camera {
this.orbit(dx, dy); this.orbit(dx, dy);
this.onChange(); 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 t1 = e.touches[0], t2 = e.touches[1];
const cx = (t1.clientX + t2.clientX) / 2; const cx = (t1.clientX + t2.clientX) / 2;
const cy = (t1.clientY + t2.clientY) / 2; const cy = (t1.clientY + t2.clientY) / 2;
@@ -146,7 +163,7 @@ export class Camera {
this.zoom(-dDist); // Pinching out zooms in this.zoom(-dDist); // Pinching out zooms in
this.onChange(); this.onChange();
} }
}, { passive: false }); }, {passive: false});
const onTouchEnd = (e) => { const onTouchEnd = (e) => {
if (e.touches.length === 0) { if (e.touches.length === 0) {

View File

@@ -1,6 +1,5 @@
// diorama.js import {Camera} from './camera.js';
import { Camera } from './camera.js'; import {World} from './world.js';
import { World } from './world.js';
export class Diorama { export class Diorama {
constructor(elementId, engine) { constructor(elementId, engine) {
@@ -8,8 +7,29 @@ export class Diorama {
this.engine = engine; this.engine = engine;
this.world = new World(); this.world = new World();
// Pass the engine's render request function to the camera this.element.style.position = "relative";
this.camera = new Camera(this.element, () => this.engine.requestRender()); 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.camera.target = [0.5, 0.5, 0.5];
this.meshes = []; this.meshes = [];
@@ -35,19 +55,12 @@ export class Diorama {
maxZ = Math.max(maxZ, block.z + 1); maxZ = Math.max(maxZ, block.z + 1);
} }
this.camera.target = [ this.camera.target = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2];
(minX + maxX) / 2,
(minY + maxY) / 2,
(minZ + maxZ) / 2
];
// Wake up the renderer so the view snaps immediately
this.engine.requestRender(); this.engine.requestRender();
} }
async update() { async update() {
this.meshes = await this.engine.buildMeshes(this.world); this.meshes = await this.engine.buildMeshes(this.world);
// Ensure the scene draws immediately after meshes are built
this.engine.requestRender(); this.engine.requestRender();
} }
} }

View File

@@ -94,7 +94,7 @@ export class Engine {
}; };
this.atlasTexture = gl.createTexture(); this.atlasTexture = gl.createTexture();
this.atlas = new TextureAtlas(512); this.atlas = new TextureAtlas(256);
this.dioramas = []; this.dioramas = [];
this.renderRequested = false; this.renderRequested = false;
@@ -160,6 +160,7 @@ export class Engine {
} }
// 2. Build geometries & instantiate WebGL buffers // 2. Build geometries & instantiate WebGL buffers
const load_tasks = [];
const newMeshes = []; const newMeshes = [];
for (const [hash, pool] of instancePool.entries()) { for (const [hash, pool] of instancePool.entries()) {
const modelJSON = await loadModel(pool.partDef.model); const modelJSON = await loadModel(pool.partDef.model);
@@ -167,7 +168,7 @@ export class Engine {
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);
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)); newMeshes.push(this.createInstancedMesh(geometry, instanceCount, matrixArray, colorArray));
} }
this.updateAtlasTexture(); await Promise.all(load_tasks)
this.updateAtlasTexture()
return newMeshes; return newMeshes;
} }

View File

@@ -45,7 +45,7 @@
dis parturient montes, nascetur ridiculus mus. dis parturient montes, nascetur ridiculus mus.
</p> </p>
<!-- <div id="atlas-container"></div>--> <div id="atlas-container" style="position: fixed; left: 0; top: 0; border: 1px solid white;"></div>
</main> </main>
<script type="module"> <script type="module">
@@ -82,10 +82,6 @@
}); });
demo1.world.setBlock('minecraft:oak_trapdoor', 2, 0, 0, {facing: 'east', half: 'bottom', open: 'true'}); demo1.world.setBlock('minecraft:oak_trapdoor', 2, 0, 0, {facing: 'east', half: 'bottom', open: 'true'});
demo1.centerView()
engine.addDiorama(demo1);
// 3. Setup Figure 2 (Shared: Wire & Repeater | Unique: Piston & Redstone Block) // 3. Setup Figure 2 (Shared: Wire & Repeater | Unique: Piston & Redstone Block)
const demo2 = new Diorama('demo-2', engine); const demo2 = new Diorama('demo-2', engine);
// Adjust camera to fit the small viewport // Adjust camera to fit the small viewport
@@ -108,14 +104,20 @@
demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 0, {facing: 'east', extended: 'false'}); demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 0, {facing: 'east', extended: 'false'});
demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 1, {facing: 'east', extended: 'true'}); demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 1, {facing: 'east', extended: 'true'});
demo2.world.setBlock('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'}); demo2.world.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'}); demo2.world.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'}); demo2.world.setBlock('minecraft:piston_head', 3, 0, 2, {facing: 'east', short: 'false', type: 'sticky'});
demo2.world.setBlock('minecraft:glass', 3, 0, 0); demo2.world.setBlock('minecraft:glass', 3, 0, 0);
demo2.world.setBlock('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'}); demo2.world.setBlock('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'});
demo2.world.setBlock('minecraft:redstone_block', 4, 0, 2); demo2.world.setBlock('minecraft:redstone_block', 4, 0, 2);
demo1.centerView()
demo1.camera.saveState();
demo2.centerView() demo2.centerView()
demo2.camera.saveState();
engine.addDiorama(demo1);
engine.addDiorama(demo2); engine.addDiorama(demo2);
// 4. Build geometries and draw // 4. Build geometries and draw