@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.
- package/bin/og.mjs +1 -1
- package/dist/web/origin-engine.d.ts +255 -5
- package/dist/web/origin-engine.js +1584 -125
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +80 -3
- package/dist/web-publish/origin-engine-lanes.js +1288 -82
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +199 -6
- package/dist/web-publish/origin-engine.js +1285 -79
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +65 -4
- package/dist/web-publish-2d/origin-engine-lanes.js +1285 -79
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +198 -5
- package/dist/web-publish-2d/origin-engine.js +1282 -76
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +64 -3
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/looks/catalogue.json +40 -40
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/distribution.json +14 -0
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/README.md +2 -1
- package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
- package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
- package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
- package/knowledge/scaffold/template/host.d.ts +46 -0
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scaffold/template/src/main.ts +1 -1
- package/knowledge/scaffold/template/tests/frames.ts +134 -0
- package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
- package/knowledge/scaffold/template/tsconfig.json +1 -1
- package/knowledge/scripts/mount.ts +2 -1
- package/knowledge/scripts/validate.ts +159 -31
- package/knowledge/skills/og-audio/SKILL.md +2 -2
- package/knowledge/skills/og-build/SKILL.md +29 -7
- package/knowledge/skills/og-core/SKILL.md +31 -5
- package/knowledge/skills/og-feel/SKILL.md +2 -2
- package/knowledge/skills/og-game/SKILL.md +1331 -16
- package/knowledge/skills/og-i18n/SKILL.md +7 -7
- package/knowledge/skills/og-model/SKILL.md +374 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +2 -1
- package/knowledge/skills/og-taste/SKILL.md +82 -73
- package/knowledge/skills/og-ui/SKILL.md +80 -58
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +392 -41
- package/knowledge/wiki/atmosphere.md +36 -13
- package/knowledge/wiki/audio.md +18 -18
- package/knowledge/wiki/camera.md +19 -13
- package/knowledge/wiki/commands.md +19 -13
- package/knowledge/wiki/debug.md +414 -0
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +317 -8
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +238 -70
- package/knowledge/wiki/math.md +2607 -0
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +340 -0
- package/knowledge/wiki/path.md +402 -0
- package/knowledge/wiki/physics.md +95 -95
- package/knowledge/wiki/platform.md +463 -29
- package/knowledge/wiki/probe.md +681 -67
- package/knowledge/wiki/reflect.md +29 -17
- package/knowledge/wiki/render.md +4993 -1998
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +727 -0
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +24 -20
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +396 -0
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +560 -654
- package/knowledge/wiki/video.md +157 -0
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -3
- package/schema/api.schema.json +28151 -19640
- package/shell/page.ts +26 -2
- package/shell/platform.ts +51 -0
- package/shell/player.ts +15 -2
- package/shell/transport.ts +6 -0
- package/src/depth.ts +71 -0
- package/src/e6.ts +32 -1
- package/src/env.d.ts +5 -1
- package/src/generated/abi.ts +226 -4
- package/src/geom.ts +260 -0
- package/src/index.ts +32 -4
- package/src/light.ts +18 -0
- package/src/math.ts +622 -0
- package/src/node/index.ts +17 -0
- package/src/node/playtest-run.ts +115 -18
- package/src/node/polish.ts +13 -3
- package/src/node/render.ts +291 -0
- package/src/node/run.ts +86 -5
- package/src/p2.ts +19 -1
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +65 -4
- package/src/runtime.ts +100 -9
- package/src/targets.ts +28 -0
- package/src/trace.ts +169 -0
- package/src/ui-kit/huds.ts +34 -34
- package/src/ui-kit/menus.ts +12 -12
- package/src/ui-kit/settings.ts +11 -2
- package/src/ui-kit/store.ts +18 -18
- package/src/ui-kit/story.ts +6 -4
- package/src/ui-kit/util.ts +8 -8
- package/src/web/shell.ts +59 -1
- package/src/web/worker.ts +105 -4
- package/types/bindings.json +9368 -6619
- 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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
1098
|
-
|
|
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 {
|
|
1110
|
-
|
|
1111
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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`, `
|
|
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.
|
|
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.
|