multiview. camera controls
This commit is contained in:
24
assets.js
24
assets.js
@@ -54,6 +54,7 @@ export class TextureAtlas {
|
||||
|
||||
this.map = new Map();
|
||||
this.cellSize = 16;
|
||||
this.padding = 2; // Add empty space between textures
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.rowHeight = 0;
|
||||
@@ -64,10 +65,10 @@ export class TextureAtlas {
|
||||
|
||||
const url = resolveResourceLocation(id, 'textures', 'png');
|
||||
|
||||
// Calculate position before trying to load, so we can draw a fallback if it fails
|
||||
// Include padding in the wrap calculation
|
||||
if (this.x + this.cellSize > this.canvas.width) {
|
||||
this.x = 0;
|
||||
this.y += this.rowHeight;
|
||||
this.y += this.rowHeight + this.padding;
|
||||
this.rowHeight = 0;
|
||||
}
|
||||
|
||||
@@ -86,27 +87,22 @@ export class TextureAtlas {
|
||||
this.ctx.drawImage(img, currentX, currentY, this.cellSize, this.cellSize);
|
||||
} catch (e) {
|
||||
console.warn(`Texture missing: ${id}`);
|
||||
// Draw magenta square ONLY for missing textures
|
||||
this.ctx.fillStyle = '#ff00ff';
|
||||
this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize);
|
||||
}
|
||||
|
||||
const epsU = 0.1 / this.canvas.width;
|
||||
const epsV = 0.1 / this.canvas.height;
|
||||
|
||||
// Clean UV mapping (padding protects the edges natively)
|
||||
const uvData = {
|
||||
// Push the start coordinate slightly inward
|
||||
u: (currentX / this.canvas.width) + epsU,
|
||||
v: (currentY / this.canvas.height) + epsV,
|
||||
// Shrink the total width/height to account for the inset on both sides
|
||||
du: (this.cellSize / this.canvas.width) - (epsU * 2),
|
||||
dv: (this.cellSize / this.canvas.height) - (epsV * 2)
|
||||
u: currentX / this.canvas.width,
|
||||
v: currentY / this.canvas.height,
|
||||
du: this.cellSize / this.canvas.width,
|
||||
dv: this.cellSize / this.canvas.height
|
||||
};
|
||||
|
||||
this.map.set(id, uvData);
|
||||
this.map.set(id, uvData);
|
||||
|
||||
this.x += this.cellSize;
|
||||
// Advance X by cell size AND padding
|
||||
this.x += this.cellSize + this.padding;
|
||||
this.rowHeight = Math.max(this.rowHeight, this.cellSize);
|
||||
|
||||
return uvData;
|
||||
|
||||
116
camera.js
Normal file
116
camera.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// camera.js
|
||||
import {mat4Ortho, mat4LookAt, mat4Multiply} from './math.js';
|
||||
|
||||
export class Camera {
|
||||
constructor(element, onChangeCallback) {
|
||||
this.element = element;
|
||||
this.onChange = onChangeCallback;
|
||||
|
||||
this.target = [0.5, 0, 0.5];
|
||||
this.radius = 4;
|
||||
this.theta = 135;
|
||||
this.phi = 30;
|
||||
|
||||
this.projMatrix = new Float32Array(16);
|
||||
this.viewMatrix = new Float32Array(16);
|
||||
this.viewProjMatrix = new Float32Array(16);
|
||||
|
||||
this.isDragging = false;
|
||||
this.dragButton = 0; // 0: Left, 1: Middle, 2: Right
|
||||
this.lastMouse = {x: 0, y: 0};
|
||||
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
// Prevent the browser context menu on right-click
|
||||
this.element.addEventListener('contextmenu', e => e.preventDefault());
|
||||
|
||||
this.element.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
this.isDragging = true;
|
||||
this.dragButton = e.button;
|
||||
this.lastMouse = {x: e.clientX, y: e.clientY};
|
||||
|
||||
// Visual feedback based on action
|
||||
if (this.dragButton === 1) this.element.style.cursor = 'move';
|
||||
else if (this.dragButton === 2) this.element.style.cursor = 'ns-resize';
|
||||
else this.element.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', () => {
|
||||
this.isDragging = false;
|
||||
this.element.style.cursor = 'grab';
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDragging) return;
|
||||
const dx = e.clientX - this.lastMouse.x;
|
||||
const dy = e.clientY - this.lastMouse.y;
|
||||
this.lastMouse = {x: e.clientX, y: e.clientY};
|
||||
|
||||
if (this.dragButton === 0) {
|
||||
// LEFT CLICK: Orbit
|
||||
this.theta -= dx * 0.4;
|
||||
this.phi += dy * 0.4;
|
||||
this.phi = Math.max(-90, Math.min(90, this.phi));
|
||||
} else if (this.dragButton === 1) {
|
||||
// MIDDLE CLICK: Pan
|
||||
const t = this.theta * Math.PI / 180;
|
||||
const p = this.phi * Math.PI / 180;
|
||||
|
||||
// Camera's local Right vector mapped to world space
|
||||
const rightX = Math.cos(t);
|
||||
const rightZ = -Math.sin(t);
|
||||
|
||||
// Camera's local Up vector mapped to world space
|
||||
const upX = -Math.sin(p) * Math.sin(t);
|
||||
const upY = Math.cos(p);
|
||||
const upZ = -Math.sin(p) * Math.cos(t);
|
||||
|
||||
// Scale pan speed based on zoom radius so it feels consistent
|
||||
const panSpeed = this.radius * 0.0025;
|
||||
|
||||
this.target[0] += (-rightX * dx + upX * dy) * panSpeed;
|
||||
this.target[1] += (upY * dy) * panSpeed;
|
||||
this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed;
|
||||
} else if (this.dragButton === 2) {
|
||||
// RIGHT CLICK: Zoom
|
||||
this.radius += dy * 0.05;
|
||||
this.radius = Math.max(1, Math.min(50, this.radius));
|
||||
}
|
||||
|
||||
this.onChange();
|
||||
});
|
||||
|
||||
this.element.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
this.radius += e.deltaY * 0.01;
|
||||
this.radius = Math.max(1, Math.min(50, this.radius));
|
||||
this.onChange();
|
||||
});
|
||||
}
|
||||
|
||||
updateMatrices(aspectRatio) {
|
||||
const t = this.theta * Math.PI / 180;
|
||||
const p = this.phi * Math.PI / 180;
|
||||
|
||||
const eyeX = this.target[0] + this.radius * Math.cos(p) * Math.sin(t);
|
||||
const eyeY = this.target[1] + this.radius * Math.sin(p);
|
||||
const eyeZ = this.target[2] + this.radius * Math.cos(p) * Math.cos(t);
|
||||
|
||||
// Calculate dynamic Up vector to prevent gimbal lock at exactly +/- 90 degrees
|
||||
const upX = -Math.sin(p) * Math.sin(t);
|
||||
const upY = Math.cos(p);
|
||||
const upZ = -Math.sin(p) * Math.cos(t);
|
||||
|
||||
const size = this.radius * 0.5;
|
||||
mat4Ortho(this.projMatrix, -size * aspectRatio, size * aspectRatio, -size, size, -50, 50);
|
||||
|
||||
// Pass the dynamic Up vector to the LookAt matrix
|
||||
mat4LookAt(this.viewMatrix, eyeX, eyeY, eyeZ, this.target[0], this.target[1], this.target[2], upX, upY, upZ);
|
||||
mat4Multiply(this.viewProjMatrix, this.projMatrix, this.viewMatrix);
|
||||
|
||||
return this.viewProjMatrix;
|
||||
}
|
||||
}
|
||||
53
diorama.js
Normal file
53
diorama.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// diorama.js
|
||||
import { Camera } from './camera.js';
|
||||
import { World } from './world.js';
|
||||
|
||||
export class Diorama {
|
||||
constructor(elementId, engine) {
|
||||
this.element = document.getElementById(elementId);
|
||||
this.engine = engine;
|
||||
this.world = new World();
|
||||
|
||||
// Pass the engine's render request function to the camera
|
||||
this.camera = new Camera(this.element, () => this.engine.requestRender());
|
||||
|
||||
this.camera.target = [0.5, 0.5, 0.5];
|
||||
this.meshes = [];
|
||||
}
|
||||
|
||||
centerView() {
|
||||
if (this.world.blocks.size === 0) {
|
||||
this.camera.target = [0.5, 0.5, 0.5]; // Default to origin
|
||||
return;
|
||||
}
|
||||
|
||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
||||
|
||||
for (const block of this.world.blocks.values()) {
|
||||
minX = Math.min(minX, block.x);
|
||||
minY = Math.min(minY, block.y);
|
||||
minZ = Math.min(minZ, block.z);
|
||||
|
||||
// Add 1 to max to account for the block's 1x1x1 volume
|
||||
maxX = Math.max(maxX, block.x + 1);
|
||||
maxY = Math.max(maxY, block.y + 1);
|
||||
maxZ = Math.max(maxZ, block.z + 1);
|
||||
}
|
||||
|
||||
this.camera.target = [
|
||||
(minX + maxX) / 2,
|
||||
(minY + maxY) / 2,
|
||||
(minZ + maxZ) / 2
|
||||
];
|
||||
|
||||
// Wake up the renderer so the view snaps immediately
|
||||
this.engine.requestRender();
|
||||
}
|
||||
|
||||
async update() {
|
||||
this.meshes = await this.engine.buildMeshes(this.world);
|
||||
// Ensure the scene draws immediately after meshes are built
|
||||
this.engine.requestRender();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {mat4Identity, mat4Multiply, mat4Ortho, mat4LookAt, mat4Translate} from './math.js';
|
||||
// engine.js
|
||||
import {mat4Identity, mat4Translate} from './math.js';
|
||||
import {loadBlockstate, loadModel, TextureAtlas} from './assets.js';
|
||||
import {resolveTexture, buildGeometry} from './geometry.js';
|
||||
import {resolveBlock, getVariantHash} from './blockstate.js';
|
||||
@@ -8,9 +9,9 @@ layout(location=0) in vec3 a_position;
|
||||
layout(location=1) in vec3 a_normal;
|
||||
layout(location=2) in vec2 a_uv;
|
||||
layout(location=3) in float a_tint;
|
||||
layout(location=4) in float a_shade; // 0.0 = emissive, 1.0 = shaded
|
||||
layout(location=4) in float a_shade;
|
||||
|
||||
// Instanced attributes shifted to account for a_shade
|
||||
// Instanced attributes
|
||||
layout(location=5) in mat4 i_matrix;
|
||||
layout(location=9) in vec3 i_color;
|
||||
|
||||
@@ -29,8 +30,6 @@ void main() {
|
||||
|
||||
// Lambertian lighting
|
||||
float baseLight = max(dot(normal, normalize(u_lightDir)), 0.0) * 0.6 + 0.4;
|
||||
|
||||
// Mix between full brightness (1.0) and shaded based on the element's shade flag
|
||||
v_light = mix(1.0, baseLight, a_shade);
|
||||
|
||||
v_color = mix(vec3(1.0), i_color, a_tint);
|
||||
@@ -61,14 +60,25 @@ function compileShader(gl, type, src) {
|
||||
return shader;
|
||||
}
|
||||
|
||||
export class Renderer {
|
||||
constructor(canvas) {
|
||||
this.gl = canvas.getContext('webgl2', {antialias: true});
|
||||
export class Engine {
|
||||
constructor() {
|
||||
// Global canvas sits fixed behind everything
|
||||
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';
|
||||
document.body.appendChild(this.canvas);
|
||||
|
||||
this.gl = this.canvas.getContext('webgl2', {antialias: true, alpha: true});
|
||||
if (!this.gl) throw new Error("WebGL2 not supported");
|
||||
|
||||
const gl = this.gl;
|
||||
gl.enable(gl.DEPTH_TEST);
|
||||
gl.enable(gl.CULL_FACE);
|
||||
gl.enable(gl.SCISSOR_TEST); // Critical for virtual viewports
|
||||
|
||||
const vs = compileShader(gl, gl.VERTEX_SHADER, VS_SRC);
|
||||
const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS_SRC);
|
||||
@@ -85,26 +95,44 @@ export class Renderer {
|
||||
|
||||
this.atlasTexture = gl.createTexture();
|
||||
this.atlas = new TextureAtlas(512);
|
||||
this.meshes = []; // Array of renderable VAO objects
|
||||
|
||||
// Default Camera Setup
|
||||
this.projMatrix = new Float32Array(16);
|
||||
this.viewMatrix = new Float32Array(16);
|
||||
this.viewProjMatrix = new Float32Array(16);
|
||||
this.setCamera(-4, 4, -4, 4, 8, 6, 8, 1, 0, 1);
|
||||
this.dioramas = [];
|
||||
this.renderRequested = false;
|
||||
|
||||
// Re-render when the page moves or changes size
|
||||
window.addEventListener('resize', () => {
|
||||
this.resize();
|
||||
this.requestRender();
|
||||
});
|
||||
window.addEventListener('scroll', () => this.requestRender(), {passive: true});
|
||||
this.resize();
|
||||
}
|
||||
|
||||
setCamera(left, right, bottom, top, eyeX, eyeY, eyeZ, targetX, targetY, targetZ) {
|
||||
mat4Ortho(this.projMatrix, left, right, bottom, top, -20, 20);
|
||||
mat4LookAt(this.viewMatrix, eyeX, eyeY, eyeZ, targetX, targetY, targetZ, 0, 1, 0);
|
||||
mat4Multiply(this.viewProjMatrix, this.projMatrix, this.viewMatrix);
|
||||
// Debounced render trigger to save battery
|
||||
requestRender() {
|
||||
if (!this.renderRequested) {
|
||||
this.renderRequested = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.renderRequested = false;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This method takes a World object and rebuilds the WebGL buffers
|
||||
async updateWorld(world) {
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
addDiorama(diorama) {
|
||||
this.dioramas.push(diorama);
|
||||
}
|
||||
|
||||
// Transforms a World's data grid into baked WebGL geometry pools
|
||||
async buildMeshes(world) {
|
||||
const instancePool = new Map();
|
||||
|
||||
// 1. Resolve all blocks in the world into part definitions
|
||||
// 1. Resolve states and matrices
|
||||
for (const block of world.blocks.values()) {
|
||||
const stateJSON = await loadBlockstate(block.id);
|
||||
const parts = resolveBlock(stateJSON, block.props);
|
||||
@@ -121,6 +149,7 @@ export class Renderer {
|
||||
const pool = instancePool.get(hash);
|
||||
pool.matrices.push(...matrix);
|
||||
|
||||
// Simple power level tint
|
||||
if (block.props.power) {
|
||||
const p = parseInt(block.props.power, 10);
|
||||
pool.colors.push((0x4B + (p * 12)) / 255, 0.0, 0.0);
|
||||
@@ -130,7 +159,7 @@ export class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Build geometries, load textures, and create WebGL buffers
|
||||
// 2. Build geometries & instantiate WebGL buffers
|
||||
const newMeshes = [];
|
||||
for (const [hash, pool] of instancePool.entries()) {
|
||||
const modelJSON = await loadModel(pool.partDef.model);
|
||||
@@ -150,9 +179,8 @@ export class Renderer {
|
||||
newMeshes.push(this.createInstancedMesh(geometry, instanceCount, matrixArray, colorArray));
|
||||
}
|
||||
|
||||
// 3. Upload updated atlas and swap the active meshes
|
||||
this.updateAtlasTexture();
|
||||
this.meshes = newMeshes;
|
||||
return newMeshes;
|
||||
}
|
||||
|
||||
updateAtlasTexture() {
|
||||
@@ -168,7 +196,6 @@ export class Renderer {
|
||||
const vao = gl.createVertexArray();
|
||||
gl.bindVertexArray(vao);
|
||||
|
||||
// 1. Bind standard geometry (divisor = 0)
|
||||
const bindGeomAttr = (loc, data, size) => {
|
||||
const buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
@@ -183,17 +210,14 @@ export class Renderer {
|
||||
bindGeomAttr(3, geometry.tints, 1);
|
||||
bindGeomAttr(4, geometry.shades, 1);
|
||||
|
||||
// 2. Bind Index Buffer
|
||||
const ebo = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW);
|
||||
|
||||
// 3. Bind Instanced Attributes (divisor = 1)
|
||||
const matrixBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, matrices, gl.STATIC_DRAW);
|
||||
|
||||
// Mat4 requires 4 separate vec4 attributes in WebGL
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const loc = 5 + i;
|
||||
gl.enableVertexAttribArray(loc);
|
||||
@@ -215,21 +239,43 @@ export class Renderer {
|
||||
|
||||
render() {
|
||||
const gl = this.gl;
|
||||
|
||||
// Reset full viewport for clearing
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
gl.clearColor(0.1, 0.1, 0.12, 1.0);
|
||||
gl.scissor(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Use transparent background so the HTML body flows underneath
|
||||
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.uniformMatrix4fv(this.uniforms.viewProj, false, this.viewProjMatrix);
|
||||
gl.uniform3f(this.uniforms.lightDir, 1.0, 2.0, 0.5);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.atlasTexture);
|
||||
gl.uniform1i(this.uniforms.texture, 0);
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
gl.bindVertexArray(mesh.vao);
|
||||
gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount);
|
||||
for (const diorama of this.dioramas) {
|
||||
const rect = diorama.element.getBoundingClientRect();
|
||||
|
||||
// Culling: Skip rendering if element is off-screen
|
||||
if (rect.bottom < 0 || rect.top > gl.canvas.height ||
|
||||
rect.right < 0 || rect.left > gl.canvas.width) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Map DOM rect to WebGL screen coordinates
|
||||
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);
|
||||
|
||||
const aspect = rect.width / rect.height;
|
||||
const viewProj = diorama.camera.updateMatrices(aspect);
|
||||
gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj);
|
||||
gl.uniform3f(this.uniforms.lightDir, 1.0, 2.0, 0.5);
|
||||
|
||||
for (const mesh of diorama.meshes) {
|
||||
gl.bindVertexArray(mesh.vao);
|
||||
gl.drawElementsInstanced(gl.TRIANGLES, mesh.indexCount, gl.UNSIGNED_SHORT, 0, mesh.instanceCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
184
index.html
184
index.html
@@ -7,127 +7,127 @@
|
||||
body {
|
||||
background: #222;
|
||||
color: #eee;
|
||||
font-family: monospace;
|
||||
padding: 20px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
main {
|
||||
width: 80ch;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
#webgl-canvas {
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
.figure {
|
||||
width: 100%;
|
||||
height: 20em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Renderer Data Pipeline Prototype</h2>
|
||||
<div id="output">Loading and building geometry...</div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="figure" id="demo-2"></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>
|
||||
|
||||
<div id="canvas-container">
|
||||
<canvas id="webgl-canvas" width="1024" height="1024"></canvas>
|
||||
<div id="atlas-container"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import {World} from './world.js';
|
||||
import {Renderer} from './renderer.js';
|
||||
import {Engine} from './engine.js';
|
||||
import {Diorama} from './diorama.js';
|
||||
|
||||
async function testPipeline() {
|
||||
async function initDocument() {
|
||||
try {
|
||||
const webglCanvas = document.getElementById('webgl-canvas');
|
||||
const renderer = new Renderer(webglCanvas);
|
||||
// Adjusted camera slightly to see the block underneath
|
||||
renderer.setCamera(-3, 3, -3, 3, 5, 4, 5, 0, 0, 0);
|
||||
// 1. Initialize the global background renderer
|
||||
const engine = new Engine();
|
||||
|
||||
document.getElementById('atlas-container').appendChild(renderer.atlas.canvas);
|
||||
// Append the texture atlas to the bottom for debugging
|
||||
document.getElementById('atlas-container').appendChild(engine.atlas.canvas);
|
||||
|
||||
const world = new World();
|
||||
// 2. Setup Figure 1 (Shared: Wire & Repeater | Unique: Trapdoor)
|
||||
const demo1 = new Diorama('demo-1', engine);
|
||||
// Adjust camera to fit the small viewport
|
||||
demo1.camera.radius = 2;
|
||||
demo1.camera.phi = 10
|
||||
demo1.camera.theta = -5
|
||||
|
||||
// Initial State: Closed trapdoor, no redstone block
|
||||
world.setBlock('minecraft:oak_trapdoor', 0, 0, 0, {
|
||||
facing: 'east',
|
||||
open: 'false',
|
||||
half: 'bottom'
|
||||
demo1.world.setBlock('minecraft:redstone_wire', 0, 0, 0, {
|
||||
east: 'side',
|
||||
west: 'none',
|
||||
north: 'none',
|
||||
south: 'none',
|
||||
power: '15'
|
||||
});
|
||||
demo1.world.setBlock('minecraft:repeater', 1, 0, 0, {
|
||||
facing: 'east',
|
||||
delay: '1',
|
||||
locked: 'false',
|
||||
powered: 'true'
|
||||
});
|
||||
demo1.world.setBlock('minecraft:oak_trapdoor', 2, 0, 0, {facing: 'east', half: 'bottom', open: 'true'});
|
||||
|
||||
world.setBlock('minecraft:coal_block', 0, -1, -3); // north -z
|
||||
world.setBlock('minecraft:diamond_block', 3, -1, 0); // east +x
|
||||
world.setBlock('minecraft:gold_block', 0, -1, 3); // south +z
|
||||
world.setBlock('minecraft:iron_block', -3, -1, 0); // west -x
|
||||
demo1.centerView()
|
||||
|
||||
// NOTE the trapdoor should open AWAY from the diamond block
|
||||
// the repeaters should point "inward"
|
||||
// the pistons should extend "outward"
|
||||
engine.addDiorama(demo1);
|
||||
|
||||
// world.setBlock('minecraft:glass', 1, 0, 0);
|
||||
// world.setBlock('minecraft:glass', 0, 0, 1);
|
||||
world.setBlock('minecraft:coal_block', 0, -1, 0);
|
||||
world.setBlock('minecraft:repeater', 1, -1, 0, {facing: 'west', delay: 1, locked: false, powered: false});
|
||||
world.setBlock('minecraft:comparator', 0, -1, 1, {facing: 'north', mode: 'compare', powered: false});
|
||||
// 3. Setup Figure 2 (Shared: Wire & Repeater | Unique: Piston & Redstone Block)
|
||||
const demo2 = new Diorama('demo-2', engine);
|
||||
// Adjust camera to fit the small viewport
|
||||
demo2.camera.radius = 3;
|
||||
// View from a slightly different angle
|
||||
|
||||
world.setBlock('minecraft:piston', 0, 0, -3, {facing: "north", extended: false})
|
||||
world.setBlock('minecraft:piston', 3, 0, 0, {facing: "east", extended: false})
|
||||
world.setBlock('minecraft:piston', 0, 0, 3, {facing: "south", extended: false})
|
||||
world.setBlock('minecraft:piston', -3, 0, 0, {facing: "west", extended: false})
|
||||
demo2.world.setBlock('minecraft:redstone_wire', 0, 0, 0, {
|
||||
east: 'side',
|
||||
west: 'none',
|
||||
north: 'none',
|
||||
south: 'none',
|
||||
power: '0'
|
||||
});
|
||||
demo2.world.setBlock('minecraft:repeater', 1, 0, 0, {
|
||||
facing: 'east',
|
||||
delay: '4',
|
||||
locked: 'false',
|
||||
powered: 'false'
|
||||
});
|
||||
demo2.world.setBlock('minecraft:piston', 2, 0, 0, {facing: 'east', extended: 'false'});
|
||||
demo2.world.setBlock('minecraft:piston', 2, 0, 1, {facing: 'east', extended: 'true'});
|
||||
demo2.world.setBlock('minecraft: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', 3, 0, 2, {facing: 'east', short: 'false',type: 'normal'});
|
||||
demo2.world.setBlock('minecraft:redstone_block', 3, 0, 0);
|
||||
demo2.world.setBlock('minecraft:redstone_block', 3.5, 0, 1);
|
||||
demo2.world.setBlock('minecraft:redstone_block', 4, 0, 2);
|
||||
|
||||
world.setBlock('minecraft:repeater', 0, 1, -3, {facing: "north", delay: 1, locked: true, powered: false})
|
||||
world.setBlock('minecraft:repeater', 3, 1, 0, {facing: "east", delay: 2, locked: true, powered: false})
|
||||
world.setBlock('minecraft:repeater', 0, 1, 3, {facing: "south", delay: 3, locked: true, powered: false})
|
||||
world.setBlock('minecraft:repeater', -3, 1, 0, {facing: "west", delay: 4, locked: true, powered: false})
|
||||
demo2.centerView()
|
||||
engine.addDiorama(demo2);
|
||||
|
||||
await renderer.updateWorld(world);
|
||||
|
||||
// Render Loop (Static, just drawing the current buffers)
|
||||
function animate() {
|
||||
renderer.render();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
// --- THE STATE MACHINE LOOP ---
|
||||
let isOpen = false;
|
||||
|
||||
setInterval(async () => {
|
||||
isOpen = !isOpen;
|
||||
|
||||
// 1. Mutate the world data
|
||||
world.updateBlock(0, 0, 0, {open: isOpen.toString()});
|
||||
|
||||
if (isOpen) {
|
||||
world.setBlock('minecraft:redstone_block', 0, -1, 0);
|
||||
world.updateBlock(1, -1, 0, {powered: true})
|
||||
world.updateBlock(0, -1, 1, {powered: true})
|
||||
} else {
|
||||
world.removeBlock(0, -1, 0);
|
||||
world.updateBlock(1, -1, 0, {powered: false})
|
||||
world.updateBlock(0, -1, 1, {mode: 'subtract', powered: false})
|
||||
}
|
||||
|
||||
// 2. Tell the renderer to sync with the new world state
|
||||
await renderer.updateWorld(world);
|
||||
|
||||
document.getElementById('output').innerHTML = `<strong>State Demo:</strong> Trapdoor is ${isOpen ? 'OPEN' : 'CLOSED'}`;
|
||||
|
||||
// await return new Promise((resolve) => setTimeout(resolve, time));
|
||||
|
||||
}, 1000);
|
||||
// 4. Build geometries and draw
|
||||
// We await these so the meshes are fully baked and the atlas is populated
|
||||
await demo1.update();
|
||||
await demo2.update();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById('output').textContent = 'Error: ' + err.message;
|
||||
console.error("Renderer Initialization Failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
testPipeline();
|
||||
initDocument();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user