普通视图

发现新文章,点击刷新页面。
昨天以前首页

用 three.js 实现 3D 地图

作者 codingWhat
2026年3月20日 20:56

之前写过一篇文章介绍了使用ECharts GL实现立体地图 => 用 ECharts GL 把地图「立」起来,那如果ECharts GL满足不了需求,就可以考虑使用Three.js啦。

Three.js

three.js 是一个在浏览器里把 3D 画出来的图形库。你可以把它想成搭舞台,那咋把舞台搭起来呢?

  1. Scene:搭舞台的“背景板”(灯光、模型、网格)。
  2. Camera:决定你从哪个方向看(透视相机更像“人眼”视角,正交相机更像“测绘”视角)。
  3. Renderer:负责“出画面”(每一帧把 scene + camera 渲染到画布)。

看一眼最小骨架,后面不迷路:

import * as THREE from "three"

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
camera.position.set(0, 0, 10)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
container.appendChild(renderer.domElement)

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}
animate()

理解了这三件事,你再把“地区轮廓”转换成 3D 网格,就等于把数据变成了可以被光照和相机看到的模型。

实现流程

  1. 读取 GeoJSON:先把 Polygon 统一成和 MultiPolygon 一样的数据形态;
  2. 遍历每个 polygon ring:把点序列写进 THREE.ShapemoveTo/lineTo
  3. ExtrudeGeometry 把二维轮廓“拉起来”成三维实体(depth 决定高度,bevel 决定边缘质感)
  4. 给几何体分配材质组合:上表面更亮、侧边更有层次,立体感才会成立
  5. Box3.expandByObject() 算中心与尺寸:一键对齐相机,让视角永远落在地图上
  6. 初始化 CSS2DRenderer:在动画循环里把 WebGL 和 2D 标签叠加起来,再驱动光柱/粒子等小动效

1) 统一 GeoJSON:让 PolygonMultiPolygon 数据结构一致

很多 GeoJSON 在实际项目里会“有时是 Polygon,有时是 MultiPolygon”。如果你写死一套遍历逻辑,就会出现:某些地区根本没被绘制出来,或者逻辑分支越写越乱。

我是这样实现的:如果几何类型是 Polygon,就把 coordinates 包一层,让它变成 MultiPolygon 风格的二维数组。这样后续只写一套循环就够了。

// 统一 GeoJSON(关键点:Polygon -> MultiPolygon-like)
export default function useConversionStandardData() {
  const transfromGeoJSON = (worldData) => {
    const features = worldData.features
    for (let i = 0; i < features.length; i++) {
      const element = features[i]
      if (element.geometry.type === "Polygon") {
        // Polygon: [ [ [x,y], ... ] ]
        // 包一层 -> MultiPolygon-like: [ [ [x,y], ... ] ]
        element.geometry.coordinates = [element.geometry.coordinates]
      }
    }
    return worldData
  }

  return { transfromGeoJSON }
}

数据与坐标:先想清楚你画的是“平面”还是“球面”

THREE.Shape 本质上只认“二维平面坐标”。所以第一件事不是写代码,而是先确认:你的 GeoJSON 点 (x, y) 在你的 Three.js 世界里,究竟应该落到哪里。

  • 如果你的数据已经是“平面化坐标”(例如直接用 GeoJSON 的 (x, y) 去描轮廓),那就可以直接 Shape -> ExtrudeGeometry,不用经纬度转换。
  • 如果你的数据是经纬度 (lon, lat),你就必须先做坐标转换。路线是:用球面映射把点投到球面上,再用四元数让面朝向球面法线。

2) 核心:从轮廓到 3D 面(Shape + ExtrudeGeometry)

这一段就是“把平面地图变成立体模型”的关键啦:只要你理解了它,后面的居中、标签、动效就都只是加配件。

在 Three.js 里:

  • THREE.Shape 负责把轮廓“描一遍”(moveTo/lineTo
  • THREE.ExtrudeGeometry 负责把描好的轮廓“拉高变厚”(depth/bevel 等参数决定你要多立体)

我们可以把GeoJSON 的 ring 逐点喂给 Shape,再一口气拉伸成网格。注意:Mesh 的材质传数组是为了“上表面更亮、侧边更有阴影感”。

import * as THREE from "three"

const extrudeSettings = { depth: 0.2, bevelEnabled: true, bevelSegments: 1, bevelThickness: 0.1 }

function buildRegion3D({ geoJson, topFaceMaterial, sideMaterial }) {
  const mapGroup = new THREE.Group()

  geoJson.features.forEach((feature) => {
    const province = new THREE.Object3D()
    const coordinates = feature.geometry.coordinates

    coordinates.forEach((multiPolygon) => {
      multiPolygon.forEach((polygon) => {
        const shape = new THREE.Shape()
        polygon.forEach(([x, y], i) => (i === 0 ? shape.moveTo(x, y) : shape.lineTo(x, y)))

        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
        province.add(new THREE.Mesh(geometry, [topFaceMaterial, sideMaterial]))
      })
    })

    mapGroup.add(province)
  })

  return mapGroup
}

3) 自动居中

你也不想每换一份 GeoJSON 就手动调相机坐标,对吧?那就用包围盒做“自动聚焦”。

Box3().expandByObject() 会把整个地图包起来,你拿到中心点以后就可以:让相机 lookAt 它,控制器 target 也跟着指向它。

import * as THREE from "three"

function initCameraTargetByBox({ group, camera, controls }) {
  const box3 = new THREE.Box3().expandByObject(group)
  const center = new THREE.Vector3()
  box3.getCenter(center)

  camera.lookAt(center.x, center.y, 0)
  if (controls?.target) controls.target = new THREE.Vector3(center.x, center.y, 0)
}

效果会立刻变“稳定”:换地区数据也能落在视野正中。


4) 标签(CSS2D)与光柱

做到“有形”还不够,得让人看得懂。标签负责告诉你“这是什么”,光柱负责把注意力“引到那里”。

标签(CSS2DRenderer)

这里我没有做3D字体几何体(太重也太麻烦),而是用 HTML div 作为“贴纸”。CSS2DObject 让它能跟随相机正确投影。

import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer"

const create2DTag = (html, className = "") => {
  const div = document.createElement("div")
  div.innerHTML = html
  div.className = className
  div.style.pointerEvents = "none"
  div.style.visibility = "hidden"

  const label = new CSS2DObject(div)
  label.show = (text, point) => {
    label.element.innerHTML = text
    label.element.style.visibility = "visible"
    label.position.copy(point)
  }
  label.hide = () => { label.element.style.visibility = "hidden" }
  return label
}

渲染时只要每帧调用一次 css2dRender.render(scene, camera),标签就会“自动跟镜头走”。

光柱

光柱的漂亮之处在于“看起来更立体”,实现方案:两张切图交叉,再配合透明渲染参数避免穿帮。

import * as THREE from "three"

// textureLoader / 纹理 url 在外层准备
const createLightPillar = (lon, lat, height, textureUrl, color = 0x00ffff) => {
  const group = new THREE.Group()
  const geometry = new THREE.PlaneBufferGeometry(height / 6.219, height)
  geometry.rotateX(Math.PI / 2)
  geometry.translate(0, 0, height / 2)

  const material = new THREE.MeshBasicMaterial({
    map: textureLoader.load(textureUrl),
    color,
    transparent: true,
    depthWrite: false,
    side: THREE.DoubleSide,
  })

  const a = new THREE.Mesh(geometry, material)
  const b = a.clone()
  b.rotateZ(Math.PI / 2)

  group.add(a, b)
  group.position.set(lon, lat, 0)
  return group
}

地图面生成后把光柱加进 mapGroup 就行

const light = createLightPillar(...lightCenter, heightScaleFactor, lightPillarTextureUrl)
light.position.z = 0.31
mapGroup.add(light)

5) 让动画活起来

你不需要把每一帧都烧到极致,但需要让“画面在动”,让它不显得生硬:

  • 背景光圈缓慢旋转
  • 粒子沿 z 轴上升再重置
  • 2D 标签每帧由 CSS2DRenderer 重新渲染

核心循环就四件小事:WebGL 渲染、2D 标签叠加、粒子/旋转等状态更新、以及 TWEEN.update() 推进动画:

loop() {
  requestAnimationFrame(() => this.loop())
  this.renderer.render(this.scene, this.camera)
  if (this.rotatingApertureMesh) this.rotatingApertureMesh.rotation.z += 0.0005
  if (this.css2dRender) this.css2dRender.render(this.scene, this.camera)
  for (const p of this.particleArr || []) {
    p.updateSequenceFrame()
    p.position.z += 0.01
    if (p.position.z >= 6) p.position.z = -6
  }
  TWEEN.update()
}

踩过的坑分享给大家,少走些弯路

  1. 坐标系不匹配THREE.Shape 只认平面坐标。你的 GeoJSON 如果和 Three.js 的绘制坐标不一致,就会出现“地图飞走了”的尴尬,需要先做投影/坐标转换。
  2. 空洞(holes)处理:把 ring 直接塞进 Shape,没有显式处理 shape.holes。一旦你的 GeoJSON 带内环(岛/湖泊/凹洞),不处理 holes 就会“该挖的地方没挖开”。
  3. bevel 参数太大:倒角太厚会让面数暴涨,性能变差。一般从小 bevel 开始试,满足质感再加料。
  4. 数据点顺序:点序自交或乱序时,Shape 可能生成失败,或者“看起来像被折弯”。这类问题通常要先检查几何数据本身。

我的Three.js3D场景编辑器免费开源啦🎉🎉🎉

作者 答案answer
2026年3月17日 13:59

前言

哈喽各位佬,这里话不多说主要是给大家简单分享一下我的另一个开源项目 (ThreeFlow),也是一个基于Three.js开发的3D编辑器类型的项目,如果你正在学习Three.js或者想使用Three.js开发一个企业级的项目那么非常推荐你去学习研究一下这个项目。

ThreeFlow 开源背景

在去年(25年)被裁员失业的背景下,我开始尝试自己开发一个商用性质的Three.js3D项目模版,然后把项目源码提供给有three.js3D开发需求公司,然后赚取一点点的报酬。

当时写的文章:juejin.cn/post/748791…

这也是ThreeFlow的前身,很庆幸也许是因为之前写的Three.js开源项目得到了大家的认可,陆陆续续也有人在看到这个项目后选择愿意付费购买这个项目,而我自己也在这段时间里不断的尝试和探索适合自己的模式。

经过25年这一年多时间的探索和尝试后,我也坚定了要开发一个更成熟稳定的3D低代码编辑器产品而非一个项目代码模版

因此在了解到有部分购买的同学只是因为想学习Three.js而去购买的这个项目时,作者个人觉得只是学习一个框架而去花费几十块或者大几百的价格去购买一个项目代码是不太值得的。

于是我也决定将ThreeFlow个人版项目代码,免费开源给大家学习使用,也希望大家在掌握熟悉了Three.js后能够找到一个更加高薪资的工作。

🔗 在线预览链接

threeflowx.cn/study/

🔥 项目描述

ThreeFlow:一个基于 Three.js+Vue3+Vite+Typescript 实现的3D场景编辑器。

采用企业级项目开发标准:

集成 ESLint + Stylelint + Prettier + Husky + CommitLint 确保代码质量规范。

对Three.js核心操作模块的功能进行单独模块化抽离封装,降低Three.js在前端现代框架(Vue3)中的开发成本。

💥 项目界面

截屏2026-02-16 15.15.12.png

截屏2026-02-16 15.21.43.png

⚖️ 项目主要技术栈

名称 版本 名称 版本
Vue 3.5.13 Typescript 5.7.x
Vite 6.1.x Element-plus 2.9.4
Three 182 Pinia 2.3.x
TWEEN 18.5.0 详见 package.json 🤗

🌐 安装/启动/打包(详见 package.json)

 pnpm install

 pnpm serve 
 
 pnpm build/pnpm build:pro

📚 项目主要功能介绍

  1. 拖拽添加场景内容功能: 通过左侧的拖拽区域选择不同的场景元素往场景中添加内容。
  2. 导入外部模型资源: 支持glb,.obj,gltf,.fbx,.stl,.usdz 等格式的3D模型导入加载。
  3. 快捷键功能: 支持 W(拖拽)、E(旋转)、R(缩放)、F(聚焦)等快捷键功能。
  4. 第一人称模式: 支持第一人称视角漫游功能。
  5. 撤销/重做功能: 支持使用快捷键 撤销(Ctrl+Z/Command+Z)重做。(Ctrl+Shift+Z/Command+Shift+Z) 实现模型位置,缩放,旋转等属性的撤销和重做。
  6. 场景内容复制,删除功能: 支持将选中的内容进行删除和复制多个。
  7. 模型材质属性编辑功能: 支持动态修改模型材质内容如:模型贴图、透明度、材质颜色、材质类型、材质渲染面、粗糙度、金属度等等
  8. 模型动画播放功能: 支持场景中多个模型多个动画同时播放。
  9. 项目配置功能: 支持动态修改Three.js渲染器配置如:色调映射、阴影、曝光度。支持动态修改Three.js场景背景、环境光内容。
  10. 场景内容数据持久化存储: 支持场景内容持久化存储:保存整个场景内容、导出整个场景内容、导入整个场景内容等功能。
  11. 模型导出功能: 支持将场景中的3D模型内容导出成一个模型并支持多种格式的导出(.gltf、.glb、.obj、.usdz)。

✅ 浏览器设备支持

浏览器 最低支持版本 说明
Chrome 57+ ✅ 最推荐,three.js 开发首选
Edge (Chromium) 79+ ✅ 与 Chrome 基本一致
Firefox 52+ ✅ 支持良好,性能略逊 Chrome
Safari (macOS) 11+ ⚠️ 支持 WebGL,但兼容性要多测试
Opera 44+ ✅ 基于 Chromium

🌺 项目目录结构介绍

image.png

1. 入口文件

  • App.vue : 应用程序的根组件,包含路由视图
  • main.js : 应用程序入口文件,负责初始化 Vue 应用、注册全局组件、全局状态、指令和插件

2. /assets 目录

存放静态资源文件:

  • iconFont/ : 阿里巴巴矢量图标库文件(地址: www.iconfont.cn/)
  • image/ : 图片资源
  • previewIcon/ : 模型预览图片
  • textures/ : 资源贴图文件

3. /components 目录

全局组件文件:

  • Loading/ : 自定义封装的页面加载loading
  • index.ts : 组件导出文件

4. /config 目录

常量配置和静态数据配置文件:

  • constant.ts : 常量定义
  • defaultDragList.ts : 左侧模型拖拽资源内容数据
  • propertyConfig.ts : 静态属性配置项

5. /enums 目录

全局枚举文件:

  • enum.ts : 场景、变换控制器、材质等相关枚举定义
  • indexDb.ts : IndexedDB 数据库相关枚举

6. /layouts 目录

布局组件文件:

  • RenderView.vue : 渲染视图布局组件,作为应用的主要承载容器

7. /router 目录

路由配置文件:

  • index.ts : Vue Router 路由配置入口,定义应用页面导航规则

8. /store 目录

Pinia 状态管理文件:

  • indexDbStore.ts : IndexedDB 数据操作状态管理
  • pinia.ts : Pinia 实例初始化配置
  • sceneEditStore.ts : 场景编辑器核心状态管理(包括场景对象、选中状态等)

9. /style 目录

样式资源文件:

  • iconFont.scss : 字体图标样式定义
  • index.scss : 全局通用样式入口
  • reset.scss : 浏览器默认样式重置

10. /types 目录

TypeScript 类型定义文件:

  • global.d.ts : 全局通用类型声明
  • indexDbTypes.ts : IndexedDB 数据结构类型定义
  • renderModelTypes.ts : 渲染模型相关接口定义
  • rightPanelTypes.ts : 右侧属性面板配置类型定义
  • three-css3d.d.ts : CSS3D 渲染器类型声明
  • three-utils.d.ts : Three.js 工具函数类型声明

11. /utils 目录

核心工具函数与逻辑封装:

  • directive.ts : Vue 自定义指令注册
  • globalComponent.ts : 全局组件自动注册逻辑
  • globalProperties.ts : Vue 全局属性挂载
  • historyModules/ : 操作历史记录(撤销/重做)模块封装
  • indexedDB.ts : IndexedDB 数据库操作封装类
  • renderScene.ts : 核心文件,Three.js 场景渲染逻辑封装(初始化、渲染循环、事件监听等)
  • sceneModules/ : 场景功能模块(灯光、动画、变换控制等)
  • utils.ts : 通用辅助函数

12. /views 目录

页面视图文件:

  • sceneEdit/ : 3D 场景编辑器主视图
    • index.vue : 编辑器入口组件
    • layouts/ : 编辑器内部布局组件(左侧拖拽栏、右侧属性面板、顶部工具栏等)

💚 项目仓库

GitHub: github.com/zhangbo126/…

Gitee: gitee.com/ZHANG_6666/…

结语

如果你觉得该项目对你有帮助那就给项目留个star⭐吧,这是对作者每次熬夜牺牲休息时间去更新开源项目最大的动力支持😄😁

Three.js一起学-如何通过官方例子高效学习 Three.js?手把手带你“抄”出一个3D动画

2026年3月16日 18:16

前言

在之前的文章中有说过,我认为学习three.js最好的方法就是通过官网的例子去学习,在实践中练习。 因为Three.js中api很多,没有系统的学习文档,因此我觉得通过例子去学习,使用到某个概念或者api在深入学习它,这是一个不错的学习方式。 那么具体如何学习呢?

45ce1782386deb25cf01ea76ee0705b6.gif

好了言归正传,下面我就教大家如何打出一套如来神掌,啊不对,是通过官方例子学习Three.js。

0CFA5E8C.jpg

准备

阅读本文需要一点点的 WebGL 的知识点,至少这个文档的基础知识部分看完即可。

此外各位同学需要下载一下 Three.js 源码。源码里有我们需要用到的模型和官网例子的源码。

搭建一个 web 工程,本文演示创建基于 vue3 的工程。

bash

npm init vue@latest

按照自己的喜好选择要安装哪些插件即可。生成的项目结构如下,我们为此项目添加 three.js,修改 package.json 如下。

json

{
  "dependencies": {
    ...
    "three": "^0.143.0"
  }
}

如需使用 typescript,添加如下依赖:

json

{
  "devDependencies": {
    ...  
    "@types/three": "^0.143.0"
  }
}

然后执行 npm install 安装依赖即可。

至此我们前期创建项目的准备工作就做完了。

体验例子

接下来我们在 three.js 官网上找到 感兴趣的例子,打开它的源码,先大致阅读一遍。通过这个例子中我们将学到如下几个知识点:相机、场景、网格、灯光、材质、形状、动画等。下面我们跟着例子写一遍代码,带大家学会如何通过例子去学习。

j1j33-c8781.gif

写(抄)代码

源码在上面的链接已经给出来,我就不在这里凑字数了。我在这里一步一步地写一下上述例子的代码,并演示如何通过此例来学习 Three.js。那么就让我们愉快地开始吧。

首先,我们在 Vue 项目中创建一个组件(比如 ThreeExample.vue),并在 mounted 生命周期中编写 Three.js 代码。当然,你也可以用原生 HTML+JS,但这里我们以 Vue3 为例。

1. 初始化场景、相机和渲染器

打开官方例子的源码,我们会看到一开始就创建了 scenecamerarenderer 这三个基本对象。这是每一个 Three.js 应用的起点。

js

// 1. 创建场景
const scene = new THREE.Scene();

// 2. 创建透视相机(参数:视野角度、宽高比、近裁面、远裁面)
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.set(2, 2, 5); // 设置相机位置
camera.lookAt(0, 0, 0);        // 让相机看向原点

// 3. 创建 WebGL 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement); // 将 canvas 添加到页面

学习点:这里我们用到了 PerspectiveCamera,如果不清楚它的参数含义,就可以去 官网文档 查看。文档会详细解释每个参数的作用,比如 fov(视野角度)决定了你能看到多大的范围,aspect(宽高比)通常设为画布的宽度/高度,否则图像会被拉伸。

2. 添加光源

例子中使用了多种光源:环境光、点光源和聚光灯。光源是让物体可见并产生阴影和立体感的关键。

js

// 环境光:提供基础照明,均匀照亮所有面
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);

// 点光源:从某个点向所有方向发射光线
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(1, 2, 3);
scene.add(pointLight);

// 还可以添加其他光源,比如聚光灯、平行光等,根据需要选择

学习点:看到 AmbientLight 和 PointLight,我们可以去文档了解每种光源的特点和适用场景。例如环境光没有方向,通常用来提亮阴影部分;点光源类似灯泡,会产生阴影(需配合阴影设置)。

3. 加载模型

例子中加载了一个带有骨骼动画和变形动画的模型(models/gltf/Soldier.glb)。我们需要使用 GLTFLoader 来加载 glTF 格式的模型。

首先,引入加载器(需要额外导入):

js

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

然后,在代码中创建加载器并加载模型:

js

const loader = new GLTFLoader();
loader.load('models/gltf/Soldier.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);

  // 获取模型的动画剪辑
  const animations = gltf.animations;
  if (animations && animations.length) {
    // 创建动画混合器并播放动画(后面会讲到)
    mixer = new THREE.AnimationMixer(model);
    const action = mixer.clipAction(animations[0]);
    action.play();
  }
}, undefined, (error) => {
  console.error('模型加载失败:', error);
});

注意:模型路径需要根据你存放的位置调整。官方源码中的模型路径是相对 examples/ 目录的,你可以把 models 文件夹复制到你的 public 目录下。

学习点:遇到 GLTFLoader,我们可以去文档或源码中查看它的用法。GLTF 是 Three.js 推荐的 3D 模型格式,支持动画、材质、骨骼等。通过这个例子,我们学会了如何加载外部模型并添加到场景。

4. 动画循环

例子中使用 requestAnimationFrame 实现动画循环,并在每一帧更新动画混合器。

js

let mixer = null; // 在加载模型时赋值

function animate() {
  requestAnimationFrame(animate);

  const delta = clock.getDelta(); // 获取时间差,用于平滑动画
  if (mixer) {
    mixer.update(delta); // 更新动画混合器
  }

  // 渲染场景
  renderer.render(scene, camera);
}

// 创建 Clock 对象用于计算时间差
const clock = new THREE.Clock();

// 启动动画循环
animate();

学习点AnimationMixer 和 Clock 是 Three.js 中处理动画的重要 API。通过查看文档,我们可以了解 mixer.update(delta) 如何基于时间差驱动模型动画。

5. 处理窗口大小变化

为了让画布自适应窗口,我们需要监听窗口的 resize 事件,更新相机宽高比和渲染器尺寸。

js

window.addEventListener('resize', onWindowResize, false);

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix(); // 必须调用,使相机参数生效
  renderer.setSize(window.innerWidth, window.innerHeight);
}

学习点updateProjectionMatrix() 方法的作用是更新相机的投影矩阵,当相机参数改变时(如宽高比),需要调用此方法让 Three.js 重新计算投影。

6. 遇到不懂的就去官网查找 API

在“抄”代码的过程中,你一定会遇到很多陌生的 API,比如 PointLightGLTFLoaderAnimationMixerClockupdateProjectionMatrix 等等。这时候,最好的学习方式就是打开 Three.js 官方文档,搜索这些 API,仔细阅读其用法和参数含义。

例如,你看到 camera.lookAt(0, 0, 0),可以查阅 Object3D 的 lookAt 方法,理解它是如何让物体朝向某个点的。

文档通常包含详细说明和示例代码,非常有助于深入理解。通过这样的方式,你不仅学会了这个例子,还能举一反三,应用到其他场景。

完整的代码整合

将上述代码片段整合到一个 Vue 组件中,大概如下(省略了样式和模板部分):

vue

<template>
  <div ref="container" style="width:100%; height:100vh;"></div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const container = ref(null);

onMounted(() => {
  // 1. 创建场景
  const scene = new THREE.Scene();

  // 2. 创建相机
  const camera = new THREE.PerspectiveCamera(45, container.value.clientWidth / container.value.clientHeight, 1, 2000);
  camera.position.set(2, 2, 5);
  camera.lookAt(0, 0, 0);

  // 3. 创建渲染器
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(container.value.clientWidth, container.value.clientHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  container.value.appendChild(renderer.domElement);

  // 4. 添加光源
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
  scene.add(ambientLight);
  const pointLight = new THREE.PointLight(0xffffff, 1);
  pointLight.position.set(1, 2, 3);
  scene.add(pointLight);

  // 5. 加载模型
  const loader = new GLTFLoader();
  let mixer = null;
  loader.load('/models/gltf/Soldier.glb', (gltf) => {
    const model = gltf.scene;
    scene.add(model);
    if (gltf.animations.length) {
      mixer = new THREE.AnimationMixer(model);
      const action = mixer.clipAction(gltf.animations[0]);
      action.play();
    }
  });

  // 6. 动画循环
  const clock = new THREE.Clock();
  function animate() {
    requestAnimationFrame(animate);

    const delta = clock.getDelta();
    if (mixer) {
      mixer.update(delta);
    }

    renderer.render(scene, camera);
  }
  animate();

  // 7. 窗口大小自适应
  const onWindowResize = () => {
    camera.aspect = container.value.clientWidth / container.value.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(container.value.clientWidth, container.value.clientHeight);
  };
  window.addEventListener('resize', onWindowResize);
});
</script>

如何进一步深入学习

通过“抄”这个例子,我们已经接触到了 Three.js 的核心概念。但这仅仅是开始。下面是一些进阶学习建议:

  1. 修改参数,观察效果:尝试调整相机位置、光源颜色和强度、模型缩放比例等,实时查看变化,加深理解。
  2. 尝试添加交互:比如使用 OrbitControls 让用户可以用鼠标旋转视角。引入对应的控制器并启用,能极大地提升体验。
  3. 研究动画:例子中只播放了第一个动画剪辑。你可以尝试播放其他动画,或者混合多个动画。
  4. 阅读更多例子:Three.js 官网有上百个例子,涵盖了粒子系统、后期处理、物理效果等。按照同样的方法,一个个“抄”过去,你的 Three.js 水平会飞速提升。
  5. 参与社区:遇到问题时,可以到 Stack Overflow、GitHub Issues 或中文社区(如掘金)提问,也可以阅读他人的源码和文章。

最后

以上就是本文的全部内容了。我们通过一个官方的骨骼动画例子,逐步学习了 Three.js 的基础:场景、相机、渲染器、光源、模型加载、动画和自适应窗口。更重要的是,我们实践了“通过例子学习”的方法——遇到不懂的 API 就去查文档,然后亲手写一遍。

这种学习方法不仅适用于 Three.js,也适用于其他任何技术栈。希望各位同学能举一反三,不再畏惧陌生的框架和库,勇敢地打开源码,开始你的“抄”级学习之旅!

如果感觉阅读本文后有所收获,欢迎点赞、收藏和评论,也欢迎分享你通过例子学习 Three.js 的心得。我们下期再见!

后续会持续更新我的WebGL和Three.js学习过程和经验分享,如果感兴趣可以关注我的专栏Three.js一起来学

相关文章会同步发布到我的公众号:【编程智匠】

❌
❌