forked from natuka/web.puabadge.com
456 lines
12 KiB
Vue
456 lines
12 KiB
Vue
<template>
|
|
<div id="webGL3d" ref="container"></div>
|
|
<video ref="videoRef" id="video" autoplay playsinline></video>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import * as THREE from "three";
|
|
import "aframe";
|
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
|
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
|
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
|
|
import { localStorage } from "@/utils/local-storage";
|
|
// 导入lil.gui
|
|
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
|
|
const props = defineProps({
|
|
modelFile: { type: String, default: () => "" },
|
|
bgModel: { type: String, default: "" },
|
|
pid: { type: Number, default: 0 },
|
|
});
|
|
import { showSuccessToast, showToast } from "vant";
|
|
const container = ref(null);
|
|
const STATE = {
|
|
NONE: -1,
|
|
MOVE: 0,
|
|
ZOOM_OR_PAN: 1,
|
|
POSITION_MOVE: 1,
|
|
};
|
|
const mouseInfo = ref({
|
|
startX: 0,
|
|
startY: 0,
|
|
isDown: false,
|
|
startPointerDistance: 0,
|
|
state: STATE.NONE,
|
|
});
|
|
|
|
let model, scene, camera, renderer, controls;
|
|
const videoRef = ref();
|
|
const twoFirst = ref(true);
|
|
const lastPoint = ref({
|
|
touchStartX1: 0,
|
|
touchStartY1: 0,
|
|
touchStartX2: 0,
|
|
touchStartY2: 0,
|
|
});
|
|
const emit = defineEmits([
|
|
"handleProgress",
|
|
"handleLoaded",
|
|
]);
|
|
|
|
onMounted(() => {
|
|
isDevices();
|
|
});
|
|
|
|
// 监听组件销毁事件
|
|
onBeforeUnmount(() => {
|
|
// 销毁 Three.js 模型
|
|
scene.remove(model);
|
|
model.traverse((child) => {
|
|
if (child.isMesh) {
|
|
child.geometry.dispose();
|
|
child.material.dispose();
|
|
}
|
|
});
|
|
|
|
// 清理渲染器
|
|
renderer.dispose();
|
|
});
|
|
|
|
watch(
|
|
() => props.modelFile,
|
|
(val, oldVal) => {
|
|
if (val) {
|
|
// 加载模型
|
|
Init();
|
|
}
|
|
},
|
|
{
|
|
immediate: true,
|
|
}
|
|
);
|
|
const modelContainer = new THREE.Object3D();
|
|
const loader = new GLTFLoader();
|
|
|
|
const Init = async () => {
|
|
const dracoLoader = new DRACOLoader();
|
|
dracoLoader.setDecoderPath("./draco/");
|
|
// 创建场景
|
|
scene = new THREE.Scene();
|
|
// 创建一个包含模型的父对象
|
|
|
|
// 旋转缩放相关配置
|
|
// 创建相机
|
|
camera = new THREE.OrthographicCamera(
|
|
window.innerWidth / -10.5,
|
|
window.innerWidth / 10.5,
|
|
window.innerHeight / 10.5,
|
|
window.innerHeight / -10.5,
|
|
0.1,
|
|
1000
|
|
// 90,
|
|
// window.innerWidth / window.innerHeight,
|
|
// .1,
|
|
// 1000
|
|
);
|
|
|
|
// 添加环境光
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 2);
|
|
scene.add(ambientLight);
|
|
|
|
// 从上方照射的白色平行光,强度为 0.5。
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
|
|
// directionalLight.castShadow = true;
|
|
directionalLight.position.x = 1;
|
|
directionalLight.position.y = 1;
|
|
directionalLight.position.z = 80;
|
|
directionalLight.target = modelContainer;
|
|
scene.add(directionalLight);
|
|
|
|
let renderParam = {
|
|
antialias: true, // true/false表示是否开启反锯齿
|
|
alpha: true, // true/false 表示是否可以设置背景色透明
|
|
precision: "highp", // highp/mediump/lowp 表示着色精度选择
|
|
premultipliedAlpha: false,
|
|
maxLights: 3,
|
|
};
|
|
|
|
// 创建渲染器
|
|
renderer = new THREE.WebGLRenderer(renderParam);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
// renderer.setClearColor(0x000000, 0);
|
|
renderer.setPixelRatio(window.devicePixelRatio); // 设置画布分辨率 提高画质
|
|
container.value.appendChild(renderer.domElement);
|
|
|
|
await startCamera();
|
|
var videoElement = document.getElementById("video");
|
|
var videoTexture = new THREE.VideoTexture(videoElement);
|
|
videoTexture.minFilter = THREE.LinearFilter;
|
|
scene.background = videoTexture;
|
|
|
|
// 加载hdr贴图
|
|
// let rgbeLoader = new RGBELoader();
|
|
// const rgbUrl = props.bgModel || "src/assets/bg.hdr";
|
|
// rgbeLoader.load(rgbUrl, envMap => {
|
|
// // 设置球形贴图
|
|
// envMap.mapping = THREE.EquirectangularReflectionMapping;
|
|
// // 设置环境贴图
|
|
// scene.background = envMap;
|
|
|
|
// scene.environment = envMap;
|
|
|
|
// })
|
|
|
|
// 创建点光源
|
|
var pointLight = new THREE.PointLight(0xffffff, 500); // 设置点光源的颜色和强度
|
|
pointLight.position.set(0, 0, 100); // 设置点光源的位置
|
|
scene.add(pointLight);
|
|
|
|
// 相机位置
|
|
camera.position.z = 200;
|
|
camera.position.y = 0;
|
|
camera.position.x = 0;
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
// 监听缩放\旋转操作
|
|
renderer.domElement.addEventListener("touchstart", (event) => {
|
|
console.log("touchstart", event);
|
|
handleTouchStart(event);
|
|
});
|
|
|
|
const handleTouchStart = (event) => {
|
|
mouseInfo.value.isDown = true;
|
|
|
|
// 双指
|
|
const touch0 = event.touches[0];
|
|
const touch1 = event.touches[1];
|
|
// 单点触控
|
|
if (event.touches.length === 1) {
|
|
mouseInfo.value.startX = touch0.pageX;
|
|
mouseInfo.value.startY = touch0.pageY;
|
|
mouseInfo.value.state = STATE.MOVE; // 这边换成rorate会比较好点
|
|
} else if (event.touches.length === 2) {
|
|
// 双指,计算两指距离
|
|
// 一般是平移或者缩放
|
|
const dx = touch0.pageX - touch1.pageX;
|
|
const dy = touch0.pageY - touch1.pageY;
|
|
|
|
mouseInfo.value.startPointerDistance = Math.sqrt(dx * dx + dy * dy);
|
|
mouseInfo.value.startX = (touch0.pageX + touch1.pageX) / 2;
|
|
mouseInfo.value.startY = (touch0.pageY + touch1.pageY) / 2;
|
|
mouseInfo.value.state = STATE.ZOOM_OR_PAN;
|
|
}
|
|
};
|
|
|
|
renderer.domElement.addEventListener("touchmove", (event) => {
|
|
if(isModel) {
|
|
handleTouchMove(event);
|
|
}
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
}, { passive: false });
|
|
|
|
renderer.domElement.addEventListener('click', (event) => {
|
|
onMouseClick(event)
|
|
});
|
|
|
|
function onMouseClick(event) {
|
|
var mouse = new THREE.Vector2();
|
|
// 计算鼠标点击位置的归一化设备坐标
|
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
if(isModel.value) {
|
|
modelContainer.position.x = mouse.x * 40;
|
|
modelContainer.position.y = mouse.y * 100;
|
|
|
|
}else {
|
|
loadModel(mouse);
|
|
}
|
|
|
|
}
|
|
const isModel = ref(false); // 是否创建模型
|
|
function loadModel(position) {
|
|
// 加载 glb 格式的 3D 模型
|
|
loader.setDRACOLoader(dracoLoader);
|
|
loader.load(
|
|
props.modelFile,
|
|
(gltf) => {
|
|
// 加载成功后的回调函数
|
|
emit("handleLoaded");
|
|
model = gltf.scene;
|
|
// model.position.set(0, 0, 0);
|
|
const box = new THREE.Box3().setFromObject(model);
|
|
// sizeBox.expandByObject(gltf.scene)
|
|
// sizeBox.getSize(v3)
|
|
|
|
|
|
// console.log("gltf", gltf)
|
|
// console.log("volume", volume)
|
|
|
|
// 获取包围盒中心点
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
model.position.sub(center); // 将模型位置移到原点处
|
|
|
|
// 将模型添加到父对象中
|
|
modelContainer.add(model);
|
|
// modelGeometry.center(); // 这个等同于上面这行代码
|
|
// model.translateX(-80)
|
|
scene.add(modelContainer);
|
|
|
|
modelContainer.position.x = position.x * 40;
|
|
modelContainer.position.y = position.y * 100;
|
|
console.log("modelContainer", modelContainer.position)
|
|
isModel.value = true;
|
|
|
|
},
|
|
(xhr) => {
|
|
// 加载过程中的回调函数
|
|
emit("handleProgress", Math.floor((xhr.loaded / xhr.total) * 100));
|
|
},
|
|
(error) => {
|
|
// 加载失败的回调函数
|
|
console.error("Failed to load model", error);
|
|
}
|
|
);
|
|
}
|
|
|
|
// 开始触碰
|
|
const handleTouchMove = (event) => {
|
|
if (!mouseInfo.value.isDown) {
|
|
return;
|
|
}
|
|
switch (mouseInfo.value.state) {
|
|
case STATE.MOVE:
|
|
if (event.touches.length === 1) {
|
|
handleRotate(event);
|
|
} else if (event.touches.length === 2) {
|
|
// 兼容处理,如果是移动过程中出现双指时,需要取消单指,然后再重新计算
|
|
renderer.domElement.removeEventListener(
|
|
"touchmove",
|
|
handleTouchMove,
|
|
false
|
|
);
|
|
renderer.domElement.removeEventListener(
|
|
"touchend",
|
|
handleTouchEnd,
|
|
false
|
|
);
|
|
handleTouchStart(event);
|
|
}
|
|
break;
|
|
case STATE.ZOOM_OR_PAN:
|
|
if (event.touches.length === 1) {
|
|
} else if (event.touches.length === 2) {
|
|
handleZoomOrPan(event);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
let rotateSpeed = 0.01;
|
|
// 模型旋转
|
|
const handleRotate = (event) => {
|
|
const x = event.touches[0].pageX;
|
|
const y = event.touches[0].pageY;
|
|
const { startX, startY } = mouseInfo.value;
|
|
const deltaX = x - startX;
|
|
const deltaY = y - startY;
|
|
|
|
modelContainer.rotation.y += deltaX * rotateSpeed;
|
|
modelContainer.rotation.x += deltaY * rotateSpeed;
|
|
|
|
mouseInfo.value.startX = x;
|
|
mouseInfo.value.startY = y;
|
|
|
|
};
|
|
// 模型缩放
|
|
const handleZoomOrPan = (event) => {
|
|
let { touchStartX1, touchStartY1, touchStartX2, touchStartY2 } =
|
|
lastPoint.value;
|
|
let initialScale = modelContainer.scale.x;
|
|
const touch0 = event.touches[0];
|
|
const touch1 = event.touches[1];
|
|
const dx = touch0.pageX - touch1.pageX;
|
|
const dy = touch0.pageY - touch1.pageY;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
let obj = {
|
|
touchStartX1: touch0.pageX,
|
|
touchStartY1: touch0.pageY,
|
|
touchStartX2: touch1.pageX,
|
|
touchStartY2: touch1.pageY,
|
|
};
|
|
if (twoFirst.value) {
|
|
twoFirst.value = false;
|
|
lastPoint.value = { ...obj };
|
|
} else {
|
|
let deltaScale =
|
|
initialScale * (distance / mouseInfo.value.startPointerDistance);
|
|
// 限制缩放距离
|
|
if (deltaScale < 0.25) {
|
|
deltaScale = 0.25;
|
|
} else if (deltaScale > 2) {
|
|
deltaScale = 2;
|
|
}
|
|
mouseInfo.value.startPointerDistance = distance;
|
|
modelContainer.scale.set(deltaScale, deltaScale, deltaScale);
|
|
|
|
const avgX = (touch0.pageX + touch1.pageX) / 2;
|
|
const avgY = (touch0.pageY + touch1.pageY) / 2;
|
|
const deltaX = avgX - (touchStartX1 + touchStartX2) / 2;
|
|
const deltaY = avgY - (touchStartY1 + touchStartY2) / 2;
|
|
|
|
modelContainer.position.x += deltaX * 0.3;
|
|
modelContainer.position.y -= deltaY * 0.3;
|
|
|
|
lastPoint.value = {
|
|
touchStartX1: touch0.pageX,
|
|
touchStartY1: touch0.pageY,
|
|
touchStartX2: touch1.pageX,
|
|
touchStartY2: touch1.pageY,
|
|
};
|
|
}
|
|
};
|
|
|
|
const handleTouchEnd = () => {
|
|
mouseInfo.value.isDown = false;
|
|
mouseInfo.value.state = STATE.NONE;
|
|
twoFirst.value = true;
|
|
lastPoint.value.touchStartX1 = 0;
|
|
lastPoint.value.touchStartY1 = 0;
|
|
lastPoint.value.touchStartX2 = 0;
|
|
lastPoint.value.touchStartY2 = 0;
|
|
// 取消移动事件监听
|
|
renderer.domElement.removeEventListener(
|
|
"touchmove",
|
|
handleTouchMove,
|
|
false
|
|
);
|
|
renderer.domElement.removeEventListener(
|
|
"touchstart",
|
|
handleTouchStart,
|
|
false
|
|
);
|
|
};
|
|
|
|
renderer.domElement.addEventListener("touchend", (event) => {
|
|
handleTouchEnd();
|
|
});
|
|
|
|
// 渲染场景
|
|
const animate = () => {
|
|
// controls.update();
|
|
|
|
requestAnimationFrame(animate);
|
|
renderer.render(scene, camera);
|
|
videoTexture.needsUpdate = true;
|
|
};
|
|
|
|
animate();
|
|
|
|
// 监听窗口缩放
|
|
window.addEventListener("resize", () => {
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
|
camera.updateProjectionMatrix();
|
|
});
|
|
};
|
|
|
|
const isDevices = () => {
|
|
if (navigator.mediaDevices === undefined) {
|
|
navigator.mediaDevices = {};
|
|
}
|
|
if (navigator.mediaDevices.getUserMedia === undefined) {
|
|
navigator.mediaDevices.getUserMedia = function (constraints) {
|
|
var getUserMedia =
|
|
navigator.getUserMedia ||
|
|
navigator.webkitGetUserMedia ||
|
|
navigator.mozGetUserMedia ||
|
|
navigator.msGetUserMedia ||
|
|
navigator.oGetUserMedia;
|
|
if (!getUserMedia) {
|
|
return Promise.reject(
|
|
new Error("getUserMedia is not implemented in this browser")
|
|
);
|
|
}
|
|
return new Promise(function (resolve, reject) {
|
|
getUserMedia.call(navigator, constraints, resolve, reject);
|
|
});
|
|
};
|
|
}
|
|
};
|
|
const cameraStream = ref(null)
|
|
const startCamera = async () => {
|
|
isDevices();
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
facingMode: { exact: "environment" },
|
|
},
|
|
});
|
|
cameraStream.value = stream;
|
|
videoRef.value.srcObject = stream;
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
</script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
}
|
|
</style> |