MVP checks / mvp (push) Canceled after 0s
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.
421 lines
15 KiB
HTML
421 lines
15 KiB
HTML
<!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 0–15 (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>
|