浅谈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 相关文章推荐
表单button的outline在firefox浏览器下的问题
Dec 24 HTML / CSS
CSS3中的content属性使用示例
Jul 20 HTML / CSS
CSS3 :not()选择器实现最后一行li去除某种css样式
Oct 19 HTML / CSS
html5中监听canvas内部元素点击事件的三种方法
Apr 28 HTML / CSS
html5跨域通讯之postMessage的用法总结
Nov 07 HTML / CSS
HTML5如何为形状图上颜色怎么绘制具有颜色和透明度的矩形
Jun 23 HTML / CSS
HTML5学习笔记之html5与传统html区别
Jan 06 HTML / CSS
详解如何通过H5(浏览器/WebView/其他)唤起本地app
Dec 11 HTML / CSS
解锁canvas导出图片跨域的N种姿势小结
Jan 24 HTML / CSS
深入了解canvas在移动端绘制模糊的问题解决
Apr 30 HTML / CSS
app内嵌H5 webview 本地缓存问题的解决
Oct 19 HTML / CSS
css3手动实现pc端横向滚动
Jun 21 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
php addslashes及其他清除空格的方法是不安全的
2012/01/25 PHP
php判断变量类型常用方法
2012/04/24 PHP
详解PHP文件的自动加载(autoloading)
2018/02/04 PHP
PHP PDO和消息队列的个人理解与应用实例分析
2019/11/25 PHP
jquery遍历筛选数组的几种方法和遍历解析json对象
2013/12/13 Javascript
基于jQuery插件实现环形图标菜单旋转切换特效
2015/05/15 Javascript
Javascript编程中几种继承方式比较分析
2015/11/28 Javascript
js跨域请求数据的3种常用的方法
2015/12/01 Javascript
Bootstrap table两种分页示例
2016/12/23 Javascript
ES6中箭头函数的定义与调用方式详解
2017/06/02 Javascript
javascript实现最长公共子序列实例代码
2018/02/05 Javascript
Vue请求JSON Server服务器数据的实现方法
2018/11/02 Javascript
JS实现点餐自动选择框(案例分析)
2019/12/10 Javascript
vue相同路由跳转强制刷新该路由组件操作
2020/08/05 Javascript
[02:14]2016国际邀请赛中国区预选赛Ehome晋级之路
2016/07/01 DOTA
Python的包管理器pip更换软件源的方法详解
2016/06/20 Python
Python装饰器用法示例小结
2018/02/11 Python
Django项目中用JS实现加载子页面并传值的方法
2018/05/28 Python
python基础学习之如何对元组各个元素进行命名详解
2018/07/12 Python
Python3匿名函数用法示例
2018/07/25 Python
pycharm安装和首次使用教程
2018/08/27 Python
Python简直是万能的,这5大主要用途你一定要知道!(推荐)
2019/04/03 Python
对python 中class与变量的使用方法详解
2019/06/26 Python
Python autoescape标签用法解析
2020/01/17 Python
Tensorflow全局设置可见GPU编号操作
2020/06/30 Python
HTML5 的新的表单元素(datalist/keygen/output)使用介绍
2013/07/19 HTML / CSS
html5小技巧之通过document.head获取head元素
2014/06/04 HTML / CSS
naturalizer加拿大官网:美国娜然女鞋
2017/04/04 全球购物
阿玛尼美妆俄罗斯官网:Giorgio Armani Beauty RU
2020/07/19 全球购物
英国户外服装、鞋类和设备的领先零售商:Millets
2020/10/12 全球购物
香港百佳网上超级市场:PARKNSHOP.com
2020/06/10 全球购物
财务部经理岗位职责
2014/02/03 职场文书
地下停车场租赁协议范本
2014/10/07 职场文书
安全生产先进个人总结
2015/02/15 职场文书
MySQL8.0.18配置多主一从
2021/06/21 MySQL
简单聊聊TypeScript只读修饰符
2022/04/06 Javascript