Skip to content

67js 0.15.0

Choose a tag to compare

@anxuanng anxuanng released this 05 Nov 11:03
  • Add fog-valley and fog-room to the reference suite (part of issue #165, which stays open). Against Cycles fog-room scores 0.1953 (EEVEE 0.3730) and fog-valley 0.1967 (EEVEE 0.1952), so the fog constants of #161 hold on both. Scaling the fog ambient by the share of the sky the volume sees was measured and not kept: it worsened the region of temple-hall (0.2305 to 0.2513), so #164 is not fixed. See bench/reference/README.md.

  • Fix a thin bright line at the corners of baked rooms (issue #165). The texel just outside a lightmap chart took the average of the neighbouring chart, so the wall of dark-shaft read 128 of 255 sky visibility at its bottom edge where the interior reads 1. rasterizeTexels in src/light-bake.js now gives that texel to the nearest triangle. Baked dark-shaft goes from 0.0653 to 0.0607 and temple-hall from 0.0578 to 0.0559 against Cycles.

  • Add three options for each grand reference scene, to pick from (part of issue #168, which stays open until An picks). bench/reference/grand-scenes.js holds mountain-valley, temple-hall and megacity-dusk as -a, -b and -c, each with its own composition, light and time of day, drawn by npm run reference -- --grand into grand.json and images/grand-<id>.jpg (baseline.json and the existing images are unchanged). The Blender side gained a gradient world, written as the same equirectangular image 67js draws. bench/reference/grand-sheets.py makes one comparison sheet per scene.

  • Hide sky light where the scene blocks it: inside tree crowns and on a valley floor (issue #191, part of it). createTree bakes a sky visibility into each foliage vertex from the leaf density above it, and createTerrain bakes one into each terrain vertex from the horizon in 16 directions. The shader multiplies it into the environment light only, and meshes without it read 1. On bench/reference, the foliage crown went from 0.298 to 0.200 (Cycles 0.175) and the score from 0.1173 to 0.1023 (EEVEE 0.0888); mountain-valley went from 0.3483 to 0.2623 (EEVEE 0.1879), and its floor from 0.417 to 0.360 (Cycles 0.272). No other scene changed to four digits. The agent scene geometry bound allows 4 more bytes for each terrain vertex.

  • Compare trees like with like (issue #166, part of it). The Blender side of bench/reference now rebuilds the 67js leaf model for foliage materials (back-face normal, colour and normal variation, sky reflection 0.3, and light from behind as a Translucent BSDF with weight 0.3 * pi / (3.7, 3.4, 2.9)), and the 67js back light is now shadowed by the sun shadow. foliage goes from 0.1290 to 0.1173 against Cycles (EEVEE 0.0864 to 0.0888) and mountain-valley from 0.3473 to 0.3483 (EEVEE 0.1828 to 0.1879); no other scene moved. The remaining gap is canopy and valley sky occlusion, recorded in bench/reference/README.md.

  • Make the recipe bake presets bake large rooms (issue #189). A preset that does not fit the 5 million path step budget at 16 samples and 1 texel per unit now gives up bounces one at a time instead of refusing, and bake.reduction tells the agent what it asked for, what ran and that explicit samples, bounces and texelsPerUnit (up to 24 million path steps, 20 s inline) buy more. temple-hall at standard now bakes (16 samples, 1 bounce, 1.37 texels per unit, 0.0705 against 0.1504 unbaked with a probe). fine is now the same as standard: five samples and density mixes within the 20 s limit scored no better on the room scenes. Table in bench/reference/README.md.

  • Score the recipe bake presets against Cycles (part of issue #170, open for the frame costs). standard scores 0.127 to 0.159 on the three rooms (no bake 0.51), fine is no better, and standard and fine refuse temple-hall. Table in bench/reference/README.md.

  • No test asserts real elapsed time any more (issue #179): work counts and a fake clock replace stopwatches. smoke:agent-review-close and smoke:film-physics are wired; the second starts its own server.

  • Show the floor and the other parts in smooth metal (part of issue #167, which stays open). traceObjectProbes in src/probe-trace.js gives each part with metallic of at least 0.5 and roughness of at most 0.6 a reflection probe of its own, traced from the middle of the part without it (up to 8 parts, lit by direct light and the environment's diffuse light), and placeProbes assigns it. metal scores 0.1294 to 0.1036 against Cycles (EEVEE 0.0916), brushed-film 0.1149 to 0.1029, car-paint 0.1059 to 0.1038, material-spheres 0.1312 to 0.1293; every scene without a reflective part is unchanged to four digits and draws the same frame (metal 1.51 ms plain, 1.58 ms with four probes). npm run reference -- --object-probes places them (opt in; the committed baseline is unchanged); recipes do not yet.

  • Make every test file pass on its own, and check it in CI (issue #172). node --test tests/agent-bake.test.js failed 16 of 19 because npm test preloaded every on-demand module into each test process, so 28 files only passed through that preload. The loading path agents use was fine (tests/agent-scene-on-demand.test.js runs it in clean processes). Each of those files now imports tests/support/load-agent-scene-modules.mjs itself, npm test no longer preloads, and npm run test:isolated (scripts/test-each-file.mjs) runs each file in its own process, 4 at a time at below-normal priority, rerunning a failure alone once so a busy machine does not count. It runs in the Test workflow.

  • Fix links in the npm package that pointed at docs it does not ship (issue #178). docs/guide.md, docs/material-maps.md, README.md and ROADMAP.md now link the 20 unshipped docs, CONTRIBUTING.md, the texture credits and the changelog by absolute GitHub URL; shipping those docs would add 64 KB gzip. npm run check:links:package packs the archive, unpacks it and checks every link inside (26 files, 0 broken), and the Test workflow runs it.

  • Take 34 KB of headroom under the npm archive budget without raising it (issue #177). CHANGELOG.md no longer ships in the package, because GitHub serves it; the archive went from 740,972 to 705,961 bytes against the 740,000 budget. bench/size/README.md now says how the budget is set and what it protects.

  • Add a test that fails when the MCP tool count in docs/guide.md, docs/mcp.md, index.html or scripts/smoke-mcp.mjs differs from the tools/list result (issue #180). The server registers 22 tools; the 23rd register( in the file is the function definition, so the documented count was right.

  • Remove the AGPL-3.0 @codepawlhq/orglet-ui package, React and react-dom from the website (issue #176). The site now uses its own tokens and button in src/site-tokens.css and src/site-controls.js, and screenshots of the pages match before and after. The site JavaScript drops from 1,778,425 to 1,531,367 bytes. The published package was never affected.

  • A renderer no longer draws its shadow map a second time on the first frame when the frame has no assembly; the constructor already drew it. On mountain-valley that removes 9.72 million shadow triangles from the first frame, with identical pictures (issue #171). node bench/reference/profile-scene.mjs <scene> prints where a scene's time goes, and frame-time.mjs --plain measures scenes that are too large for the light bake.

  • The shadow pass skips instances outside the light and draws each shadow caster at a level chosen from the world size of one shadow texel (issue #171). A part can carry shadowLods, in the format of lods, and the renderer uses the coarsest level whose error is within one texel of an orthographic shadow map; tree bark carries a trunk-only level. On mountain-valley the shadow pass fell from 9.72 to 2.34 million triangles; the score against Cycles moved from 0.3473 to 0.3478 and the other scenes kept theirs.

  • Tell an agent when its scene is a closed room with no bake (issue #163). scene_inspect now returns a closed-room-without-bake diagnostic when 90 percent or more of 48 rays from the middle of the scene hit a front face, and names bake; measured against Cycles a closed room goes from 0.1430 and 0.1436 (dark-shaft, temple-hall) to 0.0609 and 0.0560 once baked. No frame cost and no schema change; agent scene entry grows 884 bytes gzip. bakeSkyVisibility in light-bake writes only the occlusion map (0.1021 and 0.0856 in under a second) and is not applied automatically. A closed room without a bake now shows a warning in scene_inspect.

  • Clean up docs, media and scripts. Remove stale logs, six unreferenced PNG previews (about 6.1 MB) with their render sidecars, the OpenAI skill metadata and ten scripts that aimed at harness pages that no longer exist. Update the roadmap, the mesh and verification docs and the contributing guide, point the links to unshipped docs at GitHub, and add npm run check:links, smoke:modeling, smoke:material-maps and benchmark:mesh-query.

  • The GitHub repository is anxuanng/67.js, public since 2026-11-02. Links and repository.url point there, which npm provenance needs to match.

  • Follow 3 bounces by default in a light bake, up from 2 (part of issue #159, which stays open). Baked rooms scored worse after the sky visibility change (lit-interior 0.1444 to 0.1915, brick-room 0.1515 to 0.2048 against Cycles) because the occlusion map removed the world light that had been filling the light a 2 bounce path cuts off. With 3 bounces they score 0.1341 and 0.1412, dark-shaft 0.0609 and temple-hall 0.0560. A bake takes about 1.3 to 1.9 times as long (temple-hall 39 to 75 s); pass bounces: 2 for the old light and time. Recipe presets are unchanged.

  • Remove the streaks in baked dark scenes and the speckle in the fog's glow (part of issue #159, which stays open). A bake unwrapped each shared geometry once at unit size, so a 2 x 32 x 180 m wall got texels 90 times longer than wide (carpet 2,867 times), which drew as streaks fanning from the vanishing point; it now unwraps each part at its world size and smooths the gathered light and sky visibility over neighbouring texels of the same surface. The glow reads the frame's mip chain instead of 24 jittered single pixels. bench/reference/noise.py measures what FLIP misses (high frequency energy in dark regions and around the door): temple-hall baked 0.448 to 0.182 and its glow ring 1.826 to 0.990 (Cycles 0.158 and 1.053), dark-shaft baked 0.318 to 0.150; whole frame scores against Cycles stay within 0.006 and temple-hall baked goes from 0.0716 to 0.0578. A baked part's report skyVisibility now weighs texels by area, and baked thin parts get more texels (brick-room 28,576 to 98,848). Core grows 92 bytes gzip and a frame with fog costs about 0.5 ms more on a shared machine. Baked scenes with long or thin parts look smoother and lightmaps are larger.

  • Light the fog around bright sources and keep it dark in closed rooms (part of issue #159, which stays open). volumeFog added a constant ambient everywhere, so the air of a closed room glowed; it now keeps 15 percent of it where the sun's shadow map says the fog is in shadow. It also scatters the light of bright pixels sideways: 24 jittered taps of the frame, weighted by the fog opacity. Against Cycles, dark-shaft goes from 0.2321 to 0.1425 (shaft region 0.2157 to 0.1219), temple-hall from 0.2126 to 0.1438 (region 0.2512 to 0.2341), and the older fog from 0.2643 to 0.2238; all other scenes are unchanged to four digits. Core grows 115 bytes gzip and a frame with fog costs about 0.1 to 0.3 ms more. Fog scenes get brighter around bright sources and darker in shadow.

  • Stop world light from reaching closed spaces in a baked scene, and score bright regions on their own (part of issue #159, which stays open). A bake now records how much sky each texel sees from the rays it already shoots, and a part that sees under 98 percent of it gets that as an occlusionMap read through the lightmap UVs, so the shader's world diffuse light and reflections shrink inside a closed room. A part that sees the sky gets no map, a scene without a bake runs the same shaders, and there is no new recipe field. Against Cycles, dark-shaft (new: a closed room, one roof opening, a sun shaft in fog) goes from 0.2612 with the old bake to 0.1301 and temple-hall from 0.2239 to 0.1129 (EEVEE 0.2653 and 0.2341), and a brightRegion that score.py picks from the Cycles image scores the shaft pixels on their own. The no-bake scores of outdoor-hdri, character, metal, car-paint and foliage are unchanged to four digits. A baked part that sees little sky now draws darker.

  • Let a key shadow block the sun fully. It kept 18 percent of the sun, so a sealed hall was evenly lit and every shadow was too light. This darkens shadows in existing scenes that have no fill light. Against Cycles, temple-hall goes from 0.5841 to 0.2126 (EEVEE 0.2341), mountain-valley from 0.3658 to 0.3473 and long-street from 0.1555 to 0.1407; fog (+0.0031) and megacity-dusk (+0.0044) get worse.

  • Compare reference scenes under one sun, and add mountain-valley, temple-hall and megacity-dusk. The HDRI holds its own sun (6 pixels with 48.1 percent of its light), so Cycles and EEVEE drew two shadows of each object where 67js drew one. Both sides now cap the sky at radiance 16, and the mean of the 16 older scenes against Cycles goes from 0.2001 to 0.1751, then to 0.1732 with the shadow change. khronos.json and path-tracer.json still use the sky with two suns.

  • Add reflection probes and a metal-room reference scene (part of issue #55, which stays open). bake.probes ('room' or up to 4 { position, radius? }) traces the room around a point once, prefilters it through createEnvironmentResources and lets nearby parts reflect it (specular only, no parallax, not in GLB). A recipe with bake places a 'room' probe unless bake.probes is false: against Cycles a probe lowered the score in every baked room (lit-interior 0.1444 to 0.1213, brick-room 0.1515 to 0.1266, metal-room 0.1754 to 0.1537) and in the unbaked rooms (0.02 to 0.06), while a hand placed probe in an open scene scores worse. The bake report now keeps the placed probes under probes (the resolved setting used to hide it). The reference harness --scene option says it does not rescore, and frame-time.mjs prints medians of three rounds. The earlier note that a probe changed nothing on a baked room was wrong. Core growth is 48 bytes gzip (lit-box). npm run reference -- --probes and frame-time.mjs take the new variant.

  • Cut the SwiftShader frame by compiling out what a frame does not use (part of issue #74, which stays open). Measured split of instanced-objects: the lit shader is about 190 ms of 380, the post pass 65, the MSAA resolve 35, the environment background 25 to 30. SwiftShader runs both sides of a choice such as environmentEnabled ? a : b and pays for code behind a uniform that is off, so the main program now takes ENVIRONMENT_ENABLED, USES_SHEEN and USES_CLEARCOAT from the parts and the environment map (features environmentMap, studioLighting, sheen, clearcoat); the post program is built per set of steps in use (createPostFragment: transparent, occlusion, bloom, volume, explosion, finish) and a new variant is built in the background while the full program keeps drawing; the main vertex program takes USES_SKIN, USES_MORPH, USES_WIND, USES_WAVES and USES_EXPLOSION (createVertexShader); and the colour clear is skipped when the environment background covers the frame. No picture changes: all 8 benchmark scenes are pixel for pixel identical. Alternated medians of 3, before to after: large-mesh 1139 to 887 ms, many-materials 983 to 880 ms, instanced-objects 384 to 268 ms. See bench/runtime/PROFILE.md.

  • Add the suite task render-pick and measure branches on it (part of issue #67, which stays open). A gallery with a pillar and a spotlight offered at three places; the litPainting check traces rays from the exported light to 15 painting points against the pillar, so it grades the pick and not the method. 10 runs (Haiku 3 and Sonnet 2, branches on and off) all passed, none called branch, and calls did not drop (Haiku 4 to 6 on, 4 to 5 off; Sonnet 3 either way). With best-of-three that is 22 runs on two tasks with no measured gain from branches. npm run smoke:agent-branches passes on SwiftShader. Bench only; core did not change.

  • Add the weighted_normal and smooth_by_angle modifiers as edits and stack nodes (part of issue #42). smooth_by_angle marks edges over an angle sharp and the rest smooth, overriding the geometry smoothAngle; weighted_normal stores an area-weighted normal per corner that the render extraction uses as is. The edge attribute smooth is new.

  • Add the lattice, curve, hook and wave modifiers as edits and stack nodes (part of issue #42). lattice moves up to 64 control points of a grid over the mesh bounds, curve bends a mesh along a Catmull-Rom path, hook pulls the vertices near a point and wave is a travelling ripple that matches Blender 5.2 at the same frame on two cases. Lists are bounded and the errors name the limit.

  • Add the screw, wireframe and skin modifiers as edits and stack nodes (part of issue #42). screw revolves the edges of a mesh about an axis with steps, iterations and lift, and wireframe leaves a rim on every face with thickness, offset and even offset; both match Blender 5.2 on counts, area and positions (wireframe on three cases, screw on two). skin is a simple version, a closed square tube per edge with a radius per vertex and no branch solver. Sizes are bounded and the errors say what to lower. Details are in scene_capabilities meshModifiers, and the modifier entry of the mesh tool schemas is now one free record that the backend validates, so the schemas no longer grow with each modifier.

  • Compare cloth with a Blender cloth bake by picture and find why the drape did not match (part of issue #62, which stays open: soft bodies are not built). npm run reference -- --cloth renders a pinned banner in wind (1 and 2 s) and a sheet dropped on a ball (0.5, 1 and 2 s) from 67js and from Blender's cloth modifier in Cycles and EEVEE and scores them with FLIP into bench/reference/cloth.json: the banner scores 0.043 against Cycles (EEVEE 0.042) and the drape 0.08 to 0.09 (EEVEE 0.04). The earlier 63 percent drape mismatch was a mirrored vertex order in scripts/blender-cloth.py plus Blender's default air damping, which on a 3 g particle is a drag of about 54 per second against 0.3 here. Cloth damping now accepts up to 100 per second. With the drag matched, the sheet lands like Blender within 5 percent at 0.5 s; Blender then wrinkles into a bell while this solver hangs smooth panels.

  • Add walking routes over terrain with holes (part of issue #64; splat layers stay at four, see docs/terrain.md). addons/navmesh builds a walkable grid from triangles (slope limit, agent radius, holes, obstacle footprints) and findPath runs A* with line-of-sight string pulling, deterministic, with cell and search budgets that throw a RangeError naming what to raise. scene_inspect takes path: { ground, from, to, avoid, ... } and returns keyframes for a node translation track, described in scene_capabilities navigation; the scene_capabilities result grew by 624 bytes and the scene_inspect schema by one record field. The suite gets terrain-path-walk (two holes and a rock) and records pathQueries; the walkPath check takes several openings and avoidNodes. Size: entry:navmesh 3,257 B gzip, agent-scene+lazy +4.5 KB, core entries unchanged.

  • Darken the automatic floor and measure the default look with a blind pairwise judge (part of issue #53). bench/agent-modeling/default-judge.mjs draws the six cold recipes under two defaults from the three-quarter view and the recipe camera (what scene_look draws), shows a Sonnet judge both in a seeded random order and reports wins with an exact binomial p. The current default beats no floor 18 of 18 (p 0.0000076, 9 of 9 in each position); the control, one image against itself, picks "first" 17 of 18, so a tie goes to the first. The floor colour goes from 0.6, 0.6, 0.58 to 0.22, 0.22, 0.21 because the judge named a floor too close to the backdrop: it beats the old floor 18 of 18, while 0.38 won 13 of 18 (p 0.096) and was dropped. Only recipes that get the automatic floor change; the judge spend was $4.50, above its $4 plan. Core did not change.

  • Import SheenChair and score it (part of issue #51). A glTF normal map may now carry its own KHR_texture_transform offset and positive scale (same rotation) on top of the transform the other maps share: the importer bakes the shared one into the UVs and stores the remainder as uvTransform on the normal map descriptor, and the normalMapTransform shader feature in addons/material-layer-shaders applies it (a scene without such a normal map compiles the same shader as before). Any other difference between the maps of one material still fails the import, and an extension texCoord equal to the texture's own is no longer refused. Core grew by 25 bytes gzip (lit-box 60,179 to 60,204): a hook in the normal map lookup, one feature line and the bind argument; the glTF importer grew by about 370.

  • Fix the automatic floor skipping a lone flat object (part of issue #53). A lone part never counts as the recipe's own floor, so a gear lying flat, which spans its whole scene and is the lowest geometry, now gets a floor and a contact shadow (before, agentGroundPlacement returned null for it). The surface judge on the six cold recipes goes from 0 before the floor, to 6 with the first rule, to 7 with this fix (gear 0 to 1; one call per task, so direction only). A flat object with a hub on it is still read as a plate. Core did not change.

  • Try two exact-picture speed-ups for the SwiftShader frame and keep neither (part of issue #74, which stays open; no engine change). Drawing the environment background after the opaque pass on the far plane, and fetching the post pass's extra FXAA taps only on edges, both moved instanced-objects inside the noise (medians 379 to 377 ms and 388 to 386 ms over three alternated pairs). Current ratios against three.js on SwiftShader: large-mesh 6.0, many-materials 5.0, instanced-objects 7.0, so the 3 times target is not met. bench/runtime/PROFILE.md records why: a shader that returns the base colour still costs about 4 times three.js, so the rest needs MSAA, the HDR target or the shadow filter to change, which change the picture.

  • Let the path tracer stop converged pixels, draw normal and roughness maps on box-projected materials, and finish a 1080p still (part of issue #129, which stays open). noiseThreshold (0 to 1, default 0.03, 0 draws every sample) stops a pixel once the standard error of its mean luminance is under that share of the mean plus 0.05, checked from 16 samples on and after every 8 more; the decision reads only the pixel's own samples, so bands and thread counts still give the same bytes, and report.averageSamples says what was used. Shadow rays no longer interpolate the surface they hit unless it is a cutout (pixels unchanged, 1.23 to 1.36 times faster per thread on the demo room). Box-projected parts are traced with the UVs of their projection (boxProjectedGeometry, with columnLengths now exported from src/export-box-uv.js), so their colour, normal and metallic-roughness maps are all read (before, a box-projected colour map read one texel); report.ignored no longer lists them. Render jobs may set maxSeconds up to 600 (was 240; the deadline is maxSeconds plus 60 s): the 1920 by 1080, 64 sample still of tests/fixtures/demo-room.recipe.json took 246.4 s in the tracer (30.7 samples per pixel on average, 8 threads, 257 s per job), 6 s past the old limit. Measured, and not all good: at 24 fixed samples with the filter brick-graph is 0.0024 and character 0.0048 worse than at 64; adaptive sampling costs spot-area 0.0658 to 0.0900 against Cycles at the default threshold; drawing one light at the first hit was tried and dropped (twice as fast, 66 percent more error). brick-graph did not improve against the same tracer at 64 samples (0.1370 to 0.1373); the darker floor of character and outdoor-hdri was narrowed but not found. bench/reference/run.mjs takes --noise-threshold and records averageSamples. Core did not change.

  • Read the textures inside glTF layer extensions and accept TEXCOORD_1 (part of issue #51). clearcoatTexture, clearcoatRoughnessTexture, clearcoatNormalTexture, anisotropyTexture, sheenColorTexture and sheenRoughnessTexture load into material.layerMaps and draw through shader features in material-layer-shaders, and exportGlb writes them back. Occlusion, emissive and metallic-roughness textures may read TEXCOORD_1. Against Cycles, AnisotropyBarnLamp goes from 0.4758 to 0.3584, AnisotropyDiscTest from 0.3481 to 0.2808 and ClearCoatTest from 0.2344 to 0.2306; TransmissionTest is unchanged and SheenChair still waits on per-map texture transforms. The core entry grows by about 345 bytes gzip.

  • Score material scenes and Khronos sample assets against their baseline (parts of issues #50 and #51). brick-flat is the control for brick-graph: the same wall in the graph's average colour with no maps. npm run reference -- --khronos draws Khronos sample assets downloaded into gitignored artifacts/khronos/ and scores them against Cycles reading the same GLB, on the pixels the asset covers (khronos.json, images/khronos-*.jpg; baseline.json is unchanged). The brick bump is not weaker than Blender's: its bright edge is stronger and bump 0 would score better (0.2065 against 0.2269), so no engine change. The assets found two importer faults, fixed in src/gltf.js: KHR_xmp and KHR_xmp_json_ld metadata refused the whole file, and KHR_materials_variants is now accepted with a warning. Still open: texture slots in layer extensions (the anisotropy textures matter, both anisotropy assets score worse with the factor than with none) and occlusion on TEXCOORD_1 (SheenChair).

  • Let recipes reach cascaded sun shadows and light linking (part of issue #52; the lit-interior score goal is still open). environment.shadowCascades (0 to 4) and affects on point, spot and area lights plus environment.keyAffects name nodes; both addons load on demand through src/agent-scene-modules.js, scene_capabilities now reports lightLinking and shadowCascades, unknown node IDs are refused with the next action, and set-environment and scene_export_recipe carry the fields. The light bake honours affects (a linked light adds nothing at a part it skips, even as a bounce) and the path tracer lists linking in report.ignored. Default: a scene without shadowCascades whose shadow.extent is above 40 gets three cascades (the single map is then 4 cm per texel or coarser; long-street scores 0.1555 with one map and 0.1398 with three, for about 0.2 ms on an RTX 3060); every scene at or below 40 draws as before, and shadowCascades: 0 keeps one map. node scripts/smoke-mcp-linking.mjs proves one recipe with both through the MCP server, with a seek giving identical PNG bytes. The MCP schema total stays at its budget by shortening environment descriptions.

  • Add an automatic floor with a contact shadow to the MCP preview, and measure the default look on cold runs (part of issue #53; its done condition is not shown, see below). Breaking look change: a recipe with no floor of its own now draws a light grey disc under its lowest geometry at that height, so the key light's shadow falls on something; before, objects floated on a flat grey backdrop. It is not a recipe node: src/agent-ground.js builds it from world bounds on every draw (agentGroundPlacement), so the exported recipe, scene.parts, silhouettes and the suite's checks do not hold it, and it reaches the renderer through the ground option, which now also accepts a part (about 20 bytes in entry:core). It is left out when a part already forms a floor: one that spans at least 90 percent of the scene's width and depth, is thinner than a quarter of its width, and is the lowest geometry. environment.ground: false switches it off (the field is absent from recipes that do not set it). Nothing else about the default look changed: effects.exposure stays 1 (an exposure of 0.8 was tried and dropped: on the six cold recipes no pixel of the subject reached 250 at either value, and mean luma was 157 to 199 at 1 and 142 to 187 at 0.8), setup daylight, occlusion 0. The suite's look harness drew without the setup's environment map, so its images were darker than the preview shows; it now draws with the map and the floor, and regrade.mjs finds files of runs made with --runs. Measured on six cold Haiku recipes with a reference-free Sonnet vision question ("does the object sit on a surface, with a believable contact shadow", 0 to 3): 0, 0, 0, 0, 0, 0 before and 1, 1, 2, 1, 1, 0 after. The judge named the front and side cameras as the gap: they are orthographic at floor height, so a floor is edge-on and a shadow falls behind the object, and only the three-quarter view shows it. The reference-based judge (shape, proportions, features, surface) tied at 66 of 72 across the dropped exposure change, because it cannot see lighting defaults. Tool schema text was shortened to pay for the ground field.

  • Measure what an equal feature set costs against three.js (part of issue #74; no engine change, no speed-up). bench/runtime gets the engines 67js-ldr, 67js-one-sample, 67js-ldr-one-sample and three-one-sample (run with --engine) and a --cpu-only flag. Measured: with no MSAA and no HDR target 67js is still 5.2 to 5.7 times three.js on SwiftShader in large-mesh, many-materials and instanced-objects (7.8, 5.3 and 7.8 with the defaults), so the gap is the lit shader and not those two features. On the RTX 3060 large-mesh costs 3.6 ms against 0.6 ms and is not explained by HDR, MSAA, the vertex shader or the vertex attributes; on ANGLE Vulkan the ratio is 2.3 instead of 5.8. Skipping zero-intensity fill lights was tried and reverted (inside the noise). The profile is in bench/runtime/PROFILE.md.

  • Add cascaded sun shadows and light linking (part of issue #52; recipes do not reach either, and lit-interior was already improved by the light bake, not by this work). addons/cascaded-shadows: new Renderer67(canvas, parts, { shadowCascades: createCascadedShadows({ count: 3 }) }) fits up to four depth maps to slices of the camera's view, with sphere fitting and texel snapping so edges do not crawl; on the new long-street reference scene the score against Cycles goes from 0.1555 to 0.1398 for about 0.2 ms more a frame on an RTX 3060 (+25 percent on SwiftShader) when the camera moves. addons/light-linking: applyLightLinks(parts, environment) limits a light to the parts named in its affects list (environment.keyAffects for the sun) through a per-part mask in material.lightMask; the new light-linking reference scene scores 0.0998 against Cycles with the same lists as Blender receiver collections. Both keep their GLSL out of the core bundle (registerShaderFeatureCode, new slots sunVisibility, linkSun, linkPoint, linkSpot and linkArea), and npm run reference -- --cascades <n> and frame-time.mjs <scene> <backend> --cascades measure them. No earlier reference scene moved.

  • Fix the recipe light bake budget, which rejected the demo room at samples 96, bounces 2, texelsPerUnit 3 (11.5 s before the budget) and scaled standard there to 16 samples (part of issue #55). The ray work estimate multiplied by every light and so overcounted open scenes by about 4 times. The budget now counts path steps (texels x samples x bounces), which cost about the same per path in a closed 56 part room and in the demo room. A preset scales down to 5 million path steps (the demo room at standard keeps about 52 samples, not 16); explicit settings are limited only by a hard bound of 24 million path steps and by the real 20 s limit, which fails the edit with the scene unchanged and names a lower samples and texelsPerUnit to try. A bake on a worker has 90 s instead of 20 s (the page's own thread keeps 20 s), and the preview's 30 s call deadline grows by 90 s for a scene_create with a bake and a scene_apply with set-bake. bake.used and bake.reduced are unchanged; the capability limit maxRayWork is now maxPathWork.

  • Make room in the npm package without adding a dependency (part of issue #36). The archive fell from 802,581 to 695,041 bytes (353,535 free under 1 MiB; the target was 300,000), and every library and MCP install stays as light as before. Two causes: (1) every one of the 80 built files began with the full LICENSE and THIRD-PARTY-NOTICES.md text, about 4 KB each; they now carry a one-line notice and the complete texts stay once at the package root, about 61,000 bytes saved; (2) the MCP SDK's Node shim pulls in Ajv (the validator for elicitation answers, about 270 KB before minification) that this server never uses, so build/mcp/server.mjs now bundles scripts/mcp-sdk-shims.mjs instead, which fails with a clear message if elicitation is ever requested: server.mjs fell from 677,484 to 484,111 bytes and the archive by about 47,000 bytes. build/mcp/LICENSES.md no longer lists the Ajv family because that code is gone, and its unused copies left licenses/mcp/. Measured, and not worth doing: zod/mini (zod's classic layer is 30 KB of the server before gzip), source maps (none are shipped), standalone core copies (none in the archive), and docs (every packaged page is linked from the README, llms.txt, the MCP guide or the skills). npm run smoke:package now loads every exports entry in a clean consumer. Film and site code were already outside the archive. Separate lazy-loaded packages were not built: the lazy addons together are about 157 KB gzip, so splitting them out could return at most that much, for the cost of a package, a release pipeline and a version matrix each. archiveBytes budget 815,000 to 705,500. Module load of the server is no slower (minimum of 25 runs, 217 and 188 ms against 231 and 209 ms).

  • Run the heavy scene kernels on a worker in the MCP preview (part of issue #38; edits after creation still run on the main thread). The light bake, the cloth solver, uv_unwrap and pattern textures are pure functions of their input, so runKernel in the new src/kernel-offload.js computes them inline as before, or, inside buildWithKernels, throws KernelDeferred, lets a module worker (kernel-worker.js, a package entry like the path tracer's) compute the result, caches it by a hash of the input bytes and runs the synchronous build again. The preview builds scene_create that way when the page has Worker; Node and every other caller keep the synchronous path and the same bytes (a test builds one scene cold in a separate process both ways and compares a digest of geometry, textures and the bake report). A worker error comes back with its class and message, a worker that cannot start or dies leaves the job to the inline path, and an AbortSignal ends the build and the worker. Measured in Node on the demo recipe: the uv_unwrap stack takes 3.6 s, the draft light bake 7.5 s and the standard bake passes the 20 s limit on this machine, cloth at the 10 s limit takes 3.4 s, a 1024 pattern takes 1 to 7 s, a boolean 0.12 s. Measured in headless Chrome on the GPU backend with the demo room recipe (tests/fixtures/demo-room.recipe.json, 9 kernels): the longest stretch the page was unresponsive during scene_create fell from 11.8 and 11.9 s to 1.6 s with workers (two runs each), while the build took 12.3 and 12.5 s instead of 10.9 and 11.0 s, because the kernels run one after another and the build reruns after each. scene_inspect now reports kernels: how many kernels a worker served and how many ran inline, each fallback with its reason (also a 67js kernel fallback line on stderr when a page that has Worker falls back), the build time and the longest block; npm run smoke:kernel-workers installs the package, runs the demo room and fails unless kernels ran on the worker with no fallback. The longtask observer saw nothing in either mode because the browser does not count work run by a remote-debugging call, so the block is measured with a 20 ms timer. scene_apply edits still run on the main thread. Core did not grow (entry:core, lit-box unchanged); the lazy entries rose by about 900 bytes gzip.

  • Fix three gaps a demo film found in the recipe light bake (part of issue #55). A baked scene with cloth failed at render time for every frame after t=0 with "A light map needs lightUvs on rigid geometry, with no skin or morph", because the cloth simulation replaces the sheet's geometry each frame; cloth sheet nodes are now skipped and listed in report.skipped. The bake now lights with spot lights (the renderer's cone and soft edge) and area lights (front side only, one deterministic shadow ray to a point of the rectangle per hit), where it ignored both; a scene without them bakes the same bytes. The presets could not finish a normal scene (standard passed its 20 s limit on a 56 part room, fine hit the 30 s preview limit): a recipe bake now has a ray work budget of 10 million units (texels x samples x bounces x rays per hit), estimated from the unwrap before any ray is traced with no clock involved. A preset is scaled down to fit and scene_inspect bake reports used and reduced; explicit samples, bounces or texelsPerUnit are never reduced and fail with a message naming the number that fits. bakeLightmaps is now planLightmaps, estimateBake and traceLightmaps (still exported as one call). entry:core did not change; entry:light-bake grew by 1,033 bytes gzip (budget 25,900 to 26,900) and the lazy agent entries by about 1,580; the schema stayed within its total budget (the capability notes were shortened to fit).

  • Add sculpt and shrinkwrap mesh edits (part of issue #47; surface patches and a stack form are not built). sculpt applies up to 32 inflate, displace and smooth strokes to the vertices of the selected faces; shrinkwrap moves them onto a primitive, to the nearest point or projected along a direction, with an offset. Both are in scene_capabilities (meshSculpt) and in the mesh_edit schema, with the tool schema total unchanged (the face rule and tag prefix descriptions were shortened to pay for them). The stroke maths moved to src/sculpt-strokes.js and serves sculptGeometry too. Four Blender golden cases (two Shrinkwrap modifier comparisons; the sculpt reference runs the same brush formula on Blender's mesh), hostile input tests, and the suite task ball-badge, which needs a plate bent onto a ball.

  • Add ragdolls (part of issue #96): physics.ragdoll in a recipe with armatures and physics in an asset recipe (set-physics) bind a capsule body to every bone or joint and play the fall back as the pose of those bones, the same bytes for the same seek. New src/ragdoll.js, loaded with the physics code only. New suite task robot-ragdoll. The lazy budgets of agent-scene+lazy and agent-asset+lazy rise by 100 and 800 bytes gzip for it; the core entries did not grow from this. Joints have no swing limits yet.

  • Add terrain holes and a recipe terrain geometry (part of issue #64; layers beyond four and the navmesh are not built). createTerrain({ holes: [{ center: [x, z], radius }] }) leaves no surface where a cell centre is inside a circle, and terrain.isHole(x, z) answers for a point; without holes the seeded meshes are byte-identical (a digest test). A recipe geometry { type: "terrain", size, resolution, seed, amplitude, holes } builds one seeded ground mesh with the same holes; it loads on demand through the new terrain module (core did not grow), the tool schema entry is one line with the fields in scene_capabilities terrain, and the schema total budget did not change (scene_create and scene_apply rose by a few hundred bytes, mesh_edit fell by the same amount). New suite task terrain-hole-walk (ground with a 3 m opening and a walker that has to go around it) with the checks groundRelief and walkPath. Measured: Haiku passed it 2 of 2 (9 and 6 tool calls, $0.29 in all) and both exported recipes used the terrain geometry; there was no control run without the feature, so this shows agents find it, not how many could do without.

  • Bake patterned, textured and emissive parts, and skip hair hosts (part of issue #128). A recipe bake used to skip every part with a pattern, a map or emission (31 parts, the floor and every wall, in the demo room) and failed scene_create with "Textured geometry requires UV channel 0" on a node that hosts hair. Those parts now keep uvs and their look and take the bounced light through a new lightMap material slot on a second UV set (geometry.lightUvs); the fragment shader adds color * (1 - metallic) * lightMap * lightMapScale, so a brick pattern tints its own bounced light. Hair and particle host nodes are skipped and listed in report.skipped. The shader branch compiles out of scenes with no light map; the renderer carries about 650 bytes of gzip for it in entry:core (see docs/light-bake.md for the measured sizes). GLB export writes TEXCOORD_1 and the light map as the emissive texture (an approximation: glTF has no light map slot). The path tracer ignores baked maps by design. New reference scene brick-room (a recipe-built closed brick room).

  • Make the path tracer about 2.8 times faster per thread and draw normal and metallic-roughness maps (part of issue #129; a 1080p still of the 181,708 triangle demo room at 64 samples still does not fit the 240 s job limit). Profiling the demo room showed the ray query dominating: 54 percent of the time sat in the generic BVH visitor, which called a closure per box and per triangle and kept distances in a boxed array. createTracer in src/ray-query.js now walks the BVH with typed stacks, a slab test on inverse directions and triangles packed in leaf order; pixels are bit-identical to before (checksum of a band unchanged) and it runs 1.2 times faster alone. The larger gain is shadow rays: the demo room has 11 light sources, and every bounce traced all of them. The first hit of a path still samples every light; later hits draw one at random and divide by its chance (and the environment multiple importance weights use that chance), which gives 23,300 paths a second per thread against 9,800, with the average moved by less than 1 percent in the demo room and 3 percent allowed by a test. Image jobs now use 8 workers, up from 4 (the machine limit). normalMap (with normalScale, tangent frame from the UVs as in the rasterizer) and metallicRoughnessMap (green scales roughness, blue scales metallic) are drawn on UV-mapped materials; on box-projected materials (boxMapScale) they are still ignored and report.ignored says so. The entry:path-tracer size budget goes from 12,900 to 13,800 bytes gzip for the maps, light selection and the packed ray query; entry:core did not change. Tests: maps tilt and mask the light, one light drawn at random keeps the average, and bands drawn in any order give the pixels of the whole image.

  • Move the anisotropy and iridescence shader code out of the core bundle (part of issue #51). It lives in the new addons/material-layer-shaders entry, which registers it on import; the renderer asks for it only when a part uses a layer, so a lit box no longer pays for it. A scene that uses a layer in code must import the entry first, or the renderer throws a message that names the import. Recipes load it through loadAgentSceneModules, the MCP server does so itself, and the glTF loader loads it for a file that uses either extension. The unused short overloads of environmentLighting are gone. Pictures are unchanged.

  • Add anisotropy and iridescence material layers, and a depth of field for each timeline shot (part of issue #51). Recipe materials take anisotropy and anisotropyRotation (highlights stretch along a grain, as KHR_materials_anisotropy) and iridescence, iridescenceIor and iridescenceThickness (a thin film that shifts reflection colour with the view angle, as KHR_materials_iridescence, after Belcour and Barla). Both are compiled out of the shader unless a part uses them, the MCP schema takes them with one short line each, and glTF import and export read and write the two extensions, so they no longer warn as accepted but not rendered. Transmissive materials draw in their own pass and ignore both; scene_inspect diagnostics say so. Recipe materials already accepted clearcoat, transmission, thickness and ior with the surface tag (a demo film that could not ask for them was running an older build, and its murky marble came from the liquid tag, which absorbs by thickness): scene_capabilities now lists every layer with its range under scene.materialLayers, and npm run smoke:material-layers renders a glass ball through the real MCP server and checks that the stripes behind it are bent. A timeline shot also takes focusDistance (0.01 to 100000 m) and blur (0 to 1), which replace the recipe depth of field for that shot only; sampling stays stateless, so a seek gives the same bytes as the frame in a sequence. bench/reference gets a brushed-film scene scored against Cycles and EEVEE, and the 11 older scenes did not move by more than 0.002 against Cycles.

  • A path trace job keeps its own maxSeconds below the job deadline: the deadline of an image job with settings.renderer now reaches maxSeconds plus 60 s (it was 60 s plus 2 s per frame, so a 1920x1080 trace of 120 s was cancelled at about 67 s), and the traced PNG may be as large as any job frame (12 MiB, not 1 MiB).

  • Add 67js/addons/path-tracer (part of issue #57; the Cycles comparison is partial). renderPathTracedImage({ parts, environment, camera }, options) draws a still by CPU path tracing against the triangle BVH of the light bake, with next-event estimation for the sun, point, spot and area lights, an importance-sampled environment map, GGX metal, clear coat, glass, maps and cutouts, and a wavelet denoiser, on up to eight module workers. An image render job selects it with settings.renderer: { type: "pathTracer", samples, bounces, denoise, seed, maxSeconds }; the same recipe and seed give the same PNG, a job past maxSeconds fails with no image, and samples, size and ray work are bounded. It loads on demand: the core bundle and the agent scene do not carry it (the limits live in src/path-tracer-capabilities.js, listed under jobs.pathTracer in scene_capabilities; the tool schemas gain one renderer field inside the total budget). npm run reference -- --path-tracer scores it against Cycles: better than the rasterizer on 7 of the 11 reference scenes (lit-interior 0.5055 to 0.1993, spot-area 0.1242 to 0.0639), worse on outdoor-hdri, character and fog, mean 0.171 against 0.189. Not drawn: volume fog, normal maps, translucent leaves, caustics, lights seen in mirrors. src/ray-query.js now holds the ray query and light falloff that light-bake used to carry. npm run smoke:path-tracer checks it through MCP.

  • Test the docs and the MCP server on clients other than Claude Code (part of issue #69). New npm run check:tool-schemas and tests/other-clients.test.js check all 22 tool definitions against rules other clients apply: name characters and length, a description under 1024 characters, an object root, a type on every constrained schema, resolvable $ref, declared required fields, a strict JSON Schema 2020-12 compile (Ajv 8, now a dev dependency) and an empty-argument call. Reports for OpenAI strict: true and for Gemini's documented keyword subset list each gap with its schema path; the test fails when a new kind of gap appears. Measured: the MCP SDK client and the MCP Inspector CLI list the 22 tools and call scene_create; a draft-07 Ajv compile rejects every tool ($schema, then prefixItems), strict OpenAI tools would reject the optional-heavy schemas, and nothing was run on Codex, Cursor, Gemini or any non-Anthropic model. A cold Haiku read of only the README and llms.txt installed the package and rendered a box but found no route to a PNG without MCP, so docs/guide.md gains a tested "Render a PNG from a script without MCP" recipe that the README and llms.txt point to; docs/mcp.md gains an "Other clients and models" table and no longer pins the standalone launch to 0.9.1. A test checks that every page llms.txt and the README link to exists. Schema bytes are unchanged.

  • Fix the next agent tool errors (part of issue #66). scene_review and scene_look with labels on an imported asset failed with "needs the mesh code" because asset creation loaded the on-demand scene modules only for mesh edits; it now always does, and a test walks every preview path in a fresh process. geometries and materials are optional in a recipe. A failed scene_create says no scene was created and to call it again. scene_inspect on an asset accepts a source-node ID that holds one part and names the parts of one that holds several.

  • Fix the most common agent tool errors (part of issue #66). scene_look and scene_review with labels on an imported asset threw Cannot read properties of undefined (reading 'map') (an asset recipe has no materials list, and asset mesh reports are keyed by part ID); an engine fault now reaches the agent as one advice message and goes to stderr. A node that names a geometry and no material gets an explicit default material instead of an error. sessionId is optional on every tool and defaults to the active scene (189,159 to 188,872 schema bytes); an ID that names another scene is still rejected. scene_apply names the fix when set-node fields are wrapped in node. The bench look harness loads the on-demand scene modules, so motion recipes get a look score.

  • Rename the project from 67.js to 67js, with no dot. The GitHub repository anxuanng/67.js, the npm package @codepawlhq/67js, the site domain and the Worker name stay.

  • Add a recipe timeline, job audio and an encoder choice (part of issue #60, first slice). A recipe timeline lists named shots played in order over the motion clock, each with a duration, an optional motionStart, a partial camera on top of the sampled one and a dip through black; set-timeline edits it. settings.timeline: { start } renders it as an image, stills or video: one clock drives the shot, the motion and the renderer, so a seek to any time gives the same frame as inside a video. settings.audio: { file, offset } mixes one .wav, .mp3, .ogg or .flac from the new --audio <folder> option into a video, checks that it covers the video, decodes both streams afterwards and reports metrics.sync (a gap over 50 ms fails the job); settings.encoder is cpu or nvenc. File names are plain names without paths, drives or protocols, files must be regular and at most 32 MiB, and ffmpeg gets -protocol_whitelist file; nothing else comes from the agent. Video jobs can last 60 s (was 30), still capped at 900 frames, so a 60 s video runs at 15 fps. Measured through MCP on SwiftShader (npm run smoke:film-timeline): 9 PNG seeks across three shots and a dip match 8 stills byte for byte, and a 60 s, 900 frame video with sound rendered in 58 s (84 s on a loaded machine) at 128 x 128 with the click in the decoded audio on the decoded cut frame (offset 0.1 ms, streams 20 ms apart). Tool schemas stay inside their budgets (the timeline is a record reference; fields are in scene_capabilities filmTimeline). Left out: resumable long renders, several audio tracks, gain, markers inside a shot, animation events, crossfades and timelines for imported assets. The video sequence editor stays a non-goal.

  • Decide on WebGPU (part of issue #56, no engine change): wait, WebGL2 stays the only backend. Headless Chrome 156 has a WebGPU adapter on the RTX 3060 with no flags, but none under the MCP cpu flags (--use-angle=swiftshader); the SwiftShader Vulkan adapter needs --use-webgpu-adapter=swiftshader. With three.js as a proxy on 3 bench scenes, WebGPU frames were 2.6 to 4.4 times WebGL on the GPU and 1.4 to 3.0 times on SwiftShader, and first frames were slower. Browser support, shader rewrite size (about 1,400 GLSL lines and 450 call sites) and bundle cost are in docs/webgpu-decision.md.

  • Add recipe particles and hair (part of issue #63, no new tool). A particles section holds seeded emitter systems (point, box, sphere or disc; gravity, drag, wind, a landing floor, size over life) and hair systems that grow strands from a surface node, loaded on demand with the new particles module (src/particles.js, src/hair.js). Each particle is a closed-form function of its seed, index and time, so a seek never replays: the same time gives the same particles, a seek backwards repeats them and a seeded system gives identical bytes (npm run smoke:agent-particles checks PNG bytes through the MCP preview, including a second session). Counts are bounded to 4 systems, 2000 particles each, 4000 in total and 100000 drawn vertices. New set-particles operation and a particles report in scene_inspect; the schema takes one record reference, so the tool schemas stay within the total budget and the fields live in scene_capabilities. Cloth and particles now share validation helpers (src/agent-recipe-checks.js). Left out: particle collisions and bounce, rotation, colour and alpha, hair collision and combing. The smoke, fire and liquid decision (no browser solver, with measured grid costs) is in docs/physics.md.

  • Add 67js/addons/light-bake (part of issue #55; recipes, MCP, GLB export and reflection probes are not done). bakeLightmaps(parts, lights, options) traces cosine-weighted paths on the CPU against a triangle BVH and returns new parts that carry the light that bounces between surfaces (point lights and the sun, shadowed) as a lightmap texture drawn through the emissive map, with lightmap UVs from the uv_unwrap unwrap. The renderer and its shaders are untouched, so a scene without a bake renders as before and core size is unchanged. Bounded and deterministic: samples, bounces, texelsPerUnit, maxResolution and maxSeconds are range checked, texel, triangle, light and ray-work totals are checked before any ray is traced, and the same input gives the same bytes. Parts with their own maps, pattern, emission, skin or transparency are skipped and listed in the report. npm run reference -- --bake lit-interior takes lit-interior from 0.5055 to 0.1444 against Cycles (6 s bake); no other scene moved by more than 0.002. The package archive grows by 3,525 bytes (717,978 to 721,503). See docs/light-bake.md.

  • Add recipe cloth (part of issue #62, no new tool). A cloth section records sheets with a position-based solver (src/cloth.js, loaded on demand with the new cloth module, no peer dependency): pinned edges, gravity, steady wind, ball and box colliders, thickness and friction, 60 frames a second, bounded by particle, frame and solver-operation limits. A seek blends two recorded frames, so the same time gives the same positions and a seek backwards repeats them (npm run smoke:agent-cloth repeats the PNG bytes through the MCP preview). New grid geometry (a flat sheet, 1 to 64 segments), set-cloth operation and a cloth report in scene_inspect. The tool schemas stay within the total budget: set-physics now takes the same record reference as set-motion, and the physics descriptions are shorter. A hanging sheet matches a Blender cloth bake (scripts/blender-cloth.py) to 0.08 percent of its height; a sheet dropped on a ball does not match, and no parity is claimed. No self collision and no soft bodies yet.

  • Edit the topology of imported GLB parts (part of issue #48, no new tool). mesh_edit and mesh_inspect take a part ID such as mesh-10 of an imported asset: the part becomes a welded polygon mesh with axis tags and a material-<n> tag, the edit log is kept in the asset recipe as meshEdits (checkpoints, export and replay work), and every result passes validateTopology. New vertices take skin weights, morph deltas and vertex colours from the nearest original surface point and faces left without UVs read the nearest UV island; on a skinned or morphed part an edit more than a quarter of the part's bounds diagonal from the original surface is rejected with the next action. mesh_inspect reports localToWorldScale and localAxesInWorld. Measured on RobotExpressive through MCP (npm run smoke:imported-topology): a through hole in the closed left leg (98 to 210 triangles, still one closed part), a cut in the skinned right hand (124 to 109) and the morphed head (154 to 134), a 3 m extrude on the head rejected, GLB export and checkpoint restore intact. Suite task robot-leg-hole.

  • Make room in the npm package (part of issue #36). The MCP preview page, build/mcp/preview.js, is now an entry of the library build and imports the engine chunks that the package entries already ship, instead of bundling its own copy of the engine. The archive drops from 904,975 to 666,214 bytes. build/mcp/manifest.json lists those chunks under previewChunks with their hashes, the MCP runtime serves them next to preview.js, and bench/size/budgets.json gains archiveBytes, which npm run smoke:package checks.

  • Add six whole-mesh modifiers as edits and stack nodes (part of issue #42): weld, triangulate, edge_split, smooth, simple_deform (twist, bend, taper, stretch) and cast (sphere). Each has Blender cases in tests/fixtures/blender/ that match with no listed difference, triangulate and edge_split check their output size before building it, and their fields are described in scene_capabilities meshModifiers so the tool schemas grow by one compact entry. Left for later slices: screw, skin, wireframe, lattice, curve, hook, wave, shrinkwrap, weighted normal, smooth by angle and the other cast shapes. npm run parity now prints the modifier coverage.

  • Add curve and text geometry (part of issue #45). curve sweeps a round or custom profile along a cubic Bezier chain with explicit or automatic handles, a taper along the length and flat caps; text extrudes the outlines of the bundled DejaVu Sans (printable ASCII, 20 KB, loaded on demand with its own text module and never in an entry) or of a font table added with registerAgentSceneFont, with size, depth, alignment, lines and curve resolution. Volume, area and bounds match Blender 5.2 to 2e-5 for two Bezier curves and for a letter and a word set in the same font. Recipes stay plain JSON with bounded, validated fields and vertex estimates checked before generation. The MCP schemas gain two strict entries in the geometry list (1.4 KB of the 190 KB across all tools; not offered as mesh bases or cutters); field types and limits live in scene_capabilities under curveText, and the MCP takes no font file or URL. The DejaVu licence is in THIRD-PARTY-NOTICES.md. Metaballs and NURBS surfaces are recorded as out of scope in docs/parity.md. Not done: a suite task in bench/agent-modeling, kerning, bevelled text, non-ASCII glyphs in the bundled font.

  • Add frustum culling, levels of detail and live adaptive resolution (part of issue #37). Renderer67 tests each instance of a batch against the view frustum with the geometry's bounding sphere and draws only the visible ones (on by default, culling: false turns it off; skinned, morphed, swaying, fire, smoke, water, assembly and blended batches and the shadow passes are not culled). A part's lods, the array from generateGeometryLods, makes each instance pick a level by pixel error (lodPixelError, default 1). Selection is a pure function of the camera and the matrices, so a seek draws the same frame. createGpuTimer and AdaptiveResolution in src/adaptive-quality.js give live viewers GPU timing and a resolution scale with a minimum; an export can not build one, and src/main.js uses it only outside export=1. New large-outdoor scene in bench/runtime with ablations: 1.55 ms against 8.41 ms without culling and 6.0 ms for three.js on an RTX 3060, 517 ms against 4,428 ms and 944 ms on SwiftShader. Core grows 1,845 bytes gzip (budget 81,500). Recipes cannot declare lods yet. See docs/culling-lod.md.

  • Add relations between parts (part of issue #44, no new tool): a mesh node takes relations (attach, gap, flush, align, centered, up to 8) against other mesh nodes, or against the faces of one carrying a mesh tag (region). They work on world bounds, are solved after geometry exists and again on every scene_apply, so one edit to a body keeps the parts that sit against it. Unknown IDs and tags, two relations on one axis and cycles (a part waiting on its own child included) are rejected with the next action; solving is deterministic and independent of node order. The code loads on demand (relations). The schema carries only a generic relations list (597 bytes over scene_create and scene_apply together); the kinds and fields are in scene_capabilities relations, and the engine validates them. Measured on Sonnet (3 runs each), relations did not cut tool calls or errors: the kettle resize took 3.3 calls against 3.0, and a 12-part desk with three inspected resizes took 8 against 8, with 13% fewer output tokens (3,833 against 4,403) and the same cost and pass rate. The suite gains kettle-resize, desk-resize and their plain twins, and touches and centreAt checks.

  • Add render passes, depth of field and camera motion blur (issue #54, first slice). Render jobs of kind image or stills take settings.passes (albedo, normal, emission, depth, objectId) and write each as a raw, unantialiased PNG beside the lit image (render-depth.png, frame-3-objectId.png); depth is 16 bits over 100 m in red and green, object IDs are 24 bits with the part names in the job metrics. In code, renderer.render({ pass }) draws a pass into its own non-multisampled target. effects.depthOfField (blur, focusDistance) and effects.motionBlur (shutter) are new recipe effects: fixed sample patterns and a per-pixel rotation from the pixel position, so a seek to a frame repeats the same pixels. They live in the new 67js/addons/camera-effects entry; the preview installs it, and the core renderer only gains a stage hook and the pass branch (a compiled-out shader feature). Motion blur follows camera tracks only, sampled from time at motion.start + elapsed - shutter. Measured on SwiftShader through MCP (npm run smoke:render-passes): 8 stills with 5 passes each, then four seeks, all byte-identical to the same frame in the sequence. Temporal anti-aliasing, accumulation and the compositing graph of #54 are not part of this slice.

  • Expose clip layers, bone masks and name retargeting through MCP, and morph clips in createAssetPose (issue #59, in part). An asset recipe's motion.layers (up to 8) plays the file's own clips at motion time with a bone mask, weight, speed, offset, loop, additive with reference, bones (clip track to another asset node) and morph, before tracks and IK, so a character walks and waves at once; everything is checked against the asset and bounded, and a seek equals playing. morphClipFromGltfAnimation and a layer's morphs put morph weights through createAssetPose (masks pick parts by their owning bone), createAssetLayerEvaluator serves hosts that stage the pose themselves, and asset.stagePose takes morphWeights. Add the robot-walk-wave task to the agent modeling suite and extend npm run smoke:character. Not done: foot tilt on slopes, full-body IK, and rest-pose retargeting from another asset through MCP. Raise entry:skeleton-pose to 19,000 and entry:agent-asset to 85,200 bytes gzip, and the tool schema budgets by 800 bytes (scene_apply to 47,800, scene_create_asset to 20,150, total to 190,700).

  • Use one expression language for fields and drivers (issue #98). src/expression.js holds the limits, number literals, operator table, syntax and hostile-input checks; src/mesh-fields.js (per mesh element: attributes, noise, random, vectors) and src/motion-expression.js (per time: time, node reads) only add their own inputs, and each rejects the other's with a message that names the context. This changes driver expressions (neither language was released): an operator with one operand may be written bare, {"neg": x}, in drivers as in fields; if takes its second operand when the condition is not 0 (a driver used above 0); sqrt of a negative, pow overflow and mod by 0 give 0 instead of an error; smoothstep with equal edges is a step; div by 0 is still a non-finite error in a driver, because a hostile-input test pins it, and 0 in a field. Drivers gain ceil, round, sign, lte, gte, eq, and, or, not and booleans; fields gain atan2 and saturate. The unknown-operator hint of fields lists operators in a new order. Error messages for a malformed driver changed in wording and are all TypeErrors. The MCP schema for drivers now points at the field language instead of listing operators (schemas 183,741 to 183,762 bytes), and the scene_capabilities budget rises from 3,050 to 3,100 bytes for two operator names.

  • Add branches and labelled images to the agent tools (part of issue #67, no new tool). scene_apply with a branch name runs the operations on a scratch copy and stores the result in a snapshot store that holds one copy per distinct recipe (8 branches), leaving the scene, its revision and its checkpoints untouched. scene_review shots take a branch and render it in a private scene; scene_restore with branch keeps one. labels: true on scene_look and scene_review draws part names, material tags and mesh lint problems on the image and returns them as JSON with pixel positions. Branch names are bounded (^[a-z][a-z0-9_-]{0,23}$). The tool-schema budget rose by 800 bytes in total (150 for scene_apply). npm run smoke:agent-branches drives it through MCP on SwiftShader.

  • Add a bounded material node graph (issue #50): material pattern of type: "graph" bakes noise, voronoi, wave, checker, gradient, ramp, maths and uv nodes on the CPU into base colour, roughness, metallic, emission and height (a normal map, so bump). The graph is data, never shader text; nodes, depth, ramp stops, texture size and work are checked before any pixel is computed. Add mapping: "uv" to every pattern so it follows uv_unwrap UVs instead of box projection; the emissive map now box-maps like the other maps. The 11 presets bake the same bytes as before. Covered in the MCP schema, scene_capabilities (materialGraph, materialMappings), GLB export, docs/agent-scene.md, docs/material-maps.md and the brick-graph reference scene.

  • Add decimate, remesh and generated levels of detail (issue #40). decimate (a ratio of triangles) is a quadric edge collapse of our own that keeps open boundaries, the borders between tags and the tags themselves; it measured 0.25 to 0.85 of the Hausdorff error of meshoptimizer 1.3.0 on the same meshes and adds no dependency, but takes about 2.5 s on 100k triangles where meshoptimizer takes 0.09 s. remesh (voxelSize or resolution, at most 192 per side and 4,194,304 cells, checked before allocation) builds a closed quad surface from a BVH signed distance with surface nets. Both are edits and stack nodes, in the MCP schemas, and load on demand with the mesh code. generateMeshLods, generateGeometryLods and generateAssetLods (also in 67js/addons/mesh) return levels with a measured error for runtime selection (issue #37). Four decimate and two remesh cases join the Blender comparison, compared by volume, area and Hausdorff distance because face counts differ.

  • Make the default look good without tuning. Recipes get an image-based environment: environment.setup (daylight by default, overcast, sunset, studio) selects a small procedural sky map generated in code, plus its key light, with environmentIntensity and environmentRotation; no URLs or files from tool input. effects.ambientOcclusion (0 to 1) and ambientOcclusionRadius add screen-space ambient occlusion from the depth texture. It is off by default: measured with npm run reference -- --ambient-occlusion, it improved no score by more than 0.002 (strength 0.8: -0.0019 in foliage, +0.0047 in lit-interior; strength 0.4, radius 0.3: within +0.0013 everywhere). When used it compiles and allocates on first use and costs about 16 ms on a 190 ms SwiftShader frame (8 percent). scene_capabilities lists lightingSetups, defaultLightingSetup and ambientOcclusion. This changes the look of every existing recipe that sets no lighting: it now has an environment map, a key color of [3, 2.8, 2.5] instead of [5.2, 4.7, 4.1], and a fill intensity of 0 instead of 1. The engine default is unchanged (occlusion off, no map) so code scenes keep their pixels. Screen-space reflections are not part of this. The scene:lit-box size budget rises by 100 bytes gzip for the environment and occlusion code. The reference suite gets --ambient-occlusion, and its Blender renderer drops triangles that repeat a vertex, which crashed Blender on the skinned character

  • Measure and cut the cost of the MCP tools (issue #65). node scripts/measure-tool-cost.mjs reports each tool schema and each typical result in bytes and estimated tokens (3.5 characters a token). The 22 schemas came to 373,388 bytes (about 107,000 tokens) and are now 173,216 (about 49,500): recipe geometries and asset recipes point to mesh_edit, mesh_stack and scene_create for the mesh edit, stack and motion detail instead of repeating about 45 KB and 35 KB in each tool, and vectors, colours, quaternions and IDs are shared $ref definitions. The backend still validates every field. tests/tool-cost.test.js fails when the schema total, one tool's schema or a typical result passes bench/tool-cost-budgets.json. A small mesh change is 187 bytes as mesh_edit against 1,254 as dispose plus a whole-recipe scene_create on a 7-node recipe.

  • Return diagnostics that name the next action (issue #66). mesh_inspect lint adds intersecting-parts, self-intersection, thin-wall, floating-part and asymmetric warnings from the BVH, with face budgets and a skippedChecks list; they never turn ok off and the agent modeling suite does not grade them. scene_inspect adds diagnostics for material fields the renderer ignores, blended transmission and unused materials or geometries, and asset inspection adds decoder warnings (accepted but not drawn features), skinned meshes with no joints, joints with no skin and assets with no clips. Error messages now list allowed fields, known IDs and the next call: a missing session (the largest group, 21 of 68 recorded errors) names the active session or says to call scene_create, and page errors no longer carry the bundle stack. tests/tool-errors.test.js walks 27 error paths and rejects a message with no next action.

  • Add authored rigs (#46): a recipe armatures list (bones with a name, parent, head and tail, or a humanoid or quadruped preset fitted to a mesh node's bounds, with landmarks to move bones), and a skin on a mesh node that binds it with automatic weights, plus a set-armatures operation. Each bone becomes a recipe node, so keyframed bone rotation and motion.ik run through the same paths as imported rigs, and humanoid bones carry the HUMANOID_BONES names so retargetClip maps them. Weights are the distance to each bone segment with an inverse-fourth-power falloff, the nearest four kept; they depend only on position, so hard-edge seams never open. scene_inspect gains rig: unweighted vertices, bones with no vertices, and joint axis problems (zero-length bones, straight limb chains, knees that bend backward). scene_export_glb writes the bones and a walk clip baked at 30 frames a second from tracks and IK. Add the rigged-walker task to the agent modeling suite, with a reference and three failing variants. Blender 5.2 opens the exported walker as 1 armature, 21 bones, 14 meshes with 21 vertex groups each and 1 action of 24 frames; three.js loads 14 skinned meshes and the clip.

  • Load agent scene code on demand: mesh editing, lathe/extrude/tube, material pattern and motion now sit behind loadAgentSceneModules(recipe) (or loadAllAgentSceneModules()), and createAgentScene throws an error naming that call when a needed module is missing. A recipe of primitives drops from 141,241 to 73,581 bytes gzip in scene:agent-scene (every lazy chunk still totals 145,049). npm run size now bundles with code splitting and reports the initial load, plus a +lazy row for the total. npm test preloads every module. The archive no longer carries 29 docs that the site serves (physics, lighting, terrain and the other addon docs); agent docs stay

  • Add joint limits, node constraints and drivers to motion (issue #58). Two-bone IK takes limits per joint and axis in degrees and bendToward; scene_inspect motionIk now carries limited (each pose a limit changed) and warnings (hyperextended, and wrong-bend for a knee or elbow bent the unnatural way, which a wrong pole used to hide behind reached). motion.constraints adds copy_location, copy_rotation, copy_scale, limit_location, limit_rotation, track_to, child_of (with an inverse and an influence blend) and floor, and motion.drivers sets a node component or material property from a bounded JSON expression over time and other nodes (never run as code). The order is tracks, IK, drivers, constraints, with no state between samples, so a seek equals playing. copy_rotation, limit_rotation, track_to, child_of and floor match Blender 5.2 world matrices in 8 recorded cases (tests/fixtures/blender/constraints.json). Joint limits are also in 67js/addons/skeleton-pose. The leg-plant task joins the agent modeling suite. The expression language is a new small one in src/motion-expression.js: the field expressions of the mesh addon were not on main yet

  • Reach Rapier from recipes: a physics section (and a set-physics operation) puts rigid bodies on node IDs with box, ball, capsule, convex or trimesh colliders fitted to their geometry, joints, gravity and a duration of up to 10 seconds. It is recorded once at 60 steps a second and seeks like motion, so a seek back repeats the same frame (checked as identical PNG bytes on SwiftShader through MCP). scene_inspect reports per-body final positions and resting times, contacts and sensor enter and leave events. Rapier stays an optional peer, served to the preview from node_modules like Manifold, so the MCP server bundle does not grow.

  • Add trimesh and convex-hull colliders (bounded triangle and point counts, checked before allocation), sensors with enter and leave events, and 16-bit collision groups and masks to addons/physics, plus contactEvents, restFrames, bakePhysicsScene, loadPhysicsEngine and setPhysicsModuleLoader. A collider with sensor now bakes instead of throwing.

  • Add the tray-drop task to the agent modeling suite: balls dropped into a pocket cut in an authored mesh, graded on a fixed trimesh body, balls resting on the pocket floor and a repeatable seek. Raise four size budgets for the physics code (entry:physics to 17,900, entry:agent-scene to 121,100, entry:agent-asset to 128,500, scene:agent-scene to 147,300 bytes gzip).

  • Add bounded field expressions: a JSON language of math, comparison, logic, vectors, seeded noise and attributes (position, normal, index, tags) that compiles once to closures over typed arrays and never executes a string. It limits nodes (64), depth (12), elements (250,000) and node-evaluations (8 million) before it evaluates, and only reads operator names from a fixed table. It drives a { "field": expression } face selection rule, set_position and displace as edits and stack nodes, and a where filter for instances. displace and set_position match Blender's Displace modifier and a Geometry Nodes Set Position node in five new golden cases

  • Add instances on a recipe node: repeat its geometry at the points of a grid, a seeded scatter in a box or the surface of a geometry, with where, yaw, alignToNormal and scale, at most 2048 candidate points. It expands to ordinary nodes that share geometry and material, so the renderer draws one instanced batch. Add the pillar-court task to the agent modeling suite

  • Add the scene_export_glb MCP tool (22 tools now) and morph weight clips in exportGlb. The tool writes the active recipe or imported asset scene as a GLB under <jobs>/exports/ with a server-chosen name and returns the path, byte count and SHA-256; it takes no path, keeps the newest eight files and refuses more than 32 MiB. It exports the current pose (pattern textures, imported textures, skins, morph weights, IK as posed), motionTime seeks authored motion first, and clip adds an imported asset's source clip. Clip tracks with property: 'weights' write a SCALAR sampler of one weight per target. Checked in Blender 5.2: a patterned recipe opens with 2 meshes and 3 images, the robot with IK opens as 1 armature, 43 bones and 20 meshes, and its Walking clip as one action of 23 frames. Nothing in src/index.js changes, so no size budget moves

  • Add material layers: clearcoat and clearcoatRoughness, transmission with thickness, ior, attenuationColor and attenuationDistance, sheenColor and sheenRoughness, and specular and specularColor. Recipes, the MCP schema, glTF import and export (KHR_materials_clearcoat, transmission, volume, ior, specular, sheen) all take them, each checked at the boundary. Transmission refracts the opaque scene like the liquid material. Anisotropy and iridescence are not rendered yet. Import of those two still warns

  • Change the glTF import of the six layer extensions above from accepted-and-ignored to rendered, so asset.warnings no longer lists them

  • Add spot lights (environment.spotLights, 8), rectangular area lights (environment.areaLights, 4) and shadows for point and spot lights (castShadow: true). Intensity of a spot light is candela, as for a point light, with Blender's cone mask. Area lights use a representative point, within 1.2 percent of Cycles on the axis. Shadows share one 2048 x 3072 depth atlas of 24 tiles (24 MiB, allocated on first use): a point light takes 6 and a spot light 1, and a request for more is an error. The shader code and the atlas exist only in a scene that uses them. Recipes get all of it through set-environment, the MCP schema and scene_capabilities (maxSpotLights, maxAreaLights, localShadows). Cascaded directional shadows and light linking are not part of this

  • Add ten cutting and repair edits: fill_holes, dissolve_faces, dissolve_edges, dissolve_verts, dissolve_limited, bisect_plane (with fill, clearInner, clearOuter), bridge_edge_loops (with segments), subdivide_edges, grid_fill and knife_project. Each tags what it makes, and the edge rule { "open": true } picks boundary edges. The lint message for an open boundary now suggests fill_holes. 18 new cases match Blender in counts, face sizes, area, volume and positions; the knife is compared only with a straight plane cut and differs where a face runs along the projection direction

  • Export textures, skeletons and clips to GLB: exportGlb(parts, { nodes, clips, maxTextureSize }) now writes map, normalMap, metallicRoughnessMap, occlusionMap and emissiveMap as embedded PNG (limit 2048 px by default, with an error that names the option), skins with joint nodes and inverse bind matrices, and animation clips (translation, rotation and scale tracks; SMOOTH becomes cubic with zero tangents). Pattern materials export their baked textures with box projected UVs, so the picture matches without uv_unwrap. It previously threw on skins and on every texture channel. The entry:core size budget goes from 67,100 to 70,500 bytes gzip (+3,400)

  • Make the first frame and the SwiftShader frame cost less, with the same pixels. The main fragment shader now compiles only the features that a scene uses (maps, splat layers, moss, alpha and the special material tags), and a part that needs another feature later recompiles it. All programs, with the environment prefilter, now start together and use KHR_parallel_shader_compile. On an RTX 3060 the first frame went from 3.7 to 4.4 seconds down to 0.5 to 1.0 seconds, and a SwiftShader frame costs 0.4 to 0.5 times what it did. bench/runtime/PROFILE.md has the profile

  • Add subdivide as a mesh edit and stack node: Catmull-Clark subdivision to 4 levels with semi-sharp creases picked by an edge rule, sharp boundaries, kept corners, n-gon support, and tags, UVs and edge attributes carried through each level. The size is checked before it allocates. Counts, area and positions match Blender at levels 1 to 3; the pebble task joins the agent modeling suite

  • Remove 67js/addons/film from the npm package. It held the 187-second trailer demo with its baked physics recording, 75 KB gzip of the archive. Nothing else in the package used it. To keep using it, copy src/film.js, src/film-rig.js, src/film-character.js, src/film-physics-definition.js and src/film-physics-clip.json from the repository, or read the trailer on the site at /film.html. The archive goes from 901,314 to 829,085 bytes

  • Remove three.js from modeling and the agent scene: lathe, extrude and tube no longer import it, and scene:agent-scene drops from 185,681 to 117,290 bytes gzip. lathe and tube output is identical to before, vertex for vertex. extrude walls, bevels, UVs, normals and bounds are identical, and volume matches within 1e-4. Extrusion caps use a new ear-clipping triangulation, so a cap can split along other diagonals, and a cap with collinear points can add or drop a seam vertex. A clockwise hole under a clockwise contour is now flipped to face outward, where it used to keep inward walls

  • Load glTF without Three.js: the addon has its own GLB and glTF parser, node sampler and animation clips, so a box with the addon drops from 159,422 to 67,467 bytes gzip. Draco, meshopt and KTX2 still come from the decoders you pass in options.decoders. Only the default scene is decoded, and a node outside it is decoded only when a skin or an animation names it

  • Remove three.js and three-mesh-bvh from mesh queries, shrinkwrap and surface patches: a typed-array BVH and its own triangle tests replace them, so no source file in src/ except the benchmark page imports three.js and the npm archive drops from 856,115 to 798,532 bytes. entry:mesh-query drops from 85,555 to 15,535 bytes gzip, entry:shrinkwrap from 86,779 to 17,860 and entry:surface from 72,974 to 9,838. Nearest, project, raycast and pair results match three-mesh-bvh on fixtures and random queries. Nearest queries on 100k triangles are about 20 percent slower and a pair query with one mesh shifted against itself is slower (21 ms against 2 ms), while a closed-solid pair is 2.4 times faster. The surface patch clipper follows DecalGeometry and keeps its triangle order

  • Update the bundled zod to 4.6.7, which fixes a memory exhaustion advisory in the validation of oversized collections

  • Make the MCP preview server reject requests with another Host header, which closes DNS rebinding

  • Check the size of a mesh array before it allocates. Before this, the check ran after the copies existed

  • Change the bevel profile to the meaning it has in Blender: 0.5 is a circular arc, 0.25 is flat, 1 is square, and below 0.25 is concave. Before this, 0 was flat and 0.5 was a parabola, so a recipe with a profile other than 0.5 changes shape

  • Stop adding a fan of triangles where one beveled edge ends at a vertex with three faces. The third face now takes the profile's points, as in Blender

  • Add multiple scattering to the direct light specular, as the environment specular has it (part of #167): each light's specular is scaled by the gain from the BRDF table, sampled once per fragment, wherever an environment map is bound. metal goes from 0.0974 to 0.0962 against Cycles (EEVEE 0.0916) and material-spheres from 0.1234 to 0.1185; no scene is worse by more than 0.0002, and the frame cost of brick-flat and metal is unchanged within timer noise.

  • Look up an object probe along the ray from its centre to where the reflected ray meets the floor under it, so chrome shows the floor next to its contact point where the real ray lands (part of issue #167). metal goes from 0.0962 to 0.0957 against Cycles; car-paint, brushed-film and material-spheres also improve and no scene gets worse.

  • Record that casting the part's own shadow onto its reflection probe does not close the metal gap (part of #167, which stays open): a shadowed floor in the probe fixes the bottom edge of the chrome sphere but overshoots the rest, metal 0.0962 to 0.1038, and the floor is not explained by screen-space occlusion either. No engine file changed; the measurements and the parallax-corrected probe that would be needed are in �ench/reference/README.md.

  • Add multiple scattering to the environment specular (part of issue #167). environmentLighting in src/environment.js adds the energy a rough microfacet surface bounces a second time (Fdez-Aguera 2019), as Cycles' multiscatter GGX does, so rough metals are no longer dark: metal goes from 0.1036 to 0.0974 against Cycles (EEVEE 0.0916). See bench/reference/README.md.

  • Ground bounce reads the mean of a floor's base-colour texture (linear space, once per image) instead of skipping textured floors (part of issue #191, which stays open): no reference scene has a textured floor, so no score moved; terrain and a bounce under the foliage crown were measured and worsened mountain-valley by 0.0019 and the crown by 0.002, so they are not kept (bench/reference/README.md).

  • Water reflection draws its picture at three quarters of the canvas instead of half and blurs it less, so a river between towers keeps the sharp window bands Cycles mirrors: megacity-dusk-a 0.2072 to 0.1999 (no reflection 0.2084), megacity-dusk-b 0.1459 to 0.1242, mountain-valley-a 0.1946 to 0.1924 against Cycles on the current main (#195).

  • Tried darkening the ground bounce by the share of ground around the parts that sees the sky (part of issue #197): the studio shadow floors moved 0.02 to 0.2 toward Cycles but material-spheres FLIP rose 0.0093 and brick-flat 0.0061, so nothing was kept and the measurements are in bench/reference/README.md.

  • Remeasure shadow depth against Cycles on main for the grand options and the sunlit scenes (part of issue #197, which stays open): dark percentiles, shadow floor, lit-to-shadow contrast and edge width, plus three single changes (no ground bounce, ambient occlusion, less world light) that each failed the 0.002 FLIP limit. No engine change; see bench/reference/README.md.

  • The sun shadow now follows the sun (part of issue #206). When shadowBounds gives no eye, Renderer67 and the cascaded shadows derive it from environment.lightDirection (src/shadow-light.js) and redraw the map when the sun moves, and every reference scene aims its shadow map along its own sun; before, the default eye and several scene eyes sat a few degrees off the shading sun.

  • Fix the sun reaching surfaces that sit behind an open single-sided surface (part of issue #206). The sun shadow pass culled back faces, so a ray that entered a single-sided mesh from behind, such as the open edge of the terrain, wrote no depth and the sun passed through the rock. The shadow depth pass now draws both faces (applyAlphaState in src/renderer.js). mountain-valley with the world light at 0 reads 0.0000 on the floor (0.1727 before, Cycles 0.0003) and scores 0.1792 against Cycles instead of 0.2612; every other reference scene is unchanged to four digits and a shadow map redraw costs 0.05 to 0.1 ms more. The foliage crown leak is not this cause and stays open. See bench/reference/README.md.

  • The sun shadow's depth bias is now a world distance, slope times 3.5 shadow texels converted with the light's extent and far minus near, so the same scene gets the same bias in metres for any depth range; it removes the self-shadow stipple the sun-aligned map put on sloped walls (part of issue #225).

  • Fix the "bright river" of megacity-dusk-a (part of issue #232): the river, and the bay of megacity-dusk-b, stood in the same plane as the ground box, and Cycles picked one of the two surfaces at random per ray, so the Cycles river reflected about a third of a mirror and 67js looked too bright by comparison. Both water boxes now stand 1 cm above the ground, in the reference scenes only; the engine is unchanged. See bench/reference/README.md.

  • Measure the bright river of megacity-dusk-a with the sun off on both sides (issue #232, which stays open): the canyon walls and the street are slightly darker than Cycles under sky light alone, and removing all ambient light from the river closes only about a third of its excess, so the cause is the water's reflection, not missing sky visibility. render-blender.py gained --sun-strength; the numbers are in bench/reference/README.md.

  • Derive ground bounce from the scene: groundBounceEnvironment (67js/addons/ground-bounce) finds the floor part and gives the environment map a diffuse-only copy whose lower half is the light the floor sends back up, so walls and figures on an open floor get bounce light with no bake and no frame cost (brick-flat 0.1264 to 0.0547 against Cycles, brick-graph 0.1518 to 0.0982, eight more scenes better, none worse by more than 0.002; closes #244).

  • Score 67js and EEVEE with the same quantisation (part of issue #258). bench/reference/score.py read 67js from an 8-bit PNG but EEVEE from a float array; it now rounds the EEVEE array to 8 bits too (load_eevee), and tests/reference-quantisation.test.js pins it. The EEVEE columns of baseline.json were regenerated from the stored arrays where they reproduce the committed numbers: foliage EEVEE against Cycles goes from 0.0888 to 0.0924, and no scene changes order against 67js (metal becomes a tie at 0.0944). Some cells and grand.json need a full reference run. See bench/reference/README.md.

  • Record that a graph bump scale per world unit (normalScale [0.8, 1.2] on the 6 by 4 brick-graph wall) does not improve it against Cycles (0.1518 to 0.1525, nothing kept) and that the brick-face colour gap is Cycles-only light, not the colour path (part of issue #50). See bench/reference/README.md.

  • Take the sky light below the surface plane out of the ambient term for a mapped normal, as Cycles does, so the edges of a bump read as dark as Cycles draws them (part of issue #50). brick-graph goes from 0.1584 to 0.1518 against Cycles (EEVEE 0.0875 to 0.0844) and no other scene changes.

  • Compile the two fill lights out of the main fragment shader while fillIntensity is 0 (part of issue #74). A uniform branch did not skip them on SwiftShader, a FILL_ON macro does, and a renderer rebuilds its program once if the intensity later rises above 0, as for spot lights. Pictures are byte identical in all 8 runtime scenes; the SwiftShader frame of skinned-characters goes from about 161 to 125 ms, transparency from 364 to 341 ms and instanced-objects from 100 to 94 ms. The size budgets of the three scene bundles rose by 100 bytes. See bench/runtime/PROFILE.md.

  • Make the sun shadow filter one hardware comparison fetch per tap instead of four texel fetches (part of issue #74). The shadow depth texture now compares on fetch with linear filtering (TEXTURE_COMPARE_MODE), so the 9-tap filter costs 9 fetches, not 36, with the same result (every reference scene is within 0.0001 of Cycles before and after). On SwiftShader, alternated with three.js, the frame fell from 8.4 to 7.1 times three.js on transparency, 7.6 to 6.6 on skinned-characters, 5.5 to 4.0 on instanced-objects and 3.8 to 3.5 on moving-shadows. The fog volume reads the shadow through the same sampler, so its single tap is now filtered over 2 by 2 texels. Cutting the filter to 4 taps saved only 4 percent more and was not kept.

  • Fog near a glowing window or a lamp now picks up that light (part of issue #164). fogSkyVisibility also builds a grid of the light that emissive parts and point lights give their surroundings (fogBounceGrid in src/fog-bounce.js, one bounce, 64 rays per cell, built once per scene and fog box) and the post shader adds it to the fog's scattering. The temple-hall fog region against Cycles goes from 0.2611 to 0.2421, past EEVEE (0.2455), and the baked one from 0.2335 to 0.2236; scenes without emissive parts or point lights, and scenes without the option, draw as before. The sun's bounce is left out because it made dark-shaft worse.

  • The fog's ambient in the sun's shadow now follows a per-point sky visibility when Renderer67 gets the new fogSkyVisibility option from src/fog-sky-visibility.js (the agent preview and the reference harness pass it). It casts 24 upward rays from the centre of each cell of a grid of at most 2,048 cells over the fog box, once per scene and fog box, and uploads one 8-bit 3D texture; fog that sees any sky keeps all of the ambient and fog that sees none keeps 0.1 of it. Without the option, without volumeFog, or when a recipe sets shadowAmbient, the post shader has the sampling compiled out and draws as before. Against Cycles on the GPU: dark-shaft 0.0607 to 0.0559 (region 0.1802 to 0.1282), temple-hall 0.0674 to 0.0640 (region 0.2723 to 0.2611; baked region 0.2439 to 0.2335), fog-room 0.1049 to 0.1036 (region 0.2405 to 0.1625); every other scene is unchanged to four digits. The temple-hall region without a bake does not yet beat EEVEE (0.2455). The core entry grows 141 bytes gzip (part of issue #164).

  • An unset volumeFog.shadowAmbient is now derived from the scene instead of fixed at 0.15. Pass skyOpenness from src/closed-room.js to Renderer67 (the agent preview and the reference harness do) and it shoots 32 upward rays from the middle of the fog box, once per scene and fog box: a closed room keeps 0.15, open ground under a sky gets 1, a partly covered scene lands between. Without the option an unset value stays 0.15, so existing renderers draw as before. A recipe that sets the value keeps it, and the recipe echo no longer lists shadowAmbient when it was unset. The fog-valley reference scene no longer sets it and scores the same 0.1855 against Cycles (EEVEE 0.1952); dark-shaft and fog move by 0.0012 and 0.0014 (better) and temple-hall by 0.0015 (worse), the rest are unchanged (part of issue #165).

  • Add volumeFog.shadowAmbient (0 to 1, default 0.15, the value that was hard coded in src/post.js): the share of the fog's ambient that stays where the sun's shadow covers it. A closed room sees little sky, so 0.15 holds and no existing recipe changes. Open ground under a sky sees all of it, so fog-valley sets 1 and goes from 0.1967 to 0.1855 against Cycles, past EEVEE's 0.1952 (part of issue #165). Measured on the glow weight and the shadow floor first: the glow weight lowered fog-valley only at the cost of fog (+0.02) and temple-hall (+0.008), and a global floor of 0.4 cost fog-room +0.04, so neither could be one constant for rooms and open ground.

  • Trace one bounce in object probes and let the probe's surfaces mirror the sky (part of issue #167). traceProbeMaps also stops overwriting a hit's direct light with the light of the last neighbour the gatherer visited, which is why #209's bounce made the probe floor darker. Against Cycles on the GPU metal goes from 0.0964 to 0.0944 (EEVEE 0.0916), metal-room 0.5071 to 0.5045, material-spheres 0.0956 to 0.0954, car-paint 0.0979 to 0.0978, and brushed-film 0.1017 to 0.1019. The probe trace takes about twice as long (metal 0.12 s to 0.25 s); a scene with no reflective part never runs it.

  • Record three object probe measurements that were not kept (part of issue #167, which stays open): a depth-based parallax lookup (metal 0.0962 to 0.0957 against Cycles, with stepped edges), occlusion of the probe's floor (worse, 0.0982 to 0.1051) and a 1.15 gain on the probe's hits (0.0922 but no better on the other scenes). The probe's lit surfaces are about 15% darker than Cycles shows them. See bench/reference/README.md.

  • Give smooth metal parts a reflection probe by default (part of issue #167, which stays open). A recipe part with metallic 0.5 or more and roughness 0.6 or less (up to 8 such parts) now gets a probe traced from the scene around it, so a chrome sphere shows the floor and its neighbours; environment.objectReflections: false turns it off and scene_inspect reports reflections. Against Cycles metal goes from 0.1294 to 0.1036 (EEVEE 0.0916), brushed-film 0.1149 to 0.1029, car-paint 0.1059 to 0.1038, material-spheres 0.1312 to 0.1293 and metal-room 0.5128 to 0.5076. A scene with no reflective part draws the same parts and keeps its frame time; metal costs about 0.03 ms more on the GPU.

  • Rerun the reference suite under the capped sky with one sun (issue #169): khronos.json and path-tracer.json are rescored against fresh Cycles renders (asset-only 67js against Cycles improved on four of five Khronos assets, for example SheenChair 0.3745 to 0.3316 and ClearCoatTest 0.2306 to 0.2097; the path tracer got worse on outdoor-hdri 0.1016 to 0.1242, character 0.1120 to 0.1289 and material-spheres 0.1187 to 0.1383), and baked scores of lit-interior, brick-room, dark-shaft, temple-hall and metal-room are now stored in bench/reference/bake.json. npm run reference -- --bake <ids> --skip-blender compares a fresh run with it and fails when a score is worse by more than 0.01 FLIP (bench/reference/compare-scores.mjs, tests/reference-bake-regression.test.js, which reads JSON only). --scene takes several ids. The README has one current score table and a History heading.

  • Measure the recipe bake presets and the frame costs on a quiet machine (part of issue #170): draft, standard and fine scored against Cycles on the five room scenes with bake seconds and the settings each preset used, and the frame cost of a baked closed room and of the fog's glow mip chain, each with the GPU and CPU load recorded beside it. frame-time.mjs gains --bake-cost, --glow-mips and --repetitions=N (and prints the range of the per-run medians); the reference run prints used for every preset bake. See bench/reference/README.md.

  • Draw fewer triangles for forests (issue #186). Swaying foliage and grass batches are now frustum culled with a bound that grows with the wind strength, and tree foliage has thinner shadowLods (a third of the leaves, then an eighth) next to the trunk levels from #171. A tree has a fourth level of detail beyond 150 m, with a third of the rings on each limb and crown clumps 1.5 times wider, so createTree returns four levels and TREE_LEVEL_DISTANCES has three entries. mountain-valley draws 2.89 instead of 6.46 million triangles in the main pass and 1.39 instead of 2.34 million in the shadow pass; its score against Cycles stays within 0.001 of before.

  • Measure why foliage crowns and the mountain-valley floor are too light (part of issue #191, which stays open): sky light inside the crown already matches Cycles, and the excess in both scenes is sun light that the sun shadow lets through (bench/reference/README.md). A finer horizon march in terrain-sky.js was tried and not kept because it worsened mountain-valley from 0.2623 to 0.2743.

  • A large flat smooth surface (a lake, a river, a polished floor) now reflects the scene: the new addon 67js/addons/water-reflection draws the scene from the camera's mirror image into a half-size target, with Fresnel, ripples and a slight blur. mountain-valley-a goes from 0.3530 to 0.2562 against Cycles and megacity-dusk-b from 0.1575 to 0.1377. Scenes without such a surface are unchanged (issue #195).

  • Add bench/reference/shadow-depth.py (issue #197), which measures shadow depth of a 67js frame against Cycles from the reference renders: dark percentiles, the shadow tile floor and lit level, lit to shadow contrast and edge width. run.mjs gains --skip-cpu to leave out the slow SwiftShader render.

  • Keep the shadows of a closed room dark (part of issue #197, which stays open for An's blind pick). A world far dimmer than the key light (under 0.01 of it, fading out by 0.06) now gives a quarter of its light where the sun is blocked, so a sealed hall no longer glows from a sky it cannot see. Against Cycles, dark-shaft went from 0.143 to 0.062 FLIP temple-hall from 0.144 to 0.066 and fog-room from 0.195 to 0.105, the three temple-hall options from 0.253, 0.364 and 0.275 to 0.157, 0.152 and 0.102, and the darkest 1 percent of the grand and dark scenes from 1.16 stops too light to 0.68 stops. Outdoor scenes did not move. bench/reference/measure-depth.py measures the darks, render-gpu.mjs draws chosen scenes with measurement switches and depth-sheets.py makes the blind sheets.

  • Stop pull requests from conflicting on CHANGELOG.md (issue #199). GitHub ignores the merge=union driver, so each merge turned every other open pull request DIRTY. A change now adds its own file in changes/ and npm run changes:fold writes the files into CHANGELOG.md as the version's section at release time; npm run changes prints the pending notes. .gitattributes is removed. The earlier unreleased notes moved to changes/000-before-fragments.md.

  • The path tracer now honours light linking (part of issue #215). A light with affects, and the sun with environment.keyAffects, adds nothing at a part it skips, as direct light and at every bounce, and still casts shadows for the others; it reads the same per-part material.lightMask as the renderer and the light bake. report.ignored no longer lists linking. The light-linking scene against Cycles goes from 0.6518 to 0.0213 (the rasterizer scores 0.0998); scenes without links draw the same pixels as before, and the reference harness now passes its path-traced scenes through applyLightLinks.

  • Fix the path tracer drawing rough dielectric surfaces too dark at grazing views (part of #215): the diffuse layer was dimmed by the mirror Fresnel term of the view angle, so a rough floor lost most of its light toward the horizon (outdoor-hdri floor 0.21 against Cycles' 0.47 near the horizon, 0.42 against 0.47 in the front). The diffuse layer now loses only the specular layer's directional albedo for the surface roughness (Karis's fit of the split-sum BRDF), as Cycles' Principled BSDF does. The capped one-sun sky was not the cause: a floor lit from above measures 0.466 in the tracer, the closed form gives 0.458 and Cycles 0.468. FLIP against Cycles in bench/reference/path-tracer.json falls on 14 of 16 scenes (outdoor-hdri 0.1242 to 0.0226, character 0.1289 to 0.0287, material-spheres 0.1383 to 0.0449, glass 0.1406 to 0.0385, foliage 0.1753 to 0.1001, lit-interior 0.2167 to 0.0769, long-street 0.1818 to 0.0291). light-linking rises from 0.6241 to 0.6518 because the tracer still ignores affects and now lights every part more brightly.

  • Add a cabinet resize task and --relations on|off to the agent suite (part of issue #44, which stays open). cabinet-resize and cabinet-resize-plain ask for a 30 percent wider, 10 cm taller edit of a 10-part cabinet; --relations off removes the relations field and capabilities entry from the MCP tools. Measured on Haiku and Sonnet, 3 runs per arm: tool calls do not drop with relations in the start recipe (Sonnet 3 calls on the cabinet and 8 on the desk in every arm), but Haiku passes the cabinet 3 of 3 with relations against 1 of 3 without.

  • Compare the graph bump with Blender's Bump node (part of issue #50, no engine change): the formula has the same form, and the scale differs by 1.25 along U and 0.83 along V on the brick-graph wall. Matching Blender's scale moved brick-graph against Cycles from 0.1602 to 0.1599, so the scale is not the gap; the measurements and the Blender source are in bench/reference/README.md.

  • Agent modeling suite: look-pick offers three colour grades and checks the picture the exported recipe draws (mean brightness, clipped and crushed shares), so the pick cannot be traced from numbers. Branches saved no calls on it (#67).

  • Add the suite task light-pick-hard for branches (part of issue #67): a gallery with six thin posts and five places for one spotlight, where the place that leaves the least of the painting in shadow (10 per cent against 24 to 37) can only be told by rendering each place or tracing 6 posts by hand. litPainting gained blockers and least, and samples a 41 by 21 grid of the painting. The reference and the wrong places and shortcuts are graded in tests/agent-modeling-suite.test.js. Calls with branches on and off are in bench/agent-modeling/README.md.

  • Measure the lit shader cost on SwiftShader (part of issue #74, which stays open): three pixel-safe changes (shadow taps without clamp, no shadow filter on pixels facing away from the sun, multiplications for two pow calls) were each alternated with the unchanged file and none beat the noise (0.5 to 5 percent, or 3 to 5 percent slower), so none is kept. With MSAA and the HDR target off on both sides 67js is 2 to 4 times three.js in the three scenes (it was 5.2 to 5.7 on 2026-11-01), and a constant fragment shader costs the same as the lit one in the busy machine state, so the lit shader is not the proven bottleneck. See bench/runtime/PROFILE.md.

  • Add the opt-in shading: 'light' renderer option for weak machines and software rendering (part of issue #74): at most 2 MSAA samples, one sun shadow fetch, a fitted environment BRDF, the sky reflection taken from the blurred irradiance, first order irradiance only and a cheaper direct specular term. The default 'full' shader is unchanged pixel for pixel; bench/runtime gains the 67js-light engine and docs/guide.md lists the option.

  • Bring the light linking and path tracer rows of docs/parity.md up to date with issue #215: recipes reach light linking, the path tracer honours it, and the darker floor was the diffuse layer at grazing angles.

  • Add a showcase page (showcase.html) of live scenes built from real PBR models: an airport ramp, an AI data center and a container port, each a GLB with a Poly Haven sky and credits in public/showcase/<id>/scene.json. The home page gallery shows these scenes instead of the older demos.

  • The home page and the docs use the CodePawl site theme from codepawl.com: a white page, light grey cards without borders, one black primary button, system type and a faint dot grid behind the hero, with a matching dark theme that follows the system. src/site.css replaces the layered overrides of home.css and site-theme.css on those two pages. The demo pages keep their own app UI. The brand reads "67js" without a dot on every page.

  • Filter mipmapped material textures anisotropically (8x, or less where the GPU caps it). Only terrain texture arrays had it, so glTF floors, roads and aprons seen at a grazing angle fell to a blurred mip level. The far apron joints of the airport showcase read sharper; core grows 40 bytes gzip.

  • The dev server scans only the project's own pages for dependencies and does not watch .claude/. With a few hundred agent worktrees there, the first page load went from 13 s to 1.2 s, and the runtime benchmark no longer times out opening its harness.

Install: npm install @codepawlhq/67js@0.15.0