Browse Source

babylon.js 引入

master
修宁 7 months ago
parent
commit
6b5fdc3dad
  1. 1
      package.json
  2. 3
      pnpm-lock.yaml
  3. 16
      src/designer/menus/Model3DView.ts
  4. 357
      src/designer/model3DView/Model3DViewBabylonJs.vue
  5. 2
      tsconfig.app.json

1
package.json

@ -40,6 +40,7 @@
"@babylonjs/core": "^8.9.0",
"@babylonjs/inspector": "^8.9.0",
"@babylonjs/loaders": "^8.9.0",
"@babylonjs/materials": "^8.9.0",
"@element-plus/icons-vue": "^2.3.1",
"@rolldown/pluginutils": "1.0.0-beta.8-commit.56abf23",
"@tsconfig/node22": "^22.0.1",

3
pnpm-lock.yaml

@ -87,6 +87,9 @@ importers:
'@babylonjs/loaders':
specifier: ^8.9.0
version: 8.9.0(@babylonjs/core@8.9.0)(babylonjs-gltf2interface@8.9.0)
'@babylonjs/materials':
specifier: ^8.9.0
version: 8.9.0(@babylonjs/core@8.9.0)
'@element-plus/icons-vue':
specifier: ^2.3.1
version: 2.3.1(vue@3.5.14(typescript@5.8.3))

16
src/designer/menus/Model3DView.ts

@ -1,5 +1,6 @@
import { defineMenu } from '@/runtime/DefineMenu.ts'
import Model3DView from '@/designer/model3DView/Model3DView.vue'
import Model3DViewBabylonJs from '@/designer/model3DView/Model3DViewBabylonJs.vue'
export default defineMenu((menus) => {
menus.insertChildren('tool',
@ -21,6 +22,21 @@ export default defineMenu((menus) => {
dialogClass: 'model-3d-view-wrap'
})
}
},
{
name: 'model3dviewBabylon', label: '模型查看器 (BabylonJs)', order: 2,
click: () => {
system.showDialog(Model3DViewBabylonJs, {
title: '模型查看器 BabylonJs',
width: 950,
height: 400,
showClose: true,
showMax: true,
showCancelButton: false,
showOkButton: false,
dialogClass: 'model-3d-view-wrap'
})
}
}
]
)

357
src/designer/model3DView/Model3DViewBabylonJs.vue

@ -0,0 +1,357 @@
<template>
<div class="model3d-view">
<el-space :gutter="10" class="toolbar">
<el-upload :on-change="handleFileChange"
:show-file-list="false" accept=".fbx,.obj,.mtl,.3ds" action="" :auto-upload="false">
<el-button type="primary">打开模型</el-button>
</el-upload>
<el-upload :on-change="handleTextureUpload"
:show-file-list="false" accept=".png,.jpg,.jpeg" action="" :auto-upload="false"
list-type="picture">
<el-button>打开贴图</el-button>
</el-upload>
<el-upload :on-change="handleMtlUpload"
:show-file-list="false" accept=".mtl" action="" :auto-upload="false"
list-type="picture">
<el-button>打开材质</el-button>
</el-upload>
<div class="demo-color-block">
<span class="demonstration">材质颜色</span>
<el-color-picker v-model="restate.targetColor" />
</div>
<!-- 1:1 / 1:0.001 比例 -->
<div class="demo-color-block">
<span class="demonstration">默认比例</span>
<el-radio-group v-model="restate.loadScale">
<el-radio-button label="1" :value="1" />
<el-radio-button label="0.01" :value="0.01" />
<el-radio-button label="0.001" :value="0.001" />
</el-radio-group>
</div>
<div class="demo-color-block">
<span class="demonstration">物体数:<el-text type="danger">{{ restate.objects }}</el-text></span>
<span class="demonstration"> 顶点数:<el-text type="danger">{{ restate.vertices }}</el-text></span>
<span class="demonstration"> 三角形数:<el-text type="danger">{{ restate.faces }}</el-text></span>
</div>
</el-space>
<div class="main-content">
<Split class="model3d-content" :direction="'horizontal'">
<SplitArea class="model3d-canvas" :size="70">
<!-- Three.js 渲染画布 -->
<div class="canvas-container" ref="canvasContainer">
<canvas ref="canvasRef"></canvas>
<div class="canvas-left-toolbar">
<button class="Button" :class="{selected:(restate.mode==='translate')}" title="平移"
@mousedown="restate.mode='translate'">
<component :is="renderIcon('element Rank')" />
</button>
<button class="Button" :class="{selected:(restate.mode==='rotate')}" title="旋转"
@mousedown="restate.mode='rotate'">
<component :is="renderIcon('element RefreshLeft')" />
</button>
<button class="Button" :class="{selected:(restate.mode==='scale')}" title="缩放"
@mousedown="restate.mode='scale'">
<component :is="renderIcon('element ScaleToOriginal')" />
</button>
</div>
</div>
</SplitArea>
<SplitArea class="model3d-gui" :size="30">
<div class="model3d-gui-wrap">
<!-- 右侧面板 -->
<div class="gui-toolbar">
<TransformEdit ref="transformEditCtl" />
</div>
<div class="gui-panel" ref="guiPanel"></div>
</div>
</SplitArea>
</Split>
</div>
</div>
</template>
<script setup lang="ts">
import TransformEdit from '@/components/propertyEdit/TransformEdit.vue'
import { ref, onMounted, nextTick, reactive, watch, getCurrentInstance, onUnmounted, onBeforeUnmount } from 'vue'
import '@babylonjs/core/Debug/debugLayer'
import '@babylonjs/inspector'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
HemisphericLight,
MeshBuilder,
Color4,
Camera,
GizmoManager,
DirectionalLight
} from '@babylonjs/core'
import { TransformNode } from '@babylonjs/core'
import Split from '@/components/split/split.vue'
import SplitArea from '@/components/split/split-area.vue'
import { renderIcon } from '@/utils/webutils.js'
import { GridMaterial } from '@babylonjs/materials'
// DOM refs
const canvasRef = ref(null)
// Three.js
let engine: Engine
let scene: Scene
let camera: ArcRotateCamera
let gizmoManager: GizmoManager
let currentMesh = null
let modelGroup, resizeObserver
const restate = reactive({
targetColor: '#ff0000',
loadScale: 1,
mode: 'translate',
objects: 0,
vertices: 0,
faces: 0
})
//
const state = {
showAxesHelper: true,
showGridHelper: true,
camera: {
position: { x: 0, y: 5, z: 10 },
rotation: { x: 0, y: 0, z: 0 }
}
}
onMounted(() => {
nextTick(() => {
initBabylon()
const viewerDom = canvasRef.value
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(viewerDom)
handleResize()
window['model3dView'] = getCurrentInstance()
window['engine'] = engine
window['scene'] = scene
window['camera'] = camera
window['gizmoManager'] = gizmoManager
})
})
onBeforeUnmount(() => {
cleanupThree()
const viewerDom = canvasRef.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
window['model3dView'] = null
window['model3dViewRenderer'] = null
})
watch(() => restate.targetColor, (newVal) => {
// if (modelGroup) {
// modelGroup.traverse((child) => {
// if (child.isMesh && child.material?.color) {
// child.material.color.set(newVal)
// child.material.needsUpdate = true
// }
// })
// }
})
watch(() => restate.mode, (newVal) => {
// if (tcontrols) {
// tcontrols.setMode(newVal)
// }
})
function initBabylon() {
//
engine = new Engine(canvasRef.value, true)
scene = new Scene(engine)
// 线
const ground = MeshBuilder.CreateGround('ground', { width: 25, height: 25 })
ground.material = new GridMaterial('groundMat')
ground.material.backFaceCulling = false
camera = new ArcRotateCamera('ArcRotateCamera', 1, 0.8, 10, new Vector3(0, 0, 0), scene)
// camera.panningInertia = 0
camera.attachControl(true, true, 0)
camera.lowerRadiusLimit = 2 //
camera.upperRadiusLimit = 10 // upperRadiusLimitlowerRadiusLimit
camera.wheelDeltaPercentage = 0.09 //
camera.checkCollisions = true //
camera.upperBetaLimit = (Math.PI / 2) * 0.9 // beta
camera.lowerRadiusLimit = 0.1 //
camera.upperRadiusLimit = 1000 //
camera.radius = 1 //
camera.setTarget(Vector3.Zero()) //
// camera.panningSensibility = 1 //
//
new HemisphericLight('light1', new Vector3(1, 1, 0), scene)
const directionalLight = new DirectionalLight(
'dirLight',
new Vector3(-1, -1, -1),
scene
)
// Gizmo
gizmoManager = new GizmoManager(scene)
gizmoManager.usePointerToAttachGizmos = false
// //
const box = MeshBuilder.CreateBox('box', {})
box.position = new Vector3(1, 2, 5)
engine.runRenderLoop(() => {
scene.render()
})
}
/**
* 重新加载相机状态到全局状态
*/
function syncCameraState() {
if (camera) {
state.camera.position.x = camera.position.x
state.camera.position.y = camera.position.y
state.camera.position.z = camera.position.z
state.camera.rotation.x = camera.rotation.x
state.camera.rotation.y = camera.rotation.y
state.camera.rotation.z = camera.rotation.z
}
}
function handleTextureUpload(file) {
}
let lastObjfile = undefined
function handleMtlUpload(file) {
}
function addGroupToScene(group) {
}
function handleFileChange(file) {
}
function cleaupModel() {
}
function cleanupThree() {
//
}
function handleResize() {
if (canvasRef.value) {
const width = canvasRef.value.clientWidth
const height = canvasRef.value.clientHeight
if (engine) {
engine.resize()
engine.setSize(width, height)
}
}
}
</script>
<style scoped lang="less">
.model3d-view {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
.canvas-left-toolbar {
position: absolute;
left: 5px;
top: 5px;
width: 32px;
background: #eee;
text-align: center;
button {
color: #555;
background-color: #ddd;
border: 0;
margin: 0;
padding: 5px 8px;
font-size: 12px;
text-transform: uppercase;
cursor: pointer;
outline: none;
}
button.selected {
background-color: #fff;
}
}
.toolbar {
padding: 10px;
background: #f5f5f5;
border-bottom: 1px solid #ccc;
}
.dialog-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
.model3d-content {
height: 100%;
display: flex;
flex-direction: row;
}
}
.canvas-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
canvas {
width: 100%;
height: 100%;
touch-action: none;
}
}
.model3d-gui-wrap {
border-left: 1px solid #ccc;
background: #111;
width: 100%;
height: 100%;
overflow-y: auto;
}
.gui-panel {
:deep(.lil-gui.root) {
width: 100%;
}
}
}
</style>

2
tsconfig.app.json

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "esnext",
"target": "es6",
"module": "esnext",
"moduleResolution": "node",
"jsx": "preserve",

Loading…
Cancel
Save