Add WIP puppetry code
This commit is contained in:
111
addon_examples/puppetry_example.py
Normal file
111
addon_examples/puppetry_example.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Control a puppetry-enabled viewer and make your neck spin like crazy
|
||||
|
||||
It currently requires a custom rebased Firestorm with puppetry applied on top,
|
||||
and patches applied on top to make startup LEAP scripts be treated as puppetry modules.
|
||||
Basically, you probably don't want to use this yet. But hey, Puppetry is still only
|
||||
on the beta grid anyway.
|
||||
"""
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
from typing import *
|
||||
|
||||
import outleap
|
||||
|
||||
from hippolyzer.lib.base.datatypes import Quaternion
|
||||
from hippolyzer.lib.proxy.addon_utils import BaseAddon, SessionProperty
|
||||
from hippolyzer.lib.proxy.sessions import Session
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BodyPartMask(enum.IntFlag):
|
||||
"""Which joints to send the viewer as part of "move" puppetry command"""
|
||||
HEAD = 1 << 0
|
||||
FACE = 1 << 1
|
||||
LHAND = 1 << 2
|
||||
RHAND = 1 << 3
|
||||
FINGERS = 1 << 4
|
||||
|
||||
|
||||
def register_puppetry_command(func: Callable[[dict], Awaitable[None]]):
|
||||
"""Register a method as handling inbound puppetry commands from the viewer"""
|
||||
func._puppetry_command = True
|
||||
return func
|
||||
|
||||
|
||||
class PuppetryExampleAddon(BaseAddon):
|
||||
server_skeleton: Dict[str, Dict[str, Any]] = SessionProperty(dict)
|
||||
camera_num: int = SessionProperty(0)
|
||||
parts_active: BodyPartMask = SessionProperty(lambda: BodyPartMask(0x1F))
|
||||
puppetry_api: Optional[outleap.LLPuppetryAPI] = SessionProperty(None)
|
||||
leap_client: Optional[outleap.LEAPClient] = SessionProperty(None)
|
||||
|
||||
def handle_session_init(self, session: Session):
|
||||
if not session.leap_client:
|
||||
return
|
||||
self.puppetry_api = outleap.LLPuppetryAPI(session.leap_client)
|
||||
self.leap_client = session.leap_client
|
||||
self._schedule_task(self._serve())
|
||||
self._schedule_task(self._exorcist(session))
|
||||
|
||||
@register_puppetry_command
|
||||
async def enable_parts(self, args: dict):
|
||||
if (new_mask := args.get("parts_mask")) is not None:
|
||||
self.parts_active = BodyPartMask(new_mask)
|
||||
|
||||
@register_puppetry_command
|
||||
async def set_camera(self, args: dict):
|
||||
if (camera_num := args.get("camera_num")) is not None:
|
||||
self.camera_num = BodyPartMask(camera_num)
|
||||
|
||||
@register_puppetry_command
|
||||
async def stop(self, _args: dict):
|
||||
LOG.info("Viewer asked us to stop puppetry")
|
||||
|
||||
@register_puppetry_command
|
||||
async def log(self, _args: dict):
|
||||
# Intentionally ignored, we don't care about things the viewer
|
||||
# asked us to log
|
||||
pass
|
||||
|
||||
@register_puppetry_command
|
||||
async def set_skeleton(self, args: dict):
|
||||
# Don't really care about what the viewer thinks the view of the skeleton is.
|
||||
# Just log store it.
|
||||
self.server_skeleton = args
|
||||
|
||||
async def _serve(self):
|
||||
"""Handle inbound puppetry commands from viewer in a loop"""
|
||||
async with self.leap_client.listen_scoped("puppetry.controller") as listener:
|
||||
while True:
|
||||
msg = await listener.get()
|
||||
cmd = msg["command"]
|
||||
handler = getattr(self, cmd, None)
|
||||
if handler is None or not hasattr(handler, "_puppetry_command"):
|
||||
LOG.warning(f"Unknown puppetry command {cmd!r}: {msg!r}")
|
||||
continue
|
||||
await handler(msg.get("args", {}))
|
||||
|
||||
async def _exorcist(self, session):
|
||||
"""Do the Linda Blair thing with your neck"""
|
||||
spin_rad = 0.0
|
||||
while True:
|
||||
await asyncio.sleep(0.05)
|
||||
if not session.main_region:
|
||||
continue
|
||||
# Wrap spin_rad around if necessary
|
||||
while spin_rad > math.pi:
|
||||
spin_rad -= math.pi * 2
|
||||
|
||||
# LEAP wants rot as a quaternion with just the imaginary parts.
|
||||
neck_rot = Quaternion.from_euler(0, 0, spin_rad).data(3)
|
||||
self.puppetry_api.move({
|
||||
"mNeck": {"no_constraint": True, "local_rot": neck_rot},
|
||||
})
|
||||
spin_rad += math.pi / 25
|
||||
|
||||
|
||||
addons = [PuppetryExampleAddon()]
|
||||
@@ -5,13 +5,14 @@ Serialization templates for structures used in LLUDP and HTTP bodies.
|
||||
import abc
|
||||
import collections
|
||||
import dataclasses
|
||||
import enum
|
||||
import math
|
||||
import zlib
|
||||
from typing import *
|
||||
|
||||
import hippolyzer.lib.base.serialization as se
|
||||
from hippolyzer.lib.base import llsd
|
||||
from hippolyzer.lib.base.datatypes import UUID, IntEnum, IntFlag, Vector3
|
||||
from hippolyzer.lib.base.datatypes import UUID, IntEnum, IntFlag, Vector3, Quaternion
|
||||
from hippolyzer.lib.base.namevalue import NameValuesSerializer
|
||||
|
||||
|
||||
@@ -2055,3 +2056,69 @@ class RetrieveNavMeshSrcSerializer(se.BaseHTTPSerializer):
|
||||
# 15 bit window size, gzip wrapped
|
||||
deser["navmesh_data"] = zlib.decompress(deser["navmesh_data"], wbits=15 | 32)
|
||||
return deser
|
||||
|
||||
|
||||
# Beta puppetry stuff, subject to change!
|
||||
|
||||
|
||||
class PuppetryEventMask(enum.IntFlag):
|
||||
POSITION = 1 << 0
|
||||
POSITION_IN_PARENT_FRAME = 1 << 1
|
||||
ROTATION = 1 << 2
|
||||
ROTATION_IN_PARENT_FRAME = 1 << 3
|
||||
SCALE = 1 << 4
|
||||
DISABLE_CONSTRAINT = 1 << 7
|
||||
|
||||
|
||||
class PuppetryOption(se.OptionalFlagged):
|
||||
def __init__(self, flag_val, spec):
|
||||
super().__init__("mask", se.IntFlag(PuppetryEventMask, se.U8), flag_val, spec)
|
||||
|
||||
|
||||
# Range to use for puppetry's quantized floats when converting to<->from U16
|
||||
LL_PELVIS_OFFSET_RANGE = (-5.0, 5.0)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PuppetryJointData:
|
||||
# Where does this number come from? `avatar_skeleton.xml`?
|
||||
joint_id: int = se.dataclass_field(se.S16)
|
||||
# Determines which fields will follow
|
||||
mask: PuppetryEventMask = se.dataclass_field(se.IntFlag(PuppetryEventMask, se.U8))
|
||||
rotation: Optional[Quaternion] = se.dataclass_field(
|
||||
# These are very odd scales for a quantized quaternion, but that's what they are.
|
||||
PuppetryOption(PuppetryEventMask.ROTATION, se.PackedQuat(se.Vector3U16(*LL_PELVIS_OFFSET_RANGE))),
|
||||
)
|
||||
position: Optional[Vector3] = se.dataclass_field(
|
||||
PuppetryOption(PuppetryEventMask.POSITION, se.Vector3U16(*LL_PELVIS_OFFSET_RANGE)),
|
||||
)
|
||||
scale: Optional[Vector3] = se.dataclass_field(
|
||||
PuppetryOption(PuppetryEventMask.SCALE, se.Vector3U16(*LL_PELVIS_OFFSET_RANGE)),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PuppetryEventData:
|
||||
time: int = se.dataclass_field(se.S32)
|
||||
# Must be set manually due to below issue
|
||||
num_joints: int = se.dataclass_field(se.U16)
|
||||
# This field is packed in the least helpful way possible. The length field
|
||||
# is in between the collection count and the collection data, but the length
|
||||
# field essentially only tells you how many bytes until the end of the buffer
|
||||
# proper, which you already know from msgsystem. Why is this here?
|
||||
joints: List[PuppetryJointData] = se.dataclass_field(se.TypedByteArray(
|
||||
se.U32,
|
||||
# Just treat contents as a greedy collection, tries to keep reading until EOF
|
||||
se.Collection(None, se.Dataclass(PuppetryJointData)),
|
||||
))
|
||||
|
||||
|
||||
@se.subfield_serializer("AgentAnimation", "PhysicalAvatarEventList", "TypeData")
|
||||
@se.subfield_serializer("AvatarAnimation", "PhysicalAvatarEventList", "TypeData")
|
||||
class PuppetryEventDataSerializer(se.SimpleSubfieldSerializer):
|
||||
# You can have multiple joint events packed in right after the other, implicitly.
|
||||
# They may _or may not_ be split into separate PhysicalAvatarEventList blocks?
|
||||
# This doesn't seem to be handled specifically in the decoder, is this a
|
||||
# serialization bug in the viewer?
|
||||
TEMPLATE = se.Collection(None, se.Dataclass(PuppetryEventData))
|
||||
EMPTY_IS_NONE = True
|
||||
|
||||
Reference in New Issue
Block a user