UNPKG

@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
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; }