右上角陀螺仪也可点击,需要https的环境,手动下载DeviceOrientationControls.js文件
后台包含打点功能
<template>
<div id="quanjing" style="width: 100vw; height: 100vh; overflow: hidden">
<span
id="tip"
style="position: absolute; color: red; top: 0; left: 0"
></span>
<!-- 封面图切换结束 -->
<img
:src="imageUrl + aerialData.asteroidImg"
style="
position: absolute;
top: 50%;
height: 100vh;
left: 50%;
transform: translate(-50%, -50%);
"
v-if="isShowFM"
/>
<img
v-if="isShowHand"
src="../assets/images/quanjingHand.png"
style="
z-index: 2;
position: absolute;
top: 50%;
left: 50%;
margin-left: -80px;
margin-top: -90px;
border-radius: 10px;
width: 160px;
height: 180px;
"
/>
<!-- 封面图切换结束 -->
<img
src="../assets/images/quanjinglogo.png"
style="position: absolute; top: 10px; left: 10px; width: 100px"
/>
<!-- 右上角全景图标切换开始 -->
<div class="rig-list" id="hangpaiIcon" v-if="!isPc">
<img v-if="isFull" src="../assets/images/hangpaiIcon.png" />
<img v-else src="../assets/images/hangpaiIcon2.png" />
</div>
<!-- 右上角全景图标切换开始结束 -->
<!-- 标注增删改查开始 -->
<div class="addPoint" v-if="isShowPoint">
<div class="row" v-for="(item, index) in biaojiList" :key="index">
<div>地名:</div>
<input placeholder="" class="inp" v-model="item.title" />
<div>x:</div>
<input placeholder="" class="inp" v-model="item.x" />
<div>y:</div>
<input placeholder="" class="inp" v-model="item.y" />
<div>z:</div>
<input placeholder="" class="inp" v-model="item.z" />
<div
style="
background-color: rgb(253, 143, 143);
margin-left: 10px;
padding: 0 5px;
"
@click="delPoint(index)"
>
删除
</div>
<div
style="
background-color: rgb(249, 253, 143);
margin-left: 10px;
padding: 0 5px;
"
@click="huoqu(index)"
>
获取
</div>
<div
style="
background-color: rgb(104, 255, 111);
margin-left: 10px;
padding: 0 5px;
"
@click="baocun(index)"
>
保存
</div>
</div>
<div class="row">
<div
@click="addPoint"
style="
background-color: rgb(143, 200, 253);
margin-left: 10px;
padding: 0 5px;
"
>
新增
</div>
<div
@click="submitPoint"
style="
background-color: rgb(143, 253, 191);
margin-left: 10px;
padding: 0 5px;
"
>
上传
</div>
<span style="font-size: 12px"
>(*1.点击获取按钮鼠标右键点击在航拍图上。2.确认坐标后点击同行保存按钮。3.标注完成后点击上传按钮)</span
>
</div>
</div>
<!-- 标注增删改查结束 -->
<div id="biaozhudian"></div>
</div>
</template>
<!-- [{"title":"东直门","x":"445.42720890862677","y":"-218.0846523283593","z":"-57.86235074865762"}] -->
<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { DeviceOrientationControls } from "three/examples/jsm/controls/DeviceOrientationControls.js";
import TWEEN from "@tweenjs/tween.js";
import {
CSS2DRenderer,
CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";
import { aerialData, editAerial, updateMark } from "@/api/house/house.js";
export default {
name: "quanjing",
data() {
return {
isShowFM: false,
isShowHand: false,
imageUrl: "https://obj.qiniu.fangdadi.com/",
title: "房大地-全景",
scene: null,
camera: null,
renderer: null,
css2Renderer: null,
loader: null,
texture: null,
sphereGeometry: null,
mesh: null,
axesHelper: null,
tween: null,
controls: null,
controls2: null,
//标点
vector: null,
screenVector: null,
doc: null,
activePoint: null,
div: null,
raycaster: null,
mouse: null,
tagObject: null,
//陀螺仪
clock: null,
isFull: null,
dcontrols: null,
aerialData: {},
biaojiList: [],
pointList: [{ title: "", x: "", y: "", z: "" }],
pointIndex: null,
isShowPoint: false,
tagArray: [],
isPc: false,
};
},
watch: {},
mounted() {
//判断是什么环境,手机还是电脑
if (document.documentElement.clientWidth < 720) {
this.isPc = false;
} else {
this.isPc = true;
}
this.getAerialData();
// 点击右上角陀螺仪
document
.getElementById("hangpaiIcon")
.addEventListener("click", this.fullOrExit, false);
},
methods: {
//获取数据
getAerialData() {
let idNum = null;
console.log(this.$route.params.id.split("+"));
if (this.$route.params.id.split("+").length == 2) {
console.log(1111);
this.isShowPoint = true;
idNum = this.$route.params.id.split("+")[0];
} else {
console.log(22222222);
this.isShowPoint = false;
idNum = this.$route.params.id;
}
aerialData(idNum)
.then((res) => {
if (res.data) {
this.aerialData = res.data;
this.isShowFM = true;
if (res.data.markData) {
console.log(JSON.parse(res.data.markData));
this.biaojiList = JSON.parse(res.data.markData);
console.log(" this.biaojiList ", this.biaojiList.length);
}
}
})
.then(() => {
this.clock = new THREE.Clock();
this.container = document.body;
this.isFull = false;
//判断是什么环境,手机还是电脑
// if (document.documentElement.clientWidth < 720) {
// }
this.initThree();
this.clickBiaoji();
this.objTween();
window.addEventListener("pointerdown", this.onMouseDown, false);
var _this = this;
// 更改渲染器画布大小
window.onresize = function () {
// 重置渲染器输出画布canvas尺寸
_this.renderer.setSize(window.innerWidth, window.innerHeight);
// 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
_this.camera.aspect = window.innerWidth / window.innerHeight;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
_this.camera.updateProjectionMatrix();
};
});
},
// /**
// 准备全景图像:
// 首先,您需要获得或创建全景图像。全景图像是一种呈现完整360度视野的特殊图像。您可以使用专业相机拍摄全景照片,或者从互联网上获取全景图像。确保全景图像采用通常的全景图像格式,如equirectangular格式。
// 设置Three.js场景:
// 创建一个HTML页面,引入Three.js库。您可以从Three.js官方网站下载或使用CDN来加载库。
// 创建Three.js场景:
// 在您的HTML页面中,创建一个Three.js场景、相机和渲染器。
// */
// // 相机第一个参数fov 130->75
initThree() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
130,
window.innerWidth / window.innerHeight,
1,
1000
);
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.domElement.style.zIndex = -1;
this.css2Renderer = new CSS2DRenderer();
this.css2Renderer.setSize(window.innerWidth, window.innerHeight);
this.LoadingImg();
},
LoadingImg() {
var _this = this;
/*
加载全景图像:
使用Three.js的TextureLoader加载全景图像。
*/
this.loader = new THREE.TextureLoader();
this.texture = this.loader.load(
this.imageUrl + this.aerialData.panoramaImg,
function (obj) {
console.log("加载完了");
_this.isShowFM = false;
}
); // If texture is used for color information, set colorspace.
this.texture.encoding = THREE.sRGBEncoding;
this.sphereGeometry = new THREE.SphereGeometry(500, 60, 40);
this.sphereGeometry.scale(-1, 1, 1); //创建的球形几何体执行这个方法, 镜像就正回来了
this.sphereMaterial = new THREE.MeshBasicMaterial({
map: this.texture,
side: THREE.DoubleSide,
});
this.mesh = new THREE.Mesh(this.sphereGeometry, this.sphereMaterial);
this.scene.add(this.mesh);
// // AxesHelper:辅助观察的坐标系
// this.axesHelper = new THREE.AxesHelper(150);
// this.scene.add(this.axesHelper);
/*
设置相机视角:
将相机朝向全景图像的中心,以确保全景图像填充整个视野。
*/
this.camera.position.set(0, 500, 0.1);
this.camera.lookAt(0, 0, 0);
/**
* 模型旋转
*/
this.mesh.rotateY(-Math.PI / 2); //绕x轴旋转π/2
},
objTween() {
var _this = this;
// // 旋转过渡效果
this.tween = new TWEEN.Tween({
x: 0,
y: 500,
z: 10,
ry: -Math.PI / 2,
fov: 130,
}) // 开始位置(2D)
.to(
{
x: 0,
y: 0,
z: 0.1,
ry: 0,
fov: 75,
},
3000
) // 结束位置(3D)
.easing(TWEEN.Easing.Quadratic.InOut) // 缓动函数
.onUpdate(function (obj) {
_this.camera.position.set(obj.x * 1, obj.y * 1, obj.z * 1);
_this.camera.lookAt(_this.scene.position);
_this.mesh.rotation.y = obj.ry;
_this.camera.rotation.x = obj.ry;
_this.camera.fov = obj.fov;
_this.camera.updateProjectionMatrix();
})
.delay(3000)
.start()
.onComplete(function () {
//运动结束后地图打点
_this.biaoji();
_this.isShowHand = true;
setTimeout(() => {
_this.isShowHand = false;
}, 2000);
});
this.OrbitControlsFun();
},
// /*
// 渲染场景:
// 使用requestAnimationFrame函数循环渲染Three.js场景。
// */
animate() {
requestAnimationFrame(this.animate);
TWEEN.update(); // 更新Tween.js动画
this.renderer.render(this.scene, this.camera);
this.css2Renderer.render(this.scene, this.camera);
if (this.isFull) {
this.dcontrols.update(this.clock.getDelta());
}
},
/*
添加交互性(可选):
您还可以添加鼠标或触摸交互,以允许用户在全景图像中浏览。Three.js提供了相关的控制器,如OrbitControls。
*/
OrbitControlsFun() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableZoom = false;
this.controls.enablePan = false;
this.controls2 = new OrbitControls(
this.camera,
this.css2Renderer.domElement
);
this.controls2.enableZoom = false;
this.controls2.enablePan = false;
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
document.getElementById("quanjing").appendChild(this.renderer.domElement);
this.animate();
},
//设置标记点开始
biaoji() {
if (this.tagArray.length != 0) {
this.tagArray.forEach((i) => {
this.scene.remove(i);
});
}
this.biaojiList.forEach((item) => {
let info1 = document.createElement("div");
info1.setAttribute("class", "info1");
info1.style.width = "40px";
info1.style.height = "40px";
info1.style.position = "absolute";
info1.style.pointerEvents = "auto";
var img = document.createElement("img");
img.src =
"https://wimg.588ku.com/gif620/20/06/28/a47c5cb0333780628b29c0e94f3b9423.gif";
img.style.width = "40px";
img.style.height = "40px";
info1.appendChild(img);
var p = document.createElement("p");
p.innerText = item.title;
p.style.position = "absolute";
p.style.bottom = "100%";
p.style.left = "-70%";
p.style.minWidth = "100px";
p.style.textAlign = "center";
p.style.minHeight = "20px";
p.style.background = "#31313194";
p.style.padding = "5px";
p.style.borderRadius = "10px";
p.style.color = "white";
info1.appendChild(p);
let tag = new CSS2DObject(info1);
tag.name = "proLabel";
tag.position.set(item.x, item.y, item.z);
this.scene.add(tag);
this.tagArray.push(tag);
this.css2Renderer.domElement.style.position = "absolute";
this.css2Renderer.domElement.style.top = "0";
this.css2Renderer.domElement.style.pointerEvents = "none";
document
.getElementById("biaozhudian")
.appendChild(this.css2Renderer.domElement);
});
},
clickBiaoji() {
this.vector = new THREE.Vector3();
this.screenVector = new THREE.Vector3();
this.doc = document;
this.activePoint = null;
this.div = this.doc.getElementById("tip");
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.tagObject = new THREE.Object3D();
},
/**
* 鼠标点击触发
**/
onMouseDown(event) {
if (event.buttons === 2 && this.pointIndex != null) {
// 屏幕坐标转标准设备坐标
this.vector.set(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0
);
// 将标准设备坐标转为世界坐标
this.vector.unproject(this.camera);
this.raycaster = new THREE.Raycaster(
this.camera.position,
this.vector.sub(this.camera.position).normalize()
);
let intersects = this.raycaster.intersectObjects([this.mesh]);
if (intersects.length > 0) {
this.activePoint = intersects[0].point;
let point = this.toScreenPosition({
point: this.activePoint,
});
this.div.style.left = point.x + "px";
this.div.style.top = point.y + 20 + "px";
}
let xyz = this.activePoint;
this.biaojiList[this.pointIndex].x = xyz.x;
this.biaojiList[this.pointIndex].y = xyz.y;
this.biaojiList[this.pointIndex].z = xyz.z;
console.log("点击坐标-----", JSON.stringify(this.activePoint)); // 传给后台的参数
}
},
toScreenPosition({ obj = null, point = null }) {
point ? this.screenVector.set(...point) : this.screenVector.set();
// 屏幕坐标系中心
let widthHalf = this.renderer.getContext().canvas.width / 2;
let heightHalf = this.renderer.getContext().canvas.height / 2;
if (obj) {
// 更新物体及其后代的全局变换
obj.updateMatrixWorld();
// 提取位置相关的分量
this.screenVector.setFromMatrixPosition(obj.matrixWorld);
}
// 世界坐标转标准设备坐标。范围[-1,1]
this.screenVector.project(this.camera);
//标准设备坐标转屏幕坐标(2D)
this.screenVector.x = this.screenVector.x * widthHalf + widthHalf;
this.screenVector.y = -this.screenVector.y * heightHalf + heightHalf;
return {
x: this.screenVector.x,
y: this.screenVector.y,
};
},
// 陀螺仪
setOrientationControls(e) {
// 判断手机电脑端
if (!e.alpha) {
return;
}
this.isFull = true;
this.dcontrols = new DeviceOrientationControls(this.camera, true);
this.dcontrols.connect();
this.dcontrols.update();
window.removeEventListener(
"deviceorientation",
this.setOrientationControls,
true
);
},
// 陀螺仪权限判断
fullOrExit() {
let _this = this;
if (!_this.isFull) {
try {
console.log("浏览器UA---->", navigator.userAgent);
if (
navigator.userAgent.includes("iPhone") ||
navigator.userAgent.includes("iPad") ||
navigator.userAgent.includes("iPod") ||
navigator.userAgent.includes("Macintosh")
) {
// 这是苹果设备上的浏览器
console.log("这是苹果设备上的浏览器");
if (
window.DeviceOrientationEvent !== undefined &&
typeof window.DeviceOrientationEvent.requestPermission ===
"function"
) {
window.DeviceOrientationEvent.requestPermission()
.then(function (response) {
if (response == "granted") {
window.addEventListener(
"deviceorientation",
_this.setOrientationControls,
true
);
}
})
.catch(function (error) {
console.error(
"THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:",
error
);
});
} else {
window.addEventListener(
"deviceorientation",
_this.setOrientationControls,
true
);
}
} else {
// 这不是苹果设备上的浏览器
console.log("这不是苹果设备上的浏览器");
window.addEventListener(
"deviceorientation",
_this.setOrientationControls,
true
);
}
} catch (error) {
console.error("监听事件处理程序出错:", error);
}
} else {
if (_this.dcontrols) {
_this.dcontrols.disconnect();
_this.dcontrols.update();
window.removeEventListener(
"deviceorientation",
_this.setOrientationControls,
true
);
} else {
console.log("dcontrols 未创建");
}
_this.isFull = false;
}
},
// 增删改查数据开始
//删除标点
delPoint(index) {
this.pointIndex = null;
this.biaojiList.splice(index, 1);
this.biaoji();
},
//获取标点
huoqu(index) {
this.pointIndex = index;
},
//保存标点
baocun(index) {
this.pointIndex = null;
this.biaoji();
},
//上传标点信息
async submitPoint() {
let FormData = new window.FormData();
FormData.append("aerialId", this.aerialData.aerialId);
FormData.append("markData", JSON.stringify(this.biaojiList));
let res = await updateMark(FormData);
if (res.data == 1) {
this.$message.success("上传成功");
console.log(res);
}
},
//新增标点数据
addPoint() {
this.biaojiList.push({ title: "", x: "", y: "", z: "" });
},
// 增删改查数据结束
},
};
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
// ::v-deep
#quanjing {
overflow: hidden;
width: 100vw;
height: 100vh;
touch-action: none;
position: absolute;
top: 0px;
}
.info1 {
position: absolute;
width: 40px;
height: 40px;
}
.info1 img {
width: 40px;
height: 40px;
}
.info1 p {
position: absolute;
bottom: 100%;
left: -70%;
min-width: 100px;
text-align: center;
min-height: 20px;
color: white;
}
.rig-list {
position: absolute;
right: 10px;
top: 40px;
img {
width: 40px;
height: 40px;
margin-bottom: 10px;
}
#hangpaiIcon {
z-index: 2;
}
}
.addPoint {
width: 750px;
background: rgba(255, 255, 255, 0.453);
min-height: 50px;
position: absolute;
top: 0;
left: 50%;
margin-left: -375px;
}
.row {
width: 100%;
overflow: hidden;
margin-bottom: 5px;
div {
float: left;
margin-left: 10px;
}
.inp {
float: left;
border: none;
outline: none;
width: 100px;
margin-left: 10px;
}
}
</style>