@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.
- package/bin/og.mjs +1 -1
- package/dist/web/origin-engine.d.ts +219 -5
- package/dist/web/origin-engine.js +1344 -125
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +68 -3
- package/dist/web-publish/origin-engine-lanes.js +1007 -22
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +162 -5
- package/dist/web-publish/origin-engine.js +1004 -19
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +52 -3
- package/dist/web-publish-2d/origin-engine-lanes.js +1007 -22
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +162 -5
- package/dist/web-publish-2d/origin-engine.js +1004 -19
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +52 -3
- package/knowledge/looks/catalogue.json +40 -40
- package/knowledge/package.json +1 -1
- 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 +19 -10
- package/knowledge/skills/og-build/SKILL.md +8 -5
- package/knowledge/skills/og-core/SKILL.md +11 -0
- package/knowledge/skills/og-game/SKILL.md +877 -8
- package/knowledge/skills/og-platform-sdk/SKILL.md +1 -0
- package/knowledge/skills/og-taste/SKILL.md +82 -73
- package/knowledge/skills/og-ui/SKILL.md +65 -50
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +384 -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 +262 -7
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +236 -68
- 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 +15 -15
- package/knowledge/wiki/render.md +3893 -1896
- package/knowledge/wiki/rng.md +14 -14
- 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 +19 -19
- 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 +559 -629
- package/knowledge/wiki/video.md +157 -0
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -3
- package/schema/api.schema.json +21707 -14725
- package/shell/page.ts +26 -2
- package/shell/platform.ts +46 -0
- package/shell/player.ts +7 -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 +189 -3
- package/src/index.ts +25 -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 +114 -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/playtest.ts +57 -4
- package/src/runtime.ts +13 -3
- 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 +2 -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 +58 -0
- package/src/web/worker.ts +103 -2
- package/types/bindings.json +9101 -6647
- 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,
|
|
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,
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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`, `
|
|
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.
|
|
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
|