feat: 3d nds model

This commit is contained in:
2025-12-13 23:45:50 +01:00
parent 464277e58f
commit fd8f0ea86a
24 changed files with 10828 additions and 3591 deletions

85
app/components/NDS.vue Normal file
View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { useLoop } from "@tresjs/core";
import * as THREE from "three";
const props = defineProps<{
topScreenCanvas: HTMLCanvasElement | null;
bottomScreenCanvas: HTMLCanvasElement | null;
}>();
const { state: model } = useLoader(
GLTFLoader,
"/models/nintendo-ds/scene.gltf",
);
const scene = computed(() => model.value?.scene);
let topScreenTexture: THREE.CanvasTexture | null = null;
let bottomScreenTexture: THREE.CanvasTexture | null = null;
watch(
() => [props.topScreenCanvas, props.bottomScreenCanvas],
() => {
if (!props.topScreenCanvas || !props.bottomScreenCanvas) return;
topScreenTexture = new THREE.CanvasTexture(props.topScreenCanvas);
topScreenTexture.minFilter = THREE.NearestFilter;
topScreenTexture.magFilter = THREE.NearestFilter;
topScreenTexture.flipY = false;
bottomScreenTexture = new THREE.CanvasTexture(props.bottomScreenCanvas);
bottomScreenTexture.minFilter = THREE.NearestFilter;
bottomScreenTexture.magFilter = THREE.NearestFilter;
bottomScreenTexture.flipY = false;
},
{ immediate: true },
);
watch(scene, () => {
if (!scene.value) return;
scene.value.traverse((child) => {
if (child instanceof THREE.Mesh) {
const material = child.material as THREE.Material;
if (material.name?.includes("screen_up") && topScreenTexture) {
topScreenTexture.repeat.set(1, 1024 / 404);
topScreenTexture.offset.set(0, -4 / 1024);
child.material = new THREE.MeshStandardMaterial({
map: topScreenTexture,
emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5,
});
} else if (
material.name?.includes("screen_down") &&
bottomScreenTexture
) {
bottomScreenTexture.repeat.set(1, 1024 / 532);
bottomScreenTexture.offset.set(0, -1024 / 532 + 1);
child.material = new THREE.MeshStandardMaterial({
map: bottomScreenTexture,
emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5,
});
}
}
});
});
const { onRender } = useLoop();
onRender(() => {
if (topScreenTexture) topScreenTexture.needsUpdate = true;
if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true;
});
onUnmounted(() => {
topScreenTexture?.dispose();
bottomScreenTexture?.dispose();
});
</script>
<template>
<primitive v-if="scene" :object="scene" />
</template>

View File

@@ -110,6 +110,10 @@ onUnmounted(() => {
canvas.value.removeEventListener("wheel", handleCanvasWheel);
}
});
defineExpose({
canvas,
});
</script>
<template>

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
const showStats = useState("showStats", () => false);
</script>
<template>
<div
:style="{
display: 'flex',
flexDirection: 'column',
gap: '4px',
}"
>
<div :style="{ display: 'flex', alignItems: 'center', gap: '4px' }">
<input id="statsCheckbox" v-model="showStats" type="checkbox" />
<label for="statsCheckbox">Stats</label>
</div>
<div>
<Screen>
<slot name="top">
<slot />
</slot>
<Stats v-if="showStats" />
</Screen>
</div>
<div>
<Screen>
<slot name="bottom">
<slot />
</slot>
<Stats v-if="showStats" />
</Screen>
</div>
</div>
</template>

View File

@@ -1,26 +1,40 @@
<script setup lang="ts">
definePageMeta({
layout: false,
});
import { OrbitControls } from "@tresjs/cientos";
const route = useRoute();
const screen = computed(() => route.query.screen as string | undefined);
const topScreen = useTemplateRef("topScreen");
const bottomScreen = useTemplateRef("bottomScreen");
</script>
<template>
<NuxtLayout name="default">
<template #top>
<div>
<TresCanvas window-size clear-color="#181818">
<TresPerspectiveCamera :args="[45, 1, 0.001, 1000]" />
<OrbitControls />
<TresAmbientLight />
<TresDirectionalLight />
<NDS
v-if="topScreen && bottomScreen"
:top-screen-canvas="topScreen.canvas"
:bottom-screen-canvas="bottomScreen.canvas"
/>
</TresCanvas>
<Screen ref="topScreen">
<HomeTopScreen v-if="!screen" />
<ContactTopScreen v-else-if="screen === 'contact'" />
<ProjectsTopScreen v-else-if="screen === 'projects'" />
<SettingsTopScreen v-else-if="screen === 'settings'" />
</template>
<template #bottom>
</Screen>
<Screen ref="bottomScreen">
<HomeBottomScreen v-if="!screen" />
<ContactBottomScreen v-else-if="screen === 'contact'" />
<ProjectsBottomScreen v-else-if="screen === 'projects'" />
<SettingsBottomScreen v-else-if="screen === 'settings'" />
</template>
</NuxtLayout>
</Screen>
</div>
</template>

View File

@@ -8,6 +8,7 @@ export default defineNuxtConfig({
"@pinia/nuxt",
"./modules/content-assets",
"@nuxtjs/i18n",
"@tresjs/nuxt",
],
css: ["~/assets/app.css"],
ssr: false,

View File

@@ -12,8 +12,12 @@
"lint": "eslint --fix --cache ."
},
"dependencies": {
"@tresjs/cientos": "^5.1.2",
"@tresjs/core": "^5.2.0",
"@tresjs/nuxt": "^5.1.2",
"gsap": "3.13.0",
"pinia": "3.0.4",
"three": "^0.182.0",
"vue": "3.5.25",
"vue-router": "4.6.3"
},
@@ -25,6 +29,7 @@
"@nuxtjs/mdc": "0.18.4",
"@pinia/nuxt": "0.11.3",
"@types/node": "24.10.1",
"@types/three": "^0.182.0",
"better-sqlite3": "12.4.1",
"eslint": "9.39.1",
"nuxt": "4.2.1",

12185
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,3 +3,4 @@ onlyBuiltDependencies:
- better-sqlite3
- esbuild
- unrs-resolver
- vue-demi

View File

@@ -0,0 +1,11 @@
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/)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB