| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625 |
- <template>
- <el-dialog
- v-model="dialogVisible"
- :title="modelName"
- width="80%"
- :before-close="handleClose"
- destroy-on-close
- >
- <div class="model-preview-container">
- <div ref="modelContainer" class="model-container">
- <!-- 光照控制按钮 -->
- <div class="light-control-btn" @click="toggleLightControl">
- <el-icon><Sunny /></el-icon>
- </div>
-
- <!-- 光照控制面板 -->
- <div v-if="showLightControl" class="light-control-panel">
- <h4>光照控制</h4>
-
- <!-- 材质透明度调整 -->
- <div class="control-item">
- <label>材质不透明度</label>
- <el-slider
- v-model="materialOpacity"
- :min="0.1"
- :max="1"
- :step="0.05"
- @change="updateMaterialOpacity"
- />
- <span class="value">{{ materialOpacity.toFixed(2) }}</span>
- </div>
-
- <!-- 环境光强度 -->
- <div class="control-item">
- <label>环境光强度</label>
- <el-slider
- v-model="lightSettings.ambientIntensity"
- :min="0"
- :max="5"
- :step="0.1"
- @change="updateLights"
- />
- <span class="value">{{ lightSettings.ambientIntensity.toFixed(1) }}</span>
- </div>
-
- <!-- 主平行光强度 -->
- <div class="control-item">
- <label>主光源强度</label>
- <el-slider
- v-model="lightSettings.directionalIntensity"
- :min="0"
- :max="10"
- :step="0.1"
- @change="updateLights"
- />
- <span class="value">{{ lightSettings.directionalIntensity.toFixed(1) }}</span>
- </div>
-
- <!-- 辅助光强度 -->
- <div class="control-item">
- <label>辅助光强度</label>
- <el-slider
- v-model="lightSettings.directionalIntensity2"
- :min="0"
- :max="5"
- :step="0.1"
- @change="updateLights"
- />
- <span class="value">{{ lightSettings.directionalIntensity2.toFixed(1) }}</span>
- </div>
-
- <!-- 主光源角度 -->
- <div class="control-item">
- <label>主光源角度</label>
- <div class="angle-controls">
- <div>
- <span>X轴</span>
- <el-slider
- v-model="lightSettings.directionalPosition.x"
- :min="-5"
- :max="5"
- :step="0.1"
- @change="updateLights"
- />
- <span class="value">{{ lightSettings.directionalPosition.x.toFixed(1) }}</span>
- </div>
- <div>
- <span>Y轴</span>
- <el-slider
- v-model="lightSettings.directionalPosition.y"
- :min="-5"
- :max="5"
- :step="0.1"
- @change="updateLights"
- />
- <span class="value">{{ lightSettings.directionalPosition.y.toFixed(1) }}</span>
- </div>
- <div>
- <span>Z轴</span>
- <el-slider
- v-model="lightSettings.directionalPosition.z"
- :min="-5"
- :max="5"
- :step="0.1"
- @change="updateLights"
- />
- <span class="value">{{ lightSettings.directionalPosition.z.toFixed(1) }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="model-info">
- <h3>模型信息</h3>
- <el-descriptions :column="1" border>
- <el-descriptions-item label="模型名称">{{ modelData.name }}</el-descriptions-item>
- <el-descriptions-item label="模型类型">{{ getTypeDisplayName(modelData.type) }}</el-descriptions-item>
- <el-descriptions-item label="经纬度">{{ modelData.location }}</el-descriptions-item>
- <el-descriptions-item label="提交单位">{{ modelData.uploadUnit }}</el-descriptions-item>
- <el-descriptions-item label="格式">{{ modelData.format }}</el-descriptions-item>
- <el-descriptions-item label="状态">{{ modelData.status }}</el-descriptions-item>
- <el-descriptions-item label="创建时间">{{ modelData.createTime }}</el-descriptions-item>
- </el-descriptions>
- </div>
- </div>
- <template #footer>
- <el-button @click="handleClose">关闭</el-button>
- </template>
- </el-dialog>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted, watch } from 'vue'
- import * as THREE from 'three'
- import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
- import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
- import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
- import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'
- // 导入项目的request实例
- import request from '@/utils/request'
- import { Sunny } from '@element-plus/icons-vue'
- const props = defineProps({
- visible: {
- type: Boolean,
- default: false
- },
- modelData: {
- type: Object,
- default: () => ({})
- }
- })
- const emit = defineEmits(['update:visible', 'close'])
- const modelContainer = ref(null)
- let scene = null
- let camera = null
- let renderer = null
- let model = null
- let controls = null
- let animationId = null
- const dialogVisible = ref(false)
- // 光照控制相关变量
- const showLightControl = ref(false)
- let ambientLight = null
- let directionalLight = null
- let directionalLight2 = null
- const lightSettings = ref({
- ambientIntensity: 2.0,
- directionalIntensity: 4.0,
- directionalIntensity2: 2.0,
- directionalPosition: {
- x: 1,
- y: 1,
- z: 1
- }
- })
- const materialOpacity = ref(1.0)
- // 获取类型显示名称
- const getTypeDisplayName = (type) => {
- const typeMapping = {
- 'RESERVOIR': '水库',
- 'HYDROPOWER': '水电站',
- 'IRRIGATION': '灌溉',
- 'FLOOD_CONTROL': '防洪',
- 'CANAL': '渠道',
- 'PUMPING_STATION': '泵站'
- }
- return typeMapping[type] || type
- }
- // 初始化场景
- const initScene = () => {
- if (!modelContainer.value) return
- // 创建场景
- scene = new THREE.Scene()
- scene.background = new THREE.Color(0xf0f0f0)
- // 创建相机
- camera = new THREE.PerspectiveCamera(
- 75,
- modelContainer.value.clientWidth / modelContainer.value.clientHeight,
- 0.1,
- 1000
- )
- // 增加相机初始位置,让相机离模型更远,以便能看到整个模型
- camera.position.z = 10
- camera.position.y = 2
- // 创建渲染器
- renderer = new THREE.WebGLRenderer({ antialias: true })
- renderer.setSize(modelContainer.value.clientWidth, modelContainer.value.clientHeight)
- modelContainer.value.appendChild(renderer.domElement)
- // 添加光源
- ambientLight = new THREE.AmbientLight(0xffffff, lightSettings.value.ambientIntensity)
- scene.add(ambientLight)
- directionalLight = new THREE.DirectionalLight(0xffffff, lightSettings.value.directionalIntensity)
- directionalLight.position.set(
- lightSettings.value.directionalPosition.x,
- lightSettings.value.directionalPosition.y,
- lightSettings.value.directionalPosition.z
- )
- scene.add(directionalLight)
-
- // 添加第二个平行光,从不同角度照射,增强照明效果
- directionalLight2 = new THREE.DirectionalLight(0xffffff, lightSettings.value.directionalIntensity2)
- directionalLight2.position.set(-1, 1, -1)
- scene.add(directionalLight2)
- // 添加网格辅助线
- const gridHelper = new THREE.GridHelper(10, 10)
- scene.add(gridHelper)
- // 加载模型
- if (props.modelData) {
- loadModel()
- }
- // 添加鼠标控制
- initControls()
- // 添加三维坐标轴
- addAxesHelper()
- // 开始渲染
- animate()
- // 监听窗口大小变化
- window.addEventListener('resize', handleResize)
-
- // 添加光照控制面板事件阻止
- addLightControlEventPrevent()
- }
- // 初始化控制器
- const initControls = () => {
- // 简单的鼠标控制实现 - 控制相机视角
- let isDragging = false
- let isPanning = false // 中键平移状态
- let previousMousePosition = { x: 0, y: 0 }
-
- // 相机目标点(看向的点)
- let cameraTarget = new THREE.Vector3(0, 0, 0)
-
- // 更新相机目标点为模型中心
- const updateCameraTarget = () => {
- if (model) {
- model.updateMatrixWorld(true)
- const box = new THREE.Box3().setFromObject(model)
- cameraTarget = box.getCenter(new THREE.Vector3())
- console.log('相机目标点已更新为模型中心:', cameraTarget)
- }
- }
-
- // 初始化时更新目标点
- updateCameraTarget()
- const container = modelContainer.value
-
- // 检查事件是否来自光照控制面板
- const isFromLightControl = (e) => {
- let target = e.target
- while (target) {
- if (target.classList && (target.classList.contains('light-control-panel') || target.classList.contains('light-control-btn'))) {
- return true
- }
- target = target.parentElement
- }
- return false
- }
- container.addEventListener('mousedown', (e) => {
- // 如果事件来自光照控制面板,不处理
- if (isFromLightControl(e)) return
-
- // 检查是否是中键
- if (e.button === 1) { // 中键
- isPanning = true
- } else if (e.button === 0) { // 左键
- isDragging = true
- }
-
- previousMousePosition = { x: e.offsetX, y: e.offsetY }
- })
- container.addEventListener('mousemove', (e) => {
- // 如果事件来自光照控制面板,不处理
- if (isFromLightControl(e)) return
-
- const deltaMove = {
- x: e.offsetX - previousMousePosition.x,
- y: e.offsetY - previousMousePosition.y
- }
- // 中键平移相机
- if (isPanning && camera) {
- // 计算平移距离
- const distance = camera.position.distanceTo(cameraTarget)
- const panSpeed = distance * 0.001
-
- // 计算相机的右方向和上方向
- const right = new THREE.Vector3()
- const up = new THREE.Vector3()
- camera.getWorldDirection(right)
- right.crossVectors(camera.up, right).normalize()
- up.copy(camera.up).normalize()
-
- // 计算平移向量
- const pan = new THREE.Vector3()
- pan.addScaledVector(right, deltaMove.x * panSpeed)
- pan.addScaledVector(up, deltaMove.y * panSpeed)
-
- // 平移相机和目标点
- camera.position.add(pan)
- cameraTarget.add(pan)
-
- // 相机看向目标点
- camera.lookAt(cameraTarget)
- }
- // 左键旋转相机
- else if (isDragging && camera) {
- // 控制相机围绕目标点旋转
- // 计算相机到目标点的距离
- const distance = camera.position.distanceTo(cameraTarget)
-
- // 限制垂直旋转范围,避免相机翻转
- const currentPosition = new THREE.Vector3().copy(camera.position)
- const direction = new THREE.Vector3().subVectors(currentPosition, cameraTarget).normalize()
-
- // 水平旋转(围绕Y轴)
- const horizontalRotation = new THREE.Matrix4().makeRotationY(-deltaMove.x * 0.01)
- // 垂直旋转(围绕X轴,限制范围)
- const verticalRotation = new THREE.Matrix4().makeRotationX(-deltaMove.y * 0.005)
-
- // 应用旋转
- const newDirection = direction.applyMatrix4(horizontalRotation).applyMatrix4(verticalRotation)
-
- // 设置新的相机位置
- camera.position.copy(cameraTarget).add(newDirection.multiplyScalar(distance))
-
- // 相机看向目标点
- camera.lookAt(cameraTarget)
- }
- previousMousePosition = {
- x: e.offsetX,
- y: e.offsetY
- }
- })
- container.addEventListener('mouseup', (e) => {
- // 如果事件来自光照控制面板,不处理
- if (isFromLightControl(e)) return
-
- isDragging = false
- isPanning = false
- })
- container.addEventListener('mouseleave', (e) => {
- // 如果事件来自光照控制面板,不处理
- if (isFromLightControl(e)) return
-
- isDragging = false
- isPanning = false
- })
- // 滚轮缩放 - 控制相机距离目标点的距离
- container.addEventListener('wheel', (e) => {
- // 如果事件来自光照控制面板,不处理
- if (isFromLightControl(e)) return
-
- e.preventDefault()
- if (camera) {
- // 计算相机到目标点的向量
- const direction = new THREE.Vector3().subVectors(camera.position, cameraTarget)
- const distance = direction.length()
-
- // 限制最小和最大距离
- const minDistance = 0.5
- const maxDistance = 50
-
- // 计算新的距离
- const newDistance = Math.max(minDistance, Math.min(maxDistance, distance + e.deltaY * 0.01))
-
- // 设置新的相机位置
- direction.normalize().multiplyScalar(newDistance)
- camera.position.copy(cameraTarget).add(direction)
-
- // 相机看向目标点
- camera.lookAt(cameraTarget)
- }
- })
- }
- // 为光照控制面板添加事件阻止
- const addLightControlEventPrevent = () => {
- const container = modelContainer.value
- if (!container) return
-
- // 为光照控制面板添加事件监听器,阻止事件传播
- const lightControlPanel = container.querySelector('.light-control-panel')
- const lightControlBtn = container.querySelector('.light-control-btn')
-
- const preventEvents = (element) => {
- if (element) {
- element.addEventListener('mousedown', (e) => {
- e.stopPropagation()
- })
- element.addEventListener('mousemove', (e) => {
- e.stopPropagation()
- })
- element.addEventListener('mouseup', (e) => {
- e.stopPropagation()
- })
- element.addEventListener('wheel', (e) => {
- e.stopPropagation()
- e.preventDefault()
- })
- }
- }
-
- preventEvents(lightControlPanel)
- preventEvents(lightControlBtn)
- }
- // 添加三维坐标轴
- const addAxesHelper = () => {
- // 移除现有的坐标轴
- if (scene) {
- scene.traverse((object) => {
- if (object.isAxesHelper || object.userData.isCustomAxes) {
- scene.remove(object)
- }
- })
- }
-
- // 创建带箭头的坐标轴
- if (scene) {
- const axisLength = 1
- const arrowSize = 0.1
-
- // X轴 - 红色
- const xArrow = new THREE.ArrowHelper(
- new THREE.Vector3(1, 0, 0), // 方向
- new THREE.Vector3(0, 0, 0), // 起点
- axisLength, // 长度
- 0xff0000, // 颜色
- arrowSize, // 箭头大小
- arrowSize // 箭头宽度
- )
- xArrow.userData.isCustomAxes = true
- scene.add(xArrow)
-
- // Y轴 - 绿色
- const yArrow = new THREE.ArrowHelper(
- new THREE.Vector3(0, 1, 0),
- new THREE.Vector3(0, 0, 0),
- axisLength,
- 0x00ff00,
- arrowSize,
- arrowSize
- )
- yArrow.userData.isCustomAxes = true
- scene.add(yArrow)
-
- // Z轴 - 蓝色
- const zArrow = new THREE.ArrowHelper(
- new THREE.Vector3(0, 0, 1),
- new THREE.Vector3(0, 0, 0),
- axisLength,
- 0x0000ff,
- arrowSize,
- arrowSize
- )
- zArrow.userData.isCustomAxes = true
- scene.add(zArrow)
-
- console.log('带箭头的三维坐标轴已添加到场景原点')
- }
- }
- // 加载模型
- const loadModel = () => {
- console.log('========================================')
- console.log('开始加载模型流程')
- console.log('========================================')
-
- if (!props.modelData || !props.modelData.format) {
- console.error('模型数据不完整:', { modelData: props.modelData })
- console.log('========================================')
- console.log('加载模型流程结束 - 数据不完整')
- console.log('========================================')
- return
- }
- console.log('模型基本信息:', {
- modelId: props.modelData.id,
- modelFormat: props.modelData.format,
- modelName: props.modelData.name
- })
- // 打印完整的模型数据,查看是否包含文件路径或URL
- console.log('完整模型数据:', props.modelData)
- console.log('模型数据所有属性:', Object.keys(props.modelData))
- const format = props.modelData.format.toUpperCase()
-
- // 清除现有模型
- if (model) {
- scene.remove(model)
- model = null
- console.log('已清除现有模型')
- }
- try {
- // 检查模型数据中是否包含文件路径或URL
- let modelUrl = null
-
- // 尝试从模型数据中获取文件路径或URL
- if (props.modelData.path) {
- modelUrl = props.modelData.path
- console.log('✓ 从modelData.path获取模型路径:', modelUrl)
- } else if (props.modelData.url) {
- modelUrl = props.modelData.url
- console.log('✓ 从modelData.url获取模型路径:', modelUrl)
- } else if (props.modelData.filePath) {
- // 后端返回的filePath是相对路径,需要添加/profile前缀
- modelUrl = `/profile/${props.modelData.filePath}`
- console.log('✓ 从modelData.filePath获取模型路径并添加前缀:', props.modelData.filePath, '→', modelUrl)
- } else if (props.modelData.file_path) {
- // 检查file_path是否已经包含/profile前缀
- if (props.modelData.file_path.startsWith('/profile')) {
- // 已经包含/profile前缀,直接使用
- modelUrl = props.modelData.file_path
- console.log('✓ 从modelData.file_path获取模型路径(已包含/profile前缀):', modelUrl)
- } else {
- // 后端返回的file_path是相对路径,需要添加/profile前缀
- modelUrl = `/profile/${props.modelData.file_path}`
- console.log('✓ 从modelData.file_path获取模型路径并添加前缀:', props.modelData.file_path, '→', modelUrl)
- }
- } else if (props.modelData.fileUrl) {
- modelUrl = props.modelData.fileUrl
- console.log('✓ 从modelData.fileUrl获取模型路径:', modelUrl)
- } else if (props.modelData.storagePath) {
- modelUrl = props.modelData.storagePath
- console.log('✓ 从modelData.storagePath获取模型路径:', modelUrl)
- } else if (props.modelData.filename) {
- // 直接使用filename构建路径,添加/profile/upload前缀
- modelUrl = `/profile/upload/${props.modelData.filename}`
- console.log('✓ 从modelData.filename获取模型路径:', modelUrl)
- } else if (props.modelData.fileName) {
- // 直接使用fileName构建路径,添加/profile/upload前缀
- modelUrl = `/profile/upload/${props.modelData.fileName}`
- console.log('✓ 从modelData.fileName获取模型路径:', modelUrl)
- } else if (props.modelData.id) {
- // 如果没有找到文件路径,尝试使用ID构建路径
- // 尝试多种可能的静态文件路径,添加/profile前缀
- const modelId = props.modelData.id
- const possiblePaths = [
- `/profile/upload/${modelId}.${props.modelData.format.toLowerCase()}`,
- `/profile/model/${modelId}.${props.modelData.format.toLowerCase()}`
- ]
-
- // 尝试每个路径
- console.log('开始尝试构建模型路径:')
- for (const path of possiblePaths) {
- console.log(' 尝试路径:', path)
- }
-
- // 使用第一个可能的路径
- modelUrl = possiblePaths[0]
- console.log('✓ 使用ID构建模型路径:', modelUrl)
- }
- // 如果找到了模型路径
- if (modelUrl) {
- // 确保路径是完整的URL
- if (!modelUrl.startsWith('http://') && !modelUrl.startsWith('https://')) {
- // 检查是否已经是绝对路径(以/开头)
- if (!modelUrl.startsWith('/')) {
- // 如果是相对路径,添加基础路径
- const oldUrl = modelUrl
- modelUrl = `/${modelUrl}`
- console.log('✓ 添加斜杠前缀:', oldUrl, '→', modelUrl)
- }
-
- // 检查是否需要添加/upload前缀
- // 根据后端代码,FileUploadUtils.upload()返回的路径已经包含了upload前缀
- // 所以这里不需要再添加/upload/model/前缀
- console.log('✓ 路径处理完成:', modelUrl)
- }
-
- console.log('========================================')
- console.log('最终确定的模型路径:', modelUrl)
- console.log('========================================')
-
- // 根据模型格式加载
- switch (format) {
- case 'GLTF':
- case 'GLB':
- console.log('开始加载GLTF/GLB模型')
- loadGLTFModel(modelUrl)
- break
- case 'OBJ':
- console.log('开始加载OBJ模型')
- loadOBJModel(modelUrl)
- break
- case 'FBX':
- console.log('开始加载FBX模型')
- loadFBXModel(modelUrl)
- break
- case 'STL':
- console.log('开始加载STL模型')
- loadSTLModel(modelUrl)
- break
- default:
- console.error('不支持的模型格式:', format)
- // 显示默认模型
- createDefaultModel()
- console.log('========================================')
- console.log('加载模型流程结束 - 格式不支持')
- console.log('========================================')
- }
- } else {
- console.error('❌ 模型数据中未找到文件路径或URL')
- // 显示默认模型
- createDefaultModel()
- console.log('========================================')
- console.log('加载模型流程结束 - 未找到路径')
- console.log('========================================')
- }
- } catch (error) {
- console.error('❌ 加载模型出错:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- // 显示默认模型
- createDefaultModel()
- console.log('========================================')
- console.log('加载模型流程结束 - 发生错误')
- console.log('========================================')
- }
- }
- // 加载GLTF/GLB模型
- const loadGLTFModel = (url) => {
- console.log('开始加载GLTF/GLB模型:', url)
-
- // 使用后端接口获取文件
- // 直接传递包含/profile前缀的完整路径,后端会处理前缀
- const resourcePath = url
- const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
- console.log('使用后端接口获取文件:', apiUrl)
-
- request({
- url: apiUrl,
- method: 'get',
- responseType: 'blob'
- })
- .then(response => {
- const blob = response
- console.log('响应是Blob,大小:', blob.size)
- console.log('Blob类型:', blob.type)
-
- // 使用Blob创建临时URL
- const blobUrl = URL.createObjectURL(blob)
- console.log('创建Blob URL:', blobUrl)
-
- // 使用Blob URL加载模型
- const loader = new GLTFLoader()
- loader.load(
- blobUrl,
- (gltf) => {
- console.log('GLTF/GLB模型加载成功:', gltf)
- model = gltf.scene
- scene.add(model)
- // 使用模型自身的原始坐标,不进行中心调整
- console.log('模型添加到场景,使用原始坐标')
- console.log('模型原始位置:', model.position)
- console.log('模型原始旋转:', model.rotation)
- console.log('模型原始缩放:', model.scale)
- // 调整相机位置,使模型在视角中心且完整可见
- adjustCameraForModel()
- console.log('相机位置调整完成')
- // 修复半透明材质太透的问题
- fixTransparentMaterials(model)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- },
- (xhr) => {
- console.log('GLTF/GLB模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
- },
- (error) => {
- console.error('GLTF加载错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- createDefaultModel()
- }
- )
- })
- .catch(error => {
- console.error('后端接口请求错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- createDefaultModel()
- })
- }
- // 加载OBJ模型
- const loadOBJModel = (url) => {
- console.log('开始加载OBJ模型:', url)
-
- // 使用后端接口获取文件
- // 直接传递包含/profile前缀的完整路径,后端会处理前缀
- const resourcePath = url
- const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
- console.log('使用后端接口获取文件:', apiUrl)
-
- request({
- url: apiUrl,
- method: 'get',
- responseType: 'blob'
- })
- .then(response => {
- const blob = response
- console.log('OBJ模型响应是Blob,大小:', blob.size)
- console.log('Blob类型:', blob.type)
-
- // 使用Blob创建临时URL
- const blobUrl = URL.createObjectURL(blob)
- console.log('创建OBJ Blob URL:', blobUrl)
-
- // 使用Blob URL加载模型
- const loader = new OBJLoader()
- loader.load(
- blobUrl,
- (obj) => {
- console.log('OBJ模型加载成功:', obj)
- model = obj
- // 添加材质
- const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
- obj.traverse((child) => {
- if (child.isMesh) {
- child.material = material
- }
- })
- scene.add(model)
- // 使用模型自身的原始坐标,不进行中心调整
- console.log('模型添加到场景,使用原始坐标')
- console.log('模型原始位置:', model.position)
- console.log('模型原始旋转:', model.rotation)
- console.log('模型原始缩放:', model.scale)
- // 调整相机位置,使模型在视角中心且完整可见
- adjustCameraForModel()
- console.log('相机位置调整完成')
- // 修复半透明材质太透的问题
- fixTransparentMaterials(model)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- },
- (xhr) => {
- console.log('OBJ模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
- },
- (error) => {
- console.error('OBJ加载错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- createDefaultModel()
- }
- )
- })
- .catch(error => {
- console.error('后端接口请求错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- createDefaultModel()
- })
- }
- // 处理FBX模型的特殊问题
- const processFBXModel = (fbxModel) => {
- console.log('开始专业处理FBX模型')
-
- // 1. FBX模型坐标系转换
- console.log('处理FBX坐标系...')
-
- // FBX通常使用Y-up坐标系,Three.js使用Y-up,所以不需要翻转
- // 但需要确保缩放和位置正确
-
- // 2. 统一模型缩放
- console.log('处理FBX模型缩放...')
- console.log('原始模型缩放:', fbxModel.scale)
-
- // 计算模型的边界框,用于确定合适的缩放
- let tempBox = new THREE.Box3().setFromObject(fbxModel)
- let tempSize = tempBox.getSize(new THREE.Vector3())
- console.log('原始模型边界框大小:', tempSize)
-
- // 计算模型的整体尺寸
- const maxDimension = Math.max(tempSize.x, tempSize.y, tempSize.z)
- console.log('模型最大尺寸:', maxDimension)
-
- // 计算合适的缩放因子,确保模型大小适中
- let scaleFactor = 1
- if (maxDimension > 10) {
- scaleFactor = 5 / maxDimension
- console.log('模型过大,需要缩小,缩放因子:', scaleFactor)
- } else if (maxDimension < 1) {
- scaleFactor = 2 / maxDimension
- console.log('模型过小,需要放大,缩放因子:', scaleFactor)
- }
-
- // 应用缩放因子到整个模型
- if (scaleFactor !== 1) {
- fbxModel.scale.multiplyScalar(scaleFactor)
- console.log('缩放调整完成,新的模型缩放:', fbxModel.scale)
- }
-
- // 3. 强制更新所有矩阵
- console.log('强制更新所有模型矩阵...')
- fbxModel.traverse((child) => {
- if (child.isObject3D) {
- child.updateMatrix()
- child.updateMatrixWorld(true)
- }
- })
-
- // 4. 计算模型边界框
- console.log('计算FBX模型边界框...')
- const box = new THREE.Box3().setFromObject(fbxModel)
- const size = box.getSize(new THREE.Vector3())
- const center = box.getCenter(new THREE.Vector3())
-
- console.log('FBX模型边界框大小:', size)
- console.log('FBX模型边界框中心:', center)
-
- // 5. 处理材质
- console.log('处理FBX模型材质...')
- let materialCount = 0
-
- fbxModel.traverse((child) => {
- if (child.isMesh) {
- console.log('处理网格:', child.name)
-
- try {
- // 为所有FBX网格添加默认材质,确保每个网格都有材质
- console.log('为', child.name, '添加默认材质')
- const defaultMaterial = new THREE.MeshStandardMaterial({
- color: 0x409eff,
- metalness: 0.3,
- roughness: 0.7,
- transparent: false,
- opacity: 1,
- side: THREE.FrontSide
- })
- child.material = defaultMaterial
- materialCount++
- } catch (error) {
- console.error('处理材质时出错:', error.message)
- const fallbackMaterial = new THREE.MeshStandardMaterial({
- color: 0x409eff,
- metalness: 0.3,
- roughness: 0.7,
- side: THREE.FrontSide
- })
- child.material = fallbackMaterial
- materialCount++
- }
- }
- })
-
- console.log('材质处理完成,添加了', materialCount, '个默认材质')
-
- // 6. 最终矩阵更新
- fbxModel.updateMatrixWorld(true)
- console.log('FBX模型处理完成')
-
- return {
- model: fbxModel,
- box: box,
- size: size,
- center: center
- }
- }
- // 加载FBX模型
- const loadFBXModel = (url) => {
- console.log('开始加载FBX模型:', url)
-
- // 使用后端接口获取文件
- // 直接传递包含/profile前缀的完整路径,后端会处理前缀
- const resourcePath = url
- const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
- console.log('使用后端接口获取文件:', apiUrl)
-
- request({
- url: apiUrl,
- method: 'get',
- responseType: 'blob'
- })
- .then(response => {
- const blob = response
- console.log('FBX模型响应是Blob,大小:', blob.size)
- console.log('Blob类型:', blob.type)
-
- // 使用Blob创建临时URL
- const blobUrl = URL.createObjectURL(blob)
- console.log('创建FBX Blob URL:', blobUrl)
-
- // 使用Blob URL加载模型
- const loader = new FBXLoader()
- loader.load(
- blobUrl,
- (fbx) => {
- console.log('FBX模型加载成功:', fbx)
-
- // 专业处理FBX模型
- const processed = processFBXModel(fbx)
- model = processed.model
-
- // 添加到场景
- scene.add(model)
- console.log('FBX模型添加到场景')
-
- // 计算最终边界框
- console.log('计算最终模型边界框...')
- model.updateMatrixWorld(true)
- const finalBox = new THREE.Box3().setFromObject(model)
- const finalSize = finalBox.getSize(new THREE.Vector3())
- const finalCenter = finalBox.getCenter(new THREE.Vector3())
-
- console.log('最终模型边界框大小:', finalSize)
- console.log('最终模型边界框中心:', finalCenter)
- console.log('模型实际位置:', model.position)
-
- // 调整相机位置
- console.log('调整相机位置...')
- adjustCameraForModel()
- console.log('相机位置调整完成')
- // 修复半透明材质太透的问题
- fixTransparentMaterials(model)
-
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- console.log('FBX模型加载和处理完成')
- },
- (xhr) => {
- console.log('FBX模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
- },
- (error) => {
- console.error('FBX加载错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- createDefaultModel()
- }
- )
- })
- .catch(error => {
- console.error('后端接口请求错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- createDefaultModel()
- })
- }
- // 加载STL模型
- const loadSTLModel = (url) => {
- console.log('开始加载STL模型:', url)
-
- // 使用后端接口获取文件
- // 直接传递包含/profile前缀的完整路径,后端会处理前缀
- const resourcePath = url
- const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
- console.log('使用后端接口获取文件:', apiUrl)
-
- request({
- url: apiUrl,
- method: 'get',
- responseType: 'blob'
- })
- .then(response => {
- const blob = response
- console.log('STL模型响应是Blob,大小:', blob.size)
- console.log('Blob类型:', blob.type)
-
- // 使用Blob创建临时URL
- const blobUrl = URL.createObjectURL(blob)
- console.log('创建STL Blob URL:', blobUrl)
-
- // 使用Blob URL加载模型
- const loader = new STLLoader()
- loader.load(
- blobUrl,
- (geometry) => {
- console.log('STL模型加载成功,几何体顶点数:', geometry.attributes.position.count)
- const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
- const mesh = new THREE.Mesh(geometry, material)
- model = mesh
- scene.add(mesh)
- // 使用模型自身的原始坐标,不进行中心调整
- console.log('模型添加到场景,使用原始坐标')
- console.log('模型原始位置:', model.position)
- console.log('模型原始旋转:', model.rotation)
- console.log('模型原始缩放:', model.scale)
- // 调整相机位置,使模型在视角中心且完整可见
- adjustCameraForModel()
- console.log('相机位置调整完成')
- // 修复半透明材质太透的问题
- fixTransparentMaterials(model)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- },
- (xhr) => {
- console.log('STL模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
- },
- (error) => {
- console.error('STL加载错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- // 释放Blob URL
- URL.revokeObjectURL(blobUrl)
- createDefaultModel()
- }
- )
- })
- .catch(error => {
- console.error('后端接口请求错误:', error)
- console.error('错误详情:', error.message)
- console.error('错误堆栈:', error.stack)
- createDefaultModel()
- })
- }
- // 创建默认模型
- const createDefaultModel = () => {
- // 创建一个简单的立方体作为默认模型
- const geometry = new THREE.BoxGeometry(2, 2, 2)
- const material = new THREE.MeshStandardMaterial({
- color: 0x409eff,
- wireframe: true
- })
- model = new THREE.Mesh(geometry, material)
- scene.add(model)
- }
- // 调整相机位置,使模型在视角中心且完整可见
- const adjustCameraForModel = () => {
- if (!model || !camera) return
- console.log('=======================================')
- console.log('开始调整相机位置')
- console.log('当前模型位置:', model.position)
- console.log('当前相机位置:', camera.position)
- // 增强:强制更新所有模型和子对象的矩阵
- console.log('强制更新模型和子对象矩阵...')
- model.traverse((child) => {
- if (child.isObject3D) {
- child.updateMatrix()
- child.updateMatrixWorld(true)
- }
- })
- console.log('模型矩阵更新完成')
- // 计算模型边界框
- const box = new THREE.Box3().setFromObject(model)
- const size = box.getSize(new THREE.Vector3())
- const center = box.getCenter(new THREE.Vector3())
- console.log('模型边界框大小:', size)
- console.log('模型边界框中心:', center)
- console.log('边界框最小值:', box.min)
- console.log('边界框最大值:', box.max)
- // 增强:检查边界框是否有效
- if (box.isEmpty()) {
- console.warn('模型边界框为空,使用默认相机位置')
- // 使用默认相机位置
- camera.position.set(5, 5, 5)
- camera.lookAt(0, 0, 0)
- camera.updateProjectionMatrix()
- console.log('使用默认相机位置:', camera.position)
- console.log('相机位置调整完成')
- console.log('=======================================')
- return
- }
- // 计算模型对角线长度
- const modelDiagonal = Math.sqrt(
- size.x * size.x +
- size.y * size.y +
- size.z * size.z
- )
-
- console.log('模型对角线长度:', modelDiagonal)
- // 根据模型大小计算合适的相机距离
- const fov = camera.fov * (Math.PI / 180) // 转换为弧度
-
- // 增强:使用动态安全系数,根据模型大小调整
- let safetyFactor = 1.3
- if (modelDiagonal < 5) {
- safetyFactor = 1.5 // 小模型使用更大的安全系数
- } else if (modelDiagonal > 20) {
- safetyFactor = 1.1 // 大模型使用更小的安全系数
- }
-
- console.log('使用的安全系数:', safetyFactor)
- const cameraDistance = (modelDiagonal / 2) / Math.tan(fov / 2) * safetyFactor
-
- console.log('计算的相机距离:', cameraDistance)
- console.log('当前相机到模型中心距离:', camera.position.distanceTo(center))
- // 增强:确保相机距离不会太小
- const minCameraDistance = Math.max(2, modelDiagonal * 0.5)
- const finalCameraDistance = Math.max(minCameraDistance, cameraDistance)
- console.log('最终相机距离:', finalCameraDistance, '(最小距离:', minCameraDistance, ')')
- // 增强:对于FBX模型,确保相机位置计算考虑模型的实际位置
- console.log('模型实际位置:', model.position)
- console.log('边界框中心相对于模型位置:', center.clone().sub(model.position))
- // 设置相机位置,从斜上方看向模型中心
- // 这样可以获得更好的视角
- const cameraHeight = finalCameraDistance * 0.4 // 相机高度,40%的距离
-
- // 增强:考虑模型的实际位置
- const cameraPosition = new THREE.Vector3(
- center.x + finalCameraDistance * 0.2, // 稍微偏右
- center.y + cameraHeight, // 上方
- center.z + finalCameraDistance * 0.9 // 前方
- )
-
- console.log('计算的相机位置:', cameraPosition)
- camera.position.copy(cameraPosition)
-
- // 相机看向模型中心
- camera.lookAt(center)
-
- console.log('调整后相机位置:', camera.position)
- console.log('相机看向:', center)
- console.log('调整后相机到模型中心距离:', camera.position.distanceTo(center))
- // 确保相机矩阵更新
- camera.updateProjectionMatrix()
- console.log('相机投影矩阵已更新')
- // 增强:重置相机的旋转和缩放
- console.log('重置相机旋转和缩放...')
- camera.rotation.set(0, 0, 0)
- camera.scale.set(1, 1, 1)
- console.log('相机旋转和缩放已重置')
- console.log('相机位置调整完成,现在应该能完整看到模型')
- console.log('=======================================')
- }
- // 自动调整模型位置和大小(保留但不再使用)
- const centerModel = () => {
- if (!model || !camera) return
- console.log('=======================================')
- console.log('开始调整模型位置和大小')
- console.log('调整前模型位置:', model.position)
- console.log('调整前模型缩放:', model.scale)
- console.log('调整前模型旋转:', model.rotation)
- console.log('调整前模型世界矩阵:', model.matrixWorld.elements)
- // 重置模型旋转,确保计算边界框时不受旋转影响
- model.rotation.set(0, 0, 0)
- console.log('重置模型旋转后:', model.rotation)
- // 确保模型矩阵更新
- model.updateMatrixWorld(true)
- console.log('模型矩阵已更新')
- console.log('重置旋转后模型世界矩阵:', model.matrixWorld.elements)
- // 计算模型边界框
- const box = new THREE.Box3().setFromObject(model)
- const size = box.getSize(new THREE.Vector3())
- const center = box.getCenter(new THREE.Vector3())
- console.log('模型边界框大小:', size)
- console.log('模型边界框中心:', center)
- console.log('边界框最小值:', box.min)
- console.log('边界框最大值:', box.max)
- // 直接设置模型位置到原点,确保在世界中心
- console.log('准备调整模型位置到原点...')
- console.log('计算的位置调整值:', -center.x, -center.y, -center.z)
- model.position.set(-center.x, -center.y, -center.z)
- console.log('模型调整后位置:', model.position)
- console.log('模型位置是否接近原点:',
- Math.abs(model.position.x) < 0.001 &&
- Math.abs(model.position.y) < 0.001 &&
- Math.abs(model.position.z) < 0.001
- )
- // 确保模型矩阵更新
- model.updateMatrixWorld(true)
- console.log('位置调整后模型矩阵已更新')
- console.log('位置调整后模型世界矩阵:', model.matrixWorld.elements)
-
- // 重新计算调整后的边界框,验证位置是否正确
- const positionAdjustedBox = new THREE.Box3().setFromObject(model)
- const positionAdjustedCenter = positionAdjustedBox.getCenter(new THREE.Vector3())
- console.log('位置调整后模型边界框中心:', positionAdjustedCenter)
- console.log('边界框中心是否接近原点:',
- Math.abs(positionAdjustedCenter.x) < 0.001 &&
- Math.abs(positionAdjustedCenter.y) < 0.001 &&
- Math.abs(positionAdjustedCenter.z) < 0.001
- )
- // 自动调整模型大小 - 使用更大的基准值,确保模型不会太小
- const maxSize = Math.max(size.x, size.y, size.z)
- if (maxSize > 0) {
- // 使用 5 作为基准值,而不是 3,让模型更大一些
- const scale = 5 / maxSize
- console.log('准备调整模型缩放...')
- console.log('计算的缩放比例:', scale)
- model.scale.set(scale, scale, scale)
- console.log('模型调整后缩放:', model.scale)
- console.log('调整后模型最大尺寸:', maxSize * scale)
- }
- // 确保模型矩阵更新
- model.updateMatrixWorld(true)
- console.log('模型缩放后矩阵已更新')
- console.log('缩放后模型世界矩阵:', model.matrixWorld.elements)
-
- // 重新计算调整后的边界框
- const adjustedBox = new THREE.Box3().setFromObject(model)
- const adjustedSize = adjustedBox.getSize(new THREE.Vector3())
- const adjustedCenter = adjustedBox.getCenter(new THREE.Vector3())
- console.log('调整后模型边界框大小:', adjustedSize)
- console.log('调整后模型边界框中心:', adjustedCenter)
- console.log('最终边界框中心是否接近原点:',
- Math.abs(adjustedCenter.x) < 0.001 &&
- Math.abs(adjustedCenter.y) < 0.001 &&
- Math.abs(adjustedCenter.z) < 0.001
- )
- // 动态调整相机位置,确保相机能够完整看到模型
- const modelDiagonal = Math.sqrt(
- adjustedSize.x * adjustedSize.x +
- adjustedSize.y * adjustedSize.y +
- adjustedSize.z * adjustedSize.z
- )
-
- // 根据模型对角线长度计算合适的相机距离
- const fov = camera.fov * (Math.PI / 180) // 转换为弧度
- const cameraDistance = (modelDiagonal / 2) / Math.tan(fov / 2) * 1.5 // 1.5 是安全系数
-
- console.log('模型对角线长度:', modelDiagonal)
- console.log('计算的相机距离:', cameraDistance)
- // 设置相机位置,确保从适当的距离和角度观察模型
- camera.position.set(0, cameraDistance * 0.3, cameraDistance)
- camera.lookAt(0, 0, 0)
-
- console.log('调整后相机位置:', camera.position)
- console.log('相机看向:', new THREE.Vector3(0, 0, 0))
- console.log('相机到原点距离:', camera.position.distanceTo(new THREE.Vector3(0, 0, 0)))
- // 确保相机矩阵更新
- camera.updateProjectionMatrix()
- console.log('相机投影矩阵已更新')
- console.log('模型位置调整完成,现在应该在世界坐标系原点')
- console.log('=======================================')
- }
- // 渲染动画
- const animate = () => {
- animationId = requestAnimationFrame(animate)
- if (renderer && scene && camera) {
- renderer.render(scene, camera)
- }
- }
- // 处理窗口大小变化
- const handleResize = () => {
- if (!camera || !renderer || !modelContainer.value) return
- const width = modelContainer.value.clientWidth
- const height = modelContainer.value.clientHeight
- camera.aspect = width / height
- camera.updateProjectionMatrix()
- renderer.setSize(width, height)
- }
- // 处理关闭
- const handleClose = () => {
- dialogVisible.value = false
- emit('update:visible', false)
- emit('close')
- }
- // 监听可见性变化
- watch(() => props.visible, (newVal) => {
- dialogVisible.value = newVal
- if (newVal) {
- // 延迟初始化,确保DOM已更新
- setTimeout(() => {
- initScene()
- }, 100)
- }
- })
- // 组件挂载时初始化
- onMounted(() => {
- dialogVisible.value = props.visible
- if (props.visible) {
- initScene()
- }
- })
- // 组件卸载时清理
- onUnmounted(() => {
- if (animationId) {
- cancelAnimationFrame(animationId)
- }
- if (renderer) {
- renderer.dispose()
- }
- if (modelContainer.value) {
- window.removeEventListener('resize', handleResize)
- }
- })
- // 计算属性
- const modelName = ref('')
- watch(() => props.modelData, (newVal) => {
- if (newVal) {
- modelName.value = newVal.name || '模型预览'
- }
- }, { immediate: true })
- // 光照控制方法
- const toggleLightControl = () => {
- showLightControl.value = !showLightControl.value
- }
- // 更新光源设置
- const updateLights = () => {
- // 保存相机当前位置和目标点
- let cameraPosition = null
- let cameraTarget = null
- if (camera) {
- cameraPosition = camera.position.clone()
- // 获取相机看向的点
- const vector = new THREE.Vector3()
- camera.getWorldDirection(vector)
- cameraTarget = camera.position.clone().add(vector)
- }
-
- // 更新光源属性
- if (ambientLight) {
- ambientLight.intensity = lightSettings.value.ambientIntensity
- }
- if (directionalLight) {
- directionalLight.intensity = lightSettings.value.directionalIntensity
- directionalLight.position.set(
- lightSettings.value.directionalPosition.x,
- lightSettings.value.directionalPosition.y,
- lightSettings.value.directionalPosition.z
- )
- }
- if (directionalLight2) {
- directionalLight2.intensity = lightSettings.value.directionalIntensity2
- }
-
- // 恢复相机位置和目标点
- if (camera && cameraPosition) {
- camera.position.copy(cameraPosition)
- camera.lookAt(cameraTarget)
- camera.updateProjectionMatrix()
- }
- }
- // 修复半透明材质太透的问题
- const fixTransparentMaterials = (modelObject) => {
- if (!modelObject) return
-
- let fixedCount = 0
-
- modelObject.traverse((child) => {
- if (child.isMesh && child.material) {
- const materials = Array.isArray(child.material) ? child.material : [child.material]
-
- materials.forEach((material) => {
- // 检查是否使用了透明度贴图
- const hasAlphaMap = material.alphaMap && material.alphaMap !== null
- const isTransparent = material.transparent === true
- const opacity = material.opacity
-
- // 如果有透明度贴图或设置了transparent
- if (hasAlphaMap || isTransparent) {
- // 确保透明模式开启
- material.transparent = true
-
- // 启用双面渲染,确保半透明效果正确显示
- material.side = THREE.DoubleSide
-
- // 如果opacity太低或等于1(全透/全不透),设置为一个合适的值
- if (opacity === undefined || opacity >= 1 || opacity < 0.1) {
- material.opacity = 0.8
- fixedCount++
- }
-
- // 修复depthWrite问题 - 对于半透明材质,depthWrite通常应为false
- material.depthWrite = false
- material.needsUpdate = true
- }
- })
- }
- })
-
- console.log('修复了', fixedCount, '个半透明材质的透明度')
- }
- // 更新模型材质的不透明度
- const updateMaterialOpacity = () => {
- if (!model) return
-
- const opacity = materialOpacity.value
-
- model.traverse((child) => {
- if (child.isMesh && child.material) {
- const materials = Array.isArray(child.material) ? child.material : [child.material]
-
- materials.forEach((material) => {
- if (material.transparent === true || material.opacity < 1) {
- material.opacity = opacity
- material.needsUpdate = true
- }
- })
- }
- })
- }
- </script>
- <style scoped>
- .model-preview-container {
- display: flex;
- gap: 20px;
- height: 600px;
- }
- .model-container {
- flex: 1;
- background-color: #fafafa;
- border-radius: 8px;
- overflow: hidden;
- position: relative;
- }
- .model-info {
- width: 300px;
- background-color: #ffffff;
- border-radius: 8px;
- padding: 20px;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- overflow-y: auto;
- }
- .model-info h3 {
- margin-top: 0;
- margin-bottom: 20px;
- font-size: 18px;
- font-weight: 600;
- color: #303133;
- }
- @media (max-width: 768px) {
- .model-preview-container {
- flex-direction: column;
- }
- .model-info {
- width: 100%;
- max-height: 300px;
- }
- }
- /* 光照控制样式 */
- .light-control-btn {
- position: absolute;
- top: 10px;
- right: 10px;
- width: 40px;
- height: 40px;
- background-color: rgba(255, 255, 255, 0.9);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- transition: all 0.3s ease;
- }
- .light-control-btn:hover {
- background-color: #ffffff;
- transform: scale(1.1);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- }
- .light-control-panel {
- position: absolute;
- top: 60px;
- right: 10px;
- width: 300px;
- background-color: rgba(255, 255, 255, 0.95);
- border-radius: 8px;
- padding: 20px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
- z-index: 1000;
- backdrop-filter: blur(5px);
- pointer-events: auto;
- user-select: none;
- }
- .light-control-panel h4 {
- margin: 0 0 15px 0;
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- .control-item {
- margin-bottom: 20px;
- }
- .control-item label {
- display: block;
- margin-bottom: 8px;
- font-size: 14px;
- font-weight: 500;
- color: #606266;
- }
- .control-item .value {
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- font-size: 12px;
- color: #909399;
- min-width: 40px;
- text-align: right;
- }
- .angle-controls > div {
- position: relative;
- margin-bottom: 12px;
- }
- .angle-controls > div > span {
- display: inline-block;
- width: 40px;
- font-size: 12px;
- color: #909399;
- margin-right: 10px;
- }
- .angle-controls .el-slider {
- display: inline-block;
- width: calc(100% - 100px);
- vertical-align: middle;
- }
- .angle-controls .value {
- position: static;
- display: inline-block;
- transform: none;
- vertical-align: middle;
- }
- /* 响应式调整 */
- @media (max-width: 768px) {
- .light-control-panel {
- width: 250px;
- right: 5px;
- }
-
- .angle-controls .el-slider {
- width: calc(100% - 90px);
- }
- }
- </style>
|