forked from natuka/web.puabadge.com
Initial commit
This commit is contained in:
commit
84061c9a18
243 files changed
+63148
No files matched your search
@@ -0,0 +1,456 @@
|
||||
<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>
|
||||
@@ -0,0 +1,591 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-scene
|
||||
xr-mode-ui="enabled: false"
|
||||
arjs="sourceType: webcam;"
|
||||
style="width: 100%; height: 100vh;"
|
||||
@click="onSceneClick"
|
||||
>
|
||||
<a-camera position="0 1.6 0">
|
||||
<a-cursor></a-cursor>
|
||||
</a-camera>
|
||||
<a-light type="directional" position="0 1 0.5" intensity="1"></a-light>
|
||||
<a-light type="ambient" color="#ffffff"></a-light>
|
||||
|
||||
<a-plane id="ground" position="0 0 0" rotation="-90 0 0" width="200" height="200" color="#7BC8A4" opacity="0.5"></a-plane>
|
||||
|
||||
<a-entity id="model" :gltf-model="modelFile" position="0 1 -.5" scale="0.005 0.005 0.005" ></a-entity>
|
||||
</a-scene>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as THREE from "three";
|
||||
import "aframe";
|
||||
// import '@/utils/threex/ar.min.js'
|
||||
// import "@ar-js-org/ar.js"
|
||||
// import * as THREEx from "@ar-js-org/ar.js/three.js/build/ar-threex"
|
||||
import "@ar-js-org/ar.js/three.js/build/ar"
|
||||
// import "@ar-js-org/ar.js/aframe/build/aframe-ar";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
||||
// import {ARToolkit, ARController} from "@ar-js-org/artoolkit5-js"
|
||||
let model, scene, camera, renderer, controls;
|
||||
const container = ref(null);
|
||||
var onRenderFcts = [];
|
||||
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,
|
||||
});
|
||||
const twoFirst = ref(true);
|
||||
const lastPoint = ref({
|
||||
touchStartX1: 0,
|
||||
touchStartY1: 0,
|
||||
touchStartX2: 0,
|
||||
touchStartY2: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["rotateControl", "handleProgress", "handleLoaded"]);
|
||||
|
||||
const props = defineProps({
|
||||
modelFile: { type: String, default: () => "" },
|
||||
pid: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
isDevices();
|
||||
document.addEventListener('click', onSceneClick, false);
|
||||
// animate();
|
||||
});
|
||||
|
||||
// 监听组件销毁事件
|
||||
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();
|
||||
test();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const modelContainer = new THREE.Object3D();
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
const test = () => {
|
||||
}
|
||||
|
||||
|
||||
const init = () => {
|
||||
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);
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
// const {width, height} = 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
|
||||
);
|
||||
|
||||
// 相机位置
|
||||
camera.position.z = 200;
|
||||
camera.position.y = 0;
|
||||
camera.position.x = 0;
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// 添加环境光
|
||||
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);
|
||||
|
||||
// 加载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);
|
||||
|
||||
// 坐标轴
|
||||
// const axesHelper = new THREE.AxesHelper(5)
|
||||
// scene.add( axesHelper );
|
||||
|
||||
// // 添加轨道控制器
|
||||
// controls = new OrbitControls(camera, renderer.domElement);
|
||||
// controls.autoRotate = true;
|
||||
// controls.autoRotateSpeed = 5;
|
||||
runAR();
|
||||
};
|
||||
|
||||
var arToolkitSource, arToolkitContext;
|
||||
const runAR = () => {
|
||||
|
||||
// 创建AR场景和渲染器
|
||||
arToolkitSource = new THREEx.ArToolkitSource({
|
||||
sourceType: "webcam",
|
||||
// sourceWidth: window.innerWidth,
|
||||
// sourceHeight: window.innerHeight,
|
||||
displayWidth: window.innerWidth,
|
||||
displayHeight: window.innerHeight,
|
||||
});
|
||||
|
||||
|
||||
arToolkitContext = new THREEx.ArToolkitContext({
|
||||
cameraParametersUrl: "src/assets/model/camera_para.dat",
|
||||
detectionMode: "mono",
|
||||
// canvasWidth: window.innerWidth,
|
||||
// canvasHeight: window.innerHeight,
|
||||
});
|
||||
|
||||
const markerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, { type: 'pattern', patternUrl: 'src/assets/model/patt.hiro' });
|
||||
const marker = document.createElement('a-marker');
|
||||
container.value.appendChild(marker);
|
||||
|
||||
arToolkitContext.init(function onCompleted() {
|
||||
arToolkitSource.onResizeElement();
|
||||
arToolkitSource.copyElementSizeTo(renderer.domElement);
|
||||
camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
|
||||
});
|
||||
|
||||
arToolkitSource.init(() => {
|
||||
setTimeout(() => {
|
||||
arToolkitContext.arController.setPatternDetectionMode(artoolkit.AR_TEMPLATE_MATCHING_MONO_AND_MATRIX);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// arToolkitContext.addEventListener('artoolkit-mat', (event) => {
|
||||
// var detectedPlanes = event.data;
|
||||
// if (detectedPlanes.length > 0) {
|
||||
// // 检测到平面,执行相应的操作
|
||||
// var plane = detectedPlanes[0];
|
||||
// console.log("Detected plane: ", plane);
|
||||
// }
|
||||
// });
|
||||
|
||||
// 创建AR追踪器
|
||||
// var markerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, {
|
||||
// type: "pattern",
|
||||
// patternUrl: "src/assets/model/pattern-marker.patt",
|
||||
// });
|
||||
|
||||
// function onMarkerFound(event) {
|
||||
// var markerRoot = event.data.markerRoot;
|
||||
|
||||
// // 获取平面的位置
|
||||
// var position = new THREE.Vector3();
|
||||
// markerRoot.matrixWorld.decompose(
|
||||
// position,
|
||||
// new THREE.Quaternion(),
|
||||
// new THREE.Vector3()
|
||||
// );
|
||||
|
||||
// // 添加标记物体
|
||||
// addMarker(position);
|
||||
// }
|
||||
|
||||
// markerControls.addEventListener("markerFound", onMarkerFound);
|
||||
|
||||
// function addMarker(position) {
|
||||
// var markerGeometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
|
||||
// var markerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
||||
// var marker = new THREE.Mesh(markerGeometry, markerMaterial);
|
||||
// marker.position.copy(position);
|
||||
// scene.add(marker);
|
||||
// }
|
||||
|
||||
// build a smoothedControls
|
||||
// var smoothedRoot = new THREE.Group()
|
||||
// scene.add(smoothedRoot)
|
||||
// var smoothedControls = new THREEx.ArSmoothedControls(smoothedRoot, {
|
||||
// lerpPosition: 0.4,
|
||||
// lerpQuaternion: 0.3,
|
||||
// lerpScale: 1,
|
||||
// // minVisibleDelay: 1,
|
||||
// // minUnvisibleDelay: 1,
|
||||
// })
|
||||
// onRenderFcts.push(function(delta){
|
||||
// smoothedControls.update(markerRoot)
|
||||
// })
|
||||
|
||||
// 加载模型
|
||||
// loadModel(props.modelFile)
|
||||
animate();
|
||||
};
|
||||
|
||||
const loadModel = async (modelPath) => {
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath("./draco/");
|
||||
// 加载 glb 格式的 3D 模型
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
loader.load(
|
||||
modelPath,
|
||||
(gltf) => {
|
||||
// 加载成功后的回调函数
|
||||
// emit("handleLoaded")
|
||||
model = gltf.scene;
|
||||
// model.position.set(0, 0, 0);
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
let sizeBox = new THREE.Box3();
|
||||
sizeBox.expandByObject(gltf.scene);
|
||||
const v3 = new THREE.Vector3();
|
||||
sizeBox.getSize(v3);
|
||||
|
||||
// console.log('size', 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);
|
||||
},
|
||||
(xhr) => {
|
||||
// 加载过程中的回调函数
|
||||
// emit("handleProgress",Math.floor((xhr.loaded / xhr.total) * 100))
|
||||
},
|
||||
(error) => {
|
||||
// 加载失败的回调函数
|
||||
console.error("Failed to load model", error);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听缩放\旋转操作
|
||||
renderer.domElement.addEventListener("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) => {
|
||||
// console.log("touchmove",event )
|
||||
handleTouchMove(event);
|
||||
});
|
||||
|
||||
// 开始触碰
|
||||
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;
|
||||
|
||||
// const theta = (x - startX) / radius * (-rotateSpeed)
|
||||
// const phi = (y - startY) / radius * (-rotateSpeed)
|
||||
// const maxRotationAngle = Math.PI / 2;
|
||||
// // 没有变化那么停止
|
||||
// if (Math.abs(theta) < .01 && Math.abs(phi) < .01) {
|
||||
// return
|
||||
// }
|
||||
// // 移动位置判断
|
||||
// if (Math.abs(x - startX) >= Math.abs(y - startY)) {
|
||||
// // 旋转角度
|
||||
// model.rotation.y -= theta
|
||||
// } else {
|
||||
// model.rotation.x -= phi
|
||||
// model.rotation.x = Math.max(-maxRotationAngle, Math.min(maxRotationAngle, model.rotation.x))
|
||||
// // this.gltfItemLights.rotation.x -= phi
|
||||
// // this.gltfItemLights.rotation.x = Math.max(-maxRotationAngle, Math.min(maxRotationAngle, model.rotation.x));
|
||||
// }
|
||||
mouseInfo.value.startX = x;
|
||||
mouseInfo.value.startY = y;
|
||||
|
||||
// console.log("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 < -2) {
|
||||
deltaScale = -2;
|
||||
} 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();
|
||||
});
|
||||
|
||||
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 onSceneClick = (event) => {
|
||||
|
||||
const sceneEl = document.querySelector('a-scene');
|
||||
let Camera = sceneEl.querySelector('[camera]');
|
||||
let Model = sceneEl.querySelector('#model');
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, Camera.getObject3D('camera'));
|
||||
|
||||
const intersects = raycaster.intersectObjects([sceneEl.querySelector('#ground').object3D], true);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
|
||||
const intersect = intersects[0];
|
||||
console.log("--------onSceneClick-------",intersect)
|
||||
Model.setAttribute('position', intersect.point);
|
||||
Model.setAttribute('visible', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var lastTimeMsec = null;
|
||||
// 渲染场景
|
||||
const animate = () => {
|
||||
// controls.update();
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
// videoTexture.needsUpdate = true;
|
||||
if (arToolkitSource.ready) {
|
||||
arToolkitContext.update(arToolkitSource.domElement);
|
||||
}
|
||||
// measure time
|
||||
// lastTimeMsec = lastTimeMsec || nowMsec-1000/60
|
||||
// var deltaMsec = Math.min(200, nowMsec - lastTimeMsec)
|
||||
// lastTimeMsec = nowMsec
|
||||
// // call each update function
|
||||
// onRenderFcts.forEach(function(onRenderFct){
|
||||
// onRenderFct(deltaMsec/1000, nowMsec/1000)
|
||||
// })
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,499 @@
|
||||
<template>
|
||||
<div ref="container" class="container">
|
||||
<video ref="video" class="fullscreen-video" autoplay playsinline muted></video>
|
||||
<canvas ref="canvas" class="fullscreen-canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||
import * as THREE from "three";
|
||||
// import "aframe";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
||||
let model, scene, camera, renderer, controls;
|
||||
|
||||
const video = ref(null);
|
||||
const canvas = ref(null);
|
||||
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,
|
||||
});
|
||||
const twoFirst = ref(true);
|
||||
const lastPoint = ref({
|
||||
touchStartX1: 0,
|
||||
touchStartY1: 0,
|
||||
touchStartX2: 0,
|
||||
touchStartY2: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["handleProgress", "handleLoaded", "closeHint"]);
|
||||
|
||||
const props = defineProps({
|
||||
modelFile: { type: String, default: () => "" },
|
||||
pid: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
onMounted(async() => {
|
||||
await tf.setBackend('webgl'); // 或者'cpu',根据你的需求选择
|
||||
await tf.ready();
|
||||
const model = await cocoSsd.load();
|
||||
const videoElement = video.value;
|
||||
const canvasElement = canvas.value;
|
||||
const context = canvasElement.getContext('2d');
|
||||
const detectFrame = async () => {
|
||||
const predictions = await model.detect(videoElement);
|
||||
context.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
||||
context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
|
||||
|
||||
predictions.forEach((prediction) => {
|
||||
|
||||
if(prediction.class == "keyboard" && !isModel.value) {
|
||||
loadModel(props.modelFile);
|
||||
emit("closeHint")
|
||||
}
|
||||
// context.beginPath();
|
||||
// context.rect(
|
||||
// prediction.bbox[0],
|
||||
// prediction.bbox[1],
|
||||
// prediction.bbox[2],
|
||||
// prediction.bbox[3]
|
||||
// );
|
||||
// context.lineWidth = 2;
|
||||
// context.strokeStyle = 'green';
|
||||
// context.fillStyle = 'green';
|
||||
// context.stroke();
|
||||
// context.fillText(
|
||||
// prediction.class + ': ' + Math.round(prediction.score * 100) + '%',
|
||||
// prediction.bbox[0],
|
||||
// prediction.bbox[1] > 10 ? prediction.bbox[1] - 5 : 10
|
||||
// );
|
||||
});
|
||||
|
||||
requestAnimationFrame(detectFrame);
|
||||
};
|
||||
|
||||
await setupCamera(videoElement);
|
||||
resizeCanvas(canvasElement);
|
||||
detectFrame();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
});
|
||||
|
||||
// 监听组件销毁事件
|
||||
onBeforeUnmount(() => {
|
||||
// Clean up TensorFlow.js resources
|
||||
if (model) {
|
||||
model.dispose();
|
||||
model = null;
|
||||
}
|
||||
tf.engine().disposeVariables();
|
||||
console.log('TensorFlow.js resources disposed.');
|
||||
});
|
||||
|
||||
const resizeCanvas = (canvasElement) => {
|
||||
canvasElement.width = window.innerWidth;
|
||||
canvasElement.height = window.innerHeight;
|
||||
};
|
||||
|
||||
const setupCamera = async (videoElement) => {
|
||||
isDevices();
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { exact: "environment" }, },
|
||||
|
||||
});
|
||||
videoElement.srcObject = stream;
|
||||
await new Promise((resolve) => {
|
||||
videoElement.onloadedmetadata = () => {
|
||||
resolve(videoElement);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelFile,
|
||||
(val, oldVal) => {
|
||||
if (val) {
|
||||
init();
|
||||
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const modelContainer = new THREE.Object3D();
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
const init = () => {
|
||||
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);
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
// const {width, height} = 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
|
||||
);
|
||||
|
||||
// 相机位置
|
||||
camera.position.z = 200;
|
||||
camera.position.y = 0;
|
||||
camera.position.x = 0;
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// 添加环境光
|
||||
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);
|
||||
|
||||
// 加载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);
|
||||
|
||||
// 坐标轴
|
||||
// const axesHelper = new THREE.AxesHelper(5)
|
||||
// scene.add( axesHelper );
|
||||
|
||||
// // 添加轨道控制器
|
||||
// controls = new OrbitControls(camera, renderer.domElement);
|
||||
// controls.autoRotate = true;
|
||||
// controls.autoRotateSpeed = 5;
|
||||
};
|
||||
|
||||
const isModel = ref(false); // 是否创建模型
|
||||
const loadModel = async (modelPath) => {
|
||||
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath("./draco/");
|
||||
// 加载 glb 格式的 3D 模型
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
loader.load(
|
||||
modelPath,
|
||||
(gltf) => {
|
||||
// 加载成功后的回调函数
|
||||
emit("handleLoaded")
|
||||
model = gltf.scene;
|
||||
// model.position.set(0, 0, 0);
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
|
||||
// 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);
|
||||
isModel.value = true;
|
||||
},
|
||||
(xhr) => {
|
||||
// 加载过程中的回调函数
|
||||
emit("handleProgress",Math.floor((xhr.loaded / xhr.total) * 100))
|
||||
},
|
||||
(error) => {
|
||||
// 加载失败的回调函数
|
||||
console.error("Failed to load model", error);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听缩放\旋转操作
|
||||
renderer.domElement.addEventListener("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) => {
|
||||
// console.log("touchmove",event )
|
||||
handleTouchMove(event);
|
||||
});
|
||||
|
||||
// 开始触碰
|
||||
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();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var lastTimeMsec = null;
|
||||
// 渲染场景
|
||||
const animate = () => {
|
||||
// controls.update();
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
// videoTexture.needsUpdate = true;
|
||||
// if (arToolkitSource.ready) {
|
||||
// arToolkitContext.update(arToolkitSource.domElement);
|
||||
// }
|
||||
// measure time
|
||||
// lastTimeMsec = lastTimeMsec || nowMsec-1000/60
|
||||
// var deltaMsec = Math.min(200, nowMsec - lastTimeMsec)
|
||||
// lastTimeMsec = nowMsec
|
||||
// // call each update function
|
||||
// onRenderFcts.forEach(function(onRenderFct){
|
||||
// onRenderFct(deltaMsec/1000, nowMsec/1000)
|
||||
// })
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fullscreen-video, .fullscreen-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.fullscreen-video {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fullscreen-canvas {
|
||||
pointer-events: none; /* 避免画布阻挡视频的鼠标事件 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<div ref="container" class="container">
|
||||
<video ref="video" class="fullscreen-video" autoplay playsinline muted></video>
|
||||
<canvas ref="canvas" class="fullscreen-canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as tf from '@tensorflow/tfjs';
|
||||
import * as cocoSsd from '@tensorflow-models/coco-ssd';
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
||||
let model, scene, camera, renderer, controls;
|
||||
|
||||
const video = ref(null);
|
||||
const canvas = ref(null);
|
||||
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,
|
||||
});
|
||||
const twoFirst = ref(true);
|
||||
const lastPoint = ref({
|
||||
touchStartX1: 0,
|
||||
touchStartY1: 0,
|
||||
touchStartX2: 0,
|
||||
touchStartY2: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["handleProgress", "handleLoaded", "closeHint"]);
|
||||
|
||||
const props = defineProps({
|
||||
modelFile: { type: String, default: () => "" },
|
||||
pid: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
onMounted(async() => {
|
||||
await tf.setBackend('webgl'); // 或者'cpu',根据你的需求选择
|
||||
await tf.ready();
|
||||
const model = await cocoSsd.load();
|
||||
const videoElement = video.value;
|
||||
const canvasElement = canvas.value;
|
||||
const context = canvasElement.getContext('2d');
|
||||
const detectFrame = async () => {
|
||||
const predictions = await model.detect(videoElement);
|
||||
context.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
||||
context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
|
||||
|
||||
predictions.forEach((prediction) => {
|
||||
|
||||
if(prediction.class == "keyboard" && !isModel.value) {
|
||||
loadModel(props.modelFile);
|
||||
emit("closeHint")
|
||||
}
|
||||
// context.beginPath();
|
||||
// context.rect(
|
||||
// prediction.bbox[0],
|
||||
// prediction.bbox[1],
|
||||
// prediction.bbox[2],
|
||||
// prediction.bbox[3]
|
||||
// );
|
||||
// context.lineWidth = 2;
|
||||
// context.strokeStyle = 'green';
|
||||
// context.fillStyle = 'green';
|
||||
// context.stroke();
|
||||
// context.fillText(
|
||||
// prediction.class + ': ' + Math.round(prediction.score * 100) + '%',
|
||||
// prediction.bbox[0],
|
||||
// prediction.bbox[1] > 10 ? prediction.bbox[1] - 5 : 10
|
||||
// );
|
||||
});
|
||||
|
||||
requestAnimationFrame(detectFrame);
|
||||
};
|
||||
|
||||
await setupCamera(videoElement);
|
||||
resizeCanvas(canvasElement);
|
||||
detectFrame();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
});
|
||||
|
||||
// 监听组件销毁事件
|
||||
onBeforeUnmount(() => {
|
||||
// Clean up TensorFlow.js resources
|
||||
if (model) {
|
||||
model.dispose();
|
||||
model = null;
|
||||
}
|
||||
tf.engine().disposeVariables();
|
||||
console.log('TensorFlow.js resources disposed.');
|
||||
});
|
||||
|
||||
const resizeCanvas = (canvasElement) => {
|
||||
canvasElement.width = window.innerWidth;
|
||||
canvasElement.height = window.innerHeight;
|
||||
};
|
||||
|
||||
const setupCamera = async (videoElement) => {
|
||||
isDevices();
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { exact: "environment" }, },
|
||||
|
||||
});
|
||||
videoElement.srcObject = stream;
|
||||
await new Promise((resolve) => {
|
||||
videoElement.onloadedmetadata = () => {
|
||||
resolve(videoElement);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelFile,
|
||||
(val, oldVal) => {
|
||||
if (val) {
|
||||
init();
|
||||
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const modelContainer = new THREE.Object3D();
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
const init = () => {
|
||||
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);
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
// const {width, height} = 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
|
||||
);
|
||||
|
||||
// 相机位置
|
||||
camera.position.z = 200;
|
||||
camera.position.y = 0;
|
||||
camera.position.x = 0;
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// 添加环境光
|
||||
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);
|
||||
|
||||
// 加载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);
|
||||
|
||||
// 坐标轴
|
||||
// const axesHelper = new THREE.AxesHelper(5)
|
||||
// scene.add( axesHelper );
|
||||
|
||||
// // 添加轨道控制器
|
||||
// controls = new OrbitControls(camera, renderer.domElement);
|
||||
// controls.autoRotate = true;
|
||||
// controls.autoRotateSpeed = 5;
|
||||
};
|
||||
|
||||
const isModel = ref(false); // 是否创建模型
|
||||
const loadModel = async (modelPath) => {
|
||||
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath("./draco/");
|
||||
// 加载 glb 格式的 3D 模型
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
loader.load(
|
||||
modelPath,
|
||||
(gltf) => {
|
||||
// 加载成功后的回调函数
|
||||
emit("handleLoaded")
|
||||
model = gltf.scene;
|
||||
// model.position.set(0, 0, 0);
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
|
||||
// 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);
|
||||
isModel.value = true;
|
||||
},
|
||||
(xhr) => {
|
||||
// 加载过程中的回调函数
|
||||
emit("handleProgress",Math.floor((xhr.loaded / xhr.total) * 100))
|
||||
},
|
||||
(error) => {
|
||||
// 加载失败的回调函数
|
||||
console.error("Failed to load model", error);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听缩放\旋转操作
|
||||
renderer.domElement.addEventListener("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) => {
|
||||
// console.log("touchmove",event )
|
||||
handleTouchMove(event);
|
||||
});
|
||||
|
||||
// 开始触碰
|
||||
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();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var lastTimeMsec = null;
|
||||
// 渲染场景
|
||||
const animate = () => {
|
||||
// controls.update();
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
// videoTexture.needsUpdate = true;
|
||||
// if (arToolkitSource.ready) {
|
||||
// arToolkitContext.update(arToolkitSource.domElement);
|
||||
// }
|
||||
// measure time
|
||||
// lastTimeMsec = lastTimeMsec || nowMsec-1000/60
|
||||
// var deltaMsec = Math.min(200, nowMsec - lastTimeMsec)
|
||||
// lastTimeMsec = nowMsec
|
||||
// // call each update function
|
||||
// onRenderFcts.forEach(function(onRenderFct){
|
||||
// onRenderFct(deltaMsec/1000, nowMsec/1000)
|
||||
// })
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fullscreen-video, .fullscreen-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.fullscreen-video {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fullscreen-canvas {
|
||||
pointer-events: none; /* 避免画布阻挡视频的鼠标事件 */
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user