BSP map rendering with textures, lightmaps, and skybox #7

Merged
kit merged 10 commits from feature/4-bsp-rendering into dev 2026-03-17 01:06:15 +00:00
Owner

Summary

Implements full BSP map rendering in the web client (closes #4).

BSP Parser (bsp/parser.js)

  • Reads BSP v19-v21 header and 64-lump directory
  • Parses: vertexes, edges, surfedges, faces (56-byte dface_t), planes, texinfo, texdata, models, dispinfo, dispverts, entities
  • Texture name string table resolution

Geometry (bsp/geometry.js)

  • Fan-triangulates convex brush faces from surfedge indirection chain
  • Displacement surface tessellation (bilinear interpolation + offset vectors)
  • Filters nodraw/sky/hint/skip faces
  • Source→Three.js coordinate conversion
  • Per-face texture UVs from texinfo.textureVecs
  • Per-face lightmap UVs from texinfo.lightmapVecs (brush) and grid interpolation (displacements)
  • Face predicate filter for splitting world/skybox geometry

Lightmap Atlas (bsp/lightmap.js)

  • Reads ColorRGBExp32 samples from LIGHTING lump
  • Shelf-packs 6891 face lightmap rects into 2048x512 atlas
  • HDR→LDR tonemapping with exposure + gamma

Material Textures (bsp/vtf.js, bsp/vmt.js, bsp/material.js)

  • VTF decoder: DXT1, DXT3, DXT5, RGBA/BGRA/RGB/BGR formats
  • VMT parser: extracts $basetexture, follows patch/include directives
  • Instance texture fallback: maps/<mapname>/material_X_Y_Z → base material
  • Custom ShaderMaterial for proper dual-UV rendering (texture × lightmap)
  • 181/183 faces textured (remaining 2 are water shader materials)

Skybox (bsp/skybox.js)

  • 2D skybox: 6 VTF sky face textures as BoxGeometry
  • 3D skybox: sky_camera entity detection, geometry splitting by proximity, 16x scale, camera-tracking position

Stats (gm_construct)

  • 41,332 world triangles + 5,750 skybox triangles
  • 187 material groups, 342 assets loaded
  • BSP v20, 50 active lumps

Known limitations

  • 3D skybox terrain can occlude 2D sky when looking steeply up (needs multi-pass rendering)
  • Water materials not supported (special shader)
  • Skybox face orientation may have minor seam issues
## Summary Implements full BSP map rendering in the web client (closes #4). ### BSP Parser (`bsp/parser.js`) - Reads BSP v19-v21 header and 64-lump directory - Parses: vertexes, edges, surfedges, faces (56-byte `dface_t`), planes, texinfo, texdata, models, dispinfo, dispverts, entities - Texture name string table resolution ### Geometry (`bsp/geometry.js`) - Fan-triangulates convex brush faces from surfedge indirection chain - Displacement surface tessellation (bilinear interpolation + offset vectors) - Filters nodraw/sky/hint/skip faces - Source→Three.js coordinate conversion - Per-face texture UVs from `texinfo.textureVecs` - Per-face lightmap UVs from `texinfo.lightmapVecs` (brush) and grid interpolation (displacements) - Face predicate filter for splitting world/skybox geometry ### Lightmap Atlas (`bsp/lightmap.js`) - Reads ColorRGBExp32 samples from LIGHTING lump - Shelf-packs 6891 face lightmap rects into 2048x512 atlas - HDR→LDR tonemapping with exposure + gamma ### Material Textures (`bsp/vtf.js`, `bsp/vmt.js`, `bsp/material.js`) - VTF decoder: DXT1, DXT3, DXT5, RGBA/BGRA/RGB/BGR formats - VMT parser: extracts `$basetexture`, follows `patch`/`include` directives - Instance texture fallback: `maps/<mapname>/material_X_Y_Z` → base material - Custom ShaderMaterial for proper dual-UV rendering (texture × lightmap) - 181/183 faces textured (remaining 2 are water shader materials) ### Skybox (`bsp/skybox.js`) - 2D skybox: 6 VTF sky face textures as BoxGeometry - 3D skybox: `sky_camera` entity detection, geometry splitting by proximity, 16x scale, camera-tracking position ### Stats (gm_construct) - 41,332 world triangles + 5,750 skybox triangles - 187 material groups, 342 assets loaded - BSP v20, 50 active lumps ### Known limitations - 3D skybox terrain can occlude 2D sky when looking steeply up (needs multi-pass rendering) - Water materials not supported (special shader) - Skybox face orientation may have minor seam issues
JS-native BSP v19-v21 parser that extracts geometry from Source Engine
map files and renders them in Three.js:

Parser (bsp/parser.js):
- Reads BSP header and 64-lump directory
- Parses vertexes, edges, surfedges, faces, planes, texinfo, texdata,
  models, dispinfo, and dispverts lumps
- Typed struct reading via DataView

Geometry builder (bsp/geometry.js):
- Fan-triangulates convex brush faces from the surfedge indirection chain
- Tessellates displacement surfaces (bilinear interpolation + offset vectors)
- Filters nodraw/sky/hint/skip/trigger faces
- Source→Three.js coordinate conversion
- Per-face normals from BSP planes, per-triangle normals for displacements

Loader (bsp/loader.js):
- Takes BSP ArrayBuffer, produces Three.js Mesh
- Adds to scene, manages lifecycle

Tested on gm_construct: 41,332 triangles, v20 BSP, 50 active lumps.
Geometry is structurally correct including displacements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Builds a lightmap texture atlas from the BSP LIGHTING/LIGHTING_HDR lump
and maps it onto the geometry:

Lightmap atlas (bsp/lightmap.js):
- Reads per-face lightmap samples from LIGHTING lump (prefers HDR)
- Decodes ColorRGBExp32 format (RGB + signed exponent)
- Tonemaps HDR to LDR with exposure + gamma correction
- Shelf-packs 6891 face lightmap rects into a 2048x512 atlas
- Computes per-vertex lightmap UVs from texinfo.lightmapVecs

Geometry builder updated:
- Outputs UV attribute with lightmap atlas coordinates
- Passes face index and texinfo through to vertex generation
- Displacement triangles also get lightmap UVs

Loader updated:
- Uses MeshBasicMaterial with lightmap texture when available
- Falls back to MeshLambertMaterial if no lighting data
- Increased fog distance for map-scale rendering

Tested on gm_construct: visible light/shadow variation, correct
gradients on ceilings and walls, dark underground areas properly unlit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full material texture pipeline for BSP map rendering:

VTF decoder (bsp/vtf.js):
- Parses VTF header (version, dimensions, format, mip levels)
- Decodes image formats: DXT1 (BC1), DXT3 (BC2), DXT5 (BC3),
  RGBA8888, BGRA8888, ABGR8888, ARGB8888, RGB888, BGR888
- Software DXT decompression to RGBA for universal WebGL support
- Creates Three.js DataTexture with repeat wrapping and mipmaps

VMT parser (bsp/vmt.js):
- Extracts $basetexture path from Valve KeyValues format
- Handles basic material properties (translucent, alphatest)

Geometry builder restructured:
- Faces grouped by material/texture name (187 groups for gm_construct)
- Computes both texture UVs (from texinfo.textureVecs) and lightmap UVs
- Returns array of { textureName, geometry } for per-material rendering

Loader updated:
- Creates per-material meshes in a Three.js Group
- Async texture loading: VMT → extract $basetexture → load VTF → decode → apply
- Lightmap applied as lightMap on MeshBasicMaterial with uv2 channel
- 34 textures successfully decoded for gm_construct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skybox (bsp/skybox.js):
- Reads sky name from worldspawn entity (entity lump parser added)
- Loads 6 VTF sky face textures (rt, lf, bk, ft, up, dn)
- Creates skybox as 6 large planes with BackSide rendering
- gm_construct sky "painted" loads successfully

Parser additions:
- readEntities() parses the text entity lump into key-value objects
- getWorldspawn() convenience method

310 assets now loaded for gm_construct (BSP + MDL + VMT + VTF).

Known issues:
- Skybox face orientation needs adjustment
- Scene too dark — lightmap intensity needs tuning
- Will address in follow-up commits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skybox:
- Rewritten to use BoxGeometry with 6 materials (simpler, correct)
- Properly renders behind all geometry with renderOrder=-1
- Fallback to dark blue for missing sky faces
- Disables fog on skybox

Lighting:
- Increased lightMapIntensity from 1.0 to 2.5
- Removes fog when skybox is present

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes:
- Always push UV2 values for every vertex (even when face has no
  lightmap rect), preventing attribute array misalignment
- Displacement faces use grid (s,t) interpolation for lightmap UVs
  instead of lightmapVecs dot product on displaced positions
- Confirmed dface_t is 56 bytes in BSP binary (not 54 as C header
  suggests — dispinfo and surfaceFogVolumeID are separate fields)

Known issue: lightmap UVs still produce corruption on displacements
and some brush faces. Root cause is in the UV computation, not the
atlas packing or struct parsing. Lightmaps disabled in loader until
this is resolved — base textures render correctly without them.

Fallback uses MeshLambertMaterial with directional lighting for
reasonable appearance without lightmaps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three.js MeshBasicMaterial.lightMap doesn't properly read from the uv2
attribute in all cases — the shader compilation fails to include the
uv2 varying when geometry has both uv and uv2 channels.

Fix: custom ShaderMaterial (bsp/material.js) that explicitly declares
attribute vec2 uv2 and reads baseTexture from uv, lightmapTexture
from uv2, multiplying them together. This gives us full control over
both UV channels.

Tested: displacements and brush faces both render correctly with
textures × lightmaps. No more vertical streak artifacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Texture loading:
- Handle Source "patch" VMTs that use include directives instead of
  direct $basetexture — follow the include to the base material
- Fall back to base material name for map instance textures
  (maps/<mapname>/material_X_Y_Z -> material)
- Parse include directive in VMT parser
- Share decoded VTF textures across material groups (avoid re-decoding)
- 181/183 faces now textured (remaining 2 are water shader materials)

3D Skybox:
- Detect sky_camera entity and its position/scale
- Split world geometry into main world and skybox area based on
  proximity to sky_camera (3000 unit radius)
- Render skybox area scaled up by sky_camera.scale (16x)
- Update skybox position each frame to track camera
- Face predicate filter in geometry builder for spatial splitting

gm_construct: 342 assets, 41K triangles main + 5750 skybox

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Precompute sky_camera offset once during load instead of mutating
  the vector every frame (was causing exponential drift)
- Set render order for proper layering: 2D skybox → 3D skybox → world

Known limitation: 3D skybox terrain can occlude the 2D sky when
looking steeply upward — proper fix requires multi-pass rendering
with depth buffer clears between passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrates Source Engine model parsing using adapted code from
gkjohnson/source-engine-model-loader (MIT licensed).

MDL module (client/src/mdl/):
- MDLLoader.js: parses studiohdr_t, bones, body parts, meshes, textures
- VTXLoader.js: parses optimized triangle strip/list index data
- VVDLoader.js: parses interleaved vertex data (pos, normal, UV, bone weights)
- loader.js: ties parsers together with asset cache and scene manager
- LICENSE: MIT attribution for vendored parsing code

Model loading pipeline:
- Scene manager now tries to load real models for each entity
- Falls back to colored placeholder boxes while models load
- Replaces placeholders with real geometry when model finishes loading
- Model templates cached and cloned for duplicate entities
- Async texture loading via VMT/VTF pipeline (reuses BSP texture code)
- Backslash path normalization for Windows-style material paths

Integration:
- ModelLoader wired into SceneManager via modelLoader + modelTable
- 19 unique models loaded, 24 entities with real geometry on gm_construct
- 426 total assets loaded

Known issues:
- Player model (kleiner.mdl) fails to parse (DataView bounds error)
- Brush model entities (*1, *2) still use placeholders
- No animation support yet (static pose only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kit merged commit c51be5b54c into dev 2026-03-17 01:06:15 +00:00
kit deleted branch feature/4-bsp-rendering 2026-03-17 01:06:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kit/gmod-web-stream!7
No description provided.