refactor: nds as a 3d object

This commit is contained in:
2025-12-13 17:28:23 +01:00
parent 8d45b76944
commit 1a82f3c8d0
4 changed files with 76 additions and 82 deletions

View File

@@ -7,7 +7,6 @@
</head>
<body>
<div id="app"></div>
<div id="coord-display"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,8 +1,8 @@
import "./style.css";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { NDS } from "./nds";
// initialize scene
const scene = new THREE.Scene();
@@ -27,28 +27,8 @@ const handleResize = () => {
window.addEventListener("resize", handleResize, false);
handleResize();
// load nds model
const loader = new GLTFLoader();
const nds = await loader.loadAsync("/nintendo-ds/scene.gltf");
nds.scene.scale.set(50, 50, 50);
scene.add(nds.scene);
let topScreenMesh: THREE.Mesh | undefined = undefined;
let bottomScreenMesh: THREE.Mesh | undefined = undefined;
nds.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
const material = child.material as THREE.Material;
if (material.name?.includes("screen_up")) {
topScreenMesh = child;
} else if (material.name?.includes("screen_down")) {
bottomScreenMesh = child;
}
}
});
if (!topScreenMesh) throw new Error(`Top screen mesh not found`);
if (!bottomScreenMesh) throw new Error(`Bottom screen mesh not found`);
const nds = new NDS(camera, renderer.domElement);
scene.add(nds);
// create light
const color = 0xffffff;
@@ -57,54 +37,10 @@ const ambient = new THREE.AmbientLight(color, intensity);
scene.add(ambient);
const directional = new THREE.DirectionalLight(color, intensity);
directional.target = nds.scene;
directional.target = nds;
directional.position.set(0, 100, 0);
scene.add(directional);
const coordDisplay = document.getElementById("coord-display")!;
renderer.domElement.addEventListener("mousemove", (event) => {
const rect = renderer.domElement.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(
new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1,
),
camera,
);
const intersects = raycaster.intersectObjects([
topScreenMesh!,
bottomScreenMesh!,
]);
if (intersects.length > 0) {
const intersection = intersects[0];
const mesh = intersection.object as THREE.Mesh;
const uv = intersection.uv;
if (uv) {
coordDisplay.hidden = false;
const x = Math.floor(uv.x * 256);
const y =
mesh === topScreenMesh
? Math.floor(uv.y * (1024 / 404) * 192)
: // invert coords only for bottom screen
Math.floor(192 - (1 - uv.y) * (1024 / 532) * 192);
const screenName = mesh === topScreenMesh ? "top" : "bottom";
coordDisplay.textContent = `${screenName} | pos: (${x}, ${y})`;
} else {
coordDisplay.hidden = true;
}
} else {
coordDisplay.hidden = true;
}
});
// main loop
renderer.setAnimationLoop(() => {
controls.update();

72
src/nds.ts Normal file
View File

@@ -0,0 +1,72 @@
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
export class NDS extends THREE.Object3D {
topScreenMesh: THREE.Mesh | undefined;
bottomScreenMesh: THREE.Mesh | undefined;
public constructor(camera: THREE.Camera, domElement: HTMLCanvasElement) {
super();
const loader = new GLTFLoader();
// load model
loader.load("/nintendo-ds/scene.gltf", ({ scene: model }) => {
model.scale.set(50, 50, 50);
// find top and bottom screens
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
const material = child.material as THREE.Material;
if (material.name?.includes("screen_up")) {
this.topScreenMesh = child;
} else if (material.name?.includes("screen_down")) {
this.bottomScreenMesh = child;
}
}
});
if (!this.topScreenMesh) throw new Error(`Missing top screen`);
if (!this.bottomScreenMesh) throw new Error(`Missing bottom screen`);
const { topScreenMesh, bottomScreenMesh } = this;
domElement.addEventListener("mousemove", (event) => {
const rect = domElement.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(
new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1,
),
camera,
);
const intersects = raycaster.intersectObjects([
topScreenMesh,
bottomScreenMesh,
]);
if (intersects.length > 0) {
const intersection = intersects[0];
const mesh = intersection.object as THREE.Mesh;
const uv = intersection.uv;
if (uv) {
const x = Math.floor(uv.x * 256);
const y =
mesh === topScreenMesh
? Math.floor(uv.y * (1024 / 404) * 192)
: // invert coords only for bottom screen
Math.floor(192 - (1 - uv.y) * (1024 / 532) * 192);
x;
y;
}
}
});
super.add(model);
});
}
}

View File

@@ -1,16 +1,3 @@
body {
margin: 0;
}
#coord-display {
position: fixed;
top: 5px;
left: 5px;
color: white;
font-family: monospace;
font-size: 16px;
background-color: #000000;
padding: 10px;
pointer-events: none;
z-index: 1000;
}