Three.js+TypeScript+Webpack学习记录(三)

使用环境参考

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!

Three.js+TypeScript+Webpack学习记录(三)

https://www.kuokuo666.com/home/kk056.html

作者

KUOKUO众享

发布于

2023-04-23

更新于

2024-03-05

许可协议

评论