Rendering woods

1. A concise explanation of the problem you’re experiencing.

I am attempting to render large swaths of woods on a hilly landscape.

If performance was not an issue, I would do this client-side and use a single GLTF model of a relatively low-poly tree and instantiate it hundreds of times by generating a large array of points that I would create elsewhere i.e. QGIS and add in randomization for heading and scale (0.8-1.2 probably). Thanks in large part to Cesium.HeightReference.RELATIVE_TO_GROUND, I can have all of the trees clamped to ground, such that I only need to worry about placement in two dimensions.

However, as you could imagine, rendering 500+ walnut trees would be enormously process-intensive, even on a relatively low-poly model. So I looked to 3DTiles.

After learning how Ion does things, I figured I could maybe upload one GLTF of a tree and then instantiate it several times client-side and translate it on the fly. I found some snippets on translating them client-side- but since there is no client-side clamping-to-ground setting, and because I haven’t figured out a proper method of determining the height that each 3DTileset would need to be for it to them to be on top of the ground, the best I can do is a 2-dimensional mass instantiation. This results in half the trees floating in the air since my terrain is so hilly.

Can anyone suggest a better way of doing this? I haven’t seen any examples of models with omni-directional billboards (where a 2-D picture of a tree is visible to the camera no matter the angle- a sort of “faux” 3D) surviving the GLTF conversion process without losing that as well as transparency (I haven’t gotten transparent textures to render transparently after being converted, even with the alpha-channel modifications that folks have suggested), otherwise I am sure that would be much less process intensive to do entirely client-side with simple models.

2. A minimal code example. If you’ve found a bug, this helps us reproduce and repair it.

An equivalent to this:

heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND

as well as ‘position’ would be an excellent add to Cesium.Cesium3DTileset options, but obviously this isn’t a trivial add.

4. The Cesium version you’re using, your operating system and browser.

1.7.1, Chrome 72

You’ve already seen this GitHub issue but just posting it here for reference on why clamping 3D Tiles to terrain at runtime hasn’t been implemented yet: https://github.com/AnalyticalGraphicsInc/cesium/issues/7139

I don’t think you’ll actually get any special performance boost from using 3D Tiles for this. What you’ll get in 3D Tiles is probably an instanced 3d model. Since you already have a single model that you want to render a lot of times, you can just directly use the ModelInstanceCollection class. Here’s a Sandcastle example using that:

It’ll work as long as they’re all the same model, and they can be in different positions.

For clamping to terrain, you could then use sampleTerrainMostDetailed:

https://cesiumjs.org/Cesium/Build/Documentation/sampleTerrainMostDetailed.html?classFilter=sampleTerr

To get the positions of the terrain everywhere and move the individual models down to that height. Let me know if that works or if you have trouble with that approach.

Cool, the instances performance is really quite good! One thing though, would I then be using Cesium.Ellipsoid.WGS84.cartographicToCartesian to convert the newly-set heights on my positions (which I used SampleTerrainMostDetailed for)? And then create the model matrix using that, and then push it to an array of “instances”?

Mine doesn’t seem to be working at the moment.

In this snippet, you can assume that the initial parameter “points” is an array of latlons, i.e. [ [ -77, 38 ] , [ -78, 37 ] ]

function testInstances(points) {

let url = ‘./models/dry_tree.glb’;

var instances = ;

var positions = ;

points.forEach(point => {

positions.push(Cesium.Cartographic.fromDegrees(point[0], point[1]));

});

var promise = Cesium.sampleTerrainMostDetailed(myTerrainProvider, positions);

Cesium.when(promise, function(updatedPositions) {

updatedPositions.forEach((position) => {

var cartesianPosition = Cesium.Ellipsoid.WGS84.cartographicToCartesian(position);

var heading = Math.random();

var scale = 1;

var modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(cartesianPosition, new Cesium.HeadingPitchRoll(heading, 0, 0));

Cesium.Matrix4.multiplyByUniformScale(modelMatrix, scale, modelMatrix);

instances.push({

modelMatrix : modelMatrix

});

});

let collection = viewer.scene.primitives.add(new Cesium.ModelInstanceCollection({

url : url,

instances : instances

}));

});

}

I take that back, it works!

I don’t know why this wouldn’t work before, but this is great. Unrelated- do you know why after a certain distance, all my GLTF models show white edges really prominently? It all looks quite snowy until you zoom in close enough.

Now I just need to add some randomization between models as well as within the points array I’m generating from the grid in QGIS! For the example you see above I just quickly generated a regular distribution:

Glad to hear it works! This looks pretty awesome. Are you doing some kind of research work on forests?

For the white edges, I’m not sure. I don’t seem to see this artifact on other models in the instanced models demo. I wonder if it’s some kind of texture mipmap/sampling issue. Might be worth swapping out the texture with one that’s a solid color to confirm this.

I’m rendering a large historical landscape. It’s a rural location, so woods are aplenty.

Here’s a snippet of my function in which I pass in:

  1. An array of the URL in string format

  2. Rotation range (my function randomly chooses a degree in which to rotate the model, this sets the range for that)

  3. Rotation offset (where the random degree is generated is added to)

  4. Scale range (my function randomly chooses a scalar in which to scale the model, this sets the range for that)

  5. Scale floor (the smallest you want the model to be)

  6. The array of points, in my case, we’re talking thousands of [lon,lat] points.

  7. Pitch (the pitch of the thousands of instances)

  8. Height correction (add or subtract from the height which is by default clamped).

function testInstances(modelPaths, rotationRange, rotationOffset, scaleRange, scaleFloor, points, pitch, heightCorrection) {

let url = modelPaths[0];

let instances = ;

let positions = ;

points.forEach(point => {

positions.push(Cesium.Cartographic.fromDegrees(point[0], point[1]));

});

let promise = Cesium.sampleTerrainMostDetailed(myTerrainProvider, positions);

Cesium.when(promise, function(updatedPositions) {

updatedPositions.forEach((position) => {

position.height += heightCorrection || 0;

let cartesianPosition = Cesium.Ellipsoid.WGS84.cartographicToCartesian(position);

const heading = Cesium.Math.toRadians(Math.floor(Math.random() * (rotationRange || 360)) + (rotationOffset || 0));

const scale = (Math.floor(Math.random() * scaleRange) + scaleFloor*10)/10;

const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(cartesianPosition, new Cesium.HeadingPitchRoll(heading, Cesium.Math.toRadians(pitch || 0), Cesium.Math.toRadians(pitch || 0)));

Cesium.Matrix4.multiplyByUniformScale(modelMatrix, scale, modelMatrix);

instances.push({

modelMatrix : modelMatrix

});

});

const woodsCollection = new Cesium.ModelInstanceCollection({

url : url,

instances : instances,

color: Cesium.Color.BLACK,

colorBlendMode: Cesium.ColorBlendMode.HIGHLIGHT,

model: {

color: Cesium.Color.BLACK,

colorBlendMode: Cesium.ColorBlendMode.HIGHLIGHT,

luminenceAtZenith: 0.1

},

clippingPlanes: clippingPlanes,

minimumPixelSize: 1.0

})

viewer.scene.primitives.add(woodsCollection);

});

}