|
|
@ -14,13 +14,14 @@ |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
<script setup> |
|
|
<script setup> |
|
|
import { ref, onMounted } from 'vue' |
|
|
import { ref, onMounted, nextTick } from 'vue' |
|
|
import * as THREE from 'three' |
|
|
import * as THREE from 'three' |
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' |
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' |
|
|
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' |
|
|
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' |
|
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' |
|
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' |
|
|
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' |
|
|
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' |
|
|
import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js' |
|
|
import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js' |
|
|
|
|
|
import Stats from 'three/examples/jsm/libs/stats.module' |
|
|
|
|
|
|
|
|
// DOM refs |
|
|
// DOM refs |
|
|
const canvasContainer = ref(null) |
|
|
const canvasContainer = ref(null) |
|
|
@ -28,6 +29,7 @@ const guiPanel = ref(null) |
|
|
|
|
|
|
|
|
// Three.js 场景相关 |
|
|
// Three.js 场景相关 |
|
|
let scene, camera, renderer, controls |
|
|
let scene, camera, renderer, controls |
|
|
|
|
|
let statsControls, axesHelper, gridHelper |
|
|
let modelGroup = new THREE.Group() |
|
|
let modelGroup = new THREE.Group() |
|
|
let gui |
|
|
let gui |
|
|
|
|
|
|
|
|
@ -36,40 +38,63 @@ const state = { |
|
|
autoRotate: false, |
|
|
autoRotate: false, |
|
|
showAxesHelper: true, |
|
|
showAxesHelper: true, |
|
|
showGridHelper: true, |
|
|
showGridHelper: true, |
|
|
cameraPosition: [0, 5, 10] |
|
|
cameraPosition: [0, 5, 10], |
|
|
|
|
|
positionX: 0, |
|
|
|
|
|
positionY: 0, |
|
|
|
|
|
positionZ: 0, |
|
|
|
|
|
rotationX: 0, |
|
|
|
|
|
rotationY: 0, |
|
|
|
|
|
rotationZ: 0, |
|
|
|
|
|
scaleX: 1, |
|
|
|
|
|
scaleY: 1, |
|
|
|
|
|
scaleZ: 1, |
|
|
|
|
|
camera: { |
|
|
|
|
|
position: { x: 0, y: 5, z: 10 }, |
|
|
|
|
|
rotation: { x: 0, y: 0, z: 0 } |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
initThree() |
|
|
const viewerDom = canvasContainer.value |
|
|
initGUI() |
|
|
nextTick(() => { |
|
|
|
|
|
console.log('viewerDom', viewerDom) |
|
|
|
|
|
initThree() |
|
|
|
|
|
initGUI() |
|
|
|
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
function initThree() { |
|
|
function initThree() { |
|
|
|
|
|
const viewerDom = canvasContainer.value |
|
|
|
|
|
|
|
|
// 场景 |
|
|
// 场景 |
|
|
scene = new THREE.Scene() |
|
|
scene = new THREE.Scene() |
|
|
scene.background = new THREE.Color(0xeeeeee) |
|
|
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 = new THREE.WebGLRenderer({ |
|
|
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8) |
|
|
logarithmicDepthBuffer: true, |
|
|
canvasContainer.value.appendChild(renderer.domElement) |
|
|
antialias: true, |
|
|
|
|
|
alpha: true, |
|
|
|
|
|
precision: 'mediump', |
|
|
|
|
|
premultipliedAlpha: true, |
|
|
|
|
|
preserveDrawingBuffer: false, |
|
|
|
|
|
powerPreference: 'high-performance' |
|
|
|
|
|
}) |
|
|
|
|
|
renderer.clearDepth() |
|
|
|
|
|
renderer.shadowMap.enabled = true |
|
|
|
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace // 可以看到更亮的材质,同时这也影响到环境贴图。 |
|
|
|
|
|
renderer.setPixelRatio(window.devicePixelRatio) |
|
|
|
|
|
renderer.setSize(viewerDom.getBoundingClientRect().width, viewerDom.getBoundingClientRect().height) |
|
|
|
|
|
viewerDom.appendChild(renderer.domElement) |
|
|
|
|
|
|
|
|
// 控制器 |
|
|
// 摄像机 |
|
|
controls = new OrbitControls(camera, renderer.domElement) |
|
|
initMode3DCamera() |
|
|
|
|
|
|
|
|
// 辅助线 |
|
|
// 辅助线 |
|
|
const axesHelper = new THREE.AxesHelper(5) |
|
|
axesHelper = new THREE.AxesHelper(5) |
|
|
scene.add(axesHelper) |
|
|
scene.add(axesHelper) |
|
|
const gridHelper = new THREE.GridHelper(20, 20) |
|
|
|
|
|
|
|
|
gridHelper = new THREE.GridHelper(40, 40) |
|
|
scene.add(gridHelper) |
|
|
scene.add(gridHelper) |
|
|
|
|
|
|
|
|
// 光照 |
|
|
// 光照 |
|
|
@ -79,52 +104,216 @@ function initThree() { |
|
|
directionalLight.position.set(10, 10, 10) |
|
|
directionalLight.position.set(10, 10, 10) |
|
|
scene.add(directionalLight) |
|
|
scene.add(directionalLight) |
|
|
|
|
|
|
|
|
|
|
|
// 性能监控 |
|
|
|
|
|
statsControls = new Stats() |
|
|
|
|
|
statsControls.dom.style.position = 'absolute' |
|
|
|
|
|
viewerDom.appendChild(statsControls.dom) |
|
|
|
|
|
|
|
|
// 动画循环 |
|
|
// 动画循环 |
|
|
function animate() { |
|
|
function animate() { |
|
|
requestAnimationFrame(animate) |
|
|
requestAnimationFrame(animate) |
|
|
if (state.autoRotate) { |
|
|
if (state.autoRotate) { |
|
|
modelGroup.rotation.y += 0.01 |
|
|
modelGroup.rotation.y += 0.01 |
|
|
} |
|
|
} |
|
|
controls.update() |
|
|
renderView() |
|
|
renderer.render(scene, camera) |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
animate() |
|
|
animate() |
|
|
|
|
|
|
|
|
// 调整窗口大小 |
|
|
// 创建ResizeObserver实例并传入处理函数 |
|
|
window.addEventListener('resize', () => { |
|
|
const resizeObserver = new ResizeObserver(handleResize) |
|
|
camera.aspect = canvasContainer.value.clientWidth / window.innerHeight |
|
|
// 开始观察指定的DOM元素 |
|
|
camera.updateProjectionMatrix() |
|
|
resizeObserver.observe(viewerDom) |
|
|
renderer.setSize(canvasContainer.value.clientWidth, window.innerHeight * 0.8) |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initGUI() { |
|
|
function initGUI() { |
|
|
|
|
|
const guiDom = guiPanel.value |
|
|
|
|
|
|
|
|
gui = new dat.GUI({ autoPlace: false }) |
|
|
gui = new dat.GUI({ autoPlace: false }) |
|
|
guiPanel.value.appendChild(gui.domElement) |
|
|
guiDom.appendChild(gui.domElement) |
|
|
|
|
|
|
|
|
// 自动旋转 |
|
|
// 自动旋转 |
|
|
gui.add(state, 'autoRotate').name('自动旋转') |
|
|
gui.add(state, 'autoRotate').name('自动旋转') |
|
|
|
|
|
|
|
|
// 显示辅助线 |
|
|
// 显示辅助线 |
|
|
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => { |
|
|
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => { |
|
|
scene.getObjectByName('AxesHelper').visible = val |
|
|
axesHelper.visible = val |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
gui.add(state, 'showGridHelper').name('显示网格').onChange(val => { |
|
|
gui.add(state, 'showGridHelper').name('显示网格').onChange(val => { |
|
|
scene.getObjectByName('GridHelper').visible = val |
|
|
gridHelper.visible = val |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
// 相机位置 |
|
|
// Position |
|
|
const cameraFolder = gui.addFolder('相机位置') |
|
|
const cameraPosFolder = gui.addFolder('Camera Position') |
|
|
cameraFolder.add(state.cameraPosition, 0, -10, 10).name('X') |
|
|
cameraPosFolder.add(state.camera.position, 'x', -100, 100).step(0.1).listen().onChange(val => { |
|
|
cameraFolder.add(state.cameraPosition, 1, -10, 10).name('Y') |
|
|
camera.position.x = val |
|
|
cameraFolder.add(state.cameraPosition, 2, 5, 30).name('Z').onChange(() => { |
|
|
}) |
|
|
camera.position.set(...state.cameraPosition) |
|
|
cameraPosFolder.add(state.camera.position, 'y', -100, 100).step(0.1).listen().onChange(val => { |
|
|
|
|
|
camera.position.y = val |
|
|
|
|
|
}) |
|
|
|
|
|
cameraPosFolder.add(state.camera.position, 'z', -100, 100).step(0.1).listen().onChange(val => { |
|
|
|
|
|
camera.position.z = val |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// Rotation (in radians) |
|
|
|
|
|
const cameraRotationFolder = gui.addFolder('Camera Rotation') |
|
|
|
|
|
cameraRotationFolder.add(state.camera.rotation, 'x', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
|
|
camera.rotation.x = val |
|
|
|
|
|
}) |
|
|
|
|
|
cameraRotationFolder.add(state.camera.rotation, 'y', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
|
|
camera.rotation.y = val |
|
|
|
|
|
}) |
|
|
|
|
|
cameraRotationFolder.add(state.camera.rotation, 'z', -Math.PI, Math.PI).listen().onChange(val => { |
|
|
|
|
|
camera.rotation.z = val |
|
|
}) |
|
|
}) |
|
|
cameraFolder.open() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Position |
|
|
|
|
|
const positionFolder = gui.addFolder('Position') |
|
|
|
|
|
positionFolder.add(state, 'positionX', -10, 10).onChange(val => { |
|
|
|
|
|
modelGroup.position.x = val |
|
|
|
|
|
}) |
|
|
|
|
|
positionFolder.add(state, 'positionY', -10, 10).onChange(val => { |
|
|
|
|
|
modelGroup.position.y = val |
|
|
|
|
|
}) |
|
|
|
|
|
positionFolder.add(state, 'positionZ', -10, 10).onChange(val => { |
|
|
|
|
|
modelGroup.position.z = val |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// Rotation (in radians) |
|
|
|
|
|
const rotationFolder = gui.addFolder('Rotation') |
|
|
|
|
|
rotationFolder.add(state, 'rotationX', -Math.PI, Math.PI).onChange(val => { |
|
|
|
|
|
modelGroup.rotation.x = val |
|
|
|
|
|
}) |
|
|
|
|
|
rotationFolder.add(state, 'rotationY', -Math.PI, Math.PI).onChange(val => { |
|
|
|
|
|
modelGroup.rotation.y = val |
|
|
|
|
|
}) |
|
|
|
|
|
rotationFolder.add(state, 'rotationZ', -Math.PI, Math.PI).onChange(val => { |
|
|
|
|
|
modelGroup.rotation.z = val |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// Scale |
|
|
|
|
|
const scaleFolder = gui.addFolder('Scale') |
|
|
|
|
|
scaleFolder.add(state, 'scaleX', 0.001, 10).onChange(val => { |
|
|
|
|
|
modelGroup.scale.x = val |
|
|
|
|
|
}) |
|
|
|
|
|
scaleFolder.add(state, 'scaleY', 0.001, 10).onChange(val => { |
|
|
|
|
|
modelGroup.scale.y = val |
|
|
|
|
|
}) |
|
|
|
|
|
scaleFolder.add(state, 'scaleZ', 0.001, 10).onChange(val => { |
|
|
|
|
|
modelGroup.scale.z = 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 handleResize(entries) { |
|
|
|
|
|
for (let entry of entries) { |
|
|
|
|
|
// entry.contentRect包含了元素的尺寸信息 |
|
|
|
|
|
console.log('Element size changed:', entry.contentRect) |
|
|
|
|
|
|
|
|
|
|
|
const width = entry.contentRect.width |
|
|
|
|
|
const height = entry.contentRect.height |
|
|
|
|
|
|
|
|
|
|
|
if (camera instanceof THREE.PerspectiveCamera) { |
|
|
|
|
|
camera.aspect = width / height |
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
|
|
|
|
|
|
} else if (camera instanceof THREE.OrthographicCamera) { |
|
|
|
|
|
camera.left = width / -2 |
|
|
|
|
|
camera.right = width / 2 |
|
|
|
|
|
camera.top = height / 2 |
|
|
|
|
|
camera.bottom = height / -2 |
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
} |
|
|
|
|
|
renderer.setSize(width, height) |
|
|
|
|
|
break |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function initMode2DCamera() { |
|
|
|
|
|
// const controlsNew = new OrbitControls( |
|
|
|
|
|
// cameraNew, |
|
|
|
|
|
// renderer?.domElement |
|
|
|
|
|
// ) |
|
|
|
|
|
// controlsNew.enableDamping = false |
|
|
|
|
|
// controlsNew.enableZoom = true |
|
|
|
|
|
// controlsNew.enableRotate = false |
|
|
|
|
|
// controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移 |
|
|
|
|
|
// controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 |
|
|
|
|
|
// controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' } |
|
|
|
|
|
// controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件 |
|
|
|
|
|
// controlsNew.panSpeed = 1 |
|
|
|
|
|
// controlsNew.keyPanSpeed = 20 // normal 7 |
|
|
|
|
|
// controlsNew.minDistance = 0.1 |
|
|
|
|
|
// controlsNew.maxDistance = 1000 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function initMode3DCamera() { |
|
|
|
|
|
if (camera) { |
|
|
|
|
|
scene.remove(camera) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const viewerDom = canvasContainer.value |
|
|
|
|
|
|
|
|
|
|
|
// 渲染相机 |
|
|
|
|
|
const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 1, 2000) |
|
|
|
|
|
//设置相机位置 |
|
|
|
|
|
// cameraNew.position.set(4, 2, -3); |
|
|
|
|
|
cameraNew.position.set(30, 30, 30) |
|
|
|
|
|
//设置相机方向 |
|
|
|
|
|
cameraNew.lookAt(0, 0, 0) |
|
|
|
|
|
|
|
|
|
|
|
camera = cameraNew |
|
|
|
|
|
scene.add(camera) |
|
|
|
|
|
|
|
|
|
|
|
const controlsNew = new OrbitControls( |
|
|
|
|
|
camera, |
|
|
|
|
|
renderer?.domElement |
|
|
|
|
|
) |
|
|
|
|
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移 |
|
|
|
|
|
controlsNew.enableDamping = false |
|
|
|
|
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 |
|
|
|
|
|
controlsNew.minDistance = 2 |
|
|
|
|
|
controls = controlsNew |
|
|
|
|
|
|
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
|
|
|
|
|
|
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 renderView() { |
|
|
|
|
|
statsControls?.update() |
|
|
|
|
|
renderer?.render(scene, camera) |
|
|
|
|
|
|
|
|
|
|
|
if (camera) { |
|
|
|
|
|
// 同步相机位置和旋转到 cameraTransform 对象 |
|
|
|
|
|
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 handleFileUpload(event) { |
|
|
function handleFileUpload(event) { |
|
|
const file = event.target.files[0] |
|
|
const file = event.target.files[0] |
|
|
if (!file) return |
|
|
if (!file) return |
|
|
@ -158,37 +347,48 @@ function handleFileUpload(event) { |
|
|
} else { |
|
|
} else { |
|
|
alert('不支持的文件类型!') |
|
|
alert('不支持的文件类型!') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// modelGroup.position.set() |
|
|
|
|
|
// modelGroup.rotation.set(0, 0, 0) |
|
|
|
|
|
// modelGroup.scale.set(1, 1, 1) |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</script> |
|
|
<style scoped> |
|
|
<style scoped> |
|
|
.dialog-container { |
|
|
.model3d-view { |
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
flex-direction: column; |
|
|
height: 100vh; |
|
|
flex-grow: 1; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.toolbar { |
|
|
.dialog-container { |
|
|
padding: 10px; |
|
|
display: flex; |
|
|
background-color: #f0f0f0; |
|
|
flex-direction: column; |
|
|
border-bottom: 1px solid #ccc; |
|
|
height: 100vh; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.main-content { |
|
|
.toolbar { |
|
|
display: flex; |
|
|
padding: 10px; |
|
|
flex: 1; |
|
|
background-color: #f0f0f0; |
|
|
overflow: hidden; |
|
|
border-bottom: 1px solid #ccc; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.canvas-container { |
|
|
.main-content { |
|
|
flex: 3; |
|
|
display: flex; |
|
|
position: relative; |
|
|
flex: 1; |
|
|
} |
|
|
overflow: hidden; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
.gui-panel { |
|
|
.canvas-container { |
|
|
flex: 1; |
|
|
flex: 3; |
|
|
border-left: 1px solid #ccc; |
|
|
position: relative; |
|
|
background: #fff; |
|
|
} |
|
|
padding: 10px; |
|
|
|
|
|
overflow-y: auto; |
|
|
.gui-panel { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
border-left: 1px solid #ccc; |
|
|
|
|
|
background: #fff; |
|
|
|
|
|
padding: 10px; |
|
|
|
|
|
overflow-y: auto; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
</style> |
|
|
</style> |