mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-29 13:02:51 -04:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da65b231e | ||
|
|
79b8ad4307 | ||
|
|
564c4127b9 | ||
|
|
9943ad1b80 | ||
|
|
ffa191ba7f | ||
|
|
4f767aed2c | ||
|
|
7697a87931 | ||
|
|
80a42aad69 | ||
|
|
64dbfd31fd |
@@ -1,40 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>ArtCraft</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y4Y8KMLPQ7"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Y4Y8KMLPQ7');
|
||||
</script>
|
||||
<head>
|
||||
<title>ArtCraft</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- Microsoft Clarity -->
|
||||
<script type="text/javascript">
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "sixp9uu263");
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y4Y8KMLPQ7"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Y4Y8KMLPQ7');
|
||||
</script>
|
||||
<script crossOrigin="anonymous" src="//unpkg.com/react-scan/dist/auto.global.js"></script>
|
||||
|
||||
<!-- Microsoft Clarity -->
|
||||
<script type="text/javascript">
|
||||
(function (c, l, a, r, i, t, y) {
|
||||
c[a] = c[a] || function () {(c[a].q = c[a].q || []).push(arguments)};
|
||||
t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i;
|
||||
y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
|
||||
})(window, document, "clarity", "script", "sixp9uu263");
|
||||
</script>
|
||||
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
|
||||
</head>
|
||||
<body className="overflow-hidden bg-ui-background">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
|
||||
rel="stylesheet" />
|
||||
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
|
||||
</head>
|
||||
|
||||
<body className="overflow-hidden bg-ui-background">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
+38
-8
@@ -2,6 +2,8 @@ import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { FontLoader } from "three/addons/loaders/FontLoader.js";
|
||||
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
|
||||
import { SplatFileType, SplatMesh } from "@sparkjsdev/spark";
|
||||
import { SPLAT_FILE_TYPE } from "~/enums";
|
||||
|
||||
interface LoaderInterface {
|
||||
file: File;
|
||||
@@ -11,6 +13,10 @@ interface LoaderInterface {
|
||||
statusCallback: (statusObject: { type: string; message?: string }) => void;
|
||||
}
|
||||
|
||||
interface PreviewReturn {
|
||||
renderer: THREE.WebGLRenderer;
|
||||
}
|
||||
|
||||
export const loadPreviewOnCanvas = ({
|
||||
file,
|
||||
canvas,
|
||||
@@ -19,7 +25,7 @@ export const loadPreviewOnCanvas = ({
|
||||
file: File;
|
||||
canvas: HTMLCanvasElement;
|
||||
statusCallback: (error: { type: string; message?: string }) => void;
|
||||
}) => {
|
||||
}): PreviewReturn => {
|
||||
// Setup of scene, camera, and renderer in the canvas
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
@@ -32,10 +38,13 @@ export const loadPreviewOnCanvas = ({
|
||||
const camera = new THREE.PerspectiveCamera(35, aspectRatio, 0.1, 1000);
|
||||
camera.position.z = 2;
|
||||
|
||||
const gl2ctx = canvas.getContext("webgl2");
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
canvas: canvas,
|
||||
context: gl2ctx!,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
@@ -51,19 +60,37 @@ export const loadPreviewOnCanvas = ({
|
||||
|
||||
scene.add(light);
|
||||
|
||||
let splatMesh: SplatMesh | null = null;
|
||||
|
||||
// load the file into the preview mini-scene depending of the file's type
|
||||
if (file.name.includes(".glb")) {
|
||||
if (file.name.endsWith(".glb")) {
|
||||
glbLoader({ file, scene, camera, renderer, statusCallback });
|
||||
} else if (file.name.includes(".pmd")) {
|
||||
} else if (file.name.endsWith(".pmd")) {
|
||||
pmdLoader({ file, scene, camera, renderer, statusCallback });
|
||||
} else if (
|
||||
file.name.includes(".png") ||
|
||||
file.name.includes(".jpg") ||
|
||||
file.name.includes(".jpeg") ||
|
||||
file.name.includes(".gif")
|
||||
file.name.endsWith(".png") ||
|
||||
file.name.endsWith(".jpg") ||
|
||||
file.name.endsWith(".jpeg") ||
|
||||
file.name.endsWith(".gif")
|
||||
) {
|
||||
imagePlaneLoader({ file, scene, camera, renderer, statusCallback });
|
||||
} else if (file.name.includes(".vmd")) {
|
||||
} else if (file.name.endsWith(".spz")) {
|
||||
file.arrayBuffer().then((arrayBuffer) => {
|
||||
splatMesh = new SplatMesh({
|
||||
fileBytes: arrayBuffer, fileType: SplatFileType.SPZ,
|
||||
onLoad: () => { scene.add(splatMesh!) }
|
||||
});
|
||||
|
||||
if (file.name.split(".")[0].endsWith("ceramic")) {
|
||||
splatMesh.rotateX(Math.PI);
|
||||
}
|
||||
}).catch((loaderError) => {
|
||||
statusCallback({
|
||||
type: "SPLAT Loader Error",
|
||||
message: String(loaderError),
|
||||
});
|
||||
});
|
||||
} else if (file.name.endsWith(".vmd")) {
|
||||
statusCallback({
|
||||
type: "Preview Error",
|
||||
message: "Sorry, Preview is not available to VMD files yet",
|
||||
@@ -78,8 +105,11 @@ export const loadPreviewOnCanvas = ({
|
||||
// Render the loaded data
|
||||
const animate = function () {
|
||||
renderer.render(scene, camera);
|
||||
splatMesh?.rotateY(0.01);
|
||||
};
|
||||
renderer.setAnimationLoop(animate);
|
||||
|
||||
return { renderer };
|
||||
};
|
||||
|
||||
const glbLoader = ({
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { ListDropdown } from "@storyteller/ui-list-dropdown";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { FileUploader } from "@storyteller/ui-file-uploader";
|
||||
import { UploaderState } from "../../../models";
|
||||
import { FilterEngineCategories, MediaFileAnimationType, UploaderStates } from "../../../enums";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCube } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "../UploadModal3D/utilities";
|
||||
import { WebGLRenderer } from "three";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
engineCategory: FilterEngineCategories;
|
||||
options?: {
|
||||
fileSubtypes?: { [key: string]: string }[];
|
||||
hasLength?: boolean;
|
||||
hasThumbnailUpload?: boolean;
|
||||
};
|
||||
onClose: () => void;
|
||||
onUploadProgress: (newState: UploaderState) => void;
|
||||
onLocalBytes: (bytes: ArrayBuffer, shouldFlip) => void;
|
||||
}
|
||||
|
||||
export const UploadFilesSplat = ({
|
||||
fileTypes,
|
||||
engineCategory,
|
||||
options,
|
||||
onClose,
|
||||
onUploadProgress,
|
||||
onLocalBytes,
|
||||
}: Props) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | undefined>(undefined);
|
||||
const canvasCallbackRef = useCallback((node: HTMLCanvasElement) => {
|
||||
if (node !== null) {
|
||||
canvasRef.current = node;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fileSubtypes = options?.fileSubtypes;
|
||||
// const hasLength = options?.hasLength;
|
||||
// const hasThumbnailUpload = options?.hasThumbnailUpload;
|
||||
|
||||
const [subtype, setSubtype] = useState<MediaFileAnimationType | undefined>(
|
||||
fileSubtypes
|
||||
? (Object.values(fileSubtypes[0])[0] as MediaFileAnimationType)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const [assetFile, setAssetFile] = useState<{
|
||||
value: File | null;
|
||||
error?: string;
|
||||
}>({ value: null });
|
||||
|
||||
const [previewStatus, setPreviewStatus] = useState<{
|
||||
type: string;
|
||||
message?: string;
|
||||
}>({ type: "init" });
|
||||
|
||||
const [thumbnailFile, setThumbnailFile] = useState<Blob | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!assetFile.value) {
|
||||
setAssetFile((curr) => ({
|
||||
...curr,
|
||||
error: "Please select a file to upload.",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the filename as the title
|
||||
const title = assetFile.value.name.split(".")[0];
|
||||
|
||||
const shouldFlip = title.endsWith("ceramic");
|
||||
|
||||
assetFile.value.arrayBuffer().then((arrayBuffer) => {
|
||||
onLocalBytes(arrayBuffer, shouldFlip);
|
||||
}).catch((error) => {
|
||||
console.error("Error reading file as ArrayBuffer:", error);
|
||||
});
|
||||
|
||||
// upload3DObjects({
|
||||
// title: title,
|
||||
// assetFile: assetFile.value,
|
||||
// thumbnailSnapshot: thumbnailFile,
|
||||
// engineCategory: engineCategory,
|
||||
// animationType: subtype,
|
||||
// progressCallback: onUploadProgress,
|
||||
// });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Reset subtype when fileSubtypes set changes (e.g., toggling character)
|
||||
if (!fileSubtypes || fileSubtypes.length === 0) {
|
||||
setSubtype(undefined);
|
||||
return;
|
||||
}
|
||||
const nextDefault = Object.values(fileSubtypes[0])[0] as
|
||||
| MediaFileAnimationType
|
||||
| undefined;
|
||||
setSubtype(nextDefault);
|
||||
}, [fileSubtypes]);
|
||||
|
||||
useEffect(() => {
|
||||
let renderer: WebGLRenderer | null = null;
|
||||
|
||||
if (canvasRef.current && assetFile.value) {
|
||||
renderer = loadPreviewOnCanvas({
|
||||
file: assetFile.value,
|
||||
canvas: canvasRef.current,
|
||||
statusCallback: (statusObject: { type: string; message?: string }) => {
|
||||
setPreviewStatus(statusObject);
|
||||
},
|
||||
}).renderer;
|
||||
}
|
||||
|
||||
return () => {
|
||||
renderer?.setAnimationLoop(null);
|
||||
renderer?.dispose();
|
||||
}
|
||||
}, [assetFile.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewStatus.type === "OK" && canvasRef.current) {
|
||||
snapshotCanvasAsThumbnail({
|
||||
targetNode: canvasRef.current!,
|
||||
resultCallback: (snapshotBlob) => {
|
||||
if (snapshotBlob) {
|
||||
setThumbnailFile(snapshotBlob);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [previewStatus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<FileUploader
|
||||
fileTypes={fileTypes}
|
||||
file={assetFile.value ?? undefined}
|
||||
handleChange={(file: File) => {
|
||||
setAssetFile({
|
||||
value: file,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{fileSubtypes && fileSubtypes.length > 1 && (
|
||||
<ListDropdown
|
||||
list={fileSubtypes}
|
||||
onSelect={(value) => setSubtype(value as MediaFileAnimationType)}
|
||||
/>
|
||||
)}
|
||||
{assetFile.error && (
|
||||
<h6 className="z-10 text-red">{assetFile.error}</h6>
|
||||
)}
|
||||
|
||||
<div className="relative m-auto w-full overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<canvas
|
||||
className="pointer-events-none h-full w-full"
|
||||
ref={canvasCallbackRef}
|
||||
/>
|
||||
{!assetFile.value && (
|
||||
<h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
Your model preview will appear here
|
||||
</h6>
|
||||
)}
|
||||
{previewStatus.type.includes("Error") && (
|
||||
<>
|
||||
<h6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||
{previewStatus.type}
|
||||
{previewStatus.message && <br />}
|
||||
{previewStatus.message}
|
||||
</h6>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!assetFile.value}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { LoadingDots } from "@storyteller/ui-loading";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
|
||||
import { initialUploaderState, UploaderState } from "../../../models";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
UploaderStates,
|
||||
MediaFileAnimationType,
|
||||
SPLAT_FILE_TYPE,
|
||||
} from "../../../enums";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalVisibleDuringDrag,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import { UploadFilesSplat } from "./UploadFilesSplat";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSuccess: (splatArrayBuffer: ArrayBuffer, shouldFlip: boolean) => void;
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
titleIcon: IconDefinition;
|
||||
options?: {
|
||||
fileSubtypes?: { [key: string]: string }[];
|
||||
hasLength?: boolean;
|
||||
hasThumbnailUpload?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const splatFileTypes = Object.values(SPLAT_FILE_TYPE);
|
||||
|
||||
export function UploadModalSplat(props: Props) {
|
||||
const selectedCategory = FilterEngineCategories.SPLAT;
|
||||
const { isOpen, onClose, onSuccess, title, titleIcon, options } = props;
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setUploaderState(initialUploaderState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
resetModalState();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploaderState.status === UploaderStates.success) {
|
||||
// Automatically open the global Gallery modal after a successful upload
|
||||
// galleryModalVisibleViewMode.value = true;
|
||||
// galleryModalVisibleDuringDrag.value = true;
|
||||
|
||||
// onSuccess(selectedCategory);
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [uploaderState.status]);
|
||||
|
||||
const UploaderModalContent = () => {
|
||||
switch (uploaderState.status) {
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<UploadFilesSplat
|
||||
title={title}
|
||||
engineCategory={selectedCategory}
|
||||
fileTypes={splatFileTypes}
|
||||
options={{
|
||||
...(options ?? {}),
|
||||
}}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
onLocalBytes={(buffer, shouldFlip) => { onSuccess(buffer, shouldFlip); }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
case UploaderStates.settingCover:
|
||||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderStates.success: {
|
||||
return (
|
||||
<UploadSuccess
|
||||
title="Splat"
|
||||
onOk={() => {
|
||||
onClose();
|
||||
// onSuccess(selectedCategory);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case UploaderStates.assetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={selectedCategory}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
case UploaderStates.coverCreateError:
|
||||
case UploaderStates.coverSetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={"Thumbnail"}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
titleIcon={titleIcon}
|
||||
title={title}
|
||||
className="max-w-xl"
|
||||
showClose={true}
|
||||
>
|
||||
<UploaderModalContent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./UploadModalSplat";
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./UploadModal3D";
|
||||
export * from "./UploadModalMedia";
|
||||
export * from "./UploadModalSplat";
|
||||
|
||||
@@ -42,6 +42,10 @@ export enum OBJECT_FILE_TYPE {
|
||||
GLB = "glb",
|
||||
}
|
||||
|
||||
export enum SPLAT_FILE_TYPE {
|
||||
SPZ = "spz",
|
||||
}
|
||||
|
||||
export enum CHARACTER_MMD_FILE_TYPE {
|
||||
PMD = "pmd",
|
||||
PMX = "pmx",
|
||||
|
||||
@@ -23,6 +23,7 @@ export enum FilterEngineCategories {
|
||||
IMAGE_PLANE = "image_plane",
|
||||
LOCATION = "location",
|
||||
OBJECT = "object",
|
||||
SPLAT = "splat",
|
||||
SCENE = "scene",
|
||||
SET_DRESSING = "set_dressing",
|
||||
SKYBOX = "skybox",
|
||||
|
||||
@@ -53,6 +53,8 @@ import FindSurfaces from "./FindSurfaces.js";
|
||||
import Stats from "three/examples/jsm/libs/stats.module.js";
|
||||
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
|
||||
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
|
||||
import { SparkRenderer } from "@sparkjsdev/spark";
|
||||
import { SSAOPass } from "three/examples/jsm/Addons.js";
|
||||
|
||||
export type EditorInitializeConfig = {
|
||||
sceneToken: string;
|
||||
@@ -89,6 +91,7 @@ class Editor {
|
||||
render_camera_aspect_ratio: CameraAspectRatio =
|
||||
CameraAspectRatio.HORIZONTAL_3_2;
|
||||
renderer: THREE.WebGLRenderer | undefined;
|
||||
sparkRenderer: SparkRenderer | null = null;
|
||||
rawRenderer: THREE.WebGLRenderer | undefined;
|
||||
clock: THREE.Clock | undefined;
|
||||
canvReference: HTMLCanvasElement | null = null;
|
||||
@@ -99,7 +102,7 @@ class Editor {
|
||||
outlinePass: OutlinePass | undefined;
|
||||
last_cam_pos: THREE.Vector3;
|
||||
last_cam_rot: THREE.Euler;
|
||||
saoPass: SAOPass | undefined;
|
||||
ssaoPass: SSAOPass | undefined;
|
||||
outputPass: OutputPass | undefined;
|
||||
renderOutputPass: OutputPass | undefined;
|
||||
bloomPass: UnrealBloomPass | undefined;
|
||||
@@ -273,7 +276,7 @@ class Editor {
|
||||
// Recording params.
|
||||
this.render_timer = 0;
|
||||
this.fps_number = 60;
|
||||
this.cap_fps = 120;
|
||||
this.cap_fps = 60;
|
||||
// Timeline settings.
|
||||
this.can_playback = false;
|
||||
this.playback_location = 0;
|
||||
@@ -502,8 +505,8 @@ class Editor {
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
this.focalLengthToFov(mainCameraConfig.focalLength),
|
||||
width / height,
|
||||
0.01,
|
||||
200,
|
||||
0.1,
|
||||
2000,
|
||||
);
|
||||
this.camera.position.set(
|
||||
mainCameraConfig.position.x,
|
||||
@@ -552,6 +555,11 @@ class Editor {
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
// this.sparkRenderer = new SparkRenderer({
|
||||
// renderer: this.renderer,
|
||||
// autoUpdate: true,
|
||||
// });
|
||||
|
||||
this.rawRenderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
canvas: this.canvasRenderCamReference,
|
||||
@@ -667,6 +675,10 @@ class Editor {
|
||||
// Attach freecam state controller
|
||||
this.mouseOverEventHandler = this.mouseOverEventHandler.bind(this);
|
||||
|
||||
// Add spark renderer as a child of the camera
|
||||
this.activeScene.scene.add(this.sparkRenderer);
|
||||
this.camera?.add(this.sparkRenderer);
|
||||
|
||||
const onloadCallback = () => {
|
||||
console.log("Setting Scene is loaded");
|
||||
this._isEngineDataLoaded = true;
|
||||
@@ -1145,26 +1157,19 @@ class Editor {
|
||||
this.composer.addPass(this.renderPass);
|
||||
|
||||
this.outlinePass = new OutlinePass(
|
||||
new THREE.Vector2(width, height),
|
||||
new THREE.Vector2(width / 10, height / 10),
|
||||
this.activeScene.scene,
|
||||
this.camera,
|
||||
);
|
||||
|
||||
this.outlinePass.edgeStrength = 5.0;
|
||||
this.outlinePass.edgeGlow = 0.1;
|
||||
this.outlinePass.edgeThickness = 1.2;
|
||||
this.outlinePass.pulsePeriod = 3;
|
||||
this.outlinePass.usePatternTexture = false;
|
||||
this.outlinePass.visibleEdgeColor.set(0x4b9fff);
|
||||
// this.outlinePass.edgeStrength = 5.0;
|
||||
// this.outlinePass.edgeGlow = 0.1;
|
||||
// this.outlinePass.edgeThickness = 1.2;
|
||||
// this.outlinePass.pulsePeriod = 3;
|
||||
// this.outlinePass.usePatternTexture = false;
|
||||
// this.outlinePass.visibleEdgeColor.set(0x4b9fff);
|
||||
|
||||
this.composer.addPass(this.outlinePass);
|
||||
|
||||
this.saoPass = new SAOPass(this.activeScene.scene, this.camera);
|
||||
this.saoPass.params.saoBias = 4.1;
|
||||
this.saoPass.params.saoIntensity = 1.0;
|
||||
this.saoPass.params.saoScale = 32.0;
|
||||
this.saoPass.params.saoKernelRadius = 5.0;
|
||||
this.saoPass.params.saoMinResolution = 0.0;
|
||||
// this.composer.addPass(this.outlinePass);
|
||||
|
||||
this.bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(width, height),
|
||||
@@ -1174,14 +1179,16 @@ class Editor {
|
||||
);
|
||||
this.bloomPass.strength = 0.25;
|
||||
|
||||
this.smaaPass = new SMAAPass(
|
||||
width * this.renderer.getPixelRatio(),
|
||||
height * this.renderer.getPixelRatio(),
|
||||
);
|
||||
// this.smaaPass = new SMAAPass(
|
||||
// width * this.renderer.getPixelRatio(),
|
||||
// height * this.renderer.getPixelRatio(),
|
||||
// );
|
||||
|
||||
this.composer.addPass(this.saoPass);
|
||||
this.composer.addPass(this.bloomPass);
|
||||
//this.composer.addPass(this.smaaPass);
|
||||
// this.composer.addPass(this.bloomPass);
|
||||
// this.composer.addPass(this.smaaPass);
|
||||
|
||||
// this.ssaoPass = new SSAOPass(this.activeScene.scene, this.camera, width, height);
|
||||
// this.composer.addPass(this.ssaoPass);
|
||||
|
||||
this.outputPass = new OutputPass();
|
||||
this.composer.addPass(this.outputPass);
|
||||
@@ -1328,7 +1335,10 @@ class Editor {
|
||||
this.rawRenderer &&
|
||||
this.render_composer
|
||||
) {
|
||||
const then = performance.now();
|
||||
this.composer.render();
|
||||
const now = performance.now();
|
||||
console.log("Performance: ", now - then);
|
||||
//this.rawRenderer.render(this.activeScene.scene, this.render_camera!);
|
||||
// this.render_composer.render();
|
||||
} else if (this.renderer && this.render_camera && !this.rendering) {
|
||||
@@ -1337,7 +1347,7 @@ class Editor {
|
||||
} else if (this.rendering && this.renderer) {
|
||||
this.renderer.setSize(this.render_width, this.render_height);
|
||||
}
|
||||
await this.recordScene();
|
||||
// await this.recordScene();
|
||||
}
|
||||
|
||||
async useCachedMediaTokens(): Promise<boolean> {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Vector3 } from "three";
|
||||
|
||||
import { filmLength, outlinerState } from "../signals";
|
||||
import { CharacterAnimationEngine } from "./Engines/CharacterAnimationEngine";
|
||||
import { SplatMesh } from "@sparkjsdev/spark";
|
||||
|
||||
export class TimeLine {
|
||||
editorEngine: Editor;
|
||||
@@ -395,6 +396,18 @@ export class TimeLine {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// This method is for local dev testing of objects
|
||||
// and bypasses the queue system, that's kinda redundant anyway
|
||||
public addLocalSplat(fileBytes: ArrayBuffer | Uint8Array, flipVertical: boolean = false) {
|
||||
const splatMesh = new SplatMesh({ fileBytes: fileBytes });
|
||||
|
||||
if (flipVertical) {
|
||||
splatMesh.rotateX(Math.PI);
|
||||
}
|
||||
|
||||
this.editorEngine.sceneManager?.scene.scene.add(splatMesh);
|
||||
}
|
||||
|
||||
public async addShape(data: MediaItem) {
|
||||
const pos = this.getPos();
|
||||
const parim = await this.editorEngine.create_parim(data.media_id, pos);
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { UploadModalImage } from "../../../../components/reusable/UploadModalImage";
|
||||
import { UploadModalSplat } from "~/components/reusable/UploadModalSplat";
|
||||
|
||||
export const Controls3D = () => {
|
||||
useSignals();
|
||||
@@ -46,6 +47,7 @@ export const Controls3D = () => {
|
||||
const [upload3DIsShowing, setUpload3DIsShowing] = useState(false);
|
||||
const [isAddAssetPopoverOpen, setIsAddAssetPopoverOpen] = useState(false);
|
||||
const [uploadImageIsShowing, setUploadImageIsShowing] = useState(false);
|
||||
const [uploadSplatIsShowing, setUploadSplatIsShowing] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,7 +59,8 @@ export const Controls3D = () => {
|
||||
!galleryModalVisibleViewMode.value &&
|
||||
!isAddAssetPopoverOpen &&
|
||||
!upload3DIsShowing &&
|
||||
!uploadImageIsShowing;
|
||||
!uploadImageIsShowing &&
|
||||
!uploadSplatIsShowing;
|
||||
|
||||
setShowEmptySceneTooltip(isSceneEmpty);
|
||||
};
|
||||
@@ -78,6 +81,7 @@ export const Controls3D = () => {
|
||||
isAddAssetPopoverOpen,
|
||||
upload3DIsShowing,
|
||||
uploadImageIsShowing,
|
||||
uploadSplatIsShowing,
|
||||
]);
|
||||
|
||||
const handleModeChange = (value: string) => {
|
||||
@@ -170,6 +174,9 @@ export const Controls3D = () => {
|
||||
case "upload-image":
|
||||
setUploadImageIsShowing(true);
|
||||
break;
|
||||
case "upload-splat":
|
||||
setUploadSplatIsShowing(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -268,14 +275,24 @@ export const Controls3D = () => {
|
||||
),
|
||||
action: "upload-image",
|
||||
},
|
||||
{
|
||||
label: "Upload Splat",
|
||||
selected: false,
|
||||
icon: (
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpFromBracket}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
),
|
||||
action: "upload-splat",
|
||||
},
|
||||
]}
|
||||
onPanelAction={handleAddAssetAction}
|
||||
showIconsInList
|
||||
buttonClassName={`h-9 w-9 rounded-[10px] text-lg ${
|
||||
showEmptySceneTooltip
|
||||
? "bg-primary/90 hover:bg-primary/70"
|
||||
: "border-transparent bg-primary/90 hover:bg-primary/70"
|
||||
}`}
|
||||
buttonClassName={`h-9 w-9 rounded-[10px] text-lg ${showEmptySceneTooltip
|
||||
? "bg-primary/90 hover:bg-primary/70"
|
||||
: "border-transparent bg-primary/90 hover:bg-primary/70"
|
||||
}`}
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
}
|
||||
@@ -331,6 +348,17 @@ export const Controls3D = () => {
|
||||
title="Upload an Image"
|
||||
titleIcon={faImages}
|
||||
/>
|
||||
|
||||
<UploadModalSplat
|
||||
isOpen={uploadSplatIsShowing}
|
||||
onClose={() => setUploadSplatIsShowing(false)}
|
||||
onSuccess={(buffer, shouldFlip) => {
|
||||
setUploadSplatIsShowing(false)
|
||||
editorEngine?.timeline.addLocalSplat(buffer, shouldFlip);
|
||||
}}
|
||||
title="Upload an spz file"
|
||||
titleIcon={faCube}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
"uuid": "^9.0.1",
|
||||
"wavesurfer.js": "^7.7.11",
|
||||
"webm-writer": "^1.0.0"
|
||||
|
||||
@@ -1,25 +1,65 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import path from 'path'
|
||||
import { resolve } from 'node:path'
|
||||
import path from "path";
|
||||
import { resolve } from "node:path";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
// NB(bt): This configuration file can specify bundler rollup options, compiler plugins,
|
||||
// import path resolution, dev server HTTP headers, CORS options, path rewriting, etc.
|
||||
// Read the vite docs for more: https://vitejs.dev/config/
|
||||
|
||||
const SPARK_MODULE_SUBPATH = "/@sparkjsdev/spark/dist/spark.module.js";
|
||||
const SPARK_WASM_PATTERN =
|
||||
/module_or_path = new URL\("(data:application\/wasm;base64,[^"]+)", import\.meta\.url\);/;
|
||||
|
||||
const sparkWasmDataUrlFix = (): Plugin => ({
|
||||
name: "spark-wasm-data-url-fix",
|
||||
enforce: "pre",
|
||||
apply: "serve",
|
||||
transform(code, id) {
|
||||
if (!id.includes(SPARK_MODULE_SUBPATH)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = code.match(SPARK_WASM_PATTERN);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, dataUrl] = match;
|
||||
const patched = code.replace(
|
||||
SPARK_WASM_PATTERN,
|
||||
`module_or_path = "${dataUrl}";`,
|
||||
);
|
||||
|
||||
return {
|
||||
code: patched,
|
||||
map: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const projectRoot = __dirname;
|
||||
const appRoot = path.resolve(projectRoot, "app");
|
||||
const workspaceRoot = path.resolve(projectRoot, "..", "..");
|
||||
|
||||
export default defineConfig({
|
||||
root: path.resolve(__dirname, 'app'),
|
||||
root: appRoot,
|
||||
optimizeDeps: {
|
||||
exclude: ["@sparkjsdev/spark"],
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, 'dist'),
|
||||
outDir: path.resolve(projectRoot, "dist"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'app/index.html'),
|
||||
index: resolve(projectRoot, "app/index.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
],
|
||||
plugins: [sparkWasmDataUrlFix(), tsconfigPaths(), wasm()],
|
||||
server: {
|
||||
}
|
||||
fs: {
|
||||
allow: [workspaceRoot],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+498
-13
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
"@sparkjsdev/spark": "^0.1.10",
|
||||
"dayjs": "^1.11.10",
|
||||
"detect-gpu": "^5.0.38",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
@@ -36,7 +37,7 @@
|
||||
"react-router-dom": "6.29.0",
|
||||
"react-spring": "^10.0.1",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"three": "^0.162.0",
|
||||
"three": "^0.171.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
@@ -93,6 +94,8 @@
|
||||
"vite-plugin-banner": "^0.8.0",
|
||||
"vite-plugin-dts": "~4.5.0",
|
||||
"vite-plugin-static-copy": "^2.3.1",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user