淘宝人生专属“小屋”——虚拟人和虚拟场景技术探索

9be3c8f9905f020261a13da30b828da1.gif

本文将分享来自淘宝人生团队齐力打造的新玩法——“人生小屋”背后的技术方案,详细介绍如何使用3D渲染引擎从0到1搭建虚拟场景、控制虚拟角色、实现人景交互、优化渲染效果等。笔者在其中主要负责了小屋3D室内场景的搭建部分。非常欢迎大家给我们提出建议,探讨交流。

78573cd8bb7eee821f246d04bb970863.png

项目背景

333051f1254ff53b0933d96d55ad40e4.png

淘宝人生,是一款手机淘宝App内可以玩转虚拟形象的轻游戏,包含了捏脸、穿搭、美颜、拍照等功能,逛街、家园等玩法。在它核心玩法日益成熟后,我们希望虚拟角色可以和虚拟场景进行交互互动,提供一些新的社交场景和玩法。因此,我们向虚拟人和虚拟场景方向展开了新的技术探索,开发“人生小屋”玩法。(上图展示了淘宝App中人生小屋的入口路径:搜淘宝人生跳转 -> 右侧去旅行 -> 人生小屋icon,欢迎大家前来体验!!

  1. 增加游戏的可玩性。给淘宝人生提供一个“家”的场景,在该场景中可以进行室内家具摆放、装饰,人与家具交互,成为用户串门的场所,提供新的社交场景和玩法。

  2. 丰富淘宝人生旅行玩法。

    通过旅行玩法,用户可获取家具材料,合成家具,在【家】中进行摆放,提升旅行玩法的游戏性和时长。

fa2991f23e87338f8a1c20058fdc7aa0.png

技术框架

420264f1f8a857c149a5d479917f8f2e.png

人生小屋基于我们团队的自研web3d渲染引擎Hilo3d开发,使用webgl渲染到前端的canvas画布上。笔者的主要工作内容即为绿框中的业务功能,概括来讲,可以划分为:室内场景搭建、角色管理、游戏逻辑开发 三大块。下面,将会分享在一步步搭建过程中,考虑以及采用的各个技术方案。

  室内场景

  • Step 1:加载室内场景

首先,从场景搭建开始。

27f89c4669a0b3392a407053c470b23a.png

如何加载房屋3D模型,整个过程概括来讲,大致经历这几个步骤:

  1. 首先使用一些3d软件,比如MAYA,制作出3d房屋模型,把过程产物暂存在OSS,并提供预览工具

  2. 给模型上贴图,用Photoshop制作贴图,把贴图上传至CDN

  3. 导出一个USD文件

  4. 使用houdini 程序化生成阴影和lod类型的gltf

  5. 我们使用GLTF作为我们的资产标准

  6. 最后,使用hilo3d 引擎,通过GLTF Importer导入glTF模型

579c82d56bac3a2572ae2a161e1b2bde.png


  • Step2:加载家具模型

同理,使用和房间模型相似的步骤,可以用同样的方式去加载各个家具的模型:

8e87a6662e794128adcdb12c8b948def.png

在加载了所需的各个模型后,开始搭建整个场景的结构:

初始化一个场景节点作为根节点,然后,把各个平面节点加入场景节点中,再把属于各个平面的家具放入平面节点中。之所以会按照平面先划分平面节点,是为了便于统一操作平面节点,比如在某些情况下,我们想将地板的家具全部隐藏,那么选择直接隐藏地板节点即可。

dee17be33f2a7e35c14e33730467f124.png


  • Step 3:加载相机

3d场景渲染的核心三要素:scene, camera, renderer ⇒ render the scene with camera

我们所看到的的场景内的画面,就是渲染器render根据新的相机对象参数渲染出场景图像,更简单的来讲,即将相机捕捉到的场景帧,通过渲染器渲染到画布上展示给用户。

相机选择

下一步,加载相机。在相机的选择方面,有两个最常用的相机:透视相机和正交相机。

正交相机:元素在屏幕上的大小与离相机的距离无关,按照正投影算法自动计算几何体的投影结果。

c60745bb2b73c93446841a08a5ebb0e1.png

fcad1d15d2d395c03001102968709b68.png

透视相机:透视相机看见的结果除了与几何体的角度有关,还和距离相关。透视相机能够更模拟出人眼所看到的世界,有一种近大远小的效果。比如你观察一条铁路距离越远你会感到两条轨道之间的宽度越小。在3D场景的渲染中,往往会使用透视投影照相机。

831d28bb66b7ed3c3a01c523666be62f.png

8876a007ab6a07d51679c00bc4c92c20.png

相机视场角FOV

值得一提的是透视投影相机中的视场角变量:

460c773495335ccaa4253d1a739c433e.jpeg

视场角:相机视锥体的两端的夹角。视场角的大小决定了视野范围,视场角越大,视野就越大。目标物体超过这个角就不会被收在镜头里。

4ba094ca2ffdb6d86e76031e856fcf2f.gif

相机控件OrbitControl

在选好了相机后,现在在画布上已经可以看见当前相机视角捕捉的3d室内场景了,但是这是一个静态的模型。我们下一个需求点,肯定是希望这个模型可以随着鼠标转起来,也就是我们鼠标左右移动,可以切换视角,看到房屋的各个角落。于是,我们引入orbitcontrol控件。

orbitControl可以使相机可以围绕目标进行轨道运动,以场景中心为中心,左右拖动屏幕会让镜头围绕着场景中心旋转,镜头始终会看着中心点。

它的原理是OrbitControls可以控制浏览器实时监控鼠标或键盘触发事件,比如鼠标左键发生拖动,那么拖动的距离就会改变相机的数据:比如位置和拍摄角度, 渲染器就会根据新的相机对象参数渲染,渲染出来的图像就会变化。

观察视频,我们有了新的需求点:当相机在房间顶部,俯视这个房间的时候,视野可不可以变大,可以看见整个房间,这就涉及到相机视野的变换。

相机视野变换:调整FOV

4950c633a941b10351d5483ccac6ba36.png

f2d3e455127f1b7728bacbcb1432b11f.png

“相机视场角FOV”中我们介绍过,当视场角FOV增加,相机的张角变大,视野也就会变大。因此,当相机移动到顶部俯视视角的时候,我们不妨将相机的视场角调整增大。对比一下当相机平视和相机俯视时的室内场景,可以发现,当视场角的增大,我们的视野也变大,可以让房间的各个角落清楚地进入我们的视野,解决了我们的需求。

2685e76792954bb28ebae8e9f4a24678.jpeg

由于我们使用的是webgl默认的右手坐标系,其实相机的上下移动,可以认为是绕着x轴的旋转角度。

因此,在实现的时候,我们把相机的fov和相机在x轴的旋转角度rotationX关联起来。

  • Step 4:特定mesh隐藏

至此,我们可以看一下我们加载的室内模型:

我们可能会提出一些疑问:

  1. 为什么相机旋转到墙壁背后的时候,墙壁自动被隐藏了?这是因为我们在搭建3d模型的时候,把墙壁建成了单面墙,也就是只能在墙壁的正面才能看见墙。

  2. 当我们的视角切换到入户门后时,但是这个门框并没有被隐藏?怎么也能隐藏掉门框?

9a65db85688db0c22f98ab37d30c463e.png

观察一下相机的位置。既然门框是垂直于z轴的,那么其实我们只要比较相机坐标在z轴上的投影和门框在z轴上的投影位置。当相机z坐标大于门框z坐标,隐藏门框的mesh即可。同理,其实可以根据相机的位置,去隐藏掉室内模型某些特定的mesh。

// 相机z坐标 > 门框z坐标,隐藏门框mesh

camera.worldPosition.z > doorEdge.worldPosition.z

在隐藏掉门框这些特定的mesh,优化了我们的体验后,我们不妨看看搭建出来的室内场景最终结果:

  角色控制

  • Step 1:加载虚拟人并使用摇杆操控

如何创建一个虚拟角色,这是一个有很多可以探究的领域,我们团队在第十六届D2论坛上分享过关于【虚拟偶像诞生记】的专题分享,如果大家感兴趣可以去更详细的了解一下,附上相关文章链接:《虚拟数字人行业现状和技术研究》

加载虚拟人后,我们使用joystick摇杆去操控人物在室内场景内的自由移动:

在这里,我们发现了问题:操控人物走动起来后,这个相机的视角没有转动,那么操控人物就能自由的走出我们的视野。


  • Step 2:相机跟随

因此,为了能让人物能始终在我们的视野中,我们让相机跟随人物节点移动。具体做法:

把场景的相机挂载到人物node节点上,以它为父节点。相对于人物节点,相机有一个固定的偏移量,让相机始终位于人物的正前方,并且距离人物一定的距离。那么当操控人物节点移动的时候,相机的节点也就会同步跟随移动,并且始终保持人物在最中心。

我们来看看最终实现的虚拟角色控制:

  游戏逻辑

在完成了室内场景加载和虚拟角色加载后,我们来到了游戏逻辑开发部分。首先,我们来实现对家具的操作,先来看看家具是如何选中的?

  • Step 1: 家具选中

我们考虑到了一种业界非常通用的方法:raycast光线投射。

raycast多用于实现物体选择或相交。用一句话概括来讲:通过三维空间中相机视点与鼠标在屏幕上的位置的连线,形成一条直线,捕获与此直线相交的空间中的物体,即为选中的物体。

具体计算步骤:

  1. 第一步,需要确定射线方向,也就是确定射线的起点(比如常用的相机世界原点)

  2. 然后确定射线终点:也就是鼠标点击处。计算点击像素点坐标是世界坐标中的位置

  3. 知道起点和方向就可以得到一条无限长的射线,使用点击的像素点的世界坐标减去相机位置,标准化后得到方向矢量(射线的方向)

  4. 再把射线先和检测物体的包围和求交,检测是否相交。

  5. 如果一条射线和多个物体相交,则把相交的物体按照深度排序返回。

所以怎么去选中家具?

在鼠标点击处投射出屏幕射线,检测物体为家具,返回与家具碰撞的结果

  • Step 2:家具拖拽移动

第二步: 如何能拖拽家具,在我们的小屋内进行移动?

最核心的点在于:如何得知鼠标拖拽位置的世界坐标?从而实现家具随着用户触摸点的移动而移动的效果(拖拽效果)。

我们同样可以使用raycast,创建出一条相机到点击处的射线,只是此时,检测物体变成了地板。那么,当鼠标/触摸点移动的时候,就可以获取到鼠标在地板上的具体位置,然后把家具中心移动过去,实现家具拖拽的效果。

  • Step 3:人物与家具碰撞方案

第三步,当人物移动后,人物与家具如何实现碰撞?

我们首先考虑了是否可以采用上一节所讲的raycast方法,上一节讲了raycast是一种检测碰撞相交的方案。更具体的来讲,在这种情况下,射线的发射源变成了人物,射线方向变成人物走动的方向,而检测物体变成了场景内摆放的所有家具或墙壁。以这种方法,我们可以检测到人物在走动过程中,与各个家具的碰撞情况。

2746e566cb070a18ec31ec2be6795986.png

然而,我们不得不考虑到一个问题:随着室内家具的逐渐增加,每次碰撞检测都会需要遍历检测物体,即场景内全部家具的节点-> 这会带来一个问题:性能不佳。经过我们实验,当室内有大量家具时,低端机达不到30fps。

因此,我们寻求了其他的方案:格子碰撞。

我们会以地砖格子为单位,使用二维数组去描述地砖信息,保存地砖上相应家具信息。

我们保存了一张看不见的逻辑层。这个层的大小和地板等大,并且也进行了格子划分,主要目的就是为了碰撞检测,使用一个数组描述信息。

我们会以地砖格子为单位,比如将地板划分为n*n个格子,使用二维数组去描述地砖信息,假设家具所占地砖面积为x*y,那么就在这一片的地砖上保存相应家具信息。用这种方式,我们就可以实现人物和家具的碰撞,检测地图中人物是否碰到了NPC或者障碍物。


  • Step 4:人物与家具交互

至此,我们的人物可以与家具发生碰撞了。下一步,碰撞到的家具若是床,若是椅子,我们自然希望我们的人物可以实现与家具的交互:上床躺下 / 椅子坐下。

于是,我们在家具可以交互的位置,比如床的两侧,添加了动作触发点。当人物走动到足够靠近家具的动作触发点,出现动作按钮,点击后,将以动画播放的形式,让人物实现上床的一系列动作。

  视效优化

最后,我们希望用阴影来增加人生小屋的视觉效果。

  • 传统的实时渲染

首先,了解一下传统的实时渲染是怎么做的?

4a82951891cd4fbe4b71f1d886258e8c.gif

假设说场景内有n个光源:

第一个光源,spotlight,也就是视频中地上这个圆圈的光源,它会对整个场景产生一个shadow map。

第二个光源,directional light,它也会产生一个shadow map。

......

若有n个光源,则有n个shadow map。

最终将n次渲染的阴影结果进行合并,每个mesh渲染的时候,都会读取n个shadow map。

这也就是为什么这个天猫的模型,它同时会拥有来自spot light和来自directional light这两个阴影。

因此实际上,阴影可以被认为是贴图的叠加。

然而,这种传统的实时渲染,大家可以预见到,由于每个mesh渲染都要读取n个shadow map,它会需要大量的计算和性能。

  • what is a shadow map?

因此,我们可以回过头看看对于Shadow map 阴影贴图的概念定义。所谓 ”贴图“,你可以想象成 ”一层层窗户纸“。

假设现在有一个窗户,你可以一层一层的粘贴不同透明度的窗户纸,每一层窗户纸都会叠加到之前的那一层,最终窗户纸所呈现的效果是所有窗户纸最终合并后一块呈现的效果。

  • 如何降低阴影所需性能?(小屋中家具阴影)

如前面讲的,传统的实时渲染会需要大量的计算和性能,有降低阴影所需性能的方案吗?

方案1:可以有多个灯光,但只有一个平行光可产生阴影

方案2:使用 光照贴图 或 环境光照遮挡贴图 来预先计算离线照明的效果 --- 比如:床缝间的这些家具自阴影

方案3:使用 假阴影,添加一个平面放到物体下方的地面上,同时赋予一个看着像阴影的纹理图片材质。具体做法,会添加一个面光源,置于家具的顶部,向下投影出家具底部的阴影。

6886d45dba6024a6615b6b9b1577ddf3.jpeg

c22fae8ed454f3fb3c0620be18eeed2f.jpeg

可以看出,在小屋中添加家具阴影时,对针对单个家具,进行离线渲染烘焙,因此大大降低了所需性能。

  • 小屋中人物阴影渲染

同理,我们在添加人物阴影时,可以在人物的头顶放一个面光源,投射到地面上,添加一个平面放到人下方的地面上,同时赋予一个看着像阴影的纹理图片材质。

然而,不同于家具阴影允许的离线渲染,人物阴影必须要求一个实时渲染。因为人物在各个动作、姿势、服饰变化的时候,底部的渲染阴影会随之变化。如何实现人物阴影实时渲染?我们使用离屏渲染Off-Screen Rendering。在这里不再非常详细展开,感兴趣的可以去了解一下GPU屏幕渲染的两种方式:

  1. On-Screen Rendering

  2. Off-Screen Rendering

4d4a376089c948480c456be416b9aa93.png

82f40fc82065b70c2115b911df8c2308.png

0444637621a262307686935c41f0e18c.png

7845e603166c407e6af83421f1ba4e9a.png

总结与展望

在这里先附上一个人生小屋DEMO的演示视频:

人生小屋初次上线后,未来我们将积极在更多的方向展开探索:

  1. 增强社交属性,提供更多样的社交玩法,好友聚会,通信交流。

  2. 提升视觉效果,增加全局光照离线烘焙,使光影效果更加真实。

欢迎大家持续关注我们淘宝人生!未来即将有更多玩法上线!

15be0897ef29efdf9bfbb1fb4d6e3d67.jpeg

参考文献

  1. 《物体的点击和碰撞》https://www.jianshu.com/p/7b0aba80cc59

  2. 《游戏常用算法之碰撞检测 地图格子算法实例详解》https://www.jb51.net/article/152638.htm

  3. 《渲染物体raycast picking拾取交互》https://zhuanlan.zhihu.com/p/129864543

  4. 《射线拾取、缓冲区拾取原理》:   https://juejin.cn/post/6988013072686252046

  5. 《mdn Touch events》: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events

  6. 《pointer input》: https://blog.csdn.net/keneyr/article/details/99076835

  7. 《Three.js基础之阴影》https://github.com/puxiao/threejs-tutorial/blob/main/13%20Three.js%E5%9F%BA%E7%A1%80%E4%B9%8B%E9%98%B4%E5%BD%B1.md

  8. 《cartoon-shading》:http://zhangwenli.com/blog/2017/03/05/cartoon-shading-1/

  9. 《opengl-matrix-transformations》http://zhangwenli.com/blog/2015/08/28/opengl-matrix-transformations/

8b2164444e932ed432314878f102959a.jpeg

团队介绍

我们是大淘宝互动前端团队,致力于开发淘宝内多款有趣的互动产品,包括淘金币、芭芭农场、淘宝人生、斗地主、小流浪旅舍等。此外,在每年的618或双11大促期间,我们还负责推出限定的大促玩法。我们拥有丰富的技术积累,涵盖2D互动技术、3D游戏技术以及前端工程化/低代码技术等多个领域。同时,这是一只充满活力和想象力的队伍,我们有对技术的追求和对生活的热爱,欢迎加入我们,研究更多好玩的互动,打造更好的互动产品。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

猜你喜欢

转载自blog.csdn.net/Taobaojishu/article/details/129576324
今日推荐