diff --git a/src/assets/images/three/sample.glb b/src/assets/images/three/sample.glb new file mode 100644 index 0000000..a8eae0c Binary files /dev/null and b/src/assets/images/three/sample.glb differ diff --git a/src/components/Canvas/FlowCanvas/components/tools/threeModel/model.vue b/src/components/Canvas/FlowCanvas/components/tools/threeModel/model.vue index bdab5fa..cbf4fb6 100644 --- a/src/components/Canvas/FlowCanvas/components/tools/threeModel/model.vue +++ b/src/components/Canvas/FlowCanvas/components/tools/threeModel/model.vue @@ -4,7 +4,8 @@ import { useI18n } from 'vue-i18n' import gsap from 'gsap'; import * as THREE from 'three'; -import { initThree,clearModel,addModel } from './threeTool' +import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } from './threeTool' +import threeGlb from '@/assets/images/three/sample.glb' //const props = defineProps({ //}) @@ -28,27 +29,56 @@ const load = ref({ progress:0 }) // const textureLoader = ref(new THREE.TextureLoader())//材质 -//初始化 -const init = ()=>{ - //初始化threejs - if(scene.value)return - const initResult = initThree(threeDom.value) - scene.value = initResult.scene - group.value = initResult.group - camera.value = initResult.camera - renderer.value = initResult.renderer - ambient.value = initResult.ambient - controls.value = initResult.controls - pointLight.value = initResult.pointLight +const init = () => { + //初始化threejs + if (scene.value) return + const initResult = initThree(threeDom.value) + scene.value = initResult.scene + group.value = initResult.group + camera.value = initResult.camera + renderer.value = initResult.renderer + ambient.value = initResult.ambient + controls.value = initResult.controls + pointLight.value = initResult.pointLight - threeDom.value.ondblclick = (event:any)=>{ - let intersects = openModel(event); - if(!intersects || intersects.length<=0) return - const bbox = new THREE.Box3().setFromObject(intersects[0].object); - const size = new THREE.Vector3(); - let target2 = bbox.getCenter(size);//获取选中包围起来后的中心坐标 - animateCamera(camera.value.position,intersects[0].point,controls.value.target,target2) - } + threeDom.value.ondblclick = (event:any)=>{ + let intersects = openModel(event); + if(!intersects || intersects.length<=0) return + + const clickedObject = intersects[0].object; + const modelInfo = getModelInfo(clickedObject); + const { size } = modelInfo; + const maxSize = Math.max(size.x, size.y, size.z); + let distanceFactor = 1.2; + let heightFactor = 0.3; + let angle = 0; + if (size.y > size.x * 2) { + // 高瘦物体,拉远一点,稍微抬高视角 + distanceFactor = 1.5; + heightFactor = 0.4; + angle = Math.PI / 6; // 30度 + } else if (size.x > size.y * 2) { + // 扁平物体,降低视角 + heightFactor = 0.2; + angle = Math.PI / 8; // 22.5度 + } + const { position: cameraPos, target } = calculateCameraPosition( + modelInfo, + camera.value, + { + distanceFactor, + heightFactor, + angle + } + ); + // 执行相机动画 + animateCamera( + camera.value.position, + cameraPos, + controls.value.target, + target + ); + } } let openModel = (event:any)=>{ @@ -61,39 +91,43 @@ let openModel = (event:any)=>{ return intersects } let isTweening = false; -function animateCamera(current1:any, target1:any, current2:any, target2:any){ - if (isTweening) return - isTweening = true - let options = { - x1: current1.x, // 相机当前位置x - y1: current1.y, // 相机当前位置y - z1: current1.z, // 相机当前位置z - x2: current2.x, // 控制当前的中心点x - y2: current2.y, // 控制当前的中心点y - // z2: current2.z // 控制当前的中心点z - } - gsap.to(options,{ - x1: 0, // 新的相机位置x - y1: target2.y, // 新的相机位置y - z1: 1000, // 新的相机位置z - x2: 0, // 新的控制中心点位置x - y2: target2.y, // 新的控制中心点位置x - duration:1, - ease:'linear', - onUpdate:()=>{ - camera.value.position.x = options.x1; - camera.value.position.y = options.y1; - camera.value.position.z = options.z1; - controls.value.target.x = options.x2; - controls.value.target.y = options.y2; - // controls.value.target.z = object.z2; - controls.value.update(); - }, - onComplete:()=>{ - isTweening = false - } - // z2: target2.z // 新的控制中心点位置x - }) + +function animateCamera( + startCameraPos: THREE.Vector3, + endCameraPos: THREE.Vector3, + startTarget: THREE.Vector3, + endTarget: THREE.Vector3 +) { + if (isTweening) return; + isTweening = true; + + let options = { + cx: startCameraPos.x, + cy: startCameraPos.y, + cz: startCameraPos.z, + tx: startTarget.x, + ty: startTarget.y, + tz: startTarget.z + }; + + gsap.to(options, { + cx: endCameraPos.x, + cy: endCameraPos.y, + cz: endCameraPos.z, + tx: endTarget.x, + ty: endTarget.y, + tz: endTarget.z, + duration: 1, + ease: 'power2.inOut', // 使用更自然的缓动 + onUpdate: () => { + camera.value.position.set(options.cx, options.cy, options.cz); + controls.value.target.set(options.tx, options.ty, options.tz); + controls.value.update(); + }, + onComplete: () => { + isTweening = false; + } + }); } const setModel = async (url:any)=>{ clearModel(group,scene) @@ -118,7 +152,7 @@ const open = async ()=>{ // composer.render(); }; animate(); - await setModel("https://www.minio-api.aida.com.hk/aida-threed/female/glb/1%E7%9F%AD%E8%A2%96T%E6%81%A4.glb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260310%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260310T032933Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=184ec7f9ff3076dde5aca66e2d2e27f8d180add698a5b1040fe903a55cb2f85e") + await setModel(threeGlb) load.value.state = false } diff --git a/src/components/Canvas/FlowCanvas/components/tools/threeModel/threeTool.ts b/src/components/Canvas/FlowCanvas/components/tools/threeModel/threeTool.ts index 2236ced..34559a0 100644 --- a/src/components/Canvas/FlowCanvas/components/tools/threeModel/threeTool.ts +++ b/src/components/Canvas/FlowCanvas/components/tools/threeModel/threeTool.ts @@ -57,21 +57,25 @@ export const initThree = (threeDom)=>{ DirectionalLight 平行光,比如太阳光 SpotLight 聚光源 */ - const pointLight = new THREE.DirectionalLight(0xffffff,.5); - pointLight.intensity = 1.2 - pointLight.castShadow = true//开启阴影 - pointLight.shadow.mapSize = new THREE.Vector2(width, height) - scene.add(pointLight); //点光源添加到场景中 + //设置环境光全亮 + const pointLight = new THREE.AmbientLight(0xffffff,.2); + scene.add(pointLight); + // const pointLight = new THREE.AmbientLight(0xffffff,1.0); + // pointLight.intensity = 1.2//光源强度 + // pointLight.castShadow = true//开启阴影 + // pointLight.shadow.mapSize = new THREE.Vector2(width, height) + // scene.add(pointLight); //点光源添加到场景中 // pointLight.position.set(400, 200, 300); //点光源位置 - pointLight.position.y = 400; - pointLight.position.z = 200; - pointLight.position.x = 200; - let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小 - let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", }) - let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial); - floorMesh.rotation.x = -0.5 * Math.PI; - floorMesh.receiveShadow = true; - floorMesh.position.y = -0.001; + // pointLight.position.y = 100; + // pointLight.position.z = 50; + // pointLight.position.x = 100; + + // let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小 + // let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", }) + // let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial); + // floorMesh.rotation.x = -0.5 * Math.PI; + // floorMesh.receiveShadow = true; + // floorMesh.position.y = -0.001; // scene.add(floorMesh); const textureLoader = new THREE.TextureLoader(); // const texture = textureLoader.load('/3dModel/sketch-thick.jpg'); @@ -85,41 +89,144 @@ export const clearModel = (group,scene)=>{ scene.value.remove(oldGroup); } - +// 计算模型包围盒 +export const getModelInfo = (model: THREE.Object3D) => { + const box = new THREE.Box3().setFromObject(model); + const center = box.getCenter(new THREE.Vector3()); + const size = box.getSize(new THREE.Vector3()); + const maxSize = Math.max(size.x, size.y, size.z); + return { + box, + center, + size, + maxSize + }; +}; +// 根据模型信息计算相机位置 +export const calculateCameraPosition = ( + modelInfo: ReturnType, + camera: THREE.PerspectiveCamera, + options?: { + distanceFactor?: number; // 距离系数,默认1.5 + heightFactor?: number; // 高度偏移系数,默认0.3 + angle?: number; // 观察角度,默认正前方 + } +) => { + const { center, size, maxSize } = modelInfo; + const fov = camera.fov * (Math.PI / 180); + + const distanceFactor = options?.distanceFactor ?? 1.5; + const heightFactor = options?.heightFactor ?? 0.3; + + // 计算合适的相机距离 + const distance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor; + + // 根据角度计算相机位置 + const angle = options?.angle ?? 0; // 0表示正前方 + + return { + position: new THREE.Vector3( + center.x + distance * Math.sin(angle), + center.y + size.y * heightFactor, + center.z + distance * Math.cos(angle) + ), + target: center.clone(), + distance, + center, + size + }; +}; +// 根据模型信息计算光源位置 +export const calculateLightPosition = ( + modelInfo: ReturnType, + options?: { + xFactor?: number; // X轴偏移系数,默认0.5 + yFactor?: number; // Y轴偏移系数,默认0.8 + zFactor?: number; // Z轴偏移系数,默认0.5 + } +) => { + const { center, size } = modelInfo; + + const xFactor = options?.xFactor ?? 0.5; + const yFactor = options?.yFactor ?? 0.8; + const zFactor = options?.zFactor ?? 0.5; + + return new THREE.Vector3( + center.x + size.x * xFactor, + center.y + size.y * yFactor, + center.z + size.z * zFactor + ); +}; export const addModel = async ( - url:any, - controls:OrbitControls, - camera:THREE.PerspectiveCamera, - pointLight:THREE.DirectionalLight, - group:THREE.Group, - load:any)=>{ - await new Promise((resolve, reject) => { - var fbxLoader = new GLTFLoader(); - let drac = new DRACOLoader() - drac.setDecoderPath('/draco/') - fbxLoader.setDRACOLoader(drac) - // fbxLoader.load('/3dModel/222/1111.glb', - fbxLoader.load(url, - - (obj:any) => { - let scene = obj.scene; - var box = new THREE.Box3().setFromObject(scene); - var center = box.getCenter(new THREE.Vector3()); - controls.value.target.copy(center); - // controls.autoRotate = true - camera.value.position.y = center.y; - camera.value.position.z = 1000; - pointLight.value.position.y = 250; - pointLight.value.position.z = 1250; - group.value.add(scene); - resolve('') - },(xhr:any) => { // 加载进度回调 - const percent = xhr.total == 0?100:(xhr.loaded / xhr.total * 100).toFixed(2); - load.value.progress = percent - // updateProgressBar(Number(percent)); - },(error:any) => { // 加载失败回调 - console.error('模型加载失败:', error); - reject('') - }) - }) + url: any, + controls: OrbitControls, + camera: THREE.PerspectiveCamera, + pointLight: THREE.DirectionalLight, + group: THREE.Group, + load: any +) => { + await new Promise((resolve, reject) => { + var fbxLoader = new GLTFLoader(); + let drac = new DRACOLoader() + drac.setDecoderPath('/draco/') + fbxLoader.setDRACOLoader(drac) + + fbxLoader.load(url, + (obj: any) => { + let scene = obj.scene; + scene.traverse((child: any) => { + if (child.isMesh) { + // 如果是基础材质,转换为标准材质 + if (child.material instanceof THREE.MeshBasicMaterial) { + const oldMat = child.material; + child.material = new THREE.MeshStandardMaterial({ + map: oldMat.map, + color: oldMat.color, + roughness: 0.4, + metalness: 0 + }); + } + // 如果是标准材质,调整粗糙度 + else if (child.material instanceof THREE.MeshStandardMaterial) { + child.material.roughness = 0.4; + child.material.metalness = 0; + } + } + }); + // 获取模型信息 + const modelInfo = getModelInfo(scene); + const { position: cameraPos, target } = calculateCameraPosition( + modelInfo, + camera.value, + { + distanceFactor: 1.5, + heightFactor: 0.3, + angle: 0 // 正前方 + } + ); + // 设置相机位置 + camera.value.position.copy(cameraPos); + // 设置控制器目标点 + controls.value.target.copy(target); + // 计算并设置光源位置 + const lightPos = calculateLightPosition(modelInfo); + pointLight.value.position.copy(lightPos); + // 将模型添加到场景 + group.value.add(scene); + // 可选:将模型信息存储在模型上,方便后续使用 + (scene as any).userData.modelInfo = modelInfo; + + resolve('') + }, + (xhr: any) => { // 加载进度回调 + const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2); + load.value.progress = percent + console.log('模型加载进度:', percent); + }, + (error: any) => { // 加载失败回调 + console.error('模型加载失败:', error); + reject('') + } + ) + }) } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index f1bd5a0..1c71515 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -36,6 +36,7 @@ export default defineConfig(({ mode }) => { inject: 'body-last' // 注入位置优化 }) ], + assetsInclude: ['**/*.glb'], define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, },