Files
aida_front/src/component/HomePage/index/model/patternMaking3D/three.vue

552 lines
17 KiB
Vue
Raw Normal View History

2025-04-01 15:25:15 +08:00
<template>
<div class="three">
2025-04-23 09:39:24 +08:00
<div class="parameter">
<label>
2025-04-30 14:01:52 +08:00
<span>scaleX:</span>
2025-04-23 09:39:24 +08:00
<a-slider class="system_silder"
v-model:value="repeat.x"
:tooltipVisible="false"
@change="changeRepeat"
2025-04-30 14:01:52 +08:00
:max="6"
2025-04-23 09:39:24 +08:00
:step="0.001"
:min="0.002"
>
</a-slider>
</label>
<label>
2025-04-30 14:01:52 +08:00
<span>scaleY:</span>
2025-04-23 09:39:24 +08:00
<a-slider class="system_silder"
v-model:value="repeat.y"
:tooltipVisible="false"
@change="changeRepeat"
2025-04-30 14:01:52 +08:00
:max="6"
:min="0.002"
:step="0.001"
2025-04-23 09:39:24 +08:00
>
</a-slider>
</label>
2025-04-30 14:01:52 +08:00
<i class="fi fi-br-link" :class="{'fi-br-link':isLock,'fi-bs-link-slash':!isLock}" @click="setLock"></i>
2025-04-23 09:39:24 +08:00
</div>
2025-04-01 15:25:15 +08:00
<div class="model" ref="threeDom">
</div>
<div class="load" v-show="load.state">
<i class="fi fi-rr-cubes"></i>
<div class="text">Load...</div>
<div class="loadBox">
<div class="schedule" :style="{width:load.progress+'%'}"></div>
</div>
</div>
</div>
</template>
<script lang="ts">
2025-04-23 09:39:24 +08:00
import { defineComponent,computed,shallowRef,provide,nextTick,onMounted,toRefs, reactive, onBeforeUnmount} from 'vue'
2025-04-01 15:25:15 +08:00
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { Https } from "@/tool/https";
import { useStore } from "vuex";
import { useI18n } from 'vue-i18n'
// @ts-ignore
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
2025-04-09 14:53:13 +08:00
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
2025-04-01 15:25:15 +08:00
2025-04-09 14:53:13 +08:00
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
2025-04-01 15:25:15 +08:00
import gsap from 'gsap';
import { env } from 'echarts';
export default defineComponent({
components:{
},
props:{
},
emits:[],
setup(props,{emit}) {
const store = useStore();
const data = reactive({
scene:shallowRef() as any,//场景
group:shallowRef() as any,//组
camera:shallowRef() as any,//相机
renderer:shallowRef() as any,//渲染器
pointLight:shallowRef() as any,//光
controls:shallowRef() as any,//监听鼠标、键盘事件
2025-04-23 09:39:24 +08:00
textureLoader:shallowRef() as any,//材质
2025-04-01 15:25:15 +08:00
load:{
state:false,
progress:0 as any,
2025-04-23 09:39:24 +08:00
},
repeat:{
x:1,
y:1,
},
animationId:null as any,
2025-04-30 14:01:52 +08:00
isLock:false,
2025-04-01 15:25:15 +08:00
})
const dataDom = reactive({
threeDom:null as any,
})
2025-04-23 09:39:24 +08:00
const dataTime = reactive({
updataRepeat:null as any
})
2025-04-01 15:25:15 +08:00
const init = ()=>{
data.scene = new THREE.Scene();
data.group = new THREE.Group()
data.scene.add(data.group)
//创建相机对象
// this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
data.camera = new THREE.PerspectiveCamera(45, dataDom.threeDom.offsetWidth / dataDom.threeDom.offsetHeight, 0.1, 10000);
data.camera.position.set(0, 90, 6); //设置相机位置
data.camera.lookAt(data.scene.position); //设置相机方向(指向的场景对象)
/**
* 创建渲染器对象
*/
let width = dataDom.threeDom.offsetWidth; //窗口宽度
let height = dataDom.threeDom.offsetHeight; //窗口高度
data.renderer = new THREE.WebGLRenderer({
antialias: true,
logarithmicDepthBuffer: true,//深度缓存 防止模型闪烁重影
});
// data.renderer.outpuEncoding = THREE?.RGBEEncoding//设置输出颜色编码格式
data.renderer.toneMapping = THREE.ACESFilmicToneMapping;//设置色调
data.renderer.toneMappingExposure = 1.3;
data.renderer.shadowMap.enabled = true;
data.renderer.setPixelRatio(window.devicePixelRatio);
data.renderer.setSize(width, height); //设置渲染区域尺寸
data.renderer.setClearColor(0xffffff, 1); //设置背景颜色
dataDom.threeDom.innerHTML = '';
dataDom.threeDom.appendChild(data.renderer.domElement);
// 设置渲染器大小
//环境光
let ambient = new THREE.AmbientLight(0xffffff,.8);
data.scene.add(ambient);
data.controls = new OrbitControls(data.camera,data.renderer.domElement)//监听鼠标、键盘事件;
// data.controls.minDistance = 500; // 设置相机与焦点的最小距离
// data.controls.maxDistance = 4000; // 设置相机与焦点的最大距离
data.controls.mouseButtons = {
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转ROTATE)
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转ROTATE)
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
RIGHT:THREE.MOUSE.PAN // 右键 旋转默认拖动PAN
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转默认拖动PAN
}
/**
* 光源设置
*/
//点光源
/**
* AmbientLight 环境光
PointLight 点光源
DirectionalLight 平行光比如太阳光
SpotLight 聚光源
*/
data.pointLight = new THREE.DirectionalLight(0xffffff,.5);
data.pointLight.intensity = 1.2
data.pointLight.castShadow = true//开启阴影
data.pointLight.shadow.mapSize = new THREE.Vector2(width, height)
data.scene.add(data.pointLight); //点光源添加到场景中
// data.pointLight.position.set(400, 200, 300); //点光源位置
data.pointLight.position.y = 400;
data.pointLight.position.z = 200;
data.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;
2025-04-09 14:09:19 +08:00
// data.scene.add(floorMesh);
2025-04-01 15:25:15 +08:00
const textureLoader = new THREE.TextureLoader();
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
2025-04-09 14:09:19 +08:00
data.scene.background = new THREE.Color("#fff");
2025-04-01 15:25:15 +08:00
// data.scene.background = texture;
let openModel = (event:any)=>{
let mouse=new THREE.Vector2();
let raycaster=new THREE.Raycaster();
mouse.x=(event.clientX/window.innerWidth)*2-1;
mouse.y=-(event.clientY/window.innerHeight)*2+1;
raycaster.setFromCamera(mouse, data.camera);
let intersects = raycaster.intersectObjects(data.scene.children);
return intersects
}
dataDom.threeDom.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(data.camera.position,intersects[0].point,data.controls.target,target2)
}
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: 2500, // 新的相机位置z
x2: 0, // 新的控制中心点位置x
y2: target2.y, // 新的控制中心点位置x
duration:1,
ease:'linear',
onUpdate:()=>{
data.camera.position.x = options.x1;
data.camera.position.y = options.y1;
data.camera.position.z = options.z1;
data.controls.target.x = options.x2;
data.controls.target.y = options.y2;
// data.controls.target.z = object.z2;
data.controls.update();
},
onComplete:()=>{
isTweening = false
}
// z2: target2.z // 新的控制中心点位置x
})
}
// let setHighlight = (obj:any)=>{
// outlinePass.selectedObjects = obj;
// }
data.controls.enableDamping = true;
2025-04-23 09:39:24 +08:00
let animate = ()=>{
data.animationId = requestAnimationFrame(animate);
2025-04-01 15:25:15 +08:00
// data.renderer.render(data.scene, data.camera);
// model.rotation.x += 0.01; //旋转物体
var vector = data.camera.position.clone()
data.controls.update();
data.renderer.render(data.scene, data.camera);
// point.position.set(vector.x,vector.y,vector.z);
// group.rotation.y += 0.01;
// composer.render();
};
animate();
}
2025-04-23 09:39:24 +08:00
2025-04-01 15:25:15 +08:00
const setModel = async (url:any)=>{
clearModel()
await addModel(url)
// addMaterial()
}
2025-04-23 16:20:53 +08:00
const addMaterial =async (item:any)=>{
2025-04-01 15:25:15 +08:00
//添加图片材质
2025-04-23 16:20:53 +08:00
let url = item.url || item.url || item.imgUrl
2025-04-01 15:25:15 +08:00
data.load.state = true
let textureLoader = new THREE.TextureLoader()
2025-04-23 16:20:53 +08:00
await textureLoader.load(url, // 图片放在public/textures目录下
2025-04-01 15:25:15 +08:00
(texture:any) => {
2025-04-23 09:39:24 +08:00
data.textureLoader = texture
2025-04-01 15:25:15 +08:00
// 3. 配置纹理参数
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
2025-04-30 14:01:52 +08:00
// texture.wrapS = THREE.ClampToEdgeWrapping; // 水平方向不重复
// texture.wrapT = THREE.ClampToEdgeWrapping; // 垂直方向不重复
2025-04-16 10:43:54 +08:00
// texture.repeat.set(1, 1); // 纹理重复次数
2025-04-01 15:25:15 +08:00
texture.anisotropy = 32; // 提高纹理清晰度
data.group?.traverse((child:any) => {
if (child.isMesh) {
2025-04-30 14:01:52 +08:00
console.log(child.name)
2025-04-01 15:25:15 +08:00
// 5. 创建新材质(根据需求选择材质类型)
2025-04-16 10:43:54 +08:00
const textureWidth = texture.image.width;
const textureHeight = texture.image.height;
const box = new THREE.Box3().setFromObject(child);
const modelWidth = box.getSize(new THREE.Vector3()).x;
const modelHeight = box.getSize(new THREE.Vector3()).y;
2025-04-23 09:39:24 +08:00
data.repeat.x = 2 - modelWidth / textureWidth;
data.repeat.y = 2 - modelHeight / textureHeight;
2025-04-23 16:20:53 +08:00
let patternMaking3D = store.state.HomeStoreModule.patternMaking3D
if(patternMaking3D.x)data.repeat.x = patternMaking3D.x
if(patternMaking3D.y)data.repeat.y = patternMaking3D.y
2025-04-16 10:43:54 +08:00
// texture.repeat.set(1, 1); // 纹理重复次数
2025-04-23 09:39:24 +08:00
texture.repeat.set(2 - data.repeat.x, 2 - data.repeat.y); // 纹理重复次数
2025-04-01 15:25:15 +08:00
const newMaterial = new THREE.MeshStandardMaterial({
map: texture, // 基础颜色贴图
roughness: 0.7, // 表面粗糙度 (0-1)
2025-04-09 14:09:19 +08:00
metalness: .2, // 金属质感 (0-1)
2025-04-01 15:25:15 +08:00
side: THREE.DoubleSide // 双面渲染
});
// 7. 如果需要单独控制某些子模型的UV
if (child.geometry.attributes.uv) {
const uvs = child.geometry.attributes.uv.array;
2025-04-30 14:01:52 +08:00
// 计算UV边界
let minU = Infinity, maxU = -Infinity;
let minV = Infinity, maxV = -Infinity;
for (let i = 0; i < uvs.length; i += 2) {
minU = Math.min(minU, uvs[i]);
maxU = Math.max(maxU, uvs[i]);
minV = Math.min(minV, uvs[i+1]);
maxV = Math.max(maxV, uvs[i+1]);
}
const uvWidth = maxU - minU;
const uvHeight = maxV - minV;
// 仅对非小UV区域设置材质
if (!(uvWidth < 1.2 || uvHeight < 1.2)) {
const newMaterial = new THREE.MeshStandardMaterial({
map: texture,
roughness: 0.7,
metalness: 0.2,
side: THREE.DoubleSide
});
child.material = newMaterial;
}else{
// child.material = new THREE.MeshStandardMaterial({
// transparent: true,
// opacity: 0, // 完全透明
// side: THREE.DoubleSide
// });
}
child.geometry.attributes.uv.needsUpdate = true;
2025-04-01 15:25:15 +08:00
}
}
data.load.state = false
},(xhr:any) => { // 加载进度回调
const percent = xhr.total == 0?100:(xhr.loaded / xhr.total * 100).toFixed(2);
data.load.progress = percent
// updateProgressBar(Number(percent));
},(error:any) => {
console.error('纹理加载失败:', error);
data.load.state = false
});
2025-04-23 16:20:53 +08:00
let value = {
collectionElementId:item.id,
x:data.repeat.x,
y:data.repeat.y,
}
store.commit('setPatternMaking3D',value)
2025-04-23 09:39:24 +08:00
2025-04-01 15:25:15 +08:00
})
}
const addModel = async (url: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());
data.controls.target.copy(center);
// data.controls.autoRotate = true
data.camera.position.y = center.y;
data.camera.position.z = 1000;
data.pointLight.position.y = 250;
data.pointLight.position.z = 1250;
data.group.add(scene);
resolve('')
},(xhr:any) => { // 加载进度回调
const percent = xhr.total == 0?100:(xhr.loaded / xhr.total * 100).toFixed(2);
data.load.progress = percent
// updateProgressBar(Number(percent));
},(error:any) => { // 加载失败回调
console.error('模型加载失败:', error);
reject('')
})
})
}
const clearModel = ()=>{
const oldGroup:any = data.group;
data.group = new THREE.Group();
data.scene.add(data.group);
data.scene.remove(oldGroup);
}
// const loadThree = ()=>{
// init()
// }
const getModelUrl = (value:any)=>{
return new Promise((resolve, reject) => {
// Https.axiosGet(Https.httpUrls.getThreeDGlb,{params:{threeDSimpleId:value.id},env:{binary:true}}).then((rv)=>{
// //二进制流转本地路径
// console.log(rv)
// resolve(rv)
// }).catch(()=>{
// reject('')
// })
// //fetch get请求携带token
// fetch("https://develop.api.aida.com.hk/api/project/getThreeDGlb?threeDSimpleId=1", {
// headers:{
// authorization:'Bearer-eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiI4OCIsInN1YiI6IntcImNvdW50cnlcIjpcIkNoaW5hXCIsXCJpZFwiOjg4LFwibGFuZ3VhZ2VcIjpcIkVOR0xJU0hcIixcInVzZXJuYW1lXCI6XCJzaGJcIn0iLCJpYXQiOjE3NDMzNDkwNjQsImlzcyI6IkRXSiIsImF1dGhvcml0aWVzIjoiW10iLCJleHAiOjE3NTE5ODkwNjR9.gmL0JufYy9wd23qCY-ibwhgpXZ2X68WAiHSeC99I4x7cipWyxLaQmuIBk2SJSdWBm0tTN2Mx-etXO9a7MtQmpw',
// }
// }).then(res => {
// return res.blob();
// }).then((res) => {
// var url = URL.createObjectURL(res);
// console.log(url, res)
// resolve(url)
// }).catch(err => {
// console.log(err);
// })
resolve(value.threeDSimpleUrl)
})
}
const openSetData = async (value:any)=>{
if(!data.scene){
init()
}
data.load.state = true
const modeUrl = await getModelUrl(value)
await setModel(modeUrl)
2025-04-23 16:20:53 +08:00
let patternMaking3D = store.state.HomeStoreModule.patternMaking3D
if(patternMaking3D.printMinioUrl)await addMaterial({url:patternMaking3D.printMinioUrl})
2025-04-01 15:25:15 +08:00
data.load.state = false
}
2025-04-23 09:39:24 +08:00
const changeRepeat = (e:any)=>{
2025-04-30 14:01:52 +08:00
if(data.isLock)data.repeat.x = e
if(data.isLock)data.repeat.y = e
2025-04-23 09:39:24 +08:00
clearTimeout(dataTime.updataRepeat)
dataTime.updataRepeat = setTimeout(()=>{
2025-04-30 14:01:52 +08:00
data.repeat.x = data.repeat.x == 6 ? 5.999 : data.repeat.x
data.repeat.y = data.repeat.y == 6 ? 5.999 : data.repeat.y
data.textureLoader.repeat.set(6 - data.repeat.x,6 - data.repeat.y); // 纹理重复次数
2025-04-23 16:20:53 +08:00
let value = {
x:data.repeat.x,
y:data.repeat.y,
}
store.commit('setPatternMaking3D',value)
2025-04-23 09:39:24 +08:00
},1000)
}
2025-04-30 14:01:52 +08:00
const setLock = ()=>{
data.isLock = !data.isLock
}
2025-04-01 15:25:15 +08:00
onMounted(()=>{
})
2025-04-23 09:39:24 +08:00
onBeforeUnmount(()=>{
if(data.animationId){
cancelAnimationFrame(data.animationId);
data.animationId = null;
}
data.scene.traverse((child:any) => {
if (child.material) {
child.material.dispose();
}
if (child.geometry) {
child.geometry.dispose();
}
child = null;
});
data.renderer.forceContextLoss();
data.renderer.dispose();
data.scene.clear();
data.scene = null;
data.camera = null;
data.controls = null;
data.renderer.domElement = null;
data.renderer = null;
})
2025-04-01 15:25:15 +08:00
return{
...toRefs(dataDom),
...toRefs(data),
openSetData,
2025-04-16 10:43:54 +08:00
addMaterial,
2025-04-23 09:39:24 +08:00
changeRepeat,
2025-04-30 14:01:52 +08:00
setLock,
2025-04-01 15:25:15 +08:00
}
},
provide() {
return {
}
},
})
</script>
<style lang="less" scoped>
.three{
width: 100%;
height: 100%;
position: relative;
flex: 1;
overflow: hidden;
2025-04-23 09:39:24 +08:00
> .parameter{
display: flex;
2025-04-30 14:01:52 +08:00
align-items: center;
2025-04-23 09:39:24 +08:00
> label{
display: flex;
align-items: center;
flex: 1;
padding-right: 2rem;
// &:not(:last-child){
// margin-right: 3rem;
// }
> span{
font-size: 2rem;
margin-right: 1rem;
}
}
2025-04-30 14:01:52 +08:00
> i{
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.active{
opacity: .7;
}
}
2025-04-23 09:39:24 +08:00
}
2025-04-01 15:25:15 +08:00
> .model{
width: 100%;
height: 100%;
}
> .load{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, .2);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #fff;
> i{
font-size: 3rem;
}
> .loadBox{
width: 15rem;
height: 1rem;
border-radius: 1rem;
background: #fff;
overflow: hidden;
> .schedule{
height: 100%;
background: greenyellow;
}
}
}
}
</style>