2025年项目中是怎么初始化Three.js三维场景的

Three.js 是一个强大的 JavaScript 3D 库,可以创建令人惊叹的 Web 3D 应用程序。本文将深入分析一个完整的 Three.js 场景管理类 useScene,它封装了场景初始化、模型加载、事件处理等核心功能。

依赖与导入

js 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import {KTX2Loader} from 'three/examples/jsm/loaders/KTX2Loader';
import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader'
import {MeshoptDecoder} from 'three/examples/jsm/libs/meshopt_decoder.module';
import Stats from "stats-gl";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { gsap } from "gsap";
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
  • three:核心 3D 引擎。

  • OrbitControls:相机轨道控制器(旋转/平移/缩放)。

  • GLTFLoader:加载 glTF/GLB 模型。

  • KTX2Loader:加载 KTX2 压缩纹理(Basis Universal)。

  • DRACOLoader:加载 Draco 压缩几何。

  • MeshoptDecoder:Meshopt 顶点/索引压缩解码。

  • stats-gl:FPS/GPU 性能面板。

  • RGBELoader:HDR 贴图加载器(环境光照)。

  • gsap:动画库(用于相机/目标缓动)。

  • lil-gui:轻量 GUI 面板(调试、调参)。

useScene总览与成员

js 复制代码
export class useScene {
  constructor(params){ ... }
  onListener(event, handler){ ... }
  init(){ ... }
  initStates(){ ... }
  initScene(){ ... }
  initCamera(){ ... }
  initLight(){ ... }
  createPanel(){ ... }
  initControls(){ ... }
  initRenderer(){ ... }
  startRender(){ ... }
  cancelAnimation(){ ... }
  startAnimation(){ ... }
  addRender(name, fun){ ... }
  removeRender(name){ ... }
  initSize(width, height){ ... }
  initLoading(){ ... }
  loadModelFile(url){ ... }
  mergeModelGeometry(model, bufferName='position'){ ... }
  getModelObject(){ ... }
  setPosition(model){ ... }
  getPosition(model){ ... }
  getModelPosition = (name) => { ... }
}

构造器与核心字段

js 复制代码
constructor(params){
    this.gsap = gsap
    this.eventTarget = new EventTarget(),
    this.loadingManager = new THREE.LoadingManager()//加载资源管理器
    this.scene = null
    this.camera = null
    this.controls = null
    this.renderer = null
    this.stats = null
    this.raycaster = new THREE.Raycaster();
    this.width = params.width || window.innerWidth
    this.height = params.height || window.innerHeight
    this.container = params.container
    this.renderFunctions = {}
    this.init()
    this.initStates()
}
  • eventTarget:统一事件总线,向外派发加载进度、点击拾取等事件。

  • loadingManager:集中监听资源加载生命周期。

  • raycaster :用于点击拾取(鼠标射线相交)。

  • renderFunctions:自定义渲染回调注册表(按帧执行)。

  • container:渲染画布挂载的 DOM 容器(也是监听点击的区域)。

初始化流程

js 复制代码
init (){
  this.initScene()
  this.initCamera()
  this.initControls()
  this.initRenderer()
  this.startRender()
  this.initLight()
  
  this.getModelObject()
  this.initLoading()
  window.addEventListener('resize', ()=> this.initSize());
}

顺序要点:

  • 先建场景/相机/控制器,再创建渲染器并立即开始渲染循环
  • 加上环境光、点击拾取与加载器事件。
  • 监听 resize,自适应窗口或容器尺寸。

性能状态面板

js 复制代码
initStates(){
  const stats = new Stats({ horizontal: false, trackGPU: true });
  this.container.appendChild(stats.dom);
  this.stats?.init(this.renderer);
  this.stats = stats
}
  • stats-gl 面板插入容器。

  • trackGPU: true 开 GPU 指标(依赖浏览器支持)。

场景初始化

initScene() 方法创建了一个基础 Three.js 场景,并设置了环境贴图

js 复制代码
initScene(){
        const scene = new THREE.Scene();
        scene.name = "scene"
        scene.receiveShadow = true;
        scene.castShadow = true
        scene.background = new THREE.Color('#000F25')
        scene.environment = new RGBELoader().load('/hdr/drachenfels_cellar_1k.hdr' ,(texture)=>{
            scene.environment.mapping = THREE.EquirectangularReflectionMapping;
            scene.environmentIntensity = 1
            scene.environment = texture;
            
        })
        this.scene = scene;
       
    }

关键点:

  • 设置了阴影接收和投射
  • 使用 HDR 环境贴图增强场景真实感
  • 通过 EquirectangularReflectionMapping 实现环境反射

相机初始化

js 复制代码
 initCamera(){
     const camera = new THREE.PerspectiveCamera( 45, this.width / this.height, 0.01, 100000 );
     camera.position.set(0,0,0)
     this.camera = camera
 }
  • 45° 视角,近平面 0.01,远平面 10w(配合对数深度缓冲)。
  • 初始相机位置在原点 (0,0,0);通常实际会在加载模型后用 setPositiongetModelPosition 重新布置。

控制器设置

js 复制代码
initControls(){
    const controls = new OrbitControls( this.camera, this.container);
    controls.enableDamping = true;
    controls.dampingFactor = 0.5;
    controls.zoomSpeed = 2;
    controls.enableZoom = true;//是否缩放
    controls.autoRotate = false;
    controls.minPolarAngle = 0;
    controls.maxPolarAngle = (Math.PI / 180) * 80;
    controls.minAzimuthAngle = -Infinity;
    controls.maxAzimuthAngle = Infinity;
    controls.update();
    this.controls = controls
 }
  • 阻尼开启,阻尼系数 0.5(手感偏重)。

  • 缩放速度 2,禁止自动旋转

  • 俯仰角 限制 0° ~ 80°方位角无限制。

  • 重要 :第二参数绑定在 container 上,即控制器监听容器事件。通常也可绑定 renderer.domElement;此处选 container 便于拾取控制共用同一区域。

初始化灯光

js 复制代码
initLight(){
    const lightsGroup = new THREE.Group();
    lightsGroup.name = "lights"
    const light = new THREE.AmbientLight('#b0ccf0',1.0);
    light.name = '环境光'
    lightsGroup.add(light)
    this.scene.add(lightsGroup)
        
}
  • 仅添加了环境光(浅蓝、强度 1.0)。

  • 将所有灯光放入 lights 组,方便统一管理/销毁。

GUI 面板

js 复制代码
createPanel(){
  return new GUI({ width: 250 });
}

返回一个 lil-gui 面板实例,未默认挂载任何参数,供上层按需使用。

渲染器初始化

js 复制代码
initRenderer(){
        const renderer = new THREE.WebGLRenderer( {
            antialias: true,
            alpha: true,
            powerPreference: 'high-performance',
            premultipliedAlpha: true,
            preserveDrawingBuffer: true, // 允许截图
            logarithmicDepthBuffer: true, // 是否使用对数深度缓存。如果要在单个场景中处理巨大的比例差异
        });
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize(this.width, this.height );
        renderer.sortObjects = true;//启用对渲染对象的排序。这通常用于确保渲染顺序正确,以避免深度排序问题。
        renderer.autoClear =  true;//每个渲染帧之前自动清除渲染目标
        renderer.autoClearColor =  true;//自动清除渲染目标的颜色缓冲区。
        renderer.autoClearDepth = true;//自动清除渲染目标的深度缓冲区。
        renderer.autoClearStencil = true;// 自动清除渲染目标的模板缓冲区。
        renderer.localClippingEnabled = true;// 启用局部剪裁,允许使用
        renderer.outputColorSpace = THREE.SRGBColorSpace;//定义渲染器的输出编码。默认为THREE.SRGBColorSpace  LinearSRGBColorSpace outputEncoding
        renderer.toneMapping = THREE.LinearToneMapping;//禁用色调映射。色调映射用于模拟相机的曝光。
        renderer.toneMappingExposure = 1.5;
        renderer.shadowMap.enabled = true
        // renderer.shadowMap.type = THREE.PCFShadowMap;
        renderer.physicallyCorrectLights = true;//启用物理正确的光照计算
        this.container.appendChild( renderer.domElement );
        this.renderer = renderer
    }

关键项解读:

  • logarithmicDepthBuffer:解决超大场景 z-fighting(更耗性能)。
  • preserveDrawingBuffer :允许截图(会降低性能;截图完可以改回 false)。
  • sortObjects:启用对象排序,半透明/复杂层次时更安全。
  • localClippingEnabled:允许局部裁剪。
  • 色彩空间SRGBColorSpace(现代 three 写法)。
  • 色调映射:线性 + 曝光 1.5(你可按需求换 ACESFilmic)。
  • 物理光照physicallyCorrectLights = true,配合 PBR/环境贴图更真实

渲染循环

js 复制代码
startRender(){
  Object.keys(this.renderFunctions).forEach(key => {
    this.renderFunctions[key]()
  })
  this.renderer.render(this.scene, this.camera);
  this.controls.update();
  this.stats?.update();
  this.animationId = requestAnimationFrame(this.startRender.bind(this))
}

cancelAnimation(){
  cancelAnimationFrame(this.animationId)
}

// 别名:用于"替换环境贴图后重新启动"
startAnimation(){
  this.startRender()
}
  • 每帧先执行自定义渲染任务renderFunctions),再渲染画面。

  • 更新控制器阻尼、更新性能面板。

  • cancelAnimation() 用于暂停,startAnimation() 用于启动渲染

插拔式帧回调

js 复制代码
addRender(name, fun){
  this.renderFunctions[name] = fun;
}
removeRender(name){
  this.renderFunctions[name] ? delete this.renderFunctions[name] : false;
}

名字注册/移除帧级任务,避免重复与泄漏,适合粒子、后期、动画等模块化更新。

自适应尺寸

js 复制代码
initSize(width, height){
  this.width = width || this.container.clientWidth || window.innerWidth;
  this.height = height || this.container.clientHeight || window.innerHeight
  this.camera.aspect = this.width / this.height;
  this.camera.updateProjectionMatrix();
  this.renderer.setSize(this.width, this.height);
}
  • 支持外部传参或自动从容器/窗口取值。

  • 更新相机投影与渲染器大小。

资源加载事件

js 复制代码
initLoading(){
  this.loadingManager.onStart = (url, loaded, total) => {
    this.eventTarget?.dispatchEvent(new CustomEvent('onStart', {
      detail: { url, loaded, total, progress: parseFloat(loaded / total * 100).toFixed(2) }
    }))
  }
  this.loadingManager.onProgress = (url, loaded, total) => {
    this.eventTarget?.dispatchEvent(new CustomEvent('onProgress', {
      detail: { url, loaded, total, progress: parseFloat(loaded / total * 100).toFixed(2) }
    }))
  }
  this.loadingManager.onLoad = () => {
    this.eventTarget?.dispatchEvent(new CustomEvent('onLoad'))
  }
}

通过 EventTarget 对外派发 onStart / onProgress / onLoad,便于 UI 进度条与日志。

模型加载

js 复制代码
loadModelFile(url){
  const dracoLoader = new DRACOLoader();
  const ktx2Loader = new KTX2Loader();

  dracoLoader.setDecoderPath(`${location.href}/draco/gltf/`);
  dracoLoader.setDecoderConfig({ type: "js" });
  dracoLoader.preload();

  const loader = new GLTFLoader(this.loadingManager);
  loader.setKTX2Loader(ktx2Loader);
  loader.setMeshoptDecoder(MeshoptDecoder);
  loader.setDRACOLoader(dracoLoader);

  return loader.loadAsync(url, ({ loaded, total }) => {
    this.eventTarget?.dispatchEvent(new CustomEvent('progress', {
      detail: { url, loaded, total, progress: parseFloat(loaded / total * 100).toFixed(2) }
    }))
  })
}
  • Draco:设置解码路径(需放置 decoder js/wasm 资源)。

  • KTX2 :直接设置给 GLTFLoader,用于解析 KTX2 纹理。

  • Meshopt:减少模型体积并加速加载。

  • 返回 Promisegltf 对象),并在 progress 回调继续派发自定义进度事件。

点击拾取

js 复制代码
getModelObject(){
  this.container.addEventListener('click', (e)=>{
    e.stopPropagation();
    const mouse = {x: 0, y: 0}
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;

    this.raycaster.setFromCamera(mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.scene.children, true);

    if (intersects.length > 0) {
      const clickedObject = intersects[0];
      this.eventTarget?.dispatchEvent(new CustomEvent('click', {
        detail: {
          model: clickedObject.object,
          point: clickedObject.point,
          info: clickedObject
        }
      }));
    }
  });
}
  • container 上监听 click

  • 将屏幕坐标映射到 NDC ,通过 Raycaster 与场景递归相交

  • 命中后派发 click 事件,返回:

    • model:命中的 Mesh/Object3D
    • point:射线命中点世界坐标
    • info:three.js 相交结果对象

setPosition

js 复制代码
setPosition(model){
  const boundingBox = new THREE.Box3().setFromObject(model);
  const boundingSphere = new THREE.Sphere()
  boundingBox.getBoundingSphere(boundingSphere)

  const [leng,width,height] = [
    boundingBox.max.x - boundingBox.min.x,
    boundingBox.max.z - boundingBox.min.z,
    boundingBox.max.y - boundingBox.min.y
  ]

  const diagonal = Math.sqrt(Math.sqrt(leng ** 2 + width ** 2) ** 2 + height ** 2);
  const { x,y,z } = boundingSphere.center

  this.camera.position.set(boundingBox.max.x + diagonal, diagonal, boundingBox.max.z + diagonal);
  this.controls.target.set(x,y,z)
  this.controls.object.updateProjectionMatrix()
  this.camera.updateProjectionMatrix()
}

计算 包围盒/包围球 ,将相机放到盒子对角线延伸 位置,target 指向模型中心,确保整体入镜。

getPosition

js 复制代码
getPosition(model){
  const boundingBox = new THREE.Box3().setFromObject(model);
  const boundingSphere = new THREE.Sphere()
  boundingBox.getBoundingSphere(boundingSphere)
  return boundingSphere.center
}

返回给定模型中心点

getModelPosition

js 复制代码
getModelPosition = (name) => {
  const model = this.scene.getObjectByName(name)
  if(!model){ return }

  const box = new THREE.Box3().setFromObject(model)
  const size = new THREE.Vector3()
  box.getSize(size)
  const maxSize = Math.max(size.x, size.y, size.z)
  const distance = maxSize * 2

  const center = new THREE.Vector3()
  box.getCenter(center)

  const direction = new THREE.Vector3(0, 0, 1)
  const newCameraPos = new THREE.Vector3().copy(center).add(direction.multiplyScalar(distance))


  // gsap.to(this.camera.position,{ x: newCameraPos.x, y: newCameraPos.y + 1, z: newCameraPos.z, duration:1, onUpdate: () => this.controls.update() })
  // gsap.to(this.controls.target,{ x: center.x, y: center.y, z: center.z, duration:1 })
}
  • 通过名称定位目标,计算其中心与尺寸,沿 +Z 方向推远到合适距离。

  • 缓动动画已注释,打开即可实现平滑对焦。

事件对外接口

js 复制代码
onListener (event, handler){
  this.eventTarget?.addEventListener(event, handler);
}

可监听的事件包括:

  • 加载onStart / onProgress / onLoadinitLoading()
  • 模型加载进度progressloadModelFile() 的第二个回调)
  • 点击拾取clickgetModelObject()

使用示例

init.js

js 复制代码
import { useScene } from '@/hooks/useThreeModel';
export const initScene = (params) => {
    const app = new useScene({
        width: params.container.clientWidth,
        height: params.container.clientHeight,
        container: params.container,
    })
    //全局变量
    window.ThreeModel = app;
    // 载入进度
    app.onListener('onStart',() => {
        info.progress = 0;
        info.isloadingManager = true
    })
    app.onListener('onProgress',({detail:{loaded,total,progress}}) => {
        info.Loadtext = "解析中..."
        const num = (loaded / total * 100).toFixed(2);
        info.progress = parseFloat(num);
    })
    app.onListener('onLoad', () => {
        info.isloadingManager = false;
        info.Loadtext = "加载完成"
    })
    app.onListener('progress',({detail:{progress}})=>{
        info.Loadtext = "下载中..."
        info.progress = parseFloat(progress);
    })
    loadModelAsync('/model/Michelle.glb')
}
export const loadModelAsync = async (url) => {
    try {
        const { scene } = await ThreeModel.loadModelFile(url);
      
        ThreeModel.scene.add(scene)
       
    } catch (error) { console.log(error) }
}

index.vue

js 复制代码
<script setup>
import { nextTick, onMounted, reactive, useTemplateRef } from 'vue';
import {initScene} from '@/three/init.js';
const modeView = useTemplateRef('modeView');
const info = reactive({

})
onMounted(()=>{
    nextTick(()=>{
        initScene({
            container: modeView.value
        })
    })
})
</script>
<template>
    <div class="model-view" ref="modeView"></div>
</template>
<style lang="less" scoped>
.model-view{
    width: 100%;
    height: 100%;
    background-color: #000F25;
}
</style>
相关推荐
小高0074 分钟前
React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
前端·javascript·react.js
LuckySusu13 分钟前
【js篇】深入理解类数组对象及其转换为数组的多种方法
前端·javascript
LuckySusu14 分钟前
【js篇】数组遍历的方法大全:前端开发中的高效迭代
前端·javascript
LuckySusu15 分钟前
【js篇】for...in与 for...of 的区别:前端开发中的迭代器选择
前端·javascript
mon_star°20 分钟前
有趣的 npm 库 · json-server
前端·npm·json
去伪存真1 小时前
手把手教你实现用AI大模型做代码审查
前端·人工智能
科粒KL1 小时前
前端学习笔记- 从 HTTP 1.1 到 3,再从 SSE 到 Streamable HTTP
前端·http
盘子素1 小时前
前端实现有校验的大文件下载方案对比
前端
一颗奇趣蛋1 小时前
React.memo & useMemo:为什么 React 里「看起来没变的组件」还是渲染了
前端·react.js
天蓝色的鱼鱼1 小时前
Vue项目多级路径部署终极指南:基于环境变量的统一配置方案
前端·vue.js·架构