[转载]少走些three.js的弯路

1.  贴图反向

texture.flipY = false;

2.  贴图没有填充满模型

textureMap.wrapS = textureMap.wrapT = THREE.RepeatWrapping;

3.  贴图透明度

transparent: false; //树叶 blending: THREE.MultiplyBlending;

4.  深度冲突

无需深度检测的Material设置 depthTest:false new THREE.WebGLRenderer( { logarithmicDepthBuffer: true } );

5.  渲染顺序问题

WebGLRenderer设置sortObjects: false; 每个Mesh手动设置renderOrder的顺序;

6.  多层次细节

const lod = new THREE.LOD(); //Create spheres with 3 levels of detail and create new LOD levels for them for (let i = 0; i < 3; i++) { const geometry = new THREE.IcosahedronBufferGeometry(10, 3 - i); const mesh = new THREE.Mesh(geometry, material); lod.addLevel(mesh, i * 75); //addLevel ( object : Object3D, distance : Float ) : this //object —— 在这个层次中将要显示的Object3D。 //distance —— 将显示这一细节层次的距离。 } scene.add(lod);

7.  抗锯齿

//antialias - 是否执行抗锯齿。默认为false. new THREE.WebGLRenderer({ antialias: true });

8.  阴影

//渲染器开启渲染阴影效果 renderer.shadowMapEnabled = true; this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; //平面接收投影 plane.receiveShadow = true; //点光源产生投影 spotLight.castShadow = true; //物体对象产生投影 cube.castShadow = true;

阴影使用可能遇到的问题

●  阴影模糊,增加 shadowMapWidth 和 shadowMapHeight,或保证用于计算阴影区域紧密包围在对象周围(shadowCameraNear, shadowCameraFar, shadowCameraFov)

●  产生阴影与接收阴影设置,光源生成阴影,几何体是否接收或投射阴影 castShadow 和 receiveShadow

●  薄对象渲染阴影时可能出现奇怪的渲染失真,可通过 shadowBias 轻微偏移阴影来修复

●  调整 shadowDarkness 来改变阴影的暗度

●  阴影更柔和,可在 THREE. WebGLRenderer 设置不同 shadowMapType。默认 THREE. PCFShadowMap, 柔和:PCFSoftShadowMap

9.  html 标签,CSS2DRenderer

const moonDiv = document.createElement('div'); moonDiv.innerHTML = 'Moon'; //保证能点击 moonDiv.style.pointerEvents = 'auto'; const moonLabel = new CSS2DObject(moonDiv); moonLabel.position.set(0, 10, 0); moon.add(moonLabel); labelRenderer = new CSS2DRenderer(); labelRenderer.setSize(container.offsetWidth, container.offsetHeight); labelRenderer.domElement.style.position = 'absolute'; labelRenderer.domElement.style.top = '0px'; //不妨碍界面上的东东 labelRenderer.domElement.style.pointerEvents = 'none'; container.appendChild(labelRenderer.domElement); function onWindowResize() { labelRenderer.setSize(container.offsetWidth, container.offsetHeight); } function animate() { labelRenderer.render(scene, camera); }

10.  颜色问题

//底色透明 this.renderer.setClearColor(0x000000, 0); //模型渲染,默认THREE.LinearEncoding this.renderer.outputEncoding = THREE.sRGBEncoding; THREE.LinearEncoding; THREE.sRGBEncoding; THREE.GammaEncoding; THREE.RGBEEncoding; THREE.LogLuvEncoding; THREE.RGBM7Encoding; THREE.RGBM16Encoding; THREE.RGBDEncoding; THREE.BasicDepthPacking; THREE.RGBADepthPacking;

11.  分辨率问题

this.renderer.setPixelRatio(window.devicePixelRatio); //分辨率越高渲染压力就越大

12.  物体居中

function setModelCenter(object, viewControl) { if (!object) { return; } if (object.updateMatrixWorld) { object.updateMatrixWorld(); } // 获得包围盒得min和max const box = new THREE.Box3().setFromObject(object); let objSize = box.getSize(new THREE.Vector3()); // 返回包围盒的中心点 const center = box.getCenter(new THREE.Vector3()); object.position.x += object.position.x - center.x; object.position.y += object.position.y - center.y; object.position.z += object.position.z - center.z; let width = objSize.x; let height = objSize.y; let depth = objSize.z; let centroid = new THREE.Vector3().copy(objSize); centroid.multiplyScalar(0.5); if (viewControl.autoCamera) { this.camera.position.x = centroid.x * (viewControl.centerX || 0) + width * (viewControl.width || 0); this.camera.position.y = centroid.y * (viewControl.centerY || 0) + height * (viewControl.height || 0); this.camera.position.z = centroid.z * (viewControl.centerZ || 0) + depth * (viewControl.depth || 0); } else { this.camera.position.set( viewControl.cameraPosX || 0, viewControl.cameraPosY || 0, viewControl.cameraPosZ || 0 ); } this.camera.lookAt(0, 0, 0); }

13.  清空资源

function cleanNext(obj, idx) { if (idx < obj.children.length) { this.cleanElmt(obj.children[idx]); } if (idx + 1 < obj.children.length) { this.cleanNext(obj, idx + 1); } } function cleanElmt(obj) { if (obj) { if (obj.children && obj.children.length > 0) { this.cleanNext(obj, 0); obj.remove(...obj.children); } if (obj.geometry) { obj.geometry.dispose && obj.geometry.dispose(); } if (obj.material) { for (const v of Object.values(obj.material)) { if (v instanceof THREE.Texture) { v.dispose && v.dispose(); } } obj.material.dispose && obj.material.dispose(); } obj.dispose && obj.dispose(); obj.clear && obj.clear(); } } function cleanObj(obj) { this.cleanElmt(obj); obj?.parent?.remove && obj.parent.remove(obj); } function cleanAll() { window.removeEventListener('resize'); cancelAnimationFrame(this.threeAnim); if (this.stats) { this.container.removeChild(this.stats.domElement); this.stats = null; } this.cleanObj(this.scene); this.controls && this.controls.dispose(); this.renderer.renderLists && this.renderer.renderLists.dispose(); this.renderer.dispose && this.renderer.dispose(); this.renderer.forceContextLoss(); let gl = this.renderer.domElement.getContext('webgl'); gl && gl.getExtension('WEBGL_lose_context').loseContext(); this.renderer.setAnimationLoop(null); this.renderer.domElement = null; this.renderer.content = null; console.log('清空资源', this.renderer.info); this.renderer = null; THREE.Cache.clear(); if (this.map) { this.map.destroy(); } }

14.  模型显示面的问题

.side:Integer

定义将要渲染哪一面 - 正面,背面或两者。 默认为 THREE.FrontSide。其他选项有 THREE.BackSide 和 THREE.DoubleSide。

material.side = THREE.DoubleSide;

15.  Raycaster 鼠标拾取

不要检测全局,用 actionObjs 收集需要动作的物体

this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.container.style.cursor = 'pointer'; this.container.addEventListener( 'pointerdown', (event) => { event.preventDefault(); this.mouse.x = ((event.offsetX - this.container.offsetLeft) / this.container.offsetWidth) * 2 - 1; this.mouse.y = -((event.offsetY - this.container.offsetTop) / this.container.offsetHeight) * 2 + 1; let vector = new THREE.Vector3(this.mouse.x, this.mouse.y, 1).unproject(this.camera); this.raycaster.set(this.camera.position, vector.sub(this.camera.position).normalize()); this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = raycaster.intersectObjects(this.actionObjs, true); if (intersects?.length) { console.log('action', intersects[0]); this.raycasterAction(intersects[0]); } }, false );

16. Canvas 贴图

生成的 canvas 大小最好是正常模型贴图大小的五倍以上,有可能因为缩放问题,导致贴图模糊

17.打包后线上效果与开发时效果存在差异

将three的相关js提到html上,从外部引入,这样能保证three不会因为打包而乱了,导致效果有问题 FBXLoader外部引入,记得把libs里面的inflate也加上

18.THREE.js截图

new THREE.WebGLRenderer({ preserveDrawingBuffer: true //保留缓冲区 }); import { saveAs } from 'file-saver-fixed'; function convertBase64UrlToBlob(base64) { let parts = base64.split(';base64,'); let contentType = parts[0].split(':')[1]; let raw = window.atob(parts[1]); let rawLength = raw.length; let uInt8Array = new Uint8Array(rawLength); for (let i = 0; i < rawLength; i++) { uInt8Array[i] = raw.charCodeAt(i); } return new Blob([uInt8Array], { type: contentType }); } saveImage: () => { let image = threeModel.renderer.domElement.toDataURL('image/jpeg'); let blob = convertBase64UrlToBlob(image); saveAs(blob, new Date().getTime() + '.jpg'); }

18.glb压缩过的模型加载,记得加DRACOLoader

记得将three.js/examples/js/libs/draco/gltf目录下的draco解码器全部放在public/draco文件夹下,否则会导致模型加载失败!

let dracoLoader = new THREE.DRACOLoader(); dracoLoader.setDecoderPath('draco/'); dracoLoader.setDecoderConfig({ type: 'js' });//或者{type: "wasm"} dracoLoader.preload(); const loader = new THREE.GLTFLoader(); loader.setDRACOLoader(dracoLoader); return loader;

19.大量复用模型可采用InstancedMesh

减少绘制程序调用的次数,提升渲染效率

//count 需要生成的相同模型数量 let mesh = new THREE.InstancedMesh(geometry, material, count); //动态生成,`THREE.DynamicDrawUsage` mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); //设置第index个模型的位置 mesh.setMatrixAt(index, matrix); //位置更新 mesh.instanceMatrix.needsUpdate = true; //设置第index个模型的颜色 mesh.setColorAt(index,new THREE.Color(1,0,0)); //颜色更新 mesh.instanceColor.needsUpdate = true;

20.合并模型

BufferGeometry包含点线面等相关的缓冲区数据,使用它能降低将所有这些数据传递到GPU的成本

将多个形状合并成一个,减少模型数量,提升渲染效率!

const geometries = []; const geometry = new THREE.BufferGeometry().fromGeometry(new THREE.BoxGeometry( 10, 10, 10 )); geometry.applyMatrix4( matrix );//模型位置 geometries.push( geometry );//将模型缓冲几何形状添加到数组 //... const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries( geometries );

合并后的模型就属于一个整体,动作检测时只能检测到这个整体,不能检测子模型

如果需要监听到merge后的具体模型,需要做一些处理

  1. 在每个geometry添加到数组时,也要设置模型顺序索引modelIndex和选中索引selectIndex,并且收集每个geometry的面数,如果是同一个形状的话,面数直接用同一个就好,这样计算更方便。

const count = geometry.getAttribute('position').count; const modelIndex = new Uint8Array(count).fill(index); const selectIndex = new Uint8Array(count).fill(-1); geometry.setAttribute('selectIndex', new THREE.BufferAttribute(selectIndex, 1, true)); geometry.setAttribute('modelIndex', new THREE.BufferAttribute(modelIndex, 1, true));

2.而赋值给merge后形状的Mesh的材质也要对应修改,给顶点着色器和片元着色器添加代码,判断当前模型索引是否等于选中的索引,然后赋予需要的颜色。

const material = new THREE.MeshBasicMaterial({ vertexColors: true }); material.onBeforeCompile = (shader) => { shader.vertexShader = shader.vertexShader.replace( `void main() {`, ` attribute float selectIndex; attribute float modelIndex; varying float vselectIndex; varying float vmodelIndex; void main() { vmodelIndex=modelIndex; vselectIndex=selectIndex;` ); shader.fragmentShader = shader.fragmentShader.replace( `void main() {`, `varying float vselectIndex; varying float vmodelIndex; void main() {` ); shader.fragmentShader = shader.fragmentShader.replace( `vec3 outgoingLight = reflectedLight.indirectDiffuse;`, `vec3 outgoingLight = vmodelIndex ==vselectIndex ?vec3(1.0,0.0,0.0 ): reflectedLight.indirectDiffuse ;` ); };

  1. Raycaster点击后的获得的信息中有个叫做fanceIndex的属性,就是这个形状中某个面的索引,然后根据面索引和面数算出具体选中的模型索引selectIndex

    image.png

 

const intersects = this.raycaster.intersectObjects(this.actionGroup, true); if (intersects.length > 0) { let activeObj = intersects[0]; let index = parseInt(activeObj.faceIndex / 12);//正方体有12个面 let len = this.boxmesh.geometry.getAttribute('position').count; const selectIndex = new Uint8Array(len).fill(index); this.boxmesh.geometry.setAttribute( 'selectIndex', new THREE.BufferAttribute(selectIndex, 1, true) ); console.log(activeObj, this.boxmesh.geometry, index); this.boxmesh.geometry.getAttribute('selectIndex').needsUpdate = true;//一定要记得更新选中索引值 }

然后如图所见,就能选中某个形状啦!

image.png

github地址:https://github.com/xiaolidan00/my-earth/blob/main/src/mergeGeometry.html

后期处理部分辉光

官网给出的是Layer分层的方案,感觉操作起来很烦,我推荐的是直接用visible控制要渲染出泛光效果的组件和不需要渲染泛光效果组件,然后将这两组渲染结果合并就是最终的画面了

this.renderer.setViewport(0, 0, this.container.offsetWidth, this.container.offsetHeight); //必须关闭autoClear,避免渲染效果被清除 this.renderer.autoClear = false; this.renderer.clear(); //不需要发光的物体在bloom后期前隐藏 this.normalObj.visible = false; //渲染泛光的场景 this.composer.render(); //清除深度缓存 this.renderer.clearDepth(); //不需要发光的物体在bloom后期后显示 this.normalObj.visible = true; //合并两个渲染场景,即可部分泛光 this.renderer.render(this.scene, this.camera);

image.png

github地址:https://github.com/xiaolidan00/my-earth/blob/main/src/UnrealBloom.html

svg转图片

直接使用html2canvas来讲svg转成png图片

注意:

  • svg宽高获取是这样的width.baseVal.value,height.baseVal.value
  • svg正真的dom是父级元素的最后一个,第一个是协议注释,别搞错

import html2canvas from 'html2canvas'; function base64ToFile(base64) {   let arr = base64.split(',');   let bstr = atob(arr[1]);   let n = bstr.length;   let u8arr = new Uint8Array(n);   while (n--) {     u8arr[n] = bstr.charCodeAt(n);   }   return new File([u8arr], new Date().getTime() + '.png', { type: 'image/png' }); } export function svg2png(file) {   return new Promise((resolve, reject) => {     const reader = new FileReader();     reader.onload = (ev) => {       const svg = document.createElement('div');       svg.innerHTML = ev.target.result;       document.body.appendChild(svg);       html2canvas(svg, {         backgroundColor: null, //背景透明 //宽高         width: svg.lastChild.width.baseVal.value,         height: svg.lastChild.height.baseVal.value       })         .then((canvas) => {           const base64 = canvas.toDataURL('image/png');           const f = base64ToFile(base64);           resolve(f);           setTimeout(() => {             svg.parentElement.removeChild(svg);           }, 1000);         })         .catch((err) => {           console.log(err);           svg.parentElement.removeChild(svg);         });     };     reader.onerror = () => {       reject(null);     };     reader.readAsText(file);   }); } let uploadFile = file;           if (uploadFile.name.substring(uploadFile.name.lastIndexOf('.') + 1) == 'svg') {               svg2png(uploadFile).then(png=>{ console.log('svg2png', png); });           }

作者:敲敲敲敲暴你脑袋
链接:https://juejin.cn/post/7222475192933072952

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值