Docs: VR pet, its models, credits and changes outside Nix

docs/pet.md (usage, configuration, how it works), docs/pet-models.md
(the model spec), the README's feature line, options, changes outside
Nix and Credits (Toon Cat FREE and Tuxedo Cat, CC-BY 4.0; Quaternius
Shiba Inu and Fox, CC0), the cleanup artifact, the layout and runtime
names, and the template.
This commit is contained in:
Pierre Kisters committed 2026-10-01 04:33:39 +02:00
1 parent 4034fd4ded
commit 77085bad8a
7 files changed
+440 -11

No files matched your search

+52 -4
View File
@@ -6,7 +6,7 @@ work around quirks of the Frame's [two graphical sessions](#two-sessions)
(portal, KDE app menu, keyboard layout, clipboard, KDE wallet, Firefox),
enable hardware video decoding in Jellyfin, run rootless Docker, and extend
Steam's and SteamVR's UIs at runtime (VR keyboard, "+" menu, dashboard
windows, Steam close button, window curvature, window controls).
windows, Steam close button, window curvature, window controls, a VR pet).
Everything is declarative: files are links into the Nix store, UI patches
live in memory. The few things that have to be written elsewhere at runtime
@@ -48,6 +48,7 @@ configuration, limitations and how it works.
- [Steam close button](docs/steam-close-button.md) (`dashboard.steamCloseButton`): an X that hides the Steam window.
- [Window curvature](docs/window-curvature.md) (`dashboard.windowCurvature`): adjustable curvature per window.
- [Window control bar](docs/window-control-bar.md) (`dashboard.frameControls`): move controls between bar and three-dot menu.
- [VR pet](docs/pet.md) (`pet`): a cat (five coats), Shiba Inu, Fox or Dachshund in the scene that walks around you, can be picked up and petted; "Pet" in the "+" menu, the `vr-pet` command, your own models.
- [SteamVR debugger](docs/steamvr-debugger.md) (`steamvrDebugger`, automatic): SteamVR's DevTools port for these, set only while SteamVR runs.
**Apps:**
@@ -194,6 +195,7 @@ these two files ([`template/`](template), with more comments):
windowCurvature.enable = true;
frameControls.enable = true;
};
pet.enable = true; # VR pet (bakes ~0.5 GB of models)
firefox.enable = true;
firefox.disableAv1 = true;
firefox.defaultBrowser = true;
@@ -217,7 +219,7 @@ home-manager switch --flake .#steamos # manual setup, from the flake's director
In your own flake, add the input as above and
`steam-frame-nix.homeManagerModules.default` to the modules. `default`
imports all modules; single ones:
`homeManagerModules.{session,portal,applications-menu,keyboard-layout,vr-keyboard-extra-keys,vr-keyboard,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,cleanup,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox,jellyfin,launchers,docker,screenshots}`
`homeManagerModules.{session,portal,applications-menu,keyboard-layout,vr-keyboard-extra-keys,vr-keyboard,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,cleanup,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox,jellyfin,launchers,docker,screenshots,pet}`
(`steam-keyboard-patch` and `keyring` still work as the former names of
`vr-keyboard-extra-keys` and `launchers`). Every module imports `cleanup` (see
[Changes outside Nix](#changes-outside-nix-exceptions)); which file is
@@ -303,6 +305,13 @@ which: [Repository layout](docs/development.md).
| `steamFrame.dashboard.frameControls.inBar` | list of control names | `[ ]` | Controls that start in the bar: `keyboard`, `float`, `dashboard`, `theater`, `dockLeft`, `dockRight`, `close`, `curvature`, `"icon:<n>"`. |
| `steamFrame.dashboard.frameControls.inMenu` | list of control names | `[ ]` | Controls that start in the three-dot menu. |
| `steamFrame.dashboard.frameControls.floatInTheater` | bool | `false` | "Float" control on theater windows. |
| `steamFrame.pet.enable` | bool | `false` | A 3D pet in SteamVR's scene, the `vr-pet` command and "Pet" in the "+" menu, see [VR pet](docs/pet.md); the first switch bakes its models (~0.5 GB in the store). |
| `steamFrame.pet.defaultModel` | null or str | `null` | Model id shown until another is picked; `null`: the spec's `"default": true` (the Ginger cat). |
| `steamFrame.pet.extraModels` | attrs of path or attrs | `{ }` | Your own models by id: a folder with a `model.json`, or the spec as an attrset (files as paths), see [VR pet models](docs/pet-models.md). |
| `steamFrame.pet.options` | attrs | `{ }` | Behaviour options of `modules/pet/core.js` (`DEFAULTS` there), e.g. `{ walkSpeed = 0.3; follow = 3; }`. |
| `steamFrame.pet.fps` | int, 4-60 | `24` | Animation frames per second (baked frames and scene graph updates): smoother, but more frames loaded in vrcompositor. |
| `steamFrame.pet.mount` | `"dynamic"`, `"all"` | `"dynamic"` | Baked frames kept mounted in the scene graph: the clips needed now or next, or all of them. |
| `steamFrame.pet.debug.demo` | bool | `false` | Demo tour: cycles through all poses and activities. |
| `steamFrame.steamvrDebugger.enable` | bool | automatic | SteamVR dashboard DevTools on `127.0.0.1:8087` (set only while SteamVR runs); on when a dashboard patch is, see [SteamVR debugger](docs/steamvr-debugger.md). |
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
| `steamFrame.clipboardSync.package` | package | built from `dnut/clipboard-sync` | The clipboard-sync package. |
@@ -371,11 +380,12 @@ lives in memory (the UI patches). These are written at runtime:
| `VRWebHelper.DebuggerEnabled` in `~/.config/openvr/config/steamvr.vrsettings` | [SteamVR debugger](docs/steamvr-debugger.md) | only while SteamVR runs | SteamVR stopping (runtime drop-in below); `steam-frame-nix-cleanup` while SteamVR is stopped |
| `~/.local/state/steam-frame-nix/steamvr-debugger.armed` | SteamVR debugger: the key's previous value | while SteamVR runs; after a power loss until the next SteamVR start or cleanup | SteamVR stopping; `steam-frame-nix-cleanup` |
| `/run/user/1000/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf`, `/run/user/1000/steam-frame-nix/steamvr-debugger-restore` | SteamVR debugger: puts the key back when SteamVR stops, without Nix | until reboot (tmpfs) | reboot; `steam-frame-nix-cleanup` while SteamVR is stopped and the debugger is off |
| `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | Saved choices of dashboard patches ([persistent state](docs/ui-patches.md#persistent-state)): window control bar placements (`frame-controls`), "Steam hidden" (`steam-close-button`). SteamOS's `steamvr.service` deletes `~/.cache/SteamVR` (the dashboard's own browser storage) on every SteamVR start. | until removed: kept when a patch is disabled (the choices come back when you enable it again) | `steam-frame-nix-cleanup --all`, `install.sh uninstall` |
| `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | Saved choices of dashboard patches ([persistent state](docs/ui-patches.md#persistent-state)): window control bar placements (`frame-controls`), "Steam hidden" (`steam-close-button`), the VR pet's spot, pose, model and whether it is hidden (`vr-pet`). SteamOS's `steamvr.service` deletes `~/.cache/SteamVR` (the dashboard's own browser storage) on every SteamVR start. | until removed: kept when a patch is disabled (the choices come back when you enable it again) | `steam-frame-nix-cleanup --all`, `install.sh uninstall` |
| `/run/user/1000/steam-frame-nix/applications/<id>.desktop` (with `.<id>.desktop.sum`, `.lock`) | [Launchers](docs/launchers.md) of Flatpaks and host files; `~/.local/share/applications/<id>.desktop` is a Home Manager link to it | until reboot (tmpfs), written again at login | reboot; the next switch or rewrite when the launcher or its app is gone; `steam-frame-nix-cleanup --all` |
| `/run/user/1000/steam-frame-nix/screenshots` | [SteamVR screenshots](docs/screenshots.md#how-it-works) without `steamUserId`: link to the current account's folder; `~/Pictures/<name>` is a Home Manager link to it | until reboot (tmpfs), written again at login | reboot; `steam-frame-nix-cleanup` once unused |
| `/run/user/1000/steam-frame-nix/vr-pet/icon.png` (with `.lock`) | [VR pet](docs/pet.md#how-it-works): link to the current model's "+" menu icon; `~/.local/share/icons/hicolor/256x256/apps/vr-pet.png` is a Home Manager link to it | until reboot (tmpfs), written again at login | reboot; `steam-frame-nix-cleanup` once unused |
| mtime of `~/.local/share/applications` | [Launchers](docs/launchers.md#how-it-works): a running Steam rescans the "+" menu | only the directory's timestamp | nothing to remove |
| mtime of `~/.local/share/icons/hicolor` | [icon fallbacks](docs/launcher-menu.md#icon-fallbacks): a running Steam rescans icons | only the directory's timestamp | nothing to remove |
| mtime of `~/.local/share/icons/hicolor` | [icon fallbacks](docs/launcher-menu.md#icon-fallbacks), [VR pet](docs/pet.md#how-it-works) icon: a running Steam rescans icons | only the directory's timestamp | nothing to remove |
**`steam-frame-nix-cleanup`** (`install.sh cleanup`,
`steamFrame.cleanup.package`) knows everything any version of
@@ -478,6 +488,44 @@ steam-frame-nix and switch: its activation runs `cleanup --all` while Home
Manager removes its files. (`home-manager uninstall` alone doesn't load
steam-frame-nix's modules, so it can't clean up after them.)
## Credits
The [VR pet](docs/pet.md)'s models are not in this repository: Nix fetches
them at build time from the URLs pinned (with their hashes) in
`modules/pet/package.nix` and `modules/pet/models/<id>/model.json`, and
bakes them into the store.
- **Toon Cat FREE** by [Omabuarts Studio](https://sketchfab.com/omabuarts)
([model](https://sketchfab.com/3d-models/toon-cat-free-b2bd1ee7858444bda366110a2d960386)),
[CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/): the cat (mesh,
texture, rig, walk) and its coats. Modified: recoloured coats (Tuxedo,
Blue, Cream, Snow), retargeted and hand-keyed animations, baked to one OBJ
per frame. Fetched from a third-party GitHub mirror
([DevTakao/threejs-cat](https://github.com/DevTakao/threejs-cat), pinned
commit and hash).
- **Tuxedo Cat Animated 2.0** by [DreamNoms](https://sketchfab.com/DreamNoms)
([model](https://sketchfab.com/3d-models/tuxedo-cat-animated-20-783fcb78b55b4394a212c2b6392e1113)),
[CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/): only its
SitDown, IdleSit and StandUp clips, retargeted onto the Toon Cat and
baked. Fetched from a third-party GitHub mirror
([xialin-he/xialin-he.github.io](https://github.com/xialin-he/xialin-he.github.io),
pinned commit and hash).
- **Shiba Inu** and **Fox** from the
[Ultimate Animated Animal Pack](https://quaternius.com/packs/ultimateanimatedanimals.html)
by [Quaternius](https://quaternius.com),
[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) (credited
anyway). Modified: clips sampled per frame, sit, lie, sleep and the
held-by-the-scruff pose hand-keyed, a wagging tail and breathing added,
material colours turned into a texture. Fetched from the Poly Pizza
mirror ([Shiba Inu](https://poly.pizza/m/y4wdQpg767),
[Fox](https://poly.pizza/m/Bc97C66HKi), pinned hashes).
- **Dachshund:** the Quaternius Shiba Inu (CC0 1.0), reshaped (longer back
and ears, shorter legs) and recoloured black and tan.
- **three.js** (desktop preview only), [MIT](https://github.com/mrdoob/three.js/blob/dev/LICENSE),
fetched from npm.
The baked cat's store output carries a short `CREDITS.md` pointing here.
## License
[Apache License 2.0](LICENSE).
+1
View File
@@ -27,6 +27,7 @@ is reported as "left alone" and never touched:
| `ui-state`: `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | the dashboard patches' saved choices; `--all` only, never `--orphans`; stray `*.json.tmp` files |
| `launchers`: `/run/user/1000/steam-frame-nix/applications` | the entries of [launchers](launchers.md) (tmpfs); `--all` only (switches remove those of removed launchers themselves) |
| `screenshots`: `/run/user/1000/steam-frame-nix/screenshots` | a link to `*/userdata/*/760/remote/250820/screenshots` ([SteamVR screenshots](screenshots.md)); kept by `--orphans --keep screenshots` (while `steamUserId` is unset) |
| `pet`: `/run/user/1000/steam-frame-nix/vr-pet` | `icon.png`, a link to `*-vr-pet-icons/*.png` in the store ([VR pet](pet.md#how-it-works)), its empty `.lock` and stray `.icon.tmp.*` links, then the directory if empty; kept by `--orphans --keep pet` (while `pet.enable`) |
| `dirs` | `~/.local/state/steam-frame-nix` and `/run/user/1000/steam-frame-nix` when empty |
### Left by older versions
+15 -1
View File
@@ -35,7 +35,7 @@ Where things run:
## Repository layout
```text
flake.nix homeManagerModules, packages/apps (cleanup), checks, template
flake.nix homeManagerModules, packages/apps (cleanup, pet-*), checks, template
install.sh install / uninstall / cleanup (curl | bash; also the
steam-frame-nix-cleanup package and the debugger arm step)
template/ `nix flake init -t` / installer config: flake.nix, home.nix
@@ -129,6 +129,19 @@ modules/
steam-close-button.nix, steam-close-button/ SteamVR: X on the Steam window
window-curvature.nix, window-curvature/ SteamVR: curvature wheel
frame-controls.nix, frame-controls/ SteamVR: window control bar
pet.nix the VR pet (pet): patch, CLI, "+" menu entry, icon units
pet/
package.nix build: bakes, catalog, patch, CLI, icons, preview, tests
core.js SteamVR+preview+test (in the patch): behaviour, interaction
systemui.js, unpatch.js SteamVR: draws the pet in the scene graph, grip bar, menu
cli.mjs app: `vr-pet` over DevTools (also the "+" menu entry)
icon.sh service steam-frame-nix-pet-icon (+ .path), switch:
<runtimeDir>/steam-frame-nix/vr-pet/icon.png -> model icon
bake.py, bake_gltf.py, rig.py build: the cat's / a glTF animal's frames (OBJ per frame)
index.py, skin.py, thumb.py build: catalog (models.json), coats, thumbnails, icons
models/<id>/model.json build: the built-in models (spec: docs/pet-models.md)
preview/ dev: desktop preview (`nix run .#pet-preview`)
check.nix, tests/ test: core, adapter, CLI, models, icons, private-model guard
```
## Runtime names
@@ -151,6 +164,7 @@ they are kept even where a file name says more:
| `steamvr-debugger` | `steamvrDebugger` | | `steamvr-webhelper-debugger` |
| `launchers` | `launchers` | | `steam-frame-nix-launchers` (`.service`, `.path`) |
| `screenshots` | `screenshots` | | `steam-frame-nix-screenshots` (`.service`, `.path`) |
| `pet` | `pet` | `vr-pet` (SteamVR, state) | `steam-ui-patches`, `steam-frame-nix-pet-icon` (`.service`, `.path`) |
| `docker` | `docker` | | `docker` |
Log of all patches:
+254
View File
@@ -0,0 +1,254 @@
# VR pet models
How to add a model to the [VR pet](pet.md): the spec's one description (the
scripts in `modules/pet/` point here). Credits of the built-in models:
[README, Credits](../README.md#credits).
Every model is a folder with a `model.json` (its spec) and any files the
spec names (a texture, a `.glb`, a thumbnail, a Blender script). The
built-in ones are `modules/pet/models/<id>/`, found by `builtins.readDir`
(nothing else lists them). Your own go into `steamFrame.pet.extraModels`:
either such a folder (`corgi = ./pets/corgi;`) or the spec as an attrset,
with files as Nix paths (`calico = { name = "Calico"; png = ./calico.png; };`).
Rebuild, and the model shows up in the pet's ⋯ menu and in
`vr-pet models`. The id (folder or attribute name, `vr-pet model <id>`):
letters, digits, `-` and `_`.
An invalid spec fails the build with a message naming the model and the field
(index.py `validate()`, bake_gltf.py for the animal fields).
## Fields (every kind)
| field | meaning |
|-----------|---------|
| `name` | the text in the menu (required) |
| `kind` | `base`, `recolor`, `texture` or `gltf` (required; an extraModels attrset with a `png` defaults to `texture`) |
| `order` | the menu's order (the coats 0-4, the animals 10-19; default 100), then the name |
| `default` | `true` on the one model shown until another is picked (the Ginger cat); `steamFrame.pet.defaultModel = "<id>"` overrides it |
| `group` | the menu puts a line between groups: `coats` (default for the cat's kinds) or `animals` (default for `gltf`) |
| `thumb` | a picture of the model's own (a file of the folder) for the menu, instead of a render (thumb.py: the sitting animal, else the standing one; 48 px) |
| `private` | `true`: personal use only, never published ([below](#private-models)); must be a boolean; models.json carries it for every model (`false` by default) |
| `rights` | who owns the character and why it is private (a text); a spec with `rights` must have `"private": true` |
| `extends` | another model's id: its spec with this one's fields on top ([below](#another-build-of-the-same-animal-extends-proportions-colors)) |
## The cat and its coats
- `base`: the Toon Cat, baked by bake.py (its sources are pinned in
package.nix). There is one.
- `recolor`: a coat of the cat, skin.py moving the ginger fur to another
ramp: `"fur": ["#rrggbb" (dark end), "#rrggbb" (light end)]`, optional
`"white": ["chest", "muzzle", "paws"]` markings.
- `texture`: a coat of the cat with a texture of its own: `"png": "coat.png"`,
a 64 x 64 PNG laid out like the cat's (start from an edited copy of
`nix build github:lhns/steam-frame-nix#pet-models`/cat.png: the body's texels are a
side view, columns along the length, each a dark-to-light ramp).
Coats share the cat's frames and behaviour; they switch in place.
## Other animals: kind `gltf`
A rigged, animated glTF (`.glb`, or `.gltf` with its files), baked by
bake_gltf.py into one OBJ per frame (as the cat) with its own behaviour set:
the core (core.js) gives an animal what its clips allow.
| field | meaning |
|-----------|---------|
| `src` | the glTF: a file of the folder (`"model.glb"`), `{ "url": ..., "hash": "sha256-..." }` (fetched, pinned) or `{ "path": ... }` (a file of the folder, e.g. downloaded by hand), optionally turned into a glTF (below) |
| `height` | m, the standing animal's height (top of the ears) in its first idle frame; the cat is 0.30 |
| `feet` | the four foot bones (their ends), for the walk speed: how fast the feet move back while on the floor |
| `zones` | bones for the interaction points: `scruff` (the top of the skin it moves: where it is held from), `head`, `back` (the top of the skin above the joint: petting), `chin`, `tailBase` (`{ "bone": ..., "at": "joint" }`: the joint itself) |
| `follow` | `{ bone: bone it moves with }`: bones outside the leg chains that the skin follows (IK targets, as the Quaternius feet); in hand-keyed poses they keep their place relative to that bone |
| `keys` | hand-keyed poses (below) |
| `clips` | the core's clip names -> how each is made (below); `idle` and `walk` are required |
| `species` | `{ "actions": { name: { "pose": "stand", "weight": 1, "cooldown": 30 } } }`: the animal's own idle actions (a clip of that name), chosen now and then in that pose like the cat's groom or stretch |
| `texture` | px, the width of a base colour texture in the atlas (default 256; 512 for a detailed face) |
| `proportions` | per bone `scale`, `rot`, `aim` on the rest pose: a reshaped animal (below) |
| `colors` | flat materials' colours: a recoloured animal (below) |
| `thumbFrame` | the baked frame the menu's render shows instead, e.g. `"idle_0"` (the Dachshund: its long body shows standing) |
| `credit` | the source and licence (a built-in model: also in [README, Credits](../README.md#credits)) |
### Sources that are not a glTF yet
`src` (`{url, hash}` or `{path}`) may also have (package.nix `gltfOf`); the
built-in models need none of them, a model from a `.blend` all three:
- `"unzip": "Dir/*"`: it is a zip; the members matching the glob are
unpacked. For a fetched one the `hash` is of those unpacked files (a
recursive hash), not of the zip (a service may zip it on the fly).
- `"blend": "Dir/model.blend"`: the .blend to open (its path in the zip).
- `"blender": "convert.py"`: a Blender script of the folder, run at build
time as `blender -b <blend> --python convert.py -- <out.glb>`; it exports
the glTF (and may clean it up: drop meshes, add bones, key clips,
decimate). The build fails when the script does or writes nothing.
Blender (`pkgs.blender`) is then a build input of that model only.
### Clips: how animations map
The core asks for clips by name. Each pose has a loop, transitions join
poses, actions are one-shot clips that start and end in their pose:
| pose | loop | into it (and back) |
|--------|-----------|-----------------------------------------------------|
| stand | `idle` | |
| walk | `walk` | `walkstart` (stand -> walk; played backwards to stop) |
| sit | `sit` | `sitdown` (stand -> sit), `standup` (sit -> stand) |
| lie | `lie` | `liedown` (stand -> lie; backwards to get up) |
| sleep | `sleep` | `curl` (lie -> sleep; backwards to wake) |
| petted | `purr`, `sitpurr`, `liepurr` | `purrin`, `sitpurrin`, `liepurrin` |
| held | `dangle` | (hangs from the scruff: `"hang": true`) |
| dropped| `fall` | `land` (an action: lands, ends standing) |
Actions: `startle` (a fast hand close by; then it backs off), `land`, the
cat's `groom` (sitting), `stretch`, `shake`, and the animal's own
(`species.actions`).
Whatever is missing falls back: a pose without its loop is not there
(commands and a saved pose take the nearest: sit, lie -> stand, sleep ->
lie); a transition without its clip is a cut; an action without its clip is
never chosen; petting without a purr loop does nothing; no `startle` / `land`
clip: it backs off / goes on at once; `dangle` / `fall` without their loops
show the nearest (fall, then stand).
Each clip is one of:
- `"Walk"`: that clip of the glTF, sampled at the baked fps (looping for a
pose loop).
- `{ "clip": "Jump_ToIdle", "start": 0.5, "end": 1.3, "fps": 12, "loop": false }`:
a part of it, at its own fps (12 for slow ones: fewer frames).
- `{ "clip": "Jump_ToIdle", "hold": 0.5 }` (or `"hold": "last"`): one frame
held (`"seconds"`, `"layers"` as below).
- `{ "key": "sit", "seconds": 3, "fps": 8, "layers": [...] }`: a hand-keyed
pose held; with layers a loop (at half the fps, or `fps`).
- `{ "blend": ["stand", "sit"], "seconds": 0.9 }`: a transition from the first
frame of one clip or key to another's.
- `{ "reverse": "sitdown" }`: another clip backwards.
- `"hang": true` on any: hung from the scruff (the dangle), not on the floor.
- `"lift": 0.12` on any: that many m above the floor (flying): a loop all
the time; a one-shot rises over its first quarter and settles over its
last (e.g. a flying animal's `hover` and `startle`).
A layer is a sine on top: `{ "bone": "Tail1", "rot": [0, 22, 0], "period": 0.4, "phase": 0.5 }`
(degrees about the body's axes: a wagging tail) or `"scale": [0.03, 0, 0.03]`
(breathing).
### Keys: hand-keyed poses
`{ "from": "Idle", "at": 0, "rot": { bone: [x, y, z] }, "aim": { bone: [x, y, z] } }`:
that frame of a glTF clip, with bones turned (degrees about the body's axes:
x its left, + = nose down; y up; z forward) and / or aimed (the bone, toward
its first child joint, pointed along a direction in the body's axes: `[0, -1, 0]`
straight down, `[0, -0.1, 1]` forward along the floor). Parents are done
first; a child turns with its parent. Every frame is put on the floor (its
lowest point), so fold the legs until the body rests where it should.
Check the poses by rendering frames (thumb.py renders an OBJ with the
texture; `nix run github:lhns/steam-frame-nix#pet-preview` plays them all).
Turn a head with the bone that carries it and its ears: in the Quaternius
rigs the ears hang from `Neck3`, not from `Head`, and the skull's top is
skinned to them, so turning `Head` alone twists the face under still ears.
The Shiba, Fox and Dachshund turn their heads at `Neck3` (with `Neck1` /
`Neck2` for the neck; the split keeps the head where a `Head` turn had put
it) and relax the ears on top at `Ear2` (sleep).
### Budget and texture
- **Faces:** at most 3000 triangles per frame (the cat has 2636): vrcompositor
draws every mounted frame, and a few hundred are mounted. The bake fails
above it: decimate the model first (e.g. Blender's Decimate modifier).
- **Texture:** a render model has one texture. bake_gltf.py makes it:
flat material colours become a palette (one 8-texel column per material,
a little darker toward the floor by the vertex's height when standing:
baked shading like the Toon Cat's), a material's base colour texture is
stacked below it (UVs in 0..1), `texture` px wide (default 256).
Everything is `cat.png` / `cat.mtl` next to the frames (the file names
are shared by all models).
- **Facing:** the glTF's +Z forward, +Y up (as Blender's glTF export).
### A full example (the Shiba Inu, shortened)
```json
{
"name": "Shiba Inu", "kind": "gltf", "order": 10,
"credit": "Shiba Inu, Ultimate Animated Animal Pack by Quaternius, CC0 1.0",
"src": { "url": "https://static.poly.pizza/ba6d0ee3-bcc0-4ef0-9d3c-a3e245b41c77.glb",
"hash": "sha256-nL1PHhL4UCQSbqtqLFcGwJkZbfGhDbdObItnusP/9Uo=" },
"height": 0.35,
"feet": ["FrontLowerLeg.L_end", "FrontLowerLeg.R_end", "BackLowerLeg.L_end", "BackLowerLeg.R_end"],
"follow": { "IKFrontLeg.L": "FrontLowerLeg.L", "IKFrontLeg.R": "FrontLowerLeg.R",
"IKBackLeg.L": "BackLowerLeg.L", "IKBackLeg.R": "BackLowerLeg.R" },
"zones": { "scruff": "Neck2", "head": "Head", "back": "Torso2",
"chin": { "bone": "Head", "at": "joint" }, "tailBase": { "bone": "Tail1", "at": "joint" } },
"keys": {
"stand": { "from": "Idle" },
"sit": { "from": "Idle",
"rot": { "Back": [-38, 0, 0], "Neck1": [8, 0, 0], "Neck2": [6, 0, 0], "Neck3": [18, 0, 0] },
"aim": { "FrontUpperLeg.L": [0.03, -1, 0.05], "BackLeg.L": [0.2, -0.3, 1],
"BackUpperLeg.L": [0.05, -0.35, -1], "BackLowerLeg.L": [0.05, -0.2, 1] } }
},
"clips": {
"idle": { "clip": "Idle", "fps": 12 },
"walk": "Walk",
"walkstart": { "blend": ["stand", "walk"], "seconds": 0.3 },
"sit": { "key": "sit", "seconds": 3, "layers": [{ "bone": "Torso2", "scale": [0.025, 0, 0.025], "period": 3 }] },
"sitdown": { "blend": ["stand", "sit"], "seconds": 0.9 },
"standup": { "reverse": "sitdown" },
"purr": { "key": "stand", "seconds": 1.2, "fps": 24,
"layers": [{ "bone": "Tail1", "rot": [0, 22, 0], "period": 0.4 }] },
"fall": { "clip": "Jump_ToIdle", "hold": 0.5 },
"land": { "clip": "Jump_ToIdle", "start": 0.5 },
"startle": { "clip": "Idle_HitReact_Left", "loop": false },
"sniff": { "clip": "Eating", "fps": 12, "loop": false }
},
"species": { "actions": { "sniff": { "pose": "stand", "weight": 1.2, "cooldown": 45 } } }
}
```
The complete ones in `modules/pet/models/`: `shiba/model.json`,
`fox/model.json` (same rig), `dachshund/model.json` (the Shiba reshaped:
below).
### Another build of the same animal: `extends`, `proportions`, `colors`
`"extends": "<id>"` starts from another model's spec: this one's fields go
on top (an object field entry by entry, so `"keys": { "sit": … }` replaces
only that key; the other's `default` and `thumb` are not taken). Files are
looked up in this folder, then in that model's. With these two fields
(bake_gltf.py) the same glTF and clips make a different animal:
- `proportions`: `{ bone: { "scale": [x, y, z], "rot": [x, y, z], "aim": [x, y, z] } }`,
kept in every clip. `scale`: the skin the bone moves, in the bone's own
axes (y along the bone, toward its child), from its joint; its children's
joints move with it but are not scaled (a longer back keeps its legs).
`rot` / `aim`: the bone's rest pose turned / aimed as in a key (body
axes), on top of the clip's own motion (ears hanging, a straight tail).
`follow` bones (IK feet) stay where they were relative to their bone,
that offset scaled with it, turned as in the clip (flat paws).
- `colors`: `{ material name: "#rrggbb" }`: a flat material's colour (the
glTF's material names; the Quaternius dogs: `Main`, `Main_Light`,
`Black`, `Eyes_*`).
The Dachshund (`dachshund/model.json`) is the Shiba Inu this way: back and
torso ×1.25-1.45 long, legs ×0.42-0.5, muzzle ×1.3, the ears longer and aimed
down and out, the tail straightened, black and tan; the Shiba's clips and
behaviour, with its own `sit` key (the Shiba's leans back too far for short
front legs to reach the floor). Short legs lift less, so a reshaped walk's
speed counts only the feet moving back. Check the face from the front too
when rendering frames (above): a bone's
`scale` / `rot` / `aim` also moves the skin weighted partly to it (the
Shiba's eyes and brow are about a fifth `Ear1`, so the Dachshund's ears
bend at `Ear2`, `Ear1` only tilted 20° out).
## Private models
A model of a character someone else owns (the licence of a fan model does
not cover the character) is for personal use only: its spec has
`"private": true` and a `rights` note saying whose it is. index.py checks
both (`private` a boolean; `rights` only with `"private": true`), and
models.json marks it (`"private": true`).
Such a model goes into `steamFrame.pet.extraModels` from your own
configuration (a folder in your repository), never into
`modules/pet/models/`: a built-in model with `"private": true` fails
evaluation (package.nix `builtinOf`; the `pet` check tests the refusal and
that a private extra model builds).
+109
View File
@@ -0,0 +1,109 @@
# VR pet
`pet.*`, module `pet` ([options](../README.md#options)).
A [SteamVR dashboard patch](ui-patches.md#steamvr-dashboard-patches).
## What you get
A 3D pet in SteamVR's scene, next to your windows: the Toon Cat in five
coats (Ginger, Tuxedo, Blue, Cream, Snow), a Shiba Inu, a Fox and a
Dachshund, plus your own models (`pet.extraModels`). It lives in the
dashboard's scene, so it is there in SteamVR Home and over games while the
dashboard runs.
- **On its own** it walks around you, sits, lies down, sleeps, follows you
beyond 2.5 m and reappears in front of you beyond 8 m. The cat also
grooms, stretches and shakes; the dogs and the fox sniff and look around.
- **Pick it up:** point the laser just above it (a grip bar shows up, like
a window's), press and drag like a window (the thumbstick pushes and
pulls). It dangles by the scruff, turns with the controller and falls
where you let go. No controller button is read; a falling pet is not
caught.
- **Pet it:** rest or move a controller slowly (< 0.4 m/s) on its head or
back (within 10 cm) for 0.6 s: it purrs (a dog wags its tail) while the
strokes go on. A fast swipe (> 1 m/s) close by startles it.
- **Its controls**, below the grip bar: X hides it (kept across SteamVR
restarts); ⋯ opens its menu: the models (a check on the current one, kept
across restarts), then Summon (1 m in front of you), Sit, Lie down,
Sleep. Buttons act on a press and a release on them, not during a drag or
0.3 s after. The menu closes on a pick, a press elsewhere, 1 s after the
laser left it, and when the pet hides.
- **Bring it back:** "Pet" in the dashboard's "+" menu (its icon is the
current model's), i.e. `vr-pet show`: at its spot while that is within
3 m and in view, else 1 m in front of you.
The `vr-pet` command:
```text
vr-pet show [--summon] show it (--summon: always 1 m in front of you)
vr-pet hide | status hide it; its state as JSON
vr-pet models the models (* the current one)
vr-pet model <id> switch (a hidden pet switches too)
```
## Configuration
```nix
steamFrame.pet.enable = true;
# steamFrame.pet.defaultModel = "shiba";
# steamFrame.pet.options = { walkSpeed = 0.3; follow = 3; }; # core.js DEFAULTS
# steamFrame.pet.extraModels.corgi = ./pets/corgi; # a model folder
```
The first switch bakes the models: a few minutes on the Frame and about
0.5 GB in the store (1.1 GB without `auto-optimise-store`, which hardlinks
the coats' copies of the cat's frames); the sources are fetched at build
time (no Blender needed for the built-in models). Adding your own model, the spec and the
animation mapping: [VR pet models](pet-models.md). A model of a character
someone else owns belongs in your own `extraModels` with `"private": true`
([private models](pet-models.md#private-models)), never in this repository.
Without a headset: `nix run github:lhns/steam-frame-nix#pet-preview` plays
the pet in a browser (mouse as the hands, a debug panel).
## Limitations
- vrcompositor draws every mounted frame: a few hundred static OBJs per
model, mounted gradually (`pet.mount`); switching to another animal takes
a few seconds to load.
- The pet is drawn by the dashboard's page: it is gone while SteamVR's
dashboard process restarts, and the patch reattaches within 15 s.
- It walks on the floor of the standing space and knows nothing of your
room's furniture.
- The "+" menu icon changes when the model does; a running Steam picks it
up when it rescans its icons (a few seconds).
## Credits
The models' authors and licences: [README, Credits](../README.md#credits).
## How it works
`vr-pet`, with [persistent state](ui-patches.md#persistent-state) (the
spot, pose, model and whether it is hidden). Debugging in the `systemui`
page: `window.__sfuiPet.cat.command('sit' | 'summon' | …)`,
`.cat.state()`, `.stats()`; the CLI calls only `window.__sfuiPet.api`.
Journal: `journalctl --user -t vr-pet -u steam-ui-patches`.
- **Build** (`modules/pet/package.nix`): bake.py retargets and hand-keys
the Toon Cat's clips and bakes one OBJ per animation frame; bake_gltf.py
does the same for a rigged glTF (the Quaternius animals); index.py makes
the catalog (coats as recoloured textures of the cat's frames, one
directory per model, 48 px thumbnails) and the "+" menu icons. Each
animal is a derivation of its own.
- **Behaviour** (`core.js`): poses, clips and transitions, wandering,
following, petting, the scruff drag and the fall, independent of where
it is drawn (also used by the preview and the tests).
- **Drawing** (`systemui.js`): every frame is a render model node under one
world-locked root in the dashboard's scene graph; the frames a clip needs
are mounted ahead, unused ones dropped. Each change resends the
dashboard's last scene graph message with only the pet's subtree
replaced. The grip bar is a panel like a window's handle; while dragged
the root is parented to the controller, so the compositor carries it.
- **"+" menu icon:** `~/.local/share/icons/hicolor/256x256/apps/vr-pet.png`
links to `/run/user/1000/steam-frame-nix/vr-pet/icon.png`, which the
oneshot user service `steam-frame-nix-pet-icon` points to the current
model's icon: on switch, at login and when the state file changes (path
unit); it bumps hicolor's mtime so a running Steam rescans. Lifetime and
cleanup: [Changes outside Nix](../README.md#changes-outside-nix-exceptions).
+6 -6
View File
@@ -47,15 +47,15 @@ and `sudo systemctl disable` them.
[Dashboard windows](dashboard-windows.md),
[Steam close button](steam-close-button.md),
[window curvature](window-curvature.md) and
[window control bar](window-control-bar.md) patch SteamVR's dashboard while
it runs. What they have in common:
[window curvature](window-curvature.md),
[window control bar](window-control-bar.md) and the [VR pet](pet.md) patch
SteamVR's dashboard while it runs. What they have in common:
- Each turns on the [SteamVR debugger](steamvr-debugger.md) (**the first
time, restart SteamVR once**).
- They depend on SteamVR UI internals: each is found by signature (its
entry in `modules/steam-ui-patches/lib/signatures.json` has the patch's
name); after an update that changes them the dashboard stays stock (see
name; the VR pet's are inline in `modules/pet/systemui.js`); after an update that changes them the dashboard stays stock (see
[after a Steam update](#after-a-steam-update)). Tested with SteamVR build
11008059.
- All are laser-only: gamepad navigation sees the stock dashboard.
@@ -102,8 +102,8 @@ per such patch in `~/.local/state/steam-frame-nix/ui-patches/<name>.json`
service writes the file atomically, only on change, only for that page's
`state` patches, at most 64 KiB.
Used by the [window control bar](window-control-bar.md) and the
[Steam close button](steam-close-button.md). The file is user data, not
Used by the [window control bar](window-control-bar.md), the
[Steam close button](steam-close-button.md) and the [VR pet](pet.md). The file is user data, not
generated by Nix: it is kept when the patch is disabled or removed and
removed only by `steam-frame-nix-cleanup --all` or `install.sh uninstall`;
see [changes outside Nix](../README.md#changes-outside-nix-exceptions).
+3
View File
@@ -54,6 +54,9 @@
# # move it between the bar and the three-dot menu:
# frameControls.enable = true;
# };
# # A 3D cat or dog in SteamVR's scene; "Pet" in the "+" menu (the
# # first switch bakes its models, about 0.5 GB):
# pet.enable = true;
# firefox.enable = true; # launcher for the Firefox Flatpak
# # the Frame has no AV1 decoder: sites send VP9/H.264 (hardware):
# firefox.disableAv1 = true;