wip faster texture loading
This commit is contained in:
58
assets.js
58
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
camera.js
25
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) {
|
||||
|
||||
39
diorama.js
39
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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
16
index.html
16
index.html
@@ -45,7 +45,7 @@
|
||||
dis parturient montes, nascetur ridiculus mus.
|
||||
</p>
|
||||
|
||||
<!-- <div id="atlas-container"></div>-->
|
||||
<div id="atlas-container" style="position: fixed; left: 0; top: 0; border: 1px solid white;"></div>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
@@ -82,10 +82,6 @@
|
||||
});
|
||||
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)
|
||||
const demo2 = new Diorama('demo-2', engine);
|
||||
// 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, 1, {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', 3, 0, 2, {facing: 'east', short: 'false',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:glass', 3, 0, 0);
|
||||
demo2.world.setBlock('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'});
|
||||
demo2.world.setBlock('minecraft:redstone_block', 4, 0, 2);
|
||||
|
||||
demo1.centerView()
|
||||
demo1.camera.saveState();
|
||||
|
||||
demo2.centerView()
|
||||
demo2.camera.saveState();
|
||||
|
||||
engine.addDiorama(demo1);
|
||||
engine.addDiorama(demo2);
|
||||
|
||||
// 4. Build geometries and draw
|
||||
|
||||
Reference in New Issue
Block a user