[譯] JavaScript 線性代數:使用 ThreeJS 製做線性變換動畫

JavaScript 線性代數:使用 ThreeJS 製做線性變換動畫

本文是「JavaScript 線性代數」教程的一部分。javascript

最近我完成了一篇關於使用 JavaScript 進行線性變換的文章,並用 SVG 網格實現了 2D 的示例。你能夠在此處查看以前的文章。可是,那篇文章沒有三維空間的示例,所以本文將補全那篇文章的缺失。你能夠在此處查看本系列文章的 GitHub 倉庫,與本文相關的 commit 能夠在此處查看。前端

目標

在本文中,咱們將製做一個組件,用於對三維空間的對象的線性變換進行可視化。最終效果以下面的動圖所示,或者你也能夠在此網頁體驗。java

applying different linear transformations on cube

組件

當咱們要在瀏覽器中製做 3D 動畫時,第一個想到的固然就是 three.js 庫啦。因此讓咱們來安裝它以及另外一個可讓用戶移動攝像機的庫:react

npm install --save three three-orbitcontrols
複製代碼

下面構建一個組件,它能夠由父組件的屬性中接收矩陣,而且渲染一個立方體的轉換動畫。下面代碼展現了這個組件的結構。咱們用 styled-componentsreact-sizeme 庫中的函數對這個組件進行了包裝,以訪問顏色主題和檢測組件尺寸的變化。android

import React from 'react'
import { withTheme } from 'styled-components'
import { withSize } from 'react-sizeme'

class ThreeScene extends React.Component {
  constructor(props) {}
  render() {}

  componentDidMount() {}

  componentWillUnmount() {}

  animate = () => {}

  componentWillReceiveProps({ size: { width, height } }) {}
}

const WrappedScene = withTheme(withSize({ monitorHeight: true })(ThreeScene))
複製代碼

構造函數中,咱們對狀態進行了初始化,其中包括了視圖的大小。所以,咱們當接收新的狀態值時,能夠在 componentWillReceiveProps 方法中與初始狀態進行對比。因爲須要訪問實際的 DOM 元素以注入 ThreeJSrenderer,所以須要在 render 方法中用到 ref 屬性:ios

const View = styled.div` width: 100%; height: 100%; `
class ThreeScene extends React.Component {
  // ...
  constructor(props) {
    super(props)
    this.state = {
      width: 0,
      height: 0
    }
  }
  
  render() {
    return <View ref={el => (this.view = el)} /> } // ... } 複製代碼

componentDidMount 方法中,咱們對方塊變換動畫所須要的全部東西都進行了初始化。首先,咱們建立了 ThreeJS 的場景(scene)並肯定好攝像機(camera)的位置,而後咱們建立了 ThreeJS 的 renderer,爲它設置好了顏色及大小,最後將 renderer 加入到 View 組件中。git

接下來建立須要進行渲染的對象:座標軸、方塊以及方塊的邊。因爲咱們須要手動改變矩陣,所以將方塊和邊的 matrixAutoUpdate 屬性設爲 false。建立好這些對象後,將它們加入場景(scene)中。爲了讓用戶能夠經過鼠標來移動攝像機位置,咱們還用到了 OrbitControlsgithub

最後要作的,就是將咱們的庫輸出的矩陣轉換成 ThreeJS 的格式,而後獲取根據時間返回顏色和轉換矩陣的函數。在 componentWillUnmount,取消動畫(即中止 anime frame)並從 DOM 移除 renderernpm

class ThreeScene extends React.Component {
  // ...
  componentDidMount() {
    const {
      size: { width, height },
      matrix,
      theme
    } = this.props
    this.setState({ width, height })
    this.scene = new THREE.Scene()
    this.camera = new THREE.PerspectiveCamera(100, width / height)
    this.camera.position.set(1, 1, 4)

    this.renderer = new THREE.WebGLRenderer({ antialias: true })
    this.renderer.setClearColor(theme.color.background)
    this.renderer.setSize(width, height)
    this.view.appendChild(this.renderer.domElement)

    const initialColor = theme.color.red
    const axes = new THREE.AxesHelper(4)
    const geometry = new THREE.BoxGeometry(1, 1, 1)
    this.segments = new THREE.LineSegments(
      new THREE.EdgesGeometry(geometry),
      new THREE.LineBasicMaterial({ color: theme.color.mainText })
    )
    this.cube = new THREE.Mesh(
      geometry,
      new THREE.MeshBasicMaterial({ color: initialColor })
    )
    this.objects = [this.cube, this.segments]
    this.objects.forEach(obj => (obj.matrixAutoUpdate = false))
    this.scene.add(this.cube, axes, this.segments)

    this.controls = new OrbitControls(this.camera)

    this.getAnimatedColor = getGetAnimatedColor(
      initialColor,
      theme.color.blue,
      PERIOD
    )
    const fromMatrix = fromMatrix4(this.cube.matrix)
    const toMatrix = matrix.toDimension(4)
    this.getAnimatedTransformation = getGetAnimatedTransformation(
      fromMatrix,
      toMatrix,
      PERIOD
    )
    this.frameId = requestAnimationFrame(this.animate)
  }
  
  componentWillUnmount() {
    cancelAnimationFrame(this.frameId)
    this.view.removeChild(this.renderer.domElement)
  }
  // ...
}
複製代碼

不過此時咱們尚未定義 animate 函數,所以什麼也不會渲染。首先,咱們更新立方體及其邊緣的轉換矩陣,而且更新立方體的顏色,而後進行渲染而且調用 window.requestAnimationFrame後端

componentWillReceiveProps 方法將接收當前組件的大小,當它檢測到組件尺寸發生了變化時,會更新狀態,改變 renderer 的尺寸,並調整 camera 的方位。

class ThreeScene extends React.Component {
  // ...
  animate = () => {
    const transformation = this.getAnimatedTransformation()
    const matrix4 = toMatrix4(transformation)
    this.cube.material.color.set(this.getAnimatedColor())
    this.objects.forEach(obj => obj.matrix.set(...matrix4.toArray()))
    this.renderer.render(this.scene, this.camera)
    this.frameId = window.requestAnimationFrame(this.animate)
  }

  componentWillReceiveProps({ size: { width, height } }) {
    if (this.state.width !== width || this.state.height !== height) {
      this.setState({ width, height })
      this.renderer.setSize(width, height)
      this.camera.aspect = width / height
      this.camera.updateProjectionMatrix()
    }
  }
}
複製代碼

動畫

爲了將顏色變化以及矩陣變換作成動畫,須要寫個函數來返回動畫函數。在寫這塊函數前,咱們先要完成如下兩種轉換器:將咱們庫的矩陣轉換爲 ThreeJS 格式矩陣的函數,以及參考 StackOverflow 上代碼的將 RGB 轉換爲 hex 的函數:

import * as THREE from 'three'
import { Matrix } from 'linear-algebra/matrix'

export const toMatrix4 = matrix => {
  const matrix4 = new THREE.Matrix4()
  matrix4.set(...matrix.components())
  return matrix4
}

export const fromMatrix4 = matrix4 => {
  const components = matrix4.toArray()
  const rows = new Array(4)
    .fill(0)
    .map((_, i) => components.slice(i * 4, (i + 1) * 4))
  return new Matrix(...rows)
}

複製代碼
import * as THREE from 'three'
import { Matrix } from 'linear-algebra/matrix'

export const toMatrix4 = matrix => {
  const matrix4 = new THREE.Matrix4()
  matrix4.set(...matrix.components())
  return matrix4
}

export const fromMatrix4 = matrix4 => {
  const components = matrix4.toArray()
  const rows = new Array(4)
    .fill(0)
    .map((_, i) => components.slice(i * 4, (i + 1) * 4))
  return new Matrix(...rows)
}

複製代碼

顏色

首先,須要計算每種原色(RGB)變化的幅度。第一次調用 getGetAnimatedColor 時會返回新的色彩與時間戳的集合;並在後續被調用時,經過顏色變化的距離以及時間的耗費,能夠計算出當前時刻新的 RGB 顏色:

import { hexToRgb, rgbToHex } from './generic'

export const getGetAnimatedColor = (fromColor, toColor, period) => {
  const fromRgb = hexToRgb(fromColor)
  const toRgb = hexToRgb(toColor)
  const distances = fromRgb.map((fromPart, index) => {
    const toPart = toRgb[index]
    return fromPart <= toPart ? toPart - fromPart : 255 - fromPart + toPart
  })
  let start
  return () => {
    if (!start) {
      start = Date.now()
    }
    const now = Date.now()
    const timePassed = now - start
    if (timePassed > period) return toColor

    const animatedDistance = timePassed / period
    const rgb = fromRgb.map((fromPart, index) => {
      const distance = distances[index]
      const step = distance * animatedDistance
      return Math.round((fromPart + step) % 255)
    })
    return rgbToHex(...rgb)
  }
}
複製代碼

線性變換

爲了給線性變換作出動畫效果,一樣要進行上節的操做。咱們首先找到矩陣變換先後的區別,而後在動畫函數中,根據第一次調用 getGetAnimatedTransformation 時的狀態,根據時間來更新各個組件的狀態:

export const getGetAnimatedTransformation = (fromMatrix, toMatrix, period) => {
  const distances = toMatrix.subtract(fromMatrix)
  let start
  return () => {
    if (!start) {
      start = Date.now()
    }
    const now = Date.now()
    const timePassed = now - start
    if (timePassed > period) return toMatrix

    const animatedDistance = timePassed / period
    const newMatrix = fromMatrix.map((fromComponent, i, j) => {
      const distance = distances.rows[i][j]
      const step = distance * animatedDistance
      return fromComponent + step
    })
    return newMatrix
  }
}
複製代碼

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索