Skip to content

37. 实战:林海雪原

Published:

这节我们来做一个林海雪原的实战。

首先,用前面学过的噪声算法生成随机山坡地形,然后加载 gltf 树木模型,放在山坡的不同地方。

之后加上平行光和阴影,用 Sprite 来做下雪效果,并且加上雾。

这样就是一个雪中的森林的场景。

大概就是这个思路,我们直接来写吧:

npx create-vite snowy-forest

image.png

进入项目,安装依赖:

npm install
npm install --save three
npm install --save-dev @types/three

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mountainside from './mountainside.js';

const scene = new THREE.Scene();

scene.add(mountainside);

const directionLight = new THREE.DirectionalLight(0xffffff, 5);
directionLight.position.set(1000, 1000, 1000);
scene.add(directionLight);

const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(300, 300, 500);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

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

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

mountainside 是山坡的意思。

我们来写下 src/mountainside.js

import * as THREE from 'three';
import { createNoise2D } from "simplex-noise";

const geometry = new THREE.PlaneGeometry(3000, 3000, 100, 100);

const noise2D = createNoise2D();

const positions = geometry.attributes.position;

for (let i = 0 ; i < positions.count; i ++) {
    const x = positions.getX(i);
    const y = positions.getY(i);

    const z = noise2D(x / 800, y / 800) * 50;

    positions.setZ(i, z);
}

const material = new THREE.MeshLambertMaterial({
    color: new THREE.Color('white'),
    wireframe: true
});

const mountainside = new THREE.Mesh(geometry, material);
mountainside.rotateX(- Math.PI / 2);

export default mountainside;

这个效果我们前面写过,就是 PlaneGeometry 设置很多分段,这样就有了很多顶点。

用噪声库 simplex-noise 给顶点设置随机的 z,这个 z 是传入 x、y 算出来的有连续性的随机值。

npm install --save simplex-noise

看下效果:

2025-04-04 11.16.53.gif

每次刷新都是随机的山坡。

这里我们先不去掉线框 wireframe,不然效果不明显。

然后找个树的 gltf 模型:

https://github.com/QuarkGluonPlasma/threejs-course-code/tree/main/snowy-forest/public/tree

image.png

点击下载按钮,下载这两个文件

放在 public 目录下:

image.png

在代码里引入。

创建 src/tree.js

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const tree = new THREE.Group();

const loader = new GLTFLoader();

function loadTree(callback) {
    loader.load('./tree/tree.gltf', gltf => {
        console.log(gltf);
    
        tree.add(gltf.scene);

        callback(tree);
    });
}

export default loadTree;

用 GLTFLoader 加载 gltf 模型。

这个模块导出一个 loadTree 方法,回调函数里是加载好的模型。

我们在 main.js 里加载看看:

image.png

import loadTree from './tree.js';

loadTree((tree) => {
  scene.add(tree);
})

可以看到,模型很小,并且没有设置颜色:

2025-04-04 12.44.34.gif

看下 devtools 打印的信息:

image.png

gltf.scene 的某个 children 是 Mesh。

我们都打印出来看看:

image.png

scene.traverse(obj => {
    if(obj.isMesh) {
      console.log(obj.name, obj);
    }
})

遍历 scene,打印所有 Mesh。

image.png

从名字也可以看出来,一个是叶子,一个是树的主体。

image.png

loader.load('./tree/tree.gltf', gltf => {
    console.log(gltf);

    gltf.scene.scale.set(10, 10, 10);

    tree.add(gltf.scene);

    gltf.scene.traverse(obj => {
        if(obj.isMesh) {
            if(obj.name === "leaves001") {
                obj.material.color.set('green');
            } else {
                obj.material.color.set('brown');
            }
        }
    });
    callback(tree);
});

设置下 scale,放大 10 倍,并且分别设置不同的颜色。

2025-04-04 12.53.19.gif

可以看到,现在大小就合适了,并且树干、树叶分别是不同的颜色。

然后我们在山坡上种树:

image.png

拿到山坡的顶点信息,随机拿到一些顶点的 x、y、z,把树种上:

image.png

loadTree((tree) => {
    let i = 0;
    while(i< positions.count) {
        const newTree = tree.clone();
        newTree.position.x = positions.getX(i);
        newTree.position.y = positions.getY(i);
        newTree.position.z = positions.getZ(i);
        mountainside.add(newTree);
        newTree.rotateX(Math.PI / 2);

        i += Math.floor(300 * Math.random());
    }
})

这里树要旋转一下,不然方向不对。

image.png

我们把线框关掉:

image.png

image.png

是不是就有点感觉了。

接下来我们加上阴影,增加真实感。

增加阴影分为几步:

image.png

image.png

mountainside.receiveShadow = true;
obj.castShadow = true;

让山坡接收阴影,让树投射阴影。

注意,这里要遍历树的模型,对每一个 Mesh 都开启阴影投射。

在 renderer 上开启阴影渲染:

image.png

renderer.shadowMap.enabled = true;

最后来开启灯光的阴影设置,并且设置阴影相机的大小:

image.png

const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(1000, 2000, 1000);
directionLight.castShadow = true;
directionLight.shadow.camera.left = -2000;
directionLight.shadow.camera.right = 2000;
directionLight.shadow.camera.top = 2000;
directionLight.shadow.camera.bottom = -2000;
directionLight.shadow.camera.near = 0.5;
directionLight.shadow.camera.far = 10000;
scene.add(directionLight);

const cameraHelper = new THREE.CameraHelper(directionLight.shadow.camera);
scene.add(cameraHelper);

这里开启 castShow,设置了阴影相机大小覆盖住所有的树,用 CameraHelper 做了可视化。

看下效果:

2025-04-04 13.38.52.gif

阴影相机覆盖住所有的树就行。

去掉 CameraHelper 看下效果:

image.png

接下来我们来做下雪的效果:

把上节的雪花图片复制过来,放在 public 目录下:

snow.png

然后加一下 src/snow.js

image.png

import * as THREE from 'three';

const loader = new THREE.TextureLoader();
const texture = loader.load("./snow.png");
const spriteMaterial = new THREE.SpriteMaterial({
    map: texture
});

const group = new THREE.Group();

for (let i = 0; i < 1000; i ++) {
    const sprite = new THREE.Sprite(spriteMaterial);
    sprite.scale.set(5, 5);

    const x = -1500 + 3000 * Math.random();
    const y = 1000 * Math.random();
    const z = -1500 + 3000 * Math.random();
    sprite.position.set(x, y, z);

    group.add(sprite);
}

const clock = new THREE.Clock();
function render() {
    const delta = clock.getDelta();
    group.children.forEach(sprite => {
        sprite.position.y -= delta * 30;

        if (sprite.position.y < 0) {
            sprite.position.y = 1000;
        }
    });

    requestAnimationFrame(render);
}
render();

export default group;

和上节代码一样,就是创建 1000 个 Sprite,在 3000 * 1000 * 3000 的空间内下落,下落到底部的时候回到最上面。

坐标范围是从 -1500 到 1500,所以是 1500 + 3000 * Math.random()

这里把雪花稍微设置的大一点,scale 设置 5 倍。

引入看下效果:

image.png

2025-04-04 13.57.37.gif

现在就有下雪的效果了。

但有一些特别大的雪花:

image.png

这个调解下相近的近裁截面,设置的远一点就好了:

image.png

2025-04-04 14.05.05.gif

最后改个天空颜色:

image.png

renderer.setClearColor(new THREE.Color('darkblue'));

2025-04-04 14.27.26.gif

这样,我们的林海雪原就完成了。

案例代码上传了小册仓库

总结

这节我们做了一个比较综合的实战:林海雪原。

练习了 4 个基础知识点:

做完这个实战之后,你会对山脉地形、模型加载、阴影、Sprite 有更深的掌握。

评论