<script setup>
import { ref, getCurrentInstance, onMounted, beforeUnmount } from 'vue'
import * as BABYLON from '@babylonjs/core/Legacy/legacy' // 全部引入
import '@babylonjs/loaders' // 模型加载loader
import * as GUI from '@babylonjs/gui/2D' // 交互组件
const { proxy } = getCurrentInstance()
const emit = defineEmits(['customChange'])
let engine = ref(null)
let scene = ref(null)
let camera = ref(null)
// 模型加载进度百分比
let progress = ref(0)
// 是否完成模型渲染
let isRendering = ref(false)
// 是否展示视频
let showVideo = ref(false)
// 自适应渲染
const engineResize = ()=> {
engine.resize();
}
// 重置模型
const reset = ()=> {
scene.activeCamera.restoreState();
}
let activeModel = null;
let activeColor = null;
let sprite1 = null;
//Animation Camera position
const animateCameraToPosition = (speed, frameCount, newPos)=> {
let ease = new BABYLON.CubicEase();
ease.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
//BABYLON.Animation.CreateAndStartAnimation = function(name, mesh, targetProperty, framePerSecond, totalFrame, from, to, loopMode);
BABYLON.Animation.CreateAndStartAnimation('a1', scene.activeCamera, 'position', speed, frameCount, this.scene.activeCamera.position, newPos, 0, ease);
//scene.activeCamera.setPosition(newPos);
}
const animateCameraTargetToPosition = (speed, frameCount, newPos)=> {
let ease = new BABYLON.CubicEase();
ease.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
BABYLON.Animation.CreateAndStartAnimation('a2', scene.activeCamera, 'target', speed, frameCount, scene.activeCamera.target, newPos, 0, ease);
//scene.activeCamera.setTarget(newPos);
}
const onPointerDown =(e)=>{
let pickResult = scene.pick(scene.pointerX, scene.pointerY,
null, false, null, (p0, p1, p2, ray) => {
let p0p1 = p0.subtract(p1);
let p2p1 = p2.subtract(p1);
let normal = BABYLON.Vector3.Cross(p0p1, p2p1);
return (BABYLON.Vector3.Dot(ray.direction, normal) < 0);
});
if(pickResult.hit) {
if(pickResult.pickedMesh.name.includes("Box")){
// ** Disable all before apply Outline Render **
scene.meshes.forEach((mesh)=>{
if(mesh.name.indexOf("") != -1){
mesh.renderOverlay = false;
}
});
//let pbr = pickResult.pickedMesh.material
//pbr.albedoColor = new BABYLON.Color3(0.71, 0.67, 0.61) // 反射颜色
//pbr.metallic = 1 // 金属
//pbr.roughness = 0.5 // 粗糙
// Outline Render
let StudyArea = scene.getMeshByName(pickResult.pickedMesh.name);
StudyArea.renderOverlay = true;
let oldPivotTranslation = pickResult.pickedMesh.getBoundingInfo().boundingBox.centerWorld.clone();
let postion = new BABYLON.Vector3(oldPivotTranslation._x+30,oldPivotTranslation._y+30,oldPivotTranslation._z+30)
let speed1 = 500;
let speed2 = 500;
let frameCount = 200;
animateCameraToPosition(speed1, frameCount, postion);
animateCameraTargetToPosition(speed2, frameCount, oldPivotTranslation);
sprite1.size = 3;
sprite1.playAnimation(0, 6, true, 300);
sprite1.position = new BABYLON.Vector3(oldPivotTranslation._x,oldPivotTranslation._y+1.5,oldPivotTranslation._z);
// 选中模型
if (!activeModel) {
activeModel = pickResult.pickedMesh;
activeColor = new BABYLON.Color3(
...Object.values(pickResult.pickedMesh.material.albedoColor)
);
activeModel.material.albedoColor = new BABYLON.Color3(0, 1, 0.1);
}
if (activeModel && activeModel != pickResult.pickedMesh) {
// 选中新模式
activeModel.material.albedoColor = activeColor;
activeModel = pickResult.pickedMesh;
activeColor = new BABYLON.Color3(
...Object.values(pickResult.pickedMesh.material.albedoColor)
);
activeModel.material.albedoColor = new BABYLON.Color3(0, 1, 0.1);
}
}
} else if (activeModel) {
activeModel.material.albedoColor = activeColor;
activeModel = null;
activeColor = null;
sprite1.size = 0;
}
}
const onDispose = ()=>{
scene.onPrePointerObservable.removeCallback(onPointerDown());
}
let timer1 = null
onMounted(() => {
let canvas = document.getElementById('canvas');
// 初始化 BABYLON 3D engine
engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true, disableWebGL2Support: false });
// 自定义loading加载效果
function customLoadingScreen() {
console.log('customLoadingScreen creation');
}
customLoadingScreen.prototype.displayLoadingUI = function() {
console.log('customLoadingScreen loading')
};
customLoadingScreen.prototype.hideLoadingUI = function() {
window.document.getElementById('loadingScreen').style.display = 'none';
};
engine.loadingScreen = new customLoadingScreen();
// 初始化一个场景 scene
scene = new BABYLON.Scene(engine);
// 设置背景色透明
scene.clearColor = new BABYLON.Color4(0, 0, 0, 0);
// 初始化相机 camera
camera = new BABYLON.ArcRotateCamera('Camera', 0, 0, 0, new BABYLON.Vector3(0, 0, 0), scene);
// 启用框架行为
camera.useFramingBehavior = true;
// 聚焦半径
camera.framingBehavior.radiusScale = 1;
// 聚焦时间
camera.framingBehavior.framingTime = 3 * 1000;
camera.useBouncingBehavior = true;
// 将相机绑定到画布上面
camera.attachControl(canvas, true);
scene.onPrePointerObservable.add(this.onPointerDown,BABYLON.PointerEventTypes.POINTERDOWN);
scene.onDispose = onDispose();
/*
* 天空盒
*/
// 创建天空盒
const skybox = BABYLON.Mesh.CreateBox('skyBox', 21000, scene),
skyboxMaterial = new BABYLON.StandardMaterial('skyboxMaterial', scene);
// 关闭掉材质的背面剔除(在盒子内部也可以看到盒子)
skyboxMaterial.backFaceCulling = false;
// 删除盒子上的反射光(天空不会反射太阳)
skyboxMaterial.disableLighting = true;
// 载入天空贴图(CubeTexture是贴图加载器,只能被应用到reflectionTexture)
skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture('textures/sky', scene);
// 修改贴图模式(reflectionTexture是反射贴图,但我们需要天空盒贴图)
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
skybox.material = skyboxMaterial;
// 设置天空盒跟随相机位置移动(盒子不会收缩)
skybox.infiniteDistance = true;
/*
* 3D模型
*/
// 引入外部obj模型
BABYLON.SceneLoader.Append('babylon/1/', 'model.glb', scene, (object) => {
let meshes = object.meshes;
try {
meshes.map((mesh)=>{
//mesh.scaling = new BABYLON.Vector3(0.1, 0.1, 0.1);
// "冻结"网格
mesh.freezeWorldMatrix();
// 打开世界矩阵计算
mesh.unfreezeWorldMatrix();
// 不要求用户单击或选取网格体
mesh.isPickable = true;
mesh.doNotSyncBoundingInfo = true
})
} catch (e) {
console.log(e);
}
// 相机聚焦到网格
camera.setTarget(object);
// 设置默认相机和灯光
scene.createDefaultCameraOrLight(true, true, true);
const light = new BABYLON.HemisphericLight('light', new BABYLON.Vector3(1, 1, 1));
// 设置灯光亮度
light.intensity = 1;
// 镜面反射 漫反射 环境光颜色调整
light.diffuse = new BABYLON.Color3(1, 1, 1);
light.specular = new BABYLON.Color3(1, 1, 1);
// 渲染模型后调整相机角度、位置、观察对象的三维坐标
scene.activeCamera.alpha = 0.0239;
scene.activeCamera.beta = 1.51;
scene.activeCamera.radius = 51.9;
scene.activeCamera.setPosition(new BABYLON.Vector3(0, 90, 90));
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0, 0));
// 设置横向旋转角度上下限
scene.activeCamera.upperBetaLimit = Math.PI * 0.46;
scene.activeCamera.lowerBetaLimit = 0;
// 设置镜头到目标位置距离半径的最大值
scene.activeCamera.upperRadiusLimit = 150;
// 设置鼠标滚轮灵敏度(数值越小灵敏度越高)
scene.activeCamera.wheelPrecision = 10;
// 控制鼠标平移相机镜头灵敏度(数值越小灵敏度越高|为0的时候取消平移操作)
scene.activeCamera.panningSensibility = 200;
// 存储当前相机状态
scene.activeCamera.storeState();
// 也就是只有glb类型的文档才有,所以这个还是必须要用这个类型的材料
let animat1 = scene.getAnimationGroupByName('Action1');
let animat2 = scene.getAnimationGroupByName('Action2');
// animat1.stop() animat2.stop()
// animat1.pause() animat2.pause()
animat1.start()
animat2.start()
timer1 = setInterval(() => {
animat1.start()
animat2.start()
},3000);
const spriteManager = new BABYLON.SpriteManager("spriteManager", '../../../textures/img/location.png', 1, {width: 142, height: 145}, this.scene);
this.sprite1 = new BABYLON.Sprite("sprite1", spriteManager);
// 关闭自定义loading效果、展示标题、展示按钮
setTimeout(() => {
engine.hideLoadingUI();
emit('showTitle', true);
isRendering = true;
});
}, (progressEvent) => {
// 设置模型加载进度百分比
progress = (progressEvent.loaded / progressEvent.total).toFixed(0) * 100;
});
// 注册渲染循环 runRenderLoop
engine.runRenderLoop(() => {
scene.render();
});
// 在 DOM 更新后执行回调
nextTick(() => {
console.log('DOM 已更新');
// 注册resize监听事件
window.addEventListener('resize', engineResize);
});
})
beforeUnmount(() => {
// 离开页面销毁resize监听事件
window.removeEventListener('resize', engineResize, false);
clearInterval(timer1);
})
</script>
<template>
<div :class="isRendering ? 'containor bg' : 'containor'">
<div id="loadingScreen" class="flex_column_center">
<span class="loading"></span>
<span class="progress">{{ progress }}%</span>
<span class="text">3D模型加载中...</span>
</div>
<canvas id="canvas"></canvas>
<div class="btn_list flex_middle" v-if="isRendering">
<el-button type="warning" size="small" @click="reset"><i class="el-icon-refresh"></i> 重置</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
/*scrollbar styles*/
::-webkit-scrollbar {
width: 12px;
height: 12px;
// border-radius: 100px;
}
::-webkit-scrollbar-thumb {
// border-radius: 100px;
background: var(--color-ref-kl-primary10);
}
::-webkit-scrollbar-track-piece {
// border-radius: 100px;
background: transparent;
}
::-webkit-scrollbar-corner {
background: transparent;
}
/*scrollbar styles*/
#app {
height: 100%;
color: #4b4b4b;
font-size: 13px;
font-family: 'Microsoft YaHei';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.flex {
display: flex;
}
.flex_center {
@extend .flex;
align-items: center;
}
.flex_left {
@extend .flex_center;
justify-content: flex-start;
}
.flex_right {
@extend .flex_center;
justify-content: flex-end;
}
.flex_middle {
@extend .flex_center;
justify-content: center;
}
.flex_column {
@extend .flex;
flex-direction: column;
justify-content: center;
}
.flex_column_center {
@extend .flex_column;
align-items: center;
}
.public_radius {
border-radius: 8px;
}
.echarts {
height: 100%;
overflow: hidden;
}
.containor {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
&.bg {
background-color: #8ecbe3;
}
#loadingScreen {
position: absolute;
width: 100%;
height: 100%;
.loading {
display: inline-block;
position: relative;
width: 100px;
height: 100px;
border: 8px solid #0934f7;
border-radius: 50%;
animation: rotate 1s linear infinite;
&:after {
position: absolute;
left: 50%;
top: 50%;
width: 110px;
height: 110px;
content: '';
transform: translate(-50%, -50%);
border: 8px solid transparent;
border-bottom-color: #00eaff;
border-radius: 50%;
}
}
.progress {
margin-top: -60px;
color: #6be031;
font-size: 16px;
font-weight: 700;
}
.text {
margin-top: 60px;
color: #f5a327;
font-size: 14px;
}
}
canvas {
width: 100%;
height: 100%;
outline: none;
cursor: pointer;
}
.btn_list {
position: absolute;
bottom: 0;
width: 100%;
height: 50px;
z-index: 99;
button {
margin: 0 15px 0 0;
&:last-child {
margin: 0;
}
}
}
.video_main {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 800px;
height: 500px;
z-index: 999;
video {
outline: none;
}
}
}
</style>
Useful links
- Official web site: www.babylonjs.com
- Online playground to learn by experimentating
- Online sandbox where you can test your .babylon and glTF scenes with a simple drag'n'drop
- Online shader creation tool where you can learn how to create GLSL shaders
- 3DS Max exporter can be used to generate a .babylon file from 3DS Max
- Maya exporter can be used to generate a .babylon file from Maya
- Blender exporter can be used to generate a .babylon file from Blender 3d
- Unity 5 (deprecated) exporter can be used to export your geometries from Unity 5 scene editor(animations are supported)
- glTF Tools by KhronosGroup
"@babylonjs/core": "^5.24.0",
"@babylonjs/gui": "^5.24.0",
"@babylonjs/loaders": "^5.24.0",
"@babylonjs/materials": "^5.24.0",
"@babylonjs/post-processes": "^5.24.0",
"@babylonjs/procedural-textures": "^5.24.0",
"@babylonjs/serializers": "^5.24.0",
"@babylonjs/viewer": "^5.24.0",
参见:
Babylon.js: Powerful, Beautiful, Simple, Open - Web-Based 3D At Its Best