Browse Source

3D模型查看器

master
修宁 7 months ago
parent
commit
bd7d2b0b04
  1. 99
      src/components/LoadingDialog.vue
  2. 267
      src/designer/Model3DView.vue
  3. 37
      src/runtime/System.ts

99
src/components/LoadingDialog.vue

@ -0,0 +1,99 @@
<template>
<el-dialog v-model="isShow" @closed="onClosed" append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false" class="yvan-loading">
<div class="yvan-loading">
<el-icon class="is-loading">
<Loading/>
</el-icon>
<div class="yvan-loading-wait">{{ loadingMsg }}</div>
</div>
</el-dialog>
</template>
<script>
import _ from "lodash";
import {Loading} from '@element-plus/icons-vue'
export default {
props: [
'_insId',
'msg',
],
components: {
Loading
},
data() {
return {
isShow: false,
}
},
mounted() {
this.isShow = true
},
computed: {
loadingMsg() {
const msg = this.$props.msg
if (msg) {
const msgLocale = msg
if (msgLocale) {
return msgLocale
}
return msg
} else {
return '载入中...'
}
}
},
methods: {
onClosed() {
const {rootElementList} = system
console.log(this.$props._insId, rootElementList.map(r => r))
_.remove(rootElementList, (item) => item.props._insId === this.$props._insId)
},
}
}
</script>
<style lang="less">
.el-dialog.yvan-loading {
background: none;
box-shadow: none;
& > .el-dialog__header {
padding: 0;
display: none;
}
& > .el-dialog__body {
background: none;
display: flex;
flex-direction: column;
justify-items: center;
align-items: center;
.yvan-loading {
color: #fff;
text-align: center;
width: 100px;
height: 100px;
background: #000;
border-radius: 10px;
padding-top: 10px;
box-shadow: 3px 3px 3px rgba(0, 0, 0, .1);
.is-loading {
font-size: 50px;
}
.yvan-loading-wait {
font-size: 16px;
}
}
}
}
</style>

267
src/designer/Model3DView.vue

@ -1,9 +1,21 @@
<template> <template>
<div class="model3d-view"> <div class="model3d-view">
<div class="toolbar"> <el-space :gutter="10" class="toolbar">
<input type="file" @change="handleFileUpload" accept=".fbx,.obj,.mtl" /> <el-upload :on-change="handleFileChange"
<span>文件上传</span> :show-file-list="false" accept=".fbx,.obj,.mtl,.3ds" action="" :auto-upload="false">
</div> <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"> <div class="main-content">
<Split class="model3d-content" :direction="'horizontal'"> <Split class="model3d-content" :direction="'horizontal'">
<SplitArea class="model3d-canvas" :size="70"> <SplitArea class="model3d-canvas" :size="70">
@ -19,12 +31,12 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick, reactive } 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 { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader'
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' import Stats from 'three/examples/jsm/libs/stats.module'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js' import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'
@ -38,8 +50,11 @@ const guiPanel = ref(null)
// Three.js // Three.js
let scene, camera, renderer, controls let scene, camera, renderer, controls
let statsControls, axesHelper, gridHelper let statsControls, axesHelper, gridHelper
let modelGroup = new THREE.Group() let gui, tcontrols, modelGroup
let gui, transformControls
const restate = reactive({
loadScale: 1
})
// //
const state = { const state = {
@ -101,37 +116,30 @@ function initThree() {
// //
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6) const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight) scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8) // const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(10, 10, 10) // directionalLight.position.set(10, 10, 10)
scene.add(directionalLight) // scene.add(directionalLight)
// //
statsControls = new Stats() statsControls = new Stats()
statsControls.dom.style.position = 'absolute' statsControls.dom.style.position = 'absolute'
viewerDom.appendChild(statsControls.dom) viewerDom.appendChild(statsControls.dom)
// TransformControls animate()
transformControls = new TransformControls(camera, renderer.domElement)
transformControls.attach(modelGroup)
// // TransformControls
transformControls.addEventListener('change', () => { tcontrols = new TransformControls(camera, renderer.domElement)
// tcontrols.addEventListener('change', () => {
renderer.render(scene, camera) renderer.render(scene, camera)
reloadState()
}) })
tcontrols.addEventListener('dragging-changed', function(event) {
transformControls.addEventListener('dragging-changed', function(event) {
//
controls.enabled = !event.value controls.enabled = !event.value
}) })
tcontrols.setMode('translate')
// // tcontrols.size = 1
function animate() { // tcontrols.space = 'local'
requestAnimationFrame(animate) scene.add(tcontrols.getHelper())
renderView()
}
animate()
// ResizeObserver // ResizeObserver
const resizeObserver = new ResizeObserver(handleResize) const resizeObserver = new ResizeObserver(handleResize)
@ -139,6 +147,11 @@ function initThree() {
resizeObserver.observe(viewerDom) resizeObserver.observe(viewerDom)
} }
//
function animate() {
requestAnimationFrame(animate)
renderView()
}
function initGUI() { function initGUI() {
const guiDom = guiPanel.value const guiDom = guiPanel.value
@ -146,6 +159,10 @@ function initGUI() {
gui = new dat.GUI({ autoPlace: false }) gui = new dat.GUI({ autoPlace: false })
guiDom.appendChild(gui.domElement) guiDom.appendChild(gui.domElement)
gui.add(state, 'mode', ['translate', 'rotate', 'scale']).onChange(function(value) {
tcontrols.setMode(value)
})
// 线 // 线
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => { gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => {
axesHelper.visible = val axesHelper.visible = val
@ -166,6 +183,7 @@ function initGUI() {
cameraPosFolder.add(state.camera.position, 'z', -100, 100).step(0.1).listen().onChange(val => { cameraPosFolder.add(state.camera.position, 'z', -100, 100).step(0.1).listen().onChange(val => {
camera.position.z = val camera.position.z = val
}) })
cameraPosFolder.close()
// Rotation (in radians) // Rotation (in radians)
const cameraRotationFolder = gui.addFolder('摄像机旋转角') const cameraRotationFolder = gui.addFolder('摄像机旋转角')
@ -178,47 +196,47 @@ function initGUI() {
cameraRotationFolder.add(state.camera.rotation, 'z', -Math.PI, Math.PI).listen().onChange(val => { cameraRotationFolder.add(state.camera.rotation, 'z', -Math.PI, Math.PI).listen().onChange(val => {
camera.rotation.z = val camera.rotation.z = val
}) })
cameraRotationFolder.close()
// Position // Position
const positionFolder = gui.addFolder('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 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 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 modelGroup.position.z = val
}) })
positionFolder.close()
// Rotation (in radians) // Rotation (in radians)
const rotationFolder = gui.addFolder('Rotation') 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 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 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 modelGroup.rotation.z = val
}) })
rotationFolder.close()
// Scale // Scale
const scaleFolder = gui.addFolder('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 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 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 modelGroup.scale.z = val
}) })
scaleFolder.close()
gui.add(state, 'mode', ['translate', 'rotate', 'scale']).onChange(function(value) {
transformControls.setMode(value)
})
// //
// const cameraFolder = gui.addFolder('') // const cameraFolder = gui.addFolder('')
@ -284,28 +302,32 @@ function initMode3DCamera() {
// //
// cameraNew.position.set(4, 2, -3); // cameraNew.position.set(4, 2, -3);
// cameraNew.position.set(30, 30, 30) // cameraNew.position.set(30, 30, 30)
cameraNew.position.set(5, 5, 5)
// 2.4,30,1.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 // -1.57,0,1.57
cameraNew.rotation.set(-1.57, 0, 1.57) // cameraNew.rotation.set(-1.57, 0, 1.57)
camera = cameraNew camera = cameraNew
scene.add(camera) scene.add(camera)
const controlsNew = new OrbitControls( const controlsNew = new OrbitControls(camera, viewerDom)
camera,
renderer?.domElement
)
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } //
controlsNew.enableDamping = false controlsNew.enableDamping = false
controlsNew.screenSpacePanning = false // controlsNew.screenSpacePanning = false //
controlsNew.minDistance = 2 controlsNew.minDistance = 2
controls = controlsNew controls = controlsNew
controls.update()
camera.updateProjectionMatrix() camera.updateProjectionMatrix()
reloadState()
}
function reloadState() {
if (camera) {
state.camera.position.x = camera.position.x state.camera.position.x = camera.position.x
state.camera.position.y = camera.position.y state.camera.position.y = camera.position.y
state.camera.position.z = camera.position.z state.camera.position.z = camera.position.z
@ -313,6 +335,21 @@ function initMode3DCamera() {
state.camera.rotation.x = camera.rotation.x state.camera.rotation.x = camera.rotation.x
state.camera.rotation.y = camera.rotation.y state.camera.rotation.y = camera.rotation.y
state.camera.rotation.z = camera.rotation.z 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() { function renderView() {
@ -331,45 +368,127 @@ function renderView() {
} }
} }
function handleTextureUpload(file) {
console.log('file', file)
if (!file) return
function handleFileUpload(event) { file = file.raw
const file = event.target.files[0]
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 handleFileChange(file) {
console.log('file', file)
if (!file) return if (!file) return
file = file.raw
// //
if (modelGroup.children.length > 0) { if (modelGroup) {
modelGroup.children.forEach(child => modelGroup.remove(child)) scene.remove(modelGroup)
} }
const fileName = file.name.toLowerCase() const fileName = file.name.toLowerCase()
const reader = new FileReader() const reader = new FileReader()
reader.onerror = (error) => {
system.showErrorDialog('加载文件失败', error.toString())
system.clearLoading()
}
if (fileName.endsWith('.fbx')) { if (fileName.endsWith('.fbx')) {
reader.readAsArrayBuffer(file) system.showLoading()
reader.onload = () => { reader.onload = () => {
const loader = new FBXLoader() const loader = new FBXLoader()
const content = loader.parse(reader.result, '') const arrayBuffer = reader.result
modelGroup.add(content) 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) scene.add(modelGroup)
tcontrols.attach(modelGroup)
controls.target.copy(modelGroup.position)
controls.update()
system.clearLoading()
} }
reader.readAsArrayBuffer(file)
} else if (fileName.endsWith('.obj')) { } else if (fileName.endsWith('.obj')) {
reader.readAsText(file) reader.readAsText(file)
reader.onload = () => { reader.onload = () => {
const loader = new OBJLoader() const loader = new OBJLoader()
const content = loader.parse(reader.result) //@ts-ignore
modelGroup.add(content) 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) 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')) { reader.readAsArrayBuffer(file)
alert('需要同时上传 .obj 和 .mtl 文件,请先实现多文件上传处理逻辑。')
} else { } else {
alert('不支持的文件类型!') alert('不支持的文件类型!')
} }
// modelGroup.position.set()
// modelGroup.rotation.set(0, 0, 0)
// modelGroup.scale.set(1, 1, 1)
} }
window['state'] = state
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.model3d-view { .model3d-view {
@ -378,23 +497,24 @@ function handleFileUpload(event) {
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
.toolbar {
padding: 10px;
background: #f5f5f5;
border-bottom: 1px solid #ccc;
}
.dialog-container { .dialog-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
} }
.toolbar {
padding: 10px;
background-color: #f0f0f0;
border-bottom: 1px solid #ccc;
}
.main-content { .main-content {
display: flex; display: flex;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.model3d-content{
.model3d-content {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -403,7 +523,7 @@ function handleFileUpload(event) {
.canvas-container { .canvas-container {
width: 100%; width: 100%;
height:100%; height: 100%;
position: relative; position: relative;
} }
@ -411,9 +531,10 @@ function handleFileUpload(event) {
border-left: 1px solid #ccc; border-left: 1px solid #ccc;
background: #111; background: #111;
width: 100%; width: 100%;
height:100%; height: 100%;
overflow-y: auto; overflow-y: auto;
:deep(.lil-gui.root){
:deep(.lil-gui.root) {
width: 100%; width: 100%;
} }
} }

37
src/runtime/System.ts

@ -9,6 +9,7 @@ import { QuestionFilled } from '@element-plus/icons-vue'
import { renderIcon } from '@/utils/webutils.ts' import { renderIcon } from '@/utils/webutils.ts'
import type { showDialogOption } from '@/SystemOption' import type { showDialogOption } from '@/SystemOption'
import ShowDialogWrap from '@/components/ShowDialogWrap.vue' import ShowDialogWrap from '@/components/ShowDialogWrap.vue'
import LoadingDialog from '@/components/LoadingDialog.vue'
export default class System { export default class System {
_ = _ _ = _
@ -227,6 +228,42 @@ export default class System {
}) })
}) })
} }
globalLoadingHandle = null // "正在载入..." 对话框
/**
* ...
*/
public showLoading(msg?: string): void {
if (this.globalLoadingHandle) {
// 存在 "正在载入..." 对话框
this.clearLoading()
}
const _insId = _.uniqueId('_dlg')
system.rootElementList.push({
cmp: markRaw(LoadingDialog),
props: {
_insId: _insId,
msg: msg
}
})
this.globalLoadingHandle = () => {
_.remove(system.rootElementList, (item: any) => item.props._insId === _insId)
}
}
/**
* ...
*/
public clearLoading(): void {
if (typeof this.globalLoadingHandle === 'function') {
this.globalLoadingHandle()
}
this.globalLoadingHandle = null
}
} }
export interface ShowDialogOption { export interface ShowDialogOption {

Loading…
Cancel
Save