@origingame/engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/web/origin-engine.d.ts +44 -5
  2. package/dist/web/origin-engine.js +279 -19
  3. package/dist/web/origin-engine_bg.wasm +0 -0
  4. package/dist/web/origin-engine_bg.wasm.d.ts +18 -5
  5. package/dist/web-publish/origin-engine-lanes.js +289 -48
  6. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  7. package/dist/web-publish/origin-engine.d.ts +44 -5
  8. package/dist/web-publish/origin-engine.js +279 -38
  9. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  10. package/dist/web-publish/origin-engine_bg.wasm.d.ts +18 -5
  11. package/dist/web-publish-2d/origin-engine-lanes.js +289 -48
  12. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  13. package/dist/web-publish-2d/origin-engine.d.ts +44 -5
  14. package/dist/web-publish-2d/origin-engine.js +279 -38
  15. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  16. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +18 -5
  17. package/knowledge/README.md +2 -1
  18. package/knowledge/guides/three-to-og.md +393 -0
  19. package/knowledge/knowledge.json +2 -1
  20. package/knowledge/looks/catalogue.json +123 -2
  21. package/knowledge/package.json +3 -2
  22. package/knowledge/scaffold/origin-engine.json +2 -2
  23. package/knowledge/scaffold/template/package.json +1 -1
  24. package/knowledge/scaffold/template/project.ogt +1 -1
  25. package/knowledge/scripts/validate.ts +209 -27
  26. package/knowledge/skills/og-audio/SKILL.md +33 -7
  27. package/knowledge/skills/og-build/SKILL.md +66 -7
  28. package/knowledge/skills/og-core/SKILL.md +24 -8
  29. package/knowledge/skills/og-feel/SKILL.md +11 -3
  30. package/knowledge/skills/og-game/SKILL.md +680 -51
  31. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  32. package/knowledge/skills/og-model/SKILL.md +374 -0
  33. package/knowledge/skills/og-platform-sdk/SKILL.md +7 -1
  34. package/knowledge/skills/og-taste/SKILL.md +7 -0
  35. package/knowledge/skills/og-ui/SKILL.md +39 -8
  36. package/knowledge/wiki/ai.md +20 -20
  37. package/knowledge/wiki/anim.md +537 -63
  38. package/knowledge/wiki/atmosphere.md +13 -13
  39. package/knowledge/wiki/audio.md +160 -20
  40. package/knowledge/wiki/camera.md +13 -13
  41. package/knowledge/wiki/commands.md +11 -11
  42. package/knowledge/wiki/debug.md +13 -13
  43. package/knowledge/wiki/game.md +8 -8
  44. package/knowledge/wiki/i18n.md +7 -7
  45. package/knowledge/wiki/index.md +81 -3
  46. package/knowledge/wiki/input.md +47 -47
  47. package/knowledge/wiki/load.md +83 -57
  48. package/knowledge/wiki/math.md +102 -102
  49. package/knowledge/wiki/nav.md +34 -34
  50. package/knowledge/wiki/net.md +10 -10
  51. package/knowledge/wiki/path.md +14 -14
  52. package/knowledge/wiki/physics.md +111 -100
  53. package/knowledge/wiki/platform.md +44 -44
  54. package/knowledge/wiki/probe.md +86 -86
  55. package/knowledge/wiki/reflect.md +30 -18
  56. package/knowledge/wiki/render.md +1243 -176
  57. package/knowledge/wiki/rng.md +14 -14
  58. package/knowledge/wiki/shapes.md +727 -0
  59. package/knowledge/wiki/stream.md +15 -15
  60. package/knowledge/wiki/system.md +8 -8
  61. package/knowledge/wiki/systems.md +2 -2
  62. package/knowledge/wiki/terrain.md +24 -20
  63. package/knowledge/wiki/tilemap.md +13 -13
  64. package/knowledge/wiki/time.md +4 -4
  65. package/knowledge/wiki/timeline.md +14 -14
  66. package/knowledge/wiki/transform.md +8 -8
  67. package/knowledge/wiki/tween.md +17 -17
  68. package/knowledge/wiki/ui.md +245 -269
  69. package/knowledge/wiki/video.md +5 -5
  70. package/knowledge/wiki/world.md +36 -36
  71. package/package.json +4 -4
  72. package/schema/api.schema.json +2238 -81
  73. package/shell/page.ts +4 -0
  74. package/shell/platform.ts +5 -0
  75. package/shell/player.ts +8 -2
  76. package/shell/transport.ts +6 -0
  77. package/src/generated/abi.ts +40 -1
  78. package/src/geom.ts +260 -0
  79. package/src/index.ts +19 -1
  80. package/src/node/device.ts +5 -2
  81. package/src/node/playtest-run.ts +1 -0
  82. package/src/node/polish.ts +34 -15
  83. package/src/node/run.ts +23 -6
  84. package/src/platform-local.ts +2 -0
  85. package/src/playtest.ts +8 -0
  86. package/src/runtime.ts +88 -6
  87. package/src/ui-kit/settings.ts +9 -0
  88. package/src/web/gl-state.ts +907 -0
  89. package/src/web/shell.ts +1 -1
  90. package/src/web/worker.ts +8 -2
  91. package/types/bindings.json +1222 -453
  92. package/types/index.d.ts +1868 -315
@@ -116,6 +116,7 @@ Mistakes the games made, or would have, before reading the right page:
116
116
  | **og-feel** | how much: hit stop, shake, squash, easing, floating text, the camera, frame rates, sorting, parallax, controller tunables | time, tween, camera, systems |
117
117
  | **og-ui** | screens and HUDs: templates, widgets, focus, phones, UI styles and motion | ui |
118
118
  | **og-taste** | the look: a style from the premise, composition, colour, type, motion, feedback, atmosphere by intent, the done checklist | — (it uses ui and atmosphere) |
119
+ | **og-model** | a 3D model built as code from a reference image: the detail inventory, eight gated passes, the generators' recipes (§18 has their fields), budgets, the review frames and `og compare` | — (it uses render and shapes) |
119
120
  | **og-ai** | enemies and NPCs: navigation and minds as data | nav, ai |
120
121
  | **og-audio** | sound: events, presets, UI sounds, the mix, music, space | audio |
121
122
  | **og-i18n** | languages: string tables, messages, fonts, line breaking, right to left | i18n |
@@ -155,7 +156,14 @@ beside the rebuild: the reload still happens, the `reload` event carries a
155
156
  and a `check` event with the diagnostics follows; `og test --json` ends
156
157
  with a `test` event whose `check` object has the same three. Read the
157
158
  diagnostics and fix them before anything else; `--no-check` skips the
158
- check (never for `og build --publish`). A created project's
159
+ check (never for `og build --publish`). A `2d` project's publish build
160
+ leaves the 3D world out (terrain, planar water, scattered copies, the 3D
161
+ fluids, volumetric fog: `og.render.capabilities(world).world3d` is false
162
+ there), so `og build --publish` refuses one whose scenes hold `Terrain`,
163
+ `TerrainScatter`, `Water`, `WaterSurface` or `Fluid3D`, naming each
164
+ `scene.ogt:line`, and there `og.terrain.*` and a `SHAPE_HEIGHTFIELD`
165
+ collider answer a refusal naming `world3d`: remove them, or make the
166
+ project `profile "3d"`. A created project's
159
167
  `tsconfig.json` resolves `@origingame/engine` (the generated
160
168
  `types/index.d.ts`) and `@origingame/engine/playtest` from `node_modules`.
161
169
  An example inside the engine repository has no `node_modules`: its
@@ -205,6 +213,36 @@ export default og.game({
205
213
  channel and playtests can call (`0.5:spawn_wave:3`), with their usage in
206
214
  `GameDesc.commandHelp` for `help` (og-build §1). Add one for each state a
207
215
  test needs to reach quickly, instead of playing there.
216
+ - **Arguments are colon-delimited**, the fields after the name, each a
217
+ string (`Number(args[0])` to read a number); a space is part of its
218
+ field, not a separator, and `\:` writes a colon inside one. Two
219
+ arguments, and the usage that says so:
220
+
221
+ ```ts
222
+ commands: { records: (world, args) => setRecords(world, Number(args[0]), Number(args[1])) },
223
+ commandHelp: { records: { usage: "<berries>:<seconds>", doc: "Sets the isle's records." } },
224
+ ```
225
+
226
+ `0.5:records:12:34.5` hands `args` `["12", "34.5"]`; `records:12 34.5`
227
+ would hand one argument, `"12 34.5"`.
228
+ - **Reserved names.** The channel's core actions are not a game's to
229
+ take, and the game refuses to start on one (`'pause' is a core action;
230
+ register under another name`): `screenshot`, `listen`, `touch_move_to`,
231
+ `touch_move`, `touch_start`, `touch_end`, `touch`, `scroll`, `gamepad`,
232
+ `key_down`, `key_up`, `type`, `compose`, `commit`, `move_to_text`,
233
+ `teleport`, `frame_entity`, `dump_world`, `dump_entity`, `dump_ui`,
234
+ `get`, `set`, `seed`, `pause`, `resume`, `advance`, `describe`,
235
+ `expect`, `save_scene`, `quit` and `help`. The actions the engine's
236
+ modules register are refused the same way (`'view' is a registered
237
+ engine action; register under another name`), in every build:
238
+ `bake_probes`, `cover`, `debug`, `dump_ai`, `dump_desync`,
239
+ `dump_lanes`, `dump_nav`, `dump_platform`, `dump_pulse`,
240
+ `dump_render`, `dump_room`, `dump_stream`, `lanes`, `locale`,
241
+ `terrain_stream`, `timeline`, `turntable` and `view` (`help` marks
242
+ them `(registered)`). The engine's `pause`
243
+ freezes the tick from outside the game; the game's own pause (its menu
244
+ open, `Game.paused` set) is a game action under its own name,
245
+ `open_pause`.
208
246
  - **Facts** (`GameDesc.facts`) return a small JSON object for the probe's
209
247
  `state()`: score, phase, lives. Tests assert on facts and dumps.
210
248
 
@@ -247,7 +285,19 @@ registered schema and refuses a model path that does not exist.
247
285
  (ETC1S for colour, UASTC for normal and linear maps), WAV into QOA, fonts
248
286
  into MSDF atlases, scenes into component tables. Import settings live next
249
287
  to the source as `<file>.ogt` (`texture { kind normal }`,
250
- `font { charset latin chars "开始游戏" }`).
288
+ `font { charset latin chars "开始游戏" }`). The prose here writes a
289
+ block on one line for short; in a file, as in every `.ogt`, a `{` ends
290
+ its key's line and a `}` stands alone on its own (the parser refuses
291
+ `'{' must end its line`):
292
+
293
+ ```text
294
+ type import
295
+ version 1
296
+ ---
297
+ texture {
298
+ kind normal
299
+ }
300
+ ```
251
301
  - At load a cooked texture reaches the GPU block-compressed in the device's
252
302
  format with its mips (BC7, and BC5 for normal maps, on desktop GPUs; ASTC
253
303
  on Apple and most Android; ETC2 and EAC elsewhere), a quarter of RGBA8's
@@ -280,9 +330,13 @@ registered schema and refuses a model path that does not exist.
280
330
  A `.vox` with frames (MagicaVoxel's animation) cooks to clips that swap
281
331
  the frames' models: play them as skinned clips (§9). `voxFps 8` sets the
282
332
  rate (default 10), `voxClip walk 0 3` names frames 0 to 3 (repeatable;
283
- without it one clip `frames` holds every frame). Each frame is centred on
333
+ without it one clip `frames` holds the playback range set in the editor,
334
+ else every frame; a node marked to loop repeats its keys over the file's
335
+ frames). Each frame is centred on
284
336
  its own pivot, as in the editor: give the frames one height (or pad them)
285
337
  for a character whose feet stay on the ground.
338
+ - An `.ogt` of `type model` is a model written as text: og-geom parts by
339
+ their `og.render` names, placed, with materials and pivots (§18).
286
340
  - Reusable production assets come through the platform's asset catalog into
287
341
  `assets/`; record each file's licence next to it.
288
342
 
@@ -380,7 +434,13 @@ else ignores it. The stamp is world state, so a replay reproduces it.
380
434
  `flipX`, `flash`, `shadow`, nine-slice `sliceX`/`sliceY`) is drawn every
381
435
  frame at its transform, its scale multiplying the size (so squash and
382
436
  stretch is a tween of `og.Transform.scale`). `og.render.sprite` stays for
383
- one-tick, immediate drawing (HUDs, debug).
437
+ one-tick, immediate drawing (HUDs, debug). Sprites and og.ui's frames
438
+ are filtered anisotropically where the device can
439
+ (`og.render.capabilities(world).spriteAnisotropy`: 16, or 1 on a WebGL2
440
+ without `EXT_texture_filter_anisotropic` and on a software rasterizer), so a sprite or a nine-slice
441
+ squeezed along one axis keeps the other axis's detail (a button's
442
+ outline stays sharp); at 1 it blurs, and og.ui bounds a frame's middle
443
+ at twice its art for that reason.
384
444
  - **Pixel sprites in a 3D set** stand on the ground with `og.Sprite.dropShadow`
385
445
  (or `SpriteDesc.dropShadow`): 0.5 casts the sprite's shape, darkened and
386
446
  flattened onto the ground plane (y 0) along the sun, the first
@@ -393,6 +453,22 @@ else ignores it. The stamp is world state, so a replay reproduces it.
393
453
  ```ts
394
454
  world.insert(hero, og.Sprite, { texture: tex, width: 1, height: 1.5, anchor: { x: 0.5, y: 0 }, dropShadow: 0.5, shadow: 0.3 });
395
455
  ```
456
+ - **Billboards** (three.js `Sprite`): `og.Sprite.facing` (or
457
+ `SpriteDesc.facing`) turns a world sprite to the camera that draws it.
458
+ `"cameraY"` stands it upright and turns it about y (trees, characters,
459
+ props a looking-down camera must not tip back); `"camera"` lays it in
460
+ the view plane (smoke, glints, signs); `"none"`, the default, keeps the
461
+ xy plane. Every camera turns it to itself (og.CameraView, og.Mirror, reflection
462
+ probes), the turn is render-only (never read by systems or snapshots),
463
+ faced sprites in one layer draw back to front from the camera and are
464
+ culled off screen, and a faced sprite's `dropShadow` is its whole shape
465
+ from any angle. It is still a sprite: flash, glow, blend, 2D lighting,
466
+ arrays, flipbooks and `toneMapped` all apply. A billboard behind a
467
+ transparent mesh still draws over it (sprites draw after the 3D scene).
468
+
469
+ ```ts
470
+ world.insert(tree, og.Sprite, { texture: treeTex, width: 2, height: 2.6, anchor: { x: 0.5, y: 0 }, facing: "cameraY", dropShadow: 0.6 });
471
+ ```
396
472
  - **World UI that must not change with the light** (speech bubbles, name
397
473
  tags, markers) takes `og.Sprite.toneMapped: false` (or
398
474
  `SpriteDesc.toneMapped`), three.js's `Material.toneMapped`: still placed
@@ -699,6 +775,65 @@ strike, a bite or a fall at weight 1, back to 0 when `finished`.
699
775
  Transform by the root joint's xz travel and yaw, `full` by all of it,
700
776
  `off` (the default) leaves the hips travelling inside the model; switch it
701
777
  to `planar` for a lunge's clip and back to `off` after.
778
+ **Look-at and joint limits.** `og.LookAt { skeleton, joint, target }`
779
+ on its own entity turns a joint (a head, an eye, a turret's barrel) so its
780
+ `axis` (default +Z, as three.js `lookAt`) points at `target` after the
781
+ clips and IK, its `upAxis` (+Y) rolling towards `up` (world +Y; zero
782
+ keeps the animated roll). `maxYaw` and `maxPitch` (radians from the
783
+ animated forward) hold a head from turning round, `clamped` reads back
784
+ when they did; `bones: 3` spreads the turn over neck, chest and spine;
785
+ `weight` fades it in and out. `og.JointLimit { skeleton, joint, axis,
786
+ swing, twistMin, twistMax }` (radians from the rest pose) limits what
787
+ `og.IkChain` and `og.LookAt` may turn a joint to: a knee is a hinge,
788
+ `axis` +X, `swing: 0.15`, `twistMin: 0`, `twistMax: 2.6`, so a
789
+ foot IK target never bends it backwards; a shoulder is a cone (`swing`).
790
+ The defaults limit nothing, and the clip's own pose is never clamped.
791
+
792
+ ```ts
793
+ const look = must(world.spawn({ name: "hero-look" }));
794
+ must(world.insert(look, og.LookAt, { skeleton: body, joint: HEAD, bones: 2, maxYaw: 1.2, maxPitch: 0.5 }));
795
+ const knee = must(world.spawn({ name: "hero-knee-l" }));
796
+ must(world.insert(knee, og.JointLimit, { skeleton: body, joint: SHIN_L, axis: { x: 1, y: 0, z: 0 }, swing: 0.15, twistMin: 0, twistMax: 2.6 }));
797
+ // each tick, from a system that writes og.LookAt: world.set(look, og.LookAt, { target: playerPosition })
798
+ ```
799
+
800
+ **Foot locking.** `og.FootLock { skeleton, foot }` on its own entity, one
801
+ per foot, keeps a foot the clips plant (within 5 mm of its lowest in the
802
+ clip, the same test retargeting uses) where it touched the ground while
803
+ root motion carries the body on and clips cross-fade, bending the leg
804
+ (two-bone IK, `og.JointLimit`s respected); when the clips lift it the
805
+ foot rejoins its animation over `release` (0.15 s). `planted` and
806
+ `locked` read back each tick; `maxDistance` (0.5 m) lets go before a leg
807
+ would stretch. It needs clips whose feet actually rest: a foot that only
808
+ grazes its lowest for a frame is held for that frame only.
809
+
810
+ **Spring bones.** `og.SpringBone { skeleton, joint, bones }` on its own
811
+ entity, one per chain (a tail, ears, a ponytail, a single jiggling node),
812
+ swings `joint` and its `bones` − 1 parents after their parents' motion:
813
+ each joint's tail is a damped spring (`frequency` 3 Hz, `damping` 0.3;
814
+ 1 settles without overshoot) towards where the animation puts it, under
815
+ `gravity` (`{ x: 0, y: -9.8, z: 0 }` droops it), held at the bone's
816
+ length. `space` world (default) lags behind the entity's motion; model
817
+ ignores it (a teleport does not whip the tail). Applied last, after IK
818
+ and look-at; `lag` reads back how far the chain trails. Deterministic and
819
+ saved with the world. `tipFrequency` and `tipDamping` (1) scale the last
820
+ joint's frequency and damping, the joints between in proportion: 0.25 is
821
+ a strand soft at its tip on a stiff root.
822
+
823
+ Keep hair out of the head with `og.SpringCollider { skeleton, joint,
824
+ shape, offset, tail, radius }` on its own entity, a sphere about `offset`
825
+ or a capsule from `offset` to `tail` in the joint's frame: every tail of
826
+ that skeleton's springs is pushed out of it to its `radius` plus the
827
+ spring's `radius`, keeping its bone's length. A collider's `layers` (1)
828
+ must meet the spring's `colliders` (all) to count.
829
+
830
+ ```ts
831
+ const tail = must(world.spawn({ name: "hero-tail" }));
832
+ must(world.insert(tail, og.SpringBone, { skeleton: body, joint: TAIL_TIP, bones: 3, gravity: { x: 0, y: -4, z: 0 }, tipFrequency: 0.5, radius: 0.02 }));
833
+ const hips = must(world.spawn({ name: "hero-hips-collider" }));
834
+ must(world.insert(hips, og.SpringCollider, { skeleton: body, joint: HIPS, shape: "capsule", offset: { x: -0.15, y: 0, z: 0 }, tail: { x: 0.15, y: 0, z: 0 }, radius: 0.12 }));
835
+ ```
836
+
702
837
  `templates/archetypes/third-person` (`src/anim.ts`: a 1D blend by speed,
703
838
  a hero retargeted at cook time and sentinels at run time) is the pattern,
704
839
  and for a 3D action game as a whole: a CharacterController over a level,
@@ -752,6 +887,7 @@ animation {
752
887
  library "anims/humanoid-clips.glb" # its clips land in this model's #anim, retargeted (repeatable)
753
888
  rig "rigs/hero.ogt" # how this model's bones are named (`type rig`); default: detected from the node names
754
889
  footIk true # retargeting solves the legs so planted feet stay planted (default true)
890
+ mirror "turnLeft" "turnRight" # also write a clip mirrored to the other side, under that name (default "<clip>.mirrored"; repeatable)
755
891
  compress false # quantised samples and keyframe reduction (default false: f32)
756
892
  tolerance 0.0001 # the most compression may move a sampled value (default 0.0001, at most 0.1)
757
893
  }
@@ -771,6 +907,34 @@ stride), and its legs are solved so planted feet stay on the ground
771
907
  (`footIk`, on by default). The fx-showcase `retarget` station is the
772
908
  pattern.
773
909
 
910
+ Hands retarget too: the fingers are humanoid bones (VRM 1.0's
911
+ `leftThumbMetacarpal` … `leftLittleDistal`, read from Mixamo's
912
+ `LeftHandIndex1`, VRoid's `J_Bip_L_Index1`, Rigify's `DEF-f_index.01.L`,
913
+ Unreal's `index_01_l`, plain `index.01.L`; Unity's legacy `Left Thumb
914
+ Proximal` is the metacarpal), each turning from its hand as
915
+ the source's does, so a fist or a grip holds on an A-posed arm whatever
916
+ the arm does. The target's twist bones (unmapped nodes named `twist`, such
917
+ as Unreal's `lowerarm_twist_01_l`, or Rigify's `DEF-forearm.L.001`) take a
918
+ share of their segment's roll by where they sit along it: none at the
919
+ segment's head, the next bone's at its end, so a forearm does not
920
+ candy-wrap. A clip made for the rig itself that leaves its twist bones
921
+ still gets the same shares each tick (after IK); a clip that keys them
922
+ is played as keyed. A character whose rest pose does not stand on the ground (an
923
+ armature lifted off it, a crouched rest) says how high its ankles stand
924
+ in its `type rig` file, `footHeight 0.08` (or `footHeight <left>
925
+ <right>`); planted feet then land on that ground, not at the rest pose's
926
+ foot height.
927
+
928
+ **Mirroring.** `og.anim.mirror(world, { clip, skeleton })` answers a new
929
+ clip that plays `clip` on the other side (a left-handed swing, a limp on
930
+ the other leg, a turn the other way): left and right nodes are paired by
931
+ the side tokens every naming scheme writes (`LeftArm`/`RightArm`,
932
+ `thigh.L`/`thigh.R`, `J_Bip_L_`/`J_Bip_R_`, `calf_l`/`calf_r`), each
933
+ node takes its counterpart's motion reflected across the skeleton's
934
+ x = 0 plane, root motion's sideways travel and turns flip. It assumes a
935
+ symmetric rest pose; mirror a retargeted clip on the skeleton it was
936
+ retargeted for.
937
+
774
938
  **Compression** shrinks a model's clips in the bundle (not in memory: they
775
939
  are rebuilt at load). In the same `animation` block, `compress true`
776
940
  quantises rotations to 48 bits and the rest to 16 and drops the keyframes
@@ -800,6 +964,9 @@ clip walk "walk" {
800
964
  speed 1 # duration multiplier, above 0 (default 1)
801
965
  loop true # false: holds its last frame (default true)
802
966
  }
967
+ clip walkOther "walk" {
968
+ mirror true # plays the clip on the other side of the skeleton (default false): no og.anim.mirror needed
969
+ }
803
970
  clip run "run"
804
971
  clip waving "wave"
805
972
  blend1d move speed { # blend1d <motion> <param>: the two children around the value blend linearly, clamped at the ends
@@ -815,12 +982,14 @@ layer base { # the first layer is the base: root motion and og.An
815
982
  state still idle # state <name> <motion>
816
983
  state move move {
817
984
  speed 1 # playback rate, 0 or more (default 1)
985
+ sync gait # a sync group: states of one group play in step across layers (default none)
818
986
  }
819
987
  entry still # the state it starts in (default: the first)
820
988
  transition still move { # transition <from | any> <to>; in file order, the first that holds fires
821
989
  when speed gt 0.2 # when <param> gt | ge | lt | le | eq | ne <number>; every `when` must hold
822
990
  exit 0.9 # and the source has played this many cycles (a `when` or an `exit` is required)
823
991
  fade 0.25 # cross-fade seconds (default 0.2)
992
+ interrupt any # what may cut this fade: any (default) | none (it completes first) | source (only the state being left) | next (only the new state's own)
824
993
  }
825
994
  transition any still { # any: from every state but the target
826
995
  when speed le 0.2
@@ -829,7 +998,11 @@ layer base { # the first layer is the base: root motion and og.An
829
998
  layer upper {
830
999
  mode additive # override (default): replaces the pose below | additive: adds each clip's difference from its first frame
831
1000
  weight wave # a number 0 to 1, or a parameter clamped to it (default 1)
832
- state wave waving
1001
+ mask "spine" 0.5 # mask "<node>" [weight 0 to 1, default 1]: that node and every node under it (default: no mask, all 1)
1002
+ mask "arm.L" # a deeper line wins for its branch; nodes under no line take 0
1003
+ state wave waving {
1004
+ sync gait
1005
+ }
833
1006
  }
834
1007
  ```
835
1008
 
@@ -837,7 +1010,16 @@ Each state advances one phase by `dt × AnimationGraph.speed × state speed
837
1010
  / duration`, the duration weighted across a blend's clips, so blended
838
1011
  clips stay in step. Per tick and layer the transitions are checked first,
839
1012
  then the phases advance, then the poses blend; a transition during a fade
840
- replaces it. `og.anim.graph` refuses a parse error naming the line, a
1013
+ replaces it unless its `interrupt` says otherwise (`none` for a
1014
+ committed attack or landing, `source` to let an idle re-target a start
1015
+ that has just begun). A layer's `mask` lines confine it to branches of the
1016
+ skeleton (by node name, the nearest named ancestor's weight; an upper-body
1017
+ layer is `mask "spine"`), so a wave or an aim plays over a walk without
1018
+ freezing the legs. In a sync group the first layer (file order) whose
1019
+ current state is in it leads: every later layer's state of that group
1020
+ plays at the leader's phase, whatever its clip's length, and a transition
1021
+ between two states of one group keeps the phase (a walk-to-run blend
1022
+ keeps the feet in step). `og.anim.graph` refuses a parse error naming the line, a
841
1023
  clip name `clips` lacks, a parameter not declared and a motion that
842
1024
  contains itself. Write the parameters from a system every tick
843
1025
  (`world.set(body, og.AnimationGraph, { p0: speed })`); `state` and
@@ -980,7 +1162,9 @@ pixels then, per bone, `bone x y weight` in that bone's space (weights
980
1162
  summing to 1), or plain `x y` in the slot's bone. A bone's or region's
981
1163
  `skew` turns its y axis that many degrees past its x axis. A slot draws
982
1164
  `texel × color + offset`. Bone keys add to the setup pose (`rotate` and
983
- `skew` degrees, `translate` pixels, `scale` multiplies); every key eases
1165
+ `skew` degrees, `translate` pixels, `scale` multiplies); a `rotate` key
1166
+ turns the shorter way to the next, or `rotate 0 0 clockwise 1` (or
1167
+ `counterclockwise n`) that way through n more whole turns; every key eases
984
1168
  linearly to the next, holds with `step`, or follows `curve x1 y1 x2 y2`
985
1169
  (a cubic Bézier from 0,0 to 1,1, x time and y value; more segments as
986
1170
  `x y x1 y1 x2 y2`). A `deform` key offsets every influence in order, one
@@ -988,15 +1172,26 @@ linearly to the next, holds with `step`, or follows `curve x1 y1 x2 y2`
988
1172
  then (alone, the file's order); an `ik` key sets the constraint's weight
989
1173
  and bend. An `ik`'s `bones` are a chain of any length, root first: one
990
1174
  bone points, two bend at the joint, three or more (a tail, a tentacle, a
991
- three-joint leg) are solved by FABRIK and curl to the `bend` side. A `box` draws nothing: `og.anim.skeleton2dBoxes(world, hero)`
1175
+ three-joint leg) are solved by FABRIK and curl to the `bend` side. After the IK, a
1176
+ `transform t { bones a; target b; mix 1 1 1 1 }` pulls bones towards
1177
+ another bone's world rotation, origin, scale and shear by its mix, and an
1178
+ `along s { path rail; bones s1 s2; position 0 percent; spacing 0 length;
1179
+ rotate tangent 0; mix 1 1 }` lays bones along a `path` attachment (a
1180
+ cubic spline, `vertex x y` three per knot, drawn as nothing) while its
1181
+ slot shows it. A `skin armor { swap torso torso-armor; swap hat none }`
1182
+ block shows other attachments of the same slots wherever the default ones
1183
+ would show; `og.Skeleton2D.skin` picks it (`rig.skins.indexOf("armor")`,
1184
+ `default` is 0). A `box` draws nothing: `og.anim.skeleton2dBoxes(world, hero)`
992
1185
  answers its polygon (in the Transform's frame) while its slot shows it,
993
1186
  for hit and hurt boxes a clip switches. A `child` shows an `armature`
994
1187
  block (a rig of its own on the same texture) in its slot; it plays the
995
1188
  parent's clip by name when it has one, else its `clip`, at the parent's
996
- time. A DragonBones 5.5 export is imported instead of written: a rig2d
1189
+ time; its events arrive as og.Skeleton2DEvent with `armature` its name
1190
+ (empty for the skeleton's own clip). A DragonBones 5.5 export is imported instead of written: a rig2d
997
1191
  `.ogt` with `dragonbones "hero_ske.json"` and `atlas "hero_tex.json"`
998
1192
  (skew, colour offsets, zOrder and IK timelines, armature displays,
999
- bounding boxes and curves included; Spine's files are not read, convert
1193
+ bounding boxes, path displays and constraints, every skin, curves and `clockwise`
1194
+ turns included; Spine's files are not read, convert
1000
1195
  them to this text).
1001
1196
 
1002
1197
  ```ts
@@ -1369,8 +1564,10 @@ render sprite 12
1369
1564
  `underwaterDensity` and the surface from below shows the sky only
1370
1565
  straight up (on by default; set the density to 0 to see through).
1371
1566
 
1372
- **Terrain** (`reference/terrain.md`) is a height field cooked from a
1373
- `type terrain` .ogt beside a 16-bit grey PNG (or an EXR in metres):
1567
+ **Terrain** (`reference/terrain.md`, 3D projects only: a `2d` project's
1568
+ publish build has none, §2) is a height field cooked from a
1569
+ `type terrain` .ogt beside a 16-bit grey PNG (or an EXR in metres), or
1570
+ generated by the cook (below):
1374
1571
 
1375
1572
  ```text
1376
1573
  type terrain
@@ -1381,9 +1578,23 @@ height -20 120 # metres
1381
1578
  size 2048 2048 # metres along x and z, centred on the entity
1382
1579
  control "island-control.png" # optional RGBA: layer 0 in R, 1 in G, 2 in B, 3 in A
1383
1580
  heightBlend 0.15 # sharpen the blends by each albedo's alpha (its height)
1384
- layer { albedo "textures/grass.png" normal "textures/grass-n.png" tile 6 slope 0 30 }
1385
- layer { albedo "textures/rock.png" tile 12 triplanar true slope 28 90 }
1386
- layer { albedo "textures/snow.png" altitude 90 1000 altitudeFade 10 }
1581
+ layer { # a block's '}' stands alone on its line
1582
+ albedo "textures/grass.png"
1583
+ normal "textures/grass-n.png"
1584
+ tile 6
1585
+ slope 0 30
1586
+ }
1587
+ layer {
1588
+ albedo "textures/rock.png"
1589
+ tile 12
1590
+ triplanar true
1591
+ slope 28 90
1592
+ }
1593
+ layer {
1594
+ albedo "textures/snow.png"
1595
+ altitude 90 1000
1596
+ altitudeFade 10
1597
+ }
1387
1598
  ```
1388
1599
 
1389
1600
  A layer shows where its control channel (1 without a map) times its slope
@@ -1398,6 +1609,151 @@ world.insert(ground, og.Terrain, { data: info.data });
1398
1609
  world.insert(ground, og.Collider, { shape: SHAPE_HEIGHTFIELD, mesh: info.geometry });
1399
1610
  ```
1400
1611
 
1612
+ **A generated terrain.** Instead of `heightmap`, a `generate` block makes
1613
+ the field in the cook (never both: the cook refuses the pair and names
1614
+ the lines). `height` and `size` mean what they mean for a heightmap: the
1615
+ field is stretched so its lowest sample is `height`'s low and its highest
1616
+ its high, and no sample leaves that range. Every line, with its default
1617
+ (leave out what you do not need; erosion, thermal, terraces, rivers and
1618
+ the sea are off until given):
1619
+
1620
+ ```text
1621
+ type terrain
1622
+ version 1
1623
+ ---
1624
+ height -40 160 # metres
1625
+ size 1024 1024 # metres along x and z
1626
+ generate {
1627
+ samples 257 257 # columns and rows (2 to 4097)
1628
+ seed 1 # another seed, another terrain of the same kind
1629
+ noise {
1630
+ octaves 6 # 1 to 12: more is finer detail
1631
+ frequency 3 # features across the longer side
1632
+ lacunarity 2 # each octave's frequency over the last's (1 to 8)
1633
+ gain 0.5 # each octave's amplitude over the last's (0 to 1): higher is rougher
1634
+ warp 0 # domain warp in features: 0.3 to 1 bends ridges and coasts
1635
+ }
1636
+ mask island 0.4 # land fades to `height`'s low over this share of the half size
1637
+ # or `mask ridge 1`: sharp ridged mountains (0 to 1); default no mask
1638
+ erosion { # hydraulic erosion: seeded drops carve gullies and fill valleys
1639
+ drops 40000 # ~0.5 to 1 per sample; more is deeper erosion and a slower cook
1640
+ lifetime 30
1641
+ radius 3 # samples each drop erodes around it
1642
+ inertia 0.05
1643
+ capacity 4
1644
+ deposition 0.3
1645
+ erosion 0.3
1646
+ evaporation 0.01
1647
+ gravity 4
1648
+ }
1649
+ thermal { # slopes steeper than talus slump into scree
1650
+ talus 35 # degrees
1651
+ iterations 20
1652
+ rate 0.5
1653
+ }
1654
+ terraces {
1655
+ step 10 # metres between steps
1656
+ blend 0.4 # 0 hard steps, 1 smooth ramps
1657
+ }
1658
+ sea 0 # metres above the entity: og.terrain.load's seaLevel
1659
+ shore 2 # metres either side of the sea flattened into beaches (needs sea)
1660
+ rivers { # carved where the flow gathers, down to the sea or the edge
1661
+ threshold 400 # samples draining through before it is a river: lower, more rivers
1662
+ depth 2 # metres
1663
+ width 6 # metres
1664
+ }
1665
+ moisture { # what biomes read, 0 to 1
1666
+ flow 1 # weight of the gathered flow (wet in valleys and by rivers)
1667
+ scale 64 # samples draining through for half of it
1668
+ noise 0.5 # weight of a noise
1669
+ frequency 6
1670
+ }
1671
+ biome { # any number; each writes its layer's control channel
1672
+ layer 0 # required: the layer (0 to 3, one the terrain has)
1673
+ weight 1
1674
+ altitude -1000000 1000000 # metres above the entity
1675
+ altitudeFade 2
1676
+ slope 0 90 # degrees
1677
+ slopeFade 4
1678
+ moisture 0 1
1679
+ moistureFade 0.1
1680
+ }
1681
+ }
1682
+ layer {
1683
+ albedo "textures/sand.png"
1684
+ tile 4
1685
+ }
1686
+ layer {
1687
+ albedo "textures/grass.png"
1688
+ tile 6
1689
+ }
1690
+ layer {
1691
+ albedo "textures/rock.png"
1692
+ tile 12
1693
+ triplanar true
1694
+ }
1695
+ ```
1696
+
1697
+ An island with beaches, rivers and rock on its cliffs, for that file's
1698
+ three layers (a block's `{` ends its line):
1699
+
1700
+ ```text
1701
+ generate {
1702
+ seed 7
1703
+ noise {
1704
+ warp 0.5
1705
+ }
1706
+ mask island 0.45
1707
+ erosion {
1708
+ drops 40000
1709
+ }
1710
+ sea 6
1711
+ shore 3
1712
+ rivers {
1713
+ threshold 300
1714
+ depth 2.5
1715
+ width 8
1716
+ }
1717
+ biome {
1718
+ layer 0
1719
+ altitude -1000 9
1720
+ }
1721
+ biome {
1722
+ layer 1
1723
+ altitude 9 1000
1724
+ slope 0 32
1725
+ }
1726
+ biome {
1727
+ layer 2
1728
+ slope 32 90
1729
+ }
1730
+ }
1731
+ ```
1732
+
1733
+ With `biome` rules the generator writes the control map (a terrain with
1734
+ biomes names no `control`); a channel is the most any of its layer's
1735
+ biomes gives a sample, its `weight` times its three bands. Without biomes
1736
+ there is no control map and the layers' own `slope` and `altitude`
1737
+ place them, as for a heightmap. The same file makes the same terrain on
1738
+ every machine (the generator uses only exact arithmetic and its own
1739
+ random numbers). It is a credible terrain of a kind, never a terrain from
1740
+ a picture: move the seed, the mask and the erosion until it reads right.
1741
+ `og cook --report` gives its height histogram, the share of land above
1742
+ the sea and the rivers' length, and writes its height and control maps
1743
+ as PNGs to `og-out/terrain-maps/` to look at.
1744
+
1745
+ Its sea is the water's level; the water is the terrain's size:
1746
+
1747
+ ```ts
1748
+ if (info.seaLevel !== null) {
1749
+ const sea = must(world.spawn({ name: "sea", position: { x: 0, y: info.seaLevel, z: 0 } }));
1750
+ world.insert(sea, og.Water, { size: info.size, segments: 128 });
1751
+ }
1752
+ ```
1753
+
1754
+ (on the terrain's entity's x and z, under its parent if it has one; an
1755
+ `og.WaterSurface` on the same entity makes the waves move and bodies float).
1756
+
1401
1757
  It is drawn around the camera at finer detail nearby (casting and
1402
1758
  receiving shadows, under the fog), placed by its entity's world transform
1403
1759
  (turn or scale the entity and the terrain turns and scales with it), and
@@ -1479,16 +1835,24 @@ there. `world.set(e, og.Velocity, ...)` refuses on
1479
1835
  an entity without one (a body in `start`, before its first step):
1480
1836
  `world.insert` it there, which adds and sets it.
1481
1837
 
1482
- `og.CollisionStarted` is read on the tick after the physics step. Its
1483
- `normal` points from `a` to `b`; the bodies' `og.Velocity` already contains
1484
- the solver's outgoing velocity, not their incoming impact velocity. Do
1485
- not derive impact weight from those velocities: bodies may already be
1486
- separating. There is no contact-time velocity or impulse on this event.
1487
- For driven bodies, retain the velocity handed to physics in a component
1488
- and read that cache before the next drive system overwrites it (as in
1489
- `examples/rumpus-isle/src/critters.ts`). Project `velocityA - velocityB`
1490
- onto the event normal for closing speed. This estimates linear approach
1491
- speed, not contact-point speed from rotation or forces during the step.
1838
+ `og.CollisionStarted` is read on the tick after the physics step, when
1839
+ the bodies' `og.Velocity` already holds the solver's answer (they may
1840
+ already be parting), so never weigh a hit from og.Velocity. The event
1841
+ carries the hit itself, taken in the step that made the contact. `a` is
1842
+ the lower id, and `normal` points from `a` to `b`. `point` is the deepest
1843
+ contact point.
1844
+ `relativeVelocity` (m/s) is `a`'s velocity relative to `b` at `point` as
1845
+ that step began: each body's linear velocity plus its spin crossed with
1846
+ the point's offset from its centre of mass. A static collider counts as
1847
+ still, and a kinematic body moves at its velocity. Its part along
1848
+ `normal` is the closing speed:
1849
+ `Math.max(0, rv.x * n.x + rv.y * n.y + rv.z * n.z)`, which is positive
1850
+ while they approach. `impulse` (N·s, at least 0) is the normal impulse the
1851
+ solver gave the pair in that step, summed over substeps and contact points:
1852
+ `b` took `impulse` along `normal` and `a` the same against it, so heavier
1853
+ bodies hitting give more. Scale feedback on the closing speed (how fast)
1854
+ or the impulse (how hard, mass included), as
1855
+ `examples/rumpus-isle/src/critters.ts` does for its bumps.
1492
1856
 
1493
1857
  ```ts
1494
1858
  const hit = og.physics.raycast(world, { origin: eye, direction: { x: 0, y: 0, z: -1 }, maxDistance: 30, filter: { mask: 0b10, exclude: player } });
@@ -1747,6 +2111,35 @@ og.render.look(world)?.name; // the
1747
2111
  stays sharper under camera motion and `taaSharpen` (0–1) restores
1748
2112
  detail TAA softens. `RenderCapabilities.antialias` says what the device
1749
2113
  resolved it to.
2114
+ - **Graphics quality** is RenderSettings' quality fields, read every
2115
+ frame: `rectShadows`, `shadowedLights`, `shadowMapScale`,
2116
+ `volumetricFog`, `softParticles`, `screenSpaceReflections`,
2117
+ `ambientOcclusion`, `msaaSamples` (4, or 1 for FXAA where the
2118
+ antialiasing multisamples). `og.render.applyQuality(world, preset)`
2119
+ writes a preset's row of them and records it in
2120
+ `RenderSettings.quality` (`none` before any):
2121
+
2122
+ | Field | low | medium | high |
2123
+ |---|---|---|---|
2124
+ | rectShadows | false | true | true |
2125
+ | shadowedLights | 2 | 8 | 8 |
2126
+ | shadowMapScale | 0.5 | 1 | 2 |
2127
+ | volumetricFog, softParticles, screenSpaceReflections, ambientOcclusion | false | true | true |
2128
+ | msaaSamples | 1 | 4 | 4 |
2129
+
2130
+ Medium is the defaults, the frame drawn before any preset. The renderer
2131
+ reads the fields and nothing else, so `og run --commands
2132
+ "0.5:get:og.RenderSettings"` shows what is drawn, and a field set after
2133
+ the preset wins. The switches only take away: the scene's PostEffects
2134
+ and lights keep their settings. The settings screen applies the
2135
+ player's pick when given the world (`og.ui.settingsScreen(id, { world,
2136
+ settings, ... })`); apply a saved preset yourself at start:
2137
+
2138
+ ```ts
2139
+ start(world) { og.render.applyQuality(world, save.settings.quality ?? "medium"); }
2140
+ ```
2141
+
2142
+ Inside a system, declare `og.RenderSettings` among its writes.
1750
2143
 
1751
2144
  ## 17. Saving progress
1752
2145
 
@@ -1891,6 +2284,201 @@ spaced along the length, as three.js spaces them. Rings and circles are one-side
1891
2284
  standard material draws the triangle edges instead of the faces, one pixel
1892
2285
  wide on every tier.
1893
2286
 
2287
+ **Shapes, extrusions, lathes and solids.** Three.js's modelling classes
2288
+ keep their names, parameters and defaults, so a three.js extrusion is a
2289
+ paste. `og.Shape` and `og.Path2` are three.js's Shape and Path:
2290
+ `moveTo`, `lineTo`, `quadraticCurveTo`, `bezierCurveTo`, `splineThru`,
2291
+ `arc`, `absarc`, `ellipse`, `absellipse`, `closePath`, `holes` and
2292
+ `getPoints(divisions)`. They flatten to points before they cross to the
2293
+ engine, so a points array works wherever a shape does
2294
+ (`reference/shapes.md`).
2295
+ - `shapeGeometry` is a flat shape with holes, one-sided like a circle.
2296
+ - `extrudeGeometry` pushes shapes along +Z (`depth`, `steps`) or along an
2297
+ og.path (`extrudePath`). `bevelEnabled`, `bevelThickness`, `bevelSize`,
2298
+ `bevelOffset` and `bevelSegments` round its edges, as in three.js.
2299
+ - `latheGeometry` turns a profile of `points` round +Y (`segments`,
2300
+ `phiStart`, `phiLength`), for vases, bottles and columns.
2301
+ - `polyhedronGeometry` takes `vertices`, `indices`, `radius` and `detail`.
2302
+ `icosahedronGeometry`, `dodecahedronGeometry`, `octahedronGeometry` and
2303
+ `tetrahedronGeometry` take `radius` and `detail`.
2304
+ - `sweepGeometry` sweeps a 2D `section` along an og.path. Its `scale` and
2305
+ `twist` are keyed over the path's length from 0 to 1, for horns, tusks,
2306
+ handles and twisted columns.
2307
+ - `mergeGeometries` places `parts` (each a geometry with `position`,
2308
+ `quaternion`, `scale` and a `mirror` axis) into one draw. Write an
2309
+ array as the same part at several placements.
2310
+ - `csgGeometry` is `union`, `subtract` or `intersect` of two placed
2311
+ closed geometries. Chain calls for more operands.
2312
+
2313
+ Every result is a geometry like `bufferGeometry`'s, with a BVH, `physics`
2314
+ and `dynamic`, and answered by `geometryInfo`:
2315
+
2316
+ ```ts
2317
+ const gear = new og.Shape().absarc(0, 0, 1, 0, Math.PI * 2, false);
2318
+ gear.holes.push(new og.Path2().absarc(0, 0, 0.3, 0, Math.PI * 2, true));
2319
+ const disc = must(og.render.extrudeGeometry(world, {
2320
+ shapes: gear, depth: 0.2, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05,
2321
+ }));
2322
+ const vase = must(og.render.latheGeometry(world, {
2323
+ points: [{ x: 0, y: 0 }, { x: 0.5, y: 0.1 }, { x: 0.3, y: 1 }, { x: 0.4, y: 1.4 }],
2324
+ segments: 32,
2325
+ }));
2326
+ const cube = must(og.render.boxGeometry(world, { width: 1, height: 1, depth: 1 }));
2327
+ const ball = must(og.render.sphereGeometry(world, { radius: 0.15 }));
2328
+ const dice = must(og.render.csgGeometry(world, {
2329
+ op: "subtract", a: { geometry: cube }, b: { geometry: ball, position: { x: 0, y: 0, z: 0.5 } },
2330
+ }));
2331
+ ```
2332
+
2333
+ The generators and the CSG decide every cut with exact predicates and use
2334
+ no platform trigonometry, so every host makes the same triangles. A CSG
2335
+ operand must be closed (every edge shared by two triangles). Its cost
2336
+ grows with the product of the two triangle counts where their boxes
2337
+ overlap, so cut low-poly operands at load, not every tick.
2338
+
2339
+ Every generator above takes `uvRepeat` as `boxGeometry` does (its uvs
2340
+ scaled, so a texture tiles). `bevelSize` left out (or null) is
2341
+ `bevelThickness - 0.1`, as in three.js: 0.1 at the default thickness.
2342
+
2343
+ **Models written as text: `type model`.** A prop made of these parts
2344
+ belongs in an asset, not in `start`: an `.ogt` of `type model` under
2345
+ `assets/` names each part's geometry by its `og.render` call, with the
2346
+ desc's field names and defaults, places it as `mergeGeometries` places a
2347
+ part, gives it a material and hangs it from a named pivot. `og cook`
2348
+ builds it into the same model blob a glTF cooks to: meshopt, levels of
2349
+ detail with their measured errors, the raycast BVH, the nodes. Load it
2350
+ with `og.load.gltf(world, "models/clockwork.ogt")` and `world.instantiate`,
2351
+ or name it in a scene (`model "models/clockwork.ogt"`), as any model.
2352
+
2353
+ ```
2354
+ type model
2355
+ version 1
2356
+ ---
2357
+ material "brass" {
2358
+ color 0.78 0.52 0.16 # linear, as glTF's baseColorFactor
2359
+ metalness 1
2360
+ roughness 0.32
2361
+ }
2362
+ material "glaze" {
2363
+ color 0.12 0.34 0.72
2364
+ roughness 0.3
2365
+ }
2366
+ pivot "axle" { # a node: code and animation find it by name
2367
+ position 0 1.25 0
2368
+ }
2369
+ part "gear" {
2370
+ pivot "axle" # hangs from the pivot (default: the root)
2371
+ material "brass"
2372
+ position 0 0 -0.1
2373
+ extrudeGeometry {
2374
+ shape { # og.Shape's calls, one a line
2375
+ moveTo 1 0
2376
+ absarc 0 0 1 0 6.283185307179586 false
2377
+ hole {
2378
+ absarc 0 0 0.28 0 6.283185307179586 true
2379
+ }
2380
+ }
2381
+ depth 0.16
2382
+ bevelThickness 0.04
2383
+ bevelSize 0.03
2384
+ }
2385
+ }
2386
+ part "bolt" { # one mesh under three nodes: drawn instanced
2387
+ pivot "axle"
2388
+ material "brass"
2389
+ cylinderGeometry {
2390
+ radiusTop 0.06
2391
+ radiusBottom 0.06
2392
+ height 0.36
2393
+ }
2394
+ at {
2395
+ position 0.55 0 0
2396
+ quaternion 0.7071068 0 0 0.7071068
2397
+ }
2398
+ at {
2399
+ position -0.275 0.476 0
2400
+ quaternion 0.7071068 0 0 0.7071068
2401
+ }
2402
+ at {
2403
+ position -0.275 -0.476 0
2404
+ quaternion 0.7071068 0 0 0.7071068
2405
+ }
2406
+ }
2407
+ part "vase" {
2408
+ material "glaze"
2409
+ position -2.2 0 0
2410
+ latheGeometry {
2411
+ points 0 0 0.32 0.04 0.42 0.4 0.24 0.95 0.3 1.25
2412
+ segments 40
2413
+ }
2414
+ }
2415
+ part "horn" {
2416
+ position 2.2 0 0
2417
+ sweepGeometry {
2418
+ section {
2419
+ absarc 0 0 0.22 0 6.283185307179586 false
2420
+ }
2421
+ path { # og.path's lines: kind, closed, tension, point
2422
+ kind catmullRom
2423
+ point 0 0 0
2424
+ point 0.15 0.6 0
2425
+ point -0.45 1.35 0.15
2426
+ }
2427
+ scale 0 1 # t value, repeatable (twist the same)
2428
+ scale 1 0.05
2429
+ }
2430
+ }
2431
+ part "dice" {
2432
+ position 1.1 0.3 0.55
2433
+ csgGeometry {
2434
+ op subtract
2435
+ a { # an operand: a geometry block, placed
2436
+ boxGeometry {
2437
+ width 0.6
2438
+ height 0.6
2439
+ depth 0.6
2440
+ }
2441
+ }
2442
+ b {
2443
+ position 0 0 0.3
2444
+ icosahedronGeometry {
2445
+ radius 0.08
2446
+ detail 1
2447
+ }
2448
+ }
2449
+ }
2450
+ }
2451
+ ```
2452
+
2453
+ - Geometries: `boxGeometry`, `planeGeometry`, `sphereGeometry`,
2454
+ `cylinderGeometry`, `coneGeometry`, `capsuleGeometry`, `torusGeometry`,
2455
+ `shapeGeometry`, `extrudeGeometry` (`extrudePath { }` a path block),
2456
+ `latheGeometry` (`points x y x y ...`), `polyhedronGeometry`, the four
2457
+ solids, `sweepGeometry`, `csgGeometry` (`a { }`, `b { }`) and
2458
+ `mergeGeometries` (`part { }` blocks), each with `uvRepeat u v`. A
2459
+ part holds exactly one; combine more with `csgGeometry` or
2460
+ `mergeGeometries`.
2461
+ - Shapes: `moveTo`, `lineTo`, `quadraticCurveTo`, `bezierCurveTo`,
2462
+ `splineThru x y ...`, `arc`, `absarc`, `ellipse`, `absellipse` (angles in
2463
+ radians, then `true` for clockwise), `closePath`, `points x y ...`
2464
+ (setFromPoints) and `hole { }`. They flatten to the points og.Shape
2465
+ gives in TypeScript, bit for bit.
2466
+ - Placement: `position`, `quaternion x y z w`, `scale`, `mirror x|y|z`,
2467
+ on the part or in `at { }` blocks, one node each. The same part at
2468
+ several placements is one mesh drawn instanced; a mirrored placement is
2469
+ baked into a mesh of its own.
2470
+ - Materials: `color`, `opacity`, `metalness` (default 0), `roughness`
2471
+ (default 1), `emissive`, `emissiveIntensity`, `doubleSided`,
2472
+ `alphaTest`. A part without one draws glTF's grey default.
2473
+ - `og cook --report` lists the model's `triangles`, `parts`, `draws` (the
2474
+ distinct meshes; instanced placements are one) and `bvhBytes`/`bvhShare`.
2475
+ Past `budgets.triangles` or `budgets.vertices` in project.ogt the cook
2476
+ refuses it, so lower `segments`, `curveSegments`, `steps` or `detail`.
2477
+ Import settings (`models/clockwork.ogt.ogt`, `model { lods ... raycast
2478
+ false }`) apply as to a glTF.
2479
+ - `og dev` re-cooks it on an edit and the running game draws the edit
2480
+ under the same asset handle.
2481
+
1894
2482
  **Lines and points.** `og.render.lines(world, desc)` and
1895
2483
  `og.render.points(world, desc)` are immediate mode, like
1896
2484
  `og.render.sprite`: submit them every tick they should show.
@@ -2154,6 +2742,10 @@ These are the checks to use before opening frames. All of them run from
2154
2742
  `totalBytes` 64 MiB, `soundSeconds` 600, `videoSeconds` 300,
2155
2743
  `fontGlyphs` 8192, `navPolygons` 65536, `sceneEntities` 10000 and
2156
2744
  `emitterParticles` 16384, each a default a project may move.
2745
+ `bvhShare` 30 is a warning, not a limit: a model whose cooked raycast
2746
+ BVH is over 30 % of its blob (and at least 16 KiB) is named by `og cook
2747
+ --report` and `og doctor`; write `model { raycast false }` in its .ogt
2748
+ when nothing raycasts against it.
2157
2749
 
2158
2750
  ## 22. Levels of detail, reflection probes and screen-space reflections
2159
2751
 
@@ -2172,15 +2764,21 @@ model {
2172
2764
  }
2173
2765
  ```
2174
2766
 
2175
- `og cook --report` lists each model's triangles per level. The renderer
2176
- draws each `og.Model` entity at the level its size on screen calls for,
2177
- with hysteresis, so nothing flickers at a boundary. `og.Model.lodBias`
2178
- (per entity) and `RenderSettings.lodBias` (the frame) scale that choice:
2179
- 2 keeps the full mesh twice as far, and 0 always draws it. Check the
2180
- choice with `debug:lod`, which tints models green, yellow, orange and red
2181
- by level, with `dump_entity:<name>` (`render.lod`), and with the
2182
- triangles in `ProbeTimings.triangles` or `dump_render`. A field of 2,000
2183
- trees draws about 4× fewer triangles with LOD on.
2767
+ The cook measures each level's error, the largest distance between the
2768
+ level and the full mesh in the model's units, and `og cook --report`
2769
+ lists each model's triangles (`lods`) and error (`lodErrors`) per level.
2770
+ The renderer draws each `og.Model` entity at the coarsest level whose
2771
+ error covers less than a pixel at the entity's distance, with
2772
+ hysteresis, so nothing flickers at a boundary: a small prop keeps its
2773
+ full mesh until it is far away, a large tree drops levels sooner. A
2774
+ level whose error is too large for its purpose (a hard-surface model
2775
+ melting at a distance) is cooked tighter with `lodError`, or the model
2776
+ keeps more detail with `lodBias`. `og.Model.lodBias` (per entity) and
2777
+ `RenderSettings.lodBias` (the frame) scale that choice: 2 keeps the full
2778
+ mesh twice as far, and 0 always draws it. Check the choice with
2779
+ `debug:lod`, which tints models green, yellow, orange and red by level,
2780
+ with `dump_entity:<name>` (`render.lod`), and with the triangles in
2781
+ `ProbeTimings.triangles` or `dump_render`.
2184
2782
 
2185
2783
  **Reflection probes.** Smooth surfaces reflect the environment map, which
2186
2784
  is the sky. Indoors that is wrong: a chrome sphere in a room would show
@@ -2212,8 +2810,11 @@ world.insert(probe, og.ReflectionProbe, {
2212
2810
  the interval high.
2213
2811
  - Size the box to the room. Each object takes the smallest box holding
2214
2812
  its centre, so two rooms that share a model each reflect their own.
2215
- The capture shows everything in the room: characters in their pose,
2216
- terrain, grass, glass. Terrain, grass and water inside the box are lit
2813
+ The capture shows everything in the room: characters in their pose
2814
+ (and each one takes the probe of the room its pose stands in), terrain,
2815
+ grass, water (refracting what lies below it), 3D fluids, glass,
2816
+ particles (soft ones fading as in the frame), lines and world sprites
2817
+ and text. Terrain, grass and water inside the box are lit
2217
2818
  by it too. At most 8 probes (`MAX_REFLECTION_PROBES`) are used,
2218
2819
  nearest the camera first.
2219
2820
  - `dump_render` lists the frame's probes and whether each was loaded from
@@ -2261,7 +2862,8 @@ default) or `hdr`:
2261
2862
  `depth: true` also keeps the view's linear depth, from near (0) to far
2262
2863
  (1), of its opaque meshes, on every tier, WebGL2 included. It costs a
2263
2864
  second draw of the view's opaque meshes, so ask for it only when a
2264
- material reads it.
2865
+ material reads it. (A scene with soft particles, `softDistance` above 0,
2866
+ pays that draw in every view anyway: its particles fade against it.)
2265
2867
  `RenderCapabilities.maxRenderTargetSize` is the largest side.
2266
2868
 
2267
2869
  **A second camera.** `og.CameraView` on an entity is a camera with
@@ -2285,16 +2887,18 @@ world.insert(p2, og.CameraView, { viewportX: 0.5, viewportWidth: 0.5 });
2285
2887
  A view draws what the main camera draws, from its own eye and on its own
2286
2888
  `layers`: meshes and models (skinned meshes and custom shaders included),
2287
2889
  opaque then transparent back to front, the terrain and the sky, particle
2288
- systems, og.Lines and og.Points, and world sprites and text, lit and
2890
+ systems, trails, og.Lines and og.Points, and world sprites and text, lit and
2289
2891
  shadowed by the frame's lights. Each view is a second draw of everything
2290
2892
  its layers admit, so keep it cheap with `layers` and `every`.
2291
2893
  - An `ldr` target and a viewport are exposed with the view's `exposure`
2292
2894
  and tone mapped with the frame's curve. An `hdr` target holds linear
2293
2895
  radiance and is exposed by the frame that shows it.
2294
2896
  - Bloom, AO, SSR, depth of field, colour grading and custom post passes
2295
- run on the main frame only.
2296
- - In a view, sprites are not 2D-lit and do not glow. Trails, 2D fluids,
2297
- screen sprites, the UI and debug draw stay on the main frame.
2897
+ run on the main frame only (the 2D glow, `PostEffects.glow`, is the
2898
+ exception: a view adds its own).
2899
+ - In a view, sprites are 2D-lit and glow and 2D fluids and soft
2900
+ particles draw as in the main frame. Screen sprites, the UI and debug
2901
+ draw stay on the main frame.
2298
2902
 
2299
2903
  A viewport view draws over the finished image and under the UI.
2300
2904
 
@@ -2340,8 +2944,9 @@ fn fragment(f: FragmentInput, p: Params) -> SurfaceOutput // f:
2340
2944
  // standard_surface(f), noise3(p).
2341
2945
  ```
2342
2946
  An entry you leave out is the standard one.
2343
- - `lit` surfaces are lit by the sun's cascades, the point and spot lights
2344
- with their shadows, the environment and the fog. `unlit` surfaces draw
2947
+ - `lit` surfaces are lit by the sun's cascades, the point, spot and rect
2948
+ lights with their shadows and cookies (a rect light as an area, as on a
2949
+ standard material), the environment and the fog. `unlit` surfaces draw
2345
2950
  as returned.
2346
2951
  - The depth prepass, every shadow pass and every view run the same
2347
2952
  entries, and alpha under the material's `alphaTest` is cut in each of
@@ -2361,11 +2966,15 @@ with up to eight `param` lines. The function reads:
2361
2966
 
2362
2967
  It returns linear HDR. To use one:
2363
2968
  1. Load it with `og.load.post(world, "post/crt.ogt")`.
2364
- 2. Put the handle in `PostEffects.custom1` to `custom8`
2365
- (`RenderCapabilities.maxCustomPosts` is 8). They run in that order, a
2366
- slot left 0 skipped, each over the one before it, after depth of field
2367
- and motion blur and before exposure, bloom and tone mapping. There is
2368
- no `custom` list: a resource's fields hold one value each.
2969
+ 2. Put the handles in the list `PostEffects.custom`, at most
2970
+ `RenderCapabilities.maxCustomPosts` (8) of them. They run in its order,
2971
+ each over the one before it, after depth of field and motion blur and
2972
+ before exposure, bloom and tone mapping. Write the list whole:
2973
+ `post.custom = [grade, crt]`, `post.custom = [...post.custom, h]`,
2974
+ `post.custom = []`. Reading it answers a frozen copy, so
2975
+ `post.custom.push(h)` throws instead of changing nothing; nine passes
2976
+ are refused, never cut to eight. A look gives it on one line:
2977
+ `custom "post/tint.ogt" "post/crt.ogt"`.
2369
2978
  3. Set its parameters with `og.render.setPostParam(world, post, "curve",
2370
2979
  [0.1])`.
2371
2980
 
@@ -2384,11 +2993,22 @@ door. Mark the scene's unmoving models `static true` and put an
2384
2993
  entity "hall" {
2385
2994
  model "models/hall.glb"
2386
2995
  static true
2387
- Transform { position 0 0 0 quaternion 0 0 0 1 scale 1 1 1 }
2996
+ Transform {
2997
+ position 0 0 0
2998
+ quaternion 0 0 0 1
2999
+ scale 1 1 1
3000
+ }
2388
3001
  }
2389
3002
  entity "hall-probes" {
2390
- Transform { position 0 2 0 quaternion 0 0 0 1 scale 1 1 1 }
2391
- LightVolume { size 12 4 12 spacing 1 }
3003
+ Transform {
3004
+ position 0 2 0
3005
+ quaternion 0 0 0 1
3006
+ scale 1 1 1
3007
+ }
3008
+ LightVolume {
3009
+ size 12 4 12
3010
+ spacing 1
3011
+ }
2392
3012
  }
2393
3013
  ```
2394
3014
 
@@ -2450,3 +3070,12 @@ textures between lanterns to stay under it.
2450
3070
  `reference/index.md` (the generated wiki shipped with this skill) maps every
2451
3071
  module (grouped by skill, as in §1), type and function to its page, with the IDL text, defaults, ranges
2452
3072
  and units of every field.
3073
+
3074
+ `guides/three-to-og.md` (beside this skill) maps three.js's classes and
3075
+ idioms to the engine's: the scene graph to entities and og.Parent,
3076
+ Object3D to og.Transform, the geometry classes to §18's generators, the
3077
+ materials to StandardMaterialDesc, the loaders to og.load, the render
3078
+ loop to systems on the tick, Raycaster to `og.render.raycast`, and the
3079
+ gaps by name. `og migrate three <file.ts>` rewrites a three.js file's
3080
+ mechanical part with those mappings and marks every line it cannot,
3081
+ citing the guide's section.