mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-23 19:44:29 +08:00
AI3D 功能完成
This commit is contained in:
@@ -2,34 +2,6 @@
|
||||
<div class="three-d-preview">
|
||||
<div ref="container" class="preview-container"></div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<div class="control-group">
|
||||
<label>缩放</label>
|
||||
<div class="scale-controls">
|
||||
<el-button size="small" @click="zoomOut" :disabled="scale <= 0.1">
|
||||
<el-icon><Minus /></el-icon>
|
||||
</el-button>
|
||||
<span class="scale-value">{{ scale.toFixed(1) }}x</span>
|
||||
<el-button size="small" @click="zoomIn" :disabled="scale >= 3">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>模型颜色</label>
|
||||
<div class="color-picker">
|
||||
<el-color-picker
|
||||
v-model="modelColor"
|
||||
@change="updateModelColor"
|
||||
:predefine="predefineColors"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
@@ -56,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Loading, Minus, Plus, Warning } from '@element-plus/icons-vue'
|
||||
import { Loading, Warning } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElIcon } from 'element-plus'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
@@ -82,20 +54,17 @@ const container = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const loadingProgress = ref(0)
|
||||
const scale = ref(1)
|
||||
const modelColor = ref('#00ff88')
|
||||
const predefineColors = ref([
|
||||
'#00ff88', // 亮绿色
|
||||
'#ff6b6b', // 亮红色
|
||||
'#4ecdc4', // 亮青色
|
||||
'#45b7d1', // 亮蓝色
|
||||
'#f9ca24', // 亮黄色
|
||||
'#f0932b', // 亮橙色
|
||||
'#eb4d4b', // 亮粉红
|
||||
'#6c5ce7', // 亮紫色
|
||||
'#a29bfe', // 亮靛蓝
|
||||
'#fd79a8', // 亮玫瑰
|
||||
])
|
||||
const modelType = computed(() => {
|
||||
if (props.modelType) {
|
||||
return props.modelType.toLowerCase()
|
||||
}
|
||||
// 从模型URL中获取类型
|
||||
if (props.modelUrl) {
|
||||
const url = new URL(props.modelUrl)
|
||||
return url.pathname.split('.').pop()
|
||||
}
|
||||
return 'glb'
|
||||
})
|
||||
|
||||
// Three.js 相关变量
|
||||
let scene, camera, renderer, controls, model, mixer, clock
|
||||
@@ -111,12 +80,12 @@ const initThreeJS = () => {
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0x2a2a2a) // 深灰色背景,类似截图
|
||||
scene.background = new THREE.Color(0x2d2d2d) // 深灰色背景,匹配截图效果
|
||||
|
||||
// 获取容器尺寸,确保有最小尺寸
|
||||
// 获取容器尺寸,完全自适应父容器
|
||||
const containerRect = container.value.getBoundingClientRect()
|
||||
const width = Math.max(containerRect.width || 400, 400)
|
||||
const height = Math.max(containerRect.height || 300, 300)
|
||||
const width = containerRect.width || 400
|
||||
const height = containerRect.height || 300
|
||||
|
||||
// 创建相机 - 参考截图的视角(稍微俯视,从左上角观察)
|
||||
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
||||
@@ -130,9 +99,11 @@ const initThreeJS = () => {
|
||||
})
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.shadowMap.enabled = true
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
renderer.shadowMap.enabled = false // 禁用阴影
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
// 提升曝光度让模型更加高亮
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
renderer.toneMappingExposure = 2.2
|
||||
|
||||
// 添加到容器
|
||||
container.value.appendChild(renderer.domElement)
|
||||
@@ -163,60 +134,60 @@ const initThreeJS = () => {
|
||||
|
||||
//
|
||||
|
||||
// 添加光源 - 参考截图的柔和光照效果
|
||||
// 添加光源 - 高亮显示模型,无阴影效果
|
||||
const addLights = () => {
|
||||
// 环境光 - 提供基础照明,参考截图的柔和效果
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.6)
|
||||
// 强环境光 - 提供整体高亮照明
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0)
|
||||
scene.add(ambientLight)
|
||||
|
||||
// 主方向光 - 从左上角照射,模拟截图中的光照方向
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
directionalLight.position.set(5, 5, 3)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.shadow.mapSize.width = 2048
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
directionalLight.shadow.camera.near = 0.5
|
||||
directionalLight.shadow.camera.far = 50
|
||||
directionalLight.shadow.camera.left = -10
|
||||
directionalLight.shadow.camera.right = 10
|
||||
directionalLight.shadow.camera.top = 10
|
||||
directionalLight.shadow.camera.bottom = -10
|
||||
// 主方向光 - 从前上方照射,高亮度无阴影
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.8)
|
||||
directionalLight.position.set(5, 8, 5)
|
||||
directionalLight.castShadow = false
|
||||
scene.add(directionalLight)
|
||||
|
||||
// 补充光源 - 从右侧照射,提供更均匀的光照
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 0.4)
|
||||
fillLight.position.set(-3, 3, 3)
|
||||
// 补充光源 - 从左侧照射,填充光照
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
fillLight.position.set(-5, 4, 3)
|
||||
fillLight.castShadow = false
|
||||
scene.add(fillLight)
|
||||
|
||||
// 背光 - 增加轮廓,但强度较低
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.15)
|
||||
backLight.position.set(0, 2, -5)
|
||||
scene.add(backLight)
|
||||
// 背景光 - 从背后照射,增加轮廓高亮
|
||||
const rimLight = new THREE.DirectionalLight(0xffffff, 1.0)
|
||||
rimLight.position.set(0, 3, -5)
|
||||
rimLight.castShadow = false
|
||||
scene.add(rimLight)
|
||||
|
||||
// 顶部光源 - 增加顶部高亮
|
||||
const topLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
topLight.position.set(0, 10, 0)
|
||||
topLight.castShadow = false
|
||||
scene.add(topLight)
|
||||
}
|
||||
|
||||
// 添加地面网格 - 参考截图的深色背景和浅色网格线
|
||||
// 添加地面网格 - 简洁网格,无阴影
|
||||
const addGround = () => {
|
||||
// 创建网格辅助线 - 使用深色背景配浅色网格线,增加网格密度
|
||||
const gridHelper = new THREE.GridHelper(20, 40, 0x666666, 0x666666)
|
||||
gridHelper.position.y = -0.01 // 稍微向下一点,避免z-fighting
|
||||
// 创建网格辅助线 - 使用深色线条
|
||||
const gridHelper = new THREE.GridHelper(20, 20, 0x555555, 0x555555)
|
||||
gridHelper.position.y = 0
|
||||
scene.add(gridHelper)
|
||||
|
||||
// 添加半透明地面 - 使用更深的颜色
|
||||
// 简单透明地面平面
|
||||
const groundGeometry = new THREE.PlaneGeometry(20, 20)
|
||||
const groundMaterial = new THREE.MeshLambertMaterial({
|
||||
color: 0x1a1a1a, // 更深的背景色
|
||||
const groundMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0x404040,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
opacity: 0.1,
|
||||
})
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||
ground.rotation.x = -Math.PI / 2
|
||||
ground.receiveShadow = true
|
||||
ground.position.y = -0.01
|
||||
scene.add(ground)
|
||||
}
|
||||
|
||||
// 添加坐标轴辅助线 - 参考截图的样式
|
||||
// 添加坐标轴辅助线 - 匹配截图样式
|
||||
const addAxesHelper = () => {
|
||||
const axesHelper = new THREE.AxesHelper(3) // 稍微小一点的坐标轴
|
||||
const axesHelper = new THREE.AxesHelper(2)
|
||||
scene.add(axesHelper)
|
||||
}
|
||||
|
||||
@@ -244,7 +215,7 @@ const loadModel = async () => {
|
||||
|
||||
let loadedModel
|
||||
|
||||
switch (props.modelType.toLowerCase()) {
|
||||
switch (modelType.value) {
|
||||
case 'glb':
|
||||
case 'gltf':
|
||||
loadedModel = await loadGLTF(props.modelUrl)
|
||||
@@ -256,7 +227,7 @@ const loadModel = async () => {
|
||||
loadedModel = await loadSTL(props.modelUrl)
|
||||
break
|
||||
default:
|
||||
throw new Error(`不支持的模型格式: ${props.modelType}`)
|
||||
throw new Error(`不支持的模型格式: ${modelType.value}`)
|
||||
}
|
||||
|
||||
if (loadedModel) {
|
||||
@@ -276,13 +247,13 @@ const loadModel = async () => {
|
||||
baseScale = maxDim > 0 ? 2 / maxDim : 1
|
||||
|
||||
// 应用初始缩放
|
||||
model.scale.setScalar(baseScale * scale.value)
|
||||
model.scale.setScalar(baseScale)
|
||||
|
||||
// 根据模型大小调整相机距离 - 保持截图中的俯视角度
|
||||
const cameraDistance = maxDim > 0 ? maxDim * 2 : 5
|
||||
|
||||
// 设置相机位置为左上角俯视角度
|
||||
camera.position.set(cameraDistance * 0.6, cameraDistance * 0.6, cameraDistance * 0.6)
|
||||
// 设置相机位置 - 匹配截图中的正面稍俯视角度
|
||||
camera.position.set(cameraDistance * 0.3, cameraDistance * 0.4, cameraDistance * 1.2)
|
||||
camera.lookAt(0, 0, 0)
|
||||
|
||||
if (controls) {
|
||||
@@ -290,28 +261,14 @@ const loadModel = async () => {
|
||||
controls.update()
|
||||
}
|
||||
|
||||
// 设置阴影和材质
|
||||
// 移除阴影设置,让模型高亮显示
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
|
||||
// 将模型材质改为亮色
|
||||
child.castShadow = false
|
||||
child.receiveShadow = false
|
||||
// 如果材质支持,增加发光效果
|
||||
if (child.material) {
|
||||
const colorHex = modelColor.value.replace('#', '0x')
|
||||
// 如果是数组材质
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((mat) => {
|
||||
if (mat.color) {
|
||||
mat.color.setHex(colorHex)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 单个材质
|
||||
if (child.material.color) {
|
||||
child.material.color.setHex(colorHex)
|
||||
}
|
||||
}
|
||||
child.material.emissive = new THREE.Color(0x111111) // 轻微发光
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -403,51 +360,6 @@ const loadSTL = (url) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 放大
|
||||
const zoomIn = () => {
|
||||
if (scale.value < 3) {
|
||||
scale.value = Math.min(scale.value + 0.1, 3)
|
||||
updateScale(scale.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小
|
||||
const zoomOut = () => {
|
||||
if (scale.value > 0.1) {
|
||||
scale.value = Math.max(scale.value - 0.1, 0.1)
|
||||
updateScale(scale.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缩放
|
||||
const updateScale = (value) => {
|
||||
if (model) {
|
||||
model.scale.setScalar(baseScale * value)
|
||||
console.log('ThreeDPreview: 更新缩放', { value, baseScale, finalScale: baseScale * value })
|
||||
}
|
||||
}
|
||||
|
||||
// 更新模型颜色
|
||||
const updateModelColor = (color) => {
|
||||
if (model && color) {
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh && child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((mat) => {
|
||||
if (mat.color) {
|
||||
mat.color.setHex(color.replace('#', '0x'))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (child.material.color) {
|
||||
child.material.color.setHex(color.replace('#', '0x'))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// 重试加载
|
||||
@@ -460,8 +372,8 @@ const onWindowResize = () => {
|
||||
if (!container.value || !camera || !renderer) return
|
||||
|
||||
const containerRect = container.value.getBoundingClientRect()
|
||||
const width = Math.max(containerRect.width || 400, 400)
|
||||
const height = Math.max(containerRect.height || 300, 300)
|
||||
const width = containerRect.width || 400
|
||||
const height = containerRect.height || 300
|
||||
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
@@ -552,62 +464,24 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
background: #f0f0f0;
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
// 移除min-height限制,让高度完全自适应
|
||||
|
||||
.control-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.el-color-picker--small {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.scale-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.scale-value {
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
// 确保在弹窗中能正确填充
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,15 +559,4 @@ onUnmounted(() => {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.control-panel {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
margin: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user