
专注于高性能H5内核研发,用C/C++挖掘设备硬件的潜能,让JS提升开发效率,打造一种所见即所得的开发方式。
紧接上文 25 只在内网发布 WebGL的发展 WebGL2.0目前没有正式定稿 前端技术越来越精彩了,新版WebVRSDK可以支持WebGL2.0,谷歌最近也发布了JS版本的机器学习框架:搜索:教程 | 如何用30行JavaScript代码编写神经网络异或运算器 最近太忙了,也不打算维护WebVRSDK了,要不要开源,纠结中。。
紧接上文 消费升级的必然是体验升级,AR/VR技术是提升用户体验的方式之一。智能手机让用户可以AnyTime、AnyWhere的体验移动生活服务,再结合扁平化UI设计,在小小的手机屏幕上,实现了可以媲美PC的体验,用户已经从开始的惊艳,慢慢变成了适应,认为智能手机就应该是这样子,即我们所说的《审美疲劳》。为了提升用户体验,硬件厂商也是操碎了心,不断提升手机屏幕尺寸、清晰度、分辨率、曲面屏等等。其背后的推力,就是消费升级的表现。虽然我们不是硬件厂商,但在这场变革中,我们可以通过软件来实现自我价值。举个例子,如果把完整的《清明上河图》放在手机上来展示,效果和亲临世博会相差万里。如果有了VR技术,把手机屏幕作为通向虚拟展厅的眼睛,就像深入其境的到了“中国馆”,会不会给用户带来极大的体验提升呢? 本次Demo所需二维码 视频演示 增加了播放器页面,用户可以自己输入在线视频的URL,使用三种方式播放: 沉浸式体验,让用户可以体验在豪宅看电影 单屏全景视频,通过软件模拟VR分屏效果 分屏全景视频,这个需要视频源支持才可以哦 二维码如下: 操作指南 App首页指南 APP是个演示Demo不具有线上产品的体验,点检《影视ICON》进入播放器页面 视频播放器指南 图中有3个按钮一个输入框,用户可以自定义在线视频的URL,并根据视频的内容来选择哪种方式打开。这里为了方便演示,作者已经预置了一个单屏全景视频URL,提供给大家测试使用。 沉浸式2D电影体验 就是普通的电影了,优酷高清视频基本都是720P了。720P在iPhone plus上看,我是觉得有点模糊的,但在我客厅的48寸电视中,我知道视频内容往往也就是720P,我就感觉可以接受。为什么视频清晰度一样的情况下,5.5英寸的屏幕反而比48寸的屏幕更不能让人接受呢? 单屏全景电影播放 这个很简单了,参考之前的文章《22. WebAR那些事: 20行代码做全景》的那篇,把视频作为一个图像纹理,再通过three.js渲染出球形纹理,播放就好了。如果点击进入VR模式,那么就相当于生成两个一样的球形纹理,把相机的位置根据左右调整参数,就可以实现VR效果了 分屏全景电影播放 首先用户要确定视频的来源是否是分屏的,这个很重要,否则播放效果会出现乱七八糟的情况。单屏模式,只是把视频的一半,也就是左边的部分作为球形纹理,展示在屏幕上。当用户点击VR模式的时候,会根据左右两部分各生成一个球形纹理,摄像机的位置无需调整,因为视频内容已经调整过了。 用户输入框 输入自己喜欢的视频,自己体验吧。如果需要,我这里有4K的单屏全景视频《极地星空》非常漂亮,因为视频太大了,就不放到线上,可以线下分享给大家。 效果展示 沉浸式2D视频 邀请你到精装修豪宅中,体验超大屏幕播放电影的快感 支持一键切换VR模式 全屏全景/VR模式 同样支持一键进入VR模式 分屏全景视频 这个直接在首页,播放迪丽热巴故事视频就可以了。 WebVR现状的解读 天空盒VS球形纹理 之前我们一直使用球形纹理作为全景的素材,球形纹理VS天空盒的优劣在哪里? 功能对比 不清楚的可以翻看我之前写的WebGL技术文章 通常手机支持的最大纹理长度为2048x2048,好一些的手机,可以支持4096x4096。球形纹理的常见长宽比是2:1,也就是说,最简单高效的展示清晰的全景图片,可以用一张4096x2048的球形纹理。 天空盒是6面体,也就需要6张纹理作为6个面,最大的清晰度,可以做到6张4096x4096的纹理,作为贴图,清晰度明显比全景要高 开发效率 一张全景图片总比六张贴图要更方便管理。。而且天空盒需要通过球形纹理二次加工生成,目前主流的全景相机,都是支持一键生成球形纹理,目前我还没有找到一键生成天空盒的相机。有的话,请亲们留言,因为我更喜欢用天空盒。 性能对比 同等画质的情况下,天空盒要比球形纹理性能更好,这个先挖坑,后续讨论。 WebVR世界的初始坐标 当3D世界建立起来时,手机的当前位置,就是当前坐标系,如下图所示 天空盒与贴图的映射 六个面,上下左右前后,如下图: THREE.JS中映射关系 THREE.JS为了方便记忆,简单的理解为: [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] 结合上面提到的坐标轴,中文正确的理解方式为:正X轴、负X轴、正Y轴、负Y轴、正Z轴、负Z轴,分别指向的贴图 核心代码 代码示例 var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); var path = 'http://aeapp.oss-cn-hangzhou.aliyuncs.com/skybox/home/'; var cube = [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]; var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000); var effect = new THREE.VREffect(renderer); var loader = new THREE.TextureLoader(); var control = new THREE.VRControls(camera); var video = document.createElement('video'); video.src = getQueryString("src"); video.loop = true; video.muted = true; video.setAttribute('webkit-playsinline', 'webkit-playsinline'); video.play(); var texture = new THREE.VideoTexture(video); texture.format = THREE.RGBFormat; texture.minFilter = THREE.NearestFilter; texture.maxFilter = THREE.NearestFilter; texture.generateMipmaps = false; var screen = new THREE.Mesh(new THREE.PlaneBufferGeometry(40, 20), new THREE.MeshBasicMaterial({ map : texture })); screen.position.z = -40; var scene = new THREE.Scene(); scene.add(screen); scene.add(new THREE.AmbientLight(0xFFFFFF)); scene.background = new THREE.CubeTextureLoader().setPath(path).load(cube); animate(); function animate() { effect.requestAnimationFrame(animate); control.update(); effect.render(scene, camera); } function getQueryString(name) { var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); var r = window.location.search.substr(1).match(reg); if (r) { return unescape(r[2]); } return null; } 代码解读 天空盒是VR世界的背景 scene.background = new THREE.CubeTextureLoader().setPath(path).load(cube); 电视机是VR世界的一个面 var screen = new THREE.Mesh(new THREE.PlaneBufferGeometry(40, 20), new THREE.MeshBasicMaterial({ map : texture })); screen.position.z = -40; 电视机的3D坐标 [0, 0, -40]也就是说在在用户正前方,距离40个单位的地方 代码不足的地方 参数调整不够完善 应为是个Demo的缘故,这里不做具体细节的展示 内存管理的不足 业余时间开发的SDK,慢慢打磨吧 性能上的不足 性能分两块来看待: JS的运行速度,相比Native,确实慢很多,表现在加载时间上,耗时较多 GL的渲染速度,相比Unity3D,丝毫不逊色,红米Note3表现60FPS的强劲性能 体验上的不足 加载的时候,应该增加等待框 网络加载失败时,应该有对应的错误处理 视频源不够清晰 小结 这是个Demo,慢慢会好的 畅想 沉浸式体验的深度 目前只是做了一个全景,简单的模拟了沉浸式体验。但远远不够,需要更多的功能细节和技术变革来完善。比如: 虚拟世界的时间不够真实,亦辰不变导致视觉疲劳 光线没有变化,开灯、关灯、早上、夜晚等等,更接近真实 如果场景和视频的内容相互影响,需要机器学习来分析视频的场景,营造电影院效果4.虚拟伙伴,比如我想在VR世界来找个美女朋友一起观影~~ 等等 想象空间巨大 通过VR技术,可以做房产导购、家装市场、电商的垂直体验、心理/身体安慰,作为技术人员,我们可以预感到3~5年之后的变化,把空间留给产品去吹牛,把技术难度留给自己,哈哈。 下一回 我们继续讨论基于WebVRSDK的VR播放器敬请期待
紧接上文 WebGL及其WebVR技术的发展,让开发者无需编译打包,即可快速开发AR/VR业务。比如,开发一个VR播放器。那么问题来了,各大公司在追求的AR/VR技术,本质的驱动力是什么呢?2016中国文化产业峰会,《李彦宏谈文化消费新机遇》中提到: “一个明显的特征,用户更愿意为文化产品付费”。我是比较赞成这句话的,技术变革、消费升级等趋势下,好的文化、内容是消费者新的宠儿,因为满足了用户的需求。而AR/VR是内容领域的一个分支,用于更好的呈现虚拟内容,我们称之为体验升级,这也是这篇文章的主题。 本次Demo所需二维码 最新在重新拾回Android开发,把界面美化了,支持数据更新,在线升级,内容没有变化 消费升级 VIP会员制越来越普遍 如果我们回首过去,'会员卡'文化从无到有 ,从有到精,再到微信公证号。。我们不考虑上层社会的会员制度,作为码农来说,我就说说我能看到的 2009年之前,我接触到最多的校园一卡通、银行卡、准考证等等,是一种人群分类的工具 2009年之后,淘宝、口碑、网易邮箱等,网上会员制度,不需要花钱,就可以搞定,是一种身份的认证 2013年之后,开始注意到线下的超市卡、购物卡、理发卡等,是一种购物的反馈,增加用户粘性 2015年之后,亲,请关注某某微信公众号,就可以注册会员了,还可以给你一瓶加多宝哦 2016年之后,优酷、京东、山姆等付费会员,不用看广告、包邮哦 用户付费习惯的升级 天下没有免费的午餐,想要优惠,就必须付出自己的代价。淘宝读书,每天签到,可以领取一本电子书。。优酷视频,免费播放,但要看60s的广告,吐血。。淘宝购物,想要包邮,亲满99包邮哦用户对免费制度的认可,已经习以为常,或者说,无能为力,毕竟企业也要有利润空间。 付费会员的崛起 世界多彩缤纷,是因为有差异,有不同不想去看广告,可以付费免广告。想随意购物,可以自己承担快递费,或者买某东的Plus会员。坐飞机想要更舒服,可以做头等舱。感觉奔驰宝马不爽,可以买五菱荣光。。 小结 互联网经济的发展,让很多人拥有了'免费'的午餐,在这个免费的过程中,用户们反而变得越来越爱花钱了。有没有一种感觉,淘宝上的衣服很便宜的,但不知不觉,我们的预算就超标了? 体验升级 用户体验呢,好比是看电影。小的时候,搬着板凳看露天电影;有电脑了,下载盗版视频;后来,就花钱去电影院看,然后3D、巨幕、imax等等 核心内容一样 这个时代,数字产品的版权与保护,是非常重要的。很多时候,我们可以非常方便的购买、下载到最新的电影。甚至是4k视频,清晰度也不比电影院差多少。我们有客厅的大电视、投影仪,无需付费,就可以方便观看。 但体验不一样! 这么好的电影不能去电影院观看,真是可惜了。我是赞同的,我们读三国,感觉袁绍好傻X,曹操很精明。当局者迷而旁观者清,如果没有深入情景的感触,很难理解到内容中的精华。没有理解到精华,看不如不看。是的,就像一个人买了一辆跑车,每天堵在帝都4环上,对人和车来说都是一种痛苦。 小结 体验升级,不仅仅是优秀内容的产生,也是好的内容载体的升级。二者相辅相成,缺一不可,没有人愿意为一台装着拖拉机发动机的劳斯莱斯买单。 技术变革 AR/VR是一种展现形式的升级 电影拍出360°全景图片、视频、4k高清、3D音效等等优质内容,想把最真实最精华的精彩展示给用户,但并无卵用,因为普通的电视、电脑无法播放。摄影可以拍出超清晰、壮丽的珠穆朗玛峰,想把最美的一面展示给用户,但展现效果不一定比iPhone4效果好,因为用户的手机屏幕也就5英寸/720P,缩放之后更容易失真。 有没有想过,当把一幅完整的清明上河图,展示在手机屏幕上是什么感觉呢?屏幕太小了,看清了整体丢失了细节,看到了细节,无法感受到整理,是不是有一种管中窥豹的既视感? VR是一种沉浸式体验 2D视频 《贞子》很恐怖吧,我看到的时候,全场大笑,因为整个宿舍同学在夏日中午一起看的,喝着饮料磕着瓜子,当然还有个逗逼的剧透,给我提前讲着各种花絮,再加上老式电视机的那个垃圾音效,一部恐怖硬生生的变成了逗逼片。 如果有VR技术呢?我们可以构造一张全景图片,内容是阴森空旷的房间,在你面前放着一台60英寸的电视机,播放着《贞子》,窗外不时的闪过鬼影,你带着耳机,音效的随着你的变化时大时小。我想在这种情况下,很多人很难自拔。 普通3D视频 同上,VR加上人工智能,给你推荐合适的虚拟世界 固定视角的VR视频 普通播放器就可以播放了,无需VR 全景视频 这种情况更好,我们不用自己虚拟世界了,因为视频是全景的,VR播放器直接播放好了。 分屏全景视频 完美!这种电影很难得,淘宝520杨洋故事线,就是这种视频,4k高清,大气。VR播放器播起来。 AR 我们暂时先不提 小结 VR技术给我们的不仅是一种好玩,更是一种内容的展示方式,它可以把《清明上河图》虚拟成一副巨图,挂载法国的卢浮宫。可以把2D电影,虚拟用户在马尔代夫的海边别墅中,看电视机。也可以吧4k或者更高清的内容,完美的展示给用户,而不是缩放到一个5英寸的手机屏幕上。 WebVRSDK播放器 现在我们谈谈,上一节中我们提到的VR播放器,是一个什么样的播放器。 支持2D图片 支持VR世界切换,比如一键切换背景世界,把一幅画高大上的挂载到一个虚拟的展厅里面,这里我们需要更多的全景图片,作为背景的切换,构建虚拟世界,需要适当的3D建模。 支持全景图片 比较简单,已经支持了,我会做个demo 支持分屏全景图片 比较简单,已经支持了,我会做个demo 2D视频 支持VR世界切换,比如一键切换背景世界,把视频挂载到一个虚拟的世界,这里我们需要更多的全景图片,作为背景的切换,构建虚拟世界,需要适当的3D建模。 有个网络段子,虽然露骨,但是触及内心:少壮不努力,老大看VR 3D视频 手机解码器不支持 VR视频 手机普通播放器就可以了 全景VR视频 比较简单,已经支持了,我会做个demo 分屏全景VR视频 参考DemoAPP的VR视频 总结 说了这么多,要表达什么意思呢?简单一句话,逼格不够,VR来凑 现在大阿里商圈,有太多不同的商品需要展示,如果还是拘泥于传统的图片+文字。常说,线上线下要打通,可不是简单的把照片发到线上。在某一些领域,即使有再精美的商品,如果没有很好的技术载体,对于消费者来说,没有太多体验的提升,那么消费动力就容易产生瓶颈。 下一节 我们继续聊基于WebVRSDK的VR播放器,手把手的写个Demo,作为演示。
紧接上文 AR技术,全称增强现实技术,对已有的现实世界,增加一些虚拟元素,提升用户体验。传统的内容展示无非于:图片、文字、视频等组合,随着经济、技术的发展,越来越多的用户已经不满足于现状,目前需要的是消费升级、体验升级,那么AR、VR技术运用而生。 本次Demo所需二维码 本次demo已经集成到APK中,用户可以点击《全景》来体验。 AR全景 环境准备 Demo使用r82版本,向下兼容 Three.js 到http://www.threejs.org下载最新源码,这里使用three.min.js作为渲染引擎。 WebVR.js WebVR的工具库,用于切换VR状态(three.js的源码包中,自己拷贝) VREffect.js WebVR的展示库,用于分屏展示(three.js的源码包中,自己拷贝) VRControls.js WebVR的控制库,用于监控陀螺仪、Camera、重力等(three.js的源码包中,自己拷贝) 3D模型 这次增加了一张本地4k图片,所以包变大了。 核心代码 代码参考Three.js,代码只需20行 var w = window.innerWidth; var h = window.innerHeight; var renderer = new THREE.WebGLRenderer(); // 创建渲染器 renderer.setSize(w, h); // 设置渲染器为全屏 document.body.appendChild(renderer.domElement); // 将渲染器添加到body上 var camera = new THREE.PerspectiveCamera(75, w / h, 0.01, 100); var effect = new THREE.VREffect(renderer); // 控制器,用来控制VR渲染 var loader = new THREE.TextureLoader(); // 加载器,用于异步加载图片 var control = new THREE.VRControls(camera); // 控制器,用来控制摄像机 var mater = { map : new THREE.TextureLoader().load('map.jpg'), side : THREE.BackSide }; var earth = new THREE.Mesh(new THREE.SphereGeometry(20, 32, 32), new THREE.MeshBasicMaterial(mater)); var scene = new THREE.Scene(); // 创建场景 scene.add(earth); // 添加全景球形纹理 animate(); function animate() { effect.requestAnimationFrame(animate); control.update(); effect.render(scene, camera); } 效果展示 我有一间小木屋,面朝大海,春暖花开 畅想 体验升级 沉浸式体验,可以让用户更加方便的感受照片。相比传统的照片浏览方式,左右滑动、上下滑动,每张照片展示的只是一个角度。对于用户来说,展示的思路并不连贯,需要用户在脑海中二次加工,才能生成完整的场景。 消费升级 相机与照片,交卷与相片,相辅相成。以前拥有一台柯达傻瓜相机,是一件非常幸福的事情。后来是单反,拍出更加清晰的电子照片。再后来是全景相机,360°球形照片。随着人们的消费升级,不禁的会问:花了这么多钱,你就给我看这个?这种问题,在每个时代的末期,都会有人提出。我们现在就处于传统2D电子照片的末期,新的时代马上就会来临。 下一回 手把手做一个VR视频播放器,要支持在线URL输入播放的那种。敬请期待
紧接上文 使用WebAR技术,开发者只需40行代码,即可搭建一个ARDemo。同时我们也抛出了一个问题,如果把地球模型,换成一个可以动的小猫小狗,是不是就更加接近虚拟现实了? 本次Demo所需二维码 本次demo已经集成到APK中,用户可以点击《天马星空》来体验。 天马星空 环境准备 Demo使用r82版本,向下兼容 Three.js 到http://www.threejs.org下载最新源码,这里使用three.min.js作为渲染引擎。 WebVR.js WebVR的工具库,用于切换VR状态(three.js的源码包中,自己拷贝) VREffect.js WebVR的展示库,用于分屏展示(three.js的源码包中,自己拷贝) VRControls.js WebVR的控制库,用于监控陀螺仪、Camera、重力等(three.js的源码包中,自己拷贝) 3D模型 之前我们总是使用地球的3D模型,估计各位看官也看腻了,这次终于更新了! 支持动画的3D模型通过阅读THREE.JS的示例,找到了喜欢的模型,一批奔跑的马儿。 在webgl分类下的shadowmap入口,可以看到官方的demo。 3D模型的格式本次演示的3D模型,是JSON格式,可以有多种工具生成,比如3DMax、玛雅等。 这种格式虽然方便阅读,缺点也比较明显,将整个文件解析成json后,再转换成内存数据,最后上传到GPU,由于JS语言在运算处理方面的不足,加载过程会稍慢一些。 WebVRSDK支持的3D格式打开THREE.JS的官方example,可以看到loder分类的3D模型格式,初步可以理解为,只要three.js支持的格式,WebVRSDK也是可以支持的。如果有不支持,请反馈给我,我会及时更新WebVRSDK 兼容性准备 获取摄像头API,目前只支持Navigator.getUserMedia index.js核心代码 核心代码量约50行,核心代码和《上一篇》基本一致,只是将3D地球的模型,替换成了一批马。 代码参考Three.js,如下 var w = window.innerWidth; var h = window.innerHeight; var renderer = new THREE.WebGLRenderer(); // 创建渲染器 renderer.setSize(w, h); // 设置渲染器为全屏 document.body.appendChild(renderer.domElement); // 将渲染器添加到body上 var horse, delta; var clock = new THREE.Clock(); // 创建时钟 var scene = new THREE.Scene(); // 创建场景 var mixer = new THREE.AnimationMixer(scene); // 创建混合器 var camera = new THREE.PerspectiveCamera(45, w / h, 100, 3000); var control = new THREE.VRControls(camera); // 控制器,用来控制摄像机 var effect = new THREE.VREffect(renderer); // 控制器,用来控制VR渲染 var loader = new THREE.JSONLoader(); // 加载器,用于加载3D模型 loader.load("horse.json", function(geometry) { var material = new THREE.MeshLambertMaterial({ color: 0xFFAA55, morphTargets: true, vertexColors: THREE.FaceColors }); horse = new THREE.Mesh(geometry, material); horse.speed = 200; horse.position.x = -500; scene.add(horse); scene.add(new THREE.AmbientLight(0x444444));// 创建环境光 scene.add(new THREE.DirectionalLight(0xFFFFFF, 1, 0)); mixer.clipAction(geometry.animations[0], horse).setDuration(1).startAt(-Math.random()).play(); }); window.navigator.getUserMedia({ audio : true, video: { width: w, height: h }}, function(stream) { var video = document.createElement('video'); video.src = stream; video.play(); var image = new THREE.VideoTexture(video); image.generateMipmaps = false; image.format = THREE.RGBAFormat; image.maxFilter = THREE.NearestFilter; image.minFilter = THREE.NearestFilter; scene.background = image; // 背景视频纹理 }, null); animate(); function animate() { effect.requestAnimationFrame(animate); if (horse) { delta = clock.getDelta(); mixer.update(delta); horse.position.z += horse.speed * delta; if (horse.position.z > 1000) { horse.position.z = -1000 - Math.random() * 500; } } control.update(); effect.render(scene, camera); } 效果展示 很想把屏幕录制下来,再制成GIF格式的图片,方便演示,大家有没有推荐的工具软件,最好能直接截屏的那种。演示的效果,大概是这样子的,一匹马从远处跑来,然后再跑到远处,直至消失。用两张图片大概来演示吧。。 畅想 WebAR入门简单 写这篇文章的时候,我也想不到,只需要50行代码,就可以做一个带有动画的ARDemo,不得不说Three.js太强大了。那问题来了,既然代码都是用标准的JS写的,而且框架用的是开源的Three.js,那WebVRSDK做了什么?WebVRSDK是使用C/C++编写的<跨平台><极简><高性能>的<浏览器>内核,大小在400K以下,以WebGL为核心,支持WebGL、Canvas、Video、Audio、XMLHttpRequest、VRDisplay、Camera、ImageDecode等等,但不支持Dom,并且为Three.js/Pixi.js/Tiny.js等做了深度兼容。所以,前端开发者,只需要在PC浏览器开发、调试通过,就可以快速的移植到WebVRSDK平台。 千锤百炼才是精品 我对JS的理解还属于入门级别,目前写的Demo,基本都是临时学的,不要太当真。想做好WebAR/VR,还需要前端大牛的支持。如果有好的产品,欢迎移植&对接WebVRSDK。 下一回 添加个按钮,让AR可以和用户交互,但加个什么按钮呢?点击按钮之后,显示什么效果呢?敬请期待
紧接上文 WebVR+Three.js可以快速高效的搭建VR场景,降低了初学者的准入门槛,节省多平台重复开发,开始了H5在VR的新时代。 本次Demo所需二维码 用户可以本地启动HTTP服务,将URL填写到播放路径本次demo已经集成到APK中,用户可以点击《AR地球》来体验 AR地球 环境准备 Demo使用r82版本,向下兼容 Three.js 到http://www.threejs.org下载最新源码,这里使用three.min.js作为渲染引擎。 WebVR.js WebVR的工具库,用于切换VR状态(three.js的源码包中,自己拷贝) VREffect.js WebVR的展示库,用于分屏展示(three.js的源码包中,自己拷贝) VRControls.js WebVR的控制库,用于监控陀螺仪、Camera、重力等(three.js的源码包中,自己拷贝) 纹理准备 可以使用其他3D模型,为了方便起见,我们使用老的模型 地球纹理,在网上可以找到最新的地图纹理,最好使用4k,比较清晰 兼容性准备 常见的浏览器对摄像头的支持,良莠不齐,这里只是简单的支持了一种API标准 捕获音视频接口: Navigator.getUserMedia 点击进入: Mozilla官方使用文档使用方法: var params = { video: true, video: { width: 1280, height: 720 } }; window.navigator.getUserMedia(params, function(stream) { var video = document.querySelector('video'); video.src = window.URL.createObjectURL(stream); video.onloadedmetadata = function(e) { video.play(); }; }); 兼容性分析 Navigator.getUserMedia接口目前Safari是不支持的,常见的浏览器使用摄像头,往往通过Flash插件来完成。所以这些代码不一定能够成功运行在用户的浏览器上,但没有关系,我们的的WebVRSDK已经提供了支持,开发者可以放心的使用。Navigator.getUserMedia已经废弃了,使用MediaDevices.getUserMedia来取代。因为我对Promise原理不是很清楚,所以暂时只支持Navigator.getUserMedia。开发者需要提前申请App的Camera权限,Android6.0以后支持动态权限管理,开发者需要确保App拥有Camera的使用权限,最直接的的办法是在设置中手动的打开权限,否则背景会出现黑屏。 准备空白的html 我们会将所有JS代码写在单独的index.js中,因此index.html只是提供JS运行的环境,这一步为代码迁移到WebVRSDK做准备。代码如下: <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebGL Earth</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } </style> </head> <body> <script src="three.js"></script> <script src="WebVR.js"></script> <script src="VREffect.js"></script> <script src="VRControls.js"></script> <script src="index.js"></script> </body> </html> index.js核心代码 代码行数控制在40行以内 渲染器,使用WebGL渲染three.js中的3D对象 加载器,使用XMLHttpRequest或者Image,异步加载图片纹理 摄像机,模拟观察者的位置和方向,从不同角度来观看3D世界 控制器,接受用户点击、触摸、鼠标等事件,控制摄像机的参数,实现人机交互 材料,任何3D对象,是由骨骼和图像组成,材料就像油漆,粉刷过的对象更加真实 几何,3D对象的结构描述,这里只用球状几何 场景,用来管理所有需要展示的3D对象,以及他们之间的层级和关系 video标签,用于承载摄像头内容的呈现 代码如下 var w = window.innerWidth; var h = window.innerHeight; var renderer = new THREE.WebGLRenderer(); // 创建渲染器 renderer.setSize(w, h); // 设置渲染器为全屏 document.body.appendChild(renderer.domElement); // 将渲染器添加到body上 var camera = new THREE.PerspectiveCamera(45, w / h, 0.01, 100); var effect = new THREE.VREffect(renderer); // 控制器,用来控制VR渲染 var loader = new THREE.TextureLoader(); // 加载器,用于异步加载图片 var control = new THREE.VRControls(camera); // 控制器,用来控制摄像机 var mater = { map : new THREE.TextureLoader().load('earth_map.jpg') }; var earth = new THREE.Mesh(new THREE.SphereGeometry(20, 32, 32), new THREE.MeshBasicMaterial(mater)); earth.position.x = -100; var scene = new THREE.Scene(); // 创建场景 scene.add(earth); // 添加地球 scene.add(new THREE.AmbientLight(0xFFFFFF)); // 创建环境光 window.navigator.getUserMedia({ audio : true, video: { width: w, height: h }}, function(stream) { var video = document.createElement('video'); video.src = stream; video.play(); var image = new THREE.VideoTexture(video); image.generateMipmaps = false; image.format = THREE.RGBAFormat; image.maxFilter = THREE.NearestFilter; image.minFilter = THREE.NearestFilter; scene.background = image; // 背景视频纹理 }, null); animate(); function animate() { effect.requestAnimationFrame(animate); control.update(); earth.rotation.y += 0.002; effect.render(scene, camera); } 用浏览器打开index.html 在Safari上是无法打开的,因为navigator没有成员函数:getUserMedia开发者可以添加判断,因为WebVRSDK我添加了支持,所以就不判断了,代码如下: navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia; if (navigator.getUserMedia) { .... } 效果展示 悬浮的3D地球 畅想 AR距离我们很近 AR翻译过来是增强现实,简单一点说,就是在摄像头捕获的视频或者图片中,加入3D效果,并融入背景。就像二次元或者三次元一样,给用户带来更多的欢乐。 如果我们把3D地球换成一只可爱的猫咪,是不是就是淘宝双十一的抓猫猫游戏了? 下一回 我们把地球换成一个复杂的3D模型,并想办法让TA动起来,就像淘宝双十一的AR抓猫猫一样,会动会跳,还会发红包。
紧接上文 WebVR扩展了WebGL的标准,增加了HMD、PS等组件,让开发者可以在H5上开发VR程序。高级VR设备往往拥有了比手机更高精度的传感器、显示器、GPU等,让用户可以更加真实的感受虚拟世界。本节是《Html5的局》最后一节,我们手把手的写一段VR代码,感受WebVR的便捷。 本次Demo所需二维码 用户可以本地启动HTTP服务,将URL填写到播放路径 3D地球 环境准备 Three.js 到http://www.threejs.org下载最新源码,这里使用three.min.js作为渲染引擎。 TrackballControls.js,在three.js的源码里面可以找到,作为交互控制器。 纹理准备 地球纹理,在网上可以找到最新的地图纹理,最好使用4k,比较清晰 星空纹理,虽然可以使用粒子系统来模拟星空,简单起见,使用4k纹理图 准备空白的html 我们会将所有JS代码写在单独的index.js中,因此index.html只是提供JS运行的环境,这一步为代码迁移到WebVRSDK做准备。代码如下: <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebGL Earth</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } </style> </head> <body> <script src="three.min.js"></script> <script src="TrackballControls.js"></script> <script src="index.js"></script> </body> </html> index.js核心代码 渲染器,使用WebGL渲染three.js中的3D对象 加载器,使用XMLHttpRequest或者Image,异步加载图片纹理 摄像机,模拟观察者的位置和方向,从不同角度来观看3D世界 控制器,接受用户点击、触摸、鼠标等事件,控制摄像机的参数,实现人机交互 材料,任何3D对象,是由骨骼和图像组成,材料就像油漆,粉刷过的对象更加真实 几何,3D对象的结构描述,这里只用球状几何 场景,用来管理所有需要展示的3D对象,以及他们之间的层级和关系代码如下 var w = window.innerWidth; var h = window.innerHeight; var renderer = new THREE.WebGLRenderer(); // 创建渲染器 renderer.setSize(w, h); // 设置渲染器为全屏 document.body.appendChild(renderer.domElement); // 将渲染器添加到body上 var loader = new THREE.TextureLoader(); // 创建纹理加载器,用于异步加载图片 var camera = new THREE.PerspectiveCamera(45, w / h, 0.01, 1000);// 创建摄像机,表示人眼的方向 camera.position.x = 4; var controls = new THREE.TrackballControls(camera); // 创建控制器,用来控制摄像机 var earth_mate = { map : loader.load('earth_map.jpg'), side : THREE.FrontSide }; // 地球的表面,一张4096x2048的图片 var earth_mesh = new THREE.Mesh(new THREE.SphereGeometry(1.5, 32, 32), new THREE.MeshBasicMaterial(earth_mate));// 地球的模型,直径1.0,经纬各32切片 var stars_mate = { map : loader.load('stars_map.png'), side : THREE.BackSide }; // 星空的表面,一张4096x2048的图片 var stars_mesh = new THREE.Mesh(new THREE.SphereGeometry(100, 16, 16), new THREE.MeshBasicMaterial(stars_mate));// 星空的模型,直径100,经纬各32切片 var scene = new THREE.Scene(); // 创建场景 scene.add(earth_mesh); // 添加地球 scene.add(stars_mesh); // 添加星空 scene.add(new THREE.AmbientLight(0xFFFFFF));// 创建环境光 animate(); function animate() { requestAnimationFrame(animate); // 下一帧回调 earth_mesh.rotation.y += 0.002; // 绕Y轴旋转 controls.update(); // 控制器更新 renderer.render(scene, camera); // 刷新整个视图 } 用浏览器打开index.html 代码见附件:3D地球.zip一个3D地球已经出现在了浏览器页面上。我们可以双指上下滑动,来缩放地球可以点击拖动,改变摄像机的视角在手机的WebVRSDK上,可以使用三指滑动,改变摄像机的位置 从Native到H5 代码量进一步减小 核心代码仅有30行,我们就实现了3D地球,只是缩放、旋转、平移。 无需编译,所见即所得,只要能够编辑文本,就可以写代码 所见即所得,浏览器页面直接刷新,妈妈再也不用担心打包慢的问题了(C++编译尤其慢) 运行时调试,程序员都懂得。 门槛进一步降低 阅读three.js后,发现3D引擎的原理也挺简单的,以前只要提到3D游戏引擎,感觉很高深的样子。 分工更加灵活,框架可以自由切换,类似的3D框架在上一篇文章中已经列举,有兴趣的同学可以学习 从3D到VR 传感器由屏幕转交给HMD 屏幕、鼠标等传统人机交互,已经无法满足VR设备的输入,HMD会提供更加人性化的输入,但缺少了键盘,VR的I/O也成了难事。 定时器由VREffect触发 VR对于视频帧的刷新精度,要求更高,不同的WebVR标准表现不一样,可以简单的认为effect.requestAnimationFrame等同于window.requestAnimationFrame 3D地球升级为VR地球 只需要修改几行代码,就可以轻松的打开VR模式 环境准备 VREffect.js,检查&兼容WebVR的运行环境,控制VR模式的开启与否 VRControls.js,监听HMD返回的信息,并及时的操控camera来调整视角 WebVR.js,人性化的展示WebVR的信息和按钮 修改代码 切换为VR控制器 var controls = new THREE.TrackballControls(camera); 改为: var controls = new THREE.VRControls(camera); 切换帧管理器 requestAnimationFrame(animate); effect.requestAnimationFrame(animate); 增加VR特效器 // 将renderer的任务,封装为VREffect,由VR来控制双屏显示 var effect = new THREE.VREffect(renderer); 使用VR特效器渲染场景 effect.render(scene, camera); 增加VR检测 if (WEBVR.isAvailable() === true) { document.body.appendChild(WEBVR.getButton(effect)); setTimeout(function() { effect.requestPresent(); // 请求双屏显示 }, 1); } VR效果展示 由于附件只能上传一个,只放置了3D地球的源码,VR地球的源码和3D地球,除了index.js不一样,其他都一样,在线地址如下,自己下载一看即可。这里放出二维码和在线链接地址,由于部分浏览器是没有兼容WebGL和WebVR,看到的效果可能不一致。手机上,已经集成到最新的WebVRSDK中,用手机安装即可看到。 3D地球 浏览器点击: 3D地球 VR地球 浏览器点击: VR地球 如何使用WebVRSDK WebVRSDK不支持Dom WebVRSDK核心只有400k,集成了JS引擎、WebGL、WebVR、WebVideo、WebAudio、图像处理、网络处理等,如果参加Dom的支持,那和浏览器还是有什么差异? WebVR如何加载已开发的H5工程 将核心的index.js所在的URL拷贝到APP的输入框,点击打开即可 写法上有何差异 由于不支持Dom,所以加载JS的任务需要写在JS中,目前有两种写法可以支持,二者都是同步方式加载,不支持异步。 使用内置API加载外部JS资源 window.loadFile = window.__execute || function() {}; // WebVRSDK不支持Dom,所以需要自定义函数,加载JS资源。浏览器则不需要。 window.loadFile('three.js'); // 加载当前目录的three.js window.loadFile('promise.js'); // 加载当前目录的promise.js window.loadFile('RollerCoaster.js'); // 加载当前目录的RollerCoaster.js window.loadFile('WebVR.js'); // 加载当前目录的WebVR.js window.loadFile('VREffect.js'); // 加载当前目录的VREffect.js window.loadFile('VRControls.js'); // 加载当前目录的VRControls.js 使用标准JS加载JS资源 var script = document.createElement("label"); script.src = 'http://xxxx.js';// 绝对路径 // 或者 script.src = 'xxx.js';//相对路径 WebVRSDK的目标 性能的极致 现在主流的手机,都是多核心CPU、支持大型3D游戏的GPU。如果要把硬件性能发挥到极致,我认为使用C/C++,或者汇编,往往能够最大化的“榨干”硬件,但这是不现实的,因为开发效率和维护成本太高了。 所以,WebVRSDK为了追求性能的机制,底层完全使用C/C++编写,直接封装硬件最底层的API,并暴露给JS接口,实现H5的标准。 以上Demo都可以在浏览器上开发、调试、运行,运行性能可以对比IE、Chrome、Safari、FireFox,红米3也可以达到60FPS的帧率 灵活的开发方式 WebVRSDK提供的只是运行环境,开发、调试完全可以在PC的浏览器上完成,开发规则除DOM不支持外,完全遵守WebGL、WebVR、WebVideo等标准 2.不挑剔渲染引擎支持2D的pixi.js支持3D的three.js甚至自定义的JS引擎。。。 WebVRSDK的未来 提供底层API运行时支持 经过数据测试(简单测试,每种开发语言都有自的优缺点,勿喷)JS运行速度对比Java,慢10倍,甚至更多;Java运行速度对比C/C++,慢5~10倍 JS语言作为技术底层,如何提高性能呢?我们大胆的提出假设,JS在大规模数据计算时的性能不足,是否可以由C/C++来弥补呢?比如,矩阵的变换和算法,针对目前向量、矩阵、四元数、欧拉角等,数学算法已经很固定,我们是否可以大胆的提出"WebTransform"标准,将这些常用的算法,用C/C++编写后,暴漏给JS,让JS只要传数据就可以瞬间获取结果? 开放平台 我们已经走到了WebVR技术的前沿,目前主流的浏览器,特别是在移动端能够支持的,还没有正式支持,即使已经支持的浏览器,性能也比较差。这点,是WebVRSDK的优势。WebVR的发展,背后是IT巨头的博弈,毕竟谁也不愿意在同一个维度去竞争,这是时代给我们的机遇和挑战,目前VR购物、看片、游戏等场景不断增多,硬件发展又无法跟上的时候,我们技术人员是否可以做点什么呢? 下一回 Html5的局,到此结束,我也该整理整理思路。
紧接上文 WebVR使用跨平台H5技术,降低了开发者的技术门槛,提升了开发效率,我们来做个VR的Demo如何?本周小米发布了VR头盔,头盔还自带芯片,那又是什么呢? 本次Demo二维码 依旧800k,这次可以支持URL在线播放WebVR的程序了。 WebGL+WebVR+Three.js JS常见的3D引擎框架 Three.js 浏览器常见的3D动画引擎,居于WebGL的渲染技术,整合了常见的精灵、纹理、异步加载、动画、场景、骨骼等等,无需安装IDE,直接在浏览器上开发即可,目前网上已经有许多在线IDE,支持骨骼、地图、场景等绘制。同时,代码开源,易于定制,是前端同学学习成长的好榜样。 cocos-js cocos和u3d是常见的手机游戏引擎,双方都出了3D和VR版本。cocos还支持H5运行模式,开发的游戏可以支持在H5上运行,同时也可以运行在自己的内核中。虽然cocos-js的性能已经有了质的提升,不过Native的运行环境并不是基于WebGL,而是基于自己的cocos内核,所以只能使用自己的cocosjs,并不支持第三方的动画引擎。这对于前端同学来说,可能丧失了自己的技术成长空间。 其他引擎 Construct 2 (收费)PlayCanvasTurbulenzvoxel.js有喜欢的同学,可以研究一下 WebVR基础 随着移动互联网的快速发展,H5的标准在智能硬件领域的不足,逐步明显。WebVR出现,主要是弥补了头戴设备和定位设备的标准,分工了3种设备的关系。如下图所示: head mounted display(HMD) 我翻译为:头戴显示器,HMD用来模拟虚拟世界的参与者HMD在VR概念里,用来取代了显示器,同时把陀螺仪、重力感应、加速度、海拔等信息实时的同步给引擎。参考最新发布的小米VR眼镜。049元款,是基础版本,官方说是《玩具版》,只是一个玩具199元款,是高配版本,官方说是:内置独立运动传感器,硬件级加速抗眩晕。但小米不是完整的HMD,因为TA缺少了Display。玩具版之所以称为之玩具,可以对比一下HMD的概念,只有两个放大镜。。 Position Sensor(PS) 我翻译为:场景位置定位仪,用于模拟虚拟世界的位置信息,包括高度等PS在VR的概念里,用来取代我们GPS等定位设备,TA的精度要求非常高,不能让人感受到不真实,自己设想一下精度需要多高。参考HTC VIVE发布的VR眼镜不仅包含VR眼镜、主机,还包括了2个支架,对的,那两个支架在WebVR的概念里,就是PSHTC VIVI的VR眼镜,才是WebVR里面的HMD,他包含了显示器! 主机 这点相信很多人都忽略了,主机才是真正的渲染和业务处理的核心。好一点的,用于VR的显卡,基本都是万元级别的,想想都是醉了。 距离我们最近的VR CardBoard VR纸盒眼镜,提供两个镜片做屏幕到人眼的光线转换。比如小米的玩具VR眼镜 手机 手机的显示器,可以作为VRDisplay手机的处理器,可以作为VRComputer手机的传感器,可以作为HeadMounted手机的定位仪,不可以作为PS,因为她的精度最好的情况下,也只有10cm,这样的设备容易让参与者感受到明显的眩晕感。 VR内容的现状 只适合做位置保持不变的场景,或者用户的位置按照既定义的轨迹和速度,做有规律的变化。参考Demo中的《过山车》和视频类的《VR视频》,也就是固定轨迹和规定坐标的两种内容方式。 从WebVR看技术和硬件发展的方向 3D建模工具化 对显示世界的物体,需要低成本、批量的、计算机化,快速生成3D模型,把现实世界的内容,快四的搬到虚拟世界,这点很像当年PC兴起时的过程,把线下的商品交易,搬到线上 手机传感器的精度化 手机可以作为HMD和PS,只要精度到达一定程度(这个标准可能是将来决定手机成败的关键),手机可以一统HMD、PS,成为VR核心。 手机CPU、GPU性能大幅提升 手机取代PC的主机,成为业务中心。CPU、GPU将会朝着更加节能省电、超级性能怪兽方向前进,PC市场进一步消失 商业分为线下、线上、虚拟三大流量入口 VR可以降低人们的出行成本,如果体验可以很好的话,可以通过VR“进入”旅游目的地,感受当地的风情;可以实现电影院效果,自己成为电影中的一个角色可以模拟实战演习,减少不必要的消耗和伤害可以帮助受伤患者,帮助健康恢复。。。。 下集再见 接下来,我们会手把手的写一个3D小程序,无需安装任何IDE,直接在浏览器上开发&调试,感受WebGL时代的便捷。
紧接上文 在阅读WebKit源码中,讨论了Canvas在iOS平台使用的CoreGraphics框架作为渲染的工具,它运行在CPU上。WebGL是直接运行在GPU上的API,因此优化空间更大,对程序员要求更高。这次我们看看,WebGL如何对格式转换的,为我们后续three.js导入数据模型做铺设。 常见的纹理格式 OpenGL ES2.0在多终端的差异 在WebKit中,默认支持纹理格式,主要有: 现实却是很残酷,iOS设备以上格式都是支持的,Android设备差异化就不同了,简单的说,要想确认Android是否支持某种纹理,只需要在glext.h文件中查找宏定义即可.比如,在Android的glext.h中,却无此定义,而在iOS的OpenGL ES2.0库中,我们可以找到 #define GL_RGB16F_EXT 0x881B OpenGL ES2.0中纹理格式的扩展 区别于上图的主流的纹理格式,在OpenGL ES2.0中存在大量的扩展功能 Android独有的 Android作为通用平台为不同厂家的硬件做了支持,比如AMD系列有以下扩展 /*------------------------------------------------------------------------* * AMD extension tokens *------------------------------------------------------------------------*/ /* GL_AMD_compressed_3DC_texture */ #ifndef GL_AMD_compressed_3DC_texture #define GL_3DC_X_AMD 0x87F9 #define GL_3DC_XY_AMD 0x87FA #endif /* GL_AMD_compressed_ATC_texture */ #ifndef GL_AMD_compressed_ATC_texture #define GL_ATC_RGB_AMD 0x8C92 #define GL_ATC_RGBA_EXPLICIT_ALPHA_AMD 0x8C93 #define GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD 0x87EE #endif ······ iOS独有的 苹果为苹果自家的平台做了大量的高级扩展 /*------------------------------------------------------------------------* * APPLE extension tokens *------------------------------------------------------------------------*/ #if GL_APPLE_color_buffer_packed_float #define GL_R11F_G11F_B10F_APPLE 0x8C3A #define GL_RGB9_E5_APPLE 0x8C3D #endif #if GL_APPLE_clip_distance #define GL_CLIP_DISTANCE0_APPLE 0x3000 #define GL_CLIP_DISTANCE1_APPLE 0x3001 #define GL_CLIP_DISTANCE2_APPLE 0x3002 #define GL_CLIP_DISTANCE3_APPLE 0x3003 #define GL_CLIP_DISTANCE4_APPLE 0x3004 #define GL_CLIP_DISTANCE5_APPLE 0x3005 #define GL_CLIP_DISTANCE6_APPLE 0x3006 #define GL_CLIP_DISTANCE7_APPLE 0x3007 #define GL_MAX_CLIP_DISTANCES_APPLE 0x0D32 #endif ······ Android与iOS都有,命名不同的 Android中定义为: #define GL_BGRA_EXT 0x80E1 iOS中的定义: #define GL_BGRA 0x80E1 WebGL如何屏蔽底层硬件的差异 WebKit作为WebGL标准实现的程序,首先就要面临的是PC/Android/iOS/Linux等多平台,在硬件兼容上的差异。既然底层硬件无法统一,必然在标准的实现上就会有取舍。我们先看看WebGL对应OpenGL ES2.0硬件支持的定义. 使用方式相似,数值是一样的 【类型变化了,先挖个坑,后续我们在讨论】Native采用宏定义: #define GL_RGBA 0x1908 glActiveTexture(tex); typeof tex is unsigned int JS使用成员变量: var gl = canvas.getContext('webgl'); alert(gl.RGBA); gl.activeTexture(tex); typeof tex is WebGLTexture! 继承与兼容 OpenGL ES2.0的头文件有两个:一个是gl.h,一个是glext.h。为什么有两个,他们之间的关系是什么呢?从表面上看,一个基本的功能包,一个扩展功能,就像打魔兽一样,等到程序员在不断的打怪升级,经验提升到6级以后,就可以开启大招了。每个英雄就像我们的选择的GPU,基本属性都有力量、敏捷、智力、攻击力,这些功能都是大同小异的,可以完成日常的开发和bug修复。各个英雄都有自己独特的技能,就像上面我们提到的Android与iOS的差异。 gl.h是通用的 gl.h中的定义与函数,在WebGL中都可以找到,用法完全相同【暂时这么讲,先挖坑】。 定义参数 300+ 定义函数 150+ 都要背会哦 glext.h是各个平台的扩展 这个头文件里的内容,大不相同,为了满足部分高级程序员更高的渲染特效、性能优化,采用了各个平台的支持,比如:深度测试、蒙版测试、多种纹理格式、多线程支持、离屏渲染、管道操作、纹理压缩、视频流等等,包罗万象。这里告诉大家一个好消息和一个坏消息。好消息是: WebGL对这个文件的大部分API都不支持~~ 坏消息是: 该支持的API,还是要支持滴~~ 兼容glext.h 实在无法统一的API,WebGL直接屏蔽掉,主要考虑两个问题: WebGL的目标是跨平台,兼容就要有牺牲 有些功能不常用,也不是行业标注 第一步,就是统一多平台的参数命名: #define GL_DEPTH_STENCIL 0x84F9 #define GL_DEPTH_STENCIL_ATTACHMENT 0x821A ······ 第二步,增加平台无关的自定义参数 #define GL_UNPACK_FLIP_Y_WEBGL 0x9240 #define GL_UNPACK_PREMULTIPLY_ALPHA_WEBGL 0x9241 #define GL_CONTEXT_LOST_WEBGL 0x9242 #define GL_UNPACK_COLORSPACE_CONVERSION_WEBGL 0x9243 #define GL_BROWSER_DEFAULT_WEBGL 0x9244 第三步,采用C/C++补全硬件平台的缺陷例如,常见纹理格式的转换,在Android平台增加: #define GL_RGB16F_EXT 0x881B #define GL_RGBA16F_EXT 0x881A #define GL_RGB32F_EXT 0x8815 #define GL_RGBA32F_EXT 0x8814 #define GL_BGRA GL_BGRA_EXT #define GL_SRGB_EXT 0x8C40 #define GL_SRGB_ALPHA_EXT 0x8C42 #define GL_ALPHA16F_EXT 0x881C #define GL_LUMINANCE16F_EXT 0x881E #define GL_LUMINANCE_ALPHA32F_EXT 0x8819 #define GL_ALPHA32F_EXT 0x8816 #define GL_LUMINANCE32F_EXT 0x8818 #define GL_LUMINANCE_ALPHA16F_EXT 0x881F ······ 下回再见 下一章,我们继续今天的话题。 看了这么多代码,也没明白讲什么~~我也不知道该怎么讲,这可能就是OpenGL的难懂的原因吧。众所周知,WebGL是基于OpenGL ES2.0版本的标准,而OpenGL ES2.0只是OpenGL的子集。目前所有的Mobile设备通常采用OpenGL ES2.0的硬件,以达到成本、节能、性能最大化。但,如果是在PC上呢?PC拥有强大的GPU和完整的OpenGL能力,要自废武功来实现这个憋足的OpenGL ES2.0嘛。
紧接上文 WebGL是一套跨平台的渲染技术,向上提供统一的标准API,向下屏蔽了硬件厂商的差异,最大化的降低OpenGL的移植成本,同时也失去了一些特色的高级GL功能。先看看常用的纹理数据的变化。 WebGL的标准的落地与否 标准不代表WebKit支持 就像JavaScript的E6标准不是所有浏览器都支持一样,WebGL的很多标准在不同的平台不一定支持,比如Texture的GL_BGR格式,在PC的WebKit上是支持的,在移动端的WebKit上就不一定支持,因为这一特性属于OpenGL ,但不是OpenGL ES2.0的标准。查看OpenGL的gl.h文件,我们可以找到: /* bgra */ #define GL_BGR 0x80E0 #define GL_BGRA 0x80E1 但在OpenGL ES2.0的gl.h却没有这个属性的定义,反而是在扩展文件glext.h找到了不完整的定义: #if GL_APPLE_texture_format_BGRA8888 #define GL_BGRA_EXT 0x80E1 #endif 不仅属性名称发生了变化,是否支持也成了硬件的可选条件。 bold 结论:标准是扩平台的,硬件是有差异的,标准不一定会实现。 标准在硬件上的差异 WebGL只是将OpenGL在不同硬件平台做了一套,基于OpenGL和OpenGL ES2.0的统一的规范,OpenGL 与OpenGL ES2.0在标准和用法上存在差异,而ES2.0是GL的子集。那么同一个API必然在某些场景下的用法,会存在差异。比如,我们最常用的glTexImage2D在OpenGL的定义为: void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels); 在GL中,internalformat和format参数可以不一致;level可以是正整数;pixels可以为空;但在OpenGL ES2.0的规范中,API的定义可谓是完全相同: void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels); 可是真正使用起来,参数解释就不同了,比如:在ES2.0中,internalformat和format必须相同;level必须是0;pixels在某些平台为了安全不能为空。bold 结论因为GL是跨平台的标准,不可能只采用ES2.0的规范,每当遇到GL与ES2.0的边缘场景,事情就会变得复杂化,再加上不同的硬件的支持不完全一致,这个时候,怎么办呢? 标准在软件上的差异 纵观网络的技术博客,我们仍然以glTexImage2D为例,关于internaformat与format参数是否一致的情况,网上出现了两种结论: 1. 以实际主义为事实的说法:可以不一致: http://blog.csdn.net/csxiaoshui/article/details/27543615提到: internalFormat:指定OpenGL是如何管理纹理单元中数据格式的。网络上很多解释说这个参数必须和后面的format参数一样,这个说法是不正确的 2. 各大厂商的标准文档:必须保持一致 https://msdn.microsoft.com/zh-cn/library/dn302435(v=vs.85).aspx提到: format [in] Type: Number Contains the format for the source pixel data. Must match internalformat (see above). 这其实是OpenGL与WebGL的差异,也就是OpenGL的开发者转到WebGL开发,也是需要改变很多的,那么各大浏览器厂商在实现上,采用哪种说法呢? 3. WebKit VS FireFox:各自为政 对于internalformat与format,WebKit是直接拿来使用,不做强制约束: texImage2DBase(target, level, internalformat, width, height, border, format, type, data, ec); 而FireFox是严格的遵守WebGl标准,做了严格的判断: if (format != internalformat) return ErrorInvalidOperation("texImage2D: format does not match internalformat"); 总结:WebGL只是H5领域的一个标准, 无法做到对OpenGLES2.0完美的兼容,甚至有些标准属于Web独有的。熟悉WebGL的标准对于前端开发是很有帮助的,同事Native与H5的代码移植,技术挑战很大。 下回再见 WebGL弱化了OpenGL与硬件的差异,形式上统一了GL在前端的用法。WebGL的路上,既然有凸起,那么一定有凹坑,WebGL标准通过舍去一些高级特性达到统一的目的,那么就会有弥补凹坑,把一些硬件不支持的特性补充完整。下一回,我们谈谈如何使用CPU来弥补WebGL的不足。
紧接上文 WebGL作为通用的标准,屏蔽了一些硬件厂商的高级特性,有舍就会有取,那么在一些比较常用的功能上,如果硬件无法满足通用,那么就会通过CPU来弥补硬件的不足。图片的纹理格式,就是其中的一个点。 常见的纹理格式 我们引用12章:WebGL跨平台的取与舍的图片:这些在OpenGL平台都是支持的,但在WebGL中做了裁剪。 裁剪了纹理格式 这些格式在FireFox中严格的不支持,但在WebKit中,WebKit虽然在代码上支持了这些格式,最终是否支持,看各个平台的硬件特性了。 裁剪了纹理的类型 没想到纹理还有浮点数格式。。 仅支持以下格式和类型 上图列出的是主要支持的纹理格式和类型,其他类型也可能支持,最好不要使用。 纹理的扩展格式 在WebGL的使用中,我们会经常使用以下三种纹理参数: UNPACK_FLIP_Y_WEBGL 众所周知,OpenGL的原点坐标在屏幕的左下角,X轴向右,Y轴向上,Z轴向外。在OS的世界中,坐标原点在屏幕左上角,X轴向右,Y轴向下,Z轴向外。这样导致我们使用一张图片的时候,默认是从左上角为bitmap的起始点。这样到了OpenGL的世界,就变成了上下颠倒的图片,本质就是Y轴翻转导致。 UNPACK_PREMULTIPLY_ALPHA_WEBGL 为了提供OpenGL的性能,我们在CPU中提前把颜色的alpha乘以RGB,OpenGL在显示时,无需每次实时技术RGB颜色通道,进一步提升GPU性能。 UNPACK_ALIGNMENT 纹理像素的对齐参数,比如上图中我们提到GL_RGB是3个字节的长度,如果图片是3x3大小,那么数据在内存中,以GL_unsigned_byte为单位存储,大小为:9*9。在OpenGLES1.0的时代,纹理大小只能是2的整数倍,这样的图片在OpenGL中是无法识别的。就要做转换: GL_RGBA:转换后,每个像素成为4个字节 GL_RGB:不转换,图片的每行,我们增加一个byte。在增加一个空白行,成为10x10的图片 ALIGNMENT:在OpenGL ES2.0以后可以设置UNPACK_ALIGNMENT,它表示像素对齐最小公因子,我们设置为1,告诉GPU只要是1的整数倍即可。(这点会影响GPU的性能,因为GPU也像CPU一样,喜欢字节对齐的数据) 总结 看完了WebGL的纹理格式,就会发现,很多已经成熟的技术制作的3D模型,如果格式或者类型不匹配,就会导致WebGL出现兼容性问题,无法显示图片,甚至会出现崩溃。遇到问题的同学,请牢记以上信息。 下一回 看完了这些,感觉很简单的样子。仔细看看:UNPACK_FLIP_Y_WEBGLUNPACK_PREMULTIPLY_ALPHA_WEBGL是不是末尾都添加了WebGL得标志?对的,这些不是OpenGL的功能,是CPU帮忙我们实现的,如果同学们直接使用OpenGL开发,那么就有可能在WebGL上不兼容。另外,还记得上一节中我们提到的吗: void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels); internalformat和format到底存在什么关系?如果他们真的必须保持一致,那么这个接口只需要写一个参数就好了,何必要预留两个入参呢?难道程序员脑袋秀逗了? 想要弄清楚,请持续关注下一章,我们一起看看CPU是如何做格式转换的。
紧接上文 WebKit为了统一WebGL的书写规范,对OpenGL的标准进行四书五入,推出了平台无关的API标准,同时为了简化底层硬件的差异,又新增了一些纹理格式的支持,由内核提供高性能的图像转换,扩展了OpenGL得标准。那么,WebGL在底层做了些什么呢?复杂吗?可以自己实现吗? 纹理格式转换计算量大 这是WebGL为前端同学提供的福利,上层开发可以更加专注业务书写,充分挖掘C/C++语言的能力。 UNPACK_FLIP_Y_WEBGL 重新生成一张内存图片,将纹理像素上下颠倒的复制到新图片。图片在内存的大小,与图片格式没有直接关系,主要取决于尺寸。以1024x1024的png为例,有R、G、B、A四个通道,每个像素占4字节,总内存:1024 x 1024 x 4 = 4M空间。新图片上传到GPU后自动释放,不会影响到老图片的生命周期。UNPACK_FLIP_Y_WEBGL的取值:true/false UNPACK_PREMULTIPLY_ALPHA_WEBGL 对图片纹理的每个像素的R、G、B通道,乘以A的值后,并替换原先的值。我们以一张白色图片,0.5的透明度为例。每个像素的存储格式为:R、G、B、A分别为unsigned char类型,俗称uint8_t,预算过程中自动进行浮点数转换,舍去末尾的精度:R = R * A / 255.0f;G = G * A / 255.0f;B = B * A / 255.0f;A = A; 每个像素都要算一次,1024 x 1024 大小的图片,计算量为1024 x 1024 x 10 约10M次。对于C/C++来说也比较耗时,如果放到JS中处理,后果不堪设想。这也许是WebGL设计的初衷。 UNPACK_ALIGNMENT 上面我们提到的图片格式为RGBA,转换起来虽然计算量打,但算法比较直观。换成其他格式就不是这么直观了,比如:RGB、RGB565等等,不仅需要按byte进行转换,每个uint8_t都需要掰开使用。以RGB为例,每个像素占用3个字节,考虑到GPU只能接受2的整数倍的尺寸。所以CPU需要为前端同学做格式转换。这里我们定义Stride,并计算相应的像素对齐后的尺寸。 stride = (imageWidth * bytesPerPixel + unpackAlignment - 1) / unpackAlignment; 如果unpackAlignment = 1,那么新的图片尺寸是不变的,否则,新的图片尺寸就需要CPU处理做像素对齐,我们这里讨论特殊情况,就是 stride != width的时候。 从RGB到RGB FireFox的做法比较简单,申请新的图片空间: GLvoid* pixels = malloc(stride * height); 将每个像素从GL_RGB转换成GL_RGBA,保存中间pixel变量,再赋值到新的图片中: ······ GLubyte texel[4]; convert_rgb_rgba(image[i][j], texel]; conver_rgba_rgb(texel, pixels[i][j]); ······ 问题来了,既然gl.texImage2D函数的internalFormat与format的格式必须是保持一致的,为什么不把函数直接写为: memcpy(image, pixels, width * height); 理由就是为了做兼容,对比WebKit代码,我们可以找到FireFox这样做的原因。 WebKit做法复杂,但耐人寻味 申请新的图片内存空间: GLuint bytesPerPiexl = formatSize(internalFormat, type); GLvoid* pixels = malloc(width*height * bytesPerPiexl); 我们会发现,WebKit并不会按照stride来申请空间,也不会参考unpackAlignment,它使用了internalFormat和type来确定OpenGL中的纹理格式和空间结构。这两个参数可以确定每个像素的bytes,参考14. Html5的局:WebGL的纹理格式提到的常见纹理格式。 到了转换这一步,WebKit采用函数模板为路由的方式转发到对应的转换API: template<srcForamt, dstFormat, SrcType, DstType> void convert(····) { switch(srcForamt) { case GL_RGB: { switch(dstFormat) { case GL_RGB: { unpack_RGB_pack_RGB(image, pixels); break; } case GL_RGBA: { unpack_RGB_pack_RGBA(image, pixiels); break; } .... } break; } ....... } } 这里WebKit做了大量的路由功能,提高了转换效率,但是代码复杂度很高。比如:我们已知的常用纹理的Format为: GL_RGB, GL_RGBA, GL_ALPHA, GL_LUMINANCE, GL_LUMINANCE_ALPHA WebKit还支持了很多其他的Format: GL_RGB16F, GL_RGBA16F... GL_RGB32F, GL_RGBA32F... 下一篇 这篇内容太多了,我们放到下一篇继续讲,纹理格式转换。
紧接上文 WebGL+WebVR可以轻松打造出一款VR游戏,而且整个游戏代码竟然可以精简到100k以内,加上JS及时编译和调试的特性,打造一款简单的3D场景,要比U3D这种Native方式廉价、高效很多。U3D作为传统的Native引擎,包体动辄几十M,使用C/C++、C#等编译语言,效率明显不如JS+Chrome,不过好处十分明显,运行性能良好。 本次Demo二维码: 依旧800k的APK,包含VR游戏与VR视频,进一步完善了WebVR的兼容性: 使用WebGL+WebVR做VR视频 常见的VR视频有哪些 以下截图来自网络 固定视角的VR分屏视频 用户只能从摄影师提供的固定角度观看视频,视觉范围比较小,本质上是位于不同摄像头的两种视频的合并。常见的比如电影院的3D视频,通过3D/VR眼镜偏光使左右眼看到不同影响,给人以立体感。 单一视角的VR全景视频 摄影师通过全景相机拍摄的360°全景视频,视频通常是固定的长宽2:1的比例(完美比例应该是3.1415926...),用户用VRPlayer播放视频时,将视频的每一帧生成一个球形纹理,用户的视角在球心的位置,通过重力感应、陀螺仪等切换不同的视角。这种情况下,如果强行切换成VR分屏模式观看,左右视角播放的视频内容本质上是一样的,颜色没有井深,呈现的3D立体感较弱 双眼视角的VR全景视频 摄影师通过双摄像头拍摄的360°全景视频,视频原始数据相当于2个1.1.2中的视频,但视频呈现模式不同场景区别很大,由于标准尚未统一的缘故,常见的有两种 谷歌Cardboard提供的Demo中1:1的比例内容 本质上是两个2:1的图像合并成一个正方形的图像,原始图像未压缩也不失真,效果清晰但文件偏大。VRPlayer播放时,使用OpenGL生成两个球形纹理、两个摄像机,分别模拟人的左右眼。第一个球形纹理使用上半部分图像,第二个使用下半部分图像,两个全景效果。 Three.js上提供的Demo中2:1的比例内容 本质上也是两个2:1的图像合并成的一个2:1的图像,为了降低文件的大小,先将两个视频源从2:1压缩到1:1,在左右拼接,合并成2:1的图像。VRPlayer播放时,用两个1:1的图像生成两个球形纹理,理想比例是3.1415926..,看官也该知道了,视频被拉伸了,图像质量也降低了,但文件减小了一倍。 VR视频要求有多高? 手机屏幕的要求 手机屏幕经过VR眼镜之后,手机屏幕距离人眼的距离更近了,图像被放大了2倍以上,像素感明显。目前主流的视网膜屏幕,通常是眼镜看手持设备屏幕时,看不出像素感。VR行业的屏幕标准尚未统一,如果哪一天VR真的进入商业化,对屏幕的要求更高,目前iPhone等手机,作为VR屏幕,只能说是入门级 视频清晰度的要求 这里我们只说VR全景双屏视频,测试设备:我的魅蓝Note31080x540, 没法看,60FPS以上2160x1080,有点模糊,Demo中的视频杨洋/热巴故事线,43FPS以上4096x2048,可以看看,Demo中的视频,其余的是4k视频 14FPS以上 带宽的要求 4k视频,带宽建议50M以上吧 总结 全景双屏VR视频 这只是一个起步阶段,目前主流手机像素感明显,为了达到最佳性价比,最好找个这样的手机:视网膜屏或者更清晰能够支持重力、陀螺仪能够支持4K硬解码的解码器能够支持4K大小纹理的GPU 大概是这样子的Android: 5.0以后的iPhone: 5s以后的 固定视角双屏VR视频 这个比较简单,1080P已经很不错了不需要支持重力、陀螺仪,只要屏幕清晰即可。
(一)WebApp时代,追求App开发效率的同时,我们也要求终端的体验和性能,2/8原则可以很好的阐述当前的hybird开发方式:20%的Native代码+80%的H5代码。(二)H5可以发挥的性能极致是什么样子?了解这个问题,就需要知道H5的技术定位:一套H5代码支持Android、iOS、PC等多平台的前端语言,这就决定了再好的iOS平台性能,也不能忘记Android平台的用户体验;最终开发者选择了降低用户体验情况下,降低了H5的用户体验。(三)H5可以做的更好,不管是Android还是iOS!这个时候,cocos2d-js出现了,大家有兴趣的可以参考一下官网的demo《js-moonwarriors》,在多精灵动画和复杂的游戏场景下,Android平台也可以达到60FPS的效果!是不是很劲爆?(四)回归主题,引用官方的说明:Cocos2d-JS是Cocos2d-x的JavaScript版本,融合了Cocos2d-HTML5和Cocos2d-x JavaScript Bindings(JSB)。 (五)Cocos2d-HTML5Html5给我们带啦丰富且强大的API,让前端设计者开发出绚丽的显示效果,我们这里主要分析:Canvas、WebGL Canvas传统的网页游戏和动画展示,很大程度上依赖了H5中得Canavs模块,Canvas模块为JS提供了直接操作底层GPU的机会。示例: Context本质上是一个OpenGL的上下文管理对象,封装了常见的OpengGL操作,包括点、线、多边形图形库,还包括了图像、矩阵等功能。经过主流的一些游戏框架的封装,可以基本满足2D游戏的功能,例如:pixi、phaser、kissy等框架,再加上一些物理引擎,比如box2d等,就可以实现复杂的游戏场景。【阿里GCanvas引擎,就是基于Canvas的性能优化,后续讲解】 2.WebGL提供了相对OpenGL ES 2.0完整的API,可以说是一一对应,相比CanvasAPI,WebGL可以说就是简单的把openGLES的API映射给了JS,从此JS拥有了像C/C++一样的GPU操作能力。我们知道,移动平台几乎所有的图像框架的基础都是基于OpenGL,WebGL极大的降低了JS操作GPU的路径,省去了中间环节,当然JS的性能远不能媲美C/C++,但是如果js的运行速度可以媲美java,是否可以写出像Android一样高性能的页面呢?【当然可以,先挖个坑】 (六)Cocos2d-JSB【JS与Native的粘合剂】触控的JSB即:JavaScript Binding Cocos2d-x To SpiderMonkey,将高性能的Cocos2d-x直接集成到一个JsVM中,将C/C++代码直接预置成JS的内部函数或者对象,把JS不擅长的浮点数计算和矩阵转换,交给C/C++技术,JS制作数据的中转和业务的处理,为游戏引擎的大规模运算提供基础。(七)SpiderMonkey【JS运行引擎】SpiderMonkey是由C语言操作的JavaScript引擎,它支持JS1.4和ECMAScript-262规范。该引擎分析、编译和执行脚本,根据JS数据类型和对象的需要进行内存分配及释放操作。利用该引擎可以让你的应用程序具有解释JavaScript脚本的能力,目前已有若干个项目都采用了SpiderMonkey引擎,像K-3D、WebCrossing、WebMerger等。下图是Cocos2d-JS内部集成的第三方引擎库: 在这里不得不提一下为什么选择SpiderMonkey而不是V8、JavaScriptCore?【请持续关注】 (八)Cocos2d-x【图形引擎】我们的操作系统,本质上就是一个图形引擎,cocos2d我这里就不做讲解了,毕竟咱们是一家做电商的公司,做好移动app,远离游戏。 (十)Cocos2d-JS是搅动H5世界的鲶鱼请看下一章节分享。 (总结)WebGL是助推器,可能不是最终的目标上面我们提到,H5提供了WebGL功能,让JS直接调用OpenGL,绕过了复杂的浏览器沙箱,让性能不如C/C++的JS语言,直接操作核心绘图GPU,实现了性能的极大的释放,极大的推动了H5的发展,可以说,如果Css3是Dom新生的基础,而WebGL是H5发展火箭助推器,我们看到的Native绝大多数的绚丽效果,可以用WebGL轻松搞定。 即使WebGL提供了高效的GPU支持,然而,在图形世界里,大规模浮点数计算、矩阵转换、图像处理等,并不是JS的优势,随着H5的发展,JS在局部的性能处理上,必然遇到瓶颈,JS又会何去何从?
紧接上文,cocos2d-JS为我们提供了图形引擎、物理引擎、JS引擎等基础库,在多终端时代提供了非常nice的游戏引擎,在浏览器普及在各个终端的今天,为什么还要单独搞一套JS引擎呢? 我们先看看使用SpiderMonkey的技术产品有哪些? 没有看错,SpiderMonkey就是FireFox浏览器的JS虚拟机(后续简称jsvm),FireFox的实力也是赢得了众多前端开发者的芳心;cocos2d-x更是不用说了,东亚97%的2D游戏开发者的选择,手游开发者的入门技能,k-3D也是在AEPIXI(我们内部实现的跨平台的js图形引擎)的3D技术的源泉,在众多新兴的图形引擎中,我们都找到SpiderMonkey的影子,为什么开发者不选择JavaScriptCore或者V8呢? 主流的JS引擎对比,SpiderMonkey vs v8 vs JavaScriptCore (vs Rhino) a) Interpreter: 解释执行,js和java在运行时都是解释执行的一种高级语言,重要的特性之一,就是无类型untyped,比如,在执行js语句时,a = b + c;因为表达式是无类型的,js执行过程在不同的场景,可能有多种不同的含义,如:(i)b、c都是number类型,那么+号表示数字相加,(ii)b、c都是字符串类型,那么+号表示字符串链接Interpreter是如何解释执行呢?(i)从内存中读取表达式 a = b + c;(ii)从内存中读取b、c的类型,确定操作符的含义(iii)unbox b、c,获取真正的value(iv)执行 b + c,(唯一有效的步骤)(v)将结果box to x 通常如果b、c是整数,在C/C++中应该是一条指令就可以搞定,但在js中,需要更多地指令才可以完成,这里既是js的不足,也是高级语言的一些通病,场景不同,则更显神通。 JIT特性,提升JS运行速度10 ~ 100倍,又是如何提速的呢? 加载js文件时,执行预编译,将js脚本编译目标平台的机器码 (i)判断b、c的类型(ii)执行b+c(唯一有效步骤)(iii)写入a 可以看到JIT的运行方式缩短了js运行步骤,但不是所有场景都是可以使用jit的,比如 string + undefined,这个时候需要回归解释执行了。b) Interpreter vs JIT,本质来说,JIT确实极大的提升了js的运行速度,从2011年之后,几乎所有的javascript引擎都开启了jit功能,(除了Android4.4.2以后,为什么呢?下一章节我们重点解读Google的密谋) JS引擎之争与安全 上图中是2014年的分析,从android4.4.2、iOS8以后,V8和JavaScriptCore分别增加了JIT的功能,android的webkit也同步了最新的Chrome的内核,然而,仍然没有开放JS引擎的插件机制(pnp)功能,理由很简单,那就是如果开启这个功能,开发者可以直接访问js的底层数据,浏览器的安全沙箱,就没有任何意义,开发者可以肆意的窥探不同网站的内部数据,谷歌和苹果怎能轻易放手? SpiderMonkey的出现解决了这个问题,作为独立的js引擎,体积小(2.5M)、运行速度快、支持jit、pnp等特性,成为了开发的首选,虽然国内对SpiderMonkey的介绍不多,但无法阻止前端开发者对js的追求,通过简单地代码,可以让你的C++程序轻松的支持JavaScript脚本。 JS的优势与弥补缺陷 JS在逻辑处理和高级语言特性方面如:闭包、函数、类型转换、Node.JS上,极大的方便了前端开发同学,又像java一样不用担心内存释放,不仅在在H5领域拥有霸主地位,近年来扩张到服务器开发、链接数据库,处理高级业务上,也大展手脚。其所见即所得的开发方式,更是让Native开发同学羡慕不已。 当然,缺陷在上面的解释执行的过程中,也暴露无遗,Native的开发方式上,不仅可以高效的执行指令,而且可以做到硬件加速,在iOS平台就有专门为矩阵运算的硬件加速,这些领域,目前还是js无法做到的。 我们是否可以把一些复杂的计算、如加解密、编解码、图像处理、浮点数、矩阵处理,用C/C++实现,并把这些API开放到JS引擎中呢? JS单线程的性能比Java要好! 我知道,只要说一句php是世界上最好的语言,马上程序员们就不淡定了。先不要慌者评价,我们看看JS和Java的虚拟机架构图 我们可以清楚的看到,Java和JS都有一个底层的跨平台的虚拟机,且都是解释执行的高级语言和GC机制,从架构上看,不同的是Java有自己的独立的内存结构,通过JNI这一层将Java的内存转换成C++内存,才可以调用底层的内核资源,而JS运行空间完全是与C++一样的内存控件,完全是C++代码在运行。 我们知道Java启动时,需要申请独立的内存控件和自己的堆栈管理,最终运行时还是会翻译成C/C++,JS在解释之后,直接交给了C++对象来处理,少了一层JNI的中介服务,必然性能会有所提升。今年来兴起的Node.JS不也是靠C/C++撑腰,才这么牛逼哄哄的嘛? (总结)SpiderMonkey相对WebKit、Blink而言,作为一个非主流的JS的引擎,把握好了终端领域需要一个轻量级、可定制、体积小、运行快的JS虚拟机的需求下,配合移动互联网的兴起,弯道超车,在独立游戏引擎、脚本引擎领域,成了目前众多开发者的首选,是一部屌丝逆袭的励志案例。
紧接上文,JS在单线程下,性能不会比Java差,注意场景是单线程。Java的优势不仅是高级语言的特性,还具备了丰富的系统内核资源,如多线程、网络等支持,要比JS灵活的多很多,这里暂时不在讨论这些问题。 回到主题,如果我们把一个完整的C++图形引擎注入到SpiderMonkey中,把复杂的预算放到C/C++内核中,而JS只作为业务处理和内存管理,是否可以获得C/C++的运行能力,有获得了良好的逻辑处理呢? 答案是肯定的!我们直入主题,我们使用最新的SpiderMonkeyV1.8.5,一起解析一下cocos2d-js的代码,下一步,我们会将SpiderMonkey集成到iOS工程中,与JSPatch结合,打造一个跨平台的JS引擎: 1. 理解SpiderMonkey 网上有一些文章,只是用了一些皮毛,若要构建一个完整的JS引擎,要经常去看国外的SpiderMonkey论坛,如果想看中文,直接看Cocos2d-x的代码好了。今天,我们从Cocos2d-JS的入口类《ScriptingCore.cpp》开始 a) 启动JS虚拟机:ScriptingCore::start() _rt = JS_NewRuntime(8L * 1024L * 1024L); // 开辟8M内存,作为JS运行控件 JS_SetGCParameter(_rt, JSGC_MAX_BYTES, 0xffffffff); // 设置GC的条件 JS_SetTrustedPrincipals(_rt, &shellTrustedPrincipals); // 权限控制函数 JS_SetSecurityCallbacks(_rt, &securityCallbacks); // 权限回调函数 JS_SetNativeStackQuota(_rt, JSB_MAX_STACK_QUOTA); // 设置堆栈大小 _cx = JS_NewContext(_rt, 8192); // 创建当前上下文,一个Runtime可以支持多个上下文 JS::RuntimeOptionsRef(_rt).setIon(true); JS::RuntimeOptionsRef(_rt).setBaseline(true); JS_SetErrorReporter(_cx, ScriptingCore::reportError); // 设置异常回调函数 b) 就这么简单,一个JS的运行环境就启动了,我们写个小脚本测试一下《test.js》 var a = 0, b = 1, c = 2; a = b + c; c) 执行 RootedValue rval(_context); // 定义返回值 JSAutoCompartment ac(_context, _global); // 启动隔离区,类似mutex string filename = pathfile.substr(pathfile.find_last_of("/") + 1); // 查找文件名称 bool success = JS_EvaluateScript(_context, RootedObject(_context, _global), file.bytes, file.size, filename.c_str(), linecount, &rval); // 执行JS文件 free(file.bytes); string result = JS_EncodeStringToUTF8(_context, RootedString(_context, rval.toString())); JS_MaybeGC(_context); d) 打印result的value,可以看到值为3 2. 注入C++函数到SpiderMonkey a) 先回顾一下cocos2d-JS注入了那些C++对象和类 在启动函数中,我们找到如下函数:AppDelegate::applicationDidFinishLaunching ScriptingCore* sc = ScriptingCore::getInstance(); sc->addRegisterCallback(register_all_cocos2dx); sc->addRegisterCallback(register_cocos2dx_js_core); sc->addRegisterCallback(jsb_register_system); sc->addRegisterCallback(register_all_cocos2dx_extension); sc->addRegisterCallback(register_all_cocos2dx_extension_manual); sc->addRegisterCallback(jsb_register_chipmunk); sc->addRegisterCallback(JSB_register_opengl); sc->addRegisterCallback(MinXmlHttpRequest::_js_register); sc->addRegisterCallback(register_jsb_websocket); sc->addRegisterCallback(register_jsb_socketio); sc->addRegisterCallback(register_all_cocos2dx_builder); sc->addRegisterCallback(register_CCBuilderReader); sc->addRegisterCallback(register_all_cocos2dx_ui); sc->addRegisterCallback(register_all_cocos2dx_ui_manual); sc->addRegisterCallback(register_all_cocos2dx_studio); sc->addRegisterCallback(register_all_cocos2dx_studio_manual); sc->addRegisterCallback(register_all_cocos2dx_spine); sc->addRegisterCallback(register_all_cocos2dx_spine_manual); sc->addRegisterCallback(register_all_cocos2dx_3d); sc->addRegisterCallback(register_all_cocos2dx_3d_manual); sc->addRegisterCallback(register_all_cocos2dx_3d_extension); b) 定义JS类成员函数和属性(eg. CCNode) 声明 Node的JS类模板JSClass jsb_cocos2d_Node_class = (JSClass *)calloc(1, sizeof(JSClass)); jsb_cocos2d_Node_class->name = "Node"; // JS中ClassName jsb_cocos2d_Node_class->addProperty = JS_PropertyStub;// 添加属性回调函数(默认就好) jsb_cocos2d_Node_class->delProperty = JS_DeletePropertyStub;// 删除属性回调函数(默认就好) jsb_cocos2d_Node_class->getProperty = JS_PropertyStub; // 获取属性回调函数(默认就好) jsb_cocos2d_Node_class->setProperty = JS_StrictPropertyStub; //设置属性回调函数(默认就好) jsb_cocos2d_Node_class->enumerate = JS_EnumerateStub; // 遍历属性回调函数(默认就好) jsb_cocos2d_Node_class->resolve = JS_ResolveStub; // 解析属性回调函数(默认就好) jsb_cocos2d_Node_class->convert = JS_ConvertStub;// 转换属性回调函数(默认就好) jsb_cocos2d_Node_class->finalize = js_cocos2d_Node_finalize;// 析构函数的回调函数,最好用自己的 jsb_cocos2d_Node_class->flags = JSCLASS_HAS_RESERVED_SLOTS(2);// 拥有2个临时持有对象的槽位,方便持有回调函数的引用,比如点击事件 b) 声明类成员属性 static JSPropertySpec properties[] = { JS_PSG("__nativeObj", js_is_native_obj, JSPROP_PERMANENT | JSPROP_ENUMERATE), JS_PS_END }; JS_PSG(属性名称,属性回调函数,不可删除| 可以遍历); // 用于只声明只读的属性JS_PSGS(属性名称,get属性回调函数, set属性回调函数,不可删除| 可以遍历);// 用于声明读写的属性 c) 声明类成员函数 static JSFunctionSpec funcs[] = { JS_FN("addChild", js_cocos2dx_Node_addChild, 1, JSPROP_PERMANENT | JSPROP_ENUMERATE), .... JS_FN_END }; JS_FN定义JS函数声明JS_FN(函数在JS中得名称,函数的Native回调函数,参数个数,不可以删除|可以遍历); d) 有了函数和属性的声明后,我们把属性和函数注入到a)中声明的JSClass中吧 jsb_cocos2d_Node_prototype = JS_InitClass( cx, global, // 上下文,全局对象,上篇文章已经讲过 JS::NullPtr(), // 没有父类 jsb_cocos2d_Node_class, // 当前类(Node)模板 js_cocos2dx_Node_constructor, 0, // native构造函数和参数个数 properties,// 上面定义的属性字段列表 funcs,// 上面定义的函数属性列表 NULL, // 没有静态属性 st_funcs // 有静态函数 ); 好了这样就可以在JS中直接调用这个JS类对象了: var parent = new Node(); var child = new Node(); parent.addChild(child); 然后再C++代码中添加断点,看看是否断点成功进入。 (总结) a) cocos2d-JS引入了Cocos2d-x图形引擎作为JS的底层运行支持,为JS提供了更高层面的API封装,为H5展示更绚丽的画面提供基础,极大的满足了众多页游开发者多平台化的需求。不仅如此,cocos2d-js还提供了一套html5的canvas的API封装,让开发者直接在浏览器上所见即所得的开发方式,然后用JSB作为运行环境,彻底打破游戏开发的编译-调试的运行模式。 b) cocos2d-JS目前也有一些较为突出的缺陷,虽然H5的性能的得到了极大的提升,相对Native性能,还是明显差很多,究其原因有:(i)JSBinding代码太多,大约15W行左右,代码较大,频繁的转换JS与C++对象,耗时的同时产生了大量的临时对象,GC时候,卡顿感明显,类似于Android,大家可以脑补一下【性能下降】(ii)不是严格意义的JS语言规范,JS与Native的两套内存管理机制,相互制约,导致C++需要维护一个很大的Map的双向对象映射关系,来维护不同的内存管理模式【性能下降】(iii)JS只是工具,失去了JS的灵活性,为了减少binding的复杂度,Cocos2d-JS很多成员变量都是用函数方式去获取,得到的往往是个临时变量,而不能直接操作,需要前端开发者,长时间的理解规范,才能够避免技术上的缺陷 (优化) 有没有一种办法既可以满足JS的灵活性,也可以满足Native的内存管理呢?答案是肯定的,请持续关注下一阶段的AppEngine的实时动态。 (番外) QQ浏览器的独门暗器是什么?如果某些网站是支持cocos2d-JS的,那么直接启动Cocos2d-JS内核作为加载容器,性能爆表啊,有木有?!莫非以后的浏览器将是游戏市场的必争之地? (求知己) Cocos2d-JS的介绍,到此告一段落,有想法的同学,可以私聊我。
紧接上回,Cocos2d-JS通过JSBinding从C++API到JSAPI,完成了H5的跨平台加速,这一回,我们一起来见证一下JSPatch的跨平台实现,为JS语言增加消息转发机制,无需修改js脚本,让下面这段代码可以正确地运行起来: var controller = UIViewController.alloc().init(); 而不是让app翻译成: UIViewController.__c('alloc')().__c('init')(); JSPatch依赖JavaScriptCore作为运行环境,在iOS7.0之后越来越受到终端开发者的欢迎,我们简单地分析JSPatch的技术框架和原理,来引出SpiderMonkey在Patch上的lazy加载机制: 1. JSPatch通过JS动态调用OC的代码 使用反射机制` Class class = NSClassFromString("UIViewController"); id viewController = [[class alloc] init]; SEL selector = NSSelectorFromString("viewDidLoad"); [viewController performSelector:selector]; `使用runtime机制动态注册类,并添加函数` Class superCls = NSClassFromString(superClassName); cls = objc_allocateClassPair(superCls, className.UTF8String, 0); objc_registerClassPair(cls); class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@"); `替换掉掉原有的实现,增加JS拦截` IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL; IMP msgForwardIMP = _objc_msgForward; class_replaceMethod(cls, selector, msgForwardIMP, typeDescription); `通过require增加全局变量` var _require = function(clsName) { if (!global[clsName]) { global[clsName] = { __isCls: 1, __clsName: clsName; }; } return global[clsName] } ` 修改JS脚本,通过正则替换JS的表达式: UIView.alloc().init(); 替换后 UIView.__c('alloc')().__c('init')(); 2. 因为JS语言没有转发机制,无法用懒加载机制,来动态的改版语言的调用,因此,JSPatch绕了很大一个弯,使用runtime替换了Objective-C的函数,动态注册新的类和函数,新增加了一些JSAPI,尽可能的让JS的写法更贴近Native方式,降低Native开发者对JSPatch的门槛,然而并没有像wax框架一样,让脚本的写法更自然。 问题的核心是,JS能否拥有API的转发机制,通过运行时动态加载和调用,打开JSClass,查看描述信息: 查看JSResolveOp方法的说明:`解决在一个Obj对象上的懒加载属性,这些属性按照需求映射在Native对象;JS会首先在Obj上查找该属性,如果没有找到,则调用该方法解析该属性;如果解析成功,JS引擎将再次调用Obj.ID,如果没有找到,会提交给它的父类解析;JSNewResolveOp提供了更便宜的方式来解决延迟属性;` 3. 我们看一下我们将采用的技术架构: 当SpiderMonkey遇到UIVIew的时候,会触发全局global对象的属性查找,如果没有找到,交给JSResolveOp方法处理,这里我们重载的JSResolveOp方法: bool AJContext::Resolve(JSContext *cx, HandleObject handle, HandleId handId, MutableHandleObject retval) { char* name = JS_EncodeStringToUTF8(cx, RootedString(cx, JSID_TO_STRING(handId))); Class clazz = objc_getClass(name); if (clazz) { AJContext* nactx = (AJContext*)JS_GetContextPrivate(cx); RootedObject proxy(cx, JS_NewObject(cx, &AJProxy::Clazz, RootedObject(cx, nactx->prototypeProxy()), NullPtr())); JS_SetPrivate(proxy, new AJProxy(false, clazz)); JS_DefinePropertyById(cx, handle, handId, proxy, 0); retval.set(handle.get()); } return true; } 这里我们只是使用JSObject的空的代理,我们将所有数据保存在一个Native的对象中: class AJProxy { public: void* prt; bool isObject; char* className public: GetterAndSetterMethods.... } 调用对象的init方法: bool AJProxy::NewResolve(JSContext *cx, JS::HandleObject handle, JS::HandleId handId, JS::MutableHandleObject retval) { const char* name = JS_EncodeStringToUTF8(cx, RootedString(cx, JSID_TO_STRING(handId))); fprintf(stderr, "%s\n", name); AJProxy* proxy = (AJProxy*)JS_GetPrivate(handle); if (proxy->isObject()) { //...处理对象方法 } else { // ...处理类方法 id clazz = (id)proxy->target(); id object = objc_msgSend(clazz, sel_registerName(name)); AJContext* nactx = (AJContext*)JS_GetContextPrivate(cx); RootedObject proxy(cx, JS_NewObject(cx, &AJProxy::Clazz, RootedObject(cx, nactx->prototypeProxy()), NullPtr())); JS_SetPrivate(proxy, new AJProxy(true, object)); JS_DefineFunction(cx, handle, name, NewFunction, 0, 0); retval.set(handle.get()); } return true; } 4. 至此,一个跨平台的JSPatch的Demo写完了。 需要补充的地方也很多,比如继承、类型转换等,作者很喜欢SpiderMonkey、C++,这种兼容iOS和Android的事情,还需要很多很多的工作。这里我们主要讲得时SpiderMonkey,通过这个小巧的JsVM,我们可以重新改写很多JS的不擅长的地方,毕竟js的开发者要比lua、wax、swift多很多。相信不需要多久,就会有人写出跨平台的JSPatch,使用JS开发的同学会越来越多。 (总结)想要了解的更多,就要深入底层,下一站,我们带来OpenGL ES2.0 与JS的混合开发。
1. 移动互联网的兴起,我认为2009年是个分水岭。 开始的时候,我也是做Java开发,习惯了Webx架构,可以熟练的使用Spring、iBatis、veloctiy、HSF、Notify、Tair、Session这些阿里具有代表性的Java框架,也会使用IC、UIC、SC、DC等等服务集群做电商核心业务。概括起来,基本也是三层服务端架构: 2. 技术架构也非常稳定 后台MySQL分库分表、服务端HSF业务处理,前端浏览器使用Html做展示,技术人员的重叠率较低,公司内部资源达到了最大化价值体现,市场出现了一种声音:将来的计算机世界应该只有一个应用就是浏览器,浏览器入口是通向互联网的大门,谁占领了浏览器,谁就拥有了无线的用户流量。这个需求在当时是正确的,各大巨头不断的把战线迁往更靠近用户的浏览器,国内浏览器以安全、急速、稳定为名,纷纷推出自己的浏览器,妄想从用户源头一招制敌。AppStore的出现,彻底打破了这一局面:移动互联网时代,这种平衡被打破了,安全和体验成了主题,这个时代的技术架构和分工发生了巨变: 3. 且不说移动端还有WindowPhone等小众系统。 从上图可以看到,移动互联网的到来,技术资源已经成倍的增加了开发成本,以商品详情页为例,浏览器要做两遍:PC和手机;终端要做两遍:Android、iOS,因此前端团队应该扩张了4倍以上,如果那个互联网公司没有无线端的app,都觉得不好意思(数据是我推测的,大家仁者见仁)。有没有感觉这简直是逆商业的技术革命呢?技术是提供生产效率,降低成本为目的的,这也是商业公司的本质。存在即为合理,乔布斯也许真的看到了用户的本质需求,而不是一味的降低成本,长期忽略用户体验,所以等到智能手机系统爆发的那天,传统互联网企业还在停留在PC时代。 若要感知未来,必先读历史,读懂历史才会发现,人类社会的本质其实一直没有变化,只是换了一个新的名词,就像风投经常说,创业者要先学会造概念。 很荣幸,我们团队有济翼、许凡这样的资深互联网技术牛人,听听他们的经历。一个时代的落幕,总有一些关键的技术出现,或者变革,或者预示,或新生,这些时代,我们回顾一下发生了什么事情(个人总结的,勿喷) 4. ? ~ 2004年 PC端VC++App时代 代表企业:微软。。PC端桌面操作系统的兴起,以window95为里程碑,在那个能买得起64M超大内存条的时代,硬件性能和资源可能还不如iphone4,native开发方式取得了巨大的成功,新的用户需求不断被挖掘,VC++等开发工具为开发者提供了巨大的便利。2000年左右delphi的兴起,更是让桌面app的开发效率得到了极大的提升,当界面通过简单地拖拽就可以快速生成一个app的时候,下一个时代也悄悄来临了。 5. 2005 ~ 2012年 浏览器Web2.0时代 代表企业:BAT浏览器兴起了,从没有交互的Web1.0到支持JS、插件的Web2.0,无需安装、直接访问,再到后来人们把金钱交易也搬到了互联网,H5的开发方式太方便了,所见即所得。客户端的与服务端的链接,从B/S结构,变成了现在DB+Server+H5的方式,进一步释放了Native开发方式的不足。从几何时,网页三剑客:PhotoShop+Dreamweaver+Fireworks,成了这个时代的开发者宝典,可视化的拖拽的开发方式,进一步降低了H5的开发成本,下一个时代也悄悄来了。 6. 2013 ~ now 移动App互联网时代 代表企业:谷歌、苹果智能手机的硬件资源相对于PC很受限制,512M内存的手机在2012之前已经是非常令人羡慕的事情,H5的用户体验怎能和Native相比呢?这里就不多说了。这个时候,ReactNative、PhoneGap等技术兴起了。。。 总结 我们处在移动互联网最顶峰的时代,这是最好的也是最坏的 有人说,Android/iOS开发会一步步没落,新兴的JS、swift、lua、H5等脚本开发,是将来的方向。其实没有哪一个事物是一直充满光辉的,除了那些亘古不变的哲理。 下一章,我们也看看在后乔布斯时代,我们如何把技术成本进一步降低,哪怕是一点点。
紧接上文,我们提到移动互联网时代,前端的成本重复而臃肿,后端开发基本稳定,作为商业公司:利润和成本使我们不断追求的目标。这里我们从跨平台的角度,来看待如何降低成本。 1. 首先我们看一段视频: 3个平台:浏览器、Android、iOS,一套JS代码,运行在不同设备上【注意,终端展示没有用WebView相关容器】 测试的demo地址:点击进入测试:Android也可以直接扫码安装: Safari/iOS/Android H5测试也可以扫码在手机端测试: 安卓客户端只适配了480x800的分辨率,分辨率不同的屏幕点击处理就会有问题,这点我们留给后面再讲。 包的大小11M,毕竟一套C++代码需要编译多个平台的so包,armeabi和armeabi-v7a各5.8M,加起来就11M了,这就是很多大型Android游戏,首次启动要动态下载链接库的原因,ndk开发的感觉不如xcode得心应手。 2. 大家先感受一下JSBinding的魅力 ios版本的安装包就不提供了,因为的demo不是很复杂,iphone轻松60fps,随着后续我们的demo不断复杂,AEPIXI的加速效果就越明显。对JSBinding还不了解的同学,可以回顾之前的文章:Cocos2d-JS详解 为什么需要Binding?这一点很重要,希望每个技术同学可以认真地读完:底层开发者充分挖掘硬件的潜能,为上层开发者去浪费。 开始的时候,我也很难理解,拿到上层Android、IOS开发者不断优化App性能的本质,只是底层开发者故意留给我们优化的嘛?这一点,我们要从这个操作系统的架构去分析: 从上面的操作系统的架构图中,底层的硬件标准虽然大不相同,但OS内核和图形引擎大多基于C/C++ && OpenGL,Android和iOS也是相同的,我们可以试想,如果苹果将iOS框架用使用NDK编译一下,是否也可以在Android的手机上运行呢?虽说技术无国界,毕竟商业需要边界,所谓不同平台,受限于不同底层的操作系统提供的基础类库不同,导致上层代码也不相同,无法在多个平台上同时运行。 3. 终端H5开发,就是跨平台开发 跨平台开发,我们需要梳理清楚,什么场景或者技术适合跨平台,把眼光放到多个OS上,我们看看已有的那些技术是相同的,那些是不同的。 2个曲线框,我总结为:跨平台技术的衍生场景,大多基于图中的两个圈圈,我们总结为:上层跨平台技术和底层跨平台技术。 基于H5的Hybird:代表产品:Phonegap、Cordova、WindVane等等,依赖的是各个系统的Webkit的类库,这种跨平台的技术的优势明显,缺点是严重依赖WebKit的性能,大家都懂的。【前提是OS中有Webkit的类库,当然WindowXP是一个特例,我们的做法是内置一个Webkit进去。】 基于脚本语言:代表产品:Wax、ReactNative等,依赖各个系统的脚本解释器,Wax依赖Lua、JS依赖WebKit。优势在于成本低,脚本语言的灵活性,无需编译直接运行,脚本只是做了转发功能,系统各个平台的性能决定了脚本的性能,也决定了脚本语言的写法,比如LuaForJava脚本在LuaForIOS(wax)写的代码就不能在Android平台运行,因为他们底层调用的对应的语言和支持。JS没有消息转发机制,不能方便的动态调用系统API,因此JS的框架往往自成体系,读者想要弥补JS的消息转发机制,可以参考:跨平台的JSPatch 基于Java语言:代表产品:Android,下面会提到。 基于C/C++:代表产品:Cocos2d-X,一套语言多平台运行,只看兼容Android1.5以上、iOS4.3以上,就能感受到C/C++跨平台,不跨则以一跨惊人!性能更是不在话下,遇到场景复杂的业务,连iOS开发者也要考虑是否使用游戏引擎,彩票App的一些彩种,直接用Cocos2d-x,瞬间Android和iOS的体验爆表,有木有?缺点:耗电、能写好C++代码的程序员不多了。 4. 为什么技能相近的开发者,通常iOS平台App要比Android流畅? 4.1 我认为,Android不可避免的GC卡顿,在开发语言上略输iOS一筹。在上层开发者技术能力相似的情况下,我们可以明显感受到iOS的流畅度由于Android,通常我们说Java语言的性能不如C/C++/Objective-C,每次GC系统都要卡顿一下,就让很多开发者无从优化,催化了优化JVM、NDK、JNI的一批牛人。 4.2 我认为,Android底层的图形引擎的优化,要比iOS略逊一筹乔布斯一直专注于图形图像引擎,30年的积累证明了一切。iOS平台多种CoreGraphics/CoreAnimation/UIKit/TextKit/CoreXXX,等框架,每个API都值得我们去思考。 5. 选择跨平台优化终端的场景 从操作系统层面做优化,技术的优化,往往不能摆脱业务的羁绊,我选择从底层做优化,回到最初的Cocos2d-X,下一章节,我们以图片加载和渲染的性能优化为例,一起聊聊一张图片文件在App的旅程。 总结,大公司的商业壁垒往往在技术领域的中间层,向下,商业公司极致的优化底层性能,向上,提供了硬件无关的API,降低开发者的门槛,让小白写很烂的代码,也可以流畅的运行,这一点,苹果做到了。
紧接上文,终端开发使用的WindVane、wax、ReactNative等已经是一种跨平台的技术,我们称之为上层跨平台,Cocos2d-x这种直接使用C/C++,我们成为底层跨平台。上层跨平台,提升开发效率;下层跨平台,提升程序性能。 1. 为什么Cocos2d-x性能比Native开发要好? 因为Cocos2d-X是游戏引擎呗,人家是专业做游戏特效的好不好,直接调用GPU的OpenGL绘图的好不好。打开Cocos2d-X代码,感触最深的不是CCNode这些游戏节点,cocos2d-x已经开始为App开发做准备了!大家看到了什么?UIImageView?UIScrollView?OMG!当我们还在纠结要不要学习一下游戏开发的时候,人家都已经开始做UI系统了,有木有!! 很长一段时间,我们以为Game和App之间有一种天然的鸿沟,长久以来,这两种技术之间没有直接的联系,App可以做各种UI控件,游戏可以做各种UI特效,当游戏开发者想NativeApp大步的走过来的时候,我们还在学习如何用UITableView去优化图片的显示,岂不知在游戏世界里,早已实现了图片的异步加载、解码、缓存了! 别忘了,从iOS7.0开始,苹果已经默认集成了GameKit.framework,颤抖吧!游戏和App即将面临傻傻分不清楚的时候了。 2. 一张图片在OpenGL的世界里,是如何从SDCard显示到屏幕 这一次,我们不是讲UIImage如何加载一个图片,而是自己读取图片文件,解码图像,上传纹理,推送顶点缓冲,刷新帧缓冲,一步步显示图片到屏幕上。 上图之前,我们先了解几个概念:2.1 帧缓冲:我们要显示的视图绘制到屏幕上,本质是一个int32 buffer[width x height x 4]的一个数组,不管2D还是3D,我们都要吧GPU的对象投影到这个buffer中,一个像素4字节表示[R/G/B/A]。 2.2 顶点缓冲:终端设备,只支持精简版的OpenGL,称之为OpenGL ES,这里我们只用2.0版本。在这个版本中,我们只能画三角形,一个正方形的图片纹理,需要两个三角形才可以描述:我们要画图片之前,就要告诉OpenGLES图片顶点的位置,先忽略三角形的顺序问题,就这样告诉GPU:【ABDBDC】,同时设置当前的激活的纹理,就可以绘制图片了。 2.3 上传纹理:现在绝大多数手机,都有独立的显卡芯片,每个显卡芯片都有独立的显存,这么就清楚了:我们常说的内存是CPU直接操控的内存条容量,算上显存,就有两个缓存空间了,程序员可以发挥的空间又多了,是不是有点小激动? 2.4 图片解码:常见的jpg、png等图片文件,都是压缩后的文件,如果不压缩,一张1024x1024的图片,至少需要4M空间。一张图片文件解码到内存,需要解码器来解码,不幸的是,Android很多机型没有集成硬件解码,庆幸的是iOS设备内部支持硬件解码。如果一张图片文件可以瞬间在底层硬件解码完成,要比上层代码优化,更加直接!这就是我们为什么一直追求的是底层跨平台的原因。 2.5 讲了很多概念了,我们上图吧 3. 原来图片可以不占用内存! 我们用C的API来读取一张图片,用libpng来解码一张png图片,调用OpenGL ES2.0API上传图片到GPU显存,指定绘图坐标,显示一张图片,我们上代码吧: 一)读取文件: AEFileData AEFileDataWithPathfile(std::string& pathfile) { AEFileData data; FILE* file = fopen(pathfile.c_str(), "rb"); if (file == NULL) { return data; } fseek(file, 0, SEEK_END); data.size = (GLuint)ftell(file); fseek(file, 0, SEEK_SET); data.bytes = (GLchar*)malloc(sizeof(GLchar) * data.size); fread(data.bytes, sizeof(GLchar), data.size, file); fclose(file); return data; } 二)解码图片:为了跨平台通用,我们用libpng来解码图片,获取png的format和rgba数组,【代码太多,就不贴了】 三)上传GPU: glGenTextures(1, &_textureId); glBindTexture(type, _textureId); glTexImage2D(type, 0, _format, _size.width, _size.height, 0, _format, GL_UNSIGNED_BYTE, pixels); 四)释放内存 AEFileData file = AEFileDataWithPathfile(pathfile); this->initPNG((GLuchar*)file.bytes, file.size); __FREE(file.bytes); 五)指定顶点索引,并绘制 vb[0] = {d11, t11, color}; vb[1] = {d21, t21, color}; vb[2] = {d12, t12, color}; vb[3] = {d21, t21, color}; vb[4] = {d12, t12, color}; vb[5] = {d22, t22, color}; glDrawArrays(GL_TRIANGLES, 0, _vertexIndex); 4. 对比iOS与Android,图片的渲染和展示做了什么? Android的技术框架更偏于上层,看不到底层C/C++代码实现,优化的空间有限;Objective-C更靠近C语言,优化的空间明显。下面我们以iOS为例: 5. 主流框架的图片优化思路 要读懂图片的展示与优化,就要看看当前主流的图片优化框架,他们是如何做优化的?主流的ImageCache框架:SDWebImage/FastImageCache/TBImageCache 左边的是SDWebImage,中间是FastImageCache,右边是TBImageCache SDWebImage:将解码后的图片分别保存到内存和SDCard,将复杂的操作放到了子线程中; FastImageCache:直接将文件读取到mmap映射的文件中,减少了一次从系统内核到用户空间的一次拷贝【然并卵】 TBImageCache:在首次加载图片时,根据用户需要,对图片增加圆角阴影处理,增加了异步线程的处理的工作量,同时保存到sdcard中,消耗了额外的资源,换取的是更好的图片性能【解决了圆角阴影在终端的痛点】 图片缓存框架,只是让CPU负载曲线更均衡,有没有可能再进一步优化? 6. 游戏世界与App世界对比 CPU与GPU是终端的双驾马车,缺一不可,CPU load过高,导致终端卡顿,同理GPU也是如此。iOS的UIKit框架封装的太好了,也是优化做的最好的,让我们看不到底层的原理,看不到iOS是如何平衡CPU、GPU、RAM、GPU RAM的4者之间的关系,让我们大胆大猜测一下吧: 一)UIImageView管理者UIImage对象UIImage就是内存中得bitmap数组,当我们需要绘制一张图片的时候,CPU发出指令,上传BitMap到GPU的显存中,GPU返回生成的纹理ID给CPU。 二)为了提高iOS的性能,苹果做出了一个优化策略,不释放CPU的bitmap当GPU显存空间不足的时候,iOS会释放一部分图片纹理,以节省显存空间,但不会释放内存的bitmap。当一张图片重新需要绘制的时候,iO快速从内存bitmap快速上传到显存,完成绘制过程。 三)当内存的bitmap释放的时候,同步释放显存的纹理对象本质意义上说,bitmap在内存中得存储,不是真正显示的那个缓存,当bitmap上传到GPU的时候,GPU会在显存中申请同样大小的空间,来存放bitmap,并返回纹理ID,每当CPU需要显示某张图片的时候,只需要高速GPU要绘制那个纹理ID就可以了。 四)上传GPU后,我们完全可以释放内存bitmap释放内存的bitmap很简单: free(bitmap);这样内存里只剩下了对应的GPU纹理的ID,当GPU内存紧张的时候,程序要要自己释放不需要的纹理ID,如果这个ID在某个时候有需要使用了,怎么办?那要把之前的读取文件到上传GPU重走一遍,太耗性能了,我想苹果也是不想看到这个原因,才不会轻易释放bitmap的 总结:游戏的架构模式,需要开发者自己优化所有场景 游戏开发者不仅需要优化CPU内存,也要优化GPU内存,通过将所有图片合成一张大的纹理图片,来统一管理GPU内存,需要把大量浮点数运算根据不同的场景,或者交给CPU或者交给GPU,让CPU和GPU的负载达到均衡,各司其职。这一些优化,对于我们做系统上层应用的无线开发,是很难想象有多少坑等着我们,终端App的开发,还是使用上层框架好,图形图像的世界水很深,有想法的同学,可以继续关注下一章分享,想了解我们的同学,可以用Android下载安装我们的Demo: 下一章,我们一起见证所及即所得的开发方式,WebApp2.0初体验
紧接上文,在终端设备中,不管是游戏引擎还是UIKit,图形图像都是基于跨平台的OpenGL ES技术,区分不同的场景,图形图像分为两个分支,一个以高性能的图形显示为目标的cocos2d-x引擎,一个是以省电节能适合App的UIKit框架。 一)今天我们继续逆向思维 Game VS App,既然都是基于OpenGL ES,那我们找出共同点,是否可以让二者的界限,变得更模糊?既可以满足高性能的Game引擎,可以保持App开发? 请仔细观看上图,我们的框架多了一个WebKit?对的,我以为WebKit是统一Game与App的一个代表作,也就是H5框架,它提供的WebGL本质是就是一层薄薄的OpenGL的封装,可以提供高性能的图形渲染能力;上层提供的Dom框架,从一开始就定义为UI布局而生。灵活轻量的JavaScript语言,无需编译直接运行的方式,为前端开发者提供了所见即所得的开发支持。 二)开发方式的对比 我们对比H5的开发方式,与Android、iOS、Cocos2d正在推动的python、Swift、lua,脚本语言的开发架构图: 仔细回想,H5、Android、iOS都是逐步落实脚本化开发,为开发者省去大量的编译、连接、打包的过程,进一步提高开发效率。在这一领域,H5绝对是实现这一思想最早的领路人,也是最早、最全面实现跨平台的技术框架,我们的机器可能不会安装java,但一定有浏览器,浏览器不一定是IE,可能是Friefox、Chrome,但浏览器一定支持JS语言! JS语言虽然各大浏览器相互发展,有WebKit、Blink、SpiderMonkey、IE等等,我们在对比上面的架构图,基本分为三层: 三)灵活的脚本 + 高性能的图形内核 3.1上层脚本开发 js、lua、swift、python,未开发者提供所见即所得的技术支持,节约开发成本,带来了一部分资源浪费【这一层就是用来浪费硬件资源的】 3.2中层渲染引擎 WebCore、Blink、iOSUIKit,AndroidUIKit,Cocos2d-x,高性能的图形渲染引擎,直接通过JNI、OCRuntime、JSBinding、luaBinding,提供上层API支持【这一层就是用来最大化的利用硬件资源】 3.3 底层跨平台 OpenGL ES统一的跨平台的渲染API,打通Android、iOS等等终端设备,这里不得不提一下苹果公司一直是OpenGL ES的推动者,最早支持OpenGL ES2.0也是苹果,这个领域苹果是做的最好的,乔布斯30年的持续坚持,怪不得UIKit做的这么好!【这一层就是用来统一多终端多设备,跨平台的】 四)看到这里,我们必需明确一个观点 脚本语言是将来App开发的方向,这一点从苹果、谷歌等大公司的技术推动就可以看得出来。同时,C/C++/OC/Java等语言会长期共荣,也不会消亡。 这里我们引出本章的主题,所见即所得的开发技术:上层脚本语言,我选择JS语言【开发者众多,已经通吃PC、phone、pad】中层渲染引擎,我选择JSBindingPIXI,也就是WebApp2.0的主角,AEPixi框架【为06、07两篇文章填坑】底层跨平台,我选择OpenGL ES2.0【所有的终端都支持】 五)pixijs是什么? Super fast HTML 5 2D rendering engine that uses webGL with canvas fallbackThe aim of this project is to provide a fast lightweight 2D library that works across all devices. The Pixi renderer allows everyone to enjoy the power of hardware acceleration without prior knowledge of WebGL. Also, it's fast. Really fast. 六)使用pixijs做UIKit 任何一个UIKit框架,都需要向下封装OpenGL,向上提供高级API支持,这里pixijs已经提供了基础的UI组件。目前JS已经存在大量的UI框架,主流的有:xx./xxxx/xxx,我们从替换到掉底层的基础控件,如下图所示:同时也要讲讲WebGL做UI的好处。 七)使用C/C++Native化pixijs 在《Cocos2d-JS为什么选择SpiderMonkey》已经讲过,JS在执行一条命令的时候,由于是弱类型语言,在解释执行过程中,需要做多次转换,对于需要大量浮点运算的图形引擎来说,即使打开jit功能,运行效率也无法与传统的C/C++媲美。上面我们提到,中层渲染引擎就是要充分挖掘硬件性能,为上层脚本的解释执行,提供高效的支持。 八)C++与SpiderMonkey的结合 这里我们将采用SpiderMonkey + C/C++的方式,通过JSBinding注入到JS运行环境,将大量矩阵转换、浮点数计算、碰撞检测、物理系统、地图系统等等Native化为高效的C/C++,同时针对不同的平台,提供硬件加速功能。让上层的JS语言无需关心底层的实现,只要关注业务逻辑即可。 九)使用JSBinding将AEPixi注入到JS运行环境 这里我们面临的是JS、C++、OpenGL混合开发模式,需要精通三种编程理念,面向对象、面向过程、面向状态,简单看一下技术架构图,我们将在下一章节讲述。 总结 本章节,架构图比较多,没有做详细讲解,先挖坑,还请大家持续的关注。编程也是一门哲学,未来的世界我们无法断言,立足现在,回顾过去,发现技术发展过程的本质,才能更好的为将来坐准备,切勿学习诺基亚,生于忧患死于安乐。 下一章,我们来看看pixijs的原理,自己手写一个demo,运行在AEPixi上。 加载基于pixijs的demo 我们在线测试一下,来验证上面我们的技术是否正确。推荐大家使用浏览器,开发JS脚本,完成后,在终端刷新即可,无需编译,高效运行 `经过反馈,适配的问题后续逐步解决,这里示例提供的js的代码完全抄自pixijs的官网同时如果大家想适配,请修改初始化代码的宽高的值既可以适配。var renderer = PIXI.autoDetectRenderer(640, 1136);`
紧接上文,在终端硬件资源有限的大背景下,业务脚本+图形内核,将是未来主流的开发方式。AEPixi使用C/C++、JS、JNI、OC等混合语言开发,将pixi.js变成高性能的Native内核,提供上层pixi.js标准的API,无缝的兼容浏览器开发好的代码,实现浏览器开发,无需编译,到处运行的开发方式。 今天我们做一个H5的Demo,使用FireFox开发,开发完成之后,直接使用上一章节我们发布的android app,直接访问查看效果。 一)搭建前端开发环境 Subline Text2 / VIM【其实开发js我用的是eclipse写的】FireFox / Chrome / H5Builder【Chrome为了安全不支持加载本地图片,除非自己搭建一个本地的HttpServer】AEPixi.apk 【上一章节的末尾的二维码】 二)理解pixi.js的技术框架 这里先简单地了解一下概念 2.1 Node/Container相当于Android/iOS的View。这里引擎官网的图片和文字: Organize your objects in hierarchical trees, with parent-child relationships. 从上图可以看到,在pixi中,每个Node就相当于iOS中的一个View,Node.addChild(Node),就像iOS中[UIView addSubview:UIView] 2.2 Renderer集成了OpenGL ES的封装 从上图可以看到,游戏世界里,基本的元素:Sprite、Texture、MovieClip、Rope、Graphics、TilingSprite等等;就像iOS中的UIImageView、UILabel一样,创建完成对象实例后,需要将坐标、缩放、旋转、图片等数据,转换成GPU可以运行的OpenGL数据,Renderer就是OpenGLES的封装。相当于iOS的UIWindow对象,负责管理硬件资源,只在初始化的时候使用一次,每次视图更新,在调用一次接口。 2.3 AssetLoader相当于iOS/Android的UIImage/BitmapFactory 从上图可以看到,AssetLoader负责读取本地/网络的纹理资源,并将解码后的Texture(Bitmap)统一的保存在本地,多个对象可以共享同一张图片资源。 三)开始写Demo Demo的核心代码一共40行,由三部分组成:Html空壳 + JS代码 + 资源文件 3.1 搭建一个干净的html,引入pixi.js和index.js,分别用于pixi库和自己的代码 <!DOCTYPE HTML> <html> <head> <title>SpriteSheet</title> <script src="pixi.js"></script> </head> <body> <script src="index.js"></script> </body> </html> pixi没有使用css和dom来做UI,它使用了canvas和webgl作为渲染技术。H5作为前端展示工具,分为两种,一种是基于dom的css,一种是基于canvas的API,二者的优势和不足在《》中已经讲过,不熟悉的同学请回顾上文。 3.2 预置资源文件 {"frames": { "eggHead.png": { "frame": {"x":2,"y":169,"w":142,"h":157}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":142,"h":157}, "sourceSize": {"w":142,"h":157} }, "flowerTop.png": { "frame": {"x":2,"y":328,"w":119,"h":181}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":119,"h":181}, "sourceSize": {"w":119,"h":181} }, "helmlok.png": { "frame": {"x":123,"y":328,"w":123,"h":177}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":123,"h":177}, "sourceSize": {"w":123,"h":177} }, "skully.png": { "frame": {"x":2,"y":2,"w":155,"h":165}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":155,"h":165}, "sourceSize": {"w":155,"h":165} }}, "meta": { "app": "http://www.texturepacker.com", "version": "1.0", "image": "SpriteSheet.png", "format": "RGBA8888", "size": {"w":256,"h":512}, "scale": "1", "smartupdate": "$TexturePacker:SmartUpdate:9e3e5afd01ea8e418afabfbdcd724485$" } } 在计算机的世界,图形渲染和展示都是GPU的工作,为了提高性能,将多种图片资源整合成一张大图,是不错的选择。(为什么这样性能好,我们先挖坑,后续我们谈谈OpenGL的加速原理)json文件描述了各个图片资源对应大图的坐标位置,资源加载完成之后,内存中的bitmap将会释放,将不再占用内存。(原理请回顾:《》) 3.3 核心代码 千言万语,都在代码的注释中 // 创建渲染器,可以理解为Activity活着UIViewController var renderer = PIXI.autoDetectRenderer(480, 800); // 添加视图到window上,可以理解为[UIWindow addViewController]; document.body.appendChild(renderer.view); var angles = 0; // world的旋转角度 var number = 200; // 创建200个外星人图片,这个大家自觉修改,可以测试手机性能 var aliens = []; // 保存所有图片的句柄 var frames = ["eggHead.png", "flowerTop.png", "helmlok.png", "skully.png"]; // 一共4个图片 // 所有外星人图片视图,我们统一放到world容器中 var world = new PIXI.Container(); // 类比iOS: world = [[UIView alloc] init]; world.position = new PIXI.Point(240, 400); // 类比iOS: world.frame.origin = CGSizeMake(240, 400); // 创建根视图,游戏里成为舞台【iOS的self.view】 var stage = new PIXI.Container(); // 类比iOS: stage = [[UIView alloc] init]; stage.addChild(world); // 类比iOS: [stage addSubview:world]; // 加载json资源,描述了图片的信息,加载成功后回调onAssetsLoaded函数 PIXI.loader.add('SpriteSheet.json').load(onAssetsLoaded); function onAssetsLoaded() { for (var i = 0; i < number; i++) { var alien = PIXI.Sprite.fromFrame(frames[i % 4]); // 类比iOS[[UIImageView alloc] initWithImage:[UIImage imageNamed:@""]]; alien.anchor = new PIXI.Point(0.5, 0.5); // 设置围绕图片的中心旋转,默认是左上角(0, 0) alien.position = new PIXI.Point(Math.random() * renderer.width - world.x, Math.random() * renderer.height - world.y); // 随机的位置 aliens.push(alien); world.addChild(alien); // 类比iOS [world addSubview:alien]; } animate(); } function animate() { for (var i = 0; i < number; i++) { var alien = aliens[i]; // 获取每个外星人对象 alien.rotation += 0.1; // 外星人旋转角度递增0.1 } angles += 0.01; world.scale = new PIXI.Point(Math.sin(angles), Math.sin(angles)); world.rotation += 0.01 renderer.render(stage); // 设置RootView为stage,从stage开始渲染,递归的渲染所有子视图 requestAnimationFrame(animate); // 16.67ms之后调用animate函数 } 3.4 性能测试 工程源码见附件不会搭建apache的同学,可以再app中输入这个url来打开 AEPixi打开:http://download.taobaocdn.com/appengine-download/test/pixi001/index.js浏览器打开:http://download.taobaocdn.com/appengine-download/test/pixi001/index.html 3.5性能极限 代码中的number字段表示外星人的个数,有没有想法改成2000?20000?没有关系,测试一下自己手机的GPU性能极限,没有性能测试,怎么能说了解自己的手机呢?测试手机HTC G10【2010年上市】 3.5.1 100个外星人 3.5.2 500个外星人 3.5.3 2000个外星人 3.5.4 10000个外星人 四)AEPixi与H5的相同与不同 H5是建立在浏览器(我们简单地成为WebKit)上,运行环境依赖支持Dom和Css的WebCore、支持JS的JSCore上,因为各个平台都有相应地浏览器支持,所以H5的跨平台性非常赞,开发人群也非常多。 相同点: 都是JS开发,使用相同的代码,相同的pixi.js。 不同点: 运行环境不同。 H5运行在Webkit之上,AEPixi运行在SpiderMonkey+C/C++ + OpenGL之上。 AEPixi让js直接调用OpenGL,绕过了webkit的沙箱机制,不依赖dom、css,是一个纯净的js运行环境 AEPixi不支持dom、css、document等传统的API,没有WebSockit,不支持jquery等等 好处: 统一的跨平台支持,把大量浮点数运算和C的底层API封装,提供高性能的JS API支持。 不支持的功能,后续会一点点弥补上去,不要慌~~~ 总结)iOS/Android/H5的未来在哪里 有一句话,我很喜欢,技术同学有能力看到未来2~5年的变化。移动互联网兴起后,用户体验上升一个台阶,前端的开发成本成倍的增加,对商业公司来说,开发成本、效率、安全都有不同程度的挑战,成本增加的同时,盈利能力没有本质提升。商业公司的本质不就是成本、利润吗?脚本的兴起不也是这原因吗?swift、lua、python等脚本在android、ios平台写出的代码不能通用,就是因为底层的内核不同意,没有提供统一的api。ReactNative、钛、母夜叉等框架,本质就是对上层提供统一的api,让js可以写一套相同的代码~~~ 不说了,知道的太多,心里反而容易受累。 下一章)我们一起深入的谈谈pixi.js,一个号称是SuperFast的图形引擎,它究竟Fast在哪里呢?
(一)Html5新功能 最重要的功能之一当属WebGL了,和3D图形规范OpenGL、通用计算规范OpenCL一样,来自Khronos Group,2011年对外公布,到现在已经开始有WebGL2.0规范的雏形了。这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,为Html5的Canvas提供硬件3D加速渲染,如下图所示:WebGL (二)WebGL与品类繁盛的openXX家族 简单列举一下:OpenGL 1/2/3/4.. OpenGL ES 1/2/3..简单说: (A)OpenGL 是个定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。 (B)OpenGL ES 是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。 (C)WebGL 兼顾了openGL ES2.0的同时,增加了大量的扩展属性和封装API,简化了OpenGL的开发难度。 WebGL与OpenGL OpenGL ES的关联与不同 (三)WebGL与OpenGL ES 2.0相同点 (A)包含了ES2.0规范所有的常量定义: 在C/C++的宏定义中: #define GL_DEPTH_BUFFER_BIT 0x00000100 #define GL_STENCIL_BUFFER_BIT 0x00000400 #define GL_COLOR_BUFFER_BIT 0x00004000 相应的JS中,我们使用方式: var gl = canvas.getContext('webgl'); gl.DEPTH_BUFFER_BIT gl.STENCIL_BUFFER_BIT gl.COLOR_BUFFER_BIT (B)包含了ES2.0规范几乎所有的函数调用 GLenum texture = xxx; glActiveTexture (texture); 相应的JS中,我们使用方式: var texture = xxx; gl.activeTexture(texture); 是不是很简单呀。这种一对一响应的变量和API使用方式,让不少OpenGL开发者轻而易举的转移到了Html5阵营,也让那些想学OpenGL的同学,在浏览器中就可以所见既所得的开发方式,降低了准入门槛。 很多事情,我们要反过来看,如果学会了WebGL是不是就可以轻松的搞定OpenGL和OpenGL ES2.0了吗? (四)WebGL 不等于OpenGL ES2.0 (A)WebGL的规范,要比OpenGL ES大,比OpenGL要小 OpenGL ES 2.0 < WebGL < OpenGL 3.0 比如增加了OpenGL 2.0以后版本才有的一些常量: #define GL_PIXEL_PACK_BUFFER 0x88EB #define GL_PIXEL_UNPACK_BUFFER 0x88EC #define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED #define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF #define GL_DEPTH_STENCIL 0x84F9 #define GL_DEPTH_STENCIL_ATTACHMENT 0x821A 增加了一些WebGL特有的常量(以WebGL结尾): #define GL_UNPACK_FLIP_Y_WEBGL 0x9240 #define GL_UNPACK_PREMULTIPLY_ALPHA_WEBGL 0x9241 #define GL_CONTEXT_LOST_WEBGL 0x9242 #define GL_UNPACK_COLORSPACE_CONVERSION_WEBGL 0x9243 #define GL_BROWSER_DEFAULT_WEBGL 0x9244 增加了一些WebGL特有的API: createBuffer createFramebuffer createRenderbuffer createTexture deleteBuffer deleteFramebuffer deleteRenderbuffer …… 删除OpenGL ES2.0的gen开头的所有API genBuffers generateMipmap genFramebuffers genRenderbuffers genTextures …… 重命名了OpenGL ES2.0里以get开头,以v结尾的指针类型API: getBufferParameteriv getVertexAttribPointerv …… 新名称为: getBufferParameter getVertexAttribOffset …… (B)扩展面向状态编程,增加面向过程、面向对象思想 每一个canvas都有一个上下文对象 var gl = canvas.getContext('webgl', attris); canvas.getContext = function(type, attrs) { return new WebGLRenderingContext(attrs); } 上下文具有生命周期: canvas.addEventListener( 'webglcontextlost', onContextLost, false ); (C)WebGL与OpenGL的API不再一一对应 以glTexImage2D为例,OpenGL中函数定义为: glTexImage2D(target, level, infmt, width, height, border, format, type, pixels); 在WebGL中函数发生了重载,参数个数或为6或为9 // source可以是image/video/canvas标签对象 glTexImage2D(target, level, infmt, format, type, source); glTexImage2D(target, level, infmt, width, height, border, format, type, pixels); (五)WebGL为什么要这样做,他做这件事的目的又是什么呢?这件事的意义又是什么? 请关注下一篇,在Html5一统前端的背景下,WebGL的布局与解局。
紧接上文 WebGL为不同的平台/硬件提供了统一的封装,屏蔽了OpenGL ES2.0在各平台差异。后续我们会继续谈谈OpenGL ES2.0在Android/iOS平台的更多差异。这回我们分析WebKit的源码,谈谈WebGL与Cavans不同,WebGL又多做了哪些呢,性能提升在哪里呢。 在H5中渲染机制 我们可以使用多种方式来绘制图形【本文以iOS版本的WebKit为例】DOM+CSS:为前端提供了强大图形渲染技术,门槛低上手快,对硬件要求较高。 Canvas2D:元素本身并没有绘制能力,它仅仅是2D图形的容器,必须使用脚本来完成实际的绘图任务。getContext('2d') 方法可返回一个对象CanvasRenderingContext(后续简称canvas),该对象提供了用于在画布上绘图的方法和属性,可用于在画布上绘制图片、文本、图形、颜色等,并提供常见的图形转换API,进行图片的缩放、旋转、像素再加工能力,最终通过OpenGL渲染到屏幕上。 Canvas3D:同2D一样,本身没有渲染能力,通过getContext('webgl')方法可以返回一个基于OpenGL ES2.0上下文的对象WebGLRenderingContext(后续简称webgl),通过webgl可以直接访问GPU硬件资源,对码农要求更高,优化空间更大。 2D领域Canvas VS WebGL Canvas使用CoreGraphics渲染图像 WebKit的实现代码 context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height); 对应GraphicsContext.cpp中的 void GraphicsContext::drawNativeImage(imagePtr, imageSize, styleColorSpace, destRect, srcRect, op, blendMode, orientation) { ······ // Flip the coords. CGContextTranslateCTM(context, 0, adjustedDestRect.height()); CGContextScaleCTM(context, 1, -1); // Adjust the color space. image = Image::imageWithColorSpace(image.get(), styleColorSpace); // Draw the image. CGContextDrawImage(context, adjustedDestRect, image.get()); CGContextRestoreGState(context); ······ } WebGL直接使用OpenGL ES2.0渲染图片: 在《 07. WebApp2.0时代启程:倒立者赢,从CPU到GPU,一张图片的旅行 》中,这里不重新做说明了。 Canvas与WebGL的对比 CoreGraphics是运行在CPU上的图形库 它在iOS中已经深入的融合到UIView和UILayer框架中,架构图如下: WebGL只是对OpenGL ES2.0的轻量级封装 js代码 gl.bindTexture(gl.TEXTURE_2D, texID); 在WebKit中对应于C++代码: void GraphicsContext3D::bindTexture(GC3Denum target, Platform3DObject texture) { makeContextCurrent(); if (m_state.activeTexture == GL_TEXTURE0 && target == GL_TEXTURE_2D) m_state.boundTexture0 = texture; ::glBindTexture(target, texture); } 图形渲染WebGL完胜Canvas CPU在RAM中处理完图片后,仍需要上传到GPU中才可以显示,同时,GPU图形渲染速度比CPU提升10倍以上。 Canvas的不可替代和WebGL的优势 Canvas提供高级的图形渲染API 直接使用DOM对象绘制图形 var canvas = document.getElementById("myCanvas"); var cxt=canvas.getContext("2d"); var img = new Image() img.src = "flower.png" cxt.drawImage(img, 0, 0); 而WebGL需要负责的代码实现: gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultipliedAlpha); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.source); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, texture.scaleMode === CONST.SCALE_MODES.LINEAR ? gl.LINEAR : gl.NEAREST); 可以渲染文字 使用 fillText(),在画布上写文本 "Hello world!" 和 "w3school.com.cn":JS代码为: var canvas=document.getElementById("myCanvas"); var ctx=canvas.getContext("2d"); ctx.font="20px Georgia"; ctx.fillText("Hello World!",10,50); ctx.font="30px Verdana"; // 创建渐变 var gradient=ctx.createLinearGradient(0,0,c.width,0); gradient.addColorStop("0","magenta"); gradient.addColorStop("0.5","blue"); gradient.addColorStop("1.0","red"); // 用渐变填色 ctx.fillStyle=gradient; ctx.fillText("w3school.com.cn",10,90); WebGL无此API,WebKit也是通过FreeType通过CPU来实现文字的加载与渲染。如果需要渲染文字,需要生成一个Canvas对象,在Canvas绘制完成之后,在直接渲染到WebGL空间。 高效的上下文切换 多个Canvas实例可以轻松切换,因为在CPU中他只是一个静态的Bitmap。而WebGL上线文最好只有一个,切换上线文需要更改GPU硬件管线的状态和flushbuffer,频繁的切换会消耗大量的CPU资源。 WebGL的优势 GPU的framebuffer直接push到Display CPU需要将渲染好的内存Bitmap上传给GPU,然后通过display link到屏幕上。WebGL避免了CPU到GPU的大量的数据传输,只需要发送指令给GPU即可。 支持批量渲染 支持vertexArray和IndiceArray,多个精灵共享一张文理,只需要将vertext数据缓冲到顶点数据,通过glDrawElements可以一次批量渲染。 支持异步操作 Canvas的API每次都是同步操作,每次渲染必然同步到framebuffer中,不利于性能优化。WebGL可以自定义渲染机制和时机 下一回 我们将谈谈WebGL的纹理格式转换,也是WebGL区别于OpenGL ES2.0最大的区别。