Introduction
I ran into a problem while working on a CesiumJS application that ended up having a fairly common solution. However, before I could discover the solution, I spent hours stumbling through unsuccessful attempts.
Problem
I wanted to point the camera at an Entity
and in a specific direction using the viewFrom
property in the Entity API.
Other times when I've wanted this kind of behavior with the camera I was able to use the lookAt
function in the Camera API. The lookAt
function takes a HeadingPitchRange
object. Unfortunately, the viewFrom
property appears to only accept a Cartesian3
object as an offset from the origin Entity
.
Anyway, because I already had a function calculating the heading between two points (Entity's), that wasn't a problem. The problem was offsetting the camera from the origin Entity
using the heading.
Vector Subtraction
Searching online, and in the Cesium community pages, I found a couple of answers suggesting subtracting the target point from the origin point to give me a vector (Cartesian3
objects have an x, y, and z component) point from the origin to the target.
From there, I could scale the vector, negate the direction, and get a point along the vector on the opposite side of the origin from the target. This new point would become my "camera position".
Having P1 (my origin) and P2 (desired camera position) I could subtract P1 from P2 (P2 - P1) to get me the viewFrom
offset.
For reasons that I don't understand—or looked into—this didn't work.
Heading
The Cartesian3
coordinate system works in Cesium using an x and y component, which make up two out of the three components in a Cartesian3
object.
The heading is the "slope" of the line derived from the x and y components but represented in radians. For example, an entity with a heading of 0.785 radians equals 45 deg.
Therefore, if I have the heading of an entity I should be able to represent it using a Cartesian3
(yes, obviously I can hear the smarties saying).
The x and y components that make up the angle are the sine and cosine of the angle. They are the same x and y components we need in the Cartesian3
object used for the entities viewFrom
property.
Solution
To place the camera behind an origin point and point it toward a target:
- Create two
Cartesian3
orCartographic
points - Calculate the heading from the target point to the origin point (this is the opposite of the direction you want the camera to point)
- Calculate the sine and cosine of the heading
- Place the values into a
Cartesian3
object with your desired vertical offset (z-component)
Code
// adapted from original https://sandcastle.cesium.com/index.html?#c=zVVdb9owFP0rFi8JUmRCO1UrpdVWxtpKbHSUUVXLHkxiwJpjR7YDpFX/+2xsCAGmlZdpkSrV177nnnvuB3MkwJzgBRbgEjC8AB0sSZ7C0crme/Hq2OFMIcKw8OoXEYvYXHtxQaaEVb06SCj9H2KncCJ4+glPBcbSb54F4J3+Ow3D0AAYd4XEFKs3usP3BgCeBODMQUSs0QB9SyHjhCmNuqIMMVNEESwhShL/JWJAfxmX2sZZy7EO1mbtCFrAvTJfzCkXrQ0hc4K9u5vb4XXvezco32VkiekDecYtcBJu2XmuqBaqcwDm8fZu2N1/+kgSNdMo9uLVPaBojGmFmcJL1QKezdnbpdKfTCTW9y9g2QJhAArDa4NmvjkWisSIWoANt1HFDIf9e0ckYq8boYe2WMcIbet7nNA3g27361O31+s//g9S26z/sdR61DCQM57TxMmmOPCsmh4wYwE828SeHaSECBwbzfUs7c+RzMdKoFj5rh7rATg8dn7d8NhHYVykiOoS+JtoQRm43AmCK2QsX5ASZFkSGgrE5ESDSFh98lnnc+96ZoQpj4kq/DXFSgAHfx6WqBbC7oqBg33yy8sZVHyAEp2B9M/D7dTWnmlOFclo4VdZBTZSsJOPy/PoNaMP+jWyN47BtxwpLJg2VejbQH7ZSlUG1l53uHGhmzrBotK7FLOpafKmXpTb06J4ZrTIpenZ0jzmSunw7ua06pMajgTRP+1DuCBq9pFmM+SH8H39QDePiMxXjVMWc6drB6g42Ljr0lwXD3pkkNjuvFVu4ZE9bOq011k6evX0hhpTIzuuiL4uvJbwx6EYP7dEXdgl1Py7zqsttiuqo/bMeTrk/g7R+kUtqLWlKii+so4fSJpxoUAuqA9hQ+E0ozqabIzz+BdWMJbS4LYba6d2QuaAJJdRbeenP6qBmCIp9c0kp6uFHNWu2g39vuJGuW4mNu3rJUhRYZ7Mmlc9a4QQthv6uO+lOKdjJLYQfwM
var viewer = new Cesium.Viewer('cesiumContainer');
var origin = new Cesium.Cartesian3.fromDegrees(16, 46, 3000);
var target = new Cesium.Cartesian3.fromDegrees(16.8, 46.2, 3000);
// Origin point
var originEntity = viewer.entities.add({
position: origin,
point : {
color: Cesium.Color.LIGHTBLUE,
pixelSize: 20,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: 'Origin',
pixelOffset: { x: 0, y: 20 },
verticalOrigin: Cesium.VerticalOrigin.TOP
}
});
// Target point
viewer.entities.add({
position: target,
point : {
color: Cesium.Color.GREENYELLOW,
pixelSize: 20,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: 'Target',
pixelOffset: { x: 0, y: 20 },
verticalOrigin: Cesium.VerticalOrigin.TOP
}
});
// Heading
var cartoOrigin = Cesium.Cartographic.fromCartesian(origin);
var cartoTarget = Cesium.Cartographic.fromCartesian(target);
// https://stackoverflow.com/a/52079217
var heading = Math.atan2(
Math.sin(cartoOrigin.longitude - cartoTarget.longitude) * Math.cos(cartoOrigin.latitude),
Math.cos(cartoTarget.latitude) * Math.sin(cartoOrigin.latitude) -
Math.sin(cartoTarget.latitude) * Math.cos(cartoOrigin.latitude) * Math.cos(cartoOrigin.longitude - cartoTarget.longitude)
);
originEntity.viewFrom = new Cesium.Cartesian3(Math.sin(heading), Math.cos(heading), 0.1);
viewer.trackedEntity = originEntity;
Conclusion
The camera angle is not perfectly lined up between the origin and the target. Frankly, I don't know why. The solution worked great for my use case and seemed to align the camera precisely.
Below are two links to discussions and code I adapted for the example:
- https://community.cesium.com/t/how-to-get-heading-pitch-and-roll-from-the-two-points/7243/6
- https://stackoverflow.com/a/52079217
Thanks for reading!