Vue+Three.js开发
文章目录前言一、什么是Three.js?二、vue引入使用1.vue.config.js配置2.安装引入3.组件内引用4.示例代码三、官网学习1.创建模型(1)场景(scene)(2)相机(camera)正投影相机透视相机(3)渲染器(renderer)总结前言数字孪生近几年被提到的越来越多,基于此来看看学习记录一下vue+three.js学习中的一些问题一、什么是Three.js?一款运行在浏览
文章目录
前言
数字孪生近几年被提到的越来越多,基于此来看看学习记录一下vue+three.js学习中的一些问题
一、什么是Three.js?
一款运行在浏览器中的3D引擎。three.js是一个webgl为基础的库,对webGL的3D渲染工具方法与渲染循环封装的js库,省去与繁琐底层接口的交互,通过threeJS就可以快速生成三维模型。
二、vue引入使用
1.安装引入
npm install --s three
2.组件内引用
import * as THREE from “three”;
3.示例代码
<template>
<div>
<div id="container"></div>
</div>
</template>
<script>
import * as THREE from "three";
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
export default {
data() {
return {
camera: null,
scene: null,
renderer: null,
mesh: null,
controls:null
};
},
mounted() {
this.init();
this.animate();
},
methods: {
//初始化
init: function() {
// 创建场景对象Scene
this.scene = new THREE.Scene();
//网格模型添加到场景中
let geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
let material = new THREE.MeshNormalMaterial({
color: "white"
});
this.mesh = new THREE.Mesh(geometry, material);
this.scene.add(this.mesh);
/**
* 相机设置
*/
let container = document.getElementById("container");
this.camera = new THREE.PerspectiveCamera(
70,
container.clientWidth / container.clientHeight,
0.01,
10
);
this.camera.position.z = 1;
/**
* 创建渲染器对象
*/
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(this.renderer.domElement);
//创建控件对象
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
},
// 动画
animate: function() {
requestAnimationFrame(this.animate);
this.mesh.rotation.x += 0.01;
this.mesh.rotation.y += 0.02;
this.renderer.render(this.scene, this.camera);
}
}
};
</script>
<style>
#container {
position: absolute;
width: 100%;
height: 100%;
}
</style>
三、官网学习
1.创建模型
需要场景(scene)、相机(camera)和渲染器(renderer),他们是图形渲染得重要部分
(1)场景(scene)
承载所有模板的容器,允许渲染模型和位置
new THREE.Scene()
(2)相机(camera)
场景中人眼的角色,决定场景中模型的远近、高度角度等参数。
提供正投影相机、透视相机、立体相机等多种相机模式,常用的为前两种
正投影相机
new THREE.OrthographicCamera( left, right, top, bottom, near, far )
分别设置相机的左边界,右边界,上边界,下边界,远面,近面
透视相机
new THREE.PerspectiveCamera( fov, aspect, near, far )
分别设置相机的视场角度,长宽比,近面,远面
(3)渲染器(renderer)
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(container.clientWidth,
container.clientHeight);
this.renderer.render(scene, camera)
渲染器决定了渲染的结果应该画在页面的什么元素上面,并且以怎样的方式来绘制
2.处理
(1)背景透明
//创建渲染器,alpha设置为true
renderer = new THREE.WebGLRenderer({antialias: true, logarithmicDepthBuffer: true, preserveDrawingBuffer: true, alpha: true});
//设置场景显示区背景色,参数2是透明度
renderer.setClearColor(0xffffff, 0);
四、碰到的一些问题及解决方案
1.加载顺序
项目中为了代码可读性,最好保证一定的场景创建顺序
this.initRender(); //初始化场景渲染器
this.initScene(); //加载场景容器
this.initCamera(); //加载摄像机
this.initLight(); //加载灯光
this.initControls(); //鼠标操作镜头画面
this.animate();
//加载gltf模型
2.选中某个模型聚焦到该模型时,需要更改camera.position以及controls.target坐标
快捷获取到坐标方式:绑定双击事件,在模型中调整好视角之后双击,获取到当前坐标
this.container.addEventListener('dblclick', this.onDoubleClick, false); //双击
//添加双击事件
onDoubleClick() {
console.log('newT =', JSON.stringify(controls.target).replace(/\"/g, ''));
console.log('newP =', JSON.stringify(camera.position).replace(/\"/g, ''));
cssScene.traverse(item => console.log(item.name))
},
3.相机漫游效果实现
使用tween实现
animateCamera(newP, newT, callBack, time = 2000, flag = '0') {
let campos = camera.position, target = controls.target
const tween = new TWEEN.Tween({x1: campos.x, y1: campos.y, z1: campos.z, x2: target.x, y2: target.y, z2: target.z});
tween.to({x1: newP.x, y1: newP.y, z1: newP.z, x2: newT.x, y2: newT.y, z2: newT.z}, isProduct ? time : 0);
tween.onUpdate(object => {
camera.position.x = object.x1;
camera.position.y = object.y1;
camera.position.z = object.z1;
controls.target.x = object.x2;
controls.target.y = object.y2;
controls.target.z = object.z2;
controls.update();
});
tween.onComplete(() => this.callBack(flag));
tween.easing(TWEEN.Easing.Cubic.InOut);
tween.start();
},
4.点击模型,模型发光效果实现
高亮显示模型(呼吸灯)https://wow.techbrood.com/fiddle/56603
OutlineObj(selectedObjects, color = 0x00ffff) {
//移除发光标记
this.iShine = JSON.stringify(selectedObjects) !== '[]';
// 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道。
this.composer = new EffectComposer(renderer);
// 新建一个场景通道 为了覆盖到原理来的场景上
this.renderPass = new RenderPass(scene, camera);
this.composer.addPass(this.renderPass);
// 物体边缘发光通道
let object = new THREE.Vector2(this.out.offsetWidth, this.out.offsetHeight);
this.outlinePass = new OutlinePass(object, scene, camera, selectedObjects);
this.outlinePass.selectedObjects = selectedObjects;
this.outlinePass.edgeStrength = 10; // 边框的亮度,最大10
this.outlinePass.edgeGlow = 1; // 光晕[0,1]//,最大1
this.outlinePass.usePatternTexture = false; // 是否使用父级的材质,纯色的可以使用
this.outlinePass.edgeThickness = 1; // 边框宽度//最大4
this.outlinePass.downSampleRatio = 1.5; // 边框弯曲度//之前为2,,1比较合适
this.outlinePass.pulsePeriod = 1; // 呼吸闪烁的速度//5
this.outlinePass.visibleEdgeColor.set(color); // 呼吸显示的颜色16进制,0x00ffff
this.outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0); // 呼吸消失的颜色(0,0,0)
this.outlinePass.clear = true;
this.composer.addPass(this.outlinePass);
// 自定义的着色器通道 作为参数
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms.resolution.value.set(1 / this.out.offsetWidth, 1 / this.out.offsetHeight);
effectFXAA.renderToScreen = true;
this.composer.addPass(effectFXAA);
//用于更新轨道控制器
clock = new THREE.Clock();
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
//最大纵向旋转角度
controls.maxPolarAngle = Math.PI / 2;
//是否开启右键拖拽
controls.enablePan = true;
},
5.流动管道效果实现
蓝色管道是流动效果的,灰色管道是静止的
- 管道流向与数据绑定(大于零、小于零以及为零时方向)
- 流动速度可以调整
- 流程:根据路径创建曲线 =》 生成管道 =》 设置管道属性 =》创建mesh并命名 =》 将mesh加入group =》创建定时任务调用流动效果函数(图片更改时需要移除之前的mesh)
//line12是一半蓝色一半透明图片,line11是一半灰色一半透明图片
pipelineCurve:textureLoader.load(`${path}/组态图/line12.png`),
pipelineCurve2:textureLoader.load(`${path}/组态图/line12.png`),
//曲线路径以及在路径上重复铺几次
pipelineCurvePoints:[
[
new THREE.Vector3(-933,-144,-203), //1
new THREE.Vector3(-933,-143,-243),
new THREE.Vector3(-933,-142,-283),
new THREE.Vector3(-933,-141,-323),
new THREE.Vector3(-933,-140,-363),
new THREE.Vector3(-933,-140,-373),
new THREE.Vector3(-933,-140,-383),
new THREE.Vector3(-933,-140,-393),
new THREE.Vector3(-933,-140,-400),
new THREE.Vector3(-933,-140,-408), //2
new THREE.Vector3(-893,-140,-408),
new THREE.Vector3(-853,-140,-408),
new THREE.Vector3(-813,-140,-408),
new THREE.Vector3(-773,-140,-408),
new THREE.Vector3(-733,-140,-408),
new THREE.Vector3(-693,-140,-408),
new THREE.Vector3(-683,-140,-408),
new THREE.Vector3(-673,-140,-408),
new THREE.Vector3(-663,-140,-408),
new THREE.Vector3(-654,-140,-408),//3
],
[
new THREE.Vector3(-654,-144,-203), //1
new THREE.Vector3(-654,-140,-408), //2
]
],
pipelineCurveRepeatX:[10, 5]
//调用
this.pipeline(this.pipelineCurve, this.pipelineCurvePoints[0], this.pipelineCurveRepeatX[0], "pipelineCurve");
this.pipeline(this.pipelineCurve2, this.pipelineCurvePoints[1], this.pipelineCurveRepeatX[1], "pipelineCurve2");
/* region 组态图管道 */
pipeline(pipelineCurve, points, repeatX = 10, meshName) {
let pipelineCurveClone = new THREE.CatmullRomCurve3(
points,
false
);
//曲线,路径,即管道的形状|管道分成多少段|管道的半径|管道口分成多少段,即管道口是几边形|是否闭合管道,首尾相接
let tubeGeometry = new THREE.TubeGeometry(
pipelineCurveClone,
80,
8,
40,
false
); //6 0.1
// 设置阵列模式为 RepeatWrapping
pipelineCurve.wrapS = THREE.RepeatWrapping;
pipelineCurve.wrapT = THREE.RepeatWrapping;
// 设置x方向的偏移(沿着管道路径方向),y方向默认1
//等价texture.repeat= new THREE.Vector2(20,1)
pipelineCurve.repeat.x = repeatX; //此路径上重复铺几次
pipelineCurve.repeat.y = 1; //此路径上重复铺几次
//this.pipelineCurve.offset.y = 1.5;//贴图旋转
let tubeMaterial = new THREE.MeshPhongMaterial({
map: pipelineCurve,
transparent: true,
//opacity: 1,//透明的
side: THREE.DoubleSide //两面可见
});
let mesh = new THREE.Mesh(tubeGeometry, tubeMaterial);
mesh.name = meshName;
this.diagramGroup.add(mesh);
},
/* endregion */
//绑定数据以及流动效果
let arr = [];
this.loadPipeState(this.pipelineCurve, 'pvOnBuilding1', 0, 'pipelineCurve', arr)
this.loadPipeState(this.pipelineCurve2, 'chargeInBuilding1', 1, 'pipelineCurve2', arr)
if (arr.length > 0){
arr.forEach(one => this.diagramGroup.remove(one));
}
//pipelineCurveOffset是这一整块的封装
setTimeout(() => levels === 'Diagram' && this.pipelineCurveOffset(), 60);
//加载每条管道的状态
loadPipeState(pipelineCurve, valueName, number, meshName, arr){
if (parseFloat(this.diagramData[valueName]) < 0){
pipelineCurve.offset.x -= 0.08;
if (this.pipelineCurveLine[number] === 11){
this.diagramGroup.traverse(child => child instanceof THREE.Mesh && child.name === meshName&&arr.push(child));
pipelineCurve = textureLoader.load(`${path}/组态图/line12.png`);
this.pipeline(pipelineCurve, this.pipelineCurvePoints[number], this.pipelineCurveRepeatX[number], meshName);
this.pipelineCurveLine[number] = 12;
}
}else if (parseFloat(this.diagramData[valueName]) > 0) {
pipelineCurve.offset.x += 0.08;
if (this.pipelineCurveLine[number] === 11){
this.diagramGroup.traverse(child => child instanceof THREE.Mesh && child.name === meshName&&arr.push(child));
pipelineCurve = textureLoader.load(`${path}/组态图/line12.png`);
this.pipeline(pipelineCurve, this.pipelineCurvePoints[number], this.pipelineCurveRepeatX[number], meshName);
this.pipelineCurveLine[number] = 12;
}
}else {
if (this.pipelineCurveLine[number] === 12){
this.diagramGroup.traverse(child => child instanceof THREE.Mesh && child.name === meshName&&arr.push(child));
pipelineCurve = textureLoader.load(`${path}/组态图/line11.png`);
this.pipeline(pipelineCurve, this.pipelineCurvePoints[number], this.pipelineCurveRepeatX[number], meshName);
this.pipelineCurveLine[number] = 11;
}
}
},
6.解决加载gltf格式模型纹理贴图和原图不一样问题
纹理中包含的颜色信息(.map, .emissiveMap, 和 .specularMap)在glTF中总是使用sRGB颜色空间,而顶点颜色和材质属性(.color, .emissive, .specular) 则使用线性颜色空间。在典型的渲染工作流程中,纹理会被渲染器转换为线性颜色空间,进行光照计算,然后最终输出会被转换回 sRGB 颜色空间并显示在屏幕上。在render中加入以下代码
renderer.outputEncoding = THREE.sRGBEncoding;
//不加下面这句,背景也会变亮
scene.background.encoding = THREE.sRGBEncoding;
另一个方案:材质丢失,再赋值
this.diagramGroup.traverse(child =>{
//材质丢失,再赋值
if(child instanceof THREE.Mesh){
child.material.emissive = child.material.color;
child.material.emissiveMap = child.material.map
}
})
7.解决加载hdr
它根据场景的明暗对比, 把 HDR 高动态范围光照非线性的 ToneMapping 映射到显示器能显示的 LDR 低动态光照范围,尽可能的保存了明暗对比细节,使最终渲染效果更加逼真。
pmremGenerator = new THREE.PMREMGenerator( renderer );
pmremGenerator.compileEquirectangularShader();
this.getCubeMapTexture();
getCubeMapTexture () {
new RGBELoader()
.setDataType( UnsignedByteType )
.load( `${path}/组态图/venice_sunset_1k.hdr`, ( texture ) => {
console.log(texture)
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
console.log(envMap)
pmremGenerator.dispose();
scene.environment = envMap;
});
},
8.压缩gltf文件
(1)使用gltf pipeLine
功能包括:
① lTF 与 glb 的相互转换
② 将缓冲区/纹理保存为嵌入或单独的文件
③ 将 glTF 1.0 模型转换为 glTF 2.0(使用KHR_techniques_webgl和KHR_blend)
④ 使用 Draco 进行网格压缩
这里使用到的主要是④,造成的问题是模型画质的损失是肉眼可观的
- 首先需要安装gltf-pipeline
npm install -g gltf-pipeline
- 安装完成之后需要在命令窗口中进入gltf文件所在文件夹,执行以下代码(demo.gltf是需要压缩的文件名称)
gltf-pipeline -i demo.gltf -d -s
- vue将生成出来的所有文件放在public文件夹下
- 加载压缩过的gltf文件需要DRACOLoader加载器,需要引入
- 将three/examples/js/libs/draco文件复制到public下
//引入
import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader'
//创建一个gltf加载器
const loader = new GLTFLoader();
//这里是因为我将加载gltf整体封装了一个函数,compressed是true/false,代表传进来的是不是压缩过的gltf文件
if(compressed){
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(`${publicPath}draco/gltf/`);
//设置解压库文件路径
loader.setDRACOLoader(dracoLoader);
}
//loader加载gltf,省略
......
(2)使用gltfpack
①KHR_mesh_quantization
顶点属性通常使用FLOAT类型存储,将原始始浮点值转换为16位或8位存储以适应统一的3D或2D网格,也就是我们所说的quantization向量化,该插件主要就是将其向量化。 例如,静态 PBR-ready 网格通常需要每个顶点POSITION(12 字节)、TEXCOORD(8 字节)、NORMAL(12 字节)和TANGENT(16 字节),总共 48 字节。通过此扩展,可以用于SHORT存储位置和纹理坐标数据(分别为 8 和 4 字节)以及BYTE存储法线和切线数据(各 4 字节),每个顶点总共 20 字节。这种方式没有画质损失和加载时间过长的问题
-安装
npm install -g gltfpack --registry=https://registry.npmmirror.com
- 压缩命令
gltfpack -i male.glb -o male-processed.glb
②EXT_meshopt_compression
此插件假定缓冲区视图数据针对 GPU 效率进行了优化——使用量化并使用最佳数据顺序进行 GPU 渲染——并在 bufferView 数据之上提供一个压缩层。每个 bufferView 都是独立压缩的,这允许加载器最大程度地将数据直接解压缩到 GPU 存储中。 除了优化压缩率之外,压缩格式还具有两个特性——非常快速的解码(使用 WebAssembly SIMD,解码器在现代桌面硬件上以约 1 GB/秒的速度运行),以及与通用压缩兼容的字节存储。也就是说,不是尽可能地减少编码大小,而是以通用压缩器可以进一步压缩它的方式构建比特流。首次加载时间比原模型快上不少,并且这种方式没有画质损失和加载时间过长的问题
- 压缩方式与使用方式
gltfpack -i male.glb -o male-processed.glb -cc
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
const loader = new GLTFLoader()
loader.setMeshoptDecoder(MeshoptDecoder)
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
9.释放内存
项目里使用时是将页面嵌入到标签页中展示,使用的框架会对打开过的页面进行缓存,在切换标签时,触发不了beforeDestroy。所以首先需要解决页面缓存问题,之后解决释放内存问题。
首先是在缓存时增加判断,给title为"数字孪生"的项设置notCache为true
这里增加上是否需要缓存的判断,之后页面标签切换时可以正常触发beforeDestroy
下面是释放掉内存的方法, 不过存在一点问题,释放之后打印renderer.info其中的memory,依旧不是0。在释放以后再次加载时如果环境光等出现问题,可以scene先置null后new试一下
beforeDestroy() {
//清除定时器
const lastTimeoutId = setTimeout(null);
for (let i = 0; i <= lastTimeoutId._id; i++) {
clearTimeout(i);
}
scene.remove();
if (renderer){
renderer.dispose();
renderer.forceContextLoss();
renderer.content = null;
let gl = renderer.domElement.getContext('webgl');
gl && gl.getExtension('WEBGL_lose_context').loseContext();
}
THREE.Cache.clear();
window.removeEventListener('resize', this.onWindowResize);
this.container.removeEventListener('mousedown', this.onMouseDown, false) // 鼠标按下
this.container.removeEventListener('click', this.onMouseClick, false); //单击
this.container.removeEventListener('dblclick', this.onDoubleClick, false); //双击
cancelAnimationFrame(ranimationID);
},
加载之前:
加载之后:
释放以后:
DRACOLoader加载器的问题,这个加载器在加载时会创建四个Dedicated Worker,而且并不会自动释放,数量少的时候感知不明显,当不停的关闭打开数字孪生页面时,这些Dedicated Worker会累加,越来越多,必须进行手动释放
LoadGLTFOnly(modelArray, compressed = false) {
const loader = new GLTFLoader();
if(compressed){
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(`${publicPath}draco/gltf/`);
//设置解压库文件路径
loader.setDRACOLoader(dracoLoader);
let number = 0;
//加载gltf文件
modelArray.forEach(one => {
loader.load(one.gltfUrl, gltf => {
number = number + 1;
const model = gltf.scene;
model.traverse(child => {
if (child instanceof THREE.Mesh) {
child.name = one.name;
}
});
//场景中添加模型文件
one.level === 'Cabinet' && this.cabinetGroup.add(model)
if (number === modelArray.length ){
for ( let i = 0; i < loader.dracoLoader.workerPool.length; ++ i ) {
//释放
loader.dracoLoader.workerPool[ i ].terminate();
}
}
});
});
return;
}
//加载gltf文件
modelArray.forEach(one => {
loader.load(one.gltfUrl, gltf => {
const model = gltf.scene;
model.traverse(child => {
if (child instanceof THREE.Mesh) {
child.name = one.name;
}
});
//场景中添加模型文件
one.level === 'Cabinet' && this.cabinetGroup.add(model)
});
});
},
10.优化
- 方向1:默认情况下浏览器对于同一个域名的请求是有并发限制的,如果有多个同域名的资源,浏览器会等待前面的资源下载完毕,然后复用tcp连接发起后续的请求。https://segmentfault.com/q/1010000008676262
- 方向2:初次加载部分,点击具体模型或者拉近视野时加载详细模型信息
11.透明度导致的穿模问题
项目中需要给部分模型标注标签,使用的是精灵模型方式,标签后方出现这种情况,经过测试发现是透明度导致的
const spriteMaterial = new THREE.SpriteMaterial({map: textureTree, opacity: 1});
修改为:
const spriteMaterial = new THREE.SpriteMaterial({map: textureTree, transparent :false, alphaTest:0.1});
12.辉光效果实现的几种方式
(1)使用UnrealBloomPass
使用这种方式需要注意的是辉光是加在全局的,需要修改为局部。官方的代码里有具体的实现,主要使用到的是:THREE.Layers()图层、THREE.EffectComposer(renderer)后期处理通道、glsl着色器语言
three.js官方辉光链接
对于官方辉光的详细解释
实现效果:
(2)使用outlinePass
这种实现方式的思路是,使用three.js的边缘检测方法EdgesGeometry,对获取到的几何边缘创建line,最后使用outlinePass使其发光。这个地方有个坑是,需要注意渲染顺序,否则物体会遮挡住一部分的线条。
//这里只有核心部分代码
modelArray.forEach(one => {
loader.load(one.gltfUrl, gltf => {
const model = gltf.scene;
model.traverse(child => {
if (child instanceof THREE.Mesh) {
child.name = one.name;
if (one.name === 'Park_Main_Building'){
//我这修改了一下物体材质,这个不重要
child.material = new THREE.MeshBasicMaterial({
color: 0x5685cc,
transparent: true,
opacity: 0.6,
})
//渲染顺序
child.renderOrder = 5;
let edges = new THREE.EdgesGeometry(child.geometry)
let line = new THREE.LineSegments(edges, material)
this.parkGroup.add(line)
}
}
});
});
});
this.outlinePass = new OutlinePass(resolution, scene, camera, selectedObjects);
this.outlinePass.selectedObjects = selectedObjects;
this.outlinePass.edgeStrength = 10; // 边框的亮度,最大10
this.outlinePass.edgeGlow = 1; // 光晕[0,1]//,最大1
this.outlinePass.usePatternTexture = false; // 是否使用父级的材质,纯色的可以使用
this.outlinePass.edgeThickness = 1; // 边框宽度//最大4
this.outlinePass.downSampleRatio = 1.5; // 边框弯曲度//之前为2,,1比较合适
this.outlinePass.pulsePeriod = pulse ? 1 : 0; // 呼吸闪烁的速度//5
this.outlinePass.visibleEdgeColor.set(color); // 呼吸显示的颜色16进制,0x00ffff
this.outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0); // 呼吸消失的颜色(0,0,0)
this.outlinePass.clear = true;
this.composer.addPass(this.outlinePass);
实现效果:
(3)使用LineBasicMaterial模拟发光的效果
这种方式的思路与第二种有很大的相同之处,都是先获取到的几何边缘创建line,区别在于,使用LineBasicMaterial来设置line的材质
//核心代码
const material = new THREE.LineBasicMaterial({
color: 0x00FFFF,
transparent: true,
opacity: 0.5,
polygonOffset: true,
depthTest: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
});
let line = new THREE.LineSegments(edges, material)
实现效果:
五、查看学习的博客
three.js模型压缩/gltf/glb,gltf-pipeline
总结
3D渲染图形是一个很好玩的东西,欢迎大家一起交流
更多推荐
所有评论(0)