ThreeJS - 封装一个GLB模型展示组件(TypeScript)
一、引言
最近基于Three.JS,使用class封装了一个GLB模型展示,支持TypeScript、支持不同框架使用,具有多种功能。 (下图展示一些基础的功能,可以自行扩展,比如光源等)
二、主要代码
本模块依赖: three、 @types/three, 请先下载这两个npm包
yarn add three @types/three 或 npm i three @types/three
使用了class进行封装,将主要的操作代码从组件中抽离出来,便于不同框架之间的使用
// /components/ShowModel/GLBModel.tsimport { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { onErr, setting } from "./type";/**GLB模型展示 */
class GLBModel {/**当前canvas挂载的node节点 */node: HTMLElement/**判断模型是否加载完成(代表那些原本undefined的变量已经可以使用了)*/load = false/**一些模式的开关和设置,外部只读,修改无效。会把配置保存在本地存储,记录数据 */setting!: setting/**渲染器 */private renderer!: THREE.WebGLRenderer/**摄像机 */private camera!: THREE.PerspectiveCamera/**场景 */private scene!: THREE.Scene;/**操控摄像机的控制器 */private controls!: OrbitControls;/**性能统计信息的工具 */private stats!: Stats/**clock对象,用于跟踪时间的流逝,并在动画循环中提供统一的时间更新 */private clock!: THREE.Clock/**辅助观察的帮助器,包括 三维坐标、网格平面、包围盒框框 */private helpers?: ReturnType<typeof initHelper>['helper']/**包围盒有关的数据,包括放大倍数、放大后的中心坐标、放大后的模型大小 */private box?: ReturnType<typeof getBoxAndScale>['box']/**动画混合器 */private mixer?: THREE.AnimationMixer/**当前模型实例 */private gltf?: GLTF/**模型的动画列表 */private actionList: THREE.AnimationAction[] = []/**模型的原始材质Map,可以用于还原 */private originalMesh = new Map<THREE.Mesh, THREE.Mesh["material"]>()/**当内部的setting变量发生改变时,会触发这个函数,可以用于框架的响应式 */private settingChangeCallback?: (setting: setting) => void/**GLB模型展示 - 构造函数* @param node 要挂载canvas画布的节点。注意需要设置好node的宽高* @param settingChangeCallback 当内部的setting变量发生改变时,会触发这个函数,可以用于框架的响应式*/constructor(node: HTMLElement, settingChangeCallback?: (setting: setting) => void) {this.node = nodethis.settingChangeCallback = settingChangeCallbackObject.assign(this, initBaseDevice(node), initOtherDevice(node))//这个操作是,把函数的返回值赋值到this上, 省的我一个个去 this.xxx = xxxthis.resizeListen()this.settingFn.getSettingFromLocal()//给setting属性赋值}/**加载glb模型,同时进行基础设置* @param url 要加载的url* @param onload 加载成功的回调函数* @param onProgress 进度更新时触发的函数,可以用来配置进度条* @param onErr 加载失败的回调*/loadGlb(url: string, onload: (data: GLTF) => void, onProgress: (e: ProgressEvent) => void, onErr?: onErr) {/**dracoLoader模型压缩器 */const dracoLoader = new DRACOLoader();dracoLoader.setDecoderPath('https://threejs.org/examples/jsm/libs/draco/gltf/');//这段代码在部署时会不会报错?/**glb模型加载器 */const loader = new GLTFLoader();loader.setDRACOLoader(dracoLoader); //设置压缩器loader.load(url,(gltf) => {this.gltf = gltfconst model = gltf.scene;this.box = getBoxAndScale(model, this.camera, this.controls, this.scene).boxthis.helpers = initHelper(150, this.box.centerWithScale, model).helper;this.mixer = new THREE.AnimationMixer(model); //设置新的动画混合器 this.actionList = getAnimations(gltf, this.mixer); //获取动画列表this.animate()this.originalMesh = getOriginalMesh(model)//保存原始材质onload(gltf)this.load = truethis.settingFn.setFromLocal()},onProgress,(e) => {onErr && onErr(e);console.error("加载glb模型出错啦", e);});};/**卸载时需要做的事。 */destory() {try {this.resizeDestory();//清除DOM监听window.cancelAnimationFrame(this.animateKey || 0);//清除canvas动画while (this.node.firstChild) this.node.firstChild.remove(); //删除DOM下所有子元素} catch (error) {console.error('执行清除函数失败,请检查问题。可能是由于this指向的问题,请保证此函数的调用者是实例本身。', error);//注意调用时,必须保证调用者是实例本身,否则此处请改为箭头函数}}/**开启/关闭骨架模式* @param open 开启还是关闭* @param onErr 失败的回调*/changeWireframe(open: boolean, onErr?: onErr) {try {this.judgeLoad()this.gltf!.scene.traverse(function (child) {if (child instanceof THREE.Mesh) {child.material.wireframe = open; //查看骨架模式 }});this.settingFn.setSetting('wireframe', open)} catch (error) {console.error('开启/关闭骨架模式失败', error)onErr && onErr(error)}}/**开启/关闭法线模式 */changeNormal(open: boolean, onErr?: onErr) {try {this.judgeLoad()this.gltf!.scene.traverse((object) => {if (object instanceof THREE.Mesh) {if (open) {object.material = new THREE.MeshNormalMaterial({transparent: true, // 是否开启使用透明度wireframe: this.setting.wireframe, //骨架模式opacity: 0.8, // 透明度depthWrite: false, // 关闭深度写入 透视效果});} else {const origin = this.originalMesh.get(object); //原始材质object.material = origin;this.changeWireframe(this.setting.wireframe);}}});this.settingFn.setSetting('normal', open)} catch (error) {console.error('开启/关闭法线模式失败', error)onErr && onErr(error)}}/**开启/关闭动画* @param open 是否开启* @param onErr 失败回调,参数是失败提示 */changeAnimation(open: boolean, onErr?: onErr) {try {if (open && !this.actionList.length) {console.log("该模型暂无动画哦");onErr && onErr("该模型暂无动画哦")return;}this.actionList.forEach((k) => {open ? k.play() : k.stop();});this.settingFn.setSetting('animation', open)} catch (error) {console.error('开启/关闭动画失败', error)onErr && onErr(error)}};/**开启/关闭坐标系 */changeAxesHelper(open: boolean, onErr?: onErr) {try {this.judgeLoad()open ? this.scene.add(this.helpers!.axesHelper) : this.scene.remove(this.helpers!.axesHelper)this.settingFn.setSetting('axesHelper', open)} catch (error) {console.error('开启/关闭坐标系失败', error);onErr && onErr(error)}}/**开启/关闭网格 */changeGridHelper(open: boolean, onErr?: onErr) {try {this.judgeLoad()open ? this.scene.add(this.helpers!.gridHelper) : this.scene.remove(this.helpers!.gridHelper)this.settingFn.setSetting('gridHelper', open)} catch (error) {console.error('开启/关闭网格失败', error);onErr && onErr(error)}}/**开启/关闭包围盒 */changeBoundingBoxHelper(open: boolean, onErr?: onErr) {try {this.judgeLoad()open ? this.scene.add(this.helpers!.boundingBoxHelper) : this.scene.remove(this.helpers!.boundingBoxHelper)this.settingFn.setSetting('boundingBoxHelper', open)} catch (error) {console.error('开启/关闭包围盒 失败', error);onErr && onErr(error)}}/**切换背景颜色,参数是十六进制颜色字符串 */changeBgcolor(hex: string, onErr?: onErr) {try {this.judgeLoad()this.scene.background = new THREE.Color(hex); //场景背景色 this.settingFn.setSetting('bgcolor', hex)} catch (error) {console.error('开启/关闭包围盒 失败', error);onErr && onErr(error)}}/**相机归回原位 */cameraOriginalPosition(onErr?: onErr) {try {this.judgeLoad()const { camera, controls, box } = thiscamera.position.copy(box!.sizeWithScale); //设置摄像机的初始位置,乘上缩放倍数controls.target.copy(box!.centerWithScale); //设置摄像机旋转和放大等操作的目标点} catch (error) {console.error('相机归回原位 失败', error);onErr && onErr(error)}};/**有关于setting的一些函数 */private settingFn = {/**设置模块配置 */setSetting: <T extends keyof setting>(key: T, value: setting[T]) => {this.setting[key] = valuelocalStorage.setItem('glbModelSetting', JSON.stringify(this.setting))//存到本地存储 this.settingChangeCallback && this.settingChangeCallback(this.setting)},/**从本地存储读出设置,保存在实例中 */getSettingFromLocal: () => {const setting = JSON.parse(localStorage.getItem('glbModelSetting') || 'null') as setting | nullif (setting) {this.setting = setting} else {this.setting = {wireframe: false,normal: false,animation: false,axesHelper: false,gridHelper: false,boundingBoxHelper: false,bgcolor: "#000000"}}},/**根据setting,配置对应的模式 - 在加载模型后使用 */setFromLocal: () => {const setting = this.setting//设置这些设置的函数,都是 change + Xxxxx 形式的命名,所以下面直接遍历调用for (const key in setting) {if (Object.prototype.hasOwnProperty.call(setting, key)) {const fnName = 'change' + key.slice(0, 1).toUpperCase() + key.slice(1)try {(this as any)[fnName]((setting as any)[key])} catch (error) {console.log('调用', fnName, '失败', error);}}}}}/**判断是否加载完成,没完成的话会抛出错误,可以被catch捕获 */private judgeLoad = () => {if (!this.load) {throw '模型还未加载完成'}}/**窗口监听事件的卸载函数,在卸载时需要清除 */private resizeDestory!: () => void/**绑定窗口大小监听事件 */private resizeListen() {const { node, camera, renderer, scene } = this//下面这个监听,可能有性能问题吧,看左上角自带的性能指标,拖动时起伏很大,如果加节流的话,又会因为没有及时更新而大小不同/**创建 ResizeObserver 实例 */let observer: ResizeObserver | null = new ResizeObserver(entries => {for (let entry of entries) {const width = entry.contentRect.width;const height = entry.contentRect.height;camera.aspect = width / height; //设置新比例camera.updateProjectionMatrix(); //更新相机的投影矩阵renderer.setSize(width, height);renderer.render(scene, camera) //渲染}});observer.observe(node); // 开始观察目标元素this.resizeDestory = () => {observer!.unobserve(node); // 停止观察目标元素observer!.disconnect();// 停止观察所有元素observer = null //垃圾回收}}/**当前canvas的动画key,在卸载时需要清除 */private animateKey: number = 0/**canvas动画,在这里更新数据并实时render渲染 */private animate = () => {this.animateKey = window.requestAnimationFrame(this.animate);const delta = this.clock.getDelta(); // 获取每帧的时间间隔,从而可以根据时间进行动画更新,使动画在不同的设备和性能下保持一致this.mixer!.update(delta); //更新动画this.controls.update(); //操作器更新this.stats.update(); //更新性能计算器 this.renderer.render(this.scene, this.camera) //渲染}
}
export default GLBModel
/**初始化基础设备 */
const initBaseDevice = (node: HTMLElement) => {/**节点宽度 */const width = node.clientWidth;/**节点高度 */const height = node.clientHeight;/**渲染器 */const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); //antialias启用抗锯齿功能renderer.setPixelRatio(window.devicePixelRatio); //设置渲染器的设备像素比例的方法,在不同设备展示一样的东西renderer.setSize(width, height); //设置宽高node.appendChild(renderer.domElement); //挂载渲染器DOM/**摄像机 */const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);/**创建场景 */const scene = new THREE.Scene();scene.background = new THREE.Color(0x000000); //场景背景色scene.environment = new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(renderer), 0.04).texture; //将场景的当前光照信息计算为环境贴图。第二个参数 0.04 指定了纹理的精度,数值越小表示精度越高,但计算时间也越长。/**操控摄像机的控制器 */const controls = new OrbitControls(camera, renderer.domElement);controls.update(); //更新控制器的状态。在动画函数中也需要执行controls.enablePan = true; //是否启用控制器的右键平移功能。controls.enableDamping = true; //是否启用惯性功能return {/**渲染器 */renderer,/**摄像机 */camera,/**场景 */scene,/**操控摄像机的控制器 */controls,};
};
/**初始化其它设备,如性能展示器、clock时钟 */
const initOtherDevice = (node: HTMLElement) => {/**用于在 WebGL 渲染中显示性能统计信息的工具 */const stats = new Stats();stats.dom.style.position = "absolute";node.appendChild(stats.dom); //挂载性能展示DOM/**clock对象,用于跟踪时间的流逝,并在动画循环中提供统一的时间更新 */const clock = new THREE.Clock();return {/**用于在 WebGL 渲染中显示性能统计信息的工具 */stats,/**clock对象,用于跟踪时间的流逝,并在动画循环中提供统一的时间更新 */clock,};
};
/**初始化三维坐标系、网格帮助器、包围盒帮助器 */
const initHelper = (size: number, center: THREE.Vector3, model: THREE.Group<THREE.Object3DEventMap>) => {/**AxesHelper:辅助观察的坐标系 */const axesHelper = new THREE.AxesHelper(size);axesHelper.position.copy(center); //三维坐标系的位置/**网格帮助器 */const gridHelper = new THREE.GridHelper(size, size);gridHelper.position.copy(center); //网格的位置/**新包围盒辅助展示 */const boundingBoxHelper = new THREE.BoxHelper(model); //创建一个BoxHelper对象,传入模型的网格对象作为参数boundingBoxHelper.material.color.set(0xff0000); //将包围盒的材质设置为红色return {/**辅助观察的帮助器 */helper: {/**辅助观察的坐标系 */axesHelper,/**网格帮助器 */gridHelper,/**包围盒轮廓,可以添加到场景中 */boundingBoxHelper,},};
};
/**获得模型包围盒的数据,并计算模型位置、缩放倍数,设置相机位置等,最后把模型添加到场景。 */
const getBoxAndScale = (model: THREE.Group<THREE.Object3DEventMap>, camera: THREE.PerspectiveCamera, controls: OrbitControls, scene: THREE.Scene) => {/**获取模型包围盒 */const boundingBox = new THREE.Box3().expandByObject(model);/**获取包围盒的size */const size = boundingBox.getSize(new THREE.Vector3()); //设置size/**中心坐标*/const center = boundingBox.getCenter(new THREE.Vector3()); // 计算包围盒中心坐标,并将中心坐标保存在center向量中/**设置的缩放倍数,根据实际情况进行调整 */const scale = 10 / Math.max(size.x, size.y, size.z); // 分母是期望的模型大小// const scale = 1;/**中心点的三维向量 * 放大值 */const centerWithScale = center.clone().multiplyScalar(scale);/**盒子的三维向量 * 放大值 */const sizeWithScale = size.clone().multiplyScalar(scale);// console.log("boundingBox", boundingBox);// console.log("size", size);// console.log("center", center);// console.log("scale", scale);// console.log("centerWithScale", centerWithScale);// console.log("sizeWithScale", sizeWithScale);model.scale.set(scale, scale, scale); //设置模型缩放倍率 camera.position.copy(sizeWithScale); //设置摄像机的初始位置,乘上缩放倍数controls.target.copy(centerWithScale); //设置摄像机旋转和放大等操作的目标点scene.add(model); //把模型添加进去return {/**包围盒有关的信息 */box: {/**缩放倍率 */scale,/**放大后的中心点的三维向量 */centerWithScale,/**放大后的盒子的三维向量 */sizeWithScale,},};
};
/**获取模型上的全部动画,返回动画实例列表,后续操控实例列表即可 */
const getAnimations = (gltf: GLTF, mixer: THREE.AnimationMixer) => {const actionList: THREE.AnimationAction[] = [];// 遍历模型的动画数组,为个动画创建剪辑并添加到混合器中for (let i = 0; i < gltf.animations.length; i++) {const animation = gltf.animations[i];const action = mixer.clipAction(animation); //创建actionList.push(action);action.setLoop(THREE.LoopRepeat, Infinity); // 设置动画播放相关参数:循环模式、重复次数action.clampWhenFinished = true; // 动画在播放完成后会停留在最后一帧,不再继续播放 (但是上面设置了循环播放,所以不影响)// action.play(); // 播放动画}return actionList;
};
/**获取模型身上的原始材质,返回map */
const getOriginalMesh = (model: THREE.Group<THREE.Object3DEventMap>) => {const map = new Map<THREE.Mesh, THREE.Mesh["material"]>();//设置模型原始材质model.traverse((object) => {if (object instanceof THREE.Mesh) {map.set(object, object.material);}});return map;
};
其中 type.ts 本文件所需的部分内容如下: (完整内容在 三-1-(3) 里)
// /components/ShowModel/type.ts//.../**展示3D模型的组件Props */
export interface showModelProps {/**要展示的模型的URL */url: string;/**组件最外层的style。在这里面指定宽高等。不指定宽高,将会适配父元素宽高 */style?: CSSProperties;/**工具栏的扩展render。参数是内部数据 */toolBarRender?: (instance: GLBModel) => ReactNode;
}/**各个工具的开关和设置等,外部只读 */
export interface setting {/**是否开启了骨架模式 */wireframe: boolean,/**是否开启了法线模式 */normal: boolean,/**是否开启了动画 */animation: boolean/**是否开启了坐标系 */axesHelper: boolean/**是否开启了网格 */gridHelper: boolean/**是否开启了包围盒 */boundingBoxHelper: boolean/**背景色,十六进制字符串 */bgcolor: string
}/**失败的回调函数 */
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, ....... ); //加载模型
// /components/ShowModel/index.tsximport cssStyle from "./index.module.css";
import { useState, useRef, useEffect } from "react";
import { Button, ColorPicker, Dropdown, Progress, Space, Switch } from "antd";
import { showTip } from "../../utils";
import GLBModel from "./GLBModel";
import { setting, showModelProps } from "./type";
import { DownOutlined } from "@ant-design/icons";/**展示3D模型 */
export default function ShowModel({ url, style = {}, toolBarRender }: showModelProps) {/**用来承载three画布的容器 */const threeDivRef = useRef<HTMLDivElement>(null);const [progress, setProgress] = useState(0); //进度条,大于100时隐藏,小于0时代表加载失败const [instance, setInstance] = useState<GLBModel>(); //模型实例。const [setting, setSetting] = useState<setting>({wireframe: false,normal: false,animation: false,axesHelper: false,gridHelper: false,boundingBoxHelper: false,bgcolor: "#000000",}); //工具栏配置/**初始化模型并挂载 */const init = (node: HTMLDivElement) => {const modelShow = new GLBModel(node, (_setting) => setSetting({ ..._setting }));setInstance(modelShow);setProgress(0); //开始进度条modelShow.loadGlb(url,function (gltf) {setProgress(101); //隐藏进度条},function (e) {// 加载进度的处理逻辑,这里实际上是AJAX请求,如果是本地文件的话就不会有加载进度条if (e.lengthComputable) {const percentComplete = (e.loaded / e.total) * 100;if (percentComplete <= 100) {setProgress(parseInt(percentComplete.toFixed(2)));} else {//有时候会有超出100的情况setProgress(100);}}},function (e) {setProgress(-1); //错误进度条showTip("加载失败,请F12查看报错", "error", 5);});return () => {modelShow.destory();};};/**自定义下拉框渲染 */const dropdownRender = () => {if (!instance) return <></>;const items = [<SwitchonChange={(open) => instance.changeAxesHelper(open)}checkedChildren="坐标系"unCheckedChildren="坐标系"checked={setting.axesHelper}/>,<SwitchonChange={(open) => instance.changeGridHelper(open)}checkedChildren="网格面"unCheckedChildren="网格面"checked={setting.gridHelper}/>,<SwitchonChange={(open) => instance.changeBoundingBoxHelper(open)}checkedChildren="包围盒"unCheckedChildren="包围盒"checked={setting.boundingBoxHelper}/>,<Button onClick={() => instance.cameraOriginalPosition()}>相机归位</Button>,<ColorPicker showText onChange={(_, hex) => instance.changeBgcolor(hex)} size="small" value={setting.bgcolor} />,];return (<div style={{ ...bgStyle, padding: "10px", borderRadius: "10px" }}>{items.map((k, i) => {return (<div key={i} style={{ margin: "5px 0" }}>{k}</div>);})}{toolBarRender && toolBarRender(instance)}</div>);};useEffect(() => {if (!url) {showTip("请传递模型URL!", "error", 5);setProgress(-1);return;}//在react18的开发环境下,useEffect会执行两次,所以需要在return中消除副作用const dom = threeDivRef.current;if (dom) {setInstance(undefined);const destory = init(dom);return destory;}}, [url]);return (<div className={`${cssStyle.showModel}`} style={style}>{instance && progress > 100 && (<Space className="toolList" style={bgStyle}><Switch onChange={(open) => instance.changeWireframe(open)} checkedChildren="骨架" unCheckedChildren="骨架" checked={setting.wireframe} /><Switch onChange={(open) => instance.changeNormal(open)} checkedChildren="法线" unCheckedChildren="法线" checked={setting.normal} /><SwitchonChange={(open) => instance.changeAnimation(open, (e) => showTip(e, "error"))}checkedChildren="动画"unCheckedChildren="动画"checked={setting.animation}/><Dropdown dropdownRender={dropdownRender}><DownOutlined className="cursor-pointer" /></Dropdown></Space>)}<div className="canvasContain" ref={threeDivRef}></div><div className="progress"><Progresstype="dashboard"status={progress < 0 ? "exception" : "active"}percent={progress}style={{ opacity: progress > 100 ? "0" : "1" }}strokeColor={{ "0%": "#87d068", "50%": "#ffe58f", "100%": "#ffccc7" }}/></div><div className="tip">鼠标左键可以旋转,右键可以进行平移,滚轮可以控制模型放大缩小</div></div>);
}const bgStyle = { backgroundImage: "linear-gradient(135deg, #fdfcfb 0%, #e2d1c3 100%)" };
(2)index.module.css
less版:
/* /components/ShowModel/index.module.less */ .showModel {width: 100%;height: 100%;position: relative;background-color: #000;:global {//工具栏 .toolList {position: absolute;top: 0;right: 50%;transform: translate(50%);z-index: 99;display: flex;padding: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;opacity: 0.8;align-items: center;}//antd 圆环进度条中间文字的颜色.ant-progress-text {color: white !important;}//画布的容器.canvasContain {display: flex;align-items: center;justify-content: center;width: 100%;height: 100%;position: relative;}//进度条.progress {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 9999;color: white;.ant-progress {transition: all 1s;}}//提示.tip {position: absolute;bottom: 0;left: 50%;transform: translate(-50%);font-weight: 900;white-space: nowrap;color: white;}}}
css版
/* /components/ShowModel/index.module.css */ .showModel {width: 100%;height: 100%;position: relative;background-color: #000;
}
.showModel :global .toolList {position: absolute;top: 0;right: 50%;transform: translate(50%);z-index: 99;display: flex;padding: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;opacity: 0.8;align-items: center;
}
.showModel :global .ant-progress-text {color: white !important;
}
.showModel :global .canvasContain {display: flex;align-items: center;justify-content: center;width: 100%;height: 100%;position: relative;
}
.showModel :global .progress {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 9999;color: white;
}
.showModel :global .progress .ant-progress {transition: all 1s;
}
.showModel :global .tip {position: absolute;bottom: 0;left: 50%;transform: translate(-50%);font-weight: 900;white-space: nowrap;color: white;
}
(3)type.ts
/* /components/ShowModel/type.ts */ import { CSSProperties, ReactNode } from "react";
import GLBModel from "./GLBModel";/**展示3D模型的组件Props */
export interface showModelProps {/**要展示的模型的URL */url: string;/**组件最外层的style。在这里面指定宽高等。不指定宽高,将会适配父元素宽高 */style?: CSSProperties;/**工具栏的扩展render。参数是内部数据 */toolBarRender?: (instance: GLBModel) => ReactNode;
}/**各个工具的开关和设置等,外部只读 */
export interface setting {/**是否开启了骨架模式 */wireframe: boolean,/**是否开启了法线模式 */normal: boolean,/**是否开启了动画 */animation: boolean/**是否开启了坐标系 */axesHelper: boolean/**是否开启了网格 */gridHelper: boolean/**是否开启了包围盒 */boundingBoxHelper: boolean/**背景色,十六进制字符串 */bgcolor: string
}/**失败的回调函数 */
export type onErr = (e: any) => void
(4)utils
在上面用到了一个弹窗提示函数
/* /utils/index.ts *//**使用antd做弹窗,展示信息
* @param content 要提示的文字,或者一个ReactNode
* @param type 类型,默认"success"。
* @param duration 显示时间,单位s,默认2s ,0代表不关闭
* @param key 每个message唯一的key, 可以用于destroy。默认为当前时间戳
* @returns 返回弹窗实例,可以进行.then等
*/
export function showTip(content: ReactNode | string, type: NoticeType = 'success', duration: number = 2, key: any = new Date().getTime()) {return AntdMessage.open({type,content,duration,key,style: { zIndex: 99999 }})
}
2.测试示例
任意一个想使用的地方中
import ShowModel from "./components/ShowModel";const App = () => {return (<div style={{ width: "100vw", height: "100vh" }}><ShowModel url="https://threejs.org/examples/models/gltf/LittlestTokyo.glb"></ShowModel></div>);
};
export default App;
四、结语
虽然说,理论上是可以支持不同框架使用,但是我还没测试过Vue,只测试了Next和react,如果是别的框架的可以尝试试试哦 。基于class封装,就是为了能够和封装组件时解耦,所以理论上是可以支持不同框架使用的
最主要的操作,其实就是下面这两步,做完就显示模型出来了,其它就是可视化的配置了。
const modelShow = new GLBModel(node) //创建实例modelShow.loadGlb(url, ....... ); //加载模型
相关文章:
ThreeJS - 封装一个GLB模型展示组件(TypeScript)
一、引言 最近基于Three.JS,使用class封装了一个GLB模型展示,支持TypeScript、支持不同框架使用,具有多种功能。 (下图展示一些基础的功能,可以自行扩展,比如光源等) 二、主要代码 本模块依赖…...
HashMap面试题
1.hashMap底层实现 hashMap的实现我们是要分jdk 1.7及以下版本,jdk1.8及以上版本 jdk 1.7 实现是用数组链表 jdk1.8 实现是用数组链表红黑树, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,…...
Java编程技巧:swagger2、knif4j集成SpringBoot或者SpringCloud项目
目录 1、springbootswagger2knif4j2、springbootswagger3knif4j3、springcloudswagger2knif4j 1、springbootswagger2knif4j 2、springbootswagger3knif4j 3、springcloudswagger2knif4j 注意点: Api注解:Controller类上的Api注解需要添加tags属性&a…...
第三章:最新版零基础学习 PYTHON 教程(第九节 - Python 运算符—Python 中的除法运算符)
除法运算符允许您将两个数字相除并返回商,即,第一个数字或左侧的数字除以第二个数字或右侧的数字并返回商。 Python 中的除法运算符 除法运算符有两种类型: 浮点数除法整数除法(向下取整除法)整数相除时,结果四舍五入为最接近的整数,并用符号“//”表示。浮点数“/”…...
【python】导出mysql数据,输出excel!
参考https://blog.csdn.net/pengneng123/article/details/131111713 import pymysql import pandas as pd #import openpyxl import xlsxwriterdb pymysql.connect(host"10.41.241.114", port***,user***,password***,charsetutf8mb4 )cursor db.cursor() #创建游…...
【Java 进阶篇】JDBC ResultSet 遍历结果集详解
在Java数据库编程中,经常需要执行SQL查询并处理查询结果。ResultSet(结果集)是Java JDBC中用于表示查询结果的关键类之一。通过遍历ResultSet,我们可以访问和操作从数据库中检索的数据。本文将详细介绍如何使用JDBC来遍历ResultSe…...
华为数通方向HCIP-DataCom H12-831题库(单选题:161-180)
第161题 某台路由器Router LSA如图所示,下列说法中错误的是? A、本路由器已建立邻接关系 B、本路由器为DR C、本路由支持外部路由引入 D、本路由器的Router ID为10.0.12.1 答案: B 解析: 一类LSA的在transnet网络中link id值为DR的route id ,但Link id的地址不是10.0.12.…...
【VsCode】SSH远程连接Linux服务器开发,搭配cpolar内网穿透实现公网访问
文章目录 前言1、安装OpenSSH2、vscode配置ssh3. 局域网测试连接远程服务器4. 公网远程连接4.1 ubuntu安装cpolar内网穿透4.2 创建隧道映射4.3 测试公网远程连接 5. 配置固定TCP端口地址5.1 保留一个固定TCP端口地址5.2 配置固定TCP端口地址5.3 测试固定公网地址远程 前言 远程…...
java并发编程 守护线程 用户线程 main
经常使用线程,没有对守护线程和用户线程的区别做彻底了解 下面写4个例子来验证一下 源码如下 /* Whether or not the thread is a daemon thread. */ private boolean daemon false;/*** Marks this thread as either a {linkplain #isDaemon daemon} thread*…...
wxWidgets(1):在Ubuntu 环境中搭建wxWidgets 库环境,安装库和CodeBlocks的IDE,可以运行demo界面了,继续学习中
1,选择使用 wxWidgets 框架 选择这个主要是因为完全的开源,不想折腾 Qt的库,而且打包的文件比较大。 网络上面有很多的对比,而且使用QT的人比较多。 但是我觉得wxwidgets 更加偏向 c 语法本身,也有助学习C。 没有太多…...
[VIM]VIM初步学习-3
3-1 编写 vim 配置,我的 vim 我做主_哔哩哔哩_bilibili...
RocketMQ Dashboard说解
RocketMQ Dashboard 是 RocketMQ 的管控利器,为用户提供客户端和应用程序的各种事件、性能的统计信息,支持以可视化工具代替 Topic 配置、Broker 管理等命令行操作。 介绍 功能概览 面板功能运维修改nameserver 地址; 选用 VIPChannel驾驶舱查看 …...
【RabbitMQ实战】05 RabbitMQ后台管理
一、多租户与权限 1.1 vhost的概念 每一个 RabbitMQ服务器都能创建虚拟的消息服务器,我们称之为虚拟主机(virtual host),简称为 vhost。每一个 vhost本质上是一个独立的小型RabbitMQ服务器,拥有自己独立的队列、交换器及绑定关系等,并且它拥…...
PHP8中final关键字的应用-PHP8知识详解
在PHP8中,final的中文含义是最终的、最后的意思。被final修饰过的类和方法就是“最终的版本”。 如果关键字final放在类的前面,则表示该类不能被继承。 如果关键字final放在方法的前面,则表示该 方法不能被重新定义。 如果有一个类的格式为…...
基于Java的校园失物招领平台设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考源码获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…...
〔024〕Stable Diffusion 之 模型训练 篇
✨ 目录 🎈 训练集准备🎈 训练集预处理🎈 数据清洗🎈 下载训练源码🎈 训练文件配置🎈 脚本运行🎈 实战测试🎈 训练集准备 声明: 该文中所涉及到的女神图片均来自于网络,仅用作技术教程演示,图片已码一般同一个训练集需要准备 20~40 张不同角度的照片,当然可…...
【MySQL入门到精通-黑马程序员】MySQL基础篇-DML
文章目录 前言一、DML-介绍二、DML-添加数据三、DML-修改数据四、DML-删除数据总结 前言 本专栏文章为观看黑马程序员《MySQL入门到精通》所做笔记,课程地址在这。如有侵权,立即删除。 一、DML-介绍 DML(Data Manipulation Language…...
【ARMv8 SIMD和浮点指令编程】NEON 加载指令——如何将数据从内存搬到寄存器(LDxLDxR)?
将内存中的数据搬到 NEON 寄存器,有很多指令可以完成,熟悉这些指令是必须的。 1 LD1 (multiple structures) 将多个单元素结构加载到一个,两个,三个或四个寄存器上。该指令从内存中加载多个单元结构,并将结果写入一、二、三或四个 SIMD&FP 寄存器。 无偏移 一个寄存…...
华为云云耀云服务器L实例评测 | 实例场景体验之搭建个人博客:通过华为云云耀云服务器构建个人博客
华为云云耀云服务器L实例评测 | 实例场景体验之搭建个人博客:通过华为云云耀云服务器构建个人博客 介绍华为云云耀云服务器 华为云云耀云服务器 (目前已经全新升级为 华为云云耀云服务器L实例) 华为云云耀云服务器是什么华为云云耀…...
问题记录 springboot 事务方法中使用this调用其它方法
原因: 因为代理对象中调用了原始对象的toString()方法,所以两个不同的对象打印出的引用是相同的...
【Spring Cloud】Ribbon 实现负载均衡的原理,策略以及饥饿加载
文章目录 前言一、什么是 Ribbon二、Ribbon 实现负载均衡的原理2.1 负载均衡的流程2.2 Ribbon 实现负载均衡的源码剖析 三、Ribbon 负载均衡策略3.1 负载均衡策略3.2 演示 Ribbon 负载均衡策略的更改 四、Ribbon 的饥饿加载4.1查看 Ribbon 的懒加载4.2 Ribbon 的饥饿加载模式 前…...
Linux下基本指令(上)
文章内容: 1. ls 指令 语法: ls [选项][目录或文件] 功能:对于目录,该命令列出该目录下的所有子目录与文件。对于文件,将列出文件名以及其他信息。 单个ls显示当前目录下的文件和目录 常用选项&#…...
C++ 并发编程实战 第十一章 多线程应用的测试和除错
目录 11.1 与并发相关的错误类型 11.1.1 不必要的阻塞 11.1.2 条件竞争 11.2 定位并发错误的技术 11.2.1 代码审阅——发现潜在的错误 11.2.2 通过测试定位并发相关的错误 11.2.3 可测试性设计 11.2.4 多线程测试技术 11.2.5 构建多线程测试代码 11.2.6 测试多线程代…...
Redis实现API访问频率限制
🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…...
BGP服务器租用价格表_腾讯云PK阿里云
BGP云服务器像阿里云和腾讯云均是BGP多线网络,速度更快延迟更低,阿里云BGP服务器2核2G3M带宽优惠价格108元一年起,腾讯云BGP服务器2核2G3M带宽95元一年起,阿腾云分享更多云服务器配置如2核4G、4核8G、8核16G等配置价格表如下&…...
时序分解 | Matlab实现SSA-VMD麻雀算法优化变分模态分解时间序列信号分解
时序分解 | Matlab实现SSA-VMD麻雀算法优化变分模态分解时间序列信号分解 目录 时序分解 | Matlab实现SSA-VMD麻雀算法优化变分模态分解时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 SSA-VMD麻雀搜索算法SSA优化VMD变分模态分解 可直接运行 分解效果好…...
【CSS如何实现双飞翼布局】
双飞翼布局是一种基于浮动布局的设计模式,主要用于实现三栏布局。它的主要特点是左右两列是浮动的,中间一列使用margin负值来达到“自适应”的效果。这种布局模式可以避免使用嵌套的div,同时也可以保证页面的语义结构清晰。以下是实现双飞翼布…...
服务注册发现机制
二、注册中心选型 1. zk和eureka的区别 zk:CP设计(强一致性),目标是一个分布式的协调系统,用于进行资源的统一管理。 当主节点crash后,需要进行leader的选举,在这个期间内,zk服务是不可用的(当然…...
【postgresql 基础入门】多表联合查询 join与union 并,交,差等集合操作,两者的区别之处
多表数据联合查询 专栏内容: postgresql内核源码分析手写数据库toadb并发编程 开源贡献: toadb开源库 个人主页:我的主页 管理社区:开源数据库 座右铭:天行健,君子以自强不息;地势坤&#x…...
很可惜,pyinstaller不是万能的
近期活不算少,但是真正新的东西很少,基本都是做些相似的功能,所以有精力想想之前悬而未决的问题,比如前两天写的加快软件启动速度的探索,这几天又想起一个之前没有解决的问题,这个问题之前也在博客写过&…...
做网贷网站/运营培训班
题目链接:戳我 【问题描述】 小A在玩打地鼠游戏。有一个nm的网格,每个位置上地鼠都会要么冒出头要么缩进去。地鼠很狡猾,每次小A选一个地鼠冒出头的格子(x,y)把它打下去,但同一行同一列的地鼠全都会冒出头来。 小A发现这个游戏好像…...
建大型购物网站/建站公司
环境安装 想要使用jni进行ndk开发,我们首先要安装下面这些工具,否则直接从入门到放弃。 下载ndk支持 在Android studio中下载上图中框选的两个工具,版本号自己任意选一个。下载完成之后,Android Studio就拥有了进行ndk编译的能力…...
景安网站备案要多久/湖南专业的关键词优化
2016-05-31 回答实现两个mysql数据库之间同步同步原理:mysql 为了实现replication 必须打开bin-log 项,也是打开二进制的mysql 日志记录选项。mysql 的bin log 二进制日志,可以记录所有影响到数据库表中存储记录内容的sql 操作,如…...
怎么申请域名和备案/承德seo
postgreSql 常用操作总结 阅读目录: 0. 启动pgsl数据库1. 查看pgsl版本1. 命令行登录数据库2. 列出所有数据库3. 切换数据库4. 列出当前数据库的所有表5. 查看指定表的所有字段6. 查看指定表的基本情况7. 退出操作8. 新建表9. 删除表10. 清空表11. 添加字段12. 更改…...
做钢结构网站有哪些/免费seo在线优化
我们都知道调用WCF直接在Service References中引用可以远程调用的WCF Url就行了。 但是我们想过没,在Development环境中可以这样做,但是QA、UAT、Production上我们怎么做呢? WCF的通信方式主要有Http和Tcp,这次我们用Http。 好了&…...
客户为什么需要建站服务/环球资源网站网址
一. 时序图 (Sequence Diagram) 时序图 : 显示对象之间的关系, 强调对象之间消息的时间顺序, 显示对象之间的交互; 时序图是一个二维图,横轴表示对象,纵轴表示时间,消息在各对象之间横向传递,依照时间顺序纵向排列。 1.时序图的…...