蘇美 发表于 2024-9-1 20:54:02

how to create basic 360 landing using three.js, nuxt.js and vue3


大家好,我喜歡分享如何使用 Three.js、Nuxt.js 和 Vue 3 創建基本的 360 度著陸頁。


安裝新的 Nuxt.js 項目
npx nuxi@latest init simple-app
cd simple-app
npm i


安裝 Three.js
npm i three -D


現在我們準備好了,讓我們為 360 度著陸頁建立一個新元件。
npx nuxi add component Landing


現在我們將像這樣建立元件內容。
<script setup lang="ts">
import {
PerspectiveCamera,
Scene,
SphereGeometry,
TextureLoader,
MeshBasicMaterial,
Mesh,
WebGLRenderer,
MathUtils,
} from "three";

const container = ref()

const props = defineProps({
    panoramaUrl:{
      type: String,
      default: "img/hdri.webp"
    },
});

let camera, scene, renderer;
let isUserInteracting = false,
    onPointerDownMouseX = 0, onPointerDownMouseY = 0,
    lon = 0, onPointerDownLon = 0,
    lat = 0, onPointerDownLat = 0,
    phi = 0, theta = 0;

camera = new PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1100);
scene = new Scene();
const geometry = new SphereGeometry(1000, 100, 100);
const reader = new FileReader();

const onWindowResize= () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

const onPointerDown = (event) => {
    if (event.isPrimary === false) return;
    isUserInteracting = true;
    onPointerDownMouseX = event.clientX;
    onPointerDownMouseY = event.clientY;
    onPointerDownLon = lon;
    onPointerDownLat = lat;
    document.addEventListener('pointermove', onPointerMove);
    document.addEventListener('pointerup', onPointerUp);
}

const onPointerMove = (event) => {
    if (event.isPrimary === false) return;
    lon = (onPointerDownMouseX - event.clientX) * 0.1 + onPointerDownLon;
    lat = (event.clientY - onPointerDownMouseY) * 0.1 + onPointerDownLat;
}

const onPointerUp = () => {
    if (event.isPrimary === false) return;
    isUserInteracting = false;
    document.removeEventListener('pointermove', onPointerMove);
    document.removeEventListener('pointerup', onPointerUp);
}

const onDocumentMouseWheel = (event) => {
    const fov = camera.fov + event.deltaY * 0.05;
    camera.fov = MathUtils.clamp(fov, 10, 75);
    camera.updateProjectionMatrix();
}

const animate = () => {
    requestAnimationFrame(animate);
    update();
}

const update = () => {
    if (isUserInteracting === false) {
      lon += 0.1;
    }

    lat = Math.max(- 85, Math.min(85, lat));
    phi = MathUtils.degToRad(90 - lat);
    theta = MathUtils.degToRad(lon);
    const x = 500 * Math.sin(phi) * Math.cos(theta);
    const y = 500 * Math.cos(phi);
    const z = 500 * Math.sin(phi) * Math.sin(theta);
    camera.lookAt(x, y, z);
    renderer.render(scene, camera);
}

const dragover = (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy';
}

const dragenter = () => {
    document.body.style.opacity = 0.5;
}

const dragleave = () => {
    document.body.style.opacity = 1;
}

const load1 = (event) => {
    material.map.image.src = event.target.result;
    material.map.needsUpdate = true;
}

const drop = () => {
    event.preventDefault();
    reader.addEventListener('load', load1);
    reader.readAsDataURL(event.dataTransfer.files);
    document.body.style.opacity = 1;
}

onMounted(() => {
      init();
      animate();

      function init() {
            // invert the geometry on the x-axis so that all of the faces point inward
            geometry.scale(- 1, 1, 1);
            const texture = new TextureLoader().load(props.panoramaUrl);
            const material = new MeshBasicMaterial({ map: texture });
            const mesh = new Mesh(geometry, material);
            scene.add(mesh);
            renderer = new WebGLRenderer();
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            container.value.appendChild(renderer.domElement);
            container.value.style.touchAction = 'none';
            container.value.addEventListener('pointerdown', onPointerDown);
            document.addEventListener('wheel', onDocumentMouseWheel);
            document.addEventListener('dragover', dragover);
            document.addEventListener('dragenter', dragenter);
            document.addEventListener('dragleave', dragleave);
            document.addEventListener('drop', drop);
            window.addEventListener('resize', onWindowResize);
      }
});

onUnmounted(() => {
    reader.removeEventListener('load', load1);
    container.value.replaceWith(container.value.cloneNode(true));
    document.removeEventListener('wheel', onDocumentMouseWheel);
    document.removeEventListener('dragover', dragover);
    document.removeEventListener('dragenter', dragenter);
    document.removeEventListener('dragleave', dragleave);
    document.removeEventListener('drop', drop);
    window.removeEventListener('resize', onWindowResize);
});
</script>

<template>
    <div class="max-w-full h-screen w-full overflow-hidden">
      <div ref="container"></div>
    </div>
</template>

注意: 您可以在下面的連結中的 Three.js 官方文件中找到有關程式碼各部分的資訊。
Three.js 文檔


現在,我們需要在 public 資料夾中建立名為 img 的資料夾,然後建立使用 HDRI 模式的影像。您可以創建自己的圖像,也可以從本部分的任何資源中找到一個圖像。我基本上使用一個名為 HDRi Haven 的網站,並將圖像轉換為 WebP 格式來縮小圖像的總大小。在我的情況下,我不在乎陰影、反射和其他複雜的效果。
現在,我們的公用資料夾中有圖片 img/hdri.webp,我們準備在 index.vue 頁面中使用我們的元件。
注意: 如果我們在應用程式中使用 SSR,我們可能會遇到一些渲染問題。因此,當我們像下面這樣呼叫元件時,我們將使用 <ClientOnly>。


<ClientOnly>
    <Landing/>
    <template #fallback>
      <!-- this will be rendered on server side -->
      <p>Landing is loading...</p>
    </template>
</ClientOnly>

就是這樣!我們現在有了一個基本的 360 度著陸頁。


discourse.threejs.org

页: [1]
查看完整版本: how to create basic 360 landing using three.js, nuxt.js and vue3