前面几篇把材质、纹理、光源。这些模块都过了一遍。这篇不学新东西了,用它们搭一座"闹鬼小屋"------有房子、有墓碑、有鬼魂、有雾。本篇不讲每一行代码,重点讲怎么把学过的知识串起来,做成一个完整的场景。
效果预览

一、先搭房子
设计思路
房子是整个场景的核心。我把它分成几个部分:墙壁、屋顶、门、灌木丛。用 THREE.Group 把房子包起来,方便整体管理位置和旋转。
关键代码
javascript
php
// 房子组
const house = new THREE.Group()
scene.add(house)
// 墙壁
const walls = new THREE.Mesh(
new THREE.BoxGeometry(4, 2.5, 4),
new THREE.MeshStandardMaterial({
map: wallColorTexture,
aoMap: wallARMTexture,
normalMap: wallNormalTexture,
roughnessMap: wallARMTexture
})
)
walls.position.y = 1.25
house.add(walls)
// 屋顶
const roof = new THREE.Mesh(
new THREE.ConeGeometry(3.5, 1, 4),
new THREE.MeshStandardMaterial({ color: '#b35f45' })
)
roof.position.y = 2.5 + 0.5
roof.rotation.y = Math.PI * 0.25
house.add(roof)
// 门
const door = new THREE.Mesh(
new THREE.PlaneGeometry(2.2, 2.2, 100, 100),
new THREE.MeshStandardMaterial({
map: doorColorTexture,
transparent: true,
alphaMap: doorAlphaTexture,
aoMap: doorAmbientOcclusionTexture,
displacementMap: doorHeightTexture,
displacementScale: 0.1,
normalMap: doorNormalTexture,
metalnessMap: doorMetalnessTexture,
roughnessMap: doorRoughnessTexture
})
)
door.position.y = 1
door.position.z = 2 + 0.01
house.add(door)
// 灌木丛
const bushGeometry = new THREE.SphereGeometry(1, 16, 16)
const bushMaterial = new THREE.MeshStandardMaterial({ color: '#0da00d' })
const bush1 = new THREE.Mesh(bushGeometry, bushMaterial)
bush1.scale.set(0.5, 0.5, 0.5)
bush1.position.set(0.8, 0.2, 2.2)
house.add(bush1)
// 同理添加 bush2, bush3, bush4...
做了什么
- 墙壁用砖墙纹理,配合
aoMap、normalMap、roughnessMap增加真实感 - 屋顶用锥体几何体,旋转 45 度让棱角对齐墙壁
- 门是一块平面,叠了颜色、透明度、环境遮蔽、置换、法线、金属度、粗糙度 7 张贴图
- 灌木丛用球体缩放,大小不一地摆在门两侧
关键技巧
UV2 设置 :aoMap 需要用到第二套 UV 坐标,需要手动添加:
javascript
php
walls.geometry.setAttribute(
'uv2',
new THREE.Float32BufferAttribute(walls.geometry.attributes.uv.array, 2)
)
门和地面也是同样处理。
二、墓碑围成圈
设计思路
墓碑要散落在房子周围,但不能整整齐齐。用 sin 和 cos 算出圆形上的坐标,再加一点随机偏移,让墓碑看起来像是自然长出来的。
关键代码
javascript
ini
const graves = new THREE.Group()
scene.add(graves)
const graveGeometry = new THREE.BoxGeometry(0.6, 0.8, 0.2)
const graveMaterial = new THREE.MeshStandardMaterial({ color: '#b2b6b1' })
for(let i = 0; i < 50; i++) {
const angle = Math.random() * Math.PI * 2
const radius = 3 + Math.random() * 6
const x = Math.sin(angle) * radius
const z = Math.cos(angle) * radius
const grave = new THREE.Mesh(graveGeometry, graveMaterial)
grave.position.set(x, 0.3, z)
grave.rotation.y = (Math.random() - 0.5) * 0.4
grave.rotation.z = (Math.random() - 0.5) * 0.4
graves.add(grave)
}
做了什么
- 50 个墓碑分布在半径 3 到 9 的环形区域内
- 每个墓碑的位置、旋转都有随机偏移
- 看起来像是年久失修的墓地,自然散落
三、灯光设计
设计思路
三种光源各司其职:环境光提供基础照明,月光从一侧照过来营造阴森感,门灯是屋里唯一的暖色光源。鬼魂会发光,变成会移动的彩色光源。
关键代码
javascript
csharp
// 环境光
const ambientLight = new THREE.AmbientLight('#b9b5ff', 0.12)
scene.add(ambientLight)
// 月光(平行光)
const moonLight = new THREE.DirectionalLight('#b9b5ff', 0.12)
moonLight.position.set(4, 5, -2)
scene.add(moonLight)
// 门灯(点光源)
const doorLight = new THREE.PointLight('#ff7d46', 1, 7)
doorLight.position.set(0, 2.2, 2.7)
house.add(doorLight)
// 鬼魂(带颜色的点光源)在动画部分会移动
const ghost1 = new THREE.PointLight('#ca8eff', 6)
// ghost2, ghost3 同理
scene.add(ghost1, ghost2, ghost3)
做了什么
- 环境光和月光都是冷色调(蓝紫色),营造夜晚氛围
- 门灯是暖橙色,形成冷暖对比
- 三个鬼魂各自带有不同颜色的光(紫、粉、蓝),并且会移动
四、雾(Fog)
设计思路
雾的作用:让远处的物体渐渐消失,模糊场景边界,增强神秘感。同时还能掩盖远处的性能细节。
关键代码
javascript
ini
const fog = new THREE.Fog('#262837', 1, 15)
scene.fog = fog
做了什么
- 颜色和场景背景色
#262837保持一致 - 从距离 1 开始出现雾,到距离 15 完全消失
- 远处的墓碑会逐渐融入雾中
五、草地纹理
设计思路
地面用草地纹理,重复铺 8×8 次,避免一张图拉伸变形。
关键代码
javascript
ini
grassColorTexture.repeat.set(8, 8)
grassColorTexture.wrapS = THREE.RepeatWrapping
grassColorTexture.wrapT = THREE.RepeatWrapping
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({
map: grassColorTexture,
aoMap: grassARMTexture,
normalMap: grassNormalTexture
})
)
floor.rotation.x = -Math.PI * 0.5
floor.position.y = 0
scene.add(floor)
做了什么
- 一张小草纹理,重复 8×8 次铺满整个地面
wrapS和wrapT设置为RepeatWrapping,让纹理在水平/垂直方向都能重复- 配合
aoMap和normalMap增加细节
六、鬼魂动画
设计思路
三个鬼魂用不同颜色、不同速度、不同半径绕圈飞行。用 sin 和 cos 控制水平运动,用多层 sin 叠加控制垂直起伏,产生飘忽不定的效果。
关键代码
javascript
ini
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// 鬼魂1
const ghost1Angle = elapsedTime * 0.5
ghost1.position.x = Math.cos(ghost1Angle) * 4
ghost1.position.z = Math.sin(ghost1Angle) * 4
ghost1.position.y = Math.sin(ghost1Angle) * Math.sin(ghost1Angle * 2.34) * Math.sin(ghost1Angle * 3.45)
// 鬼魂2(速度不同,半径不同)
const ghost2Angle = -elapsedTime * 0.38
ghost2.position.x = Math.cos(ghost2Angle) * 5
ghost2.position.z = Math.sin(ghost2Angle) * 5
ghost2.position.y = Math.sin(ghost2Angle) * Math.sin(ghost2Angle * 2.34) * Math.sin(ghost2Angle * 3.45)
// 鬼魂3
const ghost3Angle = elapsedTime * 0.23
ghost3.position.x = Math.cos(ghost3Angle) * 6
ghost3.position.z = Math.sin(ghost3Angle) * 6
ghost3.position.y = Math.sin(ghost3Angle) * Math.sin(ghost3Angle * 2.34) * Math.sin(ghost3Angle * 3.45)
}
做了什么
- 鬼魂1:速度 0.5,半径 4,沿顺时针飞行
- 鬼魂2:速度 0.38(逆时针),半径 5
- 鬼魂3:速度 0.23,半径 6
- 垂直运动用三个
sin相乘,产生不规则的飘忽感
阴影配置
关键代码
javascript
ini
// 渲染器
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
// 光源投射阴影
moonLight.castShadow = true
ghost1.castShadow = true
// ghost2, ghost3 同理
// 物体投射/接收阴影
walls.castShadow = true
walls.receiveShadow = true
floor.receiveShadow = true
for(const grave of graves.children) {
grave.castShadow = true
grave.receiveShadow = true
}
// 阴影贴图配置
moonLight.shadow.mapSize.width = 256
moonLight.shadow.mapSize.height = 256
moonLight.shadow.camera.top = 8
moonLight.shadow.camera.right = 8
moonLight.shadow.camera.bottom = -8
moonLight.shadow.camera.left = -8
moonLight.shadow.camera.near = 1
moonLight.shadow.camera.far = 20
做了什么
- 开启渲染器阴影,选择柔和阴影类型
- 月光和鬼魂都投射阴影
- 墙壁投射阴影,地面接收阴影
- 墓碑逐个设置投射和接收阴影
- 阴影贴图分辨率设为 256,平衡性能和质量
总结
这个场景把之前学过的知识点都串起来了:
| 模块 | 用在哪 |
|---|---|
| 几何体 | 房子、屋顶、门、灌木丛、墓碑 |
| 纹理 | 墙壁砖纹、门的多张贴图、草地 |
| 材质 | StandardMaterial 配合多张贴图 |
| 光源 | 环境光、平行光(月光)、点光源(门灯、鬼魂) |
| 阴影 | 月光、鬼魂投射阴影,地面接收 |
| 三角函数 | 墓碑圆形分布、鬼魂飘移动画 |
| 雾 | 场景氛围营造 |
做完这个场景,你就不再是"学过 Three.js 的各个模块"了,而是"用 Three.js 完整地做过一个项目"。接下来你可以开始做你自己的场景了。
📚 本文是学习 Three.js Journey(Bruno Simon 的付费课程)的学习笔记,代码为个人练习所写,概念讲解融入了个人的理解和比喻。