Three.js开发中遇到的常见问题总结和性能优化

3,399 阅读7分钟

关于Three.js开发中遇到的一些问题总结

1.加载外部模型文件无法在场景中显示

(1) 确保当前文件内容是否能被读取,在Javascript的console中查找错误,并确定当你调用.load()的时候,使用了onError回调函数来输出结果, 如果err 输出则表示当前glb 文件内容无法被读取从新换一个glb文件尝试

	const loader = new GLTFLoader()
	loader.load('/three/1.glb', (result) => {
			 console.log(result)
			}, () => {
			}, (err) => {
				console.log(err)
		})

(2) 尝试将模型放大缩小到原来的1000倍。许多模型的缩放比例各不相同,如果摄像机位于模型内,则大型模型将可能不会显示。或者可以在模型加载完成之后根据模型比例大小设置合适的缩放值

		//设置模型位置
		this.model.updateMatrixWorld()
		const box = new THREE.Box3().setFromObject(this.model);
		const size = box.getSize(new THREE.Vector3());
		const center = box.getCenter(new THREE.Vector3());
		// 计算缩放比例
		const maxSize = Math.max(size.x, size.y, size.z);
		const targetSize = 2.5; // 目标大小
		const scale = targetSize / (maxSize > 1 ? maxSize : .5);
		this.model.scale.set(scale, scale, scale)
	     // 设置控制器最小缩放值
		this.controls.maxDistance = size.length() * 10
		// 设置相机位置
		this.camera.position.set(0, 2, 6)
		// 设置相机坐标系
		this.camera.lookAt(center)
		this.camera.updateProjectionMatrix();

(3)增加相机远端面的值 far,如果在创建相机时摄像机视锥体远端面的值设置过小也无法蒋模型正确的显示出来

this.camera = new THREE.PerspectiveCamera(45, clientWidth / clientHeight, 0.25, 100)
this.camera.far = 2000
this.camera.updateProjectionMatrix()

(4) 尝试添加一个光源并改变其位置。模型或许被隐藏在黑暗中。

       // 创建一个平行光
		this.directionalLight = new THREE.DirectionalLight('#1E90FF', 1)
		this.directionalLight.position.set(-1.44, 2.2, 1)
		this.directionalLight.castShadow = true
		this.scene.add(this.directionalLight)

2.模型材质辉光效果影响背景图的正常显示

(1)在场景动画帧渲染中对背景图进行单独处理

	sceneAnimation() {
		this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation())
		// 将不需要处理辉光的材质进行存储备份
		this.scene.traverse((v) => {
			if (v instanceof THREE.Scene) {
				this.materials.scene = v.background
				v.background = null
			}
			if (!this.glowMaterialList.includes(v.name) && v.isMesh) {
				this.materials[v.uuid] = v.material
				v.material = new THREE.MeshBasicMaterial({ color: 'black' })
			}
		})
		this.glowComposer.render()
		// 在辉光渲染器执行完之后在恢复材质原效果
		this.scene.traverse((v) => {
			if (this.materials[v.uuid]) {
				v.material = this.materials[v.uuid]
				delete this.materials[v.uuid]
			}
			if (v instanceof THREE.Scene) {
				v.background = this.materials.scene
				delete this.materials.scene
			}
		})

		this.effectComposer.render()
	}

3.窗口大小改变场景画面像素变得模糊

在窗口监听方法中更新相机,渲染器等相关信息

	window.addEventListener("resize", this.onWindowResize.bind(this))
				// 监听窗口变化
	onWindowResize() {
		const { clientHeight, clientWidth } = this.container
		//调整屏幕大小
		this.camera.aspect = clientWidth / clientHeight //摄像机宽高比例
		this.camera.updateProjectionMatrix() //相机更新矩阵,将3d内容投射到2d面上转换
		this.renderer.setSize(clientWidth, clientHeight)
		this.effectComposer.setSize(clientWidth, clientHeight)
		this.glowComposer.setSize(clientWidth, clientHeight)
	}
			

4.修改材质的position(x,y,z)没有实际的效果

模型材质类型为 Mesh 的材质支持修改 position

	const mesh = this.model.getObjectByName(name)
	if(mesh.type == 'Mesh){
        mesh.position.set(1,10,1)
   }

5.材质设置网格,透明度和颜色没有实际效果或者造成材质显示不正确?

three.js支持 修改网格,透明渡和颜色的材质有: MeshBasicMaterial,MeshLambertMaterial,MeshPhongMaterial,MeshStandardMaterial,MeshPhysicalMaterial,MeshToonMaterial 这六种

6.鼠标(点击,拖拽,缩放,移动)等操作不影响场景内容

// 启用或禁用摄像机平移,默认为true。
this.controls.enablePan = false
// 当设置为false时,控制器将不会响应用户的操作。默认值为true。
thsi.controls.enabled =false
// 启用或禁用摄像机水平或垂直旋转。默认值为true。
thsi.controls.enableRotate  =false
// 启用或禁用摄像机的缩放。
thsi.controls.enableZoom =false

7.将three.js color 值转化为普通 css 值

const colot = new THREE.Color(colorRGB).getStyle()

8.调整相机角度,模型的部分材质内容显示不完整或者不显示

将 frustumCulled值设置为false不管是否在相机视椎体都会渲染

   this.model.traverse(item => {
        if (item.isMesh && item.material) {
            item.frustumCulled = false  
        }
    })

9.模型文件加载进度条 total 为0 lengthComputable为false?

可能原因有以下几种:

1.文件资源在项目本地文件夹public(vue项目)

2.服务器未提供 Content-Length: 服务器在响应头中未提供正确的 Content-Length 字段,或者提供的值无效。在这种情况下,浏览器无法确定要加载的总字节数,因此 lengthComputable 被设置为 false

3.流式传输: 一些加载场景中,数据是通过流式传输的方式加载的,因此无法预先知道要加载的总字节数。例如,通过 WebSocket 或 SSE (Server-Sent Events) 加载数据时,通常无法确定总字节数。 lengthComputable 为false 情况单独处理 :

  onModelProgress(progressEvent ){
      const { loaded ,total ,lengthComputable } = progressEvent 
        //lengthComputable  true 可以获取到当前文件的总字节大小
       if(lengthComputable ){
        const progress = loaded / total * 100
        console.log( progress + '% loaded' )
        }else{
          console.log('已加载'+loaded+'字节' )
      }
   }

10.使用 GLTFExporter 导出模型文件(.glb, .gltf) 动画丢失?

在需要把当前场景的内容导出为一个glb或者gltf文件时,使用 GLTFExporter方法对场景进行导出,在导出来新的glb文件时发现原来的模型文件的动画丢失了?

解决方法: three.js GLTFExporter 方法的第三个参数提供 导出相关配置的传入 options , 获取到当前场景需要的导出的剪辑动画 animations 在调用导出方法时传入即可。

代码如下示例: 注意是GLTFExporter第三个参数第二个参数为 导出错误的回调函数

    const type = 'glb'
  	const exporter = new GLTFExporter();
		const options = {
			trs: true,      // 是否保留位置、旋转、缩放信息
			animations: this.modelAnimation, // 导出的动画
			binary: type == 'glb' ? true : false,  // 是否以二进制格式输出
			embedImages: true,//是否嵌入贴图
			onlyVisible: true, //是否只导出可见物体展
		}
		exporter.parse(this.scene, function (result) {
			if (result instanceof ArrayBuffer) {
				// 将结果保存为GLB二进制文件
				saveArrayBuffer(result, `${new Date().toLocaleString()}.glb`);
			} else {
				// 将结果保存为GLTF JSON文件
				saveString(JSON.stringify(result), `${new Date().toLocaleString()}.gltf`);
			}
			function saveArrayBuffer(buffer, filename) {
				// 将二进制数据保存为文件
				const blob = new Blob([buffer], { type: 'application/octet-stream' });
				const url = URL.createObjectURL(blob);
				const link = document.createElement('a');
				link.href = url;
				link.download = filename;
				link.click();
				URL.revokeObjectURL(url);
				console.log('导出成功')
			}
			function saveString(text, filename) {
				// 将字符串数据保存为文件
				const blob = new Blob([text], { type: 'application/json' });
				const url = URL.createObjectURL(blob);
				const link = document.createElement('a');
				link.href = url;
				link.download = filename;
				link.click();
				URL.revokeObjectURL(url);
				console.log('导出成功')
			}
		}, (err) => {
			console.log('导出错误',err)
		}, options);

11.模型复制克隆多个失效?或者深拷贝模型对象报错?

three.js 中如果模型包含了 (Skeleton)或动画(Animation) 属性,需要通过 SkeletonUtils API 来实现复制(克隆)多个

       import * as  SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
       loader.load('your_model.fbx', (model) => {
            // 检查加载是否完成
            if (model) {
              const clonedModel = SkeletonUtils.clone(model);
              this.scene.add(clonedModel);
            } else {
              console.error('Failed to load the model.');
            }
        });

同一个模型复制多个

   
          loader.load('your_model.fbx', (model) => {
            // 检查加载是否完成
            if (model) {
                const numCopies = 5; // 设置要创建的副本数量
                for (let i = 0; i < numCopies; i++) {
                  // 克隆模型
                  const clone = SkeletonUtils.clone(model)
                  const x =Math.random()*(i+1)*20
                  const y =Math.random()*(i+1)*50
                  const z =Math.random()*(i+1)*50
                  clone.position.set(x,y,z)
                  // 将副本添加到场景中
                  this.three.scene.add(clone);
                }
            } else {
              console.error('Failed to load the model.');
            }
        });

11.添加或者修改模型材质贴图时,出现浏览器警告?

THREE.WebGLRenderer: Texture marked for update but no image data found.

原因:在贴图未加载完成之前修改了贴图的属性值

          const mapTexture = new THREE.TextureLoader().load(url)
	        mapTexture.wrapS = THREE.RepeatWrapping;
		mapTexture.wrapT = THREE.RepeatWrapping;
		mapTexture.repeat.set(1, 1);
		mapTexture.center.set(0.5, 0.5);
		mapTexture.offset.set(0, 0);
		mapTexture.rotation = 0
		mapTexture.magFilter = THREE.NearestFilter;
		mapTexture.minFilter = THREE.LinearMipmapLinearFilter;
		mapTexture.needsUpdate = true;
          material.map = mapTexture

解决:确保你正确加载了纹理图像。在使用纹理之前,你需要确保图像已经加载完成,并在加载完成后将其应用到材质上

	const texture = new THREE.TextureLoader()
	texture.load(url, (mapTexture) => {	
		mapTexture.wrapS = THREE.RepeatWrapping;
		mapTexture.wrapT = THREE.RepeatWrapping;
		mapTexture.repeat.set(1, 1);
		mapTexture.center.set(0.5, 0.5);
		mapTexture.offset.set(0, 0);
		mapTexture.rotation = 0
		mapTexture.magFilter = THREE.NearestFilter;
		mapTexture.minFilter = THREE.LinearMipmapLinearFilter;
		mapTexture.needsUpdate = true;
          material.map = mapTexture
	})

12.OutlinePass 后期处理效果影响 TransformControls 控制器的显示?

描述:同时使用outlinePass和TransformControls,选中其他物体时,TransformControls辅助对象也加上了描边

原因:在定义OutlinePass时传入的参数是整个场景(scene)

const { clientHeight, clientWidth } = this.container
this.outlinePass = new OutlinePass(new THREE.Vector2(clientWidth, clientHeight), this.scene, this.camera)

解决:将场景(scene) 改为当前模型(model)

const { clientHeight, clientWidth } = this.container
this.outlinePass = new OutlinePass(new THREE.Vector2(clientWidth, clientHeight), this.model, this.camera)

产生的新问题:当模型动态切换或者被改变了 outlinePass 效果失效或者显示异常?

解决:在模型更换成功后,动态修改 outlinePassrenderScene

  this.outlinePass.renderScene = this.model

关于Three.js开发中性能优化

在页面关闭销毁跳转离开时清除代码中 定时器事件监听动画帧等相关方法。释放场景中的材质内存,清除场景和模型相关信息

   // 清除模型数据
	onClearModelData(){
		cancelAnimationFrame(this.rotationAnimationFrame)
		cancelAnimationFrame(this.renderAnimation)
		cancelAnimationFrame(this.animationFrame)
		this.container.removeEventListener('click', this.onMouseClickModel)
		this.container.removeEventListener('mousedown', this.onMouseDownModel)
		this.container.removeEventListener('mousemove', this.onMouseMoveModel)
		window.removeEventListener("resize", this.onWindowResize)
		// 材质释放内存
	 	this.scene.traverse((v) => {
			  if (v.type === 'Mesh') {
			     v.geometry.dispose();
			     v.material.dispose();
		    	}
		  })
		  // 清除场景和模型相关信息
	         this.model.clear()
           this.scene.clear()
	}

外部资源加载完成之后或者一些控制器不需要在使用通过 dispose 及进行销毁

   const texture = new THREE.TextureLoader().load(url);
   this.scene.background = texture
   texture.dispose()
       
  this.transformControls = new TransformControls(this.camera, this.renderer.domElement);
  this.scene.remove(this.transformControls)
  this.transformControls.dispose()

完整的代码可参考:

gitee.com/ZHANG_6666/…基于three.js开发3d模型可视化编辑器

在这里插入图片描述