Character3D Plugin
The Character3D plugin provides a framework for creating animated 3D characters in Clayground applications. It features a modular body part system, procedural animation capabilities, and integrates with the Canvas3D toon shading system for stylized cartoon characters.
Where to look at it
Every aspect of a character has a scenario in the character lab,
labs/character-101 (./build/bin/claydojo --sbx labs/character-101/Sandbox.qml):
the builds, the walk cycle sheet, the gesture set, the two whole-body actions,
the loadable move set, the hands, the six faces, the head’s detail tiers, the
lip-sync tiers, a listener and a crowd. Each scenario says what it is for on
its card, answers scene.report() headless, and records its numbers into a
run record - labs/kits/character/README.md is the contract, and
labs/character-101/paper.md the lesson.
Getting Started
To use Character3D components, import the module in your QML file:
import Clayground.Character3D
Core Components
- Character - Base component managing body parts and animations with extensive dimension properties
- ParametricCharacter - High-level parameters (bodyHeight, realism, maturity, femininity, mass) that auto-calculate dimensions
- RatioBasedCharacter - Dimension ratios for fine-tuned proportion control
- CharacterEditor - Visual editor overlay for character customization with persistence
- MoveSet - A loadable set of named moves played onto one character, loaded on demand instead of carried by every character
- MartialArts - The move set that ships with the plugin: fourteen moves from a stance to a knockdown and a get-up
- Performance - A character directed from one script string: cues for pointing, presenting, marks, emotions and timing, with
cueFiredfor choreography - Gait - Walk and run derived from a preset and eleven factors, driven by emotion, age, gender and build
- Speech - Voice output (text-to-speech or wav/mp3) with approximate lip-sync
- ThoughtBubble - Simple text bubble for speech/thought display
Character.roundness chamfers every box in the figure (Box3D.bevel underneath): 0 is the hard-edged original, about 0.3 nearly spherical, for no extra draw calls.
Usage Examples
Basic Character
import QtQuick
import QtQuick3D
import Clayground.Canvas3D
import Clayground.Character3D
View3D {
anchors.fill: parent
PerspectiveCamera {
position: Qt.vector3d(0, 200, 400)
eulerRotation.x: -20
}
DirectionalLight {
eulerRotation.x: -35
castsShadow: true
shadowFactor: 78
shadowMapQuality: Light.ShadowMapQualityVeryHigh
}
Character {
y: 0
activity: Character.Activity.Idle
}
}
Parametric Character Creation
ParametricCharacter {
name: "hero"
bodyHeight: 10.0
// Body shape
realism: 0.3 // Cartoon-like
maturity: 0.7 // Adult
femininity: 0.3 // Masculine
mass: 0.5 // Average
muscle: 0.7 // Athletic
// Face
faceShape: 0.5
eyes: 1.2
hair: 0.8
// Colors
skin: "#d38d5f"
hairTone: "#734120"
topClothing: "#4169e1"
bottomClothing: "#708090"
}
mass and muscle reach the body as a belly and a chest, not only as a
width. The trunk is two boxes on a waist joint (see
The trunk is two segments), so mass bulges the
belly forward over the hip and muscle deepens the chest, and a build shows
in the shape of the body rather than in how wide all of it is. Both are
exactly neutral at 0.5, and muscle also draws a waist in that a single
tapered box could not make.
The same three dials are on Character directly for a body built by hand:
| property | default | what it does |
|---|---|---|
bellyRatio |
0.45 | the belly’s share of torsoHeight; the waist joint sits between the two segments |
bellyBulge |
1 | how far the belly swells past the plain trunk taper - mostly depth, and forward. 1.3 is a gut |
chestSwell |
1 | how much deeper the chest is. Depth only: the chest’s width is shoulderWidth |
waistPinch |
0 | how far in the waist joint is drawn |
bellyColor, chestColor |
torsoColor |
either segment can take its own colour |
At every default the two boxes trace exactly the tapered box the torso used to be: same shoulders, same waist, same depth, same height. The only thing that is drawn and was not is the seam at the joint.
Character with Movement
ParametricCharacter {
id: player
name: "player"
// Activity controls animation
activity: isMoving ? Character.Activity.Running : Character.Activity.Idle
// Movement derived from animation geometry
property bool isMoving: controller.axisX !== 0 || controller.axisY !== 0
// Move based on currentSpeed (auto-calculated from animation)
x: x + controller.axisX * currentSpeed * dt
z: z - controller.axisY * currentSpeed * dt
}
Character Editor Integration
import Clayground.Character3D
import Clayground.GameController
Item {
View3D {
id: view3d
anchors.fill: parent
ParametricCharacter {
id: character1
name: "char1"
}
ParametricCharacter {
id: character2
name: "char2"
x: 20
}
}
GameController {
id: gameController
Component.onCompleted: selectKeyboard(
Qt.Key_W, Qt.Key_S, Qt.Key_A, Qt.Key_D,
Qt.Key_Shift, Qt.Key_Space
)
}
CharacterEditor {
anchors.fill: parent
characters: [character1, character2]
view3d: view3d
gameController: gameController
enabled: true
}
}
Facial Expressions
Six of them, and they are meant to be told apart at a glance rather than studied: the mouth first (a smile, a frown, a shout, a sneer or an O), the brows’ angle second, the lids last. Which lid moves is not interchangeable - up from below is pleasure or revulsion, down from above is a glare or a droop, and neither of them is surprise.
| expression | Head.Activity |
setEmotion |
the mouth | the brows | the lids |
|---|---|---|---|---|---|
| neutral | Idle |
"neutral", "" |
flat | level | open |
| joy | ShowJoy |
"happy" |
an open grin | up, flat | squint, from below |
| sadness | ShowSadness |
"sad" |
small, fully down | up, inner ends in | hooded |
| anger | ShowAnger |
"angry" |
open and wide - a shout | down into a V | hooded |
| disgust | ShowDisgust |
"disgust" |
a one-sided sneer | one up, one down | squint |
| surprise | ShowSurprise |
"surprised" |
a round O | high | wide open |
Disgust is the only one whose halves disagree, and that is deliberate: made
symmetric it is a quieter anger and nothing else. It is worth two parameters
of its own - Head.mouthSkew and the brow skew behind it - which anything
can drive without an emotion.
Character {
id: character
// Set facial expression
faceActivity: Head.Activity.ShowJoy
// Animate expressions
SequentialAnimation on faceActivity {
loops: Animation.Infinite
PropertyAnimation { to: Head.Activity.ShowJoy; duration: 2000 }
PropertyAnimation { to: Head.Activity.Idle; duration: 1000 }
PropertyAnimation { to: Head.Activity.Talk; duration: 2000 }
PropertyAnimation { to: Head.Activity.Idle; duration: 1000 }
}
}
Eyes: blinking, gaze and thinking
On by default, and it is what stops a face from reading as a mannequin:
Character {
id: npc
autoBlink: true // default
gazeBehaviour: true // default
blinkSeed: 7 // give each of a crowd its own, or they blink in step
}
npc.lookAt(player.scenePosition) // eyes first, head after
npc.thinking = true // eyes leave the target and settle off-axis
lookAt() aims the head; GazeAnim aims the eyes inside it, and the
difference in when they arrive is the whole effect. The target is mapped
into the head’s own frame, so what comes back is the angle the head has
not covered yet — large while it is still easing round, large again when
the target is past its 65° limit, and nothing once it has arrived. Point
the eyes at that residual and they lead on the way out and re-centre on
arrival, with no second animator racing the first.
thinking is the most legible signal a boxy face has for working
something out — there is no brow furrow to read at ninety pixels. Set it
around the gap between being asked and answering.
Everything idle here is deterministic for a given blinkSeed: the
blink spacing, the wander, the micro-saccades and the direction of an
aversion. Two runs of a sandbox render identically, which keeps a
clayrender comparison meaningful; two characters with different seeds
do not, which is what a crowd needs.
Off at Detail.Minimal regardless, where there is no eye left to move.
Listening
The other half of a conversation:
npcB.listeningTo = npcA // hold A's face, break away now and then,
// mark the ends of A's phrases
npcB.listeningTo = null // done
Everything else in this plugin describes a character while it speaks. Without this the one who is not speaking does nothing at all, which is what makes two characters talking read as two monologues taking turns.
Phrase boundaries are read off the speaker’s mouth, not its script: a
gap in Speech.mouthOpen while it is still speaking ends a phrase,
whatever produced the timeline. So it works on an unknown recording read
by the envelope tier exactly as it does on an aligned one — which is what
makes it usable on dialogue nobody wrote down.
listeningTo owns the look target while it is set; it and lookAt() are
the same channel by construction.
The head does two things at once
A nod has to happen while an aim holds, and be given back without the aim having been forgotten. So the head is the one joint that does not own its own rotation:
| Channel | Driven by | For |
|---|---|---|
Head.poseEuler |
the body animators, via HeadEulerAnim |
where the head is aimed |
Head.offsetEuler |
anyone | a momentary rotation on top |
Head.nod(deg, times) |
— | the built-in one |
eulerRotation is the sum, and a binding. Animating a head’s
eulerRotation directly writes to that sum and will be overwritten the
next time any part of it changes — animate poseEuler instead. Every
other joint is unchanged and still uses EulerAnim on eulerRotation.
Speech with Lip-Sync
Characters can speak text (via text-to-speech when available) or play recorded audio (wav/mp3) - the mouth movement approximates the speech in both cases:
Character {
id: npc
Component.onCompleted: {
// Text: spoken aloud when a TTS engine is available,
// otherwise the mouth animates silently at an estimated pace
npc.say("Hello! Welcome to Clayground.")
}
}
// Recorded dialog line - the mouth follows the recording
npc.say("dialog/intro.wav")
// Emotional conversation: colors face, voice (TTS pitch/rate) and -
// while the character is idle - body language gestures
npc.say("I lost my favorite shovel...", "sad")
npc.say("We found the treasure!", "happy")
npc.say("Give it back right now!", "angry")
npc.say("You want me to eat THAT?", "disgust")
npc.say("It was here a second ago!", "surprised")
// Inline annotations switch the emotion mid-speech
npc.say("*angry* Get off my ground immediately! " +
"*happy* Just a joke - come in and have a cup of tea with me.")
// Body language is optional: disable it (or just keep the character
// walking/fighting) and only face and voice carry the emotion
npc.speechBodyLanguage = false
// How closely a recorded line is read. Spectral (the default) measures
// formant bands, so vowels get their own shapes; Envelope reads loudness
// only and is the floor everything else falls back to.
npc.speechAccuracy = Speech.Envelope
console.log(npc.speech.effectiveAccuracy) // what the last line ACTUALLY got
// Advanced configuration
npc.speech.rate = 0.2 // a bit faster
npc.speech.volume = 0.8
npc.speech.finished.connect(() => console.log("done talking"))
The mouth is driven by continuous shape parameters on Head
(mouthOpen, mouthWide, mouthRound - readonly outputs - plus the
writable mouthCornerLift). Emotions keep control of the mouth corners
while speaking, so characters can smile and talk at the same time.
For fully manual mouth control, assign any object with speaking,
mouthOpen, mouthWide and mouthRound properties to
head.speechSource.
How closely a recording is read
say() with a file decodes it in full before playback starts, and how
hard it looks at what it decoded is speechAccuracy:
| Tier | Reads | Gets |
|---|---|---|
Speech.Envelope |
loudness, zero crossings | how far the jaw dropped |
Speech.Spectral (default) |
formant bands | which shape - open/closed, spread/rounded, fricative |
Speech.Aligned |
the above, plus a transcript | the script’s own shapes on the recording’s clock |
Aligned is the one that needs something from you:
npc.speechAccuracy = Speech.Aligned
npc.say("dialog/intro.wav", "", "Hello! Welcome to Clayground.")
Given the script, the sequence of shapes stops being a guess - the text
says there is an /m/ there, so the mouth closes. No acoustic tier can
do that reliably, because a bilabial is voiced: every measurement of
one says “loud”, not “shut”. Only the timing then comes from the audio,
by dynamic-time-warping the script against the measured frames.
A transcript that does not match the recording is worse than none - it would drag the mouth confidently through the wrong syllables for a whole line - so a pairing whose durations disagree by more than about 2.5x is rejected and the tier below takes over.
Alignment also carries word marks onto the recording, so currentWord
and per-word callbacks work for recorded dialogue, not just for TTS.
The tiers are not a performance dial. A 512-point transform every
16 ms is a few hundred thousand flops for a whole line, once, which is
below the noise floor of the decode that produced the samples. What
separates them is time-to-first-sound, the samples held while the
analysis runs, and - for Aligned - whether anyone wrote the line down.
That last one is an authoring cost rather than a runtime one, and it is
the real reason a lecture and a barked NPC line want different answers.
No tier leaves the mouth dead. A recording the analyser cannot read
falls back to the one below on its own, and Envelope is the floor.
Read speech.effectiveAccuracy for what the last line actually got -
asking for Spectral and getting Envelope back is a normal outcome,
not an error.
What Spectral assumes is one close-miked speaker on a reasonably dry
recording. Against a music bed the formant bands read the instruments,
and heavy reverb fills in the gaps that mark a closure. Both degrade to
the envelope rather than to a guess, per-frame, via a confidence gate.
The speech scenario of labs/character-101 puts the three tiers on one
recording side by side (labs/kits/character/SpeechRow.qml).
Gestures
Walk, run, idle, working and boxing are cycles. A gesture is the other kind of
animation: a pose that eases in, is held for as long as it is wanted,
and eases back. Both are driven from Character:
Character {
id: prof
view: view3d // so Auto can see how big it lands on screen
Component.onCompleted: {
prof.turnTo(board.scenePosition) // whole body, shortest way round
prof.pointAt(stone.scenePosition) // held until told otherwise
prof.setEmotion("happy") // face, until changed again
}
}
// once the arm has arrived, talk about it facing the reader
if (prof.gestureSettled) {
prof.lookAt(camera.scenePosition)
prof.gesticulate()
prof.say("This is the stone I meant.")
}
prof.stopGesture() // eases everything back to rest
| verb | what it does |
|---|---|
pointAt(worldPos, which) |
Holds a point at a scene position. which is "auto" (default), "left" or "right". |
presentAt(worldPos, which) |
Offers an open hand toward a scene position - palm up, at chest height, elbow bent - and holds it. Same which as pointAt. |
thumbsUp(which) |
Holds a thumbs up; "right" by default. |
gesticulate() |
Two-handed talking gesture, looping until stopped. |
stopGesture() |
Eases every held joint - and the head - back to rest. |
lookAt(worldPos) |
Aims the head only; outranks the gesture’s own head aim. null releases it. |
turnTo(worldPos) |
Turns the whole body on the spot; changes the resting orientation. |
setEmotion(name) |
A lasting face: "happy", "sad", "angry", "disgust", "surprised", "neutral"/"". |
What to assert on, rather than watching:
| property | meaning |
|---|---|
gesture |
"point", "present", "thumbsUp", "talk" or "". Set the moment a gesture is asked for. |
gestureSettled |
The pose has arrived. Measuring joint angles before this reports the pose being left. |
gestureHand |
"left", "right", or "" while released or talking. |
emotion |
The lasting face, as distinct from speechEmotion, which belongs to one line. |
Point or present? A point is for one thing: the finger goes on it, and
the arm reaches as far as the target asks. A present is for a group or an
area - several parts, a whole circuit - where a finger at the centroid would
point at bare board. The hand stays in front of the body at chest height,
palm up, and only turns toward the target; the head and the body turn as they
do for a point. Something far above or below the hand is a point’s job.
The busy hand takes the open pose.
Two rules the layer depends on:
- Idle only. Gestures run only while
activity === Character.Activity.Idle, and every verb above is ignored otherwise. Setting any other activity drops the gesture immediately and hands the joints to that activity’s animation. This is what keeps two animators off one joint;IdleAnimand the speech body language stay switched off for as long as a gesture is held. safeSilhouette, on by default. A straight arm raised forward reads as a fascist salute, so a point that aims high forces the elbow to bend and lets the forearm do the reaching. The aim is unaffected - the bend is given back through the shoulder and the wrist. Turn it off for a character whose job is that shape (a salute, a hand-raise, a throw).
One trap when reacting to the layer: gesture and gestureSettled are
properties, and a handler on their change notification runs while that
notification is still being delivered. Calling a mutating verb
(stopGesture(), another pointAt(), an activity switch) synchronously
from such a handler therefore logs a QML binding loop - harmless but noisy.
Defer the reaction with Qt.callLater(...), or react from a Timer, as
the professor kit’s FlowGuide does.
Standing, working and boxing
Everything the arms do that is neither a walk nor a held gesture lives in one
Qt-free model, animation/action.js, the way the walk and the run live in
animation/gait.js:
| what | where it is | who plays it |
|---|---|---|
| standing still | REST |
IdleAnim, and GestureAnim when it releases |
| working at something | base use |
UseAnim |
| boxing | base fight |
FightAnim |
UseAnim and FightAnim are ActionCycleAnim with one property set. Unlike
the gait cycle, which spells its poses out as animations and keeps a matching
poseAt() beside them, an action cycle animates ONE number - the phase - and
writes what actionPoseAt() answers for it. There is no second copy to keep in
step: the frozen boxing and working columns of the character lab’s gesture
sheet (labs/kits/character/GestureSheet.qml) are the same function the
shipped cycle plays.
Character {
activity: Character.Activity.Using
workHeight: 0.2 // 0 a table at the waist, 0.5 a counter, 1 a shelf at head height
actionIntensity: 0.7 // amplitude, tempo and how much of the body joins in
}
| property / method | meaning |
|---|---|
actionIntensity |
0..1. A harder fight is a faster one with a tighter guard and a bigger bounce; harder work is bigger, quicker, and past six tenths one hand holds while the other hits. |
workHeight |
0..1, Using only. A posture, not a hand height: the back rounds over a table, the forearms angle up to a counter, the back arches and the head comes up at a shelf. |
actionHandPose |
What the running activity wants the hands to be doing, "" when none does. A fist while boxing. |
actionPoseAt(action, t) |
The joint angles at phase t, with nothing running. Pure. |
actionTable(action) |
The derived numbers the cycle is replayed from, cycleMs among them. |
applyActionPose(action, t) |
Freezes an idle character at that phase - what the sheets draw. |
Boxing is an amateur’s, on purpose, and orthodox: the left leads. One cycle is jab, jab, cross - short, short, LONG - and then the guard, which bounces on the knees and rolls a little around its blade until the next. The guard is what the whole thing is judged on, and the five things that make it read as a guard are kept whatever else moves: both fists above both elbows, both elbows below the shoulders and inside the ribs, the fists at the cheeks in front of the face, a bladed and staggered stance on bent knees with the rear heel up, and the hand that is not punching welded to the cheek. The jab barely winds up; the cross draws back, turns the hips a third of a turn and the shoulders further, leans in past what a professional would, and the trunk comes home before the arm does. The head turns back part of the blade to look at the opponent, and drops behind the shoulder on the cross.
Working is generic on purpose - it has to pass for cooking, tinkering,
sorting and typing alike - so it is built from what those share, which is a
rhythm rather than a stroke. A cycle is four beats: the lead hand reaches for
something, both hands work at it in short strokes, the lead hand presses or
places it, and the body settles and glances up. The two hands are never level
and never mirrored (the lead sits ahead and above; the off hand does two thirds
as much and lags by four tenths of a stroke); the head leads the reach and
lags the press; and every fourth cycle the glance is a proper look up, off
ActionCycleAnim.cycle, so the loop is not noticed as one.
actionHandPose sits between a gesture and gaitHandPose in the chain that
decides a hand’s shape, and it is why a punch is thrown with a closed hand:
nothing on the Fighting path could reach handPose before it, so the boxing
cycle ran with the fingers open and read as clawing.
node plugins/clay_character3d/animation/action.test.js checks the model -
that a guard keeps both fists above the elbows and both elbows below the
shoulders, that the rear hand does not move for a jab, that a cross winds up
further and turns the trunk more than a jab, that the working loop has beats
and its two hands are never in step. It runs under ctest as
node_character3d_action.
Where to look at it. The action scenario of labs/character-101
(labs/kits/character/ActionStage.qml) poses one figure from the lab’s clock
through the same pose model, with a see-through bag at a straight’s reach or
a table under the hands; the lab’s transport pauses and steps the clock to
freeze a frame, and the scene’s report() measures each fist against its
own shoulder and against the chin in head heights, which is what a guard is
a claim about:
clayrender labs/character-101/Sandbox.qml --size 1400x900 --paused \
--eval 'applyScenario("action"); act("action", ["fight"])' \
--wait-for 'sceneReady' --result - --eval 'JSON.stringify(scene.report())' \
--out /tmp/cross.png
One sign in the model was measured there rather than reasoned: a positive Y
rotation on an upper arm carries a forward-pointing forearm OUTWARD on the
right side, so arm() negates the yaw against the side. Written the other way
round, the rear fist of the guard sat a head and a half outside the face and
looked right in every sheet that had no reference to measure it against.
Loadable move sets
Everything above is the basic set - what a character IS. Walking, running,
standing, gazing, listening, gesturing, talking, working and boxing are wanted
by every game, so they are always resident on every character. A move set is
what a character KNOWS: a martial art, a dance, a trade’s hand-work, wanted by
one game and not the next, so it is loaded onto a character on demand, replaces
whatever set was loaded before it, and is unloaded again by clearing
moveSet. A set runs only while the activity is idle, exactly as a gesture
does.
ParametricCharacter {
id: fighter
moveSet: "martial arts" // or the URL of a MoveSet of your own
Component.onCompleted: fighter.playMove("stance")
}
The shipped set offers fourteen moves: stance, step, guard, jab,
cross, uppercut (standing), lowGuard, sweep (crouched), frontKick,
roundhouse (kicks), jumpPunch, jumpKick (airborne), knockdown, getUp
(ground). knockdown holds its last frame on the floor until stopMove() or
another move releases it - which is what getUp starts from.
What a set offers and what it is doing is readable from the character
(moves, moveSetName, activeMove, movePlaying, moveHolding,
moveFinished); see Character’s moveSet documentation for the whole API.
CharacterEditor has a “Moves” section that loads a set and plays any of its
moves, and demo/Sandbox.qml binds the same to the keyboard: J loads and
unloads the set, , and . step through the moves, V plays the selected
one and B stops.
The gesture sheet
The sheet for everything above and for the held gestures with it: the
gestures scenario of labs/character-101 (labs/kits/character/GestureSheet.qml)
puts them side by side, one frozen figure each, same light, same angle,
labelled — the gait cycle sheet’s trick applied to poses rather than to
phases.
clayrender labs/character-101/Sandbox.qml --size 2200x900 --paused \
--eval 'applyScenario("gestures")' --wait-for 'sceneReady' \
--out /tmp/gestures.png
The set is the thing being judged, not any one pose. A gesture looked at on its own is looked at against a memory of the last one, and a memory grades generously — which is how a fist that folded back past its own knuckles survived for as long as it was only ever seen one at a time.
silhouette=true takes the lighting and the colour away and leaves the
outline, which is all a gesture has at any distance; scale shrinks the
figures in place for the small-on-screen read. Every pose on it is frozen and
deterministic — the cycles from applyActionPose(), the aimed gestures through
the real solver with its settle cut to a frame — so ready is the property to
wait for and two renders across a change are comparable.
For one hand very close up, the hands scenario (labs/kits/character/HandBench.qml)
is still the bench: the sheet answers “is this recognisable”, the hand bench
answers “is this a hand”.
CharacterEditor carries the same set as chips, for turning a knob and looking.
Where this workflow lives. In labs/kits/character/, as scenes of the
character lab, and not
as a lab under labs/. A lab is a teaching artifact with an authoring contract
to match — a paper, a .grafli overview, EN and DE strings from the first
commit, committed .labrec records — and it is aimed at a reader learning a
domain. A character-tuning rig has no teaching content and exactly one
audience: whoever is editing this plugin. It also needs to sit beside the
component it is judging, because the two are edited in the same breath. The
benches build nothing, cost nothing and are already how the gait, the face and
the hand are judged; a fourth of them is the cheap, consistent answer. If a
character-tuning LAB is ever wanted — expressions, sprites and animation review
for a reader rather than for a maintainer — it is a different artifact with a
different audience, and it can be built on top of these benches rather than
instead of them.
Hands and faces, and how much of each to draw
handPose says what the hands are doing - relax, open, point,
thumbsUp, fist - and a gesture overrides it for as long as it holds them.
Both levels of detail answer it.
detail says how much hand, and how much face, to spend on that:
Character.Detail |
what is drawn | draw calls |
|---|---|---|
Minimal |
one box per hand; the head keeps its skull, its hair and a drawn face, and loses its nose, its ears, the pupil highlights, the brows and the mouth corners | ~20 |
Low |
the whole body, one box per hand, reshaped per pose — a fist is a stubby block, an open hand a long flat one; the face keeps its irises and loses its brows and ears | ~21 |
High |
ten boxes per hand as well: four fingers and a thumb that fold, so a point extends a real index finger; the whole face | ~43 |
Auto (default) |
picks between the three by how tall the character lands on screen |
The head is nine boxes at High — a cranium, a jaw, four of hair, a nose and
two ears — seven at Low and six at Minimal. It used to be nineteen,
because the eyes, their irises, their brows and the four pieces of the mouth
were all boxes standing in front of it. They are drawn into the head’s own
surfaces by a fragment shader now, and cost nothing.
That has flattened the top of this table on purpose. Minimal used to be much
the cheapest level because it deleted the face, thirteen of a character’s
thirty-three draw calls; now it saves one box over Low, and the only real
saving left between levels is the twenty boxes of fingers. What the two cheap
levels buy is no longer speed - it is a face that stays legible at twenty
pixels rather than one that shimmers. No level removes a face any more: a
character without one reads as broken rather than as distant.
Auto crosses into High at detailThreshold (240 px of figure) and drops to
Minimal below minimalThreshold (60 px). Both are measured rather than
picked — see their docs. Each character decides for itself, so a crowd pays
only for the ones that are near.
Auto needs view - a character cannot ask how big it looks without knowing
what it is being looked at through - and stays Low without one. It biases
toward fingers while a gesture is shaping the hands, since that is what they
are for, and it has a hysteresis band so a character drifting across the line
does not grow and shed ten boxes a hand every few frames.
detailedHands is read-only and reports which one is on screen right now.
Making a gesture readable
Two properties, and they work together:
Character {
gloves: true // hands get their own colour, and a cuff at the wrist
handScale: 1.45 // and they are drawn bigger than the tables give
}
The oldest trick in cartoon animation, and it is about legibility rather than costume. A hand the colour of the arm it is on has to be found before it can be read, and a hand the colour of the background cannot be found at all — so the glove is a single high-contrast shape that separates from both. The cuff is the half that does the separating: a pale hand is just a pale hand, the band across the wrist is what says where the arm stops.
handScale scales the wrist joint, so the hand grows out of the cuff instead
of drifting off the end of the arm — and it scales the whole hand rather
than lengthening the finger, because stretching the one part that has to stay
legible is what produces a spike where an index finger should be.
detail accounts for it: bigger hands mean the fingers are worth drawing from
further away, so the Auto threshold divides by handScale.
A third knob, and this one is about not being noticed. ParametricCharacter’s
two width sliders scale the arm over a spread of two and a half from thin and
unmuscled to heavy and muscular, and the palm is a fixed fraction of the arm —
so the hand used to take all of it, which came out as claws on one figure and
mittens on the other. handBuildResponse (0.5 by default) is how much of the
build the hand takes: at 1 it is glued to the arm as before, at 0 it is the
same hand on every body. Only the cross-section — hand length follows the
arm’s length, which is a matter of maturity. tests/qml_head/tst_build.qml
pins it, and the hands scenario of labs/character-101 takes a build and
the handBuild knob so the sweep can be looked at:
clayrender labs/character-101/Sandbox.qml --size 1400x900 --paused \
--eval 'applyScenario("hands"); act("build", ["thin"]); act("arm", ["level"]); act("pose", ["open"])' \
--wait-for 'sceneReady' --result - --eval 'JSON.stringify(scene.report())' \
--out /tmp/thin.png
report() carries palmArm, which is the number the question is actually
about: 1.05 at every build with the response at 1, and 1.41 / 1.05 / 0.88
across thin / neutral / heavy at the default.
The two levels are built to match in outline, so the switch is meant to go
unnoticed; the hands scenario of labs/character-101 is where that is
checked, and its fingers verb flips them on one character without moving
anything else.
Tuning a hand pose. n on that bench opens a slider per field of the row
the current pose resolves to — the four curls, the fan, the five thumb numbers,
and the fold shape shared by every finger. They are written onto the right hand
as you drag, 0 puts back what ships, and k prints the row in exactly the
form DetailedHand’s table is written in, ready to paste:
if (name === "fist")
return { i: 1.00, m: 1.00, r: 1.00, l: 1.00, sp: 0.00,
tx: 120, tz: 10, tc: 0.45, tl: 1.15, toff: 0.55 }
Every number in that table was arrived at by looking, and looking is done with
the hand in front of you rather than in an editor with a rebuild between each
guess. The channel is Arm.poseOverride → DetailedHand.poseOverride, a
partial replacement of the pose row; it is a debug channel and nothing ships
with it set.
How much character to draw
detail is Character.Detail.Auto, High, Low or Minimal. Auto measures
how big the character lands on screen and picks between the other three; it
needs view set, and stays Low without one.
Pin it for a character the camera lives on — a player above all. Auto is a
policy about distance, and a character that is always in close-up has no
distance to decide anything about; CharacterEditor’s Detail row does it
by hand, and shows what Auto currently resolves to next to what it was asked
for.
Auto measures the character’s apparent size off whichever of its three axes is
least foreshortened, not off its height alone. That is not a refinement: a
camera looking along a character’s own length — up at it from the floor, down
at it from above — projects a ten-unit body to a few pixels, and measuring the
body axis alone said tiny about a figure filling the screen. Measured at a
fixed sixteen units, a figure that was High at eye level fell to Low by 70
degrees of camera pitch and to Minimal by 85, up and down alike. The two
horizontal axes are only projected when the body axis has already gone short,
so a character that is plainly close enough costs nothing extra, and at eye
level the horizontal estimate never wins — the distance thresholds are exactly
what they were.
The face, and how it is drawn
The eyes, their lids and irises, the brows and the mouth are not geometry. They
are signed distance fields evaluated in a fragment shader and drawn into the
front of the two head boxes - bodyparts/FaceBox.qml carries the material,
face3d_main.glsl draws the shapes. A face therefore costs no draw calls and
no vertices at all.
That buys three things a face built from boxes could not have. An eye is a
marking on a head rather than an object in front of one, so it no longer shows
its own side wall at twenty degrees off axis. Head.gaze aims the irises
without moving the head, which sliding a built iris sideways could never do -
it would carry the iris off its own eyeball. And Head.autoBlink is one
animated float rather than a pair of boxes resized every frame, which is why
the eyes never blinked before.
The heads scenario of labs/character-101 (labs/kits/character/HeadRow.qml)
shows all three detail levels side by side with named shots, a blink, a gaze
and a talking mouth, and a readout giving the head and eye size in pixels -
so “still readable at ninety pixels” is a claim that can be checked rather
than an impression.
Face anchors
Head publishes where its features are, in the head node’s own frame, so
accessories parented to character.head (beards, spectacles, hair) do not
restate its layout arithmetic and then drift from it:
faceOffsetZ, faceFront, faceBack, jawFront, upperHeadBottom,
crownTop, eyeLine, eyeWidth, eyeSpacing, eyeRelief, noseBottom,
earPos, earSize, earTop, hairOuterX, mouthLine, mouthWidth,
mouthBottom, chinBottom.
mouthLine stays put while the jaw stretches open; mouthBottom and
chinBottom move with it. eyeRelief is how far the eyes stand proud of
faceFront - zero, now that they are drawn rather than built, which is what
lets a spectacle rim settle onto the face instead of being pushed clear of a
pair of protruding cubes.
Use them. The professor kit spent a long time re-deriving all of this by hand
from the six head dimensions - fifteen-odd constants copied out of Head.qml -
and every one of them was a place a beard could slide off a chin with nothing
raising an error. These anchors also went unread for long enough that a binding
loop sat undetected in one of them: an anchor nothing evaluates is an anchor
nothing checks.
Character publishes rightShoulderPos, leftShoulderPos and headPos in
character-local coordinates for the same reason.
Gait
Walk and run are one cycle, GaitCycleAnim, animated from a table that
animation/gait.js derives from a base - the walk or the run as it was
authored - and thirteen factors around neutral. A factor that scales an amplitude
is 1 at neutral, one that offsets is 0, and the all-neutral gait is the walk
and run the framework always had, to the digit: gait.test.js asserts the
derived table against the formulas WalkAnim and RunAnim used to carry, so
a retyped digit cannot pass as neutral.
Three sources feed a character’s gait, and each defaults to nothing:
ParametricCharacter {
maturity: 0.9 // the build: an elderly shuffle
gait: Gait { preset: "elderly"; tempo: 1.2 } // the author: elderly, a fifth quicker
Component.onCompleted: setEmotion("sad") // the mood: slows and slumps the walk too
}
- The build. A
ParametricCharacterhandsmaturity,femininity,massandmuscleto the gait model asgaitBuild. - The emotion. The same channel the face uses: a spoken line’s emotion
while it is spoken,
emotionotherwise.setEmotion("sad")slows the walk, shortens the step and hangs the head as well as pulling the face. - A
Gaitobject with a preset, factors, or both.
Character.gaitFactors is the three folded into one vector: multiplicative
factors multiply, additive ones add, and the result is clamped once. There is
no override layer on purpose - preset: "elderly" with tempo: 1.2 is
“elderly, a fifth quicker” and reads that way, and a sad and heavy character
is slower than either alone. gaitFromBuild and gaitFromEmotion switch the
first two sources off:
Character {
gaitFromBuild: false // the walk ignores the body
gaitFromEmotion: false // and the mood
}
Speed follows the feet, whatever the gait. walkSpeed, runSpeed and
currentSpeed come out of the derived table and the leg height with the
formula the old cycles used, so a controller that moves the character by
currentSpeed needs no change: a shorter stride or a slower tempo covers
less ground, and the feet do not slide.
The factors, as Gait exposes them:
| factor | kind | neutral | what it moves |
|---|---|---|---|
tempo |
multiplies | 1 | cadence; 1.2 takes steps a fifth quicker and covers ground a fifth faster |
stride |
multiplies | 1 | how far the legs swing, and the speed with it |
bounce |
adds | 0 | how high the whole figure rises at mid-step, in leg heights; 0.1 is the cap |
lean |
adds | 0 | trunk pitch in degrees on top of the cycle’s own, bending at the waist so the legs stay planted; positive forward, negative chest out |
spineCurve |
adds | 0 | how round the back is: the angle between belly and chest. Differential, so it changes the shape of the trunk without moving the head - positive rounds it forward, negative arches it and lifts the chest |
headPitch |
adds | 0 | head pitch in degrees; positive looks down, negative lifts the chin |
armSwing |
multiplies | 1 | arm swing amplitude; 0 hangs the arms |
armForward |
adds | 0 | degrees the whole swing is carried ahead of the body, same amplitude; bent elbows plus this is fists pumping before the chest |
armOut |
adds | 0 | degrees the upper arms are carried out from the ribs, held through the cycle rather than alternating. Only reads head-on, and it is what separates a walk with somewhere to be from one ready to hit something |
elbow |
adds | 0 | elbow bend in degrees on top of the cycle’s own (a walk bends 10, a run 70) |
kneeLift |
multiplies | 1 | knee lift; below 1 drags the feet, above it high-steps; the foot angles follow |
sway |
adds | 0 | hip yaw in degrees, alternating with the step, the shoulders countering by half |
rock |
adds | 0 | trunk roll in degrees over the planted leg, alternating with the step |
The trunk is two segments
Character.torso is a group that draws nothing. What draws is belly and
chest, two boxes on a waist joint, and that is where a trunk pitch lives -
the group carries only the sway and the rock. The two pitches always add up to
lean, so the head and the shoulders end up exactly where a single-box torso
put them; their DIFFERENCE is spineCurve, and it is the difference that
reads. A lean on its own tips the figure like a plank; a lean with a curve
settles the belly back and rounds the chest forward over it, which is what
makes a slump look like weight and a proud walk like air in the chest.
A factor lean brings a little curve with it on its own, because a body that
is asked to lean bends. A BASE lean does not: a run’s authored 12 degrees is a
sprinter’s straight line from the ankles, so it goes on the belly whole and
the waist joint stays shut.
The hip hangs off the belly and gives the belly’s share of the bend straight
back, so the legs stay where the base asked for them - upright under a factor
lean, tipped with the whole figure under a run’s. gaitPoseAt() reports all
four: hip, torso, belly, chest.
The same split is what TalkGestureAnim, GestureAnim’s talking beats and
UseAnim bend with, so a character that leans while it speaks and a character
that leans while it walks bend the same way.
Presets, by name (Gait.presetNames): neutral changes nothing; cheerful,
dejected and furious are exactly what the happy, sad and angry
emotions do to a walk - they share their rows in gait.js, so
setEmotion("sad") and preset: "dejected" cannot drift apart; elderly is
the top of the maturity slider (slow, short, shuffling, stooped) and toddler
its bottom with a touch more tempo; heavy is the top of the mass slider with
more rock, leaning BACK over the weight it is carrying; sneak is slow and
short with high knees, a rounded back, head down, elbows bent and the arms
held still; proud is an arched back and a lifted chest, chin up and arms
swinging over a slightly longer, slower step; march is high knees, a wide
arm swing, straight elbows and a straight back.
furious is short, hard, quick steps with the knees stamping, the shoulders
hunched over a forward head, the upper arms carried off the ribs and the fists
before the chest by a bent elbow - not a bigger walk. Two earlier versions of it were: one slid the whole
arm swing 22 degrees forward, the other folded the elbow to 80, and both put
the forearms out in front where they barely alternated, which reads as a
sleepwalker from every angle. The cycle sheet is what settled it. A name that is not in the list does nothing
and clears Gait.presetKnown.
How the build maps (buildFactors() in gait.js): every default is exactly
neutral, and each effect fades in linearly toward the end of its slider.
maturity below 0.4 is the child zone (quicker, high-stepping, a little
bounce), above 0.75 the elderly zone, and neutral in between - the proportion
tables treat 1 as a full adult, so only the top quarter reads as elderly, for
gait alone. femininity, mass and muscle fade out toward 0.5: feminine
sways and swings the arms less, masculine rocks a little; heavy is slower,
rocks more and leans back over its weight, light a touch quicker; athletic
leans in with the chest up and swings the arms, soft slumps and rounds its
back. bodyHeight and realism do not enter the gait (the leg height they
set enters the speed). The effects are kept subtle so the sliders stay a body
and not a costume: gait.test.js checks that no corner of the four sliders
leaves a walk.
A factor change lands at the next half-cycle, when the phase that is starting reads its targets. There is no blend, which is how every other activity switch behaves.
To look at a gait, draw it as a cycle sheet rather than watching it: the
gait scenario of labs/character-101 (labs/kits/character/GaitSheet.qml)
freezes a row of figures at successive phases of one cycle, so the whole
walk is on one sheet. Its header comment says how to read one; sceneReady
is the property to wait for, since a verb lands after the first pose pass.
clayrender labs/character-101/Sandbox.qml --size 1800x900 --paused \
--eval 'applyScenario("gait"); act("preset", ["elderly"]); act("emotion", ["sad"]); Lab.set("maturity", 0.9)' \
--wait-for 'sceneReady' --out /tmp/elderly.png
The scene’s shots - side, front, back, top (goShot("top"), or N
in the lab) - are the four angles to check a change against: a silhouette
that reads as walking from the side and from nowhere else is a side view,
and the overhead is the only one that shows sway and shoulder
counter-rotation honestly.
To assert on a gait, read gaitFactors: it says what the character was asked
to do, where a joint angle mid-swing says only where the leg happens to be.
gaitPoseAt(base, t) gives the joint angles at any phase with nothing
running, and applyGaitPose(base, t) freezes an idle character there.
Best Practices
-
Use ParametricCharacter for quick character creation with intuitive parameters.
-
Activity-Based Animation: Set the
activityproperty to control animations - speeds are auto-derived from geometry. -
Toon Shading: Use the Canvas3D DirectionalLight setup for consistent cartoon rendering.
-
Character Editor: Add CharacterEditor during development for visual tuning, remove for production.
-
Proportions: Adjust
realism(0-1) to shift between cartoon and realistic body ratios.
Technical Implementation
The Character3D plugin implements:
- Modular Body Parts: Head, a two-segment trunk on a waist joint, arms, legs, all with independent dimensions
- Procedural Animation: Idle derived from body geometry; walk and run are one
GaitCycleAnimover a table thatgait.jsderives from a base (the authored walk or run) plus the composed gait factors - Animation-Speed Coupling: Movement speeds calculated from the derived table’s leg swing angles and the leg height, so speed still follows the feet whatever the gait
- Facial Expressions: Six expression states (neutral, joy, sadness, anger, disgust, surprise) plus talk, each a table of ten shader parameters rather than a shared set of building blocks
- Editor Integration: 3D picking, parameter sliders, and per-character persistence
- Coordinate System: Origin at ground level (Y=0 at feet), character faces +Z when rotation is (0,0,0) - the nose sits on the +Z face of the head and
CharacterControllerwalks along +Z at yaw 0
The animation system uses frame-based updates with biomechanically-inspired joint rotations and parent-child transform hierarchies.
Performance Scripts
A performance script is one string carrying what a character says and what it does, in the order it happens - the way a director writes a scene. It is authored text: it diffs, it translates, and it can be asserted on without watching it run.
Performance {
id: perf
performer: prof // a Character, or any object with the verbs below
searchRoot: view3d.scene // where target names are looked up
}
perf.play("*point at battery* This is the battery. (2s) " +
"*face viewer* *happy* It stores the energy our circuit spends.")
Two rules carry the format:
- Directives are instant, speech and pauses are not. A directive is
dispatched and the script moves straight on, so “point at it while saying
this” is the natural thing to write. Only a spoken line and an explicit
*pause*consume time. - Parsing is strict. An unknown directive is a reported error with its
position in the source, and
play()refuses to run the script. A typo never becomes dialogue. (Character.say()keeps its lenient parse - seeparse(script, {strict: false})below.)
Vocabulary
Directives are matched case-insensitively and their inner whitespace is
normalized, so *Point At battery* is *point at battery*. Everything
outside a *...* is spoken.
| Directive | What it does | Performer method |
|---|---|---|
*happy* *sad* *angry* *disgust* *surprised* *neutral* |
Sets the emotion for the lines that follow. Aliases: joy, sadness, anger, disgusted, surprise, shocked, calm |
setEmotion(value), else the *emotion* annotation is prefixed to the next say() |
*point at NAME* |
Points at the target | pointAt(pos) |
*present NAME* / *show NAME* |
Offers an open hand toward the target - for a group or an area | presentAt(pos) |
*look at NAME* |
Head-only aim at the target | lookAt(pos), else turnTo(pos) |
*face NAME* |
Whole-body turn to the target | turnTo(pos) |
*look at viewer* / *face viewer* |
Same, at the camera | faceViewer() for face, else the position from viewerPosition |
*thumbs up* |
Approval gesture | thumbsUp() |
*gesticulate* |
Talking hands on | gesticulate() |
*rest* |
Drops any gesture | stopGesture() |
*mark NAME* / *mark A, B, C* |
Raises markers on the named things for the length of the line that follows | none - the points come out in marks |
*pause 800ms* *pause 2s* *pause 1.5s* |
Consumes that much time | - |
| anything else | A reported error in strict mode | - |
NAME is a QML objectName, taken verbatim from the script (case included)
and resolved against the scene - there is no second naming scheme. It may
contain spaces: *point at the big red battery*. viewer is the one reserved
name and means the camera. A duration is a number plus a unit, ms or s,
both required; *pause 800* is an error rather than a guess.
*mark* is the one directive that takes several targets, comma separated,
because naming a group is exactly when it earns its keep:
*mark the battery, the switch, the LED* Four parts, one loop.
Marks
A line that names things - “collector on the left, emitter on the right, base
facing you” - asks the eye to find each of them by ear. *mark ...* resolves
its names the way *point at* resolves its target and publishes the world
points in marks; markNames holds the names that resolved, in the same
order. A name that does not resolve is skipped and recorded, and the rest of
the list still marks.
Nothing here draws them. A sequencer has no view, so the points go to whatever
is showing the scene - Clayground.Lab’s MarkLayer is the overlay the labs
use, and a lab’s own is one binding away.
The lifetime is the whole of the rule an author has to hold: a mark set is
raised by its cue, lives for the length of the one line that follows it, and
is gone by the time the next cue starts. stop() and the end of the script
clear it too. A marker says “this one, now”, not “this one, still” - so a
sentence that keeps a mark up needs its own *mark*:
*mark the collector* Collector on the left.
*mark the emitter* Emitter on the right.
*mark the base* And the base, facing you.
Time hints
A spoken run may end with a duration in parentheses:
This is the battery. (2s)
The hint is stripped from the spoken text and becomes that line’s authoritative
duration: the script advances 2 s after the line starts, whether or not the
performer is still talking. Without a hint the line ends when the performer
stops reporting that it is talking, and a backstop timeout
(estimate * 1.5 + 2000 ms, off Speech.estimateDurationMs()) ends it if the
performer never reports anything at all.
The rule is deliberately narrow: the parenthetical counts as a hint only at the
very end of a run and only when whitespace precedes it. This (2s) is the
battery. and Battery(2s) are text.
A whole script
perf.play(
'*point at battery* This is the battery. (2s)\n' +
'*face viewer* *happy* It stores the energy our circuit spends.\n' +
'*pause 500ms* *gesticulate* Watch what happens when I close the switch.\n' +
'*rest* *neutral*')
Ten cues: point, say (hinted at 2000 ms), face, emotion, say, pause, gesticulate, say, rest, emotion.
Performance
| Property | Meaning |
|---|---|
performer |
The character. Duck-typed: each cue calls the method named in the table above if the performer has it, and is skipped and recorded if it does not |
searchRoot |
Node whose children are walked (recursively, by objectName) to resolve a target name to its scenePosition |
resolveTarget |
function(name) returning a vector3d or null; replaces the searchRoot walk |
viewerPosition |
A vector3d or a function() returning one - where “viewer” is |
voiceOf |
function(sayIndex) returning a clip url per spoken line; with a clip and a performer that has tell(), the line is played from the file. sayIndex counts spoken lines from 0 |
spoken |
What a bare line (no clip) does. True (default) goes through say() - the speech engine. False keeps the lab silent: a performer with tell() shows and mouths the line without audio, the professor’s narration mode |
extraVerbs |
Extra directive names the parser accepts |
debug |
Logs [perform] 1234ms cue 3/7: point at battery per cue |
| Method | Meaning |
|---|---|
play(script) |
Parses strictly and plays from the first cue. Returns false and plays nothing when the script has errors |
playFrom(index) |
Plays the last parsed script from a cue - a debugging aid |
stop() |
Disarms every timer, ends speech (stopSpeaking()/quiet()) and drops gestures. Does not emit finished() |
registerVerb(name, handler) |
Teaches the parser a directive and dispatches it to handler(arg). The seam for actions only one character has; a handler that throws is recorded, the script continues |
estimateMs(text) |
What the performer’s speech engine expects the line to take, or 72 ms per character |
Observability
A script is verified by reading state, not by watching it.
| Property / signal | Meaning |
|---|---|
running |
True between play() and the last cue |
done |
True once the last cue has fired |
cueIndex / cueCount |
Which cue is playing, out of how many |
currentCue |
The current cue as a one-liner, e.g. point at battery |
errors |
Parse errors of the last play(), each {at, directive, message} |
skipped |
Cues that could not be carried out, each {cue, reason} - an unresolved target, a missing verb, a handler that threw |
firedLog |
Every cue that fired, each {ms, cue}, ms measured from play() |
marks / markNames |
The world points a *mark ...* cue currently raises, and the names behind them. Empty whenever nothing is marked |
finished() |
Emitted after the last cue |
customCue(verb, arg) |
Emitted for a custom cue with no registered handler |
cueFired(type, arg) |
Emitted for every cue as it fires, so a scene can choreograph around the script |
An unresolved target is never fatal: the cue is skipped, recorded in skipped
and warned about once.
The parser on its own
scripting/performancescript.js is Qt-free - no Qt types, no clock, no
randomness - so scripts can be checked without an engine:
node plugins/clay_character3d/scripting/performancescript.test.js
parse(script, options)->{cues, errors}.options.strict(default true);options.extraVerbsaccepts additional directive names as{type: "custom", verb, arg}cues. Every cue carriesat, its character index in the source.describe(cue)-> the one-linerPerformance.currentCuepublishes.lint(scriptA, scriptB)-> divergences between the directive sequences of two languages of the same script, each{index, a, b, message}.
Cross-language lint
Direction lives inside the translated string, which is what lets a German
script time its cues to German word order - and what lets a translator reorder,
drop or translate a stage direction by accident. lint() compares the two
directive sequences, ignoring the spoken text and the time hints:
const Script = require('.../performancescript.js')
Script.lint(strings.en.introScript, strings.de.introScript)
// [] when the two are in sync
// [{index: 2, a: "point at battery", b: "point at Batterie",
// message: "argument differs: point at battery vs point at Batterie"}]
Speech timing
Speech publishes the numbers a script schedules against, instead of every
caller measuring a speech rate of its own:
estimateDurationMs(text)- how long the engine would take over the text at the current rate, without saying it.durationMs- the current line’s length; stale once the line ends.wordMarks()- the current line’s words as{offset, ms}.
Two engine behaviours a scheduler depends on: an empty or whitespace-only line
reports started() and finished() (asynchronously, never re-entrantly), so a
queue advancing on finished() cannot hang on it; and of several say() calls
in one tick, exactly the last one runs and it is the only one that reports
anything - a line replaced before it began emits neither signal.
API Reference
ActionCycleAnim A looping whole-body action - working at something, or boxing - replayed from the pure pose model in action.js
Properties
| Name | Type | Description |
|---|---|---|
action | string | Which action: "use" or "fight" |
cycle readonly | int | How many whole cycles have played since this started |
cycleMs readonly | real | One full cycle in milliseconds |
entity required | var | Character whose joints this writes |
handPose readonly | string | What the hands are shaped like while this runs, "" when it is not running - a fist for the fight, a loose hand for the work |
intensity | real | 0 is unhurried, 1 is hard at it. Changes the speed of the cycle and the size of the movement, never the shape of the pose |
phase | real | Where in the cycle it is, 0..1. Set it with the animation stopped to hold one frame of the action |
table readonly | var | Derived numbers the cycle is replayed from |
workHeight | real | Where the work is: 0 a table at the waist, 0.5 a counter at the chest, 1 a shelf at head height. Ignored by the fight |
Methods
| Method | Returns | Description |
|---|---|---|
apply(real t) | void |
Arm A complete arm with upper arm, lower arm, and hand
Properties
| Name | Type | Description |
|---|---|---|
articulated | bool | Whether the hand has fingers |
color | color | Color of the arm (upper and lower segments) |
fingers readonly | var | DetailedHand on this arm, or null when it has none |
gloveColor | color | Colour of the glove and its cuff |
gloved | bool | Whether the hand wears a glove: its own colour, and a cuff |
hand readonly | Node | Reference to the wrist joint for animation |
handColor | color | Colour of the bare hand. Ignored while gloved |
handDepth | real | Depth of the hand |
handHeight | real | Height of the hand |
handPose | string | What the hand is doing: "relax", "open", "point", "thumbsUp" or "fist" |
handScale | real | How much bigger the hand is drawn than the proportion tables give |
handWidth | real | Width of the hand |
indexTip readonly | vector3d | Where the hand ends, in hand's own frame - the point to aim along, and the point to measure a gesture against |
lowerArm readonly | Node | Reference to the elbow joint for animation |
lowerTaper | real | Taper factor for lower arm width/depth (0-1) |
mirrored | bool | Set on a left arm, so an articulated hand puts its thumb on the correct side |
poseOverride | var | Fields to replace in the articulated hand's pose row, or null |
upperArm readonly | Node | Reference to the shoulder joint for animation |
upperRatio | real | Proportion of total arm length for upper arm (0.4-0.6) |
BodyPart Base component for character body parts
Properties
| Name | Type | Description |
|---|---|---|
baseEuler | vector3d | Base rotation of the body part in Euler angles |
basePos | vector3d | Base position of the body part |
BodyPartsGroup Groups multiple body parts together as an invisible container
Character A fully animated 3D humanoid character with modular body parts
Properties
| Name | Type | Description |
|---|---|---|
actionHandPose readonly | string | What the running activity wants the hands to be doing, "" when none does |
actionIntensity | real | How hard at it a Using or Fighting character is, 0..1 |
activeMove readonly | string | Move being played, or the one whose last frame is being held; "" when nothing is |
activity | int | |
armColor | alias | |
armDepth | alias | |
armHeight | alias | |
armLowerTaper | alias | |
armUpperRatio | alias | |
armWidth | alias | |
autoBlink | bool | Whether the character blinks on its own. On by default |
belly readonly | BodyPart | Lower trunk. Pitching it bends the body at the hip; the chest, the arms and the head come with it |
bellyBulge | real | How far the belly swells past the plain trunk taper. 1 leaves it alone, 1.3 is a gut, below 1 tucks it in |
bellyColor | color | Colour of the lower trunk. Follows torsoColor until set |
bellyRatio | real | Belly's share of torsoHeight, 0..1. The rest is the chest, and the waist joint sits between them |
blinkSeed | int | Which repeatable blink-and-glance rhythm this character gets |
bodyDrift readonly | real | How far the body has travelled forward over its own feet, in the character's own units |
chest readonly | BodyPart | Upper trunk, on the waist joint. Pitching it against the belly is what rounds a back or lifts a chest - a curve rather than a tilt |
chestColor | color | Colour of the upper trunk. Follows torsoColor until set |
chestSwell | real | How much deeper the chest is than the plain trunk taper. 1 leaves it alone |
chinPointiness | alias | |
currentSpeed readonly | real | |
detail | enumeration | How much character to draw. Auto by default |
detailThreshold | real | How tall the character has to be on screen, in pixels, before Auto gives it fingers |
detailedHands readonly | bool | Whether the hands have fingers right now |
effectiveDetail readonly | enumeration | Which Detail level is actually being drawn - never Auto |
emotion readonly | string | Face the character is wearing between lines: "happy", "sad", "angry", "disgust", "surprised" or "" for neutral |
eyeColor | alias | |
eyeSize | alias | |
faceActivity | alias | |
footColor | alias | |
footDepth | alias | |
footHeight | alias | |
footWidth | alias | |
gait | Gait | How this character walks and runs, by preset and by factor |
gaitBuild | var | Build as the gait model reads it: an object with maturity, femininity, mass and muscle in 0..1, or null. A ParametricCharacter binds its sliders here |
gaitFactors readonly | var | Composed, clamped factor vector the cycles are walking with |
gaitFromBuild | bool | Whether the body's build shapes the gait. On by default |
gaitFromEmotion | bool | Whether the mood shapes the gait. On by default |
gazeBehaviour | bool | Whether the eyes move inside the head. On by default |
gesture readonly | string | What the hands are doing: "point", "present", "thumbsUp", "talk", or "" for nothing |
gestureBeatScale | real | Stretches the rhythm of gesticulate(). 1 is as authored, above 1 is a slower speaker |
gestureHand readonly | string | Which arm is doing it: "left", "right", or "" while released or while talking, which is two-handed |
gestureSettleMs | int | How long a gesture takes to arrive at, and to leave |
gestureSettled readonly | bool | True once the pose has arrived - the cue to start talking about the thing that was pointed at |
gloveColor | color | Colour of the gloves and their cuffs |
gloves | bool | Whether the hands are gloved - their own colour, and a cuff at each wrist |
hairColor | alias | |
hairVolume | alias | |
handColor | alias | |
handDepth | alias | |
handHeight | alias | |
handPose | string | What the hands do when no gesture is claiming them: "relax", "open", "point", "thumbsUp" or "fist" |
handRestRoll | real | Wrist's resting roll in degrees, signed per side by whoever applies it: 90 turns the palms in to face the body |
handScale | real | How much bigger the hands are drawn than the proportion tables give. 1 leaves them alone |
handWidth | alias | |
head readonly | Head | |
headHeight readonly | real | |
headPos readonly | vector3d | Where the head node sits, in the character's own coordinates - the origin of the anchors published by Head |
hip readonly | BodyPart | |
hipColor | alias | |
hipDepth | alias | |
hipHeight | alias | |
hipWidth | alias | |
idleCycleDuration | int | Duration of the idle animation cycle in milliseconds |
leftArm readonly | Arm | |
leftLeg readonly | Leg | |
leftShoulderPos readonly | vector3d | Where the left shoulder joint sits, in the character's own coordinates |
legColor | alias | |
legDepth | alias | |
legHeight | alias | |
legLowerTaper | alias | |
legUpperRatio | alias | |
legWidth | alias | |
listening readonly | bool | Whether this character is attending to someone |
listeningTo | var | Who this character is listening to, or null |
lowerHeadDepth | alias | |
lowerHeadHeight | alias | |
lowerHeadWidth | alias | |
minimalThreshold | real | How small the character has to get, in pixels of figure height, before Auto takes its face away |
mouthSize | alias | |
moveHandPose readonly | string | What the running move wants the hands to be doing, "" when no move is running. Folded into actionHandPose |
moveHolding readonly | bool | True while the loaded set owns the joints - playing, or sitting on the last frame of a move that holds |
movePlaying readonly | bool | True while a move is actually animating - false while a knockdown lies on the floor, which moveHolding is for |
moveSet | string | Loadable move set on this character, "" for none |
moveSetName readonly | string | What the loaded set calls itself, "" when none is loaded |
moves readonly | var | What the loaded set offers: a list of {name, label, group, loop, holds}, empty when no set is loaded |
name | string | Character identifier name |
neckHeight | real | |
noseSize | alias | |
rightArm readonly | Arm | |
rightLeg readonly | Leg | |
rightShoulderPos readonly | vector3d | Where the right shoulder joint sits, in the character's own coordinates (origin between the feet, +Z is the way the character faces) |
roundness | real | How rounded every box in the character is, 0 for the hard-edged original and about 0.3 for something nearly spherical |
runSpeed readonly | real | Running speed derived from animation geometry |
safeSilhouette | bool | Whether a raised pointing arm is forced to bend at the elbow |
shippedMoveSets readonly | var | Sets that ship with the plugin, as a map of name to the URL moveSet resolves it to |
shoulderWidth | alias | |
skinColor | alias | |
speaking readonly | bool | True while the character is speaking (text or audio), including between the segments of annotated text |
speech readonly | Speech | Character's speech engine for advanced configuration (volume, rate, pitch) and signals (started/finished) |
speechAccuracy | enumeration | How closely a recorded line is analysed for lip-sync |
speechBodyLanguage | bool | Whether emotional speech may use body language gestures |
speechEmotion readonly | string | Emotion of the current speech ("happy", "sad", "angry", "disgust", "surprised", or empty for neutral) |
thinking | bool | Whether the character is working something out |
torso readonly | BodyPart | Trunk as a whole - the frame belly and chest hang in. It draws nothing; turning it turns the whole upper body, which is what sway and rock do |
torsoColor | color | |
torsoDepth | alias | |
torsoHeight | alias | |
upperHeadDepth | alias | |
upperHeadHeight | alias | |
upperHeadWidth | alias | |
view | QtObject | View3D this character is being seen in |
waistPinch | real | How far in the waist joint is drawn, as a fraction of the width the taper would have there. 0 (the default) is no pinch |
waistWidth | alias | |
walkSpeed readonly | real | Walking speed derived from animation geometry |
workHeight | real | Where the work is while activity is Using: 0 a table at the waist, 0.5 a counter at the chest, 1 a shelf at head height |
Methods
| Method | Returns | Description |
|---|---|---|
actionPoseAt(string action, real t) | var | |
actionTable(string action) | var | |
applyActionPose(string action, real t) | void | |
applyGaitPose(string base, real t) | void | |
applyMovePose(string move, real t) | void | |
gaitPoseAt(string base, real t) | var | |
gaitTable(string base) | var | |
gesticulate() | void | |
lookAt(vector3d worldPos) | void | |
movePoseAt(string move, real t) | var | |
nod(real degrees, int times) | void | |
playMove(string move) | bool | |
pointAt(vector3d worldPos, string which) | void | |
presentAt(vector3d worldPos, string which) | void | |
say(string what, string emotion, string transcript) | void | |
setEmotion(string name) | void | |
stopGesture() | void | |
stopMove() | void | |
stopSpeaking() | void | |
thumbsUp(string which) | void | |
turnTo(vector3d worldPos) | void |
Signals
| Signal | Description |
|---|---|
moveFinished(string move) |
CharacterCamera Third-person camera that follows a Character
Properties
| Name | Type | Description |
|---|---|---|
character | Character | To follow |
clipFar | real | Far clipping plane distance |
clipNear | real | Near clipping plane distance |
maxOrbitDistance | real | Maximum allowed orbit distance |
maxOrbitPitch | real | Maximum pitch angle (looking up limit) |
minOrbitDistance | real | Minimum allowed orbit distance |
minOrbitPitch | real | Minimum pitch angle (looking down limit) |
orbitDistance | real | Distance from the character |
orbitPitch | real | Vertical angle above horizontal (degrees) |
orbitYawOffset | real | Horizontal offset from character's facing direction (degrees) |
CharacterController Player input controller for Character movement
Properties
| Name | Type | Description |
|---|---|---|
axisX required | real | Horizontal input axis for turning (-1 to 1, required) |
axisY required | real | Vertical input axis for forward/backward (-1 to 1, required) |
character required | QtObject | To control (required) |
enabled | bool | Whether the controller is active |
inputDeadzone | real | Axis values below this threshold are treated as zero |
isMoving readonly | bool | True when forward/backward input is active |
isTurning readonly | bool | True when turn input is active |
processedAxisX readonly | real | Horizontal axis value after deadzone is applied |
processedAxisY readonly | real | Vertical axis value after deadzone is applied |
sprinting | bool | When true, character runs instead of walks |
turnSpeed | real | Turn speed in degrees per frame per unit axis input |
updateInterval | int | Update frequency in milliseconds |
Signals
| Signal | Description |
|---|---|
moved(real deltaX, real deltaZ) | |
turned(real deltaYaw) |
DetailedHand Four fingers and a thumb on the end of an Arm
Properties
| Name | Type | Description |
|---|---|---|
foldFar | real | And how far the second joint bends on top of it |
foldNear | real | How far the knuckle bends at full curl, in degrees |
indexTip readonly | vector3d | Where the extended index finger ends, in the wrist joint's own frame - the point to aim, and the point to measure |
mirrored | bool | Set on the left hand, so the thumb ends up on the other side |
palmDepth | real | Depth of the palm |
palmHeight | real | Height of the palm, measured down from the wrist joint. The knuckles sit at its far end |
palmWidth | real | Width of the palm the fingers are packed across |
pose | string | Which shape the hand takes: "relax" (default), "open", "point", "thumbsUp" or "fist" |
poseOverride | var | Fields to replace in the current pose's row, or null |
settleMs | real | How long a pose change takes, in milliseconds |
thumbDown | real | Where the thumb leaves the palm, as a fraction of the palm's length measured down from the wrist |
thumbOut | real | And how far out to the side, as a fraction of the palm's width |
tone | color | Skin colour of the fingers |
tuckFar | real | Same for the far segment, which does the reaching |
tuckNear | real | How much of its length the near segment gives up at full curl |
FaceBox A body-part box that draws a face into its own front surface
Properties
| Name | Type | Description |
|---|---|---|
browAngle | real | |
browHalf | vector2d | |
browOffset | vector2d | |
browSkew | real | |
eyeCentre | vector2d | |
eyeHalf | real | |
eyeHood | real | |
eyeSquint | real | |
faceDetail | int | |
gaze | vector2d | |
mouthCentre | vector2d | |
mouthCornerLift | real | |
mouthGap | real | |
mouthHalf | vector2d | |
mouthRound | real | |
mouthSkew | real | |
panel | int |
FightAnim Boxing: a bladed stance, a guard, and alternating straights
Foot A foot body part with extended toe shape
Gait How a character walks and runs: a named preset, thirteen factors, or both
Properties
| Name | Type | Description |
|---|---|---|
armForward | real | Degrees the whole arm swing is carried ahead of the body: more reach in front, less behind, the same amplitude. Bent elbows plus this is fists pumping before the chest |
armOut | real | How far the upper arms are carried out from the ribs, in degrees. 0 hangs them against the body |
armSwing | real | Arm swing amplitude. 1 is as authored, 0 hangs the arms |
bounce | real | How high the whole figure rises at mid-step, as a fraction of leg height. 0 is flat, 0.05 is a visible spring, 0.1 the cap |
elbow | real | Elbow bend in degrees on top of the cycle's own (a walk bends 10, a run 70). Bent elbows with a quick tempo read as fists pumping |
factors readonly | var | Preset and the factors above composed into one clamped factor vector - this object's whole contribution to a gait |
headPitch | real | Head pitch in degrees. Positive looks down, negative lifts the chin |
kneeLift | real | Knee lift. 1 is as authored; below it drags the feet, above it high-steps. The foot angles follow it |
lean | real | Torso pitch in degrees on top of the cycle's own. Positive leans forward (a slump, a charge), negative back (chest out). Bends at the waist: the hip counters it, so the legs stay planted and only belly, chest, head and arms tip. It is shared between the two trunk segments and brings a little curve with it; spineCurve asks for more |
preset | string | A gait by name, or empty for none |
presetKnown readonly | bool | False while preset names something the table does not have |
presetNames readonly | list | Every name preset accepts, for a picker |
rock | real | Torso roll in degrees over the planted leg, alternating with the step - the side-to-side shift of weight. 0 is none |
spineCurve | real | How round the back is, in degrees: the angle between belly and chest at the waist joint. Positive rounds it forward (sad, elderly, sneaking), negative arches it and lifts the chest (proud, marching) |
stride | real | How far the legs swing. 1 is as authored; below it shortens the step and the speed with it |
sway | real | Hip yaw in degrees, alternating with the step, the torso countering by half. 0 is none |
tempo | real | Cadence. 1 is as authored; 1.2 takes steps a fifth quicker and covers ground a fifth faster, since speed follows the feet |
GaitCycleAnim One locomotion cycle: two mirrored steps over a table that gait.js derives from a base (walk or run) and a factor vector
Properties
| Name | Type | Description |
|---|---|---|
base | string | "walk" or "run": whose authored numbers the table starts from |
cycleMs readonly | real | One full cycle, two steps, in milliseconds |
derivedSpeed readonly | real | Ground speed that keeps the feet from sliding, from the hip angles, the leg and the cycle length |
factors | var | Composed factor vector, or null for neutral. Taken from the entity's gaitFactors when it has one |
lift readonly | real | How far the figure is raised right now, in world units: up to bounce x legHeight at mid-step, zero at contact, and zero whenever the cycle is not running |
strideLength readonly | real | How far the feet travel in one full cycle |
table readonly | var | Derived angles and cycle length the phases animate to |
GazeAnim Where the eyes point inside a head that is aiming itself
Properties
| Name | Type | Description |
|---|---|---|
averting | bool | |
gaze readonly | vector2d | |
head | var | |
interval | int | |
running | bool | |
seed | int | |
target | var | |
yawRange | real |
Signals
| Signal | Description |
|---|---|
saccaded() |
GestureAnim Held poses - pointing, presenting, thumbs up, gesticulation and look-at - for one character
Properties
| Name | Type | Description |
|---|---|---|
activeGesture readonly | string | Which gesture is being held: "point", "present", "thumbsUp", "talk" or "" |
activeHand readonly | string | Which arm is doing it: "left", "right", or "" while released or while talking, which is two-handed |
beatScale | real | Stretches or compresses the talking rhythm. 1 is as authored, above 1 is a slower speaker |
entity | var | Character to drive. Null means the layer does nothing |
gesture | string | Which pose is wanted: "point" or "present" (both need target), "thumbsUp", "talk", or "" to release |
hand | string | Which arm gestures: "auto" (the one nearer the target), "left" or "right" |
holding readonly | bool | True while this layer owns the joints - including the ease back to rest, which is not finished until it is false |
leftHandPose readonly | string | DetailedHand pose for the left hand, or "" |
lookTarget | var | Where the head looks, a scene-space vector3d, or null |
rightHandPose readonly | string | DetailedHand pose for the right hand, or "" for "not mine to say" - then the character's own handPose applies |
safeSilhouette | bool | Whether a raised arm is forced to bend at the elbow |
settleMs | int | How long a pose takes to arrive at - and to leave |
settled readonly | bool | True once the pose has arrived - the cue to start talking about the thing that was pointed at |
target | var | Where to point or present: a scene-space vector3d, or null for nowhere |
Methods
| Method | Returns | Description |
|---|---|---|
drop() | void | |
look(var where) | void | |
request(string what, var where, string which) | void | |
turnTo(var where) | void |
Hand A whole hand in a single box, or the palm under a DetailedHand
Properties
| Name | Type | Description |
|---|---|---|
palmDepth | real | Thickness of the palm |
palmHeight | real | Wrist-to-knuckles length of the palm. A whole hand is roughly twice this |
palmWidth | real | Width of the palm. Every pose is measured against it |
pose | string | What the hand is doing: "open", "relax", "point", "thumbsUp", "fist" - or "" for a palm under a set of real fingers |
settleMs | real | How long the shape takes to change, in milliseconds. Matches DetailedHand, so the two levels of detail take the same time to answer a pose change |
Head A complete head with facial features and expressions
Properties
| Name | Type | Description |
|---|---|---|
activity | int | Current facial expression activity |
autoBlink | bool | Whether the eyes blink by themselves |
blinkAmount | real | How shut the blink currently has the eyes, 0 to 1 |
blinkInterval | int | Milliseconds between blinks when autoBlink is on |
blinkSeed | int | Which irregular-but-repeatable blink rhythm this head gets |
browAngle readonly | real | Angle the current expression is holding the brows at, in degrees. Positive drops the inner ends into a V |
browFlash | real | A momentary brow raise on top of whatever the face is wearing |
browRise readonly | real | How far above their resting place the brows are, as a length in the same units eyeWidth is measured in |
browSkew readonly | real | How far one brow is raised above the other, as a length |
chinBottom readonly | real | Y of the chin, which drops below the head origin while the mouth is open |
chinPointiness | real | How pointed the chin is (0-1) |
crownTop readonly | real | Y of the top of the skull, hair not included |
detail | int | Current level of head detail. Use the Head.Detail enum |
earPos readonly | vector3d | Origin of the right ear (bottom centre of its box). The left ear is the same point with x negated |
earSize readonly | real | Edge length of one ear, which is a cube like the eyes |
earTop readonly | real | Y of the top of the ears |
eyeColor | color | Colour of the IRISES - not of the eye as a whole |
eyeHood | real | How far the upper lid is lowered, 0 open to 1 nearly shut |
eyeLine readonly | real | Y of the eye centres |
eyeRelief readonly | real | How far the eyes stand proud of faceFront |
eyeSize | real | Eye size multiplier (0.5 = small, 1.0 = normal, 1.5 = large) |
eyeSpacing readonly | real | How far each eye centre sits from the head's centre line |
eyeSquint | real | How far the lower lid is raised, 0 open to 1 nearly shut |
eyeWidth readonly | real | Edge length of one eye. The eyes are cubes, so this is their height and depth as well |
faceBack readonly | real | Z of the back face of the cranium - where hair that wraps the skull has to stop |
faceDetail readonly | int | How much of the drawn face to draw: 1 all of it, 0 only what survives being small |
faceFront readonly | real | Z of the front face of the cranium - the plane the eyes and the nose stand on |
faceOffsetZ readonly | real | How far forward of the head node both head boxes sit |
features | bool | Whether the face is drawn at all - eyes, brows, nose, ears and mouth |
gaze | vector2d | Where the eyes look, as a fraction of the irises' free travel inside the whites. (0,0) is straight ahead, (1,0) hard right |
hairColor | color | Color of the hair and eyebrows |
hairOuterX readonly | real | How far from the centre line the head's own side hair reaches |
hairVolume | real | Hair volume multiplier (0 = bald, 1.0 = normal, 1.5 = voluminous) |
jawDrop | real | How far the chin stretches down when mouthOpen is 1, as a fraction of the lower head height |
jawFront readonly | real | Z of the front face of the jaw - the plane the mouth sits on |
lowerHeadDepth | real | Depth of the lower head (jaw) |
lowerHeadHeight | real | Height of the lower head (jaw) at rest |
lowerHeadWidth | real | Width of the lower head (jaw) |
mouthBottom readonly | real | Y of the lowest point the mouth currently reaches - the bottom of the cavity, which grows downward as it opens |
mouthCornerLift | real | Mouth corner position from frown (-1) over neutral (0) to smile (1) |
mouthLine readonly | real | Y of the upper lip |
mouthOpen readonly | real | How far the mouth/jaw is opened (0 = closed, 1 = fully open) |
mouthRound readonly | real | How rounded/puckered the mouth is (0-1), e.g. for "oo" sounds |
mouthSize | real | Mouth size multiplier (0.5 = small, 1.0 = normal, 1.5 = large) |
mouthSkew | real | A one-sided lip curl, -1 to 1. A sneer, not a frown |
mouthWide readonly | real | How far the mouth is stretched sideways (0-1), e.g. for "ee" sounds |
mouthWidth readonly | real | Width of the closed, unstretched mouth. What mouthWide and mouthRound do to it is momentary and not included here |
nodAmount readonly | real | How far into a nod the head is, in degrees |
noseBottom readonly | real | Y of the underside of the nose - as far down the face as spectacles can slip |
noseSize | real | Nose size multiplier (0.5 = small, 1.0 = normal, 1.5 = large) |
offsetEuler | vector3d | A momentary rotation on top of whatever the head is aiming at |
poseEuler | vector3d | Where a body animation is aiming the head |
skinColor | color | Color of the skin (face, ears, nose) |
speechActive readonly | bool | True while the speechSource is speaking and driving the mouth |
speechSource | var | Optional lip-sync driver, typically a Speech instance |
talkDuration | int | Duration of mouth open/close cycle when talking in milliseconds |
toEmotionDuration | int | Duration of emotion transition animations in milliseconds |
upperHeadBottom readonly | real | Y of the seam between jaw and cranium - the bottom of the upper head box |
upperHeadDepth | real | Depth of the upper head (cranium) |
upperHeadHeight | real | Height of the upper head (cranium) |
upperHeadWidth | real | Width of the upper head (cranium) |
Methods
| Method | Returns | Description |
|---|---|---|
blink() | void | |
expressionTargets(int activity) | var | |
flashBrows(real amount) | void | |
nod(real degrees, int times) | void |
HeadEulerAnim EulerAnim for the head, which does not own its own rotation
IdleAnim Resting pose: what a body holds when no activity and no gesture is driving it
Leg A complete leg with upper leg, lower leg, and foot
Properties
| Name | Type | Description |
|---|---|---|
color | color | Color of the leg (upper and lower segments) |
foot readonly | Node | Reference to the ankle joint for animation |
footColor | color | Color of the foot (shoe color) |
footDepth | real | Depth of the foot |
footHeight | real | Height of the foot |
footWidth | real | Width of the foot |
lowerLeg readonly | Node | Reference to the knee joint for animation |
lowerTaper | real | Taper factor for lower leg width/depth (0-1) |
upperLeg readonly | Node | Reference to the hip joint for animation |
upperRatio | real | Proportion of total leg length for upper leg (0.4-0.6) |
ListenAnim What a character does while somebody else is talking
Properties
| Name | Type | Description |
|---|---|---|
listener | var | |
seed | int | |
speaker | var |
Signals
| Signal | Description |
|---|---|
phraseEnded() |
MartialArts Martial-arts move set: fourteen moves, from the neutral stance to a knockdown and back up off the floor
MoveSet A loadable set of named moves, played onto one Character
Properties
| Name | Type | Description |
|---|---|---|
autoRest | bool | Whether a one-shot hands back to restMove when it ends |
blendMs | int | How long a move takes to ease out of the pose the body was already in, in milliseconds |
entity | var | Character whose joints this writes |
handPose readonly | string | What the current frame wants the hands to be doing, "" when the set is idle - a fist through most of a fighting set, a flat hand where one is planted on the floor |
holding readonly | bool | True while the set owns the joints - playing a move, or sitting on the last frame of one that holds |
intensity | real | How hard the moves are thrown, 0..1 |
model | var | Set's pose model - the object answering moves(), derive(), poseAt(), mixPose() and restMove() |
move readonly | string | Being played, or the one whose last frame is being held; "" when the set is idle |
moves readonly | var | What the set offers: a list of {name, label, group, loop, holds}, in the model's own order |
name | string | What the set is called, for a UI to show |
phase | real | Where in the move it is, 0..1. Set it with the animation stopped to hold one frame |
playing readonly | bool | True while a move is actually animating |
restMove readonly | string | Move a character in this set stands in when it is doing nothing else, "" when the set names none |
Methods
| Method | Returns | Description |
|---|---|---|
apply(string move, real t) | void | |
has(string move) | bool | |
play(string move) | bool | |
poseAt(string move, real t) | var | |
stop() | void | |
tableFor(string move) | var |
Signals
| Signal | Description |
|---|---|
finished(string move) |
PatrolController AI controller for character patrol behavior
Properties
| Name | Type | Description |
|---|---|---|
arrivalThreshold | real | Distance at which character is considered to have arrived |
character required | QtObject | To control (required) |
destX | real | Current destination X coordinate |
destZ | real | Current destination Z coordinate |
enabled | bool | Whether the controller is active |
isIdle | bool | True when character is pausing between destinations |
maxIdleTime | int | Maximum idle pause duration in milliseconds |
maxX | real | Maximum X coordinate of patrol area |
maxZ | real | Maximum Z coordinate of patrol area |
minIdleTime | int | Minimum idle pause duration in milliseconds |
minX | real | Minimum X coordinate of patrol area |
minZ | real | Minimum Z coordinate of patrol area |
turnSpeed | real | Turn speed in degrees per update |
updateInterval | int | Update frequency in milliseconds |
Performance Plays a performance script - speech and stage directions in one string - against a character
Properties
| Name | Type | Description |
|---|---|---|
cueCount readonly | int | |
cueIndex readonly | int | |
currentCue readonly | string | |
debug | bool | Narrates every cue to the console as it fires |
done readonly | bool | |
errors readonly | var | |
extraVerbs | var | Extra directive names the parser accepts, lower case |
firedLog readonly | var | |
markNames readonly | var | |
marks readonly | var | |
performer | var | Character that acts the script |
resolveTarget | var | Optional function(name) returning a vector3d, or null |
running readonly | bool | |
searchRoot | var | Where the default target resolver looks |
skipped readonly | var | |
spoken | bool | Whether lines without a clip are voiced at all |
viewerPosition | var | Where "viewer" is: a vector3d, or a function() returning one |
voiceOf | var | Optional function(sayIndex) returning a clip url per spoken line |
Methods
| Method | Returns | Description |
|---|---|---|
estimateMs(string text) | int | |
play(string script) | bool | |
playFrom(int index) | bool | |
registerVerb(string name, var handler) | void | |
stop() | void |
Signals
| Signal | Description |
|---|---|
cueFired(string type, string arg) | |
customCue(string verb, string arg) | |
finished() |
RunAnim Run: GaitCycleAnim over the run base - the classic high knees, pumping arms and forward lean, at nearly twice the cadence
Properties
| Name | Type | Description |
|---|---|---|
derivedRunSpeed readonly | real | Ground speed that keeps the feet from sliding |
Speech Voice output with approximate lip-sync for characters
UseAnim Working at something - a desk, a table, a workbench
WalkAnim Walk: GaitCycleAnim over the walk base
Properties
| Name | Type | Description |
|---|---|---|
derivedWalkSpeed readonly | real | Ground speed that keeps the feet from sliding |