multi target renderer instead of background scissor

This commit is contained in:
2026-07-04 23:10:38 -04:00
parent 3ba219697e
commit f248f9ac0f
3 changed files with 154 additions and 52 deletions

View File

@@ -4,6 +4,9 @@ import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
export class Diorama {
constructor(elementId, world, requestRenderCallback) {
this.element = document.getElementById(elementId);
this.canvas = document.createElement('canvas')
this.element.appendChild(this.canvas)
this.ctx2d = this.canvas.getContext('2d'); // Lightweight 2D drawing surface
this.world = world;
this.requestRender = requestRenderCallback;
@@ -32,7 +35,9 @@ export class Diorama {
}
initUI() {
// Because the element is a canvas itself, buttons are appended to its parent container
this.element.style.position = "relative";
this.resetBtn = document.createElement('button');
this.resetBtn.textContent = 'Reset View';
this.resetBtn.style.cssText = `
@@ -48,8 +53,10 @@ export class Diorama {
this.loadState();
this.resetBtn.style.display = 'none';
});
this.element.appendChild(this.resetBtn);
this.touchTimer = null;
this.touchTimedOut = false;
}
saveState() {
@@ -128,8 +135,6 @@ export class Diorama {
return this.viewProjMatrix;
}
// --- INTERACTION MATH ---
orbit(dx, dy) {
this.theta -= dx * 0.4;
this.phi += dy * 0.4;
@@ -147,19 +152,23 @@ export class Diorama {
const upY = Math.cos(p);
const upZ = -Math.sin(p) * Math.cos(t);
const panSpeed = this.radius * 0.0025;
const rect = this.canvas.getBoundingClientRect();
const panSpeed = this.radius / rect.height;
this.target[0] += (-rightX * dx + upX * dy) * panSpeed;
this.target[1] += (upY * dy) * panSpeed;
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
}
zoom(delta) {
this.radius += delta * 0.05;
zoomRatio(ratio) {
this.radius *= ratio;
this.radius = Math.max(1, Math.min(50, this.radius));
}
// --- EVENT LISTENERS ---
zoomLinear(delta) {
this.radius += delta * 0.05;
this.radius = Math.max(1, Math.min(50, this.radius));
}
attachEvents() {
this.element.addEventListener('contextmenu', e => e.preventDefault());
@@ -199,20 +208,27 @@ export class Diorama {
if (this.dragButton === 0) this.orbit(dx, dy);
else if (this.dragButton === 1) this.pan(dx, dy);
else if (this.dragButton === 2) this.zoom(dy);
else if (this.dragButton === 2) this.zoomLinear(dy);
notifyChange();
});
// --- TOUCH EVENTS ---
this.element.addEventListener('touchstart', (e) => {
e.preventDefault();
this.isDragging = true;
if (this.touchTimer) clearTimeout(this.touchTimer);
this.touchTimedOut = false;
if (e.touches.length === 1) {
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
this.touchTimer = setTimeout(() => {
this.touchTimedOut = true;
this.isDragging = true;
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};
this.element.style.cursor = 'grabbing';
}, 200);
} else if (e.touches.length >= 2) {
this.touchTimedOut = true;
this.isDragging = true;
this.touchMode = 'pan-zoom';
const t1 = e.touches[0], t2 = e.touches[1];
this.lastMouse = {
@@ -221,11 +237,20 @@ export class Diorama {
};
this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
}
}, {passive: false});
}, {passive: true});
this.element.addEventListener('touchmove', (e) => {
if (!this.touchTimedOut) {
if (this.touchTimer) {
clearTimeout(this.touchTimer);
this.touchTimer = null;
}
this.isDragging = false;
return;
}
if (!this.isDragging) return;
e.preventDefault();
if (e.cancelable) e.preventDefault();
if (this.touchMode === 'orbit' && e.touches.length === 1) {
const dx = e.touches[0].clientX - this.lastMouse.x;
@@ -242,20 +267,26 @@ export class Diorama {
const dx = cx - this.lastMouse.x;
const dy = cy - this.lastMouse.y;
const dDist = dist - this.lastPinchDist;
const zRatio = this.lastPinchDist > 0 ? (this.lastPinchDist / dist) : 1;
this.lastMouse = {x: cx, y: cy};
this.lastPinchDist = dist;
this.pan(dx, dy);
this.zoom(-dDist);
this.zoomRatio(zRatio);
notifyChange();
}
}, {passive: false});
const onTouchEnd = (e) => {
if (this.touchTimer) {
clearTimeout(this.touchTimer);
this.touchTimer = null;
}
if (e.touches.length === 0) {
this.isDragging = false;
this.touchMode = '';
} else if (e.touches.length === 1) {
this.touchMode = 'orbit';
this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY};

View File

@@ -62,20 +62,15 @@ function compileShader(gl, type, src) {
export class Engine {
constructor() {
// One solitary hidden canvas to drive all WebGL multi-target logic
this.canvas = document.createElement('canvas');
this.canvas.style.position = 'fixed';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.canvas.style.width = '100vw';
this.canvas.style.height = '100vh';
this.canvas.style.zIndex = '-1';
this.canvas.style.display = 'none';
document.body.appendChild(this.canvas);
this.gl = this.canvas.getContext('webgl2', {antialias: true, alpha: true});
const gl = this.gl;
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.enable(gl.SCISSOR_TEST);
const vs = compileShader(gl, gl.VERTEX_SHADER, VS_SRC);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS_SRC);
@@ -93,7 +88,6 @@ export class Engine {
this.atlasTexture = gl.createTexture();
// --- Initialize the new Resource Pipeline ---
this.cache = new Cache('assets');
this.atlas = new TextureHandler(256);
this.cache.register('blockstates', new BlockstateHandler());
@@ -103,22 +97,18 @@ export class Engine {
this.dioramas = [];
this.worldMeshes = new Map();
this.renderRequested = false;
this.observedWorlds = new Set();
this.updateRequested = false;
window.addEventListener('resize', () => {
this.resize();
this.requestRender();
});
window.addEventListener('scroll', () => this.requestRender(), {passive: true});
this.resize();
window.addEventListener('resize', () => this.requestRender());
// to make sure offscreen dioramas are rendered as soon as they become visible
window.addEventListener('scroll', () => this.requestRender());
}
requestUpdate() {
if (!this.updateRequested) {
this.updateRequested = true;
// Add 'async' here and 'await' updateAll
Promise.resolve().then(async () => {
await this.updateAll();
this.updateRequested = false;
@@ -136,14 +126,9 @@ export class Engine {
}
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
addDiorama(diorama) {
this.dioramas.push(diorama);
if (!this.observedWorlds.has(diorama.worlds)) {
if (!this.observedWorlds.has(diorama.world)) {
this.observedWorlds.add(diorama.world);
diorama.world.subscribe(() => this.requestUpdate());
}
@@ -153,14 +138,12 @@ export class Engine {
const uniqueWorlds = new Set(this.dioramas.map(d => d.world));
if (uniqueWorlds.size === 0) return;
// --- SWEEP 1: Collect & Fetch Blockstates ---
const uniqueBlockIds = new Set();
for (const world of uniqueWorlds) {
for (const block of world.blocks.values()) uniqueBlockIds.add(block.id);
}
await Promise.all(Array.from(uniqueBlockIds).map(id => this.cache.get(id, 'blockstates')));
// --- SWEEP 2: Resolve Parts & Fetch Models ---
const uniqueModelIds = new Set();
const parsedWorlds = new Map();
@@ -176,7 +159,6 @@ export class Engine {
}
await Promise.all(Array.from(uniqueModelIds).map(id => this.cache.get(id, 'models')));
// --- SWEEP 3: Pool Instances & Fetch Textures ---
const textureTasks = [];
const worldPools = new Map();
@@ -209,7 +191,6 @@ export class Engine {
for (const face of Object.values(el.faces || {})) {
const texPath = blockModel.resolveTexture(face.texture);
if (texPath) {
// Synchronously allocates UV, asynchronously fetches image
textureTasks.push(this.cache.get(texPath, 'textures', 'png'));
}
}
@@ -221,7 +202,6 @@ export class Engine {
await Promise.all(textureTasks);
this.updateAtlasTexture();
// --- SWEEP 4: Bake Geometry ---
for (const world of uniqueWorlds) {
const newMeshes = [];
for (const pool of worldPools.get(world).values()) {
@@ -295,23 +275,39 @@ export class Engine {
render() {
const gl = this.gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.scissor(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(this.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
gl.uniform1i(this.uniforms.texture, 0);
// High-Performance Multi-Target Loop
for (const diorama of this.dioramas) {
const rect = diorama.element.getBoundingClientRect();
if (rect.bottom < 0 || rect.top > gl.canvas.height || rect.right < 0 || rect.left > gl.canvas.width) continue;
const rect = diorama.canvas.getBoundingClientRect();
const bottom = gl.canvas.height - rect.bottom;
gl.viewport(rect.left, bottom, rect.width, rect.height);
gl.scissor(rect.left, bottom, rect.width, rect.height);
// Fast Frustum Culling via native DOM properties
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) continue;
// Sync layout device-pixel ratios explicitly to prevent jagged texturing
const dpr = window.devicePixelRatio || 1;
const targetW = Math.floor(rect.width * dpr);
const targetH = Math.floor(rect.height * dpr);
// Guard: Only update diorama canvas internal buffers if physical dimensions shifted
if (diorama.canvas.width !== targetW || diorama.canvas.height !== targetH) {
diorama.canvas.width = targetW;
diorama.canvas.height = targetH;
}
// Apply size changes to hidden canvas ONLY when growing
if (this.canvas.width < targetW || this.canvas.height < targetH) {
this.canvas.width = targetW;
this.canvas.height = targetH;
}
gl.viewport(0, 0, targetW, targetH);
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = rect.width / rect.height;
const viewProj = diorama.updateMatrices(aspect);
@@ -325,6 +321,13 @@ export class Engine {
gl.bindVertexArray(mesh.vao);
gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount);
}
diorama.ctx2d.clearRect(0, 0, targetW, targetH);
diorama.ctx2d.drawImage(
this.canvas,
0, 0, targetW, targetH, // Source sub-rect coordinates from WebGL
0, 0, targetW, targetH // Destination coordinates on 2D surface
);
}
}
}

View File

@@ -16,14 +16,30 @@
}
.figure {
position: relative;
width: 100%;
height: 20em;
}
.figure > canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<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
@@ -42,6 +58,46 @@
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>
<div style="display: grid; grid-template-columns: 1fr 1fr;">
<div class="figure" id="demo-2"></div>
<div class="figure" id="demo-3"></div>
@@ -53,6 +109,18 @@
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>