Code Monkey home page Code Monkey logo

cesium-martini's Introduction

Cesium-Martini

On-the-fly meshing of raster elevation tiles for the CesiumJS virtual globe

Himalayas

This package contains a Cesium TerrainProvider that uses right-triangular irregular networks (RTIN) pioneered by Mapbox's Martini to transform Terrain-RGB elevation tiles into quantized mesh terrain, for rendering in the CesiumJS digital globe. The module provides a general technique applicable to all raster imagery (although the Terrain-RGB format is near-ideal for streaming elevation data). Fixes for performance and better control of rendering quality are in progress.

This module was created to support our geologic map visualization work at Macrostrat and as a building block for future rich geoscience visualizations.

Installation

This package is listed on NPM as @macrostrat/cesium-martini. It can be installed using the command

npm install --save @macrostrat/cesium-martini

Cesium-Martini

Development

As of version 1.3.x, cesium-martini development is tested with the Yarn package manager. Your mileage with npm may vary.

Quick start

  1. Clone the repository
  2. Run git submodule update --init to fetch the martini submodule
  3. Install dependencies with yarn install
  4. Build the package with yarn run build

After cloning this repository, you can build the module (using Rollup) with yarn run build, or build and watch for changes with yarn run watch.

Example applications

Several example applications are available in the examples/ directory and runnable from the root project. The main example is built with Vite and others are built with Webpack v5. As well as showing how to use this module, these examples contain configuration for bundling Cesium in each packaging environment.

To run an example application, add MAPBOX_API_TOKEN=<your-mapbox-token> to a .env file. in the root of this repository, and then start the appropriate example:

  • yarn run example (Vite)
  • yarn run example:webpack (Webpack)
  • yarn run example:webpack-react (Webpack + React)

Contributions in the form of bug reports and pull requests are welcome. These can be to add functionality (e.g. optional normal-map generation) or for performance. See list of known limitations below.

Motivation

The Cesium digital globe is a powerful platform for visualization of geospatial data in 3D. Cesium maintains a global elevation dataset as a prebuilt terrain mesh, which caches the computationally-intensive step of meshing height-field data into a triangle irregular network (TIN). Unfortunately, this quantized mesh format is relatively new, narrowly supported and tailored to Cesium itself. Going forward, supporting a TIN format for elevation datasets requires maintenance of significant single-purpose processing pipelines and storage resources.

Mapbox maintains a multiscale global elevation dataset in their clever terrain-RGB format, which bridges web standard file formats (PNG images) with traditional raster GIS formats for representing terrain. Rasters are the standard representation of elevation data across the geosciences, and many pipelines are available to create and modify raster images. Basically any elevation dataset can be easily rescaled to the Terrain-RGB format, but the jump from there to a "Quantized mesh" is more complicated.

Recently, the MARTINI project by Vladimir Agafonkin at Mapbox demonstrated an elegant algorithmic approach that sidesteps this issue. MARTINI meshes based on right-triangulated irregular networks (RTIN, Evans et al., 1998) and is far quicker than the traditional TIN generation techniques.

A speedy meshing algorithm allows this data-preparation step to be handled in the browser after elevation tiles are loaded. Integrating this toolchain into the Cesium digital globe enables the usage of Mapbox global data and other raster terrain layers (e.g. planetary and bathymetric data!), without adding overhead of TIN processing and storage.

Current limitations

Lack of support for overzooming

Cesium's implementations of the TerrainProvider interface are generally geared towards representing static terrain meshes. The RTIN algorithm used here can dynamically build meshes at a variety of error levels, and the input height field are data-dense and can represent extremely detailed meshes at a given zoom level. Currently, meshes are generated at levels of detail that undersample the available structure in a terrain tile — levels of detail are calibrated to what Cesium needs to render visually pleasing output at a given zoom level.

A smarter and more parsimonious solution would use much lower zoom levels for terrain than imagery, using the full resolution of the dataset in mesh construction. Done correctly, this could lead to an extremely data-efficient and adaptive terrain render, but this seems to run somewhat counter to how Cesium internally manages levels of detail. Ideally, someone familiar with the inner workings of Cesium would provide some guidance here.

Outstanding bugs and issues

  • High-resolution @2x tiles are notionally supported but not well-tested.
  • There is no formal testing framework to catch regressions.
  • TypeScript types are discarded on compilation rather than checked properly.

Prior art and relevant examples

TODO

  • Make compatible with Mapbox's new terrain-dem tileset if possible
  • Better masking of unavailable tiles
  • Bathymetry option
  • Tie to hillshade generator so the same tiles are loaded

Pull requests for any and all of these priorities are appreciated!

Changelog

[1.3.0]: September 2023

  • Add compilation to ESModules, which allows the package to be used with modern bundlers like Vite. Contributed by @fc.
  • Change latitude-based scaling factor for tile error to improve fidelity at high latitudes.
  • Remove regenerator-runtime from web-worker code, targeting modern platforms.

Development environment

We reorganized the development environment and examples through a set of interrelated changes, for a more modern overall design.

  • Switched to yarn from npm as the default package manager
  • Enabled Yarn Plug'n'Play for faster development
  • Created a Vite example application
  • Migrated Webpack examples to Webpack 5
  • Moved all examples to the examples/ directory as Yarn workspaces, enabling separation of dependencies

[1.2.0]: November 2021

  • Globe caps! (disable using the fillPoles option).
  • Some fixes for efficiency
  • Fixed small errors in tile occlusion code
  • Added a minZoom configuration option to prevent excessive loading of low-resolution tiles
  • Four (!) pull requests from @stuarta0 to improve loading of non-Mapbox tilesets

[1.1.3]: June 2021

  • Fix memory leak where ArrayBuffers were retained due to console logging.

[1.1.2]: May 2021

  • Fixed a bug with loading high-resolution tiles
  • Added a skipOddLevels option that significantly reduces the load of zooming through many terrain levels. This is enabled by default.
  • Greatly increase skirt height

[1.1.0]: May 2021

  • Fixed a bug with tile occlusion south of the equator for high-detail tiles
  • A quicker and more robust mesh-densification algorithm for low zoom levels
  • More configurability with options like detailScalar and minimumErrorLevel.
  • Updated README and examples
  • Uses web workers for rapid tile generation off the main thread

cesium-martini's People

Contributors

davenquinn avatar stuarta0 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

cesium-martini's Issues

Low zoom levels

Tiles at low zoom levels must to respond to the curvature of the Earth, while their minimal scaled topographic range often yields only two triangles covering the entire tile. For zoom levels less than 5, we currently fall back to a basic height field, but we should ideally have a method that subdivides triangles to densify the mesh to a certain threshold size.

Breaks "requestRenderMode"

Cesium's requestRenderMode is an opt-in that dramatically reduces CPU use when viewing static content. This usually works just fine, but when this TerrainProvider is enabled, it is broken for some reason, and Cesium targets 60 fps in all cases. This can be seen by toggling requestRenderMode in the examples.

Request cancellation

Requests are not appropriately canceled when executing large movements, leading to a lot of effort spent loading tiles that are no longer relevant. This may just be a networking issue, but it's quite noticeable on slower networks.

Globe caps

One major element of this terrain provider that is lacking currently is the fact that there are enormous holes at the poles. This is true of all tiled terrain providers from Cesium that use the web mercator tiling scheme. Cesium's own CesiumTerrainProvider does not suffer from this issue, because tiles extend to the poles. We need to figure out how to add caps to the globe in order to make low zooms not look jarring. One possibility would be to just extend the northernmost tile to the pole. I think imagery would stretch atop it to make the globe look relatively seamless.

image

Generating client-side hillshade using Terrain RGB tiles?

@davenquinn In your post:
https://community.cesium.com/t/hillshade-using-cesium-terrain/9590/2
you mention that you were able to generate client-side hillshade using Terrain RGB tiles.
I'm trying to do the same thing you are, but in my case I'd like to generate both a hillshare and also a high-res slope rendering from the rgb tiles. From your post I gather that you created a custom Cesium ImageryProvider and did not do it in the cesium-martini code. Is that effort something you have open sourced (I can't find it in your repo list) or plan on doing so at some point? If not, I would be so thankful if you could point me in the right direction as to what it took for you to code this?
Thank you!
-Martin

Needs upsampling support

This terrain provider should provide support for upsampling tiles to use the maximum resolution of high-scale data. This could be rendered as a standalone tile cache, using Cesium's built-in utilities, or sharing a backend with Maplibre GL JS. The latter could potentially allow efficient cooperation between this layer and hillshading utilities.

Cesium-martini has stopped working against latest cesium 1.104.0 :(

@davenquinn We've been happily updating Cesium versions, and cesium-martini happily run against all the versions until two weeks ago, when cesium 1.104.0 came out.
Seems like Cesium finally made a breaking change. The errors below come from running the commands below on a clean pull from this git repo. Do you have any immediate insights from the errors that might help address cesium breaking backwards compatibility for cesium-martini? Your library has been crucial for our development, as I'm sure it's been for many others.

Note: The example UI in this repo actually stopped working on Cesium v 0.97.0, but our application kept working with with the dist files of cesium-martini compiled with Cesium 0.97. That is, when we used your library compiled against 0.97.0 we could keep updating Cesium in our application (up to 0.103.0 - we can't move to 0.104.0 regardless how we compile cesium-martini now).

Errors when we run:
rm -rf node_modules;npm install; npm run build; npm run dev
are:

ERROR in ./node_modules/@cesium/engine/Source/Scene/Model/B3dmLoader.js 142:30
Module parse failed: Unexpected token (142:30)
File was processed with these loaders:
 * ./node_modules/source-map-loader/index.js
You may need an additional loader to handle the result of these loaders.
|   texturesLoaded: {
|     get: function () {
>       return this._gltfLoader?.texturesLoaded;
|     },
|   },
 @ ./node_modules/@cesium/engine/index.js 925:0-75 925:0-75
 @ ./node_modules/cesium/Source/Cesium.js
 @ ./examples/simple/index.ts

ERROR in ./node_modules/@cesium/engine/Source/Scene/Model/I3dmLoader.js 150:30
Module parse failed: Unexpected token (150:30)
File was processed with these loaders:
 * ./node_modules/source-map-loader/index.js
You may need an additional loader to handle the result of these loaders.
|   texturesLoaded: {
|     get: function () {
>       return this._gltfLoader?.texturesLoaded;
|     },
|   },
 @ ./node_modules/@cesium/engine/index.js 938:0-75 938:0-75
 @ ./node_modules/cesium/Source/Cesium.js
 @ ./examples/simple/index.ts

ERROR in ./node_modules/@cesium/engine/Source/DataSources/ModelVisualizer.js 161:28
Module parse failed: Unexpected token (161:28)
File was processed with these loaders:
 * ./node_modules/source-map-loader/index.js
You may need an additional loader to handle the result of these loaders.
| 
|     if (!defined(modelData) || resource.url !== modelData.url) {
>       if (defined(modelData?.modelPrimitive)) {
|         primitives.removeAndDestroy(modelData.modelPrimitive);
|         delete modelHash[entity.id];
 @ ./node_modules/@cesium/engine/index.js 97:0-85 97:0-85
 @ ./node_modules/cesium/Source/Cesium.js
 @ ./examples/simple/index.ts

ERROR in ./node_modules/@cesium/engine/Source/Scene/Cesium3DTilesVoxelProvider.js 438:37
Module parse failed: Unexpected token (438:37)
File was processed with these loaders:
 * ./node_modules/source-map-loader/index.js
You may need an additional loader to handle the result of these loaders.
| function addAttributeInfo(provider, metadata, className) {
|   const { schema, statistics } = metadata;
>   const classStatistics = statistics?.classes[className];
|   const properties = schema.classes[className].properties;
| 
 @ ./node_modules/@cesium/engine/index.js 518:0-101 518:0-101
 @ ./node_modules/cesium/Source/Cesium.js
 @ ./examples/simple/index.ts

ERROR in ./node_modules/@cesium/engine/Source/Scene/Model/MetadataPipelineStage.js 86:29
Module parse failed: Unexpected token (86:29)
File was processed with these loaders:
 * ./node_modules/source-map-loader/index.js
You may need an additional loader to handle the result of these loaders.
|   const { shaderBuilder, model } = renderResources;
|   const { structuralMetadata = {}, content } = model;
>   const statistics = content?.tileset.metadataExtension?.statistics;
| 
|   const propertyAttributesInfo = getPropertyAttributesInfo(
 @ ./node_modules/@cesium/engine/index.js 944:0-97 944:0-97
 @ ./node_modules/cesium/Source/Cesium.js
 @ ./examples/simple/index.ts

Error in using

Hello I tried to use this library but I am receiving this error in the browser. How could I fix?

index.cjs:14 Uncaught ReferenceError: __dirname is not defined
at node_modules/@macrostrat/cesium-martini/node_modules/cesium/index.cjs (index.cjs:14:3)
at __require2 (chunk-OZI5HTJH.js?v=df6a9d37:15:50)
at node_modules/@macrostrat/cesium-martini/dist/index.js (index.cjs:16:2)
at __require2 (chunk-OZI5HTJH.js?v=df6a9d37:15:50)
at terrain-provider.ts:315:52

Incompatible with vite

When trying to include @macrostrat/cesium-martini in a bare bones Vite project the following error occurs:

browser-external:path:9 Module "path" has been externalized for browser compatibility. Cannot access "path.join" in client code.
get @ browser-external:path:9
18:12:23.384 index.cjs:14 Uncaught ReferenceError: __dirname is not defined
    at node_modules/cesium/index.cjs (index.cjs:14:3)
    at __require2 (chunk-7FP5O474.js?v=6ac06d1a:10:50)
    at node_modules/@macrostrat/cesium-martini/dist/index.js (index.cjs:16:1)
    at __require2 (chunk-7FP5O474.js?v=6ac06d1a:10:50)
    at dep:@macrostrat_cesium-martini:1:16

You can see this in a very basic repro here (check your browser's dev console):
https://stackblitz.com/edit/vitejs-vite-pqnxx6?file=package.json,src%2FApp.jsx&terminal=dev

By hacking the cesium module to import the cesium module directly to bypass the error (import cesium using cesium/Build/Cesium/Cesium instead of cesium), then I see this error:

Uncaught ReferenceError: regeneratorRuntime is not defined
    at worker-farm.ts:52:3
    at worker-farm.ts:52:3
    at node_modules/@macrostrat/cesium-martini/dist/index.js (worker-farm.ts:52:3)
    at __require2 (chunk-7FP5O474.js?v=4361fb93:10:50)
    at dep:@macrostrat_cesium-martini:1:16

It seems like I can workaround that with this - yarn add -D regenerator-runtime then:

import 'regenerator-runtime/runtime';
// import cesium-martini module here

note: using the @vitejs/plugin-legacy didn't appear to work

I think this can be resolved by adding @babel/plugin-transform-runtime to your project's babelrc

Cool project!

First as a little background, I've been working on some similar terrain projects with deck.gl a 3D-capable geospatial rendering engine, which now has support for extruded terrain. I've found that using Martini to generate the mesh client-side has good, but not necessarily great, performance. So I'm testing out an alternative approach: to run Martini and generate a Quantized Mesh format in a serverless function in AWS Lambda. This should be fast and relatively cheap, maybe ~$10 per million tile requests.

Regardless, I'm tackling essentially the same problem you are: running Martini and generating a Quantized Mesh-compliant tile, just that I'm doing it in Node and you're doing it client side. My WIP Quantized Mesh encoder and the WIP lambda function to take a heightmap, run Martini on it, and then encode into quantized mesh.

A few notes:

BoundingSphere, HorizonOcclusionPoint

const tileRect = this.tilingScheme.tileXYToRectangle(x, y, z)
const tileCenter = Cartographic.toCartesian(Rectangle.center(tileRect))
const horizonOcclusionPoint = Ellipsoid.WGS84.transformPositionToScaledSpace(
tileCenter
)
let boundingSphere = new BoundingSphere(
Cartesian3.ZERO,
// radius (seems to be max height of Earth terrain?)
6379792.481506292
)

I think these might be wrong. First, the BoundingSphere is the minimal bounding sphere of the tile not of the earth. It's used to prevent rendering unnecessary tiles. Second, the HorizonOcclusionPoint is defined as a point where if the point is not visible, then no portion of the tile is visible. Here's an image from Cesium; P is the horizon occlusion point, which by definition must be "above" the tile in 3D space.

if (tileRect.width < CMath.PI_OVER_TWO + CMath.EPSILON5) {
// @ts-ignore
orientedBoundingBox = OrientedBoundingBox.fromRectangle(tileRect, minHeight, maxHeight)
// @ts-ignore
boundingSphere = BoundingSphere.fromOrientedBoundingBox(orientedBoundingBox)
}

Why inside the if block? This seems like an ideal way to create the bounding sphere for all tiles.

A couple helpful references if you haven't seen them:

Free Terrarium heightmap tiles

You're currently pulling Mapbox's Terrain RGB tiles, but you can also use AWS Terrain Tiles for free. Just note that they use a different encoding.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.