项目概况及开发设计本次尝鲜的商业伙伴为美食系,最终落地项目为《奇遇奇缘》:用户使用左侧“摇杆”操作IP角色,前往您感兴趣的美食广场,调整当前视角,体验3D虚拟线下场所购物。“元界”虚拟食堂美食数字人像沉浸式体验第一视角如下:想要体验的同学可直接在APP上扫码(打开APP首页,访问“食堂”》,点击右下角的悬浮按钮层数也可以体验):《旅行奇遇记》的两个业务指标是:页面停留时间和页面重访率。因此,在与设计商量方案后,在任务和产品两个维度进行功能开发,加上任务反馈奖励列表和新手教程,整体项目开发拆分如下:最终目标3D沉浸式体验是为了保证商业目标,但是还是要考虑到原来二维领域的购物过程。因此在前端架构上设计了过渡方案,将渲染分为两部分。一部分是3D渲染的处理,另一部分是普通的DOM节点渲染。其中,用于3D渲染的技术库是Babylon。实际的前端设计如下图所示:Rendering实现3D场景渲染场景包括街道氛围,各种特色美食场所,以及HTML中的IP角色渲染。在早期的Demo中,3D基础渲染已经实现。本项目主要优化了以下两点:对比竖屏90度以下的视野,横屏强制翻转界面,横屏更符合人的生理视觉范围(约114度)。在之前的技术方案中,前端采用自适应手机横竖屏显示。由于APP不支持横屏,需要人为的将竖屏的内容翻转为横屏显示。调整后,无论横屏还是竖屏,界面如下:涉及的主要代码如下://全局容器在竖屏情况下,width为屏幕高度,height为屏幕宽度,rotate90deg.wrapper{位置:固定;宽度:100vh;身高:100vw;顶部:0;左:0;变换原点:左上;变换:旋转(90度)平移(-100%);transform-style:preserve-3d;}//不要在横向模式下旋转@mediaonlyscreenand(orientation:landscape){.wrapper{width:100vw;高度:100vh;变换:无;}}封装资源管理中心3D页面需要渲染的文件很多,而且质量不小,所以封装了资源管理中心,对需要的资源进行集中处理和加载。包括:3D模型、纹理图像、纹理等,以及相同资源文件的去重。以加载模型为例,涉及的主要代码如下:asyncappendMeshAsync(tasks:Tasks,withLoading=true){//加载处理...constpromiseList:Promise[]=[]//过滤模型同名constunqiTasks=_.uniqWith(tasks,(a,b)=>a.name===b.name)for(constitemofunqiTasks){const{name,rootUrl,fileName,modelRoot=''}=item//避免重复加载if(this.modelAssets.has(name)){console.log(`${name}modelhasbeenloaded`)continue}constpromise=newPromise((res,reject)=>{consttask=this.assertManager.addMeshTask(`${Tools.RandomId}_task`,modelRoot,rootUrl,fileName)task.onSuccess=result=>{this.savemodelAssets(name,result)res(result)}task.onError=()=>{reject(null)}})promiseList.push(promise)}//加载this.assertManager.loadAsync()constret=awaitPromise.all(promiseList)returnret}资源管理中心的本质还是利用Babylon的addMeshTask、addTextureTask等加载模型和贴图。项目申请过程中,发现APP中批量加载多个模型时,会造成内存损坏。压力小,因此实际模型的加载过程采用单一加载方式(需要根据当前环境决定)DOM组件DOM组件主要覆盖日常电商活动中业务相关的流程接口,比如产品下图列表展示,商品详情内容,奖品发放弹窗等:商品列表页列表页实现拉取操作配置商品组素材,展示商品名称,商品图片,价格,促销信息信息等问题是:在App只支持竖屏的前提下,Demo的横屏方案是样式层面的90度旋转,需要考虑横屏翻转对触摸操作的影响。横屏模式下的水平滑动操作,Webview会将其识别为竖屏下的垂直滑动操作,导致滑动方向与预期不一致。解决方案:放弃日常的CSS方案(overflow:scroll),重写滑动组件。具体实现:先隐藏显示区域外的部分;根据设计的滑动方向,获取用户在垂直于方向的方向上的触摸距离。轴的触摸距离,然后根据触摸距离确定商品列表的水平偏移量和虚拟滚动条的滑动距离,从而修正旋转屏幕导致的错误滑动方向。列表页运行后,商家详情页展示了更详细的商品信息,涵盖了“加购”的业务链接,是本次活动相关订单成交的重要环节。在这里可以复用公共场所的商品组件。3D模型展示弹窗项目接入京豆、优惠券等奖品的发放,同时增加累计签到奖励,实现回头率目标。因此,也涉及到大量的弹窗提示。普通的弹窗都是复用会场中的Toast组件实现的,开发量不大。比较特别的是收藏展示界面,类似于商品列表和店铺详情的关系。点击收藏后,会出现对应物品的3D模型。由于层级关系,集合的3D模型无法在展示场景的画板上进行渲染,所以搭建了另外一个WebGL画板进行渲染。为了减少渲染量,在展示采集模型画板时,暂停场景画板的渲染。其作用和关键代码如下:)if(isStampShow){this.engineRunningStatus=false}elseif(!this.engineRunningStatus){this.engine.runRenderLoop(()=>{this.mainScene.render()})}}}混合模式3D渲染和DOMcomponents的迁移和复用,覆盖了大部分渲染场景,但还是有一些例外:左边是上一节出现的商品列表页(DOM组件),右边是3D模型当前的产品类型。因此,除了独立的3D模型渲染和普通的DOM渲染之外,还需要考虑混合在一起的情况以及相互之间的通信联动。Babylon是一个优秀的渲染框架。除了渲染3D模型,完成模型交互,还可以在3D场景中混合渲染2D界面。但是,与原生DOM渲染相比,使用Babylon呈现2D图像的开发成本和成品效果是无可比拟的。所以在需要渲染2D的场景中,我们尽量使用DOM渲染方式。在这样的页面结构下,3D模型渲染逻辑由Babylon框架处理,普通DOM渲染逻辑由React处理,两者之间的状态和行为由事件管理中心处理。两者之间的交互可以与两个组件之间的交互相同。比如某个品类的商品列表页,用户触发商品列表页显示后,Babylon会截取当前用户的场景框并保存,并加暗。模糊之后,设置为整个画布的背景,然后相机只渲染右边的模型部分,当右边的模型触发一些交互时,事件管理通知左边的DOM层去请求相应的产品信息并切换显示。设置页面背面:Tools.CreateScreenshot(this.scene.getEngine(),activeCamera,{width,height},data=>{...setProductBg(data)...})privatesetProductBg(pic:string){constproductUI=this.UIList.get('productUI')if(productUI&&productUI.layer){constimage=newImage('bg',pic)image.width='100%'image.height='100%'productUI.addControl(image)this.mask=newRectangle('mask')this.mask.width='100%'this.mask.height='100%'this.mask.thickness=0this.mask.background='rgba(0,0,0,0.5)'productUI.addControl(this.mask)productUI.layer.layerMask=detailLaymask}}Babylon通过事件同步DOM层:privateshowSlider=(slider:Slider):void=>{slider.saveActive()...constindex=slider.getCurrentIndex()constdataItem=this.currentDataList[index]constgroupId=dataItem?.comments?.[0]||''这个.eventCenter.trigger(EVENT_TYPES.SHOWDETSILS,{name:dataItem.name,groupId,index})if(actCamera&&slider){(sliderasSlider).expand()slider.layerMask=detailLaymask}...}最终效果如图下图:AirWallIPofcameraprocessing主角在美食街的探索需要寻路。整个街景中哪些道路可以通过,哪些不能通过,视觉同学在设计的时候由各个建筑物的坐标和空隙来决定,但是我们必须要处理好当前的情况。当主角走在比较狭窄的道路上时,视角的切换和位移可能会造成霉菌穿透、不应该出现的视角等问题,角色和建筑之间的跨模型可以通过设置碰撞属性来解决空气墙(包括地面)和角色模型的组合,这样两个模型就不会相交。对应的主要代码如下://设置场景的全局碰撞属性this.scene.collisionsEnabled=true...//遍历模型中的气墙节点,设置为检测碰撞if(mesh.id.toLowerCase().indexOf('wall')===0){mesh.visibility=0mesh.checkCollisions=trueobstacle.push(meshasMesh)}镜头可以避免进入建筑物,地面也可以通过设置碰撞属性,但是在障碍物位置在相机和角色之间的时候,比如拐角,会导致相机卡住,角色继续行走。所以思路是找到从角色位置朝向相机的射线,与空气墙相交的最近点,然后将相机移动到结果点的位置,使相机和障碍物不相交.在实际开发中,在镜头(下图中用球表示)和角色之间插入了3个六面体。当六面体与空气壁相交时,镜头的位置移动到离角色最近的六面体端点,发生穿插。将移动到角色头部的点。实际画面结合项目举例如下:默认镜头在默认镜头半径处的位置如下图所示:其中一个六面体与空气墙相交时,将相机移动到的位置碰撞六面体的终点如图所示:相机运动过程中,加上位置过渡动画,可以准确判断无级缩放的效果。穿插引用的是Babylonjs讨论区的一个帖子。Babylon内置的intersecMesh方法使用AABB框判断。地图功能“探索奇遇”中设计的街景比想象中略大,同时增加了“宝箱”游戏的趣味性。对于第一次参加活动的用户来说,他们对自己目前所在的位置没有任何期待。的。针对这种情况,设计增加了地图功能,在左上角显示入口,可以查看当前位置。具体如下:地图功能的实现依赖于视觉设计和当前位置的计算。涉及的主要环节如下:显示地图弹窗时,事件派发器触发事件??让场景将角色当前位置和方向写入地图弹窗存储,并显示角色标识到地图上的相应位置;处理地图与场景位置对应关系。首先找到地图对应场景的0点位置,然后根据场景大小和图像大小计算出大概的缩放值,最后微调得到准确对应的缩放值。exportfunctiontoggleShow(isShow?:boolean){//地图弹窗打开时,触发获取角色位置信息事件eventCenter.getInstance().trigger(EVENT_TYPES.GET_CHARACTER_POSITION)store.dispatch({type:EActionTypes.TOGGLE_SHOW,payload:{isShow}})}//场景捕捉时间将角色的位置和方向写入storethis.appEventCenter.on(EVENT_TYPES.GET_CHARACTER_POSITION,()=>{constpos=this.character?.position??{x:0,z:0}constrotation=this.character?.mesh.rotation.y??0if(pos){updatePos({x:-pos.x,y:pos.z,旋转:rotation-Math.PI})}})新手指南除了日常功能指南外,新手指南还增加了同类赛车游戏中特定建筑位置的路线指引。其理念是通过用户已经熟悉的游戏引导功能,降低新玩家的认知成本。在开发和实现方面,它被拆解成数学问题。当引导线的方向和终点固定时,引导线显示的长度为字符所在位置到引导线方向终点的向量投影长度,如下图坐标示例://使用向量点乘计算投影长度setProgress(val:number|Vector3){if(typeofval==='number'){this.progress=val}elseif(this.startPoint&&this.endPoint){conststartToEnd=this.endPoint.add(this.startPoint.negate())conststartToEndNormal=Vector3.Normalize(startToEnd)this.progress=1-Vector3.Dot(startToEndNormal,(this.endPoint.add(val.negate())))/startToEnd.length()}}bootstrap直线是一个平面,两端的渐变效果和动画使用自定义shader实现#vertexshaderuniformmat4worldViewProjection;uniformvec2uScale;#[1,引导线长度/引导线宽度]用于计算纹理vec的uv坐标属性4position;attributevec2uv;varyingvec2st;voidmain(void){gl_Position=worldViewProjection*position;st=vec2(uv.x*uScale.x,uv.y*uScale.y);}#fragmentshaderuniformsampler2DtextureSampler;uniformvec2uScale;#和vertexshaderuniformfloatuOffset;#textureoffset,把这个值改成实现动画uniformfloatuAlphaTransStart;#引导线起点透明渐变长度/引导线宽度uniformfloatuAlphaTransEnd;#引导线末端透明渐变长度/引导线宽度变化ingvec2st;voidmain(void){vec2rst=vec2(st.x,-st.y+uOffset);floatalphaEnd=smoothstep(0.,uAlphaTransEnd,st.y);floatalphaStart=smoothstep(uScale.y,uScale.y-uAlphaTransStart,st.y);floatalpha=alphaStart*alphaEnd;gl_FragColor=vec4(texture2D(textureSampler,rst).rgb,alpha);}性能优化在HTML中,3D模型是通过第三方库实现的渲染,而构建街景和多个场地确实是一笔巨大的开销记忆方面。在开发过程中,由于与视觉设计并行,前期没有发现异常。随着视觉逐渐提供模型文件,慢慢呈现出整体氛围,游戏过程中的卡顿和崩溃也出现了。通过对PC端开发环境内存的监控以及与AppWebview老大的沟通,我们提前进入了性能优化的讨论。目前整个项目有30个模型,单个文件平均大小在2M左右(街景最大7M)。因此,在性能优化方面,主要采用以下方法:控制纹理精度。在优化过程中,发现纹理数据占据了模型文件的大部分体积。经过和视觉的沟通和尝试,大部分贴图都控制在1Kx1K以下。一些纹理的大小为512x512。使用压缩纹理使用传统的jpg/png格式作为纹理文件会导致图像文件在浏览器图像缓存和GPU存储中都占用空间,增加页面的内存占用。如果内存占用一定大小,在IOS设备下会出现闪退,在Android设备下会出现卡顿和掉帧。使用压缩纹理格式可以在GPU存储中只保留一份图像缓存,大大减少了内存占用。具体操作实现:在项目中使用贴图压缩工具,将原始的jpg/png图片文件转换为pvrtc/astc等压缩贴图格式文件,并将模型的贴图文件等信息进行拆分,根据需要在不同的设备上使用以支持的程度加载不同格式的纹理文件。Bakehigh-resolutionmodeldetailstolow-resolutionmodels:将高精度模型的细节烘焙到纹理,然后将纹理应用到低精度模型,保证纹理的准确性,同时隐藏模型精度的缺陷尽可能多。视觉为“光”处理提供的“白色模型”初稿中,由于颜色和整体街景是过渡版,大家对渲染效果没有异议,但是导出整体颜色和贴图时同时,前端渲染结果与可视化烘焙导出预览有较大差异。通过仔细对比,以及在自己开发的3D素材管理平台上的预览对比,我们发现“光”的影响是非常大的。下图左右两边分别是加“光”和去掉“光”的情况:和视觉沟通,经过多次实验和尝试,最终解决方案如下:环境光加入HDR贴图,让场景获得更明亮的表现和反射信息;在相机的前进方向添加定向光,以增加整体亮度。对应外延优化:增加地面光反射,实现反光,提升街景氛围。GUI渲染清晰度项目中使用了Babylon自带的GUI层来展示图片,但是展示效果的清晰度太差,无法满足还原设计稿的要求。比如下图中的文字和返回箭头的锯齿:因为在默认配置下,3D画布的大小就是屏幕的显示分辨率,比如iPhone13是390x844,Babylon官方提供的方法是使用屏幕点的物理分辨率作为大小画布的大小,不仅可以点对点渲染GUI,而且场景的分辨率也更清晰;但该方案增加了整个项目的GPU渲染压力,对于本就紧张的资源,再使用该方案增加计算量并不容易。因此,我们修改,将渲染质量明显较差的GUI组件从3D绘图板中去除,改用DOM组件进行渲染。在使用相同资源的情况下,可以渲染按钮图像以达到设计稿所需的效果。后期处理效果如下:发光材质的处理在视觉设计的过程中,会在部分模型上使用自发光材质,让该模型影响周围的模型呈现出细腻的光影效果,如图在下面的视觉稿中:但是用于开发的框架并没有发光材料的应用层实现,当我们把模型放到页面中,在不设置光照的情况下,只有模型发光的部分可以被渲染,其他部分为黑色(如下图左侧);打光后整个模型是最亮的,没有任何光影效果(如下图右图):解决方法:配合设计师将受局部灯光影响的光影效果烘焙到材质贴图中,并只还原页面还原效果需要设置合理的灯位,如下图:结语感谢餐饮部营销运营团队和平台营销设计部创新的营销设计团队感谢他们的探索精神和支持。美食节于5月启动。《探味奇遇记》是对未来购物的一次尝试与探索,满足顾客对美好新奇未来的需求。让购物场景变得有趣,给顾客带来美妙的购物体验。在查看项目数据时发现,点击率和转化率数据均略高于同期。在这次落地3D技术的过程中,虽然踩了很多坑,但也收获颇多。作为开荒集团,我们确实离“Metaverse”的目标又近了一步。我相信我们的技术和产品会越来越成熟,也敬请期待团队的可视化3D剪辑工具!参考文章和链接探索文章链接横屏VS竖屏Babylonjs讨论帖