大家好,我是CC,在这里欢迎大家的到来~
工作时机械麻木,蹦迪时欢乐激情,边工作边蹦迪,是怎样的呢?
这个想法源于我初入职场时对于 3D 的好奇和蹦迪的憧憬,刷着网上的视频,想着不如实现自己的蹦迪主场。
蹦迪舞台
实现流程
基于 Three.js 实现,整合舞动的人形模型、摇摆聚光灯、固定的点灯和嗨翻天的背景音乐。
创建 canvas
javascript
<template>
<canvas id="djClub"></canvas>
</template>
添加三要素
场景、相机、渲染器统称为 3D 场景下的三要素。
javascript
const canvas = document.getElementById("djClub")
// 场景
scene = new Scene()
scene.background = new Color(0xdcdcdc)
// 相机
camera = new PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(77.59014070493724, 27.281956522211484, 173.9190071215408)
// 渲染器
renderer = new WebGLRenderer({
canvas,
antialias: true,
alpha: true,
})
renderer.shadowMap.enabled = true
renderer.shadowMap.type = PCFSoftShadowMap // 默认 PCFShadowMap // 阴影类型
renderer.setClearColor(0xffffff)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
添加素材
素材包括房屋、草地、前后舞台和天花板。
javascript
// 雾
scene.fog = new Fog(0xdcdcdc, 200, 1000)
// 创建纹理贴图加载器
textureLoader = new TextureLoader()
// 添加长方体草地
const addGrassland = function() {
const geometry = new BoxGeometry(landSide, 10, landSide * 2)
const materials = [
new MeshBasicMaterial({
map: textureLoader.load('/mesh/around.png'),
side: DoubleSide
}),
new MeshBasicMaterial({
map: textureLoader.load('/mesh/around.png'),
side: DoubleSide
}),
new MeshPhongMaterial({
map: textureLoader.load('/mesh/grasslight-big.jpg'),
side: DoubleSide
}),
new MeshBasicMaterial({
map: textureLoader.load('/mesh/bottom.png'),
side: DoubleSide
}),
new MeshBasicMaterial({
map: textureLoader.load('/mesh/around.png'),
side: DoubleSide
}),
new MeshBasicMaterial({
map: textureLoader.load('/mesh/around.png'),
side: DoubleSide
}),
]
grassland = new Mesh(geometry, materials)
grassland.position.y = -5
grassland.name = 'grassland'
grassland.receiveShadow = true // 开启接收阴影投影
scene.add(grassland)
}
// 添加前置舞台
const addFrontStage = function() {
const geometry = new PlaneGeometry(200, 60)
const gt = textureLoader.load('/mesh/front.png')
const materials = new MeshPhongMaterial({ color: 0xffffff, map: gt, side: DoubleSide })
const frontStage = new Mesh(geometry, materials)
frontStage.rotation.x = Math.PI * 2
frontStage.rotation.y = Math.PI
frontStage.position.z = 200
frontStage.position.y = 30
scene.add(frontStage)
}
// 添加后置舞台
const addBackStage = function() {
const geometry = new PlaneGeometry(200, 60)
const gt = textureLoader.load('/mesh/back.png')
const materials = new MeshPhongMaterial({ color: 0xffffff, map: gt, side: DoubleSide })
const backStage = new Mesh(geometry, materials)
backStage.rotation.x = Math.PI * 2
backStage.position.z = -200
backStage.position.y = 30
scene.add(backStage)
}
// 添加左右墙壁
const addLeftRightStage = function() {
const geometry = new PlaneGeometry(400, 60)
const gt = textureLoader.load('/mesh/hardwood2_diffuse.jpg')
const materials = new MeshPhongMaterial({ color: 0xffffff, map: gt, side: DoubleSide })
const leftStage = new Mesh(geometry, materials)
leftStage.rotation.y = Math.PI / 2
leftStage.position.x = 100
leftStage.position.y = 30
leftStage.position.z = 0
scene.add(leftStage)
const rightStage = new Mesh(geometry, materials)
rightStage.rotation.y = Math.PI / 2
rightStage.position.x = -100
rightStage.position.y = 30
rightStage.position.z = 0
scene.add(rightStage)
}
// 添加天花板
const addUpStage = function() {
const geometry = new PlaneGeometry(400, 200)
const gt = textureLoader.load('/mesh/up.png')
const materials = new MeshPhongMaterial({ color: 0xffffff, map: gt, side: DoubleSide })
const frontStage = new Mesh(geometry, materials)
frontStage.rotation.x = Math.PI / 2
frontStage.rotation.z = Math.PI / 2
frontStage.position.z = 0
frontStage.position.y = 60
scene.add(frontStage)
}
加载人形模型蹦迪
使用 Three.js 官网上的模型,且自带了一些动作。
javascript
// 模型站位坐标数据
export const modelPosition = [
{
path: '',
x: 0,
y: 0,
z: 50,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: 40,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: 30,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: 20,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: 10,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: 0,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: -10,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: -20,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: -30,
index: 0,
},
{
path: '',
x: 0,
y: 0,
z: -40,
index: 0,
},
{
path: '',
x: 10,
y: 0,
z: 40,
index: 0,
},
{
path: '',
x: 20,
y: 0,
z: 30,
index: 0,
},
{
path: '',
x: 30,
y: 0,
z: 20,
index: 0,
},
{
path: '',
x: 40,
y: 0,
z: 10,
index: 0,
},
{
path: '',
x: -40,
y: 0,
z: 10,
index: 0,
},
{
path: '',
x: -30,
y: 0,
z: 20,
index: 0,
},
{
path: '',
x: -20,
y: 0,
z: 30,
index: 0,
},
{
path: '',
x: -10,
y: 0,
z: 40,
index: 0,
},
]
javascript
// 加载模型
const loadModels = function(path: string) {
const loader = new FBXLoader()
loader.load(path, (obj: any) => {
obj.traverse((child : Mesh) => {
if (child.isMesh) {
child.castShadow = true
child.receiveShadow = true
}
})
let models: any[] = []
modelPosition.forEach(item => {
const model1 = SkeletonUtils.clone(obj)
const mixer1 = new AnimationMixer(model1)
mixer1.clipAction(obj.animations[0]).play()
model1.position.set(item.x, item.y, item.z)
model1.scale.set(0.08, 0.08, 0.08)
models.push(model1)
mixers.push(mixer1)
})
scene.add(...models)
})
}
加载花式光源
光源才是可以烘托气氛的,于是加了很多光源。
聚光灯是不断旋转角度的,还有一些固定点位的点光源起到微照明的作用。
javascript
// 聚光灯光源信息坐标
export const spotLights = [
{
name: '幕布左上角第一个',
spotLight: {
color: 0xff0000,
intensity: 100,
distance: 200,
angle: Math.PI / 36,
penumbra: 0.05,
decay: 2
},
position: {
x: 50,
y: 60,
z: 100
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
name: '幕布左上角第二个',
spotLight: {
color: 0x54FF9F,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: 25,
y: 60,
z: 100
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0x483D8B,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: 0,
y: 60,
z: 100
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0x1E90FF,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: -25,
y: 60,
z: 100
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0xffff00,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: -50,
y: 60,
z: 100
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0x1E90FF,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: 50,
y: 60,
z: 50
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0x1E90FF,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: 50,
y: 60,
z: 0
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0xffff00,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: 50,
y: 60,
z: -50
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0xffff00,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: 50,
y: 60,
z: -100
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0x1E90FF,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: -50,
y: 60,
z: 50
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0x1E90FF,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: -50,
y: 60,
z: 0
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0xffff00,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: -50,
y: 60,
z: -50
},
range: {
x: 100,
y: 60,
z: 200
}
},
{
spotLight: {
color: 0xffff00,
intensity: 10,
distance: 200,
angle: Math.PI / 36,
penumbra: 1,
decay: 2
},
position: {
x: -50,
y: 60,
z: -100
},
range: {
x: 100,
y: 60,
z: 200
}
}
]
// 点光源信息坐标
export const pointLights = [
{
pointLight: {
color: 0xFFF68F,
intensity: 10,
distance: 200,
decay: 10
},
position: {
x: 37.5,
y: 55,
z: 180
}
},
{
pointLight: {
color: 0xFFF68F,
intensity: 10,
distance: 200,
decay: 10
},
position: {
x: -37.5,
y: 55,
z: 180
}
},
{
pointLight: {
color: 0xFFF68F,
intensity: 10,
distance: 200,
decay: 10
},
position: {
x: 37.5,
y: 55,
z: -180
}
},
{
pointLight: {
color: 0xFFF68F,
intensity: 10,
distance: 200,
decay: 10
},
position: {
x: -37.5,
y: 55,
z: -180
}
},
{
pointLight: {
color: 0xFFF68F,
intensity: 10,
distance: 200,
decay: 10
},
position: {
x: 60,
y: 55,
z: 0
}
},
{
pointLight: {
color: 0xFFF68F,
intensity: 10,
distance: 200,
decay: 10
},
position: {
x: -60,
y: 55,
z: 0
}
}
]
javascript
// 添加聚光灯
const addSpotLight = function() {
spotLights.forEach(item => {
const spotLightRed = new SpotLight(item.spotLight.color, item.spotLight.intensity, item.spotLight.distance, item.spotLight.angle, item.spotLight.penumbra, item.spotLight.decay)
spotLightRed.position.set(item.position.x, item.position.y, item.position.z)
spotLightRed.target = grassland
spotLightRed.castShadow = true
spotLightRed.shadow.mapSize.width = 1024
spotLightRed.shadow.mapSize.height = 1024
// 设置计算阴影的区域,注意包裹对象的周围
spotLightRed.shadow.camera.near = 1
spotLightRed.shadow.camera.far = 300
spotLightRed.shadow.camera.fov = 20
spotLightRed.shadow.focus = 1
scene.add(spotLightRed)
spotObject.push(spotLightRed)
const spotLightRedHelper = new SpotLightHelper(spotLightRed)
spotObjectHelper.push(spotLightRedHelper)
})
}
// 聚光灯动画
const spotLightTween = function(light: SpotLight, spot: any) {
new Tween(light).to({
angle: (Math.random() * 0.7) + 0.1,
penumbra: Math.random() + 1
}, Math.random() * 3000 + 2000).easing(Easing.Quadratic.Out).start()
new Tween(light.position).to({
x: (Math.random() * spot.range.x) * (Math.floor(Math.random() * 100) % 2 == 0 ? 1 : -1),
y: spot.range.y,
z: (Math.random() * spot.range.z) * (Math.floor(Math.random() * 100) % 2 == 0 ? 1 : -1)
}, Math.random() * 3000 + 2000).easing(Easing.Quadratic.Out).start()
}
// 多个聚光灯动画执行
const spotLightAnimate = function() {
for(let i = 0; i < spotLights.length; i++) {
spotLightTween(spotObject[i], spotLights[i])
}
setTimeout(spotLightAnimate, 1000)
}
// 添加点光源 灯泡
const addPointLight = function() {
pointLights.forEach(item => {
let pointLight = new PointLight(item.pointLight.color, item.pointLight.intensity, item.pointLight.distance, item.pointLight.decay)
pointLight.position.set(item.position.x, item.position.y, item.position.z)
scene.add(pointLight)
})
}
在调整光源时可以使用光源辅助线进行位置的调整。
javascript
// 添加辅助线
const addHelper = function(sunLight : DirectionalLight) {
// 添加辅助线
const gridHelper: any = new GridHelper(landSide, landSide);
gridHelper.material.opacity = 0;
gridHelper.material.transparent = true;
scene.add(gridHelper);
// 添加三维箭头坐标
// X
let dirX = new Vector3(10, 0, 0) // 三维向量
dirX.normalize()
let origin = new Vector3(0, 0, 0)
let length = 1
let hexX = 0xff0000
let arrowHelperX = new ArrowHelper(dirX, origin, length, hexX)
scene.add(arrowHelperX)
// Y
let dirY = new Vector3(0, 10, 0) // 三维向量
dirX.normalize()
let hexY = 0xffff00
let arrowHelperY = new ArrowHelper(dirY, origin, length, hexY)
scene.add(arrowHelperY)
// Z
let dirZ = new Vector3(0, 0, 10) // 三维向量
dirZ.normalize()
let hexZ = 0x0000ff
let arrowHelperZ = new ArrowHelper(dirZ, origin, length, hexZ)
scene.add(arrowHelperZ)
// 简单模拟3个坐标轴的对象 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴
let axesHelper = new AxesHelper(landSide / 2)
scene.add(axesHelper)
// 模拟相机视锥体的辅助对象
let cameraHelper = new CameraHelper(sunLight.shadow.camera)
scene.add(cameraHelper)
}
添加播放嗨曲
javascript
document.documentElement.addEventListener('dblclick', addMusic)
// 添加音乐
const addMusic = function() {
let listener = new AudioListener()
camera.add(listener)
let sound = new PositionalAudio(listener)
let audioLoader = new AudioLoader()
audioLoader.load('/music/wavefile_short.mp3', (buffer: any) => {
sound.setBuffer(buffer)
sound.setRefDistance(100)
sound.play()
})
const sphere = new SphereGeometry(20, 32, 16, 0, 3.11645991236108, 6.283185307179586, 3.19185813604723)
const material = new MeshPhongMaterial({ color: 0xffffff, side: DoubleSide })
const mesh = new Mesh(sphere, material)
mesh.position.set(0, 60, 0)
mesh.rotation.x = Math.PI / 2
scene.add(mesh)
grassland.add(sound)
document.documentElement.removeEventListener('dblclick', addMusic)
}
蹦起来
javascript
// 创建时钟
clock = new Clock()
// 渲染
const render = function() {
stats.begin()
let delta = clock.getDelta()
// 模型动画的执行更新
for ( const mixer of mixers ) mixer.update(delta)
renderer.render(scene, camera)
TWEEN.update()
for(let i = 0; i < spotObjectHelper.length; i++) {
spotObjectHelper[i].update()
}
stats.end()
requestAnimationFrame(render)
}
性能监控
3D 场景下对浏览器和电脑的性能要求更高,可以通过性能监控判断来进行优化。
依赖于 stats.js,在 render 时进行进行监听。
javascript
import Stats from 'stats.js'
// 性能监视器
const loadStats = function() {
stats = new Stats()
stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom)
}
const render = function() {
stats.begin()
// 监听内容代码...
stats.end()
requestAnimationFrame(render)
}
尽善尽美
页面响应式才可以有更好的体验。
javascript
window.addEventListener('resize', () => onWindowResize(), false)
// 响应式
const onWindowResize = function() {
renderer.setSize(window.innerWidth, window.innerHeight)
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
}