2022年我们开始研究web3D的相关技术,并找到了蚂蚁开发的开发引擎oasis,具有组件化设计、移动优先、前端友好三大特点。并于2023年春运活动中,首次应用此引擎将3D模型引入运营活动web页中。
整个页面开发过程中,web3D的开发流程比较长,最主要的原因是由于web3D的学习成本比较高,需要了解很多基础知识,例如:模型、相机、坐标系、光照、动画等。基于这些基础知识,我们才可以去写代码并实现我们想要的功能。这些基础知识,在文档《web3D开发指北》一文中有详细介绍,同学们可以自行参阅。
为了缩短后续同学的学习成本,下面将总结学习过程中一些关键流程。
由UI同学设计提供,oasis首选3D文件类型为glTF,glTF 的产物一般分为(.gltf + .bin + png)或者 (.glb),前者适合图片体积大的场景,所以将图片和模型拆分开来,可以异步加载模型和纹理;后者适合模型文件较大的场景,会将所有数据进行二进制保存,需要等所有数据解析完毕才能展示模型。
模型示例:https://wos2.58cdn.com.cn/DeFazYxWvDti/frsupload/6bfe96d52087d560e63aa09ac0cb06cc.glb
oasis官网还提供了在线模型查看器:https://oasisengine.cn/#/gltf-viewer,可以直接上传查看模型具体形态及动画。
注意:模型文件大小关系页面的加载速度,如果是首屏加载3D模型,大小最好1M以内
oasis提供npm安装包,前端vue/react项目都可以方便得安装使用。不同版本代码略有不同,具体参考相应版本变更文档。
// 创建一个canvas标签,并设定id
<canvas id="canvas" style="width: 500px; height: 500px"/>
// 创建 WebGLEngine 实例
const engine = new WebGLEngine("canvas");
// 自动适配宽高
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
let cameraEntity = rootEntity.createChild('camera');
// 设置相机位置
cameraEntity.transform.position = new Vector3(0, 5, 10);
// 设置相机朝向位置
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));
光照类型可分为方向光、点光源、聚光灯、环境光四种
具体类型可参考文档:https://oasisengine.cn/#/docs/latest/cn/light
let lightEntity = rootEntity.createChild('light');
// 添加方向光
let directLight = lightEntity.addComponent(DirectLight)
directLight.color = new Color( 1.0, 1.0, 1.0 );
directLight.intensity = 0.5;
lightEntity.transform.rotation = new Vector3(45, 45, 45);
// 加载3D模型
const resource = 'https://wos2.58cdn.com.cn/DeFazYxWvDti/frsupload/6bfe96d52087d560e63aa09ac0cb06cc.glb';
engine.resourceManager.load(resource).then((gltf) => {
entity.addChild(gltf.defaultSceneRoot);
});
engine.run();
引擎启动后,这样在页面中我们就可以看到渲染的3D模型了。
加载完模型后,如果想要给模型加点动画怎么做呢?在 Oasis Engine 中,功能以组件形式添加到实体上,其中脚本组件提供了最灵活的扩展能力。例如让模型旋转、缩放、坐标移动等。
以给定模型增加旋转功能举例:
// 定义动画脚本
class Rotate extends Script {
onUpdate() {
// 逐帧调用,模型实体围绕Y轴旋转1度
this.entity.transform.rotate(new Vector3(0, 1, 0));
}
}
// 给特定模型中挂载脚本
engine.resourceManager.load(resource).then((gltf) => {
const model = gltf.defaultSceneRoot;
entity.addChild(model);
model.addComponent(Rotate);
});
那么脚本中的onUpdate是什么呢?
onUpdate属于脚本的生命周期中的其中一个回调函数,意思是逐帧调用,跟window.requestAnimationFrame作用一样,每帧旋转1度,连续执行下就会看到不停旋转的动画效果。
脚本的整个生命周期如下图所示,可以根据需要添加不同的回调方法:
web中我们可以监听鼠标事件来操作交互,那我们如何与3D场景进行交互呢?
oasis引擎也提供了一套相应的实体类来控制模型,比如说鼠标与触控、键盘操作等,需要结合脚本来监听控制。
oasis引擎还抹平了MouseEvent与TouchEvent的区别,统一使用PointEvent,提供了多个触控相关事件,可结合实际业务使用:
接口 | 触发时机与频率 |
---|---|
onPointerEnter | 当触控点进入 Entity 的碰撞体范围时触发一次 |
onPointerExit | 当触控点离开 Entity 的碰撞体范围时触发一次 |
onPointerDown | 当触控点在 Entity 的碰撞体范围内按下时触发一次 |
onPointerUp | 当触控点在 Entity 的碰撞体范围内松开时触发一次 |
onPointerClick | 当触控点在 Entity 的碰撞体范围内按下并松开,在松开时触发一次 |
onPointerDrag | 当触控点在 Entity 的碰撞体范围内按下时持续触发,直至触控点解除按下状态 |
比如鼠标点击模型时,随机改变模型颜色:
class ClickScript extends Script {
private material: BlinnPhongMaterial;
onStart() {
this.material = <BlinnPhongMaterial>this.entity.getComponent(MeshRenderer).getInstanceMaterial();
}
// 点击时将模型实体材质颜色随机变化
onPointerClick() {
this.material.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0);
}
}
对于整个场景的光标位置信息,也有专门InputManager来管理。通过调用相关的方法可以获取当前全局的光标信息以及键位操作记录。
方法名称 | 方法释义 |
---|---|
pointers | 返回当前活跃的光标 |
isPointerHeldDown | 返回这个光标按键是否被持续按住 |
isPointerDown | 返回当前帧是否按下过此光标按键 |
isPointerUp | 返回当前帧是否抬起过此光标按键 |
比如鼠标在3D场景内滑动时,获取鼠标当前坐标
class TouchScript extends Script {
onUpdate(){
const input = this.engine.inputManager;
// 判断是否触控了屏幕,并获取按下坐标
if(input.isPointerDown(window.OasisEngine.PointerButton.Primary)){
let startX = input.pointerPosition.x;
let startY = input.pointerPosition.y;
}
// 判断是否触控持续按住屏幕,并获取当前坐标
if(input.isPointerHeldDown(window.OasisEngine.PointerButton.Primary)){
let PosX = input.pointerPosition.x;
let PosY = input.pointerPosition.y;
}
}
}
模型过大会使得加载变慢,此时将模型拆解为小文件有助于同时加载提高速度。另外oasis引擎打包跟业务打包到一起,也会使js变大,可以使用单独的js压缩包。
模型加载时,可以添加loading动画展示,模型加载完成后的回调中隐藏loading。如果需要加载多个模型,可以使用promise.all等所有模型加载后再隐藏loading。
oasis引擎也提供了2D图片的加载,与3D模型加载相比,load参数的类型不同。
// 3D模型加载
this.engine.resourceManager.load<GLTFResource>("test.gltf")
.then(gltf=>{});
// 2D图片加载
this.engine.resourceManager.load<Texture2D>("test.png")
oasis引擎并没有直接提供lottie的加载方法,需要通过额外的插件包@oasis-engine/lottie 来完成lottie文件的加载展示。
注意:目前仅支持 Lottie 节点树中的精灵元素(Sprite Elements)的绘制
import { LottieAnimation } from "@oasis-engine/lottie";
// 加载lottie文件
engine.resourceManager.load({
urls: [
"https://gw.alipayobjects.com/os/bmw-prod/b46be138-e48b-4957-8071-7229661aba53.json",
"https://gw.alipayobjects.com/os/bmw-prod/6447fc36-db32-4834-9579-24fe33534f55.atlas"
],
type: 'lottie'
}).then((lottieEntity) => {
// 将lottie实例挂载到场景中
root.addChild(lottieEntity);
// 获取lottie组件并播放
const lottie = lottieEntity.getComponent(LottieAnimation);
lottie.isLooping = true;
lottie.speed = 1;
lottie.play();
});
web3D通过oasis引擎进行开发,学习难度并不算高,相信有了以上主要流程介绍,大家都可以很快上手操作。
oasis将各种资源都作为实例进行加载,并需要挂载到整体场景的根节点上。并通过脚本的不同生命周期回调函数,对内部模型实例进行操控,实现不同的动画效果。
oasis还提供基础的触控函数以及鼠标实例,可以监听光标与模型/场景的动作,来完成一些交互需求。
另外,oasis还提供了非常丰富的模型材质与光照设置,及物体碰撞等监控,属于web3D场景中比较复杂的内容了,有兴趣的同学可以深入研究下。
【推荐阅读】