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

<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>