重生之我在浏览器里“蹦迪”

大家好,我是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()
}
相关推荐
m0_639817152 小时前
基于springboot纺织品企业财务管理系统【带源码和文档】
java·服务器·前端
石小石Orz2 小时前
qinkun的缓存机制也有弊端,建议官方个参数控制
前端
用户9714171814272 小时前
Vue3实现拖拽排序
javascript·vue.js
用户4099322502122 小时前
Vue浅响应式如何解决深层响应式的性能问题?适用场景有哪些?
前端·ai编程·trae
阡陌昏晨2 小时前
H5性能优化-打开效率提升了62%
前端·javascript·vue.js
鹏北海2 小时前
TypeScript 类型工具与 NestJS Mapped Types
前端·后端·typescript
烟袅2 小时前
一文搞懂 CSS 定位:relative、absolute、fixed、sticky
前端·css
孟祥_成都2 小时前
你离前端动画高手只差这个秘籍!GSAP ScrollTrigger 动画完全指南!(第一章)
前端·动效
小小前端_我自坚强2 小时前
React 18 新特性深度解析
前端·javascript·react.js