397 lines
13 KiB
Vue
397 lines
13 KiB
Vue
<template>
|
||
<div class="three">
|
||
<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">
|
||
import { defineComponent,computed,shallowRef,provide,nextTick,onMounted,toRefs, reactive} from 'vue'
|
||
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'
|
||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||
|
||
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";
|
||
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,//监听鼠标、键盘事件
|
||
load:{
|
||
state:false,
|
||
progress:0 as any,
|
||
}
|
||
})
|
||
const dataDom = reactive({
|
||
threeDom:null as any,
|
||
})
|
||
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;
|
||
// data.scene.add(floorMesh);
|
||
const textureLoader = new THREE.TextureLoader();
|
||
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
|
||
data.scene.background = new THREE.Color("#fff");
|
||
// 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;
|
||
let animate = function () {
|
||
requestAnimationFrame(animate);
|
||
// 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();
|
||
}
|
||
const setModel = async (url:any)=>{
|
||
clearModel()
|
||
await addModel(url)
|
||
// addMaterial()
|
||
}
|
||
const addMaterial = (url:any)=>{
|
||
//添加图片材质
|
||
data.load.state = true
|
||
let textureLoader = new THREE.TextureLoader()
|
||
textureLoader.load(url, // 图片放在public/textures目录下
|
||
(texture:any) => {
|
||
// 3. 配置纹理参数
|
||
texture.wrapS = THREE.RepeatWrapping;
|
||
texture.wrapT = THREE.RepeatWrapping;
|
||
|
||
// texture.repeat.set(1, 1); // 纹理重复次数
|
||
texture.anisotropy = 32; // 提高纹理清晰度
|
||
data.group?.traverse((child:any) => {
|
||
if (child.isMesh) {
|
||
// 5. 创建新材质(根据需求选择材质类型)
|
||
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;
|
||
const repeatX = modelWidth / textureWidth;
|
||
const repeatY = modelHeight / textureHeight;
|
||
// texture.repeat.set(1, 1); // 纹理重复次数
|
||
texture.repeat.set(repeatX, repeatY); // 纹理重复次数
|
||
|
||
const newMaterial = new THREE.MeshStandardMaterial({
|
||
map: texture, // 基础颜色贴图
|
||
roughness: 0.7, // 表面粗糙度 (0-1)
|
||
metalness: .2, // 金属质感 (0-1)
|
||
side: THREE.DoubleSide // 双面渲染
|
||
});
|
||
// 6. 替换原有材质
|
||
child.material = newMaterial;
|
||
// 7. 如果需要单独控制某些子模型的UV
|
||
if (child.geometry.attributes.uv) {
|
||
// 可以在这里修改UV坐标
|
||
const uvs = child.geometry.attributes.uv.array;
|
||
// ...UV操作逻辑...
|
||
}
|
||
}
|
||
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
|
||
});
|
||
})
|
||
}
|
||
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)
|
||
data.load.state = false
|
||
}
|
||
onMounted(()=>{
|
||
})
|
||
return{
|
||
...toRefs(dataDom),
|
||
...toRefs(data),
|
||
openSetData,
|
||
addMaterial,
|
||
}
|
||
},
|
||
provide() {
|
||
return {
|
||
}
|
||
},
|
||
})
|
||
</script>
|
||
<style lang="less" scoped>
|
||
.three{
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
> .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> |