Repository navigation
67js 0.15.0
-
Add
fog-valleyandfog-roomto the reference suite (part of issue #165, which stays open). Against Cyclesfog-roomscores 0.1953 (EEVEE 0.3730) andfog-valley0.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 oftemple-hall(0.2305 to 0.2513), so #164 is not fixed. Seebench/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-shaftread 128 of 255 sky visibility at its bottom edge where the interior reads 1.rasterizeTexelsinsrc/light-bake.jsnow gives that texel to the nearest triangle. Bakeddark-shaftgoes from 0.0653 to 0.0607 andtemple-hallfrom 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.jsholdsmountain-valley,temple-hallandmegacity-duskas-a,-band-c, each with its own composition, light and time of day, drawn bynpm run reference -- --grandintogrand.jsonandimages/grand-<id>.jpg(baseline.jsonand the existing images are unchanged). The Blender side gained agradientworld, written as the same equirectangular image 67js draws.bench/reference/grand-sheets.pymakes 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).
createTreebakes a sky visibility into each foliage vertex from the leaf density above it, andcreateTerrainbakes 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. Onbench/reference, thefoliagecrown went from 0.298 to 0.200 (Cycles 0.175) and the score from 0.1173 to 0.1023 (EEVEE 0.0888);mountain-valleywent 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/referencenow rebuilds the 67js leaf model forfoliagematerials (back-face normal, colour and normal variation, sky reflection 0.3, and light from behind as a Translucent BSDF with weight0.3 * pi / (3.7, 3.4, 2.9)), and the 67js back light is now shadowed by the sun shadow.foliagegoes from 0.1290 to 0.1173 against Cycles (EEVEE 0.0864 to 0.0888) andmountain-valleyfrom 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 inbench/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.reductiontells 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-hallatstandardnow bakes (16 samples, 1 bounce, 1.37 texels per unit, 0.0705 against 0.1504 unbaked with a probe).fineis now the same asstandard: five samples and density mixes within the 20 s limit scored no better on the room scenes. Table inbench/reference/README.md. -
Score the recipe bake presets against Cycles (part of issue #170, open for the frame costs).
standardscores 0.127 to 0.159 on the three rooms (no bake 0.51),fineis no better, andstandardandfinerefusetemple-hall. Table inbench/reference/README.md. -
No test asserts real elapsed time any more (issue #179): work counts and a fake clock replace stopwatches.
smoke:agent-review-closeandsmoke:film-physicsare wired; the second starts its own server. -
Show the floor and the other parts in smooth metal (part of issue #167, which stays open).
traceObjectProbesinsrc/probe-trace.jsgives each part withmetallicof at least 0.5 androughnessof 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), andplaceProbesassigns it.metalscores 0.1294 to 0.1036 against Cycles (EEVEE 0.0916),brushed-film0.1149 to 0.1029,car-paint0.1059 to 0.1038,material-spheres0.1312 to 0.1293; every scene without a reflective part is unchanged to four digits and draws the same frame (metal1.51 ms plain, 1.58 ms with four probes).npm run reference -- --object-probesplaces 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.jsfailed 16 of 19 becausenpm testpreloaded 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.jsruns it in clean processes). Each of those files now importstests/support/load-agent-scene-modules.mjsitself,npm testno longer preloads, andnpm 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 theTestworkflow. -
Fix links in the npm package that pointed at docs it does not ship (issue #178).
docs/guide.md,docs/material-maps.md,README.mdandROADMAP.mdnow 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:packagepacks the archive, unpacks it and checks every link inside (26 files, 0 broken), and theTestworkflow runs it. -
Take 34 KB of headroom under the npm archive budget without raising it (issue #177).
CHANGELOG.mdno 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.mdnow 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.htmlorscripts/smoke-mcp.mjsdiffers from thetools/listresult (issue #180). The server registers 22 tools; the 23rdregister(in the file is the function definition, so the documented count was right. -
Remove the AGPL-3.0
@codepawlhq/orglet-uipackage, React and react-dom from the website (issue #176). The site now uses its own tokens and button insrc/site-tokens.cssandsrc/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-valleythat 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, andframe-time.mjs --plainmeasures 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 oflods, 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. Onmountain-valleythe 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_inspectnow returns aclosed-room-without-bakediagnostic when 90 percent or more of 48 rays from the middle of the scene hit a front face, and namesbake; 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.bakeSkyVisibilityinlight-bakewrites 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 inscene_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-mapsandbenchmark:mesh-query. -
The GitHub repository is
anxuanng/67.js, public since 2026-11-02. Links andrepository.urlpoint 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-interior0.1444 to 0.1915,brick-room0.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-shaft0.0609 andtemple-hall0.0560. A bake takes about 1.3 to 1.9 times as long (temple-hall39 to 75 s); passbounces: 2for 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.pymeasures what FLIP misses (high frequency energy in dark regions and around the door):temple-hallbaked 0.448 to 0.182 and its glow ring 1.826 to 0.990 (Cycles 0.158 and 1.053),dark-shaftbaked 0.318 to 0.150; whole frame scores against Cycles stay within 0.006 andtemple-hallbaked goes from 0.0716 to 0.0578. A baked part's reportskyVisibilitynow weighs texels by area, and baked thin parts get more texels (brick-room28,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).
volumeFogadded 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-shaftgoes from 0.2321 to 0.1425 (shaft region 0.2157 to 0.1219),temple-hallfrom 0.2126 to 0.1438 (region 0.2512 to 0.2341), and the olderfogfrom 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
occlusionMapread 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 andtemple-hallfrom 0.2239 to 0.1129 (EEVEE 0.2653 and 0.2341), and abrightRegionthatscore.pypicks from the Cycles image scores the shaft pixels on their own. The no-bake scores ofoutdoor-hdri,character,metal,car-paintandfoliageare 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-hallgoes from 0.5841 to 0.2126 (EEVEE 0.2341),mountain-valleyfrom 0.3658 to 0.3473 andlong-streetfrom 0.1555 to 0.1407;fog(+0.0031) andmegacity-dusk(+0.0044) get worse. -
Compare reference scenes under one sun, and add
mountain-valley,temple-hallandmegacity-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.jsonandpath-tracer.jsonstill use the sky with two suns. -
Add reflection probes and a
metal-roomreference 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 throughcreateEnvironmentResourcesand lets nearby parts reflect it (specular only, no parallax, not in GLB). A recipe withbakeplaces a'room'probe unlessbake.probesisfalse: against Cycles a probe lowered the score in every baked room (lit-interior0.1444 to 0.1213,brick-room0.1515 to 0.1266,metal-room0.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 underprobes(the resolved setting used to hide it). The reference harness--sceneoption says it does not rescore, andframe-time.mjsprints 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 -- --probesandframe-time.mjstake 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 asenvironmentEnabled ? a : band pays for code behind a uniform that is off, so the main program now takesENVIRONMENT_ENABLED,USES_SHEENandUSES_CLEARCOATfrom the parts and the environment map (featuresenvironmentMap,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 takesUSES_SKIN,USES_MORPH,USES_WIND,USES_WAVESandUSES_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-mesh1139 to 887 ms,many-materials983 to 880 ms,instanced-objects384 to 268 ms. Seebench/runtime/PROFILE.md. -
Add the suite task
render-pickand measure branches on it (part of issue #67, which stays open). A gallery with a pillar and a spotlight offered at three places; thelitPaintingcheck 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 calledbranch, and calls did not drop (Haiku 4 to 6 on, 4 to 5 off; Sonnet 3 either way). Withbest-of-threethat is 22 runs on two tasks with no measured gain from branches.npm run smoke:agent-branchespasses on SwiftShader. Bench only; core did not change. -
Add the
weighted_normalandsmooth_by_anglemodifiers as edits and stack nodes (part of issue #42).smooth_by_anglemarks edges over an angle sharp and the rest smooth, overriding the geometrysmoothAngle;weighted_normalstores an area-weighted normal per corner that the render extraction uses as is. The edge attributesmoothis new. -
Add the
lattice,curve,hookandwavemodifiers as edits and stack nodes (part of issue #42).latticemoves up to 64 control points of a grid over the mesh bounds,curvebends a mesh along a Catmull-Rom path,hookpulls the vertices near a point andwaveis 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,wireframeandskinmodifiers as edits and stack nodes (part of issue #42).screwrevolves the edges of a mesh about an axis with steps, iterations and lift, andwireframeleaves a rim on every face with thickness, offset and even offset; both match Blender 5.2 on counts, area and positions (wireframeon three cases,screwon two).skinis 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 inscene_capabilitiesmeshModifiers, 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 -- --clothrenders 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 intobench/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 inscripts/blender-cloth.pyplus Blender's default air damping, which on a 3 g particle is a drag of about 54 per second against 0.3 here. Clothdampingnow 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/navmeshbuilds a walkable grid from triangles (slope limit, agent radius, holes, obstacle footprints) andfindPathruns A* with line-of-sight string pulling, deterministic, with cell and search budgets that throw aRangeErrornaming what to raise.scene_inspecttakespath: { ground, from, to, avoid, ... }and returns keyframes for a node translation track, described inscene_capabilitiesnavigation; thescene_capabilitiesresult grew by 624 bytes and thescene_inspectschema by one record field. The suite getsterrain-path-walk(two holes and a rock) and recordspathQueries; thewalkPathcheck takes several openings andavoidNodes. Size:entry:navmesh3,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.mjsdraws the six cold recipes under two defaults from the three-quarter view and the recipe camera (whatscene_lookdraws), 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
SheenChairand score it (part of issue #51). A glTF normal map may now carry its ownKHR_texture_transformoffset 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 asuvTransformon the normal map descriptor, and thenormalMapTransformshader feature inaddons/material-layer-shadersapplies 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 extensiontexCoordequal to the texture's own is no longer refused. Core grew by 25 bytes gzip (lit-box60,179 to 60,204): a hook in the normal map lookup, one feature line and thebindargument; 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,
agentGroundPlacementreturned 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-objectsinside the noise (medians 379 to 377 ms and 388 to 386 ms over three alternated pairs). Current ratios against three.js on SwiftShader:large-mesh6.0,many-materials5.0,instanced-objects7.0, so the 3 times target is not met.bench/runtime/PROFILE.mdrecords 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, andreport.averageSamplessays 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, withcolumnLengthsnow exported fromsrc/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.ignoredno longer lists them. Render jobs may setmaxSecondsup to 600 (was 240; the deadline ismaxSecondsplus 60 s): the 1920 by 1080, 64 sample still oftests/fixtures/demo-room.recipe.jsontook 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 filterbrick-graphis 0.0024 andcharacter0.0048 worse than at 64; adaptive sampling costsspot-area0.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-graphdid not improve against the same tracer at 64 samples (0.1370 to 0.1373); the darker floor ofcharacterandoutdoor-hdriwas narrowed but not found.bench/reference/run.mjstakes--noise-thresholdand recordsaverageSamples. Core did not change. -
Read the textures inside glTF layer extensions and accept TEXCOORD_1 (part of issue #51).
clearcoatTexture,clearcoatRoughnessTexture,clearcoatNormalTexture,anisotropyTexture,sheenColorTextureandsheenRoughnessTextureload intomaterial.layerMapsand draw through shader features inmaterial-layer-shaders, andexportGlbwrites them back. Occlusion, emissive and metallic-roughness textures may read TEXCOORD_1. Against Cycles,AnisotropyBarnLampgoes from 0.4758 to 0.3584,AnisotropyDiscTestfrom 0.3481 to 0.2808 andClearCoatTestfrom 0.2344 to 0.2306;TransmissionTestis unchanged andSheenChairstill 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-flatis the control forbrick-graph: the same wall in the graph's average colour with no maps.npm run reference -- --khronosdraws Khronos sample assets downloaded into gitignoredartifacts/khronos/and scores them against Cycles reading the same GLB, on the pixels the asset covers (khronos.json,images/khronos-*.jpg;baseline.jsonis 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 insrc/gltf.js:KHR_xmpandKHR_xmp_json_ldmetadata refused the whole file, andKHR_materials_variantsis 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) andaffectson point, spot and area lights plusenvironment.keyAffectsname nodes; both addons load on demand throughsrc/agent-scene-modules.js,scene_capabilitiesnow reportslightLinkingandshadowCascades, unknown node IDs are refused with the next action, andset-environmentandscene_export_recipecarry the fields. The light bake honoursaffects(a linked light adds nothing at a part it skips, even as a bounce) and the path tracer lists linking inreport.ignored. Default: a scene withoutshadowCascadeswhoseshadow.extentis above 40 gets three cascades (the single map is then 4 cm per texel or coarser;long-streetscores 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, andshadowCascades: 0keeps one map.node scripts/smoke-mcp-linking.mjsproves 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.jsbuilds 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 thegroundoption, which now also accepts a part (about 20 bytes inentry: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: falseswitches it off (the field is absent from recipes that do not set it). Nothing else about the default look changed:effects.exposurestays 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), setupdaylight, 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, andregrade.mjsfinds 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 thegroundfield. -
Measure what an equal feature set costs against three.js (part of issue #74; no engine change, no speed-up).
bench/runtimegets the engines67js-ldr,67js-one-sample,67js-ldr-one-sampleandthree-one-sample(run with--engine) and a--cpu-onlyflag. Measured: with no MSAA and no HDR target 67js is still 5.2 to 5.7 times three.js on SwiftShader inlarge-mesh,many-materialsandinstanced-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 3060large-meshcosts 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 inbench/runtime/PROFILE.md. -
Add cascaded sun shadows and light linking (part of issue #52; recipes do not reach either, and
lit-interiorwas 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 newlong-streetreference 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 itsaffectslist (environment.keyAffectsfor the sun) through a per-part mask inmaterial.lightMask; the newlight-linkingreference 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 slotssunVisibility,linkSun,linkPoint,linkSpotandlinkArea), andnpm run reference -- --cascades <n>andframe-time.mjs <scene> <backend> --cascadesmeasure 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
standardthere 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 atstandardkeeps 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 ascene_createwith abakeand ascene_applywithset-bake.bake.usedandbake.reducedare unchanged; the capability limitmaxRayWorkis nowmaxPathWork. -
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
LICENSEandTHIRD-PARTY-NOTICES.mdtext, 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, sobuild/mcp/server.mjsnow bundlesscripts/mcp-sdk-shims.mjsinstead, which fails with a clear message if elicitation is ever requested:server.mjsfell from 677,484 to 484,111 bytes and the archive by about 47,000 bytes.build/mcp/LICENSES.mdno longer lists the Ajv family because that code is gone, and its unused copies leftlicenses/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:packagenow loads everyexportsentry 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.archiveBytesbudget 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_unwrapand pattern textures are pure functions of their input, sorunKernelin the newsrc/kernel-offload.jscomputes them inline as before, or, insidebuildWithKernels, throwsKernelDeferred, 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 buildsscene_createthat way when the page hasWorker; 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 anAbortSignalends the build and the worker. Measured in Node on the demo recipe: theuv_unwrapstack 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 duringscene_createfell 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_inspectnow reportskernels: how many kernels a worker served and how many ran inline, each fallback with its reason (also a67js kernel fallbackline on stderr when a page that hasWorkerfalls back), the build time and the longest block;npm run smoke:kernel-workersinstalls the package, runs the demo room and fails unless kernels ran on the worker with no fallback. Thelongtaskobserver 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_applyedits still run on the main thread. Core did not grow (entry:core,lit-boxunchanged); 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
clothfailed 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 inreport.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 (standardpassed its 20 s limit on a 56 part room,finehit 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 andscene_inspectbakereportsusedandreduced; explicitsamples,bouncesortexelsPerUnitare never reduced and fail with a message naming the number that fits.bakeLightmapsis nowplanLightmaps,estimateBakeandtraceLightmaps(still exported as one call).entry:coredid not change;entry:light-bakegrew 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
sculptandshrinkwrapmesh edits (part of issue #47; surface patches and a stack form are not built).sculptapplies up to 32 inflate, displace and smooth strokes to the vertices of the selected faces;shrinkwrapmoves them onto a primitive, to the nearest point or projected along a direction, with an offset. Both are inscene_capabilities(meshSculpt) and in themesh_editschema, with the tool schema total unchanged (the face rule and tag prefix descriptions were shortened to pay for them). The stroke maths moved tosrc/sculpt-strokes.jsand servessculptGeometrytoo. 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 taskball-badge, which needs a plate bent onto a ball. -
Add ragdolls (part of issue #96):
physics.ragdollin a recipe with armatures andphysicsin 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. Newsrc/ragdoll.js, loaded with the physics code only. New suite taskrobot-ragdoll. The lazy budgets ofagent-scene+lazyandagent-asset+lazyrise 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
terraingeometry (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, andterrain.isHole(x, z)answers for a point; withoutholesthe 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 newterrainmodule (core did not grow), the tool schema entry is one line with the fields inscene_capabilitiesterrain, 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 taskterrain-hole-walk(ground with a 3 m opening and a walker that has to go around it) with the checksgroundReliefandwalkPath. Measured: Haiku passed it 2 of 2 (9 and 6 tool calls, $0.29 in all) and both exported recipes used theterraingeometry; 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
bakeused to skip every part with a pattern, a map or emission (31 parts, the floor and every wall, in the demo room) and failedscene_createwith "Textured geometry requires UV channel 0" on a node that hosts hair. Those parts now keepuvsand their look and take the bounced light through a newlightMapmaterial slot on a second UV set (geometry.lightUvs); the fragment shader addscolor * (1 - metallic) * lightMap * lightMapScale, so a brick pattern tints its own bounced light. Hair and particle host nodes are skipped and listed inreport.skipped. The shader branch compiles out of scenes with no light map; the renderer carries about 650 bytes of gzip for it inentry:core(seedocs/light-bake.mdfor the measured sizes). GLB export writesTEXCOORD_1and 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 scenebrick-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.
createTracerinsrc/ray-query.jsnow 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(withnormalScale, tangent frame from the UVs as in the rasterizer) andmetallicRoughnessMap(green scales roughness, blue scales metallic) are drawn on UV-mapped materials; on box-projected materials (boxMapScale) they are still ignored andreport.ignoredsays so. Theentry:path-tracersize budget goes from 12,900 to 13,800 bytes gzip for the maps, light selection and the packed ray query;entry:coredid 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-shadersentry, 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 throughloadAgentSceneModules, the MCP server does so itself, and the glTF loader loads it for a file that uses either extension. The unused short overloads ofenvironmentLightingare 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
anisotropyandanisotropyRotation(highlights stretch along a grain, asKHR_materials_anisotropy) andiridescence,iridescenceIorandiridescenceThickness(a thin film that shifts reflection colour with the view angle, asKHR_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_inspectdiagnostics say so. Recipe materials already acceptedclearcoat,transmission,thicknessandiorwith thesurfacetag (a demo film that could not ask for them was running an older build, and its murky marble came from theliquidtag, which absorbs by thickness):scene_capabilitiesnow lists every layer with its range underscene.materialLayers, andnpm run smoke:material-layersrenders a glass ball through the real MCP server and checks that the stripes behind it are bent. A timeline shot also takesfocusDistance(0.01 to 100000 m) andblur(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/referencegets abrushed-filmscene 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
maxSecondsbelow the job deadline: the deadline of an image job withsettings.renderernow reachesmaxSecondsplus 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. Animagerender job selects it withsettings.renderer: { type: "pathTracer", samples, bounces, denoise, seed, maxSeconds }; the same recipe and seed give the same PNG, a job pastmaxSecondsfails 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 insrc/path-tracer-capabilities.js, listed underjobs.pathTracerinscene_capabilities; the tool schemas gain onerendererfield inside the total budget).npm run reference -- --path-tracerscores it against Cycles: better than the rasterizer on 7 of the 11 reference scenes (lit-interior0.5055 to 0.1993,spot-area0.1242 to 0.0639), worse onoutdoor-hdri,characterandfog, mean 0.171 against 0.189. Not drawn: volume fog, normal maps, translucent leaves, caustics, lights seen in mirrors.src/ray-query.jsnow holds the ray query and light falloff thatlight-bakeused to carry.npm run smoke:path-tracerchecks 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-schemasandtests/other-clients.test.jscheck 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, declaredrequiredfields, a strict JSON Schema 2020-12 compile (Ajv 8, now a dev dependency) and an empty-argument call. Reports for OpenAIstrict: trueand 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 callscene_create; a draft-07 Ajv compile rejects every tool ($schema, thenprefixItems), 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 andllms.txtinstalled the package and rendered a box but found no route to a PNG without MCP, sodocs/guide.mdgains a tested "Render a PNG from a script without MCP" recipe that the README andllms.txtpoint to;docs/mcp.mdgains an "Other clients and models" table and no longer pins the standalone launch to 0.9.1. A test checks that every pagellms.txtand the README link to exists. Schema bytes are unchanged. -
Fix the next agent tool errors (part of issue #66).
scene_reviewandscene_lookwithlabelson 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.geometriesandmaterialsare optional in a recipe. A failedscene_createsays no scene was created and to call it again.scene_inspecton 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_lookandscene_reviewwithlabelson an imported asset threwCannot 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 explicitdefaultmaterial instead of an error.sessionIdis 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_applynames the fix whenset-nodefields are wrapped innode. 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
timelinelists named shots played in order over the motion clock, each with aduration, an optionalmotionStart, a partialcameraon top of the sampled one and adipthrough black;set-timelineedits 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,.oggor.flacfrom the new--audio <folder>option into a video, checks that it covers the video, decodes both streams afterwards and reportsmetrics.sync(a gap over 50 ms fails the job);settings.encoderiscpuornvenc. 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 inscene_capabilitiesfilmTimeline). 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
cpuflags (--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 indocs/webgpu-decision.md. -
Add recipe particles and hair (part of issue #63, no new tool). A
particlessection 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 newparticlesmodule (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-particleschecks 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. Newset-particlesoperation and aparticlesreport inscene_inspect; the schema takes one record reference, so the tool schemas stay within the total budget and the fields live inscene_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 indocs/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 theuv_unwrapunwrap. 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,maxResolutionandmaxSecondsare 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-interiortakeslit-interiorfrom 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). Seedocs/light-bake.md. -
Add recipe cloth (part of issue #62, no new tool). A
clothsection records sheets with a position-based solver (src/cloth.js, loaded on demand with the newclothmodule, 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-clothrepeats the PNG bytes through the MCP preview). Newgridgeometry (a flat sheet, 1 to 64 segments),set-clothoperation and aclothreport inscene_inspect. The tool schemas stay within the total budget:set-physicsnow takes the same record reference asset-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_editandmesh_inspecttake a part ID such asmesh-10of an imported asset: the part becomes a welded polygon mesh with axis tags and amaterial-<n>tag, the edit log is kept in the asset recipe asmeshEdits(checkpoints, export and replay work), and every result passesvalidateTopology. 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_inspectreportslocalToWorldScaleandlocalAxesInWorld. 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 taskrobot-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.jsonlists those chunks underpreviewChunkswith their hashes, the MCP runtime serves them next topreview.js, andbench/size/budgets.jsongainsarchiveBytes, whichnpm run smoke:packagechecks. -
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) andcast(sphere). Each has Blender cases intests/fixtures/blender/that match with no listed difference,triangulateandedge_splitcheck their output size before building it, and their fields are described inscene_capabilitiesmeshModifiersso 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 paritynow prints the modifier coverage. -
Add
curveandtextgeometry (part of issue #45).curvesweeps a round or custom profile along a cubic Bezier chain with explicit or automatic handles, a taper along the length and flat caps;textextrudes the outlines of the bundled DejaVu Sans (printable ASCII, 20 KB, loaded on demand with its owntextmodule and never in an entry) or of a font table added withregisterAgentSceneFont, 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 inscene_capabilitiesundercurveText, and the MCP takes no font file or URL. The DejaVu licence is inTHIRD-PARTY-NOTICES.md. Metaballs and NURBS surfaces are recorded as out of scope indocs/parity.md. Not done: a suite task inbench/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).
Renderer67tests 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: falseturns it off; skinned, morphed, swaying, fire, smoke, water, assembly and blended batches and the shadow passes are not culled). A part'slods, the array fromgenerateGeometryLods, 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.createGpuTimerandAdaptiveResolutioninsrc/adaptive-quality.jsgive live viewers GPU timing and a resolution scale with a minimum; an export can not build one, andsrc/main.jsuses it only outsideexport=1. Newlarge-outdoorscene inbench/runtimewith 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 declarelodsyet. Seedocs/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 everyscene_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 genericrelationslist (597 bytes overscene_createandscene_applytogether); the kinds and fields are inscene_capabilitiesrelations, 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 gainskettle-resize,desk-resizeand their plain twins, andtouchesandcentreAtchecks. -
Add render passes, depth of field and camera motion blur (issue #54, first slice). Render jobs of kind
imageorstillstakesettings.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) andeffects.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 new67js/addons/camera-effectsentry; 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 atmotion.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'smotion.layers(up to 8) plays the file's own clips at motion time with a bonemask,weight,speed,offset,loop,additivewithreference,bones(clip track to another asset node) andmorph, 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.morphClipFromGltfAnimationand a layer'smorphsput morph weights throughcreateAssetPose(masks pick parts by their owning bone),createAssetLayerEvaluatorserves hosts that stage the pose themselves, andasset.stagePosetakesmorphWeights. Add therobot-walk-wavetask to the agent modeling suite and extendnpm run smoke:character. Not done: foot tilt on slopes, full-body IK, and rest-pose retargeting from another asset through MCP. Raiseentry:skeleton-poseto 19,000 andentry:agent-assetto 85,200 bytes gzip, and the tool schema budgets by 800 bytes (scene_applyto 47,800,scene_create_assetto 20,150, total to 190,700). -
Use one expression language for fields and drivers (issue #98).
src/expression.jsholds the limits, number literals, operator table, syntax and hostile-input checks;src/mesh-fields.js(per mesh element: attributes, noise, random, vectors) andsrc/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;iftakes its second operand when the condition is not 0 (a driver used above 0);sqrtof a negative,powoverflow andmodby 0 give 0 instead of an error;smoothstepwith equal edges is a step;divby 0 is still a non-finite error in a driver, because a hostile-input test pins it, and 0 in a field. Drivers gainceil,round,sign,lte,gte,eq,and,or,notand booleans; fields gainatan2andsaturate. 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 thescene_capabilitiesbudget 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_applywith abranchname 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_reviewshots take abranchand render it in a private scene;scene_restorewithbranchkeeps one.labels: trueonscene_lookandscene_reviewdraws 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 forscene_apply).npm run smoke:agent-branchesdrives it through MCP on SwiftShader. -
Add a bounded material node graph (issue #50): material
patternoftype: "graph"bakes noise, voronoi, wave, checker, gradient, ramp, maths anduvnodes 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. Addmapping: "uv"to every pattern so it followsuv_unwrapUVs 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.mdand thebrick-graphreference scene. -
Add
decimate,remeshand generated levels of detail (issue #40).decimate(aratioof 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(voxelSizeorresolution, 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,generateGeometryLodsandgenerateAssetLods(also in67js/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(daylightby default,overcast,sunset,studio) selects a small procedural sky map generated in code, plus its key light, withenvironmentIntensityandenvironmentRotation; no URLs or files from tool input.effects.ambientOcclusion(0 to 1) andambientOcclusionRadiusadd screen-space ambient occlusion from the depth texture. It is off by default: measured withnpm 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_capabilitieslistslightingSetups,defaultLightingSetupandambientOcclusion. 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 of0instead of1. The engine default is unchanged (occlusion off, no map) so code scenes keep their pixels. Screen-space reflections are not part of this. Thescene:lit-boxsize 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.mjsreports 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 tomesh_edit,mesh_stackandscene_createfor 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$refdefinitions. The backend still validates every field.tests/tool-cost.test.jsfails when the schema total, one tool's schema or a typical result passesbench/tool-cost-budgets.json. A small mesh change is 187 bytes asmesh_editagainst 1,254 as dispose plus a whole-recipescene_createon a 7-node recipe. -
Return diagnostics that name the next action (issue #66).
mesh_inspectlint addsintersecting-parts,self-intersection,thin-wall,floating-partandasymmetricwarnings from the BVH, with face budgets and askippedCheckslist; they never turnokoff and the agent modeling suite does not grade them.scene_inspectaddsdiagnosticsfor 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 callscene_create, and page errors no longer carry the bundle stack.tests/tool-errors.test.jswalks 27 error paths and rejects a message with no next action. -
Add authored rigs (#46): a recipe
armatureslist (bones with a name, parent, head and tail, or ahumanoidorquadrupedpreset fitted to a mesh node's bounds, withlandmarksto move bones), and askinon a mesh node that binds it with automatic weights, plus aset-armaturesoperation. Each bone becomes a recipe node, so keyframed bone rotation andmotion.ikrun through the same paths as imported rigs, and humanoid bones carry theHUMANOID_BONESnames soretargetClipmaps 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_inspectgainsrig: unweighted vertices, bones with no vertices, and joint axis problems (zero-length bones, straight limb chains, knees that bend backward).scene_export_glbwrites the bones and a walk clip baked at 30 frames a second from tracks and IK. Add therigged-walkertask 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, materialpatternandmotionnow sit behindloadAgentSceneModules(recipe)(orloadAllAgentSceneModules()), andcreateAgentScenethrows an error naming that call when a needed module is missing. A recipe of primitives drops from 141,241 to 73,581 bytes gzip inscene:agent-scene(every lazy chunk still totals 145,049).npm run sizenow bundles with code splitting and reports the initial load, plus a+lazyrow for the total.npm testpreloads 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
limitsper joint and axis in degrees andbendToward;scene_inspectmotionIknow carrieslimited(each pose a limit changed) andwarnings(hyperextended, andwrong-bendfor a knee or elbow bent the unnatural way, which a wrong pole used to hide behindreached).motion.constraintsaddscopy_location,copy_rotation,copy_scale,limit_location,limit_rotation,track_to,child_of(with an inverse and an influence blend) andfloor, andmotion.driverssets 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_ofandfloormatch Blender 5.2 world matrices in 8 recorded cases (tests/fixtures/blender/constraints.json). Joint limits are also in67js/addons/skeleton-pose. Theleg-planttask joins the agent modeling suite. The expression language is a new small one insrc/motion-expression.js: the field expressions of the mesh addon were not onmainyet -
Reach Rapier from recipes: a
physicssection (and aset-physicsoperation) 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 likemotion, so a seek back repeats the same frame (checked as identical PNG bytes on SwiftShader through MCP).scene_inspectreports per-body final positions and resting times, contacts and sensor enter and leave events. Rapier stays an optional peer, served to the preview fromnode_moduleslike 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, pluscontactEvents,restFrames,bakePhysicsScene,loadPhysicsEngineandsetPhysicsModuleLoader. A collider withsensornow bakes instead of throwing. -
Add the
tray-droptask 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:physicsto 17,900,entry:agent-sceneto 121,100,entry:agent-assetto 128,500,scene:agent-sceneto 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_positionanddisplaceas edits and stack nodes, and awherefilter for instances.displaceandset_positionmatch Blender's Displace modifier and a Geometry Nodes Set Position node in five new golden cases -
Add
instanceson a recipe node: repeat its geometry at the points of a grid, a seeded scatter in a box or the surface of a geometry, withwhere,yaw,alignToNormalandscale, at most 2048 candidate points. It expands to ordinary nodes that share geometry and material, so the renderer draws one instanced batch. Add thepillar-courttask to the agent modeling suite -
Add the
scene_export_glbMCP tool (22 tools now) and morph weight clips inexportGlb. 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),motionTimeseeks authored motion first, andclipadds an imported asset's source clip. Clip tracks withproperty: '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 insrc/index.jschanges, so no size budget moves -
Add material layers:
clearcoatandclearcoatRoughness,transmissionwiththickness,ior,attenuationColorandattenuationDistance,sheenColorandsheenRoughness, andspecularandspecularColor. 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 theliquidmaterial. 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.warningsno 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 throughset-environment, the MCP schema andscene_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(withfill,clearInner,clearOuter),bridge_edge_loops(withsegments),subdivide_edges,grid_fillandknife_project. Each tags what it makes, and the edge rule{ "open": true }picks boundary edges. The lint message for an open boundary now suggestsfill_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 writesmap,normalMap,metallicRoughnessMap,occlusionMapandemissiveMapas 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 withoutuv_unwrap. It previously threw on skins and on every texture channel. Theentry:coresize 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.mdhas the profile -
Add
subdivideas 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; thepebbletask joins the agent modeling suite -
Remove
67js/addons/filmfrom 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, copysrc/film.js,src/film-rig.js,src/film-character.js,src/film-physics-definition.jsandsrc/film-physics-clip.jsonfrom 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,extrudeandtubeno longer import it, andscene:agent-scenedrops from 185,681 to 117,290 bytes gzip.latheandtubeoutput is identical to before, vertex for vertex.extrudewalls, 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-querydrops from 85,555 to 15,535 bytes gzip,entry:shrinkwrapfrom 86,779 to 17,860 andentry:surfacefrom 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
zodto 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
arraybefore it allocates. Before this, the check ran after the copies existed -
Change the bevel
profileto 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 aprofileother 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.
metalgoes from 0.0974 to 0.0962 against Cycles (EEVEE 0.0916) andmaterial-spheresfrom 0.1234 to 0.1185; no scene is worse by more than 0.0002, and the frame cost ofbrick-flatandmetalis 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).
metalgoes from 0.0962 to 0.0957 against Cycles;car-paint,brushed-filmandmaterial-spheresalso 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).
environmentLightinginsrc/environment.jsadds 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:metalgoes from 0.1036 to 0.0974 against Cycles (EEVEE 0.0916). Seebench/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
foliagecrown were measured and worsenedmountain-valleyby 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-a0.2072 to 0.1999 (no reflection 0.2084),megacity-dusk-b0.1459 to 0.1242,mountain-valley-a0.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-spheresFLIP rose 0.0093 andbrick-flat0.0061, so nothing was kept and the measurements are inbench/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
shadowBoundsgives noeye,Renderer67and the cascaded shadows derive it fromenvironment.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 (
applyAlphaStateinsrc/renderer.js).mountain-valleywith 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. Thefoliagecrown leak is not this cause and stays open. Seebench/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 ofmegacity-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. Seebench/reference/README.md. -
Measure the bright river of
megacity-dusk-awith 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.pygained--sun-strength; the numbers are inbench/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-flat0.1264 to 0.0547 against Cycles,brick-graph0.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.pyread 67js from an 8-bit PNG but EEVEE from a float array; it now rounds the EEVEE array to 8 bits too (load_eevee), andtests/reference-quantisation.test.jspins it. The EEVEE columns ofbaseline.jsonwere regenerated from the stored arrays where they reproduce the committed numbers:foliageEEVEE against Cycles goes from 0.0888 to 0.0924, and no scene changes order against 67js (metalbecomes a tie at 0.0944). Some cells andgrand.jsonneed a full reference run. Seebench/reference/README.md. -
Record that a graph bump scale per world unit (
normalScale[0.8, 1.2]on the 6 by 4brick-graphwall) 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). Seebench/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-graphgoes 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
fillIntensityis 0 (part of issue #74). A uniform branch did not skip them on SwiftShader, aFILL_ONmacro 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 ofskinned-charactersgoes from about 161 to 125 ms,transparencyfrom 364 to 341 ms andinstanced-objectsfrom 100 to 94 ms. The size budgets of the three scene bundles rose by 100 bytes. Seebench/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 ontransparency, 7.6 to 6.6 onskinned-characters, 5.5 to 4.0 oninstanced-objectsand 3.8 to 3.5 onmoving-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).
fogSkyVisibilityalso builds a grid of the light that emissive parts and point lights give their surroundings (fogBounceGridinsrc/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. Thetemple-hallfog 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 madedark-shaftworse. -
The fog's ambient in the sun's shadow now follows a per-point sky visibility when
Renderer67gets the newfogSkyVisibilityoption fromsrc/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, withoutvolumeFog, or when a recipe setsshadowAmbient, the post shader has the sampling compiled out and draws as before. Against Cycles on the GPU:dark-shaft0.0607 to 0.0559 (region 0.1802 to 0.1282),temple-hall0.0674 to 0.0640 (region 0.2723 to 0.2611; baked region 0.2439 to 0.2335),fog-room0.1049 to 0.1036 (region 0.2405 to 0.1625); every other scene is unchanged to four digits. Thetemple-hallregion without a bake does not yet beat EEVEE (0.2455). The core entry grows 141 bytes gzip (part of issue #164). -
An unset
volumeFog.shadowAmbientis now derived from the scene instead of fixed at 0.15. PassskyOpennessfromsrc/closed-room.jstoRenderer67(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 listsshadowAmbientwhen it was unset. Thefog-valleyreference scene no longer sets it and scores the same 0.1855 against Cycles (EEVEE 0.1952);dark-shaftandfogmove by 0.0012 and 0.0014 (better) andtemple-hallby 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 insrc/post.js): the share of the fog'sambientthat 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, sofog-valleysets 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 loweredfog-valleyonly at the cost offog(+0.02) andtemple-hall(+0.008), and a global floor of 0.4 costfog-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).
traceProbeMapsalso 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 GPUmetalgoes from 0.0964 to 0.0944 (EEVEE 0.0916),metal-room0.5071 to 0.5045,material-spheres0.0956 to 0.0954,car-paint0.0979 to 0.0978, andbrushed-film0.1017 to 0.1019. The probe trace takes about twice as long (metal0.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 (
metal0.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. Seebench/reference/README.md. -
Give smooth metal parts a reflection probe by default (part of issue #167, which stays open). A recipe part with
metallic0.5 or more androughness0.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: falseturns it off andscene_inspectreportsreflections. Against Cyclesmetalgoes from 0.1294 to 0.1036 (EEVEE 0.0916),brushed-film0.1149 to 0.1029,car-paint0.1059 to 0.1038,material-spheres0.1312 to 0.1293 andmetal-room0.5128 to 0.5076. A scene with no reflective part draws the same parts and keeps its frame time;metalcosts about 0.03 ms more on the GPU. -
Rerun the reference suite under the capped sky with one sun (issue #169):
khronos.jsonandpath-tracer.jsonare rescored against fresh Cycles renders (asset-only 67js against Cycles improved on four of five Khronos assets, for exampleSheenChair0.3745 to 0.3316 andClearCoatTest0.2306 to 0.2097; the path tracer got worse onoutdoor-hdri0.1016 to 0.1242,character0.1120 to 0.1289 andmaterial-spheres0.1187 to 0.1383), and baked scores oflit-interior,brick-room,dark-shaft,temple-hallandmetal-roomare now stored inbench/reference/bake.json.npm run reference -- --bake <ids> --skip-blendercompares 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).--scenetakes 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,standardandfinescored 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.mjsgains--bake-cost,--glow-mipsand--repetitions=N(and prints the range of the per-run medians); the reference run printsusedfor every preset bake. Seebench/reference/README.md. -
Draw fewer triangles for forests (issue #186). Swaying
foliageandgrassbatches are now frustum culled with a bound that grows with the wind strength, and tree foliage has thinnershadowLods(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, socreateTreereturns four levels andTREE_LEVEL_DISTANCEShas three entries.mountain-valleydraws 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
foliagecrowns and themountain-valleyfloor 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 interrain-sky.jswas tried and not kept because it worsenedmountain-valleyfrom 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-reflectiondraws the scene from the camera's mirror image into a half-size target, with Fresnel, ripples and a slight blur.mountain-valley-agoes from 0.3530 to 0.2562 against Cycles andmegacity-dusk-bfrom 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.mjsgains--skip-cputo 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-shaftwent from 0.143 to 0.062 FLIPtemple-hallfrom 0.144 to 0.066 andfog-roomfrom 0.195 to 0.105, the threetemple-halloptions 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.pymeasures the darks,render-gpu.mjsdraws chosen scenes with measurement switches anddepth-sheets.pymakes the blind sheets. -
Stop pull requests from conflicting on
CHANGELOG.md(issue #199). GitHub ignores themerge=uniondriver, so each merge turned every other open pull request DIRTY. A change now adds its own file inchanges/andnpm run changes:foldwrites the files intoCHANGELOG.mdas the version's section at release time;npm run changesprints the pending notes..gitattributesis removed. The earlier unreleased notes moved tochanges/000-before-fragments.md. -
The path tracer now honours light linking (part of issue #215). A light with
affects, and the sun withenvironment.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-partmaterial.lightMaskas the renderer and the light bake.report.ignoredno longer lists linking. Thelight-linkingscene 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 throughapplyLightLinks. -
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-hdrifloor 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 inbench/reference/path-tracer.jsonfalls on 14 of 16 scenes (outdoor-hdri0.1242 to 0.0226,character0.1289 to 0.0287,material-spheres0.1383 to 0.0449,glass0.1406 to 0.0385,foliage0.1753 to 0.1001,lit-interior0.2167 to 0.0769,long-street0.1818 to 0.0291).light-linkingrises from 0.6241 to 0.6518 because the tracer still ignoresaffectsand now lights every part more brightly. -
Add a cabinet resize task and
--relations on|offto the agent suite (part of issue #44, which stays open).cabinet-resizeandcabinet-resize-plainask for a 30 percent wider, 10 cm taller edit of a 10-part cabinet;--relations offremoves 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-graphwall. Matching Blender's scale movedbrick-graphagainst Cycles from 0.1602 to 0.1599, so the scale is not the gap; the measurements and the Blender source are inbench/reference/README.md. -
Agent modeling suite:
look-pickoffers 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-hardfor 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.litPaintinggainedblockersandleast, and samples a 41 by 21 grid of the painting. The reference and the wrong places and shortcuts are graded intests/agent-modeling-suite.test.js. Calls with branches on and off are inbench/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 twopowcalls) 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. Seebench/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/runtimegains the67js-lightengine anddocs/guide.mdlists the option. -
Bring the light linking and path tracer rows of
docs/parity.mdup 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 inpublic/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.cssreplaces the layered overrides ofhome.cssandsite-theme.csson 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