Files
Emil c7e86663d8
MVP checks / mvp (push) Canceled after 0s
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

421 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Shacraft daylight renderer fixture</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font:
13px/1.4 system-ui,
sans-serif;
color: #172c23;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
.controls {
position: fixed;
top: 14px;
left: 14px;
right: 14px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
width: fit-content;
max-width: calc(100vw - 28px);
padding: 10px 14px;
border: 1px solid #ffffffb3;
border-radius: 7px;
background: #fffffff0;
box-shadow: 0 2px 10px #153c2515;
}
label {
display: flex;
align-items: center;
gap: 6px;
}
select,
button {
font: inherit;
color: inherit;
padding: 5px 9px;
background: #f6f8f3;
border: 1px solid #aebdaf;
border-radius: 4px;
}
button {
cursor: pointer;
}
input {
accent-color: #436953;
}
output {
color: #47604c;
font-variant-numeric: tabular-nums;
}
.legend {
position: fixed;
left: 14px;
bottom: 14px;
margin: 0;
padding: 8px 12px;
background: #ffffffe6;
border-radius: 5px;
max-width: calc(100vw - 28px);
}
#error {
position: fixed;
inset: 80px 14px auto;
white-space: pre-wrap;
background: #fff0ec;
color: #7f2217;
padding: 16px;
}
[hidden] {
display: none !important;
}
</style>
</head>
<body>
<canvas id="c" aria-label="Daylight renderer test scene"></canvas>
<div class="controls" aria-label="Renderer test controls">
<strong>Daylight fixture</strong>
<label
>Camera
<select id="camera">
<option value="overview">Overview</option>
<option value="corner">Corner</option>
<option value="sun-facing">Sun facing</option>
<option value="lamp-room">Lamp room</option>
<option value="actor-boundary-before">Actors · before section boundary</option>
<option value="actor-boundary-after">Actors · after section boundary</option>
</select>
</label>
<label><input id="shadows" type="checkbox" checked /> Shadows</label>
<button id="pillar" type="button">Remove pillar</button>
<label
>Room lamp
<select id="lamp">
<option value="torch">Torch · 14</option>
<option value="sea-lantern">Sea lantern · 15</option>
<option value="off">Off · 0</option>
</select>
</label>
<button id="wall" type="button">Remove room partition</button>
<output id="status" role="status">Building scene…</output>
</div>
<p class="legend">
Stone corner · arch and posts · slabs and stairs · glass and water · sea
lantern · avatar and entity · sealed lamp room with an internal glass
window
<br /><output id="light-samples">Light field initializing…</output>
</p>
<pre id="error" hidden></pre>
<script type="module">
import { Renderer } from "../renderer.js?v=block-light-1";
const canvas = document.querySelector("canvas"),
status = document.querySelector("#status"),
cameraSelect = document.querySelector("#camera"),
shadowToggle = document.querySelector("#shadows"),
pillarButton = document.querySelector("#pillar"),
lampSelect = document.querySelector("#lamp"),
wallButton = document.querySelector("#wall"),
lightStatus = document.querySelector("#light-samples");
const cube = { min: [0, 0, 0], max: [1, 1, 1] };
const lookAt = (eye, target) => {
const direction = target.map((value, axis) => value - eye[axis]);
return {
eye,
yaw: Math.atan2(direction[0], -direction[2]),
pitch: Math.atan2(
direction[1],
Math.hypot(direction[0], direction[2]),
),
};
};
const cameras = {
overview: lookAt([12, 10, 15], [0, 1.8, -3]),
corner: lookAt([-2.4, 3.6, -2.1], [-6.2, 1.8, -8.6]),
"sun-facing": {
eye: [7, 1.9, -9],
yaw: Math.atan2(-0.45, -0.3),
pitch: 0.65,
},
"lamp-room": lookAt([-15.5, 2.65, 12.5], [-13.5, 2.1, 7]),
"actor-boundary-before": lookAt([15.99, 4, 8], [3.5, 2, 1.4]),
"actor-boundary-after": lookAt([16.01, 4, 8], [3.5, 2, 1.4]),
};
function fail(error) {
document.body.dataset.status = "error:" + error.message;
status.textContent = "Renderer error";
const output = document.querySelector("#error");
output.hidden = false;
output.textContent = error.stack || String(error);
}
try {
const renderer = new Renderer(canvas),
blocks = new Map(),
materials = new Map(),
material = (id, state, color, options = {}) =>
materials.set(id, {
state: `minecraft:${state}`,
color,
opacity: 1,
render: [cube],
light: 0,
...options,
}),
put = (x, y, z, block) => blocks.set(`${x},${y},${z}`, block);
material(1, "grass_block", [112, 155, 69]);
material(2, "stone", [177, 176, 163]);
material(3, "stone_bricks", [186, 181, 164]);
material(4, "oak_log[axis=y]", [139, 108, 69]);
material(5, "oak_planks", [188, 152, 100]);
material(6, "glass", [183, 221, 221], { opacity: 0.28 });
material(7, "water[level=0]", [74, 149, 184], {
opacity: 0.58,
render: [{ min: [0, 0, 0], max: [1, 0.88, 1] }],
});
material(8, "stone_slab[type=bottom]", [178, 176, 159], {
render: [{ min: [0, 0, 0], max: [1, 0.5, 1] }],
});
material(
9,
"stone_stairs[facing=north,half=bottom,shape=straight]",
[179, 177, 160],
{
render: [
{ min: [0, 0, 0], max: [1, 0.5, 1] },
{ min: [0, 0.5, 0], max: [1, 1, 0.5] },
],
},
);
material(10, "sea_lantern", [221, 244, 224], { light: 15 });
material(11, "torch", [229, 158, 70], {
light: 14,
render: [{ min: [0.43, 0, 0.43], max: [0.57, 0.8, 0.57] }],
});
material(12, "quartz_block", [211, 207, 189]);
for (let x = -18; x <= 18; x++)
for (let z = -20; z <= 16; z++)
put(
x,
0,
z,
Math.abs(x) <= 2 || (x >= -7 && x <= -3 && z >= -10 && z <= -3)
? 2
: 1,
);
// A tall inside corner and a small roof expose contact and indirect light.
for (let y = 1; y <= 5; y++) {
for (let x = -7; x <= -3; x++) put(x, y, -10, 3);
for (let z = -9; z <= -4; z++) put(-7, y, z, 3);
}
for (let x = -7; x <= -5; x++)
for (let z = -10; z <= -8; z++) put(x, 5, z, 3);
put(-6, 1, -9, 10);
// The right support can be removed independently of the lintel.
const removablePillar = [];
for (let y = 1; y <= 4; y++) {
put(0, y, -6, 4);
put(5, y, -6, 4);
removablePillar.push([5, y, -6]);
}
for (let x = 0; x <= 5; x++) put(x, 5, -6, 5);
for (let y = 1; y <= 3; y++) put(7, y, 1, 4);
for (let y = 1; y <= 5; y++) put(9, y, -3, 3);
for (let x = -5; x <= -2; x++) put(x, 1, 3, 8);
for (let x = -5; x <= -2; x++) put(x, 1, 1, 9);
put(-5, 2, 1, 8);
for (let x = 3; x <= 5; x++)
for (let y = 1; y <= 3; y++) put(x, y, 4, 6);
for (let x = 6; x <= 10; x++)
for (let z = -11; z <= -7; z++) {
put(x, -1, z, 2);
put(x, 0, z, 7);
}
for (let x = 5; x <= 11; x++) {
put(x, 0, -12, 3);
put(x, 0, -6, 3);
}
for (let z = -11; z <= -7; z++) {
put(5, 0, z, 3);
put(11, 0, z, 3);
}
// All exterior faces are solid, including the roof. The window connects
// two indoor chambers so lamp-off darkness is independent of daylight.
for (let x = -17; x <= -9; x++)
for (let z = 4; z <= 14; z++) {
put(x, 0, z, 2);
put(x, 5, z, 12);
if (x === -17 || x === -9 || z === 4 || z === 14)
for (let y = 1; y <= 4; y++) put(x, y, z, 12);
}
for (let z = 5; z <= 13; z++)
for (let y = 1; y <= 4; y++) put(-13, y, z, 12);
put(-13, 2, 5, 6);
put(-13, 3, 5, 6);
const lampPosition = [-15, 2, 8],
partitionPanel = [];
put(-15, 1, 8, 2);
put(...lampPosition, 11);
for (let y = 1; y <= 3; y++)
for (let z = 8; z <= 9; z++) partitionPanel.push([-13, y, z]);
const players = [
{
id: "fixture-player",
name: "Avatar",
position: [3.5, 1, 1.4],
yaw: -0.5,
pose: "standing",
height: 1.8,
eye_height: 1.62,
color: [0.31, 0.47, 0.64],
},
];
const entities = [
{ kind: "fixture-crate", position: [-3, 1, -4], yaw: 0.3 },
];
const entityDefinitions = new Map([
[
"fixture-crate",
{
color: [168, 121, 81],
render: [{ min: [-0.5, 0, -0.5], max: [0.5, 1, 0.5] }],
},
],
]);
renderer.replace(blocks, materials);
renderer.shadowsEnabled = shadowToggle.checked;
let pillarPresent = true,
wallPresent = true,
frameCount = 0,
lastStatus = -Infinity;
const errors = new Set();
document.body.dataset.camera = cameraSelect.value;
document.body.dataset.pillar = "present";
document.body.dataset.lamp = lampSelect.value;
document.body.dataset.wall = "present";
shadowToggle.onchange = () => {
renderer.shadowsEnabled = shadowToggle.checked;
};
cameraSelect.onchange = () => {
document.body.dataset.camera = cameraSelect.value;
};
pillarButton.onclick = () => {
pillarPresent = !pillarPresent;
const changes = removablePillar.map((pos) => {
if (pillarPresent) blocks.set(pos.join(","), 4);
else blocks.delete(pos.join(","));
return { pos, block: pillarPresent ? 4 : 0 };
});
renderer.change(changes);
pillarButton.textContent = pillarPresent
? "Remove pillar"
: "Restore pillar";
document.body.dataset.pillar = pillarPresent ? "present" : "removed";
};
lampSelect.onchange = () => {
const block = { torch: 11, "sea-lantern": 10, off: 0 }[
lampSelect.value
];
if (block) blocks.set(lampPosition.join(","), block);
else blocks.delete(lampPosition.join(","));
renderer.change([{ pos: lampPosition, block }]);
document.body.dataset.lamp = lampSelect.value;
};
wallButton.onclick = () => {
wallPresent = !wallPresent;
const changes = partitionPanel.map((pos) => {
if (wallPresent) blocks.set(pos.join(","), 12);
else blocks.delete(pos.join(","));
return { pos, block: wallPresent ? 12 : 0 };
});
renderer.change(changes);
wallButton.textContent = wallPresent
? "Remove room partition"
: "Restore room partition";
document.body.dataset.wall = wallPresent ? "present" : "removed";
};
const lightSamplePositions = {
source: lampPosition,
near: [-15, 2, 9],
window: [-13, 2, 5],
behindWall: [-12, 2, 8],
far: [-10, 2, 12],
outside: [-15, 2, 15],
};
function updateLightSamples() {
const field = renderer.blockLightField;
if (typeof field?.sample !== "function") return;
const samples = Object.fromEntries(
Object.entries(lightSamplePositions).map(([name, position]) => {
const sample = field.sample(position);
return [name, { block: sample.blockLevel, sky: sample.skyLevel }];
}),
);
document.body.dataset.lightSamples = JSON.stringify(samples);
document.body.dataset.lightSources = String(field.sourceCount);
lightStatus.textContent = `Light 015 (block/sky): ${Object.entries(
samples,
)
.map(([name, sample]) => `${name} ${sample.block}/${sample.sky}`)
.join(" · ")}`;
}
function frame(time) {
try {
renderer.draw(
cameras[cameraSelect.value],
players,
entities,
entityDefinitions,
);
frameCount++;
const error = renderer.gl.getError();
if (error !== renderer.gl.NO_ERROR)
errors.add(`0x${error.toString(16)}`);
if (time - lastStatus >= 200) {
lastStatus = time;
const ready = frameCount >= 3 && renderer.dirty.size === 0;
document.body.dataset.status = errors.size
? `error:WebGL ${[...errors].join(",")}`
: `${ready ? "ready" : "building"}:${renderer.triangles}`;
document.body.dataset.glError = errors.size
? [...errors].join(",")
: "0";
document.body.dataset.triangles = String(renderer.triangles);
document.body.dataset.sections = String(renderer.sections.size);
document.body.dataset.pendingSections = String(
renderer.dirty.size,
);
document.body.dataset.shadowsEnabled = String(
renderer.shadowsEnabled,
);
document.body.dataset.frames = String(frameCount);
document.body.dataset.meshRebuilds = String(
renderer.meshRebuilds,
);
updateLightSamples();
status.textContent = `${errors.size ? "WebGL " + [...errors].join(",") : "WebGL OK"} · ${renderer.triangles.toLocaleString("en-US")} triangles · shadows ${renderer.shadowsEnabled ? "on" : "off"}`;
}
requestAnimationFrame(frame);
} catch (error) {
fail(error);
}
}
requestAnimationFrame(frame);
} catch (error) {
fail(error);
}
</script>
</body>
</html>