Plumbiu
banner
avatar
Plumbiu
这人很勤奋,啥都没留

Threejs examples

2024-11-07
Note

浏览器对 WebGL Contexts 数量有要求,数量过多会将旧的删除,因此单独分一个 example 文章:

为了方便创建渲染器,这里写了一个 buildRendererbuildCamera 函数:

第一个例子没有使用这个函数,因为我觉得注释对理解很有必要。

import { type RefObject } from 'react'import * as THREE from 'three'export function buildRenderer(ref: RefObject<HTMLDivElement>) {  const container = ref.current!  const size = container.clientWidth  const renderer = new THREE.WebGLRenderer({ antialias: true })  renderer.setSize(size, size)  renderer.pixelRatio = window.devicePixelRatio  container.appendChild(renderer.domElement)  return renderer}export function buildCamera(x: number, y: number, z: number) {  const fov = 45  const aspect = 1  const near = 0.1  const far = 1000  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)  camera.position.set(x, y, z)  camera.lookAt(0, 0, 0)  return camera}

Rotating cube

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'function FirstScence() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const container = containerRef.current!    const size = container.clientWidth    // 1. 创建渲染器    const renderer = new THREE.WebGLRenderer({ antialias: true })    // 设置背景色透明    renderer.setClearAlpha(0)    // 指定设备像素密度    renderer.pixelRatio = window.devicePixelRatio    // 设置大小    renderer.setSize(size, size)    // 将渲染器的 dom 节点加入 container 下    container.appendChild(renderer.domElement)    // 2. 创建场景    const scene = new THREE.Scene()    // 2.1 创建几何体    const geometry = new THREE.BoxGeometry(4, 4, 4)    // 2.2 创建材质    const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })    // 2.3 集合体和材质组合成 Mesh 对象    const cube = new THREE.Mesh(geometry, material)    // 2.4 将 Mesh 对象添加到场景中    scene.add(cube)    // 2.5 旋转动画    function animate() {      requestAnimationFrame(animate)      cube.rotation.y += 0.01      renderer.render(scene, camera)    }    // 3. 创建相机    const camera = new THREE.PerspectiveCamera(      75, // fov      1 / 1, // aspect      0.1, // near      1000, // far    )    // 设置相机的位置 (x, y, z) = (5, 5, 10)    camera.position.set(5, 5, 10)    // 设置相机看向原点 0, 0, 0    camera.lookAt(0, 0, 0)    // 4. 创建物体    const axis = new THREE.AxesHelper(5)    // 添加到场景中    scene.add(axis)    // 5. 渲染    const cancleFaf = animate() // renderer.render(scene, camera)    return cancleFaf  }, [])  return <div ref={containerRef} />}export default FirstScence

如何确保自转时间?上面的例子中,使用的是 requestAnimationFrame api,调用频率与用户的浏览器刷新率有关,不同硬件、浏览器环境下,自转的速度也不一样,为了使得旋转速度一致,我们可以使用 Clock 解决:

const clock = new Three.Clock()function animate() {  requestAnimationFrame(animate)  const elapsedTime = clock.getElapsedTime()  cube.rotation.y = elapsedTime * Math.PI  renderer.render(scene, camera)}

Controls

OrbitControls 允许我们对场景内容进行旋转、放大缩小等操作。

注意,如果使用之前的 animate 函数不断调用渲染器的 render 方法渲染的,是不需要为 OrbitControls 添加 change 事件的。

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'import { OrbitControls } from 'three/examples/jsm/Addons.js'import { buildCamera, buildRenderer } from './utils'function ThreeControlPureFirstScene() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const renderer = buildRenderer(containerRef)    const scene = new THREE.Scene()    const geometry = new THREE.BoxGeometry(4, 4, 4)    const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })    const cube = new THREE.Mesh(geometry, material)    scene.add(cube)    function render() {      renderer.render(scene, camera)    }    const camera = buildCamera(5, 5, 10)    const controls = new OrbitControls(camera, renderer.domElement)    controls.update()    controls.addEventListener('change', render)    const axis = new THREE.AxesHelper(5)    scene.add(axis)    render()    return () => {      controls.removeEventListener('change', render)    }  }, [])  return <div ref={containerRef} />}export default ThreeControlPureFirstScene

Light

MeshBasicMaterial 不受光源影响,需要设置为 MeshStandardMaterial

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'import { OrbitControls } from 'three/examples/jsm/Addons.js'import { buildCamera, buildRenderer } from './utils'const White = 0xfffffffunction LightPureFirstScence() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const renderer = buildRenderer(containerRef)    // 设置渲染器渲染阴影    renderer.shadowMap.enabled = true    const scene = new THREE.Scene()    const geometry = new THREE.BoxGeometry(4, 4, 4)    const material = new THREE.MeshStandardMaterial({ color: 0xff0000 })    material.emissive = new THREE.Color(0x48211a)    const cube = new THREE.Mesh(geometry, material)    // 表示 cube 可以投射阴影    cube.castShadow = true    // 平面几何    const planeGeometry = new THREE.PlaneGeometry(20, 20)    const planeMaterial = new THREE.MeshStandardMaterial({ color: White })    // 材质的放射光颜色,不受其他光照影响的固有颜色,默认为黑色    planeMaterial.emissive = new THREE.Color(0x444444)    const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial)    planeMesh.rotation.x = -0.5 * Math.PI    planeMesh.position.set(0, -6, 0)    // 接受投射的阴影    planeMesh.receiveShadow = true    // 方向光    const directionalLight = new THREE.DirectionalLight(White, 0.5)    // 设置方向(根据源点设置)    directionalLight.position.set(15, 15, 15)    // 表示该方向会投射阴影效果    directionalLight.castShadow = true    scene.add(directionalLight)    scene.add(planeMesh)    scene.add(cube)    function render() {      renderer.render(scene, camera)    }    const axis = new THREE.AxesHelper(5)    scene.add(axis)    const camera = buildCamera(15, 15, 15)    const controls = new OrbitControls(camera, renderer.domElement)    controls.addEventListener('change', render)    controls.update()    render()    return () => {      controls.removeEventListener('change', render)    }  }, [])  return <div ref={containerRef} />}export default LightPureFirstScence

Drawing lines

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'import { OrbitControls } from 'three/examples/jsm/Addons.js'import { buildCamera, buildRenderer } from './utils'function PureLine() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const renderer = buildRenderer(containerRef)    const material = new THREE.LineBasicMaterial({ color: 0xff0000 })    const points: THREE.Vector3[] = []    points.push(      ...[        new THREE.Vector3(-5, 0, 0),        new THREE.Vector3(0, 5, 0),        new THREE.Vector3(5, 0, 0),      ],    )    const geometry = new THREE.BufferGeometry().setFromPoints(points)    const line = new THREE.Line(geometry, material)    const camera = buildCamera(10, 10, 10)    const scene = new THREE.Scene()    const controls = new OrbitControls(camera, renderer.domElement)    controls.addEventListener('change', render)    controls.update()    scene.add(line)    function render() {      renderer.render(scene, camera)    }    render()    return () => {      controls.removeEventListener('change', render)    }  }, [])  return <div ref={containerRef} />}export default PureLine

Box with edge

Code Playground
import { useRef, useEffect } from 'react'import * as THREE from 'three'import { buildCamera, buildRenderer } from './utils'function ThreeLearnPrimitivesBox() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const renderer = buildRenderer(containerRef)    const scene = new THREE.Scene()    const camera = buildCamera(10, 10, 10)    const box = new THREE.BoxGeometry(4, 4, 4)    const material = new THREE.MeshBasicMaterial({ color: 0x3451b2 })    const mesh = new THREE.Mesh(box, material)    // 获取 box 的边界    const edges = new THREE.EdgesGeometry(box)    const edgesMaterial = new THREE.LineBasicMaterial({      color: 0x00ffff,    })    const line = new THREE.LineSegments(edges, edgesMaterial)    mesh.add(line)    scene.add(mesh)    function render() {      renderer.render(scene, camera)    }    function animate() {      requestAnimationFrame(animate)      mesh.rotation.x += 0.01      mesh.rotation.y += 0.01      mesh.rotation.z += 0.01      render()    }    const cancelRaf = animate()    return cancelRaf  }, [])  return <div ref={containerRef} />}export default ThreeLearnPrimitivesBox

Render Text

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'import { FontLoader, OrbitControls } from 'three/examples/jsm/Addons.js'import { buildCamera, buildRenderer } from './utils'function PureText() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const fontLoadr = new FontLoader()    fontLoadr.load('/threejs/fonts/gentilis_regular.typeface.json', (font) => {      const renderer = buildRenderer(containerRef)      const scene = new THREE.Scene()      const camera = buildCamera(0, 0, 10)      const material = new THREE.MeshStandardMaterial({        color: 0xff0000,      })      material.emissive = new THREE.Color(0x48211a)      const geometry = new TextGeometry('Hello', {        font,        size: 24,        depth: 24,      })      const light = new THREE.DirectionalLight(0xffffff, 0.4)      light.position.set(1, 1, 1)      light.castShadow = true      const mesh = new THREE.Mesh(geometry, material)      mesh.castShadow = true      scene.add(mesh)      scene.add(light)      const controls = new OrbitControls(camera, renderer.domElement)      controls.update()      renderer.render(scene, camera)      function animate() {        requestAnimationFrame(animate)        renderer.render(scene, camera)      }      const cancelRaf = animate()      return cancelRaf    })  }, [])  return <div ref={containerRef} />}export default PureText

Loading 3D modales

Three.js 支持很多导入工具,官网推荐 glTF(gl 传输格式),这种格式传输效率和加载速度都非常优秀,而且也包括了网格、材质、纹理、皮肤、骨骼、变形目标、动画、灯光和摄像机等,很多工具都包含了 glTF 导出功能:

three.js 内置了 GLTFLoader,可以载入 glTF 模型。

另外一些环境贴图加载器也是必须的,例如 RGBELoader 可以加载高动态范围(HDR)环境贴图,通常用于创建更逼真的光照和反射效果,详见 wiki

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'import {  GLTFLoader,  OrbitControls,  RGBELoader,} from 'three/examples/jsm/Addons.js'import { buildCamera, buildRenderer } from './utils'function ThreePureModel() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const renderer = buildRenderer(containerRef)    const scene = new THREE.Scene()    const camera = buildCamera(-1.8, 0.6, 2.7)    function render() {      renderer.render(scene, camera)    }    function renderModal() {      const loader = new GLTFLoader()      loader.setPath('/threejs/models/glTF/DamagedHelmet/')      loader.load(        'DamagedHelmet.gltf',        async (gltf) => {          const model = gltf.scene          await renderer.compileAsync(model, camera, scene)          scene.add(model)          render()        },        undefined,        (error) => {          console.log(error)        },      )    }    new RGBELoader().load(      '/threejs/textures/royal_esplanade_1k.hdr',      (texture) => {        texture.mapping = THREE.EquirectangularReflectionMapping        scene.background = texture        scene.environment = texture        renderModal()      },    )    const controls = new OrbitControls(camera, renderer.domElement)    controls.addEventListener('change', render)    controls.update()    return () => {      controls.removeEventListener('change', render)    }  }, [])  return <div ref={containerRef} />}export default ThreePureModel
Preview
Console

Sun Earth and moon

这个 demo 的关键是了解 three,js 的场景图。场景图在 3D 引擎是一个图中节点的层次结构,每个节点代表了一个局部空间(local space):

例如下面系统的场景图:

scenegraph-sun-earth-moon

可能很多人有疑问:为什么需要单独设置 solarSystemearthOrbitmoonOrbit?至少第一次我认为下面的场景图更加简洁:

scenegraph-solarsystem

这里的重点在于局部空间,参考下面第 22、25 行代码,如果我们将 sun 放大五倍,作为子节点的 earth 也会放大五倍,同理 moon 也会放大五倍,为了避免缩放之间互相影响,我们使用了 Object3D将对象进行了组合,这样每个图元就不会互相影响了。

Code Playground
import { useEffect, useRef } from 'react'import * as THREE from 'three'import { buildCamera, buildRenderer } from './utils'function ThreeSunEarthMoon() {  const containerRef = useRef<HTMLDivElement>(null)  useEffect(() => {    const renderer = buildRenderer(containerRef)    const camera = buildCamera(0, 35, 0)    // 参数:https://threejs.org/manual/#zh/primitives#Diagram-SphereGeometry    const sphereGeometry = new THREE.SphereGeometry(1, 6, 6)    const sunMaterial = new THREE.MeshPhongMaterial({ emissive: 0xffff00 })    const earthMaterial = new THREE.MeshPhongMaterial({      emissive: 0x000ff,      color: 0x2233ff,    })    const moonMaterial = new THREE.MeshPhongMaterial({ emissive: 0x888888 })    const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial)    sunMesh.scale.set(4, 4, 4)    const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial)    const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial)    moonMesh.scale.set(0.5, 0.5, 0.5)    const scene = new THREE.Scene()    const solarSystem = new THREE.Object3D()    const earthOrbit = new THREE.Object3D()    earthOrbit.position.x = 10    const moonOrbit = new THREE.Object3D()    moonOrbit.position.x = 2    const light = new THREE.PointLight(0xffffff, 1500)    scene.add(light)    scene.add(solarSystem)    solarSystem.add(sunMesh)    solarSystem.add(earthOrbit)    earthOrbit.add(earthMesh)    earthOrbit.add(moonOrbit)    moonOrbit.add(moonMesh)    const points = [solarSystem, sunMesh, earthOrbit, earthMesh, moonOrbit]    function animate() {      requestAnimationFrame(animate)      points.forEach((point) => {        point.rotation.y += 0.015      })      renderer.render(scene, camera)    }    const cancelRaf = animate()    return cancelRaf  }, [])  return <div ref={containerRef} />}export default ThreeSunEarthMoon
评论区
CC BY-NC-SA 4.0 2024 © Plumbiu