Merge pull request #2 from storytold/tanish/refactor

Implement Character Animation Engine
This commit is contained in:
Tanish Manku authored and GitHub committed 2025-01-02 16:39:28 -08:00
commit 7b34000792
51 files changed
+5826 -9490

No files matched your search

+18
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
export default interface Ijson {
toJSON(): unknown;
}
+1 -1
View File
@@ -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!!!
@@ -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);
}
}
@@ -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();
}
@@ -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,4 +1,4 @@
import { EmotionClip } from "../datastructures/clips/emotion_clip";
import { EmotionClip } from "../../clips/emotion_clip";
class EmotionEngine {
clips: { [key: string]: EmotionClip } = {};
@@ -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,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 {
+33 -18
View File
@@ -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;
+36 -28
View File
@@ -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);
}
}
+1 -1
View File
@@ -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;
+64 -50
View File
@@ -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[
+59 -35
View File
@@ -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;
}
@@ -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.
@@ -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";
@@ -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 {
};
}
}
@@ -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 (
@@ -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}`;
@@ -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];
+1 -1
View File
@@ -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>({
+1 -1
View File
@@ -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>({
+3 -3
View File
@@ -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,
+4624 -8819
View File
File diff suppressed because it is too large. Load diff
+555 -321
View File
File diff suppressed because it is too large. Load diff
+14 -3
View File
@@ -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