设计和开发交互式地球仪

2020-09-02 21:33:20

传统的球体制造机塑造球体,将其安装在车轴上,用隐藏的重量来平衡球体,并精确地应用GORE(三角形印制地球条),以避免重叠并对齐纬度。制图员在绘制地图时面临着不令人羡慕的取舍。它们要么保持国家的形状,但扭曲它们的大小;要么保持国家的大小,但扭曲它们的形状。在保护我们世界的一个方面时,他们扭曲了另一个方面。

作为视觉设计师和软件工程师,我们每次构建软件时都是在对世界的一部分进行建模。在某些情况下,它是整个世界-而这个数字世界是动态的和互动的。有一些工具可以在网络上渲染3D对象,但许多人认为它们很有魔力。并召唤着魔法不是没有汗水就会到来的。在WebGL中,显示一个没有灯光、纹理、交互性或动作的三角形--就像一个地球仪的血块--需要50多行代码。

对于新的stripe.com,我们建立了一个1:40万比例的交互式地球3D模型。我们希望传达互联网经济的互联本质和我们服务的全球规模,同时承认还有多少领域尚未覆盖。尽管业务扩展到40个国家,支付处理来自195个国家,但我们每天都在努力应对跨境运营和扩张的复杂性。

我们着手建造一个能激发敬畏之感、邀请人们去探索、并隐藏细节以供发现的地球仪。在此过程中,我们评估了现有的工具,设计了我们自己的解决方案,解决了四个有趣的技术挑战,并改进了我们的协作方式。这是我们了解到的情况。

我们并不一定会在登陆页面上建立一个互动的3D地球仪。我们设计了我们的第一个版本的地球,以交流关于每个国家之间发生的在线跨境贸易数量的细微差别的数据。出于这个原因,它包含了额外的视觉细节,如国家边界。对于我们的登录页面,地球的目标是捕捉我们的全球规模,并将视觉隐喻带入生活。在发布前一周,我们有一张很好的动画地图,现在地球就在那里,但我们并不喜欢它。尽管发布在即,但一位高管(他就是帕特里克)向我们提出:如果你有时间按照你希望的方式去做,你会构建什么?

我们决定选择一个地球仪,并认为这是一个更好的选择,原因有三个。首先,使用球体显示地球只占二维显示世界所需屏幕面积的不到20%。其次,地球仪更准确地描绘了国家和水体的相对大小、形状和方向,尽管使用地图可以更容易地一目了然地看到整个世界。(地球上超过1/4的区域要么隐藏在反半球上,要么被它的曲率遮挡住了。)。最后,作为一种互动体验,旋转地球仪比浏览地图更令人满意。

一旦我们选定了一个地球仪,我们就得想办法让它活起来。

如果我们准确地知道我们想要建造的地球,我们不雇佣GlobeKit就太愚蠢了。取而代之的是,由于不知道我们不知道的东西,我们决定自己弄清楚。用于在Web上渲染3D对象的主要工具WebGL和GLSL着色器可能令人望而生畏。编写着色器的开发人员可以在不需要深入了解三角学和线性代数的情况下勉强过活,但对这些学科的良好理解会使3D图形开发变得非常容易。

我们团队中没有人认为自己是3D艺术家,所以我们互相依靠,依靠互联网和朋友来帮助解决技术问题。首先,该项目的设计负责人在Photoshop中创建了与她的地球视觉最接近的效果。我们自然而然地保持了全球设计的流动性,当更好的想法出现时,我们可以很容易地采用它们,而不会对被丢弃的东西感到珍贵。

当很明显编写我们自己的3D引擎超出了范围时,我们决定使用Three.js。Three是WebGL的一个可接近的层,它在一个文档齐全的API背后抽象了它的大部分复杂性。它最初是在2010年从ActionScript(Flash)移植而来的,它帮助我们创建了丰富的3D图形,可以在浏览器中实时渲染,而无需定义光线如何反射到每个形状的每个像素上。

发布十年后,Three.js赶上或超过了十年前Flash可能实现的大部分功能。网络上最吸引人的互动体验现在有三个。社区的热情与早期的ActionScript有相似之处,还有一个额外的好处,就是可以在移动浏览器上运行,而且不需要插件。随着Three.js和WebGL越来越受欢迎、平易近人和获得支持,Web正准备全面拥抱3D。由于WebGL是GPU加速的,即使在低端消费硬件上,它也能够处理惊人数量的连续视觉变化,而不会对CPU造成瓶颈。找到让我们的地球感觉有活力和使浏览器崩溃之间的界限将成为我们最大的技术障碍。但这不会是唯一的一个。

就像我们面临的挑战一样,我们的地球深入了几个层次。它由三个不同的层组成,尽管看起来像一个单一的表面。基础层表示海洋,是一个半透明球体,在水平和垂直方向上都有大约50个分段。第二层是另一个带有数万个闪烁圆点的球体。最外层由彩色动画弧线组成,这些弧线从一个脉动圆盘移动到另一个圆盘,将自己包裹在两个球体周围。ARC从任何STRIPE接受付款的国家旅行到企业接受使用STRIPE付款的国家。

在此过程中,我们遇到了几个重大的技术挑战,每个挑战都可能阻碍我们实现我们的愿景。为了那些生成他们自己的交互式地球仪(或类似的复杂3D对象)的人的利益,让我们将这些挑战中的几个拆分开来。

最外层的球体--由数万个点组成的一层--的主要目的是定义大陆。但是,当我们去除边界的视觉复杂性并使每个点具有动画效果时,它们所做的不仅仅是传达陆地;它们让地球感觉充满活力。要使它们工作,我们有两个主要要求。第一步是找到一种方法,在每行和每列点之间,从一极到另一极保持一致的间距。其次,我们需要分别为每个点设置动画。

在我们的最终设计落地之前,我们测试并考虑了三种不同的方法来用一簇点填充开放空间(如下图所示)。每一次尝试都有它的优点和缺点。

均匀分布的点的图像。这种方法最容易创建,但很快就会出现问题。当每一排的圆周在接近地球两极时缩小时,这些圆点就会融合在一起。作为静态位图而不是几何体,如果没有过于复杂的着色器,我们无法单独为每个点设置动画。

不均匀分布的点的图像。我们增加了点行的宽度和水平间距。这张近8万个点的图像在顶部和底部放置了更少、更宽的点。这一调整有助于防止地球两极的点被捏和结块,这使以前的方法无效。当作为纹理映射到球体上时,该图像会创建几乎均匀的点间距。首先,我们手工创建此图像纹理,然后使用JavaScript生成SVG。这个选项更好地满足了我们的视觉目标,但仍然不能让我们对每个点进行动画处理。我们假设Three.js可以很好地处理SVG,但是由于每个形状都必须转换为三角形,转换的复杂性阻碍了我们采用这种方法。

以编程方式生成的层(与图像相比)制作单个点动画的最直接方法是在三维空间中生成它们。为此,我们重用SVG中的代码,在Three.js中生成点行作为几何图形。每一行包括不同数量的点,从两极的零到赤道的500个。我们使用正弦函数选择每行的点数,绘制每个点,并应用lookAt方法旋转每个点以面向球体的中心。然而,点的数量沿着几个纬度不一致地跳跃,在纵向柱子上造成了粗糙的线条和不自然的效果。

最后一次尝试-也是正确的设计-使用了向日葵图案。就像向日葵的种子图案一样,这些圆点是一系列从球体顶部到底部紧紧缠绕在纬度上的六边形。使用内置的setFromSphericalCocords方法,我们确定了这个解决方案:

//创建60000个小点并围绕球体旋转。const DOT_COUNT=60000;//半径为2个像素的六边形看起来像一个圆形。dotGeometry=new THREE。CircleGeometry(2,5);//每个点的位置的XYZ坐标=[];//每个点的随机标识符rndId=[];//每个点所在的国家边界Ids=[];const向量。I--){constφ=Math.acos(-1+(2*i)/DOT_COUNT);constθ=Math.sqrt(DOT_COUNT*Math.PI)*Phi;//传递该点与Y轴之间的角度(φ)//传递该点围绕y轴的角度(θ)//将每个位置缩放600(地球半径)矢量。setFromSphericalCoods(600,//将点移动到新计算出的位置dotGeometry.late(Vector tor.x,Vector tor.y,Vector tor.z);…。}。

在全球的stripe.com网站上,圆点组成了大陆,照亮了斯利普居住的地方。在我们为stripe.com/Enterprise创建的前一个迭代中,我们按照国家/地区对点进行了分组,以指示Strip的运营位置。我们决定在登陆页面上关闭互动地球仪的这一功能,但我们认为分享一下我们是如何按国家对点进行分组的可能是值得的。

一旦我们在地球上填满了点,下一步就是通过定义国家将我们层层的球体转变成一个球体。我们的第一个目标是让圆点只出现在条纹所在国家的边界内。一旦这样做了,我们需要那些活着的国家中的点作为一个群体的动画目标。

一位最近为一个游戏项目试验着色器的队友为这项挑战带来了灵感。他想为条纹所在的每个国家用独特的颜色编码一张PNG图像(见上文)。我们使用内置画布getImageData为我们提供图像中每个像素的颜色。然后,我们将每种颜色与一组国家颜色进行匹配,在将每个点的坐标传递给着色器进行渲染之前,使用唯一的Country Id标记每个点。现在,我们可以在任何国家分离出这组点,并在z空间中设置其颜色、不透明度和位置的动画。

2020年,条纹所在的每个国家都用独特的颜色表示。假设将所有点生成为单个几何图形的缺点是每秒60,000个点的属性动画化所需的天文计算次数为60,000次。对我们来说幸运的是,地球表面大部分是水。通过只渲染条纹所在国家/地区的几何体,我们可以将几何体从60,000个点减少到~20,000个点,并将部分数据传递给顶点着色器。通过将较少的数据推送到着色器,我们释放了渲染预算以供其他动画使用。

//我们为每个国际标准化组织国家/地区编解码器Country_Mapping=[[0,';#99cc99';,';],[1,';#993333';,';au';],[2,';#cccc00';,';be';],…]指定颜色。;//加载颜色编码的图像,然后获取每个像素的颜色new ImageLoader().load(';map.png';,(MapImage)=>;{const ImageData=getImageData(mapImage);});dotGeometry.computeBoundingSphere();const UV=pointToUV(dotGeometry.bound ingSphere.center,this.position);const Sample=sampleImage(UV,ImageData);//如果没有颜色数据,则返回并移动到下一个dotif(!Sample。DotGeometry.faces.length;i++){const face=dotGeometry.faces[i];//创建构成每个点位置的面的顶点。Push(dotGeometry.vertracs[face.a].x,dotGeometry.verties[face.a].y,dotGeometry.veregs[face.a].z,…。//face.b,face.c);const[Country Id]=getCountryId(Sample);Country Ids.ush(Country Id,Country Id,Country Id);}//将RGB转换为十六进制,通过color function getCountryId([r,g,b,_]){const hex=[r,g,b].map((Color)=>;Color.toString(16).padStart(2,';0&。Const Country Id=COUNTRY_MAPPING.find(([_,id])=>;id=十六进制);返回Country Id;}。

在用圆点填充表面并将它们分组到国家之后,我们需要将这些圆点连接起来,以显示全球范围内如何以及在哪里开展业务。我们的目标是让地球变得栩栩如生,这意味着要增加动画。早些时候,我们知道我们需要让地球旋转,每个点都在闪烁,并在国家之间弯曲弧线来指示交易模式。我们希望游客能够控制和旋转地球。

大约在我们开始制作动画的时候,我们有了一个新的队友。在过去的生活中,他设计了标志性的铅笔by 53网站的滚动。在很短的时间内,他为起伏的北极光添加了动画,让地球在页面加载时旋转,并在用户滚动页面时旋转地球。我们使用自定义片段着色器(以及bookofshaders.com提供的大量帮助)处理了微妙闪烁的点和弧,但动画的其余部分都是普通的JavaScript。RequestAnimationFrame驱动圆弧的运动、球体的旋转和颜色的变化。

//在两个坐标之间绘制圆弧...构造函数(start,end,Radius){Super();//将地球上的纬度/经度转换为XYZ const start=toXYZ(start[0],start[1],Radius);const endXYZ=toXYZ(end[0],end[1],Radius);//D3沿同时通过起点和//终点的大圆弧插值const d3Interpolate=GeoInterpolate([。Const control 1=d3Interpolate(0.25);const Control2=d3 Interpolate(0.75);//将圆弧高度设置为点间距的一半const arcHeight=start.DistanceTo(End)*0.5+Radius;const control XYZ1=toXYZ(control 1[1],control 1[0],arcHeight);const control XYZ2=toXYZ(Control2[1],Control2[0],arcHeight。//圆弧是半径为0.5px、边为8的曲管//每条曲线被分成44段this.geometry=new THREE.TubeBufferGeometry(curve,44,0.5,8);this.Material=new THREE.ShaderMaterial({//自定义片段着色器设置弧形颜色});this.esh=new THREE.Mesh(this.geometry,this.Material);this.add(this.esh);//设置。}drawAnimatedLine=()=>;{let drawRangeCount=this.geometry.drawRange.count;const timeElapsed=performance.now()-this.startTime;//为曲线设置2.5秒的动画const Progress=timeElapsed/2500;//圆弧由大约3,000个折点组成drawRangeCount=Progress*3000;if(Progress<;0.999){//更新绘制范围以显示。

早些时候,我们讨论了对地球在不同浏览器上的性能的期望,以确定我们的需求。我们将这些期望归结为一个要求:所有动画和滚动效果都必须以60fps的速度执行(以匹配普通设备60 hz的刷新率)。如果不能满足这个条件,我们准备退回到静态图像。多亏了WebGL的GPU加速和这里提到的一些发现,我们从来没有放弃过我们的互动地球。

最初,我们排除了移动支持。我们假设滚动和3D动画对任何机器来说都太多了,我们要么不得不在更小、动力更弱的机器上接受一些滞后或减少运动,要么就只能满足于后备。但是,随着我们了解到GPU的功能,我们不断提高了我们的期望。WebGL中的大部分功能都可以在移动设备上使用

//关闭webgl抗锯齿以提高性能ethis.renender=新的webgl渲染器({antialias:False,Alpha:True});…。//从';lowash/throttle';;const SCROLL_EPSILON=0.0016;const GLOBLE_TRIGGER_TOP=window.innerHeight;…旋转滚动端口油门上的地球仪。Document.addEventListener(';Scroroll';,this.Universal alScrollHandler);…。//事件处理程序:根据当前浏览器滚动位置通用ScrollHandler=throttle(this.scllHandler.bind(This),16);scroll Handler(){//关闭所有其他动画this.isScrolling=true;this.oldScrollTop=this.scllTop;this.scroll Top=document.scroll ingElement.scllTop||document.body.scllTop;this.oldScrollTop=this.scllTop;this.scllTop||document.body.scllTop;this.oldScrollTop=this.scllTop。//一旦浏览器滚动过页面上的地球仪//如果(globe_rigger_top<;this.scllTop){this.globeOff=true;this.globeEl.style.Transform=';TranslateX(100vw)';}Else{this.globeOff=false;this.globeEl.style.Transform=';TranslateX(0)&,则停止所有动画并将地球仪移出屏幕。

构造板块排列着大陆,但是国家--我们如何组织地球--是由人们来定义的。组织也是如此:我们定义团队的方式决定了我们的运作方式。我们发现,建立设计师和工程师之间的联系、协作和组织方式对我们的构建方式有着巨大的影响。有一大批设计师和开发人员,他们对像素和代码都很尊重。这种融洽的关系避免了制造产品时的许多陷阱:将压力从设计师转移到开发人员,以提供令人惊叹的视觉效果,转移到工程师身上,在最后一刻冲淡了愿景。混合设计和工程使过程变得复杂,但丰富了结果。

只有在我们建立了一个功能原型,在屏幕上显示一个球体进行检查之后,我们才能正确评估我们的球体。现代软件开发通常是模块化构建的,将组件咬合在一起,直到可以发布为止。我们承诺要制造真正的、完整的产品,即使是在最早也是最丑陋的阶段。这使我们能够将其功能性与终结性分开,较少关注。

.