V1 · C++ VP2
Line-of-action visualiser for Maya 2024–2026 · Long Winter Studios

How It Works

AXE Thread draws a continuous line-of-action curve through a chain of rig controls in VP2 — no geometry, no deformers, no scene clutter. Select controls in order, click Add Thread, and a live colour-coded arc follows the chain as you animate.

1
Sphere Shapes — One Per Control

For each control in the selection, an axeThreadShape C++ node is created as a direct child under that control's transform (not a sibling shape — child transform). This means each sphere node lives in the control's local space and its world-space pivot position is trivially available to the draw override every frame. The shape stores its chain identity (chainId, chainIndex, chainCount) and a minimal pointData float array.

2
Position Sharing via optionVars

In its prepareForDraw call, each sphere node reads its own world-space rotatePivot position and writes it to a Maya optionVar keyed by chainId and chainIndex. These optionVars are the shared memory bus between the sphere nodes and the curve node — no DG connections carrying positional data, just fast key-value reads and writes.

3
Curve Node — One Per Chain

A single axeThreadShape in "curve mode" (chainIndex = -1) is created under the hidden AXE_thread_root transform. In its prepareForDraw, it reads all the sphere positions for its chain out of the optionVars and builds a smooth interpolated line through them. The line colour at each segment is computed from the bend angle between adjacent controls — blue for straight, through orange, to hot-pink for sharp bends.

4
Dirty Propagation — No Polling

Each sphere's .message attribute is connected to the curve node's .sphereMessages[i] array. When a control moves, VP2 marks the sphere shape dirty. That dirty state flows through the message connection to the curve node, marking it dirty in the same evaluation frame. VP2 then redraws the curve node immediately — no scriptJobs, no dgdirty calls, no per-frame polling. The curve always shows the current pose with zero latency.

5
Chain ID and Lookup

All nodes in a chain share a chainId string (derived from the first control's short name, e.g. spine_C0_ctl_thread). The CHAIN_TAG attribute on each node stores this ID and is used by the Python layer to find, list, and remove chains without any additional tracking structures. The entire chain state lives in the scene's node graph.

Non-destructive. Thread nodes are shapes under existing control transforms — no new transforms, no rig connections, no weight or constraint data. Removing a thread deletes only the axeThreadShape nodes and the curve node under AXE_thread_root. The rig is untouched. Build and remove are both fully undoable.

Colour Coding

The thread line colour is computed in the C++ draw override from the bend angle between adjacent controls in the chain. It reads the arc as a continuous gradient — at a glance you can see where the spine is straight, where it's easing, and where it's buckling.

Cyan
Straight — low bend
arc flows smoothly
Orange
Mid bend
pose has shape
Hot-Pink
Sharp bend
possible hinge point
Colour mapping is hard-coded in C++ and cannot currently be customised per-chain. The gradient is designed to match the same cyan → orange → pink palette used across the AXE suite. A sensitivity slider exists in the UI code but is hidden — it was superseded by the fixed C++ mapping, which proved more reliable across different rig scales.

UI Reference

Control What it does
Active Threads list Displays all axeThreadChain-tagged nodes in the current scene, grouped by chain ID. Each entry shows the chain name and control count. The list is populated by scanning for axeThreadShape nodes — it reflects the live scene state. Use Refresh to resync after external scene changes.
Refresh Rescans the scene for axeThreadShape nodes and rebuilds the list. Useful after opening a saved scene, undoing a build, or any operation that adds or removes thread nodes outside the UI.
Remove Removes the thread chain selected in the list. Deletes all sphere shapes under each control in the chain, then deletes the curve node from AXE_thread_root. Fully undoable. Does not affect the rig controls themselves.
Thread Size slider Adjusts the visual weight of all threads in the scene simultaneously. Range 0.01–1.0 (displayed as 0.00–1.00). Drives lineWidth on all curve nodes — larger values give thicker, more readable lines. Updates live as you drag. Does not have a per-chain control; it's a global scene-wide setting.
Add Thread Primary action. Builds a new thread chain from the current Maya selection. Controls are used in the order they appear in the selection (the order you clicked them). Requires at least 2 controls. Requires a valid license — the button silently re-validates the stored key before proceeding.
License section Token entry and validation UI. Enter your key from Long Winter Members → My Account → Maya Tool Licenses. The token is cached to disk after a successful validation; subsequent launches validate silently in the background using the stored key without blocking the UI.
Selection order matters. Controls are chained in the order they are selected when you click Add Thread. For a spine chain, click from hips to neck in order — not randomly. If the line-of-action looks wrong (zigzagging rather than flowing), undo and reselect in anatomical order from root to tip.

Node Anatomy

A single thread chain creates the following nodes in the scene. All are tagged with the axeThreadChain attribute for reliable lookup and removal.

SPH
spine_C0_ctl_axeThreadShape
Sphere node. One per control. Created as a direct child shape under the control transform. Stores chain identity, index, and count. Writes world-space position to optionVars in prepareForDraw.
SPH
spine_C1_ctl_axeThreadShape
Next sphere in the chain. chainIndex = 1. Inherits its control's world matrix — moves with the control automatically, no connections needed.
···
… (one per control)
CRV
axeThread_curve_1
Curve node. One per chain. chainIndex = -1 (curve mode). Parented under AXE_thread_root, hidden in outliner. Reads sphere positions from optionVars and draws the interpolated coloured line. Connected to all sphere .message attrs for dirty propagation.
ROOT
AXE_thread_root
Shared parent transform for all curve nodes in the scene. Hidden in outliner. Created on first thread build, persists until manually deleted or the last chain is removed.
Do not parent, rename, or reparent thread nodes. The chain lookup is purely attribute-based (axeThreadChain string tag) but the dirty propagation relies on the message connections between sphere shapes and the curve node. Breaking those connections will cause the curve to stop updating when controls move.
Do not delete AXE_thread_root manually. Deleting it will orphan all curve nodes if Maya's undo history is not intact. Use Remove in the UI or the Python API — these find and clean up curve nodes by chain ID before removing the root if it becomes empty.

FAQ

The thread draws but doesn't update when I scrub — it lags one frame behind

This should not happen under normal conditions — the dirty propagation via message connections is designed to update the curve node in the same evaluation frame as the sphere nodes. If you're seeing lag, a likely cause is that one or more message connections between sphere shapes and the curve node have been broken.

Check via the Node Editor: open the curve node (axeThread_curve_#) and inspect the sphereMessages array. All indices from 0 to chainCount-1 should have incoming connections from their respective sphere shapes. If any are missing, rebuild the chain (Remove + Add Thread).

Another rare cause: if VP2 is in a degraded state (many overrides active, complex scenes), the dirty flush may be deferred. Toggle the renderer or force a refresh: cmds.refresh(force=True).

The thread is drawing in the wrong place — it's offset from the controls

The curve reads its positions from optionVars written by the sphere nodes' rotatePivot world position. If the thread looks offset, either the sphere nodes were built when the rig was in a different pose, or the rotatePivot of the control has a non-default offset.

Most mGear controls have their rotate pivot at the origin of the local transform, so this is rarely an issue. If you're using custom rig controls with an intentionally offset pivot, the thread will draw from that pivot point — which may not be the visual centre of the control.

Fix: remove and rebuild the thread with the rig in a neutral bind pose.

I added a thread but nothing appears in the viewport

In order:

VP2 must be active. The C++ draw override only runs under Viewport 2.0. Switch via Renderer → Viewport 2.0 in the panel menu.

Plugin not loaded. Without axeThreadPlugin.mll, the node type doesn't exist and creation silently fails. Check the Script Editor for a warning like [AXE Thread] Plugin 'axeThreadPlugin' could not be loaded. Ensure the axeThread.mod file is in Maya's modules directory.

License not validated. The Add Thread button checks the license before calling create_thread(). If the license check fails, the build is skipped with a warning in the Script Editor. Enter your key in the License section of the UI.

Only one control selected. The chain requires at least 2 controls. Single-control selections are rejected with a warning.

The thread list is empty after reopening the scene

Thread nodes are stored in the Maya scene file and survive save/open — they are ordinary DG nodes with custom node types, not transient objects. If the list is empty after reopening, the most likely cause is that the axeThreadPlugin.mll was not loaded when the scene was opened, so Maya couldn't recognise the node type and may have stripped or deferred the nodes.

Ensure the plugin loads automatically via the axeThread.mod file (which sets up the auto-load path). Once the plugin is loaded, click Refresh in the UI to rescan the scene — if the nodes are present, they'll appear.

If the nodes genuinely didn't survive the save, check if the scene was saved in ASCII (.ma) mode — sometimes studio pipeline tools strip unknown nodes on save. Binary (.mb) scenes are generally safer for custom node types.

The chain name in the list looks wrong / is a long namespace string

The chain ID is generated from the first control's short name, with namespace and DAG path stripped, plus a _thread suffix. For example, selecting char_rig_RN:spine_C0_ctl as the first control produces a chain ID of spine_C0_ctl_thread.

If you selected a control with a very long or oddly formatted short name (e.g. an auto-generated node), the chain ID will reflect that. This is cosmetic only — it doesn't affect functionality. You can't rename chains post-build in the current version.

Can I have multiple threads in the same scene?

Yes — each Add Thread call creates an independent chain with its own unique chain ID, its own sphere nodes, and its own curve node. You can have as many chains as you need (spine, arms, legs, tail, etc.) and they all live in the scene simultaneously.

All chains are listed in the Active Threads list. The Thread Size slider affects all chains at once (it's a global scene setting), but each chain's curve node has independent connections and dirty propagation.

Can I add or remove individual controls from an existing chain?

Not directly in the current version. Chains are built as atomic units — the sphere count, chain indices, and message connections are all set at build time and the curve node expects exactly chainCount sphere positions in the optionVar slots.

To modify a chain, remove it entirely and rebuild with the updated control list. Build and remove are both undoable, so you can iterate quickly.

Does it work with referenced rigs?

Yes. Sphere nodes are built as direct children of the control transforms. For referenced rigs this means the sphere nodes are created in the referencing scene, not inside the rig file — exactly the same pattern as AXE Proxy. The nodes live in the top-level scene and reference the rig controls only through their parent transform hierarchy (not through stored paths or attribute connections that could break on namespace changes).

One important note: if the reference is removed from the scene, the sphere nodes lose their parent transforms and will error on next evaluation. Remove the thread before removing the reference.

Thread Size slider affects all threads — can I control them individually?

Not through the UI currently. The size slider calls set_thread_size(chain_id, value) for every chain in the scene. For per-chain control, you can set line width directly via the Python API or the Node Editor — find the curve node (axeThread_curve_#) and adjust its lineWidth attribute directly.

The colour isn't changing even though the spine is bending

The bend angle colour is computed in the C++ draw override from the angle between adjacent segment vectors. If the colour is stuck at one value, the most likely cause is that the sphere positions in the optionVars are not updating — meaning the sphere nodes' prepareForDraw isn't being called.

This typically happens when the sphere nodes are marked as not visible (their transforms or shapes may have visibility off). Check that the control transforms and all shapes under them are visible. Also confirm VP2 is the active renderer — other renderers won't call the custom draw override.

Will threads slow down playback?

The per-frame cost is very low. Each sphere node writes 3 floats (XYZ) to an optionVar — essentially free. The curve node reads N×3 floats, fits a smooth interpolated polyline, and issues a single draw call. For a typical spine of 5–7 controls this is negligible.

The main cost is the VP2 draw call itself — but unlike AXE Onion, AXE Thread does not force viewport refreshes. It only redraws when the dirty propagation fires (i.e. when a control actually moves). During playback this is every frame; at rest it's zero cost.

With many simultaneous chains (10+) on a slow GPU, the draw calls can add up. In that case, use the display layer or visibility controls to hide threads during heavy scrubbing or final playblast.

Undo doesn't seem to restore the thread after Remove

Both create_thread and remove_thread are wrapped in undo chunks (axeThread_create and axeThread_remove) and should be fully undoable. If undo isn't working, check the Script Editor for errors during the operation — a Python exception that escapes the finally block can leave the undo chunk open.

After undoing, click Refresh in the list to rescan the scene — the list is not automatically updated by undo events.

Python API

All operations are available as clean Python calls for shelf buttons, pipeline scripts, and batch builds.

axe_thread.create_thread(controls=None)
Build a thread chain. Pass an explicit list of control paths, or leave as None to use the current Maya selection (in selection order). Returns a dict with spheres, curve, and chain_id. Requires 2+ controls.
BUILD
axe_thread.remove_thread(controls=None)
Remove thread nodes from the given controls (or current selection). Finds chain IDs from the control shapes, deletes sphere shapes, then finds and deletes the associated curve node from AXE_thread_root.
BUILD
axe_thread.set_thread_size(chain_id, size)
Set the visual size of a specific chain. size is 0.0–1.0. Drives lineWidth on the chain's curve node: maps to roughly 2–20px. To set all chains at once, iterate get_thread_shapes across all controls.
DISPLAY
axe_thread.get_thread_shapes(control)
Returns a list of axeThreadShape node paths under the given control transform. Useful for querying whether a control is already part of a chain, or for direct attribute access on the shape node.
QUERY
axe_thread.show()
Open the AXE Thread UI panel. Safe to call multiple times — closes and recreates the window if it already exists.
QUERY
# Build a spine thread from an explicit control list import axe_thread spine_controls = [ "|char_rig_RN:spine_C0_ctl", "|char_rig_RN:spine_C1_ctl", "|char_rig_RN:spine_C2_ctl", "|char_rig_RN:chest_C0_ctl", ] result = axe_thread.create_thread(controls=spine_controls) print(result["chain_id"]) # e.g. "spine_C0_ctl_thread" # Adjust size after build axe_thread.set_thread_size(result["chain_id"], 0.4)

Requirements

Maya Version
2024, 2025, 2026 (Windows)
Renderer
Viewport 2.0 — required for C++ draw override
Plugin
axeThreadPlugin.mll — C++ MPxLocatorNode + DrawOverride
Python
Python 3 (bundled with Maya 2024+)
Minimum Controls
2 per chain — single-control selections are rejected
Qt Binding
PySide2 (Maya 2024) or PySide6 (Maya 2025+) — auto-detected