mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 05:00:24 +02:00
VR pet: a cat or dog in SteamVR's scene (steamFrame.pet)
A SteamVR dashboard patch (vr-pet) drawing a baked 3D pet next to the windows: the Toon Cat in five coats, a Shiba Inu, a Fox and a Dachshund, plus models from steamFrame.pet.extraModels. It walks around, follows, sits, lies and sleeps, can be picked up by its grip bar, petted and switched in its menu; its spot, pose and model are saved state. Also the vr-pet command, "Pet" in the "+" menu with the current model's icon (steam-frame-nix-pet-icon), the flake outputs pet-models, pet-icons and pet-preview, and the check pet. A built-in model with "private": true fails evaluation; private models belong in extraModels.
This commit is contained in:
1 parent
fbf9519be0
commit
4034fd4ded
38 files changed
+7216
-5
No files matched your search
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks of bake_gltf.py's reshaping ("proportions": per-bone scale / rot /
|
||||
aim, IK followers) and "colors", on a tiny synthetic skinned glTF.
|
||||
usage: gltf.test.py (exit 1 on failure)
|
||||
Run by the flake check `pet` (package.nix `tests`).
|
||||
|
||||
The rig: root -> A (at the origin, pointing up +y) -> B (1 up); IK (a child
|
||||
of the root, 2 up: the tip of B) that follows B. One vertex per bone,
|
||||
weighted fully to it: a (0.1, 0.5, 0) on A, b (0, 1.5, 0) on B, c (0, 2, 0)
|
||||
on IK, d (-0.1, 0.5, 0) on A.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
import bake_gltf # noqa: E402
|
||||
|
||||
failed = 0
|
||||
|
||||
|
||||
def test(name):
|
||||
def run(f):
|
||||
global failed
|
||||
try:
|
||||
f()
|
||||
print(f'ok {name}')
|
||||
except (AssertionError, SystemExit) as e:
|
||||
failed += 1
|
||||
print(f'FAIL {name}: {e}')
|
||||
return run
|
||||
|
||||
|
||||
def rig_gltf(d):
|
||||
P = np.array([[0.1, 0.5, 0], [0, 1.5, 0], [0, 2, 0], [-0.1, 0.5, 0]], np.float32)
|
||||
N = np.tile(np.array([[0, 0, 1]], np.float32), (4, 1))
|
||||
J = np.array([[0, 0, 0, 0], [1, 0, 0, 0], [2, 0, 0, 0], [0, 0, 0, 0]], np.uint16)
|
||||
W = np.array([[1, 0, 0, 0]] * 4, np.float32)
|
||||
I = np.array([0, 1, 2, 0, 2, 3], np.uint16)
|
||||
rest = {0: (0, 0, 0), 1: (0, 1, 0), 2: (0, 2, 0)} # joints' world positions
|
||||
IBM = np.concatenate([np.linalg.inv(np.array([[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], np.float32)).T.ravel()
|
||||
for x, y, z in rest.values()]).astype(np.float32)
|
||||
blobs = [P, N, J, W, I, IBM]
|
||||
views, acc, off, buf = [], [], 0, b''
|
||||
kinds = [('VEC3', 5126, 4), ('VEC3', 5126, 4), ('VEC4', 5123, 4), ('VEC4', 5126, 4), ('SCALAR', 5123, 6), ('MAT4', 5126, 3)]
|
||||
for arr, (typ, ct, n) in zip(blobs, kinds):
|
||||
raw = arr.tobytes()
|
||||
views.append({'buffer': 0, 'byteOffset': off, 'byteLength': len(raw)})
|
||||
a = {'bufferView': len(views) - 1, 'componentType': ct, 'count': n, 'type': typ}
|
||||
if arr is P:
|
||||
a['min'], a['max'] = P.min(0).tolist(), P.max(0).tolist()
|
||||
acc.append(a)
|
||||
raw += b'\0' * (-len(raw) % 4)
|
||||
buf += raw
|
||||
off += len(raw)
|
||||
j = {
|
||||
'asset': {'version': '2.0'}, 'scene': 0, 'scenes': [{'nodes': [0, 4]}],
|
||||
'nodes': [{'name': 'root', 'children': [1, 3]}, {'name': 'A', 'children': [2]},
|
||||
{'name': 'B', 'translation': [0, 1, 0]}, {'name': 'IK', 'translation': [0, 2, 0]},
|
||||
{'name': 'mesh', 'mesh': 0, 'skin': 0}],
|
||||
'skins': [{'joints': [1, 2, 3], 'inverseBindMatrices': 5}],
|
||||
'meshes': [{'primitives': [{'attributes': {'POSITION': 0, 'NORMAL': 1, 'JOINTS_0': 2, 'WEIGHTS_0': 3},
|
||||
'indices': 4, 'material': 0}]}],
|
||||
'materials': [{'name': 'Fur', 'pbrMetallicRoughness': {'baseColorFactor': [1, 0, 0, 1]}}],
|
||||
'buffers': [{'uri': 'rig.bin', 'byteLength': len(buf)}], 'bufferViews': views, 'accessors': acc,
|
||||
}
|
||||
open(os.path.join(d, 'rig.bin'), 'wb').write(buf)
|
||||
path = os.path.join(d, 'rig.gltf')
|
||||
json.dump(j, open(path, 'w'))
|
||||
return path
|
||||
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
PATH = rig_gltf(tmp)
|
||||
|
||||
|
||||
def posed(proportions=None, follow=None):
|
||||
m = bake_gltf.Model(PATH)
|
||||
if proportions is not None:
|
||||
m.reshape(proportions, follow)
|
||||
p, n, _ = m.posed(m.rest)
|
||||
return p, n
|
||||
|
||||
|
||||
def near(a, b, what):
|
||||
assert np.allclose(a, b, atol=1e-5), f'{what}: {np.round(a, 4).tolist()} != {b}'
|
||||
|
||||
|
||||
@test('no proportions: the rest pose as modelled')
|
||||
def _():
|
||||
p, _ = posed()
|
||||
near(p, [[0.1, 0.5, 0], [0, 1.5, 0], [0, 2, 0], [-0.1, 0.5, 0]], 'rest')
|
||||
|
||||
|
||||
@test('scale: the bone\'s own skin along its axes; its child moves, not scaled itself')
|
||||
def _():
|
||||
p, _ = posed({'A': {'scale': [3, 2, 1]}})
|
||||
near(p[0], [0.3, 1.0, 0], 'a (on A, scaled)')
|
||||
near(p[1], [0, 2.5, 0], 'b (on B: B moved to 2, half a unit above it, unscaled)')
|
||||
near(p[2], [0, 2, 0], 'c (IK, not following: stays)')
|
||||
|
||||
|
||||
@test('follow: an IK bone keeps its place relative to the bone, the offset scaled with it')
|
||||
def _():
|
||||
p, _ = posed({'A': {'scale': [1, 2, 1]}}, {'IK': 'B'})
|
||||
near(p[2], [0, 3, 0], 'c follows B (moved up 1)')
|
||||
p, _ = posed({'B': {'scale': [1, 0.5, 1]}}, {'IK': 'B'})
|
||||
near(p[1], [0, 1.25, 0], 'b (B shortened)')
|
||||
near(p[2], [0, 1.5, 0], 'c at the shortened tip of B')
|
||||
|
||||
|
||||
@test('aim / rot: the rest pose turned in the body\'s axes, children with it')
|
||||
def _():
|
||||
p, _ = posed({'A': {'aim': [1, 0, 0]}}, {'IK': 'B'})
|
||||
near(p[0], [0.5, -0.1, 0], 'a (A aimed along +x)')
|
||||
near(p[1], [1.5, 0, 0], 'b (B carried)')
|
||||
near(p[2], [2, 0, 0], 'c (following B)')
|
||||
p, _ = posed({'A': {'rot': [0, 0, -90]}})
|
||||
near(p[1], [1.5, 0, 0], 'b (A turned -90 about z)')
|
||||
|
||||
|
||||
@test('proportions survive the clip\'s own rotation of the bone')
|
||||
def _():
|
||||
m = bake_gltf.Model(PATH)
|
||||
m.reshape({'A': {'scale': [1, 2, 1]}, 'B': {'rot': [0, 0, -90]}})
|
||||
pose = [list(x) for x in m.rest]
|
||||
pose[m['A']][1] = np.array([0, 0, np.sin(np.pi / 4), np.cos(np.pi / 4)]) # the clip turns A +90 about z
|
||||
p, _, _ = m.posed(pose)
|
||||
near(p[0], [-1.0, 0.1, 0], 'a (A pointing -x, twice as long)')
|
||||
near(p[1], [-2, 0.5, 0], 'b (B at -2, turned back up by its own -90)')
|
||||
|
||||
|
||||
@test('normals stay unit length and perpendicular under a non-uniform scale')
|
||||
def _():
|
||||
p, n = posed({'A': {'scale': [3, 1, 0.5]}})
|
||||
assert np.allclose(np.linalg.norm(n, axis=1), 1, atol=1e-6), n
|
||||
|
||||
|
||||
@test('invalid proportions fail naming the bone / field')
|
||||
def _():
|
||||
for props, want in [({'Nope': {'scale': [1, 1, 1]}}, "no bone 'Nope'"),
|
||||
({'A': {'scale': [1, 0, 1]}}, '"scale" must be three numbers'),
|
||||
({'B': {'aim': [1, 0, 0]}}, '"aim" needs a bone with a child')]:
|
||||
try:
|
||||
bake_gltf.Model(PATH).reshape(props)
|
||||
except SystemExit as e:
|
||||
assert want in str(e), f'{want!r} not in {e}'
|
||||
else:
|
||||
raise AssertionError(f'{props}: no error')
|
||||
|
||||
|
||||
@test('colors: "#rrggbb" (sRGB) -> linear, and back in the palette')
|
||||
def _():
|
||||
near(bake_gltf.linear('#ffffff'), [1, 1, 1], 'white')
|
||||
near(bake_gltf.linear('#000000'), [0, 0, 0], 'black')
|
||||
c = bake_gltf.linear('#3a2b24')
|
||||
near(bake_gltf.srgb(c) * 255, [0x3a, 0x2b, 0x24], 'round trip')
|
||||
|
||||
|
||||
if failed:
|
||||
print(f'{failed} failed')
|
||||
sys.exit(1)
|
||||
print('all passed')
|
||||
Reference in new issue
Block a user