Changing the camera reference point and orientation based on a moving model.

Is it possible to base the camera's orientation and reference point solely on the position and orientation of model?

I'm trying to have the camera follow a model with a SampledPositionProperty and have all camera position and orientation modifications use the current model position as the reference frame. This is partially functional, as the reference frame can easily be set each clock tick with a lookAtTransform at the current position.

The issue I'm having is that the axis still seem to be based on the globe, where even if the orientation of the model is changed the axis are stationary based on the globe. Is there any workaround for this that I'm not seeing?

Thanks in advance,
Alfredo

Sandcastle code modified from an example that shows what I described above :

Essentially, the green polyline always points towards the north pole, the blue away from the center of the globe, and the red to the east. Ideally I'd like to have those directions change with the orientation of the plane, e.g. the red line would extend from the nose of the plane no matter the orientation with the other lines placed relative to that.

var viewer = new Cesium.Viewer('cesiumContainer', {
    terrainProviderViewModels : , //Disable terrain changing
    infoBox : false, //Disable InfoBox widget
    selectionIndicator : false //Disable selection indicator
});

//Enable lighting based on sun/moon positions
viewer.scene.globe.enableLighting = true;

//Use STK World Terrain
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
    url : '//cesiumjs.org/stk-terrain/world',
    requestWaterMask : true,
    requestVertexNormals : true
});

//Enable depth testing so things behind the terrain disappear.
viewer.scene.globe.depthTestAgainstTerrain = true;

//Set the random number seed for consistent results.
Cesium.Math.setRandomNumberSeed(3);

//Set bounds of our simulation time
var start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));
var stop = Cesium.JulianDate.addSeconds(start, 360, new Cesium.JulianDate());

//Make sure viewer is at the desired time.
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the end
viewer.clock.multiplier = 10;

//Set timeline to simulation bounds
viewer.timeline.zoomTo(start, stop);

//Generate a random circular pattern with varying heights.
function computeCirclularFlight(lon, lat, radius) {
    var property = new Cesium.SampledPositionProperty();
    for (var i = 0; i <= 360; i += 45) {
        var radians = Cesium.Math.toRadians(i);
        var time = Cesium.JulianDate.addSeconds(start, i, new Cesium.JulianDate());
        var position = Cesium.Cartesian3.fromDegrees(lon + (radius * 1.5 * Math.cos(radians)), lat + (radius * Math.sin(radians)), Cesium.Math.nextRandomNumber() * 500 + 1750);
        property.addSample(time, position);

        //Also create a point for each sample we generate.
        viewer.entities.add({
            position : position,
            point : {
                pixelSize : 8,
                color : Cesium.Color.TRANSPARENT,
                outlineColor : Cesium.Color.YELLOW,
                outlineWidth : 3
            }
        });
    }
    return property;
}

//Compute the entity position property.
var position = computeCirclularFlight(-112.110693, 36.0994841, 0.03);

//Actually create the entity
var entity = viewer.entities.add({

    //Set the entity availability to the same interval as the simulation time.
    availability : new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
        start : start,
        stop : stop
    })]),

    //Use our computed positions
    position : position,

    //Automatically compute orientation based on position movement.
    orientation : new Cesium.VelocityOrientationProperty(position),

    //Load the Cesium plane model to represent the entity
    model : {
        uri : '../../SampleData/models/CesiumAir/Cesium_Air.gltf',
        minimumPixelSize : 64
    },

    //Show the path as a pink line sampled in 1 second increments.
    path : {
        resolution : 1,
        material : new Cesium.PolylineGlowMaterialProperty({
            glowPower : 0.1,
            color : Cesium.Color.YELLOW
        }),
        width : 10
    }
});

var prim = undefined;
viewer.clock.onTick.addEventListener(function(clock) {
    viewer.scene.primitives.remove(prim);
    var referencePoint = position.getValue(clock.currentTime);
    var transform = Cesium.Transforms.eastNorthUpToFixedFrame(referencePoint, new Cesium.Ellipsoid(500, 500, 500));
    prim = viewer.scene.primitives.add(new Cesium.DebugModelMatrixPrimitive({
        modelMatrix : transform,
        length : 10000.0,
    }));
    viewer.camera.lookAtTransform(transform, viewer.camera.position.clone());
});

The lookAtTransform controls how the camera rotates around a point and does not affect the angle at which a camera is viewing the point. If you want to lock the view to a given angle, then camera.lookAt each frame is what would be used. You might also find some value in looking at the Monster Milk Truck. https://github.com/AnalyticalGraphicsInc/cesium-google-earth-examples/blob/master/demos/milktruck/milktruck.js.

I think the code below will do what you described, It locks the camera on the models orientation, essentially making it a “chase cam”. If not, can you describe the end result you’re trying to accomplish with the camera.

Replace everything in your code sample you posted after “var prim = undefined;” with this code.

var matrix3Scratch = new Cesium.Matrix3();
var positionScratch = new Cesium.Cartesian3();
var orientationScratch = new Cesium.Quaternion();

function getModelMatrix(entity, time, result) {
var position = Cesium.Property.getValueOrUndefined(entity.position, time, positionScratch);

if (!Cesium.defined(position)) {
    return undefined;
}

var orientation = Cesium.Property.getValueOrUndefined(entity.orientation, time, orientationScratch);

if (!Cesium.defined(orientation)) {
    result = Cesium.Transforms.eastNorthUpToFixedFrame(position, undefined, result);

} else {
    result = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromQuaternion(orientation, matrix3Scratch), position, result);
}

return result;

}

function getHeading(matrix, ellipsoid) {
var position = Cesium.Matrix4.getTranslation(matrix, new Cesium.Cartesian3());
var transform = Cesium.Transforms.eastNorthUpToFixedFrame(position, ellipsoid);

Cesium.Matrix3.transpose(transform, transform);

var right = Cesium.Cartesian3.fromCartesian4(Cesium.Matrix4.getColumn(matrix, 0, new Cesium.Cartesian4()));
var direction = Cesium.Cartesian3.fromCartesian4(Cesium.Matrix4.getColumn(matrix, 1, new Cesium.Cartesian4()));

Cesium.Matrix3.multiplyByVector(transform, right, right);
Cesium.Matrix3.multiplyByVector(transform, direction, direction);

var heading = Math.atan2(right.y, right.x);
return Cesium.Math.TWO_PI - Cesium.Math.zeroToTwoPi(heading);

}

var CAM_HEIGHT = 0;
var TRAILING_DISTANCE = 100;
var PITCH = Cesium.Math.toRadians(0);
var RANGE = Cesium.Cartesian3.magnitude(new Cesium.Cartesian3(TRAILING_DISTANCE, 0.0, CAM_HEIGHT));

function cameraFollow(time, entity) {
var camera = viewer.scene.camera;
var modelMatrix = getModelMatrix(entity, time, new Cesium.Matrix4());
var position = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3());
var heading = Cesium.Math.zeroToTwoPi(getHeading(modelMatrix, Cesium.Ellipsoid.WGS84) + Cesium.Math.PI_OVER_TWO);

camera.lookAt(position, new Cesium.HeadingPitchRange(heading, PITCH, RANGE));

}

viewer.clock.onTick.addEventListener(function(clock) {
cameraFollow(clock.currentTime, entity);
});

``

Thanks for the reply Mike,

Essentially the function I'm trying to build is a mixture of both your posted code and mine. I'd like to have the camera act as a chase cam, like in the milk truck example, but still have the ability to manipulate the camera position like in mine. The end result would be a camera using the model as the focal point with the ability to move the camera around this point without any of the swinging or other observed changes that come with the change of the model's orientation.

What I am trying to accomplish is basically the camera movement around the model seen in my code, with the constant orientation of the reference frame seen in yours. I've been trying to work around this for a while now, and haven't made much progress. I can do one or the other, but trying to combine the two hasn't worked thus far.

I’m asking our camera guru to take a look and see what he says. It might not be until Monday afternoon that he is able to get back to me. I hope that isn’t a problem.

No worries, I'm about to head out for the weekend as well. I'll probably work on a bit but I don't plan on dedicating much time to it until Monday. I appreciate your help.

Also, I just noticed that the example Sandcastle code that you posted still rotates with the orientation of the model, albeit less so than in what I've got working. What I'm looking for is essentially treating the model as the globe, where the camera's reference frame is constant, eg when crossing from one side of the pole to the other there isn't any swinging of the axis. I've got a few ideas and I'll continue to brainstorm a bit over the weekend, and I'll update this thread if I get anything working. Thanks again for your input.

That sounds an awful lot like what the Cesium Viewer already does for its “trackedEntity” behavior. I just updated our own app to leverage that property as needed, and it does exactly that - centers the camera around a particular Entity instead of the center of the globe. Might want to go look at how the Viewer implements that and see if some of that helps you.

That is pretty much what I've been trying to work from, just looking through the trackedEntity behavior and trying to modify it to fit my needs. The same issue is still present, though. The base coordinate frame still persists, so the "up" direction for the entity is still based on the "up" direction of the globe. This means that any time the entity crosses the poles, for example, there is a swinging sort of camera motion without any programmatic or input based camera change. For example, if you position the camera trailing the model, when it crosses one of the Earth's poles the camera will be repositioned ahead of the model. Here is a link to a Gyazo showing this.

The EntityView class, which is what trackedEntity uses, takes this into account for fast moving data and uses VVLH instead of NED for camera rotation. You can see this for yourself in the CZML example. Double click on one of the satellites and speed up time. There is no camera swing.

How are you defining your satellite position?

Thanks Matthew,

That’s odd, the behavior of the trackedEntity in the simple.czml example isn’t consistent with that of my application. I’ve tried using entityView and the same swinging motion was still present. This may have something to do with my entity then. As of now, all my data is contained in .kml/.kmz format, so I’ve been processing that information after the data source is loaded and creating a sampled position property.

After observing the czml example for a bit longer, the Molniya still shows the issue that I’m seeing, while the Geoeye and ISS behave as I would like them to.

ISS : http://gyazo.com/909d1518e7676887ce3a68abbe679af3
Molniya : http://gyazo.com/5552426bf6173dbc05887ea304105e2d

Solution for anyone trying to do the same thing in the future:

I
used Matrix4.fromTranslationQuaternionRotationScale() to generate the transform for Camera.lookAtTransform. The translation is the position of
the entity, the quaternion is the entity orientation quaternion, and I just used (1, 1, 1) for the Cartesian scale. The camera position with respect to the entity is kept track of by storing the current camera position on each tick and using that position as the lookAt offset. Sandcastle example below.

Seems fairly “hacky” but it behaves as I want it to.

var viewer = new Cesium.Viewer(‘cesiumContainer’, {
terrainProviderViewModels : , //Disable terrain changing
infoBox : false, //Disable InfoBox widget
selectionIndicator : false //Disable selection indicator
});

//Enable lighting based on sun/moon positions
viewer.scene.globe.enableLighting = true;

//Enable depth testing so things behind the terrain disappear.
viewer.scene.globe.depthTestAgainstTerrain = true;

//Set the random number seed for consistent results.
Cesium.Math.setRandomNumberSeed(3);

//Set bounds of our simulation time
var start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));
var stop = Cesium.JulianDate.addSeconds(start, 360, new Cesium.JulianDate());

//Make sure viewer is at the desired time.
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the end
viewer.clock.multiplier = 10;

//Set timeline to simulation bounds
viewer.timeline.zoomTo(start, stop);

//Generate a random circular pattern with varying heights.
function computeCirclularFlight(lon, lat, radius) {
var property = new Cesium.SampledPositionProperty();
for (var i = 0; i <= 360; i += 45) {
var radians = Cesium.Math.toRadians(i);
var time = Cesium.JulianDate.addSeconds(start, i, new Cesium.JulianDate());
var position = Cesium.Cartesian3.fromDegrees(lon + (radius * 1.5 * Math.cos(radians)), lat + (radius * Math.sin(radians)), Cesium.Math.nextRandomNumber() * 500 + 1750);
property.addSample(time, position);

    //Also create a point for each sample we generate.
    viewer.entities.add({
        position : position,
        point : {
            pixelSize : 8,
            color : Cesium.Color.TRANSPARENT,
            outlineColor : Cesium.Color.YELLOW,
            outlineWidth : 3
        }
    });
}
return property;

}

//Compute the entity position property.
var position = computeCirclularFlight(-112.110693, 36.0994841, 0.03);

//Actually create the entity
var entity = viewer.entities.add({

//Set the entity availability to the same interval as the simulation time.
availability : new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
    start : start,
    stop : stop
})]),

//Use our computed positions
position : position,

//Automatically compute orientation based on position movement.
orientation : new Cesium.VelocityOrientationProperty(position),

//Load the Cesium plane model to represent the entity
model : {
    uri : '../../SampleData/models/CesiumAir/Cesium_Air.gltf',
    minimumPixelSize : 64
},

//Show the path as a pink line sampled in 1 second increments.
path : {
    resolution : 1,
    material : new Cesium.PolylineGlowMaterialProperty({
        glowPower : 0.1,
        color : Cesium.Color.YELLOW
    }),
    width : 10
}

});

var prim = undefined;
viewer.clock.onTick.addEventListener(function(clock) {
viewer.scene.primitives.remove(prim);
var referencePoint = position.getValue(clock.currentTime);
var transform = Cesium.Matrix4.fromTranslationQuaternionRotationScale(
entity.position.getValue(clock.currentTime),
entity.orientation.getValue(clock.currentTime),
new Cesium.Cartesian3(1, 1, 1),
new Cesium.Matrix4()
);
var cameraPos = Cesium.Cartesian3.clone(viewer.camera.position);
viewer.camera.lookAtTransform(transform, cameraPos);
prim = viewer.scene.primitives.add(new Cesium.DebugModelMatrixPrimitive({
modelMatrix : transform,
length : 10000.0,
}));
viewer.camera.lookAtTransform(transform, viewer.camera.position.clone());
});

Molniya actually triggers a third behavior mode in order to keep the earth oriented north in distance. I’m not sure why the correct camera mode isn’t being triggered automatically for your object, if you’re using a sampled position property everything should work. Our heuristic in EntityView.js is definitely not perfect, so it’s possible the velocity of your object is what’s causing the issue.

If you have a workaround you’re happy with, I would continue to use it. If you can share the data you are using for SampledPositionProperty, we could further debug the problem on our end and figure out why it’s not working for you.