preface
When I was working on a smart city project, the 3D map page of the project was provided by a third party. The product manager kept asking for 3D maps, which made me contact with the third party every day. The third party said that it could not be done or the performance was not good. Unable to start their own 3D map, using CesiumJS map framework development. Cesium loading a large number of Label entities a solution to the lag before this article is a solution to the relatively large problems encountered in making 3D maps.
Recently, the product manager did not know where to go and saw a 3D map with compass to achieve perspective movement, so he copied it down and asked me if I could achieve it. Haha, I told him directly that the project was too urgent to be realized. WDNMD(Only you don’t understand)
Well, it’s all said and done, because I’m a cool developer.
The effect
Navigation compass function
Before you can implement the navigation compass, you must first understand how to control the compass and how to move the scene camera.
Compass area operation
So here's a question: is camera movement a camera position move or a camera Angle move? In the case of showing an object, it would be 360 degrees around the object so our compass camera is determined to move around the point where the center of the screen isCopy the code
Take the center of the compass region as the origin
- Move the mouse left and right around the center of the camera’s view area
- Move the mouse up and down around the center of the camera’s view area
Function implementation
Mouse down event
When the mouse is pressed, it is necessary to record the center position of the camera’s viewing Angle area and record the time (in order to smooth the picture when the camera moves behind).
let result = new Cartesian3();
const { camera, scene } = this.$refs.map.viewer;
let dom = e.target;
const rayScratch = new Ray();
rayScratch.origin = camera.positionWC;
rayScratch.direction = camera.directionWC;
result = scene.globe.pick(rayScratch, scene, result);
result = camera.worldToCameraCoordinatesPoint(result, result);
const newTransformScratch = new Matrix4();
/ * Transforms. EastNorthUpToFixedFrame calculate a 4 x4 transformation matrix, the matrix from one up to provide the origin at the center of the northeast axis reference coordinate system to provide fixed reference ellipsoid coordinate system. The local axis is defined as: the X-axis points east of the local area. The y axis points north to the local area. The z-axis points in the direction of the normal of the ellipsoid passing through that position. * /
// How to understand?
// When the 4x4 matrix is used to set the position of the camera, the rotation of the camera takes the origin provided by the 4x4 matrix as the rotation center.
// When the 4x4 matrix is the identity matrix, the camera rotation is a translation of position
let frame = Transforms.eastNorthUpToFixedFrame(
result,
scene.globe.ellipsoid,
newTransformScratch
);
let time = getTimestamp();
document.addEventListener('mousemove', mousemoveHandler);
document.addEventListener('mouseup', mouseupHandler);
this.$refs.map.viewer.clock.onTick.addEventListener(
orbitTickFunction
);
Copy the code
Mouse movement event
As the mouse moves, calculate the coordinate value of the mouse position relative to the compass origin.
- Computes the coordinates of the central point of the compass element with respect to the compass element region
x0 = (right - left) / 2
y0 = (bottom - top) / 2
- Coordinates of the mouse position relative to the compass element
Tx = e.clientX - left
Ty = e.clientY - top
- Given the relative position and origin coordinates, we can construct a two-dimensional vector (to calculate the Angle) using
CesiumJS
theCartesian2.subtract
Let’s figure out the two dimensional vector
// dom is compass DOM
let compassR = dom.getBoundingClientRect();
// Click the center of the element xy
let center = new Cesium.Cartesian2(
(compassR.right - Cesium.compassR.left) / 2,
(compassR.bottom - Cesium.compassR.top) / 2
);
// Distance from mousedown element Tx, Ty
let movePosition = new Cesium.Cartesian2(
e.clientX - compassR.left,
e.clientY - compassR.top
);
// Calculate the distance between Tx, Ty and the center of the element
let vector = Cesium.Cartesian2.subtract(movePosition, center, new Cesium.Cartesian2());
Copy the code
Due to theSubtract is parameter 1- parameter 2
To get thevector.x
The components are correct, but we’re specifying that the upper Y-axis is positive, and the page is going to be positive Y-axis down here, sovector.y
In practice you have to take the negative.
Computing Angle
Using math.atan2 (y,x), we can calculate the radian of the Angle between the line from the origin to the coordinate point and the positive X-axis. But in fact, when we move the mouse along the positive y axis, the calculated Angle is 90°, and we want the positive y axis pointing due north (0°), and anticlockwise is positive, so the azimuth should be math.atan2 (y,x) – Math.pi / 2. Interval [0, 2π]. The purpose of calculating this Angle is to rotate the sector.
// zeroToTwoPi returns the value between 0 and 2π
const angle = Cesium.Math.zeroToTwoPi(Math.atan2(-vector.y, vector.x) - Math.PI / 2);
Copy the code
Calculates sector transparency
We want sector opacity to be 50% when the mouse is in the center of the compass, increasing gradually as the mouse moves away from the center, and reaching zero (opacity) at the edge of the compass.
Cartesian2.magnitude = 1, magnitude = 1, magnitude = 1, magnitude = 1
const distance = Cesium.Cartesian2.magnitude(vector);
// Distance from center point to compass edge, radius
const maxDistance = compassR.width;
// The length ratio from the center, up to 1
const distanceF = Math.min(distance / maxDistance, 1.0);
// Use the ease algorithm t^2 * coefficient (here the coefficient is 0.5 because the sector defaults to 50% transparency)
let easedOpacity = 0.5 * distanceF * distanceF + 0.5;
Copy the code
Let the camera move
Now that we’ve figured out the Angle and transparency, we can use them to rotate the camera
// This code is executed in the tick frame of Cesium clock.
// The execution intervals are not necessarily equal due to single-threaded reasons,
// So the amount of each rotation of the camera should be related to the time interval, otherwise there will be a pause when you rotate
const tempstamp = getTimestamp();
const deltaT = tempstamp - this.time;
EasedOpacity is calculated by comparing the length of the mouse to the center of the compass,
Therefore, the rotation rate can be calculated directly using easedOpacity.
// The two coefficients can be adjusted as desired
const rate = ((easedOpacity - 0.5) * 2.5) / 1000;
// Calculate the distance traveled
const distance = deltaT * rate;
// We need to use this Angle to figure out which direction the camera is rotating.
// We add 90 degrees here because we calculated that this Angle is the Angle between the line and the positive Y-axis,
// The Angle passed in by math. sin and math. cos is the Angle between the positive half of the x axis.
const angle = this.cursorAngle + CesiumMath.PI_OVER_TWO;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
// Where does the lookAtTransform set the camera Angle
viewer.camera.lookAtTransform(frame);
viewer.camera.rotateLeft(x);
viewer.camera.rotateUp(y);
// matrix4. IDENTITY 4x4 IDENTITY matrix, set the effect of this, as described in the 'mouse-down events' section above
viewer.camera.lookAtTransform(Matrix4.IDENTITY);
Copy the code
At the end
Here, the function of compass is complete, this article refers to cesium-navigation- ES6, cesium-navigation can actually say the above code ideas are from these two authors, open source long live!
The complete code
<template>
<div class="home">
<Map ref="map"/>
<div class="ng" @mousedown="handleMousedown">
<div
class="shan"
:style="{ opacity: easedOpacity, transform: `rotate(${-cursorAngle + Math.PI / 4}rad)`, }"
></div>
<div class="ng-box"></div>
</div>
</div>
</template>
<script>
import {
Cartographic,
Cartesian3,
Matrix4,
Cartesian2,
Math as CesiumMath,
Transforms,
getTimestamp,
Ray,
} from 'cesium/Build/Cesium/Cesium';
import Map from '@/components/Map.vue';
export default {
components: {
Map,},data() {
return {
vector2: undefined.cursorAngle: 0.easedOpacity: 0}; },created() {
this.time = undefined;
this.frame = undefined;
this.dom = undefined;
},
methods: {
handleMousedown(e) {
let result = new Cartesian3();
const { camera, scene } = this.$refs.map.viewer;
this.dom = e.target;
const rayScratch = new Ray();
rayScratch.origin = camera.positionWC;
rayScratch.direction = camera.directionWC;
result = scene.globe.pick(rayScratch, scene, result);
result = camera.worldToCameraCoordinatesPoint(result, result);
const newTransformScratch = new Matrix4();
this.frame = Transforms.eastNorthUpToFixedFrame(
result,
scene.globe.ellipsoid,
newTransformScratch
);
this.time = getTimestamp();
document.addEventListener('mousemove'.this.mousemoveHandler);
document.addEventListener('mouseup'.this.mouseupHandler);
this.$refs.map.viewer.clock.onTick.addEventListener(
this.orbitTickFunction
);
},
mouseupHandler() {
document.removeEventListener('mousemove'.this.mousemoveHandler);
document.removeEventListener('mouseup'.this.mouseupHandler);
this.$refs.map.viewer.clock.onTick.removeEventListener(
this.orbitTickFunction
);
this.easedOpacity = 0;
},
mousemoveHandler(e) {
const rect = this.dom.getBoundingClientRect();
const center = new Cartesian2(
(rect.right - rect.left) / 2,
(rect.bottom - rect.top) / 2
);
const movePosition = new Cartesian2(
e.clientX - rect.left,
e.clientY - rect.top
);
const vector2 = Cartesian2.subtract(
movePosition,
center,
new Cartesian2()
);
this.computeAngleAndOpacity(vector2, rect.width);
},
computeAngleAndOpacity(vector2, domWidth) {
const angle = Math.atan2(-vector2.y, vector2.x);
this.cursorAngle = CesiumMath.zeroToTwoPi(angle - CesiumMath.PI_OVER_TWO);
const distance = Cartesian2.magnitude(vector2);
const maxDistance = domWidth / 2;
const distanceF = Math.min(distance / maxDistance, 1.0);
this.easedOpacity = 0.5 * distanceF * distanceF + 0.5;
},
orbitTickFunction() {
const tempstamp = getTimestamp();
const deltaT = tempstamp - this.time;
const rate = ((this.easedOpacity - 0.5) * 2.5) / 1000;
const distance = deltaT * rate;
const angle = this.cursorAngle + CesiumMath.PI_OVER_TWO;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
this.$refs.map.viewer.camera.lookAtTransform(this.frame);
this.$refs.map.viewer.camera.rotateLeft(x);
this.$refs.map.viewer.camera.rotateUp(y);
this.$refs.map.viewer.camera.lookAtTransform(Matrix4.IDENTITY);
this.time = tempstamp; ,}}};</script>
<style lang="scss" scoped>
.home {
width: 100%;
height: 100%;
position: relative;
}
.option {
position: absolute;
top: 40px;
left: 40px;
.switch {
border: 1px solid;
background-color: transparent;
text-transform: uppercase;
font-size: 14px;
padding: 10px 40px;
font-weight: 300;
cursor: pointer;
border-radius: 4px;
}
.open {
background-color: #4cc9f0;
color: #fff;
-webkit-box-shadow: 10px 10px 99px 6px rgba(76.201.240.1);
-moz-box-shadow: 10px 10px 99px 6px rgba(76.201.240.1);
box-shadow: 10px 10px 99px 6px rgba(76.201.240.1);
&:hover {
background-color: unset;
color: #4cc9f0;
-webkit-box-shadow: 10px 10px 99px 6px rgb(21.22.22);
-moz-box-shadow: 10px 10px 99px 6px rgb(21.22.22);
box-shadow: 10px 10px 99px 6px rgb(21.22.22); }}.close {
color: #4cc9f0;
&:hover {
background-color: #4cc9f0;
color: #fff;
-webkit-box-shadow: 10px 10px 99px 6px rgba(76.201.240.1);
-moz-box-shadow: 10px 10px 99px 6px rgba(76.201.240.1);
box-shadow: 10px 10px 99px 6px rgba(76.201.240.1); }}}.label {
position: absolute;
left: 0;
right: 0;
top: 0;
margin: 20px auto 0;
font-size: 24px;
color: #fff;
}
.change-mode-btn {
position: absolute;
right: 10px;
top: 10px;
width: 150px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
border: 1px solid #fff;
color: #fff;
background-color: #26354a;
cursor: pointer;
}
.ng {
position: absolute;
bottom: 30px;
right: 30px;
width: 200px;
height: 200px;
.ng-box {
position: absolute;
width: 100%;
height: 100%;
background-image: url('~@/assets/image/ng.png');
background-size: 100% 100%;
pointer-events: none;
}
.shan {
position: absolute;
width: 35%;
height: 35%;
right: 50%;
bottom: 50%;
transform-origin: 100% 100%;
border-top-left-radius: 70%;
background-color: #4cc9f0;
pointer-events: none; }}</style>
Copy the code