1
概述
开发背景
在当今高度数字化的企业环境中,提高员工工作效率和满足内部需求至关重要。为了适应快速变化的企业要求,以及为公司伙伴提供更加智能、高效的工作体验,我们灵犀项目组决定启动一个内部AI智能助手项目,旨在提高工作效率、改善员工的工作体验。
用户需求
我们开发这个智能助手将成为员工的数字伙伴,以流程化且标准化的方式执行各种工作任务,提供信息并提供支持,从而释放员工的时间和精力以便他们能专注于具有更大战略性的工作。
AI赋能:目前已经接入的ChatGPT受到了广泛的好评,帮助业务伙伴以更快速、更简便的方式享受到AI带来的工作效率提升。除此之外,我们还在计划中的“灵犀智库”项目也希望能将各个不同的系统和平台的知识,借助AI进行封装和存储于向量数据库中。而当面向最终用户时,他们只需要提出问题,我们就能帮他们找到最适合且能解答他们真正想知道的问题。
3D模型渲染:为了使用户感觉更加亲切有趣,提高用户的使用意愿,我们采用了3D的数字人,并打造了专门的“灵晓汐”IP,也希望可以给伙伴们真切的感受到我们是一个一直在成长完善的“数字伙伴”,而不是冷冰冰的流程机器。后续无论是在伙伴圈中,或者其他功能内,大家都将见到晓汐的身影。另外我们作为一个IM应用,在晓汐这里的3D模型渲染也将作为试点,后续我们也将考虑为每位伙伴生成自己的3D模型,请大家敬请期待吧。
灵晓汐整合了灵犀和人工智能两个团队的集体智慧,为伙伴们提供了很多便捷功能,整体结构如下图所示,本文重点分享关于前端的3D模型渲染方案。
2
方案选型
灵犀的客户端包括PC端(Mac和Windows)、移动端(安卓和iOS)。权衡效果和成本,我们最终决定采用H5方式进行渲染,以实现多平台的统一,这也方便了后续的维护和升级工作。
原生开发/electron开发 | H5页面 | |
开发成本 | 多套代码,移动端和PC端需要不同选型 | 一套代码,统一开发,统一维护 |
后续维护成本 | 更新需要发版 | 无需发版,随时上线 |
用户体验 | 更好,加载页面无需等待 | 次于原生,需要加载h5资源 |
在H5中,我们可以使用Canvas、SVG、WebGL等技术实现3D模型渲染。考虑到性能和兼容性,我们最终选择了使用WebGL技术。而在WebGL技术的实现方案中,例如Three.js、Babylon.js、Pixi.js等,我们最终选择了Three.js,它是一个成熟的3D渲染引擎,拥有活跃的社区和许多优秀的案例,对于我们的项目来说,Three.js相当合适。另外,Three.js的官网也提供了很多demo,可以直接拿来使用,非常方便。Three.js从2013年问世以来,到现在正好10年,近十万的star,还是很值得信赖的。Three.js官网在文末~
Three.js | Babylon.js | PixiJS | |
star | 94.6k | 21.4k | 41k |
npm 周下载 | 857k | 4465k | 84 |
版本数 | 273 | 132 | 12 |
issue | 387open 11543closed | 91open 2831closed | 169open 5082closed |
各自优点 | 支持多种效果、材质和光源,并且可以用于创建复杂的3D场景,更为灵活。 具有很多文档和教程,学习曲线相对较平。 它有广泛的社区支持,并且有许多已经构建的工具和插件。 | Babylon.js是以游戏开发为重点的库。 支持更多的特性和优化,包括物理引擎、碰撞检测和粒子系统。 它直接在库中内置了一些游戏开发的实用工具,如 character controller 和 audio engine。 | 专门用于创建2D WebGL图形的库,主要用于游戏和交互式网站。 提供许多高效率的2D渲染选项,包括sprites、filters和BlendModes |
擅长 | 全场景,专注渲染层 | 高特效,3D游戏 | 2D渲染,交互式网址 |
(至于官方文档是否完善等,可能主观性太强,我就没有列在表格对比中,不过个人感觉相比babylonjs,Three.js的官网更为完善)
另外由于我们基础技术栈是react,所以我们采用的是react-three-fiber,这个类库帮我们将Three.js中一些对象概念封成了组件形式供我们使用,更适合react开发伙伴们声明式开发的口味~(正常官网上原生开发Three.js,是面向过程,命令式开发)。另外再推荐一个高阶类库,帮我们封装了一些现成的天空环境啊~、镜头抖动啊等更复杂的方法。git链接附于文章末尾。
各位伙伴在react环境开发3d场景过程中,可以三个官网,切换着看👍换换口味(后两个是纯英文的),下面是个Three.js开发的地牢闯关的小游戏,很有意思(链接在文末)
3
技术要点
场景
Three.js其实是使用canvas画制的3D效果,场景我们可以理解为一个三维空间,他是所有3D对象的容器。所以我们加入的所有物体都需要有自己的x,y,z坐标。
对于不透明的一些物体,如果镜头和光源在镜头的两端(摄像头-物体-光),那么我们看到的物体就是没有光的,就像现实生活中的日全食一样。而如果是 光-摄像头-物体,则什么都不会发生,因为光源和摄像机都没有大小,他们都不会挡光。
在原生js中:
...
// 定义场景对象
const scene = new THREE.Scene();
// 定义一个摄像机
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
// 定义一个物体(material是材质,geometry是形状)
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
// 把物体,摄像机,加入场景~
scene.add( cube );
.....
同样的效果而在react-three-fiber中:
// 场景
<Canvas
//摄像头
camera={{fov: 75, near: 0.1, far: 1000}}
>
// 物体
<mesh>
// 形状
<boxGeometry args={[1, 1, 1]} />
// 材质
<meshStandardMaterial color={'0x00ff00'} />
</mesh>
</Canvas>
摄像头
摄像头分为两种:正交相机和透视相机
透视相机:近大远小,是最常用的相机,有四个常用参数,aspect(高宽比,默认为1),fav(视角,默认为50),near(能看到的最近距离,默认0.1),far(能看到的最远距离,默认2000)
注意!并不会像人眼一样,从模糊到消失,是突然消失的
正交相机:近远大小都一样,比较常见在2d渲染中,有六个常用参数,比透视相机少了视角和高宽比,但是多了left,right,top,bottom。和far与near一样,超出了就不再展示。
渲染器
渲染器是Three.js中的一个重要概念,他负责将场景中的3d对象渲染到屏幕上。渲染器包含多种参数及方法,比如设置渲染精度,阴影质量,调整曝光,清除缓存等。
更类似于生命周期,当我们在场景(scene)中加入光源(light),物体(mesh),摄像头(camera)后,我们调用渲染器(renderer)的渲染方法(renderer.render),界面上就会出现内容。
值得一提,在react-three-fiber中,我们不再需要手动定义渲染器及调用render方法了
在原生js中:
...
function animate() {
requestAnimationFrame( animate );
// 物体自动旋转,每次改变物体属性后,都再次调用render
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render( scene, camera );
}
animate();
...
光源
灯光决定了场景中的光照效果,一个场景中可以有多个光源,包括环境光,点光源,聚光灯,直射灯等等。其中点光源会产生较大阴影,直射光则相比较阴影更小,而环境光不会产生阴影,相当于光从四面八方打来。
图中效果依次为点光源,直射光,以及环境光。
除了环境光外,其余光源都有位置属性,所以有时候光源不生效可以有多种原因,比如场景中没有物体,或者光源放在了不透光物体中,物体的材质不透光/不反光等。
合理的配置场景中的光照效果能使我们的人物更加生动立体。比方说我们可以设置一个定时移动的点光源,模拟太阳的东升西落,人物产生的不同阴影效果。
...
useEffect(() => {
const angle = 0;
function lightMove() {
angle = angle + Math.Pi / 24;
setLightPosition([1000 * Math.sin(angle), 1000 * Math.cos(angle), 0])
}
// 每小时移动一次光源
setInterval(() => {
lightMove();
}, 1000 * 60 * 60);
}, []);
...
另外光源支持叠加,一个场景中支持多个光源,在晓汐中,我们采用了一个直接的点光源加上基础的自然光,来实现基础的效果
官方示例链接附于文末。
物体
这里就先略过基础模型(圆形,圆柱体,球等等了),直接快进到引入模型了。
上代码:
// 不同类型的模型需要使用不同的加载器,比如vrm模型就需要使用useGLTF加载器,fbx模型需要使用,useFBX加载器
import {useFBX, useAnimations} from '@react-three/drei';
const Modal = () => {
// 这里可以是在线地址或者是public下相对路径(在线地址要注意跨域问题)
const model = useFBX('/person.fbx');
// 允许跨域
model.crossOrigin = '';
// 我们可以遍历模型对象上的child,修改他们的材质,属性等
model.scene.traverse(child => {
.....
})
// Three.js有个优化策略,一旦物体有一部分不在屏幕内,为了优化内存,就直接不显示该物体了,加这个属性使其不生效
model.scene.frustumCulled = false;
// 拿到模型后我们就可以动画加载器取出其中的动画属性了(注意,不同类型的文件使用不同加载器拿到的对象不一致,比如vrm文件加载完后,下面第二个参数传的就是model.scene)
const {actions} = useAnimations(model.animations, model);
useEffect(() => {
const actionList = Object.values(actions);
// 随机取出一个动画
const actionNow = actionList[actionList.length * Math.random() | 0];
// 执行动画
actionNow.play();
}, [actions]);
}
另外,模型可以执行多个动画,互不矛盾,所以如果我们想从A动画举起左手,变为B动画举起右手,需要先停止A动画(stop),再去执行B动画,否则就成为法国军礼了~
另外动画上还有过度属性,A.crossFadeTo(B, 0.25),这样就允许我们从A动画在0.25秒内平滑的变为B动画。
下面就是同时执行多个动画的奇怪效果。
链接层
所谓链接层,即如何将AI与3D模型做沟通,我们这里是分为两步,先借用IM能力,将以自定义消息携带指令,由灵犀接收到对应的人物事件指令(比如找到答案的得意,找不到答案的委屈等)。
而后调用在h5页面提前注册好的动画方法,以AI返回的指令作为参数,驱动数字人进行相应动作,而对于一些其他的(比如点击数字人的害羞动作),直接在h5页面注册点击事件即可。
性能优化
另外,Three.js的优化一定要做好,不然用户体验很差,结合一些博客经验(其实Three.js还有一些其他问题,包括透明物体重叠等,但是由于无相关需求,我们并未遇到),以及我们内测期间伙伴们的反馈,主要的两个问题:加载时间长,以及性能消耗大(耗电量高、内存占用高、以及卡顿问题)。我们主要针对性的进行了以下处理。
3.7.1 前期等待
因为模型较大,用户需要等待时间很长,用户体验会很差,这从多个方面着手。
1、首先减少模型体积,可以减少模型的面数,使用默认材质,将模型分为多个部分分次加载等。
2、其次,加载中给用户一个提示或者有趣的loading等,我们现在在模型未加载出来前会使用小汐的初始化的照片放在这里。
3、最后也就是我们的王炸了,我们会在幻视页面初始化的时候监测是否在灵犀内打开,如果在灵犀内打开,我们使用本地模型,这样直接绕开了需要下载模型的问题。(移动端采用的是拦截url做替换,pc端使用的是h5页面放了一个钩子,在页面初始化时候使用钩子将本地url传给h5进行加载)。
3.7.2 内存占用
Three.js对GPU消耗很大,如果正常一个网页端打开多个Three.js页面聚焦,长时间使用会有卡及费电的问题。
除了减少模型大小外,我们现在在灵犀未聚焦的时候,直接销毁了模型,替换为展示静态图片。
另外要做好内存检测,防止内存泄漏问题,重中之重!销毁重新加载是一个万金油方法,但是对于不在使用的模型一定要及时销毁,哪怕即将销毁窗体。
4
总结
"晓汐"作为用于提升工作效率的内部AI助手项目,除了执行各种工作任务并提供信息和支持外,也希望可以提供给用户更“好玩、有趣“的使用体验。
项目选择使用H5渲染,适用于PC端和移动端,而渲染技术则选择了性能和兼容性良好的WebGL技术。在三款实现WebGL的方案中,最终选用了成熟且社区活跃的Three.js。并在此基础上使用了react-three-fiber库,将Three.js对象概念封装成组件形式,以便在react环境下使用。
需要特别提醒的是,虽然Three.js在3D模型渲染方面是一种非常成熟的解决方案,但是3D模型加载速度可能较慢,且占用的内存较大,因此我们需要结合实际的使用场景进行适度优化。
除了本文介绍的内容,如有其他疑问或者感兴趣的题目,我们欢迎大家随时向我们提问或者提出建议,希望可以与大家一起探索前进。
链接传送门
1、Three.js官网传送门:https://threejs.org/。
2、Three.js地牢闯关小游戏传送门:http://www.playkeepout.com/
3、@Three.js/drei库git地址:https://github.com/pmndrs/drei
4、Three.js光源效果:https://threejs.org/docs/index.html?q=light#api/en/lights/Light