Reference

Migrating from 2.x to 3.x

What broke between Meep 2 and Meep 3 - the Shade renderer, WebGPU-only, no three.js - with the full import map, the new constructor shapes, and a worked port of a real demo.

Meep 3 replaced the renderer. Everything else in the engine survived the transition close to unchanged; the graphics half did not. This page is the practical map: what moved, what changed shape, what is simply gone, and how to work through a file with the answers in front of you.

Verified against 3.21.0. Every specifier below is prefixed @woosh/meep-engine/ - the package’s exports map is "./src/*": "./src/*", so the path in the table is the path on disk.

The four changes that cause everything else

1. The renderer is Shade, and it ships in the package. Meep 2 drew through three.js. Meep 3 draws through Shade, meep’s own renderer, which lives in-tree under src/shade/. It is GPU-driven and visibility-buffer deferred: meshlet-clustered geometry sits GPU-resident in one buffer, culling runs on the GPU, and the rasterizer writes a visibility buffer and a G-buffer instead of shading as it draws. Lights are binned into froxels and applied deferred, then the frame is temporally resolved, tonemapped and presented. Nothing about “Forward+” applies any more. See rendering overview.

2. three.js is gone. Not optional, not a peer, not present. No module under src/ imports it. Any code you have that constructs a THREE.* type, subclasses one, or reaches through a meep component to a .object is dead. three should come out of your package.json at the same time you bump the engine.

3. WebGPU only. There is no WebGL path and no fallback. Below the device floor, startup fails cleanly rather than degrading. The floor: adapter features indirect-first-instance and float32-blendable, maxStorageBuffersPerShaderStage >= 10, and 32 bytes of colour-attachment budget per sample. A missing navigator.gpu throws a plain Error; anything below the floor throws ShadeDeviceFailure - catch both. A device lost later is not recovered. In practice the browser floor is Chromium 149 or newer, because Shade’s shaders use the WGSL immediate_address_space extension that shipped there.

4. The rendering ECS systems moved to src/engine/graphics3/ and every constructor changed. Meep 2’s uniform new XSystem(engine) is gone; the new shapes take explicit collaborators, usually (graphics, scene).

Everything outside rendering is close to a no-op upgrade. Physics, navigation, AI and behaviour trees, generation, math, ECS core, the DOM view system, networking, plugins and options, storage, logging and localization are unchanged or near enough. The two exceptions are audio and input.

Order of work

  1. Bump @woosh/meep-engine to 3.x and delete three from package.json. Node’s floor is >= 24.
  2. Rewrite the graphics system registrations - new paths, new constructors, and the scene argument.
  3. Wire the scene once, deliberately. See the black-frame trap.
  4. Replace three.js geometry, material, colour and math types with their Shade equivalents.
  5. Rename EntityManager.simulate to update.
  6. If you deserialize old saves that carry sound: call convertLegacySoundComponents.
  7. Delete what has no replacement, and decide what to do about each hole.

Import map

Named (N) vs default (D) export is called out because several flipped in 3.x - a default import from a module that now exports a name fails at build time if you are lucky and at runtime if you are not.

Rendering ECS systems

2.x3.xexport3.x constructor
src/engine/graphics/GraphicsEngine.js (N)src/engine/graphics3/GraphicsEngine.jsN() - built by Engine, started from Engine.start()
src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js (N)src/engine/graphics3/ShadedGeometrySystem.jsN(graphics, scene)
src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js (N)src/engine/graphics3/MeshSystem.jsN(graphics, scene, load)
src/engine/graphics/ecs/mesh/MeshSystem.js (N, the v1 system)src/engine/graphics3/MeshSystem.jsNas above - and the v1 Mesh component is gone, use SGMesh
src/engine/graphics/ecs/light/LightSystem.js (D)src/engine/graphics3/LightSystem.jsN(graphics, scene)
src/engine/graphics/ecs/camera/CameraSystem.js (N)src/engine/graphics3/CameraSystem.jsN(graphics)
src/engine/ecs/terrain/ecs/TerrainSystem.js (D)src/engine/graphics3/TerrainSystem.jsN(graphics, scene, assetManager)
src/engine/graphics/ecs/water/WaterSystem.js (D)src/engine/graphics3/WaterSystem.jsN(graphics)
src/engine/graphics/ecs/decal/v2/FPDecalSystem.js (N)src/engine/graphics3/DecalSystem.jsN(graphics, assets)
src/engine/graphics/particles/ecs/ParticleEmitterSystem.js (N)src/engine/graphics3/ParticleEmitterSystem.jsN(graphics, assets)
src/engine/graphics/ecs/trail3d/Trail3DSystem.js (D)src/engine/graphics3/Trail3DSystem.jsN(graphics)
src/engine/graphics/ecs/path/PathDisplaySystem.js (N)src/engine/graphics3/PathDisplaySystem.jsN(graphics)
graphics/ecs/highlight/system/: MeshHighlightSystem (D), RenderableHighlightSystem, HighlightSystemBasesrc/engine/graphics3/HighlightOutlineSystem.js (the pass) + .../mesh-v2/aggregate/SGMeshHighlightSystem.js (the model source)N(graphics) and (outline, meshes)
graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.jssame pathN(outline), dependencies = [Highlight, ShadedGeometry]
src/engine/ecs/fow/FogOfWarSystem.js (N)src/engine/graphics3/FogOfWarSystem.jsN(graphics)
src/engine/graphics/render/gizmo/GizmoRenderingPlugin.jssrc/engine/graphics3/DebugDrawSystem.jsN(graphics) - the Gizmo component keeps its path
src/engine/ecs/systems/AnimationSystem.js (D)src/engine/graphics3/AnimationSystem.jsN(graphics, meshes)
src/engine/graphics/ecs/animation/animator/AnimationGraphSystem.js (N)src/engine/graphics3/AnimationGraphSystem.jsN(graphics, meshes)
src/engine/ecs/tooltip/TooltipComponentSystem.js (N)src/engine/graphics3/TooltipComponentSystem.jsN({graphics, tooltips, pointer, localization, picking})
-src/engine/graphics3/PickingSystem.jsN (new)(meshes)
-src/engine/graphics3/VolumetricLightMapSystem.jsN (new)(graphics, scene)
-src/engine/graphics3/ParticipatingMediaSystem.jsN (new)(graphics, scene)

Two traps in that table worth stating on their own. src/engine/ecs/terrain/ecs/TerrainSystem.js still exists in 3.x as a default export - it just renders nothing, so an unchanged import compiles and silently draws no terrain. And outlining is now a pass plus a source system per kind of highlighted thing: graphics3/HighlightSystem.js still resolves (it re-exports SGMeshHighlightSystem) but its constructor changed, so new HighlightSystem(graphics, meshes) has to become new HighlightOutlineSystem(graphics) plus new SGMeshHighlightSystem(outline, meshes) - and new ShadedGeometryHighlightSystem(outline) if you outline primitives.

Renderer plumbing that was replaced

2.x3.x
graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.jsGone. Froxel binning is internal; there is nothing to register or size.
graphics/StandardFrameBuffers.js, graphics.frameBuffersGone. Typed frame records inside RenderExtension.record(frame). See render extensions.
graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.jsGone. GTAO is always present; renderer.feature_ssao_enabled defaults to true. Delete the import and its config.addPlugin(...) line.
graphics/render/visibility/hiz/buffer/HierarchicalZBuffer.js, hiz/query/BatchOcclusionQuery.jsThe HZB moved inside Shade (src/shade/renderer/hiz/) and is not something you drive; the batch occlusion query has no replacement.
graphics/trail/x/** (RibbonX, RibbonXPlugin, RibbonMaterialX)Gone. Use Trail3D, or make_gradient_stroke on the dynamic-mesh path.
graphics/geometry/optimization/merge/merge_geometry_hierarchy.jsGone, no replacement. GPU culling makes CPU merging pointless.
graphics/geometry/MikkT/**, buffer_geometry_ensure_tangentsGeometry#ensureTangents() / Geometry#computeTangents()
graphics/texture/virtual/VirtualTextureSystem.js and friendsPer-material opt-in: StandardShadeMaterial.vt_stack = VirtualTextureStack.from({...}). See virtual texturing.
graphics/sh3/lpg/**, sh3/lpv/**, sh3/lightmap/**, sh3/gi/**All gone. GI is now renderer.indirect_lighting_mode plus the VolumetricLightMap component. See global illumination.
graphics/sh3/sky/hosek/make_environment_sky_hosek.jsGone as glue. The Hosek CPU sky itself survives; see sky and environment for the conversion recipe.
graphics/context/WebGLContextMonitor.js, engine.graphics.contextGone. engine.graphics.on.{contextLost,contextFailed} carry a ShadeDeviceFailure; contextRestored never fires.
view/graphics/WebGLContextFailureView.js (N)src/view/graphics/GraphicsContextFailureView.js (N) - and Engine now installs it for you, so remove your own subscription or you get two overlays.
graphics/camera/CameraShake*.jsUnchanged, same paths.

Assets and models

2.x3.x
engine/asset/loaders/GLTFAssetLoader.js (N)engine/asset/loaders/GLTFSceneBundleAssetLoader.js (N) - yields a SceneBundle, and create() returns the same shared instance every call
engine/asset/loaders/texture/TextureAssetLoader.js, loaders/material/**Gone. DecalSystem and ParticleEmitterSystem register an ImageRGBADataLoader for GameAssetType.Image themselves. GameAssetType.Texture and DeferredTexture remain as strings, but no loader produces them.
-engine/asset/loaders/image/ImageBitmapAssetLoader.js (N, new). load_gltf asks the asset manager for a model’s images, so without this registered against GameAssetType.ImageBitmap you load geometry with no textures.
-engine/asset/load_model_scene_bundle.js -> load_model_scene_bundle(assetManager, url) (N)
-engine/graphics3/instantiate_scene_bundle.js -> instantiate_scene_bundle, SceneBundleInstance (N)
GameAssetType.ModelThreeJs, GameAssetType.AttachmentSocketsRemoved from the enum.
engine/save/GameStateLoader.jsGone, no replacement. See persistence for the serializer flow.

The loader registration that most ports need:

import { GameAssetType } from "@woosh/meep-engine/src/engine/asset/GameAssetType.js";
import { GLTFSceneBundleAssetLoader } from "@woosh/meep-engine/src/engine/asset/loaders/GLTFSceneBundleAssetLoader.js";
import { ImageBitmapAssetLoader } from "@woosh/meep-engine/src/engine/asset/loaders/image/ImageBitmapAssetLoader.js";

const gltf = new GLTFSceneBundleAssetLoader();

config.addLoader(GameAssetType.ModelGLTF, gltf);
config.addLoader(GameAssetType.ModelGLTF_JSON, gltf);
config.addLoader(GameAssetType.ImageBitmap, new ImageBitmapAssetLoader());

ECS, hierarchy, animation

2.x3.x
EntityManager#simulate(dt)EntityManager#update(dt)
engine/ecs/transform/Transform.js - the pose componentengine/ecs/transform/Transform64.js. Every system’s dependency tuple names it; Transform still ships and is still read by the serialization registry, but nothing depends on it. position.set(x,y,z) becomes setTranslation(x,y,z), rotation.set(...) becomes setRotation(...) plus updateMatrix(), position.x becomes translation_x, and fromJSON keys the translation translation rather than position. rotation.fromAxisAngle(axis, angle) has no member to call - the getter hands back a bare Float64Array - so it becomes t64_set_rotation_axis_angle(t, ax, ay, az, angle) (engine/ecs/transform/t64_set_rotation_axis_angle.js), whose axis must be unit length. A Transform64 has no signals, so a write ends with t64_announce_change(ecd, entity). See the ECS overview.
engine/ecs/parent/ChildEntities.jsengine/ecs/hierarchy/EntityChildIndex.js (N) - derived, never serialized
engine/ecs/attachment/**, engine/ecs/sockets/**engine/ecs/transform-attachment/{TransformAttachment,TransformAttachmentSystem}.js (N)
bone lookup by HumanoidBoneTypetransform_attachment_find_descendant_by_name(dataset, root, name); world pose via engine/graphics3/pose/query_entity_node_world_pose.js. BoneMapping and HumanoidBoneType still exist but nothing reads them.
graphics/ecs/animation/animator/graph/AnimationGraph.jsengine/graphics3/animation/AnimationGraphController.js (N) - it is the component. See animation graphs.
engine/ecs/animation/AnimationOptimizer.jsShadeAnimationClip#optimize()
engine/ecs/transform/copy_three_transform.jst64_copy_from_transform.js, transform_copy_from_t64.js (N)
engine/ecs/renderable/{RenderSystem,Renderable}.jsGone (both were already deprecated in 2.x).
engine/ecs/systems/TimerSystem.js, engine/ecs/components/Timer.jsGone, no replacement.

Colour and geometry helpers

core/color/ renamed its name2name modules to name_to_name (hex2rgb.js -> hex_to_rgb.js, rgb2hsv.js -> rgb_to_hsv.js, and so on). The old paths still ship as deprecated re-exports, so nothing breaks on the bump - but one pair changed meaning rather than spelling:

2.x3.xnote
hsv2rgb (0..255)hsv_to_rgb_uint8the byte variant kept its behaviour, not its name
hsv2rgb_float (0..1)hsv_to_rgbthe unsuffixed name now means 0..1

The object half-edge mesh is gone: TopoMesh, TopoVertex, TopoEdge, TopoTriangle and the whole tm_* family (~91 modules) were replaced by the flat binary topology under core/geom/3d/topology/struct/binary/ with a bt_* prefix. The mapping is mechanical - query_edge_is_manifold becomes bt_edge_is_manifold, computeTopoMeshBoundingSphere becomes bt_mesh_compute_bounding_sphere. Quadric3 moved to core/geom/3d/quadric/. See geometry.

The constructor-shape change

In 2.x every graphics system took the engine and dug what it needed out of it:

// 2.x
config.addSystem(new ShadedGeometrySystem(engine));
config.addSystem(new LightSystem(engine));

In 3.x each takes exactly the collaborators it uses, and the Shade Scene is one of them:

// 3.x
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import { LightSystem } from "@woosh/meep-engine/src/engine/graphics3/LightSystem.js";

const scene = EngineHarness.shadeScene(engine);

config.addSystem(new ShadedGeometrySystem(engine.graphics, scene));
config.addSystem(new LightSystem(engine.graphics, scene));

engine.graphics is the GraphicsEngine facade; it is null on an engine configured without graphics, which is what makes headless work possible. EngineHarness.shadeScene(engine) hands out the one Shade Scene per engine, memoized in a WeakMap - call it as often as you like, you get the same scene, which is the point.

The scene-wiring rule and the black-frame trap

GraphicsEngine constructs its own empty Scene and renders that until it is told otherwise. set_scene(scene) is what tells it. Only three systems call set_scene for you: LightSystem, MeshSystem and ParticipatingMediaSystem.

So an app that registers, say, ShadedGeometrySystem alone puts its meshes in the harness scene while the renderer is still drawing the facade’s empty one. Nothing throws. You get a black frame.

take the scene from EngineHarness.shadeSceneregister the graphics systemsregistered LightSystem or MeshSystem?the scene is set for youcall graphics.set_scene yourselfframes draw that sceneyesno
engine.graphics.set_scene(EngineHarness.shadeScene(engine));

Calling it when a system would have done it anyway is harmless - it is an assignment. Two related facts, both from set_scene itself: a scene that arrives without an environment map gets make_default_environment() installed, because a scene with no environment renders unlit; and a scene that arrives with one keeps it.

simulate becomes update

// 2.x
engine.entityManager.simulate(dt);

// 3.x
engine.entityManager.update(dt);

simulate survives as a deprecated alias assigned onto the prototype, so old code keeps running. Rename anyway - and note that the class’s own JSDoc example still shows simulate, so don’t take that as a signal.

The deprecated *3 aliases

Every module in src/engine/graphics3/ has a sibling with a 3 suffix - MeshSystem3.js next to MeshSystem.js, twenty of them in total - each a one-line deprecated re-export:

export const MeshSystem3 = MeshSystem;

The suffix marked the 2.x-to-3.x transition and disambiguated against a 2.x name that no longer exists. They are kept because an import path is published interface, not because they are the name to use. Always import the unsuffixed name. If you have code written against an early 3.x preview that uses the suffixed names, dropping the 3 is a safe mechanical rename.

Deleted with no replacement

These are the holes. Each one is a decision you have to make, not an import you can rewrite.

GoneWhat to do
Impostors and LOD - every baker and shader under graphics/impostors/**There is no impostor system and no LOD system at all in Meep 3. Meshlet clustering and GPU culling carry the load instead. Ten orphaned data and UV-encoder modules remain in the tree with no importers; ignore them.
SH3 light probe grids and volumes, lightmap baking - graphics/sh3/lpg/**, sh3/lpv/**, sh3/lightmap/**Rebuild on renderer.indirect_lighting_mode - IBL, Brick4 or LPV. See global illumination.
Trail2DSystemThe Trail2D component still ships, JSON round-trip intact; the system that drew it does not exist, so it renders nothing at all. Move to Trail3D, or draw the beam yourself on the dynamic-mesh path.
RibbonX and its plugin, material and meshTrail3D plus make_gradient_stroke.
The old virtual texture systemPer-material vt_stack, opaque materials only. The 2.x tiled converter output still reads, via new VTSourceTiled({ layers, legacy_meep_mips: true }).
MeshPreview / MeshView widgetsNo widget replacement. engine/graphics3/preview/make_model_thumbnail returns pixels and is tooling-only.
RenderSystem / RenderableAlready deprecated in 2.x. Use ShadedGeometry or SGMesh.
AnimationOptimizerShadeAnimationClip#optimize().
CapsuleGeometryNo visual capsule. Build one from make_cylinder_geometry plus two hemispheres, or use a different shape. The physics CapsuleShape3D is unaffected.
TextureAssetLoader and the material loadersDelete the registration. Register ImageRGBADataLoader for GameAssetType.Image and ImageBitmapAssetLoader for GameAssetType.ImageBitmap instead.
GameStateLoaderNo replacement. Use BinaryBufferSerializer / BinaryBufferDeSerializer directly.
The TopoMesh familyThe bt_* binary topology, as above.
The node-based particle GLSL codegen - graphics/particles/node-based/**No replacement you can use. src/shade/renderer/particles/ is a bytecode VM and is not wired into the renderer; do not build on it. The CPU particle simulation itself is unchanged.
TopDownCameraControllerHelperNo replacement.
merge_geometry_hierarchy, BatchOcclusionQuery, TimerSystemNo replacement; all three solved problems the new renderer does not have.

Audio: run the legacy conversion

The legacy sound stack moved to src/engine/sound/sopra/legacy/ under the same filenames and was demoted to data only. SoundEmitter, SoundTrack and SoundController still deserialize; nothing plays them. SoundEmitterSystem and SoundControllerSystem are deleted, and there is no system of those names to swap in - the replacements are AudioEmitterSystem (src/engine/sound/ecs/audio/) and AudioEventTriggerSystem (src/engine/sound/ecs/trigger/).

Old scenes and saves keep loading, but what they load is inert until you convert it. That pass is deliberately not run by the deserializer - a host owns its load pipeline - so you call it yourself, after deserialization:

import { convertLegacySoundComponents } from "@woosh/meep-engine/src/engine/sound/sopra/legacy/convertLegacySoundComponents.js";
import { SopraDefaultBus } from "@woosh/meep-engine/src/engine/sound/sopra/SopraEngine.js";

const { emitters, triggers, discarded } = convertLegacySoundComponents(dataset, {
    knownBusIds: Object.values(SopraDefaultBus),
});

It mutates the dataset in place, turning SoundEmitter into AudioEmitter and SoundController into AudioEventTrigger, and returns counts of each plus the legacy components it removed with nothing to play.

Pass knownBusIds. A legacy channel is a free string on old authored data, and sopra’s BusGraph.getInput throws on an id it does not have - so without the set, a stale channel name survives conversion and fails at the emitter’s first play. Given the set, an unrecognised channel is logged and rewritten to effects, which is what the 2.x runtime did at play time anyway.

Two things conversion drops on purpose, because neither survived a load in 2.x either: track.time (the legacy runtime always restarted a restored track from zero) and panningModel (the binary adapter never wrote it). See events and mixing.

Input: the deprecations

Input (engine/input/ecs/components/Input.js) and InputSystem (engine/input/ecs/systems/InputSystem.js) still ship and still work, and both are marked @deprecated. The replacement is the Input Map System - InputMap and InputMapSystem under engine/input/ecs/ism/ - which binds typed triggers instead of magic strings, expresses chords and sequences, arbitrates between contexts, and serializes.

One piece of 2.x advice inverted: InputController and InputControllerSystem are not deprecated. They own pointer gesture binding - tap, drag, pinch - which the binding layer does not model, and they remain the only way to get it. If your 2.x notes say “prefer InputSystem over InputControllerSystem”, that is now backwards. See input devices.

TopDownCameraControllerHelper was removed outright.

three.js to Shade

The equivalence table for the types you are most likely to be holding. Shade’s own modules all use named exports.

three.jsMeep 3
WebGLRendererRenderer (src/shade/renderer/Renderer.js), normally reached through engine.graphics
SceneScene (src/shade/renderer/scene/Scene.js), obtained via EngineHarness.shadeScene(engine)
Object3DNode3D (src/shade/renderer/scene/Node3D.js)
MeshShade Mesh / SkinnedMesh, or the ECS components ShadedGeometry and SGMesh
PerspectiveCameraPerspectiveCamera (src/shade/renderer/camera/), or the ECS Camera component
OrbitControlsOrbitalCameraController (src/shade/renderer/camera/), or TopDownCameraController in the ECS
BufferGeometry, Float32BufferAttributeGeometry and Attribute (src/shade/renderer/geometry/), plus StandardAttributes for the names
MeshStandardMaterialStandardShadeMaterial (src/shade/renderer/material/)
MeshBasicMaterial and the other material classesNo equivalent. StandardShadeMaterial is the only concrete material; there is no unlit material.
DoubleSide / FrontSide / BackSidematerial.draw_side = ShadeDrawSide.Double (.Front, .Back). Double-sided drawing works.
Color, 0xrrggbb literalsmeep’s Color (src/core/color/Color.js) - see the sRGB note below
Vector3, Quaternionmeep’s Vector3 and Quaternion (src/core/geom/), both default and named exports, both Float64Array subclasses
Matrix4No matrix class. Matrices are 16-element arrays; use the m4_* family in src/core/geom/3d/mat4/ (m4_multiply, m4_look_at, m4_perspective, m4_invert, m4_transpose, …)
RaycasterPickingSystem + PickingQuery - async, batched, and against AABBs rather than triangles. See picking.
CanvasTexturecreateImageBitmap(canvas) -> ShadeImage -> ShadeTexture
GLTFLoaderGLTFSceneBundleAssetLoader, or load_gltf directly
RGBELoader + PMREMGeneratorload_cube_environment / load_environment_map, then graphics.set_environment_map(texture). No prefilter step - Shade convolves on the GPU.
AnimationMixer, AnimationActionAnimationGraphController + GPUAnimationManager
ArrowHelper, GridHelper, AxesHelperthe Gizmo component + DebugDrawSystem
EdgesGeometry + LineSegmentsNo equivalent. Debug lines go through Gizmo; wireframe overlays have to be built as geometry.
mergeBufferGeometriesNo equivalent. Merge by hand into one Geometry, or don’t - GPU culling makes CPU merging much less useful.
SkeletonUtilstransform_attachment_find_descendant_by_name + query_entity_node_world_pose

Geometry generators

Shade ships seven primitive generators, all in src/shade/renderer/geometry/primitives/. Two shapes three.js gives you are not among them.

three.jsShade
BoxGeometry(w, h, d)make_box_geometry(width, height, depth, widthSegments, heightSegments, depthSegments)
PlaneGeometry(w, h)make_plane_geometry(width, height, widthSegments, heightSegments)
SphereGeometry(r, wSeg, hSeg)No UV sphere. make_octahedron_geometry(radius, detail) - a subdivided octahedron; detail 3 or 4 reads as smooth at typical prop scale.
CylinderGeometry(rTop, rBottom, h, seg)make_cylinder_geometry(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength)
ConeGeometry(r, h, seg)make_cylinder_geometry(0, r, h, seg)
TorusGeometry(...)make_torus_geometry(radius, tube, radialSegments, tubularSegments, arc)
TorusKnotGeometry(...)make_torus_knot_geometry(radius, tube, tubularSegments, radialSegments, p, q)
PolyhedronGeometry(...)make_polyhedron_geometry(vertices, indices, radius, detail)
CapsuleGeometry(...)No equivalent.

A generator returns an authoring-form Geometry. The renderer draws the clustered GPU form, so run it through meshlet_geometry_build_from_geometry once per shape - it is a precompute, not a per-frame cost, and the result is shareable across every entity using that shape.

import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";

const geometry = meshlet_geometry_build_from_geometry(make_box_geometry(1, 1, 1));

Materials

StandardShadeMaterial is the whole material story. Its fields, and what they replace:

FieldType2.x counterpart
diffuse_colorColorMeshStandardMaterial.color
roughness_factornumber.roughness
metallic_factornumber.metalness
emissive_factorColor.emissive
texture_albedo, texture_normal, texture_orm, texture_emissiveShadeTexturethe corresponding maps; ORM packs occlusion/roughness/metalness in one texture
transmission_factor, ior_factornumbertransmission extension
vt_stackVirtualTextureStackthe old virtual texture material

Inherited from ShadeMaterial: name, transparency_mode (TransparencyMode.Opaque / AlphaTested / Transparent), draw_mode, draw_side.

KHR_texture_basisu (KTX2) is read and transcoded and meshopt bufferViews are decoded; Draco is not - see meshes and materials.

The sRGB trap

This one bites everybody, because it fails quietly and only looks slightly wrong.

Color is colour-space agnostic. Color.parse, toHex and toCssRGBAString speak encoded sRGB - the hex a designer types. Everything the renderer does, and everything in core/color/operations/, expects linear. Handing a parsed hex straight to a material makes it visibly too bright:

// wrong - #4ef0a8 as sRGB bytes, interpreted as linear
material.diffuse_color.copy(Color.parse("#4ef0a8"));

// right
material.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse("#4ef0a8")));

Decode once, at the point where the authored colour enters your code - not at every use.

What is linear and what is not. Decode a hex on its way into any of these:

SlotSpace
StandardShadeMaterial.diffuse_color, .emissive_factorlinear
Light.colorlinear
Decal.colorlinear - the decal pass composites into the G-buffer albedo, before anything is lit
HighlightDefinition.colorlinear - the outline pass blends into the colour buffer before tone mapping
glTF baseColorFactoralready linear, by the glTF spec. The loader assigns it untouched; do not decode it
an albedo texturedecoded by the sampler, because the loader marks the image ColorSpace.SRGB

Your ported scene will look more saturated than it did under 2.x

Same hexes, deeper colours. That is the port working, not a mistake.

meep 2 drove three.js 0.136, which predates three’s colour management, and configured the renderer with outputEncoding = LinearEncoding. Two conversions were missing, one at each end of the frame:

  • new THREE.Color(0x4ef0a8) kept the encoded bytes, and the shader used them as a linear albedo - too bright, and with the channel ratios flattened, which is what reads as washed out.
  • the tone-mapped frame went to the canvas without the sRGB encode, which darkened it again.

The two omissions pull in opposite directions, so a flat mid-tone came out roughly where it belonged and the pipeline looked plausible. What did not survive is everything in between: ACES saw albedo values two to four times brighter than they should have been, and ACES desaturates as it approaches white.

Shade does both conversions. diffuse_color is linear, and the frame is tone-mapped and then encoded exactly once, by sRGBTransferOETF. Measured on this site’s highlight demo, whose floor is an untextured #161c24 under the standard 6 lux sun:

floor albedorenders as
from_sRGB_to_linear(parse("#161c24"))rgb(23, 29, 37) - the authored hex is rgb(22, 28, 36)
the #161c24 bytes, undecodedrgb(90, 109, 131)

A neutrally lit surface at roughly unit exposure coming back as the hex you typed is what a correct round trip looks like; it is not a promise the renderer makes at every exposure. The useful number is the other one - undecoded, that floor is four times too bright.

If the new look is too much in a given scene, reach for the palette or for exposure, not for the colour space: the hexes in a 2.x project were chosen against a pipeline that lightened and flattened them.

Porting a demo, start to finish

The simple-cube example is the smallest complete port. Here is the whole of it.

package.json - bump the engine, drop three.js:

   "dependencies": {
-    "@woosh/meep-engine": "2.170.0",
-    "three": "0.136.0"
+    "@woosh/meep-engine": "3.21.0"
   }

Imports - import * as THREE from "three" goes; ShadedGeometrySystem moves to graphics3; the shape, the material and the colour type arrive from Shade and from core/color:

-import * as THREE from "three";
-
 import { EngineHarness }     from "@woosh/meep-engine/src/engine/EngineHarness.js";
 import Entity                from "@woosh/meep-engine/src/engine/ecs/Entity.js";
-import { Transform }         from "@woosh/meep-engine/src/engine/ecs/transform/Transform.js";
+import { Transform64 }       from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";

 import { ShadedGeometry }       from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
-import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
+import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
+
+import { make_box_geometry }                    from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
+import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
+import { StandardShadeMaterial }                from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
+import { Color }                                from "@woosh/meep-engine/src/core/color/Color.js";

Note that ShadedGeometry - the component - did not move. Components largely kept their paths; it is the systems that relocated.

Registration - the constructor gains its two collaborators:

 const engine = await EngineHarness.bootstrap({
     configuration: (config, engine) => {
-        config.addSystem(new ShadedGeometrySystem(engine));
+        config.addSystem(new ShadedGeometrySystem(engine.graphics, EngineHarness.shadeScene(engine)));
     },
 });

The cube itself - geometry through the meshlet builder, material from Shade, colour decoded from sRGB:

-const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
-const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0x4ef0a8 });
-const cubeMesh     = ShadedGeometry.from(cubeGeometry, cubeMaterial);
+const cubeGeometry = meshlet_geometry_build_from_geometry(make_box_geometry(1, 1, 1));
+
+const cubeMaterial = new StandardShadeMaterial();
+cubeMaterial.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse("#4ef0a8")));
+
+const cubeMesh = ShadedGeometry.from(cubeGeometry, cubeMaterial);

The pose - the one ECS change in the file:

-const t = new Transform();
-t.position.set(0, 0, 0);
+const t = new Transform64();
+t.setTranslation(0, 0, 0);

Everything else - EngineHarness.buildBasics, new Entity().add(...).build(ecd), the postRender FPS hook - is untouched. That is the shape of a typical port: the ECS scaffolding survives, the pose component changes name and accessors, the three.js objects at the leaves get swapped, and the system registration line grows two arguments.

The sibling cube-wall example adds the two remaining moves you will hit constantly. Spheres:

-const ballGeometry = new THREE.SphereGeometry(BALL_RADIUS, 28, 20);
+const ballGeometry = meshlet_geometry_build_from_geometry(make_octahedron_geometry(BALL_RADIUS, 4));

and hex colour constants, which become strings and go through one shared factory rather than being decoded at every call site:

function standardMaterial(hexColor, roughness, metalness) {
    const material = new StandardShadeMaterial();

    material.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse(hexColor)));
    material.roughness_factor = roughness;
    material.metallic_factor = metalness;

    return material;
}

Where to go next