Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ <h2>Controls</h2>
<li><kbd>E</kbd><span>Open inventory / switch mode</span></li>
<li><kbd>F</kbd><span>Cycle selected block</span></li>
<li><kbd>P</kbd><span>Capture screenshot</span></li>
<li><kbd>F3</kbd><span>Toggle perf overlay (FPS / draw / chunks)</span></li>
<li><kbd>Esc</kbd><span>Pause</span></li>
</ul>
<button class="btn btn-primary" data-back>Back</button>
Expand All @@ -66,7 +67,7 @@ <h2>Settings</h2>
<div class="setting">
<label for="set-viewdistance">View distance</label>
<div class="setting-row">
<input type="range" id="set-viewdistance" min="2" max="10" step="1" />
<input type="range" id="set-viewdistance" min="2" max="12" step="1" />
<output id="out-viewdistance">—</output>
</div>
</div>
Expand All @@ -90,12 +91,74 @@ <h2>Settings</h2>
<input type="checkbox" id="set-fps" />
</div>
</div>

<div class="setting-divider"><span>Graphics</span></div>

<div class="setting">
<label>Graphics preset</label>
<div class="preset-row" role="group" aria-label="Graphics preset">
<button type="button" class="btn btn-small" data-preset="low">Low</button>
<button type="button" class="btn btn-small" data-preset="medium">Medium</button>
<button type="button" class="btn btn-small" data-preset="high">High</button>
</div>
</div>
<div class="setting">
<label for="set-renderscale">Render scale</label>
<div class="setting-row">
<select id="set-renderscale" class="select-input">
<option value="0.75">0.75 (faster)</option>
<option value="1">1.0 (native)</option>
</select>
</div>
</div>
<div class="setting">
<label for="set-shadows">Shadows</label>
<div class="setting-row">
<select id="set-shadows" class="select-input">
<option value="off">Off</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div class="setting">
<label for="set-clouds">Clouds</label>
<label for="set-water">Water</label>
<div class="setting-row">
<input type="checkbox" id="set-clouds" checked />
<select id="set-water" class="select-input">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div class="setting">
<label for="set-foliage">Foliage</label>
<div class="setting-row">
<select id="set-foliage" class="select-input">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div class="setting">
<label for="set-clouds2">Clouds</label>
<div class="setting-row">
<select id="set-clouds2" class="select-input">
<option value="off">Off</option>
<option value="simple">Simple</option>
<option value="fancy">Fancy</option>
</select>
</div>
</div>
<div class="setting">
<label for="set-aa">Anti-aliasing (FXAA)</label>
<div class="setting-row">
<input type="checkbox" id="set-aa" />
</div>
</div>

<div class="setting">
<label for="set-seed">World seed</label>
<div class="setting-row">
Expand Down Expand Up @@ -126,6 +189,7 @@ <h2>Paused</h2>

<!-- ===== In-game HUD ===== -->
<div id="hud" hidden>
<div id="perf" hidden></div>
<div id="crosshair" aria-hidden="true"></div>
<div id="status">
<div id="fps" class="badge" hidden>0 fps</div>
Expand Down
56 changes: 48 additions & 8 deletions src/engine/Clouds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ export class Clouds {
private lastCenterX = Infinity;
private lastCenterZ = Infinity;

/** Scratch fog-range vector, reused each frame to avoid a per-frame allocation. */
private readonly _fogRange = new Vector2(0, 0);

private enabled = true;
/**
* "Simple" tier skips the cloud TOP faces — the player is almost always below
* the cloud layer (CLOUD_HEIGHT=130 vs player ~y=40) so tops are rarely seen,
* and culling them roughly halves the cloud triangle count. Toggled by the
* graphics settings (simple vs fancy clouds).
*/
private simple = false;

// Preallocated geometry buffers, reused across rebuilds (no per-build GC).
// RGBA colors (4 floats/vertex) — required by Babylon's vertex-color path.
Expand All @@ -74,7 +84,7 @@ export class Clouds {
this.scene = scene;
this.noise = new Noise(seed || "voxl");

this.cTop = Color3.FromHexString("#ffffff");
this.cTop = Color3.FromHexString("#e2eaf2"); // soft cool white, not pure #ffffff
// side1 (N/S walls) and side2 (E/W walls) get progressively more shadow,
// bottom gets full shadow — gives the slabs simple directional depth.
this.cSide1 = SHADOW.scale(0.25).add(new Color3(0.75, 0.75, 0.75));
Expand Down Expand Up @@ -125,6 +135,17 @@ export class Clouds {
this.mesh.setEnabled(enabled);
}

/**
* Toggle the simple tier (skips cloud top faces for ~half the triangles).
* Forces a rebuild so the change is immediate rather than waiting for drift.
*/
setSimple(simple: boolean): void {
if (this.simple === simple) return;
this.simple = simple;
this.lastCenterX = Infinity;
this.lastCenterZ = Infinity;
}

setSeed(seed: string): void {
this.noise = new Noise(seed || "voxl");
this.lastCenterX = Infinity;
Expand Down Expand Up @@ -198,14 +219,14 @@ export class Clouds {
const addQuad = (
ax: number, ay: number, az: number,
bx: number, by: number, bz: number,
c: number, cy: number, cz: number,
cx2: number, cy2: number, cz2: number,
dx: number, dy: number, dz: number,
color: Color3,
): void => {
const o = v * 3;
pos[o] = ax; pos[o + 1] = ay; pos[o + 2] = az;
pos[o + 3] = bx; pos[o + 4] = by; pos[o + 5] = bz;
pos[o + 6] = c; pos[o + 7] = cy; pos[o + 8] = cz;
pos[o + 6] = cx2; pos[o + 7] = cy2; pos[o + 8] = cz2;
pos[o + 9] = dx; pos[o + 10] = dy; pos[o + 11] = dz;
const co = v * 4;
for (let i = 0; i < 4; i++) {
Expand Down Expand Up @@ -233,8 +254,9 @@ export class Clouds {
const filledW = inArea(xi - 1, zi) && grid[gi(xi - 1, zi)] === 1;

// Top and bottom faces (DoubleSide + flat shading via vertex colours,
// so winding is irrelevant here).
addQuad(x0, top, z1, x1, top, z1, x1, top, z0, x0, top, z0, this.cTop);
// so winding is irrelevant here). Simple tier skips tops — the player is
// below the layer and the tops are almost never visible.
if (!this.simple) addQuad(x0, top, z1, x1, top, z1, x1, top, z0, x0, top, z0, this.cTop);
addQuad(x0, bot, z0, x1, bot, z0, x1, bot, z1, x0, bot, z1, this.cBottom);

// Side walls only where the neighbour is open (Minetest-style culling).
Expand Down Expand Up @@ -263,10 +285,23 @@ export class Clouds {
/** Per-frame fog binding (called from Sky.update). */
bindFog(color: Color3, start: number, end: number, cameraPos: Vector3): void {
this.material.setColor3("fogColor", color);
this.material.setVector2("fogRange", new Vector2(start, end));
this._fogRange.set(start, end);
this.material.setVector2("fogRange", this._fogRange);
this.material.setVector3("cameraPos", cameraPos);
}

/**
* Apply a day/night brightness factor to the cloud layer (0..1). Clouds are
* otherwise unlit vertex colours, so without this they'd glow white at night.
* Pushed each frame by the lighting system via Sky.
*/
setDayFactor(dayFactor: number): void {
// Bright at midday (1.0), dim but not black at midnight (~0.32) so clouds
// read as grey shapes against the night sky.
const light = 0.32 + 0.68 * dayFactor;
this.material.setFloat("uCloudLight", light);
}

dispose(): void {
this.material.dispose();
this.mesh.dispose();
Expand Down Expand Up @@ -306,19 +341,24 @@ function makeCloudMaterial(scene: Scene): ShaderMaterial {
uniform vec3 fogColor;
uniform vec2 fogRange;
uniform vec3 cameraPos;
uniform float uCloudLight;
void main() {
float dist = length(vPositionW - cameraPos);
float fog = clamp((fogRange.y - dist) / (fogRange.y - fogRange.x), 0.0, 1.0);
vec3 col = mix(fogColor, vColor.rgb, fog);
// Clouds are unlit by the voxel system, so apply a day/night brightness
// factor here so they dim to grey at night instead of glowing white.
vec3 lit = vColor.rgb * uCloudLight;
vec3 col = mix(fogColor, lit, fog);
gl_FragColor = vec4(col, vColor.a);
}
`,
},
{
attributes: ["position", "color"],
uniforms: ["world", "worldViewProjection", "fogColor", "fogRange", "cameraPos"],
uniforms: ["world", "worldViewProjection", "fogColor", "fogRange", "cameraPos", "uCloudLight"],
},
);
material.backFaceCulling = false;
material.setFloat("uCloudLight", 1.0);
return material;
}
1 change: 1 addition & 0 deletions src/engine/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class Input {
return;
}
if (e.code === "Space") e.preventDefault();
if (e.code === "F3") e.preventDefault(); // dev perf overlay (avoid browser find-bar)
if (!e.repeat) {
this.onKey?.(e.code, true);
if (e.code === "Space") {
Expand Down
14 changes: 13 additions & 1 deletion src/engine/Sky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,20 @@ export class Sky {
this.domeMat.setVector3("bottomColor", this._horVec);
}

setCloudsEnabled(enabled: boolean): void {
/** Push the day/night brightness factor to the cloud layer. */
setCloudDayFactor(dayFactor: number): void {
this.clouds.setDayFactor(dayFactor);
}

/**
* Apply the cloud quality tier in one call:
* enabled = false → clouds off entirely
* enabled = true, simple = true → simple tier (top faces skipped)
* enabled = true, simple = false → full fancy clouds
*/
setClouds(enabled: boolean, simple: boolean): void {
this.clouds.setEnabled(enabled);
this.clouds.setSimple(enabled && simple);
}

setCloudSeed(seed: string): void {
Expand Down
13 changes: 7 additions & 6 deletions src/game/ChunkMesher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ function pushFace(
const du = uv.u1 - uv.u0;
const dv = uv.v1 - uv.v0;
const base = b.vertexCount;
// Water pass keeps a single scalar brightness (its material is Standard). The
// opaque/cutout pass bakes two channels: r=shadedSun g=shadedBlock b=sunLevel
// a=blockLevel, consumed by the VoxelTerrainMaterial shader.
// All faces currently bake the same four-channel colour data
// (r=shadedSun, g=shadedBlock, b=sunLevel, a=blockLevel) consumed by the
// VoxelTerrainMaterial shader. The water (transparent) pass's colours are
// discarded later (World strips them) since water uses a plain StandardMaterial.
let cr: number, cg: number, cb: number, ca: number;
if (twoChannel) {
cr = sample.sunBright;
Expand Down Expand Up @@ -304,9 +305,9 @@ export function buildChunkGeometry(
// exposed to (the neighbour air/space), combined with face shade.
const sample = sampleBrightness(nwx, nwy, nwz, FACE_BRIGHTNESS[f]);
const isWaterTop = def.liquid && n[1] === 1;
// Water keeps a single scalar brightness; opaque/cutout bake two
// light channels for the VoxelTerrainMaterial shader.
pushFace(builder, f, wx, wy, wz, def.tiles[f], sample, !def.liquid, isWaterTop);
// All faces bake four-channel light colours; the water pass discards
// them (World strips the colour kind) since it uses a StandardMaterial.
pushFace(builder, f, wx, wy, wz, def.tiles[f], sample, true, isWaterTop);
}
}
}
Expand Down
Loading
Loading