unify timeline, world. shadeless schematics.

This commit is contained in:
2026-07-05 17:18:11 -04:00
parent 53a4a5b9c5
commit 1759e89b3b
3 changed files with 265 additions and 186 deletions

View File

@@ -17,6 +17,7 @@ layout(location=9) in vec3 i_color;
uniform mat4 u_viewProj;
uniform vec3 u_lightDir;
uniform vec3 u_viewDir;
uniform vec3 u_upDir;
out vec2 v_uv;
out float v_light;
@@ -32,6 +33,15 @@ void main() {
float head = max(dot(normal, normalize(u_viewDir)), 0.0);
float baseLight = clamp(sun + head * 0.3, 0.0, 1.0) * 0.6 + 0.4;
// Strict Schematic Detection: Both vectors must be perfectly axis-aligned
vec3 absV = abs(u_viewDir);
vec3 absU = abs(u_upDir);
bool isSchematic = (absV.x + absV.y + absV.z == 1.0) && (absU.x + absU.y + absU.z == 1.0);
if (isSchematic && head == 1.0) {
baseLight = 1.0;
}
v_light = mix(1.0, baseLight, a_shade);
v_color = mix(vec3(1.0), i_color, a_tint);
}
@@ -81,6 +91,7 @@ export class Engine {
viewProj: gl.getUniformLocation(this.program, "u_viewProj"),
lightDir: gl.getUniformLocation(this.program, "u_lightDir"),
viewDir: gl.getUniformLocation(this.program, "u_viewDir"),
upDir: gl.getUniformLocation(this.program, "u_upDir"),
texture: gl.getUniformLocation(this.program, "u_texture")
};
@@ -306,8 +317,9 @@ export class Engine {
const viewProj = diorama.updateMatrices(aspect);
gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj);
gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, 2.0);
gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, -2.0);
gl.uniform3f(this.uniforms.viewDir, diorama.viewDir[0], diorama.viewDir[1], diorama.viewDir[2]);
gl.uniform3f(this.uniforms.upDir, diorama.upDir[0], diorama.upDir[1], diorama.upDir[2]);
const meshes = this.worldMeshes.get(diorama.world) || [];
for (const mesh of meshes) {

View File

@@ -32,95 +32,16 @@
<main>
<h1>Visualizer</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed
tempor. Nunc
vehicula tempor purus, non vestibulum libero ornare non. Morbi fringilla sapien diam, sed
rhoncus lacus egestas
vel.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed
tempor. Nunc
vehicula tempor purus, non vestibulum libero ornare non. Morbi fringilla sapien diam, sed
rhoncus lacus egestas
vel.
</p>
<h2>Static</h2>
<div class="figure" id="demo-1"></div>
<p>
Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
aliquam tellus,
vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
ligula eu maximus.
Mauris ullamcorper accumsan dui vel pulvinar.
</p>
<p>
Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
aliquam tellus,
vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
ligula eu maximus.
Mauris ullamcorper accumsan dui vel pulvinar.
</p>
<p>
Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
aliquam tellus,
vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
ligula eu maximus.
Mauris ullamcorper accumsan dui vel pulvinar.
</p>
<p>
Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
aliquam tellus,
vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
ligula eu maximus.
Mauris ullamcorper accumsan dui vel pulvinar.
</p>
<p>
Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
aliquam tellus,
vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
ligula eu maximus.
Mauris ullamcorper accumsan dui vel pulvinar.
</p>
<p>
Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis
aliquam tellus,
vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar
ligula eu maximus.
Mauris ullamcorper accumsan dui vel pulvinar.
</p>
<h2>Dynamic</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr;">
<div class="figure" id="demo-2"></div>
<div class="figure" id="demo-3"></div>
</div>
<p>
Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
penatibus et magnis
dis parturient montes, nascetur ridiculus mus.
</p>
<p>
Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
penatibus et magnis
dis parturient montes, nascetur ridiculus mus.
</p>
<p>
Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque
penatibus et magnis
dis parturient montes, nascetur ridiculus mus.
</p>
<div id="atlas-container"
style="position: fixed; left: 0; top: 0; border: 1px solid white;"></div>
</main>
@@ -135,61 +56,50 @@
const engine = new Engine();
document.getElementById('atlas-container')?.appendChild(engine.atlas.canvas);
const world1 = new World();
world1.set('minecraft:redstone_wire', -1, 0, 0, {
east: 'side',
west: 'none',
north: 'none',
south: 'none',
power: '15'
});
world1.set('minecraft:repeater', 0, 0, 0, {
facing: 'east',
delay: '1',
locked: 'false',
powered: 'true'
});
world1.set('minecraft:oak_trapdoor', 2, 0, 0, {
facing: 'east',
half: 'bottom',
open: 'true'
});
world1.set('lodestone', 0, -1, 0);
world1.set('piston', 0, -2, 0, {facing: 'north', extended: 'false'});
const world1 = new World(`
p -1 0 0 redstone_wire east=side west=none north=none south=none power=15
p 0 0 0 repeater facing=east delay=1 locked=false powered=true
p 2 0 0 oak_trapdoor facing=east half=bottom open=true
p 0 -1 0 lodestone
p 0 -2 0 piston facing=north extended=false
`)
const world2 = new World();
world2.set('minecraft:redstone_wire', 0, 0, 0, {
east: 'side',
west: 'none',
north: 'none',
south: 'none',
power: '0'
});
world2.set('minecraft:repeater', 1, 0, 0, {
facing: 'east',
delay: '4',
locked: 'false',
powered: 'false'
});
world2.set('minecraft:sticky_piston', 2, 0, 0, {facing: 'east', extended: 'false'});
world2.set('minecraft:sticky_piston', 2, 0, 1, {facing: 'east', extended: 'true'});
world2.set('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'});
world2.set('minecraft:piston_head', 2.5, 0, 1, {
facing: 'east',
short: 'true',
type: 'sticky'
});
world2.set('minecraft:piston_head', 3, 0, 2, {
facing: 'east',
short: 'false',
type: 'sticky'
});
world2.set('minecraft:glass', 3, 0, 0);
world2.set('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'});
world2.set('minecraft:redstone_block', 4, 0, 2);
const world2 = new World(`
p 0 0 0 redstone_wire power=0 east=side west=none north=none south=none
p 1 0 0 repeater facing=west powered=false locked=false delay=2
p 2 0 0 piston facing=east extended=false
t 2
p 0 0 0 power=15
t 6
p 1 0 0 powered=true
t 7
p 2 0 0 extended=true
p 2.5 0 0 piston_head facing=east short=true type=normal
t 8
p 2.5 0 0 air
p 3 0 0 piston_head facing=east short=false type=normal
t 12
p 0 0 0 power=0
t 16
p 1 0 0 powered=false
t 17
p 3 0 0 air
p 2.5 0 0 piston_head facing=east short=true type=normal
t 18
p 2.5 0 0 air
p 2 0 0 extended=false
`);
const demo1 = new Diorama('demo-1', world1, () => engine.requestRender())
demo1.radius = 2;
demo1.radius = 3;
demo1.phi = 10;
demo1.theta = -5;
demo1.target = [0.5, 0, 0.5]
@@ -197,7 +107,7 @@
engine.addDiorama(demo1);
const demo2 = new Diorama('demo-2', world2, () => engine.requestRender())
demo2.radius = 4;
demo2.radius = 5;
demo2.theta = 180;
demo2.phi = 90;
demo2.centerView()
@@ -205,9 +115,7 @@
engine.addDiorama(demo2);
const demo3 = new Diorama('demo-3', world2, () => engine.requestRender())
demo3.radius = 4;
demo3.theta = 0;
demo3.phi = 0;
demo3.radius = 5;
demo3.centerView()
demo3.saveState()
engine.addDiorama(demo3);
@@ -215,51 +123,21 @@
// Fetch and build everything concurrently!
await engine.updateAll();
function set0() {
world2.set('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'false'});
world2.set('minecraft:redstone_block', 3, 0, 2);
world2.del(2.5, 0, 2);
world2.del(3.5, 0, 2);
setTimeout(set1, 250)
}
const factor = 1;
setInterval(() => {
for (let i = 0; i < 20; i++) {
setTimeout(() => world2.seek(i), i * factor * 50)
}
}, factor * 1000)
function set1() {
world2.set('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'});
world2.del(3, 0, 2)
world2.set('minecraft:piston_head', 2.5, 0, 2, {
facing: 'east',
short: 'true',
type: 'sticky'
});
world2.set('minecraft:redstone_block', 3.5, 0, 2);
setTimeout(set2, 250)
}
// let i = 0;
// let n = world2.eventCount;
// setInterval(() => {
// world2.seekIndex(i);
// i = (i + 1) % n;
// }, 500);
function set2() {
world2.set('minecraft:piston_head', 3, 0, 2, {
facing: 'east',
short: 'false',
type: 'sticky'
});
world2.del(2.5, 0, 2);
world2.del(3.5, 0, 2);
world2.set('minecraft:redstone_block', 4, 0, 2);
setTimeout(set3, 250)
}
function set3() {
world2.del(4, 0, 2)
world2.del(3, 0, 2)
world2.set('minecraft:piston_head', 2.5, 0, 2, {
facing: 'east',
short: 'true',
type: 'sticky'
});
world2.set('minecraft:redstone_block', 3.5, 0, 2);
setTimeout(set0, 250)
}
setTimeout(set3, 1000)
} catch (err) {
console.error("Renderer Initialization Failed:", err);
}

201
world.js
View File

@@ -7,9 +7,25 @@ export class Block {
}
export class World {
constructor() {
constructor(script = '') {
this.blocks = new Map();
this.listeners = new Set();
// Timeline State
this.events = [];
this.labels = new Map();
this.currentIndex = 0;
this.currentTime = -Infinity;
this.initialState = new Map();
this.initialIndex = 0;
this.eventCount = 0;
if (script) {
this.#compile(script);
}
}
subscribe(callback) {
@@ -23,12 +39,14 @@ export class World {
set(id, x, y, z, state = {}) {
const key = `${x},${y},${z}`;
this.blocks.set(key, new Block(id, [x, y, z], state));
this.notify();
}
const isAir = id === 'air' || id === 'minecraft:air';
del(x, y, z) {
if (this.blocks.delete(`${x},${y},${z}`)) {
if (isAir) {
if (this.blocks.delete(key)) {
this.notify();
}
} else {
this.blocks.set(key, new Block(id, [x, y, z], state));
this.notify();
}
}
@@ -36,4 +54,175 @@ export class World {
get(x, y, z) {
return this.blocks.get(`${x},${y},${z}`);
}
#compile(script) {
let currentTime = -1;
let subTick = 0;
let capturedInitialState = false;
const shadowWorld = new Map();
const captureInitial = () => {
if (capturedInitialState) return;
for (const [k, v] of shadowWorld.entries()) {
this.initialState.set(k, {id: v.id, pos: [...v.pos], state: {...v.state}});
}
this.initialIndex = this.events.length;
capturedInitialState = true;
};
const lines = script.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
for (const line of lines) {
const tokens = line.split(/\s+/);
const cmd = tokens[0];
if (cmd === 't') {
const newTime = parseFloat(tokens[1]);
// If we are crossing the tick 0 threshold, lock in the initial state
if (newTime >= 0 && !capturedInitialState) captureInitial();
currentTime = newTime;
subTick = 0;
continue;
}
if (cmd === 'l') {
this.labels.set(tokens[1], currentTime);
continue;
}
if (cmd === 'p') {
const x = parseFloat(tokens[1]);
const y = parseFloat(tokens[2]);
const z = parseFloat(tokens[3]);
const key = `${x},${y},${z}`;
let id = null;
const newState = {};
for (let i = 4; i < tokens.length; i++) {
const token = tokens[i];
if (token.includes('=')) {
const [k, v] = token.split('=');
newState[k] = v;
} else {
id = token;
}
}
const prevBlock = shadowWorld.get(key);
const prev = prevBlock ? {id: prevBlock.id, state: {...prevBlock.state}} : null;
const isAir = id === 'air' || id === 'minecraft:air';
if (isAir) {
shadowWorld.delete(key);
} else if (id) {
shadowWorld.set(key, {id, pos: [x, y, z], state: newState});
} else if (prevBlock) {
Object.assign(prevBlock.state, newState);
} else {
console.warn(`Timeline warning: Attempted to update state of non-existent block at ${key}`);
continue;
}
const nextBlock = shadowWorld.get(key);
const next = nextBlock ? {id: nextBlock.id, state: {...nextBlock.state}} : null;
this.events.push({
time: currentTime,
subTick: subTick++,
x, y, z,
prev,
next
});
}
}
// Fallback catch if the script ends entirely before tick 0
if (!capturedInitialState) captureInitial();
this.eventCount = this.events.length - this.initialIndex;
this.reset();
}
#apply(data, x, y, z) {
const key = `${x},${y},${z}`;
if (data) {
this.blocks.set(key, new Block(data.id, [x, y, z], {...data.state}));
} else {
this.blocks.delete(key);
}
}
seek(target) {
let targetTime = target;
if (typeof target === 'string') {
targetTime = this.labels.get(target);
if (targetTime === undefined) throw new Error(`Label not found: ${target}`);
}
// Scrub Forward
while (this.currentIndex < this.events.length && this.events[this.currentIndex].time <= targetTime) {
const ev = this.events[this.currentIndex];
this.#apply(ev.next, ev.x, ev.y, ev.z);
this.currentIndex++;
}
// Scrub Backward
while (this.currentIndex > 0 && this.events[this.currentIndex - 1].time > targetTime) {
this.currentIndex--;
const ev = this.events[this.currentIndex];
this.#apply(ev.prev, ev.x, ev.y, ev.z);
}
this.currentTime = targetTime;
this.notify(); // Dispatch a single batched update to the Engine
}
seekIndex(targetIndex) {
targetIndex = targetIndex + this.initialIndex;
while (this.currentIndex < this.events.length && this.currentIndex < targetIndex) {
const ev = this.events[this.currentIndex];
this.#apply(ev.next, ev.x, ev.y, ev.z);
this.currentIndex++;
}
while (this.currentIndex > 0 && this.currentIndex > targetIndex) {
this.currentIndex--;
const ev = this.events[this.currentIndex];
this.#apply(ev.prev, ev.x, ev.y, ev.z);
}
if (this.currentIndex < this.events.length) {
this.currentTime = this.events[this.currentIndex].time;
} else if (this.events.length > 0) {
this.currentTime = this.events[this.events.length - 1].time;
}
this.notify();
}
reset() {
this.blocks.clear();
for (const [key, block] of this.initialState.entries()) {
this.blocks.set(key, new Block(block.id, [...block.pos], {...block.state}));
}
this.currentIndex = this.initialIndex;
if (this.initialIndex > 0 && this.events.length > 0) {
this.currentTime = this.events[this.initialIndex - 1].time;
} else {
this.currentTime = -Infinity;
}
this.notify();
}
}