5 changed files with 378 additions and 1 deletions
@ -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 //相机放大半径上限 upperRadiusLimit的值不应小于lowerRadiusLimit,避免出现错误或不起作用。 |
||||
|
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> |
||||
Loading…
Reference in new issue