GLTFLoader 和 DRACOLoader
这是一个可以直接在 VitePress 页面中运行的 Three.js 示例。上方显示运行效果,下方展示对应源码。
运行效果
源码
js
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
const canvas = document.querySelector('canvas')
const demo = canvas.parentElement
const loading = document.createElement('div')
loading.className = 'model-loading'
loading.innerHTML = '<span></span><p>模型加载中...</p>'
loading.hidden = true
demo.appendChild(loading)
// 浏览器不能直接读取 public/models 目录,所以这里用数组维护可选择模型。
const modelOptions = [
{
name: 'ironman.glb',
path: withBase('/models/ironman.glb')
},
{
name: 'mega.glb',
path: withBase('/models/mega.glb')
},
{
name: 'porsche_911.glb',
path: withBase('/models/porsche_911.glb')
}
]
const scene = new THREE.Scene()
scene.background = new THREE.Color('#111827')
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 1.8, 4.5)
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
})
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
const ambientLight = new THREE.AmbientLight('#ffffff', 1.25)
const skyLight = new THREE.HemisphereLight('#e0f2fe', '#fef3c7', 1.8)
const sunLight = new THREE.DirectionalLight('#fff8dc', 5.2)
sunLight.position.set(5, 8, 4)
sunLight.castShadow = true
sunLight.shadow.mapSize.set(2048, 2048)
sunLight.shadow.camera.near = 0.5
sunLight.shadow.camera.far = 30
sunLight.shadow.camera.left = -6
sunLight.shadow.camera.right = 6
sunLight.shadow.camera.top = 6
sunLight.shadow.camera.bottom = -6
scene.add(ambientLight, skyLight, sunLight)
const shadowPlane = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10),
new THREE.ShadowMaterial({
color: '#475569',
opacity: 0.18
})
)
shadowPlane.rotation.x = -Math.PI / 2
shadowPlane.receiveShadow = true
scene.add(shadowPlane)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.target.set(0, 0.8, 0)
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
let currentModel = null
let mixer = null
let currentAction = null
let animationIndex = 0
let animationClips = []
let loadId = 0
const clock = new THREE.Clock()
const playAnimationByIndex = (index) => {
if (!mixer || !animationClips.length) {
return
}
const clip = animationClips[index]
const action = mixer.clipAction(clip)
if (currentAction) {
currentAction.stop()
}
action.reset()
action.setLoop(THREE.LoopOnce, 1)
action.clampWhenFinished = true
action.play()
currentAction = action
}
const resetAnimations = () => {
if (currentAction) {
currentAction.stop()
currentAction = null
}
if (mixer && currentModel) {
mixer.stopAllAction()
mixer.uncacheRoot(currentModel)
}
mixer = null
animationIndex = 0
animationClips = []
clock.stop()
clock.start()
}
const removeCurrentModel = () => {
if (!currentModel) {
return
}
resetAnimations()
scene.remove(currentModel)
currentModel = null
}
const moveModelCenterToOrigin = (model) => {
const box = new THREE.Box3().setFromObject(model)
const center = box.getCenter(new THREE.Vector3())
model.position.sub(center)
const fittedBox = new THREE.Box3().setFromObject(model)
shadowPlane.position.y = fittedBox.min.y - 0.02
}
const loadModel = (modelName) => {
const option = modelOptions.find((item) => item.name === modelName)
if (!option) {
return
}
const currentLoadId = loadId + 1
loadId = currentLoadId
loading.hidden = false
loading.querySelector('p').textContent = '模型加载中...'
removeCurrentModel()
gltfLoader.load(option.path, (gltf) => {
if (currentLoadId !== loadId) {
return
}
removeCurrentModel()
currentModel = gltf.scene
moveModelCenterToOrigin(currentModel)
currentModel.traverse((object) => {
if (object.isMesh) {
object.castShadow = true
object.receiveShadow = true
}
})
scene.add(currentModel)
// 按顺序播放模型自带的全部动画,最后一个结束后回到第一个。
if (gltf.animations.length) {
mixer = new THREE.AnimationMixer(currentModel)
animationClips = gltf.animations
mixer.addEventListener('finished', () => {
animationIndex = (animationIndex + 1) % animationClips.length
playAnimationByIndex(animationIndex)
})
playAnimationByIndex(animationIndex)
}
loading.hidden = true
}, (event) => {
if (currentLoadId !== loadId) {
return
}
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100)
loading.querySelector('p').textContent = `模型加载中... ${progress}%`
}
}, () => {
if (currentLoadId !== loadId) {
return
}
loading.querySelector('p').textContent = '模型加载失败'
})
}
const guiState = {
model: modelOptions[0].name
}
const gui = new GUI({
title: '模型控制',
autoPlace: false
})
gui.domElement.classList.add('three-gui')
demo.appendChild(gui.domElement)
gui
.add(guiState, 'model', modelOptions.map((item) => item.name))
.name('模型')
.onChange(loadModel)
loadModel(guiState.model)
function animate() {
requestAnimationFrame(animate)
const delta = clock.getDelta()
if (mixer) {
mixer.update(delta)
}
controls.update()
renderer.render(scene, camera)
}
animate()