@origingame/engine 0.2.0 → 0.3.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 (107) hide show
  1. package/bin/og.mjs +1 -1
  2. package/dist/web/origin-engine.d.ts +219 -5
  3. package/dist/web/origin-engine.js +1344 -125
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +68 -3
  6. package/dist/web-publish/origin-engine-lanes.js +1007 -22
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +162 -5
  9. package/dist/web-publish/origin-engine.js +1004 -19
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +52 -3
  12. package/dist/web-publish-2d/origin-engine-lanes.js +1007 -22
  13. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  14. package/dist/web-publish-2d/origin-engine.d.ts +162 -5
  15. package/dist/web-publish-2d/origin-engine.js +1004 -19
  16. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +52 -3
  18. package/knowledge/looks/catalogue.json +40 -40
  19. package/knowledge/package.json +1 -1
  20. package/knowledge/scaffold/distribution.json +14 -0
  21. package/knowledge/scaffold/origin-engine.json +2 -2
  22. package/knowledge/scaffold/template/README.md +2 -1
  23. package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
  24. package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
  25. package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
  26. package/knowledge/scaffold/template/host.d.ts +46 -0
  27. package/knowledge/scaffold/template/package.json +1 -1
  28. package/knowledge/scaffold/template/project.ogt +1 -1
  29. package/knowledge/scaffold/template/src/main.ts +1 -1
  30. package/knowledge/scaffold/template/tests/frames.ts +134 -0
  31. package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
  32. package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
  33. package/knowledge/scaffold/template/tsconfig.json +1 -1
  34. package/knowledge/scripts/mount.ts +2 -1
  35. package/knowledge/scripts/validate.ts +19 -10
  36. package/knowledge/skills/og-build/SKILL.md +8 -5
  37. package/knowledge/skills/og-core/SKILL.md +11 -0
  38. package/knowledge/skills/og-game/SKILL.md +877 -8
  39. package/knowledge/skills/og-platform-sdk/SKILL.md +1 -0
  40. package/knowledge/skills/og-taste/SKILL.md +82 -73
  41. package/knowledge/skills/og-ui/SKILL.md +65 -50
  42. package/knowledge/wiki/ai.md +20 -20
  43. package/knowledge/wiki/anim.md +384 -41
  44. package/knowledge/wiki/atmosphere.md +36 -13
  45. package/knowledge/wiki/audio.md +18 -18
  46. package/knowledge/wiki/camera.md +19 -13
  47. package/knowledge/wiki/commands.md +19 -13
  48. package/knowledge/wiki/debug.md +414 -0
  49. package/knowledge/wiki/game.md +8 -8
  50. package/knowledge/wiki/i18n.md +7 -7
  51. package/knowledge/wiki/index.md +262 -7
  52. package/knowledge/wiki/input.md +47 -47
  53. package/knowledge/wiki/load.md +236 -68
  54. package/knowledge/wiki/math.md +2607 -0
  55. package/knowledge/wiki/nav.md +34 -34
  56. package/knowledge/wiki/net.md +340 -0
  57. package/knowledge/wiki/path.md +402 -0
  58. package/knowledge/wiki/physics.md +95 -95
  59. package/knowledge/wiki/platform.md +463 -29
  60. package/knowledge/wiki/probe.md +681 -67
  61. package/knowledge/wiki/reflect.md +15 -15
  62. package/knowledge/wiki/render.md +3893 -1896
  63. package/knowledge/wiki/rng.md +14 -14
  64. package/knowledge/wiki/stream.md +15 -15
  65. package/knowledge/wiki/system.md +8 -8
  66. package/knowledge/wiki/systems.md +2 -2
  67. package/knowledge/wiki/terrain.md +19 -19
  68. package/knowledge/wiki/tilemap.md +13 -13
  69. package/knowledge/wiki/time.md +4 -4
  70. package/knowledge/wiki/timeline.md +396 -0
  71. package/knowledge/wiki/transform.md +8 -8
  72. package/knowledge/wiki/tween.md +17 -17
  73. package/knowledge/wiki/ui.md +559 -629
  74. package/knowledge/wiki/video.md +157 -0
  75. package/knowledge/wiki/world.md +36 -36
  76. package/package.json +4 -3
  77. package/schema/api.schema.json +21707 -14725
  78. package/shell/page.ts +26 -2
  79. package/shell/platform.ts +46 -0
  80. package/shell/player.ts +7 -0
  81. package/src/depth.ts +71 -0
  82. package/src/e6.ts +32 -1
  83. package/src/env.d.ts +5 -1
  84. package/src/generated/abi.ts +189 -3
  85. package/src/index.ts +25 -4
  86. package/src/light.ts +18 -0
  87. package/src/math.ts +622 -0
  88. package/src/node/index.ts +17 -0
  89. package/src/node/playtest-run.ts +114 -18
  90. package/src/node/polish.ts +13 -3
  91. package/src/node/render.ts +291 -0
  92. package/src/node/run.ts +86 -5
  93. package/src/p2.ts +19 -1
  94. package/src/playtest.ts +57 -4
  95. package/src/runtime.ts +13 -3
  96. package/src/targets.ts +28 -0
  97. package/src/trace.ts +169 -0
  98. package/src/ui-kit/huds.ts +34 -34
  99. package/src/ui-kit/menus.ts +12 -12
  100. package/src/ui-kit/settings.ts +2 -2
  101. package/src/ui-kit/store.ts +18 -18
  102. package/src/ui-kit/story.ts +6 -4
  103. package/src/ui-kit/util.ts +8 -8
  104. package/src/web/shell.ts +58 -0
  105. package/src/web/worker.ts +103 -2
  106. package/types/bindings.json +9101 -6647
  107. package/types/index.d.ts +15503 -11564
@@ -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,7 +114,7 @@ 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) |
114
119
  | **og-ai** | enemies and NPCs: navigation and minds as data | nav, ai |
115
120
  | **og-audio** | sound: events, presets, UI sounds, the mix, music, space | audio |
@@ -883,6 +888,276 @@ world.insert(body, og.AnimationGraph, { asset: graph });
883
888
  // every tick: world.set(body, og.AnimationGraph, { p0: groundSpeed, p1: airborne ? 1 : 0 });
884
889
  ```
885
890
 
891
+ ### 2D skeletons: the rig2d grammar
892
+
893
+ A cut-out character (bones, a cape that bends, feet that stay on a slope)
894
+ is an `.ogt` of `type rig2d` beside its PNG, loaded with
895
+ `og.load.rig2d(world, "rigs/hero.ogt")` (`reference/anim.md`). Pixels,
896
+ +y up, degrees counter-clockwise, `unit` pixels per world unit; bones
897
+ parent first, slots draw in file order:
898
+
899
+ ```text
900
+ type rig2d
901
+ version 1
902
+ ---
903
+ texture "hero.png"
904
+ filter nearest
905
+ unit 20
906
+ bone root {
907
+ }
908
+ bone hip {
909
+ parent root
910
+ at 0 22
911
+ rotation 90
912
+ length 16
913
+ }
914
+ slot torso {
915
+ bone hip
916
+ attachment torso
917
+ color 1 1 1 1
918
+ offset 0 0 0 0
919
+ }
920
+ region torso {
921
+ slot torso
922
+ rect 0 0 12 20
923
+ at 8 0
924
+ rotation -90
925
+ size 12 20
926
+ }
927
+ mesh cape {
928
+ slot cape
929
+ vertex 0 32 hip 16 9 1
930
+ vertex 6 40 hip 8 4 0.4 cape-1 7 0 0.6
931
+ triangles 0 1 2
932
+ }
933
+ box hurt {
934
+ slot torso
935
+ polygon -6 0 6 0 6 20 -6 20
936
+ }
937
+ child flame {
938
+ slot lantern
939
+ armature fire
940
+ clip burn
941
+ at 15 0
942
+ }
943
+ ik leg {
944
+ bones thigh shin
945
+ target foot
946
+ bend clockwise
947
+ }
948
+ armature fire {
949
+ bone base {
950
+ }
951
+ # slots, regions, meshes, boxes, ik and clips, as at the top
952
+ }
953
+ clip idle {
954
+ duration 2
955
+ loop true
956
+ bone hip {
957
+ translate 0 0 0 curve 0.42 0 0.58 1
958
+ translate 1 0 -1.5
959
+ skew 0 0
960
+ }
961
+ slot torso {
962
+ offset 0 0 0 0 0
963
+ offset 1 0.2 0.1 0 0
964
+ }
965
+ deform cape {
966
+ key 0 0 0 0 0
967
+ key 1 0 -3 0 -6
968
+ }
969
+ order 1.5 cape torso
970
+ ik leg {
971
+ key 0 1 clockwise
972
+ key 1 0.5 clockwise
973
+ }
974
+ event 0 "inhale"
975
+ }
976
+ ```
977
+
978
+ `rect` is pixels from the image's top left; a mesh `vertex` is `u v` in
979
+ pixels then, per bone, `bone x y weight` in that bone's space (weights
980
+ summing to 1), or plain `x y` in the slot's bone. A bone's or region's
981
+ `skew` turns its y axis that many degrees past its x axis. A slot draws
982
+ `texel × color + offset`. Bone keys add to the setup pose (`rotate` and
983
+ `skew` degrees, `translate` pixels, `scale` multiplies); every key eases
984
+ linearly to the next, holds with `step`, or follows `curve x1 y1 x2 y2`
985
+ (a cubic Bézier from 0,0 to 1,1, x time and y value; more segments as
986
+ `x y x1 y1 x2 y2`). A `deform` key offsets every influence in order, one
987
+ `x y` pair each; `order <time> <every slot>` redraws back to front from
988
+ then (alone, the file's order); an `ik` key sets the constraint's weight
989
+ and bend. An `ik`'s `bones` are a chain of any length, root first: one
990
+ 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)`
992
+ answers its polygon (in the Transform's frame) while its slot shows it,
993
+ for hit and hurt boxes a clip switches. A `child` shows an `armature`
994
+ block (a rig of its own on the same texture) in its slot; it plays the
995
+ 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
997
+ `.ogt` with `dragonbones "hero_ske.json"` and `atlas "hero_tex.json"`
998
+ (skew, colour offsets, zOrder and IK timelines, armature displays,
999
+ bounding boxes and curves included; Spine's files are not read, convert
1000
+ them to this text).
1001
+
1002
+ ```ts
1003
+ const rig = must(og.load.rig2d(world, "rigs/hero.ogt"));
1004
+ const hero = must(world.spawn({ name: "hero", position: { x, y, z: 0 } }));
1005
+ must(world.insert(hero, og.Skeleton2D, { rig: rig.rig, clip: og.anim.clip(rig, "idle"), layer: 1 }));
1006
+ // A foot planted on the ground: one og.IkTarget2D per constraint, moved every tick.
1007
+ const foot = must(world.spawn({ name: "hero-foot" }));
1008
+ must(world.insert(foot, og.IkTarget2D, { skeleton: hero, constraint: rig.constraints.indexOf("leg"), target: { x, y: groundY } }));
1009
+ ```
1010
+
1011
+ `og.Skeleton2D` plays `clip` at `speed` (`time` is written each tick;
1012
+ setting it seeks), wraps when `loop`, mirrors with `flipX` and draws at
1013
+ sprite `layer`, lit by `og.Lighting2D` like a sprite there. Clip events
1014
+ arrive as `og.Skeleton2DEvent` (`entity`, `clip`, `name`); an
1015
+ `og.IkTarget2D` writes `reached` and `error` (its `weight` multiplies the
1016
+ constraint's own). The pose is a function of the component, the
1017
+ Transform and the targets, so a snapshot carries only the playback
1018
+ position and the boxes. `templates/game-2d`'s lamp keeper
1019
+ (`assets/rigs/keeper.ogt`, `spawnKeeper`, `Walk`, `Plant`) shows all of
1020
+ it. A walk is the walk clip played while a system moves the entity, with
1021
+ the feet's targets stepping in time: each planted for half the cycle
1022
+ (sliding back as fast as the body goes), then swung a stride ahead, so
1023
+ the feet stay on the ground the IK finds.
1024
+
1025
+ ### Cutscenes: timelines and paths
1026
+
1027
+ A cutscene, an intro, a scripted beat or a fly-through is a `type timeline`
1028
+ file, not code (`reference/timeline.md`, `reference/path.md`;
1029
+ `templates/game`'s `assets/timelines/intro.ogt` and `tests/intro.og.ts`
1030
+ are the pattern). Tracks of keys at seconds: between two keys the later
1031
+ key's `ease` (og.tween's names: `cubicInOut`, `quadIn`, ...) mixes them,
1032
+ outside them the value is held. Every value is evaluated at the
1033
+ timeline's time, never accumulated, so a seek lands exactly where playing
1034
+ would (a playtest or a video can start at any second).
1035
+
1036
+ ```text
1037
+ type timeline
1038
+ version 1
1039
+ ---
1040
+ name "Intro"
1041
+ duration 10
1042
+ path "flyby" {
1043
+ kind catmullRom # or bezier (p0 c0 c1 p1 ...) or linear; closed true wraps
1044
+ point -5 3.2 9
1045
+ point -1.5 2.4 7.5
1046
+ point 2 2 7
1047
+ point 5 2.8 8.5
1048
+ }
1049
+ track camera {
1050
+ rail "flyby" # or position/target keys, or follow "<entity>"
1051
+ key 0 {
1052
+ at 0
1053
+ target 0 1 0
1054
+ fov 50
1055
+ }
1056
+ key 9.5 {
1057
+ at 1
1058
+ target 1.5 1 0
1059
+ fov 42
1060
+ ease cubicInOut
1061
+ }
1062
+ }
1063
+ track transform "trickster" {
1064
+ key 2 {
1065
+ position 2 0 0
1066
+ }
1067
+ key 5 {
1068
+ position 0.5 0 1.5
1069
+ rotation 0 90 0
1070
+ ease cubicInOut
1071
+ }
1072
+ }
1073
+ track animation "trickster" { # the entity's og.Model's clips (or `model "<glb>"`)
1074
+ key 0 {
1075
+ clip "idle"
1076
+ loop true
1077
+ }
1078
+ key 2 {
1079
+ clip "walk"
1080
+ loop true
1081
+ blendIn 0.3
1082
+ }
1083
+ }
1084
+ track caption {
1085
+ captions "narration.srt" # beside the timeline; or word timestamps .json
1086
+ style lg # a UI type step
1087
+ anchor bottom
1088
+ }
1089
+ track audio {
1090
+ key 1 {
1091
+ sound "audio/blip.wav"
1092
+ volume 0.6
1093
+ bus sfx
1094
+ }
1095
+ }
1096
+ track music {
1097
+ key 0 {
1098
+ sound "audio/theme.ogg"
1099
+ fadeIn 1
1100
+ fadeOut 2
1101
+ end 9
1102
+ }
1103
+ }
1104
+ track event {
1105
+ key 5.5 {
1106
+ do "spawn_wave:2"
1107
+ retroactive true
1108
+ }
1109
+ }
1110
+ track transition {
1111
+ key 0 { # cut, fade, wipe, dissolve, iris, slide
1112
+ kind fade
1113
+ mode in
1114
+ duration 1
1115
+ }
1116
+ key 9 {
1117
+ kind fade
1118
+ mode out
1119
+ duration 1
1120
+ }
1121
+ }
1122
+ track marker {
1123
+ key 5.5 {
1124
+ name "drop"
1125
+ }
1126
+ }
1127
+ ```
1128
+
1129
+ The cook checks
1130
+ the sounds and models it names and imports the captions, so an `.srt`
1131
+ edit recooks the timeline. Play it from code or from the command channel:
1132
+
1133
+ ```ts
1134
+ const intro = must(og.load.timeline(world, "timelines/intro.ogt"));
1135
+ const e = world.spawn({ name: "intro" });
1136
+ world.insert(e, og.Timeline, { asset: intro });
1137
+ og.timeline.play(world, e); // pause, seek(world, e, 4), stop
1138
+ // a system reading og.TimelineMarker (name "drop") or og.TimelineEnded reacts to it
1139
+ ```
1140
+
1141
+ `og run . --commands "0:timeline:play:intro;4:screenshot;8:screenshot"`
1142
+ plays `timelines/intro.ogt` on an entity named `timeline-intro`;
1143
+ `timeline:seek:4`, `timeline:pause`, `timeline:resume`, `timeline:stop`
1144
+ take an optional `:<name>`. While a timeline with a camera track plays
1145
+ (or is paused) it owns `og.Camera`; it hands back the camera it found
1146
+ when it stops or ends. Event keys run their action at the start of the
1147
+ next tick; a seek skips the events it passes unless they are
1148
+ `retroactive`, and markers are sent only by playing past them. A capture
1149
+ at `t:` shows the world after the ticks before t: the play's first tick
1150
+ is the timeline's time 0, so the frame at `4:` shows 3.983 s at 60 Hz.
1151
+
1152
+ Paths on their own: `og.path.create(world, { points, kind, closed,
1153
+ tension })` or `og.path.find(world, "<name>")` for one a scene declares
1154
+ (the same `path` block in a `type scene`); `og.path.sample(world, p, t)`
1155
+ gives position, tangent and up at arc-length t 0..1, `length` and
1156
+ `nearest` the rest. `og.PathFollower { path, speed, loop, align, offset }`
1157
+ moves an entity along one each tick (`distance` is its own field, so a
1158
+ snapshot keeps it); `og.CameraRail { path }` rides one instead of its
1159
+ inline points.
1160
+
886
1161
  ## 10. Effects
887
1162
 
888
1163
  Every effect is off until the game turns it on, as data:
@@ -1353,7 +1628,7 @@ cloth's points (about 1 ms per 1,000) are the ones to count (§13).
1353
1628
  A **look** is an `.ogt` of `type look` under `assets/looks/` that sets the
1354
1629
  whole treatment of the frame at once: the resources `RenderSettings`,
1355
1630
  `PostEffects`, `Stylise`, `MaterialStyle`, `Fog`, `Sky` and `Environment`
1356
- (one block each, fields by name), the UI `skin` and the `antialias`.
1631
+ (one block each, fields by name), the UI style (`ui`) and the `antialias`.
1357
1632
  Choosing one for a premise is og-taste §1 and §12; this is the mechanism.
1358
1633
 
1359
1634
  ```text
@@ -1362,7 +1637,7 @@ version 1
1362
1637
  ---
1363
1638
  name "Harbour"
1364
1639
  extends "anime-cel-3d" # a built-in preset (templates/looks/<slug>.ogt) or another look's id
1365
- skin "skins/harbour/skin.ogt"
1640
+ ui "styles/harbour/style.ogt" # the game's own UI style, or a built-in by slug (ui "fantasy")
1366
1641
  Stylise {
1367
1642
  ink true
1368
1643
  inkWidth 1.5
@@ -1384,14 +1659,14 @@ og.render.look(world)?.name; // the
1384
1659
  - Applying a look replaces each resource it names **whole** (the fields it
1385
1660
  sets, the rest their defaults; `PostEffects.flash` and `shakeTrauma`
1386
1661
  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
1662
+ its style. Write per-game tweaks in `start` after it, or in the look. A
1388
1663
  system that applies one declares those resources among its writes.
1389
1664
  - `extends` flattens at cook time, field by field (a field written wins);
1390
1665
  `assets/looks/<slug>.ogt` of the game's own replaces the preset of that
1391
1666
  slug for every look that extends it. The cook refuses an unknown field,
1392
1667
  a value out of range and an asset not in the bundle, naming the line.
1393
1668
  - `og.load.lookInfo` (and `og.render.look`) answer `LookInfo`: `name`,
1394
- `extends`, `resources`, `skin`, `antialias`. A look's `antialias` (tier,
1669
+ `extends`, `resources`, `style`, `antialias`. A look's `antialias` (tier,
1395
1670
  off, msaa4 or taa) is the renderer's when `GameDesc.render` names none.
1396
1671
  - **`og.Stylise`** moves the frame away from the photographic, every
1397
1672
  primitive off at zero: the pixel grid (`pixelHeight`, `pixelUpscale`,
@@ -1576,7 +1851,601 @@ Test either across a relaunch with a playtest's `store: "keep"` and
1576
1851
  tape of inputs) and a ghost (a tape of poses) are og-core §2's
1577
1852
  "Recording, replaying and ghosts".
1578
1853
 
1579
- ## 18. Where to look things up
1854
+ ## 18. Geometry made at run time, lines, points and debug draw
1855
+
1856
+ Meshes need not come from a file. `og.render.bufferGeometry(world, desc)`
1857
+ takes three.js BufferGeometry's attributes: `positions` (a Float32Array of
1858
+ x y z), and optionally `normals`, `uvs` (v down the image), `colors`
1859
+ (r g b or r g b a, multiplied into the material), `tangents` and `indices`
1860
+ (a Uint32Array, counter-clockwise from the front). Missing normals are
1861
+ computed as `computeVertexNormals` does. The geometry draws as any other
1862
+ through `og.Mesh`, and it is a `Collider.mesh` at once (`physics` is true by
1863
+ default), so a procedural level collides with what is drawn:
1864
+
1865
+ ```ts
1866
+ const terrain = must(og.render.bufferGeometry(world, { positions, colors, indices, dynamic: true }));
1867
+ world.insert(e, og.Mesh, { geometry: terrain, material });
1868
+ world.insert(e, og.RigidBody, { kind: BODY_STATIC });
1869
+ world.insert(e, og.Collider, { shape: 5 /* SHAPE_MESH */, mesh: terrain });
1870
+ // Each tick (water, a deforming surface): every attribute again.
1871
+ must(og.render.updateGeometry(world, terrain, { positions, colors, indices }));
1872
+ ```
1873
+
1874
+ Only a geometry made with `dynamic: true` can be updated. The same vertex
1875
+ and index counts rewrite it in place, and other counts reallocate it. Its
1876
+ physics mesh is re-registered, and its colliders are rebuilt at the next
1877
+ step. `og.render.geometryInfo` answers the counts and bounds of any
1878
+ geometry.
1879
+
1880
+ Beside `boxGeometry`, `planeGeometry` and `sphereGeometry`, three.js's
1881
+ primitives keep their parameters and defaults: `cylinderGeometry`,
1882
+ `coneGeometry`, `capsuleGeometry` (an og.Collider capsule of the same
1883
+ radius and `height` matches it), `torusGeometry`, `ringGeometry`,
1884
+ `circleGeometry` and `tubeGeometry`. A tube follows a centripetal
1885
+ Catmull-Rom curve through `points`, for pipes, rails and cables; give it
1886
+ `path` instead (an og.path from `og.path.create`, or a scene's `path`
1887
+ block found by `og.path.find`) and it follows that path's own curve,
1888
+ Bezier or linear included, closed when the path is. Its rings are evenly
1889
+ spaced along the length, as three.js spaces them. Rings and circles are one-sided, so use
1890
+ `side: "double"` to see them from behind. `wireframe: true` on a
1891
+ standard material draws the triangle edges instead of the faces, one pixel
1892
+ wide on every tier.
1893
+
1894
+ **Lines and points.** `og.render.lines(world, desc)` and
1895
+ `og.render.points(world, desc)` are immediate mode, like
1896
+ `og.render.sprite`: submit them every tick they should show.
1897
+ - Lines: `mode` is `strip`, `segments` or `loop`. `width` is in CSS
1898
+ pixels, or in world units with `worldUnits`. They also take `colors` per
1899
+ point, `depthTest` (false draws over the scene), `dashed` with `dashSize`
1900
+ and `gapSize`, and `screen: true`. Screen lines use CSS pixels from the
1901
+ top left of the play surface and draw over the finished frame, under
1902
+ the UI.
1903
+ - Points: `sizeAttenuation` makes `size` a size in world units; without it
1904
+ `size` is a fixed size in CSS pixels.
1905
+ - A static set is data: the `og.Lines` and `og.Points` components draw a
1906
+ geometry's vertices in order at the entity's transform. Make the
1907
+ geometry with `bufferGeometry({ positions, colors, physics: false })`,
1908
+ unindexed, any count.
1909
+
1910
+ **What is under the cursor.** `og.render.raycast(world, ray, desc)` tests
1911
+ what is drawn (every og.Mesh and og.Model at its world transform), not the
1912
+ colliders. It goes through a BVH per geometry and answers every hit,
1913
+ nearest first; `first: true` answers only the nearest. Each hit has
1914
+ `entity`, `geometry`, `distance`, `point`, `normal` (the face's, in world
1915
+ space), `uv` and `faceIndex`. Inside a system it reads the meshes as the
1916
+ tick started, as `og.physics.raycast` does. Use it for picking, decals on
1917
+ detailed meshes, and line of sight against geometry without colliders.
1918
+
1919
+ A model's BVH is cooked: `og cook` builds each primitive's into the
1920
+ model's `.ogb` (about 6 to 9 bytes per triangle compressed, listed as
1921
+ `bvhBytes` in `og cook --report`), and it loads with the mesh, so the
1922
+ first raycast never stalls. A model no ray is cast at can leave it out
1923
+ with import settings; its first raycast then builds it:
1924
+
1925
+ ```
1926
+ type import
1927
+ version 1
1928
+ ---
1929
+ model {
1930
+ raycast false
1931
+ }
1932
+ ```
1933
+
1934
+ Leave it on for anything a player can click, aim at or stand a decal on.
1935
+ Turn it off for a large model that is only scenery (templates/game's
1936
+ 16,512-triangle dropship gains 108 KB, 64 % of its blob). Geometries made
1937
+ at run time (`bufferGeometry`, the primitives) build theirs when made or
1938
+ updated.
1939
+
1940
+ **Debug draw.** `og.debug.line`, `box`, `sphere`, `arrow`, `axes` and
1941
+ `text` (a label above a world point, in the UI's body font; every field is
1942
+ in `reference/debug.md`) each take
1943
+ `{ color, width, duration, rotation }`. `duration` is in ticks; 0 shows the
1944
+ shape this frame only, so draw it every tick. Debug draws show over the
1945
+ finished frame and never hide behind geometry, so they read in every
1946
+ capture. The command channel switches the views:
1947
+ - `debug:physics` shows the colliders, joints and contacts.
1948
+ - `debug:nav` shows the grids' blocked cells, the navmesh polygons and
1949
+ every agent's path.
1950
+ - `debug:bounds` shows every drawn mesh's box.
1951
+ - `debug:none` shows only the draws; `debug:off` and `debug:on` hide and
1952
+ show everything.
1953
+
1954
+ `og.debug.setView` does the same from code, and `RenderOptions.debugView`
1955
+ picks a view from the first frame. A publish build drops all of it, so
1956
+ debug draws may stay in the code. Debug draws are not world state:
1957
+ snapshots and replays do not carry them.
1958
+
1959
+ ## 19. Video: footage in, videos out
1960
+
1961
+ **Footage in.** An `.ogt` of `type video` names an `.mp4`, `.webm` or
1962
+ `.mov`; `og cook` turns it with ffmpeg (`og doctor` says whether ffmpeg
1963
+ and ffprobe are there) into a texture array of its frames and its sound:
1964
+
1965
+ ```text
1966
+ type video
1967
+ version 1
1968
+ ---
1969
+ source "clips/intro.mp4" # beside this file, or under assets/
1970
+ fps 30 # frames kept per second (default 30)
1971
+ maxSeconds 8 # what is kept from the start (default 8)
1972
+ size 320 180 # each frame's size (default the source's, at most 1024 wide)
1973
+ kind color # or pixel: nearest, lossless
1974
+ audio true # its sound track (default true)
1975
+ ```
1976
+
1977
+ At most 256 frames (fps × seconds): the cook refuses more, naming the
1978
+ `maxSeconds` that fits, and warns past 64 MiB of cooked frames. Keep
1979
+ footage small: a 320×180 clip of 8 s at 30 fps is about 2 MiB cooked.
1980
+ Load it in `start`, then play it with `og.VideoPlayer`; its frame is a
1981
+ plain `Texture`, so a sprite, a material map or a UI image shows it:
1982
+
1983
+ ```ts
1984
+ const info = must(og.load.video(world, "clips/intro.ogt"));
1985
+ const screen = must(world.spawn({ name: "screen", position: { x: 0, y: 0, z: 0 } }));
1986
+ must(world.insert(screen, og.Sprite, { texture: info.texture, width: 16, height: 9 }));
1987
+ must(world.insert(screen, og.VideoPlayer, { video: info.video, loop: true }));
1988
+ must(world.insert(screen, og.AudioSource, {})); // its sound, kept in step
1989
+ ```
1990
+
1991
+ The `og.video` system plays it by the tick (not the time scale, so the
1992
+ picture stays with its sound): with a 30 Hz tick, tick 90 shows the
1993
+ frame 3 s in, the same in every run and after a restore. Write
1994
+ `VideoPlayer.time` to seek (the sound starts again there on the next
1995
+ tick), `playing` to pause, `speed` for slow motion (the pitch follows).
1996
+ `frame` and `finished` are written for you, and so is `texture`: each
1997
+ player of a video has a texture of its own (the first takes
1998
+ `info.texture`, each further one a texture made for it over the same
1999
+ frames), so two players at different times show different frames. With
2000
+ more than one player, draw each player's `VideoPlayer.texture`. Every field and the loaders are in `reference/video.md`.
2001
+
2002
+ **Videos out.** `og render` plays a command script headless and makes a
2003
+ video of it: a frame every `tick / fps` ticks (the fps divides the tick
2004
+ rate; the error names the nearest that do), read back without the
2005
+ probe's round trip.
2006
+
2007
+ ```sh
2008
+ og render . --commands "0.5:type:s;2:key_down:Space" --fps 30 --to 8 \
2009
+ --size 1280x720 --out og-out/intro.mp4 --audio --contact-sheet 6 --jobs 4
2010
+ ```
2011
+
2012
+ It writes `intro.mp4` (H.264; `.webm` is VP9) with the run's audio,
2013
+ `intro.frames.json` (each frame's tick, time and SHA-256, and the
2014
+ audio's), and `intro.contact-sheet.png`: a grid of frames over the range,
2015
+ each labelled with its time and tick. **Review the contact sheet first**:
2016
+ one image shows whether the scene moves as the script meant, without
2017
+ playing anything. `--out <dir>` (or `--frames png|jpg`) writes the
2018
+ frames too; without ffmpeg the frames are what you get. Two runs of one
2019
+ script give the same SHA-256s, so a frame that changed is a change you
2020
+ made. `--jobs N` splits the range over processes (each starts from a
2021
+ snapshot of the world) and gives the same frames, faster.
2022
+ `--timeline <name>` renders a timeline (`type timeline`) over its
2023
+ duration once timelines exist (stream S2 of docs/depth.md); until then
2024
+ use `--commands`. On a CPU-only machine (lavapipe) a 1280×720 frame of the
2025
+ 3D template takes about 0.4 s in one job, 2.6× less with `--jobs 4`:
2026
+ render at 640×360 (about 0.1 s) to iterate, full size once.
2027
+
2028
+ ## 20. Two players over a room: lockstep
2029
+
2030
+ To play together, every peer runs the whole world and only inputs cross
2031
+ the room (`reference/net.md`; the rooms themselves are og.platform's, taught by
2032
+ the og-platform-sdk skill). Follow three rules:
2033
+
2034
+ 1. In `start`, set `og.Lockstep`: `enabled = true`, `players`,
2035
+ `inputDelay` (2 on a LAN, 4 to 6 over the internet) and `hashEvery`
2036
+ (10). Spawn one entity per player with `og.PlayerInput { player: seat }`.
2037
+ 2. Read `og.Input`, `og.Gamepad` and the UI's touch controls in **one**
2038
+ system, and hand them over with
2039
+ `og.net.submit(world, { x, y, buttons, aimX, aimY })` every tick.
2040
+ 3. Every system that changes the world reads `og.PlayerInput` (its `x`,
2041
+ `y` and `buttons` bits), the world and `og.Rng`, and nothing else: no
2042
+ `Math.random()`, no `Date.now()`, no module variables, no
2043
+ `og.RoomMessage`. Draw per-peer things (whose turn it is, "waiting")
2044
+ with og.ui only, and keep them out of the world.
2045
+
2046
+ ```ts
2047
+ const Submit = defineSystem("Submit", { reads: [og.Input] }, (_q, _t, world) => {
2048
+ const input = must(world.resource(og.Input));
2049
+ must(og.net.submit(world, { x: input.axis("KeyA", "KeyD"), y: input.axis("KeyS", "KeyW"), buttons: input.down("Space") ? 1 : 0 }));
2050
+ });
2051
+ const Move = defineSystem("Move", { reads: [og.PlayerInput], writes: [og.Transform] }, (q, time) => {
2052
+ for (const a of q.archetypes()) {
2053
+ const pin = a.column(og.PlayerInput);
2054
+ const p = a.column(og.Transform).position;
2055
+ for (let i = 0; i < a.count; i++) p[i * 3] += pin.x[i]! * 6 * time.dt;
2056
+ }
2057
+ });
2058
+ ```
2059
+
2060
+ Alone, or with lockstep off, `submit` drives the local player on the next
2061
+ tick, so the same code plays single-player. Natively, run `og relay` and
2062
+ then `og run . --room ws://127.0.0.1:47900/<room>` in two terminals; on
2063
+ the web the shell carries the platform's rooms, and a Play link with
2064
+ `?room=<id>` puts the page in that room at load (the game hears
2065
+ og.RoomJoined as for its own roomJoin; `og.platform.roomInfo(world).link`
2066
+ names the link's room), so a game that plays over a room need not ask for
2067
+ one when the link named it. A playtest plays itself in
2068
+ a room with `room: { players: 2, commands: [...] }`: its runs are `run`
2069
+ and `run.peers`, and each has `netHash` and `lockstep`. Equal `netHash`
2070
+ values at the same tick mean the peers stayed in step. Once lockstep runs
2071
+ a script's steps are on the room's shared ticks (a peer that took the
2072
+ authority's snapshot keeps its steps on theirs), so end each script with
2073
+ `quit` (or give `room.ticks`) and every place stops on one tick; a `then`
2074
+ step relaunches every place at once into a room of its own, its runs in
2075
+ `run.then[n]` and `run.then[n].peers`.
2076
+
2077
+ When the worlds part, `og.Desync { tick, peer }` fires once and
2078
+ `og.net.status(world).desync` holds the tick. Then
2079
+ `--commands "...:dump_desync:d.json"` names the first divergent tick and
2080
+ the entity, component and field that differ, with both values: fix the
2081
+ system that writes that field. The archetype `templates/archetypes/arena`
2082
+ (Coin Duel) is the worked example.
2083
+
2084
+ ## 21. Checking a run: describe, expect, trace, write-back, budgets
2085
+
2086
+ These are the checks to use before opening frames. All of them run from
2087
+ `og run --commands`, a playtest's steps or the live probe (`og probe`).
2088
+
2089
+ - **`describe[:path]`** (probe `describe`) writes the current frame as
2090
+ JSON. `entities` holds every visible entity, nearest first (`order`):
2091
+ `id` (the probe's id), `name`, `archetype`, `labels`, `box` {x, y, width,
2092
+ height} of its visible pixels and `point` (a pixel `pick` answers it at),
2093
+ both in CSS pixels of the play surface, `drawn` (its box drawn alone),
2094
+ `depth`, `coverage` and `occlusion` (0–1, the share of it others hide).
2095
+ `ui` lists the UI elements with `box`, `kind` and `text`, and `camera`
2096
+ the camera. The boxes come from an entity-id pass over the
2097
+ extracted frame on the CPU, so a run without a renderer describes the
2098
+ same frame. `pick` at an entity's `point` answers that entity.
2099
+ - **`expect:<op>`** asserts at its tick. A failure is logged with the tick
2100
+ and the observed value, `og run --json` lists it under `expectations`,
2101
+ and the run exits 1:
2102
+
2103
+ ```text
2104
+ expect:entity:crate:within:380,360,280,240 its described box inside the screen box
2105
+ expect:entity:crate:within:-3,0.4,-1,1,0.6,2 its position inside the world box (x0,y0,z0,x1,y1,z1)
2106
+ expect:event:og.CollisionStarted[:2] at least one (or exactly 2) since the script began
2107
+ expect:ui:text:=Started a UI element shows the text (= exact, else a substring)
2108
+ expect:region:560,16,64,24:color:#9da5b1:24 the region's mean colour within 24 per channel
2109
+ expect:frame:references/start.png:rmse:<=6 the frame against a reference PNG
2110
+ expect:state:score>=3 a fact of GameDesc.facts by dotted path (== != < <= > >=)
2111
+ ```
2112
+
2113
+ A playtest may be expect steps alone: `templates/game/tests/smoke.og.ts`
2114
+ is. Each expectation counts as a check. Without a renderer a frame or
2115
+ region expectation is unchecked, never failed. Take screen boxes from a
2116
+ `describe` of a good run, with a margin.
2117
+ - **`og run --trace out.json`** writes the run in the Chrome trace format;
2118
+ open it in Perfetto (ui.perfetto.dev). Every span lies where it ran.
2119
+ The engine thread holds each tick with its systems, render each frame
2120
+ with its render passes (CPU encoding), gpu the same passes as the GPU
2121
+ ran them (where `og.render.capabilities(world).gpuTimestamps`), load
2122
+ each cooked file read, cook the cook and each output on the cook thread
2123
+ that made it, and `lane N` the jobs og-lanes ran on pool thread N
2124
+ (physics, cull, anim, extract). The probe's `state().timings.systems`,
2125
+ `.passes` and `.gpuPasses` give the last tick's and frame's numbers
2126
+ live.
2127
+ - **`save_scene[:path][:spawned]`** (probe `saveScene(write, spawned)`)
2128
+ writes the scenes back after `teleport`, `set` or the Tune panel moved
2129
+ things: the boot scene and every scene `world.instantiate` made, each
2130
+ to its own `.ogt`. Only the lines whose values changed are rewritten;
2131
+ comments and order stay, and a tuned field the text lacks gets a line.
2132
+ Resources are scene text (`resource Run { chimed true }` at the top
2133
+ level of a scene); a tuned resource no scene sets gets a block in the
2134
+ boot scene. `spawned` also writes the entities your code spawned under
2135
+ `# ── spawned at run time ──` at the end of the boot scene; if you keep
2136
+ them, guard their spawn (`if (world.find("bolt") === null)`) so the next
2137
+ run does not make them twice. With a path, the texts go under `--out`
2138
+ to diff first; without one, they replace `assets/<scene>.ogt`. `set`
2139
+ names engine components by their registered name
2140
+ (`set:marker.og.Transform.scale=0.5,0.5,0.5`).
2141
+ - **`og cook --report`** writes `og-out/cook-report.json`: per asset its
2142
+ triangles per LOD, vertices, textures (size, format), bones, clips,
2143
+ bytes cooked and flags, and for every other asset its figures under its
2144
+ kind (`sound` seconds/channels/sampleRate/chunks, `font`
2145
+ glyphs/kerningPairs/pages, `scene` entities/instances/components,
2146
+ `navmesh` tiles/polygons/vertices, `timeline` tracks/seconds, `strings`
2147
+ locale/keys, `style`, `behaviour`, `tileset`, `tilemap`, `rig`,
2148
+ `rig2d`, `flipbook`, `shader`, `post`, `emitter`, `look`, `palette`,
2149
+ `lut`, `environment`, `video`, `terrain`, `streamPack`). **`og doctor
2150
+ --project . [--json]`** checks it against `budgets { }` in project.ogt
2151
+ and exits 1 on an over-budget asset, naming the asset, the limit, the
2152
+ value and the fix. The keys are `triangles` 100000, `vertices` 65536,
2153
+ `textureSize` 2048, `bones` 128, `clips` 64, `assetBytes` 8 MiB,
2154
+ `totalBytes` 64 MiB, `soundSeconds` 600, `videoSeconds` 300,
2155
+ `fontGlyphs` 8192, `navPolygons` 65536, `sceneEntities` 10000 and
2156
+ `emitterParticles` 16384, each a default a project may move.
2157
+
2158
+ ## 22. Levels of detail, reflection probes and screen-space reflections
2159
+
2160
+ **Levels of detail.** Every model the cook reads is simplified into
2161
+ levels: by default `lods 1 0.5 0.25`, each level a fraction of the full
2162
+ mesh's triangles. A glTF's own `MSFT_lod` levels are kept instead. Import
2163
+ settings change them per model, and `lods 1` keeps only the full mesh:
2164
+
2165
+ ```
2166
+ type import
2167
+ version 1
2168
+ ---
2169
+ model {
2170
+ lods 1 0.4 0.15
2171
+ lodError 0.08
2172
+ }
2173
+ ```
2174
+
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.
2184
+
2185
+ **Reflection probes.** Smooth surfaces reflect the environment map, which
2186
+ is the sky. Indoors that is wrong: a chrome sphere in a room would show
2187
+ the sky. Put an `og.ReflectionProbe` in the room, and lit surfaces inside
2188
+ its box reflect a cube captured from its position instead, box-projected
2189
+ so the walls reflect where they are:
2190
+
2191
+ ```ts
2192
+ const probe = must(world.spawn({ name: "hall-probe", position: { x: 0, y: 1.6, z: 0 } }));
2193
+ world.insert(probe, og.ReflectionProbe, {
2194
+ size: { x: 10, y: 4.2, z: 10 }, // the room's extents, centred on the entity
2195
+ resolution: 128, // texels per cube face
2196
+ mode: "baked", // or "realtime", every `interval` ticks
2197
+ blendDistance: 0.5, // fade into the environment at the box's faces
2198
+ });
2199
+ ```
2200
+
2201
+ - Choose the probe's mode by what moves in the room:
2202
+ - A `baked` probe is captured once, on its first frame. For a static
2203
+ room, run `og bake probes [project] --commands "0:station:hall"` (the
2204
+ commands reach the scene). It writes the captures into
2205
+ `cooked/probes/`, and runs then load them instead of drawing them.
2206
+ A bake is keyed by the probe's place and size and by its room's
2207
+ content: moving the probe or changing what is in its box (a mesh,
2208
+ a material, a texture, a light) needs a new bake, and until then it
2209
+ draws its own capture. `og doctor --project` lists stale bakes
2210
+ (`stale-probe`).
2211
+ - A `realtime` probe redraws six faces every `interval` ticks. Keep
2212
+ the interval high.
2213
+ - Size the box to the room. Each object takes the smallest box holding
2214
+ 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
2217
+ by it too. At most 8 probes (`MAX_REFLECTION_PROBES`) are used,
2218
+ nearest the camera first.
2219
+ - `dump_render` lists the frame's probes and whether each was loaded from
2220
+ the cache. In a playtest, assert what the sphere shows with
2221
+ `expect:region:x,y,w,h:color:#rrggbb:tol`, and a model's level of
2222
+ detail and the frame's triangles with `expect:entity:<name>:render.lod==1`
2223
+ and `expect:timings:triangles<=1500000`.
2224
+
2225
+ **Screen-space reflections.** On the full tier, `PostEffects.ssr` adds the
2226
+ frame's own reflections to smooth surfaces: a wet street, a polished
2227
+ floor. They show what is on screen, including moving things:
2228
+
2229
+ ```ts
2230
+ const post = must(world.resource(og.PostEffects));
2231
+ post.ssr = true;
2232
+ post.ssrMaxDistance = 12; // metres a ray travels
2233
+ post.ssrSteps = 48; // more for long, thin reflections
2234
+ post.ssrThickness = 0.3; // how far behind a surface a ray still hits it
2235
+ post.ssrFade = 0.15; // fade at the screen's edges
2236
+ ```
2237
+
2238
+ SSR is weighted by roughness as the surface is shaded, its roughness map
2239
+ and a terrain layer's roughness included (`roughness` 0.05 reflects
2240
+ sharply; above 0.75 it reflects nothing), and by Fresnel. What it misses (off screen,
2241
+ behind objects) falls back to the probes and the environment, so use SSR
2242
+ together with a probe. WebGL2 draws no SSR: check
2243
+ `og.render.capabilities(world).ssr`, and on that tier the probe is the
2244
+ whole reflection.
2245
+
2246
+ ## 23. Render targets, second cameras, layers, custom shaders and post passes
2247
+
2248
+ **Render to a texture.** `og.render.renderTarget(world, { width, height,
2249
+ format, depth })` makes a target, as three.js `WebGLRenderTarget`. It
2250
+ answers `{ target, texture, depthTexture, width, height }`. `texture` is
2251
+ an ordinary Texture: a material's `map` or `emissiveMap`, an og.Sprite, an
2252
+ `og.render.sprite` or an og.ui image shows it. `format` is `ldr` (the
2253
+ default) or `hdr`:
2254
+ - `ldr`: 8-bit and finished (exposed with the view's `exposure`, tone
2255
+ mapped). Use it for a monitor, a minimap or a portrait, shown through an
2256
+ unlit material's `map`.
2257
+ - `hdr`: linear radiance, exposed once with the frame that shows it. Use
2258
+ it for a mirror or a portal, shown as `emissiveMap` on a material whose
2259
+ `color` is black.
2260
+
2261
+ `depth: true` also keeps the view's linear depth, from near (0) to far
2262
+ (1), of its opaque meshes, on every tier, WebGL2 included. It costs a
2263
+ second draw of the view's opaque meshes, so ask for it only when a
2264
+ material reads it.
2265
+ `RenderCapabilities.maxRenderTargetSize` is the largest side.
2266
+
2267
+ **A second camera.** `og.CameraView` on an entity is a camera with
2268
+ og.Camera's fields (`fov`, `near`, `far`, `zoom`, `orthographic`,
2269
+ `height`, `exposure`). It looks down the entity's local -Z:
2270
+ ```ts
2271
+ const screen = must(og.render.renderTarget(world, { width: 480, height: 270 }));
2272
+ world.insert(monitorMesh, og.Mesh, { geometry: plane, material: must(og.render.standardMaterial(world, { map: screen.texture, unlit: true })) });
2273
+ const cam = must(world.spawn({ name: "cctv", position: eye, quaternion: og.transform.lookAt(eye, target) }));
2274
+ world.insert(cam, og.CameraView, { target: screen.target, fov: 60, every: 2 });
2275
+ // Split-screen: no target, a rect of the frame (fractions from the top left).
2276
+ world.insert(p2, og.CameraView, { viewportX: 0.5, viewportWidth: 0.5 });
2277
+ ```
2278
+ - `every: n` draws it on every n-th tick, and the target keeps its last
2279
+ image in between.
2280
+ - `order` sorts the views. Every view draws before the frame, so a
2281
+ material shows this frame's image.
2282
+ - `clear` with `clearColor` replaces the sky or background colour.
2283
+ - `active: false` stops it.
2284
+
2285
+ A view draws what the main camera draws, from its own eye and on its own
2286
+ `layers`: meshes and models (skinned meshes and custom shaders included),
2287
+ 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
2289
+ shadowed by the frame's lights. Each view is a second draw of everything
2290
+ its layers admit, so keep it cheap with `layers` and `every`.
2291
+ - An `ldr` target and a viewport are exposed with the view's `exposure`
2292
+ and tone mapped with the frame's curve. An `hdr` target holds linear
2293
+ radiance and is exposed by the frame that shows it.
2294
+ - 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.
2298
+
2299
+ A viewport view draws over the finished image and under the UI.
2300
+
2301
+ **A mirror.** Put `og.Mirror` on the glass's entity. It reflects the main
2302
+ camera in the entity's xy plane (its +Z faces the room) and fits the image
2303
+ to a `width` × `height` rect:
2304
+ ```ts
2305
+ const rt = must(og.render.renderTarget(world, { width: 512, height: 384, format: "hdr" }));
2306
+ world.insert(glass, og.Mesh, { geometry: must(og.render.planeGeometry(world, { width: 4, height: 2.4 })),
2307
+ material: mat({ color: { r: 0, g: 0, b: 0 }, emissive: { r: 1, g: 1, b: 1 }, emissiveMap: rt.texture }) });
2308
+ world.insert(glass, og.Mirror, { target: rt.target, width: 4, height: 2.4 });
2309
+ ```
2310
+
2311
+ **Layers.** `layers` on og.Mesh, og.Model, og.Sprite, og.ParticleSystem,
2312
+ og.Lines, og.Points and og.Camera (and on og.CameraView and og.Mirror) is a
2313
+ three.js bit mask:
2314
+ bit n is layer n, and the default 1 is layer 0. A camera draws an object
2315
+ when `object.layers & camera.layers` is not 0. `og.render.raycast`'s
2316
+ `layers` uses the same test. Some uses:
2317
+ - A first-person body on layer 1 (mask 2) is hidden from the player's
2318
+ camera (mask 1) and shown on a third-person monitor (mask 3).
2319
+ - A minimap marker only the minimap camera draws.
2320
+
2321
+ Shadow maps take every caster whatever its layers, so a hidden object
2322
+ still casts a shadow. Give it `castShadow: false` if it should not.
2323
+
2324
+ The probe's `describe` lists what the main camera shows, and each
2325
+ og.CameraView and og.Mirror under `views` with what its own image shows,
2326
+ on its layers. An entity on a layer only a view sees is found under that
2327
+ view, not in `entities`. A main-camera entity names the views that also
2328
+ show it in its `views`. `pick` and `expect:entity:...:within` read the
2329
+ main camera.
2330
+
2331
+ **Full custom shaders.** `mode full lit` (or `mode full unlit`) in a `type
2332
+ shader` asset replaces the lit shader's stages with your own entries:
2333
+ ```wgsl
2334
+ fn vertex(v: VertexInput, model: mat4x4<f32>, p: Params) -> VertexOutput // v: position normal uv color tangent (object space; skin and morphs applied)
2335
+ fn fragment(f: FragmentInput, p: Params) -> SurfaceOutput // f: world local normal uv color data view pixel time
2336
+ // VertexOutput: world (projected by the engine), normal, uv, color, data (free, interpolated)
2337
+ // SurfaceOutput: color (albedo + alpha; unlit: the colour), normal, metallic, roughness, emissive
2338
+ // camera(): view_proj, position, forward, time. tex0(uv)..tex7(uv): the material's map, metalnessMap,
2339
+ // roughnessMap, normalMap, emissiveMap, aoMap, gradientMap, dissolveMap. Helpers: standard_vertex(v, model),
2340
+ // standard_surface(f), noise3(p).
2341
+ ```
2342
+ 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
2345
+ as returned.
2346
+ - The depth prepass, every shadow pass and every view run the same
2347
+ entries, and alpha under the material's `alphaTest` is cut in each of
2348
+ them. A dissolve's shadow therefore has its holes.
2349
+ - Make the material with `og.render.shaderMaterial` and set parameters with
2350
+ `og.render.setShaderParam`, as for hooks (§12).
2351
+ - `templates/fx-showcase/assets/shaders/dissolve-full.*` is the example.
2352
+
2353
+ **Custom post passes.** A `type post` asset declares
2354
+ `fn post(f: PostInput, p: Params) -> vec4<f32>` in its `code` or `source`,
2355
+ with up to eight `param` lines. The function reads:
2356
+ - `previous(uv)`: the pass before it, linear HDR;
2357
+ - `scene_color(uv)`: the lit image;
2358
+ - `depth(uv)`: view depth in metres;
2359
+ - `normal(uv)`: the world normal;
2360
+ - `f.uv`, `f.pixel`, `f.size` and `f.time`.
2361
+
2362
+ It returns linear HDR. To use one:
2363
+ 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.
2369
+ 3. Set its parameters with `og.render.setPostParam(world, post, "curve",
2370
+ [0.1])`.
2371
+
2372
+ The cook compiles it for every back end. A pass that a device still fails
2373
+ to compile is skipped and listed by `og.render.postErrors(world)`.
2374
+ `templates/fx-showcase/assets/post/crt.*` is a CRT example.
2375
+
2376
+ ## 24. Baked lighting, volumetric fog, area lights and cookies
2377
+
2378
+ **Baked lighting.** Use it for light that bounces around a static space,
2379
+ such as a red wall tinting the floor beside it or a room lit through its
2380
+ door. Mark the scene's unmoving models `static true` and put an
2381
+ `og.LightVolume` box over the space where dynamic objects move:
2382
+
2383
+ ```
2384
+ entity "hall" {
2385
+ model "models/hall.glb"
2386
+ static true
2387
+ Transform { position 0 0 0 quaternion 0 0 0 1 scale 1 1 1 }
2388
+ }
2389
+ 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 }
2392
+ }
2393
+ ```
2394
+
2395
+ Then run `og bake lightmaps` in the project. Its options are
2396
+ `--samples 256`, `--bounces 3`, `--texels-per-metre 16`, `--max-size 1024`,
2397
+ `--sky r,g,b`, `--seconds S` and `--scene <id>`; the fx-showcase Cornell
2398
+ box takes 1.4 s at 512 samples. The bake is a CPU path tracer and needs
2399
+ no GPU. It writes `<scene>.lightmap.ogt` and `.ktx2` beside the scene;
2400
+ commit both.
2401
+
2402
+ A static entity needs a name. The bake holds the light that bounces, the
2403
+ sky and emissive surfaces; lights still shine and cast shadows live on
2404
+ top. Static models draw with their lightmap, and everything else inside a
2405
+ LightVolume takes its ambient light from the probes. After you change the
2406
+ scene's text or a static model's geometry, the bake is stale and the scene
2407
+ renders unbaked until you bake again; a re-cook that keeps the geometry
2408
+ (a cooker update, a texture or raycast option) keeps the bake. `og doctor
2409
+ --project` warns `unbaked`, and
2410
+ `og.render.bakedLighting(world)` lists each scene with `baked` and a
2411
+ `reason`. `RenderSettings.bakedLighting = false` draws everything unbaked,
2412
+ which is useful to compare. Every instantiated scene draws its own bake,
2413
+ so baked scenes loaded together or streamed in side by side all keep
2414
+ their light.
2415
+
2416
+ **Volumetric fog.** Set `Fog.volumetric = true` to light the fog as a
2417
+ medium, which gives shafts through windows and the cones of spots. Tune it
2418
+ with `density`, `heightDensity` and `falloff` (thicker low down),
2419
+ `scattering` (how bright) and `anisotropy` (towards 1, bright looking into
2420
+ the light), up to `volumetricDistance`. Fog of 0.03 to 0.1 per metre reads
2421
+ well indoors. With `og.DayNight`, the shafts move with the sun.
2422
+ `RenderCapabilities.volumetricFog` is `froxel` on the full tier or
2423
+ `raymarch` on WebGL2. On both, the sun, spots, points and rect lights light
2424
+ the fog, so a lantern glows in it on WebGL2 too.
2425
+ `RenderCapabilities.volumetricLights` is how many local lights light one
2426
+ sample (64 for the froxel grid, 8 for the raymarch, brightest first).
2427
+
2428
+ **Area lights.** `og.RectLight { width, height, color, intensity,
2429
+ twoSided }` emits from a rectangle facing the entity's -Z. Aim it with
2430
+ `og.transform.lookAt`, as you would a spot. A glossy floor shows the
2431
+ rectangle's shape in its highlight. Pair one with an emissive slab for
2432
+ the fixture. Set `castShadow` for a soft shadow: sharp where a caster
2433
+ touches the floor, widening with the panel's size away from it. It takes
2434
+ one shadow layer (two when `twoSided`) from the same budget as shadowed
2435
+ spots and points, and is drawn from the panel's centre, so a caster right
2436
+ against a large panel is shadowed as from its middle.
2437
+
2438
+ **Cookies.** `SpotLight.cookie` is a texture projected along the cone,
2439
+ like a window's lattice or stained glass. `PointLight.cookie` is six
2440
+ square faces stacked top to bottom (+X, -X, +Y, -Y, +Z, -Z), like a
2441
+ pierced lantern. A cookie multiplies the light and its colour. A frame
2442
+ draws at most `og.render.capabilities(world).maxLightCookies` (24) cookie
2443
+ layers: a spot cookie takes one, a point cookie six, and a texture shared
2444
+ by lights of one kind counts once. Past that, the later lights shine
2445
+ without their cookies and the log warns, naming them; share cookie
2446
+ textures between lanterns to stay under it.
2447
+
2448
+ ## 25. Where to look things up
1580
2449
 
1581
2450
  `reference/index.md` (the generated wiki shipped with this skill) maps every
1582
2451
  module (grouped by skill, as in §1), type and function to its page, with the IDL text, defaults, ranges