React实现3D海洋Three.js

155 阅读2分钟

大家好,我是glume。 今天我们来做一个小玩意,用React结合Three.js做一个3D动画。效果如下:

2023-01-06-14-07-31.gif 这个立方几何体、水看起来是不是特有灵性呢?没错,它就是用ThreeJS实现的。 接下来,就让我们深入细节,体会其中的奥秘。相信这个实现的过程,会比动画本身更加精彩!

一、梳理思路

分析这样的一个过程,其中大致会经历一下的关键步骤:

  • 1、创建场景、相机、渲染器
  • 2、建立V3向量、水几何、天空、立方几何体、轨道控制器
  • 3、控制动画
  • 4、监听resize
  • 5、销毁renderer和resize监听

二、初步实现

当大部分问题考虑清楚之后,现在开始实现。 首先是基本的样式,比较简单。

import React, { useEffect, useRef } from 'react'
import { PageView } from '@components/PageView'
import { Box } from '@mui/material'
import { Scene, PerspectiveCamera, WebGLRenderer, Color, ACESFilmicToneMapping, Vector3, PlaneGeometry, TextureLoader, RepeatWrapping, PMREMGenerator, MathUtils, BoxGeometry, MeshStandardMaterial, Mesh } from 'three'

import { OrbitControls } from 'three/addons/controls/OrbitControls'
import { Water } from 'three/addons/objects/Water'
import { Sky } from 'three/addons/objects/Sky.js'

const ShadersOceanPage = () => {
  const containerRef = useRef<any>(null)

  let scene = null
  let camera = null
  let renderer = null
  let sun = null
  let water = null
  let sky = null
  let mesh = null
  let controls = null

  useEffect(() => {
    __createScene()
    _createCamera()
    _createRenderer()
    _createSun()
    _createWaterGeometry()
    _createSky()
    _createMesh()
    _createControls()

    _animate()
    _addListeners()

    return () => {
      renderer.dispose()
      _removeListeners()
    }
  }, [])

  // 建立场景
  const __createScene = () => {
    scene = new Scene()
  }

  // 建立相机
  const _createCamera = () => {
    camera = new PerspectiveCamera(75, containerRef.current.clientWidth / containerRef.current.clientHeight, 0.1, 1000)
    camera.position.set(0, 0, config.cameraRadius)
  }
  // 建立渲染器
  const _createRenderer = () => {
    renderer = new WebGLRenderer({ alpha: true, antialias: true })
    containerRef.current.replaceChildren(renderer.domElement)
    renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
    renderer.setPixelRatio(Math.min(1.5, window.devicePixelRatio))
    renderer.toneMapping = ACESFilmicToneMapping
    renderer.setClearColor(config.backgroundColor)
  }

  // 建立向量
  const _createSun = () => {
    sun = new Vector3()
  }

  // 建立水几何
  const _createWaterGeometry = () => {
    const waterGeometry = new PlaneGeometry(10000, 10000)
    water = new Water(waterGeometry, {
      textureWidth: 512,
      textureHeight: 512,
      waterNormals: new TextureLoader().load('textures/waternormals.jpg', function (texture) {
        texture.wrapS = texture.wrapT = RepeatWrapping
      }),
      sunDirection: new Vector3(),
      sunColor: 0xffffff,
      waterColor: 0x001e0f,
      distortionScale: 3.7,
      fog: scene.fog !== undefined
    })
    water.rotation.x = -Math.PI / 2

    scene.add(water)
  }

  // 建立天空
  const _createSky = () => {
    sky = new Sky()
    sky.scale.setScalar(10000)
    scene.add(sky)

    const skyUniforms = sky.material.uniforms
    skyUniforms.turbidity.value = 10
    skyUniforms.rayleigh.value = 2
    skyUniforms.mieCoefficient.value = 0.005
    skyUniforms.mieDirectionalG.value = 0.8
    _updateSun()
  }

  // 创建天空之口更新太阳
  function _updateSun() {
    const pmremGenerator = new PMREMGenerator(renderer)
    let renderTarget = null

    const phi = MathUtils.degToRad(90 - config.elevation)
    const theta = MathUtils.degToRad(config.azimuth)

    sun.setFromSphericalCoords(1, phi, theta)

    sky.material.uniforms.sunPosition.value.copy(sun)
    water.material.uniforms.sunDirection.value.copy(sun).normalize()

    if (renderTarget) renderTarget.dispose()

    renderTarget = pmremGenerator.fromScene(sky)

    scene.environment = renderTarget.texture
  }

  // 创建立方几何体
  const _createMesh = () => {
    const geometry = new BoxGeometry(10, 10, 10)
    const material = new MeshStandardMaterial({ roughness: 0 })

    mesh = new Mesh(geometry, material)
    scene.add(mesh)
  }

  // 创建轨道控制器
  const _createControls = () => {
    controls = new OrbitControls(camera, renderer.domElement)
    controls.maxPolarAngle = Math.PI * 0.495
    controls.target.set(0, 10, 0)
    controls.minDistance = 40.0
    controls.maxDistance = 200.0
    controls.update()
  }

  const _animate = () => {
    requestAnimationFrame(_animate)
    _render()
  }

  const _render = () => {
    const time = performance.now() * 0.001

    mesh.position.y = Math.sin(time) * 10 + 5
    mesh.rotation.x = time * 0.5
    mesh.rotation.z = time * 0.51

    water.material.uniforms.time.value += 1.0 / 60.0

    renderer.render(scene, camera)
  }

  const _addListeners = () => {
    window.addEventListener('resize', _onWindowResize, { passive: true })
  }

  const _removeListeners = () => {
    window.removeEventListener('resize', _onWindowResize)
  }

  const _onWindowResize = () => {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()

    renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
  }

  return (
    <PageView sx={{ position: 'relative' }}>
      <Box sx={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }} ref={containerRef}></Box>
    </PageView>
  )
}

const config = {
  backgroundColor: new Color('#0d021f'),
  cameraRadius: 4.5,
  elevation: 2,
  azimuth: 180
}

export default ShadersOceanPage

注意,three/addons为变名,本人在tsconfig.base.json做了如下控制:

"three/addons/*": ["node_modules/three/examples/jsm/*"]

到此,完整的效果就出来了,你可以自己复制体验一下。

三、结语

代码最近学习ThreeJS写的,参考官方文档,除了页面销毁销毁相应对象,无其余优化。给大家看下 代码: gitee.com/qingfen526/…