Threejs examples

Nov 07, 2024
8 min read
1722

浏览器对 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 = 0xffffff

function 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}></div>
}

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 = []
    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