mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
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:
1 parent
4034fd4ded
commit
77085bad8a
7 files changed
+440
-11
No files matched your search
@@ -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).
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user