streaming assets
This commit is contained in:
@@ -13,6 +13,10 @@ export class BlockstateHandler {
|
||||
return new Blockstate({variants: {"": {model: ":missing"}}});
|
||||
}
|
||||
}
|
||||
|
||||
getFallback(id) {
|
||||
return new Blockstate({variants: {"": {model: ":missing"}}});
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateWhen(properties, condition) {
|
||||
|
||||
64
cache.js
64
cache.js
@@ -1,31 +1,67 @@
|
||||
// cache.js
|
||||
|
||||
export class Cache {
|
||||
constructor(root) {
|
||||
constructor(root, onResolve) {
|
||||
this.root = root;
|
||||
this.onResolve = onResolve || (() => {
|
||||
});
|
||||
|
||||
this.handlers = new Map();
|
||||
this.promises = new Map();
|
||||
this.data = new Map();
|
||||
}
|
||||
|
||||
register(kind, handler) {
|
||||
this.handlers.set(kind, handler);
|
||||
}
|
||||
|
||||
get(id, kind) {
|
||||
// Used by internal loaders (like models loading parent models)
|
||||
getAsync(id, kind) {
|
||||
this.getSync(id, kind); // Triggers the load if it hasn't started
|
||||
|
||||
const [namespace, resource] = id.includes(':') ? id.split(':', 2) : ["minecraft", id];
|
||||
id = `${namespace}:${resource}`;
|
||||
const url = `${this.root}/${namespace}/${kind}/${resource}`;
|
||||
|
||||
const key = `${kind}:${namespace}:${resource}`;
|
||||
if (this.promises.has(key)) return this.promises.get(key);
|
||||
|
||||
const handler = this.handlers.get(kind);
|
||||
if (!handler) throw new Error(`No handler registered for resource kind: ${kind}`);
|
||||
|
||||
if (handler.prepare) {
|
||||
handler.prepare(this, id, url);
|
||||
if (this.promises.has(key)) {
|
||||
return this.promises.get(key);
|
||||
}
|
||||
|
||||
const promise = handler.process(this, id, url);
|
||||
this.promises.set(key, promise);
|
||||
return promise;
|
||||
return Promise.resolve(this.data.get(key));
|
||||
}
|
||||
|
||||
// Used by the Engine for immediate, non-blocking rendering
|
||||
getSync(id, kind) {
|
||||
const [namespace, resource] = id.includes(':') ? id.split(':', 2) : ["minecraft", id];
|
||||
const normalizedId = `${namespace}:${resource}`;
|
||||
const key = `${kind}:${normalizedId}`;
|
||||
const url = `${this.root}/${namespace}/${kind}/${resource}`;
|
||||
|
||||
// 1. If we already have the real data, return it instantly
|
||||
if (this.data.has(key)) return this.data.get(key);
|
||||
|
||||
// 2. If we haven't even started loading it yet, kick off the fetch
|
||||
if (!this.promises.has(key)) {
|
||||
const handler = this.handlers.get(kind);
|
||||
if (!handler) throw new Error(`No handler registered for resource kind: ${kind}`);
|
||||
|
||||
if (handler.prepare) handler.prepare(this, normalizedId, url);
|
||||
|
||||
const promise = handler.process(this, normalizedId, url).then(result => {
|
||||
this.data.set(key, result);
|
||||
this.onResolve(); // Ping the engine to re-render!
|
||||
return result;
|
||||
}).catch(err => {
|
||||
console.warn(`Failed to load ${key}:`, err);
|
||||
const fallback = handler.getFallback(normalizedId);
|
||||
this.data.set(key, fallback);
|
||||
this.onResolve();
|
||||
return fallback;
|
||||
});
|
||||
|
||||
this.promises.set(key, promise);
|
||||
}
|
||||
|
||||
// 3. Always return the synchronous fallback while the promise resolves in the background
|
||||
return this.handlers.get(kind).getFallback(normalizedId);
|
||||
}
|
||||
}
|
||||
72
engine.js
72
engine.js
@@ -97,7 +97,11 @@ export class Engine {
|
||||
|
||||
this.atlasTexture = gl.createTexture();
|
||||
|
||||
this.cache = new Cache('assets');
|
||||
this.cache = new Cache('assets', () => {
|
||||
for (const frame of this.observedFrames) this.dirtyFrames.add(frame);
|
||||
for (const diorama of this.dioramas) diorama.dirty = true;
|
||||
this.requestUpdate();
|
||||
});
|
||||
this.atlas = new TextureHandler(256);
|
||||
this.cache.register('blockstates', new BlockstateHandler());
|
||||
this.cache.register('models', new ModelHandler());
|
||||
@@ -120,7 +124,7 @@ export class Engine {
|
||||
if (!this.updateRequested) {
|
||||
this.updateRequested = true;
|
||||
Promise.resolve().then(async () => {
|
||||
await this.updateAll();
|
||||
this.updateAll();
|
||||
this.updateRequested = false;
|
||||
});
|
||||
}
|
||||
@@ -142,11 +146,12 @@ export class Engine {
|
||||
|
||||
if (!this.observedFrames.has(diorama.frame)) {
|
||||
this.observedFrames.add(diorama.frame);
|
||||
this.dirtyFrames.add(diorama.frame); // Force initial build
|
||||
this.dirtyFrames.add(diorama.frame);
|
||||
|
||||
this.preloadWorld(diorama.frame.world);
|
||||
|
||||
diorama.frame.subscribe(() => {
|
||||
this.dirtyFrames.add(diorama.frame);
|
||||
// Mark any diorama observing this shared timeline as dirty
|
||||
for (const d of this.dioramas) {
|
||||
if (d.frame === diorama.frame) d.dirty = true;
|
||||
}
|
||||
@@ -157,35 +162,59 @@ export class Engine {
|
||||
}
|
||||
}
|
||||
|
||||
async updateAll() {
|
||||
async preloadWorld(world) {
|
||||
const blocks = world.getUniqueBlockStates();
|
||||
|
||||
// 1. Await all Blockstates
|
||||
const uniqueIds = new Set(blocks.map(b => b.id));
|
||||
await Promise.all(Array.from(uniqueIds).map(id => this.cache.getAsync(id, 'blockstates')));
|
||||
|
||||
// 2. Resolve permutations and await all Models
|
||||
const uniqueModels = new Set();
|
||||
for (const block of blocks) {
|
||||
const stateDef = this.cache.getSync(block.id, 'blockstates');
|
||||
const parts = stateDef.resolveParts(block.state);
|
||||
for (const p of parts) uniqueModels.add(p.model);
|
||||
}
|
||||
await Promise.all(Array.from(uniqueModels).map(id => this.cache.getAsync(id, 'models')));
|
||||
|
||||
// 3. Scan geometry and await all Textures
|
||||
const textureTasks = [];
|
||||
for (const modelId of uniqueModels) {
|
||||
const blockModel = this.cache.getSync(modelId, 'models');
|
||||
for (const el of blockModel.elements) {
|
||||
for (const face of Object.values(el.faces || {})) {
|
||||
const texPath = blockModel.resolveTexture(face.texture);
|
||||
if (texPath && texPath !== ':missing') {
|
||||
textureTasks.push(this.cache.getAsync(texPath, 'textures'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(textureTasks);
|
||||
}
|
||||
|
||||
updateAll() {
|
||||
if (this.dirtyFrames.size === 0) return;
|
||||
|
||||
// Isolate only the frames that mutated
|
||||
const framesToUpdate = Array.from(this.dirtyFrames);
|
||||
this.dirtyFrames.clear();
|
||||
|
||||
const uniqueBlockIds = new Set();
|
||||
for (const frame of framesToUpdate) {
|
||||
for (const block of frame.blocks.values()) uniqueBlockIds.add(block.id);
|
||||
}
|
||||
await Promise.all(Array.from(uniqueBlockIds).map(id => this.cache.get(id, 'blockstates')));
|
||||
|
||||
const parsedFrames = new Map();
|
||||
const uniqueModelIds = new Set();
|
||||
|
||||
for (const frame of framesToUpdate) {
|
||||
const parsedBlocks = [];
|
||||
for (const block of frame.blocks.values()) {
|
||||
const state = await this.cache.get(block.id, 'blockstates');
|
||||
// Sync grab (returns fallback instantly if loading)
|
||||
const state = this.cache.getSync(block.id, 'blockstates');
|
||||
const parts = state.resolveParts(block.state);
|
||||
parsedBlocks.push({block, parts});
|
||||
for (const p of parts) uniqueModelIds.add(p.model);
|
||||
}
|
||||
parsedFrames.set(frame, parsedBlocks);
|
||||
}
|
||||
await Promise.all(Array.from(uniqueModelIds).map(id => this.cache.get(id, 'models')));
|
||||
|
||||
const textureTasks = [];
|
||||
const framePools = new Map();
|
||||
|
||||
for (const frame of framesToUpdate) {
|
||||
const instancePool = new Map();
|
||||
for (const {block, parts} of parsedFrames.get(frame)) {
|
||||
@@ -210,12 +239,12 @@ export class Engine {
|
||||
}
|
||||
|
||||
for (const pool of instancePool.values()) {
|
||||
const blockModel = await this.cache.get(pool.partDef.model, 'models');
|
||||
const blockModel = this.cache.getSync(pool.partDef.model, 'models');
|
||||
for (const el of blockModel.elements) {
|
||||
for (const face of Object.values(el.faces || {})) {
|
||||
const texPath = blockModel.resolveTexture(face.texture);
|
||||
if (texPath) {
|
||||
textureTasks.push(this.cache.get(texPath, 'textures', 'png'));
|
||||
this.cache.getSync(texPath, 'textures');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,11 +252,9 @@ export class Engine {
|
||||
framePools.set(frame, instancePool);
|
||||
}
|
||||
|
||||
await Promise.all(textureTasks);
|
||||
this.updateAtlasTexture();
|
||||
|
||||
for (const frame of framesToUpdate) {
|
||||
// Memory Cleanup: Free old buffers from the GPU before creating new ones
|
||||
const oldMeshes = this.frameMeshes.get(frame);
|
||||
if (oldMeshes) {
|
||||
for (const mesh of oldMeshes) {
|
||||
@@ -238,7 +265,7 @@ export class Engine {
|
||||
|
||||
const newMeshes = [];
|
||||
for (const pool of framePools.get(frame).values()) {
|
||||
const blockModel = await this.cache.get(pool.partDef.model, 'models');
|
||||
const blockModel = this.cache.getSync(pool.partDef.model, 'models');
|
||||
const geometry = blockModel.buildGeometry(this.atlas.uvmap, pool.partDef);
|
||||
|
||||
const matrixArray = new Float32Array(pool.matrices);
|
||||
@@ -250,7 +277,6 @@ export class Engine {
|
||||
this.frameMeshes.set(frame, newMeshes);
|
||||
}
|
||||
|
||||
// Guarantee that dioramas relying on these newly built meshes are flagged for drawing
|
||||
for (const d of this.dioramas) {
|
||||
if (framesToUpdate.includes(d.frame)) d.dirty = true;
|
||||
}
|
||||
|
||||
6
model.js
6
model.js
@@ -28,7 +28,7 @@ export class ModelHandler {
|
||||
const json = await res.json();
|
||||
|
||||
if (json.parent) {
|
||||
const parent = await cache.get(json.parent, 'models');
|
||||
const parent = await cache.getAsync(json.parent, 'models');
|
||||
json.textures = {...parent.textures, ...json.textures};
|
||||
if (!json.elements && parent.elements) {
|
||||
json.elements = parent.elements;
|
||||
@@ -40,6 +40,10 @@ export class ModelHandler {
|
||||
return new BlockModel(MISSING_MODEL_JSON);
|
||||
}
|
||||
}
|
||||
|
||||
getFallback(id) {
|
||||
return new BlockModel(MISSING_MODEL_JSON);
|
||||
}
|
||||
}
|
||||
|
||||
const CUBOID_FACES = {
|
||||
|
||||
43
texture.js
43
texture.js
@@ -38,6 +38,13 @@ export class TextureHandler {
|
||||
|
||||
this.uvmap.set(id, uvData);
|
||||
this.pendingDraws.set(id, {x: currentX, y: currentY});
|
||||
|
||||
this.ctx.fillStyle = '#00000066';
|
||||
this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize);
|
||||
}
|
||||
|
||||
getFallback(id) {
|
||||
return this.uvmap.get(id); // UVs map to the black silhouette!
|
||||
}
|
||||
|
||||
async process(cache, id, url) {
|
||||
@@ -47,31 +54,31 @@ export class TextureHandler {
|
||||
|
||||
const drawMissing = () => {
|
||||
const half = this.cellSize / 2;
|
||||
this.ctx.fillStyle = '#ff00ff'; // Magenta
|
||||
this.ctx.fillStyle = '#33333366'; // Gray
|
||||
this.ctx.fillRect(pos.x, pos.y, half, half);
|
||||
this.ctx.fillRect(pos.x + half, pos.y + half, half, half);
|
||||
this.ctx.fillStyle = '#000000'; // Black
|
||||
this.ctx.fillStyle = '#00000066'; // Black
|
||||
this.ctx.fillRect(pos.x + half, pos.y, half, half);
|
||||
this.ctx.fillRect(pos.x, pos.y + half, half, half);
|
||||
};
|
||||
|
||||
if (id === ':missing') {
|
||||
drawMissing();
|
||||
return this.uvmap.get(id);
|
||||
}
|
||||
drawMissing();
|
||||
|
||||
try {
|
||||
const img = await new Promise((resolve, reject) => {
|
||||
const i = new Image();
|
||||
i.crossOrigin = 'anonymous';
|
||||
i.onload = () => resolve(i);
|
||||
i.onerror = () => reject(new Error(`Image load failed`));
|
||||
i.src = url + '.png';
|
||||
});
|
||||
this.ctx.drawImage(img, pos.x, pos.y, this.cellSize, this.cellSize);
|
||||
} catch (e) {
|
||||
console.warn(`Missing texture: ${id}`);
|
||||
drawMissing();
|
||||
if (id !== ':missing') {
|
||||
try {
|
||||
const img = await new Promise((resolve, reject) => {
|
||||
const i = new Image();
|
||||
i.crossOrigin = 'anonymous';
|
||||
i.onload = () => resolve(i);
|
||||
i.onerror = () => reject(new Error(`Image load failed`));
|
||||
i.src = url + '.png';
|
||||
});
|
||||
// await new Promise(r => setTimeout(r, 2000))
|
||||
this.ctx.clearRect(pos.x, pos.y, this.cellSize, this.cellSize);
|
||||
this.ctx.drawImage(img, pos.x, pos.y, this.cellSize, this.cellSize);
|
||||
} catch (e) {
|
||||
console.warn(`Missing texture: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return this.uvmap.get(id);
|
||||
|
||||
27
world.js
27
world.js
@@ -123,6 +123,33 @@ export class World {
|
||||
|
||||
if (!capturedInitialState) captureInitial();
|
||||
}
|
||||
|
||||
getUniqueBlockStates() {
|
||||
const unique = new Map();
|
||||
const add = (block) => {
|
||||
if (!block || !block.id || block.id === 'air' || block.id === 'minecraft:air') return;
|
||||
|
||||
// Create a stable hash for the state object so we don't duplicate requests
|
||||
const stateHash = Object.entries(block.state)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(e => `${e[0]}=${e[1]}`)
|
||||
.join(',');
|
||||
const hash = `${block.id}[${stateHash}]`;
|
||||
|
||||
if (!unique.has(hash)) {
|
||||
unique.set(hash, {id: block.id, state: {...block.state}});
|
||||
}
|
||||
};
|
||||
|
||||
for (const block of this.initialState.values()) add(block);
|
||||
for (const ev of this.events) {
|
||||
for (const action of ev.actions) {
|
||||
add(action.prev);
|
||||
add(action.next);
|
||||
}
|
||||
}
|
||||
return Array.from(unique.values());
|
||||
}
|
||||
}
|
||||
|
||||
export class WorldFrame {
|
||||
|
||||
Reference in New Issue
Block a user