• ThreeJS - 封装一个GLB模型展示组件(TypeScript)


    一、引言

            最近基于Three.JS,使用class封装了一个GLB模型展示,支持TypeScript、支持不同框架使用,具有多种功能。  (下图展示一些基础的功能,可以自行扩展,比如光源等)

    二、主要代码 

    本模块依赖: three、 @types/three, 请先下载这两个npm

    yarn add three @types/three   或    npm i three @types/three 

    使用了class进行封装,将主要的操作代码从组件中抽离出来,便于不同框架之间的使用 

    1. // /components/ShowModel/GLBModel.ts
    2. import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
    3. import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
    4. import * as THREE from "three";
    5. import Stats from "three/examples/jsm/libs/stats.module.js";
    6. import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
    7. import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
    8. import { onErr, setting } from "./type";
    9. /**GLB模型展示 */
    10. class GLBModel {
    11. /**当前canvas挂载的node节点 */
    12. node: HTMLElement
    13. /**判断模型是否加载完成(代表那些原本undefined的变量已经可以使用了)*/
    14. load = false
    15. /**一些模式的开关和设置,外部只读,修改无效。会把配置保存在本地存储,记录数据 */
    16. setting!: setting
    17. /**渲染器 */
    18. private renderer!: THREE.WebGLRenderer
    19. /**摄像机 */
    20. private camera!: THREE.PerspectiveCamera
    21. /**场景 */
    22. private scene!: THREE.Scene;
    23. /**操控摄像机的控制器 */
    24. private controls!: OrbitControls;
    25. /**性能统计信息的工具 */
    26. private stats!: Stats
    27. /**clock对象,用于跟踪时间的流逝,并在动画循环中提供统一的时间更新 */
    28. private clock!: THREE.Clock
    29. /**辅助观察的帮助器,包括 三维坐标、网格平面、包围盒框框 */
    30. private helpers?: ReturnType<typeof initHelper>['helper']
    31. /**包围盒有关的数据,包括放大倍数、放大后的中心坐标、放大后的模型大小 */
    32. private box?: ReturnType<typeof getBoxAndScale>['box']
    33. /**动画混合器 */
    34. private mixer?: THREE.AnimationMixer
    35. /**当前模型实例 */
    36. private gltf?: GLTF
    37. /**模型的动画列表 */
    38. private actionList: THREE.AnimationAction[] = []
    39. /**模型的原始材质Map,可以用于还原 */
    40. private originalMesh = new Map<THREE.Mesh, THREE.Mesh["material"]>()
    41. /**当内部的setting变量发生改变时,会触发这个函数,可以用于框架的响应式 */
    42. private settingChangeCallback?: (setting: setting) => void
    43. /**GLB模型展示 - 构造函数
    44. * @param node 要挂载canvas画布的节点。注意需要设置好node的宽高
    45. * @param settingChangeCallback 当内部的setting变量发生改变时,会触发这个函数,可以用于框架的响应式
    46. */
    47. constructor(node: HTMLElement, settingChangeCallback?: (setting: setting) => void) {
    48. this.node = node
    49. this.settingChangeCallback = settingChangeCallback
    50. Object.assign(this, initBaseDevice(node), initOtherDevice(node))//这个操作是,把函数的返回值赋值到this上, 省的我一个个去 this.xxx = xxx
    51. this.resizeListen()
    52. this.settingFn.getSettingFromLocal()//给setting属性赋值
    53. }
    54. /**加载glb模型,同时进行基础设置
    55. * @param url 要加载的url
    56. * @param onload 加载成功的回调函数
    57. * @param onProgress 进度更新时触发的函数,可以用来配置进度条
    58. * @param onErr 加载失败的回调
    59. */
    60. loadGlb(url: string, onload: (data: GLTF) => void, onProgress: (e: ProgressEvent) => void, onErr?: onErr) {
    61. /**dracoLoader模型压缩器 */
    62. const dracoLoader = new DRACOLoader();
    63. dracoLoader.setDecoderPath('https://threejs.org/examples/jsm/libs/draco/gltf/');//这段代码在部署时会不会报错?
    64. /**glb模型加载器 */
    65. const loader = new GLTFLoader();
    66. loader.setDRACOLoader(dracoLoader); //设置压缩器
    67. loader.load(
    68. url,
    69. (gltf) => {
    70. this.gltf = gltf
    71. const model = gltf.scene;
    72. this.box = getBoxAndScale(model, this.camera, this.controls, this.scene).box
    73. this.helpers = initHelper(150, this.box.centerWithScale, model).helper;
    74. this.mixer = new THREE.AnimationMixer(model); //设置新的动画混合器
    75. this.actionList = getAnimations(gltf, this.mixer); //获取动画列表
    76. this.animate()
    77. this.originalMesh = getOriginalMesh(model)//保存原始材质
    78. onload(gltf)
    79. this.load = true
    80. this.settingFn.setFromLocal()
    81. },
    82. onProgress,
    83. (e) => {
    84. onErr && onErr(e);
    85. console.error("加载glb模型出错啦", e);
    86. });
    87. };
    88. /**卸载时需要做的事。 */
    89. destory() {
    90. try {
    91. this.resizeDestory();//清除DOM监听
    92. window.cancelAnimationFrame(this.animateKey || 0);//清除canvas动画
    93. while (this.node.firstChild) this.node.firstChild.remove(); //删除DOM下所有子元素
    94. } catch (error) {
    95. console.error('执行清除函数失败,请检查问题。可能是由于this指向的问题,请保证此函数的调用者是实例本身。', error);
    96. //注意调用时,必须保证调用者是实例本身,否则此处请改为箭头函数
    97. }
    98. }
    99. /**开启/关闭骨架模式
    100. * @param open 开启还是关闭
    101. * @param onErr 失败的回调
    102. */
    103. changeWireframe(open: boolean, onErr?: onErr) {
    104. try {
    105. this.judgeLoad()
    106. this.gltf!.scene.traverse(function (child) {
    107. if (child instanceof THREE.Mesh) {
    108. child.material.wireframe = open; //查看骨架模式
    109. }
    110. });
    111. this.settingFn.setSetting('wireframe', open)
    112. } catch (error) {
    113. console.error('开启/关闭骨架模式失败', error)
    114. onErr && onErr(error)
    115. }
    116. }
    117. /**开启/关闭法线模式 */
    118. changeNormal(open: boolean, onErr?: onErr) {
    119. try {
    120. this.judgeLoad()
    121. this.gltf!.scene.traverse((object) => {
    122. if (object instanceof THREE.Mesh) {
    123. if (open) {
    124. object.material = new THREE.MeshNormalMaterial({
    125. transparent: true, // 是否开启使用透明度
    126. wireframe: this.setting.wireframe, //骨架模式
    127. opacity: 0.8, // 透明度
    128. depthWrite: false, // 关闭深度写入 透视效果
    129. });
    130. } else {
    131. const origin = this.originalMesh.get(object); //原始材质
    132. object.material = origin;
    133. this.changeWireframe(this.setting.wireframe);
    134. }
    135. }
    136. });
    137. this.settingFn.setSetting('normal', open)
    138. } catch (error) {
    139. console.error('开启/关闭法线模式失败', error)
    140. onErr && onErr(error)
    141. }
    142. }
    143. /**开启/关闭动画
    144. * @param open 是否开启
    145. * @param onErr 失败回调,参数是失败提示
    146. */
    147. changeAnimation(open: boolean, onErr?: onErr) {
    148. try {
    149. if (open && !this.actionList.length) {
    150. console.log("该模型暂无动画哦");
    151. onErr && onErr("该模型暂无动画哦")
    152. return;
    153. }
    154. this.actionList.forEach((k) => {
    155. open ? k.play() : k.stop();
    156. });
    157. this.settingFn.setSetting('animation', open)
    158. } catch (error) {
    159. console.error('开启/关闭动画失败', error)
    160. onErr && onErr(error)
    161. }
    162. };
    163. /**开启/关闭坐标系 */
    164. changeAxesHelper(open: boolean, onErr?: onErr) {
    165. try {
    166. this.judgeLoad()
    167. open ? this.scene.add(this.helpers!.axesHelper) : this.scene.remove(this.helpers!.axesHelper)
    168. this.settingFn.setSetting('axesHelper', open)
    169. } catch (error) {
    170. console.error('开启/关闭坐标系失败', error);
    171. onErr && onErr(error)
    172. }
    173. }
    174. /**开启/关闭网格 */
    175. changeGridHelper(open: boolean, onErr?: onErr) {
    176. try {
    177. this.judgeLoad()
    178. open ? this.scene.add(this.helpers!.gridHelper) : this.scene.remove(this.helpers!.gridHelper)
    179. this.settingFn.setSetting('gridHelper', open)
    180. } catch (error) {
    181. console.error('开启/关闭网格失败', error);
    182. onErr && onErr(error)
    183. }
    184. }
    185. /**开启/关闭包围盒 */
    186. changeBoundingBoxHelper(open: boolean, onErr?: onErr) {
    187. try {
    188. this.judgeLoad()
    189. open ? this.scene.add(this.helpers!.boundingBoxHelper) : this.scene.remove(this.helpers!.boundingBoxHelper)
    190. this.settingFn.setSetting('boundingBoxHelper', open)
    191. } catch (error) {
    192. console.error('开启/关闭包围盒 失败', error);
    193. onErr && onErr(error)
    194. }
    195. }
    196. /**切换背景颜色,参数是十六进制颜色字符串 */
    197. changeBgcolor(hex: string, onErr?: onErr) {
    198. try {
    199. this.judgeLoad()
    200. this.scene.background = new THREE.Color(hex); //场景背景色
    201. this.settingFn.setSetting('bgcolor', hex)
    202. } catch (error) {
    203. console.error('开启/关闭包围盒 失败', error);
    204. onErr && onErr(error)
    205. }
    206. }
    207. /**相机归回原位 */
    208. cameraOriginalPosition(onErr?: onErr) {
    209. try {
    210. this.judgeLoad()
    211. const { camera, controls, box } = this
    212. camera.position.copy(box!.sizeWithScale); //设置摄像机的初始位置,乘上缩放倍数
    213. controls.target.copy(box!.centerWithScale); //设置摄像机旋转和放大等操作的目标点
    214. } catch (error) {
    215. console.error('相机归回原位 失败', error);
    216. onErr && onErr(error)
    217. }
    218. };
    219. /**有关于setting的一些函数 */
    220. private settingFn = {
    221. /**设置模块配置 */
    222. setSetting: extends keyof setting>(key: T, value: setting[T]) => {
    223. this.setting[key] = value
    224. localStorage.setItem('glbModelSetting', JSON.stringify(this.setting))//存到本地存储
    225. this.settingChangeCallback && this.settingChangeCallback(this.setting)
    226. },
    227. /**从本地存储读出设置,保存在实例中 */
    228. getSettingFromLocal: () => {
    229. const setting = JSON.parse(localStorage.getItem('glbModelSetting') || 'null') as setting | null
    230. if (setting) {
    231. this.setting = setting
    232. } else {
    233. this.setting = {
    234. wireframe: false,
    235. normal: false,
    236. animation: false,
    237. axesHelper: false,
    238. gridHelper: false,
    239. boundingBoxHelper: false,
    240. bgcolor: "#000000"
    241. }
    242. }
    243. },
    244. /**根据setting,配置对应的模式 - 在加载模型后使用 */
    245. setFromLocal: () => {
    246. const setting = this.setting
    247. //设置这些设置的函数,都是 change + Xxxxx 形式的命名,所以下面直接遍历调用
    248. for (const key in setting) {
    249. if (Object.prototype.hasOwnProperty.call(setting, key)) {
    250. const fnName = 'change' + key.slice(0, 1).toUpperCase() + key.slice(1)
    251. try {
    252. (this as any)[fnName]((setting as any)[key])
    253. } catch (error) {
    254. console.log('调用', fnName, '失败', error);
    255. }
    256. }
    257. }
    258. }
    259. }
    260. /**判断是否加载完成,没完成的话会抛出错误,可以被catch捕获 */
    261. private judgeLoad = () => {
    262. if (!this.load) {
    263. throw '模型还未加载完成'
    264. }
    265. }
    266. /**窗口监听事件的卸载函数,在卸载时需要清除 */
    267. private resizeDestory!: () => void
    268. /**绑定窗口大小监听事件 */
    269. private resizeListen() {
    270. const { node, camera, renderer, scene } = this
    271. //下面这个监听,可能有性能问题吧,看左上角自带的性能指标,拖动时起伏很大,如果加节流的话,又会因为没有及时更新而大小不同
    272. /**创建 ResizeObserver 实例 */
    273. let observer: ResizeObserver | null = new ResizeObserver(entries => {
    274. for (let entry of entries) {
    275. const width = entry.contentRect.width;
    276. const height = entry.contentRect.height;
    277. camera.aspect = width / height; //设置新比例
    278. camera.updateProjectionMatrix(); //更新相机的投影矩阵
    279. renderer.setSize(width, height);
    280. renderer.render(scene, camera) //渲染
    281. }
    282. });
    283. observer.observe(node); // 开始观察目标元素
    284. this.resizeDestory = () => {
    285. observer!.unobserve(node); // 停止观察目标元素
    286. observer!.disconnect();// 停止观察所有元素
    287. observer = null //垃圾回收
    288. }
    289. }
    290. /**当前canvas的动画key,在卸载时需要清除 */
    291. private animateKey: number = 0
    292. /**canvas动画,在这里更新数据并实时render渲染 */
    293. private animate = () => {
    294. this.animateKey = window.requestAnimationFrame(this.animate);
    295. const delta = this.clock.getDelta(); // 获取每帧的时间间隔,从而可以根据时间进行动画更新,使动画在不同的设备和性能下保持一致
    296. this.mixer!.update(delta); //更新动画
    297. this.controls.update(); //操作器更新
    298. this.stats.update(); //更新性能计算器
    299. this.renderer.render(this.scene, this.camera) //渲染
    300. }
    301. }
    302. export default GLBModel
    303. /**初始化基础设备 */
    304. const initBaseDevice = (node: HTMLElement) => {
    305. /**节点宽度 */
    306. const width = node.clientWidth;
    307. /**节点高度 */
    308. const height = node.clientHeight;
    309. /**渲染器 */
    310. const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); //antialias启用抗锯齿功能
    311. renderer.setPixelRatio(window.devicePixelRatio); //设置渲染器的设备像素比例的方法,在不同设备展示一样的东西
    312. renderer.setSize(width, height); //设置宽高
    313. node.appendChild(renderer.domElement); //挂载渲染器DOM
    314. /**摄像机 */
    315. const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
    316. /**创建场景 */
    317. const scene = new THREE.Scene();
    318. scene.background = new THREE.Color(0x000000); //场景背景色
    319. scene.environment = new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(renderer), 0.04).texture; //将场景的当前光照信息计算为环境贴图。第二个参数 0.04 指定了纹理的精度,数值越小表示精度越高,但计算时间也越长。
    320. /**操控摄像机的控制器 */
    321. const controls = new OrbitControls(camera, renderer.domElement);
    322. controls.update(); //更新控制器的状态。在动画函数中也需要执行
    323. controls.enablePan = true; //是否启用控制器的右键平移功能。
    324. controls.enableDamping = true; //是否启用惯性功能
    325. return {
    326. /**渲染器 */
    327. renderer,
    328. /**摄像机 */
    329. camera,
    330. /**场景 */
    331. scene,
    332. /**操控摄像机的控制器 */
    333. controls,
    334. };
    335. };
    336. /**初始化其它设备,如性能展示器、clock时钟 */
    337. const initOtherDevice = (node: HTMLElement) => {
    338. /**用于在 WebGL 渲染中显示性能统计信息的工具 */
    339. const stats = new Stats();
    340. stats.dom.style.position = "absolute";
    341. node.appendChild(stats.dom); //挂载性能展示DOM
    342. /**clock对象,用于跟踪时间的流逝,并在动画循环中提供统一的时间更新 */
    343. const clock = new THREE.Clock();
    344. return {
    345. /**用于在 WebGL 渲染中显示性能统计信息的工具 */
    346. stats,
    347. /**clock对象,用于跟踪时间的流逝,并在动画循环中提供统一的时间更新 */
    348. clock,
    349. };
    350. };
    351. /**初始化三维坐标系、网格帮助器、包围盒帮助器 */
    352. const initHelper = (size: number, center: THREE.Vector3, model: THREE.Group) => {
    353. /**AxesHelper:辅助观察的坐标系 */
    354. const axesHelper = new THREE.AxesHelper(size);
    355. axesHelper.position.copy(center); //三维坐标系的位置
    356. /**网格帮助器 */
    357. const gridHelper = new THREE.GridHelper(size, size);
    358. gridHelper.position.copy(center); //网格的位置
    359. /**新包围盒辅助展示 */
    360. const boundingBoxHelper = new THREE.BoxHelper(model); //创建一个BoxHelper对象,传入模型的网格对象作为参数
    361. boundingBoxHelper.material.color.set(0xff0000); //将包围盒的材质设置为红色
    362. return {
    363. /**辅助观察的帮助器 */
    364. helper: {
    365. /**辅助观察的坐标系 */
    366. axesHelper,
    367. /**网格帮助器 */
    368. gridHelper,
    369. /**包围盒轮廓,可以添加到场景中 */
    370. boundingBoxHelper,
    371. },
    372. };
    373. };
    374. /**获得模型包围盒的数据,并计算模型位置、缩放倍数,设置相机位置等,最后把模型添加到场景。 */
    375. const getBoxAndScale = (model: THREE.Group, camera: THREE.PerspectiveCamera, controls: OrbitControls, scene: THREE.Scene) => {
    376. /**获取模型包围盒 */
    377. const boundingBox = new THREE.Box3().expandByObject(model);
    378. /**获取包围盒的size */
    379. const size = boundingBox.getSize(new THREE.Vector3()); //设置size
    380. /**中心坐标*/
    381. const center = boundingBox.getCenter(new THREE.Vector3()); // 计算包围盒中心坐标,并将中心坐标保存在center向量中
    382. /**设置的缩放倍数,根据实际情况进行调整 */
    383. const scale = 10 / Math.max(size.x, size.y, size.z); // 分母是期望的模型大小
    384. // const scale = 1;
    385. /**中心点的三维向量 * 放大值 */
    386. const centerWithScale = center.clone().multiplyScalar(scale);
    387. /**盒子的三维向量 * 放大值 */
    388. const sizeWithScale = size.clone().multiplyScalar(scale);
    389. // console.log("boundingBox", boundingBox);
    390. // console.log("size", size);
    391. // console.log("center", center);
    392. // console.log("scale", scale);
    393. // console.log("centerWithScale", centerWithScale);
    394. // console.log("sizeWithScale", sizeWithScale);
    395. model.scale.set(scale, scale, scale); //设置模型缩放倍率
    396. camera.position.copy(sizeWithScale); //设置摄像机的初始位置,乘上缩放倍数
    397. controls.target.copy(centerWithScale); //设置摄像机旋转和放大等操作的目标点
    398. scene.add(model); //把模型添加进去
    399. return {
    400. /**包围盒有关的信息 */
    401. box: {
    402. /**缩放倍率 */
    403. scale,
    404. /**放大后的中心点的三维向量 */
    405. centerWithScale,
    406. /**放大后的盒子的三维向量 */
    407. sizeWithScale,
    408. },
    409. };
    410. };
    411. /**获取模型上的全部动画,返回动画实例列表,后续操控实例列表即可 */
    412. const getAnimations = (gltf: GLTF, mixer: THREE.AnimationMixer) => {
    413. const actionList: THREE.AnimationAction[] = [];
    414. // 遍历模型的动画数组,为个动画创建剪辑并添加到混合器中
    415. for (let i = 0; i < gltf.animations.length; i++) {
    416. const animation = gltf.animations[i];
    417. const action = mixer.clipAction(animation); //创建
    418. actionList.push(action);
    419. action.setLoop(THREE.LoopRepeat, Infinity); // 设置动画播放相关参数:循环模式、重复次数
    420. action.clampWhenFinished = true; // 动画在播放完成后会停留在最后一帧,不再继续播放 (但是上面设置了循环播放,所以不影响)
    421. // action.play(); // 播放动画
    422. }
    423. return actionList;
    424. };
    425. /**获取模型身上的原始材质,返回map */
    426. const getOriginalMesh = (model: THREE.Group) => {
    427. const map = new Map<THREE.Mesh, THREE.Mesh["material"]>();
    428. //设置模型原始材质
    429. model.traverse((object) => {
    430. if (object instanceof THREE.Mesh) {
    431. map.set(object, object.material);
    432. }
    433. });
    434. return map;
    435. };

    其中 type.ts 本文件所需的部分内容如下: (完整内容在 三-1-(3) 里)

    1. // /components/ShowModel/type.ts
    2. //...
    3. /**展示3D模型的组件Props */
    4. export interface showModelProps {
    5. /**要展示的模型的URL */
    6. url: string;
    7. /**组件最外层的style。在这里面指定宽高等。不指定宽高,将会适配父元素宽高 */
    8. style?: CSSProperties;
    9. /**工具栏的扩展render。参数是内部数据 */
    10. toolBarRender?: (instance: GLBModel) => ReactNode;
    11. }
    12. /**各个工具的开关和设置等,外部只读 */
    13. export interface setting {
    14. /**是否开启了骨架模式 */
    15. wireframe: boolean,
    16. /**是否开启了法线模式 */
    17. normal: boolean,
    18. /**是否开启了动画 */
    19. animation: boolean
    20. /**是否开启了坐标系 */
    21. axesHelper: boolean
    22. /**是否开启了网格 */
    23. gridHelper: boolean
    24. /**是否开启了包围盒 */
    25. boundingBoxHelper: boolean
    26. /**背景色,十六进制字符串 */
    27. bgcolor: string
    28. }
    29. /**失败的回调函数 */
    30. export type onErr = (e: any) => void

    三、示例 - 在React中使用

    本文以react示例,演示如何封装组件

    1. 封装组件

    基于antd组件库,所以请先下载依赖(不想使用antd的话,可以把下文有关的组件替换成自己的)

    npm i antd @ant-design/icons  或  yarn add antd @ant-design/icons

     (1)index.tsx

    主要的操作,其实就是下面这两步,做完就显示模型出来了,其它就是可视化的配置了。

     const modelShow = new GLBModel(node)  //创建实例

     modelShow.loadGlb(url, ....... );  //加载模型

    1. // /components/ShowModel/index.tsx
    2. import cssStyle from "./index.module.css";
    3. import { useState, useRef, useEffect } from "react";
    4. import { Button, ColorPicker, Dropdown, Progress, Space, Switch } from "antd";
    5. import { showTip } from "../../utils";
    6. import GLBModel from "./GLBModel";
    7. import { setting, showModelProps } from "./type";
    8. import { DownOutlined } from "@ant-design/icons";
    9. /**展示3D模型 */
    10. export default function ShowModel({ url, style = {}, toolBarRender }: showModelProps) {
    11. /**用来承载three画布的容器 */
    12. const threeDivRef = useRef<HTMLDivElement>(null);
    13. const [progress, setProgress] = useState(0); //进度条,大于100时隐藏,小于0时代表加载失败
    14. const [instance, setInstance] = useState<GLBModel>(); //模型实例。
    15. const [setting, setSetting] = useState({
    16. wireframe: false,
    17. normal: false,
    18. animation: false,
    19. axesHelper: false,
    20. gridHelper: false,
    21. boundingBoxHelper: false,
    22. bgcolor: "#000000",
    23. }); //工具栏配置
    24. /**初始化模型并挂载 */
    25. const init = (node: HTMLDivElement) => {
    26. const modelShow = new GLBModel(node, (_setting) => setSetting({ ..._setting }));
    27. setInstance(modelShow);
    28. setProgress(0); //开始进度条
    29. modelShow.loadGlb(
    30. url,
    31. function (gltf) {
    32. setProgress(101); //隐藏进度条
    33. },
    34. function (e) {
    35. // 加载进度的处理逻辑,这里实际上是AJAX请求,如果是本地文件的话就不会有加载进度条
    36. if (e.lengthComputable) {
    37. const percentComplete = (e.loaded / e.total) * 100;
    38. if (percentComplete <= 100) {
    39. setProgress(parseInt(percentComplete.toFixed(2)));
    40. } else {
    41. //有时候会有超出100的情况
    42. setProgress(100);
    43. }
    44. }
    45. },
    46. function (e) {
    47. setProgress(-1); //错误进度条
    48. showTip("加载失败,请F12查看报错", "error", 5);
    49. }
    50. );
    51. return () => {
    52. modelShow.destory();
    53. };
    54. };
    55. /**自定义下拉框渲染 */
    56. const dropdownRender = () => {
    57. if (!instance) return <>;
    58. const items = [
    59. <Switch
    60. onChange={(open) => instance.changeAxesHelper(open)}
    61. checkedChildren="坐标系"
    62. unCheckedChildren="坐标系"
    63. checked={setting.axesHelper}
    64. />,
    65. <Switch
    66. onChange={(open) => instance.changeGridHelper(open)}
    67. checkedChildren="网格面"
    68. unCheckedChildren="网格面"
    69. checked={setting.gridHelper}
    70. />,
    71. <Switch
    72. onChange={(open) => instance.changeBoundingBoxHelper(open)}
    73. checkedChildren="包围盒"
    74. unCheckedChildren="包围盒"
    75. checked={setting.boundingBoxHelper}
    76. />,
    77. <Button onClick={() => instance.cameraOriginalPosition()}>相机归位Button>,
    78. <ColorPicker showText onChange={(_, hex) => instance.changeBgcolor(hex)} size="small" value={setting.bgcolor} />,
    79. ];
    80. return (
    81. <div style={{ ...bgStyle, padding: "10px", borderRadius: "10px" }}>
    82. {items.map((k, i) => {
    83. return (
    84. <div key={i} style={{ margin: "5px 0" }}>
    85. {k}
    86. div>
    87. );
    88. })}
    89. {toolBarRender && toolBarRender(instance)}
    90. div>
    91. );
    92. };
    93. useEffect(() => {
    94. if (!url) {
    95. showTip("请传递模型URL!", "error", 5);
    96. setProgress(-1);
    97. return;
    98. }
    99. //在react18的开发环境下,useEffect会执行两次,所以需要在return中消除副作用
    100. const dom = threeDivRef.current;
    101. if (dom) {
    102. setInstance(undefined);
    103. const destory = init(dom);
    104. return destory;
    105. }
    106. }, [url]);
    107. return (
    108. <div className={`${cssStyle.showModel}`} style={style}>
    109. {instance && progress > 100 && (
    110. <Space className="toolList" style={bgStyle}>
    111. <Switch onChange={(open) => instance.changeWireframe(open)} checkedChildren="骨架" unCheckedChildren="骨架" checked={setting.wireframe} />
    112. <Switch onChange={(open) => instance.changeNormal(open)} checkedChildren="法线" unCheckedChildren="法线" checked={setting.normal} />
    113. <Switch
    114. onChange={(open) => instance.changeAnimation(open, (e) => showTip(e, "error"))}
    115. checkedChildren="动画"
    116. unCheckedChildren="动画"
    117. checked={setting.animation}
    118. />
    119. <Dropdown dropdownRender={dropdownRender}>
    120. <DownOutlined className="cursor-pointer" />
    121. Dropdown>
    122. Space>
    123. )}
    124. <div className="canvasContain" ref={threeDivRef}>div>
    125. <div className="progress">
    126. <Progress
    127. type="dashboard"
    128. status={progress < 0 ? "exception" : "active"}
    129. percent={progress}
    130. style={{ opacity: progress > 100 ? "0" : "1" }}
    131. strokeColor={{ "0%": "#87d068", "50%": "#ffe58f", "100%": "#ffccc7" }}
    132. />
    133. div>
    134. <div className="tip">
    135. 鼠标左键可以旋转,右键可以进行平移,滚轮可以控制模型放大缩小
    136. div>
    137. div>
    138. );
    139. }
    140. const bgStyle = { backgroundImage: "linear-gradient(135deg, #fdfcfb 0%, #e2d1c3 100%)" };

    (2)index.module.css

    less版: 

    1. /* /components/ShowModel/index.module.less */
    2. .showModel {
    3. width: 100%;
    4. height: 100%;
    5. position: relative;
    6. background-color: #000;
    7. :global {
    8. //工具栏
    9. .toolList {
    10. position: absolute;
    11. top: 0;
    12. right: 50%;
    13. transform: translate(50%);
    14. z-index: 99;
    15. display: flex;
    16. padding: 10px;
    17. border-bottom-right-radius: 10px;
    18. border-bottom-left-radius: 10px;
    19. opacity: 0.8;
    20. align-items: center;
    21. }
    22. //antd 圆环进度条中间文字的颜色
    23. .ant-progress-text {
    24. color: white !important;
    25. }
    26. //画布的容器
    27. .canvasContain {
    28. display: flex;
    29. align-items: center;
    30. justify-content: center;
    31. width: 100%;
    32. height: 100%;
    33. position: relative;
    34. }
    35. //进度条
    36. .progress {
    37. position: absolute;
    38. top: 50%;
    39. left: 50%;
    40. transform: translate(-50%, -50%);
    41. z-index: 9999;
    42. color: white;
    43. .ant-progress {
    44. transition: all 1s;
    45. }
    46. }
    47. //提示
    48. .tip {
    49. position: absolute;
    50. bottom: 0;
    51. left: 50%;
    52. transform: translate(-50%);
    53. font-weight: 900;
    54. white-space: nowrap;
    55. color: white;
    56. }
    57. }
    58. }

    css版

    1. /* /components/ShowModel/index.module.css */
    2. .showModel {
    3. width: 100%;
    4. height: 100%;
    5. position: relative;
    6. background-color: #000;
    7. }
    8. .showModel :global .toolList {
    9. position: absolute;
    10. top: 0;
    11. right: 50%;
    12. transform: translate(50%);
    13. z-index: 99;
    14. display: flex;
    15. padding: 10px;
    16. border-bottom-right-radius: 10px;
    17. border-bottom-left-radius: 10px;
    18. opacity: 0.8;
    19. align-items: center;
    20. }
    21. .showModel :global .ant-progress-text {
    22. color: white !important;
    23. }
    24. .showModel :global .canvasContain {
    25. display: flex;
    26. align-items: center;
    27. justify-content: center;
    28. width: 100%;
    29. height: 100%;
    30. position: relative;
    31. }
    32. .showModel :global .progress {
    33. position: absolute;
    34. top: 50%;
    35. left: 50%;
    36. transform: translate(-50%, -50%);
    37. z-index: 9999;
    38. color: white;
    39. }
    40. .showModel :global .progress .ant-progress {
    41. transition: all 1s;
    42. }
    43. .showModel :global .tip {
    44. position: absolute;
    45. bottom: 0;
    46. left: 50%;
    47. transform: translate(-50%);
    48. font-weight: 900;
    49. white-space: nowrap;
    50. color: white;
    51. }

    (3)type.ts

    1. /* /components/ShowModel/type.ts */
    2. import { CSSProperties, ReactNode } from "react";
    3. import GLBModel from "./GLBModel";
    4. /**展示3D模型的组件Props */
    5. export interface showModelProps {
    6. /**要展示的模型的URL */
    7. url: string;
    8. /**组件最外层的style。在这里面指定宽高等。不指定宽高,将会适配父元素宽高 */
    9. style?: CSSProperties;
    10. /**工具栏的扩展render。参数是内部数据 */
    11. toolBarRender?: (instance: GLBModel) => ReactNode;
    12. }
    13. /**各个工具的开关和设置等,外部只读 */
    14. export interface setting {
    15. /**是否开启了骨架模式 */
    16. wireframe: boolean,
    17. /**是否开启了法线模式 */
    18. normal: boolean,
    19. /**是否开启了动画 */
    20. animation: boolean
    21. /**是否开启了坐标系 */
    22. axesHelper: boolean
    23. /**是否开启了网格 */
    24. gridHelper: boolean
    25. /**是否开启了包围盒 */
    26. boundingBoxHelper: boolean
    27. /**背景色,十六进制字符串 */
    28. bgcolor: string
    29. }
    30. /**失败的回调函数 */
    31. export type onErr = (e: any) => void

    (4)utils

    在上面用到了一个弹窗提示函数

    1. /* /utils/index.ts */
    2. /**使用antd做弹窗,展示信息
    3. * @param content 要提示的文字,或者一个ReactNode
    4. * @param type 类型,默认"success"。
    5. * @param duration 显示时间,单位s,默认2s ,0代表不关闭
    6. * @param key 每个message唯一的key, 可以用于destroy。默认为当前时间戳
    7. * @returns 返回弹窗实例,可以进行.then等
    8. */
    9. export function showTip(content: ReactNode | string, type: NoticeType = 'success', duration: number = 2, key: any = new Date().getTime()) {
    10. return AntdMessage.open({
    11. type,
    12. content,
    13. duration,
    14. key,
    15. style: { zIndex: 99999 }
    16. })
    17. }

    2.测试示例

    任意一个想使用的地方中

    1. import ShowModel from "./components/ShowModel";
    2. const App = () => {
    3. return (
    4. <div style={{ width: "100vw", height: "100vh" }}>
    5. <ShowModel url="https://threejs.org/examples/models/gltf/LittlestTokyo.glb">ShowModel>
    6. div>
    7. );
    8. };
    9. export default App;

    四、结语

            虽然说,理论上是可以支持不同框架使用,但是我还没测试过Vue,只测试了Next和react,如果是别的框架的可以尝试试试哦 。基于class封装,就是为了能够和封装组件时解耦,所以理论上是可以支持不同框架使用的

            最主要的操作,其实就是下面这两步,做完就显示模型出来了,其它就是可视化的配置了。

    1.          const modelShow = new GLBModel(node)  //创建实例
    2.          modelShow.loadGlb(url, ....... );  //加载模型

           

  • 相关阅读:
    view-design组件使用Vue+Input+Table实现动态搜索值并单选
    动态规划——背包问题
    Keycloak服务开发-认证服务SPI
    分支与循环(2)
    Kubernetes二进制部署——理论部分
    python每日一题【剑指 Offer 26. 树的子结构】
    C++常成员函数 - const 关键字
    剑指offer64 求1+2+3+...+n
    React.createRef
    C#开发的OpenRA游戏之属性RenderSprites(8)
  • 原文地址:https://blog.csdn.net/m0_64130892/article/details/133501096