使用环境参考
Node.js v16.19.1
正文
独立功能文件
我们不可能一直在 index.ts 中写代码,分离文件:
// init.ts
import * as THREE from 'three'
export const initScene = () => {
const scene = new THREE.Scene()
scene.background = new THREE.Color('white')
const light = new THREE.AmbientLight('white', 1.3)
scene.add(light)
return scene
}
export const initCamera = () => {
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 3, 5)
camera.lookAt(new THREE.Vector3(0, 0, 0))
return camera
}
export const initWebGLRenderer = () => {
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
return renderer
}
// load.ts
import * as THREE from 'three'
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
export const loadGLTF = (url: string) => new Promise<THREE.Group>((resolve, reject) => {
const loader = new GLTFLoader()
loader.load(url, (gltf: GLTF) => {
console.log(gltf)
resolve(gltf.scene)
})
})
// index.ts
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { initCamera, initScene, initWebGLRenderer } from './init'
import { loadGLTF } from './load'
class Game {
scene: THREE.Scene
camera: THREE.PerspectiveCamera
renderer: THREE.WebGLRenderer
orbitControls: OrbitControls
constructor() {
this.scene = initScene()
this.camera = initCamera()
this.scene.add(this.camera)
this.renderer = initWebGLRenderer()
this.orbitControls = this.addOrbitControls(this.camera, this.renderer)
this.addModel()
this.addResizeEventListener()
}
addOrbitControls(camera: THREE.Camera, renderer: THREE.WebGLRenderer) {
const controls = new OrbitControls(camera, renderer.domElement)
controls.autoRotate = true
controls.enableDamping = true
controls.update()
return controls
}
async addModel() {
const model = await loadGLTF('gltf/SheenChair.glb')
this.scene.add(model)
}
addResizeEventListener() {
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
})
}
startMainLoop() {
// 等待一帧用于初始化
Promise.resolve().then(() => {
this.step()
})
}
step() {
requestAnimationFrame(this.step.bind(this))
this.orbitControls && this.orbitControls.update()
this.renderer.render(this.scene, this.camera)
}
}
const game = new Game()
game.startMainLoop()
射线检测
鼠标点击物体是最常见的一个需求,对 dom 新增点击事件,然后计算相对于 canvas 的坐标比例,计算进 three 的坐标系(-1 ~ 1)。
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { initCamera, initScene, initWebGLRenderer } from './init'
import { loadGLTF } from './load'
class Game {
scene: THREE.Scene
camera: THREE.PerspectiveCamera
renderer: THREE.WebGLRenderer
orbitControls: OrbitControls
raycaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
constructor() {
this.scene = initScene()
this.camera = initCamera()
this.scene.add(this.camera)
this.renderer = initWebGLRenderer()
this.orbitControls = this.addOrbitControls(this.camera, this.renderer)
this.addModel()
this.addResizeEventListener()
this.addClickEvent()
}
addClickEvent() {
this.renderer.domElement.addEventListener('click', (ev) => {
this.mouse.x = (ev.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1
this.raycaster.setFromCamera(this.mouse, this.camera)
const intersects = this.raycaster.intersectObject(this.scene, true)
console.log(intersects)
})
}
addOrbitControls(camera: THREE.Camera, renderer: THREE.WebGLRenderer) {
const controls = new OrbitControls(camera, renderer.domElement)
controls.autoRotate = true
controls.enableDamping = true
controls.update()
return controls
}
async addModel() {
const model = await loadGLTF('gltf/SheenChair.glb')
this.scene.add(model)
}
addResizeEventListener() {
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
})
}
startMainLoop() {
// 等待一帧用于初始化
Promise.resolve().then(() => {
this.step()
})
}
step() {
requestAnimationFrame(this.step.bind(this))
this.orbitControls && this.orbitControls.update()
this.renderer.render(this.scene, this.camera)
}
}
const game = new Game()
game.startMainLoop()
动画轨道
鼠标点击某个模型,对这个模型进行动画处理。
新建一个 animation.ts 负责处理动画
// animation.ts
import * as THREE from 'three'
export class AnimationManager {
mixers: THREE.AnimationMixer[] = []
clock = new THREE.Clock()
addOnePosAnima(obj: THREE.Object3D) {
const positionTimes = [0, 1, 2]
const positionArr = [
0, 0, 0,
0, 1, 0,
0, 0, 0
]
const track = new THREE.VectorKeyframeTrack(
`${obj.name}.position`,
positionTimes,
positionArr,
THREE.InterpolateSmooth
)
// 动画名称,持续时间,track
const clip = new THREE.AnimationClip('clip1', 2, [track])
const mixer = new THREE.AnimationMixer(obj)
const action = mixer.clipAction(clip)
action.play()
this.mixers.push(mixer)
}
step() {
const dt = this.clock.getDelta()
this.mixers.forEach(m => m.update(dt))
}
}
在 index.ts 中,点击到哪儿模型就为其添加一个动画。
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { initCamera, initScene, initWebGLRenderer } from './init'
import { loadGLTF } from './load'
import { AnimationManager } from './animation'
class Game {
scene: THREE.Scene
camera: THREE.PerspectiveCamera
renderer: THREE.WebGLRenderer
orbitControls: OrbitControls
raycaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
animationManager = new AnimationManager()
constructor() {
this.scene = initScene()
this.camera = initCamera()
this.scene.add(this.camera)
this.renderer = initWebGLRenderer()
this.orbitControls = this.addOrbitControls(this.camera, this.renderer)
this.addModel()
this.addResizeEventListener()
this.addClickEvent()
}
addClickEvent() {
this.renderer.domElement.addEventListener('click', (ev) => {
this.mouse.x = (ev.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1
this.raycaster.setFromCamera(this.mouse, this.camera)
const intersects = this.raycaster.intersectObject(this.scene, true)
console.log(intersects)
if (intersects[0]) {
this.animationManager.addOnePosAnima(intersects[0].object)
}
})
}
addOrbitControls(camera: THREE.Camera, renderer: THREE.WebGLRenderer) {
const controls = new OrbitControls(camera, renderer.domElement)
controls.autoRotate = true
controls.enableDamping = true
controls.update()
return controls
}
async addModel() {
const model = await loadGLTF('gltf/SheenChair.glb')
this.scene.add(model)
}
addResizeEventListener() {
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
})
}
startMainLoop() {
// 等待一帧用于初始化
Promise.resolve().then(() => {
this.step()
})
}
step() {
requestAnimationFrame(this.step.bind(this))
this.orbitControls && this.orbitControls.update()
this.animationManager.step()
this.renderer.render(this.scene, this.camera)
}
}
const game = new Game()
game.startMainLoop()
要注意模型是由多个子 Mesh 组成,如果想动整体,查询 parent 或者提前用变量存好或者记录 name 进行查找。
更多文章与分享
Three 学习项目链接:https://github.com/KuoKuo666/threejs-study
个人网站:www.kuokuo666.com
2023!Day Day Up!