feat(ludic.lab): the visual lab as a package - a scene on a plate, never a world
A scene is an entry in the open registry LabScenes, shown on a plate: a flat lit disc, the package's own small sky with the sun at one hour, and a frame clock. Headless each camera (lab_shot, lab_shot_at) settles and is written as build/lab/<scene>/<shot>.png (a stored PNG, written here); in a window N and P step through them. tools/lab/run.sh and sheet.py make the contact sheet; tools/lab/plate_build.py makes plate/. ludic.render3d gains r3d_plate_mode (no terrain, grass or water is made) and r3d_sky_path. quit() in a program with no frame loop links. The example takes ~120 MB resident, 417 MiB peak footprint. Reseed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cc930a114f
commit
b401f7acc1
35 changed files with 4184 additions and 2986 deletions
9
changes/ludic-lab.md
Normal file
9
changes/ludic-lab.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**ludic.lab - the visual lab as a package.** A scene is an entry in the open registry `LabScenes`,
|
||||||
|
shown on a plate: a flat lit disc, a small sky of the package's own with the sun at one hour, and a
|
||||||
|
frame clock. Headless, each of its cameras (`lab_shot`, `lab_shot_at`) settles and is written as
|
||||||
|
`build/lab/<scene>/<shot>.png`; in a window N and P step through them. `tools/lab/run.sh` and
|
||||||
|
`sheet.py` make a contact sheet. ludic.render3d gains `r3d_plate_mode(true)` - no terrain, grass or
|
||||||
|
water is made - and `r3d_sky_path`; the example scene takes about 120 MB resident. `quit()` in a
|
||||||
|
program with no frame loop no longer fails to link.
|
||||||
|
|
@ -37,6 +37,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [ludic.render3d](ludic.render3d/) | the physically based 3D renderer over OpenGL and Vulkan (named `ludic_render3d` under `uses`) |
|
| [ludic.render3d](ludic.render3d/) | the physically based 3D renderer over OpenGL and Vulkan (named `ludic_render3d` under `uses`) |
|
||||||
| [ludic.ui](ludic.ui/) | components: templates, stylesheets, layout, input and focus (`module ludic_ui`) |
|
| [ludic.ui](ludic.ui/) | components: templates, stylesheets, layout, input and focus (`module ludic_ui`) |
|
||||||
|
| [ludic.lab](ludic.lab/README.md) | the visual lab: a scene on a lit plate, fixed cameras, PNG shots and a contact sheet |
|
||||||
| [ludic.core](ludic.core/) | the canonical engine-ABI components |
|
| [ludic.core](ludic.core/) | the canonical engine-ABI components |
|
||||||
| [ludic.prefs](ludic.prefs/) | a small `key=value` store for preferences and records |
|
| [ludic.prefs](ludic.prefs/) | a small `key=value` store for preferences and records |
|
||||||
|
|
||||||
|
|
|
||||||
73
packages/ludic.lab/README.md
Normal file
73
packages/ludic.lab/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# ludic.lab
|
||||||
|
|
||||||
|
The visual lab: one model or one mechanic on a **plate**, photographed from fixed cameras. A lab
|
||||||
|
scene is a minimal, isolated, reproducible piece of a game - never the whole world. The plate is a
|
||||||
|
flat, lit disc 40 m across with a line every metre, a small sky of its own with the sun 40 degrees
|
||||||
|
up (one fixed hour), and a clock of frames: 60 a second, whatever the machine. Nothing else is
|
||||||
|
loaded - no terrain, no grass, no water (`r3d_plate_mode` in ludic.render3d) - so a scene starts in
|
||||||
|
about a second and takes about 120 MB resident (420 MB peak footprint with the GPU driver's share)
|
||||||
|
where a lab on a whole valley took 7.8 GB.
|
||||||
|
|
||||||
|
`module ludic_lab uses ludic_render3d, ludic_base`.
|
||||||
|
|
||||||
|
## A lab program
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
import "ludic.lab"
|
||||||
|
program Lab {
|
||||||
|
numbers float
|
||||||
|
function probe_setup() -> void {
|
||||||
|
lab_model(lab_plate_dir(), "probe.gltf", "probe", 0.0, 0.0, 0.5)
|
||||||
|
lab_shot_at("front", 0.0, 0.7, 0.0, 4.0, 0.0, 12.0)
|
||||||
|
lab_shot_at("above", 0.0, 0.5, 0.0, 7.0, 30.0, 55.0)
|
||||||
|
}
|
||||||
|
def LabScenes probe { setup: fn probe_setup }
|
||||||
|
handler Boot phase Start { lab_boot() }
|
||||||
|
handler Look phase Input { lab_input() }
|
||||||
|
handler Draw phase Render { lab_render() }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`example/plate.ludic` is this program. Scenes are an open registry, so a game - or a package
|
||||||
|
showing its own models - adds them from its own module with `def LabScenes <name> { ... }`:
|
||||||
|
|
||||||
|
| field | |
|
||||||
|
| --- | --- |
|
||||||
|
| `setup: fn()` | build what the scene shows and name its cameras |
|
||||||
|
| `tick: fn(float)` | move it, by the lab's fixed step (1/60 s) |
|
||||||
|
| `draw: fn()` | draw what is not an actor or a scatter layer (actors and layers are drawn already) |
|
||||||
|
|
||||||
|
`R3D_SCENE=<name>` picks the scene (the first one added otherwise); an unknown name lists them.
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
- **Headless** (`ludic build ... --headless`): every shot is held `lab_settle` frames (45), written
|
||||||
|
to `build/lab/<scene>/<shot>.png`, and after the last the program quits. The headless loop reads a
|
||||||
|
line of stdin a frame: `yes '' | head -n 20000 | R3D_SCENE=probe ./lab`.
|
||||||
|
- **In a window**: N and P step through the shots.
|
||||||
|
- `tools/lab/run.sh <lab.ludic> <scene> ...` (in the toolchain's checkout) builds the program, runs
|
||||||
|
each scene and makes `build/lab/<scene>/sheet.png`, a contact sheet (`tools/lab/sheet.py`).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| `lab_boot()`, `lab_input()`, `lab_render()` | the program's Start, Input and Render handlers |
|
||||||
|
| `lab_shot(name, x, y, z, yaw_deg, pitch_deg)` | a camera at a point, looking along yaw and pitch (degrees) |
|
||||||
|
| `lab_shot_at(name, tx, ty, tz, dist, from_deg, up_deg)` | a camera `dist` metres from a point, from a bearing (0 = from +z), `up_deg` above it, looking at it |
|
||||||
|
| `lab_shot_count()` | the cameras named so far |
|
||||||
|
| `lab_model(dir, file, node, x, z, yaw) -> Actor`, `lab_stand(a, x, z, yaw)`, `lab_ground_y(x, z)` | a glTF node stood on the plate; the plate is flat at y = 0 |
|
||||||
|
| `lab_plate_dir()` | the package's `plate/`: the disc, its sky and the probe model |
|
||||||
|
| `lab_settle(frames)`, `lab_font(dir)`, `lab_title(s)`, `lab_note(s)` | how long a shot settles; the overlay font for captions (none by default); the window's title; a line under the caption |
|
||||||
|
| `lab_scene()`, `lab_frame()`, `lab_time()` | the scene, and the lab's clock |
|
||||||
|
| `lab_ppm_to_png(ppm, png)` | a frame as a PNG (stored, not compressed) |
|
||||||
|
|
||||||
|
## The plate's files
|
||||||
|
|
||||||
|
`plate/` is built by `tools/lab/plate_build.py` in the toolchain's checkout and is deterministic:
|
||||||
|
`plate.gltf` (the disc), `probe.gltf` (a box with a ball on it, for looking at the light), and
|
||||||
|
`sky.hdr` (512 x 256, the sun at 40 degrees). Change the script and run it again; never edit them.
|
||||||
|
|
||||||
|
## Not here yet
|
||||||
|
|
||||||
|
- A heightfield patch for a scene that needs a slope: the plate is flat.
|
||||||
56
packages/ludic.lab/boot.ludic
Normal file
56
packages/ludic.lab/boot.ludic
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# boot.ludic - the window, the renderer on a plate (no terrain, no grass, no water), the plate and
|
||||||
|
# its sky, then the scene named by R3D_SCENE (else the first one added)
|
||||||
|
function lab_draw() -> void {
|
||||||
|
actor_draw()
|
||||||
|
scatter_draw()
|
||||||
|
if lb_draw != null { lb_draw() }
|
||||||
|
}
|
||||||
|
function lab_casters(light_vp: floats) -> void {
|
||||||
|
actor_draw_casters(light_vp)
|
||||||
|
scatter_draw_casters(light_vp)
|
||||||
|
}
|
||||||
|
function lab_pick_scene() -> int {
|
||||||
|
if LAB_COUNT == 0 { return -1 }
|
||||||
|
lb_scene = LabScenes[0].key
|
||||||
|
if Os.has_env("R3D_SCENE") { lb_scene = Os.env("R3D_SCENE") }
|
||||||
|
return labscenes_find(lb_scene)
|
||||||
|
}
|
||||||
|
# once, from the program's Start handler
|
||||||
|
export function lab_boot() -> void {
|
||||||
|
lb_windowed = is_windowed()
|
||||||
|
let k = lab_pick_scene()
|
||||||
|
if k < 0 {
|
||||||
|
print(`lab: no scene called '{lb_scene}' - {lab_scene_names()}`)
|
||||||
|
quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var ww = 1920
|
||||||
|
var wh = 1080
|
||||||
|
if lb_windowed {
|
||||||
|
ww = 960
|
||||||
|
wh = 540
|
||||||
|
}
|
||||||
|
r3d_plate_mode(true)
|
||||||
|
r3d_sky_path = `{lab_plate_dir()}/sky.hdr`
|
||||||
|
if not r3d_init(ww, wh, lb_title) {
|
||||||
|
quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lb_font != "" { overlay_init(lb_font) }
|
||||||
|
r3d_on_draw(fn lab_draw)
|
||||||
|
r3d_on_casters(fn lab_casters)
|
||||||
|
lab_plate_load()
|
||||||
|
lab_shots_reset()
|
||||||
|
lb_out = `build/lab/{lb_scene}`
|
||||||
|
let sc = LabScenes[k]
|
||||||
|
lb_draw = sc.draw
|
||||||
|
if sc.setup != null { sc.setup() }
|
||||||
|
if lab_shot_count() == 0 { lab_shot_at("plate", 0.0, 1.0, 0.0, 8.0, 0.0, 15.0) }
|
||||||
|
lab_shot_apply(0)
|
||||||
|
if not lb_windowed {
|
||||||
|
Fs.mkdir("build")
|
||||||
|
Fs.mkdir("build/lab")
|
||||||
|
Fs.mkdir(lb_out)
|
||||||
|
}
|
||||||
|
print(`lab: scene {lb_scene}, {lab_shot_count()} shots`)
|
||||||
|
}
|
||||||
20
packages/ludic.lab/example/plate.ludic
Normal file
20
packages/ludic.lab/example/plate.ludic
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# plate.ludic - the lab's example: one model on the plate, photographed from three cameras.
|
||||||
|
# ludic build packages/ludic.lab/example/plate.ludic --headless -o build/lab_plate
|
||||||
|
# yes '' | head -300 | ./build/lab_plate -> build/lab/probe/{front,side,above}.png
|
||||||
|
# The model is the package's own probe (a box with a ball on it), so the example needs nothing
|
||||||
|
# from any game.
|
||||||
|
import "ludic.lab"
|
||||||
|
program LabPlate {
|
||||||
|
numbers float
|
||||||
|
function probe_setup() -> void {
|
||||||
|
lab_model(lab_plate_dir(), "probe.gltf", "probe", 0.0, 0.0, 0.5)
|
||||||
|
lab_note("a box and a ball, in the plate's light")
|
||||||
|
lab_shot_at("front", 0.0, 0.7, 0.0, 4.0, 0.0, 12.0)
|
||||||
|
lab_shot_at("side", 0.0, 0.7, 0.0, 4.0, 90.0, 12.0)
|
||||||
|
lab_shot_at("above", 0.0, 0.5, 0.0, 7.0, 30.0, 55.0)
|
||||||
|
}
|
||||||
|
def LabScenes probe { setup: fn probe_setup }
|
||||||
|
handler Boot phase Start { lab_boot() }
|
||||||
|
handler Look phase Input { lab_input() }
|
||||||
|
handler Draw phase Render { lab_render() }
|
||||||
|
}
|
||||||
12
packages/ludic.lab/index.ludic
Normal file
12
packages/ludic.lab/index.ludic
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# ludic.lab - a scene on a plate (a lit disc, a sky, a sun at one hour, a clock of frames) and the
|
||||||
|
# cameras that photograph it; a game or a package adds scenes to LabScenes. See README.md.
|
||||||
|
module ludic_lab uses ludic_render3d, ludic_base
|
||||||
|
numbers float
|
||||||
|
import "ludic.render3d/r3d.ludic"
|
||||||
|
import "state.ludic"
|
||||||
|
import "scenes.ludic"
|
||||||
|
import "plate.ludic"
|
||||||
|
import "shots.ludic"
|
||||||
|
import "png.ludic"
|
||||||
|
import "boot.ludic"
|
||||||
|
import "loop.ludic"
|
||||||
50
packages/ludic.lab/loop.ludic
Normal file
50
packages/ludic.lab/loop.ludic
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# loop.ludic - each frame: the scene's tick by a fixed step, the shot's camera, its caption, and -
|
||||||
|
# headless - the picture once the shot has settled, then the next shot, then quit. In a window
|
||||||
|
# N and P step through the shots.
|
||||||
|
export function lab_input() -> void {
|
||||||
|
if not lb_windowed { return }
|
||||||
|
Input.poll()
|
||||||
|
let n = lab_shot_count()
|
||||||
|
if n > 0 and Input.key_pressed(key: 'n') {
|
||||||
|
lb_shot = (lb_shot + 1) % n
|
||||||
|
lab_shot_apply(lb_shot)
|
||||||
|
}
|
||||||
|
if n > 0 and Input.key_pressed(key: 'p') {
|
||||||
|
lb_shot = (lb_shot + n - 1) % n
|
||||||
|
lab_shot_apply(lb_shot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function lab_render() -> void {
|
||||||
|
lb_frame += 1
|
||||||
|
let k = labscenes_find(lb_scene)
|
||||||
|
if k >= 0 and LabScenes[k].tick != null { LabScenes[k].tick(1.0 / 60.0) }
|
||||||
|
r3d_frame(lab_time())
|
||||||
|
lab_caption()
|
||||||
|
if not lb_windowed { lab_take() }
|
||||||
|
r3d_present()
|
||||||
|
}
|
||||||
|
function lab_caption() -> void {
|
||||||
|
if lb_font == "" { return }
|
||||||
|
ov_begin()
|
||||||
|
var name = ""
|
||||||
|
if lb_shot < lab_shot_count() { name = lb_shot_names[lb_shot] }
|
||||||
|
ov_rect(0, 0, 900, 64, 0.0, 0.0, 0.0, 0.55)
|
||||||
|
ov_text(20, 10, 30, `{lb_scene} / {name} ({lb_shot + 1} of {lab_shot_count()})`, 1.0, 1.0, 1.0, 1.0)
|
||||||
|
if len(lb_note) > 0 { ov_text(20, 42, 18, lb_note, 0.8, 0.8, 0.8, 1.0) }
|
||||||
|
ov_end()
|
||||||
|
}
|
||||||
|
function lab_take() -> void {
|
||||||
|
lb_wait += 1
|
||||||
|
if lb_wait < lb_settle { return }
|
||||||
|
let base = `{lb_out}/{lb_shot_names[lb_shot]}`
|
||||||
|
r3d_screenshot(`{base}.ppm`)
|
||||||
|
if lab_ppm_to_png(`{base}.ppm`, `{base}.png`) { print(`lab: shot {base}.png`) } else { print(`lab: shot {base}.ppm`) }
|
||||||
|
lb_shots_taken += 1
|
||||||
|
lb_wait = 0
|
||||||
|
lb_shot += 1
|
||||||
|
if lb_shot >= lab_shot_count() {
|
||||||
|
quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lab_shot_apply(lb_shot)
|
||||||
|
}
|
||||||
5
packages/ludic.lab/package.ludic
Normal file
5
packages/ludic.lab/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# ludic.lab - the visual lab: one model or one mechanic on a plate, photographed from fixed
|
||||||
|
# cameras, never a whole game world. See README.md.
|
||||||
|
package "ludic.lab"
|
||||||
|
version "0.1.0"
|
||||||
|
kind source
|
||||||
36
packages/ludic.lab/plate.ludic
Normal file
36
packages/ludic.lab/plate.ludic
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# plate.ludic - the stage: a flat disc 40 m across at y = 0 with a line every metre, lit by the
|
||||||
|
# package's own small sky (a sun 40 degrees up), and nothing else. Its files are in plate/.
|
||||||
|
var lb_plate: Actor = null
|
||||||
|
var lb_plate_dir: string = ""
|
||||||
|
|
||||||
|
# the package's plate/ directory: beside the project (a checkout of the toolchain) or installed
|
||||||
|
export function lab_plate_dir() -> string {
|
||||||
|
if lb_plate_dir != "" { return lb_plate_dir }
|
||||||
|
lb_plate_dir = "packages/ludic.lab/plate"
|
||||||
|
if not Fs.exists(`{lb_plate_dir}/plate.gltf`) { lb_plate_dir = `{r3d_home()}/packages/ludic.lab/plate` }
|
||||||
|
return lb_plate_dir
|
||||||
|
}
|
||||||
|
function lab_plate_load() -> void {
|
||||||
|
let m = gltf_load(lab_plate_dir(), "plate.gltf", "plate")
|
||||||
|
if m == null { return }
|
||||||
|
lb_plate = actor_new(m)
|
||||||
|
lb_plate.casts = false
|
||||||
|
lb_plate.cull = 0.0
|
||||||
|
lb_plate.radius = 0.0
|
||||||
|
actor_place(lb_plate, 0.0, 0.0, 0.0, 0.0)
|
||||||
|
}
|
||||||
|
# the ground under (x, z): the plate is flat at 0
|
||||||
|
export function lab_ground_y(x: float, z: float) -> float { return 0.0 }
|
||||||
|
# an actor stood on the plate at (x, z), facing yaw (radians)
|
||||||
|
export function lab_stand(a: Actor, x: float, z: float, yaw: float) -> void {
|
||||||
|
if a == null { return }
|
||||||
|
actor_place(a, x, lab_ground_y(x, z), z, yaw)
|
||||||
|
}
|
||||||
|
# a model from a glTF file stood on the plate: the one call a scene usually needs
|
||||||
|
export function lab_model(dir: string, file: string, node: string, x: float, z: float, yaw: float) -> Actor {
|
||||||
|
let m = gltf_load(dir, file, node)
|
||||||
|
if m == null { return null }
|
||||||
|
let a = actor_new(m)
|
||||||
|
lab_stand(a, x, z, yaw)
|
||||||
|
return a
|
||||||
|
}
|
||||||
BIN
packages/ludic.lab/plate/plate.bin
Normal file
BIN
packages/ludic.lab/plate/plate.bin
Normal file
Binary file not shown.
129
packages/ludic.lab/plate/plate.gltf
Normal file
129
packages/ludic.lab/plate/plate.gltf
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
"asset": {
|
||||||
|
"version": "2.0",
|
||||||
|
"generator": "ludic tools/lab/plate_build.py"
|
||||||
|
},
|
||||||
|
"scene": 0,
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "plate",
|
||||||
|
"mesh": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meshes": [
|
||||||
|
{
|
||||||
|
"name": "plate",
|
||||||
|
"primitives": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"POSITION": 0,
|
||||||
|
"NORMAL": 1,
|
||||||
|
"TEXCOORD_0": 2
|
||||||
|
},
|
||||||
|
"indices": 3,
|
||||||
|
"material": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"name": "plate",
|
||||||
|
"pbrMetallicRoughness": {
|
||||||
|
"baseColorTexture": {
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"metallicRoughnessTexture": {
|
||||||
|
"index": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"source": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"uri": "plate_diff.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "plate_arm.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buffers": [
|
||||||
|
{
|
||||||
|
"uri": "plate.bin",
|
||||||
|
"byteLength": 4256
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bufferViews": [
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 0,
|
||||||
|
"byteLength": 1164
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 1164,
|
||||||
|
"byteLength": 1164
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 2328,
|
||||||
|
"byteLength": 776
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 3104,
|
||||||
|
"byteLength": 1152
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accessors": [
|
||||||
|
{
|
||||||
|
"bufferView": 0,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 97,
|
||||||
|
"type": "VEC3",
|
||||||
|
"min": [
|
||||||
|
-20.0,
|
||||||
|
0.0,
|
||||||
|
-20.0
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
20.0,
|
||||||
|
0.0,
|
||||||
|
20.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 1,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 97,
|
||||||
|
"type": "VEC3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 2,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 97,
|
||||||
|
"type": "VEC2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 3,
|
||||||
|
"componentType": 5125,
|
||||||
|
"count": 288,
|
||||||
|
"type": "SCALAR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
packages/ludic.lab/plate/plate_arm.png
Normal file
BIN
packages/ludic.lab/plate/plate_arm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 B |
BIN
packages/ludic.lab/plate/plate_diff.png
Normal file
BIN
packages/ludic.lab/plate/plate_diff.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 843 B |
BIN
packages/ludic.lab/plate/probe.bin
Normal file
BIN
packages/ludic.lab/plate/probe.bin
Normal file
Binary file not shown.
209
packages/ludic.lab/plate/probe.gltf
Normal file
209
packages/ludic.lab/plate/probe.gltf
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
{
|
||||||
|
"asset": {
|
||||||
|
"version": "2.0",
|
||||||
|
"generator": "ludic tools/lab/plate_build.py"
|
||||||
|
},
|
||||||
|
"scene": 0,
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "probe",
|
||||||
|
"mesh": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meshes": [
|
||||||
|
{
|
||||||
|
"name": "probe",
|
||||||
|
"primitives": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"POSITION": 0,
|
||||||
|
"NORMAL": 1,
|
||||||
|
"TEXCOORD_0": 2
|
||||||
|
},
|
||||||
|
"indices": 3,
|
||||||
|
"material": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"POSITION": 4,
|
||||||
|
"NORMAL": 5,
|
||||||
|
"TEXCOORD_0": 6
|
||||||
|
},
|
||||||
|
"indices": 7,
|
||||||
|
"material": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"name": "probe_box",
|
||||||
|
"pbrMetallicRoughness": {
|
||||||
|
"baseColorTexture": {
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"metallicRoughnessTexture": {
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "probe_ball",
|
||||||
|
"pbrMetallicRoughness": {
|
||||||
|
"baseColorTexture": {
|
||||||
|
"index": 1
|
||||||
|
},
|
||||||
|
"metallicRoughnessTexture": {
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"source": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"uri": "probe_box.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "probe_ball.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "probe_arm.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buffers": [
|
||||||
|
{
|
||||||
|
"uri": "probe.bin",
|
||||||
|
"byteLength": 31152
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bufferViews": [
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 0,
|
||||||
|
"byteLength": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 288,
|
||||||
|
"byteLength": 288
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 576,
|
||||||
|
"byteLength": 192
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 768,
|
||||||
|
"byteLength": 144
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 912,
|
||||||
|
"byteLength": 6732
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 7644,
|
||||||
|
"byteLength": 6732
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 14376,
|
||||||
|
"byteLength": 4488
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"buffer": 0,
|
||||||
|
"byteOffset": 18864,
|
||||||
|
"byteLength": 12288
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accessors": [
|
||||||
|
{
|
||||||
|
"bufferView": 0,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 24,
|
||||||
|
"type": "VEC3",
|
||||||
|
"min": [
|
||||||
|
-0.4,
|
||||||
|
0.0,
|
||||||
|
-0.4
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.4,
|
||||||
|
0.8,
|
||||||
|
0.4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 1,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 24,
|
||||||
|
"type": "VEC3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 2,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 24,
|
||||||
|
"type": "VEC2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 3,
|
||||||
|
"componentType": 5125,
|
||||||
|
"count": 36,
|
||||||
|
"type": "SCALAR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 4,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 561,
|
||||||
|
"type": "VEC3",
|
||||||
|
"min": [
|
||||||
|
-0.3,
|
||||||
|
0.8,
|
||||||
|
-0.3
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.3,
|
||||||
|
1.4000000000000001,
|
||||||
|
0.3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 5,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 561,
|
||||||
|
"type": "VEC3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 6,
|
||||||
|
"componentType": 5126,
|
||||||
|
"count": 561,
|
||||||
|
"type": "VEC2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bufferView": 7,
|
||||||
|
"componentType": 5125,
|
||||||
|
"count": 3072,
|
||||||
|
"type": "SCALAR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
packages/ludic.lab/plate/probe_arm.png
Normal file
BIN
packages/ludic.lab/plate/probe_arm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 B |
BIN
packages/ludic.lab/plate/probe_ball.png
Normal file
BIN
packages/ludic.lab/plate/probe_ball.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 B |
BIN
packages/ludic.lab/plate/probe_box.png
Normal file
BIN
packages/ludic.lab/plate/probe_box.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 B |
5
packages/ludic.lab/plate/sky.hdr
Normal file
5
packages/ludic.lab/plate/sky.hdr
Normal file
File diff suppressed because one or more lines are too long
131
packages/ludic.lab/png.ludic
Normal file
131
packages/ludic.lab/png.ludic
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# png.ludic - the renderer writes a frame as a PPM; the lab keeps it as a PNG, stored rather than
|
||||||
|
# compressed (a PNG anything opens, with no zlib here): the shot is the PPM's pixels, filter 0
|
||||||
|
# on every row, in deflate's stored blocks, with its CRCs and its Adler-32.
|
||||||
|
var lp_table: []long = null
|
||||||
|
|
||||||
|
function lp_u32(hi: int, lo: int) -> long { return long(hi) * long(65536) + long(lo) }
|
||||||
|
function lp_table_init() -> void {
|
||||||
|
if lp_table != null { return }
|
||||||
|
lp_table = new []long
|
||||||
|
let poly = lp_u32(60856, 33568) # 0xEDB88320
|
||||||
|
for n in 0 .. 256 {
|
||||||
|
var c = long(n)
|
||||||
|
for k in 0 .. 8 {
|
||||||
|
if (c & long(1)) != long(0) { c = (c >> long(1)) ^ poly } else { c = c >> long(1) }
|
||||||
|
}
|
||||||
|
push(lp_table, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# CRC-32 of out[from .. from + n)
|
||||||
|
function lp_crc(out: []byte, from: int, n: int) -> long {
|
||||||
|
lp_table_init()
|
||||||
|
let mask = lp_u32(65535, 65535)
|
||||||
|
var c = mask
|
||||||
|
for i in from .. from + n {
|
||||||
|
let k = int((c ^ long(out[i])) & long(255))
|
||||||
|
c = lp_table[k] ^ (c >> long(8))
|
||||||
|
}
|
||||||
|
return c ^ mask
|
||||||
|
}
|
||||||
|
function lp_put32(out: []byte, at: int, v: long) -> void {
|
||||||
|
out[at] = int((v >> long(24)) & long(255))
|
||||||
|
out[at + 1] = int((v >> long(16)) & long(255))
|
||||||
|
out[at + 2] = int((v >> long(8)) & long(255))
|
||||||
|
out[at + 3] = int(v & long(255))
|
||||||
|
}
|
||||||
|
function lp_tag(out: []byte, at: int, tag: string) -> void {
|
||||||
|
for i in 0 .. 4 { out[at + i] = tag[i] }
|
||||||
|
}
|
||||||
|
# a PPM's header: "P6", width, height, maxval, each after white space; returns the pixel offset
|
||||||
|
var lp_w: int = 0
|
||||||
|
var lp_h: int = 0
|
||||||
|
function lp_ppm_header(d: []byte) -> int {
|
||||||
|
var i = 2
|
||||||
|
var got = 0
|
||||||
|
var vals = new []int
|
||||||
|
while got < 3 and i < len(d) {
|
||||||
|
while i < len(d) and (d[i] == 32 or d[i] == 10 or d[i] == 13 or d[i] == 9) { i += 1 }
|
||||||
|
var v = 0
|
||||||
|
while i < len(d) and d[i] >= 48 and d[i] <= 57 {
|
||||||
|
v = v * 10 + (d[i] - 48)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
push(vals, v)
|
||||||
|
got += 1
|
||||||
|
}
|
||||||
|
lp_w = vals[0]
|
||||||
|
lp_h = vals[1]
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
# the PPM at `ppm` as a PNG at `png`; the PPM is removed
|
||||||
|
export function lab_ppm_to_png(ppm: string, png: string) -> bool {
|
||||||
|
let d = Fs.read_bytes(ppm)
|
||||||
|
if d == null or len(d) < 8 { return false }
|
||||||
|
let px = lp_ppm_header(d)
|
||||||
|
let w = lp_w
|
||||||
|
let h = lp_h
|
||||||
|
let row = w * 3 + 1
|
||||||
|
let raw = row * h
|
||||||
|
let blocks = (raw + 65534) / 65535
|
||||||
|
let idat = 2 + blocks * 5 + raw + 4
|
||||||
|
let total = 8 + 25 + 12 + idat + 12
|
||||||
|
let out = buffer(total)
|
||||||
|
let sig = [137, 80, 78, 71, 13, 10, 26, 10]
|
||||||
|
for i in 0 .. 8 { out[i] = sig[i] }
|
||||||
|
var at = 8
|
||||||
|
lp_put32(out, at, long(13))
|
||||||
|
lp_tag(out, at + 4, "IHDR")
|
||||||
|
lp_put32(out, at + 8, long(w))
|
||||||
|
lp_put32(out, at + 12, long(h))
|
||||||
|
out[at + 16] = 8
|
||||||
|
out[at + 17] = 2
|
||||||
|
out[at + 18] = 0
|
||||||
|
out[at + 19] = 0
|
||||||
|
out[at + 20] = 0
|
||||||
|
lp_put32(out, at + 21, lp_crc(out, at + 4, 17))
|
||||||
|
at += 25
|
||||||
|
lp_put32(out, at, long(idat))
|
||||||
|
lp_tag(out, at + 4, "IDAT")
|
||||||
|
var o = at + 8
|
||||||
|
out[o] = 120
|
||||||
|
out[o + 1] = 1
|
||||||
|
o += 2
|
||||||
|
var a = 1
|
||||||
|
var b = 0
|
||||||
|
var left = raw
|
||||||
|
var k = 0 # the byte of the raw stream being written
|
||||||
|
while left > 0 {
|
||||||
|
let n = Math.min(left, 65535)
|
||||||
|
left -= n
|
||||||
|
var fin = 0
|
||||||
|
if left == 0 { fin = 1 }
|
||||||
|
out[o] = fin
|
||||||
|
out[o + 1] = n & 255
|
||||||
|
out[o + 2] = (n >> 8) & 255
|
||||||
|
out[o + 3] = (~n) & 255
|
||||||
|
out[o + 4] = ((~n) >> 8) & 255
|
||||||
|
o += 5
|
||||||
|
for j in 0 .. n {
|
||||||
|
let y = k / row
|
||||||
|
let x = k % row
|
||||||
|
var v = 0
|
||||||
|
if x > 0 { v = d[px + y * w * 3 + x - 1] }
|
||||||
|
out[o] = v
|
||||||
|
a = (a + v) % 65521
|
||||||
|
b = (b + a) % 65521
|
||||||
|
o += 1
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lp_put32(out, o, long(b) * long(65536) + long(a))
|
||||||
|
o += 4
|
||||||
|
lp_put32(out, o, lp_crc(out, at + 4, o - at - 4))
|
||||||
|
o += 4
|
||||||
|
lp_put32(out, o, long(0))
|
||||||
|
lp_tag(out, o + 4, "IEND")
|
||||||
|
lp_put32(out, o + 8, lp_crc(out, o + 4, 4))
|
||||||
|
o += 12
|
||||||
|
if not Fs.write_bytes(png, out, o) { return false }
|
||||||
|
Fs.remove(ppm)
|
||||||
|
return true
|
||||||
|
}
|
||||||
20
packages/ludic.lab/scenes.ludic
Normal file
20
packages/ludic.lab/scenes.ludic
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# scenes.ludic - every scene the lab can show, added by whoever has something to look at:
|
||||||
|
# def LabScenes deer { setup: fn deer_setup, tick: fn deer_tick }
|
||||||
|
# setup builds what the scene shows and names its cameras (lab_shot); tick, if any, moves it each
|
||||||
|
# frame by the lab's fixed step; draw, if any, draws what is not an actor or a scatter layer.
|
||||||
|
export property LabScene {
|
||||||
|
key: string = ""
|
||||||
|
setup: fn() -> void = null
|
||||||
|
tick: fn(float) -> void = null
|
||||||
|
draw: fn() -> void = null
|
||||||
|
}
|
||||||
|
export open registry LabScenes of LabScene as LAB
|
||||||
|
|
||||||
|
export function lab_scene_names() -> string {
|
||||||
|
var s = ""
|
||||||
|
for i in 0 .. LAB_COUNT {
|
||||||
|
if i > 0 { s = s + ", " }
|
||||||
|
s = s + LabScenes[i].key
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
35
packages/ludic.lab/shots.ludic
Normal file
35
packages/ludic.lab/shots.ludic
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# shots.ludic - a scene names its cameras; the loop takes them in order
|
||||||
|
function lab_shots_reset() -> void {
|
||||||
|
lb_shot_names = new []string
|
||||||
|
lb_shot_pose = new []float
|
||||||
|
}
|
||||||
|
# a camera at (x, y, z) looking along yaw / pitch, both in DEGREES (cam_set's units)
|
||||||
|
export function lab_shot(name: string, x: float, y: float, z: float, yaw_deg: float, pitch_deg: float) -> void {
|
||||||
|
push(lb_shot_names, name)
|
||||||
|
push(lb_shot_pose, x)
|
||||||
|
push(lb_shot_pose, y)
|
||||||
|
push(lb_shot_pose, z)
|
||||||
|
push(lb_shot_pose, yaw_deg)
|
||||||
|
push(lb_shot_pose, pitch_deg)
|
||||||
|
}
|
||||||
|
# a camera `dist` metres from the point (tx, ty, tz), from bearing `from_deg` (0 = from +z),
|
||||||
|
# `up_deg` above the horizontal, looking at the point
|
||||||
|
export function lab_shot_at(name: string, tx: float, ty: float, tz: float, dist: float, from_deg: float, up_deg: float) -> void {
|
||||||
|
let b = Math.deg_to_rad(from_deg)
|
||||||
|
let e = Math.deg_to_rad(up_deg)
|
||||||
|
let hd = dist * Math.cos(e)
|
||||||
|
let cx = tx + Math.sin(b) * hd
|
||||||
|
let cz = tz + Math.cos(b) * hd
|
||||||
|
let cy = ty + dist * Math.sin(e)
|
||||||
|
let yaw = Math.atan2(cx - tx, cz - tz) * (180.0 / PI)
|
||||||
|
lab_shot(name, cx, cy, cz, yaw, -up_deg)
|
||||||
|
}
|
||||||
|
export function lab_shot_count() -> int {
|
||||||
|
if lb_shot_names == null { return 0 }
|
||||||
|
return len(lb_shot_names)
|
||||||
|
}
|
||||||
|
function lab_shot_apply(i: int) -> void {
|
||||||
|
if i < 0 or i >= lab_shot_count() { return }
|
||||||
|
let o = i * 5
|
||||||
|
cam_set(lb_shot_pose[o], lb_shot_pose[o + 1], lb_shot_pose[o + 2], lb_shot_pose[o + 3], lb_shot_pose[o + 4])
|
||||||
|
}
|
||||||
27
packages/ludic.lab/state.ludic
Normal file
27
packages/ludic.lab/state.ludic
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# state.ludic - which scene, its shots, and where the loop is in them
|
||||||
|
var lb_scene: string = ""
|
||||||
|
var lb_frame: int = 0
|
||||||
|
var lb_shot_names: []string = null
|
||||||
|
var lb_shot_pose: []float = null # 5 per shot: x, y, z, yaw and pitch in degrees
|
||||||
|
var lb_shot: int = 0 # the shot being taken or looked at
|
||||||
|
var lb_wait: int = 0 # frames held on it so far
|
||||||
|
var lb_settle: int = 45 # frames a shot is held before it is taken
|
||||||
|
var lb_out: string = "" # build/lab/<scene>
|
||||||
|
var lb_windowed: bool = false
|
||||||
|
var lb_note: string = "" # one line a scene may show under its title
|
||||||
|
var lb_font: string = "" # an overlay font atlas directory; "" draws no captions
|
||||||
|
var lb_title: string = "Ludic Lab"
|
||||||
|
var lb_draw: fn() = null # the scene's own drawing, beside the actors
|
||||||
|
var lb_shots_taken: int = 0
|
||||||
|
|
||||||
|
# frames a shot settles before it is taken (streaming, auto-exposure); 45 by default
|
||||||
|
export function lab_settle(frames: int) -> void { lb_settle = Math.max(1, frames) }
|
||||||
|
# the overlay's font atlas (a directory with font.json and font.png) for the captions
|
||||||
|
export function lab_font(dir: string) -> void { lb_font = dir }
|
||||||
|
export function lab_title(title: string) -> void { lb_title = title }
|
||||||
|
# one line under the scene's title in a window
|
||||||
|
export function lab_note(s: string) -> void { lb_note = s }
|
||||||
|
# the scene being shown, and the frame the clock is on (60 a second, whatever the machine)
|
||||||
|
export function lab_scene() -> string { return lb_scene }
|
||||||
|
export function lab_frame() -> int { return lb_frame }
|
||||||
|
export function lab_time() -> float { return float(lb_frame) / 60.0 }
|
||||||
54
packages/ludic.lab/tests/lab_test.ludic
Normal file
54
packages/ludic.lab/tests/lab_test.ludic
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# lab_test.ludic - what the lab computes without a window: the PNG it keeps a shot as, the cameras
|
||||||
|
# a scene names, and the scenes a program adds
|
||||||
|
import "ludic.lab"
|
||||||
|
program LabTest {
|
||||||
|
numbers float
|
||||||
|
friend module lab_test of ludic_lab
|
||||||
|
function nothing() -> void { }
|
||||||
|
def LabScenes one { setup: fn nothing }
|
||||||
|
def LabScenes two { setup: fn nothing }
|
||||||
|
|
||||||
|
function ppm(path: string) -> void {
|
||||||
|
let hdr = "P6\n2 2\n255\n"
|
||||||
|
let b = buffer(len(hdr) + 12)
|
||||||
|
for i in 0 .. len(hdr) { b[i] = hdr[i] }
|
||||||
|
for i in 0 .. 12 { b[len(hdr) + i] = i * 20 }
|
||||||
|
Fs.write_bytes(path, b, len(hdr) + 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a PPM becomes a PNG with its signature, its size and a valid IEND" {
|
||||||
|
let dir = Os.temp_dir()
|
||||||
|
ppm(`{dir}/lab_test.ppm`)
|
||||||
|
expect(lab_ppm_to_png(`{dir}/lab_test.ppm`, `{dir}/lab_test.png`))
|
||||||
|
let p = Fs.read_bytes(`{dir}/lab_test.png`)
|
||||||
|
expect(p != null)
|
||||||
|
expect_eq(p[0], 137)
|
||||||
|
expect_eq(p[1], 80)
|
||||||
|
expect_eq(p[19], 2) # IHDR width, low byte
|
||||||
|
expect_eq(p[23], 2) # height
|
||||||
|
let n = len(p)
|
||||||
|
# the IEND chunk's CRC is always AE 42 60 82
|
||||||
|
expect_eq(p[n - 4], 174)
|
||||||
|
expect_eq(p[n - 3], 66)
|
||||||
|
expect_eq(p[n - 2], 96)
|
||||||
|
expect_eq(p[n - 1], 130)
|
||||||
|
expect(not Fs.exists(`{dir}/lab_test.ppm`))
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a camera at a point looks back at it" {
|
||||||
|
lab_shots_reset()
|
||||||
|
lab_shot_at("front", 0.0, 1.0, 0.0, 10.0, 0.0, 0.0)
|
||||||
|
lab_shot_at("side", 0.0, 1.0, 0.0, 10.0, 90.0, 30.0)
|
||||||
|
expect_eq(lab_shot_count(), 2)
|
||||||
|
expect_near(lb_shot_pose[2], 10.0, 0.001) # front: 10 m out along +z
|
||||||
|
expect_near(lb_shot_pose[5 + 0], 8.660, 0.01) # side: along +x, 30 degrees up
|
||||||
|
expect_near(lb_shot_pose[5 + 1], 6.0, 0.01)
|
||||||
|
expect_near(lb_shot_pose[5 + 4], -30.0, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "scenes are what the program added, in its order" {
|
||||||
|
expect_eq(LAB_COUNT, 2)
|
||||||
|
expect(lab_scene_names() == "one, two")
|
||||||
|
expect_eq(labscenes_find("two"), LAB_TWO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,13 @@ var r3d_dem_base: float = 0.0
|
||||||
var r3d_dem_ox: float = 0.0
|
var r3d_dem_ox: float = 0.0
|
||||||
var r3d_dem_oz: float = 0.0
|
var r3d_dem_oz: float = 0.0
|
||||||
var r3d_ortho_path: string = null
|
var r3d_ortho_path: string = null
|
||||||
|
# A plate: no terrain, no grass, no water - the sky, the sun, the shadows and whatever the scene
|
||||||
|
# draws (a lab's stage, ludic.lab). Set before the load; nothing of the landscape is made, so
|
||||||
|
# its hundreds of megabytes of height field, materials and grass never exist.
|
||||||
|
var r3d_plate: bool = false
|
||||||
|
function r3d_plate_mode(on: bool) -> void { r3d_plate = on }
|
||||||
|
# the sky's HDR image, when it is not the default photograph under r3d_assets
|
||||||
|
var r3d_sky_path: string = null
|
||||||
var r3d_debug: bool = false
|
var r3d_debug: bool = false
|
||||||
var r3d_debug_shadow: bool = false
|
var r3d_debug_shadow: bool = false
|
||||||
var r3d_debug_max: bool = false
|
var r3d_debug_max: bool = false
|
||||||
|
|
@ -123,9 +130,12 @@ function r3d_load_count() -> int { return 4 + TERRAIN_INIT_STEPS }
|
||||||
function r3d_load_step(i: int) -> bool {
|
function r3d_load_step(i: int) -> bool {
|
||||||
let t = i - 1
|
let t = i - 1
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false }
|
var sky = r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr"
|
||||||
|
if r3d_sky_path != null { sky = r3d_sky_path }
|
||||||
|
if not sky_load(sky) { return false }
|
||||||
daylight_init()
|
daylight_init()
|
||||||
} else if t >= 0 and t < TERRAIN_INIT_STEPS {
|
} else if t >= 0 and t < TERRAIN_INIT_STEPS {
|
||||||
|
if r3d_plate { return true }
|
||||||
if t == 0 {
|
if t == 0 {
|
||||||
if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) }
|
if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) }
|
||||||
if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) }
|
if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) }
|
||||||
|
|
@ -138,7 +148,7 @@ function r3d_load_step(i: int) -> bool {
|
||||||
scatter_init()
|
scatter_init()
|
||||||
actor_init()
|
actor_init()
|
||||||
} else if i == 3 + TERRAIN_INIT_STEPS {
|
} else if i == 3 + TERRAIN_INIT_STEPS {
|
||||||
grass_init()
|
if not r3d_plate { grass_init() }
|
||||||
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
|
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
|
||||||
# The base is NOON's air, and it was tuned when nothing desaturated with distance - so it
|
# The base is NOON's air, and it was tuned when nothing desaturated with distance - so it
|
||||||
# was doing its whole job through colour and could not be raised without the valley going
|
# was doing its whole job through colour and could not be raised without the valley going
|
||||||
|
|
@ -221,7 +231,7 @@ function r3d_frame(time: float) -> void {
|
||||||
print(`cam {r3d_test_frame} pos {int(cam_pos[0] * 1000.0)} {int(cam_pos[1] * 1000.0)} {int(cam_pos[2] * 1000.0)} yaw {int(cam_yaw * 1000.0)} pitch {int(cam_pitch * 1000.0)} jitter {int(gsl_jitter_x * 1000000.0)} {int(gsl_jitter_y * 1000000.0)} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`)
|
print(`cam {r3d_test_frame} pos {int(cam_pos[0] * 1000.0)} {int(cam_pos[1] * 1000.0)} {int(cam_pos[2] * 1000.0)} yaw {int(cam_yaw * 1000.0)} pitch {int(cam_pitch * 1000.0)} jitter {int(gsl_jitter_x * 1000000.0)} {int(gsl_jitter_y * 1000000.0)} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`)
|
||||||
}
|
}
|
||||||
# the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock
|
# the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock
|
||||||
if (not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen {
|
if not r3d_plate and ((not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen) {
|
||||||
ter_shadow_gen = day_gen
|
ter_shadow_gen = day_gen
|
||||||
let t_bk = gl_now_us()
|
let t_bk = gl_now_us()
|
||||||
terrain_bake_shadow()
|
terrain_bake_shadow()
|
||||||
|
|
@ -249,12 +259,14 @@ function r3d_frame(time: float) -> void {
|
||||||
sc_prepass = true
|
sc_prepass = true
|
||||||
}
|
}
|
||||||
prof_cpu_mark("foliage prepass")
|
prof_cpu_mark("foliage prepass")
|
||||||
|
if not r3d_plate {
|
||||||
prof_begin("terrain sun")
|
prof_begin("terrain sun")
|
||||||
terrain_sun_prepare()
|
terrain_sun_prepare()
|
||||||
prof_end()
|
prof_end()
|
||||||
prof_begin("terrain")
|
prof_begin("terrain")
|
||||||
terrain_draw()
|
terrain_draw()
|
||||||
prof_end()
|
prof_end()
|
||||||
|
}
|
||||||
prof_cpu_mark("terrain")
|
prof_cpu_mark("terrain")
|
||||||
prof_begin("scene (vegetation)")
|
prof_begin("scene (vegetation)")
|
||||||
r3d_scene_draw()
|
r3d_scene_draw()
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ var ter_h_scale: float = 0.0
|
||||||
var ter_o_scale: float = 0.0
|
var ter_o_scale: float = 0.0
|
||||||
|
|
||||||
function terrain_height(x: float, z: float) -> float {
|
function terrain_height(x: float, z: float) -> float {
|
||||||
|
if ter_heights == null { return 0.0 } # a plate (r3d_plate_mode): flat at y = 0
|
||||||
if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
|
if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
|
||||||
let scale = ter_h_scale
|
let scale = ter_h_scale
|
||||||
let fx = (x - ter_ox + float(TERRAIN_HALF)) * scale
|
let fx = (x - ter_ox + float(TERRAIN_HALF)) * scale
|
||||||
|
|
@ -342,6 +343,7 @@ function terrain_height_fx(x: fixed, z: fixed) -> fixed { return fixed(terrain_h
|
||||||
# thousands of placement queries a chunk makes.
|
# thousands of placement queries a chunk makes.
|
||||||
var ter_bw: floats = null
|
var ter_bw: floats = null
|
||||||
function terrain_height_smooth(x: float, z: float) -> float {
|
function terrain_height_smooth(x: float, z: float) -> float {
|
||||||
|
if ter_heights == null { return 0.0 }
|
||||||
if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
|
if ter_h_scale == 0.0 { ter_h_scale = float(TERRAIN_RES) / float(TERRAIN_HALF * 2) }
|
||||||
if ter_bw == null { ter_bw = floats(8) }
|
if ter_bw == null { ter_bw = floats(8) }
|
||||||
let scale = ter_h_scale
|
let scale = ter_h_scale
|
||||||
|
|
|
||||||
|
|
@ -582,7 +582,7 @@ function emit_call(e: Node) -> Val {
|
||||||
emit(" call void @L_world_load(ptr "); emit(b.code); emit(", i32 "); emit(l.code); emit(")\n")
|
emit(" call void @L_world_load(ptr "); emit(b.code); emit(", i32 "); emit(l.code); emit(")\n")
|
||||||
return val("0", "void")
|
return val("0", "void")
|
||||||
}
|
}
|
||||||
if (name == "quit") { emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
if (name == "quit") { g_uses_quit = true; emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
||||||
# NETWORKING (NETWORKING-DESIGN §5) — the low-level freedom layer, callable from
|
# NETWORKING (NETWORKING-DESIGN §5) — the low-level freedom layer, callable from
|
||||||
# Ludic. serialize/apply/sync_size lower to the @Sync by-kind dispatchers (N2);
|
# Ludic. serialize/apply/sync_size lower to the @Sync by-kind dispatchers (N2);
|
||||||
# owner/set_owner/is_owner to the @Owned storage (N3); is_server/local_id read
|
# owner/set_owner/is_owner to the @Owned storage (N3); is_server/local_id read
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ var g_fprintf_declared: bool = false # @fprintf is declared once, by whoever ne
|
||||||
var g_bounds: bool = true # check every slice index (--no-bounds turns it off)
|
var g_bounds: bool = true # check every slice index (--no-bounds turns it off)
|
||||||
var g_uses_result: bool = false # ok()/err()/try was emitted -> define the %Result value type (issue #46)
|
var g_uses_result: bool = false # ok()/err()/try was emitted -> define the %Result value type (issue #46)
|
||||||
var g_uses_option: bool = false # some()/none() was emitted -> define the %Option value type (issue #53)
|
var g_uses_option: bool = false # some()/none() was emitted -> define the %Option value type (issue #53)
|
||||||
|
var g_uses_quit: bool = false # quit() was lowered: a program with no frame loop still needs @L_running
|
||||||
var g_tests: []Node # test "name" { ... } blocks collected by the parser
|
var g_tests: []Node # test "name" { ... } blocks collected by the parser
|
||||||
var g_src_name: pointer = "?" # base name of the source file, for panic/expect file:line messages
|
var g_src_name: pointer = "?" # base name of the source file, for panic/expect file:line messages
|
||||||
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,7 @@ function emit_program() -> void {
|
||||||
g_fprintf_declared = false
|
g_fprintf_declared = false
|
||||||
g_uses_result = false
|
g_uses_result = false
|
||||||
g_uses_option = false
|
g_uses_option = false
|
||||||
|
g_uses_quit = false
|
||||||
g_uses_world_despawn = false # #84: set when a world_despawn call is emitted (below)
|
g_uses_world_despawn = false # #84: set when a world_despawn call is emitted (below)
|
||||||
g_cov_lines = new []int
|
g_cov_lines = new []int
|
||||||
g_cov_active = true
|
g_cov_active = true
|
||||||
|
|
@ -312,6 +313,8 @@ function emit_program() -> void {
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i += 1 }
|
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i += 1 }
|
||||||
} } }
|
} } }
|
||||||
|
# quit() in a program with no frame loop (a package's code in a test program): the flag it sets
|
||||||
|
if g_uses_quit and not has_ecs() { emith("@L_running = internal global i32 1\n") }
|
||||||
if g_uses_world_despawn { emit_world_despawn_fn() } # #84: @fn_world_despawn, after all World.despawn / esys_bounds-kill uses are seen
|
if g_uses_world_despawn { emit_world_despawn_fn() } # #84: @fn_world_despawn, after all World.despawn / esys_bounds-kill uses are seen
|
||||||
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
||||||
if g_uses_str { emit_str_prelude() } # @lp_str_eq / @lp_str_concat, after all uses are seen
|
if g_uses_str { emit_str_prelude() } # @lp_str_eq / @lp_str_concat, after all uses are seen
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
181
tools/lab/plate_build.py
Executable file
181
tools/lab/plate_build.py
Executable file
|
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""plate_build.py - the lab's stage, built here so the package needs no downloads.
|
||||||
|
|
||||||
|
Writes packages/ludic.lab/plate/:
|
||||||
|
plate.gltf / plate.bin a flat disc, 40 m across, normals up, uv in metres / 4
|
||||||
|
plate_diff.png a pale grey-green ground with a line every metre, a heavier one every 5
|
||||||
|
plate_arm.png AO 1, roughness 0.9, metal 0
|
||||||
|
sky.hdr a small clear sky (512 x 256 RGBE): blue overhead, pale at the horizon,
|
||||||
|
a dim ground below it, and a sun 40 degrees up - the plate's one fixed hour
|
||||||
|
Deterministic: the same bytes every run.
|
||||||
|
"""
|
||||||
|
import json, math, os, struct, sys
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), '..', '..', 'packages', 'ludic.lab', 'plate')
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
|
# ---- the disc -------------------------------------------------------------
|
||||||
|
R, SEG = 20.0, 96
|
||||||
|
pos, nrm, uv, idx = [], [], [], []
|
||||||
|
pos += [0.0, 0.0, 0.0]; nrm += [0.0, 1.0, 0.0]; uv += [0.5, 0.5]
|
||||||
|
for i in range(SEG):
|
||||||
|
a = 2.0 * math.pi * i / SEG
|
||||||
|
x, z = R * math.cos(a), R * math.sin(a)
|
||||||
|
pos += [x, 0.0, z]; nrm += [0.0, 1.0, 0.0]; uv += [x / 4.0, z / 4.0]
|
||||||
|
uv[0], uv[1] = 0.0, 0.0
|
||||||
|
for i in range(SEG):
|
||||||
|
a, b = 1 + i, 1 + (i + 1) % SEG
|
||||||
|
idx += [0, b, a]
|
||||||
|
pb = struct.pack(f'<{len(pos)}f', *pos)
|
||||||
|
nb = struct.pack(f'<{len(nrm)}f', *nrm)
|
||||||
|
ub = struct.pack(f'<{len(uv)}f', *uv)
|
||||||
|
ib = struct.pack(f'<{len(idx)}I', *idx)
|
||||||
|
blob = pb + nb + ub + ib
|
||||||
|
with open(os.path.join(OUT, 'plate.bin'), 'wb') as f:
|
||||||
|
f.write(blob)
|
||||||
|
n = len(pos) // 3
|
||||||
|
views, off = [], 0
|
||||||
|
for b in (pb, nb, ub, ib):
|
||||||
|
views.append({'buffer': 0, 'byteOffset': off, 'byteLength': len(b)}); off += len(b)
|
||||||
|
doc = {
|
||||||
|
'asset': {'version': '2.0', 'generator': 'ludic tools/lab/plate_build.py'},
|
||||||
|
'scene': 0, 'scenes': [{'nodes': [0]}],
|
||||||
|
'nodes': [{'name': 'plate', 'mesh': 0}],
|
||||||
|
'meshes': [{'name': 'plate', 'primitives': [{'attributes': {'POSITION': 0, 'NORMAL': 1, 'TEXCOORD_0': 2}, 'indices': 3, 'material': 0}]}],
|
||||||
|
'materials': [{'name': 'plate', 'pbrMetallicRoughness': {'baseColorTexture': {'index': 0}, 'metallicRoughnessTexture': {'index': 1}}}],
|
||||||
|
'textures': [{'source': 0}, {'source': 1}],
|
||||||
|
'images': [{'uri': 'plate_diff.png'}, {'uri': 'plate_arm.png'}],
|
||||||
|
'buffers': [{'uri': 'plate.bin', 'byteLength': len(blob)}],
|
||||||
|
'bufferViews': views,
|
||||||
|
'accessors': [
|
||||||
|
{'bufferView': 0, 'componentType': 5126, 'count': n, 'type': 'VEC3', 'min': [-R, 0.0, -R], 'max': [R, 0.0, R]},
|
||||||
|
{'bufferView': 1, 'componentType': 5126, 'count': n, 'type': 'VEC3'},
|
||||||
|
{'bufferView': 2, 'componentType': 5126, 'count': n, 'type': 'VEC2'},
|
||||||
|
{'bufferView': 3, 'componentType': 5125, 'count': len(idx), 'type': 'SCALAR'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with open(os.path.join(OUT, 'plate.gltf'), 'w') as f:
|
||||||
|
json.dump(doc, f, indent=1)
|
||||||
|
|
||||||
|
# ---- its material: 4 m of ground per tile, a line a metre -----------------
|
||||||
|
T = 256
|
||||||
|
im = Image.new('RGB', (T, T), (142, 146, 132))
|
||||||
|
px = im.load()
|
||||||
|
for y in range(T):
|
||||||
|
for x in range(T):
|
||||||
|
heavy = x == 0 or y == 0
|
||||||
|
light = x % (T // 4) == 0 or y % (T // 4) == 0
|
||||||
|
if heavy:
|
||||||
|
px[x, y] = (96, 100, 90)
|
||||||
|
elif light:
|
||||||
|
px[x, y] = (122, 126, 114)
|
||||||
|
im.save(os.path.join(OUT, 'plate_diff.png'))
|
||||||
|
Image.new('RGB', (4, 4), (255, 230, 0)).save(os.path.join(OUT, 'plate_arm.png'))
|
||||||
|
|
||||||
|
# ---- the sky: one fixed hour ----------------------------------------------
|
||||||
|
W, H = 512, 256
|
||||||
|
SUN_EL, SUN_AZ = math.radians(40.0), math.radians(135.0)
|
||||||
|
sx, sy, sz = math.cos(SUN_EL) * math.sin(SUN_AZ), math.sin(SUN_EL), math.cos(SUN_EL) * math.cos(SUN_AZ)
|
||||||
|
|
||||||
|
def rgbe(r, g, b):
|
||||||
|
m = max(r, g, b)
|
||||||
|
if m < 1e-32:
|
||||||
|
return bytes((0, 0, 0, 0))
|
||||||
|
e = math.frexp(m)[1]
|
||||||
|
s = 256.0 / (2.0 ** e)
|
||||||
|
return bytes((min(255, int(r * s)), min(255, int(g * s)), min(255, int(b * s)), e + 128))
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for y in range(H):
|
||||||
|
th = (y + 0.5) / H * math.pi # 0 at the zenith
|
||||||
|
el = math.pi / 2 - th
|
||||||
|
row = bytearray()
|
||||||
|
for x in range(W):
|
||||||
|
ph = (x + 0.5) / W * 2.0 * math.pi
|
||||||
|
dx, dy, dz = math.sin(th) * math.sin(ph), math.cos(th), math.sin(th) * math.cos(ph)
|
||||||
|
if el >= 0:
|
||||||
|
t = 1.0 - min(1.0, el / (math.pi / 2)) ** 0.6
|
||||||
|
r, g, b = 0.25 + 0.75 * t, 0.45 + 0.55 * t, 1.0
|
||||||
|
k = 1.1
|
||||||
|
r, g, b = r * k, g * k, b * k
|
||||||
|
else:
|
||||||
|
r, g, b = 0.22, 0.21, 0.18
|
||||||
|
c = dx * sx + dy * sy + dz * sz
|
||||||
|
if c > math.cos(math.radians(1.2)):
|
||||||
|
r, g, b = 2600.0, 2450.0, 2250.0 # the sun: integrated past the clip, lights the plate
|
||||||
|
elif c > math.cos(math.radians(8.0)):
|
||||||
|
glow = (c - math.cos(math.radians(8.0))) / (1.0 - math.cos(math.radians(8.0)))
|
||||||
|
r, g, b = r + 6.0 * glow, g + 5.5 * glow, b + 4.5 * glow
|
||||||
|
row += rgbe(r, g, b)
|
||||||
|
rows.append(bytes(row))
|
||||||
|
with open(os.path.join(OUT, 'sky.hdr'), 'wb') as f:
|
||||||
|
f.write(b'#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n')
|
||||||
|
f.write(f'-Y {H} +X {W}\n'.encode())
|
||||||
|
for r in rows:
|
||||||
|
f.write(r)
|
||||||
|
print(f'plate: {OUT}')
|
||||||
|
|
||||||
|
# ---- a probe to look at: a box with a ball on it, two materials --------------
|
||||||
|
def box(s):
|
||||||
|
P, N, U, I = [], [], [], []
|
||||||
|
faces = [((1,0,0),(0,0,-1),(0,1,0)), ((-1,0,0),(0,0,1),(0,1,0)), ((0,1,0),(1,0,0),(0,0,-1)),
|
||||||
|
((0,-1,0),(1,0,0),(0,0,1)), ((0,0,1),(1,0,0),(0,1,0)), ((0,0,-1),(-1,0,0),(0,1,0))]
|
||||||
|
for n, u, v in faces:
|
||||||
|
base = len(P) // 3
|
||||||
|
for a, b in ((-1,-1),(1,-1),(1,1),(-1,1)):
|
||||||
|
p = [s * (n[k] + a * u[k] + b * v[k]) for k in range(3)]
|
||||||
|
p[1] += s
|
||||||
|
P += p; N += list(n); U += [(a + 1) / 2, (b + 1) / 2]
|
||||||
|
I += [base, base + 1, base + 2, base, base + 2, base + 3]
|
||||||
|
return P, N, U, I
|
||||||
|
|
||||||
|
def ball(r, cy, seg=32, rings=16):
|
||||||
|
P, N, U, I = [], [], [], []
|
||||||
|
for j in range(rings + 1):
|
||||||
|
th = math.pi * j / rings
|
||||||
|
for i in range(seg + 1):
|
||||||
|
ph = 2 * math.pi * i / seg
|
||||||
|
n = (math.sin(th) * math.cos(ph), math.cos(th), math.sin(th) * math.sin(ph))
|
||||||
|
P += [r * n[0], cy + r * n[1], r * n[2]]; N += list(n); U += [i / seg, j / rings]
|
||||||
|
for j in range(rings):
|
||||||
|
for i in range(seg):
|
||||||
|
a = j * (seg + 1) + i; b = a + seg + 1
|
||||||
|
I += [a, a + 1, b, a + 1, b + 1, b]
|
||||||
|
return P, N, U, I
|
||||||
|
|
||||||
|
parts = [box(0.4), ball(0.3, 1.1)]
|
||||||
|
blob2, views2, accs2, prims2 = b'', [], [], []
|
||||||
|
for m, (P, N, U, I) in enumerate(parts):
|
||||||
|
for data, fmt, typ, comp in ((P, 'f', 'VEC3', 5126), (N, 'f', 'VEC3', 5126), (U, 'f', 'VEC2', 5126), (I, 'I', 'SCALAR', 5125)):
|
||||||
|
b = struct.pack(f'<{len(data)}{fmt}', *data)
|
||||||
|
views2.append({'buffer': 0, 'byteOffset': len(blob2), 'byteLength': len(b)}); blob2 += b
|
||||||
|
cnt = len(data) // {'VEC3': 3, 'VEC2': 2, 'SCALAR': 1}[typ]
|
||||||
|
acc = {'bufferView': len(views2) - 1, 'componentType': comp, 'count': cnt, 'type': typ}
|
||||||
|
if typ == 'VEC3' and len(accs2) % 4 == 0:
|
||||||
|
acc['min'] = [min(P[k::3]) for k in range(3)]; acc['max'] = [max(P[k::3]) for k in range(3)]
|
||||||
|
accs2.append(acc)
|
||||||
|
k = m * 4
|
||||||
|
prims2.append({'attributes': {'POSITION': k, 'NORMAL': k + 1, 'TEXCOORD_0': k + 2}, 'indices': k + 3, 'material': m})
|
||||||
|
with open(os.path.join(OUT, 'probe.bin'), 'wb') as f:
|
||||||
|
f.write(blob2)
|
||||||
|
Image.new('RGB', (4, 4), (196, 110, 52)).save(os.path.join(OUT, 'probe_box.png'))
|
||||||
|
Image.new('RGB', (4, 4), (225, 225, 220)).save(os.path.join(OUT, 'probe_ball.png'))
|
||||||
|
Image.new('RGB', (4, 4), (255, 140, 0)).save(os.path.join(OUT, 'probe_arm.png'))
|
||||||
|
doc2 = {
|
||||||
|
'asset': {'version': '2.0', 'generator': 'ludic tools/lab/plate_build.py'},
|
||||||
|
'scene': 0, 'scenes': [{'nodes': [0]}],
|
||||||
|
'nodes': [{'name': 'probe', 'mesh': 0}],
|
||||||
|
'meshes': [{'name': 'probe', 'primitives': prims2}],
|
||||||
|
'materials': [
|
||||||
|
{'name': 'probe_box', 'pbrMetallicRoughness': {'baseColorTexture': {'index': 0}, 'metallicRoughnessTexture': {'index': 2}}},
|
||||||
|
{'name': 'probe_ball', 'pbrMetallicRoughness': {'baseColorTexture': {'index': 1}, 'metallicRoughnessTexture': {'index': 2}}},
|
||||||
|
],
|
||||||
|
'textures': [{'source': 0}, {'source': 1}, {'source': 2}],
|
||||||
|
'images': [{'uri': 'probe_box.png'}, {'uri': 'probe_ball.png'}, {'uri': 'probe_arm.png'}],
|
||||||
|
'buffers': [{'uri': 'probe.bin', 'byteLength': len(blob2)}],
|
||||||
|
'bufferViews': views2,
|
||||||
|
'accessors': accs2,
|
||||||
|
}
|
||||||
|
with open(os.path.join(OUT, 'probe.gltf'), 'w') as f:
|
||||||
|
json.dump(doc2, f, indent=1)
|
||||||
20
tools/lab/run.sh
Executable file
20
tools/lab/run.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# run.sh <lab.ludic> [<scene> ...]: build a ludic.lab program headless, render every shot of each
|
||||||
|
# scene to build/lab/<scene>/<shot>.png and make build/lab/<scene>/sheet.png of them.
|
||||||
|
# LAB_NOBUILD=1 reuse build/lab_headless as it is LUDIC=<ludic> the CLI to build with
|
||||||
|
# With no scene it lists the program's scenes.
|
||||||
|
set -e
|
||||||
|
prog=$1
|
||||||
|
[ -n "$prog" ] || { echo "usage: run.sh <lab.ludic> [scene ...]" >&2; exit 2; }
|
||||||
|
shift
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
LUDIC=${LUDIC:-ludic}
|
||||||
|
if [ -z "$LAB_NOBUILD" ]; then "$LUDIC" build "$prog" --headless -o build/lab_headless >/dev/null; fi
|
||||||
|
if [ $# -eq 0 ]; then echo q | R3D_SCENE=__list__ ./build/lab_headless 2>/dev/null | grep "no scene" || true; exit 0; fi
|
||||||
|
for scene in "$@"; do
|
||||||
|
out=build/lab/$scene
|
||||||
|
mkdir -p "$out"; rm -f "$out"/*.png "$out"/*.ppm
|
||||||
|
# one line of stdin per frame: the headless loop ends when it runs dry
|
||||||
|
yes '' | head -n 20000 | R3D_SCENE=$scene R3D_TEST_ROOT=lab_$scene ./build/lab_headless > "$out/run.log" 2>&1 || true
|
||||||
|
python3 "$here/sheet.py" "$out"
|
||||||
|
done
|
||||||
29
tools/lab/sheet.py
Executable file
29
tools/lab/sheet.py
Executable file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""sheet.py DIR [DIR ...] - a contact sheet of a lab scene's shots: DIR/sheet.png, the PNGs in DIR
|
||||||
|
(sheet.png itself excepted) in name order, two to a row at 640 wide, each captioned with its name."""
|
||||||
|
import glob, os, sys
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
def sheet(out):
|
||||||
|
paths = [p for p in sorted(glob.glob(os.path.join(out, '*.png'))) if os.path.basename(p) != 'sheet.png']
|
||||||
|
if not paths:
|
||||||
|
print(f'lab: {out}: no shots')
|
||||||
|
return 1
|
||||||
|
ims = [Image.open(p).convert('RGB') for p in paths]
|
||||||
|
w = 640
|
||||||
|
h = w * ims[0].height // ims[0].width
|
||||||
|
cols = 2 if len(ims) > 1 else 1
|
||||||
|
rows = (len(ims) + cols - 1) // cols
|
||||||
|
s = Image.new('RGB', (w * cols, h * rows), (0, 0, 0))
|
||||||
|
d = ImageDraw.Draw(s)
|
||||||
|
for i, (p, im) in enumerate(zip(paths, ims)):
|
||||||
|
x, y = (i % cols) * w, (i // cols) * h
|
||||||
|
s.paste(im.resize((w, h)), (x, y))
|
||||||
|
d.rectangle((x, y, x + w, y + 18), fill=(0, 0, 0))
|
||||||
|
d.text((x + 6, y + 3), os.path.splitext(os.path.basename(p))[0], fill=(255, 255, 255))
|
||||||
|
s.save(os.path.join(out, 'sheet.png'))
|
||||||
|
print(f'lab: {out}: {len(ims)} shots, sheet.png')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(max([sheet(d) for d in sys.argv[1:]] or [2]))
|
||||||
|
|
@ -446,6 +446,31 @@ function expect_fp_fail_case() -> void {
|
||||||
if got == `{src}:4: expect_near failed (got 1.5, want 2)` { ok(lbl) } else { bad2(lbl, `said [{got}]`) }
|
if got == `{src}:4: expect_near failed (got 1.5, want 2)` { ok(lbl) } else { bad2(lbl, `said [{got}]`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ludic.lab: a scene on the plate, headless, three shots as PNGs, in well under a gigabyte
|
||||||
|
function lab_plate_case() -> void {
|
||||||
|
let lbl = "ludic.lab: the example's three shots on the plate, as PNGs, under 1 GB"
|
||||||
|
if not is_darwin() { skip(lbl); return }
|
||||||
|
let bin = `{tmp_dir()}/lab_plate`
|
||||||
|
if not shq(`bin/ludic build packages/ludic.lab/example/plate.ludic --headless -o {bin} > {tmp_dir()}/lab.out 2>&1`) { bad2(lbl, capture_line(`grep -i error {tmp_dir()}/lab.out | head -1`)); return }
|
||||||
|
# from the checkout's root, where the renderer's shaders and the plate are found beside it
|
||||||
|
let work = `{tmp_dir()}/labrun`
|
||||||
|
shell(`rm -rf {work} build/lab/probe && mkdir -p {work}`)
|
||||||
|
if not shq(`yes '' | head -300 | /usr/bin/time -l {bin} > {work}/run.out 2> {work}/time.out`) { bad2(lbl, capture_line(`tail -1 {work}/run.out`)); return }
|
||||||
|
var n = 0
|
||||||
|
let names = ["front", "side", "above"]
|
||||||
|
for i in 0 .. 3 {
|
||||||
|
if shq(`file build/lab/probe/{names[i]}.png | grep -q 'PNG image data, 1920 x 1080'`) { n += 1 }
|
||||||
|
}
|
||||||
|
let last = capture_line(`tail -1 {work}/run.out`)
|
||||||
|
if n != 3 { bad2(lbl, `{string(n)} of 3 shots are PNGs: {last}`); return }
|
||||||
|
let pl = s_trim(capture_line(`grep 'peak memory footprint' {work}/time.out`))
|
||||||
|
var k = 0
|
||||||
|
while k < len(pl) and pl[k] >= '0' and pl[k] <= '9' { k += 1 }
|
||||||
|
let peak = pl[0 .. k]
|
||||||
|
if len(peak) >= 10 { bad2(lbl, `peak memory footprint {peak} bytes`); return }
|
||||||
|
ok(`{lbl} (peak {peak} bytes)`)
|
||||||
|
}
|
||||||
|
|
||||||
# every package's own tests, the way a package author runs them: `ludic test packages`
|
# every package's own tests, the way a package author runs them: `ludic test packages`
|
||||||
function packages_test_case() -> void {
|
function packages_test_case() -> void {
|
||||||
let lbl = "ludic test packages: every package's tests pass"
|
let lbl = "ludic test packages: every package's tests pass"
|
||||||
|
|
@ -1013,6 +1038,7 @@ function cmd_dev_test() -> int {
|
||||||
package_scripts_case()
|
package_scripts_case()
|
||||||
test_dir_case()
|
test_dir_case()
|
||||||
packages_test_case()
|
packages_test_case()
|
||||||
|
lab_plate_case()
|
||||||
pack_roundtrip_case()
|
pack_roundtrip_case()
|
||||||
packignore_case()
|
packignore_case()
|
||||||
os_dirs_case()
|
os_dirs_case()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue