The glb-surface.1 profile adds tangent-space normal maps and ambient occlusion to the imported material path. The Engine Observatory now finishes with an original furnished room study: chamfered walnut joinery, warm plaster, brass fittings, an arched garden window, a woven rug, a writing table and amber lanterns. It is development content in Engine Zoo; the default playable Darkwind world remains unchanged.
Run the development build's /diagnostics.html and choose Run the checks. The last image is the Hearth study, rendered by the same WebGPU renderer used for gameplay. The panel releases its workers, canvases and GPU ownership after retaining the image. The study is a static art fixture, not a new playable or multiplayer place.
#Portable surface contract
Models with authored tangents or normal/occlusion maps and no mask materials use format 3 and glb-surface.1. Format 1 and format 2 keep their existing identities. This stage introduced isolated build jobs asset-job.3 / glb-compiler.3 (now superseded by the masked profile); old cached compiler jobs cannot satisfy this contract. A primitive retains its existing eight-float position/normal/UV layout and may carry a separate four-float tangent array. Collision continues to read position/index geometry independently.
Provided TANGENT is float VEC4, one frame per vertex. Frames must be unit length, perpendicular to the normalized normal, with handedness exactly +1 or -1 and consistent within each triangle. Missing normals are generated flat; in that case authored tangents are ignored as required by glTF. Missing tangents for a normal map are generated with default MikkTSpace from expanded triangle corners, then vertices are welded only when position, normal, UV and all four tangent values match. UV seams and handedness changes cannot be averaged together. Occlusion-only surfaces use deterministic orthogonal fallback frames.
Normal maps are linear RGB, decoded to [-1,1], with scale applied to X/Y before normalization. This bounded profile accepts finite scale in [-100,100], including zero and negative values. An exactly cancelled sampled vector falls back to the geometric normal. Normal alpha is unused. Occlusion is linear red, with strength in [0,1] and factor 1 + strength * (red - 1). It multiplies ambient illumination only; direct sun, point lights and emission remain unchanged. The Khronos glTF specification defines these material conventions.
World normals use the inverse transpose; tangents use the linear transform and are reorthogonalized against the world normal. The bitangent is cross(N,T) * W. Reflected prepared geometry flips tangent X and W alongside reflected positions/normals and reversed winding. Back faces flip the reconstructed normal for double-sided shading. The instance ABI stays at 112 bytes. Two surface pipelines bring the complete atomic shader bundle to nine under forward-shaders.4.
#Bounded MikkTSpace distribution
The pinned development dependency is mikktspace 1.1.1. Runtime code uses a checked-in derivative of its release WASM, with only the memory section changed to a hard maximum of 1,024 pages (64 MiB). tools/cli/vendor-mikktspace.mjs verifies the upstream binary SHA-256, applies this change and regenerates the embedded bytes without network access. The wrapper verifies the derivative hash, caches only immutable compiled code, and creates a fresh memory instance per model import. Input generation is bounded to 131,072 expanded corners; the same emitted-vertex limit applies after seam splitting. This is a WASM-memory bound, not total process RSS.
The upstream integration guidance requires unwelded input and flipping output W for glTF conventions. Tests compare the adapter to the upstream npm build and independently check authored frames and actual rendered light response. public/mikktspace-notices.txt carries upstream and release dependency notices; the release Cargo lock and checksum-verified notice generator are retained. Browser and standalone author bundles both carry these notices and require no runtime WASM download or development dependency.
#Resource accounting
Each tangent-bearing oriented mesh owns a separate vertex buffer. A distinct surface material group owns a 16-byte uniform containing normal scale, occlusion strength and whether a normal map is present. Images, samplers and material groups are deduplicated within each candidate. Normal/occlusion texture slots share the existing white fallback; occlusion-only draws bypass normal perturbation. Catalog admission includes tangent bytes and conservative surface uniform bytes. Accepted-plus-candidate logical storage remains capped at 32 MiB, including geometry, tangents, uniforms, unique image mips and the fallback.
The two-triangle analytic panel owns 276 logical bytes, four model buffers and three model textures (including fallback). The existing three-map panel retains its 552-byte identity/accounting. Partial allocation failures destroy all candidate resources; invalid surface artifacts preserve the prior frame; loss and reconstruction restore surface catalogs from validated CPU data.
#Fixtures and verification
Regenerate original fixtures with:
node --import tsx tools/cli/surface-model-fixture.ts
node --import tsx tools/cli/material-study.ts
node tools/cli/vendor-mikktspace.mjsThe Hearth study contains 5,305 triangles in seven material primitives, 16 source images, original deterministic textures and chamfered geometry. Its generator is content tooling, outside the engine packages. It does not add a graphics dependency or alter the default world hashes.
Headless tests cover generated/authored frame agreement, mirrored UV seams, ignored tangents with generated flat normals, malformed frames and rehashed metadata, the native memory maximum, portable isolated compiler execution, reflected geometry, exact resource accounting, allocation failures, and reproducible authored content. The browser worker compiles the same GLB to the Node-pinned identity. It compares seven normal-map lighting cases and one occlusion-strength case against geometric/material references, with at most one eight-bit channel value of error across the complete image. Additional controls cover zero scale, ignored alpha, AO red-only semantics and endpoints, independent direct sun/point/emission behavior, replacement, rejected candidates, catalog deduplication and actual device destruction/reconstruction.
The subsequent masked profile adds alpha cutoff, matching shadows and ivy. Alpha blending, skeletal animation, streaming/eviction, production environment replacement, larger art kits and physical-device acceptance remain open. No full engine milestone is closed by this increment.
At the surface-stage acceptance: all 235 headless tests passed; type/package boundaries, default-world hashes and the production server build pass. All 26 browser scenarios pass with zero skips, failures or retries, including the surface controls and furnished study. The local Docker daemon is unavailable. Subsequent CI on evidence commit 8b51363 passed core, container, probe and multiplayer jobs. The player job failed during a screenshot in the graphics-recovery visibility check; the world was visible in the trace, but capture took about 75 seconds against a 60-second poll budget. The capture stall cause remains unproven; this is not an all-green CI result.
Source cdadb44: acceptance record, worker evidence, native study render, desktop Observatory and narrow Observatory.