Skip to content

Three.js 代码示例

参考:探索three.js

Three.js

世界应用

  • 世界类
    • 创建场景
    • 创建相机
    • 创建渲染器
    • 创建控制器
    • 创建动画循环
    • 创建灯光
    • 创建响应性
    • 创建 GUI 调试工具
    • 创建性能监视工具
    • 加载模型
    • 使用

创建场景

js
import { Scene, Color } from 'three'

import type { TCreateScene } from '@types'

/**
 * 创建场景
 */
export const createScene = (options: TCreateScene = { background: new Color('skyblue') }) => {
  const scene = new Scene()

  scene.background = options.background ?? scene.background

  return scene
}
import { Scene, Color } from 'three'

import type { TCreateScene } from '@types'

/**
 * 创建场景
 */
export const createScene = (options: TCreateScene = { background: new Color('skyblue') }) => {
  const scene = new Scene()

  scene.background = options.background ?? scene.background

  return scene
}

创建相机

js
import { PerspectiveCamera, Vector3 } from 'three'

import type { TCreateCamera } from '@types'

/**
 * 创建相机
 */
export const createCamera = (options: TCreateCamera = { position: new Vector3(0, 10, 10) }) => {
  const { position } = options
  const camera = new PerspectiveCamera(45, 1, 1, 1000)

  if (position?.x) {
    camera.position.set(position.x, position.y, position.z)
  }

  return camera
}
import { PerspectiveCamera, Vector3 } from 'three'

import type { TCreateCamera } from '@types'

/**
 * 创建相机
 */
export const createCamera = (options: TCreateCamera = { position: new Vector3(0, 10, 10) }) => {
  const { position } = options
  const camera = new PerspectiveCamera(45, 1, 1, 1000)

  if (position?.x) {
    camera.position.set(position.x, position.y, position.z)
  }

  return camera
}

创建渲染器

js
import { WebGLRenderer } from 'three'

/**
 * 创建渲染器
 */
export const createRenderer = () => {
  const renderer = new WebGLRenderer({ antialias: true, alpha: true })

  renderer.setPixelRatio(window.devicePixelRatio)

  return renderer
}
import { WebGLRenderer } from 'three'

/**
 * 创建渲染器
 */
export const createRenderer = () => {
  const renderer = new WebGLRenderer({ antialias: true, alpha: true })

  renderer.setPixelRatio(window.devicePixelRatio)

  return renderer
}

创建控制器

js
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

import type { TCreateControls } from '@types'

/**
 * 创建控制器
 */
export const createControls = (options: TCreateControls) => {
  const { camera, canvas, target } = options
  const controls = new OrbitControls(camera, canvas)

  controls.target.set(target.x, target.y, target.z)

  // 启用阻尼
  controls.enableDamping = true
  controls.dampingFactor = 0.5

  controls.tick = () => controls.update()

  return controls
}
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

import type { TCreateControls } from '@types'

/**
 * 创建控制器
 */
export const createControls = (options: TCreateControls) => {
  const { camera, canvas, target } = options
  const controls = new OrbitControls(camera, canvas)

  controls.target.set(target.x, target.y, target.z)

  // 启用阻尼
  controls.enableDamping = true
  controls.dampingFactor = 0.5

  controls.tick = () => controls.update()

  return controls
}

创建动画循环

js
import { Clock } from 'three'

const clock = new Clock()

/**
 * 更新类
 */
export default class Loop {
  private renderer: THREE.WebGLRenderer
  private loopRenderer: () => void

  /** 可更新的 */
  public updatables: Set<any>

  constructor(renderer: THREE.WebGLRenderer, loopRenderer: () => void) {
    this.renderer = renderer
    this.loopRenderer = loopRenderer

    this.updatables = new Set<any>()
  }

  public start() {
    this.renderer.setAnimationLoop(() => {
      this.tick()

      this.loopRenderer()
    })
  }

  public stop() {
    this.renderer.setAnimationLoop(null)
  }

  private tick() {
    const delta = clock.getDelta()

    for (const object of this.updatables.values()) {
      object.tick(delta)
    }
  }
}
import { Clock } from 'three'

const clock = new Clock()

/**
 * 更新类
 */
export default class Loop {
  private renderer: THREE.WebGLRenderer
  private loopRenderer: () => void

  /** 可更新的 */
  public updatables: Set<any>

  constructor(renderer: THREE.WebGLRenderer, loopRenderer: () => void) {
    this.renderer = renderer
    this.loopRenderer = loopRenderer

    this.updatables = new Set<any>()
  }

  public start() {
    this.renderer.setAnimationLoop(() => {
      this.tick()

      this.loopRenderer()
    })
  }

  public stop() {
    this.renderer.setAnimationLoop(null)
  }

  private tick() {
    const delta = clock.getDelta()

    for (const object of this.updatables.values()) {
      object.tick(delta)
    }
  }
}

创建灯光

js
import { DirectionalLight, HemisphereLight } from 'three'

/**
 * 创建定向光光源
 */
export const createLights = () => {
  // 半球光
  const hemisphereLight = new HemisphereLight('white', 'darkslategrey', 5)
  // 定向光
  const directionalLight = new DirectionalLight('white', 4)

  directionalLight.position.set(10, 10, 10)

  directionalLight.tick = (delta: number) => {}

  return { directionalLight, hemisphereLight }
}
import { DirectionalLight, HemisphereLight } from 'three'

/**
 * 创建定向光光源
 */
export const createLights = () => {
  // 半球光
  const hemisphereLight = new HemisphereLight('white', 'darkslategrey', 5)
  // 定向光
  const directionalLight = new DirectionalLight('white', 4)

  directionalLight.position.set(10, 10, 10)

  directionalLight.tick = (delta: number) => {}

  return { directionalLight, hemisphereLight }
}

创建响应性

js
/**
 * 缩放类
 */
export default class Resizer {
  constructor(container: Element, camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {
    this.setSize(container, camera, renderer)

    window.addEventListener('resize', () => {
      this.setSize(container, camera, renderer)

      // this.onResize()
    })
  }

  /**
   * 自定义操作
   */
  onResize() {}

  setSize(container: Element, camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {
    const { clientWidth, clientHeight } = container

    camera.aspect = clientWidth / clientHeight

    camera.updateProjectionMatrix()

    renderer.setSize(clientWidth, clientHeight)

    renderer.setPixelRatio(window.devicePixelRatio)
  }
}
/**
 * 缩放类
 */
export default class Resizer {
  constructor(container: Element, camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {
    this.setSize(container, camera, renderer)

    window.addEventListener('resize', () => {
      this.setSize(container, camera, renderer)

      // this.onResize()
    })
  }

  /**
   * 自定义操作
   */
  onResize() {}

  setSize(container: Element, camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {
    const { clientWidth, clientHeight } = container

    camera.aspect = clientWidth / clientHeight

    camera.updateProjectionMatrix()

    renderer.setSize(clientWidth, clientHeight)

    renderer.setPixelRatio(window.devicePixelRatio)
  }
}

创建 GUI 调试工具

js
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'

/**
 * 创建光源 GUI
 */
export const createLightsGUI = (gui: GUI, ...lights: any[]) => {
  const [hemisphereLight, directionalLight] = lights

  const folderAmbient = gui.addFolder('半球光')
  folderAmbient.close()
  folderAmbient.addColor(hemisphereLight, 'color')
  folderAmbient.add(hemisphereLight, 'intensity', 0, 2.0, 0.1)

  if (directionalLight) {
    const folderDirectional = gui.addFolder('平行光')
    folderDirectional.close()
    folderDirectional.addColor(directionalLight, 'color')
    folderDirectional.add(directionalLight, 'intensity', 0, 100, 0.1)
    folderDirectional.add(directionalLight.position, 'x', -300, 300, 1)
    folderDirectional.add(directionalLight.position, 'y', -300, 300, 1)
    folderDirectional.add(directionalLight.position, 'z', -300, 300, 1)
  }
}
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'

/**
 * 创建光源 GUI
 */
export const createLightsGUI = (gui: GUI, ...lights: any[]) => {
  const [hemisphereLight, directionalLight] = lights

  const folderAmbient = gui.addFolder('半球光')
  folderAmbient.close()
  folderAmbient.addColor(hemisphereLight, 'color')
  folderAmbient.add(hemisphereLight, 'intensity', 0, 2.0, 0.1)

  if (directionalLight) {
    const folderDirectional = gui.addFolder('平行光')
    folderDirectional.close()
    folderDirectional.addColor(directionalLight, 'color')
    folderDirectional.add(directionalLight, 'intensity', 0, 100, 0.1)
    folderDirectional.add(directionalLight.position, 'x', -300, 300, 1)
    folderDirectional.add(directionalLight.position, 'y', -300, 300, 1)
    folderDirectional.add(directionalLight.position, 'z', -300, 300, 1)
  }
}

创建性能监视工具

js
import Stats from 'three/examples/jsm/libs/stats.module.js'

/**
 * 创建性能监视
 * @description
 *   FPS: 最后一秒渲染的 FPS 帧。数字越高越好。
 *   MS: 渲染一帧所需的毫秒数。数字越低越好。
 *   MB: MB 已分配内存的兆字节数。
 */
export const createStats = (position: 'tl' | 'tr' | 'bl' | 'br' = 'bl') => {
  const stats = new Stats()

  stats.showPanel(0)

  const positions = {
    tl: { top: '0px', right: 'auto', bottom: 'auto', left: '0px' },
    tr: { top: '0px', right: '0px', bottom: 'auto', left: 'auto' },
    bl: { top: 'auto', right: 'auto', bottom: '0px', left: '0px' },
    br: { top: 'auto', right: '0px', bottom: '0px', left: 'auto' }
  }

  Object.assign(stats.dom.style, {
    position: 'absolute',
    ...positions[position]
  })

  document.body.appendChild(stats.dom)

  return stats
}
import Stats from 'three/examples/jsm/libs/stats.module.js'

/**
 * 创建性能监视
 * @description
 *   FPS: 最后一秒渲染的 FPS 帧。数字越高越好。
 *   MS: 渲染一帧所需的毫秒数。数字越低越好。
 *   MB: MB 已分配内存的兆字节数。
 */
export const createStats = (position: 'tl' | 'tr' | 'bl' | 'br' = 'bl') => {
  const stats = new Stats()

  stats.showPanel(0)

  const positions = {
    tl: { top: '0px', right: 'auto', bottom: 'auto', left: '0px' },
    tr: { top: '0px', right: '0px', bottom: 'auto', left: 'auto' },
    bl: { top: 'auto', right: 'auto', bottom: '0px', left: '0px' },
    br: { top: 'auto', right: '0px', bottom: '0px', left: 'auto' }
  }

  Object.assign(stats.dom.style, {
    position: 'absolute',
    ...positions[position]
  })

  document.body.appendChild(stats.dom)

  return stats
}

加载模型

js
import ModelLoader from '@utils/loader-model'
import { AnimationMixer, Vector3 } from 'three'

import type { TLoaderModel, THandleModelEffect, TLoadAllModels } from '@types'
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'

/**
 * 加载全部模型
 */
export const loadAllModels = (options: TLoadAllModels) => {
  const floorLoadList: TLoaderModel[] = [
    {
      name: '鹦鹉',
      type: 'glb',
      url: 'src/assets/3d/models/birds/Parrot.glb',
      dracoUrl: 'src/assets/3d/draco/',
      onLoad: <T extends GLTF>(value1: T) => handleFModelEffect(value1, options, 'PARROT')
    },
    {
      name: '鹳',
      type: 'glb',
      url: 'src/assets/3d/models/birds/Stork.glb',
      dracoUrl: 'src/assets/3d/draco/',
      onLoad: <T extends GLTF>(value1: T) => handleFModelEffect(value1, options, 'STORK')
    },
    {
      name: '火烈鸟',
      type: 'glb',
      url: 'src/assets/3d/models/birds/Flamingo.glb',
      dracoUrl: 'src/assets/3d/draco/',
      onLoad: <T extends GLTF>(value1: T) => handleFModelEffect(value1, options, 'FLAMINGO')
    }
  ]

  return new Promise<boolean>((resolve, reject) => {
    try {
      ModelLoader.handleIterateLoaderModel({
        list: floorLoadList,
        onAllLoad: () => {
          resolve(true)
        },
        onAllProgress(event, index) {
          // 计算加载进度百分比
          const progress = (event.loaded / event.total) * 100

          console.log('🚀 ~ onAllProgress ~ progress:', progress)
        }
      })
    } catch (error) {
      console.log('🚀 ~ loadAllModels ~ error:', error)
      reject(error)
    }
  })
}

const stupeModel = (data: GLTF) => {
  if (!data || !data.scene) return void 0

  const model = data.scene.children[0]
  const clip = data.animations[0]

  const mixer = new AnimationMixer(model)
  const action = mixer.clipAction(clip)

  action.reset().play()

  // @ts-ignore
  model.tick = (delta: number) => {
    mixer.update(delta)
  }

  return model
}

/**
 * 处理模型效果
 */
export const handleFModelEffect = (
  value: GLTF,
  options: THandleModelEffect,
  type: 'PARROT' | 'STORK' | 'FLAMINGO'
) => {
  const { scene, loop } = options

  if (!scene || !loop) return

  const model = stupeModel(value)

  if (!model) return

  const defPosition = {
    PARROT: new Vector3(0, 0, 0.25),
    STORK: new Vector3(7.5, 0, -10),
    FLAMINGO: new Vector3(0, 2.5, -10)
  }
  const pos = defPosition[type]

  model.position.set(pos.x, pos.y, pos.z)

  scene.add(model)
  loop.updatables.add(model)
}
import ModelLoader from '@utils/loader-model'
import { AnimationMixer, Vector3 } from 'three'

import type { TLoaderModel, THandleModelEffect, TLoadAllModels } from '@types'
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'

/**
 * 加载全部模型
 */
export const loadAllModels = (options: TLoadAllModels) => {
  const floorLoadList: TLoaderModel[] = [
    {
      name: '鹦鹉',
      type: 'glb',
      url: 'src/assets/3d/models/birds/Parrot.glb',
      dracoUrl: 'src/assets/3d/draco/',
      onLoad: <T extends GLTF>(value1: T) => handleFModelEffect(value1, options, 'PARROT')
    },
    {
      name: '鹳',
      type: 'glb',
      url: 'src/assets/3d/models/birds/Stork.glb',
      dracoUrl: 'src/assets/3d/draco/',
      onLoad: <T extends GLTF>(value1: T) => handleFModelEffect(value1, options, 'STORK')
    },
    {
      name: '火烈鸟',
      type: 'glb',
      url: 'src/assets/3d/models/birds/Flamingo.glb',
      dracoUrl: 'src/assets/3d/draco/',
      onLoad: <T extends GLTF>(value1: T) => handleFModelEffect(value1, options, 'FLAMINGO')
    }
  ]

  return new Promise<boolean>((resolve, reject) => {
    try {
      ModelLoader.handleIterateLoaderModel({
        list: floorLoadList,
        onAllLoad: () => {
          resolve(true)
        },
        onAllProgress(event, index) {
          // 计算加载进度百分比
          const progress = (event.loaded / event.total) * 100

          console.log('🚀 ~ onAllProgress ~ progress:', progress)
        }
      })
    } catch (error) {
      console.log('🚀 ~ loadAllModels ~ error:', error)
      reject(error)
    }
  })
}

const stupeModel = (data: GLTF) => {
  if (!data || !data.scene) return void 0

  const model = data.scene.children[0]
  const clip = data.animations[0]

  const mixer = new AnimationMixer(model)
  const action = mixer.clipAction(clip)

  action.reset().play()

  // @ts-ignore
  model.tick = (delta: number) => {
    mixer.update(delta)
  }

  return model
}

/**
 * 处理模型效果
 */
export const handleFModelEffect = (
  value: GLTF,
  options: THandleModelEffect,
  type: 'PARROT' | 'STORK' | 'FLAMINGO'
) => {
  const { scene, loop } = options

  if (!scene || !loop) return

  const model = stupeModel(value)

  if (!model) return

  const defPosition = {
    PARROT: new Vector3(0, 0, 0.25),
    STORK: new Vector3(7.5, 0, -10),
    FLAMINGO: new Vector3(0, 2.5, -10)
  }
  const pos = defPosition[type]

  model.position.set(pos.x, pos.y, pos.z)

  scene.add(model)
  loop.updatables.add(model)
}

世界类

汇总上述所有方法...

js
import { createCamera } from '@components/camera'
import { createScene } from '@components/scene'
import { createAxesHelper } from '@components/helpers'
import { createStats } from '@components/stats'
import { createLights } from '@components/lights'
import { createLightsGUI } from '@components/gui'

import { createRenderer } from '@systems/renderer'
import Resizer from '@systems/resizer'
import Loop from '@systems/loop'
import { createControls } from '@systems/controls'

import { load } from '@containers/birds'

import { Vector3 } from 'three'
import Stats from 'three/examples/jsm/libs/stats.module.js'
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'

import type { PerspectiveCamera, WebGLRenderer, Scene } from 'three'
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

/**
 * 世界类
 */
export default class BaseWorld {
  /** 摄像机 */
  public camera: PerspectiveCamera
  /** 摄像机默认位置 */
  public defaultCameraPosition = new Vector3(-1, 1.5, 6.5)
  /** 渲染器 */
  public renderer: WebGLRenderer
  /** 场景 */
  public scene: Scene
  /** 控制器 */
  public controls: OrbitControls
  /** 控制器默认位置 */
  public defaultControlsPosition = new Vector3(0, 0, 0)
  /** 挂载元素 */
  public el: HTMLDivElement
  /** 循环类 */
  public loop: Loop
  /** 调试类 */
  public gui: GUI

  /** 性能监视 */
  private stats?: Stats

  /** 是否为生产模式 */
  public isProd: boolean

  constructor(element: HTMLDivElement, isProd = false) {
    this.isProd = isProd

    this.gui = new GUI()
    this.camera = createCamera({ position: this.defaultCameraPosition })
    this.renderer = createRenderer()
    this.scene = createScene()
    this.controls = createControls({
      camera: this.camera,
      canvas: this.renderer.domElement,
      target: this.defaultControlsPosition
    })
    this.el = element
    this.loop = new Loop(this.renderer, () => this.render())

    element.append(this.renderer.domElement)

    const { directionalLight, hemisphereLight } = createLights()

    this.scene.add(directionalLight, hemisphereLight)

    this.loop.updatables.add(this.controls)

    new Resizer(element, this.camera, this.renderer)

    if (!isProd) {
      this.stats = createStats('tl')
      this.scene.add(createAxesHelper(500))
      this.loop.updatables.add(this.handleStats())

      createLightsGUI(this.gui, hemisphereLight, directionalLight)
    } else {
      this.gui.hide()
    }
  }

  public async init() {
    const { scene, renderer, loop } = this

    await load.loadAllModels({ scene, renderer, loop })
  }

  public render() {
    this.renderer.render(this.scene, this.camera)
  }

  public start() {
    this.loop.start()
  }

  public stop() {
    this.loop.stop()
  }

  private handleStats() {
    return {
      tick: () => this.stats?.update()
    }
  }
}
import { createCamera } from '@components/camera'
import { createScene } from '@components/scene'
import { createAxesHelper } from '@components/helpers'
import { createStats } from '@components/stats'
import { createLights } from '@components/lights'
import { createLightsGUI } from '@components/gui'

import { createRenderer } from '@systems/renderer'
import Resizer from '@systems/resizer'
import Loop from '@systems/loop'
import { createControls } from '@systems/controls'

import { load } from '@containers/birds'

import { Vector3 } from 'three'
import Stats from 'three/examples/jsm/libs/stats.module.js'
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'

import type { PerspectiveCamera, WebGLRenderer, Scene } from 'three'
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

/**
 * 世界类
 */
export default class BaseWorld {
  /** 摄像机 */
  public camera: PerspectiveCamera
  /** 摄像机默认位置 */
  public defaultCameraPosition = new Vector3(-1, 1.5, 6.5)
  /** 渲染器 */
  public renderer: WebGLRenderer
  /** 场景 */
  public scene: Scene
  /** 控制器 */
  public controls: OrbitControls
  /** 控制器默认位置 */
  public defaultControlsPosition = new Vector3(0, 0, 0)
  /** 挂载元素 */
  public el: HTMLDivElement
  /** 循环类 */
  public loop: Loop
  /** 调试类 */
  public gui: GUI

  /** 性能监视 */
  private stats?: Stats

  /** 是否为生产模式 */
  public isProd: boolean

  constructor(element: HTMLDivElement, isProd = false) {
    this.isProd = isProd

    this.gui = new GUI()
    this.camera = createCamera({ position: this.defaultCameraPosition })
    this.renderer = createRenderer()
    this.scene = createScene()
    this.controls = createControls({
      camera: this.camera,
      canvas: this.renderer.domElement,
      target: this.defaultControlsPosition
    })
    this.el = element
    this.loop = new Loop(this.renderer, () => this.render())

    element.append(this.renderer.domElement)

    const { directionalLight, hemisphereLight } = createLights()

    this.scene.add(directionalLight, hemisphereLight)

    this.loop.updatables.add(this.controls)

    new Resizer(element, this.camera, this.renderer)

    if (!isProd) {
      this.stats = createStats('tl')
      this.scene.add(createAxesHelper(500))
      this.loop.updatables.add(this.handleStats())

      createLightsGUI(this.gui, hemisphereLight, directionalLight)
    } else {
      this.gui.hide()
    }
  }

  public async init() {
    const { scene, renderer, loop } = this

    await load.loadAllModels({ scene, renderer, loop })
  }

  public render() {
    this.renderer.render(this.scene, this.camera)
  }

  public start() {
    this.loop.start()
  }

  public stop() {
    this.loop.stop()
  }

  private handleStats() {
    return {
      tick: () => this.stats?.update()
    }
  }
}

使用

js
import './style.css'

import BaseWorld from '@worlds/base-world'

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `<div class="container" id="container"></div>`

const init = async () => {
  const ele = document.querySelector<HTMLDivElement>('#container')

  if (ele) {
    const world = new BaseWorld(ele, false)

    await world.init()

    world.start()
  }
}

init()
import './style.css'

import BaseWorld from '@worlds/base-world'

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `<div class="container" id="container"></div>`

const init = async () => {
  const ele = document.querySelector<HTMLDivElement>('#container')

  if (ele) {
    const world = new BaseWorld(ele, false)

    await world.init()

    world.start()
  }
}

init()

备注

Three.js 示例,可能尚未完善,欢迎提供反馈和建议!完整代码

更新时间: :

Released under the MIT License.