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