@origingame/engine 0.2.0 → 0.4.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 (118) hide show
  1. package/bin/og.mjs +1 -1
  2. package/dist/web/origin-engine.d.ts +255 -5
  3. package/dist/web/origin-engine.js +1584 -125
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +80 -3
  6. package/dist/web-publish/origin-engine-lanes.js +1288 -82
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +199 -6
  9. package/dist/web-publish/origin-engine.js +1285 -79
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +65 -4
  12. package/dist/web-publish-2d/origin-engine-lanes.js +1285 -79
  13. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  14. package/dist/web-publish-2d/origin-engine.d.ts +198 -5
  15. package/dist/web-publish-2d/origin-engine.js +1282 -76
  16. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +64 -3
  18. package/knowledge/README.md +2 -1
  19. package/knowledge/guides/three-to-og.md +393 -0
  20. package/knowledge/knowledge.json +2 -1
  21. package/knowledge/looks/catalogue.json +40 -40
  22. package/knowledge/package.json +3 -2
  23. package/knowledge/scaffold/distribution.json +14 -0
  24. package/knowledge/scaffold/origin-engine.json +2 -2
  25. package/knowledge/scaffold/template/README.md +2 -1
  26. package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
  27. package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
  28. package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
  29. package/knowledge/scaffold/template/host.d.ts +46 -0
  30. package/knowledge/scaffold/template/package.json +1 -1
  31. package/knowledge/scaffold/template/project.ogt +1 -1
  32. package/knowledge/scaffold/template/src/main.ts +1 -1
  33. package/knowledge/scaffold/template/tests/frames.ts +134 -0
  34. package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
  35. package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
  36. package/knowledge/scaffold/template/tsconfig.json +1 -1
  37. package/knowledge/scripts/mount.ts +2 -1
  38. package/knowledge/scripts/validate.ts +159 -31
  39. package/knowledge/skills/og-audio/SKILL.md +2 -2
  40. package/knowledge/skills/og-build/SKILL.md +29 -7
  41. package/knowledge/skills/og-core/SKILL.md +31 -5
  42. package/knowledge/skills/og-feel/SKILL.md +2 -2
  43. package/knowledge/skills/og-game/SKILL.md +1331 -16
  44. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  45. package/knowledge/skills/og-model/SKILL.md +374 -0
  46. package/knowledge/skills/og-platform-sdk/SKILL.md +2 -1
  47. package/knowledge/skills/og-taste/SKILL.md +82 -73
  48. package/knowledge/skills/og-ui/SKILL.md +80 -58
  49. package/knowledge/wiki/ai.md +20 -20
  50. package/knowledge/wiki/anim.md +392 -41
  51. package/knowledge/wiki/atmosphere.md +36 -13
  52. package/knowledge/wiki/audio.md +18 -18
  53. package/knowledge/wiki/camera.md +19 -13
  54. package/knowledge/wiki/commands.md +19 -13
  55. package/knowledge/wiki/debug.md +414 -0
  56. package/knowledge/wiki/game.md +8 -8
  57. package/knowledge/wiki/i18n.md +7 -7
  58. package/knowledge/wiki/index.md +317 -8
  59. package/knowledge/wiki/input.md +47 -47
  60. package/knowledge/wiki/load.md +238 -70
  61. package/knowledge/wiki/math.md +2607 -0
  62. package/knowledge/wiki/nav.md +34 -34
  63. package/knowledge/wiki/net.md +340 -0
  64. package/knowledge/wiki/path.md +402 -0
  65. package/knowledge/wiki/physics.md +95 -95
  66. package/knowledge/wiki/platform.md +463 -29
  67. package/knowledge/wiki/probe.md +681 -67
  68. package/knowledge/wiki/reflect.md +29 -17
  69. package/knowledge/wiki/render.md +4993 -1998
  70. package/knowledge/wiki/rng.md +14 -14
  71. package/knowledge/wiki/shapes.md +727 -0
  72. package/knowledge/wiki/stream.md +15 -15
  73. package/knowledge/wiki/system.md +8 -8
  74. package/knowledge/wiki/systems.md +2 -2
  75. package/knowledge/wiki/terrain.md +24 -20
  76. package/knowledge/wiki/tilemap.md +13 -13
  77. package/knowledge/wiki/time.md +4 -4
  78. package/knowledge/wiki/timeline.md +396 -0
  79. package/knowledge/wiki/transform.md +8 -8
  80. package/knowledge/wiki/tween.md +17 -17
  81. package/knowledge/wiki/ui.md +560 -654
  82. package/knowledge/wiki/video.md +157 -0
  83. package/knowledge/wiki/world.md +36 -36
  84. package/package.json +4 -3
  85. package/schema/api.schema.json +28151 -19640
  86. package/shell/page.ts +26 -2
  87. package/shell/platform.ts +51 -0
  88. package/shell/player.ts +15 -2
  89. package/shell/transport.ts +6 -0
  90. package/src/depth.ts +71 -0
  91. package/src/e6.ts +32 -1
  92. package/src/env.d.ts +5 -1
  93. package/src/generated/abi.ts +226 -4
  94. package/src/geom.ts +260 -0
  95. package/src/index.ts +32 -4
  96. package/src/light.ts +18 -0
  97. package/src/math.ts +622 -0
  98. package/src/node/index.ts +17 -0
  99. package/src/node/playtest-run.ts +115 -18
  100. package/src/node/polish.ts +13 -3
  101. package/src/node/render.ts +291 -0
  102. package/src/node/run.ts +86 -5
  103. package/src/p2.ts +19 -1
  104. package/src/platform-local.ts +2 -0
  105. package/src/playtest.ts +65 -4
  106. package/src/runtime.ts +100 -9
  107. package/src/targets.ts +28 -0
  108. package/src/trace.ts +169 -0
  109. package/src/ui-kit/huds.ts +34 -34
  110. package/src/ui-kit/menus.ts +12 -12
  111. package/src/ui-kit/settings.ts +11 -2
  112. package/src/ui-kit/store.ts +18 -18
  113. package/src/ui-kit/story.ts +6 -4
  114. package/src/ui-kit/util.ts +8 -8
  115. package/src/web/shell.ts +59 -1
  116. package/src/web/worker.ts +105 -4
  117. package/types/bindings.json +9368 -6619
  118. package/types/index.d.ts +16377 -11520
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: og-game
3
- description: Building a game on Origin Engine — the project layout, components and systems on the ECS, the game entry, scenes as .ogt text, assets cooked from assets/ to cooked/ (glTF and MagicaVoxel models), the og.load loaders, streaming groups, sprites, texture arrays and drop shadows, skeletal and stepped animation, effects and physical materials, looks (the frame's art direction as data: og.Stylise, og.MaterialStyle, presets), tone mapping, exposure, environment maps, shadows and antialiasing, fog, sky, day and night, weather and water, terrain, material shaders in WGSL, physics beyond rigid bodies (joints, character and platformer controllers, vehicles, ropes, cloth, 2D and 3D fluids, ragdolls) and its queries, and the performance budget; the entry point, with the map of the other nine skills and the modules each teaches. Use first, and when creating, structuring or extending a TypeScript Origin Engine game (a project with a project.ogt; not a Godot project.godot).
3
+ description: Building a game on Origin Engine — the project layout, components and systems on the ECS, the game entry, scenes as .ogt text, assets cooked from assets/ to cooked/ (glTF and MagicaVoxel models), the og.load loaders, streaming groups, sprites, texture arrays and drop shadows, skeletal and stepped animation, effects and physical materials, looks (the frame's art direction as data: og.Stylise, og.MaterialStyle, presets), tone mapping, exposure, environment maps, shadows and antialiasing, fog, sky, day and night, weather and water, terrain, material shaders in WGSL, physics beyond rigid bodies (joints, character and platformer controllers, vehicles, ropes, cloth, 2D and 3D fluids, ragdolls) and its queries, footage and og render's videos, and the performance budget; the entry point, with the map of the other nine skills and the modules each teaches. Use first, and when creating, structuring or extending a TypeScript Origin Engine game (a project with a project.ogt; not a Godot project.godot).
4
4
  ---
5
5
 
6
6
  An Origin Engine project is plain files: TypeScript in `src/`, source assets
@@ -64,8 +64,13 @@ The order the games found productive:
64
64
  6. **Run with commands** (og-build §1, §4): `og run . --headless
65
65
  --commands "...;screenshot:x.png;...;quit"`, then open each frame and
66
66
  say what it shows. Do this after every change that shows on screen.
67
+ Add `describe:frame.json` beside a screenshot to get the frame as data:
68
+ the visible entities nearest first, each with its screen box, pick point,
69
+ depth and the fraction hidden; the UI elements with rects and text; the
70
+ camera (§21).
67
71
  7. **Playtests with reference frames** (og-build §3): `tests/*.og.ts`
68
- asserting on facts, `get:` answers and dumps, one per feature, and a
72
+ asserting on facts, `get:` answers and dumps, or written as `expect:`
73
+ steps alone, which `og run` itself checks (§21), one per feature, and a
69
74
  frames playtest comparing screenshots with `references/<tier>/`, its
70
75
  frame reads inside `t.frame`: in a sandbox without a renderer each
71
76
  screenshot is reported "no renderer, frame unchecked" and passes while
@@ -109,8 +114,9 @@ Mistakes the games made, or would have, before reading the right page:
109
114
  | **og-game** (this one) | the project, scenes, assets and loading, input, 2D, animation, effects, atmosphere, terrain, shaders, physics, performance, looks, light and shadows | game, transform, input, load, stream, render, physics, atmosphere, anim, tilemap, terrain |
110
115
  | **og-build** | the loop: cook, run with a script, read frames and dumps, hot reload, playtests, `og polish` (§5 there: the one list of what it scores), shipping | commands, probe |
111
116
  | **og-feel** | how much: hit stop, shake, squash, easing, floating text, the camera, frame rates, sorting, parallax, controller tunables | time, tween, camera, systems |
112
- | **og-ui** | screens and HUDs: templates, widgets, focus, phones, skins and motion | ui |
117
+ | **og-ui** | screens and HUDs: templates, widgets, focus, phones, UI styles and motion | ui |
113
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) |
114
120
  | **og-ai** | enemies and NPCs: navigation and minds as data | nav, ai |
115
121
  | **og-audio** | sound: events, presets, UI sounds, the mix, music, space | audio |
116
122
  | **og-i18n** | languages: string tables, messages, fonts, line breaking, right to left | i18n |
@@ -150,7 +156,14 @@ beside the rebuild: the reload still happens, the `reload` event carries a
150
156
  and a `check` event with the diagnostics follows; `og test --json` ends
151
157
  with a `test` event whose `check` object has the same three. Read the
152
158
  diagnostics and fix them before anything else; `--no-check` skips the
153
- 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
154
167
  `tsconfig.json` resolves `@origingame/engine` (the generated
155
168
  `types/index.d.ts`) and `@origingame/engine/playtest` from `node_modules`.
156
169
  An example inside the engine repository has no `node_modules`: its
@@ -275,9 +288,13 @@ registered schema and refuses a model path that does not exist.
275
288
  A `.vox` with frames (MagicaVoxel's animation) cooks to clips that swap
276
289
  the frames' models: play them as skinned clips (§9). `voxFps 8` sets the
277
290
  rate (default 10), `voxClip walk 0 3` names frames 0 to 3 (repeatable;
278
- without it one clip `frames` holds every frame). Each frame is centred on
291
+ without it one clip `frames` holds the playback range set in the editor,
292
+ else every frame; a node marked to loop repeats its keys over the file's
293
+ frames). Each frame is centred on
279
294
  its own pivot, as in the editor: give the frames one height (or pad them)
280
295
  for a character whose feet stay on the ground.
296
+ - An `.ogt` of `type model` is a model written as text: og-geom parts by
297
+ their `og.render` names, placed, with materials and pivots (§18).
281
298
  - Reusable production assets come through the platform's asset catalog into
282
299
  `assets/`; record each file's licence next to it.
283
300
 
@@ -375,7 +392,13 @@ else ignores it. The stamp is world state, so a replay reproduces it.
375
392
  `flipX`, `flash`, `shadow`, nine-slice `sliceX`/`sliceY`) is drawn every
376
393
  frame at its transform, its scale multiplying the size (so squash and
377
394
  stretch is a tween of `og.Transform.scale`). `og.render.sprite` stays for
378
- one-tick, immediate drawing (HUDs, debug).
395
+ one-tick, immediate drawing (HUDs, debug). Sprites and og.ui's frames
396
+ are filtered anisotropically where the device can
397
+ (`og.render.capabilities(world).spriteAnisotropy`: 16, or 1 on a WebGL2
398
+ without `EXT_texture_filter_anisotropic` and on a software rasterizer), so a sprite or a nine-slice
399
+ squeezed along one axis keeps the other axis's detail (a button's
400
+ outline stays sharp); at 1 it blurs, and og.ui bounds a frame's middle
401
+ at twice its art for that reason.
379
402
  - **Pixel sprites in a 3D set** stand on the ground with `og.Sprite.dropShadow`
380
403
  (or `SpriteDesc.dropShadow`): 0.5 casts the sprite's shape, darkened and
381
404
  flattened onto the ground plane (y 0) along the sun, the first
@@ -883,6 +906,289 @@ world.insert(body, og.AnimationGraph, { asset: graph });
883
906
  // every tick: world.set(body, og.AnimationGraph, { p0: groundSpeed, p1: airborne ? 1 : 0 });
884
907
  ```
885
908
 
909
+ ### 2D skeletons: the rig2d grammar
910
+
911
+ A cut-out character (bones, a cape that bends, feet that stay on a slope)
912
+ is an `.ogt` of `type rig2d` beside its PNG, loaded with
913
+ `og.load.rig2d(world, "rigs/hero.ogt")` (`reference/anim.md`). Pixels,
914
+ +y up, degrees counter-clockwise, `unit` pixels per world unit; bones
915
+ parent first, slots draw in file order:
916
+
917
+ ```text
918
+ type rig2d
919
+ version 1
920
+ ---
921
+ texture "hero.png"
922
+ filter nearest
923
+ unit 20
924
+ bone root {
925
+ }
926
+ bone hip {
927
+ parent root
928
+ at 0 22
929
+ rotation 90
930
+ length 16
931
+ }
932
+ slot torso {
933
+ bone hip
934
+ attachment torso
935
+ color 1 1 1 1
936
+ offset 0 0 0 0
937
+ }
938
+ region torso {
939
+ slot torso
940
+ rect 0 0 12 20
941
+ at 8 0
942
+ rotation -90
943
+ size 12 20
944
+ }
945
+ mesh cape {
946
+ slot cape
947
+ vertex 0 32 hip 16 9 1
948
+ vertex 6 40 hip 8 4 0.4 cape-1 7 0 0.6
949
+ triangles 0 1 2
950
+ }
951
+ box hurt {
952
+ slot torso
953
+ polygon -6 0 6 0 6 20 -6 20
954
+ }
955
+ child flame {
956
+ slot lantern
957
+ armature fire
958
+ clip burn
959
+ at 15 0
960
+ }
961
+ ik leg {
962
+ bones thigh shin
963
+ target foot
964
+ bend clockwise
965
+ }
966
+ armature fire {
967
+ bone base {
968
+ }
969
+ # slots, regions, meshes, boxes, ik and clips, as at the top
970
+ }
971
+ clip idle {
972
+ duration 2
973
+ loop true
974
+ bone hip {
975
+ translate 0 0 0 curve 0.42 0 0.58 1
976
+ translate 1 0 -1.5
977
+ skew 0 0
978
+ }
979
+ slot torso {
980
+ offset 0 0 0 0 0
981
+ offset 1 0.2 0.1 0 0
982
+ }
983
+ deform cape {
984
+ key 0 0 0 0 0
985
+ key 1 0 -3 0 -6
986
+ }
987
+ order 1.5 cape torso
988
+ ik leg {
989
+ key 0 1 clockwise
990
+ key 1 0.5 clockwise
991
+ }
992
+ event 0 "inhale"
993
+ }
994
+ ```
995
+
996
+ `rect` is pixels from the image's top left; a mesh `vertex` is `u v` in
997
+ pixels then, per bone, `bone x y weight` in that bone's space (weights
998
+ summing to 1), or plain `x y` in the slot's bone. A bone's or region's
999
+ `skew` turns its y axis that many degrees past its x axis. A slot draws
1000
+ `texel × color + offset`. Bone keys add to the setup pose (`rotate` and
1001
+ `skew` degrees, `translate` pixels, `scale` multiplies); a `rotate` key
1002
+ turns the shorter way to the next, or `rotate 0 0 clockwise 1` (or
1003
+ `counterclockwise n`) that way through n more whole turns; every key eases
1004
+ linearly to the next, holds with `step`, or follows `curve x1 y1 x2 y2`
1005
+ (a cubic Bézier from 0,0 to 1,1, x time and y value; more segments as
1006
+ `x y x1 y1 x2 y2`). A `deform` key offsets every influence in order, one
1007
+ `x y` pair each; `order <time> <every slot>` redraws back to front from
1008
+ then (alone, the file's order); an `ik` key sets the constraint's weight
1009
+ and bend. An `ik`'s `bones` are a chain of any length, root first: one
1010
+ bone points, two bend at the joint, three or more (a tail, a tentacle, a
1011
+ three-joint leg) are solved by FABRIK and curl to the `bend` side. After the IK, a
1012
+ `transform t { bones a; target b; mix 1 1 1 1 }` pulls bones towards
1013
+ another bone's world rotation, origin, scale and shear by its mix, and an
1014
+ `along s { path rail; bones s1 s2; position 0 percent; spacing 0 length;
1015
+ rotate tangent 0; mix 1 1 }` lays bones along a `path` attachment (a
1016
+ cubic spline, `vertex x y` three per knot, drawn as nothing) while its
1017
+ slot shows it. A `skin armor { swap torso torso-armor; swap hat none }`
1018
+ block shows other attachments of the same slots wherever the default ones
1019
+ would show; `og.Skeleton2D.skin` picks it (`rig.skins.indexOf("armor")`,
1020
+ `default` is 0). A `box` draws nothing: `og.anim.skeleton2dBoxes(world, hero)`
1021
+ answers its polygon (in the Transform's frame) while its slot shows it,
1022
+ for hit and hurt boxes a clip switches. A `child` shows an `armature`
1023
+ block (a rig of its own on the same texture) in its slot; it plays the
1024
+ parent's clip by name when it has one, else its `clip`, at the parent's
1025
+ time; its events arrive as og.Skeleton2DEvent with `armature` its name
1026
+ (empty for the skeleton's own clip). A DragonBones 5.5 export is imported instead of written: a rig2d
1027
+ `.ogt` with `dragonbones "hero_ske.json"` and `atlas "hero_tex.json"`
1028
+ (skew, colour offsets, zOrder and IK timelines, armature displays,
1029
+ bounding boxes, path displays and constraints, every skin, curves and `clockwise`
1030
+ turns included; Spine's files are not read, convert
1031
+ them to this text).
1032
+
1033
+ ```ts
1034
+ const rig = must(og.load.rig2d(world, "rigs/hero.ogt"));
1035
+ const hero = must(world.spawn({ name: "hero", position: { x, y, z: 0 } }));
1036
+ must(world.insert(hero, og.Skeleton2D, { rig: rig.rig, clip: og.anim.clip(rig, "idle"), layer: 1 }));
1037
+ // A foot planted on the ground: one og.IkTarget2D per constraint, moved every tick.
1038
+ const foot = must(world.spawn({ name: "hero-foot" }));
1039
+ must(world.insert(foot, og.IkTarget2D, { skeleton: hero, constraint: rig.constraints.indexOf("leg"), target: { x, y: groundY } }));
1040
+ ```
1041
+
1042
+ `og.Skeleton2D` plays `clip` at `speed` (`time` is written each tick;
1043
+ setting it seeks), wraps when `loop`, mirrors with `flipX` and draws at
1044
+ sprite `layer`, lit by `og.Lighting2D` like a sprite there. Clip events
1045
+ arrive as `og.Skeleton2DEvent` (`entity`, `clip`, `name`); an
1046
+ `og.IkTarget2D` writes `reached` and `error` (its `weight` multiplies the
1047
+ constraint's own). The pose is a function of the component, the
1048
+ Transform and the targets, so a snapshot carries only the playback
1049
+ position and the boxes. `templates/game-2d`'s lamp keeper
1050
+ (`assets/rigs/keeper.ogt`, `spawnKeeper`, `Walk`, `Plant`) shows all of
1051
+ it. A walk is the walk clip played while a system moves the entity, with
1052
+ the feet's targets stepping in time: each planted for half the cycle
1053
+ (sliding back as fast as the body goes), then swung a stride ahead, so
1054
+ the feet stay on the ground the IK finds.
1055
+
1056
+ ### Cutscenes: timelines and paths
1057
+
1058
+ A cutscene, an intro, a scripted beat or a fly-through is a `type timeline`
1059
+ file, not code (`reference/timeline.md`, `reference/path.md`;
1060
+ `templates/game`'s `assets/timelines/intro.ogt` and `tests/intro.og.ts`
1061
+ are the pattern). Tracks of keys at seconds: between two keys the later
1062
+ key's `ease` (og.tween's names: `cubicInOut`, `quadIn`, ...) mixes them,
1063
+ outside them the value is held. Every value is evaluated at the
1064
+ timeline's time, never accumulated, so a seek lands exactly where playing
1065
+ would (a playtest or a video can start at any second).
1066
+
1067
+ ```text
1068
+ type timeline
1069
+ version 1
1070
+ ---
1071
+ name "Intro"
1072
+ duration 10
1073
+ path "flyby" {
1074
+ kind catmullRom # or bezier (p0 c0 c1 p1 ...) or linear; closed true wraps
1075
+ point -5 3.2 9
1076
+ point -1.5 2.4 7.5
1077
+ point 2 2 7
1078
+ point 5 2.8 8.5
1079
+ }
1080
+ track camera {
1081
+ rail "flyby" # or position/target keys, or follow "<entity>"
1082
+ key 0 {
1083
+ at 0
1084
+ target 0 1 0
1085
+ fov 50
1086
+ }
1087
+ key 9.5 {
1088
+ at 1
1089
+ target 1.5 1 0
1090
+ fov 42
1091
+ ease cubicInOut
1092
+ }
1093
+ }
1094
+ track transform "trickster" {
1095
+ key 2 {
1096
+ position 2 0 0
1097
+ }
1098
+ key 5 {
1099
+ position 0.5 0 1.5
1100
+ rotation 0 90 0
1101
+ ease cubicInOut
1102
+ }
1103
+ }
1104
+ track animation "trickster" { # the entity's og.Model's clips (or `model "<glb>"`)
1105
+ key 0 {
1106
+ clip "idle"
1107
+ loop true
1108
+ }
1109
+ key 2 {
1110
+ clip "walk"
1111
+ loop true
1112
+ blendIn 0.3
1113
+ }
1114
+ }
1115
+ track caption {
1116
+ captions "narration.srt" # beside the timeline; or word timestamps .json
1117
+ style lg # a UI type step
1118
+ anchor bottom
1119
+ }
1120
+ track audio {
1121
+ key 1 {
1122
+ sound "audio/blip.wav"
1123
+ volume 0.6
1124
+ bus sfx
1125
+ }
1126
+ }
1127
+ track music {
1128
+ key 0 {
1129
+ sound "audio/theme.ogg"
1130
+ fadeIn 1
1131
+ fadeOut 2
1132
+ end 9
1133
+ }
1134
+ }
1135
+ track event {
1136
+ key 5.5 {
1137
+ do "spawn_wave:2"
1138
+ retroactive true
1139
+ }
1140
+ }
1141
+ track transition {
1142
+ key 0 { # cut, fade, wipe, dissolve, iris, slide
1143
+ kind fade
1144
+ mode in
1145
+ duration 1
1146
+ }
1147
+ key 9 {
1148
+ kind fade
1149
+ mode out
1150
+ duration 1
1151
+ }
1152
+ }
1153
+ track marker {
1154
+ key 5.5 {
1155
+ name "drop"
1156
+ }
1157
+ }
1158
+ ```
1159
+
1160
+ The cook checks
1161
+ the sounds and models it names and imports the captions, so an `.srt`
1162
+ edit recooks the timeline. Play it from code or from the command channel:
1163
+
1164
+ ```ts
1165
+ const intro = must(og.load.timeline(world, "timelines/intro.ogt"));
1166
+ const e = world.spawn({ name: "intro" });
1167
+ world.insert(e, og.Timeline, { asset: intro });
1168
+ og.timeline.play(world, e); // pause, seek(world, e, 4), stop
1169
+ // a system reading og.TimelineMarker (name "drop") or og.TimelineEnded reacts to it
1170
+ ```
1171
+
1172
+ `og run . --commands "0:timeline:play:intro;4:screenshot;8:screenshot"`
1173
+ plays `timelines/intro.ogt` on an entity named `timeline-intro`;
1174
+ `timeline:seek:4`, `timeline:pause`, `timeline:resume`, `timeline:stop`
1175
+ take an optional `:<name>`. While a timeline with a camera track plays
1176
+ (or is paused) it owns `og.Camera`; it hands back the camera it found
1177
+ when it stops or ends. Event keys run their action at the start of the
1178
+ next tick; a seek skips the events it passes unless they are
1179
+ `retroactive`, and markers are sent only by playing past them. A capture
1180
+ at `t:` shows the world after the ticks before t: the play's first tick
1181
+ is the timeline's time 0, so the frame at `4:` shows 3.983 s at 60 Hz.
1182
+
1183
+ Paths on their own: `og.path.create(world, { points, kind, closed,
1184
+ tension })` or `og.path.find(world, "<name>")` for one a scene declares
1185
+ (the same `path` block in a `type scene`); `og.path.sample(world, p, t)`
1186
+ gives position, tangent and up at arc-length t 0..1, `length` and
1187
+ `nearest` the rest. `og.PathFollower { path, speed, loop, align, offset }`
1188
+ moves an entity along one each tick (`distance` is its own field, so a
1189
+ snapshot keeps it); `og.CameraRail { path }` rides one instead of its
1190
+ inline points.
1191
+
886
1192
  ## 10. Effects
887
1193
 
888
1194
  Every effect is off until the game turns it on, as data:
@@ -1094,8 +1400,10 @@ render sprite 12
1094
1400
  `underwaterDensity` and the surface from below shows the sky only
1095
1401
  straight up (on by default; set the density to 0 to see through).
1096
1402
 
1097
- **Terrain** (`reference/terrain.md`) is a height field cooked from a
1098
- `type terrain` .ogt beside a 16-bit grey PNG (or an EXR in metres):
1403
+ **Terrain** (`reference/terrain.md`, 3D projects only: a `2d` project's
1404
+ publish build has none, §2) is a height field cooked from a
1405
+ `type terrain` .ogt beside a 16-bit grey PNG (or an EXR in metres), or
1406
+ generated by the cook (below):
1099
1407
 
1100
1408
  ```text
1101
1409
  type terrain
@@ -1106,9 +1414,23 @@ height -20 120 # metres
1106
1414
  size 2048 2048 # metres along x and z, centred on the entity
1107
1415
  control "island-control.png" # optional RGBA: layer 0 in R, 1 in G, 2 in B, 3 in A
1108
1416
  heightBlend 0.15 # sharpen the blends by each albedo's alpha (its height)
1109
- layer { albedo "textures/grass.png" normal "textures/grass-n.png" tile 6 slope 0 30 }
1110
- layer { albedo "textures/rock.png" tile 12 triplanar true slope 28 90 }
1111
- layer { albedo "textures/snow.png" altitude 90 1000 altitudeFade 10 }
1417
+ layer { # a block's '}' stands alone on its line
1418
+ albedo "textures/grass.png"
1419
+ normal "textures/grass-n.png"
1420
+ tile 6
1421
+ slope 0 30
1422
+ }
1423
+ layer {
1424
+ albedo "textures/rock.png"
1425
+ tile 12
1426
+ triplanar true
1427
+ slope 28 90
1428
+ }
1429
+ layer {
1430
+ albedo "textures/snow.png"
1431
+ altitude 90 1000
1432
+ altitudeFade 10
1433
+ }
1112
1434
  ```
1113
1435
 
1114
1436
  A layer shows where its control channel (1 without a map) times its slope
@@ -1123,6 +1445,151 @@ world.insert(ground, og.Terrain, { data: info.data });
1123
1445
  world.insert(ground, og.Collider, { shape: SHAPE_HEIGHTFIELD, mesh: info.geometry });
1124
1446
  ```
1125
1447
 
1448
+ **A generated terrain.** Instead of `heightmap`, a `generate` block makes
1449
+ the field in the cook (never both: the cook refuses the pair and names
1450
+ the lines). `height` and `size` mean what they mean for a heightmap: the
1451
+ field is stretched so its lowest sample is `height`'s low and its highest
1452
+ its high, and no sample leaves that range. Every line, with its default
1453
+ (leave out what you do not need; erosion, thermal, terraces, rivers and
1454
+ the sea are off until given):
1455
+
1456
+ ```text
1457
+ type terrain
1458
+ version 1
1459
+ ---
1460
+ height -40 160 # metres
1461
+ size 1024 1024 # metres along x and z
1462
+ generate {
1463
+ samples 257 257 # columns and rows (2 to 4097)
1464
+ seed 1 # another seed, another terrain of the same kind
1465
+ noise {
1466
+ octaves 6 # 1 to 12: more is finer detail
1467
+ frequency 3 # features across the longer side
1468
+ lacunarity 2 # each octave's frequency over the last's (1 to 8)
1469
+ gain 0.5 # each octave's amplitude over the last's (0 to 1): higher is rougher
1470
+ warp 0 # domain warp in features: 0.3 to 1 bends ridges and coasts
1471
+ }
1472
+ mask island 0.4 # land fades to `height`'s low over this share of the half size
1473
+ # or `mask ridge 1`: sharp ridged mountains (0 to 1); default no mask
1474
+ erosion { # hydraulic erosion: seeded drops carve gullies and fill valleys
1475
+ drops 40000 # ~0.5 to 1 per sample; more is deeper erosion and a slower cook
1476
+ lifetime 30
1477
+ radius 3 # samples each drop erodes around it
1478
+ inertia 0.05
1479
+ capacity 4
1480
+ deposition 0.3
1481
+ erosion 0.3
1482
+ evaporation 0.01
1483
+ gravity 4
1484
+ }
1485
+ thermal { # slopes steeper than talus slump into scree
1486
+ talus 35 # degrees
1487
+ iterations 20
1488
+ rate 0.5
1489
+ }
1490
+ terraces {
1491
+ step 10 # metres between steps
1492
+ blend 0.4 # 0 hard steps, 1 smooth ramps
1493
+ }
1494
+ sea 0 # metres above the entity: og.terrain.load's seaLevel
1495
+ shore 2 # metres either side of the sea flattened into beaches (needs sea)
1496
+ rivers { # carved where the flow gathers, down to the sea or the edge
1497
+ threshold 400 # samples draining through before it is a river: lower, more rivers
1498
+ depth 2 # metres
1499
+ width 6 # metres
1500
+ }
1501
+ moisture { # what biomes read, 0 to 1
1502
+ flow 1 # weight of the gathered flow (wet in valleys and by rivers)
1503
+ scale 64 # samples draining through for half of it
1504
+ noise 0.5 # weight of a noise
1505
+ frequency 6
1506
+ }
1507
+ biome { # any number; each writes its layer's control channel
1508
+ layer 0 # required: the layer (0 to 3, one the terrain has)
1509
+ weight 1
1510
+ altitude -1000000 1000000 # metres above the entity
1511
+ altitudeFade 2
1512
+ slope 0 90 # degrees
1513
+ slopeFade 4
1514
+ moisture 0 1
1515
+ moistureFade 0.1
1516
+ }
1517
+ }
1518
+ layer {
1519
+ albedo "textures/sand.png"
1520
+ tile 4
1521
+ }
1522
+ layer {
1523
+ albedo "textures/grass.png"
1524
+ tile 6
1525
+ }
1526
+ layer {
1527
+ albedo "textures/rock.png"
1528
+ tile 12
1529
+ triplanar true
1530
+ }
1531
+ ```
1532
+
1533
+ An island with beaches, rivers and rock on its cliffs, for that file's
1534
+ three layers (a block's `{` ends its line):
1535
+
1536
+ ```text
1537
+ generate {
1538
+ seed 7
1539
+ noise {
1540
+ warp 0.5
1541
+ }
1542
+ mask island 0.45
1543
+ erosion {
1544
+ drops 40000
1545
+ }
1546
+ sea 6
1547
+ shore 3
1548
+ rivers {
1549
+ threshold 300
1550
+ depth 2.5
1551
+ width 8
1552
+ }
1553
+ biome {
1554
+ layer 0
1555
+ altitude -1000 9
1556
+ }
1557
+ biome {
1558
+ layer 1
1559
+ altitude 9 1000
1560
+ slope 0 32
1561
+ }
1562
+ biome {
1563
+ layer 2
1564
+ slope 32 90
1565
+ }
1566
+ }
1567
+ ```
1568
+
1569
+ With `biome` rules the generator writes the control map (a terrain with
1570
+ biomes names no `control`); a channel is the most any of its layer's
1571
+ biomes gives a sample, its `weight` times its three bands. Without biomes
1572
+ there is no control map and the layers' own `slope` and `altitude`
1573
+ place them, as for a heightmap. The same file makes the same terrain on
1574
+ every machine (the generator uses only exact arithmetic and its own
1575
+ random numbers). It is a credible terrain of a kind, never a terrain from
1576
+ a picture: move the seed, the mask and the erosion until it reads right.
1577
+ `og cook --report` gives its height histogram, the share of land above
1578
+ the sea and the rivers' length, and writes its height and control maps
1579
+ as PNGs to `og-out/terrain-maps/` to look at.
1580
+
1581
+ Its sea is the water's level; the water is the terrain's size:
1582
+
1583
+ ```ts
1584
+ if (info.seaLevel !== null) {
1585
+ const sea = must(world.spawn({ name: "sea", position: { x: 0, y: info.seaLevel, z: 0 } }));
1586
+ world.insert(sea, og.Water, { size: info.size, segments: 128 });
1587
+ }
1588
+ ```
1589
+
1590
+ (on the terrain's entity's x and z, under its parent if it has one; an
1591
+ `og.WaterSurface` on the same entity makes the waves move and bodies float).
1592
+
1126
1593
  It is drawn around the camera at finer detail nearby (casting and
1127
1594
  receiving shadows, under the fog), placed by its entity's world transform
1128
1595
  (turn or scale the entity and the terrain turns and scales with it), and
@@ -1353,7 +1820,7 @@ cloth's points (about 1 ms per 1,000) are the ones to count (§13).
1353
1820
  A **look** is an `.ogt` of `type look` under `assets/looks/` that sets the
1354
1821
  whole treatment of the frame at once: the resources `RenderSettings`,
1355
1822
  `PostEffects`, `Stylise`, `MaterialStyle`, `Fog`, `Sky` and `Environment`
1356
- (one block each, fields by name), the UI `skin` and the `antialias`.
1823
+ (one block each, fields by name), the UI style (`ui`) and the `antialias`.
1357
1824
  Choosing one for a premise is og-taste §1 and §12; this is the mechanism.
1358
1825
 
1359
1826
  ```text
@@ -1362,7 +1829,7 @@ version 1
1362
1829
  ---
1363
1830
  name "Harbour"
1364
1831
  extends "anime-cel-3d" # a built-in preset (templates/looks/<slug>.ogt) or another look's id
1365
- skin "skins/harbour/skin.ogt"
1832
+ ui "styles/harbour/style.ogt" # the game's own UI style, or a built-in by slug (ui "fantasy")
1366
1833
  Stylise {
1367
1834
  ink true
1368
1835
  inkWidth 1.5
@@ -1384,14 +1851,14 @@ og.render.look(world)?.name; // the
1384
1851
  - Applying a look replaces each resource it names **whole** (the fields it
1385
1852
  sets, the rest their defaults; `PostEffects.flash` and `shakeTrauma`
1386
1853
  keep theirs) and leaves the others alone, then configures the UI with
1387
- its skin. Write per-game tweaks in `start` after it, or in the look. A
1854
+ its style. Write per-game tweaks in `start` after it, or in the look. A
1388
1855
  system that applies one declares those resources among its writes.
1389
1856
  - `extends` flattens at cook time, field by field (a field written wins);
1390
1857
  `assets/looks/<slug>.ogt` of the game's own replaces the preset of that
1391
1858
  slug for every look that extends it. The cook refuses an unknown field,
1392
1859
  a value out of range and an asset not in the bundle, naming the line.
1393
1860
  - `og.load.lookInfo` (and `og.render.look`) answer `LookInfo`: `name`,
1394
- `extends`, `resources`, `skin`, `antialias`. A look's `antialias` (tier,
1861
+ `extends`, `resources`, `style`, `antialias`. A look's `antialias` (tier,
1395
1862
  off, msaa4 or taa) is the renderer's when `GameDesc.render` names none.
1396
1863
  - **`og.Stylise`** moves the frame away from the photographic, every
1397
1864
  primitive off at zero: the pixel grid (`pixelHeight`, `pixelUpscale`,
@@ -1472,6 +1939,35 @@ og.render.look(world)?.name; // the
1472
1939
  stays sharper under camera motion and `taaSharpen` (0–1) restores
1473
1940
  detail TAA softens. `RenderCapabilities.antialias` says what the device
1474
1941
  resolved it to.
1942
+ - **Graphics quality** is RenderSettings' quality fields, read every
1943
+ frame: `rectShadows`, `shadowedLights`, `shadowMapScale`,
1944
+ `volumetricFog`, `softParticles`, `screenSpaceReflections`,
1945
+ `ambientOcclusion`, `msaaSamples` (4, or 1 for FXAA where the
1946
+ antialiasing multisamples). `og.render.applyQuality(world, preset)`
1947
+ writes a preset's row of them and records it in
1948
+ `RenderSettings.quality` (`none` before any):
1949
+
1950
+ | Field | low | medium | high |
1951
+ |---|---|---|---|
1952
+ | rectShadows | false | true | true |
1953
+ | shadowedLights | 2 | 8 | 8 |
1954
+ | shadowMapScale | 0.5 | 1 | 2 |
1955
+ | volumetricFog, softParticles, screenSpaceReflections, ambientOcclusion | false | true | true |
1956
+ | msaaSamples | 1 | 4 | 4 |
1957
+
1958
+ Medium is the defaults, the frame drawn before any preset. The renderer
1959
+ reads the fields and nothing else, so `og run --commands
1960
+ "0.5:get:og.RenderSettings"` shows what is drawn, and a field set after
1961
+ the preset wins. The switches only take away: the scene's PostEffects
1962
+ and lights keep their settings. The settings screen applies the
1963
+ player's pick when given the world (`og.ui.settingsScreen(id, { world,
1964
+ settings, ... })`); apply a saved preset yourself at start:
1965
+
1966
+ ```ts
1967
+ start(world) { og.render.applyQuality(world, save.settings.quality ?? "medium"); }
1968
+ ```
1969
+
1970
+ Inside a system, declare `og.RenderSettings` among its writes.
1475
1971
 
1476
1972
  ## 17. Saving progress
1477
1973
 
@@ -1576,8 +2072,827 @@ Test either across a relaunch with a playtest's `store: "keep"` and
1576
2072
  tape of inputs) and a ghost (a tape of poses) are og-core §2's
1577
2073
  "Recording, replaying and ghosts".
1578
2074
 
1579
- ## 18. Where to look things up
2075
+ ## 18. Geometry made at run time, lines, points and debug draw
2076
+
2077
+ Meshes need not come from a file. `og.render.bufferGeometry(world, desc)`
2078
+ takes three.js BufferGeometry's attributes: `positions` (a Float32Array of
2079
+ x y z), and optionally `normals`, `uvs` (v down the image), `colors`
2080
+ (r g b or r g b a, multiplied into the material), `tangents` and `indices`
2081
+ (a Uint32Array, counter-clockwise from the front). Missing normals are
2082
+ computed as `computeVertexNormals` does. The geometry draws as any other
2083
+ through `og.Mesh`, and it is a `Collider.mesh` at once (`physics` is true by
2084
+ default), so a procedural level collides with what is drawn:
2085
+
2086
+ ```ts
2087
+ const terrain = must(og.render.bufferGeometry(world, { positions, colors, indices, dynamic: true }));
2088
+ world.insert(e, og.Mesh, { geometry: terrain, material });
2089
+ world.insert(e, og.RigidBody, { kind: BODY_STATIC });
2090
+ world.insert(e, og.Collider, { shape: 5 /* SHAPE_MESH */, mesh: terrain });
2091
+ // Each tick (water, a deforming surface): every attribute again.
2092
+ must(og.render.updateGeometry(world, terrain, { positions, colors, indices }));
2093
+ ```
2094
+
2095
+ Only a geometry made with `dynamic: true` can be updated. The same vertex
2096
+ and index counts rewrite it in place, and other counts reallocate it. Its
2097
+ physics mesh is re-registered, and its colliders are rebuilt at the next
2098
+ step. `og.render.geometryInfo` answers the counts and bounds of any
2099
+ geometry.
2100
+
2101
+ Beside `boxGeometry`, `planeGeometry` and `sphereGeometry`, three.js's
2102
+ primitives keep their parameters and defaults: `cylinderGeometry`,
2103
+ `coneGeometry`, `capsuleGeometry` (an og.Collider capsule of the same
2104
+ radius and `height` matches it), `torusGeometry`, `ringGeometry`,
2105
+ `circleGeometry` and `tubeGeometry`. A tube follows a centripetal
2106
+ Catmull-Rom curve through `points`, for pipes, rails and cables; give it
2107
+ `path` instead (an og.path from `og.path.create`, or a scene's `path`
2108
+ block found by `og.path.find`) and it follows that path's own curve,
2109
+ Bezier or linear included, closed when the path is. Its rings are evenly
2110
+ spaced along the length, as three.js spaces them. Rings and circles are one-sided, so use
2111
+ `side: "double"` to see them from behind. `wireframe: true` on a
2112
+ standard material draws the triangle edges instead of the faces, one pixel
2113
+ wide on every tier.
2114
+
2115
+ **Shapes, extrusions, lathes and solids.** Three.js's modelling classes
2116
+ keep their names, parameters and defaults, so a three.js extrusion is a
2117
+ paste. `og.Shape` and `og.Path2` are three.js's Shape and Path:
2118
+ `moveTo`, `lineTo`, `quadraticCurveTo`, `bezierCurveTo`, `splineThru`,
2119
+ `arc`, `absarc`, `ellipse`, `absellipse`, `closePath`, `holes` and
2120
+ `getPoints(divisions)`. They flatten to points before they cross to the
2121
+ engine, so a points array works wherever a shape does
2122
+ (`reference/shapes.md`).
2123
+ - `shapeGeometry` is a flat shape with holes, one-sided like a circle.
2124
+ - `extrudeGeometry` pushes shapes along +Z (`depth`, `steps`) or along an
2125
+ og.path (`extrudePath`). `bevelEnabled`, `bevelThickness`, `bevelSize`,
2126
+ `bevelOffset` and `bevelSegments` round its edges, as in three.js.
2127
+ - `latheGeometry` turns a profile of `points` round +Y (`segments`,
2128
+ `phiStart`, `phiLength`), for vases, bottles and columns.
2129
+ - `polyhedronGeometry` takes `vertices`, `indices`, `radius` and `detail`.
2130
+ `icosahedronGeometry`, `dodecahedronGeometry`, `octahedronGeometry` and
2131
+ `tetrahedronGeometry` take `radius` and `detail`.
2132
+ - `sweepGeometry` sweeps a 2D `section` along an og.path. Its `scale` and
2133
+ `twist` are keyed over the path's length from 0 to 1, for horns, tusks,
2134
+ handles and twisted columns.
2135
+ - `mergeGeometries` places `parts` (each a geometry with `position`,
2136
+ `quaternion`, `scale` and a `mirror` axis) into one draw. Write an
2137
+ array as the same part at several placements.
2138
+ - `csgGeometry` is `union`, `subtract` or `intersect` of two placed
2139
+ closed geometries. Chain calls for more operands.
2140
+
2141
+ Every result is a geometry like `bufferGeometry`'s, with a BVH, `physics`
2142
+ and `dynamic`, and answered by `geometryInfo`:
2143
+
2144
+ ```ts
2145
+ const gear = new og.Shape().absarc(0, 0, 1, 0, Math.PI * 2, false);
2146
+ gear.holes.push(new og.Path2().absarc(0, 0, 0.3, 0, Math.PI * 2, true));
2147
+ const disc = must(og.render.extrudeGeometry(world, {
2148
+ shapes: gear, depth: 0.2, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05,
2149
+ }));
2150
+ const vase = must(og.render.latheGeometry(world, {
2151
+ points: [{ x: 0, y: 0 }, { x: 0.5, y: 0.1 }, { x: 0.3, y: 1 }, { x: 0.4, y: 1.4 }],
2152
+ segments: 32,
2153
+ }));
2154
+ const cube = must(og.render.boxGeometry(world, { width: 1, height: 1, depth: 1 }));
2155
+ const ball = must(og.render.sphereGeometry(world, { radius: 0.15 }));
2156
+ const dice = must(og.render.csgGeometry(world, {
2157
+ op: "subtract", a: { geometry: cube }, b: { geometry: ball, position: { x: 0, y: 0, z: 0.5 } },
2158
+ }));
2159
+ ```
2160
+
2161
+ The generators and the CSG decide every cut with exact predicates and use
2162
+ no platform trigonometry, so every host makes the same triangles. A CSG
2163
+ operand must be closed (every edge shared by two triangles). Its cost
2164
+ grows with the product of the two triangle counts where their boxes
2165
+ overlap, so cut low-poly operands at load, not every tick.
2166
+
2167
+ Every generator above takes `uvRepeat` as `boxGeometry` does (its uvs
2168
+ scaled, so a texture tiles). `bevelSize` left out (or null) is
2169
+ `bevelThickness - 0.1`, as in three.js: 0.1 at the default thickness.
2170
+
2171
+ **Models written as text: `type model`.** A prop made of these parts
2172
+ belongs in an asset, not in `start`: an `.ogt` of `type model` under
2173
+ `assets/` names each part's geometry by its `og.render` call, with the
2174
+ desc's field names and defaults, places it as `mergeGeometries` places a
2175
+ part, gives it a material and hangs it from a named pivot. `og cook`
2176
+ builds it into the same model blob a glTF cooks to: meshopt, levels of
2177
+ detail with their measured errors, the raycast BVH, the nodes. Load it
2178
+ with `og.load.gltf(world, "models/clockwork.ogt")` and `world.instantiate`,
2179
+ or name it in a scene (`model "models/clockwork.ogt"`), as any model.
2180
+
2181
+ ```
2182
+ type model
2183
+ version 1
2184
+ ---
2185
+ material "brass" {
2186
+ color 0.78 0.52 0.16 # linear, as glTF's baseColorFactor
2187
+ metalness 1
2188
+ roughness 0.32
2189
+ }
2190
+ material "glaze" {
2191
+ color 0.12 0.34 0.72
2192
+ roughness 0.3
2193
+ }
2194
+ pivot "axle" { # a node: code and animation find it by name
2195
+ position 0 1.25 0
2196
+ }
2197
+ part "gear" {
2198
+ pivot "axle" # hangs from the pivot (default: the root)
2199
+ material "brass"
2200
+ position 0 0 -0.1
2201
+ extrudeGeometry {
2202
+ shape { # og.Shape's calls, one a line
2203
+ moveTo 1 0
2204
+ absarc 0 0 1 0 6.283185307179586 false
2205
+ hole {
2206
+ absarc 0 0 0.28 0 6.283185307179586 true
2207
+ }
2208
+ }
2209
+ depth 0.16
2210
+ bevelThickness 0.04
2211
+ bevelSize 0.03
2212
+ }
2213
+ }
2214
+ part "bolt" { # one mesh under three nodes: drawn instanced
2215
+ pivot "axle"
2216
+ material "brass"
2217
+ cylinderGeometry {
2218
+ radiusTop 0.06
2219
+ radiusBottom 0.06
2220
+ height 0.36
2221
+ }
2222
+ at {
2223
+ position 0.55 0 0
2224
+ quaternion 0.7071068 0 0 0.7071068
2225
+ }
2226
+ at {
2227
+ position -0.275 0.476 0
2228
+ quaternion 0.7071068 0 0 0.7071068
2229
+ }
2230
+ at {
2231
+ position -0.275 -0.476 0
2232
+ quaternion 0.7071068 0 0 0.7071068
2233
+ }
2234
+ }
2235
+ part "vase" {
2236
+ material "glaze"
2237
+ position -2.2 0 0
2238
+ latheGeometry {
2239
+ points 0 0 0.32 0.04 0.42 0.4 0.24 0.95 0.3 1.25
2240
+ segments 40
2241
+ }
2242
+ }
2243
+ part "horn" {
2244
+ position 2.2 0 0
2245
+ sweepGeometry {
2246
+ section {
2247
+ absarc 0 0 0.22 0 6.283185307179586 false
2248
+ }
2249
+ path { # og.path's lines: kind, closed, tension, point
2250
+ kind catmullRom
2251
+ point 0 0 0
2252
+ point 0.15 0.6 0
2253
+ point -0.45 1.35 0.15
2254
+ }
2255
+ scale 0 1 # t value, repeatable (twist the same)
2256
+ scale 1 0.05
2257
+ }
2258
+ }
2259
+ part "dice" {
2260
+ position 1.1 0.3 0.55
2261
+ csgGeometry {
2262
+ op subtract
2263
+ a { # an operand: a geometry block, placed
2264
+ boxGeometry {
2265
+ width 0.6
2266
+ height 0.6
2267
+ depth 0.6
2268
+ }
2269
+ }
2270
+ b {
2271
+ position 0 0 0.3
2272
+ icosahedronGeometry {
2273
+ radius 0.08
2274
+ detail 1
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+ ```
2280
+
2281
+ - Geometries: `boxGeometry`, `planeGeometry`, `sphereGeometry`,
2282
+ `cylinderGeometry`, `coneGeometry`, `capsuleGeometry`, `torusGeometry`,
2283
+ `shapeGeometry`, `extrudeGeometry` (`extrudePath { }` a path block),
2284
+ `latheGeometry` (`points x y x y ...`), `polyhedronGeometry`, the four
2285
+ solids, `sweepGeometry`, `csgGeometry` (`a { }`, `b { }`) and
2286
+ `mergeGeometries` (`part { }` blocks), each with `uvRepeat u v`. A
2287
+ part holds exactly one; combine more with `csgGeometry` or
2288
+ `mergeGeometries`.
2289
+ - Shapes: `moveTo`, `lineTo`, `quadraticCurveTo`, `bezierCurveTo`,
2290
+ `splineThru x y ...`, `arc`, `absarc`, `ellipse`, `absellipse` (angles in
2291
+ radians, then `true` for clockwise), `closePath`, `points x y ...`
2292
+ (setFromPoints) and `hole { }`. They flatten to the points og.Shape
2293
+ gives in TypeScript, bit for bit.
2294
+ - Placement: `position`, `quaternion x y z w`, `scale`, `mirror x|y|z`,
2295
+ on the part or in `at { }` blocks, one node each. The same part at
2296
+ several placements is one mesh drawn instanced; a mirrored placement is
2297
+ baked into a mesh of its own.
2298
+ - Materials: `color`, `opacity`, `metalness` (default 0), `roughness`
2299
+ (default 1), `emissive`, `emissiveIntensity`, `doubleSided`,
2300
+ `alphaTest`. A part without one draws glTF's grey default.
2301
+ - `og cook --report` lists the model's `triangles`, `parts`, `draws` (the
2302
+ distinct meshes; instanced placements are one) and `bvhBytes`/`bvhShare`.
2303
+ Past `budgets.triangles` or `budgets.vertices` in project.ogt the cook
2304
+ refuses it, so lower `segments`, `curveSegments`, `steps` or `detail`.
2305
+ Import settings (`models/clockwork.ogt.ogt`, `model { lods ... raycast
2306
+ false }`) apply as to a glTF.
2307
+ - `og dev` re-cooks it on an edit and the running game draws the edit
2308
+ under the same asset handle.
2309
+
2310
+ **Lines and points.** `og.render.lines(world, desc)` and
2311
+ `og.render.points(world, desc)` are immediate mode, like
2312
+ `og.render.sprite`: submit them every tick they should show.
2313
+ - Lines: `mode` is `strip`, `segments` or `loop`. `width` is in CSS
2314
+ pixels, or in world units with `worldUnits`. They also take `colors` per
2315
+ point, `depthTest` (false draws over the scene), `dashed` with `dashSize`
2316
+ and `gapSize`, and `screen: true`. Screen lines use CSS pixels from the
2317
+ top left of the play surface and draw over the finished frame, under
2318
+ the UI.
2319
+ - Points: `sizeAttenuation` makes `size` a size in world units; without it
2320
+ `size` is a fixed size in CSS pixels.
2321
+ - A static set is data: the `og.Lines` and `og.Points` components draw a
2322
+ geometry's vertices in order at the entity's transform. Make the
2323
+ geometry with `bufferGeometry({ positions, colors, physics: false })`,
2324
+ unindexed, any count.
2325
+
2326
+ **What is under the cursor.** `og.render.raycast(world, ray, desc)` tests
2327
+ what is drawn (every og.Mesh and og.Model at its world transform), not the
2328
+ colliders. It goes through a BVH per geometry and answers every hit,
2329
+ nearest first; `first: true` answers only the nearest. Each hit has
2330
+ `entity`, `geometry`, `distance`, `point`, `normal` (the face's, in world
2331
+ space), `uv` and `faceIndex`. Inside a system it reads the meshes as the
2332
+ tick started, as `og.physics.raycast` does. Use it for picking, decals on
2333
+ detailed meshes, and line of sight against geometry without colliders.
2334
+
2335
+ A model's BVH is cooked: `og cook` builds each primitive's into the
2336
+ model's `.ogb` (about 6 to 9 bytes per triangle compressed, listed as
2337
+ `bvhBytes` in `og cook --report`), and it loads with the mesh, so the
2338
+ first raycast never stalls. A model no ray is cast at can leave it out
2339
+ with import settings; its first raycast then builds it:
2340
+
2341
+ ```
2342
+ type import
2343
+ version 1
2344
+ ---
2345
+ model {
2346
+ raycast false
2347
+ }
2348
+ ```
2349
+
2350
+ Leave it on for anything a player can click, aim at or stand a decal on.
2351
+ Turn it off for a large model that is only scenery (templates/game's
2352
+ 16,512-triangle dropship gains 108 KB, 64 % of its blob). Geometries made
2353
+ at run time (`bufferGeometry`, the primitives) build theirs when made or
2354
+ updated.
2355
+
2356
+ **Debug draw.** `og.debug.line`, `box`, `sphere`, `arrow`, `axes` and
2357
+ `text` (a label above a world point, in the UI's body font; every field is
2358
+ in `reference/debug.md`) each take
2359
+ `{ color, width, duration, rotation }`. `duration` is in ticks; 0 shows the
2360
+ shape this frame only, so draw it every tick. Debug draws show over the
2361
+ finished frame and never hide behind geometry, so they read in every
2362
+ capture. The command channel switches the views:
2363
+ - `debug:physics` shows the colliders, joints and contacts.
2364
+ - `debug:nav` shows the grids' blocked cells, the navmesh polygons and
2365
+ every agent's path.
2366
+ - `debug:bounds` shows every drawn mesh's box.
2367
+ - `debug:none` shows only the draws; `debug:off` and `debug:on` hide and
2368
+ show everything.
2369
+
2370
+ `og.debug.setView` does the same from code, and `RenderOptions.debugView`
2371
+ picks a view from the first frame. A publish build drops all of it, so
2372
+ debug draws may stay in the code. Debug draws are not world state:
2373
+ snapshots and replays do not carry them.
2374
+
2375
+ ## 19. Video: footage in, videos out
2376
+
2377
+ **Footage in.** An `.ogt` of `type video` names an `.mp4`, `.webm` or
2378
+ `.mov`; `og cook` turns it with ffmpeg (`og doctor` says whether ffmpeg
2379
+ and ffprobe are there) into a texture array of its frames and its sound:
2380
+
2381
+ ```text
2382
+ type video
2383
+ version 1
2384
+ ---
2385
+ source "clips/intro.mp4" # beside this file, or under assets/
2386
+ fps 30 # frames kept per second (default 30)
2387
+ maxSeconds 8 # what is kept from the start (default 8)
2388
+ size 320 180 # each frame's size (default the source's, at most 1024 wide)
2389
+ kind color # or pixel: nearest, lossless
2390
+ audio true # its sound track (default true)
2391
+ ```
2392
+
2393
+ At most 256 frames (fps × seconds): the cook refuses more, naming the
2394
+ `maxSeconds` that fits, and warns past 64 MiB of cooked frames. Keep
2395
+ footage small: a 320×180 clip of 8 s at 30 fps is about 2 MiB cooked.
2396
+ Load it in `start`, then play it with `og.VideoPlayer`; its frame is a
2397
+ plain `Texture`, so a sprite, a material map or a UI image shows it:
2398
+
2399
+ ```ts
2400
+ const info = must(og.load.video(world, "clips/intro.ogt"));
2401
+ const screen = must(world.spawn({ name: "screen", position: { x: 0, y: 0, z: 0 } }));
2402
+ must(world.insert(screen, og.Sprite, { texture: info.texture, width: 16, height: 9 }));
2403
+ must(world.insert(screen, og.VideoPlayer, { video: info.video, loop: true }));
2404
+ must(world.insert(screen, og.AudioSource, {})); // its sound, kept in step
2405
+ ```
2406
+
2407
+ The `og.video` system plays it by the tick (not the time scale, so the
2408
+ picture stays with its sound): with a 30 Hz tick, tick 90 shows the
2409
+ frame 3 s in, the same in every run and after a restore. Write
2410
+ `VideoPlayer.time` to seek (the sound starts again there on the next
2411
+ tick), `playing` to pause, `speed` for slow motion (the pitch follows).
2412
+ `frame` and `finished` are written for you, and so is `texture`: each
2413
+ player of a video has a texture of its own (the first takes
2414
+ `info.texture`, each further one a texture made for it over the same
2415
+ frames), so two players at different times show different frames. With
2416
+ more than one player, draw each player's `VideoPlayer.texture`. Every field and the loaders are in `reference/video.md`.
2417
+
2418
+ **Videos out.** `og render` plays a command script headless and makes a
2419
+ video of it: a frame every `tick / fps` ticks (the fps divides the tick
2420
+ rate; the error names the nearest that do), read back without the
2421
+ probe's round trip.
2422
+
2423
+ ```sh
2424
+ og render . --commands "0.5:type:s;2:key_down:Space" --fps 30 --to 8 \
2425
+ --size 1280x720 --out og-out/intro.mp4 --audio --contact-sheet 6 --jobs 4
2426
+ ```
2427
+
2428
+ It writes `intro.mp4` (H.264; `.webm` is VP9) with the run's audio,
2429
+ `intro.frames.json` (each frame's tick, time and SHA-256, and the
2430
+ audio's), and `intro.contact-sheet.png`: a grid of frames over the range,
2431
+ each labelled with its time and tick. **Review the contact sheet first**:
2432
+ one image shows whether the scene moves as the script meant, without
2433
+ playing anything. `--out <dir>` (or `--frames png|jpg`) writes the
2434
+ frames too; without ffmpeg the frames are what you get. Two runs of one
2435
+ script give the same SHA-256s, so a frame that changed is a change you
2436
+ made. `--jobs N` splits the range over processes (each starts from a
2437
+ snapshot of the world) and gives the same frames, faster.
2438
+ `--timeline <name>` renders a timeline (`type timeline`) over its
2439
+ duration once timelines exist (stream S2 of docs/depth.md); until then
2440
+ use `--commands`. On a CPU-only machine (lavapipe) a 1280×720 frame of the
2441
+ 3D template takes about 0.4 s in one job, 2.6× less with `--jobs 4`:
2442
+ render at 640×360 (about 0.1 s) to iterate, full size once.
2443
+
2444
+ ## 20. Two players over a room: lockstep
2445
+
2446
+ To play together, every peer runs the whole world and only inputs cross
2447
+ the room (`reference/net.md`; the rooms themselves are og.platform's, taught by
2448
+ the og-platform-sdk skill). Follow three rules:
2449
+
2450
+ 1. In `start`, set `og.Lockstep`: `enabled = true`, `players`,
2451
+ `inputDelay` (2 on a LAN, 4 to 6 over the internet) and `hashEvery`
2452
+ (10). Spawn one entity per player with `og.PlayerInput { player: seat }`.
2453
+ 2. Read `og.Input`, `og.Gamepad` and the UI's touch controls in **one**
2454
+ system, and hand them over with
2455
+ `og.net.submit(world, { x, y, buttons, aimX, aimY })` every tick.
2456
+ 3. Every system that changes the world reads `og.PlayerInput` (its `x`,
2457
+ `y` and `buttons` bits), the world and `og.Rng`, and nothing else: no
2458
+ `Math.random()`, no `Date.now()`, no module variables, no
2459
+ `og.RoomMessage`. Draw per-peer things (whose turn it is, "waiting")
2460
+ with og.ui only, and keep them out of the world.
2461
+
2462
+ ```ts
2463
+ const Submit = defineSystem("Submit", { reads: [og.Input] }, (_q, _t, world) => {
2464
+ const input = must(world.resource(og.Input));
2465
+ must(og.net.submit(world, { x: input.axis("KeyA", "KeyD"), y: input.axis("KeyS", "KeyW"), buttons: input.down("Space") ? 1 : 0 }));
2466
+ });
2467
+ const Move = defineSystem("Move", { reads: [og.PlayerInput], writes: [og.Transform] }, (q, time) => {
2468
+ for (const a of q.archetypes()) {
2469
+ const pin = a.column(og.PlayerInput);
2470
+ const p = a.column(og.Transform).position;
2471
+ for (let i = 0; i < a.count; i++) p[i * 3] += pin.x[i]! * 6 * time.dt;
2472
+ }
2473
+ });
2474
+ ```
2475
+
2476
+ Alone, or with lockstep off, `submit` drives the local player on the next
2477
+ tick, so the same code plays single-player. Natively, run `og relay` and
2478
+ then `og run . --room ws://127.0.0.1:47900/<room>` in two terminals; on
2479
+ the web the shell carries the platform's rooms, and a Play link with
2480
+ `?room=<id>` puts the page in that room at load (the game hears
2481
+ og.RoomJoined as for its own roomJoin; `og.platform.roomInfo(world).link`
2482
+ names the link's room), so a game that plays over a room need not ask for
2483
+ one when the link named it. A playtest plays itself in
2484
+ a room with `room: { players: 2, commands: [...] }`: its runs are `run`
2485
+ and `run.peers`, and each has `netHash` and `lockstep`. Equal `netHash`
2486
+ values at the same tick mean the peers stayed in step. Once lockstep runs
2487
+ a script's steps are on the room's shared ticks (a peer that took the
2488
+ authority's snapshot keeps its steps on theirs), so end each script with
2489
+ `quit` (or give `room.ticks`) and every place stops on one tick; a `then`
2490
+ step relaunches every place at once into a room of its own, its runs in
2491
+ `run.then[n]` and `run.then[n].peers`.
2492
+
2493
+ When the worlds part, `og.Desync { tick, peer }` fires once and
2494
+ `og.net.status(world).desync` holds the tick. Then
2495
+ `--commands "...:dump_desync:d.json"` names the first divergent tick and
2496
+ the entity, component and field that differ, with both values: fix the
2497
+ system that writes that field. The archetype `templates/archetypes/arena`
2498
+ (Coin Duel) is the worked example.
2499
+
2500
+ ## 21. Checking a run: describe, expect, trace, write-back, budgets
2501
+
2502
+ These are the checks to use before opening frames. All of them run from
2503
+ `og run --commands`, a playtest's steps or the live probe (`og probe`).
2504
+
2505
+ - **`describe[:path]`** (probe `describe`) writes the current frame as
2506
+ JSON. `entities` holds every visible entity, nearest first (`order`):
2507
+ `id` (the probe's id), `name`, `archetype`, `labels`, `box` {x, y, width,
2508
+ height} of its visible pixels and `point` (a pixel `pick` answers it at),
2509
+ both in CSS pixels of the play surface, `drawn` (its box drawn alone),
2510
+ `depth`, `coverage` and `occlusion` (0–1, the share of it others hide).
2511
+ `ui` lists the UI elements with `box`, `kind` and `text`, and `camera`
2512
+ the camera. The boxes come from an entity-id pass over the
2513
+ extracted frame on the CPU, so a run without a renderer describes the
2514
+ same frame. `pick` at an entity's `point` answers that entity.
2515
+ - **`expect:<op>`** asserts at its tick. A failure is logged with the tick
2516
+ and the observed value, `og run --json` lists it under `expectations`,
2517
+ and the run exits 1:
2518
+
2519
+ ```text
2520
+ expect:entity:crate:within:380,360,280,240 its described box inside the screen box
2521
+ expect:entity:crate:within:-3,0.4,-1,1,0.6,2 its position inside the world box (x0,y0,z0,x1,y1,z1)
2522
+ expect:event:og.CollisionStarted[:2] at least one (or exactly 2) since the script began
2523
+ expect:ui:text:=Started a UI element shows the text (= exact, else a substring)
2524
+ expect:region:560,16,64,24:color:#9da5b1:24 the region's mean colour within 24 per channel
2525
+ expect:frame:references/start.png:rmse:<=6 the frame against a reference PNG
2526
+ expect:state:score>=3 a fact of GameDesc.facts by dotted path (== != < <= > >=)
2527
+ ```
2528
+
2529
+ A playtest may be expect steps alone: `templates/game/tests/smoke.og.ts`
2530
+ is. Each expectation counts as a check. Without a renderer a frame or
2531
+ region expectation is unchecked, never failed. Take screen boxes from a
2532
+ `describe` of a good run, with a margin.
2533
+ - **`og run --trace out.json`** writes the run in the Chrome trace format;
2534
+ open it in Perfetto (ui.perfetto.dev). Every span lies where it ran.
2535
+ The engine thread holds each tick with its systems, render each frame
2536
+ with its render passes (CPU encoding), gpu the same passes as the GPU
2537
+ ran them (where `og.render.capabilities(world).gpuTimestamps`), load
2538
+ each cooked file read, cook the cook and each output on the cook thread
2539
+ that made it, and `lane N` the jobs og-lanes ran on pool thread N
2540
+ (physics, cull, anim, extract). The probe's `state().timings.systems`,
2541
+ `.passes` and `.gpuPasses` give the last tick's and frame's numbers
2542
+ live.
2543
+ - **`save_scene[:path][:spawned]`** (probe `saveScene(write, spawned)`)
2544
+ writes the scenes back after `teleport`, `set` or the Tune panel moved
2545
+ things: the boot scene and every scene `world.instantiate` made, each
2546
+ to its own `.ogt`. Only the lines whose values changed are rewritten;
2547
+ comments and order stay, and a tuned field the text lacks gets a line.
2548
+ Resources are scene text (`resource Run { chimed true }` at the top
2549
+ level of a scene); a tuned resource no scene sets gets a block in the
2550
+ boot scene. `spawned` also writes the entities your code spawned under
2551
+ `# ── spawned at run time ──` at the end of the boot scene; if you keep
2552
+ them, guard their spawn (`if (world.find("bolt") === null)`) so the next
2553
+ run does not make them twice. With a path, the texts go under `--out`
2554
+ to diff first; without one, they replace `assets/<scene>.ogt`. `set`
2555
+ names engine components by their registered name
2556
+ (`set:marker.og.Transform.scale=0.5,0.5,0.5`).
2557
+ - **`og cook --report`** writes `og-out/cook-report.json`: per asset its
2558
+ triangles per LOD, vertices, textures (size, format), bones, clips,
2559
+ bytes cooked and flags, and for every other asset its figures under its
2560
+ kind (`sound` seconds/channels/sampleRate/chunks, `font`
2561
+ glyphs/kerningPairs/pages, `scene` entities/instances/components,
2562
+ `navmesh` tiles/polygons/vertices, `timeline` tracks/seconds, `strings`
2563
+ locale/keys, `style`, `behaviour`, `tileset`, `tilemap`, `rig`,
2564
+ `rig2d`, `flipbook`, `shader`, `post`, `emitter`, `look`, `palette`,
2565
+ `lut`, `environment`, `video`, `terrain`, `streamPack`). **`og doctor
2566
+ --project . [--json]`** checks it against `budgets { }` in project.ogt
2567
+ and exits 1 on an over-budget asset, naming the asset, the limit, the
2568
+ value and the fix. The keys are `triangles` 100000, `vertices` 65536,
2569
+ `textureSize` 2048, `bones` 128, `clips` 64, `assetBytes` 8 MiB,
2570
+ `totalBytes` 64 MiB, `soundSeconds` 600, `videoSeconds` 300,
2571
+ `fontGlyphs` 8192, `navPolygons` 65536, `sceneEntities` 10000 and
2572
+ `emitterParticles` 16384, each a default a project may move.
2573
+ `bvhShare` 30 is a warning, not a limit: a model whose cooked raycast
2574
+ BVH is over 30 % of its blob (and at least 16 KiB) is named by `og cook
2575
+ --report` and `og doctor`; write `model { raycast false }` in its .ogt
2576
+ when nothing raycasts against it.
2577
+
2578
+ ## 22. Levels of detail, reflection probes and screen-space reflections
2579
+
2580
+ **Levels of detail.** Every model the cook reads is simplified into
2581
+ levels: by default `lods 1 0.5 0.25`, each level a fraction of the full
2582
+ mesh's triangles. A glTF's own `MSFT_lod` levels are kept instead. Import
2583
+ settings change them per model, and `lods 1` keeps only the full mesh:
2584
+
2585
+ ```
2586
+ type import
2587
+ version 1
2588
+ ---
2589
+ model {
2590
+ lods 1 0.4 0.15
2591
+ lodError 0.08
2592
+ }
2593
+ ```
2594
+
2595
+ The cook measures each level's error, the largest distance between the
2596
+ level and the full mesh in the model's units, and `og cook --report`
2597
+ lists each model's triangles (`lods`) and error (`lodErrors`) per level.
2598
+ The renderer draws each `og.Model` entity at the coarsest level whose
2599
+ error covers less than a pixel at the entity's distance, with
2600
+ hysteresis, so nothing flickers at a boundary: a small prop keeps its
2601
+ full mesh until it is far away, a large tree drops levels sooner. A
2602
+ level whose error is too large for its purpose (a hard-surface model
2603
+ melting at a distance) is cooked tighter with `lodError`, or the model
2604
+ keeps more detail with `lodBias`. `og.Model.lodBias` (per entity) and
2605
+ `RenderSettings.lodBias` (the frame) scale that choice: 2 keeps the full
2606
+ mesh twice as far, and 0 always draws it. Check the choice with
2607
+ `debug:lod`, which tints models green, yellow, orange and red by level,
2608
+ with `dump_entity:<name>` (`render.lod`), and with the triangles in
2609
+ `ProbeTimings.triangles` or `dump_render`.
2610
+
2611
+ **Reflection probes.** Smooth surfaces reflect the environment map, which
2612
+ is the sky. Indoors that is wrong: a chrome sphere in a room would show
2613
+ the sky. Put an `og.ReflectionProbe` in the room, and lit surfaces inside
2614
+ its box reflect a cube captured from its position instead, box-projected
2615
+ so the walls reflect where they are:
2616
+
2617
+ ```ts
2618
+ const probe = must(world.spawn({ name: "hall-probe", position: { x: 0, y: 1.6, z: 0 } }));
2619
+ world.insert(probe, og.ReflectionProbe, {
2620
+ size: { x: 10, y: 4.2, z: 10 }, // the room's extents, centred on the entity
2621
+ resolution: 128, // texels per cube face
2622
+ mode: "baked", // or "realtime", every `interval` ticks
2623
+ blendDistance: 0.5, // fade into the environment at the box's faces
2624
+ });
2625
+ ```
2626
+
2627
+ - Choose the probe's mode by what moves in the room:
2628
+ - A `baked` probe is captured once, on its first frame. For a static
2629
+ room, run `og bake probes [project] --commands "0:station:hall"` (the
2630
+ commands reach the scene). It writes the captures into
2631
+ `cooked/probes/`, and runs then load them instead of drawing them.
2632
+ A bake is keyed by the probe's place and size and by its room's
2633
+ content: moving the probe or changing what is in its box (a mesh,
2634
+ a material, a texture, a light) needs a new bake, and until then it
2635
+ draws its own capture. `og doctor --project` lists stale bakes
2636
+ (`stale-probe`).
2637
+ - A `realtime` probe redraws six faces every `interval` ticks. Keep
2638
+ the interval high.
2639
+ - Size the box to the room. Each object takes the smallest box holding
2640
+ its centre, so two rooms that share a model each reflect their own.
2641
+ The capture shows everything in the room: characters in their pose
2642
+ (and each one takes the probe of the room its pose stands in), terrain,
2643
+ grass, water (refracting what lies below it), 3D fluids, glass,
2644
+ particles (soft ones fading as in the frame), lines and world sprites
2645
+ and text. Terrain, grass and water inside the box are lit
2646
+ by it too. At most 8 probes (`MAX_REFLECTION_PROBES`) are used,
2647
+ nearest the camera first.
2648
+ - `dump_render` lists the frame's probes and whether each was loaded from
2649
+ the cache. In a playtest, assert what the sphere shows with
2650
+ `expect:region:x,y,w,h:color:#rrggbb:tol`, and a model's level of
2651
+ detail and the frame's triangles with `expect:entity:<name>:render.lod==1`
2652
+ and `expect:timings:triangles<=1500000`.
2653
+
2654
+ **Screen-space reflections.** On the full tier, `PostEffects.ssr` adds the
2655
+ frame's own reflections to smooth surfaces: a wet street, a polished
2656
+ floor. They show what is on screen, including moving things:
2657
+
2658
+ ```ts
2659
+ const post = must(world.resource(og.PostEffects));
2660
+ post.ssr = true;
2661
+ post.ssrMaxDistance = 12; // metres a ray travels
2662
+ post.ssrSteps = 48; // more for long, thin reflections
2663
+ post.ssrThickness = 0.3; // how far behind a surface a ray still hits it
2664
+ post.ssrFade = 0.15; // fade at the screen's edges
2665
+ ```
2666
+
2667
+ SSR is weighted by roughness as the surface is shaded, its roughness map
2668
+ and a terrain layer's roughness included (`roughness` 0.05 reflects
2669
+ sharply; above 0.75 it reflects nothing), and by Fresnel. What it misses (off screen,
2670
+ behind objects) falls back to the probes and the environment, so use SSR
2671
+ together with a probe. WebGL2 draws no SSR: check
2672
+ `og.render.capabilities(world).ssr`, and on that tier the probe is the
2673
+ whole reflection.
2674
+
2675
+ ## 23. Render targets, second cameras, layers, custom shaders and post passes
2676
+
2677
+ **Render to a texture.** `og.render.renderTarget(world, { width, height,
2678
+ format, depth })` makes a target, as three.js `WebGLRenderTarget`. It
2679
+ answers `{ target, texture, depthTexture, width, height }`. `texture` is
2680
+ an ordinary Texture: a material's `map` or `emissiveMap`, an og.Sprite, an
2681
+ `og.render.sprite` or an og.ui image shows it. `format` is `ldr` (the
2682
+ default) or `hdr`:
2683
+ - `ldr`: 8-bit and finished (exposed with the view's `exposure`, tone
2684
+ mapped). Use it for a monitor, a minimap or a portrait, shown through an
2685
+ unlit material's `map`.
2686
+ - `hdr`: linear radiance, exposed once with the frame that shows it. Use
2687
+ it for a mirror or a portal, shown as `emissiveMap` on a material whose
2688
+ `color` is black.
2689
+
2690
+ `depth: true` also keeps the view's linear depth, from near (0) to far
2691
+ (1), of its opaque meshes, on every tier, WebGL2 included. It costs a
2692
+ second draw of the view's opaque meshes, so ask for it only when a
2693
+ material reads it. (A scene with soft particles, `softDistance` above 0,
2694
+ pays that draw in every view anyway: its particles fade against it.)
2695
+ `RenderCapabilities.maxRenderTargetSize` is the largest side.
2696
+
2697
+ **A second camera.** `og.CameraView` on an entity is a camera with
2698
+ og.Camera's fields (`fov`, `near`, `far`, `zoom`, `orthographic`,
2699
+ `height`, `exposure`). It looks down the entity's local -Z:
2700
+ ```ts
2701
+ const screen = must(og.render.renderTarget(world, { width: 480, height: 270 }));
2702
+ world.insert(monitorMesh, og.Mesh, { geometry: plane, material: must(og.render.standardMaterial(world, { map: screen.texture, unlit: true })) });
2703
+ const cam = must(world.spawn({ name: "cctv", position: eye, quaternion: og.transform.lookAt(eye, target) }));
2704
+ world.insert(cam, og.CameraView, { target: screen.target, fov: 60, every: 2 });
2705
+ // Split-screen: no target, a rect of the frame (fractions from the top left).
2706
+ world.insert(p2, og.CameraView, { viewportX: 0.5, viewportWidth: 0.5 });
2707
+ ```
2708
+ - `every: n` draws it on every n-th tick, and the target keeps its last
2709
+ image in between.
2710
+ - `order` sorts the views. Every view draws before the frame, so a
2711
+ material shows this frame's image.
2712
+ - `clear` with `clearColor` replaces the sky or background colour.
2713
+ - `active: false` stops it.
2714
+
2715
+ A view draws what the main camera draws, from its own eye and on its own
2716
+ `layers`: meshes and models (skinned meshes and custom shaders included),
2717
+ opaque then transparent back to front, the terrain and the sky, particle
2718
+ systems, trails, og.Lines and og.Points, and world sprites and text, lit and
2719
+ shadowed by the frame's lights. Each view is a second draw of everything
2720
+ its layers admit, so keep it cheap with `layers` and `every`.
2721
+ - An `ldr` target and a viewport are exposed with the view's `exposure`
2722
+ and tone mapped with the frame's curve. An `hdr` target holds linear
2723
+ radiance and is exposed by the frame that shows it.
2724
+ - Bloom, AO, SSR, depth of field, colour grading and custom post passes
2725
+ run on the main frame only (the 2D glow, `PostEffects.glow`, is the
2726
+ exception: a view adds its own).
2727
+ - In a view, sprites are 2D-lit and glow and 2D fluids and soft
2728
+ particles draw as in the main frame. Screen sprites, the UI and debug
2729
+ draw stay on the main frame.
2730
+
2731
+ A viewport view draws over the finished image and under the UI.
2732
+
2733
+ **A mirror.** Put `og.Mirror` on the glass's entity. It reflects the main
2734
+ camera in the entity's xy plane (its +Z faces the room) and fits the image
2735
+ to a `width` × `height` rect:
2736
+ ```ts
2737
+ const rt = must(og.render.renderTarget(world, { width: 512, height: 384, format: "hdr" }));
2738
+ world.insert(glass, og.Mesh, { geometry: must(og.render.planeGeometry(world, { width: 4, height: 2.4 })),
2739
+ material: mat({ color: { r: 0, g: 0, b: 0 }, emissive: { r: 1, g: 1, b: 1 }, emissiveMap: rt.texture }) });
2740
+ world.insert(glass, og.Mirror, { target: rt.target, width: 4, height: 2.4 });
2741
+ ```
2742
+
2743
+ **Layers.** `layers` on og.Mesh, og.Model, og.Sprite, og.ParticleSystem,
2744
+ og.Lines, og.Points and og.Camera (and on og.CameraView and og.Mirror) is a
2745
+ three.js bit mask:
2746
+ bit n is layer n, and the default 1 is layer 0. A camera draws an object
2747
+ when `object.layers & camera.layers` is not 0. `og.render.raycast`'s
2748
+ `layers` uses the same test. Some uses:
2749
+ - A first-person body on layer 1 (mask 2) is hidden from the player's
2750
+ camera (mask 1) and shown on a third-person monitor (mask 3).
2751
+ - A minimap marker only the minimap camera draws.
2752
+
2753
+ Shadow maps take every caster whatever its layers, so a hidden object
2754
+ still casts a shadow. Give it `castShadow: false` if it should not.
2755
+
2756
+ The probe's `describe` lists what the main camera shows, and each
2757
+ og.CameraView and og.Mirror under `views` with what its own image shows,
2758
+ on its layers. An entity on a layer only a view sees is found under that
2759
+ view, not in `entities`. A main-camera entity names the views that also
2760
+ show it in its `views`. `pick` and `expect:entity:...:within` read the
2761
+ main camera.
2762
+
2763
+ **Full custom shaders.** `mode full lit` (or `mode full unlit`) in a `type
2764
+ shader` asset replaces the lit shader's stages with your own entries:
2765
+ ```wgsl
2766
+ fn vertex(v: VertexInput, model: mat4x4<f32>, p: Params) -> VertexOutput // v: position normal uv color tangent (object space; skin and morphs applied)
2767
+ fn fragment(f: FragmentInput, p: Params) -> SurfaceOutput // f: world local normal uv color data view pixel time
2768
+ // VertexOutput: world (projected by the engine), normal, uv, color, data (free, interpolated)
2769
+ // SurfaceOutput: color (albedo + alpha; unlit: the colour), normal, metallic, roughness, emissive
2770
+ // camera(): view_proj, position, forward, time. tex0(uv)..tex7(uv): the material's map, metalnessMap,
2771
+ // roughnessMap, normalMap, emissiveMap, aoMap, gradientMap, dissolveMap. Helpers: standard_vertex(v, model),
2772
+ // standard_surface(f), noise3(p).
2773
+ ```
2774
+ An entry you leave out is the standard one.
2775
+ - `lit` surfaces are lit by the sun's cascades, the point, spot and rect
2776
+ lights with their shadows and cookies (a rect light as an area, as on a
2777
+ standard material), the environment and the fog. `unlit` surfaces draw
2778
+ as returned.
2779
+ - The depth prepass, every shadow pass and every view run the same
2780
+ entries, and alpha under the material's `alphaTest` is cut in each of
2781
+ them. A dissolve's shadow therefore has its holes.
2782
+ - Make the material with `og.render.shaderMaterial` and set parameters with
2783
+ `og.render.setShaderParam`, as for hooks (§12).
2784
+ - `templates/fx-showcase/assets/shaders/dissolve-full.*` is the example.
2785
+
2786
+ **Custom post passes.** A `type post` asset declares
2787
+ `fn post(f: PostInput, p: Params) -> vec4<f32>` in its `code` or `source`,
2788
+ with up to eight `param` lines. The function reads:
2789
+ - `previous(uv)`: the pass before it, linear HDR;
2790
+ - `scene_color(uv)`: the lit image;
2791
+ - `depth(uv)`: view depth in metres;
2792
+ - `normal(uv)`: the world normal;
2793
+ - `f.uv`, `f.pixel`, `f.size` and `f.time`.
2794
+
2795
+ It returns linear HDR. To use one:
2796
+ 1. Load it with `og.load.post(world, "post/crt.ogt")`.
2797
+ 2. Put the handles in the list `PostEffects.custom`, at most
2798
+ `RenderCapabilities.maxCustomPosts` (8) of them. They run in its order,
2799
+ each over the one before it, after depth of field and motion blur and
2800
+ before exposure, bloom and tone mapping. Write the list whole:
2801
+ `post.custom = [grade, crt]`, `post.custom = [...post.custom, h]`,
2802
+ `post.custom = []`. Reading it answers a frozen copy, so
2803
+ `post.custom.push(h)` throws instead of changing nothing; nine passes
2804
+ are refused, never cut to eight. A look gives it on one line:
2805
+ `custom "post/tint.ogt" "post/crt.ogt"`.
2806
+ 3. Set its parameters with `og.render.setPostParam(world, post, "curve",
2807
+ [0.1])`.
2808
+
2809
+ The cook compiles it for every back end. A pass that a device still fails
2810
+ to compile is skipped and listed by `og.render.postErrors(world)`.
2811
+ `templates/fx-showcase/assets/post/crt.*` is a CRT example.
2812
+
2813
+ ## 24. Baked lighting, volumetric fog, area lights and cookies
2814
+
2815
+ **Baked lighting.** Use it for light that bounces around a static space,
2816
+ such as a red wall tinting the floor beside it or a room lit through its
2817
+ door. Mark the scene's unmoving models `static true` and put an
2818
+ `og.LightVolume` box over the space where dynamic objects move:
2819
+
2820
+ ```
2821
+ entity "hall" {
2822
+ model "models/hall.glb"
2823
+ static true
2824
+ Transform { position 0 0 0 quaternion 0 0 0 1 scale 1 1 1 }
2825
+ }
2826
+ entity "hall-probes" {
2827
+ Transform { position 0 2 0 quaternion 0 0 0 1 scale 1 1 1 }
2828
+ LightVolume { size 12 4 12 spacing 1 }
2829
+ }
2830
+ ```
2831
+
2832
+ Then run `og bake lightmaps` in the project. Its options are
2833
+ `--samples 256`, `--bounces 3`, `--texels-per-metre 16`, `--max-size 1024`,
2834
+ `--sky r,g,b`, `--seconds S` and `--scene <id>`; the fx-showcase Cornell
2835
+ box takes 1.4 s at 512 samples. The bake is a CPU path tracer and needs
2836
+ no GPU. It writes `<scene>.lightmap.ogt` and `.ktx2` beside the scene;
2837
+ commit both.
2838
+
2839
+ A static entity needs a name. The bake holds the light that bounces, the
2840
+ sky and emissive surfaces; lights still shine and cast shadows live on
2841
+ top. Static models draw with their lightmap, and everything else inside a
2842
+ LightVolume takes its ambient light from the probes. After you change the
2843
+ scene's text or a static model's geometry, the bake is stale and the scene
2844
+ renders unbaked until you bake again; a re-cook that keeps the geometry
2845
+ (a cooker update, a texture or raycast option) keeps the bake. `og doctor
2846
+ --project` warns `unbaked`, and
2847
+ `og.render.bakedLighting(world)` lists each scene with `baked` and a
2848
+ `reason`. `RenderSettings.bakedLighting = false` draws everything unbaked,
2849
+ which is useful to compare. Every instantiated scene draws its own bake,
2850
+ so baked scenes loaded together or streamed in side by side all keep
2851
+ their light.
2852
+
2853
+ **Volumetric fog.** Set `Fog.volumetric = true` to light the fog as a
2854
+ medium, which gives shafts through windows and the cones of spots. Tune it
2855
+ with `density`, `heightDensity` and `falloff` (thicker low down),
2856
+ `scattering` (how bright) and `anisotropy` (towards 1, bright looking into
2857
+ the light), up to `volumetricDistance`. Fog of 0.03 to 0.1 per metre reads
2858
+ well indoors. With `og.DayNight`, the shafts move with the sun.
2859
+ `RenderCapabilities.volumetricFog` is `froxel` on the full tier or
2860
+ `raymarch` on WebGL2. On both, the sun, spots, points and rect lights light
2861
+ the fog, so a lantern glows in it on WebGL2 too.
2862
+ `RenderCapabilities.volumetricLights` is how many local lights light one
2863
+ sample (64 for the froxel grid, 8 for the raymarch, brightest first).
2864
+
2865
+ **Area lights.** `og.RectLight { width, height, color, intensity,
2866
+ twoSided }` emits from a rectangle facing the entity's -Z. Aim it with
2867
+ `og.transform.lookAt`, as you would a spot. A glossy floor shows the
2868
+ rectangle's shape in its highlight. Pair one with an emissive slab for
2869
+ the fixture. Set `castShadow` for a soft shadow: sharp where a caster
2870
+ touches the floor, widening with the panel's size away from it. It takes
2871
+ one shadow layer (two when `twoSided`) from the same budget as shadowed
2872
+ spots and points, and is drawn from the panel's centre, so a caster right
2873
+ against a large panel is shadowed as from its middle.
2874
+
2875
+ **Cookies.** `SpotLight.cookie` is a texture projected along the cone,
2876
+ like a window's lattice or stained glass. `PointLight.cookie` is six
2877
+ square faces stacked top to bottom (+X, -X, +Y, -Y, +Z, -Z), like a
2878
+ pierced lantern. A cookie multiplies the light and its colour. A frame
2879
+ draws at most `og.render.capabilities(world).maxLightCookies` (24) cookie
2880
+ layers: a spot cookie takes one, a point cookie six, and a texture shared
2881
+ by lights of one kind counts once. Past that, the later lights shine
2882
+ without their cookies and the log warns, naming them; share cookie
2883
+ textures between lanterns to stay under it.
2884
+
2885
+ ## 25. Where to look things up
1580
2886
 
1581
2887
  `reference/index.md` (the generated wiki shipped with this skill) maps every
1582
2888
  module (grouped by skill, as in §1), type and function to its page, with the IDL text, defaults, ranges
1583
2889
  and units of every field.
2890
+
2891
+ `guides/three-to-og.md` (beside this skill) maps three.js's classes and
2892
+ idioms to the engine's: the scene graph to entities and og.Parent,
2893
+ Object3D to og.Transform, the geometry classes to §18's generators, the
2894
+ materials to StandardMaterialDesc, the loaders to og.load, the render
2895
+ loop to systems on the tick, Raycaster to `og.render.raycast`, and the
2896
+ gaps by name. `og migrate three <file.ts>` rewrites a three.js file's
2897
+ mechanical part with those mappings and marks every line it cannot,
2898
+ citing the guide's section.