Commit graph

14 commits

Author SHA1 Message Date
Maximus Gorog
0dca49f475 Round C: typed camera accessors, post-pass helper, leaf translucency (#20, #23, #25)
shader.wgsl:
  - Camera.frame layout now documented slot-by-slot in the struct
    comment; named accessors scene_time() / exposure_bias() / eye_world()
    so call sites read as intent. exposure_bias plumbed (default 1.0)
    as the foundation for a future Settings.exposure slider. [#20]
  - Leaves now get sun-aware backlit translucency: leaf_translucency()
    peaks when the sun rakes across the leaf plane AND comes from
    behind the surface. Cheap stand-in for subsurface scattering —
    reads as "dappled sun through canopy" without per-pixel ray
    sampling. [#25]

render/mod.rs:
  - run_fullscreen_pass(encoder, label, target, pipeline, bgs, clear)
    helper. The three post-chain steps (mask → shafts → composite)
    are now three sequential calls instead of three copies of the
    same begin_render_pass boilerplate. Adding a new effect (bloom,
    motion blur, vignette) is one extra row. [#23]
  - upload_camera fills frame[1] = 1.0 (exposure_bias default).

render/uniform.rs:
  - CameraUniform.frame doc updated to enumerate each slot.

Tests: 63 passing. Native + wasm release clean.
2026-05-24 10:16:05 -06:00
Maximus Gorog
bd6b3fadb0 Round B: screen-space god rays + FXAA (#12, #14)
New WGSL files:
  mask.wgsl     Sun-cone × sky-alpha mask at ¼ resolution. Marks sky
                pixels (scene_color.a == 0) that fall within a tight
                cone around the sun direction. Sun direction derived
                from scene time via the injected DAY_PERIOD /
                SUN_OFFSET constants — same source as sim::lighting,
                so visual rays align with the mechanical sun.
  shafts.wgsl   Radial blur at ¼ resolution. Projects the sun
                direction-at-infinity to screen space via view_proj,
                steps 32 samples from each pixel toward the
                sun_screen_pos accumulating mask intensity with
                exponential decay, outputs sun-tinted ray color.

shader.wgsl:
  - fs_sky now writes alpha = 0 so the mask pass can identify sky
    pixels without a separate occluder pass. Terrain / outline /
    remote-player pipelines continue writing alpha = 1.

post.wgsl (rewritten):
  - Reads scene_color + shafts.
  - Cheap edge-aware FXAA (5-tap diagonal blur, blends toward neighbor
    average where luminance gradient exceeds threshold). Catches the
    axis-aligned staircase aliasing that voxel games produce.
  - Adds shafts additively before tonemap so rays go through the
    filmic curve and don't blow out.

render/scene_target.rs:
  - create_image_bind_group (single texture + sampler) — used by
    mask pass (binds scene_color) and shafts pass (binds mask_view).
  - create_composite_bind_group (scene + shafts + sampler) — used by
    the final post pass.
  - create_quarter_res_view (¹⁄₁₆ fillrate, RENDER_ATTACHMENT +
    TEXTURE_BINDING) — used for both mask and shafts targets.

render/pipelines.rs:
  - image_bgl / composite_bgl as separate layouts.
  - fullscreen_pipeline factory replaces the post-specific one;
    takes vs/fs entry-point names so mask, shafts, and post all
    build through the same shape.

render/mod.rs:
  - Renderer grows mask_view, shafts_view, image_bgl, composite_bgl,
    mask_pipeline, shafts_pipeline, mask_bg, shafts_bg fields. The
    old single-pass post_pipeline becomes the final composite pass.
  - render() now does scene → mask → shafts → post (each in its own
    encoder block).
  - resize() recreates all three render targets and all three bind
    groups in the right order.

Tests: 63 passing (added 2 for mask/shafts source assembly). Native
+ wasm release clean.

Visual change: when you face roughly toward the sun and there's
geometry blocking the disc, you'll see warm light shafts radiating
outward. FXAA softens the worst pixel-step edges. Tonemap (from
Round A) is now the final step of the new pipeline.
2026-05-24 10:09:10 -06:00
Maximus Gorog
94585b1ab2 Round A: sunset family + ACES tonemap (#1-4, #6)
shader.wgsl:
  - sky_dome: zenith now tints toward a dusky-purple zenith_twi during
    twilight (was previously only the horizon picking up the warm tint).
    Top faces at sunset will read warm through the sky_vis pathway
    instead of staying cold blue. [#1]
  - apply_fog: at twilight the fog tints toward sun_tint by twi×0.45,
    so distant terrain reads warm against an orange sky instead of
    cold. [#2]
  - sky_color stars: gate flipped from (1 - day) — which still showed
    stars during dusk while the sky was bright — to a direct
    smoothstep on sun.y (-0.22..0.04). Stars now fade in only after
    civil twilight. [#3]
  - sun disc / halo: sharpness + intensity now altitude-dependent. At
    zenith it's a sharp pinpoint (sharpness 800, intensity 1.5); near
    the horizon it softens to a big atmospheric bloom (sharpness 160,
    intensity 2.2). Halo also widens and brightens at low sun. [#4]

post.wgsl:
  - ACES filmic tonemap (Narkowicz approximation) runs as the final
    step. Brightens midtones, compresses highlights smoothly into
    [0,1] instead of the previous hard clamp. Output stays linear; the
    sRGB surface handles display encoding. Foundation for everything
    HDR that follows (god rays, bloom). [#6]
2026-05-24 00:20:25 -06:00
Maximus Gorog
511798b6eb Phase 1 lighting: bake per-vertex sky visibility from voxel construction
The "side faces don't adjust to daytime ambiance" bug was caused by
conflating geometric sky visibility with radiometric sky intensity
into a single ambient_strength knob. This commit separates them.

sim/lighting.rs:
  - Refactor: extract walks_to_sky(world, origin, dir) -> bool as the
    shared DDA primitive. is_in_direct_sun now calls it.
  - New: sky_visibility(world, pos, normal) -> f32. Casts 8 cosine-
    weighted hemisphere rays through the voxel grid (Hammersley(2)
    distribution, deterministic). Returns fraction that escape to
    the world top. Pure function of (world, position, normal) — depends
    only on the surrounding voxel construction. Architecturally, this
    means a player-built sealed roof on the surface produces the same
    sky_vis as the same enclosure underground; no "is this the surface
    or underground?" hack anywhere.
  - 4 new tests including the construction-invariance test that pins
    "same enclosure geometry → same sky_vis regardless of world Y".

mesh.rs:
  - Vertex gains a sky_vis: f32 field at @location(5).
  - build_chunk_mesh computes sky_vis per quad corner via
    sky_visibility (one ray-walk per vertex; amortized at mesh build,
    free at runtime).
  - emit_oriented_box sets sky_vis = 1.0 for remote-player boxes
    (they float in open air).
  - 2 new tests: open-top has high sky_vis, slab-covered top has low.

shader.wgsl:
  - VsIn / VsOut grow @location(5) sky_vis: f32.
  - ambient_term rewritten as the principled split:
        sky_radiance(N) × sky_vis  +  bounce × (1 − sky_vis)
    where bounce is sun-tinted ground albedo scaled by day. No more
    `face_up = normal.y * 0.5 + 0.5` flat hemisphere assumption — the
    per-vertex geometric weight does the work.
  - ambient_strength bumped from mix(0.25, 0.85, day) to mix(0.35,
    1.00, day) since sky_vis now carries the geometric attenuation.

Result: side faces in the open get bright sky-colored ambient at noon;
side faces in pits dim correctly; player-built shelters darken by
construction without any time-of-day weirdness. Same model also sets
up the bounce-color bake (Phase 2) and the CSM shadow infrastructure
(Phase 3) — both extend the per-vertex visibility-attribute pattern
introduced here.

Tests: 61 passing (up from 53). Native + wasm release + server clean.
2026-05-24 00:00:04 -06:00
Maximus Gorog
8aafc7a939 Add ARCHITECTURE.md mapping the functional-core/imperative-shell split
Documents every module's responsibility, the tick pipeline, the
sim::lighting shared-truth invariant (sun direction shared between
Rust and WGSL via shader_source.rs), and the test coverage. Companion
to DEPLOY.md — that one covers ops, this one covers code.
2026-05-23 23:33:02 -06:00
Maximus Gorog
55276b7ce0 Phase 4: split state.rs → bridges.rs + app.rs
state.rs is gone. Its content split by responsibility:

  src/bridges.rs (~290 lines)
    The only place we own shared mutable state. Holds the four
    thread_locals (touch input, game status, network, settings) and
    the wasm-bindgen JS interface that mutates them. Exposes a typed
    accessor API — current_settings(), snapshot_touch_bridge(),
    with_touch_bridge(f), is_touch_mode(), clear_touch_inputs(),
    take_respawn_request(), set_status(hp, alive), take_inbox(),
    push_outbox(msg), with_net_bridge(f), net_connection_snapshot(),
    is_connected(), set_my_id(id), snapshot_remote_players(). Callers
    never touch the RefCells; if storage ever moves to a Mutex or
    OnceCell, only this file changes.

  src/app.rs (~510 lines)
    The App + winit ApplicationHandler + tick + drain_net_inbox +
    do_respawn + render_frame + FrameClock. Uses the bridges
    accessors instead of poking thread_locals directly, so the call
    sites read as a pipeline rather than a chain of `.with(|x|
    x.borrow_mut())`.

  src/lib.rs
    Modules now declared in alphabetical order:
      app, bridges, camera, mesh, net, proto, render,
      shader_source, sim, world.
    `run()` constructs `app::App::default()` instead of the old
    `state::App`.

  src/render/mod.rs
    `use crate::bridges::RemotePlayer` (was `crate::state::`).

Net effect of the four phases combined: state.rs at the start of
alpha-0.0.2 was 2500 lines doing everything; today the same logic
spans nine focused modules totalling ~3000 lines (with full doc
comments). 53 tests pass, native + wasm release build green, server
build green.
2026-05-23 23:27:03 -06:00
Maximus Gorog
549662ddc8 Phase 3: extract render/ module from state.rs
state.rs is now 870 lines (down from 1700+). All GPU code moved to a
dedicated render/ module.

New src/render/:
  mod.rs           Renderer struct + impl with all methods; the only
                   place that owns wgpu device/surface/pipelines. The
                   wasm-only WebGPU probe + compat-error overlay live
                   here as a private wasm_compat sub-module.
  pipelines.rs     Pipeline factory functions: camera_bgl, post_bgl,
                   pipeline_layout, sky_pipeline, terrain_pipeline,
                   outline_pipeline, post_pipeline. Each takes
                   device + shader + format and returns a configured
                   wgpu::RenderPipeline — pure factories, no hidden
                   state.
  scene_target.rs  create_depth_view, create_scene_color_view,
                   create_post_bind_group — fresh-resource builders
                   called from Renderer::new and Renderer::resize.
  uniform.rs       CameraUniform (matches WGSL camera.frame layout),
                   OutlineVertex, ChunkBuffers.

state.rs:
  - drops ~760 lines of pipeline + scene-target plumbing
  - imports Renderer from crate::render
  - keeps App + tick + drain_net_inbox + thread_locals + wasm_api

Tests: 53/53 pass. Native + wasm release build clean.
2026-05-23 23:21:47 -06:00
Maximus Gorog
accbf67bf2 Phase 2: shader decomposition + Rust-shared constants
shader.wgsl restructured:
  - Section headers: Camera → Sky horizon → Atmosphere extras → Terrain
    lighting decomposition → Pipelines.
  - sky_dome() is now the single source of truth for the horizon→zenith
    mix; sky_color() builds on it instead of duplicating the gradient.
  - fs_main decomposed into named helpers:
      ambient_term(n, sun, day)    hemisphere ambient via sky_dome lookup
      direct_sun_term(n, sun)      Lambert × visibility ramp
      leaf_jitter(world_pos)       per-pixel canopy variation
      fog_factor(dist)             0..1 fog ramp
      apply_fog(lit, dist, view)   defers sky_color call until needed
  - Camera.misc renamed to Camera.frame with named accessors
    scene_time() / eye_world() so callsites read intent.
  - DAY_PERIOD / SUN_OFFSET REMOVED from the shader — injected at the
    top by shader_source::wgsl_constants_header(). One source of truth
    in sim::lighting now drives both Rust and WGSL; mob burn and
    visible sun direction can never disagree.

New src/shader_source.rs:
  - wgsl_constants_header()  Rust → WGSL constants prelude
  - terrain_shader_source()  header + shader.wgsl, used at Renderer::new
  - post_shader_source()     pass-through

state.rs CameraUniform:
  - misc → frame to match the renamed WGSL field
  - Renderer uses the assembled shader source instead of include_str! directly

Tests: 53 passing (added 2 shader_source assembly tests). Native build,
wasm release build, server build all green.
2026-05-23 23:15:25 -06:00
Maximus Gorog
989de4f43d Phase 1: sim/lighting + sim/visibility + mesh utilities
New modules:
  src/sim/lighting.rs   - DAY_PERIOD, SUN_OFFSET constants; sun_direction,
                          day_strength, twilight_amount, LightingFrame::at;
                          is_in_direct_sun(world, p, t) via 3D DDA — the
                          mechanical sunlight predicate that future mob
                          burn / plant growth / shade pathfinding will all
                          consume. Mirrors the shader's sun math exactly
                          (Phase 2 will wire the WGSL side to consume the
                          same constants from this module).
  src/sim/visibility.rs - compute_visible_chunks moved out of state.rs;
                          frustum_planes + chunk_in_frustum decomposed.

Moves into mesh.rs:
  emit_oriented_box, name_hash + their tests. These are mesh utilities
  (Vertex output, color hash for remote-player boxes), not GPU shell.

state.rs:
  - drops the moved functions
  - imports compute_visible_chunks / emit_oriented_box / name_hash from
    their new homes
  - 230 lines lighter

Tests: 51 passing (up from 45). New coverage: sun-below-horizon=>night,
open-sky-at-noon-is-lit, occluded-by-overhead-block, ramp shapes match
the WGSL smoothstep, name_hash determinism.
2026-05-23 23:12:01 -06:00
Maximus Gorog
c6f50bcb50 DEPLOY.md: add active-deployment runbook for voxel.mxvs.art
Records the live production state (Linode IP, paths, redeploy command,
firewall + fail2ban + SSH hardening, TLS via Caddy, DNS via Namecheap
Advanced DNS, rollback steps, troubleshooting checklist) so a fresh
session can pick this up without re-deriving any of it.
2026-05-23 22:52:43 -06:00
Maximus Gorog
e4cf5a9bed Refactor: extract functional core into sim/ and net/
Reshape the engine into a functional-core / imperative-shell split.
The 2500-line state.rs blob is now 1700 lines of mostly GPU plumbing
plus a thin tick() shell that composes pure transformations from the
new sim/ and net/ modules.

src/sim/ (no GPU, no winit, no thread-locals — all pure):
  body.rs       PlayerBody value type + take_damage / respawned_at
  collision.rs  AABB primitives, sweep_axis returning (Vec3, bool)
                instead of mutating &mut Vec3
  edit.rs       block_from_u8, apply_edit, chunks_for_edit
  event.rs      SimEvent enum (Landed, VoidDeath, BlockEdited)
  input.rs      TouchBridge data type + merge_held
  physics.rs    step_movement(world, body, MoveInput) -> MoveOutcome
                — the central morphism: one tick of player movement
                + collision + landing detection + void check, returns
                a new body value and a list of events
  spawn.rs      find_safe_spawn, fall_damage

src/net/:
  parse_inbox(Vec<String>) -> Vec<NetEvent>
  Malformed lines drop silently; the shell folds typed events into
  the world / remote-player map.

src/state.rs:
  - App now holds a single PlayerBody value instead of scattered
    velocity / on_ground / hp / alive / max_y_since_ground fields.
  - tick() is a pipeline: collect input → merge_held → step_movement
    → fold sim events → block interaction → render_frame.
  - drain_net_inbox() parses to events first, then applies.
  - render_frame() extracted so the paused and active branches share
    the upload + cull + draw path.

Tests: 45 passing (up from 33). New coverage for the physics step
itself: airborne gravity, jump-only-when-grounded, long-fall-emits-
Landed, void floor, dead-body-does-not-move. Net parsing tests for
malformed lines and round-trips. Body tests for damage / respawn.

Build: cargo test (45/45), cargo build --target wasm32-unknown-unknown
--lib --release, and the axum server all green.
2026-05-23 18:55:05 -06:00
Maximus Gorog
f239a939ce Add Docker + Caddy deploy for voxel.mxvs.art
Multi-stage Dockerfile compiles wasm client + axum server in one Rust
builder and copies into a debian:bookworm-slim runtime (non-root uid).
docker-compose.yml binds localhost:8080 by default; docker-compose.prod.yml
replaces ports with a Caddy reverse proxy on host 80/443 that talks to
the voxel container over the internal network. Caddy auto-issues Let's
Encrypt certs.

DEPLOY.md covers the three deployment modes (local-only, VPS with
Cloudflare or Caddy, Cloudflare Tunnel from a workstation).
2026-05-23 18:45:05 -06:00
Maximus Gorog
b52c1927cf Render + UI polish since pre-alpha-0.0.1
- Greedy meshing now bakes per-vertex AO with 4-corner sampling and an
  anisotropic-diagonal split when corner AO disagrees.
- WGSL: extracted sky_dome() for hemisphere ambient sampling so vertical
  faces match the sun-side sky tint at day; ambient_strength mixed by
  day strength instead of a flat constant.
- Step-1 post pipeline: render scene into an offscreen color texture,
  pass-through to the surface. Foundation for FXAA/shafts that will
  follow.
- Input bug: merge_held() now recomputes per tick from sticky keyboard +
  live touch bridge, so releasing the joystick actually stops the
  player (previous OR-into-self bug ate playtests).
- Touch UI hit-zones reordered (menu/hotbar above the joystick z-index);
  hotbar widened to 10 slots with tap-to-select on mobile.
- find_safe_spawn anchors on natural_surface_y so spawn is deterministic
  from noise — towers built at spawn no longer climb the spawn point.
- move_axis is sub-stepped (0.45-block max) so high-velocity falls can't
  teleport the player inside terrain.
2026-05-23 18:44:56 -06:00
Maximus Gorog
3a4ae970b2 pre-alpha 0.0.1 — initial multiplayer voxel sandbox
Web/wasm Rust voxel game with:
- wgpu 23 client (WebGPU when available, WebGL2 fallback)
- Chunked terrain (17x17 chunks, deterministic value-noise generator)
- Greedy meshing with frustum + distance culling
- Sky shader, leaf wind shader, distance fog
- Player physics: substepped AABB collision, gravity, fall damage,
  natural-surface respawn
- Touch UI (MCPE-style joystick + jump/break/place/sprint),
  gamepad polling with axis calibration, mouse+keyboard
- HP / death-screen / respawn flow
- 10-slot hotbar with mouse-wheel + hotkey + tap cycling
- Settings menu (mouse sens, FOV, render distance, input mode toggle)
- Axum multiplayer server: WebSocket protocol, edit log,
  10Hz player broadcasts
- 31 unit tests covering spawn invariants, collision sweeps,
  raycast hit/miss, greedy mesh winding, fall damage, oriented
  box rotation, hotbar block roundtrip, and the input-merge
  regression that latched movement after touch release

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:33:47 -06:00