- 基础场景:some lights、no shadow、a Dat.GUI panel
// 导入three.js
import * as THREE from 'three'
// 导入轨道控制器
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 导入gui
import * as dat from 'dat.gui'
/**
* Scene
**/
const scene = new THREE.Scene()
/**
* Camera
**/
const camera = new THREE.PerspectiveCamera(
75, // 视角
window.innerWidth / window.innerHeight, // 视椎体长宽比
0.1, // 近端面
100 // 远端面
)
camera.position.set(4, 2, 5)
/**
* Renderer
**/
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
/**
* 坐标轴
**/
const axesHelper = new THREE.AxesHelper(5) // 坐标轴线段长度
scene.add(axesHelper)
/**
控制器(使相机围绕目标运动)
**/
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true // 添加轨道阻尼效果
/**
* 渲染
**/
const clock = new THREE.Clock()
function animate () {
let elapsedTime = clock.getElapsedTime()
controls.update()
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
/**
* Lights
**/
// AmbientLight
const ambientLight = new THREE.AmbientLight('#fff', 0.8)
scene.add(ambientLight)
/**
* gui
**/
const gui = new dat.GUI()
gui.add(ambientLight, 'intensity').name('AmbientLight').min(0).max(1).step(0.001)
-
House
- Floor
/** * House **/ // Floor const floor = new THREE.Mesh( new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardMaterial({ color: '#a9c388', }) floor.rotation.x = - Math.PI * 0.5 floor.position.y = 0 scene.add(floor)
- create a House group 以便于需要整体调整House的位置、大小等
/** * House **/ // Group const house = new THREE.Group() scene.add(house) ... /** * floor **/ ...
- create the Walls
/** * House **/ // Group const house = new THREE.Group() scene.add(house) // Walls const walls = new THREE.Mesh( new THREE.BoxGeometry(4, 2.5, 4), new THREE.MeshStandardMaterial({ color: '#ac8e82' }) ) walls.position.y = 2.5 / 2 // 立方体初始有一半的部分是在坐标轴以下的,也就是在floor的下面 house.add(walls) // 后续house的部分都添加在house上面 ... // foor ...
- create the Roof, with a pyramid 棱锥体
// Roof const roof = new THREE.Mesh( new THREE.ConeGeometry(3.5, 1, 4), new THREE.MeshStandardMaterial({ color: '#b35f45' }) ) roof.rotation.y = Math.PI * 0.25 roof.position.y = 2.5 + 0.5 // walls的高 + 自身高的一半 house.add(roof)
- creat the Door with a plane
// Door const door = new THREE.Mesh( new THREE.PlaneGeometry(2, 2), new THREE.MeshStandardMaterial({ color: '#aa7b7b', }) ) door.position.y = 1 door.position.z = 2 + 0.01 // walls的深度 + 0.01,+0.01避免处在同一层级 house.add(door)
- add Bushes and use the same geometry and the same material for every bushes
// Bushes const bushGeometry = new THREE.SphereGeometry(1, 16, 16) const bushMaterial = new THREE.MeshStandardMaterial({ color: '#89c854' }) 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) const bush2 = new THREE.Mesh(bushGeometry, bushMaterial) bush2.scale.set(0.25, 0.25, 0.25) bush2.position.set(1.4, 0.1, 2.1) const bush3 = new THREE.Mesh(bushGeometry, bushMaterial) bush3.scale.set(0.4, 0.4, 0.4) bush3.position.set(-0.8, 0.1, 2.2) const bush4 = new THREE.Mesh(bushGeometry, bushMaterial) bush4.scale.set(0.15, 0.15, 0.15) bush4.position.set(-1, 0.05, 2.6) house.add(bush1, bush2, bush3, bush4)
-
Graves
- instead of placing each grave manually, we are going to create and place them procedurally
- grave可出现的范围需要被限制在floor的范围内,同时围绕在house的外部出现(环形)
/** * Graves **/ 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 // 360° const radius = 3 + Math.random() * 6 // 随机半径, 3-9 // 分别设置x z轴的坐标,使grave围绕house画一个圈 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) // y值小于高度的一半,避免z轴出现旋转后底部悬空 // 旋转随机参数减去0.5,范围-0.5至0.5,确保旋转方向不一致 grave.rotation.y = (Math.random() - 0.5) * 0.6 grave.rotation.z = (Math.random() - 0.5) * 0.6 grave.rotation.x = (Math.random() - 0.5) * 0.6 graves.add(grave) }
- Lights
- dim the ambient and moon lights
- give those a more blue-ish color 青调
- add a warm
PointLight
above the door and add it to the house
/* * Lights */ // AmbientLight const ambientLight = new THREE.AmbientLight('#b9d5ff', 0.12) scene.add(ambientLight) // DirectionalLight const moonLight = new THREE.DirectionalLight('#b9d5ff', 0.12) moonLight.position.set(4, 5, -2) scene.add(moonLight) // Door Light const doorLight = new THREE.PointLight('#ff7d46', 1, 7) doorLight.position.set(0, 2.2, 2.7) house.add(doorLight)
- 完成House部分后,我们应该会得到如下图所示
-
Fog
- 当前我们看到的floor的边缘部分过于清晰
- 使用three.js本身就支持的 Fog
-
new THREE.Fog(color, near, far)
,near
和far
分别指的是开始应用雾的最小距离和最大距离,这里的“距离”是指距离camera
的远近
/**
* Fog
**/
const fog = new THREE.Fog('#262837', 1, 15) // color 距离camera的near 距离camera的far
scene.fog = fog
-
Background
- 继续模糊边缘,使floor和背景融为一体
- to fix the background, use the same color as Fog
renderer.setClearColor('#262837')
-
Textures
/** * Textures **/ const textureLoader = new THREE.TextureLoader()
- door
const doorColorTexture = textureLoader.load('/imgs/haunted-house/door/color.jpg') const doorAlphaTexture = textureLoader.load('/imgs/haunted-house/door/alpha.jpg') const doorAmbientOcclusionTexture = textureLoader.load('/imgs/haunted-house/door/ambientOcclusion.jpg') const doorHeightTexture = textureLoader.load('/imgs/haunted-house/door/height.jpg') const doorNormalTexture = textureLoader.load('/imgs/haunted-house/door/normal.jpg') const doorMetalnessTexture = textureLoader.load('/imgs/haunted-house/door/metalness.jpg') const doorRoughnessTexture = textureLoader.load('/imgs/haunted-house/door/roughness.jpg')
// Door const door = new THREE.Mesh( new THREE.PlaneGeometry(2.2, 2.2, 100, 100), // displacementMap会移动顶点实现立体感 new THREE.MeshStandardMaterial({ // color: '#aa7b7b', // 添加texture之前作为替代 map: doorColorTexture, // 将简单的纹理作为颜色 transparent: true, alphaMap: doorAlphaTexture, // 需要与transparent同时使用 aoMap: doorAmbientOcclusionTexture, // 需要提供uv2坐标支持 displacementMap: doorHeightTexture, // 使door更加立体,不仅仅是一个平面 displacementScale: 0.1, // 减小移动顶点的高度效应 normalMap: doorNormalTexture, // 法线贴图 metalnessMap: doorMetalnessTexture, roughnessMap: doorRoughnessTexture }) ) // support aoMap // door.geometry.attributes.uv 自动创建的uv坐标,2个值组成一个坐标值 door.geometry.setAttribute( 'uv2', new THREE.Float32BufferAttribute(door.geometry.attributes.uv.array, 2) ) door.position.y = 1 door.position.z = 2 + 0.01 // walls的深度 + 0.01,+0.01避免处在同一层级 house.add(door)
- walls
const bricksColorTexture = textureLoader.load('/imgs/haunted-house/bricks/color.jpg') const bricksAmbientTexture = textureLoader.load('/imgs/haunted-house/bricks/ambientOcclusion.jpg') const bricksNormalTexture = textureLoader.load('/imgs/haunted-house/bricks/normal.jpg') const bricksRoughnessTexture = textureLoader.load('/imgs/haunted-house/bricks/roughness.jpg')
// Walls const walls = new THREE.Mesh( new THREE.BoxGeometry(4, 2.5, 4), new THREE.MeshStandardMaterial({ map: bricksColorTexture, aoMap: bricksAmbientTexture, // uv2 normalMap: bricksNormalTexture, roughnessMap: bricksRoughnessTexture }) ) // support aoMap walls.geometry.setAttribute( 'uv2', new THREE.Float32BufferAttribute(walls.geometry.attributes.uv.array, 2) ) walls.position.y = 2.5 / 2 house.add(walls)
- floor
const grassColorTexture = textureLoader.load('/imgs/haunted-house/grass/color.jpg') const grassAmbientTexture = textureLoader.load('/imgs/haunted-house/grass/ambientOcclusion.jpg') const grassNormalTexture = textureLoader.load('/imgs/haunted-house/grass/normal.jpg') const grassRoughnessTexture = textureLoader.load('/imgs/haunted-house/grass/roughness.jpg')
// 仅添加以上纹理后会发现,grass的大小和house不匹配,继续做如下优化 // repeat grassColorTexture.repeat.set(8, 8) grassAmbientTexture.repeat.set(8, 8) grassNormalTexture.repeat.set(8, 8) grassRoughnessTexture.repeat.set(8, 8) // 避免repeat时拉伸最后一个像素,change the wrapS and wrapT properties grassColorTexture.wrapS = THREE.RepeatWrapping grassAmbientTexture.wrapS = THREE.RepeatWrapping grassNormalTexture.wrapS = THREE.RepeatWrapping grassRoughnessTexture.wrapS = THREE.RepeatWrapping grassColorTexture.wrapT = THREE.RepeatWrapping grassAmbientTexture.wrapT = THREE.RepeatWrapping grassNormalTexture.wrapT = THREE.RepeatWrapping grassRoughnessTexture.wrapT = THREE.RepeatWrapping
/** * Floor **/ const floor = new THREE.Mesh( new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardMaterial({ map: grassColorTexture, aoMap: grassAmbientTexture, // uv2 normalMap: grassNormalTexture, roughnessMap: grassRoughnessTexture }) ) // support aoMap floor.geometry.setAttribute( 'uv2', new THREE.Float32BufferAttribute(floor.geometry.attributes.uv.array, 2) ) floor.rotation.x = - Math.PI * 0.5 floor.position.y = 0 scene.add(floor)
-
Ghosts
- we are going to represent them with simple lights floating around the house and passing through the ground and graves
- animate these lights 让ghost以不同的频率围绕house旋转并上下起伏
- 这里的动画思路可以参考上面的graves的部分
/*
* Ghosts
*/
const ghost1 = new THREE.PointLight('#ff00ff', 2, 3)
scene.add(ghost1)
const ghost2 = new THREE.PointLight('#00ffff', 2, 3)
scene.add(ghost2)
const ghost3 = new THREE.PointLight('#ffff00', 2, 3)
scene.add(ghost3)
/*
* 渲染
*/
const clock = new THREE.Clock()
function animate () {
let elapsedTime = clock.getElapsedTime()
// Ghosts
const ghost1Angle = elapsedTime * 0.5
ghost1.position.x = Math.cos(ghost1Angle) * 4
ghost1.position.z = Math.sin(ghost1Angle) * 4
ghost1.position.y = Math.sin(elapsedTime * 3) // 上下起伏
const ghost2Angle = - elapsedTime * 0.32 // 设为负数,与ghost1反向动画
ghost2.position.x = Math.cos(ghost2Angle) * 5
ghost2.position.z = Math.sin(ghost2Angle) * 5
ghost2.position.y = Math.sin(elapsedTime * 4) + Math.sin(elapsedTime * 2.5)
const ghost3Angle = - elapsedTime * 0.18
ghost3.position.x = Math.cos(ghost3Angle) * (7 + Math.sin(elapsedTime * 0.32)) // 不固定旋转半径
ghost3.position.z = Math.sin(ghost3Angle) * (7 + Math.sin(elapsedTime * 0.5)) // 不固定旋转半径
ghost3.position.y = Math.sin(elapsedTime * 4) + Math.sin(elapsedTime * 2.5)
controls.update()
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
-
Shadows
- 我们将阴影设置统一放在一起方便管理
/** * Renderer **/ ... ... // Shadows ... 以下部分为shadows
- activate the shadow map on the renderer
// 开启场景中的阴影贴图 renderer.shadowMap.enabled = true
- activate the shadows on the lights that should cast shadows
moonLight.castShadow = true doorLight.castShadow = true ghost1.castShadow = true ghost2.castShadow = true ghost3.castShadow = true
- go through each objects of your scene and decide if that object can cast and/or receive shadows
walls.castShadow = true bush1.castShadow = true bush2.castShadow = true bush3.castShadow = true bush4.castShadow = true
/** * Graves **/ for(let i = 0; i < 50; i++) { ... ... grave.castShadow = true ... }
floor.receiveShadow = true // receive shadow
- optimize the shadow maps
doorLight.shadow.mapSize.width = 256 doorLight.shadow.mapSize.height = 256 doorLight.shadow.camera.far = 7 ghost1.shadow.mapSize.width = 256 ghost1.shadow.mapSize.height = 256 ghost1.shadow.camera.far = 7 ghost2.shadow.mapSize.width = 256 ghost2.shadow.mapSize.height = 256 ghost2.shadow.camera.far = 7 ghost3.shadow.mapSize.width = 256 ghost3.shadow.mapSize.height = 256 ghost3.shadow.camera.far = 7
- 改变阴影映射的算法
renderer.shadowMap.type = THREE.PCFSoftShadowMap