这节我们来做一个林海雪原的实战。
首先,用前面学过的噪声算法生成随机山坡地形,然后加载 gltf 树木模型,放在山坡的不同地方。
之后加上平行光和阴影,用 Sprite 来做下雪效果,并且加上雾。
这样就是一个雪中的森林的场景。
大概就是这个思路,我们直接来写吧:
npx create-vite snowy-forest

进入项目,安装依赖:
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
看下效果:

每次刷新都是随机的山坡。
这里我们先不去掉线框 wireframe,不然效果不明显。
然后找个树的 gltf 模型:
https://github.com/QuarkGluonPlasma/threejs-course-code/tree/main/snowy-forest/public/tree

点击下载按钮,下载这两个文件
放在 public 目录下:

在代码里引入。
创建 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 里加载看看:

import loadTree from './tree.js';
loadTree((tree) => {
scene.add(tree);
})
可以看到,模型很小,并且没有设置颜色:

看下 devtools 打印的信息:

gltf.scene 的某个 children 是 Mesh。
我们都打印出来看看:

scene.traverse(obj => {
if(obj.isMesh) {
console.log(obj.name, obj);
}
})
遍历 scene,打印所有 Mesh。

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

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 倍,并且分别设置不同的颜色。

可以看到,现在大小就合适了,并且树干、树叶分别是不同的颜色。
然后我们在山坡上种树:

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

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());
}
})
这里树要旋转一下,不然方向不对。

我们把线框关掉:


是不是就有点感觉了。
接下来我们加上阴影,增加真实感。
增加阴影分为几步:


mountainside.receiveShadow = true;
obj.castShadow = true;
让山坡接收阴影,让树投射阴影。
注意,这里要遍历树的模型,对每一个 Mesh 都开启阴影投射。
在 renderer 上开启阴影渲染:

renderer.shadowMap.enabled = true;
最后来开启灯光的阴影设置,并且设置阴影相机的大小:

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 做了可视化。
看下效果:

阴影相机覆盖住所有的树就行。
去掉 CameraHelper 看下效果:

接下来我们来做下雪的效果:
把上节的雪花图片复制过来,放在 public 目录下:

然后加一下 src/snow.js

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 倍。
引入看下效果:


现在就有下雪的效果了。
但有一些特别大的雪花:

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


最后改个天空颜色:

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

这样,我们的林海雪原就完成了。
案例代码上传了小册仓库。
总结
这节我们做了一个比较综合的实战:林海雪原。
练习了 4 个基础知识点:
- 噪声算法 + 自定义几何体顶点坐标,生成山坡地形
- 模型加载和遍历设置材质,实现树林
- 阴影效果
- Sprite 实现下雪效果
做完这个实战之后,你会对山脉地形、模型加载、阴影、Sprite 有更深的掌握。