Characters#
What a puppet must declare, and how a declared puppet becomes a posed figure in panel coordinates. The solver sees only this contract, never artwork.
scenet.assets.contract#
.. py:module:: scenet.assets.contract
What a character puppet must declare.
The solver never sees artwork – only this contract. That is what keeps rendering swappable: the same panel lays out identically whether it is drawn as wireframe boxes, as vector puppets, or eventually as real artwork.
A character is a skeleton plus parametric limbs rather than a picture, so a pose is a set of joint angles and not a drawing. That avoids the combinatorial explosion of one image per pose per expression per facing direction.
.. py:class:: AnchorSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictA named point that rides along with a joint.
Anchors are how the solver addresses anatomy without knowing anatomy: the balloon tail terminates at
mouth, and it neither knows nor cares how the head is drawn... py:attribute:: AnchorSpec.joint
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: AnchorSpec.offset
- module:
scenet.assets.contract
- type:
tuple[float, float]
.. py:class:: BlobPart
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictA rounded mass – the head, or a torso – centred on a joint.
.. py:attribute:: BlobPart.at
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: BlobPart.radius
- module:
scenet.assets.contract
- type:
float
.. py:attribute:: BlobPart.offset
- module:
scenet.assets.contract
- type:
tuple[float, float]
.. py:class:: BonePart
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictA limb segment drawn as a capsule between two joints.
.. py:attribute:: BonePart.from_joint
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: BonePart.to_joint
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: BonePart.width
- module:
scenet.assets.contract
- type:
float
.. py:class:: FaceSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictThe region a balloon may never cover.
A circle rather than a polygon: faces are roughly round, the test is cheap, and the cost of being slightly generous here is a balloon placed a little further away, which is never wrong.
.. py:attribute:: FaceSpec.joint
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: FaceSpec.radius
- module:
scenet.assets.contract
- type:
float
.. py:attribute:: FaceSpec.offset
- module:
scenet.assets.contract
- type:
tuple[float, float]
.. py:class:: GazeSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictWhere a character’s line of sight starts.
.. attribute:: origin
Name of a declared anchor, conventionally
eyes. Validated to exist.The direction is not stored: it is derived at solve time from whom the character is looking at, so a
looking_atrelation is enough and nobody has to compute an angle by hand... py:attribute:: GazeSpec.origin
- module:
scenet.assets.contract
- type:
str
.. py:class:: JointSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictOne joint in the skeleton.
offsetis the rest-pose displacement from the parent joint, in native units. A joint’s pose angle rotates the bone arriving at it and everything below it, which is the formulation that makes posing read naturally: bendingelbow_lswings the upper arm and takes the forearm and hand with it... py:attribute:: JointSpec.parent
- module:
scenet.assets.contract
- type:
str | None
.. py:attribute:: JointSpec.offset
- module:
scenet.assets.contract
- type:
tuple[float, float]
.. py:class:: Landmark
- module:
scenet.assets.contract
Bases: :py:class:
~enum.StrEnumVertical body landmarks, measured downward from the top of the head.
These are the crop lines a shot type names – see docs/reference/shot_types.md.
.. py:attribute:: Landmark.HEAD_TOP
- module:
scenet.assets.contract
- value:
‘head_top’
.. py:attribute:: Landmark.EYES
- module:
scenet.assets.contract
- value:
‘eyes’
.. py:attribute:: Landmark.CHIN
- module:
scenet.assets.contract
- value:
‘chin’
.. py:attribute:: Landmark.SHOULDERS
- module:
scenet.assets.contract
- value:
‘shoulders’
.. py:attribute:: Landmark.CHEST
- module:
scenet.assets.contract
- value:
‘chest’
.. py:attribute:: Landmark.WAIST
- module:
scenet.assets.contract
- value:
‘waist’
.. py:attribute:: Landmark.MID_THIGH
- module:
scenet.assets.contract
- value:
‘mid_thigh’
.. py:attribute:: Landmark.KNEES
- module:
scenet.assets.contract
- value:
‘knees’
.. py:attribute:: Landmark.FEET
- module:
scenet.assets.contract
- value:
‘feet’
.. py:method:: Landmark.new(value)
- module:
scenet.assets.contract
.. py:class:: PuppetLibrary
- module:
scenet.assets.contract
Bases: :py:class:
objectPuppets loaded from a directory of
*.puppet.yamlfiles... py:method:: PuppetLibrary.init(puppets)
- module:
scenet.assets.contract
Wrap an already-loaded mapping of puppets.
- type puppets:
- sphinx_autodoc_typehints_type:
\:py\:class\:\dict`\ \[:py:class:`str`, :py:class:`~scenet.assets.contract.PuppetSpec`]`
- param puppets:
Puppet name to specification. Usually built by
- meth:
from_directory <scenet.assets.contract.PuppetLibrary.from_directory>rather than passed in directly – but constructing one by hand is how you supply your own characters without touching the filesystem.
.. py:method:: PuppetLibrary.from_directory(directory)
- module:
scenet.assets.contract
- classmethod:
Load every `*.puppet.yaml` in a directory. :type directory: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~pathlib.Path\`` :param directory: Directory to scan. Not searched recursively. :rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~typing.Self\`` :returns: A library containing every puppet found. :raises ValueError: Two files declare the same puppet name. :raises AssetError: A file is not a YAML mapping. Files are visited in sorted order so that a duplicate-name collision reports the same offender on every platform, whatever order the filesystem hands them back.
.. py:method:: PuppetLibrary.get(name) :module: scenet.assets.contract
Look up one puppet by name. :type name: :sphinx_autodoc_typehints_type:`\:py\:class\:\`str\`` :param name: The name a cast member's `reference` field points at. :rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`\~scenet.assets.contract.PuppetSpec\`` :returns: That puppet's specification. :raises UnknownPuppetError: No puppet by that name. The message lists what is
available, because the usual cause is a typo.
.. py:method:: PuppetLibrary.names() :module: scenet.assets.contract
Every puppet name in this library, sorted. .. admonition:: Example >>> from scenet import default_library >>> default_library().names() ('alice', 'bob') :rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`tuple\`\\ \\\[\:py\:class\:\`str\`\, \:py\:data\:\`...\<Ellipsis\>\`\]`
.. py:class:: PuppetSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictThe complete geometric contract for one character.
.. py:attribute:: PuppetSpec.name
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: PuppetSpec.units_per_head
- module:
scenet.assets.contract
- type:
float
.. py:attribute:: PuppetSpec.landmarks
- module:
scenet.assets.contract
- type:
dict[~scenet.assets.contract.Landmark, float]
.. py:attribute:: PuppetSpec.joints
- module:
scenet.assets.contract
- type:
dict[str, ~scenet.assets.contract.JointSpec]
.. py:attribute:: PuppetSpec.root
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: PuppetSpec.root_landmark
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.Landmark
.. py:attribute:: PuppetSpec.parts
- module:
scenet.assets.contract
- type:
tuple[~scenet.assets.contract.BonePart | ~scenet.assets.contract.BlobPart, …]
.. py:attribute:: PuppetSpec.anchors
- module:
scenet.assets.contract
- type:
dict[str, ~scenet.assets.contract.AnchorSpec]
.. py:attribute:: PuppetSpec.face
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.FaceSpec
.. py:attribute:: PuppetSpec.gaze
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.GazeSpec
.. py:attribute:: PuppetSpec.poses
- module:
scenet.assets.contract
- type:
dict[str, dict[str, float]]
.. py:method:: PuppetSpec.check_landmarks_complete_and_ordered()
- module:
scenet.assets.contract
Require every landmark, in head-to-foot order.
- rtype:
:sphinx_autodoc_typehints_type:
\:py\:class\:\~typing.Self``- returns:
The validated puppet.
- raises ValueError:
A landmark is missing,
head_topis not zero, or the values do not increase downward.
All nine landmarks are required rather than optional-with-defaults because any shot type may crop at any of them, so a puppet missing one is a puppet that cannot be framed at some perfectly ordinary shot.
.. py:method:: PuppetSpec.check_skeleton_is_a_tree()
- module:
scenet.assets.contract
Require the skeleton to be a tree rooted at
root.- rtype:
:sphinx_autodoc_typehints_type:
\:py\:class\:\~typing.Self``- returns:
The validated puppet.
- raises ValueError:
The root is undefined or has a parent, a joint names a parent that does not exist, or a joint sits in a cycle.
Forward kinematics accumulates each joint’s transform from its parent’s. A cycle would make that non-terminating and an orphan would leave a limb with no defined position, so both are rejected here rather than discovered at pose time.
.. py:method:: PuppetSpec.check_joint_references()
- module:
scenet.assets.contract
Require every joint name mentioned anywhere to exist.
Covers parts, anchors, the face, the gaze origin, and every angle in every declared pose.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~typing.Self``
- returns:
The validated puppet.
- raises ValueError:
Something references a joint or anchor that is not declared.
.. py:property:: PuppetSpec.total_height
- module:
scenet.assets.contract
- type:
float
Head top to feet, in the puppet’s own native units.
.. py:property:: PuppetSpec.heads_tall
- module:
scenet.assets.contract
- type:
float
Height in head-heights – the classic figure-drawing proportion.
The unit the camera works in. Two puppets of different
heads_tallframed at the same shot produce figures of visibly different build, which is the whole reason the shipped library has a 7.5-head character and a taller one.
.. py:method:: PuppetSpec.pose_angles(pose)
- module:
scenet.assets.contract
Look up the joint angles for a named pose.
- type pose:
:sphinx_autodoc_typehints_type:
\:py\:class\:\str``- param pose:
Name of a pose this puppet declares.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\dict`\ \[:py:class:`str`, :py:class:`float`]`
- returns:
Joint name to angle in degrees. Joints absent from the mapping keep their rest angle.
- raises KeyError:
This puppet has no pose by that name. The message lists the ones it does have.
.. py:class:: Strict
- module:
scenet.assets.contract
Bases: :py:class:
~pydantic.main.BaseModelBase for every puppet model: frozen, and rejecting unknown keys.
A misspelled key in a puppet file that was silently ignored would produce a character that is subtly wrong – an arm the wrong length, an anchor in the wrong place – with nothing to point at.
.. py:function:: default_library()
- module:
scenet.assets.contract
Load the puppets shipped with Scenet.
Two characters of deliberately different build, so that a bug in camera scaling cannot hide behind two figures that happen to be the same height.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.assets.contract.PuppetLibrary``
- returns:
A library containing
aliceandbob.
.. admonition:: Example
from scenet import default_library library = default_library() round(library.get(“alice”).heads_tall, 1) 7.5
.. py:function:: load_puppet(path)
- module:
scenet.assets.contract
Read one
*.puppet.yamlfile into a validated specification.- type path:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~pathlib.Path``
- param path:
The puppet file to read.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.assets.contract.PuppetSpec``
- returns:
The validated puppet, ready to be posed.
- raises AssetError:
The file is not a YAML mapping.
- raises pydantic.ValidationError:
The mapping is not a well-formed puppet – an out-of-order landmark, a skeleton that is not a tree, a joint referring to a parent that does not exist.
.. admonition:: Example
from scenet import default_library, load_puppet from scenet.assets.contract import DEFAULT_LIBRARY_PATH alice = load_puppet(DEFAULT_LIBRARY_PATH / “alice.puppet.yaml”) alice.name ‘alice’ round(alice.heads_tall, 1) 7.5
.. seealso::
- meth:
PuppetLibrary.from_directory <scenet.assets.contract.PuppetLibrary.from_directory>, to read a whole directory at once.
scenet.assets.kinematics#
.. py:module:: scenet.assets.kinematics
Forward kinematics: skeleton plus pose, resolved into concrete geometry.
Everything downstream – camera scaling, actor placement, balloon avoidance, tail routing, rendering – consumes the output of this module and nothing else from the asset layer. That boundary is the whole point: swap the puppet for hand-drawn artwork exposing the same anchors and hulls, and layout is unchanged.
.. py:class:: ResolvedCapsule
- module:
scenet.assets.kinematics
Bases: :py:class:
objectA limb segment: a thick line with rounded ends.
.. py:attribute:: ResolvedCapsule.start
- module:
scenet.assets.kinematics
- type:
~scenet.geom.Point
.. py:attribute:: ResolvedCapsule.end
- module:
scenet.assets.kinematics
- type:
~scenet.geom.Point
.. py:attribute:: ResolvedCapsule.width
- module:
scenet.assets.kinematics
- type:
float
.. py:method:: ResolvedCapsule.init(start, end, width)
- module:
scenet.assets.kinematics
.. py:class:: ResolvedBlob
- module:
scenet.assets.kinematics
Bases: :py:class:
objectA rounded mass – head, hand, foot – after posing.
.. attribute:: centre
Where it sits, in panel coordinates.
.. attribute:: radius
Radius in panel units, already scaled by the camera.
.. py:attribute:: ResolvedBlob.centre
- module:
scenet.assets.kinematics
- type:
~scenet.geom.Point
.. py:attribute:: ResolvedBlob.radius
- module:
scenet.assets.kinematics
- type:
float
.. py:method:: ResolvedBlob.init(centre, radius)
- module:
scenet.assets.kinematics
.. py:class:: ResolvedPuppet
- module:
scenet.assets.kinematics
Bases: :py:class:
objectA posed figure in panel coordinates.
Produced once per actor per compile, then treated as read-only by every consumer.
.. py:attribute:: ResolvedPuppet.name
- module:
scenet.assets.kinematics
- type:
str
.. py:attribute:: ResolvedPuppet.pose
- module:
scenet.assets.kinematics
- type:
str
.. py:attribute:: ResolvedPuppet.facing_right
- module:
scenet.assets.kinematics
- type:
bool
.. py:attribute:: ResolvedPuppet.scale
- module:
scenet.assets.kinematics
- type:
float
.. py:attribute:: ResolvedPuppet.joints
- module:
scenet.assets.kinematics
- type:
dict[str, ~scenet.geom.Point]
.. py:attribute:: ResolvedPuppet.anchors
- module:
scenet.assets.kinematics
- type:
dict[str, ~scenet.geom.Point]
.. py:attribute:: ResolvedPuppet.landmarks
- module:
scenet.assets.kinematics
- type:
dict[~scenet.assets.contract.Landmark, float]
.. py:attribute:: ResolvedPuppet.capsules
- module:
scenet.assets.kinematics
- type:
tuple[~scenet.assets.kinematics.ResolvedCapsule, …]
.. py:attribute:: ResolvedPuppet.blobs
- module:
scenet.assets.kinematics
- type:
tuple[~scenet.assets.kinematics.ResolvedBlob, …]
.. py:attribute:: ResolvedPuppet.face
- module:
scenet.assets.kinematics
- type:
~scenet.geom.Circle
.. py:attribute:: ResolvedPuppet.gaze
- module:
scenet.assets.kinematics
- type:
~scenet.geom.Vector
.. py:attribute:: ResolvedPuppet.hull
- module:
scenet.assets.kinematics
- type:
tuple[~scenet.geom.Point, …]
.. py:property:: ResolvedPuppet.bounds
- module:
scenet.assets.kinematics
- type:
~scenet.geom.BBox
Axis-aligned bounds of the posed silhouette.
.. py:method:: ResolvedPuppet.anchor(name)
- module:
scenet.assets.kinematics
Look up one named attachment point in panel coordinates.
- type name:
- sphinx_autodoc_typehints_type:
\:py\:class\:\str``
- param name:
An anchor the puppet declared –
mouthandeyesare the ones the compiler itself relies on.- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.geom.Point``
- returns:
Where that anchor ended up after posing, scaling and mirroring.
- raises KeyError:
This puppet declares no anchor by that name.
.. py:method:: ResolvedPuppet.init(name, pose, facing_right, scale, joints, anchors, landmarks, capsules, blobs, face, gaze, hull)
- module:
scenet.assets.kinematics
.. py:function:: solve_pose(spec, pose)
- module:
scenet.assets.kinematics
Resolve joint positions in the puppet’s own units, root at the origin.
Returns the joint positions and the accumulated world angle at each joint, the latter being what anchors and gaze need in order to ride along with rotation.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\tuple`\ \[:py:class:`dict`\ \[:py:class:`str`, :py:class:`~scenet.geom.Point`], :py:class:`dict`\ \[:py:class:`str`, :py:class:`float`]]`
.. py:function:: resolve(spec, *, pose, facing_right, scale, origin)
- module:
scenet.assets.kinematics
Pose, mirror, scale and place a puppet.
originis where the puppet’s root joint lands in panel coordinates. Mirroring happens in the puppet’s own frame before scaling, so a mirrored figure is the exact reflection of the original rather than being offset by rounding.- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.assets.kinematics.ResolvedPuppet``
.. py:function:: convex_hull(points)
- module:
scenet.assets.kinematics
Andrew’s monotone chain, returning hull vertices counter-clockwise.
Implemented here rather than delegated to shapely because it runs on a handful of points per actor and the result must be bit-for-bit reproducible; shapely is reserved for the genuinely hard polygon work in balloon placement.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\tuple`\ \[:py:class:`~scenet.geom.Point`, :py:data:`…<Ellipsis>`]`