Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public void OnDestroy()
BasisObjectSyncDriver.OnDestroy();
Application.onBeforeRender -= OnBeforeRender;
RemoteBoneJobSystem.Dispose();
BasisAuthoredMotionSystem.Dispose();
BasisAvatarBufferPool.Deinitialize();
}

Expand Down Expand Up @@ -342,6 +343,9 @@ public void LateUpdate()
}
ProfileEnd(PROF_BLENDSHAPE_APPLY);

// ── Authored motion: write non-humanoid authored bones before jiggle samples them ──
BasisAuthoredMotionSystem.Complete(BasisAuthoredMotionSystem.Schedule());

// ── JigglePhysics schedule ──
ProfileBegin(PROF_JIGGLE_SCHEDULE);

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ public void InitialLocalCalibration(BasisLocalPlayer player, List<BasisHeadChop.
Rig.OnInitialize();
}

// Register authored motion (drives non-humanoid transforms IK doesn't touch); rest captured at the current TPose.
var authoredMotions = player.BasisAvatar.GetComponentsInChildren<BasisAuthoredMotion>(true);
for (int i = 0; i < authoredMotions.Length; i++)
{
BasisAuthoredMotionSystem.Register(authoredMotions[i]);
}

player.LocalRigDriver.Builder = BasisHelpers.GetOrAddComponent<RigBuilder>(AvatarAnimatorParent);
player.LocalRigDriver.Builder.enabled = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ public void RemoteCalibration(BasisRemotePlayer RemotePlayer)
Rig.OnInitialize();
}

// Register authored motion (drives non-humanoid transforms the bone job / IK don't touch); rest captured at the current TPose.
var authoredMotions = RemotePlayer.BasisAvatar.GetComponentsInChildren<BasisAuthoredMotion>(true);
for (int i = 0; i < authoredMotions.Length; i++)
{
BasisAuthoredMotionSystem.Register(authoredMotions[i]);
}

// Face visibility setup
Player.FaceIsVisible = false;
if (RemotePlayer.BasisAvatar == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,17 @@ public void OnDestroy()

OnRemotePlayerDestroying?.Invoke();

// Unregister authored motion while the avatar transforms are still alive, so the
// TransformAccessArray entries drop cleanly before Unity destroys them.
if (BasisAvatar != null)
{
var authoredMotions = BasisAvatar.GetComponentsInChildren<BasisAuthoredMotion>(true);
for (int i = 0; i < authoredMotions.Length; i++)
{
BasisAuthoredMotionSystem.Unregister(authoredMotions[i]);
}
}

if (RemoteAvatarDriver.InBoneDriver)
{
RemoteBoneJobSystem.RemoveRemotePlayer(NetworkReceiver.playerId);
Expand Down
69 changes: 69 additions & 0 deletions Basis/Packages/com.basis.sdk/Localization/Languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,75 @@
{ "key": "sdk.parameterDriver.field.destMin", "value": "Dest Min" },
{ "key": "sdk.parameterDriver.field.destMax", "value": "Dest Max" },

{ "key": "sdk.authoredMotion.movements.header", "value": "Movements ({0})" },
{ "key": "sdk.authoredMotion.movements.empty", "value": "No movements — click '+ Add Movement' below." },
{ "key": "sdk.authoredMotion.movements.add", "value": "+ Add Movement" },
{ "key": "sdk.authoredMotion.label.placeholder", "value": "<movement>" },
{ "key": "sdk.authoredMotion.field.kind.label", "value": "Kind" },
{ "key": "sdk.authoredMotion.field.kind.tooltip", "value": "Which authored-motion primitive this entry drives. The fields below change to match the kind." },
{ "key": "sdk.authoredMotion.field.label.label", "value": "Label" },
{ "key": "sdk.authoredMotion.field.label.tooltip", "value": "Author-facing identifier only; has no runtime effect." },
{ "key": "sdk.authoredMotion.field.enabled.label", "value": "Enabled" },
{ "key": "sdk.authoredMotion.field.enabled.tooltip", "value": "Author default for this movement. The runtime on/off toggle rides the component's own enabled state." },
{ "key": "sdk.authoredMotion.field.axis.label", "value": "Axis" },
{ "key": "sdk.authoredMotion.field.axis.tooltip", "value": "Local axis the movement acts about." },
{ "key": "sdk.authoredMotion.field.channel.label", "value": "Channel" },
{ "key": "sdk.authoredMotion.field.channel.tooltip", "value": "What the value drives — Rotation (degrees), Position (metres) or Scale (factor)." },
{ "key": "sdk.authoredMotion.field.waveform.label", "value": "Waveform" },
{ "key": "sdk.authoredMotion.field.waveform.tooltip", "value": "Oscillation shape: Sine, Triangle, Square or Pulse." },
{ "key": "sdk.authoredMotion.field.pulseWidth.label", "value": "Pulse Width" },
{ "key": "sdk.authoredMotion.field.pulseWidth.tooltip", "value": "Duty cycle (0–1) for the Square and Pulse waveforms." },
{ "key": "sdk.authoredMotion.field.amplitude.label", "value": "Amplitude" },
{ "key": "sdk.authoredMotion.field.amplitude.tooltip", "value": "Peak deviation from rest. Units follow Channel: degrees, metres or scale-factor." },
{ "key": "sdk.authoredMotion.field.frequencyHz.label", "value": "Frequency (Hz)" },
{ "key": "sdk.authoredMotion.field.frequencyHz.tooltip", "value": "Cycles per second." },
{ "key": "sdk.authoredMotion.field.phase.label", "value": "Phase" },
{ "key": "sdk.authoredMotion.field.phase.tooltip", "value": "Starting phase offset, in radians." },
{ "key": "sdk.authoredMotion.field.chain.label", "value": "Chain" },
{ "key": "sdk.authoredMotion.field.chain.tooltip", "value": "Transforms this movement drives. One entry is a simple sway on a single bone; multiple entries form a travelling wave down the chain. Oscillate and Noise are driven by this list — not by Target." },
{ "key": "sdk.authoredMotion.field.chainPhaseStep.label", "value": "Chain Phase Step" },
{ "key": "sdk.authoredMotion.field.chainPhaseStep.tooltip", "value": "Phase delay (radians) added per element down the chain — produces the travelling-wave look." },
{ "key": "sdk.authoredMotion.field.chainFalloff.label", "value": "Chain Falloff" },
{ "key": "sdk.authoredMotion.field.chainFalloff.tooltip", "value": "Amplitude multiplier applied per element down the chain (1 = no falloff)." },
{ "key": "sdk.authoredMotion.field.target.label", "value": "Target" },
{ "key": "sdk.authoredMotion.field.target.tooltip", "value": "Transform to drive. Used by Rotate and Orbit; Oscillate and Noise use Chain instead." },
{ "key": "sdk.authoredMotion.field.speedDeg.label", "value": "Speed (deg/sec)" },
{ "key": "sdk.authoredMotion.field.speedDeg.tooltip", "value": "Constant angular velocity about Axis, in degrees per second." },
{ "key": "sdk.authoredMotion.field.pivot.label", "value": "Pivot" },
{ "key": "sdk.authoredMotion.field.pivot.tooltip", "value": "Point the Target revolves around. Defaults to the Target's own position when unset." },
{ "key": "sdk.authoredMotion.field.radius.label", "value": "Radius" },
{ "key": "sdk.authoredMotion.field.radius.tooltip", "value": "Distance from the pivot, in metres." },
{ "key": "sdk.authoredMotion.field.orbitSpeedDeg.label", "value": "Orbit Speed (deg/sec)" },
{ "key": "sdk.authoredMotion.field.orbitSpeedDeg.tooltip", "value": "Revolution speed around the pivot, in degrees per second." },
{ "key": "sdk.authoredMotion.field.selectTarget.label", "value": "Select Target" },
{ "key": "sdk.authoredMotion.field.selectTarget.tooltip", "value": "Default target for options that don't set their own. Lets one component pose a single bone several ways, while options that name their own target can each drive a different bone." },
{ "key": "sdk.authoredMotion.field.options.label", "value": "Options" },
{ "key": "sdk.authoredMotion.field.options.tooltip", "value": "Weighted poses to pick between. Each option may target its own bone (falling back to Select Target) and is chosen in proportion to its weight." },
{ "key": "sdk.authoredMotion.field.idleWeight.label", "value": "Idle Weight" },
{ "key": "sdk.authoredMotion.field.idleWeight.tooltip", "value": "Relative weight of the 'pose nothing' outcome. Larger values make a rest cycle (all targets returning to rest) more likely than any single option." },
{ "key": "sdk.authoredMotion.field.intervalRange.label", "value": "Interval Range" },
{ "key": "sdk.authoredMotion.field.intervalRange.tooltip", "value": "Time between picks. The X value sets the fixed period in seconds; Y is reserved." },
{ "key": "sdk.authoredMotion.field.attack.label", "value": "Attack" },
{ "key": "sdk.authoredMotion.field.attack.tooltip", "value": "Ease-in time toward a newly chosen pose, in seconds." },
{ "key": "sdk.authoredMotion.field.release.label", "value": "Release" },
{ "key": "sdk.authoredMotion.field.release.tooltip", "value": "Ease-out time when a target returns to rest, in seconds." },
{ "key": "sdk.authoredMotion.field.preventRepeats.label", "value": "Prevent Repeats" },
{ "key": "sdk.authoredMotion.field.preventRepeats.tooltip", "value": "Avoid picking the same option twice in a row. Not yet honored by the deterministic picker." },
{ "key": "sdk.authoredMotion.field.seed.label", "value": "Seed" },
{ "key": "sdk.authoredMotion.field.seed.tooltip", "value": "Random seed for Noise and RandomSelect. 0 derives one from the registration index." },
{ "key": "sdk.authoredMotion.field.noiseSpeed.label", "value": "Noise Speed" },
{ "key": "sdk.authoredMotion.field.noiseSpeed.tooltip", "value": "How fast the simplex-noise field is sampled." },
{ "key": "sdk.authoredMotion.field.sequenceTarget.label", "value": "Sequence Target" },
{ "key": "sdk.authoredMotion.field.sequenceTarget.tooltip", "value": "Transform the timeline drives." },
{ "key": "sdk.authoredMotion.field.sequenceRoot.label", "value": "Sequence Root" },
{ "key": "sdk.authoredMotion.field.sequenceRoot.tooltip", "value": "The baked clip's transform paths resolve under this root (e.g. the avatar root the clip was authored against). Defaults to this component's transform when unset." },
{ "key": "sdk.authoredMotion.field.bakedClip.label", "value": "Baked Clip" },
{ "key": "sdk.authoredMotion.field.bakedClip.tooltip", "value": "Shared, read-only baked-curve asset produced by the clip baker. Drives every bone the source AnimationClip animated." },
{ "key": "sdk.authoredMotion.field.keyframes.label", "value": "Keyframes" },
{ "key": "sdk.authoredMotion.field.keyframes.tooltip", "value": "Inline pose-delta timeline for short motion. Ignored when a Baked Clip is assigned." },
{ "key": "sdk.authoredMotion.field.loop.label", "value": "Loop" },
{ "key": "sdk.authoredMotion.field.loop.tooltip", "value": "Loop the sequence, or play it once." },

{ "key": "sdk.buildReport.window.title", "value": "Basis Build Report Viewer" },
{ "key": "sdk.buildReport.window.tabTitle", "value": "Basis Bundle Report" },
{ "key": "sdk.buildReport.noDirectory", "value": "No build reports directory found." },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public static string GetTagColor(LogTag logTag)
LogTag.Shims => "#FF00FF", // Magenta
LogTag.Props => "#FFB6C1", // Light Pink
LogTag.LocalNetwork => "#ff0055",
LogTag.AuthoredMotion => "#BA55D3", // Medium Orchid
_ => "#FFFFFF" // Default White
};
}
Expand Down Expand Up @@ -141,6 +142,7 @@ public enum LogTag
Shims,
Props,
LocalNetwork,
AuthoredMotion,
}

public enum MessageType
Expand Down
111 changes: 111 additions & 0 deletions Basis/Packages/com.basis.sdk/Scripts/BasisAuthoredMotion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System;
using UnityEngine;

/// <summary>
/// Data-only avatar component declaring authored, deterministic dynamic motion on transforms
/// the humanoid rig and IK don't drive (tail/ear chains, accessories, etc.). It holds pure
/// serialized configuration and runs no per-instance per-frame <c>Update</c> — all runtime
/// evaluation happens in the batched <c>BasisAuthoredMotionSystem</c> job, which reads this
/// component at calibration. The config model mirrors <see cref="BasisParameterDriver"/>'s
/// <c>Operation[]</c> shape (an enum kind + per-kind fields).
///
/// Allow it onto an avatar by adding its type to the Content Police
/// (<c>ContentPoliceSelector.selectedTypes</c> in <c>AvatarContentPoliceSelector.asset</c>).
/// Group movements that toggle together into one component; an avatar may carry several.
/// </summary>
public class BasisAuthoredMotion : MonoBehaviour
{
/// <summary>
/// Raised on enable/disable so a registered motion system can flip this component's slice
/// of its valid-mask without a per-frame poll. The system subscribes at registration; the
/// component holds no reference to any toggle package, so any actuator that flips
/// <see cref="Behaviour.enabled"/> (e.g. an HVR.Vixxy activation) drives it unchanged.
/// </summary>
public event Action<BasisAuthoredMotion, bool> EnabledStateChanged;

public Movement[] movements = Array.Empty<Movement>();

private void OnEnable() => EnabledStateChanged?.Invoke(this, true);
private void OnDisable() => EnabledStateChanged?.Invoke(this, false);

[Serializable]
public class Movement
{
// Open, extensible set — new kinds slot in without disturbing registration / scheduling / toggles.
public enum Kind { Oscillate, Rotate, Orbit, RandomSelect, Sequence, Noise }
public enum Channel { Rotation, Position, Scale } // what Oscillate / Noise drive
public enum Waveform { Sine, Triangle, Square, Pulse }

public Kind kind = Kind.Oscillate;
public string label; // author-facing identifier only
public bool enabled = true; // author default; runtime toggle rides the component's own enabled
public Vector3 axis = Vector3.up; // local axis the movement acts about

// Oscillate — periodic motion on `channel`; a chain makes a travelling wave (1 entry = simple sway).
public Channel channel = Channel.Rotation; // amplitude unit: deg | metres | scale-factor
public Waveform waveform = Waveform.Sine;
public float pulseWidth = 0.5f; // square/pulse duty cycle (0–1)
public Transform[] chain;
public float amplitude = 15f;
public float frequencyHz = 0.5f;
public float phase = 0f;
public float chainPhaseStep = 0f; // phase delay per element down the chain
public float chainFalloff = 1f; // amplitude scale per element down the chain

// Rotate — constant angular velocity about `axis`, in place.
public Transform target;
public float speedDeg = 36f; // deg/sec

// Orbit — revolve `target` around `pivot` at `radius` (not a spin-in-place).
public Transform pivot;
public float radius = 0.1f;
public float orbitSpeedDeg = 90f; // deg/sec around the pivot

// RandomSelect — every `intervalRange.x` seconds, deterministically pick one weighted option (or idle)
// and ease the target in/out. Each Option may set its own `target`, else falls back to `selectTarget`.
public Transform selectTarget; // default target for options that leave their own target null
public Option[] options = Array.Empty<Option>();
public float idleWeight = 0f; // relative weight of the "pose nothing" outcome
public Vector2 intervalRange = new Vector2(2f, 6f); // x = fixed period (seconds between picks)
public float attack = 0.06f, release = 0.25f; // ease in / out seconds
public bool preventRepeats = true;
public uint seed = 0; // 0 = derive from registration index

// Sequence — authored timeline, loop or one-shot. A baked clip drives many bones via paths under
// `sequenceRoot`; inline keyframes (deferred) use `sequenceTarget`.
public Transform sequenceTarget; // single-bone inline-keyframe target (inline path; deferred)
public Transform sequenceRoot; // baked-clip paths resolve under this (defaults to the avatar root)
public Keyframe[] keyframes = Array.Empty<Keyframe>();
public BasisMotionClip bakedClip; // shared baked curves; null when using inline keyframes
public bool loop = true;

// Noise — simplex drift on `channel` about `axis`; reuses amplitude / chain / chainFalloff / seed; `noiseSpeed` = sample rate.
public float noiseSpeed = 0.5f;
}

[Serializable]
public class Option
{
[Tooltip("Transform this option poses. Falls back to the movement's Select Target when null.")]
public Transform target;
[Tooltip("Local axis to rotate about.")]
public Vector3 axis = Vector3.up;
[Tooltip("Rotation applied about Axis when this option is selected, in degrees.")]
public float angleDeg;
[Tooltip("Relative likelihood this option is picked.")]
public float weight = 1f;
}

[Serializable]
public class Keyframe
{
[Tooltip("Time of this key, in seconds from the start of the sequence.")]
public float time;
[Tooltip("Rotation delta from rest at this key, in euler degrees.")]
public Vector3 eulerDelta;
[Tooltip("Local position delta from rest at this key.")]
public Vector3 positionDelta;
[Tooltip("Local scale delta from rest at this key.")]
public Vector3 scaleDelta;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Basis/Packages/com.basis.sdk/Scripts/BasisMotionClip.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using UnityEngine;

/// <summary>
/// Shared, read-only baked motion for a <see cref="BasisAuthoredMotion.Movement"/> of kind
/// <c>Sequence</c>: an AnimationClip's curves sampled to a fixed-rate, blittable buffer so the
/// batched Burst job can interpolate without touching a managed <c>AnimationCurve</c>. One asset
/// is referenced by every instance of an avatar; per-instance runtime state is just a playhead.
/// Rotations bake as absolute local rotations and the job writes them straight to the bone, so a
/// converted clip reproduces its authored pose exactly regardless of the avatar's rest pose.
/// </summary>
[CreateAssetMenu(fileName = "BasisMotionClip", menuName = "Basis/Authored Motion Clip")]
public class BasisMotionClip : ScriptableObject
{
[Tooltip("Samples per second the curves were baked at.")]
public float frameRate = 30f;

[Tooltip("Number of frames per driven transform.")]
public int frameCount;

[Tooltip("Number of driven transforms this clip covers.")]
public int transformCount;

[Tooltip("Transform path each row drives, relative to the sequence root. One per transform.")]
public string[] paths;

// Flattened samples, laid out [transform0_frame0..frameN-1, transform1_...].
// Index a (transform, frame) as transformIndex * frameCount + frame.
public Vector4[] rotationSamples; // absolute local rotation, xyzw
public Vector3[] positionSamples; // reserved (unused in v1)
public Vector3[] scaleSamples; // reserved (unused in v1)

/// <summary>Clip length in seconds (<c>frameCount / frameRate</c>).</summary>
public float Length => frameRate > 0f ? frameCount / frameRate : 0f;
}
2 changes: 2 additions & 0 deletions Basis/Packages/com.basis.sdk/Scripts/BasisMotionClip.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading