Search code examples
javascriptcesiumjs

Change length of cylinder or extrudedHeight of circle


I'm trying to change the length of a cylinder or the extrudedHeight of a circle when it has been added to the primitives and is shown in the cesium widget/viewer. For example this cylinder:

var length = 100;

var cylinderGeometry = new Cesium.CylinderGeometry({
    length : length,
    topRadius : cylinderradius,
    bottomRadius : cylinderradius,
    slices: cylinderslices,
    vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
});
var cylinder = new Cesium.GeometryInstance({
    geometry: cylinderGeometry,
    modelMatrix: Cesium.Matrix4.multiplyByTranslation(  
        Cesium.Transforms.eastNorthUpToFixedFrame(ellipsoid.cartographicToCartesian(Cesium.Cartographic.fromDegrees(lon, lat))), 
        new Cesium.Cartesian3(0.0, 0.0, length * 0.5)),
    attributes: {
        color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED)
    },
    id: "Cylinder1"
});
var primitive = new Cesium.Primitive({
    geometryInstances : cylinder ,
    appearance : new Cesium.PerInstanceColorAppearance({
        closed : false,
        translucent: true,
        flat: false,
        faceForward: true
    }),
    allow3DOnly: true,
    vertexCacheOptimize: true,
    allowPicking: true,
    releaseGeometryInstances: false
});

widget.scene.primitives.add(primitive);

Because it's added to the primitives array it will be shown in the widget, but after 2 seconds for example I get a notification that the length should be halved (that means set to 50). Is there any way to do this? Simply changing it in cylinderGeometry doesn't seem to do the job.

I kind of have it working by creating a new cylinder with the new height, adding it and removing the old one. This however tends to flicker the cylinder (it's gone for a fraction of a second) before the new one is shown. I fixed this problem by removing the old instance after a set time after the new one is added. This whole solution isn't very elegant and doesn't work very well on devices with a small amount of computing power, hence my search for a better solution.

I don't care if this is achieved using cylinders or extruded circles. If you need any more information don't hesitate to ask in the comments below the question.

EDIT

I implemented the second solution Matthew suggested but after a while of it running perfectly the cylinders stop changing height (which didn't occur when I used my solution. The callback in the interval does get called. Here is some code showing what my new solution is (not working):

primitives.add(prim);

window.nodeValuesInterval = setInterval(function () {
    if (prim._state == Cesium.PrimitiveState.COMPLETE) {
        clearInterval(window.nodeValuesInterval);
        clearTimeout(window.nodeValuesTimeout);
        primitives.remove(primitiveObjects.value);
        primitiveObjects.value = prim;
    }
}, cylindervalueinterval);

window.nodeValuesTimeout = setTimeout(function () {
    clearInterval(window.nodeValuesInterval);
    primitives.remove(primitiveObjects.value);
    primitiveObjects.value = prim;
}, cylindervaluedelay);

Solution

  • Cesium's geometry is currently optimized for static data. Some attributes, such as visibility, color, and material can be changed on the fly, but items that actually modify the geometry (like cylinder height) require you to remove the primitive and recompute the geometry. The flickering your seeing is the result of asynchronous primitive creation being on by default. There are two ways to do want you want.

    1. Disable asynchronous primitive create by passing [options.asynchronous: false to the Primitive constructor. This means that when you add a new primitive, Cesium will not render until it is ready. For one or two objects, you won't notice anything. For lots of objects it will lock up the browser until everything is ready. This does guarantee that you can remove old/add new primitives without any flicker.

    2. The second option is to add your new primitive (without removing the old one) and then every frame, check the _state property of your new Primitive (I thought this was part of the public API but apparently it's not). When the _state is equal to Cesium.PrimitiveState.COMPLETE you can safely remove the old primitive and your guaranteed the new one will render (hence no flicker).

    I think we have a bug/feature request to expose the state variable publicly or otherwise notify when the Primitive is ready; but using _state should be fine for the forseeable future. I'll update this issue if we add an official way sometime soon.

    Hope that helps.

    EDIT: Since more help was requested; here's a complete example. You can copy and paste the below code into Sandcastle using this link.

    Basically it uses the scene.preRender event instead of a timeout (preRender is almost always the better answer here). Also, if you receive a new update before the old one is finished processing, it's important to remove that one before computing the new one. Let me know if you are still having problems.

    require(['Cesium'], function(Cesium) {
        "use strict";
    
        var widget = new Cesium.CesiumWidget('cesiumContainer');
    
        var ellipsoid = widget.scene.globe.ellipsoid;
        var lon = 0;
        var lat = 0;
        var cylinderradius = 30000;
        var length = 10000000;
        var cylinderslices = 32;
    
        var newPrimitive;
        var currentPrimitive;
    
        //This function creates a new cylinder that is half the length of the old one.
        function decreaseLength() {
            //If there's a pending primitive already, remove it.
            if(Cesium.defined(newPrimitive)){
                widget.scene.primitives.remove(newPrimitive);
            }
    
            length /= 2;
    
            var cylinderGeometry = new Cesium.CylinderGeometry({
                length : length,
                topRadius : cylinderradius,
                bottomRadius : cylinderradius,
                slices: cylinderslices,
                vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
            });
    
            var cylinder = new Cesium.GeometryInstance({
                geometry: cylinderGeometry,
                modelMatrix: Cesium.Matrix4.multiplyByTranslation(Cesium.Transforms.eastNorthUpToFixedFrame(ellipsoid.cartographicToCartesian(Cesium.Cartographic.fromDegrees(lon, lat))),
                                                                  new Cesium.Cartesian3(0.0, 0.0, length * 0.5)),
                attributes: {
                    color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED)
                },
                id: "Cylinder1"
            });
    
            newPrimitive = new Cesium.Primitive({
                geometryInstances : cylinder ,
                appearance : new Cesium.PerInstanceColorAppearance({
                    closed : false,
                    translucent: true,
                    flat: false,
                    faceForward: true
                }),
                allow3DOnly: true,
                vertexCacheOptimize: true,
                allowPicking: true,
                releaseGeometryInstances: false
            });
    
            //We add the new cylinder immediately, but don't remove the old one yet.
            widget.scene.primitives.add(newPrimitive);
        }
    
        //Create the initial cylinder.
        decreaseLength();
    
        //Subscribe to the preRender event so we can check the primitive every frame.
        widget.scene.preRender.addEventListener(function(scene, time) {
            //Remove the old cylinder once the new one is ready.
            if(Cesium.defined(newPrimitive) && newPrimitive._state === Cesium.PrimitiveState.COMPLETE){
                if(Cesium.defined(currentPrimitive)){
                    widget.scene.primitives.remove(currentPrimitive);
                }
                currentPrimitive = newPrimitive;
                newPrimitive = undefined;
            }
        });
    
        Sandcastle.addToolbarButton('Decrease Length', decreaseLength);
        Sandcastle.finishedLoading();
    });