使用环境参考
Node.js v16.19.1
正文
独立功能文件
我们不可能一直在 index.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 }
|
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) }) })
|
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 负责处理动画
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 ) 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!