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:: BrowState
- module:
scenet.assets.contract
Bases: :py:class:
~enum.StrEnumWhat the eyebrows are doing.
angled_inputs the inner ends down, which is the anger brow;angled_outputs them up, which is the sad or frightened one. Inner and outer are resolved against the face centre rather than the screen, so both survive mirroring... py:attribute:: BrowState.NEUTRAL
- module:
scenet.assets.contract
- value:
‘neutral’
.. py:attribute:: BrowState.RAISED
- module:
scenet.assets.contract
- value:
‘raised’
.. py:attribute:: BrowState.LOWERED
- module:
scenet.assets.contract
- value:
‘lowered’
.. py:attribute:: BrowState.ANGLED_IN
- module:
scenet.assets.contract
- value:
‘angled_in’
.. py:attribute:: BrowState.ANGLED_OUT
- module:
scenet.assets.contract
- value:
‘angled_out’
.. py:method:: BrowState.new(value)
- module:
scenet.assets.contract
.. py:class:: ExpressionSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictOne named expression, as a state per feature.
An expression is to features what a pose is to joints: a record the panel selects by name. A face deforms rather than rotating about bones, which is why this is a set of states and not a set of angles – a nose joint would swing a nose.
Every field defaults, so a
neutralexpression is{}and a happy one is{mouth: smile}. Unknown keys are rejected, somouth: raised– a real state, on the wrong feature – is an error rather than a silently ignored line... py:attribute:: ExpressionSpec.brow
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.BrowState
.. py:attribute:: ExpressionSpec.eyes
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.EyeState
.. py:attribute:: ExpressionSpec.mouth
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.MouthState
.. py:class:: EyeState
- module:
scenet.assets.contract
Bases: :py:class:
~enum.StrEnumHow open the eyes are.
halfis the heavy-lidded eye of boredom, distinct fromnarrowed, which is the squint of anger or suspicion: one is drooping, the other is tightened... py:attribute:: EyeState.OPEN
- module:
scenet.assets.contract
- value:
‘open’
.. py:attribute:: EyeState.WIDE
- module:
scenet.assets.contract
- value:
‘wide’
.. py:attribute:: EyeState.NARROWED
- module:
scenet.assets.contract
- value:
‘narrowed’
.. py:attribute:: EyeState.HALF
- module:
scenet.assets.contract
- value:
‘half’
.. py:attribute:: EyeState.CLOSED
- module:
scenet.assets.contract
- value:
‘closed’
.. py:method:: EyeState.new(value)
- module:
scenet.assets.contract
.. py:class:: FaceSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictThe region a balloon may never cover, and what is drawn inside it.
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.
featuresis optional. A puppet that declares none is drawn exactly as it was before faces existed – a head is a filled circle – and a puppet that declares some has those drawn and no others. There is no requirement to declare all of them, because a stylised character genuinely may have no eyebrows... 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:attribute:: FaceSpec.features
- module:
scenet.assets.contract
- type:
dict[~scenet.assets.contract.Feature, ~scenet.assets.contract.FeatureSpec]
.. py:class:: Feature
- module:
scenet.assets.contract
Bases: :py:class:
~enum.StrEnumThe points on a face that an expression can move.
Named after the groups of the MPEG-4 FBA facial definition parameters – brow, eye, nose, mouth – rather than its full point set. The standard defines 66 displacements over dozens of points, which is a measurement rather than a notation; the grouping is the part worth reusing, and it lands at about the right size for a drawn face.
docs/reference/asset_contract.mdrecords the mapping to MediaPipe landmark indices, which is what buys convertibility later without importing 478 points into the language now.Two omissions are deliberate. Pupils are derived, not declared – they are offset inside the eye by where the character is looking, so authoring them would be authoring something the compiler already knows. And there is no jaw: the head is a circle that does not deform, so a jaw group would have no geometry to move.
.. py:attribute:: Feature.BROW_L
- module:
scenet.assets.contract
- value:
‘brow_l’
.. py:attribute:: Feature.BROW_R
- module:
scenet.assets.contract
- value:
‘brow_r’
.. py:attribute:: Feature.EYE_L
- module:
scenet.assets.contract
- value:
‘eye_l’
.. py:attribute:: Feature.EYE_R
- module:
scenet.assets.contract
- value:
‘eye_r’
.. py:attribute:: Feature.NOSE
- module:
scenet.assets.contract
- value:
‘nose’
.. py:attribute:: Feature.MOUTH
- module:
scenet.assets.contract
- value:
‘mouth’
.. py:property:: Feature.is_paired
- module:
scenet.assets.contract
- type:
bool
Whether this feature is one of a left/right pair.
.. py:property:: Feature.twin
- module:
scenet.assets.contract
- type:
~scenet.assets.contract.Feature | None
The other half of a left/right pair, or None for a single feature.
.. py:method:: Feature.new(value)
- module:
scenet.assets.contract
.. py:class:: FeatureSpec
- module:
scenet.assets.contract
Bases: :py:class:
~scenet.assets.contract.StrictWhere one facial feature sits, and how big it is.
Structurally an :class:
AnchorSpec <scenet.assets.contract.AnchorSpec>with a size, and resolved through exactly the same forward kinematics – but declared underfacerather than inanchors, deliberately. Anchors are how the solver addresses anatomy: the balloon tail terminates atmouthand the solver never learns how a head is drawn. Features are artwork. Mixing them would put drawing landmarks into the one namespace that is supposed to be free of them... attribute:: joint
Which joint this feature rides. Practically always
head... attribute:: offset
Rest-pose displacement from that joint, in native units.
.. attribute:: size
What the number means depends on the feature – an eye’s radius, a brow’s half-width, a mouth’s half-width, a nose’s length. Zero means the feature has no extent and is drawn as a bare point, which is rarely what anybody wants.
.. py:attribute:: FeatureSpec.joint
- module:
scenet.assets.contract
- type:
str
.. py:attribute:: FeatureSpec.offset
- module:
scenet.assets.contract
- type:
tuple[float, float]
.. py:attribute:: FeatureSpec.size
- module:
scenet.assets.contract
- type:
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:: MouthState
- module:
scenet.assets.contract
Bases: :py:class:
~enum.StrEnumWhat the mouth is doing.
grinis an open smile,smilea closed one, andsmallthe reticent little mouth that does most of the work in a coy face... py:attribute:: MouthState.NEUTRAL
- module:
scenet.assets.contract
- value:
‘neutral’
.. py:attribute:: MouthState.FLAT
- module:
scenet.assets.contract
- value:
‘flat’
.. py:attribute:: MouthState.SMILE
- module:
scenet.assets.contract
- value:
‘smile’
.. py:attribute:: MouthState.GRIN
- module:
scenet.assets.contract
- value:
‘grin’
.. py:attribute:: MouthState.FROWN
- module:
scenet.assets.contract
- value:
‘frown’
.. py:attribute:: MouthState.OPEN
- module:
scenet.assets.contract
- value:
‘open’
.. py:attribute:: MouthState.SMALL
- module:
scenet.assets.contract
- value:
‘small’
.. py:method:: MouthState.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:attribute:: PuppetSpec.expressions
- module:
scenet.assets.contract
- type:
dict[str, ~scenet.assets.contract.ExpressionSpec]
.. 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:method:: PuppetSpec.check_face_is_drawable()
- module:
scenet.assets.contract
Require paired features to come in pairs, and expressions to be usable.
- rtype:
:sphinx_autodoc_typehints_type:
\:py\:class\:\~typing.Self``- returns:
The validated puppet.
- raises ValueError:
One half of a left/right pair is declared without the other, a puppet declares expressions but no features for them to move, or it declares expressions without a
neutralone.
The
neutralcheck is not pedantry.CastMember.expressiondefaults toneutral, so a puppet that declares expressions without one fails on every panel that does not name an expression explicitly – which is most of them.
.. 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 UnknownPoseError:
This puppet has no pose by that name. The message lists the ones it does have. Also a
KeyError, soexcept KeyErrorkeeps working.
.. py:method:: PuppetSpec.expression_states(expression)
- module:
scenet.assets.contract
Look up the feature states for a named expression.
The counterpart of :meth:
pose_angles <scenet.assets.contract.PuppetSpec.pose_angles>, and deliberately identical in shape down to the error: an expression is selected by name exactly as a pose is.- type expression:
:sphinx_autodoc_typehints_type:
\:py\:class\:\str``- param expression:
Name of an expression this puppet declares.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.assets.contract.ExpressionSpec``
- returns:
The states its features take.
- raises UnknownExpressionError:
This puppet has no expression by that name. The message lists the ones it does have. Also a
KeyError, soexcept KeyErrorkeeps working.
.. admonition:: Example
from scenet import default_library alice = default_library().get(“alice”) alice.expression_states(“angry”).mouth.value ‘frown’
.. 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:: ResolvedFeature
- module:
scenet.assets.kinematics
Bases: :py:class:
objectOne facial feature point after posing.
.. attribute:: centre
Where it sits, in panel coordinates.
.. attribute:: size
Its extent in panel units, already scaled. What the number measures depends on the feature – see
- class:
FeatureSpec <scenet.assets.contract.FeatureSpec>.
.. py:attribute:: ResolvedFeature.centre
- module:
scenet.assets.kinematics
- type:
~scenet.geom.Point
.. py:attribute:: ResolvedFeature.size
- module:
scenet.assets.kinematics
- type:
float
.. py:method:: ResolvedFeature.init(centre, size)
- 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:attribute:: ResolvedPuppet.expression
- module:
scenet.assets.kinematics
- type:
str
.. py:attribute:: ResolvedPuppet.features
- module:
scenet.assets.kinematics
- type:
dict[~scenet.assets.contract.Feature, ~scenet.assets.kinematics.ResolvedFeature]
.. 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, expression=’neutral’, features=
) - 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, expression=’neutral’)
- 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.expressionselects which face is drawn. It is validated and carried here, but the states it names are not applied until the marks are built – feature points are where the anatomy is, and are the same whatever the face is doing.- 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>`]`
scenet.assets.emanata#
.. py:module:: scenet.assets.emanata
Drawing emanata: the marks around a character that say what state they are in.
Plewds, squeans, grawlixes and briffits – Mort Walker’s names, from The Lexicon of
Comicana. See :class:Mark <scenet.ir.Mark> for what each one says.
Like a face, this is artwork, and lives in the asset layer for the same reason. Unlike a face, it is drawn outside the head circle, in the space balloons are placed in, so it has to say something to the solver. What it says is a zone per mark: a convex polygon around everything that mark draws. The solver reads the zones as a soft cost and never sees a plewd. Replace these drawings with hand-drawn ones that occupy the same zones and no layout changes.
Emanata never enter the hull. The hull is what staging spaces characters by, so a sweating character would otherwise stand further from everyone else, and the camera might retreat to fit them – a mark that moved the people in a panel would be saying something the author never wrote.
Everything is placed from the face circle, the facing direction and the feet landmark, which every puppet has. So marks need nothing from a puppet beyond the contract it already meets, and are not declared per puppet the way expressions are.
Level of detail follows the face’s rule. A plewd at long_shot is a dot: below
MIN_EMANATA_DETAIL, each symbol collapses to a filled dot where it would have been
drawn. Below the face’s own MIN_FEATURE_RADIUS, nothing is drawn – a character too
small to have a face is too small to be sweating.
.. py:data:: MIN_EMANATA_DETAIL
- module:
scenet.assets.emanata
- value:
60.0
Face radius in panel units below which emanata are drawn as dots rather than as shapes. Calibrated by eye against
scripts/contact_sheet.py --marks: in a 1000-unit panel,full_shot(a face radius of about 75) still draws a plewd you can tell is a drop, andlong_shot(about 51) does not.
.. py:data:: STROKE_FRACTION
- module:
scenet.assets.emanata
- value:
0.035
Stroke width, as a fraction of the face radius. Lighter than a face’s own lines: these are small symbols, and drawn at face weight they clot into blots.
.. py:data:: ZONE_PADDING
- module:
scenet.assets.emanata
- value:
0.05
How far a zone reaches past what it encloses, as a fraction of the face radius, on top of the stroke width. Enough that a balloon cannot sit flush against a mark.
.. py:data:: ZONE_SAMPLES
- module:
scenet.assets.emanata
- value:
8
Points sampled around each disc and each padded point when a zone is built. Eight is plenty: the zone feeds a soft cost, not a hard edge.
.. py:data:: DROP_SAMPLES
- module:
scenet.assets.emanata
- value:
11
points along its rounded end. Odd, so the samples are symmetric about the drop’s own axis and a mirrored figure’s sweat is the exact reflection of the original.
- type:
How a drop is sampled
.. py:data:: SINGULAR
- module:
scenet.assets.emanata
- type:
dict[~scenet.ir.Mark, str]
- value:
{Mark.BRIFFITS: ‘briffit’, Mark.GRAWLIXES: ‘grawlix’, Mark.PLEWDS: ‘plewd’, Mark.SQUEANS: ‘squean’}
The id each mark’s primitives are numbered under,
plewd_0,plewd_1, …
.. py:class:: ResolvedEmanata
- module:
scenet.assets.emanata
Bases: :py:class:
objectEvery mark drawn around one character, and the space they take up.
.. attribute:: marks
The drawing, as the same strokes and discs a face is made of, in a fixed order – by mark, then by position – so the same marks always serialise to the same bytes.
.. attribute:: zones
One convex polygon per mark, enclosing everything it draws. This is the whole of what the solver sees.
.. py:attribute:: ResolvedEmanata.marks
- module:
scenet.assets.emanata
- type:
tuple[~scenet.assets.face.ResolvedStroke | ~scenet.assets.face.ResolvedDisc, …]
.. py:attribute:: ResolvedEmanata.zones
- module:
scenet.assets.emanata
- type:
tuple[tuple[~scenet.geom.Point, …], …]
.. py:method:: ResolvedEmanata.init(marks=(), zones=())
- module:
scenet.assets.emanata
.. py:function:: build_emanata(puppet, marks)
- module:
scenet.assets.emanata
Draw the marks around one character.
- type puppet:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.assets.kinematics.ResolvedPuppet``
- param puppet:
The posed figure. Its face circle, facing and feet are all that is read.
- type marks:
- sphinx_autodoc_typehints_type:
\:py\:class\:\tuple`\ \[:py:class:`~scenet.ir.Mark`, :py:data:`…<Ellipsis>`]`
- param marks:
Which marks to draw. Order does not matter.
- rtype:
- sphinx_autodoc_typehints_type:
\:py\:class\:\~scenet.assets.emanata.ResolvedEmanata``
- returns:
The drawing and its zones, or nothing at all when there are no marks or the figure is too small for them to read.
.. admonition:: Example
from scenet import compile_source core = compile_source( … “{camera: {shot: medium_shot}, cast: {a: {reference: alice, marks: [plewds]}}}” … ).core core.actor(“a”).emanata[0].id ‘plewd_0’