大家好,我是glume。 今天我们来做一个小玩意,用React结合Three.js做一个3D动画。效果如下:
这个立方几何体、水看起来是不是特有灵性呢?没错,它就是用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/…