浅谈three.js中的needsUpdate的应用


Posted in HTML / CSS onNovember 12, 2012

three.js里的很多对象都有一个needsUpdate属性,文档中很少有写(不过three.js的文档本来就没多少,很多问题还得靠github上的issues),网上各式各样的教程中也不太会写这个,因为对于简单的入门程序而言,是用不到这个属性的。
那么这个属性到底是用来干嘛的,一言以敝之就是告诉renderer这一帧我该更新缓存了,尽管作为一个标志位用途很简单,但是因为要知道为什么要更新缓存,要更新哪些缓存,所以还是有必要好好了解下的。
为什么需要needsUpdate
首先还是来看下为什么需要缓存,缓存的存在一般都是为了减少数据传输的次数,从而减少程序在数据传输上消耗的时间,这里也是,一般一个物体(Mesh)要最后能够成功显示到屏幕前是很不容易的,需要转三次战场
首先是通过程序将所有的顶点数据和纹理数据从本地磁盘读取到内存当中。
然后程序在内存中做了适当的处理之后就要将那些需要绘制到屏幕前的物体的顶点数据和纹理数据传输到显存当中。
最后在每一帧渲染的时候将显存中的顶点数据和纹理数据flush到GPU中进行装配,绘制。
根据那个金字塔式的数据传输模型,第一步显然是最慢的,如果是在WebGL这样的环境中通过网络来传输,那就更加慢了,其次是从内存传输到显存的时间,这个后面会做一个简单的数据测试。
然后是这三步操作的使用频率,对于小场景来说,第一步是一次性的,就是每次初始化程序的时候就会将一个场景的所有数据都加载到内存中了,对于大场景来说,可能会做一些异步加载,但是目前暂时不在我们考虑的问题当中。 对于第二步的频率,应该是这次要讲的最主要的,首先写个简单的程序测试一下做这一步传输所带来的消耗

复制代码
代码如下:

var canvas = document.createElement('canvas');
var _gl = canvas.getContext('experimental-webgl');
var vertices = [];
for(var i = 0; i < 1000*3; i++){
vertices.push(i * Math.random() );
}
var buffer = _gl.createBuffer();
console.profile('buffer_test');
bindBuffer();
console.profileEnd('buffer_test');
function bindBuffer(){
for(var i = 0; i < 1000; i++){
_gl.bindBuffer(_gl.ARRAY_BUFFER, buffer);
_gl.bufferData(_gl.ARRAY_BUFFER, new Float32Array(vertices), _gl.STATIC_DRAW);
}
}

先简单解释下这个程序,vertices是一个保存顶点的数组,这里是随机生成了1000个顶点,因为每个顶点都有x,y,z三个坐标,所以需要一个3000大小的数组, _gl.createBuffer命令在显存中开辟了一块用来存放顶点数据的缓存,然后使用_gl.bufferData将生成的顶点数据从内存传输一份copy到显存中。 这里假设了一个场景中有1000个1000个顶点的物体,每个顶点是3个32位4个字节的float数据,计算一下就是差不多1000 x 1000 x 12 = 11M的数据,profile一下差不多消耗了15ms的时间,这里可能看看15ms才这么点时间,但是对于一个实时的程序来说,如果要保证30fps的帧率,每一帧所需要的时间要控制在30ms左右,仅仅是做一次数据的传输就花去了一半的时间怎么成,要知道大头应该是GPU中的绘制操作和在CPU中的各种各样的处理啊,应该吝啬整个渲染过程中的每一步操作。
所以应该尽量减少这一步的传输次数,其实可以做到刚加载的时候就把所有的顶点数据和纹理数据从内存一并传输到显存当中,这就是现在three.js做的,第一次就把需要绘制的物体(Geometry)的顶点数据传输到显存中,并且缓存这个buffer到geometry.__webglVertexBuffer,之后每次绘制的时候都会判断Geometry的verticesNeedUpdate属性,如果不需要更新就直接使用现在的缓存,如果看到verticesNeedUpate为true, 就会重新将Geometry中的顶点数据传输到geometry.__webglVertexBuffer中,一般对于静态物体我们是不需要这一步操作的,但是如果遇到顶点会频繁改变的物体,例如用顶点来做粒子的粒子系统,还有使用了骨骼动画的Mesh, 这些物体每一帧都会改变自己的顶点,所以需要每一帧都需要将其verticesNeedUpdate属性设为true来告诉renderer我需要重新传输数据了!
其实在WebGL程序中,更多的会在vertex shader中去改变顶点的位置来完成粒子效果和骨骼动画,尽管如果放在cpu端计算更容易扩展,但是因为javascript的计算能力的限制,更多的还是会把这些计算量大的操作放到gpu端操作。 这种情况下并不需要重新传输一次顶点数据,所以上面那种case在实际程序中其实用到的不多,更多的还是会去更新纹理和材质的缓存。
上面那个case主要描述的是一个传输顶点数据的场景,除了顶点数据,还有一个大头就是纹理,一张1024*1024大小的R8G8B8A8格式的纹理所要占用的内存大小也要高达4M,于是看下面这个例子
复制代码
代码如下:

var canvas = document.createElement('canvas');
var _gl = canvas.getContext('experimental-webgl');
var texture = _gl.createTexture();
var img = new Image;
img.onload = function(){
console.profile('texture test');
bindTexture();
console.profileEnd('texture test');
}
img.src = 'test_tex.jpg';
function bindTexture(){
_gl.bindTexture(_gl.TEXTURE_2D, texture);
_gl.texImage2D(_gl.TEXTURE_2D, 0, _gl.RGBA, _gl.RGBA, _gl.UNSIGNED_BYTE, img);
}

这里就不需要变态的重复1000次了,一次传输10241024的纹理就已经花了30ms,一张256256的差不多是2ms,所以three.js中对于纹理也是尽量只在最开始的时候传输一次,之后如果texture.needsUpdate属性不手动设为true的话就会一直直接使用已经传输到显存中的纹理。
需要更新哪些缓存
上面通过两个case描述了为什么three.js要加这么一个needsUpdate属性,接下来列举一下几个场景来知道在什么情况下需要手动的更新这些缓存。
纹理的异步加载
这算是一个小坑吧,因为前端的图片是异步加载的,如果在创建好img后直接写texture.needsUpdate=true的话,three.js的renderer中会这一帧中就使用_gl.texImage2D将空的纹理数据传输到显存中,然后就将这个标志位设成false, 之后真正等到图片加载完成的时候确不再更新显存数据了,所以必须要在onload事件中等整张图片加载完成后再写texture.needsUpdate = true
视频纹理
大部分纹理都是像上面那个case直接加载和传输一次图片就行了,但是对于视频纹理来说并不是,因为视频是一个图片流,每一帧要显示的画面都不一样,所以每一帧都需要将needsUpdate设为true来更新显卡中的纹理数据。
使用render buffer
render buffer是比较特殊的对象,一般的程序在整个场景绘制出来后都是直接flush到屏幕了,但是如果多了post processing或这screen based xxx(例如screen based ambient occlusion)的话,就需要将场景先绘制到一个render buffer上,这个buffer其实就是一张纹理,只不过是上一步绘制生成的,而不是从磁盘加载的。three.js中有一个专门的texture对象WebGLRenderTarget来初始化和保存renderbuffer, 这种纹理也需要在每一帧设置一下needsUpdate为true
Material的needsUpdate
材质在three.js中是通过THREE.Material来描述的,其实材质并没有什么数据要传输,但是为什么还要搞一个needsUpdate呢,这里还要说一下shader这个东西,shader直译过来是着色器,提供了在gpu中编程处理顶点和像素的可能性,在绘画中有个shading的术语来表示绘画的明暗法,GPU中的shading也类似,通过程序计算光照的明暗来表现物体的材质,ok, 既然shader是一段跑在GPU上的程序,那么像所有程序一样都需要进行一次编译链接的操作, WebGL中是在运行时对shader程序进行编译的,这当然需要消耗时间,因此也是最好能够一次编译就运行到程序结束。所以three.js中就在material初始化的时候就编译链接了shader程序并且缓存了编译链接后得到的program对象。一般一个material是不需要再去重新编译整个shader了,材质的调整只需要修改shader的uniform参数就行了。但是如果是替换了整个材质,比如将原来phong的shader替换成了一个lambert的shader,就需要将material.needsUpdate设置成true去重新做一次编译。不过这种情况不多见,更常见的是下面提到的一种情况。
添加和删除灯光
这个应该还是在场景中比较常见了的吧,可能很多刚开始用three.js的人都会掉进这个坑里,在给场景动态添加了一个灯光后发现这个灯光怎么不起作用,不过这是在用three.js内置的shader的情况下,例如phong, lambert,看renderer里的源代码就会发现three.js在内置的shader代码中使用#define来设置场景中灯光的个数,而这个#define的值是在每次更新材质的时候通过字符串拼接shader得到,代码如下
复制代码
代码如下:

"#define MAX_DIR_LIGHTS " + parameters.maxDirLights,
"#define MAX_POINT_LIGHTS " + parameters.maxPointLights,
"#define MAX_SPOT_LIGHTS " + parameters.maxSpotLights,
"#define MAX_HEMI_LIGHTS " + parameters.maxHemiLights,

确实这种写法能够有效的减少了gpu寄存器的使用,如果只有一盏灯光就可以只声明一个一盏灯光所需要的uniform变量,但是在每次灯光数量改变,特别是添加的时候就需要重新拼接编译链接一次shader,这时候也需要将所有材质的material.needsUpdate设为true;
改变纹理
这里的改变纹理指的并不是更新纹理数据,而是原来材质使用了纹理,后来不使用了,或者原来材质不使用纹理后来又加上去了,如果不手动强制更新材质都会导致最后出来的效果跟自己想的不一样,产生这种问题的原因跟上面添加灯光差不多,也是因为shader中加了一个宏来判断是否使用了纹理,
复制代码
代码如下:

parameters.map ? "#define USE_MAP" : "",
parameters.envMap ? "#define USE_ENVMAP" : "",
parameters.lightMap ? "#define USE_LIGHTMAP" : "",
parameters.bumpMap ? "#define USE_BUMPMAP" : "",
parameters.normalMap ? "#define USE_NORMALMAP" : "",
parameters.specularMap ? "#define USE_SPECULARMAP" : "",

所以每次map, 或者envMap或者lightMap等改变真值的时候都需要更新材质
其它顶点数据的改变
其实上面纹理的改变还会产生一个问题,主要是在初始化的时候没有纹理,但是后来动态添加上去这种环境下,光是将material.needsUpdate设为true还不够,还需要将geometry.uvsNeedsUpdate设成true, 为什么会有这种问题呢,还是因为three.js对程序的优化,在renderer中第一次初始化geometry, material的时候,如果判断为没有纹理,尽管内存中的数据中有每个顶点uv数据,但 three.js 还是不会将这些数据copy到显存中,初衷应该还是为了节省点宝贵的显存空间,但是在添加纹理后geometry并不会很智能的重新去传输这些uv数据以供纹理使用,必须要我们手动的将设置uvsNeedsUpdate来告知它该更新uv了, 这个问题真是开始的时候坑了我很长时间。
关于几种顶点数据的needUpdate属性可以看这条issue
https://github.com/mrdoob/three.js/wiki/Updates
最后
three.js的优化做的是不错,但是在各种优化下带来的是各种可能踩到的坑,这种情况最好的办法也只能是看源代码了,或者去github上提issues
HTML / CSS 相关文章推荐
HTML5 CSS3给网站设计带来出色效果
Jul 16 HTML / CSS
使用CSS3的appearance属性改变任何元素的浏览器默认风格
Dec 24 HTML / CSS
css3弹性盒模型实例介绍
May 27 HTML / CSS
HTML5自定义data-* data(obj)属性和jquery的data()方法的使用
Dec 13 HTML / CSS
html5指南-1.html5全局属性(html5 global attributes)深入理解
Jan 07 HTML / CSS
html5实现canvas阴影效果示例
May 07 HTML / CSS
HTML5的一个显示电池状态的API简介
Jun 18 HTML / CSS
html5实现九宫格抽奖可固定抽中某项奖品
Jun 15 HTML / CSS
简洁自适应404页面HTML好看的404源码
Dec 16 HTML / CSS
css实现文章分割线样式的多种方法总结
Apr 21 HTML / CSS
CSS实现章节添加自增序号的方法
Jun 23 HTML / CSS
html粘性页脚的具体使用
Jan 18 HTML / CSS
HTML5教程之html 5 本地数据库(Web Sql Database)
Apr 03 #HTML / CSS
用HTML5实现手机摇一摇的功能的教程
Oct 30 #HTML / CSS
HTML5是什么 HTML5是什么意思 HTML5简介
Oct 26 #HTML / CSS
HTML 5 标签、属性、事件及浏览器兼容性速查表 附打包下载
Oct 20 #HTML / CSS
HTML5上传文件显示进度的实现代码
Aug 30 #HTML / CSS
input元素的url类型和email类型简介
Jul 11 #HTML / CSS
HTML5安全介绍之内容安全策略(CSP)简介
Jul 10 #HTML / CSS
You might like
PHP7 mongoDB扩展使用的方法分享
2019/05/02 PHP
Jquery ajaxsubmit上传图片实现代码
2010/11/04 Javascript
IE6 fixed的完美解决方案
2011/03/31 Javascript
js操作label给label赋值及取label的值示例
2013/11/07 Javascript
Javascript异步编程模型Promise模式详细介绍
2014/05/08 Javascript
chrome下img加载对height()的影响示例探讨
2014/05/26 Javascript
基于Turn.js 实现翻书效果实例解析
2016/06/20 Javascript
javascript回到顶部特效
2016/07/30 Javascript
jQuery双向列表选择器DIV模拟版
2016/11/01 Javascript
js精确的加减乘除实例
2017/11/14 Javascript
JS通过ajax + 多列布局 + 自动加载实现瀑布流效果
2019/05/30 Javascript
javascript+css实现进度条效果
2020/03/25 Javascript
详解 javascript对象创建模式
2020/10/30 Javascript
[00:57]英雄,你的补给到了!
2020/11/13 DOTA
pymongo实现多结果进行多列排序的方法
2015/05/16 Python
Mac 上切换Python多版本
2017/06/17 Python
pandas 小数位数 精度的处理方法
2018/06/09 Python
django DRF图片路径问题的解决方法
2018/09/10 Python
Python 移动光标位置的方法
2019/01/20 Python
如何在Python中实现goto语句的方法
2019/05/18 Python
python爬虫beautifulsoup解析html方法
2020/12/07 Python
Django视图类型总结
2021/02/17 Python
导出HTML5 Canvas图片并上传服务器功能
2019/08/16 HTML / CSS
For Art’s Sake官网:手工制作的奢华眼镜
2018/12/15 全球购物
大学生涯自我鉴定
2014/01/16 职场文书
大学毕业生求职自荐书
2014/06/05 职场文书
群众路线四风对照检查材料
2014/11/04 职场文书
优秀教师单行材料
2014/12/16 职场文书
写给孩子的新学期寄语
2015/02/27 职场文书
护士自荐信范文
2015/03/25 职场文书
杨善洲观后感
2015/06/04 职场文书
安全生产隐患排查制度
2015/08/05 职场文书
大队委员竞选稿
2015/11/20 职场文书
奖学金发言稿(范文)
2019/08/21 职场文书
导游词之天下银坑景区
2019/11/21 职场文书
python中sys模块的介绍与实例
2021/04/17 Python