Expand voxel gameplay, lighting, full-height streaming and world imports
MVP checks / mvp (push) Canceled after 0s
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.
This commit is contained in:
@@ -1,64 +1,420 @@
|
||||
<!doctype html><meta charset="utf-8" /><title>Shacraft renderer fixture</title
|
||||
><style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: block;
|
||||
}</style
|
||||
><canvas id="c"></canvas>
|
||||
<script type="module">
|
||||
import { Renderer } from "../renderer.js";
|
||||
const canvas = document.querySelector("canvas");
|
||||
try {
|
||||
const r = new Renderer(canvas),
|
||||
blocks = new Map(),
|
||||
mats = new Map([
|
||||
[
|
||||
1,
|
||||
<!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],
|
||||
{
|
||||
state: "minecraft:grass_block",
|
||||
color: [107, 151, 65],
|
||||
opacity: 1,
|
||||
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
|
||||
render: [
|
||||
{ min: [0, 0, 0], max: [1, 0.5, 1] },
|
||||
{ min: [0, 0.5, 0], max: [1, 1, 0.5] },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
2,
|
||||
{
|
||||
state: "minecraft:bricks",
|
||||
color: [169, 88, 68],
|
||||
opacity: 1,
|
||||
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
|
||||
},
|
||||
],
|
||||
[
|
||||
3,
|
||||
{
|
||||
state: "minecraft:glass",
|
||||
color: [169, 220, 219],
|
||||
opacity: 0.35,
|
||||
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
|
||||
},
|
||||
],
|
||||
]);
|
||||
for (let x = -16; x <= 16; x++)
|
||||
for (let z = -16; z <= 16; z++) blocks.set(`${x},0,${z}`, 1);
|
||||
for (let x = -3; x < 3; x++)
|
||||
for (let y = 1; y < 5; y++)
|
||||
blocks.set(
|
||||
`${x},${y},-7`,
|
||||
(x === 0 || x === 1) && y > 1 && y < 4 ? 3 : 2,
|
||||
);
|
||||
r.replace(blocks, mats);
|
||||
for (let n = 0; n < 24; n++) {
|
||||
r.draw({ eye: [7, 5, 12], yaw: -0.38, pitch: -0.2 }, [], [], new Map());
|
||||
}
|
||||
document.body.dataset.status = `ready:${r.triangles}`;
|
||||
} catch (error) {
|
||||
document.body.dataset.status = "error:" + error.message;
|
||||
document.body.textContent = error.stack;
|
||||
}
|
||||
</script>
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user