@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
192 lines (152 loc) • 8.59 kB
text/typescript
import { AnimationClip,Object3D } from "three";
import { getParam } from "../../../../engine/engine_utils.js";
import { Animation } from "../../../Animation.js";
import { Animator } from "../../../Animator.js";
import { AudioSource } from "../../../AudioSource.js";
import { GameObject } from "../../../Component.js";
import type { AnimationExtension } from "../extensions/Animation.js";
import type { AudioExtension } from "../extensions/behavior/AudioExtension.js";
import { PlayAnimationOnClick, PlayAudioOnClick } from "../extensions/behavior/BehaviourComponents.js";
import { ActionBuilder,BehaviorModel, TriggerBuilder } from "../extensions/behavior/BehavioursBuilder.js";
const debug = getParam("debugusdz");
export function registerAnimatorsImplictly(root: Object3D, ext: AnimationExtension): Array<Object3D> {
// TODO: currently we're simply appending all animations to each other.
// We are not currently emitting PlayAnimation actions that would make sure
// that only a specific clip or a set of clips is playing.
// Better would be to scratch the functionality here entirely, and instead
// inject PlayAnimationOnStart actions. This way, animations can loop independently, and
// Animator state chaining would be respected out-of-the-box.
// For non-QuickLook USDZ export it's still useful to have implicit registration per root.
// collect animators and their clips
const animationClips: { root: Object3D, clips: AnimationClip[] }[] = [];
const animators = GameObject.getComponentsInChildren(root, Animator);
const animationComponents = GameObject.getComponentsInChildren(root, Animation);
const animatorsWithPlayAtStart = new Array<Animator | Animation>();
const constructedObjects = new Array<Object3D>();
if (ext.injectImplicitBehaviours) {
// We're registering animators with implicit PlayAnimationOnClick (with hacked "start" trigger) here.
for (const animator of animators) {
if (!animator || !animator.runtimeAnimatorController || !animator.enabled) continue;
const activeState = animator.runtimeAnimatorController.activeState;
// skip missing data, empty states or motions
if (!activeState) continue;
if (!activeState.motion || !activeState.motion.clip) continue;
if (activeState.motion.clip.tracks?.length < 1) continue;
// We could skip if the current state has already ended playing
// but there's an edge case where we'd do this exactly in a transition,
// and also it's a bit weird when someone has watched the animation in the browser already
// and then it doesn't play in AR, so this is disabled for now.
// if (animator.getCurrentStateInfo()?.normalizedTime == 1) continue;
if (animatorsWithPlayAtStart.includes(animator)) continue;
// Create a PlayAnimationOnClick component that will play the animation on start
// This is a bit hacky right now (we're using the internal "start" trigger)
const newComponent = new PlayAnimationOnClick();
newComponent.animator = animator;
newComponent.stateName = activeState.name;
newComponent.trigger = "start";
newComponent.name = "PlayAnimationOnClick_implicitAtStart_" + newComponent.stateName;
const go = new Object3D();
GameObject.addComponent(go, newComponent);
constructedObjects.push(go);
animatorsWithPlayAtStart.push(animator);
// the behaviour can be anywhere in the hierarchy
root.add(go);
}
// TODO add Animation handling, otherwise multi-animation files
// directly loaded from GLB may glitch due to the added rest poses
}
else {
for (const animator of animators) {
if (!animator || !animator.runtimeAnimatorController || !animator.enabled) continue;
if (debug) console.log(animator);
const clips: AnimationClip[] = [];
for (const action of animator.runtimeAnimatorController.enumerateActions()) {
if (debug)
console.log(action);
const clip = action.getClip();
if (!clips.includes(clip))
clips.push(clip);
}
animationClips.push({ root: animator.gameObject, clips: clips });
}
}
// TODO once PlayAnimationOnClick can use animation components as well,
// we can treat them the same as we treat Animators above.
if (ext.injectImplicitBehaviours) {
for (const animationComponent of animationComponents) {
if (!animationComponent || !animationComponent.clip ||!animationComponent.enabled) continue;
if (!animationComponent.playAutomatically) continue;
if (animatorsWithPlayAtStart.includes(animationComponent)) continue;
// Create a PlayAnimationOnClick component that will play the animation on start
// This is a bit hacky right now (we're using the internal "start" trigger)
const newComponent = new PlayAnimationOnClick();
newComponent.animation = animationComponent;
newComponent.stateName = animationComponent.clip.name;
newComponent.trigger = "start";
newComponent.name = "PlayAnimationOnClick_implicitAtStart_" + newComponent.stateName;
const go = new Object3D();
GameObject.addComponent(go, newComponent);
constructedObjects.push(go);
animatorsWithPlayAtStart.push(animationComponent);
// the behaviour can be anywhere in the hierarchy
root.add(go);
}
}
else {
for (const animationComponent of animationComponents) {
if (debug)
console.log(animationComponent);
const clips: AnimationClip[] = [];
for (const clip of animationComponent.animations) {
if (!clips.includes(clip))
clips.push(clip);
}
animationClips.push({ root: animationComponent.gameObject, clips: clips });
}
}
if (debug && animationClips?.length > 0) console.log("USDZ Animation Clips without behaviours", animationClips);
for (const pair of animationClips) {
for (const clip of pair.clips)
ext.registerAnimation(pair.root, clip);
}
return constructedObjects;
}
export function registerAudioSourcesImplictly(root: Object3D, _ext: AudioExtension): Array<Object3D> {
const audioSources = GameObject.getComponentsInChildren(root, AudioSource);
const playAudioOnClicks = GameObject.getComponentsInChildren(root, PlayAudioOnClick);
const audioWithPlayAtStart = new Array<AudioSource>();
const constructedObjects = new Array<Object3D>();
if (debug) {
console.log({ audioSources, playAudioOnClicks });
}
// Remove all audio sources that are already referenced from existing PlayAudioOnClick components
for (const player of playAudioOnClicks) {
if (!player.target) continue;
const index = audioSources.indexOf(player.target);
if (index > -1) audioSources.splice(index, 1);
}
// for the remaning ones, we want to build a PlayAudioOnClick component
for (const audioSource of audioSources) {
if (!audioSource || !audioSource.clip) continue;
if (audioSource.volume <= 0) continue;
if (audioWithPlayAtStart.includes(audioSource)) continue;
const newComponent = new PlayAudioOnClick();
newComponent.target = audioSource;
newComponent.name = "PlayAudioOnClick_implicitAtStart_";
newComponent.trigger = "start";
const go = new Object3D();
GameObject.addComponent(go, newComponent);
console.log("implicit PlayAudioOnStart", go, newComponent);
constructedObjects.push(go);
audioWithPlayAtStart.push(audioSource);
root.add(go);
}
return constructedObjects;
}
export function disableObjectsAtStart(objects: Array<Object3D>) {
const newComponent = new BehaviorModel("DisableAtStart",
TriggerBuilder.sceneStartTrigger(),
ActionBuilder.fadeAction(objects, 0, false),
)
return newComponent;
}