iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作系列 第 16 篇

Day 16 - OrbitControls:用滑鼠自由觀看整個太陽系

  • 分享至 

  • xImage
  •  

Day 15 我們已經讓 Camera 可以透過鍵盤切換不同視角。
但都還是固定好的位置。
今天我們要加入 OrbitControls
讓使用者可以直接用滑鼠自由操作 Camera。

一、今天完成目標

今天會完成:

  • 匯入 OrbitControls
  • 讓滑鼠可以拖曳旋轉 Camera
  • 使用滑鼠滾輪放大與縮小
  • 使用滑鼠右鍵平移畫面
  • 設定 enableDamping
  • 在動畫中加入 controls.update()
  • 設定 Camera 最小與最大距離
  • 讓鍵盤切換與 OrbitControls 可以一起使用

今天完成之後,太陽系就不再只能看固定角度。
可以直接拖曳,縮放、旋轉、平移自由探索整個 3D 場景。

二、完整程式碼

index.html

<!DOCTYPE html>
<html lang="zh-TW">

<head>
    <meta charset="UTF-8">

    <meta
        name="viewport"
        content="width=device-width, initial-scale=1.0"
    >

    <title>從 Web 到宇宙</title>

    <link
        rel="stylesheet"
        href="css/style.css"
    >

    <script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
        }
    }
    </script>
</head>

<body>

    <div id="scene"></div>

    <div class="title">
        <h1>從 Web 到宇宙</h1>
        <p>DAY 16|OrbitControls</p>
    </div>

    <script
        type="module"
        src="js/main.js"
    ></script>

</body>

</html>

css/style.css

與 Day 15 相同,不需要修改。

js/main.js

import * as THREE from
    "three";

import { OrbitControls } from
    "three/addons/controls/OrbitControls.js";

    
// ========================
// Scene
// ========================

const scene =
    new THREE.Scene();

scene.background =
    new THREE.Color(
        0x000000
    );


// ========================
// Camera
// ========================

const camera =
    new THREE.PerspectiveCamera(
        60,
        window.innerWidth /
        window.innerHeight,
        0.1,
        1000
    );

camera.position.set(
    30,
    25,
    30
);

camera.lookAt(
    0,
    0,
    0
);


// ========================
// Renderer
// ========================

const renderer =
    new THREE.WebGLRenderer({
        antialias: true
    });

renderer.setSize(
    window.innerWidth,
    window.innerHeight
);

renderer.setPixelRatio(
    Math.min(
        window.devicePixelRatio,
        2
    )
);

document
    .getElementById("scene")
    .appendChild(
        renderer.domElement
    );


// ========================
// OrbitControls
// ========================

const controls =
    new OrbitControls(
        camera,
        renderer.domElement
    );


// 開啟阻尼效果
controls.enableDamping =
    true;


// 阻尼強度
controls.dampingFactor =
    0.05;


// 最小縮放距離
controls.minDistance =
    10;


// 最大縮放距離
controls.maxDistance =
    80;


// Camera 主要看向太陽系中心
controls.target.set(
    0,
    0,
    0
);

controls.update();


// ========================
// Clock
// ========================

const clock =
    new THREE.Clock();


// ========================
// Sun
// ========================

const sunGeometry =
    new THREE.SphereGeometry(
        2,
        32,
        16
    );

const sunMaterial =
    new THREE.MeshBasicMaterial({
        color: 0xffaa00
    });

const sun =
    new THREE.Mesh(
        sunGeometry,
        sunMaterial
    );

const sunwireMaterial =
    new THREE.MeshBasicMaterial({
        color: 0xffffff,
        wireframe: true
    });

const sunwire =
    new THREE.Mesh(
        sunGeometry,
        sunwireMaterial
    );

sunwire.scale.set(
    1.03,
    1.03,
    1.03
);

sun.add(
    sunwire
);

scene.add(
    sun
);


// ========================
// Planet Data
// ========================

const planetData = [

    {
        name: "Mercury",
        size: 0.45,
        color: 0xaaaaaa,
        distance: 4,
        rotationSpeed: 0.8,
        orbitSpeed: 1.2
    },

    {
        name: "Venus",
        size: 0.65,
        color: 0xd9a066,
        distance: 6,
        rotationSpeed: -0.3,
        orbitSpeed: 0.95
    },

    {
        name: "Earth",
        size: 0.7,
        color: 0x2277ff,
        distance: 8,
        rotationSpeed: 1.0,
        orbitSpeed: 0.8
    },

    {
        name: "Mars",
        size: 0.55,
        color: 0xcc4422,
        distance: 10,
        rotationSpeed: 0.9,
        orbitSpeed: 0.65
    },

    {
        name: "Jupiter",
        size: 1.3,
        color: 0xd8b38a,
        distance: 13,
        rotationSpeed: 2.0,
        orbitSpeed: 0.4
    },

    {
        name: "Saturn",
        size: 1.1,
        color: 0xe6c87a,
        distance: 16,
        rotationSpeed: 1.8,
        orbitSpeed: 0.3
    },

    {
        name: "Uranus",
        size: 0.9,
        color: 0x66ccdd,
        distance: 19,
        rotationSpeed: 1.2,
        orbitSpeed: 0.22
    },

    {
        name: "Neptune",
        size: 0.85,
        color: 0x3355ff,
        distance: 22,
        rotationSpeed: 1.1,
        orbitSpeed: 0.16
    }

];


// ========================
// Create Orbits
// ========================

planetData.forEach(
    (data) => {

        const points = [];

        const segments = 128;

        for (
            let i = 0;
            i < segments;
            i++
        ) {

            const angle =
                (i / segments) *
                Math.PI * 2;

            const x =
                Math.cos(angle) *
                data.distance;

            const z =
                Math.sin(angle) *
                data.distance;

            points.push(
                new THREE.Vector3(
                    x,
                    0,
                    z
                )
            );
        }

        const orbitGeometry =
            new THREE.BufferGeometry()
                .setFromPoints(
                    points
                );

        const orbitMaterial =
            new THREE.LineBasicMaterial({
                color: 0x444444
            });

        const orbit =
            new THREE.LineLoop(
                orbitGeometry,
                orbitMaterial
            );

        scene.add(
            orbit
        );
    }
);


// ========================
// Create Planets
// ========================

const planets = [];

planetData.forEach(
    (data, index) => {

        const geometry =
            new THREE.SphereGeometry(
                data.size,
                12,
                8
            );

        const material =
            new THREE.MeshBasicMaterial({
                color: data.color
            });

        const planet =
            new THREE.Mesh(
                geometry,
                material
            );


        // ========================
        // Wireframe
        // ========================

        const wireMaterial =
            new THREE.MeshBasicMaterial({
                color: 0xffffff,
                wireframe: true
            });

        const wire =
            new THREE.Mesh(
                geometry,
                wireMaterial
            );

        wire.scale.set(
            1.03,
            1.03,
            1.03
        );

        planet.add(
            wire
        );

        scene.add(
            planet
        );


        const startAngle =
            index * 0.8;


        planets.push({
            mesh: planet,

            distance:
                data.distance,

            rotationSpeed:
                data.rotationSpeed,

            orbitSpeed:
                data.orbitSpeed,

            angle:
                startAngle
        });
    }
);


// ========================
// Camera Controls
// ========================

window.addEventListener(
    "keydown",
    (event) => {

        // 1:正面視角
        if (event.key === "1") {

            camera.position.set(
                0,
                0,
                40
            );

        }


        // 2:斜角視角
        if (event.key === "2") {

            camera.position.set(
                30,
                25,
                30
            );

        }


        // 3:俯視視角
        if (event.key === "3") {

            camera.position.set(
                0,
                45,
                0.1
            );

        }


        // 4:近距離視角
        if (event.key === "4") {

            camera.position.set(
                0,
                12,
                20
            );

        }


        // OrbitControls 的目標
        // 回到太陽系中心
        controls.target.set(
            0,
            0,
            0
        );

        controls.update();

    }
);


// ========================
// Resize
// ========================

window.addEventListener(
    "resize",
    () => {

        camera.aspect =
            window.innerWidth /
            window.innerHeight;

        camera.updateProjectionMatrix();

        renderer.setSize(
            window.innerWidth,
            window.innerHeight
        );

        renderer.setPixelRatio(
            Math.min(
                window.devicePixelRatio,
                2
            )
        );
    }
);


// ========================
// Animation
// ========================

function animate() {

    requestAnimationFrame(
        animate
    );


    const delta =
        clock.getDelta();


    // 太陽自轉
    sun.rotation.y +=
        0.5 * delta;


    planets.forEach(
        (planet) => {

            // 自轉
            planet.mesh.rotation.y +=
                planet.rotationSpeed *
                delta;


            // 公轉
            planet.angle +=
                planet.orbitSpeed *
                delta;


            planet.mesh.position.x =
                Math.cos(
                    planet.angle
                ) *
                planet.distance;


            planet.mesh.position.z =
                Math.sin(
                    planet.angle
                ) *
                planet.distance;

        }
    );


    // OrbitControls 更新
    controls.update();


    renderer.render(
        scene,
        camera
    );
}

animate();

三、加入 OrbitControls

今天最重要的新功能就是 OrbitControls,
和前幾天有一個比較大的差別,
以前我們直接從 CDN 匯入 Three.js::

import * as THREE from
    "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";

但今天開始要使用 OrbitControls,它屬於 Three.js 的 Addons。
所以我們改成在 index.html 加入 importmap:

<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
    }
}
</script>

之後 JavaScript 就可以簡化成:

import * as THREE from
    "three";

import { OrbitControls } from
    "three/addons/controls/OrbitControls.js";

接著建立:

const controls =
    new OrbitControls(
        camera,
        renderer.domElement
    );

這裡需要兩個東西 camera + renderer.domElement

camera 就是我們要控制的相機。
而 renderer.domElement 就是 Three.js 畫面中的 Canvas。
所以可以理解成:
滑鼠操作 → Canvas OrbitControls → 控制 Camera

四、程式碼簡單說明

  1. 滑鼠可以做什麼?

加入 OrbitControls 之後,預設就可以使用:

滑鼠左鍵拖曳 → 旋轉 Camera
滑鼠滾輪 → 放大 / 縮小
滑鼠右鍵拖曳 → 平移 Camera

所以以前想換角度需要camera.position.set(...)現在可以直接用滑鼠完成。

  1. controls.target

我們加入:

controls.target.set(
    0,
    0,
    0
);

這個 0, 0, 0

就是太陽的位置。
所以 OrbitControls 旋轉時,可以把它想像成
Camera 繞著太陽系中心旋轉,也就是使用者是在「繞著太陽系看」。

  1. enableDamping

今天另外加入:

controls.enableDamping =
    true;

如果沒有 Damping,滑鼠一放開 Camera 就會立刻停止。
開啟 enableDamping = true 之後,操作會多一點慣性。
效果會比較像:拖曳 → 放開滑鼠 → Camera 稍微滑動 → 慢慢停止
整體操作會比較自然。

  1. dampingFactor

我們設定:

controls.dampingFactor =
    0.05;

這個數字控制阻尼效果。
數字比較小滑得比較久
數字比較大比較快停止
例如可以自己測試:

controls.dampingFactor =
    0.15;

看看手感有什麼不同。

  1. 為什麼動畫裡要寫 controls.update()

因為我們開啟:

controls.enableDamping =
    true;

所以動畫中要持續執行 controls.update();
也就是:

function animate() {

    ...

    controls.update();

    renderer.render(
        scene,
        camera
    );
}

這樣阻尼動畫才會持續更新。

  1. 限制縮放距離
    今天我們還加入:
controls.minDistance =
    10;

以及:

controls.maxDistance =
    80;

意思是 Camera 最靠近太陽系只能到 10,最遠只能到 80 。
如果完全不限制,使用者可能會,
滾輪一直往前 Camera 跑進太陽裡
或者滾輪一直往後整個太陽系變成一個小點
所以設定範圍之後會比較好控制。

  1. 鍵盤控制還可以繼續使用

Day 15 我們加入鍵盤控制四個 Camera 視角,今天沒有把它刪掉。
所以現在鍵盤滑鼠可以一起操作。

五、今天小結

今天加入 OrbitControls 之後現在使用者已經可以真正自由探索整個太陽系。
目前我們完成:

全螢幕場景、太陽、八大行星、Wireframe、自轉、公轉、公轉軌道、不同運行速度、THREE.Clock、Camera、鍵盤視角切換,以及 OrbitControls。

做到 Day 16,其實整個太陽系的:
3D 基礎 + 動畫 + Camera + 使用者互動
已經大致完成。
接下來要開始處理目前最明顯的問題:
這些行星還只是彩色球。

Day 17 預告

「Texture:替八大行星換上真正的表面」

目前地球只是color: 0x2277ff
所以看起來只有一顆藍球

Day 17 我們會開始使用 THREE.TextureLoader 載入圖片。
接著把圖片貼到球體表面。
而且到了這一天之後,我們之前為了觀察自轉而加入的:
Wireframe 就可以開始準備移除。
因為行星有貼圖之後,自轉本身就會很明顯。


上一篇
Day 15 - Camera:從不同角度觀察太陽系
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言