feat: nds render system

This commit is contained in:
2025-11-09 16:20:30 +01:00
parent d39ca6ea9b
commit f70a8868dd
31 changed files with 6017 additions and 2898 deletions

View File

@@ -1,10 +0,0 @@
body {
margin: 0;
width: 100vw;
height: 100vh;
}
#root {
width: 100%;
height: 100%;
}

View File

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

View File

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

View File

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

View File

@@ -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>,
);

View File

@@ -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
View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />