You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
194 lines
4.8 KiB
194 lines
4.8 KiB
<template>
|
|
<div class="model3d-view">
|
|
<div class="toolbar">
|
|
<input type="file" @change="handleFileUpload" accept=".fbx,.obj,.mtl" />
|
|
<span>文件上传</span>
|
|
</div>
|
|
<div class="main-content">
|
|
<!-- Three.js 渲染画布 -->
|
|
<div class="canvas-container" ref="canvasContainer"></div>
|
|
|
|
<!-- 右侧面板 -->
|
|
<div class="gui-panel" ref="guiPanel"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup>
|
|
import { ref, onMounted } 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 * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js'
|
|
|
|
// DOM refs
|
|
const canvasContainer = ref(null)
|
|
const guiPanel = ref(null)
|
|
|
|
// Three.js 场景相关
|
|
let scene, camera, renderer, controls
|
|
let modelGroup = new THREE.Group()
|
|
let gui
|
|
|
|
// 状态变量
|
|
const state = {
|
|
autoRotate: false,
|
|
showAxesHelper: true,
|
|
showGridHelper: true,
|
|
cameraPosition: [0, 5, 10]
|
|
}
|
|
|
|
onMounted(() => {
|
|
initThree()
|
|
initGUI()
|
|
})
|
|
|
|
function initThree() {
|
|
// 场景
|
|
scene = new THREE.Scene()
|
|
scene.background = new THREE.Color(0xeeeeee)
|
|
|
|
// 相机
|
|
camera = new THREE.PerspectiveCamera(
|
|
75,
|
|
window.innerWidth / 2,
|
|
0.1,
|
|
1000
|
|
)
|
|
camera.position.set(...state.cameraPosition)
|
|
|
|
// 渲染器
|
|
renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8)
|
|
canvasContainer.value.appendChild(renderer.domElement)
|
|
|
|
// 控制器
|
|
controls = new OrbitControls(camera, renderer.domElement)
|
|
|
|
// 辅助线
|
|
const axesHelper = new THREE.AxesHelper(5)
|
|
scene.add(axesHelper)
|
|
const gridHelper = new THREE.GridHelper(20, 20)
|
|
scene.add(gridHelper)
|
|
|
|
// 光照
|
|
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)
|
|
|
|
// 动画循环
|
|
function animate() {
|
|
requestAnimationFrame(animate)
|
|
if (state.autoRotate) {
|
|
modelGroup.rotation.y += 0.01
|
|
}
|
|
controls.update()
|
|
renderer.render(scene, camera)
|
|
}
|
|
|
|
animate()
|
|
|
|
// 调整窗口大小
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = canvasContainer.value.clientWidth / window.innerHeight
|
|
camera.updateProjectionMatrix()
|
|
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8)
|
|
})
|
|
}
|
|
|
|
function initGUI() {
|
|
gui = new dat.GUI({ autoPlace: false })
|
|
guiPanel.value.appendChild(gui.domElement)
|
|
|
|
// 自动旋转
|
|
gui.add(state, 'autoRotate').name('自动旋转')
|
|
|
|
// 显示辅助线
|
|
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => {
|
|
scene.getObjectByName('AxesHelper').visible = val
|
|
})
|
|
|
|
gui.add(state, 'showGridHelper').name('显示网格').onChange(val => {
|
|
scene.getObjectByName('GridHelper').visible = val
|
|
})
|
|
|
|
// 相机位置
|
|
const cameraFolder = gui.addFolder('相机位置')
|
|
cameraFolder.add(state.cameraPosition, 0, -10, 10).name('X')
|
|
cameraFolder.add(state.cameraPosition, 1, -10, 10).name('Y')
|
|
cameraFolder.add(state.cameraPosition, 2, 5, 30).name('Z').onChange(() => {
|
|
camera.position.set(...state.cameraPosition)
|
|
})
|
|
cameraFolder.open()
|
|
}
|
|
|
|
function handleFileUpload(event) {
|
|
const file = event.target.files[0]
|
|
if (!file) return
|
|
|
|
// 移除旧模型
|
|
if (modelGroup.children.length > 0) {
|
|
modelGroup.children.forEach(child => modelGroup.remove(child))
|
|
}
|
|
|
|
const fileName = file.name.toLowerCase()
|
|
const reader = new FileReader()
|
|
|
|
if (fileName.endsWith('.fbx')) {
|
|
reader.readAsArrayBuffer(file)
|
|
reader.onload = () => {
|
|
const loader = new FBXLoader()
|
|
const content = loader.parse(reader.result, '')
|
|
modelGroup.add(content)
|
|
scene.add(modelGroup)
|
|
}
|
|
} else if (fileName.endsWith('.obj')) {
|
|
reader.readAsText(file)
|
|
reader.onload = () => {
|
|
const loader = new OBJLoader()
|
|
const content = loader.parse(reader.result)
|
|
modelGroup.add(content)
|
|
scene.add(modelGroup)
|
|
}
|
|
} else if (fileName.endsWith('.mtl')) {
|
|
alert('需要同时上传 .obj 和 .mtl 文件,请先实现多文件上传处理逻辑。')
|
|
} else {
|
|
alert('不支持的文件类型!')
|
|
}
|
|
}
|
|
</script>
|
|
<style scoped>
|
|
.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;
|
|
}
|
|
|
|
.canvas-container {
|
|
flex: 3;
|
|
position: relative;
|
|
}
|
|
|
|
.gui-panel {
|
|
flex: 1;
|
|
border-left: 1px solid #ccc;
|
|
background: #fff;
|
|
padding: 10px;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|