Initial commit

This commit is contained in:
natuka committed 2025-06-05 09:25:26 +08:00
commit 84061c9a18
243 files changed
+63148

No files matched your search

+456
View File
@@ -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>
+591
View File
@@ -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>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
+499
View File
@@ -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>
+498
View File
@@ -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>
+112
View File
@@ -0,0 +1,112 @@
<template>
<div class="container" ref="container">
<model-viewer
id="model-viewer"
:src="testModel"
tone-mapping="commerce"
antialias="true"
shadow-intensity="0"
ar
xr-environment
camera-controls
auto-rotate
style="width: 100%; height: 70vh;"
scale="0.006 0.006 0.006"
>
</model-viewer>
<div id="error" class="hide">AR is not supported on this device</div>
</div>
</template>
<script setup lang="ts">
import '@google/model-viewer';
import { showFailToast } from 'vant';
import UAParser from 'ua-parser-js';
const container = ref(null)
const testModel = ref("https://suwa3d-test.oss-cn-shanghai.aliyuncs.com/glbs/3d/151322.glb")
const props = defineProps({
modelFile: { type: String, default: () => "", },
pid: { type: Number, default: 0 },
});
// 监听组件销毁事件
onBeforeUnmount(() => {
container.value.remove()
});
watch(() => props.modelFile, (val, oldVal) => {
if (val) {
isAr();
}
}, { immediate: true })
const init = () => {
const modelViewer = document.querySelector('model-viewer');
console.log("modelViewer",modelViewer)
modelViewer.addEventListener('load', (event) => {
modelViewer.activateAR();
});
modelViewer.addEventListener('ar-status', (event) => {
if(event.detail.status === 'failed'){
const error = document.querySelector("#error");
error.classList.remove('hide');
error.addEventListener('transitionend',(event) => {
error.classList.add('hide');
});
}
});
}
const isAr = () => {
const modelViewer = document.querySelector('model-viewer');
const uaParser = new UAParser();
const userAgent = uaParser.getResult();
console.log('Browser:', userAgent.browser.name);
// console.log('OS:', userAgent.os.name, userAgent.os.version);
// console.log('Device:', userAgent.device.model);
// if(userAgent.browser.name == "Chrome" || userAgent.browser.name == "Edge" || userAgent.browser.name == "Mobile Safari") {
// console.log('AR is supported!');
init();
// }else {
// showFailToast('抱歉,当前浏览器不支持AR,请切换谷歌或者edge浏览器。');
// }
}
</script>
<style lang="less" scoped>
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
margin: 0px;
}
model-viewer::part(default-ar-button){
top: 15px;
left: 25px;
right: 0;
bottom: 0;
}
#error {
background-color: #ffffffdd;
border-radius: 16px;
padding: 16px;
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
transition: opacity 0.3s;
}
#error.hide {
opacity: 0;
visibility: hidden;
transition: visibility 2s, opacity 1s 1s;
}
</style>
+38
View File
@@ -0,0 +1,38 @@
<script lang="tsx" setup name="sw-article">
const list = ref([
{
title: '元宇宙与少儿科普的关系',
src: 'https://wechat.suwa3d.com/data/meta/a03.jpg',
link: 'https://mp.weixin.qq.com/s?__biz=MzkwMDYzMTY3NA==&mid=2247483875&idx=1&sn=8d99989e8c016bc51ae27a80244bbb86&chksm=c0405cf6f737d5e04d2c711922001cdb19307743893f1193bb76043cbf8b7fef360652b73a81#rd',
},
{
title: '小课堂————什么是元宇宙?',
src: 'https://wechat.suwa3d.com/data/meta/a02.jpg',
link: 'https://mp.weixin.qq.com/s?__biz=MzkwMDYzMTY3NA==&mid=2247483876&idx=1&sn=7c549372edf3ec3dfa7e9bb5935ec2fe&chksm=c0405cf1f737d5e787d2cbf6d05f77a5de185a2cea058ef07f3b421875a289e3e4996e6fd745#rd',
},
{
title: '小课堂————什么是虚拟数字人?',
src: 'https://wechat.suwa3d.com/data/meta/a01.jpg',
link: 'https://mp.weixin.qq.com/s?__biz=MzkwMDYzMTY3NA==&mid=2247483889&idx=1&sn=179e9db11d2f386a48635bc2e4fae5ff&chksm=c0405ce4f737d5f2b94c9aa55cbcf45d4005081d7537578878c6c6d8474ead446c9ac65a67fe#rd',
},
])
function onClick(item: any) {
window.location.href = item.link
}
</script>
<template>
<van-grid :border="false" :column-num="1">
<van-grid-item v-for="(item, i) in list" :key="i" @click="onClick(item)">
<van-image
:src="item.src"
/>
<p>{{ item.title }}</p>
</van-grid-item>
</van-grid>
</template>
<style>
</style>
+81
View File
@@ -0,0 +1,81 @@
<script lang="tsx" setup name="sw-brand">
import { localStorage } from '@/utils/local-storage'
let brandList = ref()
let brandList_cn = ref([
{
pic: 'https://wechat.suwa3d.com/data/banner/banner1.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner2.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner3.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner4.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner5.png',
},
])
let brandList_tw = ref([
{
pic: 'https://wechat.suwa3d.com/data/banner/banner1_tw.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner2_tw.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner3_tw.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner4_tw.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner5_tw.png',
},
])
let brandList_en = ref([
{
pic: 'https://wechat.suwa3d.com/data/banner/banner1_en.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner2_en.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner3_en.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner4_en.png',
},
{
pic: 'https://wechat.suwa3d.com/data/banner/banner5_en.png',
},
])
const lang = ref("");
onMounted(()=> {
lang.value = localStorage.get("i18nextLng");console.log("lang.value", lang.value)
})
watch(lang, (val)=> {
switch(val) {
case "zh-CN": brandList.value = brandList_cn.value;break;
case "zh-TW": brandList.value = brandList_tw.value;break;
case "en": brandList.value = brandList_en.value;break;
}
}, { immediate: true })
</script>
<template>
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="(item, i) in brandList" :key="i">
<van-image :src="item.pic" />
</van-swipe-item>
</van-swipe>
</template>
<style>
.my-swipe {
width: 345px;
}
</style>
+249
View File
@@ -0,0 +1,249 @@
<template>
<div>
<!-- 隐私协议弹窗 -->
<van-popup
v-model:show="popupShow"
:close-on-click-overlay="false"
round
>
<!-- <div class="hint_container">
<div class="hint_title">
{{'用户协议及隐私政策'.$t}}
</div>
<div class="hint_content">
<div class="hint_text">{{'无论数据在何处进行处理和存储,我们将始终按照隐私政策及用户协议收集、处理您的数据。'.$t}}</div>
<div class="hint_text">{{"遵照中国大陆隐私保护法律的要求,需要你签署本知情同意书。".$t}}</div>
<div class="hint_contract">
<div class="contract_item">
<van-checkbox class="check_box" icon-size="20px" v-model="checked" checked-color="#009944" @change="onChange"></van-checkbox>
<div class="a-text">{{'我同意'.$t}}
<div class="active" @click="secretJump('user_service', 1)">{{'用户服务协议'.$t}}</div>
</div>
</div>
<div class="contract_item">
<van-checkbox class="check_box" icon-size="20px" v-model="checked2" checked-color="#009944" @change="onChange2"></van-checkbox>
<div class="a-text">{{'我同意'.$t}}
<div class="active" @click="secretJump('user_privacy', 2)">{{'用户隐私政策'.$t}}</div>
</div>
</div>
<div class="contract_item">
<van-checkbox class="check_box" icon-size="20px" v-model="checked3" checked-color="#009944" @change="onChange3"></van-checkbox>
<div class="a-text">{{'我同意'.$t}}
<div class="active" @click="secretJump('child_privacy', 3)">{{'儿童隐私政策'.$t}}</div>
</div>
</div>
</div>
<div class="hint_btn" @click="handleConfirm">
<div :class="['btn', {checked: isChecked}]">{{'同意并继续'.$t}}</div>
</div>
</div>
</div> -->
<div class="xieyi-body">
<div class="xieyi-box">
<div class="xieyi-title">服务协议及隐私保护</div>
<div>
<div class="xieyi-text">为更好的保护你的合法权益,请阅读并同意
<span class="xieyi-active" @click="secretJump('user_service', 1)">《用户服务协议》</span>
<span class="xieyi-active" @click="secretJump('user_service', 2)">《用户隐私政策》</span>
<span class="xieyi-active" @click="secretJump('user_service', 3)">《儿童隐私政策》</span>
</div>
</div>
<div class="xieyi-btn">
<div class="hint_no_btn" @click="closeHint">
不同意
</div>
<div class="hint_btn" @click="handleConfirm">
同意
</div>
</div>
</div>
</div>
</van-popup>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
hintShow: {
type: Boolean,
default: true
}
})
const popupShow = ref(false)
const checked = ref(false)
const checked2 = ref(false)
const checked3 = ref(false)
const isChecked = ref(false)
const emit = defineEmits([
"check",
"close",
"onViewAgree",
]);
watch(() => props.hintShow, (val, oldVal) => {
console.log("props.hintShow",val)
popupShow.value = val ? true : false
}, { immediate: true })
// const onChange = (e) => {
// checked.value = e;
// isChecked.value = e && checked2.value && checked3.value;
// }
// const onChange2 = (e) => {
// checked2.value = e;
// isChecked.value = e && checked.value && checked3.value;
// }
// const onChange3 = (e) => {
// checked3.value = e;
// isChecked.value = e && checked.value && checked2.value;
// }
const handleConfirm = () => {
// if(checked.value && checked2.value && checked3.value) {
emit("check", true)
emit("close")
// 清空数据
// checked.value = false
// checked2.value = false
// checked3.value = false
// isChecked.value = false
// }
}
const closeHint = () => {
emit("check", false)
emit("close")
}
// 隐私跳转
const secretJump = (title, val) => {
emit("onViewAgree", title, val)
}
</script>
<style lang="less" scoped>
.hint_container {
position: relative;
padding: 20px 0;
.hint_title {
display: flex;
justify-content: center;
align-items: center;
position: relative;
font-size: 17px;
}
.hint_content {
position: relative;
width: 85%;
margin: 10px auto 0 auto;
.hint_text {
margin-bottom: 7px;
font-size: 15px;
line-height: 25px;
color: #444444;
}
.hint_contract {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
position: relative;
margin-top: 10px;
.contract_item {
display: flex;
margin: 5px 0;
.check_box {
display: flex;
}
.a-text {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #767676;
font-size: 14px;
line-height: 14px;
.active {
color: #009944;
margin: 0 5px;
}
}
}
}
// .hint_btn {
// display: flex;
// justify-content: center;
// align-items: center;
// position: relative;
// width: 100%;
// margin-top: 25px;
// .btn {
// display: flex;
// justify-content: center;
// align-items: center;
// position: relative;
// width: 150px;
// height: 40px;
// border-radius: 5px;
// background: #009944;
// color: #fff;
// font-size: 15px;
// filter: opacity(0.5)
// }
// .checked {
// filter: brightness(1);
// }
// }
}
}
.xieyi-body {
padding: 15px;
}
.xieyi-box {
padding: 10px;
background-color: #fff;
border-radius: 8px;
.xieyi-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
.xieyi-text {
font-size: 14px;
}
}
}
.xieyi-active {
color: #1aad19!important;
cursor: pointer;
}
.xieyi-btn {
display: flex;
justify-content: space-around;
align-items: center;
margin-top: 20px;
.hint_btn {
width: 80px;
height: 30px;
text-align: center;
line-height: 30px;
background: #009944;
color: #fff;
border-radius: 6px;
}
.hint_no_btn {
width: 80px;
height: 30px;
text-align: center;
line-height: 30px;
border: 1px solid #333;
color: #000;
border-radius: 6px;
}
}
</style>
+31
View File
@@ -0,0 +1,31 @@
<script lang="tsx" setup name="sw-loading">
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
})
</script>
<template>
<van-overlay :show="props.loading">
<div class="wrapper" @click.stop>
<van-loading type="spinner" />
</div>
</van-overlay>
</template>
<style lang="less" scoped>
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
}
</style>
+560
View File
@@ -0,0 +1,560 @@
<template>
<div ref="container"></div>
</template>
<script lang="ts" setup>
import {ref} from "vue"
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import {ConvexHull} from 'three/examples/jsm/math/ConvexHull.js'
import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js';
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 preventStop = ref(false)
const rotateTimer = ref(null)
const autoRotate = ref(null)
const twoFirst = ref(true)
const lastPoint = ref(
{
touchStartX1: 0,
touchStartY1: 0,
touchStartX2: 0,
touchStartY2: 0,
}
)
const emit = defineEmits([
"changeBackgound",
"rotateControl",
"handleProgress",
"handleLoaded",
]);
// 监听组件销毁事件
onBeforeUnmount(() => {
// 销毁 Three.js 模型
scene.remove(model);
clearTimer();
model.traverse((child) => {
if (child.isMesh) {
child.geometry.dispose();
child.material.dispose();
}
});
// 清理渲染器
renderer.dispose();
});
const initData = () => {
let storage = localStorage.get("positionData");
console.log("在模型加载之前", storage)
let pid = props.pid;
let obj= {
pid: pid,
scale: {
x: modelContainer.scale.x,
y: modelContainer.scale.y,
z: modelContainer.scale.z,
},
initScale: {
x: modelContainer.scale.x,
y: modelContainer.scale.y,
z: modelContainer.scale.z,
},
position: {
x: modelContainer.position.x,
y: modelContainer.position.y,
z: modelContainer.position.z,
},
rotation: {
x: modelContainer.rotation.x,
y: modelContainer.rotation.y,
z: modelContainer.rotation.z,
},
}
if(!storage || storage.length == 0) {
localStorage.set("positionData", [obj])
}else {
let hasFoundData = false;
storage.filter((item, i) => {
// 只更新
if (item.pid == pid) {
hasFoundData = true;
modelContainer.rotation.x = item.rotation.x
modelContainer.rotation.y = item.rotation.y
modelContainer.rotation.z = item.rotation.z
modelContainer.scale.x = item.scale.x
modelContainer.scale.y = item.scale.y
modelContainer.scale.z = item.scale.z
modelContainer.position.x = item.position.x
modelContainer.position.y = item.position.y
modelContainer.position.z = item.position.z
// this.gltfItemLights.position.x = item.lightPosition.x
// this.gltfItemLights.position.y = item.lightPosition.y
// this.gltfItemLights.position.z = item.lightPosition.z
// this.gltfItemLights.rotation.x = item.lightRotate.x
// this.gltfItemLights.rotation.y = item.lightRotate.y
// this.gltfItemLights.rotation.z = item.lightRotate.z
}
});
if (!hasFoundData) {
storage.push(obj);
localStorage.set("positionData", storage)
}
}
// 加载模型
loadModel(props.modelFile)
}
watch(() => props.modelFile, (val, oldVal) => {
if (val) {
initData()
// setTimer()
console.log("modelFile", props.modelFile)
}
}, {
immediate: true
})
const modelContainer = new THREE.Object3D();
const loader = new GLTFLoader();
const loadModel = (modelPath) => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
// 创建场景
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
);
// 加载 glb 格式的 3D 模型
loader.setDRACOLoader(dracoLoader)
loader.load(
// "src/assets/model/scene.gltf",
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)
// 获取包围盒中心点
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);
}
);
// 添加环境光
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;
console.log("directionalLight",directionalLight)
directionalLight.target = modelContainer;
scene.add( directionalLight );
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(window.devicePixelRatio); // 设置画布分辨率 提高画质
container.value.appendChild(renderer.domElement);
// 加载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)
// 坐标轴
// const axesHelper = new THREE.AxesHelper(5)
// scene.add( axesHelper );
// // 添加轨道控制器
// controls = new OrbitControls(camera, renderer.domElement);
// controls.autoRotate = true;
// controls.autoRotateSpeed = 5;
// 监听缩放\旋转操作
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) => {
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.010;
// 模型旋转
const handleRotate = (event) => {
// 当自动旋转的时候 短暂停止
if (autoRotate.value && !preventStop.value) {
clearTimer();
// 防抖
preventStop.value = true
setTimeout(() => {
setTimer();
preventStop.value = false
}, 2000);
}
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) => {
// 当自动旋转的时候 短暂停止
if (autoRotate.value && !preventStop.value) {
clearTimer();
// 防抖
preventStop.value = true
setTimeout(() => {
setTimer();
preventStop.value = false
}, 2000);
}
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);
};
animate();
// 监听窗口缩放
window.addEventListener("resize", () => {
renderer.setSize( window.innerWidth, window.innerHeight );
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
}
//-------------------------------事件-------------------------------
// 设置背景颜色
const changeColorChild = (type, index, param) => {
scene.background = new THREE.Color(param);
emit("changeBackgound", index)
}
// 设置自定义背景
const changeTransparent = (type, index, param) => {
scene.background = null;
emit("changeBackgound", index)
}
// 按钮事件
const controlEvent = (val) => {
let data = localStorage.get("positionData");
let foundData = false;
data.filter(item=>{
if (item.pid == props.pid && props.pid > 0 && !foundData) {
foundData = true;
if(val == 1) {
ResetPosition();
}else if(val == 2) {
// 保存当前位置
item.rotation.x = modelContainer.rotation.x
item.rotation.y = modelContainer.rotation.y
item.rotation.z = modelContainer.rotation.z
item.scale.x = modelContainer.scale.x
item.scale.y = modelContainer.scale.y
item.scale.z = modelContainer.scale.z
item.position.x = modelContainer.position.x
item.position.y = modelContainer.position.y
item.position.z = modelContainer.position.z
// item.lightPosition.x = this.gltfItemLights.position.x
// item.lightPosition.y = this.gltfItemLights.position.y
// item.lightPosition.z = this.gltfItemLights.position.z
// item.lightRotate.x = this.gltfItemLights.rotation.x
// item.lightRotate.y = this.gltfItemLights.rotation.y
// item.lightRotate.z = this.gltfItemLights.rotation.z
// item.lightScale = this.data.lightScale;
// item.lightScalePoint = this.data.lightScalePoint;
localStorage.set("positionData",data )
showSuccessToast("保存成功".$t)
}else if(val == 3){
spin();
}
}
})
}
// 重置位置
const ResetPosition = () => {
modelContainer.position.z = 0;
modelContainer.position.y = 0;
modelContainer.position.x = 0;
modelContainer.scale.z = 1;
modelContainer.scale.y = 1;
modelContainer.scale.x = 1;
modelContainer.rotation.z = 0;
modelContainer.rotation.y = 0;
modelContainer.rotation.x = 0;
}
// 自动旋转
const spin = () => {
if (!autoRotate.value) {
if (preventStop.value) {
return
}
setTimer()
} else {
clearTimer()
}
}
// 关闭自动旋转
const clearTimer = () => {
clearInterval(rotateTimer.value)
rotateTimer.value = null;
autoRotate.value = false;
emit("rotateControl", false)
}
// 开启自动旋转
const setTimer = () => {
clearInterval(rotateTimer.value)
// 防抖
rotateTimer.value = setInterval(() => {
modelContainer.rotation.y -= 0.1
}, 100)
autoRotate.value = true;
emit("rotateControl", true)
}
defineExpose({
changeColorChild,
changeTransparent,
ResetPosition,
controlEvent,
spin,
})
</script>
<style>
body {
margin: 0;
/* overflow: hidden; */
}
.control_bar {
position: absolute;
}
</style>
+271
View File
@@ -0,0 +1,271 @@
<script lang="ts" setup>
import { ref } from 'vue'
import * as SPLAT from 'gsplat'
import { showSuccessToast, showToast } from 'vant'
const props = defineProps({
modelFile: { type: String, default: () => '' },
pid: { type: Number, default: 0 },
})
const emit = defineEmits([
'changeBackgound',
'rotateControl',
'handleProgress',
'handleLoaded',
])
const scene = new SPLAT.Scene()
const camera = new SPLAT.Camera()
const splat = ref(null)
const color = new SPLAT.Color32()
const renderer = new SPLAT.WebGLRenderer()
const controls = new SPLAT.OrbitControls(camera, renderer.canvas)
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 preventStop = ref(false)
const autoRotate = ref(false)
const rotateInterval = ref(null)
const twoFirst = ref(true)
const lastPoint = ref(
{
touchStartX1: 0,
touchStartY1: 0,
touchStartX2: 0,
touchStartY2: 0,
},
)
watch(() => props.modelFile, (val, oldVal) => {
if (val) {
main(val)
// setTimer()
}
}, {
immediate: true,
})
const initCameraRotation = ref(null)
const initCameraPosition = ref(null)
const initCameraScale = ref(null)
const initialRotation = ref(null)
const DEG2RAD = Math.PI / 180
let animationFrameId
// onMounted(async () => {
// await main()
// })
// 监听组件销毁事件
onBeforeUnmount(() => {
// 销毁 Three.js 模型
splat.value = null
// 清理渲染器
renderer.dispose()
if (animationFrameId)
cancelAnimationFrame(animationFrameId)
if (renderer.canvas.parentNode)
renderer.canvas.parentNode.removeChild(renderer.canvas)
})
async function main(val) {
// 初始背景
renderer.canvas.style.background = 'transparent'
controls.orbitSpeed = 3 // 设置相机旋转速度
// 初始化模型
const url = val;
// const url = 'https://suwa3d-test.oss-cn-shanghai.aliyuncs.com/plys/151322_superpoint.ply'
// const url = "https://suwa3d-test.oss-cn-shanghai.aliyuncs.com/test/glbs/auto/195231.splat";
// const url = "https://suwa3d-test.oss-cn-shanghai.aliyuncs.com/test/glbs/auto/192571.splat";
splat.value = await SPLAT.Loader.LoadAsync(url, scene, (e) => {
if(e == 1) {
emit("handleLoaded")
}else {
emit("handleProgress",e * 100)
}
});
console.log("splat.value", splat.value)
// splat.value = await SPLAT.PLYLoader.LoadAsync(url, scene, (e) => {
// console.log('e', e)
// if (e == 1)
// emit('handleLoaded')
// else
// emit('handleProgress', e * 100)
// })
const rotation = new SPLAT.Vector3(-3.2, -0.5, 0)
splat.value.rotation = SPLAT.Quaternion.FromEuler(rotation)
splat.value.position = new SPLAT.Vector3(0, -0.8, 0)
splat.value.scale = new SPLAT.Vector3(1.5, 1.5, 1.5)
splat.value.applyPosition()
splat.value.applyRotation()
splat.value.applyScale()
// splat.value.scale = new SPLAT.Vector3(1, 1, 1)
const frame = () => {
controls.update()
renderer.render(scene, camera)
animationFrameId = requestAnimationFrame(frame)
}
animationFrameId = requestAnimationFrame(frame)
// 获取到初始状态的位置
initCameraRotation.value = camera.rotation
initCameraPosition.value = camera.position
initCameraScale.value = camera.scale
initialRotation.value = splat.value.rotation.clone()
// 监听缩放\旋转操作
renderer.canvas.addEventListener('touchmove', (event) => {
// console.log("camera.rotation", camera.rotation)
// handleTouchStart(event)
})
}
// -------------------------------事件-------------------------------
// 设置背景颜色
const changeColorChild = (type, index, param) => {
renderer.canvas.style.background = param
emit('changeBackgound', index)
}
// 设置自定义背景
const changeTransparent = (type, index, param) => {
renderer.canvas.style.background = 'transparent'
emit('changeBackgound', index)
}
// 按钮事件
const controlEvent = (val) => {
// let data = localStorage.get("positionData");
// let foundData = false;
// data.filter(item=>{
// if (item.pid == props.pid && props.pid > 0 && !foundData) {
// foundData = true;
if (val == 1) {
ResetPosition()
} else if (val == 2) {
// 保存当前位置
initCameraRotation.value = camera.rotation;
initCameraPosition.value = camera.position;
initCameraScale.value = camera.scale;
showSuccessToast('保存成功'.$t)
} else if (val == 3) {
spin()
} else if (val == 4) {
spin()
}
// })
}
const ResetPosition = () => {
if(!autoRotate.value) {
camera.rotation = initCameraRotation.value
camera.position = initCameraPosition.value
camera.scale = initCameraScale.value
const rotation = new SPLAT.Vector3(0, -count.value, 0)
splat.value.rotation = SPLAT.Quaternion.FromEuler(rotation)
// splat.value.scale = new SPLAT.Vector3(0.5, 0.5, 0.5)
splat.value.applyRotation()
// splat.value.applyScale();
// splat.value.rotation = SPLAT.Quaternion.FromAxisAngle(new SPLAT.Vector3(0, 1, 0), DEG2RAD * 87);
// splat.value.applyRotation();
console.log('splat.value.scale', splat.value.scale)
}
}
// 旋转
const spin = () => {
if (!autoRotate.value) {
if (preventStop.value)
return
setTimer()
} else {
clearTimer()
}
}
const count = ref(0);
// 开启自动旋转
const setTimer = () => {
const val = initCameraRotation.value;
clearInterval(rotateInterval.value);
rotateInterval.value = setInterval(() => {
const a = 0.03
const rotation = new SPLAT.Vector3(0, a, 0)
splat.value.rotation = SPLAT.Quaternion.FromEuler(rotation)
splat.value.applyRotation()
count.value +=0.03
// let cameraRotation = new SPLAT.Vector3(0, a, 0)
// camera.position = new SPLAT.Vector3(x, y, z);
// console.log("initCameraRotation", scene.objects[0])
// val.y -=0.05;
// camera.rotation = SPLAT.Quaternion.FromAxisAngle(val, DEG2RAD * 87);
}, 100)
autoRotate.value = true
emit('rotateControl', true)
}
// 关闭自动旋转
const clearTimer = () => {
clearInterval(rotateInterval.value)
rotateInterval.value = null
autoRotate.value = false
emit('rotateControl', false)
}
const onKeyDown = (event: KeyboardEvent) => {
// Use i, j, k, l to move the camera around
let translation = new SPLAT.Vector3()
if (event.key === 'j')
translation = translation.add(new SPLAT.Vector3(-1, 0, 0))
if (event.key === 'l')
translation = translation.add(new SPLAT.Vector3(1, 0, 0))
if (event.key === 'i')
translation = translation.add(new SPLAT.Vector3(0, 0, 1))
if (event.key === 'k')
translation = translation.add(new SPLAT.Vector3(0, 0, -1))
camera.position = camera.position.add(translation)
// Use u to set a random look target near the origin
if (event.key === 'u') {
const target = new SPLAT.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5)
controls.setCameraTarget(target)
}
// Use space to reset the camera
if (event.key === ' ') {
camera.position = new SPLAT.Vector3()
camera.rotation = new SPLAT.Quaternion()
}
}
window.addEventListener('keydown', onKeyDown)
defineExpose({
changeColorChild,
changeTransparent,
ResetPosition,
controlEvent,
})
</script>
<template>
<div>
<!-- <canvas ref="glcanvas"></canvas> -->
</div>
</template>
<style lang="less" scoped>
.viewer {
/* width: 100vw;
height: 100vh; */
}
</style>
+73
View File
@@ -0,0 +1,73 @@
<template>
<div class="tabbar_wrap">
<van-tabbar route v-model="active" active-color="#009944" inactive-color="#AAAAAA">
<!-- <van-tabbar-item to="/">
<span class="tabbar_text">{{'首页'.$t}}</span>
<template #icon="props">
<img class="tabbar_img" :src="props.active ? home.active : home.inactive" />
</template>
</van-tabbar-item> -->
<van-tabbar-item to="/myOrder">
<span class="tabbar_text">{{'订单'.$t}}</span>
<template #icon="props">
<img class="tabbar_img" :src="props.active ? order.active : order.inactive" />
</template>
</van-tabbar-item>
<van-tabbar-item to="/user">
<span class="tabbar_text">{{'我的'.$t}}</span>
<template #icon="props">
<img class="tabbar_img" :src="props.active ? user.active : user.inactive" />
</template>
</van-tabbar-item>
<!-- <van-tabbar-item to="/message">
<span>消息</span>
<template #icon="props">
<img :src="props.active ? message.active : message.inactive" />
</template>
</van-tabbar-item>
<van-tabbar-item to="/user">
<span class="tabbar_text">我的</span>
<template #icon="props">
<img class="tabbar_img" :src="props.active ? user.active : user.inactive" />
</template>
</van-tabbar-item> -->
</van-tabbar>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const active = ref(0);
import orderActive from '@/assets/common/my-order-select.png';
import orderInactive from '@/assets/common/my-order.png';
import homeActive from '@/assets/common/home-select.png';
import homeInactive from '@/assets/common/home.png';
import userActive from '@/assets/common/user-info-select.png';
import userInactive from '@/assets/common/user-info.png';
const order = {
active: orderActive,
inactive: orderInactive,
};
const home = {
active: homeActive,
inactive: homeInactive,
};
// const message = {
// active: 'src/assets/common/message-select.png',
// inactive: 'src/assets/common/message.png',
// };
const user = {
active: userActive,
inactive: userInactive,
};
</script>
<style lang="less" scoped>
:deep(.van-tabbar) {
width: 375px;
}
</style>