- VKSession中模型部分纹理丢失,显示黑色
期望为右侧纹理,结果为左侧黑色代码是在官方的vksession代码的基础上,修复gltf模型加载纹理问题,但是发现部分纹理会显示黑色(部分模型纹理可正常),https://file.ljcdn.com/bim-oss/20220304/1646383982124scene100.gltf;使用该模型(很普通的平面+普通纹理)很容易复现纹理分辨率是要求的2的次幂*2的次幂问题大致是 renderGL(frame){} 函数的问题,即为摄像头纹理绘制到canvas 导致模型纹理丢失其实和gltf无关,和textureloader也无关,纹理其实已经正常加载了,但是renderGL会将该纹理冲掉 [图片] [图片]
2022-03-04 - Three.js 文字渲染那些事
THREE.js开发的应用运行在iphone5下发现有些时候会崩溃,跟了几天发现是因为Sprite太多频繁更新纹理占用显存导致的。通常解决纹理频繁更新问题就要用到one draw all方法,放到纹理上就是把所有纹理图片生成一张大图片的方式 一、阻止纹理重复上传 我们需要一张大纹理,先将所有的内容绘制在大纹理上,需要显示局部纹理的时候通过纹理坐标控制去大纹理上取图像。那么这个时候问题来了,THREE.js内部实现方式是将Texture与图片、纹理坐标绑定,即使为所有的Texture对象设置同一张图片,THREE.js仍然会将每个Texture中的图片上传给GPU。每次上传一张大纹理严重阻塞UI渲染进程。[图片] [图片] 首先要解决的是让这张大纹理值上传一次。 这个问题需要我们对THREE.js源码进行深入了解,可以看到setTexture2D函数中有一个properties变量,这个变量是一个WebGLProperties类型的变量,而该类型存储各种东西:Texture、Material、RenderTarget、Object的buffers等。我们继续深入该类的源码,发现get方法会根据对象的uuid来获取相关WebGL属性,比如gl.createTexture、gl.createBuffer创建的各种缓冲区。 [图片] 对应Texture得到的webgl属性如下,其中__webglTexture就是对应的纹理图片创建的缓冲区对象。 [图片] 那么我们可以来一个取巧的方法,将所有纹理的的uuid都设置唯一,那么THREE.js只会对第一个Texture的纹理进行上传,后面的texture对象取到的都是第一个的properties,这样就能避免纹理重复上传。 [图片] 二、建立纹理索引 我们需要自己维护一套索引关系,通过这套索引关系得到每个贴图在大纹理中纹理坐标。这里要为每一个poi记录它的起始位置和区域范围,其中要用到canvasContext.measureText来测量文本的宽度,文本高度可以直接根据fontSize取得。 [图片] 同时索引建立完毕后,需要计算每个poi区域在全局纹理中的纹理坐标范围: [图片] 要注意的是,这里纹理坐标的原点在左下方,有时候原点在左上方。建立索引代码如下 [图片] 三、局部更新 上述方案虽然能够避免频繁上传纹理,但是需要每次将需要绘制的内容准备好,当有内容需要更新时,还是需要重新上传整个全局纹理,反而使得性能下降巨大。经过查阅资料后发现webgl中有一种局部纹理更新技术,简单来说先在内存中开辟一块的纹理区域,将所有内容绘制在这张全局纹理中,每次有更新时,只需要更新它的一个局部区域即可。 但是这里要解决的问题是THREE.js并没有提供局部纹理更新的方式,也没有相应的自定义接口,那么这时候就需要我们自己来处理了。 这里自定义一个Texture的子类 [图片] 开辟一块内存区域 [图片] 在需要的时候动态更新局部纹理,其中src这里是ImageData对象[图片] 具体代码可以参考这里,我这里也是基于它来定制的。 https://github.com/spite/THREE.UpdatableTexture 原文作者通过更改THREE.js源码的方式实现,而我是直接把下面这个函数拷贝到这个子类中 [图片] 四、高清屏的大坑 现在我们的方案是,先在gpu中开辟一块全局纹理区域,然后绘制时将poi绘制到一张与全局纹理同样大小的canvas上,然后从canvas中调用createImageData来获取像素,将像素局部更新到gpu中。那么在pc上我们得到的结果很完美。 [图片] 然而放到移动端上后,我们得到的结果是:[图片] [图片] TMMD中间那块哪去了!找了大半天发现问题出现在高清屏上,挡在高清屏上绘制canvas上时,我们通常会做一些高清处理,比如四像素绘制一像素。 我们做高清处理的方式是利用radio*radio设备像素绘制一css像素,看起来是css像素的大小,但实际在浏览器内部,看起来css上一像素实际在canvas里的像素是radio * radio(radio代表window.devicePixelRatio) [图片] 但实际上在浏览器内部绘制canvas图像的单位是设备像素。那么如果我们还以上面的rectW、rectH来获取像素的话,我们得到的这部分像素并不是这个poi真正占有的像素数目。 [图片] 所以,问题就来了我们需要在gpu开辟的全局纹理的单位跟canvas中获取像素的单位要保持一致,我们统一使用设备像素。 [图片] 我们对canvas也不用使用style来设置样式宽高了。 [图片] 那么获取poi图像的真正像素范围时: [图片] 所以利用getImageData取像素时候,就要小心取到真正的像素区域,(startX * radio,startY * radio)- (poiRectW * radio, poiRectH * radio);否则某些像素就会被丢弃掉,这部分像素才是浏览器真正使用的设备像素。 现在在移动设备上能够获取正确的高清label啦! [图片] 五、局部更新引起的新问题 当全局纹理被占满时候,在继续绘制poi,这时候新的poi区域需要更新到gpu中,那么也就带来了新的问题,在gpu中的纹理还保持着之前的像素,而新的poi会覆盖这部分区域,但有时候往往会与之前的文字叠加起来,效果如下: [图片] 可以看到新更新的poi,在计算纹理坐标时候,有一部分像素包含了其他poi的像素。这个问题是因为新poi的区域刚好叠在了先前poi的边界上,那么我们只要给新的poi加一点buffer,这个buffer是白素透明区域,buffer会把之前的poi像素覆盖掉,而我们计算纹理坐标时,只取poi的边界,那么就可以解决这个问题。 [图片] 那么首先绘制的时候就要保留buffer [图片] 上传的时候使用buffer [图片] 计算纹理坐标时,排除buffer [图片] 六、局部更新带来的性能问题 根据目前的结果,局部更新能后解决crash的问题,但是带来了严重的性能开销,与同事应用局部更新提升性能的结果相反。这个问题还要继续跟踪。 目前发现问题是因为使用了getImageData来获取数据,然后传递到gpu中,非ios设备用这种方式有时候getImageData的开销特别大,而ios设备相对好一些。 测试发现非ios设备直接上传一张大纹理的效果反而比getImageData这种方式更好。但是依然不如之前上传多个canvas的性能。而在iphone5的测试机和iphone6的机器上性能比之前直接上传多个canvas的方式好一些,且没有崩溃问题。但是在岳阳的iphone6 plus 16g内存的手机上发现用具局部纹理更新性能很差,而且经常崩溃。 后来发现原因是因为,虽然getImageData在IOS上性能好过非IOS设备,但性能开销仍然比较大,所以当场景中POI很多时,仍然会引起主线程卡顿,甚至计算太密集引起浏览器崩溃。其中层尝试使用cesium方式,每个poi创建新的canvas,将canvas进行局部上传,本以为这种方式不需要getImageData会更快一些,然而实践发现每次创建canvas设置参数的过程更耗时。 最终的方案是仍然使用getImageData,但是将getImageData的过程分块处理,每50ms处理一次,分块放到场景中,这样就解决密集计算引起的崩溃问题,虽然增加了控制成本,但是能够有效解决IOS崩溃问题。有趣的是在安卓上getImageData方式开销很大,即使分块也不适合,而且安卓用一张大纹理的方式来处理,会发现很多POI绘制效果不好。 [图片] 最终方案是,IOS使用getImageData局部纹理+分块加载方式绘制POI。安卓使用POI独立创建canvas+全量加载方式。(安卓不适用分块加载,是为了尽快把所有POI呈现给用户) 七、文字黑色描边问题 这个问题自始至终困扰我好久一直没找到黑边的原因; [图片] 将原始的canvas导出后发现这是因为原始的canvas就有一层边界 [图片] 曾经怀疑是minFilter的设置不对在pc端纹理使用NEARESTFilter方式取值发现的确能够消除黑边,然而移动端仍然会出现黑边,最后使用颜色混合公式解决问题。 gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 在Three.js中需要设置SpriteMaterial的blending为CustomBlending 八、颜色混合新问题 但是使用上述方式同样引来新问题,设计反映poi的icon四周被裁切掉, [图片] 看着没问题是吧,设计同学截了图之后放大了20倍。。。。。 [图片] 刚开始我确实以为这是webgl渲染问题,后来仔细考虑了下这外圈白色的由来(遇到问题还是得静下心分析)。 原因是设置了blendFunc(SrcAlphaFactor,OneMinusSrcAlphaFactor)导致有些icon周围的像素alpha比较低 [图片] 颜色混合后增加了target的颜色分量,导致最终这些区域的颜色范围接近255,所以泛白。从而把原来图片四周有切边的问题充分暴露出 解决方法是设置alphaTest,如果原始纹理的alpha小于这个值则直接discard。最终得到的效果是: [图片][图片][图片] 九、TextureAlta问题 前面因为sprite的旋转中心只能放在sprite纹理区域的中心所以,上面做了很多冗余纹理,有很多空白区域,目前改造了Sprite加了pivot可以动态改变选中中心点,改变后IOS下纹理的使用率提升了60%,安卓下因为是单个纹理上传所以,需要保证纹理的大小是2的n次方,纹理的浪费率降低了50% 上述问题虽然解决了崩溃问题,但是实际使用中每个poi都要getImageData和texSubImage2D这个方法,造成单个poi耗时基本在25ms(iphone5 8.4.4);虽然上面使用setTimeout 50ms分块方式上传,但是如果poi过多比如1000多的停车场,这样会导致停车场数据需要50s才能完全显示出来。这次优化的方案是等待所有poi图片拿到后,绘制所有的poi把画布调用一次getImageData和一次texSubImage2D上传到gpu,同时下次更新时,只会增量一次性上传更新。 [图片] 十、Frstrum增量更新 原来是在每一级别缩放时把所有的poi都生成好,现在的做法是只生成视锥体中能看得到的poi,然后在每次OrbitControl出发change事件时根据视锥体判断poi,做去重后增量更新 [图片] 目前还是有些问题,有时候会碰到视锥体中的poi很少,可能是判断问题,后续会加入空间索引,根据索引和视锥体结合起来做增量更新 后续使用发现在停车场这种大数据的poi全部加载到地图下,使用这种方式每次都要做去重处理,性能开销很大,处理方式是使用{}做hash代替数组includes方法,结果发现性能提示很大,原来3600个节点每次去重处理在iphone 16g 10.3.3上性能基本在28帧每秒,经过优化后数据帧率达到50+(主流iPhone7fps60);iphone5 16g 8.4.1 性能在24左右优化后帧率在44+,安卓华为荣耀9优化前25帧,优化后 40+ [图片]安卓之所以不适用IOS的绘制方式,是因为这种在安卓上的绘制效果不理想,被设计挑战 安卓后面也做了一些优化,之前安卓是每次都会重新创建canvas并上传至gpu纹理中,导致使用视景体增量更新poi时,性能有所下降,后来每一层中的poi都根据icon、文字组成key缓存起来,并且缓存纹理,不但阻止canvas的重复创建,还阻止canvas重复上传至gpu纹理(three中使用同一uuid),使用该方案荣耀9的fps达到50+ 十一、text glyphs该方式还有待尝试 https://webglfundamentals.org/webgl/lessons/webgl-text-glyphs.html 十二、真正解决POI文字黑边问题 由于要做poi渐变出现效果,但是因为之前处理黑边问题用的是颜色混合的方式,所以当动态改变透明度时,受颜色混合影响往往是文字颜色先消失,剩下透明度部分还存在显示先过很差。所以要实现渐变效果,不能使用颜色混合方法,但不适用颜色混合就会有黑边问题,所以要从源头上解决黑边问题。(看到最后会发现有残影) [图片] 那么思考黑边到底是怎么产生的,这与webgl中纹理插值的颜色有关,有的设备像素取纹理时有不同的方案,但一般情况下纹理像素和设备像素都不是一一对应,所以有插值取值问题。 [图片] 这是正常情况下利用canvas绘图时背景颜色不设置,那么可以看到我们绘制出来的canvas的确有一层奇怪的黑边。当设备取到纹理中这些边界时就会产生黑边。那么就要思考怎么不让它取到这层黑边,这个问题想了好久曾经试过用opacity过滤,发现不能解决问题。 [图片] 有一天突然想到如果canvas背景为有颜色,每个设备像素都能取到颜色,那么就不会有这个问题。所以我们能否通过改一下canvas的背景颜色同时有通过透明度过滤掉不合格的像素?最终发现这个问题还真可以。 首先在绘制时将canvas背景设置为白色,但是有很低的透明度 [图片] 这时候canvas绘制出来的效果是 [图片] 可以看到已经没有黑边了,那么这时候设备像素永远不会取到黑色边界,也就彻底解决了黑边问题。 那么就可以利用tween来做动画了
2019-03-14 - 使用VKSession时如何获取camera的position?
使用的API: wx.createVKSession 微信版本号:8.0.16 基础库版本号:2.21.3 使用了官方提供的示例代码:https://developers.weixin.qq.com/s/NYZcIJmZ7cvJ 只在上述示例代码的页面index2/index.js之中,在render方法的最后面,增加了如下所示代码: const distance = this.model.position.distanceTo(this.camera.position); console.log(disatnce); 我们的功能需要计算出camera和3d模型的距离。但发现无论我怎么移动手机,distance的值是恒定的不变。 想了解一下,是camera本身就不会移动,还是需要我怎么处理呢?
2022-01-06 - 小程序下WebGL截图
最近做了关于小程序下WebGL截图的项目,遇到一些坑,记录下。 安卓切换页面后返回,再一次获取像素为空 IOS偶尔获取到的为空白图像 获取到的数据绘制出来后上下颠倒 翻THREE.JS的代码,发现正确的截图方法 https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLRenderer.js#L1903 [代码]if ( _gl.checkFramebufferStatus( _gl.FRAMEBUFFER ) === _gl.FRAMEBUFFER_COMPLETE ) { // the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604) if ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) { _gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), buffer ); } } else { console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete.' ); } [代码] 所以解决方法 [代码]// 在渲染循环里检测checkFramebufferStatus状态,完成后再读取像素 const render = () => { if (this.disposing) return requestAnimationFrame(render); demo.update() renderer.render(scene, camera); if (this.screenshotResolve) { // @ts-ignore 参考 Threejs WebGLRenderer.readRenderTargetPixels if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { // @ts-ignore gl.readPixels(0, 0, frameBuffer.x, frameBuffer.y, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); // 翻转Y轴 flip(pixelData, frameBuffer.x, frameBuffer.y, 4); // 确保有像素,微信小程序安卓在进入子页面返回本页面后,再一次readPixels稳定无像素 if (pixelData.some(i => i !== 0)) { this.screenshotResolve([pixelData, frameBuffer.x, frameBuffer.y]) this.screenshotResolve = null as unknown as Function; } } } } [代码] 具体可阅读代码片段 https://developers.weixin.qq.com/s/syS4xzmw7AnO
2021-01-23