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.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
camera.js
25
camera.js
@@ -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) {
|
||||||
|
|||||||
39
diorama.js
39
diorama.js
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
index.html
16
index.html
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user