mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-15 06:12:18 +00:00
Merge pull request #2 from storytold/tanish/refactor
Implement Character Animation Engine
This commit is contained in:
51 files changed
+5826
-9490
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import environmentVariables from "./EnvironmentVariables";
|
||||
|
||||
export async function get_media_url(mediaId: string) {
|
||||
//This is for prod when we have the proper info on the url.
|
||||
const api_base_url = environmentVariables.values.BASE_API;
|
||||
const url = `${api_base_url}/v1/media_files/file/${mediaId}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const json = await JSON.parse(await response.text());
|
||||
const bucketPath = json["media_file"]["public_bucket_path"];
|
||||
|
||||
//const media_api_base_url = environmentVariables.values.GOOGLE_API;
|
||||
const media_api_base_url = "https://cdn-2.fakeyou.com";
|
||||
//const media_base_url = `${media_api_base_url}/vocodes-public`;
|
||||
//const media_url = `${media_base_url}${bucketPath}`;
|
||||
const media_url = `${media_api_base_url}${bucketPath}`;
|
||||
return media_url;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default interface Ijson {
|
||||
toJSON(): unknown;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class DndAsset {
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
FIXME:
|
||||
THIS IS A TEMPARARY SOLUTION TO A LONG PROBLEM WITH SKYBOXES
|
||||
UPDATE THIS WHEN UPLOADING SKYBOXES ARE FULLY IMPLEMENTED.
|
||||
THIS IS JUST TEMPARARY!!!
|
||||
|
||||
+11
-9
@@ -1,23 +1,24 @@
|
||||
import { deepCopySerializableObjects as deepCopy } from "~/utilities";
|
||||
import { AnimationEngine } from "../animation_engine";
|
||||
import AudioEngine from "../audio_engine";
|
||||
import EmotionEngine from "../emotion_engine";
|
||||
import { LipSyncEngine } from "../lip_sync_engine";
|
||||
import { ClipUI } from "../../clips/clip_ui";
|
||||
import AudioEngine from "../Engines/audio_engine";
|
||||
import EmotionEngine from "../Engines/emotion_engine";
|
||||
import LipSyncEngine from "../Engines/lip_sync_engine";
|
||||
import { TimeLine } from "../timeline";
|
||||
import TransformEngine from "../transform_engine";
|
||||
import TransformEngine from "../Engines/transform_engine";
|
||||
import { TimelineStateJson } from "./EditorStateJSON/timeline_state_json";
|
||||
import { ClipUI } from "../../datastructures/clips/clip_ui";
|
||||
import { CharacterAnimationEngine } from "../Engines/CharacterAnimationEngine";
|
||||
|
||||
|
||||
export class TimelineState {
|
||||
version: number;
|
||||
timeline_items: ClipUI[] | undefined;
|
||||
transform_engine: TransformEngine | undefined;
|
||||
animation_engine: AnimationEngine | undefined;
|
||||
animation_engine: CharacterAnimationEngine | undefined;
|
||||
audio_engine: AudioEngine | undefined;
|
||||
lipsync_engine: LipSyncEngine | undefined;
|
||||
emotion_engine: EmotionEngine | undefined;
|
||||
|
||||
constructor({ editorVersion }: { editorVersion: number }) {
|
||||
constructor({ editorVersion }: { editorVersion: number; }) {
|
||||
this.version = editorVersion;
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ export class TimelineState {
|
||||
});
|
||||
}
|
||||
public async initializeFromTimelineStateJson(
|
||||
jsonObject: TimelineStateJson,
|
||||
jsonObject: TimelineStateJson
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
@@ -62,3 +63,4 @@ export class TimelineState {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EditorStateJson } from "./EditorStateJSON";
|
||||
// import Scene from "../scene";
|
||||
// import { TimeLine } from "../timeline";
|
||||
import { SceneState } from "./scene_state";
|
||||
import { TimelineState } from "./timeline_state";
|
||||
import { TimelineState } from "./TimelineState";
|
||||
|
||||
export class EditorState {
|
||||
version: number;
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import * as THREE from "three";
|
||||
import { Retarget } from "../retargeting";
|
||||
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader.js";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { get_media_url } from "~/Classes/ApiHelpers";
|
||||
import { MMDLoader } from "three/examples/jsm/loaders/MMDLoader.js";
|
||||
import { ClipUI } from "../../clips/clip_ui";
|
||||
|
||||
export class CharacterAnimationEngine {
|
||||
version: number;
|
||||
characterAnimations: Map<THREE.Object3D<THREE.Object3DEventMap>, ClipUI[]> = new Map();
|
||||
characterMixers = new Map<THREE.Object3D<THREE.Object3DEventMap>, THREE.AnimationMixer>();
|
||||
|
||||
constructor(version: number) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single character model is capable of tracking multiple animations, in its own track
|
||||
* These tracks can be blended with weights or additively
|
||||
* The weight change can be done via custom interpolation logic
|
||||
*
|
||||
* @param objectUUID The UUID of the object.
|
||||
* @param clip The 3JS animation clip
|
||||
* @param clipUI The clip info
|
||||
*/
|
||||
addCharacterAnimation(character: THREE.Object3D, clip: THREE.AnimationClip, clipUI: ClipUI) {
|
||||
// If character doesn't exist, create a clip object
|
||||
// If it does exist, add a new animation to the existing character by extracting it
|
||||
// TODO: FIXME: Duplicate prevention?
|
||||
clip.name = clipUI.media_id;
|
||||
character.animations.push(clip);
|
||||
console.log("Adding animation track:")
|
||||
console.log(clip);
|
||||
|
||||
if (!this.characterAnimations.has(character)) {
|
||||
this.characterAnimations.set(character, []);
|
||||
this.#ensureMixerExists(character);
|
||||
}
|
||||
|
||||
// Add the clip info to the character map and sort the clips
|
||||
const characterClips = this.characterAnimations.get(character)!;
|
||||
characterClips.push(clipUI);
|
||||
characterClips.sort((a, b) => a.offset - b.offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param objectUUID The UUID of the object.
|
||||
* @param mediaId The media ID of the animation.
|
||||
* @param clipUI The clip info
|
||||
*/
|
||||
async addCharacterAnimationMedia(character: THREE.Object3D, mediaId: string, clipUI: ClipUI) {
|
||||
// If character doesn't exist, create a clip object
|
||||
// If it does exist, add a new animation to the existing character by extracting it
|
||||
// TODO: FIXME: Duplicate prevention?
|
||||
const animationTrack = await this.#load_animation(character, mediaId);
|
||||
this.addCharacterAnimation(character, animationTrack, clipUI);
|
||||
}
|
||||
|
||||
#ensureMixerExists(character: THREE.Object3D) {
|
||||
if (!this.characterMixers.has(character)) {
|
||||
this.characterMixers.set(character, new THREE.AnimationMixer(character));
|
||||
}
|
||||
}
|
||||
|
||||
getMixer(character: THREE.Object3D) {
|
||||
this.#ensureMixerExists(character);
|
||||
return this.characterMixers.get(character)!;
|
||||
}
|
||||
|
||||
#interpolateClips(character: THREE.Object3D, timestamp: number, maxTime: number) {
|
||||
const clips = this.characterAnimations.get(character)!;
|
||||
|
||||
// The timestamp isn't inside any clip, we're in interpolation land
|
||||
// If there's no clips to interpolate between, we're done
|
||||
if (clips.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mixer = this.getMixer(character);
|
||||
|
||||
// Find the two clips we're interpolating between
|
||||
// TODO: Binary search? We won't need it because the amount of clips is too low
|
||||
let prevClip: ClipUI | null = null;
|
||||
let nextClip: ClipUI | null = null;
|
||||
const lastClip = clips[clips.length - 1];
|
||||
|
||||
// If we're before the first clip, interpolate it in from base pose
|
||||
// TODO: Replace with starting frame
|
||||
if (clips[0].offset > timestamp) {
|
||||
prevClip = null;
|
||||
nextClip = clips[0];
|
||||
} else if (lastClip.offset + lastClip.length < timestamp) { // After last clip, interpolate it out
|
||||
prevClip = lastClip;
|
||||
nextClip = null;
|
||||
} else {
|
||||
let prevClipIndex = 0;
|
||||
|
||||
// Loop until the next clip is after the timestamp
|
||||
while (prevClipIndex < clips.length - 1 && clips[prevClipIndex + 1].offset < timestamp) {
|
||||
prevClipIndex++;
|
||||
}
|
||||
|
||||
prevClip = clips[prevClipIndex];
|
||||
nextClip = clips[prevClipIndex + 1];
|
||||
}
|
||||
|
||||
const prevAction = prevClip ? mixer.clipAction(this.getCharacterAnimationTrack(character, prevClip!.media_id)!) : null;
|
||||
const nextAction = nextClip ? mixer.clipAction(this.getCharacterAnimationTrack(character, nextClip!.media_id)!) : null;
|
||||
|
||||
// Calculate the progress of timestamp from end of prev action to start of next action
|
||||
const left = (prevClip?.offset ?? 0) + (prevClip?.length ?? 0);
|
||||
const right = nextClip?.offset ?? maxTime;
|
||||
|
||||
// Simple Linear interpolation for now
|
||||
// TODO: Take an interpolation dependency, or better yet, write a transition engine
|
||||
const progress = (timestamp - left) / (right - left);
|
||||
prevAction?.setEffectiveWeight(1 - progress);
|
||||
nextAction?.setEffectiveWeight(progress);
|
||||
|
||||
// Make sure we hold that last frame for the previous action
|
||||
if (prevAction) {
|
||||
prevAction.clampWhenFinished = true;
|
||||
}
|
||||
|
||||
// The next action should stay at the first frame
|
||||
if (nextAction) {
|
||||
nextAction.paused = true;
|
||||
}
|
||||
|
||||
// Necessary to ensure the actions are active - the default is inactive, mixer won't do anything
|
||||
prevAction?.play();
|
||||
nextAction?.play();
|
||||
|
||||
// The clip time would still be relative to the previous clip
|
||||
const clipTime = timestamp - (prevClip?.offset ?? 0);
|
||||
mixer.setTime(clipTime / 1000);
|
||||
console.log("Prev action status")
|
||||
console.log(prevAction);
|
||||
}
|
||||
|
||||
evaluateCharacter(character: THREE.Object3D, timestamp: number, maxTime: number) {
|
||||
const mixer = this.getMixer(character);
|
||||
const clips = this.characterAnimations.get(character)!;
|
||||
|
||||
// Find the clip we're in right now
|
||||
const currentClip = clips.find((clip) => {
|
||||
return clip.offset <= timestamp && clip.offset + clip.length >= timestamp;
|
||||
})
|
||||
|
||||
// If timestamp not in any clip, do nothing.
|
||||
// If timestamp in clip, set mixer to the timestamp inside the clip
|
||||
if (!currentClip) {
|
||||
console.log("INTERPOLATING CLIPS")
|
||||
// Let the interpolation function handle this actions
|
||||
this.#interpolateClips(character, timestamp, maxTime)
|
||||
return;
|
||||
}
|
||||
|
||||
const clipTime = timestamp - currentClip.offset;
|
||||
const animationTrack = this.getCharacterAnimationTrack(character, currentClip.media_id);
|
||||
|
||||
// NOTE: This shouldn't really happen unless the UI was desynced at some point from the engine
|
||||
if (!animationTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Play only this action
|
||||
const animationAction = mixer.clipAction(animationTrack);
|
||||
|
||||
// Since it's the only clip in this timestamp, make it full weight and make sure it's not paused (from interpolation or otherwise)
|
||||
animationAction.setEffectiveWeight(1);
|
||||
animationAction.paused = false;
|
||||
|
||||
// Necessary to ensure the actions are active - the default is inactive, mixer won't do anything
|
||||
animationAction.play();
|
||||
|
||||
mixer.setTime(clipTime / 1000);
|
||||
console.log("Action status")
|
||||
console.log(animationAction);
|
||||
}
|
||||
|
||||
getCharacterAnimationTrack(character: THREE.Object3D, mediaId: string) {
|
||||
return character.animations.find((clip) => clip.name === mediaId);
|
||||
}
|
||||
|
||||
/** Evaluate all character animations at a given timestamp (milliseconds) */
|
||||
evaluate(timestamp: number, maxTime: number) {
|
||||
this.characterMixers.forEach((_, character) => {
|
||||
this.evaluateCharacter(character, timestamp, maxTime);
|
||||
})
|
||||
}
|
||||
|
||||
stopCharacter(character: THREE.Object3D) {
|
||||
const mixer = this.characterMixers.get(character);
|
||||
mixer?.stopAllAction();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.characterMixers.forEach((mixer) => {
|
||||
mixer.stopAllAction();
|
||||
});
|
||||
}
|
||||
|
||||
async #load_animation(character: THREE.Object3D, mediaId: string): Promise<THREE.AnimationClip> {
|
||||
// Get the file URL and extract the (first) animation track from it
|
||||
// TODO: Support for multiple animations in a single file?
|
||||
const url = await get_media_url(mediaId);
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
if (url.includes(".glb")) {
|
||||
const glbLoader = new GLTFLoader();
|
||||
|
||||
glbLoader.load(url, (gltf) => {
|
||||
const animationClip = gltf.animations[0];
|
||||
resolve(animationClip);
|
||||
});
|
||||
} else if (url.includes(".fbx")) {
|
||||
const fbxLoader = new FBXLoader();
|
||||
fbxLoader.load(url, (fbx) => {
|
||||
const animationClip_1 = fbx.animations[0];
|
||||
animationClip_1.tracks.forEach((track) => {
|
||||
const retarget = new Retarget();
|
||||
const retarget_value = retarget.retarget(track.name);
|
||||
track.name = retarget_value.bone;
|
||||
console.log(track);
|
||||
if (retarget_value.is_special) {
|
||||
// TODO: Revisit special properties later
|
||||
// this.special_properties.push(retarget_value);
|
||||
}
|
||||
});
|
||||
resolve(animationClip_1);
|
||||
});
|
||||
} else if (url.includes(".vmd")) {
|
||||
const mmdLoader = new MMDLoader();
|
||||
mmdLoader.loadAnimation(url, character as THREE.SkinnedMesh, (mmd) => {
|
||||
mmd.name = url;
|
||||
resolve(mmd as THREE.AnimationClip);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeCharacter(character: THREE.Object3D) {
|
||||
console.debug("Removing character from animation engine", character);
|
||||
this.characterAnimations.delete(character);
|
||||
this.characterMixers.delete(character);
|
||||
}
|
||||
|
||||
removeAnimation(character: THREE.Object3D, clip: ClipUI) {
|
||||
console.debug("Removing animation from character", character, clip);
|
||||
const clips = this.characterAnimations.get(character)!;
|
||||
|
||||
const clipIndex = clips.findIndex((c) => c.clip_uuid === clip.clip_uuid);
|
||||
if (clipIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the clip from character's tracked clips
|
||||
clips.splice(clipIndex, 1);
|
||||
|
||||
// Stop all character animations to cleanly reset
|
||||
this.stopCharacter(character);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
import { AnimationClip } from "../datastructures/clips/animation_clip";
|
||||
import { AnimationClip } from "../../clips/animation_clip";
|
||||
|
||||
export class AnimationEngine {
|
||||
clips: { [key: string]: AnimationClip } = {};
|
||||
@@ -10,6 +10,7 @@ export class AnimationEngine {
|
||||
}
|
||||
|
||||
load_object(object_uuid: string, media_id: string, clip_name: string) {
|
||||
console.debug("load_object", object_uuid, media_id, clip_name);
|
||||
this.clips[object_uuid + media_id] = new AnimationClip(
|
||||
this.version,
|
||||
media_id,
|
||||
@@ -18,6 +19,8 @@ export class AnimationEngine {
|
||||
1.0,
|
||||
1.0,
|
||||
clip_name,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
//this.clips[object_uuid + media_id]._load_animation();
|
||||
}
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
import { AudioClip } from "../datastructures/clips/audio_clip";
|
||||
import { AudioClip } from "../../clips/audio_clip";
|
||||
|
||||
class AudioEngine {
|
||||
clips: { [key: string]: AudioClip } = {};
|
||||
@@ -37,7 +37,7 @@ class AudioEngine {
|
||||
async step(audio_media_id: string, frame: number, offset: number) {
|
||||
const frameBuffer = 12;
|
||||
|
||||
let pos = frame - offset;
|
||||
const pos = frame - offset;
|
||||
|
||||
let doPlay = true;
|
||||
if (
|
||||
@@ -50,12 +50,13 @@ class AudioEngine {
|
||||
doPlay = true;
|
||||
}
|
||||
|
||||
let clip = this.clips[audio_media_id];
|
||||
const clip = this.clips[audio_media_id];
|
||||
|
||||
if (doPlay && clip.audio_data?.audioContext) {
|
||||
const startTime = pos / 60.0;
|
||||
const endTime = frameBuffer / 60.0;
|
||||
clip.audio_data.source = clip.audio_data.audioContext.createBufferSource();
|
||||
clip.audio_data.source =
|
||||
clip.audio_data.audioContext.createBufferSource();
|
||||
clip.audio_data.source.buffer = clip.audio_data.audioBuffer;
|
||||
clip.audio_data.source.connect(clip.audio_data.audioContext.destination);
|
||||
clip.audio_data.source.start(0, startTime, endTime);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { EmotionClip } from "../datastructures/clips/emotion_clip";
|
||||
import { EmotionClip } from "../../clips/emotion_clip";
|
||||
|
||||
class EmotionEngine {
|
||||
clips: { [key: string]: EmotionClip } = {};
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { LipSyncClip } from "../datastructures/clips/lipsync_clip";
|
||||
import { LipSyncClip } from "../../clips/lipsync_clip";
|
||||
|
||||
// needs to be apart of the editor window.
|
||||
export class LipSyncEngine {
|
||||
export default class LipSyncEngine {
|
||||
clips: { [key: string]: LipSyncClip } = {};
|
||||
audio_sources: { [key: string]: AudioBufferSourceNode } = {};
|
||||
version: number;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { TransformClip } from "../datastructures/clips/transform_clip";
|
||||
import { TransformClip } from "../../clips/transform_clip";
|
||||
import * as THREE from "three";
|
||||
|
||||
class TransformEngine {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
// import { createFFmpeg, fetchFile, FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { ClipUI } from "../../datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "../../clips/clip_ui";
|
||||
|
||||
export interface VideoPreProcessorError {
|
||||
message: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ interface Task {
|
||||
id: number;
|
||||
}
|
||||
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
|
||||
class FrameProcessorWorker {
|
||||
|
||||
@@ -11,12 +11,11 @@ import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
|
||||
import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";
|
||||
import { SAOPass } from "three/addons/postprocessing/SAOPass.js";
|
||||
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
||||
import AudioEngine from "./audio_engine.js";
|
||||
import TransformEngine from "./transform_engine.js";
|
||||
import EmotionEngine from "./emotion_engine";
|
||||
import AudioEngine from "./Engines/audio_engine.js";
|
||||
import TransformEngine from "./Engines/transform_engine.js";
|
||||
import EmotionEngine from "./Engines/emotion_engine";
|
||||
import { TimeLine } from "./timeline.js";
|
||||
import { LipSyncEngine } from "./lip_sync_engine.js";
|
||||
import { AnimationEngine } from "./animation_engine.js";
|
||||
import LipSyncEngine from "./Engines/lip_sync_engine.js";
|
||||
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
|
||||
import { EditorStates, CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { AssetType, ClipGroup } from "~/enums";
|
||||
@@ -26,7 +25,7 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { editorState } from "../signals/engine";
|
||||
import { Utils } from "./helper";
|
||||
import { SceneUtils } from "./helper";
|
||||
import { VideoGeneration } from "./video_generation";
|
||||
import { MouseControls } from "./keybinds_controls";
|
||||
import { SaveManager } from "./save_manager";
|
||||
@@ -52,6 +51,7 @@ import {
|
||||
import { BufferType, EngineFrameBuffers } from "./VideoProcessor/engine_buffer";
|
||||
|
||||
import Stats from "three/examples/jsm/libs/stats.module.js";
|
||||
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
|
||||
|
||||
export type EditorInitializeConfig = {
|
||||
sceneToken: string;
|
||||
@@ -105,7 +105,7 @@ class Editor {
|
||||
transform_engine: TransformEngine;
|
||||
emotion_engine: EmotionEngine;
|
||||
lipsync_engine: LipSyncEngine;
|
||||
animation_engine: AnimationEngine;
|
||||
animation_engine: CharacterAnimationEngine;
|
||||
timeline: TimeLine;
|
||||
current_frame: number;
|
||||
lockControls: PointerLockControls | undefined;
|
||||
@@ -114,6 +114,7 @@ class Editor {
|
||||
renderPass: RenderPass | undefined;
|
||||
generating_preview: boolean;
|
||||
frames: number;
|
||||
lastFrameTime: number;
|
||||
|
||||
camera_person_mode: boolean;
|
||||
current_scene_media_token: string | null;
|
||||
@@ -147,7 +148,7 @@ class Editor {
|
||||
// global names of scene entities
|
||||
camera_name: string;
|
||||
|
||||
utils: Utils;
|
||||
utils: SceneUtils;
|
||||
videoGeneration: VideoGeneration;
|
||||
mouse_controls: MouseControls | undefined;
|
||||
save_manager: SaveManager;
|
||||
@@ -221,7 +222,7 @@ class Editor {
|
||||
"" + this.version,
|
||||
this.camera_name,
|
||||
this.updateSurfaceIdAttributeToMesh.bind(this),
|
||||
this.version
|
||||
this.version,
|
||||
);
|
||||
this.activeScene.initialize();
|
||||
this.generating_preview = false;
|
||||
@@ -247,6 +248,7 @@ class Editor {
|
||||
this.playback_location = 0;
|
||||
this.last_scrub = 0;
|
||||
this.frames = 0;
|
||||
this.lastFrameTime = 0;
|
||||
this.last_selected_sum = 0;
|
||||
this.selectedCanvas = false;
|
||||
// Audio Engine Test.
|
||||
@@ -259,7 +261,7 @@ class Editor {
|
||||
this.emotion_engine = new EmotionEngine(this.version);
|
||||
this.transform_engine = new TransformEngine(this.version);
|
||||
this.lipsync_engine = new LipSyncEngine();
|
||||
this.animation_engine = new AnimationEngine(this.version);
|
||||
this.animation_engine = new CharacterAnimationEngine(this.version);
|
||||
|
||||
this.timeline = new TimeLine(
|
||||
this,
|
||||
@@ -276,7 +278,7 @@ class Editor {
|
||||
|
||||
this.activeScene.timeline = this.timeline;
|
||||
|
||||
this.utils = new Utils(this, this.activeScene);
|
||||
this.utils = new SceneUtils(this, this.activeScene);
|
||||
this.videoGeneration = new VideoGeneration(this);
|
||||
this.save_manager = new SaveManager(this);
|
||||
this.current_frame = 0;
|
||||
@@ -645,7 +647,7 @@ class Editor {
|
||||
this.emotion_engine = new EmotionEngine(this.version);
|
||||
this.transform_engine = new TransformEngine(this.version);
|
||||
this.lipsync_engine = new LipSyncEngine();
|
||||
this.animation_engine = new AnimationEngine(this.version);
|
||||
this.animation_engine = new CharacterAnimationEngine(this.version);
|
||||
|
||||
this.timeline = new TimeLine(
|
||||
this,
|
||||
@@ -946,14 +948,14 @@ class Editor {
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
? 576
|
||||
: 1000;
|
||||
const height =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
? 1024
|
||||
: 1000;
|
||||
|
||||
@@ -1070,8 +1072,7 @@ class Editor {
|
||||
return decision;
|
||||
}
|
||||
|
||||
// Basicly Unity 3D's update loop.
|
||||
async updateLoop() {
|
||||
async renderSingleFrame() {
|
||||
//console.timeEnd("Single Frame Time");
|
||||
//console.time("Single Frame Time");
|
||||
this.containerMayReset();
|
||||
@@ -1144,11 +1145,11 @@ class Editor {
|
||||
}
|
||||
} else if (
|
||||
this.last_scrub === this.timeline.scrubber_frame_position &&
|
||||
this.utils.getselectedSum() !== this.last_selected_sum
|
||||
this.utils.getSelectedSum() !== this.last_selected_sum
|
||||
) {
|
||||
this.updateSelectedUI();
|
||||
}
|
||||
this.last_selected_sum = this.utils.getselectedSum();
|
||||
this.last_selected_sum = this.utils.getSelectedSum();
|
||||
|
||||
await this.renderScene();
|
||||
this.last_scrub = this.timeline.scrubber_frame_position;
|
||||
@@ -1163,6 +1164,20 @@ class Editor {
|
||||
); // Get the most FPS we can out of the renderer.
|
||||
}
|
||||
|
||||
// Basicly Unity 3D's update loop.
|
||||
async updateLoop() {
|
||||
// Performance improvement: Handle frame cap
|
||||
// Request the next render already - this is necessary so the loop doesn't stop if the fps cap is hit
|
||||
requestAnimationFrame(this.updateLoop.bind(this));
|
||||
const frameTime = performance.now();
|
||||
if (frameTime - this.lastFrameTime < 1000 / this.cap_fps) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastFrameTime = frameTime;
|
||||
this.renderSingleFrame();
|
||||
}
|
||||
|
||||
change_mode(type: "translate" | "rotate" | "scale") {
|
||||
if (this.control == undefined) {
|
||||
return;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { AssetType } from "~/enums";
|
||||
import { hideObjectPanel } from "../signals";
|
||||
|
||||
export class Utils {
|
||||
export class SceneUtils {
|
||||
scene: Scene;
|
||||
editor: Editor;
|
||||
constructor(editor: Editor, scene: Scene) {
|
||||
@@ -105,7 +105,7 @@ export class Utils {
|
||||
// TO UPDATE selected objects in the scene might want to add to the scene ...
|
||||
async setSelectedObject(position: XYZ, rotation: XYZ, scale: XYZ) {
|
||||
if (this.editor.sceneManager?.selected_objects) {
|
||||
let object = this.editor.sceneManager?.selected_objects[0];
|
||||
const object = this.editor.sceneManager?.selected_objects[0];
|
||||
if (object != undefined || object != null) {
|
||||
object.position.x = position.x;
|
||||
object.position.y = position.y;
|
||||
@@ -184,7 +184,7 @@ export class Utils {
|
||||
}
|
||||
|
||||
// Returns the "check sum" of the editors selected object.
|
||||
getselectedSum(): number {
|
||||
getSelectedSum(): number {
|
||||
if (this.editor.sceneManager?.selected_objects === undefined) {
|
||||
return 0;
|
||||
}
|
||||
@@ -230,13 +230,18 @@ function removeObject3D(object3D) {
|
||||
|
||||
*/
|
||||
|
||||
deleteObject(uuid: string) {
|
||||
const obj = this.scene.get_object_by_uuid(uuid);
|
||||
this.removeTransformControls();
|
||||
if (obj?.name === this.editor.camera_name) {
|
||||
return;
|
||||
}
|
||||
if (obj) {
|
||||
deleteObject(uuid: string) {
|
||||
const obj = this.scene.get_object_by_uuid(uuid);
|
||||
|
||||
if (!obj) {
|
||||
return
|
||||
}
|
||||
|
||||
this.removeTransformControls();
|
||||
if (obj.name === this.editor.camera_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally remove the object from the scene
|
||||
this.scene.scene.remove(obj);
|
||||
|
||||
@@ -267,25 +272,28 @@ deleteObject(uuid: string) {
|
||||
(obj as THREE.Mesh).material.dispose();
|
||||
}
|
||||
|
||||
if((obj as THREE.Mesh).geometry){
|
||||
if ((obj as THREE.Mesh).geometry) {
|
||||
(obj as THREE.Mesh).geometry.dispose()
|
||||
}
|
||||
|
||||
// TODO
|
||||
// FIXME: Timeline deletion is called twice, as well as a queue event.
|
||||
// Why is this necessary?
|
||||
this.editor.timeline.deleteObject(obj);
|
||||
Queue.publish({
|
||||
queueName: QueueNames.FROM_ENGINE,
|
||||
action: fromEngineActions.DELETE_OBJECT,
|
||||
data: {
|
||||
version: 1,
|
||||
type: AssetType.OBJECT,
|
||||
media_id: "",
|
||||
object_uuid: uuid,
|
||||
name: "",
|
||||
} as MediaItem,
|
||||
});
|
||||
this.editor.selected = undefined;
|
||||
this.editor.publishSelect();
|
||||
hideObjectPanel();
|
||||
this.editor.timeline.deleteObject(obj);
|
||||
}
|
||||
this.editor.timeline.deleteObject(uuid);
|
||||
Queue.publish({
|
||||
queueName: QueueNames.FROM_ENGINE,
|
||||
action: fromEngineActions.DELETE_OBJECT,
|
||||
data: {
|
||||
version: 1,
|
||||
type: AssetType.OBJECT,
|
||||
media_id: "",
|
||||
object_uuid: uuid,
|
||||
name: "",
|
||||
} as MediaItem,
|
||||
});
|
||||
this.editor.selected = undefined;
|
||||
this.editor.publishSelect();
|
||||
hideObjectPanel();
|
||||
this.editor.timeline.deleteObject(uuid);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { MediaFileType } from "../enums";
|
||||
import { ChromaKeyMaterial } from "./chromakey";
|
||||
import { TimeLine } from "./timeline";
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
import { ClipUI } from "../datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
|
||||
class Scene {
|
||||
name: string;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import { ClipUI } from "../datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
|
||||
import Scene from "./scene.js";
|
||||
import AudioEngine from "./audio_engine";
|
||||
import TransformEngine from "./transform_engine";
|
||||
import { LipSyncEngine } from "./lip_sync_engine";
|
||||
import { AnimationEngine } from "./animation_engine";
|
||||
import AudioEngine from "./Engines/audio_engine";
|
||||
import TransformEngine from "./Engines/transform_engine";
|
||||
import LipSyncEngine from "./Engines/lip_sync_engine";
|
||||
|
||||
import Queue, {
|
||||
UnionedActionTypes,
|
||||
@@ -19,11 +18,12 @@ import { ClipGroup, ClipType, AssetType } from "~/enums";
|
||||
import { CameraAspectRatio, MediaFileType } from "~/pages/PageEnigma/enums";
|
||||
import { Keyframe, MediaItem, UpdateTime } from "~/pages/PageEnigma/models";
|
||||
import Editor from "~/pages/PageEnigma/Editor/editor";
|
||||
import EmotionEngine from "./emotion_engine";
|
||||
import EmotionEngine from "./Engines/emotion_engine";
|
||||
import { IGenerationOptions } from "~/pages/PageEnigma/models/generationOptions";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
import { outlinerState } from "../signals";
|
||||
import { filmLength, outlinerState } from "../signals";
|
||||
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
|
||||
|
||||
export class TimeLine {
|
||||
editorEngine: Editor;
|
||||
@@ -40,7 +40,7 @@ export class TimeLine {
|
||||
// key framing
|
||||
transform_engine: TransformEngine;
|
||||
// animation engine
|
||||
animation_engine: AnimationEngine;
|
||||
animation_engine: CharacterAnimationEngine;
|
||||
// lip sync engine
|
||||
lipSync_engine: LipSyncEngine;
|
||||
// emotion engine
|
||||
@@ -65,7 +65,7 @@ export class TimeLine {
|
||||
audio_engine: AudioEngine,
|
||||
transform_engine: TransformEngine,
|
||||
lipsync_engine: LipSyncEngine,
|
||||
animation_engine: AnimationEngine,
|
||||
animation_engine: CharacterAnimationEngine,
|
||||
emotion_engine: EmotionEngine,
|
||||
scene: Scene,
|
||||
camera: THREE.Camera | null,
|
||||
@@ -194,7 +194,6 @@ export class TimeLine {
|
||||
const result = this.editorEngine.sceneManager?.render_outliner(
|
||||
this.characters,
|
||||
);
|
||||
//console.log(result);
|
||||
if (result) outlinerState.items.value = result.items;
|
||||
break;
|
||||
}
|
||||
@@ -452,27 +451,41 @@ export class TimeLine {
|
||||
}
|
||||
|
||||
public checkEditorCanPlay() {
|
||||
this.editorEngine.can_playback = this.getEndPoint() > 1;
|
||||
this.editorEngine.can_playback = this.getLastClipEnd() > 1;
|
||||
this.editorEngine.updateSelectedUI();
|
||||
}
|
||||
|
||||
public deleteObject(object_uuid: string) {
|
||||
const object = this.scene.get_object_by_uuid(object_uuid);
|
||||
if (object?.name === this.camera_name) {
|
||||
// This method is NOT responsible for deletion of the object from scene or other editor parts
|
||||
// It is only responsible for deleting the object from the timeline and its corresponding elements
|
||||
public deleteObject(object: THREE.Object3D<THREE.Object3DEventMap>) {
|
||||
if (!object) {
|
||||
return
|
||||
}
|
||||
|
||||
if (object.name === this.camera_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const object_uuid = object.uuid;
|
||||
|
||||
this.timeline_items.forEach((element) => {
|
||||
if (
|
||||
element.type == ClipType.TRANSFORM &&
|
||||
element.object_uuid == object_uuid
|
||||
) {
|
||||
// console.log(element);
|
||||
this.scene.deletePoint(element.clip_uuid);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: In the future object should have relations that can be deleted inside the engines
|
||||
// and the engines handle the deletion of objects.
|
||||
// Character Animation Engine already does this.
|
||||
this.timeline_items = this.timeline_items.filter(
|
||||
(element) => element.object_uuid !== object_uuid,
|
||||
);
|
||||
|
||||
this.animation_engine.removeCharacter(object);
|
||||
|
||||
// Update react land here.
|
||||
}
|
||||
|
||||
@@ -485,13 +498,15 @@ export class TimeLine {
|
||||
const type = data.type;
|
||||
const offset = data.offset;
|
||||
const end_offset = data.length + offset;
|
||||
const object = this.scene.get_object_by_uuid(object_uuid);
|
||||
const object_name =
|
||||
this.scene.get_object_by_uuid(object_uuid)?.name ?? "undefined";
|
||||
object?.name ?? "undefined";
|
||||
const clip_uuid = data.clip_uuid;
|
||||
|
||||
switch (type) {
|
||||
case "animation":
|
||||
this.animation_engine.load_object(object_uuid, media_id, name);
|
||||
await this.animation_engine.addCharacterAnimationMedia(object!, media_id, data);
|
||||
this.animation_engine.evaluate(this.current_time, this.timeline_limit);
|
||||
break;
|
||||
case "transform":
|
||||
this.transform_engine.loadObject(object_uuid, data.length);
|
||||
@@ -517,7 +532,7 @@ export class TimeLine {
|
||||
end_offset,
|
||||
0, // length
|
||||
this.scene.get_object_by_uuid(object_uuid)?.userData[
|
||||
"media_file_type"
|
||||
"media_file_type"
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -549,7 +564,10 @@ export class TimeLine {
|
||||
this.checkEditorCanPlay();
|
||||
}
|
||||
|
||||
public async addSelfAnimationClip(data: ClipUI, animation_clip: THREE.AnimationClip) {
|
||||
public async addSelfAnimationClip(
|
||||
data: ClipUI,
|
||||
animation_clip: THREE.AnimationClip,
|
||||
) {
|
||||
const object_uuid = data.object_uuid;
|
||||
const media_id = data.media_id;
|
||||
const name = data.name;
|
||||
@@ -558,13 +576,13 @@ export class TimeLine {
|
||||
const type = data.type;
|
||||
const offset = data.offset;
|
||||
const end_offset = data.length + offset;
|
||||
const object = this.scene.get_object_by_uuid(object_uuid);
|
||||
const object_name =
|
||||
this.scene.get_object_by_uuid(object_uuid)?.name ?? "undefined";
|
||||
object?.name ?? "undefined";
|
||||
const clip_uuid = data.clip_uuid;
|
||||
|
||||
this.animation_engine.load_object(object_uuid, media_id, name);
|
||||
this.animation_engine.clips[object_uuid + media_id].animation_clip = animation_clip;
|
||||
|
||||
this.animation_engine.addCharacterAnimation(object!, animation_clip, data);
|
||||
|
||||
// media id for this as well it can be downloaded
|
||||
this.addPlayableClip(
|
||||
new ClipUI(
|
||||
@@ -663,6 +681,7 @@ export class TimeLine {
|
||||
const media_id = data.media_id;
|
||||
const clip_uuid = data.clip_uuid;
|
||||
|
||||
// Remove the clip from the items array
|
||||
for (let i = 0; i < this.timeline_items.length; i++) {
|
||||
const element = this.timeline_items[i];
|
||||
if (
|
||||
@@ -675,6 +694,14 @@ export class TimeLine {
|
||||
}
|
||||
}
|
||||
|
||||
// Once the clip is removed, we need to make sure the engines aren't tracking it either
|
||||
// TODO: Ideally the engines should just handle this individually and we check them based on clip type/group
|
||||
// This way we won't need to iterate over all timeline items
|
||||
if (data.type === ClipType.ANIMATION) {
|
||||
const object = this.scene.get_object_by_uuid(object_uuid)!;
|
||||
this.animation_engine.removeAnimation(object, data)
|
||||
}
|
||||
|
||||
this.checkEditorCanPlay();
|
||||
}
|
||||
|
||||
@@ -729,9 +756,7 @@ export class TimeLine {
|
||||
this.audio_engine.loadClip(element.media_id);
|
||||
this.audio_engine.stopClip(element.media_id);
|
||||
} else if (element.type === ClipType.ANIMATION) {
|
||||
this.animation_engine.clips[
|
||||
element.object_uuid + element.media_id
|
||||
].stop();
|
||||
this.animation_engine.stop();
|
||||
} else if (
|
||||
element.type === ClipType.AUDIO &&
|
||||
element.group === ClipGroup.CHARACTER
|
||||
@@ -752,7 +777,7 @@ export class TimeLine {
|
||||
}
|
||||
}
|
||||
|
||||
public getEndPoint(): number {
|
||||
public getLastClipEnd(): number {
|
||||
let longest = 0;
|
||||
for (const element of this.timeline_items) {
|
||||
if (longest < element.length) {
|
||||
@@ -762,20 +787,19 @@ export class TimeLine {
|
||||
return longest;
|
||||
}
|
||||
|
||||
public getTimelineEnd(): number {
|
||||
return filmLength.peek() * 1000;
|
||||
}
|
||||
|
||||
// called by the editor update loop on each frame
|
||||
public async update(
|
||||
isRendering = false,
|
||||
delta_time: number = 0,
|
||||
): Promise<boolean> {
|
||||
//if (this.is_playing === false) return; // start and stop
|
||||
this.timeline_limit = this.getEndPoint();
|
||||
this.timeline_limit = this.getTimelineEnd();
|
||||
if (this.is_playing) {
|
||||
// When rendering we want to increase it by 1 but when in playback we want it dynamic based on deltatime.
|
||||
if (isRendering) {
|
||||
this.current_time += 1;
|
||||
} else {
|
||||
this.current_time += delta_time * this.editorEngine.cap_fps;
|
||||
}
|
||||
this.current_time += delta_time * 1000;
|
||||
this.pushEvent(fromEngineActions.UPDATE_TIME, {
|
||||
currentTime: this.current_time,
|
||||
});
|
||||
@@ -809,6 +833,11 @@ export class TimeLine {
|
||||
//this.scrubber_frame_position += 1;
|
||||
//2. allow stopping.
|
||||
//3. smallest unit is a frame and it is set by the scene and is in fps, our videos will be 60fps but we can reprocess them using the pipeline.
|
||||
|
||||
// Since the animation engine is newer we can just call an evaluation on it instead of having the timeline process it
|
||||
this.animation_engine.evaluate(this.current_time, this.timeline_limit);
|
||||
|
||||
// Iterate over the clips and play them as fit
|
||||
for (const element of this.timeline_items) {
|
||||
if (
|
||||
element.offset <= this.scrubber_frame_position &&
|
||||
@@ -843,7 +872,7 @@ export class TimeLine {
|
||||
// element.object_uuid + element.media_id,
|
||||
// this.scrubber_frame_position, element.offset);
|
||||
// }
|
||||
await this.audio_engine.playClip(element.media_id);
|
||||
this.audio_engine.playClip(element.media_id);
|
||||
await this.audio_engine.step(
|
||||
element.media_id,
|
||||
this.scrubber_frame_position,
|
||||
@@ -862,21 +891,6 @@ export class TimeLine {
|
||||
element.object_uuid + element.media_id
|
||||
].step(this.scrubber_frame_position, element.offset);
|
||||
}
|
||||
} else if (element.type === ClipType.ANIMATION) {
|
||||
if (object) {
|
||||
await this.animation_engine.clips[
|
||||
object.uuid + element.media_id
|
||||
].play(object);
|
||||
const fps = 60;
|
||||
await this.animation_engine.clips[
|
||||
object.uuid + element.media_id
|
||||
].step(
|
||||
(this.scrubber_frame_position - element.offset) / fps,
|
||||
this.is_playing,
|
||||
this.scrubber_frame_position, // Double FPS for best result.
|
||||
);
|
||||
//this.animation_engine.clips[object.uuid + element.media_id].update_bones();
|
||||
}
|
||||
} else if (element.type === ClipType.EXPRESSION) {
|
||||
if (object) {
|
||||
await this.emotion_engine.clips[
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
} from "~/pages/PageEnigma/models";
|
||||
import { toTimelineActions } from "./toTimelineActions";
|
||||
|
||||
import { ClipUI } from "../datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
import { IGenerationOptions } from "../models/generationOptions";
|
||||
import { ToastDataType } from "~/components";
|
||||
import { QueueNames } from "./QueueNames";
|
||||
|
||||
export type UnionedActionTypes =
|
||||
| fromEngineActions
|
||||
@@ -35,65 +36,88 @@ export type QueueSubscribeType = {
|
||||
data: UnionedDataTypes;
|
||||
};
|
||||
|
||||
export type SubscribeHandler = (entry: {
|
||||
action: UnionedActionTypes;
|
||||
data: UnionedDataTypes;
|
||||
}) => void;
|
||||
|
||||
class Queue {
|
||||
private _queue: Record<
|
||||
string,
|
||||
{
|
||||
private _queues: Record<
|
||||
QueueNames,
|
||||
Array<{
|
||||
action: UnionedActionTypes;
|
||||
data: UnionedDataTypes;
|
||||
}[]
|
||||
> = {};
|
||||
private _subscribers: Record<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
handler: (entry: {
|
||||
action: UnionedActionTypes;
|
||||
data: UnionedDataTypes;
|
||||
}) => void;
|
||||
}[]
|
||||
> = {};
|
||||
}>
|
||||
> = {
|
||||
[QueueNames.FROM_ENGINE]: [],
|
||||
[QueueNames.TO_ENGINE]: [],
|
||||
[QueueNames.TO_TIMELINE]: []
|
||||
};
|
||||
|
||||
private _subscribers: Record<
|
||||
QueueNames,
|
||||
Map<
|
||||
string,
|
||||
SubscribeHandler
|
||||
>
|
||||
> = {
|
||||
[QueueNames.FROM_ENGINE]: new Map<string, SubscribeHandler>(),
|
||||
[QueueNames.TO_ENGINE]: new Map<string, SubscribeHandler>(),
|
||||
[QueueNames.TO_TIMELINE]: new Map<string, SubscribeHandler>(),
|
||||
};
|
||||
|
||||
// Enqueues actions to a specified queue
|
||||
public publish({
|
||||
queueName,
|
||||
action,
|
||||
data,
|
||||
}: {
|
||||
queueName: string;
|
||||
queueName: QueueNames;
|
||||
action: UnionedActionTypes;
|
||||
data: UnionedDataTypes;
|
||||
}) {
|
||||
if (!this._queue[queueName]) {
|
||||
this._queue[queueName] = [];
|
||||
// Queues for all QueueNames should be initialized above but this is a safeguard for future enum additions
|
||||
// Make sure the queue exists
|
||||
if (!this._queues[queueName]) {
|
||||
this._queues[queueName] = [];
|
||||
}
|
||||
this._queue[queueName].push({ action, data });
|
||||
// console.log("Queued", queueName, action, data);
|
||||
|
||||
if (this._subscribers[queueName].length) {
|
||||
this._subscribers[queueName].forEach((item) =>
|
||||
item.handler(this._queue[queueName][0]),
|
||||
// Queue the task in the right queue
|
||||
this._queues[queueName].push({ action, data });
|
||||
|
||||
// TODO: Ideally the publisher shouldn't deal at all with the subscribers
|
||||
// It should only publish the events to the right queue
|
||||
// It also doesn't make sense to fire an event on publish because the operations are then dispersed
|
||||
// leading to potential race conditions
|
||||
// FIXME: Add some sort of queue loop using internal event and add queue locks using promises to serialize queue publishes
|
||||
if (this._subscribers[queueName]) {
|
||||
this._subscribers[queueName].forEach((subcribeHandler) =>
|
||||
subcribeHandler(this._queues[queueName][0]),
|
||||
);
|
||||
this._queue[queueName].splice(0, 1);
|
||||
this._queues[queueName].shift();
|
||||
}
|
||||
}
|
||||
|
||||
public subscribe(
|
||||
queueName: string,
|
||||
queueName: QueueNames,
|
||||
id: string,
|
||||
onMessage: (entry: QueueSubscribeType) => void,
|
||||
) {
|
||||
|
||||
// Queues for all QueueNames should be initialized above but this is a safeguard for future enum additions
|
||||
// Make sure the queue exists
|
||||
if (!this._subscribers[queueName]) {
|
||||
this._subscribers[queueName] = [];
|
||||
this._subscribers[queueName] = new Map<string, SubscribeHandler>();
|
||||
}
|
||||
this._subscribers[queueName] = this._subscribers[queueName].filter(
|
||||
(handler) => handler.id !== id,
|
||||
);
|
||||
this._subscribers[queueName].push({ id, handler: onMessage });
|
||||
while (this._queue[queueName]?.length) {
|
||||
this._subscribers[queueName].forEach((item) =>
|
||||
item.handler(this._queue[queueName][0]),
|
||||
|
||||
// Set the handler mapped to the ID
|
||||
this._subscribers[queueName].set(id, onMessage);
|
||||
|
||||
while (this._queues[queueName]?.length) {
|
||||
this._subscribers[queueName].forEach((subscribeHandler) =>
|
||||
subscribeHandler(this._queues[queueName][0]),
|
||||
);
|
||||
this._queue[queueName].splice(0, 1);
|
||||
this._queues[queueName].shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default interface IAnimationClip {
|
||||
speed: number;
|
||||
duration: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
evaluate: (time: number) => void;
|
||||
}
|
||||
+28
-12
@@ -7,16 +7,19 @@ import {
|
||||
} from "three/examples/jsm/Addons.js";
|
||||
import { MMDLoader } from "three/addons/loaders/MMDLoader.js";
|
||||
|
||||
import { MoveAIResult, Retarget } from "../../Editor/retargeting";
|
||||
import { MoveAIResult, Retarget } from "../Editor/retargeting";
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
import IAnimationClip from "./ClipInterfaces/IAnimationClip";
|
||||
|
||||
export class AnimationClip {
|
||||
export class AnimationClip implements IAnimationClip {
|
||||
version: number;
|
||||
media_id: string; // comes from the server
|
||||
object_uuid: string;
|
||||
type: "animation";
|
||||
location: "glb" | "remote";
|
||||
speed: number;
|
||||
duration: number;
|
||||
startTime: number;
|
||||
length: number;
|
||||
clip_name: string;
|
||||
mixer: THREE.AnimationMixer | undefined;
|
||||
@@ -26,6 +29,7 @@ export class AnimationClip {
|
||||
retargeted: boolean;
|
||||
last_frame: number;
|
||||
|
||||
|
||||
private isMMD: boolean = false;
|
||||
private obj: MMDAnimationHelperMixer | undefined;
|
||||
private helper: MMDAnimationHelper | undefined;
|
||||
@@ -42,6 +46,8 @@ export class AnimationClip {
|
||||
speed: number,
|
||||
length: number,
|
||||
clip_name: string,
|
||||
startTime: number,
|
||||
duration: number,
|
||||
) {
|
||||
this.version = version;
|
||||
this.media_id = media_id;
|
||||
@@ -50,6 +56,8 @@ export class AnimationClip {
|
||||
this.location = location;
|
||||
this.speed = speed;
|
||||
this.length = length;
|
||||
this.startTime = startTime;
|
||||
this.duration = duration;
|
||||
this.clip_name = clip_name;
|
||||
this.animation_clip;
|
||||
this.mixer;
|
||||
@@ -59,6 +67,12 @@ export class AnimationClip {
|
||||
this.last_frame = 0;
|
||||
}
|
||||
|
||||
get endTime() {
|
||||
return this.startTime + this.duration
|
||||
}
|
||||
|
||||
evaluate: (time: number) => void;
|
||||
|
||||
async get_media_url() {
|
||||
//This is for prod when we have the proper info on the url.
|
||||
const api_base_url = environmentVariables.values.BASE_API;
|
||||
@@ -69,7 +83,7 @@ export class AnimationClip {
|
||||
const bucketPath = json["media_file"]["public_bucket_path"];
|
||||
|
||||
//const media_api_base_url = environmentVariables.values.GOOGLE_API;
|
||||
const media_api_base_url = 'https://cdn-2.fakeyou.com';
|
||||
const media_api_base_url = "https://cdn-2.fakeyou.com";
|
||||
//const media_base_url = `${media_api_base_url}/vocodes-public`;
|
||||
//const media_url = `${media_base_url}${bucketPath}`;
|
||||
const media_url = `${media_api_base_url}${bucketPath}`;
|
||||
@@ -131,11 +145,13 @@ export class AnimationClip {
|
||||
|
||||
async _get_clip() {
|
||||
if (this.animation_clip == null && this.mixer !== null) {
|
||||
console.log(this.media_id)
|
||||
if(this.media_id === "SelfClip") {
|
||||
console.log(this.media_id);
|
||||
if (this.media_id === "SelfClip") {
|
||||
const rootObject = this.mixer?.getRoot();
|
||||
if(rootObject)
|
||||
this.animation_clip = (rootObject as THREE.Object3D<THREE.Object3DEventMap>).animations[0];
|
||||
if (rootObject)
|
||||
this.animation_clip = (
|
||||
rootObject as THREE.Object3D<THREE.Object3DEventMap>
|
||||
).animations[0];
|
||||
} else {
|
||||
this.animation_clip = await this._load_animation();
|
||||
}
|
||||
@@ -245,16 +261,16 @@ export class AnimationClip {
|
||||
|
||||
const mesh = this.mixer?.getRoot() as THREE.SkinnedMesh;
|
||||
|
||||
if(this.helper){
|
||||
if (this.helper) {
|
||||
this.helper._restoreBones(mesh); // Privateish js function for resetting bones.
|
||||
this.obj?.mixer?.setTime(deltatime); // Sets the time.
|
||||
this.helper._saveBones( mesh ); // Saves the bones location for restoration later.
|
||||
this.helper._saveBones(mesh); // Saves the bones location for restoration later.
|
||||
|
||||
mesh.updateMatrixWorld( true ); // Updates mesh.
|
||||
mesh.updateMatrixWorld(true); // Updates mesh.
|
||||
this.obj?.ikSolver.update(); // Updates IK Solver.
|
||||
this.obj?.grantSolver.update(); // Updates Grant which moves the bones.
|
||||
}
|
||||
|
||||
|
||||
// Changing of animation.
|
||||
if (
|
||||
this.obj &&
|
||||
@@ -262,7 +278,7 @@ export class AnimationClip {
|
||||
this.processingClip === false &&
|
||||
this.obj.mixer &&
|
||||
this.mixer._actions[0]._clip.name !==
|
||||
this.obj.mixer._actions[0]._clip.name &&
|
||||
this.obj.mixer._actions[0]._clip.name &&
|
||||
this.mixer !== undefined
|
||||
) {
|
||||
// On animation change.
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
import Ijson from "~/interfaces/Ijson";
|
||||
|
||||
interface AudioDataInterface {
|
||||
audioContext: AudioContext;
|
||||
@@ -16,7 +17,7 @@ class AudioData implements AudioDataInterface {
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioClip {
|
||||
export class AudioClip implements Ijson {
|
||||
version: number;
|
||||
media_id: string;
|
||||
type: "audio";
|
||||
+4
-2
@@ -2,8 +2,9 @@
|
||||
// it is created from a media id.
|
||||
import { MediaFileType } from "~/pages/PageEnigma/enums";
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
import Ijson from "~/interfaces/Ijson";
|
||||
|
||||
export class ClipUI {
|
||||
export class ClipUI implements Ijson {
|
||||
version: number;
|
||||
type: ClipType;
|
||||
group: ClipGroup;
|
||||
@@ -50,7 +51,7 @@ export class ClipUI {
|
||||
this.media_file_type = media_file_type;
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
toJSON() {
|
||||
return {
|
||||
version: this.version,
|
||||
group: this.group,
|
||||
@@ -67,3 +68,4 @@ export class ClipUI {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
import Ijson from "~/interfaces/Ijson";
|
||||
interface CsvJson {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export class EmotionClip {
|
||||
export class EmotionClip implements Ijson {
|
||||
version: number;
|
||||
media_id: string;
|
||||
type: "expression";
|
||||
@@ -94,7 +95,7 @@ export class EmotionClip {
|
||||
if (index === undefined) {
|
||||
index =
|
||||
element.morphTargetDictionary?.[
|
||||
key.charAt(0).toLowerCase() + key.slice(1)
|
||||
key.charAt(0).toLowerCase() + key.slice(1)
|
||||
];
|
||||
}
|
||||
if (
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { LipSync } from "../../Editor/lipsync";
|
||||
import { LipSync } from "../Editor/lipsync";
|
||||
import * as THREE from "three";
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
import Ijson from "~/interfaces/Ijson";
|
||||
|
||||
interface AudioDataInterface {
|
||||
audioContext: AudioContext;
|
||||
@@ -29,7 +30,7 @@ class BlendShapeHelper {
|
||||
}
|
||||
}
|
||||
|
||||
export class LipSyncClip {
|
||||
export class LipSyncClip implements Ijson {
|
||||
version: number;
|
||||
media_id: string;
|
||||
type: "lipsync";
|
||||
@@ -66,7 +67,7 @@ export class LipSyncClip {
|
||||
const json = await JSON.parse(await response.text());
|
||||
const bucketPath = json["media_file"]["public_bucket_path"];
|
||||
//const media_api_base_url = environmentVariables.values.GOOGLE_API;
|
||||
const media_api_base_url = 'https://cdn-2.fakeyou.com';
|
||||
const media_api_base_url = "https://cdn-2.fakeyou.com";
|
||||
//const media_base_url = `${media_api_base_url}/vocodes-public`;
|
||||
//const media_url = `${media_base_url}${bucketPath}`;
|
||||
const media_url = `${media_api_base_url}${bucketPath}`;
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
import Scene from "../../Editor/scene";
|
||||
import Scene from "../Editor/scene";
|
||||
|
||||
export class TransformFrame {
|
||||
position: THREE.Vector3;
|
||||
@@ -112,11 +112,18 @@ export class TransformClip {
|
||||
|
||||
request_point_location(keyframe_uuid: string, scene: Scene) {
|
||||
const point = scene.getPoint(keyframe_uuid);
|
||||
if (point == undefined) {console.log("Could not find point."); return;}
|
||||
this.keyframes.forEach(keyframe => {
|
||||
if(keyframe.keyframe_uuid == keyframe_uuid) {
|
||||
if (point == undefined) {
|
||||
console.log("Could not find point.");
|
||||
return;
|
||||
}
|
||||
this.keyframes.forEach((keyframe) => {
|
||||
if (keyframe.keyframe_uuid == keyframe_uuid) {
|
||||
keyframe.position = point.position;
|
||||
keyframe.rotation = new THREE.Vector3(point.rotation.x, point.rotation.y, point.rotation.z);
|
||||
keyframe.rotation = new THREE.Vector3(
|
||||
point.rotation.x,
|
||||
point.rotation.y,
|
||||
point.rotation.z,
|
||||
);
|
||||
keyframe.scale = point.scale;
|
||||
}
|
||||
});
|
||||
@@ -30,11 +30,11 @@ export const ControlsVideo = () => {
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const showStylePage = usePosthogFeatureFlag(FeatureFlags.SHOW_STYLE_PAGE);
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const date = new Date(seconds * 1000);
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
function formatTime(millis: number) {
|
||||
const date = new Date(millis);
|
||||
const secs = String(date.getUTCSeconds()).padStart(2, "0");
|
||||
return `00:${minutes}:${secs}`;
|
||||
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
|
||||
return `00:${secs}:${ms}`;
|
||||
}
|
||||
|
||||
const isPlaying =
|
||||
@@ -52,7 +52,7 @@ export const ControlsVideo = () => {
|
||||
};
|
||||
|
||||
const handleToEnd = () => {
|
||||
currentTime.value = filmLength.value * 60;
|
||||
currentTime.value = filmLength.value * 1000;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
@@ -80,7 +80,7 @@ export const ControlsVideo = () => {
|
||||
editorEngine?.togglePlayback();
|
||||
};
|
||||
const handleForwardStep = () => {
|
||||
currentTime.value = Math.min(currentTime.value + 1, filmLength.value * 60);
|
||||
currentTime.value = Math.min(currentTime.value + 1, filmLength.value * 1000);
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
@@ -127,7 +127,7 @@ export const ControlsVideo = () => {
|
||||
</span>
|
||||
<span className="opacity-50">/</span>
|
||||
<span className="w-[54px] opacity-50">
|
||||
{formatTime(filmLength.value * 60)}
|
||||
{formatTime(filmLength.value * 1000)}
|
||||
</span>
|
||||
</div>
|
||||
{showStylePage && (
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureFlag";
|
||||
import { FeatureFlags } from "~/enums";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
dndSidePanelWidth,
|
||||
@@ -14,9 +12,8 @@ import { TabItem, tabList } from "./tabList";
|
||||
|
||||
export const SidePanel = () => {
|
||||
useSignals();
|
||||
const initialTabIdx = usePosthogFeatureFlag(FeatureFlags.SHOW_SETS_TAB)
|
||||
? 0
|
||||
: 0;
|
||||
|
||||
const initialTabIdx = 0;
|
||||
const [selectedTab, setSelectedTab] = useState<TabItem>(
|
||||
tabList[initialTabIdx],
|
||||
);
|
||||
|
||||
@@ -3,25 +3,11 @@ import { FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBrush,
|
||||
faCatSpace,
|
||||
faGlobeSnow,
|
||||
faMountainCity,
|
||||
faPresentationScreen,
|
||||
faRaygun,
|
||||
faSpaghettiMonsterFlying,
|
||||
faUserAstronaut,
|
||||
faVolume,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { TabTitles } from "~/enums";
|
||||
import { AudioTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/AudioTab";
|
||||
import { CreaturesTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/CreaturesTab";
|
||||
import { PlanesTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/PlanesTab";
|
||||
import { ObjectsTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/ObjectsTab";
|
||||
import { SetsTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/SetsTab";
|
||||
import { SkyboxesTab } from "../SidePanelTabs/tabComps/SkyboxesTab";
|
||||
import { StylizeTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/StylizeTab/StylizeTab";
|
||||
import {
|
||||
AnimeTab,
|
||||
CartoonsTab,
|
||||
} from "../SidePanelTabs/tabComps/CharactersComboTabs";
|
||||
|
||||
@@ -31,55 +17,15 @@ export interface TabItem {
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export const tabList = [
|
||||
{
|
||||
icon: faMountainCity,
|
||||
title: TabTitles.OBJECTS_SETS,
|
||||
component: <SetsTab />,
|
||||
},
|
||||
{
|
||||
icon: faGlobeSnow,
|
||||
title: TabTitles.SKYBOXES,
|
||||
component: <SkyboxesTab />,
|
||||
},
|
||||
{
|
||||
icon: faSpaghettiMonsterFlying,
|
||||
title: TabTitles.OBJECTS_CREATURES,
|
||||
component: <CreaturesTab />,
|
||||
},
|
||||
{
|
||||
icon: faUserAstronaut,
|
||||
title: TabTitles.GROUP_ANIME,
|
||||
component: <AnimeTab />,
|
||||
},
|
||||
export const tabList: TabItem[] = [
|
||||
{
|
||||
icon: faCatSpace,
|
||||
title: TabTitles.GROUP_CARTOONS,
|
||||
component: <CartoonsTab />,
|
||||
},
|
||||
{
|
||||
icon: faRaygun,
|
||||
title: TabTitles.OBJECTS,
|
||||
component: <ObjectsTab />,
|
||||
},
|
||||
{
|
||||
icon: faPresentationScreen,
|
||||
title: TabTitles.PLANES,
|
||||
component: <PlanesTab />,
|
||||
},
|
||||
{
|
||||
icon: faVolume,
|
||||
title: TabTitles.AUDIO,
|
||||
component: <AudioTab />,
|
||||
},
|
||||
{
|
||||
icon: faBrush,
|
||||
title: TabTitles.STYLIZE,
|
||||
component: <StylizeTab />,
|
||||
},
|
||||
{
|
||||
icon: faBrush,
|
||||
title: TabTitles.RENDER,
|
||||
component: <div />,
|
||||
},
|
||||
}
|
||||
];
|
||||
@@ -26,11 +26,6 @@ export const SidePanelMenu = ({
|
||||
selectedTab: TabItem;
|
||||
}) => {
|
||||
useSignals();
|
||||
const showSetsTab = usePosthogFeatureFlag(FeatureFlags.SHOW_SETS_TAB);
|
||||
const showSkyboxesTab = usePosthogFeatureFlag(FeatureFlags.SHOW_SKYBOXES_TAB);
|
||||
const showCreaturesTab = usePosthogFeatureFlag(
|
||||
FeatureFlags.SHOW_CREATURES_TAB,
|
||||
);
|
||||
const showStylePage = usePosthogFeatureFlag(FeatureFlags.SHOW_STYLE_PAGE);
|
||||
return (
|
||||
<div
|
||||
@@ -45,18 +40,6 @@ export const SidePanelMenu = ({
|
||||
>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{(tabs ?? []).map((tab) => {
|
||||
if (tab.title === TabTitles.OBJECTS_SETS && !showSetsTab) {
|
||||
return;
|
||||
}
|
||||
if (tab.title === TabTitles.SKYBOXES && !showSkyboxesTab) {
|
||||
return;
|
||||
}
|
||||
if (tab.title === TabTitles.OBJECTS_CREATURES && !showCreaturesTab) {
|
||||
return;
|
||||
}
|
||||
if (tab.title === TabTitles.RENDER && !showStylePage) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={tab.title}
|
||||
@@ -66,9 +49,9 @@ export const SidePanelMenu = ({
|
||||
? "border-[#363636] bg-brand-secondary-900/60 opacity-100 hover:bg-brand-secondary-900/60"
|
||||
: "opacity-60",
|
||||
tab.title === TabTitles.STYLIZE &&
|
||||
"bg-brand-primary font-medium opacity-90 hover:border-white/25 hover:bg-brand-primary hover:opacity-100",
|
||||
"bg-brand-primary font-medium opacity-90 hover:border-white/25 hover:bg-brand-primary hover:opacity-100",
|
||||
tab.title === selectedTab.title &&
|
||||
tab.title === TabTitles.STYLIZE
|
||||
tab.title === TabTitles.STYLIZE
|
||||
? "border-white/50 opacity-100 hover:border-white/50"
|
||||
: "",
|
||||
])}
|
||||
|
||||
@@ -8,12 +8,11 @@ import {
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
|
||||
import { CharacterTrack } from "~/pages/PageEnigma/models";
|
||||
import { MediaFileType } from "~/pages/PageEnigma/enums";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { MediaFileAnimationType, ClipGroup, ClipType } from "~/enums";
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
|
||||
function buildUpdaters(
|
||||
updateCharacters: (options: {
|
||||
@@ -143,26 +142,6 @@ export const Character = ({ character }: Props) => {
|
||||
updateKeyframe={updateClipPosition}
|
||||
group={ClipGroup.CHARACTER}
|
||||
/>
|
||||
{(character.animationType === MediaFileAnimationType.Mixamo ||
|
||||
character.mediaType === MediaFileType.GLB) && (
|
||||
<TrackClips
|
||||
id={character.object_uuid}
|
||||
clips={expressionClips}
|
||||
updateClip={updateClipEmotions}
|
||||
group={ClipGroup.CHARACTER}
|
||||
type={ClipType.EXPRESSION}
|
||||
/>
|
||||
)}
|
||||
{editorEngine &&
|
||||
editorEngine.isObjectLipsync(character.object_uuid) && (
|
||||
<TrackClips
|
||||
id={character.object_uuid}
|
||||
clips={lipSyncClips}
|
||||
updateClip={updateClipLipSync}
|
||||
group={ClipGroup.CHARACTER}
|
||||
type={ClipType.AUDIO}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -99,13 +99,6 @@ export const RowHeaders = () => {
|
||||
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Position
|
||||
</div>
|
||||
{(character.animationType === MediaFileAnimationType.Mixamo ||
|
||||
character.mediaType === MediaFileType.GLB) && (
|
||||
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Expression
|
||||
</div>
|
||||
)}
|
||||
<LipSyncSubHeader character={character} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -141,7 +141,7 @@ export const Timeline = () => {
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
style={{ width: filmLength.value * 60 * 4 * scale.value + 72 }}
|
||||
style={{ width: filmLength.value * 1000 * 4 * scale.value + 72 }}
|
||||
>
|
||||
{currentPage.value === Pages.EDIT ? (
|
||||
<>
|
||||
|
||||
@@ -60,7 +60,7 @@ export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
|
||||
(event.clientX - position.x) / 4 / scale.value,
|
||||
);
|
||||
|
||||
if (clipOffset + 30 > filmLength.value * 60) {
|
||||
if (clipOffset + 500 > filmLength.value * 1000) {
|
||||
DndAsset.notDropText = "Not enough room to hold item";
|
||||
canDrop.value = false;
|
||||
return;
|
||||
@@ -116,7 +116,7 @@ export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
|
||||
max={
|
||||
index < clips.length - 1
|
||||
? clips[index + 1].offset
|
||||
: filmLength.value * 60
|
||||
: filmLength.value * 1000
|
||||
}
|
||||
group={group}
|
||||
updateClip={updateClip}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const TrackClipsPrompt = ({ id, clips, updateClip }: Props) => {
|
||||
const clientX = useRef(0);
|
||||
|
||||
function onPointerOver(event: PointerEvent) {
|
||||
const max = filmLength.value * 60 * 4 * scale.value;
|
||||
const max = filmLength.value * 1000 * 4 * scale.value;
|
||||
if (event.pageX < 320) {
|
||||
setHover(320);
|
||||
} else {
|
||||
@@ -41,7 +41,7 @@ export const TrackClipsPrompt = ({ id, clips, updateClip }: Props) => {
|
||||
if (hover === -1) {
|
||||
return;
|
||||
}
|
||||
const max = filmLength.value * 60 * 4 * scale.value;
|
||||
const max = filmLength.value * 1000 * 4 * scale.value;
|
||||
const offset =
|
||||
(event.pageX < 320 ? 0 : Math.min(event.pageX - 320, max + 80)) /
|
||||
4 /
|
||||
@@ -86,7 +86,7 @@ export const TrackClipsPrompt = ({ id, clips, updateClip }: Props) => {
|
||||
max={
|
||||
index < clips.length - 1
|
||||
? clips[index + 1].offset
|
||||
: filmLength.value * 60
|
||||
: filmLength.value * 1000
|
||||
}
|
||||
group={ClipGroup.PROMPT_TRAVEL}
|
||||
updateClip={updateClip}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const TrackKeyFrame = ({ keyframe, updateKeyframe }: Props) => {
|
||||
|
||||
const { onPointerDown, offset } = useMouseEventsKeyframe({
|
||||
keyframe,
|
||||
max: filmLength.value * 60,
|
||||
max: filmLength.value * 1000,
|
||||
min: 0,
|
||||
updateKeyframe,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export const useMouseEventsScrubber = () => {
|
||||
const [time, setTime] = useState(-1);
|
||||
|
||||
const getDelta = useCallback((event: MouseEvent) => {
|
||||
const max = filmLength.value * 60;
|
||||
const max = filmLength.value * 1000;
|
||||
|
||||
const delta = Math.round(
|
||||
(event.clientX - clientX.current) / 4 / scale.value + currentTime.value,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const useMouseEventsTimeline = () => {
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0;
|
||||
}
|
||||
const max = filmLength.value * 60;
|
||||
const max = filmLength.value * 1000;
|
||||
if (newPosition > max) {
|
||||
newPosition = max;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function useTimelineClick(page: Pages) {
|
||||
if (newTime < 0) {
|
||||
return;
|
||||
}
|
||||
const max = filmLength.value * 60;
|
||||
const max = filmLength.value * 1000;
|
||||
if (newTime > max) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const TimerGrid = () => {
|
||||
useSignals();
|
||||
const sectionWidth = 60 * 4 * scale.value;
|
||||
const sectionWidth = 1000 * 4 * scale.value;
|
||||
const scrollX = timelineScrollX.value;
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { addToast } from "~/signals";
|
||||
import { ToastTypes, ClipGroup } from "~/enums";
|
||||
import { toTimelineActions } from "~/pages/PageEnigma/Queue/toTimelineActions";
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { cameraAspectRatio } from "../signals/engine";
|
||||
import { loadPromptTravelData } from "~/pages/PageEnigma/signals/promptTravelGroup";
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { AnimationClip } from "../datastructures/clips/animation_clip";
|
||||
import { AudioClip } from "../datastructures/clips/audio_clip";
|
||||
import { ClipUI } from "../datastructures/clips/clip_ui";
|
||||
import { EmotionClip } from "../datastructures/clips/emotion_clip";
|
||||
import { LipSyncClip } from "../datastructures/clips/lipsync_clip";
|
||||
import { TransformClip } from "../datastructures/clips/transform_clip";
|
||||
import AnimationEngine from "../Editor/animation_engine";
|
||||
import AudioEngine from "../Editor/audio_engine";
|
||||
import EmotionEngine from "../Editor/emotion_engine";
|
||||
import LipSyncEngine from "../Editor/lip_sync_engine";
|
||||
import { AnimationClip } from "../clips/animation_clip";
|
||||
import { AudioClip } from "../clips/audio_clip";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
import { EmotionClip } from "../clips/emotion_clip";
|
||||
import { LipSyncClip } from "../clips/lipsync_clip";
|
||||
import { TransformClip } from "../clips/transform_clip";
|
||||
import { CharacterAnimationEngine } from "../Editor/Engines/CharacterAnimationEngine";
|
||||
import AudioEngine from "../Editor/Engines/audio_engine";
|
||||
import EmotionEngine from "../Editor/Engines/emotion_engine";
|
||||
import LipSyncEngine from "../Editor/Engines/lip_sync_engine";
|
||||
import { TimeLine } from "../Editor/timeline";
|
||||
import TransformEngine from "../Editor/transform_engine";
|
||||
import TransformEngine from "../Editor/Engines/transform_engine";
|
||||
|
||||
export class StoryTellerProxyTimeline {
|
||||
timeline: TimeLine;
|
||||
transform_engine: TransformEngine;
|
||||
animation_engine: AnimationEngine;
|
||||
animation_engine: CharacterAnimationEngine;
|
||||
audio_engine: AudioEngine;
|
||||
lipsync_engine: LipSyncEngine;
|
||||
emotion_engine: EmotionEngine;
|
||||
@@ -23,7 +23,7 @@ export class StoryTellerProxyTimeline {
|
||||
version: number,
|
||||
timeline: TimeLine,
|
||||
transform_engine: TransformEngine,
|
||||
animation_engine: AnimationEngine,
|
||||
animation_engine: CharacterAnimationEngine,
|
||||
audio_engine: AudioEngine,
|
||||
lipsync_engine: LipSyncEngine,
|
||||
emotion_engine: EmotionEngine,
|
||||
@@ -36,19 +36,17 @@ export class StoryTellerProxyTimeline {
|
||||
this.emotion_engine = emotion_engine;
|
||||
}
|
||||
|
||||
private async getItems(items: any[]): Promise<any[]> {
|
||||
let timeline_items_data = [];
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const element = items[index];
|
||||
timeline_items_data.push(element.toJSON());
|
||||
}
|
||||
private async getItemsToJson(items: unknown[]): Promise<unknown[]> {
|
||||
const timeline_items_data: unknown[] = [];
|
||||
items.forEach((element) => { timeline_items_data.push(element.toJSON()); });
|
||||
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
private async getItemsDict(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
let timeline_items_data: { [key: string]: any } = {};
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
@@ -58,9 +56,9 @@ export class StoryTellerProxyTimeline {
|
||||
return timeline_items_data;
|
||||
}
|
||||
|
||||
public async saveToJson(): Promise<any> {
|
||||
let timeline_json = {
|
||||
timeline: await this.getItems(this.timeline.timeline_items),
|
||||
public async saveToJson(): Promise<unknown> {
|
||||
const timeline_json = {
|
||||
timeline: await this.getItemsToJson(this.timeline.timeline_items),
|
||||
transform: await this.getItemsDict(this.transform_engine.clips),
|
||||
animation: await this.getItemsDict(this.animation_engine.clips),
|
||||
audio: await this.getItemsDict(this.audio_engine.clips),
|
||||
@@ -73,11 +71,11 @@ export class StoryTellerProxyTimeline {
|
||||
}
|
||||
|
||||
private async loadTimelineClips(timeline_clips: any[]): Promise<any[]> {
|
||||
let new_clips = [];
|
||||
const new_clips = [];
|
||||
if (timeline_clips) {
|
||||
for (let index = 0; index < timeline_clips.length; index++) {
|
||||
const element = timeline_clips[index];
|
||||
let clip = new ClipUI(
|
||||
const clip = new ClipUI(
|
||||
element.version,
|
||||
element.type,
|
||||
element.group,
|
||||
@@ -100,7 +98,7 @@ export class StoryTellerProxyTimeline {
|
||||
private async loadTransformClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
let timeline_items_data: { [key: string]: any } = {};
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
@@ -119,7 +117,7 @@ export class StoryTellerProxyTimeline {
|
||||
private async loadAudioClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
let timeline_items_data: { [key: string]: any } = {};
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
@@ -136,7 +134,7 @@ export class StoryTellerProxyTimeline {
|
||||
private async loadAnimationClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
let timeline_items_data: { [key: string]: any } = {};
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
@@ -157,7 +155,7 @@ export class StoryTellerProxyTimeline {
|
||||
private async loadLipsyncClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
let timeline_items_data: { [key: string]: any } = {};
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
@@ -174,7 +172,7 @@ export class StoryTellerProxyTimeline {
|
||||
private async loadEmotionClips(items: {
|
||||
[key: string]: any;
|
||||
}): Promise<{ [key: string]: any }> {
|
||||
let timeline_items_data: { [key: string]: any } = {};
|
||||
const timeline_items_data: { [key: string]: any } = {};
|
||||
for (const key in items) {
|
||||
if (items.hasOwnProperty(key)) {
|
||||
const element = items[key];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import * as uuid from "uuid";
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { publishClip } from "~/pages/PageEnigma/signals/utils/publishClip";
|
||||
|
||||
export const audioGroup = signal<AudioGroup>({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import * as uuid from "uuid";
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { ToastTypes } from "~/enums";
|
||||
import { addToast } from "~/signals";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { CharacterTrack, Clip, Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { ClipType } from "~/enums";
|
||||
import { characterGroup } from "~/pages/PageEnigma/signals";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { Keyframe, ObjectTrack } from "~/pages/PageEnigma/models";
|
||||
import { objectGroup } from "~/pages/PageEnigma/signals";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import * as uuid from "uuid";
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
|
||||
import { ClipUI } from "~/pages/PageEnigma/clips/clip_ui";
|
||||
import { publishClip } from "~/pages/PageEnigma/signals/utils/publishClip";
|
||||
|
||||
export const promptTravelGroup = signal<PromptTravelGroup>({
|
||||
|
||||
@@ -5,8 +5,8 @@ import { pageWidth } from "~/signals";
|
||||
import { timelineScrollX } from "~/pages/PageEnigma/signals/timing";
|
||||
|
||||
// timeline
|
||||
export const scale = signal(1);
|
||||
export const filmLength = signal(7);
|
||||
export const scale = signal(0.1);
|
||||
export const filmLength = signal(7); // seconds
|
||||
export const timelineHeight = signal(0);
|
||||
|
||||
export const cameraMinimized = signal(false);
|
||||
@@ -59,7 +59,7 @@ export const timelinePremiumLockPosition = computed(() => {
|
||||
});
|
||||
|
||||
export const fullWidth = computed(() => {
|
||||
return filmLength.value * 60 * 4 * scale.value;
|
||||
return filmLength.value * 1000 * 4 * scale.value;
|
||||
});
|
||||
|
||||
// side panel
|
||||
|
||||
@@ -14,8 +14,8 @@ export function publishClip(
|
||||
action: toEngineActions.ADD_CLIP,
|
||||
data: newClip,
|
||||
});
|
||||
if (offset + (dragItem.length ?? 0) > filmLength.value * 60) {
|
||||
newClip.length = filmLength.value * 60 - offset;
|
||||
if (offset + (dragItem.length ?? 0) > filmLength.value * 1000) {
|
||||
newClip.length = filmLength.value * 1000 - offset;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_CLIP,
|
||||
|
||||
Generated
+4624
-8819
File diff suppressed because it is too large.
Load diff
Generated
+555
-321
File diff suppressed because it is too large.
Load diff
+14
-3
@@ -1,7 +1,16 @@
|
||||
{
|
||||
"include": ["env.d.ts", "**/*.ts", "**/*.tsx", "netlify-edge-plugin.ts"],
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"netlify-edge-plugin.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
@@ -15,7 +24,9 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
"~/*": [
|
||||
"./app/*"
|
||||
]
|
||||
},
|
||||
// Vite takes care of building everything, not tsc.
|
||||
"noEmit": true
|
||||
|
||||
Reference in new issue
Block a user