multi target renderer instead of background scissor
This commit is contained in:
63
diorama.js
63
diorama.js
@@ -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};
|
||||
|
||||
75
engine.js
75
engine.js
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
index.html
68
index.html
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user