Vue 3.0 + Three.js 学习总结

Vue 3.0 + Three.js 学习总结

最近在学习Three.js相关的技术,恰逢Vue 3.0正式版也已推出。现总结一下在Vue 3.0 + TypeScript中如何使用Three.js,如有不足,望在评论区说明。

项目使用Vue 3.0+ TypeScript+Ant Design Vue搭建。使用Options APIComposition API两种方式来使用Three.js。最终展示效果如图:

Vue 3.0 + Three.js 学习总结_第1张图片

Options API

<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>

Composition API

新建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.0Options API,这两种书写方式有以下不同:

Vue 3.0 + Three.js 学习总结_第2张图片

  • 使用Options API时,相同的逻辑写在不同的地方,各个逻辑的代码交叉错乱,这对维护别人代码的开发者来说绝不是一件简单的事,理清楚这些代码都需要花费不少时间。
  • 使用Composition API时,相同的逻辑可以写在同一个地方,这些逻辑甚至可以使用函数抽离出来,各个逻辑之间界限分明,即便维护别人的代码也不会在“读代码”上花费太多时间(前提是你的前任会写代码)。

必须指出的是,Composition API提高了代码的上限,也降低了代码的下限。在使用Options API时,即便再菜的鸟也能保证各种代码按其种类进行划分。使用Composition API时,由于其开放性,很容易写出面条式代码。毫无疑问,Options APIComposition APIVue的一个巨大进步,Vue从此可以从容面对大型项目。我们打工人在以后的项目中Options APIComposition API如何选择,如何更好得封装代码,这是一个一直都要考虑的问题。

源码地址

GitHub地址:https://github.com/wkl007/three-gallery

在线访问地址:https://wkl007.github.io/three-gallery

你可能感兴趣的:(vue学习总结,three.js,vue,three.js)