feat(nds): improve 3d model and texture display

This commit is contained in:
2026-01-08 13:10:26 +01:00
parent 8aa6ab4fd0
commit fc506495c9
24 changed files with 2027 additions and 1757 deletions

View File

@@ -2,6 +2,7 @@
import { useLoop, useTresContext } from "@tresjs/core";
import * as THREE from "three";
import gsap from "gsap";
import { LOGICAL_WIDTH, LOGICAL_HEIGHT } from "~/utils/screen";
const INTRO_ANIMATION = {
MODEL_SPIN_DURATION: 3,
@@ -26,25 +27,25 @@ const props = defineProps<{
const { assets } = useAssets();
const app = useAppStore();
const model = assets.nintendoDs.scene.clone(true);
const model = assets.nitendoDs.model.clone(true);
let topScreenTexture: THREE.CanvasTexture | null = null;
let bottomScreenTexture: THREE.CanvasTexture | null = null;
/// meshes ///
// screens
const TOP_SCREEN = "Object_9";
const BOTTOM_SCREEN = "Object_28";
const LID = "Object_8";
const TOP_SCREEN = "top_screen";
const BOTTOM_SCREEN = "bottom_screen";
const LID = "lid";
// buttons
const CROSS_BUTTON = "Object_21";
const X_BUTTON = "Object_6";
const A_BUTTON = "Object_32";
const Y_BUTTON = "Object_4";
const B_BUTTON = "Object_30";
const SELECT_BUTTON = "Object_17";
const START_BUTTON = "Object_11";
const CROSS_BUTTON = "button_pad";
const X_BUTTON = "button_x";
const A_BUTTON = "button_a";
const Y_BUTTON = "button_y";
const B_BUTTON = "button_b";
const SELECT_BUTTON = "button_select";
const START_BUTTON = "button_start";
const meshes = new Map<string, THREE.Mesh>();
@@ -163,21 +164,25 @@ watch(
() => {
if (!props.topScreenCanvas || !props.bottomScreenCanvas) return;
topScreenTexture = new THREE.CanvasTexture(props.topScreenCanvas);
topScreenTexture.minFilter = THREE.NearestFilter;
topScreenTexture.magFilter = THREE.NearestFilter;
topScreenTexture.colorSpace = THREE.SRGBColorSpace;
topScreenTexture.flipY = false;
topScreenTexture.repeat.set(1, 1024 / 404);
topScreenTexture.offset.set(0, -4 / 1024);
const webglRenderer = renderer.instance;
if (!(webglRenderer instanceof THREE.WebGLRenderer)) return;
bottomScreenTexture = new THREE.CanvasTexture(props.bottomScreenCanvas);
bottomScreenTexture.minFilter = THREE.NearestFilter;
bottomScreenTexture.magFilter = THREE.NearestFilter;
bottomScreenTexture.colorSpace = THREE.SRGBColorSpace;
bottomScreenTexture.flipY = false;
bottomScreenTexture.repeat.set(1, 1024 / 532);
bottomScreenTexture.offset.set(0, -1024 / 532 + 1);
const createScreenTexture = (
canvas: HTMLCanvasElement,
): THREE.CanvasTexture => {
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.colorSpace = THREE.SRGBColorSpace;
texture.wrapS = THREE.RepeatWrapping;
texture.repeat.x = -1;
texture.anisotropy = webglRenderer.capabilities.getMaxAnisotropy();
texture.generateMipmaps = false;
return texture;
};
topScreenTexture = createScreenTexture(props.topScreenCanvas);
bottomScreenTexture = createScreenTexture(props.bottomScreenCanvas);
requireMesh(TOP_SCREEN).material = new THREE.MeshStandardMaterial({
map: topScreenTexture,
@@ -314,7 +319,6 @@ const handleClick = (event: MouseEvent) => {
switch (intersection.object.name) {
case TOP_SCREEN:
case BOTTOM_SCREEN: {
console.log(intersection);
const canvas =
intersection.object.name === TOP_SCREEN
? props.topScreenCanvas
@@ -322,22 +326,16 @@ const handleClick = (event: MouseEvent) => {
if (!canvas) break;
const x = Math.floor(intersection.uv.x * 256);
let y: number;
if (intersection.object.name === TOP_SCREEN) {
y = Math.floor(intersection.uv.y * (1024 / 404) * 192);
} else {
y = Math.floor(192 - (1 - intersection.uv.y) * (1024 / 532) * 192);
}
const logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;
const rect = canvas.getBoundingClientRect();
canvas.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: (x / 256) * rect.width + rect.left,
clientY: (y / 192) * rect.height + rect.top,
clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
clientY: (logicalY / LOGICAL_HEIGHT) * rect.height + rect.top,
}),
);

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import { LOGICAL_WIDTH, LOGICAL_HEIGHT, SCREEN_SCALE } from "~/utils/screen";
const canvas = useTemplateRef("canvas");
const renderCallbacks = new Map<RenderCallback, number>();
@@ -31,8 +33,8 @@ const handleCanvasClick = (event: MouseEvent) => {
if (!canvas.value) return;
const rect = canvas.value.getBoundingClientRect();
const scaleX = SCREEN_WIDTH / rect.width;
const scaleY = SCREEN_HEIGHT / rect.height;
const scaleX = LOGICAL_WIDTH / rect.width;
const scaleY = LOGICAL_HEIGHT / rect.height;
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
@@ -59,6 +61,9 @@ const renderFrame = (timestamp: number) => {
// render
ctx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
ctx.save();
ctx.scale(SCREEN_SCALE, SCREEN_SCALE);
const sortedCallbacks = Array.from(renderCallbacks.entries())
.sort((a, b) => a[1] - b[1])
.map(([callback]) => callback);
@@ -71,6 +76,8 @@ const renderFrame = (timestamp: number) => {
ctx.restore();
}
ctx.restore();
lastRealFrameTime = Date.now() - start;
animationFrameId = requestAnimationFrame(renderFrame);
@@ -86,6 +93,8 @@ onMounted(() => {
ctx = canvas.value.getContext("2d");
if (!ctx) throw new Error("Missing 2d context");
ctx.imageSmoothingEnabled = false;
canvas.value.addEventListener("click", handleCanvasClick);
canvas.value.addEventListener("wheel", handleCanvasWheel, { passive: true });

View File

@@ -1,2 +1,7 @@
export const SCREEN_WIDTH = 256;
export const SCREEN_HEIGHT = 192;
export const LOGICAL_WIDTH = 256;
export const LOGICAL_HEIGHT = 192;
export const SCREEN_SCALE = 2;
export const SCREEN_WIDTH = LOGICAL_WIDTH * SCREEN_SCALE;
export const SCREEN_HEIGHT = LOGICAL_HEIGHT * SCREEN_SCALE;

View File

@@ -1,11 +0,0 @@
Model Information:
* title: Nintendo DS
* source: https://sketchfab.com/3d-models/nintendo-ds-934fdde060874d5c99b40cdf1dbb37f2
* author: solal.sblt (https://sketchfab.com/Solal.Sebillotte)
Model License:
* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
* requirements: Author must be credited. Commercial use is allowed.
If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
This work is based on "Nintendo DS" (https://sketchfab.com/3d-models/nintendo-ds-934fdde060874d5c99b40cdf1dbb37f2) by solal.sblt (https://sketchfab.com/Solal.Sebillotte) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

Before

Width:  |  Height:  |  Size: 534 KiB

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 1000 KiB

After

Width:  |  Height:  |  Size: 1000 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB