音效素材网提供各类素材,打造精品素材网站!

站内导航 站长工具 投稿中心 手机访问

音效素材

浅谈three.js中的needsUpdate的应用
日期:2021-09-06 12:14:00   来源:脚本之家

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

    您感兴趣的教程

    在docker中安装mysql详解

    本篇文章主要介绍了在docker中安装mysql详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编...

    详解 安装 docker mysql

    win10中文输入法仅在桌面显示怎么办?

    win10中文输入法仅在桌面显示怎么办?

    win10系统使用搜狗,QQ输入法只有在显示桌面的时候才出来,在使用其他程序输入框里面却只能输入字母数字,win10中...

    win10 中文输入法

    一分钟掌握linux系统目录结构

    这篇文章主要介绍了linux系统目录结构,通过结构图和多张表格了解linux系统目录结构,感兴趣的小伙伴们可以参考一...

    结构 目录 系统 linux

    PHP程序员玩转Linux系列 Linux和Windows安装

    这篇文章主要为大家详细介绍了PHP程序员玩转Linux系列文章,Linux和Windows安装nginx教程,具有一定的参考价值,感兴趣...

    玩转 程序员 安装 系列 PHP

    win10怎么安装杜比音效Doby V4.1 win10安装杜

    第四代杜比®家庭影院®技术包含了一整套协同工作的技术,让PC 发出清晰的环绕声同时第四代杜比家庭影院技术...

    win10杜比音效

    纯CSS实现iOS风格打开关闭选择框功能

    这篇文章主要介绍了纯CSS实现iOS风格打开关闭选择框,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作...

    css ios c

    Win7如何给C盘扩容 Win7系统电脑C盘扩容的办法

    Win7如何给C盘扩容 Win7系统电脑C盘扩容的

    Win7给电脑C盘扩容的办法大家知道吗?当系统分区C盘空间不足时,就需要给它扩容了,如果不管,C盘没有足够的空间...

    Win7 C盘 扩容

    百度推广竞品词的投放策略

    SEM是基于关键词搜索的营销活动。作为推广人员,我们所做的工作,就是打理成千上万的关键词,关注它们的质量度...

    百度推广 竞品词

    Visual Studio Code(vscode) git的使用教程

    这篇文章主要介绍了详解Visual Studio Code(vscode) git的使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。...

    教程 Studio Visual Code git

    七牛云储存创始人分享七牛的创立故事与

    这篇文章主要介绍了七牛云储存创始人分享七牛的创立故事与对Go语言的应用,七牛选用Go语言这门新兴的编程语言进行...

    七牛 Go语言

    Win10预览版Mobile 10547即将发布 9月19日上午

    微软副总裁Gabriel Aul的Twitter透露了 Win10 Mobile预览版10536即将发布,他表示该版本已进入内部慢速版阶段,发布时间目...

    Win10 预览版

    HTML标签meta总结,HTML5 head meta 属性整理

    移动前端开发中添加一些webkit专属的HTML5头部标签,帮助浏览器更好解析HTML代码,更好地将移动web前端页面表现出来...

    移动端html5模拟长按事件的实现方法

    这篇文章主要介绍了移动端html5模拟长按事件的实现方法的相关资料,小编觉得挺不错的,现在分享给大家,也给大家...

    移动端 html5 长按

    HTML常用meta大全(推荐)

    这篇文章主要介绍了HTML常用meta大全(推荐),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参...

    cdr怎么把图片转换成位图? cdr图片转换为位图的教程

    cdr怎么把图片转换成位图? cdr图片转换为

    cdr怎么把图片转换成位图?cdr中插入的图片想要转换成位图,该怎么转换呢?下面我们就来看看cdr图片转换为位图的...

    cdr 图片 位图

    win10系统怎么录屏?win10系统自带录屏详细教程

    win10系统怎么录屏?win10系统自带录屏详细

    当我们是使用win10系统的时候,想要录制电脑上的画面,这时候有人会想到下个第三方软件,其实可以用电脑上的自带...

    win10 系统自带录屏 详细教程

    + 更多教程 +
    HTMLCSSDreamweaverFrontpage