|
|
|
@ -1,9 +1,21 @@ |
|
|
|
<template> |
|
|
|
<div class="model3d-view"> |
|
|
|
<div class="toolbar"> |
|
|
|
<input type="file" @change="handleFileUpload" accept=".fbx,.obj,.mtl" /> |
|
|
|
<span>文件上传</span> |
|
|
|
</div> |
|
|
|
<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> |
|
|
|
<!-- 按 1:1 / 1:0.001 比例 --> |
|
|
|
<el-radio-group v-model="restate.loadScale" size="small"> |
|
|
|
<el-radio-button label="1:1" :value="1" /> |
|
|
|
<el-radio-button label="1:0.001" :value="0.001" /> |
|
|
|
</el-radio-group> |
|
|
|
</el-space> |
|
|
|
<div class="main-content"> |
|
|
|
<Split class="model3d-content" :direction="'horizontal'"> |
|
|
|
<SplitArea class="model3d-canvas" :size="70"> |
|
|
|
@ -19,12 +31,12 @@ |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
<script setup> |
|
|
|
import { ref, onMounted, nextTick } from 'vue' |
|
|
|
import { ref, onMounted, nextTick, reactive } from 'vue' |
|
|
|
import * as THREE from 'three' |
|
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' |
|
|
|
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' |
|
|
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' |
|
|
|
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' |
|
|
|
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader' |
|
|
|
import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js' |
|
|
|
import Stats from 'three/examples/jsm/libs/stats.module' |
|
|
|
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js' |
|
|
|
@ -38,8 +50,11 @@ const guiPanel = ref(null) |
|
|
|
// Three.js 场景相关 |
|
|
|
let scene, camera, renderer, controls |
|
|
|
let statsControls, axesHelper, gridHelper |
|
|
|
let modelGroup = new THREE.Group() |
|
|
|
let gui, transformControls |
|
|
|
let gui, tcontrols, modelGroup |
|
|
|
|
|
|
|
const restate = reactive({ |
|
|
|
loadScale: 1 |
|
|
|
}) |
|
|
|
|
|
|
|
// 状态变量 |
|
|
|
const state = { |
|
|
|
@ -101,37 +116,30 @@ function initThree() { |
|
|
|
// 光照 |
|
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6) |
|
|
|
scene.add(ambientLight) |
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8) |
|
|
|
directionalLight.position.set(10, 10, 10) |
|
|
|
scene.add(directionalLight) |
|
|
|
// const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8) |
|
|
|
// directionalLight.position.set(10, 10, 10) |
|
|
|
// scene.add(directionalLight) |
|
|
|
|
|
|
|
// 性能监控 |
|
|
|
statsControls = new Stats() |
|
|
|
statsControls.dom.style.position = 'absolute' |
|
|
|
viewerDom.appendChild(statsControls.dom) |
|
|
|
|
|
|
|
// 转换控制器 TransformControls |
|
|
|
transformControls = new TransformControls(camera, renderer.domElement) |
|
|
|
transformControls.attach(modelGroup) |
|
|
|
animate() |
|
|
|
|
|
|
|
// 监听控制模式变化事件 |
|
|
|
transformControls.addEventListener('change', () => { |
|
|
|
// 当变换控件发生变化时触发,用于更新渲染 |
|
|
|
// 转换控制器 TransformControls |
|
|
|
tcontrols = new TransformControls(camera, renderer.domElement) |
|
|
|
tcontrols.addEventListener('change', () => { |
|
|
|
renderer.render(scene, camera) |
|
|
|
reloadState() |
|
|
|
}) |
|
|
|
|
|
|
|
transformControls.addEventListener('dragging-changed', function(event) { |
|
|
|
// 在拖动开始或结束时禁用或启用轨道控制器 |
|
|
|
tcontrols.addEventListener('dragging-changed', function(event) { |
|
|
|
controls.enabled = !event.value |
|
|
|
}) |
|
|
|
|
|
|
|
// 动画循环 |
|
|
|
function animate() { |
|
|
|
requestAnimationFrame(animate) |
|
|
|
renderView() |
|
|
|
} |
|
|
|
|
|
|
|
animate() |
|
|
|
tcontrols.setMode('translate') |
|
|
|
// tcontrols.size = 1 |
|
|
|
// tcontrols.space = 'local' |
|
|
|
scene.add(tcontrols.getHelper()) |
|
|
|
|
|
|
|
// 创建ResizeObserver实例并传入处理函数 |
|
|
|
const resizeObserver = new ResizeObserver(handleResize) |
|
|
|
@ -139,6 +147,11 @@ function initThree() { |
|
|
|
resizeObserver.observe(viewerDom) |
|
|
|
} |
|
|
|
|
|
|
|
// 动画循环 |
|
|
|
function animate() { |
|
|
|
requestAnimationFrame(animate) |
|
|
|
renderView() |
|
|
|
} |
|
|
|
|
|
|
|
function initGUI() { |
|
|
|
const guiDom = guiPanel.value |
|
|
|
@ -146,6 +159,10 @@ function initGUI() { |
|
|
|
gui = new dat.GUI({ autoPlace: false }) |
|
|
|
guiDom.appendChild(gui.domElement) |
|
|
|
|
|
|
|
gui.add(state, 'mode', ['translate', 'rotate', 'scale']).onChange(function(value) { |
|
|
|
tcontrols.setMode(value) |
|
|
|
}) |
|
|
|
|
|
|
|
// 显示辅助线 |
|
|
|
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => { |
|
|
|
axesHelper.visible = val |
|
|
|
@ -166,6 +183,7 @@ function initGUI() { |
|
|
|
cameraPosFolder.add(state.camera.position, 'z', -100, 100).step(0.1).listen().onChange(val => { |
|
|
|
camera.position.z = val |
|
|
|
}) |
|
|
|
cameraPosFolder.close() |
|
|
|
|
|
|
|
// Rotation (in radians) |
|
|
|
const cameraRotationFolder = gui.addFolder('摄像机旋转角') |
|
|
|
@ -178,47 +196,47 @@ function initGUI() { |
|
|
|
cameraRotationFolder.add(state.camera.rotation, 'z', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
camera.rotation.z = val |
|
|
|
}) |
|
|
|
cameraRotationFolder.close() |
|
|
|
|
|
|
|
|
|
|
|
// Position |
|
|
|
const positionFolder = gui.addFolder('Position') |
|
|
|
positionFolder.add(state.obj.position, 'x', -10, 10).onChange(val => { |
|
|
|
positionFolder.add(state.obj.position, 'x', -10, 10).listen().onChange(val => { |
|
|
|
modelGroup.position.x = val |
|
|
|
}) |
|
|
|
positionFolder.add(state.obj.position, 'y', -10, 10).onChange(val => { |
|
|
|
positionFolder.add(state.obj.position, 'y', -10, 10).listen().onChange(val => { |
|
|
|
modelGroup.position.y = val |
|
|
|
}) |
|
|
|
positionFolder.add(state.obj.position, 'z', -10, 10).onChange(val => { |
|
|
|
positionFolder.add(state.obj.position, 'z', -10, 10).listen().onChange(val => { |
|
|
|
modelGroup.position.z = val |
|
|
|
}) |
|
|
|
positionFolder.close() |
|
|
|
|
|
|
|
// Rotation (in radians) |
|
|
|
const rotationFolder = gui.addFolder('Rotation') |
|
|
|
rotationFolder.add(state.obj.rotation, 'x', -Math.PI, Math.PI).onChange(val => { |
|
|
|
rotationFolder.add(state.obj.rotation, 'x', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
modelGroup.rotation.x = val |
|
|
|
}) |
|
|
|
rotationFolder.add(state.obj.rotation, 'y', -Math.PI, Math.PI).onChange(val => { |
|
|
|
rotationFolder.add(state.obj.rotation, 'y', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
modelGroup.rotation.y = val |
|
|
|
}) |
|
|
|
rotationFolder.add(state.obj.rotation, 'z', -Math.PI, Math.PI).onChange(val => { |
|
|
|
rotationFolder.add(state.obj.rotation, 'z', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
modelGroup.rotation.z = val |
|
|
|
}) |
|
|
|
rotationFolder.close() |
|
|
|
|
|
|
|
// Scale |
|
|
|
const scaleFolder = gui.addFolder('Scale') |
|
|
|
scaleFolder.add(state.obj.scale, 'x', 0.001, 10).onChange(val => { |
|
|
|
scaleFolder.add(state.obj.scale, 'x', 0.001, 10).listen().onChange(val => { |
|
|
|
modelGroup.scale.x = val |
|
|
|
}) |
|
|
|
scaleFolder.add(state.obj.scale, 'y', 0.001, 10).onChange(val => { |
|
|
|
scaleFolder.add(state.obj.scale, 'y', 0.001, 10).listen().onChange(val => { |
|
|
|
modelGroup.scale.y = val |
|
|
|
}) |
|
|
|
scaleFolder.add(state.obj.scale, 'z', 0.001, 10).onChange(val => { |
|
|
|
scaleFolder.add(state.obj.scale, 'z', 0.001, 10).listen().onChange(val => { |
|
|
|
modelGroup.scale.z = val |
|
|
|
}) |
|
|
|
|
|
|
|
gui.add(state, 'mode', ['translate', 'rotate', 'scale']).onChange(function(value) { |
|
|
|
transformControls.setMode(value) |
|
|
|
}) |
|
|
|
scaleFolder.close() |
|
|
|
|
|
|
|
// 相机位置 |
|
|
|
// const cameraFolder = gui.addFolder('相机位置') |
|
|
|
@ -284,35 +302,54 @@ function initMode3DCamera() { |
|
|
|
//设置相机位置 |
|
|
|
// cameraNew.position.set(4, 2, -3); |
|
|
|
// cameraNew.position.set(30, 30, 30) |
|
|
|
cameraNew.position.set(5, 5, 5) |
|
|
|
// 2.4,30,1.5; |
|
|
|
cameraNew.position.set(2.4, 20, 1.5) |
|
|
|
// cameraNew.position.set(2.4, 20, 1.5) |
|
|
|
//设置相机方向 |
|
|
|
// cameraNew.lookAt(0, 0, 0) |
|
|
|
cameraNew.lookAt(0, 0, 0) |
|
|
|
// -1.57,0,1.57 |
|
|
|
cameraNew.rotation.set(-1.57, 0, 1.57) |
|
|
|
// cameraNew.rotation.set(-1.57, 0, 1.57) |
|
|
|
|
|
|
|
camera = cameraNew |
|
|
|
scene.add(camera) |
|
|
|
|
|
|
|
const controlsNew = new OrbitControls( |
|
|
|
camera, |
|
|
|
renderer?.domElement |
|
|
|
) |
|
|
|
const controlsNew = new OrbitControls(camera, viewerDom) |
|
|
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移 |
|
|
|
controlsNew.enableDamping = false |
|
|
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 |
|
|
|
controlsNew.minDistance = 2 |
|
|
|
controls = controlsNew |
|
|
|
controls.update() |
|
|
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
|
|
state.camera.position.x = camera.position.x |
|
|
|
state.camera.position.y = camera.position.y |
|
|
|
state.camera.position.z = camera.position.z |
|
|
|
reloadState() |
|
|
|
} |
|
|
|
|
|
|
|
state.camera.rotation.x = camera.rotation.x |
|
|
|
state.camera.rotation.y = camera.rotation.y |
|
|
|
state.camera.rotation.z = camera.rotation.z |
|
|
|
function reloadState() { |
|
|
|
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 |
|
|
|
} |
|
|
|
if (modelGroup) { |
|
|
|
state.obj.position.x = modelGroup.position.x |
|
|
|
state.obj.position.y = modelGroup.position.y |
|
|
|
state.obj.position.z = modelGroup.position.z |
|
|
|
|
|
|
|
state.obj.rotation.x = modelGroup.rotation.x |
|
|
|
state.obj.rotation.y = modelGroup.rotation.y |
|
|
|
state.obj.rotation.z = modelGroup.rotation.z |
|
|
|
|
|
|
|
state.obj.scale.x = modelGroup.scale.x |
|
|
|
state.obj.scale.y = modelGroup.scale.y |
|
|
|
state.obj.scale.z = modelGroup.scale.z |
|
|
|
console.log('state.obj', state.obj) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function renderView() { |
|
|
|
@ -331,45 +368,127 @@ function renderView() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function handleTextureUpload(file) { |
|
|
|
console.log('file', file) |
|
|
|
if (!file) return |
|
|
|
|
|
|
|
file = file.raw |
|
|
|
|
|
|
|
const fileName = file.name.toLowerCase() |
|
|
|
const reader = new FileReader() |
|
|
|
reader.onerror = (error) => { |
|
|
|
system.showErrorDialog('加载文件失败', error.toString()) |
|
|
|
system.clearLoading() |
|
|
|
} |
|
|
|
|
|
|
|
if (fileName.endsWith('.png') || fileName.endsWith('.jpg') || fileName.endsWith('.jpeg')) { |
|
|
|
reader.onload = () => { |
|
|
|
// const textureLoader = new THREE.TextureLoader() |
|
|
|
// const texture = textureLoader.load(reader.result) |
|
|
|
// tcontrols.setTexture(texture) |
|
|
|
// system.clearLoading() |
|
|
|
const texture = new THREE.TextureLoader().load(reader.result) |
|
|
|
|
|
|
|
// 假设模型的所有网格都应用相同的材质 |
|
|
|
modelGroup.traverse(function(child) { |
|
|
|
if (child.isMesh) { |
|
|
|
child.material.map = texture |
|
|
|
child.material.needsUpdate = true |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
reader.readAsDataURL(file) |
|
|
|
} else { |
|
|
|
alert('不支持的文件类型!') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function handleFileUpload(event) { |
|
|
|
const file = event.target.files[0] |
|
|
|
function handleFileChange(file) { |
|
|
|
console.log('file', file) |
|
|
|
if (!file) return |
|
|
|
|
|
|
|
file = file.raw |
|
|
|
|
|
|
|
// 移除旧模型 |
|
|
|
if (modelGroup.children.length > 0) { |
|
|
|
modelGroup.children.forEach(child => modelGroup.remove(child)) |
|
|
|
if (modelGroup) { |
|
|
|
scene.remove(modelGroup) |
|
|
|
} |
|
|
|
|
|
|
|
const fileName = file.name.toLowerCase() |
|
|
|
const reader = new FileReader() |
|
|
|
reader.onerror = (error) => { |
|
|
|
system.showErrorDialog('加载文件失败', error.toString()) |
|
|
|
system.clearLoading() |
|
|
|
} |
|
|
|
|
|
|
|
if (fileName.endsWith('.fbx')) { |
|
|
|
reader.readAsArrayBuffer(file) |
|
|
|
system.showLoading() |
|
|
|
reader.onload = () => { |
|
|
|
const loader = new FBXLoader() |
|
|
|
const content = loader.parse(reader.result, '') |
|
|
|
modelGroup.add(content) |
|
|
|
const arrayBuffer = reader.result |
|
|
|
const content = loader.parse(arrayBuffer, '') |
|
|
|
modelGroup = content |
|
|
|
if (restate.loadScale) { |
|
|
|
modelGroup.scale.set(restate.loadScale, restate.loadScale, restate.loadScale) |
|
|
|
state.obj.scale.x = modelGroup.scale.x |
|
|
|
state.obj.scale.y = modelGroup.scale.y |
|
|
|
state.obj.scale.z = modelGroup.scale.z |
|
|
|
} |
|
|
|
scene.add(modelGroup) |
|
|
|
tcontrols.attach(modelGroup) |
|
|
|
|
|
|
|
controls.target.copy(modelGroup.position) |
|
|
|
controls.update() |
|
|
|
|
|
|
|
system.clearLoading() |
|
|
|
} |
|
|
|
reader.readAsArrayBuffer(file) |
|
|
|
|
|
|
|
} else if (fileName.endsWith('.obj')) { |
|
|
|
reader.readAsText(file) |
|
|
|
reader.onload = () => { |
|
|
|
const loader = new OBJLoader() |
|
|
|
const content = loader.parse(reader.result) |
|
|
|
modelGroup.add(content) |
|
|
|
//@ts-ignore |
|
|
|
modelGroup = loader.parse(reader.result) |
|
|
|
if (restate.loadScale) { |
|
|
|
modelGroup.scale.set(restate.loadScale, restate.loadScale, restate.loadScale) |
|
|
|
state.obj.scale.x = modelGroup.scale.x |
|
|
|
state.obj.scale.y = modelGroup.scale.y |
|
|
|
state.obj.scale.z = modelGroup.scale.z |
|
|
|
} |
|
|
|
scene.add(modelGroup) |
|
|
|
tcontrols.attach(modelGroup) |
|
|
|
|
|
|
|
system.clearLoading() |
|
|
|
} |
|
|
|
|
|
|
|
} else if (fileName.endsWith('.3ds')) { |
|
|
|
reader.onload = () => { |
|
|
|
const loader = new TDSLoader() |
|
|
|
const arrayBuffer = reader.result |
|
|
|
//@ts-ignore |
|
|
|
const content = loader.parse(arrayBuffer, '') |
|
|
|
modelGroup = content |
|
|
|
if (restate.loadScale) { |
|
|
|
modelGroup.scale.set(restate.loadScale, restate.loadScale, restate.loadScale) |
|
|
|
state.obj.scale.x = modelGroup.scale.x |
|
|
|
state.obj.scale.y = modelGroup.scale.y |
|
|
|
state.obj.scale.z = modelGroup.scale.z |
|
|
|
} |
|
|
|
scene.add(modelGroup) |
|
|
|
tcontrols.attach(modelGroup) |
|
|
|
|
|
|
|
system.clearLoading() |
|
|
|
} |
|
|
|
} else if (fileName.endsWith('.mtl')) { |
|
|
|
alert('需要同时上传 .obj 和 .mtl 文件,请先实现多文件上传处理逻辑。') |
|
|
|
reader.readAsArrayBuffer(file) |
|
|
|
|
|
|
|
} else { |
|
|
|
alert('不支持的文件类型!') |
|
|
|
} |
|
|
|
|
|
|
|
// modelGroup.position.set() |
|
|
|
// modelGroup.rotation.set(0, 0, 0) |
|
|
|
// modelGroup.scale.set(1, 1, 1) |
|
|
|
} |
|
|
|
|
|
|
|
window['state'] = state |
|
|
|
|
|
|
|
</script> |
|
|
|
<style scoped lang="less"> |
|
|
|
.model3d-view { |
|
|
|
@ -378,23 +497,24 @@ function handleFileUpload(event) { |
|
|
|
flex-grow: 1; |
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
.toolbar { |
|
|
|
padding: 10px; |
|
|
|
background: #f5f5f5; |
|
|
|
border-bottom: 1px solid #ccc; |
|
|
|
} |
|
|
|
|
|
|
|
.dialog-container { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
height: 100vh; |
|
|
|
} |
|
|
|
|
|
|
|
.toolbar { |
|
|
|
padding: 10px; |
|
|
|
background-color: #f0f0f0; |
|
|
|
border-bottom: 1px solid #ccc; |
|
|
|
} |
|
|
|
|
|
|
|
.main-content { |
|
|
|
display: flex; |
|
|
|
flex: 1; |
|
|
|
overflow: hidden; |
|
|
|
.model3d-content{ |
|
|
|
|
|
|
|
.model3d-content { |
|
|
|
height: 100%; |
|
|
|
display: flex; |
|
|
|
flex-direction: row; |
|
|
|
@ -403,7 +523,7 @@ function handleFileUpload(event) { |
|
|
|
|
|
|
|
.canvas-container { |
|
|
|
width: 100%; |
|
|
|
height:100%; |
|
|
|
height: 100%; |
|
|
|
position: relative; |
|
|
|
} |
|
|
|
|
|
|
|
@ -411,9 +531,10 @@ function handleFileUpload(event) { |
|
|
|
border-left: 1px solid #ccc; |
|
|
|
background: #111; |
|
|
|
width: 100%; |
|
|
|
height:100%; |
|
|
|
height: 100%; |
|
|
|
overflow-y: auto; |
|
|
|
:deep(.lil-gui.root){ |
|
|
|
|
|
|
|
:deep(.lil-gui.root) { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
} |
|
|
|
|