最近在学习Three.js
相关的技术,恰逢Vue 3.0
正式版也已推出。现总结一下在Vue 3.0
+ TypeScript
中如何使用Three.js
,如有不足,望在评论区说明。
项目使用Vue 3.0
+ TypeScript
+Ant Design Vue
搭建。使用Options API
与Composition API
两种方式来使用Three.js
。最终展示效果如图:
<template>
<div id="three" ref="three"></div>
</template>
<script lang="ts">
import type {
AmbientLight,
AxesHelper,
Clock,
DirectionalLight,
Mesh,
OrthographicCamera,
Scene,
WebGLRenderer
} from 'three'
import { defineComponent } from 'vue'
import * as THREE from 'three'
import CameraControls from 'camera-controls'
import Stats from 'stats.js'
CameraControls.install({ THREE })
const x: number = window.innerWidth
const y: number = window.innerHeight
const pixelRatio: number = window.devicePixelRatio
const s = 200 // 三维场景显示范围控制系数,系数越大,显示的范围越大
let scene: Scene,
camera: OrthographicCamera,
renderer: WebGLRenderer,
point: DirectionalLight,
ambient: AmbientLight,
axesHelper: AxesHelper,
mesh: Mesh,
stats: Stats,
animation: number,
three: HTMLElement,
cameraControls: CameraControls
const clock: Clock = new THREE.Clock()
export default defineComponent({
name: 'gallery1',
mounted () {
three = this.$refs.three as HTMLElement
this.init(three)
window.addEventListener('resize', this.windowResize)
},
beforeUnmount () {
window.addEventListener('resize', this.windowResize)
cancelAnimationFrame(animation)
cameraControls.dispose()
},
methods: {
// 初始化
init (el: HTMLElement) {
this.initScene()
this.initCamera()
this.initRenderer(el)
this.initLight()
this.initModel()
this.initControls()
this.initHelpers()
this.initStats(el)
this.render()
},
// 初始化场景
initScene () {
scene = new THREE.Scene()
},
// 初始化相机
initCamera () {
// 正投影相机
const k: number = x / y // 窗口宽高比
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000)
camera.position.set(200, 300, 200)
camera.lookAt(scene.position)
},
// 初始化渲染器
initRenderer (el: HTMLElement) {
renderer = new THREE.WebGLRenderer()
renderer.setPixelRatio(pixelRatio) // 设置dpr
renderer.setSize(x, y) // 设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff) // 设置背景颜色
el && el.appendChild(renderer.domElement)
},
// 初始化光源
initLight () {
// 点光源
point = new THREE.DirectionalLight(0xffffff)
point.position.set(400, 200, 300)
scene.add(point)
// 环境光
ambient = new THREE.AmbientLight(0x444444)
scene.add(ambient)
},
// 初始化轨道控制插件
initControls () {
cameraControls = new CameraControls(camera, renderer.domElement)
cameraControls.draggingDampingFactor = 1 // 拖动阻尼惯性
},
// 初始化辅助内容
initHelpers () {
axesHelper = new THREE.AxesHelper(250)
scene.add(axesHelper)
},
// 初始化性能检测插件
initStats (el: HTMLElement) {
stats = new Stats()
stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
stats.dom.style.position = 'absolute'
stats.dom.style.left = '0px'
stats.dom.style.top = '0px'
el && el.appendChild(stats.dom)
},
// 初始化模型
initModel () {
const geometry = new THREE.SphereGeometry(100, 25, 25)
const material = new THREE.MeshPhongMaterial({
color: 0xff00ff,
specular: 0x4488ee,
shininess: 20
})
mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
},
// 渲染
render () {
renderer.render(scene, camera)
stats.update()
cameraControls.update(clock.getDelta())
animation = requestAnimationFrame(this.render)
},
// 窗口缩放事件
windowResize () {
// TODO 窗口滚动事件添加截流函数
const innerWidth: number = window.innerWidth
const innerHeight: number = window.innerHeight
// 重置正投影相机相关参数
const k: number = innerWidth / innerHeight // 窗口宽高比
camera.left = -s * k
camera.right = s * k
camera.top = s
camera.bottom = -s
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix()
renderer.setSize(innerWidth, innerHeight)
}
}
})
</script>
新建hooks/index.ts
文件
import * as THREE from 'three'
import type { AmbientLight, AxesHelper, Clock, DirectionalLight, OrthographicCamera, Scene, WebGLRenderer } from 'three'
import CameraControls from 'camera-controls'
import Stats from 'stats.js'
CameraControls.install({ THREE })
interface UseThree {
initScene: () => Scene;
initCamera: () => OrthographicCamera;
initRenderer: (el: HTMLElement) => WebGLRenderer;
initLight: () => void;
initControls: () => CameraControls;
initClock: () => Clock;
initHelpers: () => void;
initStats: (el: HTMLElement) => Stats;
windowResize: () => void;
}
/**
* Three.js hooks
* @param scene
* @param camera
* @param renderer
* @param point
* @param ambient
* @param axesHelper
* @param stats
* @param cameraControls
* @param clock
*/
export function useThree (scene: Scene, camera: OrthographicCamera, renderer: WebGLRenderer, point: DirectionalLight, ambient: AmbientLight, axesHelper: AxesHelper, stats: Stats, cameraControls: CameraControls, clock: Clock): UseThree {
const x: number = window.innerWidth // 宽
const y: number = window.innerHeight // 高
const pixelRatio: number = window.devicePixelRatio // dpr
const s = 200 // 三维场景显示范围控制系数,系数越大,显示的范围越大
// 初始化场景
function initScene () {
scene = new THREE.Scene()
return scene
}
// 初始化创建
function initCamera () {
// 正投影相机
const k = x / y
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000)
camera.position.set(200, 200, 200)
camera.lookAt(scene.position)
return camera
}
// 初始化渲染器
function initRenderer (el: HTMLElement) {
renderer = new THREE.WebGLRenderer()
renderer.setPixelRatio(pixelRatio) // 设置dpr
renderer.setSize(x, y) // 设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff) // 设置背景颜色
el.appendChild(renderer.domElement)
return renderer
}
// 初始化光源
function initLight () {
// 点光源
point = new THREE.DirectionalLight(0xffffff)
point.position.set(400, 200, 300)
scene.add(point)
// 环境光
ambient = new THREE.AmbientLight(0x444444)
scene.add(ambient)
}
// 初始化轨道控制插件
function initControls () {
cameraControls = new CameraControls(camera, renderer.domElement)
cameraControls.draggingDampingFactor = 1 // 拖动阻尼惯性
return cameraControls
}
// 初始化clock
function initClock () {
clock = new THREE.Clock()
return clock
}
// 初始化辅助内容
function initHelpers () {
axesHelper = new THREE.AxesHelper(250)
scene.add(axesHelper)
}
// 初始化性能检测插件
function initStats (el: HTMLElement) {
stats = new Stats()
stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom
stats.dom.style.position = 'absolute'
stats.dom.style.left = '0px'
stats.dom.style.top = '0px'
el && el.appendChild(stats.dom)
return stats
}
// 窗口缩放事件
function windowResize () {
// TODO 窗口滚动事件添加截流函数
const innerWidth: number = window.innerWidth
const innerHeight: number = window.innerHeight
// 重置正投影相机相关参数
const k: number = innerWidth / innerHeight // 窗口宽高比
camera.left = -s * k
camera.right = s * k
camera.top = s
camera.bottom = -s
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix()
renderer.setSize(innerWidth, innerHeight)
}
return {
initScene,
initCamera,
initRenderer,
initLight,
initControls,
initClock,
initHelpers,
initStats,
windowResize
}
}
在页面中使用
<template>
<div id="three" ref="three"></div>
</template>
<script lang="ts">
import type {
AmbientLight,
AxesHelper,
Clock,
DirectionalLight,
Mesh,
OrthographicCamera,
Scene,
WebGLRenderer
} from 'three'
import * as THREE from 'three'
import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'
import Stats from 'stats.js'
import CameraControls from 'camera-controls'
import { useThree } from '@/hooks'
let scene: Scene,
camera: OrthographicCamera,
renderer: WebGLRenderer,
point: DirectionalLight,
ambient: AmbientLight,
axesHelper: AxesHelper,
mesh: Mesh,
cameraControls: CameraControls,
clock: Clock,
stats: Stats,
animation: number
export default defineComponent({
name: 'gallery2',
setup () {
const three = ref<HTMLElement>(document.createElement('div'))
const {
initScene,
initCamera,
initRenderer,
initLight,
initHelpers,
initStats,
windowResize,
initControls,
initClock
} = useThree(scene, camera, renderer, point, ambient, axesHelper, stats, cameraControls, clock)
// 初始化模型
function initModel () {
const geometry = new THREE.SphereGeometry(100, 25, 25)
const material = new THREE.MeshPhongMaterial({
color: 0xff00ff,
specular: 0x4488ee,
shininess: 20
})
mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
}
// 渲染
function render () {
scene && renderer.render(scene, camera)
stats && stats.update()
cameraControls && cameraControls.update(clock.getDelta())
animation = requestAnimationFrame(render)
}
// 初始化
function init (el: HTMLElement) {
scene = initScene()
camera = initCamera()
renderer = initRenderer(el)
stats = initStats(el)
cameraControls = initControls()
clock = initClock()
initLight()
initHelpers()
initModel()
render()
}
onMounted(() => {
const el = three.value
init(el)
window.addEventListener('resize', windowResize)
})
onBeforeUnmount(() => {
cancelAnimationFrame(animation)
cameraControls && cameraControls.dispose()
window.removeEventListener('resize', windowResize)
})
return {
three
}
}
})
</script>
Vue 3.0
相比Vue 2.0
有了很多新特性,具体变化可以查看官网 从Vue 2.0迁移 。其中最吸引人的就是Composition API
,相比 Vue 2.0
的Options API
,这两种书写方式有以下不同:
Options API
时,相同的逻辑写在不同的地方,各个逻辑的代码交叉错乱,这对维护别人代码的开发者来说绝不是一件简单的事,理清楚这些代码都需要花费不少时间。Composition API
时,相同的逻辑可以写在同一个地方,这些逻辑甚至可以使用函数抽离出来,各个逻辑之间界限分明,即便维护别人的代码也不会在“读代码”上花费太多时间(前提是你的前任会写代码)。必须指出的是,Composition API
提高了代码的上限,也降低了代码的下限。在使用Options API
时,即便再菜的鸟也能保证各种代码按其种类进行划分。使用Composition API
时,由于其开放性,很容易写出面条式代码。毫无疑问,Options API
到Composition API
是Vue
的一个巨大进步,Vue
从此可以从容面对大型项目。我们打工人在以后的项目中Options API
与Composition API
如何选择,如何更好得封装代码,这是一个一直都要考虑的问题。
GitHub地址:https://github.com/wkl007/three-gallery
在线访问地址:https://wkl007.github.io/three-gallery