feat: nds render system
This commit is contained in:
10
src/App.css
10
src/App.css
@@ -1,10 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
125
src/App.tsx
125
src/App.tsx
@@ -1,125 +0,0 @@
|
||||
// TODO: handle moving the camera vs actual click
|
||||
|
||||
import { useRef, useState, type ComponentRef } from "react";
|
||||
import {
|
||||
Canvas,
|
||||
useFrame,
|
||||
useThree,
|
||||
type ThreeEvent,
|
||||
useLoader,
|
||||
} from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { GLTFLoader } from "three/examples/jsm/Addons.js";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
import "./App.css";
|
||||
import { state, anim } from "./utils/animator";
|
||||
import { useAnimator } from "./hooks/useAnimator";
|
||||
|
||||
const Room = () => {
|
||||
const controls = useRef<ComponentRef<typeof OrbitControls>>(null);
|
||||
const room = useLoader(GLTFLoader, "/room.glb");
|
||||
|
||||
const windowAnimator = useAnimator(
|
||||
room.animations,
|
||||
[
|
||||
state(anim("Cube.010Action", false), anim("ShadeHandleAction", false)),
|
||||
state(anim("LowerWindowAction", false)),
|
||||
state(anim("LowerWindowAction", true)),
|
||||
state(anim("Cube.010Action", true), anim("Cube.010Action", true)),
|
||||
],
|
||||
room.scene,
|
||||
);
|
||||
|
||||
const [focus, setFocus] = useState<string | null>(null);
|
||||
|
||||
const cameraPosition = useRef<Vector3 | null>(null);
|
||||
const cameraTarget = useRef<Vector3>(new Vector3(0, 1, 0));
|
||||
|
||||
const pointerDownCameraPos = useRef(new Vector3());
|
||||
|
||||
const { camera } = useThree();
|
||||
|
||||
useFrame(() => {
|
||||
if (!controls.current) return;
|
||||
|
||||
if (cameraPosition.current) {
|
||||
camera.position.lerp(cameraPosition.current, 0.09);
|
||||
if (camera.position.distanceTo(cameraPosition.current) < 0.012) {
|
||||
if (!focus) {
|
||||
controls.current.enableZoom = true;
|
||||
controls.current.enableRotate = true;
|
||||
}
|
||||
|
||||
cameraPosition.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
controls.current.target.lerp(cameraTarget.current, 0.1);
|
||||
});
|
||||
|
||||
const handlePointerUp = (e: ThreeEvent<MouseEvent>) => {
|
||||
if (!controls.current) return;
|
||||
if (pointerDownCameraPos.current.distanceTo(camera.position) >= 0.05)
|
||||
return;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.object.name === "WindowClickTarget") {
|
||||
windowAnimator.play();
|
||||
}
|
||||
|
||||
if (e.object.name.includes("Poster") && e.object.name !== focus) {
|
||||
const objectPos = new Vector3();
|
||||
const objectDir = new Vector3();
|
||||
e.object.getWorldPosition(objectPos);
|
||||
e.object.getWorldDirection(objectDir);
|
||||
|
||||
cameraPosition.current = objectPos
|
||||
.clone()
|
||||
.add(objectDir.multiplyScalar(-0.65));
|
||||
cameraTarget.current = objectPos;
|
||||
|
||||
controls.current.enableZoom = false;
|
||||
controls.current.enableRotate = false;
|
||||
|
||||
setFocus(e.object.name);
|
||||
} else if (focus) {
|
||||
cameraPosition.current = new Vector3(3, 3, -3);
|
||||
cameraTarget.current = new Vector3(0, 1, 0);
|
||||
|
||||
setFocus(null);
|
||||
}
|
||||
|
||||
controls.current.update();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={Math.PI / 2} color={"#ffffff"} />
|
||||
<directionalLight intensity={5} position={[5, 10, 5]} castShadow />
|
||||
<OrbitControls
|
||||
ref={controls}
|
||||
target={[0, 1, 0]}
|
||||
minPolarAngle={Math.PI / 5}
|
||||
maxPolarAngle={Math.PI / 2}
|
||||
enablePan={false}
|
||||
/>
|
||||
|
||||
<mesh
|
||||
onPointerDown={() => pointerDownCameraPos.current.copy(camera.position)}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<primitive object={room.scene} />
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Canvas camera={{ position: [3, 3, -3] }}>
|
||||
<Room />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useAnimations } from "@react-three/drei";
|
||||
import { LoopOnce, type AnimationClip } from "three";
|
||||
import type { AnimationAction, Object3D } from "three";
|
||||
import type { Animation, AnimatorState } from "../utils/animator";
|
||||
import { useState } from "react";
|
||||
|
||||
// NOTE: maybe root is not necessary
|
||||
|
||||
export const useAnimator = (
|
||||
clips: AnimationClip[],
|
||||
states: AnimatorState[],
|
||||
root?: React.RefObject<Object3D | undefined | null> | Object3D,
|
||||
) => {
|
||||
const [stateIndex, setStateIndex] = useState(0);
|
||||
const { mixer } = useAnimations(clips, root);
|
||||
|
||||
const getAction = (name: string): AnimationAction => {
|
||||
const clip = clips.find((x) => x.name === name);
|
||||
if (!clip) throw `Animation not found: '${name}'`;
|
||||
return mixer.clipAction(clip);
|
||||
};
|
||||
|
||||
const playAnimation = (animation: Animation): void => {
|
||||
const action = getAction(animation.name);
|
||||
|
||||
action.clampWhenFinished = true;
|
||||
action.setLoop(LoopOnce, 1);
|
||||
action.timeScale = (animation.reverse ? -1 : 1) * 1.5;
|
||||
if (animation.reverse) {
|
||||
action.paused = false;
|
||||
action.time = action.getClip().duration;
|
||||
} else {
|
||||
action.reset();
|
||||
}
|
||||
action.play();
|
||||
};
|
||||
|
||||
return {
|
||||
play() {
|
||||
if (this.isPlaying()) return;
|
||||
for (const animation of states[stateIndex]) {
|
||||
playAnimation(animation);
|
||||
}
|
||||
|
||||
setStateIndex((stateIndex + 1) % states.length);
|
||||
},
|
||||
|
||||
isPlaying(): boolean {
|
||||
for (const state of states) {
|
||||
for (const animation of state) {
|
||||
const action = getAction(animation.name);
|
||||
if (action.isRunning()) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode } from "react";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { AnimationAction } from "three";
|
||||
|
||||
export type Animation = {
|
||||
name: string;
|
||||
reverse: boolean;
|
||||
};
|
||||
|
||||
export type AnimatorState = Animation[];
|
||||
|
||||
export type Animator = {
|
||||
actions: Map<string, AnimationAction>;
|
||||
states: AnimatorState[];
|
||||
};
|
||||
|
||||
export const state = (...animations: Animation[]): Animation[] => animations;
|
||||
|
||||
export const anim = (name: string, reverse: boolean): Animation => ({
|
||||
name,
|
||||
reverse,
|
||||
});
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user