iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

Day 19 我們已經完成基本的光照系統,現在太陽會透過 PointLight 照亮行星,也加入了 AmbientLight 讓暗面保留一些細節。
今天不會加入新的行星,而是把 Three.js 的 Shadow 系統補上。

一、今天完成目標

今天會完成:

  • 開啟 Three.js Shadow
  • 讓太陽光可以產生陰影
  • 讓行星可以投射陰影
  • 讓行星可以接收陰影
  • 認識 castShadow
  • 認識 receiveShadow
  • 認識 Shadow Map

今天順便更改一下文章的順序,之後都會將程式碼放在文章的後面。

二、第一步:開啟 Shadow

在 Renderer 建立完成之後加入:

renderer.shadowMap.enabled =
    true;

這一行可以理解成:
整個場景的 Shadow 總開關

如果沒有開啟它,即使後面設定 castShadow = true;
也不會出現陰影,我們另外再加入:

renderer.shadowMap.type =
    THREE.PCFSoftShadowMap;

讓陰影邊緣看起來柔和一點。
所以 Renderer 這一段現在會變成:

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

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

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

renderer.shadowMap.enabled =
    true;

renderer.shadowMap.type =
    THREE.PCFSoftShadowMap;

三、讓太陽光產生陰影,設定 Shadow Map

今天在 const sunLight 下面加入:

sunLight.castShadow =
    true;

意思就是這個 Light 可以計算 Shadow
完整修改:

const sunLight =
    new THREE.PointLight(
        0xffffff,
        500,
        100
    );

sunLight.position.set(
    0,
    0,
    0
);
    
sunLight.castShadow =
    true;

接著加入:

sunLight.shadow.mapSize.width =
    512;

sunLight.shadow.mapSize.height =
    512;

這裡設定的是 Shadow Map 的解析度。
可以簡單理解成:
解析度越高則陰影越清楚 GPU 就負擔越高

目前先使用 512 × 512

就可以了。
另外再設定:

sunLight.shadow.camera.near =
    0.1;

sunLight.shadow.camera.far =
    100;

代表 Shadow 計算的距離範圍。
所以 PointLight 完整修改會變成:

const sunLight =
    new THREE.PointLight(
        0xffffff,
        500,
        100
    );

sunLight.position.set(
    0,
    0,
    0
);

sunLight.castShadow =
    true;

sunLight.shadow.mapSize.width =
    512;

sunLight.shadow.mapSize.height =
    512;

sunLight.shadow.camera.near =
    0.1;

sunLight.shadow.camera.far =
    100;

scene.add(
    sunLight
);

四、讓行星可以產生、接收 Shadow

今天在 const planet 下面加入:

planet.castShadow =
    true;

意思是這顆行星可以擋住光線產生陰影
所以 castShadow = true;
可以記成 我可以製造陰影

接著再加入:

planet.receiveShadow =
    true;

這代表這個物體可以顯示其他物體投射過來的陰影
所以現在行星建立的部分會變成:

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

planet.castShadow =
    true;

planet.receiveShadow =
    true;

scene.add(
    planet
);

兩個設定可以這樣記:
castShadow = 可以投射陰影
receiveShadow = 可以接收陰影

五、Light 和 Shadow 不一樣

這裡有一個很重要的觀念。
我們讓地球出現 亮面 + 暗面
這是 Lighting 但暗面不代表 Shadow。
真正的 Shadow 是中間有一個物體把光擋住。
也就是:Light → Object A → 光線被擋住 → Object B 出現 Shadow
所以 Lighting ≠ Shadow

六、完整程式碼

今天一樣只有修改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
    )
);

renderer.shadowMap.enabled =
    true;

renderer.shadowMap.type =
    THREE.PCFSoftShadowMap;

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;

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

controls.update();


// Clock

const clock =
    new THREE.Clock();


// Texture Loader

const textureLoader =
    new THREE.TextureLoader();


// Sun

const sunTexture =
    textureLoader.load(
        "textures/sun.jpg"
    );

sunTexture.colorSpace =
    THREE.SRGBColorSpace;

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

const sunMaterial =
    new THREE.MeshBasicMaterial({
        map: sunTexture
    });

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

scene.add(
    sun
);


// Point Light

const sunLight =
    new THREE.PointLight(
        0xffffff,
        500,
        100
    );

sunLight.position.set(
    0,
    0,
    0
);

sunLight.castShadow =
    true;

sunLight.shadow.mapSize.width =
    512;

sunLight.shadow.mapSize.height =
    512;

sunLight.shadow.camera.near =
    0.1;

sunLight.shadow.camera.far =
    100;

scene.add(
    sunLight
);


// Ambient Light

const ambientLight =
    new THREE.AmbientLight(
        0xffffff,
        0.08
    );

scene.add(
    ambientLight
);


// Planet Data

const planetData = [

    {
        name: "Mercury",
        size: 0.45,
        distance: 4,
        rotationSpeed: 0.8,
        orbitSpeed: 1.2,
        texture:
            "textures/mercury.jpg"
    },

    {
        name: "Venus",
        size: 0.65,
        distance: 6,
        rotationSpeed: -0.3,
        orbitSpeed: 0.95,
        texture:
            "textures/venus.jpg"
    },

    {
        name: "Earth",
        size: 0.7,
        distance: 8,
        rotationSpeed: 1.0,
        orbitSpeed: 0.8,
        texture:
            "textures/earth.jpg"
    },

    {
        name: "Mars",
        size: 0.55,
        distance: 10,
        rotationSpeed: 0.9,
        orbitSpeed: 0.65,
        texture:
            "textures/mars.jpg"
    },

    {
        name: "Jupiter",
        size: 1.3,
        distance: 13,
        rotationSpeed: 2.0,
        orbitSpeed: 0.4,
        texture:
            "textures/jupiter.jpg"
    },

    {
        name: "Saturn",
        size: 1.1,
        distance: 16,
        rotationSpeed: 1.8,
        orbitSpeed: 0.3,
        texture:
            "textures/saturn.jpg"
    },

    {
        name: "Uranus",
        size: 0.9,
        distance: 19,
        rotationSpeed: 1.2,
        orbitSpeed: 0.22,
        texture:
            "textures/uranus.jpg"
    },

    {
        name: "Neptune",
        size: 0.85,
        distance: 22,
        rotationSpeed: 1.1,
        orbitSpeed: 0.16,
        texture:
            "textures/neptune.jpg"
    }

];


// 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 planetTexture =
            textureLoader.load(
                data.texture
            );

        planetTexture.colorSpace =
            THREE.SRGBColorSpace;

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

        const material =
            new THREE.MeshStandardMaterial({
                map: planetTexture,
                roughness: 1,
                metalness: 0
            });

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

        planet.castShadow =
            true;

        planet.receiveShadow =
            true;

        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) => {

        if (event.key === "1") {
            camera.position.set(
                0,
                0,
                40
            );
        }

        if (event.key === "2") {
            camera.position.set(
                30,
                25,
                30
            );
        }

        if (event.key === "3") {
            camera.position.set(
                0,
                45,
                0.1
            );
        }

        if (event.key === "4") {
            camera.position.set(
                0,
                12,
                20
            );
        }

        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;
        }
    );

    controls.update();

    renderer.render(
        scene,
        camera
    );
}

animate();

七、今天小結

今天真正新增的程式不多,
但它讓我們的太陽系多了一個新的系統:Lighting + Shadow。
目前 Shadow 還不一定非常明顯,但從今天開始,每顆行星都已經具備
投射陰影 + 接收陰影 的能力。

Day 21 預告

「Moon:建立地球與月球系統」

Day 21 我們會第一次建立衛星,並讓月亮繞著地球,
到時候今天建立的 castShadow 以及 receiveShadow
就會開始真正派上用場。


上一篇
Day 19 - Lighting:調整太陽系的明暗效果
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言