Merge branch 'main' into billboard-terrain-clip-fix

This commit is contained in:
Gabby Getz 2025-10-07 11:48:22 -04:00 committed by GitHub
commit 9c9705c794
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
100 changed files with 4754 additions and 1352 deletions

View File

@ -1,8 +1,8 @@
name: prod
on:
on:
push:
branches:
- 'cesium.com'
branches:
- "cesium.com"
jobs:
lint:
runs-on: ubuntu-latest
@ -11,7 +11,7 @@ jobs:
- name: install node 22
uses: actions/setup-node@v5
with:
node-version: '22'
node-version: "22"
- name: npm install
run: npm install
- name: lint *.js
@ -36,13 +36,17 @@ jobs:
- name: install node 22
uses: actions/setup-node@v5
with:
node-version: '22'
node-version: "22"
- name: npm install
run: npm install
- name: build website release
run: npm run website-release
- name: build apps
run: npm run build-apps
- name: build types
run: npm run build-ts
- name: build prod sandcastle
run: npm run build-prod -w packages/sandcastle -- -l warn
- name: deploy to cesium.com
if: ${{ env.AWS_ACCESS_KEY_ID != '' }}
run: |
@ -51,4 +55,4 @@ jobs:
aws s3 sync Build/release/ s3://cesium-website/cesiumjs/releases/$(cat package.json | jq -r '.version' | sed 's/\.0$//')/ --cache-control "public, max-age=1800" --delete
aws s3 sync Build/Documentation/ s3://cesium-website/cesiumjs/ref-doc/ --cache-control "public, max-age=1800" --delete
aws s3 sync Build/CesiumViewer/ s3://cesium-website/cesiumjs/cesium-viewer/ --cache-control "public, max-age=1800" --delete
aws s3 sync Build/Sandcastle/ s3://cesium-sandcastle-website/ --cache-control "public, max-age=1800" --delete
aws s3 sync Build/Sandcastle2/ s3://cesium-sandcastle-website/ --cache-control "public, max-age=1800" --delete

View File

@ -0,0 +1,94 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta
name="description"
content="Imagery tiles from Google Maps with additional parameters to create overlays and custom styles."
/>
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
const assetId = 3830184;
const base = Cesium.ImageryLayer.fromProviderAsync(
Cesium.Google2DImageryProvider.fromIonAssetId({
assetId,
mapType: "satellite",
}),
);
const overlay = Cesium.ImageryLayer.fromProviderAsync(
Cesium.Google2DImageryProvider.fromIonAssetId({
assetId,
overlayLayerType: "layerRoadmap",
styles: [
{
stylers: [{ hue: "#00ffe6" }, { saturation: -20 }],
},
{
featureType: "road",
elementType: "geometry",
stylers: [{ lightness: 100 }, { visibility: "simplified" }],
},
],
}),
);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
viewer.imageryLayers.add(base);
viewer.imageryLayers.add(overlay);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
});
//Sandcastle_End
Sandcastle.finishedLoading();
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,69 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta name="description" content="Global imagery data from Google Maps." />
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
const assetId = 3830184;
const google = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(assetId),
);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
viewer.imageryLayers.add(google);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
}); //Sandcastle_End
Sandcastle.finishedLoading();
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,98 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta name="description" content="Global imagery assets available from Cesium ion." />
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
const menuOptions = [];
const dropdownOptions = [
{ label: "Google Maps 2D Contour", assetId: 3830186 },
{ label: "Google Maps 2D Labels Only", assetId: 3830185 },
{ label: "Google Maps 2D Roadmap", assetId: 3830184 },
{ label: "Google Maps 2D Satellite", assetId: 3830182 },
{ label: "Google Maps 2D Satellite with Labels", assetId: 3830183 },
{ label: "Bing Maps Aerial", assetId: 2 },
{ label: "Bing Maps Aerial with Labels", assetId: 3 },
{ label: "Bing Maps Road", assetId: 4 },
{ label: "Bing Maps Labels Only", assetId: 2411391 },
{ label: "Sentinel-2", assetId: 3954 },
];
function showLayer(assetId) {
viewer.imageryLayers.removeAll(true);
const layer = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(assetId),
);
viewer.imageryLayers.add(layer);
}
dropdownOptions.forEach((opt) => {
const option = {
text: opt.label,
onselect: function () {
showLayer(opt.assetId);
},
};
menuOptions.push(option);
});
Sandcastle.addToolbarMenu(menuOptions);
showLayer(3830186);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
}); //Sandcastle_End
Sandcastle.finishedLoading();
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -80,15 +80,17 @@
this.names = ["color"];
this.types = [Cesium.MetadataType.VEC4];
this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
this._levelCount = 3;
this.availableLevels = 3;
this.globalTransform = globalTransform;
}
ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) {
const { tileLevel, tileX, tileY, tileZ } = options;
if (tileLevel >= this._levelCount) {
return Promise.reject(`No tiles available beyond level ${this._levelCount}`);
if (tileLevel >= this.availableLevels) {
return Promise.reject(
`No tiles available beyond level ${this.availableLevels - 1}`,
);
}
const dimensions = this.dimensions;
@ -174,6 +176,7 @@
customShader: customShader,
});
voxelPrimitive.nearestSampling = true;
voxelPrimitive.stepSize = 0.7;
viewer.scene.primitives.add(voxelPrimitive);
camera.flyToBoundingSphere(voxelPrimitive.boundingSphere, {

View File

@ -123,14 +123,14 @@
this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
this.globalTransform = globalTransform;
this._levelCount = 2;
this._allVoxelData = new Array(this._levelCount);
this.availableLevels = 2;
this._allVoxelData = new Array(this.availableLevels);
const allVoxelData = this._allVoxelData;
const channelCount = Cesium.MetadataType.getComponentCount(this.types[0]);
const { dimensions } = this;
for (let level = 0; level < this._levelCount; level++) {
for (let level = 0; level < this.availableLevels; level++) {
const dimAtLevel = Math.pow(2, level);
const voxelCountX = dimensions.x * dimAtLevel;
const voxelCountY = dimensions.y * dimAtLevel;
@ -158,9 +158,9 @@
ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) {
const { tileLevel, tileX, tileY, tileZ } = options;
if (tileLevel >= this._levelCount) {
if (tileLevel >= this.availableLevels) {
return Promise.reject(
`No tiles available beyond level ${this._levelCount - 1}`,
`No tiles available beyond level ${this.availableLevels - 1}`,
);
}

View File

@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta name="description" content="Global imagery data from Azure Maps." />
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
Cesium.Ion.defaultServer = "https://api.ion-staging.cesium.com";
Cesium.Ion.defaultAccessToken = "";
const assetId = 1683;
const azure = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(assetId),
);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
viewer.imageryLayers.add(azure);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
}); //Sandcastle_End
Sandcastle.finishedLoading();
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -81,7 +81,7 @@
data-dojo-type="dijit.layout.ContentPane"
data-dojo-props="region: 'top'"
>
<a href="https://dev-sandcastle.cesium.com"
<a href="https://sandcastle.cesium.com"
>Try the new version of Sandcastle today!</a
>
</div>

View File

@ -2,20 +2,33 @@
## 1.134 - 2025-10-01
- [Sandcastle](https://sandcastle.cesium.com/) has been updated at `https://sandcastle.cesium.com`! The [legacy Sandcastle app](https://cesium.com/downloads/cesiumjs/releases/1.134/Apps/Sandcastle/index.html) will remain available through November 3, 2025.
### @cesium/engine
#### Fixes :wrench:
#### Breaking Changes :mega:
- Materials loaded from type now respect submaterials present in the referenced material type. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
- Reverts `createImageBitmap` options update to continue support for older browsers [#12846](https://github.com/CesiumGS/cesium/issues/12846)
- Fix flickering artifact in Gaussian splat models caused by incorrect sorting results. [#12662](https://github.com/CesiumGS/cesium/issues/12662)
- Improved performance and reduced memory usage of `Event` class. [#12896](https://github.com/CesiumGS/cesium/pull/12896)
- Fixes vertical misalignment of glyphs in labels with small fonts [#8474](https://github.com/CesiumGS/cesium/issues/8474)
- Prevent runtime errors for certain forms of invalid PNTS files [#12872](https://github.com/CesiumGS/cesium/issues/12872)
- Voxel rendering now requires a WebGL2 context, which is [enabled by default since 1.101](https://github.com/CesiumGS/cesium/pull/10894). Make sure the `requestWebGl1` flag in `contextOptions` is NOT set to true.
- The `defaultValue` function has been removed. Instead, use the [nullish coalescing (`??`)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) operator. See the [Coding Guide](https://github.com/CesiumGS/cesium/tree/main/Documentation/Contributors/CodingGuide#default-parameter-values) for usage information and examples.
- `defaultValue.EMPTY_OBJECT` has been removed. Instead, use `Frozen.EMPTY_OBJECT`. See the [Coding Guide](https://github.com/CesiumGS/cesium/tree/main/Documentation/Contributors/CodingGuide#default-parameter-values) for usage information and examples.
#### Additions :tada:
- Adds an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
- Added Google2DImageryProvider to load imagery from [Google Maps](https://developers.google.com/maps/documentation/tile/2d-tiles-overview) [#12913](https://github.com/CesiumGS/cesium/pull/12913)
- Added an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
#### Fixes :wrench:
- Fixed vertical misalignment of glyphs in labels with small fonts [#8474](https://github.com/CesiumGS/cesium/issues/8474)
- Converted voxel raymarching to eye coordinates to fix precision issues in large datasets. [#12061](https://github.com/CesiumGS/cesium/issues/12061)
- Fixed flickering artifact in Gaussian splat models caused by incorrect sorting results. [#12662](https://github.com/CesiumGS/cesium/issues/12662)
- Fixed issue where multiple instances of a Gaussian splat tileset would transform tile positions incorrectly and render out of position. [#12795](https://github.com/CesiumGS/cesium/issues/12795)
- Fixed rendering for geometry entities when `requestRenderMode` is enabled. [#12841](https://github.com/CesiumGS/cesium/pull/12841)
- Improved performance and reduced memory usage of `Event` class. [#12896](https://github.com/CesiumGS/cesium/pull/12896)
- Improved performance of clamped labels. [#12905](https://github.com/CesiumGS/cesium/pull/12905)
- Materials loaded from type now respect submaterials present in the referenced material type. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
- Prevent runtime errors for certain forms of invalid PNTS files [#12872](https://github.com/CesiumGS/cesium/issues/12872)
- Revert `createImageBitmap` options update to continue support for older browsers [#12846](https://github.com/CesiumGS/cesium/issues/12846)
## 1.133.1 - 2025-09-08

View File

@ -432,3 +432,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
- [Pamela Augustine](https://github.com/pamelaAugustine)
- [宋时旺](https://github.com/BlockCnFuture)
- [Marco Zhan](https://github.com/marcoYxz)
- [Mikhail Porotkin](https://github.com/porotkin)

View File

@ -12,7 +12,7 @@
"license": [
"BSD-3-Clause"
],
"version": "2.7.73",
"version": "2.8.4",
"url": "https://www.npmjs.com/package/@zip.js/zip.js"
},
{
@ -44,7 +44,7 @@
"license": [
"Apache-2.0"
],
"version": "3.2.6",
"version": "3.2.7",
"url": "https://www.npmjs.com/package/dompurify",
"notes": "dompurify is available as both MPL-2.0 OR Apache-2.0"
},

View File

@ -447,6 +447,13 @@ export const websiteRelease = gulp.series(
node: false,
});
},
function () {
return buildCesium({
minify: true,
removePragmas: true,
node: false,
});
},
combineForSandcastle,
buildDocs,
);
@ -663,6 +670,7 @@ export const makeZip = gulp.series(release, async function createZipFile() {
"!**/*.gitignore",
"!Specs/e2e/*-snapshots/**",
"!Apps/Sandcastle/gallery/development/**",
"!Apps/Sandcastle2/**",
],
{
encoding: false,

View File

@ -32,12 +32,11 @@
<a href="Apps/HelloWorld.html">Hello World</a>
</li>
<li>
<a href="Apps/Sandcastle/index.html">Sandcastle</a>
<a href="Apps/Sandcastle2/index.html">Sandcastle</a>
<ul>
<li>
<a href="Build/Apps/Sandcastle/index.html">Built Sandcastle</a>
<a href="Apps/Sandcastle/index.html">Legacy Sandcastle</a>
</li>
<li><a href="Apps/Sandcastle2/index.html">Sandcastle v2</a></li>
</ul>
</li>
<li>

View File

@ -1,6 +1,6 @@
{
"name": "cesium",
"version": "1.133.1",
"version": "1.134.0",
"description": "CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.",
"homepage": "http://cesium.com/cesiumjs/",
"license": "Apache-2.0",
@ -51,8 +51,8 @@
"./Specs/**/*"
],
"dependencies": {
"@cesium/engine": "^20.0.1",
"@cesium/widgets": "^13.1.1"
"@cesium/engine": "^21.0.0",
"@cesium/widgets": "^13.2.0"
},
"devDependencies": {
"@cesium/eslint-config": "^12.0.0",

View File

@ -25,10 +25,10 @@ GoogleMaps.defaultApiKey = undefined;
* Gets or sets the default Google Map Tiles API endpoint.
*
* @type {string|Resource}
* @default https://tile.googleapis.com/v1/
* @default https://tile.googleapis.com/
*/
GoogleMaps.mapTilesApiEndpoint = new Resource({
url: "https://tile.googleapis.com/v1/",
url: "https://tile.googleapis.com/",
});
GoogleMaps.getDefaultCredit = function () {

View File

@ -4,7 +4,7 @@ import Resource from "./Resource.js";
let defaultTokenCredit;
const defaultAccessToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NTEwZTU2Yi0wOGEyLTQyZjgtOTJjNi04Mzc2NGRlNzA4NTkiLCJpZCI6MjU5LCJpYXQiOjE3NTY4NDExOTJ9._Y3MIsYgGKTVTpkEpKPNT0cQSa_hUocY0DdH7h0U-xM";
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjNjI5ZTViNy0wY2FhLTQ0ZDUtYTIzMi0wMWEyMzZkYWYwYWYiLCJpZCI6MjU5LCJpYXQiOjE3NTkzNDcyNDZ9.xyOPig1igKFQvOTaXfTE0KQ7dU7jyn_c3OQPaQ1hEiI";
/**
* Default settings for accessing the Cesium ion API.
*

View File

@ -39,6 +39,12 @@ function IonResource(endpoint, endpointResource) {
retryAttempts: 1,
retryCallback: retryCallback,
};
} else if (["GOOGLE_2D_MAPS", "AZURE_MAPS"].includes(externalType)) {
options = {
url: endpoint.options.url,
retryAttempts: 1,
retryCallback: retryCallback,
};
} else if (
externalType === "3DTILES" ||
externalType === "STK_TERRAIN_SERVER"
@ -84,7 +90,7 @@ if (defined(Object.create)) {
* @param {object} [options] An object with the following properties:
* @param {string} [options.accessToken=Ion.defaultAccessToken] The access token to use.
* @param {string|Resource} [options.server=Ion.defaultServer] The resource to the Cesium ion API server.
* @returns {Promise<IonResource>} A Promise to am instance representing the Cesium ion Asset.
* @returns {Promise<IonResource>} A Promise to an instance representing the Cesium ion Asset.
*
* @example
* // Load a Cesium3DTileset with asset ID of 124624234
@ -207,7 +213,7 @@ IonResource.prototype._makeRequest = function (options) {
/**
* @private
*/
**/
IonResource._createEndpointResource = function (assetId, options) {
//>>includeStart('debug', pragmas.debug);
Check.defined("assetId", assetId);
@ -226,6 +232,13 @@ IonResource._createEndpointResource = function (assetId, options) {
resourceOptions.queryParameters = { access_token: accessToken };
}
if (defined(options.queryParameters)) {
resourceOptions.queryParameters = {
...resourceOptions.queryParameters,
...options.queryParameters,
};
}
addClientHeaders(resourceOptions);
return server.getDerivedResource(resourceOptions);
@ -267,9 +280,21 @@ function retryCallback(that, error) {
ionRoot._pendingPromise = endpointResource
.fetchJson()
.then(function (newEndpoint) {
//Set the token for root resource so new derived resources automatically pick it up
// Set the token for root resource so new derived resources automatically pick it up
ionRoot._ionEndpoint = newEndpoint;
return newEndpoint;
// Reset the session token for Google 2D imagery
if (newEndpoint.externalType === "GOOGLE_2D_MAPS") {
ionRoot.setQueryParameters({
session: newEndpoint.options.session,
key: newEndpoint.options.key,
});
}
if (newEndpoint.externalType === "AZURE_MAPS") {
ionRoot.setQueryParameters({
"subscription-key": newEndpoint.options["subscription-key"],
});
}
return ionRoot._ionEndpoint;
})
.finally(function (newEndpoint) {
// Pass or fail, we're done with this promise, the next failure should use a new one.
@ -284,4 +309,5 @@ function retryCallback(that, error) {
return true;
});
}
export default IonResource;

View File

@ -1,48 +0,0 @@
import deprecationWarning from "./deprecationWarning.js";
import Frozen from "./Frozen.js";
/**
* Returns the first parameter if not undefined, otherwise the second parameter.
* Useful for setting a default value for a parameter.
*
* @function
*
* @param {*} a
* @param {*} b
* @returns {*} Returns the first parameter if not undefined, otherwise the second parameter.
*
* @example
* param = Cesium.defaultValue(param, 'default');
* @deprecated This function is deprecated and will be removed in Cesium 1.134. See <a href="https://github.com/CesiumGS/cesium/issues/11674">Issue 11674</a>.
* Use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing">Nullish coalescing operator</a> instead
*/
function defaultValue(a, b) {
deprecationWarning(
"defaultValue",
`defaultValue has been deprecated and will be removed in Cesium 1.134. Use the nullish coalescing operator instead: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing`,
);
if (a !== undefined && a !== null) {
return a;
}
return b;
}
/**
* A frozen empty object that can be used as the default value for options passed as
* an object literal.
* @type {object}
* @memberof defaultValue
* @deprecated This property has been deprecated and will be removed in Cesium 1.134. See <a href="https://github.com/CesiumGS/cesium/issues/11326">Issue 11326</a>.
* Use `Frozen.EMPTY_OBJECT` instead
*/
Object.defineProperty(defaultValue, "EMPTY_OBJECT", {
get: function () {
deprecationWarning(
"defaultValue.EMPTY_OBJECT",
"defaultValue.EMPTY_OBJECT has been deprecated and will be removed in Cesium 1.134. Use Frozen.EMPTY_OBJECT instead",
);
return Frozen.EMPTY_OBJECT;
},
});
export default defaultValue;

View File

@ -4,7 +4,7 @@ import Resource from "../Core/Resource.js";
let defaultTokenCredit;
const defaultAccessToken =
"AAPTxy8BH1VEsoebNVZXo8HurEOF051kAEKlhkOhBEc9BmRXcPUeuOd6ZTh-Z86vRv6teqZdBYLUWIxB6HTYajPhFtAlXhMdqTg07UwlBQ59KbiReC5wCeOF4LzLTW4oaSPp-t4pEcJHnFePlnFcIPXM7R565gTy6f6fMuZAXPvUEuZSMf8dvTqHyV9-Zd9eZGWokDzKC4uEtHME7OeAk3oyZ7vzkjEo8fpuIfFw0sH8vQU.AT1_PLgWE6Lc";
"AAPTxy8BH1VEsoebNVZXo8HurEOF051kAEKlhkOhBEc9BmQqEeLjCLZBBnNm_y_K-Cs-tuPcsgUmlxDWvPdTmGIEhrihfZMTG77j2nmCeqeYCPAenHGG1YfJqJaeRLuy5YjARJcFLP2I_judomiDS-8A_LVZxWUObwIQNE5wcsQKxJl7RHiKm-81XRDuUJZXGqy9B4PwPxWkS-N9PZ7NmTT-sP6BOGn5ouiAN8dkxwNx3tA.AT1_BU0Co4D8";
/**
* Default options for accessing the ArcGIS image tile service.
*

View File

@ -0,0 +1,308 @@
import Check from "../Core/Check.js";
import Credit from "../Core/Credit.js";
import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
import IonResource from "../Core/IonResource.js";
import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
const trailingSlashRegex = /\/$/;
/**
* @typedef {object} Azure2DImageryProvider.ConstructorOptions
*
* Initialization options for the Azure2DImageryProvider constructor
*
* @property {object} options Object with the following properties:
* @property {string} [options.url="https://atlas.microsoft.com/"] The Azure server url.
* @property {string} [options.tilesetId="microsoft.imagery"] The Azure tileset ID. Valid options are {@link microsoft.imagery}, {@link microsoft.base.road}, and {@link microsoft.base.labels.road}
* @property {string} options.subscriptionKey The public subscription key for the imagery.
* @property {Ellipsoid} [options.ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @property {number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
* this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely
* to result in rendering problems.
* @property {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider.
* @property {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image.
*/
/**
* Provides 2D image tiles from Azure.
*
* @alias Azure2DImageryProvider
* @constructor
* @private
* @param {Azure2DImageryProvider.ConstructorOptions} options Object describing initialization options
*
* @example
* // Azure 2D imagery provider
* const azureImageryProvider = new Cesium.Azure2DImageryProvider({
* subscriptionKey: "subscription-key",
* tilesetId: "microsoft.base.road"
* });
*/
function Azure2DImageryProvider(options) {
options = options ?? {};
options.maximumLevel = options.maximumLevel ?? 22;
options.minimumLevel = options.minimumLevel ?? 0;
const subscriptionKey =
options.subscriptionKey ?? options["subscription-key"];
//>>includeStart('debug', pragmas.debug);
Check.defined("options.tilesetId", options.tilesetId);
Check.defined("options.subscriptionKey", subscriptionKey);
//>>includeEnd('debug');
const resource =
options.url instanceof IonResource
? options.url
: Resource.createIfNeeded(options.url ?? "https://atlas.microsoft.com/");
let templateUrl = resource.getUrlComponent();
if (!trailingSlashRegex.test(templateUrl)) {
templateUrl += "/";
}
templateUrl += `map/tile`;
resource.url = templateUrl;
resource.setQueryParameters({
"api-version": "2024-04-01",
tilesetId: options.tilesetId,
zoom: `{z}`,
x: `{x}`,
y: `{y}`,
"subscription-key": subscriptionKey,
});
let credit;
if (defined(options.credit)) {
credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
}
const provider = new UrlTemplateImageryProvider({
...options,
url: resource,
credit: credit,
});
provider._resource = resource;
this._imageryProvider = provider;
// This will be defined for ion resources
this._tileCredits = resource.credits;
}
Object.defineProperties(Azure2DImageryProvider.prototype, {
/**
* Gets the URL of the Azure 2D Imagery server.
* @memberof Azure2DImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._imageryProvider.url;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by the instance.
* @memberof Azure2DImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._imageryProvider.rectangle;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof Azure2DImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._imageryProvider.tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof Azure2DImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._imageryProvider.tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof Azure2DImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._imageryProvider.maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested. Generally,
* a minimum level should only be used when the rectangle of the imagery is small
* enough that the number of tiles at the minimum level is small. An imagery
* provider with more than a few tiles at the minimum level will lead to
* rendering problems.
* @memberof Azure2DImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return this._imageryProvider.minimumLevel;
},
},
/**
* Gets the tiling scheme used by the provider.
* @memberof Azure2DImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._imageryProvider.tilingScheme;
},
},
/**
* Gets the tile discard policy. If not undefined, the discard policy is responsible
* for filtering out "missing" tiles via its shouldDiscardImage function. If this function
* returns undefined, no tiles are filtered.
* @memberof Azure2DImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._imageryProvider.tileDiscardPolicy;
},
},
/**
* Gets an event that is raised when the imagery provider encounters an asynchronous error.. By subscribing
* to the event, you will be notified of the error and can potentially recover from it. Event listeners
* are passed an instance of {@link TileProviderError}.
* @memberof Azure2DImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._imageryProvider.errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof Azure2DImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._imageryProvider.credit;
},
},
/**
* Gets the proxy used by this provider.
* @memberof Azure2DImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._imageryProvider.proxy;
},
},
/**
* Gets a value indicating whether or not the images provided by this imagery provider
* include an alpha channel. If this property is false, an alpha channel, if present, will
* be ignored. If this property is true, any images without an alpha channel will be treated
* as if their alpha is 1.0 everywhere. When this property is false, memory usage
* and texture upload time are reduced.
* @memberof Azure2DImageryProvider.prototype
* @type {boolean}
* @readonly
*/
hasAlphaChannel: {
get: function () {
return this._imageryProvider.hasAlphaChannel;
},
},
});
/**
* Gets the credits to be displayed when a given tile is displayed.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level;
* @returns {Credit[]|undefined} The credits to be displayed when the tile is displayed.
*/
Azure2DImageryProvider.prototype.getTileCredits = function (x, y, level) {
return this._imageryProvider.getTileCredits(x, y, level);
};
/**
* Requests the image for a given tile.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {Request} [request] The request object. Intended for internal use only.
* @returns {Promise<ImageryTypes>|undefined} A promise for the image that will resolve when the image is available, or
* undefined if there are too many active requests to the server, and the request should be retried later.
*/
Azure2DImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
return this._imageryProvider.requestImage(x, y, level, request);
};
/**
* Picking features is not currently supported by this imagery provider, so this function simply returns
* undefined.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {number} longitude The longitude at which to pick features.
* @param {number} latitude The latitude at which to pick features.
* @return {undefined} Undefined since picking is not supported.
*/
Azure2DImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
return undefined;
};
// Exposed for tests
export default Azure2DImageryProvider;

View File

@ -227,6 +227,9 @@ function Billboard(options, billboardCollection) {
this._updateClamping();
this._splitDirection = options.splitDirection ?? SplitDirection.NONE;
// Primarily used by labels to indicate that the position is derived from the parent.
// and expensive operations like clamping can be skipped.
this._positionFromParent = false;
}
const SHOW_INDEX = (Billboard.SHOW_INDEX = 0);
@ -1147,6 +1150,7 @@ Billboard._updateClamping = function (collection, owner) {
if (
owner._heightReference === HeightReference.NONE ||
owner._positionFromParent ||
!defined(owner._position)
) {
return;

View File

@ -106,7 +106,7 @@ function ClippingPlaneCollection(options) {
* An event triggered when a new clipping plane is added to the collection. Event handlers
* are passed the new plane and the index at which it was added.
* @type {Event}
* @default Event()
* @readonly
*/
this.planeAdded = new Event();
@ -114,7 +114,7 @@ function ClippingPlaneCollection(options) {
* An event triggered when a new clipping plane is removed from the collection. Event handlers
* are passed the new plane and the index from which it was removed.
* @type {Event}
* @default Event()
* @readonly
*/
this.planeRemoved = new Event();
@ -472,7 +472,7 @@ ClippingPlaneCollection.prototype.update = function (frameState) {
// Compute texture requirements for current planes
// In RGBA FLOAT, A plane is 4 floats packed to a RGBA.
// In RGBA UNSIGNED_BYTE, A plane is a float in [0, 1) packed to RGBA and an Oct32 quantized normal,
// so 8 bits or 2 pixels in RGBA.
// so 8 bytes or 2 pixels in RGBA.
const pixelsNeeded = useFloatTexture ? this.length : this.length * 2;
if (defined(clippingPlanesTexture)) {

View File

@ -194,7 +194,7 @@ function appendCss(container) {
.cesium-credit-lightbox.cesium-credit-lightbox-expanded {
border: 1px solid #444;
border-radius: 5px;
max-width: 370px;
max-width: 470px;
}
.cesium-credit-lightbox.cesium-credit-lightbox-mobile {
height: 100%;

View File

@ -31,15 +31,28 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) {
}
/**
* Original position, scale and rotation values for splats. Used to maintain
* consistency when multiple transforms may occur. Downstream consumers otherwise may not know
* the underlying data was modified.
* Local copy of the position attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._originalPositions = undefined;
this._originalRotations = undefined;
this._originalScales = undefined;
this._positions = undefined;
/**
* Local copy of the rotation attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._rotations = undefined;
/**
* Local copy of the scale attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._scales = undefined;
/**
* glTF primitive data that contains the Gaussian splat data needed for rendering.
@ -368,6 +381,38 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, {
},
},
/**
* Get the transformed positions of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
positions: {
get: function () {
return this._positions;
},
},
/**
* Get the transformed rotations of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
rotations: {
get: function () {
return this._rotations;
},
},
/**
* Get the transformed scales of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
scales: {
get: function () {
return this._scales;
},
},
/**
* The number of spherical harmonic coefficients used for the Gaussian splats.
* @type {number}
@ -629,21 +674,21 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) {
this.worldTransform = loader.components.scene.nodes[0].matrix;
this._ready = true;
this._originalPositions = new Float32Array(
this._positions = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray,
);
this._originalRotations = new Float32Array(
this._rotations = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray,
);
this._originalScales = new Float32Array(
this._scales = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.SCALE,

View File

@ -450,9 +450,9 @@ GaussianSplatPrimitive.transformTile = function (tile) {
computedModelMatrix,
scratchMatrix4A,
);
const positions = tile.content._originalPositions;
const rotations = tile.content._originalRotations;
const scales = tile.content._originalScales;
const positions = tile.content.positions;
const rotations = tile.content.rotations;
const scales = tile.content.scales;
const attributePositions = ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.POSITION,
@ -471,19 +471,19 @@ GaussianSplatPrimitive.transformTile = function (tile) {
const position = new Cartesian3();
const rotation = new Quaternion();
const scale = new Cartesian3();
for (let i = 0; i < positions.length / 3; ++i) {
position.x = positions[i * 3];
position.y = positions[i * 3 + 1];
position.z = positions[i * 3 + 2];
for (let i = 0; i < attributePositions.length / 3; ++i) {
position.x = attributePositions[i * 3];
position.y = attributePositions[i * 3 + 1];
position.z = attributePositions[i * 3 + 2];
rotation.x = rotations[i * 4];
rotation.y = rotations[i * 4 + 1];
rotation.z = rotations[i * 4 + 2];
rotation.w = rotations[i * 4 + 3];
rotation.x = attributeRotations[i * 4];
rotation.y = attributeRotations[i * 4 + 1];
rotation.z = attributeRotations[i * 4 + 2];
rotation.w = attributeRotations[i * 4 + 3];
scale.x = scales[i * 3];
scale.y = scales[i * 3 + 1];
scale.z = scales[i * 3 + 2];
scale.x = attributeScales[i * 3];
scale.y = attributeScales[i * 3 + 1];
scale.z = attributeScales[i * 3 + 2];
Matrix4.fromTranslationQuaternionRotationScale(
position,
@ -498,18 +498,18 @@ GaussianSplatPrimitive.transformTile = function (tile) {
Matrix4.getRotation(scratchMatrix4C, rotation);
Matrix4.getScale(scratchMatrix4C, scale);
attributePositions[i * 3] = position.x;
attributePositions[i * 3 + 1] = position.y;
attributePositions[i * 3 + 2] = position.z;
positions[i * 3] = position.x;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z;
attributeRotations[i * 4] = rotation.x;
attributeRotations[i * 4 + 1] = rotation.y;
attributeRotations[i * 4 + 2] = rotation.z;
attributeRotations[i * 4 + 3] = rotation.w;
rotations[i * 4] = rotation.x;
rotations[i * 4 + 1] = rotation.y;
rotations[i * 4 + 2] = rotation.z;
rotations[i * 4 + 3] = rotation.w;
attributeScales[i * 3] = scale.x;
attributeScales[i * 3 + 1] = scale.y;
attributeScales[i * 3 + 2] = scale.z;
scales[i * 3] = scale.x;
scales[i * 3 + 1] = scale.y;
scales[i * 3 + 2] = scale.z;
}
};
@ -860,21 +860,27 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
const aggregateAttributeValues = (
componentDatatype,
getAttributeCallback,
numberOfComponents,
) => {
let aggregate;
let offset = 0;
for (const tile of tiles) {
const primitive = tile.content.gltfPrimitive;
const attribute = getAttributeCallback(primitive);
const content = tile.content;
const attribute = getAttributeCallback(content);
const componentsPerAttribute = defined(numberOfComponents)
? numberOfComponents
: AttributeType.getNumberOfComponents(attribute.type);
const buffer = defined(attribute.typedArray)
? attribute.typedArray
: attribute;
if (!defined(aggregate)) {
aggregate = ComponentDatatype.createTypedArray(
componentDatatype,
totalElements *
AttributeType.getNumberOfComponents(attribute.type),
totalElements * componentsPerAttribute,
);
}
aggregate.set(attribute.typedArray, offset);
offset += attribute.typedArray.length;
aggregate.set(buffer, offset);
offset += buffer.length;
}
return aggregate;
};
@ -906,36 +912,27 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
this._positions = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.POSITION,
),
(content) => content.positions,
3,
);
this._scales = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.SCALE,
),
(content) => content.scales,
3,
);
this._rotations = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
),
(content) => content.rotations,
4,
);
this._colors = aggregateAttributeValues(
ComponentDatatype.UNSIGNED_BYTE,
(gltfPrimitive) =>
(content) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
content.gltfPrimitive,
VertexAttributeSemantic.COLOR,
),
);

View File

@ -0,0 +1,613 @@
import Credit from "../Core/Credit.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Resource from "../Core/Resource.js";
import IonResource from "../Core/IonResource.js";
import Check from "../Core/Check.js";
import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
import GoogleMaps from "../Core/GoogleMaps.js";
const trailingSlashRegex = /\/$/;
/**
* @typedef {Object} Google2DImageryProvider.ConstructorOptions
*
* Initialization options for the Google2DImageryProvider constructor
*
* @property {string} key The Google api key to send with tile requests.
* @property {string} session The Google session token that tracks the current state of your map and viewport.
* @property {string|Resource|IonResource} url The Google 2D maps endpoint.
* @property {string} tileWidth The width of each tile in pixels.
* @property {string} tileHeight The height of each tile in pixels.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @property {number} [minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
* this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely
* to result in rendering problems.
* @property {number} [maximumLevel=22] The maximum level-of-detail supported by the imagery provider.
* @property {Rectangle} [rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image.
*/
/**
* <div class="notice">
* This object is normally not instantiated directly, use {@link Google2DImageryProvider.fromIonAssetId} or {@link Google2DImageryProvider.fromUrl}.
* </div>
*
*
* Provides 2D image tiles from {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview|Google 2D Tiles}.
*
* Google 2D Tiles can only be used with the Google geocoder.
*
* @alias Google2DImageryProvider
* @constructor
*
* @param {Google2DImageryProvider.ConstructorOptions} options Object describing initialization options
*
* @example
* // Google 2D imagery provider
* const googleTilesProvider = Cesium.Google2DImageryProvider.fromIonAssetId({
* assetId: 3830184
* });
* @example
* // Use your own Google api key
* Cesium.GoogleMaps.defaultApiKey = "your-api-key";
*
* const googleTilesProvider = Cesium.Google2DImageryProvider.fromUrl({
* mapType: "SATELLITE"
* });
*
*
* @see {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview}
* @see {@link https://developers.google.com/maps/documentation/tile/session_tokens}
* @see {@link https://en.wikipedia.org/wiki/IETF_language_tag|IETF Language Tags}
* @see {@link https://cldr.unicode.org/|Common Locale Data Repository region identifiers}
*/
function Google2DImageryProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._maximumLevel = options.maximumLevel ?? 22;
this._minimumLevel = options.minimumLevel ?? 0;
//>>includeStart("debug", pragmas.debug);
Check.defined("options.session", options.session);
Check.defined("options.tileWidth", options.tileWidth);
Check.defined("options.tileHeight", options.tileHeight);
Check.defined("options.key", options.key);
//>>includeEnd("debug");
this._session = options.session;
this._key = options.key;
this._tileWidth = options.tileWidth;
this._tileHeight = options.tileHeight;
const resource =
options.url instanceof IonResource
? options.url
: Resource.createIfNeeded(options.url ?? GoogleMaps.mapTilesApiEndpoint);
let templateUrl = resource.getUrlComponent();
if (!trailingSlashRegex.test(templateUrl)) {
templateUrl += "/";
}
const tilesUrl = `${templateUrl}v1/2dtiles/{z}/{x}/{y}`;
this._viewportUrl = `${templateUrl}tile/v1/viewport`;
resource.url = tilesUrl;
resource.setQueryParameters({
session: encodeURIComponent(options.session),
key: encodeURIComponent(options.key),
});
let credit;
if (defined(options.credit)) {
credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
}
const provider = new UrlTemplateImageryProvider({
url: resource,
credit: credit,
tileWidth: options.tileWidth,
tileHeight: options.tileHeight,
ellipsoid: options.ellipsoid,
rectangle: options.rectangle,
maximumLevel: this._maximumLevel,
minimumLevel: this._minimumLevel,
});
provider._resource = resource;
this._imageryProvider = provider;
// This will be defined for ion resources
this._tileCredits = resource.credits;
this._attributionsByLevel = undefined;
// Asynchronously request and populate _attributionsByLevel
this.getViewportCredits();
}
Object.defineProperties(Google2DImageryProvider.prototype, {
/**
* Gets the URL of the Google 2D Imagery server.
* @memberof Google2DImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._imageryProvider.url;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by the instance.
* @memberof Google2DImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._imageryProvider.rectangle;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof Google2DImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._imageryProvider.tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof Google2DImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._imageryProvider.tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof Google2DImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._imageryProvider.maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested. Generally,
* a minimum level should only be used when the rectangle of the imagery is small
* enough that the number of tiles at the minimum level is small. An imagery
* provider with more than a few tiles at the minimum level will lead to
* rendering problems.
* @memberof Google2DImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return this._imageryProvider.minimumLevel;
},
},
/**
* Gets the tiling scheme used by the provider.
* @memberof Google2DImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._imageryProvider.tilingScheme;
},
},
/**
* Gets the tile discard policy. If not undefined, the discard policy is responsible
* for filtering out "missing" tiles via its shouldDiscardImage function. If this function
* returns undefined, no tiles are filtered.
* @memberof Google2DImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._imageryProvider.tileDiscardPolicy;
},
},
/**
* Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing
* to the event, you will be notified of the error and can potentially recover from it. Event listeners
* are passed an instance of {@link TileProviderError}.
* @memberof Google2DImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._imageryProvider.errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof Google2DImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._imageryProvider.credit;
},
},
/**
* Gets the proxy used by this provider.
* @memberof Google2DImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._imageryProvider.proxy;
},
},
/**
* Gets a value indicating whether or not the images provided by this imagery provider
* include an alpha channel. If this property is false, an alpha channel, if present, will
* be ignored. If this property is true, any images without an alpha channel will be treated
* as if their alpha is 1.0 everywhere. When this property is false, memory usage
* and texture upload time are reduced.
* @memberof Google2DImageryProvider.prototype
* @type {boolean}
* @readonly
*/
hasAlphaChannel: {
get: function () {
return this._imageryProvider.hasAlphaChannel;
},
},
});
/**
* Creates an {@link ImageryProvider} which provides 2D global tiled imagery from {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview|Google 2D Tiles}, streamed using the Cesium ion REST API.
* @param {object} options Object with the following properties:
* @param {string} options.assetId The Cesium ion asset id.
* @param {"satellite" | "terrain" | "roadmap"} [options.mapType="satellite"] The map type of the Google map imagery. Valid options are satellite, terrain, and roadmap. If overlayLayerType is set, mapType is ignored and a transparent overlay is returned. If overlayMapType is undefined, then a basemap of mapType is returned. layerRoadmap overlayLayerType is included in terrain and roadmap mapTypes.
* @param {string} [options.language="en_US"] an IETF language tag that specifies the language used to display information on the tiles
* @param {string} [options.region="US"] A Common Locale Data Repository region identifier (two uppercase letters) that represents the physical location of the user.
* @param {"layerRoadmap" | "layerStreetview" | "layerTraffic"} [options.overlayLayerType] Returns a transparent overlay map with the specified layerType. If no value is provided, a basemap of mapType is returned. Use multiple instances of Google2DImageryProvider to add multiple Google Maps overlays to a scene. layerRoadmap is included in terrain and roadmap mapTypes, so adding as overlay to terrain or roadmap has no effect.
* @param {Object} [options.styles] An array of JSON style objects that specify the appearance and detail level of map features such as roads, parks, and built-up areas. Styling is used to customize the standard Google base map. The styles parameter is valid only if the mapType is roadmap. For the complete style syntax, see the ({@link https://developers.google.com/maps/documentation/tile/style-reference|Google Style Reference}).
* @param {Ellipsoid} [options.ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @param {number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
* this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely
* to result in rendering problems.
* @param {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider.
* @param {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image.
* @param {Credit|string} [options.credit] A credit for the data source, which is displayed on the canvas.
*
* @returns {Promise<Google2DImageryProvider>} A promise that resolves to the created Google2DImageryProvider.
*
* @example
* // Google 2D imagery provider
* const googleTilesProvider = Cesium.Google2DImageryProvider.fromIonAssetId({
* assetId: 3830184
* });
* @example
* // Google 2D roadmap overlay with custom styles
* const googleTileProvider = Cesium.Google2DImageryProvider.fromIonAssetId({
* assetId: 3830184,
* overlayLayerType: "layerRoadmap",
* styles: [
* {
* stylers: [{ hue: "#00ffe6" }, { saturation: -20 }],
* },
* {
* featureType: "road",
* elementType: "geometry",
* stylers: [{ lightness: 100 }, { visibility: "simplified" }],
* },
* ],
* });
*/
Google2DImageryProvider.fromIonAssetId = async function (options) {
options = options ?? {};
options.mapType = options.mapType ?? "satellite";
options.language = options.language ?? "en_US";
options.region = options.region ?? "US";
const overlayLayerType = options.overlayLayerType;
//>>includeStart("debug", pragmas.debug);
if (defined(overlayLayerType)) {
Check.typeOf.string("options.overlayLayerType", overlayLayerType);
}
Check.defined("options.assetId", options.assetId);
//>>includeEnd("debug");
const queryOptions = buildQueryOptions(options);
const endpointResource = IonResource._createEndpointResource(
options.assetId,
{
queryParameters: {
options: JSON.stringify(queryOptions),
},
},
);
const endpoint = await endpointResource.fetchJson();
const endpointOptions = { ...endpoint.options };
delete endpointOptions.url;
const providerOptions = {
language: options.language,
region: options.region,
ellipsoid: options.ellipsoid,
minimumLevel: options.minimumLevel,
maximumLevel: options.maximumLevel,
rectangle: options.rectangle,
credit: options.credit,
};
return new Google2DImageryProvider({
...endpointOptions,
...providerOptions,
url: new IonResource(endpoint, endpointResource),
});
};
/**
* Creates an {@link ImageryProvider} which provides 2D global tiled imagery from {@link https://developers.google.com/maps/documentation/tile/2d-tiles-overview|Google 2D Tiles}.
* @param {object} options Object with the following properties:
* @param {string} [options.key=GoogleMaps.defaultApiKey] Your API key to access Google 2D Tiles. See {@link https://developers.google.com/maps/documentation/javascript/get-api-key} for instructions on how to create your own key.
* @param {"satellite" | "terrain" | "roadmap"} [options.mapType="satellite"] The map type of the Google map imagery. Valid options are satellite, terrain, and roadmap. If overlayLayerType is set, mapType is ignored and a transparent overlay is returned. If overlayMapType is undefined, then a basemap of mapType is returned. layerRoadmap overlayLayerType is included in terrain and roadmap mapTypes.
* @param {string} [options.language="en_US"] an IETF language tag that specifies the language used to display information on the tiles
* @param {string} [options.region="US"] A Common Locale Data Repository region identifier (two uppercase letters) that represents the physical location of the user.
* @param {"layerRoadmap" | "layerStreetview" | "layerTraffic"} [options.overlayLayerType] Returns a transparent overlay map with the specified layerType. If no value is provided, a basemap of mapType is returned. Use multiple instances of Google2DImageryProvider to add multiple Google Maps overlays to a scene. layerRoadmap is included in terrain and roadmap mapTypes, so adding as overlay to terrain or roadmap has no effect.
* @param {Object} [options.styles] An array of JSON style objects that specify the appearance and detail level of map features such as roads, parks, and built-up areas. Styling is used to customize the standard Google base map. The styles parameter is valid only if the mapType is roadmap. For the complete style syntax, see the ({@link https://developers.google.com/maps/documentation/tile/style-reference|Google Style Reference}).
* @param {Ellipsoid} [options.ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @param {number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
* this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely
* to result in rendering problems.
* @param {number} [options.maximumLevel=22] The maximum level-of-detail supported by the imagery provider.
* @param {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image.
* @param {Credit|string} [options.credit] A credit for the data source, which is displayed on the canvas.
*
* @returns {Promise<Google2DImageryProvider>} A promise that resolves to the created Google2DImageryProvider.
*
* @example
* // Use your own Google api key
* Cesium.GoogleMaps.defaultApiKey = "your-api-key";
*
* const googleTilesProvider = Cesium.Google2DImageryProvider.fromUrl({
* mapType: "satellite"
* });
* @example
* // Google 2D roadmap overlay with custom styles
* Cesium.GoogleMaps.defaultApiKey = "your-api-key";
*
* const googleTileProvider = Cesium.Google2DImageryProvider.fromUrl({
* overlayLayerType: "layerRoadmap",
* styles: [
* {
* stylers: [{ hue: "#00ffe6" }, { saturation: -20 }],
* },
* {
* featureType: "road",
* elementType: "geometry",
* stylers: [{ lightness: 100 }, { visibility: "simplified" }],
* },
* ],
* });
*/
Google2DImageryProvider.fromUrl = async function (options) {
options = options ?? {};
options.mapType = options.mapType ?? "satellite";
options.language = options.language ?? "en_US";
options.region = options.region ?? "US";
options.url = options.url ?? GoogleMaps.mapTilesApiEndpoint;
options.key = options.key ?? GoogleMaps.defaultApiKey;
const overlayLayerType = options.overlayLayerType;
//>>includeStart("debug", pragmas.debug);
if (defined(overlayLayerType)) {
Check.typeOf.string("overlayLayerType", overlayLayerType);
}
if (!defined(options.key) && !defined(GoogleMaps.defaultApiKey)) {
throw new DeveloperError(
"options.key or GoogleMaps.defaultApiKey is required.",
);
}
//>>includeEnd("debug");
const sessionJson = await createGoogleImagerySession(options);
return new Google2DImageryProvider({
...sessionJson,
...options,
credit: options.credit ?? GoogleMaps.getDefaultCredit(),
});
};
/**
* Gets the credits to be displayed when a given tile is displayed.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level;
* @returns {Credit[]|undefined} The credits to be displayed when the tile is displayed.
*/
Google2DImageryProvider.prototype.getTileCredits = function (x, y, level) {
const hasAttributions = defined(this._attributionsByLevel);
if (!hasAttributions || !defined(this._tileCredits)) {
return undefined;
}
const innerCredits = this._attributionsByLevel.get(level);
if (!defined(this._tileCredits)) {
return innerCredits;
}
return this._tileCredits.concat(innerCredits);
};
/**
* Requests the image for a given tile.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {Request} [request] The request object. Intended for internal use only.
* @returns {Promise<ImageryTypes>|undefined} A promise for the image that will resolve when the image is available, or
* undefined if there are too many active requests to the server, and the request should be retried later.
*/
Google2DImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
return this._imageryProvider.requestImage(x, y, level, request);
};
/**
* Picking features is not currently supported by this imagery provider, so this function simply returns
* undefined.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {number} longitude The longitude at which to pick features.
* @param {number} latitude The latitude at which to pick features.
* @return {undefined} Undefined since picking is not supported.
*/
Google2DImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
return undefined;
};
/**
* Get attribution for imagery from Google Maps to display in the credits
* @private
* @return {Promise<Map<Credit[]>>} The list of attribution sources to display in the credits.
*/
Google2DImageryProvider.prototype.getViewportCredits = async function () {
const maximumLevel = this._maximumLevel;
const promises = [];
for (let level = 0; level < maximumLevel + 1; level++) {
promises.push(
fetchViewportAttribution(
this._viewportUrl,
this._key,
this._session,
level,
),
);
}
const results = await Promise.all(promises);
const attributionsByLevel = new Map();
for (let level = 0; level < maximumLevel + 1; level++) {
const credits = [];
const attributions = results[level];
if (attributions) {
const levelCredits = new Credit(attributions);
credits.push(levelCredits);
}
attributionsByLevel.set(level, credits);
}
this._attributionsByLevel = attributionsByLevel;
return attributionsByLevel;
};
async function fetchViewportAttribution(url, key, session, level) {
const viewport = await Resource.fetch({
url: url,
queryParameters: {
key,
session,
zoom: level,
north: 90,
south: -90,
east: 180,
west: -180,
},
data: JSON.stringify(Frozen.EMPTY_OBJECT),
});
const viewportJson = JSON.parse(viewport);
return viewportJson.copyright;
}
function buildQueryOptions(options) {
const { mapType, overlayLayerType, styles } = options;
const queryOptions = {
mapType,
overlay: false,
};
if (mapType === "terrain" && !defined(overlayLayerType)) {
queryOptions.layerTypes = ["layerRoadmap"];
}
if (defined(overlayLayerType)) {
queryOptions.mapType = "satellite";
queryOptions.overlay = true;
queryOptions.layerTypes = [overlayLayerType];
}
if (defined(styles)) {
queryOptions.styles = styles;
}
return queryOptions;
}
async function createGoogleImagerySession(options) {
const { language, region, key, url } = options;
const queryOptions = buildQueryOptions(options);
let baseUrl = url.url ?? url;
if (!trailingSlashRegex.test(baseUrl)) {
baseUrl += "/";
}
const response = await Resource.post({
url: `${baseUrl}v1/createSession`,
queryParameters: { key: key },
data: JSON.stringify({
...queryOptions,
language,
region,
}),
});
const responseJson = JSON.parse(response);
return responseJson;
}
export default Google2DImageryProvider;

View File

@ -13,6 +13,8 @@ import SingleTileImageryProvider from "./SingleTileImageryProvider.js";
import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
import WebMapServiceImageryProvider from "./WebMapServiceImageryProvider.js";
import WebMapTileServiceImageryProvider from "./WebMapTileServiceImageryProvider.js";
import Google2DImageryProvider from "./Google2DImageryProvider.js";
import Azure2DImageryProvider from "./Azure2DImageryProvider.js";
// These values are the list of supported external imagery
// assets in the Cesium ion beta. They are subject to change.
@ -52,6 +54,18 @@ const ImageryProviderAsyncMapping = {
...options,
});
},
GOOGLE_2D_MAPS: (ionResource, options) => {
return new Google2DImageryProvider({
...options,
url: ionResource,
});
},
AZURE_MAPS: (ionResource, options) => {
return new Azure2DImageryProvider({
...options,
url: ionResource,
});
},
};
/**
@ -308,7 +322,14 @@ IonImageryProvider.fromAssetId = async function (assetId, options) {
const options = { ...endpoint.options };
const url = options.url;
delete options.url;
imageryProvider = await factory(url, options);
if (["GOOGLE_2D_MAPS", "AZURE_MAPS"].includes(endpoint.externalType)) {
imageryProvider = await factory(
new IonResource(endpoint, endpointResource),
options,
);
} else {
imageryProvider = await factory(url, options);
}
}
const provider = new IonImageryProvider(options);

View File

@ -294,6 +294,7 @@ function rebindAllGlyphs(labelCollection, label) {
});
billboard._labelDimensions = new Cartesian2();
billboard._labelTranslate = new Cartesian2();
billboard._positionFromParent = true;
}
glyph.billboard = billboard;
}

View File

@ -2489,10 +2489,16 @@ Primitive.prototype.destroy = function () {
function setReady(primitive, frameState, state, error) {
primitive._error = error;
primitive._state = state;
frameState.afterRender.push(function () {
primitive._ready =
primitive._state === PrimitiveState.COMPLETE ||
primitive._state === PrimitiveState.FAILED;
// Returning 'true' here will ensure that another rendering pass is
// triggered after the primitive actually became ready, to make sure
// that it is in fact rendered even in "request render mode"
return true;
});
}
export default Primitive;

View File

@ -1,13 +1,136 @@
/**
* The states that describe the lifecycle of a <code>Primitive</code>, as
* represented by the <code>primitive._state</code>.
*
* The state transitions are triggered by calls to the <code>update</code>
* function, but the actual state changes may happen asynchronously if the
* <code>asynchronous</code> flag of the primitive was set to
* <code>true</code>.
*
* @private
*/
const PrimitiveState = {
/**
* The initial state of a primitive.
*
* Note that this does NOT mean that the primitive is "ready", as indicated
* by the <code>_ready</code> property. It means the opposite: Nothing was
* done with the primitive at all.
*
* For primitives that are created with the <code>asynchronous:true</code>
* setting and that are in this state, the <code>update</code> call starts
* the creation of the geometry using web workers, and the primitive goes
* into the <code>CREATING</code> state.
*
* For synchronously created primitives, this state never matters. They will
* go into the COMBINED (or FAILED) state directly due to a call to the
* <code>update</code> function, if they are not yet FAILED, COMBINED,
* or COMPLETE.
*/
READY: 0,
/**
* The process of creating the primitive geometry is ongoing.
*
* A primitive can only ever be in this state when it was created
* with the <code>asynchronous:true</code> setting.
*
* It means that web workers are currently creating the geometry
* of the primitive.
*
* When the geometry creation succeeds, then the primitive will go
* into the CREATED state. Otherwise, it will go into the FAILED
* state. Both will happen asynchronously.
*
* The <code>update</code> function has to be called regularly
* until either of these states is reached.
*/
CREATING: 1,
/**
* The geometry for the primitive has been created.
*
* A primitive can only ever be in this state when it was created
* with the <code>asynchronous:true</code> setting.
*
* It means that web workers have (asynchronously) finished the
* creation of the geometry, but further (asynchronous) processing
* is necessary: If a primitive is determined to be in this state
* during a call to <code>update</code>, an asynchronous process
* is triggered to "combine" the geometry, meaning that the primitive
* will go into the COMBINING state.
*/
CREATED: 2,
/**
* The asynchronous creation of the geometry has been finished, but the
* asynchronous process of combining the geometry has not finished yet.
*
* A primitive can only ever be in this state when it was created
* with the <code>asynchronous:true</code> setting.
*
* It means that whatever is done with
* <code>PrimitivePipeline.packCombineGeometryParameters</code> has
* not finished yet. When combining the geometry succeeds, the
* primitive will go into the COMBINED state. Otherwise, it will
* go into the FAILED state.
*/
COMBINING: 3,
/**
* The geometry data is in a form that can be uploaded to the GPU.
*
* For <i>synchronous</i> primitives, this means that the geometry
* has been created (synchronously) due to the first call to the
* <code>update</code> function.
*
* For <i>asynchronous</i> primitives, this means that the asynchronous
* creation of the geometry and the asynchronous combination of the
* geometry have both finished.
*
* The <code>update</code> function has to be called regularly until
* this state is reached. When it is reached, the <code>update</code>
* call will cause the transition into the COMPLETE state.
*/
COMBINED: 4,
/**
* The geometry has been created and uploaded to the GPU.
*
* When this state is reached, it eventually causes the <code>_ready</code>
* flag of the primitive to become <code>true</code>.
*
* Note: Setting the <code>ready</code> flag does NOT happen in the
* <code>update</code> call: It only happens after rendering the next
* frame!
*
* Note: This state does not mean that nothing has to be done
* anymore (so the work is not "complete"). When the primitive is in
* this state, the <code>update</code> function still has to be
* called regularly.
*/
COMPLETE: 5,
/**
* The creation of the primitive failed.
*
* When this state is reached, it eventually causes the <code>_ready</code>
* flag of the primitive to become <code>true</code>.
*
* Note: Setting the <code>ready</code> flag does NOT happen in the
* <code>update</code> call: It only happens after rendering the next
* frame!
*
* This state can be reached when the (synchronous or asynchronous)
* creation of the geometry, or the (asynchronous) combination of
* the geometry caused any form of error.
*
* It may or may not imply the presence of the <code>_error</code> property.
* When the <code>_error</code> property is present on a FAILED primitive,
* this error will be thrown during the <code>update</code> call. When it
* is not present for a FAILED primitive, then the <code>update</code> call
* will do nothing.
*/
FAILED: 6,
};
export default Object.freeze(PrimitiveState);

View File

@ -0,0 +1,494 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Check from "../Core/Check.js";
import ClippingPlane from "./ClippingPlane.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import Event from "../Core/Event.js";
import Frozen from "../Core/Frozen.js";
import Intersect from "../Core/Intersect.js";
import Matrix4 from "../Core/Matrix4.js";
import PixelFormat from "../Core/PixelFormat.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Plane from "../Core/Plane.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
/**
* Specifies a set of clipping planes defining rendering bounds for a {@link VoxelPrimitive}.
*
* @alias VoxelBoundsCollection
* @constructor
*
* @param {object} [options] Object with the following properties:
* @param {ClippingPlane[]} [options.planes=[]] An array of {@link ClippingPlane} objects used to selectively disable rendering on the outside of each plane.
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix specifying an additional transform relative to the clipping planes original coordinate system.
* @param {boolean} [options.unionClippingRegions=false] If true, a region will be clipped if it is on the outside of any plane in the collection. Otherwise, a region will only be clipped if it is on the outside of every plane.
*
* @private
*/
function VoxelBoundsCollection(options) {
const {
planes,
modelMatrix = Matrix4.IDENTITY,
unionClippingRegions = false,
} = options ?? Frozen.EMPTY_OBJECT;
this._planes = [];
/**
* The 4x4 transformation matrix specifying an additional transform relative to the clipping planes
* original coordinate system.
*
* @type {Matrix4}
* @default Matrix4.IDENTITY
*/
this.modelMatrix = Matrix4.clone(modelMatrix);
/**
* An event triggered when a new clipping plane is added to the collection. Event handlers
* are passed the new plane and the index at which it was added.
* @type {Event}
* @readonly
*/
this.planeAdded = new Event();
/**
* An event triggered when a new clipping plane is removed from the collection. Event handlers
* are passed the new plane and the index from which it was removed.
* @type {Event}
* @readonly
*/
this.planeRemoved = new Event();
this._unionClippingRegions = unionClippingRegions;
this._testIntersection = unionClippingRegions
? unionIntersectFunction
: defaultIntersectFunction;
this._float32View = undefined;
this._clippingPlanesTexture = undefined;
// Add each ClippingPlane object.
if (defined(planes)) {
for (let i = 0; i < planes.length; ++i) {
this.add(planes[i]);
}
}
}
function unionIntersectFunction(value) {
return value === Intersect.OUTSIDE;
}
function defaultIntersectFunction(value) {
return value === Intersect.INSIDE;
}
Object.defineProperties(VoxelBoundsCollection.prototype, {
/**
* Returns the number of planes in this collection. This is commonly used with
* {@link VoxelBoundsCollection#get} to iterate over all the planes
* in the collection.
*
* @memberof VoxelBoundsCollection.prototype
* @type {number}
* @readonly
*/
length: {
get: function () {
return this._planes.length;
},
},
/**
* If true, a region will be clipped if it is on the outside of any plane in the
* collection. Otherwise, a region will only be clipped if it is on the
* outside of every plane.
*
* @memberof VoxelBoundsCollection.prototype
* @type {boolean}
* @default false
*/
unionClippingRegions: {
get: function () {
return this._unionClippingRegions;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.bool("value", value);
//>>includeEnd('debug');
if (this._unionClippingRegions === value) {
return;
}
this._unionClippingRegions = value;
this._testIntersection = value
? unionIntersectFunction
: defaultIntersectFunction;
},
},
/**
* Returns a texture containing packed, untransformed clipping planes.
*
* @memberof VoxelBoundsCollection.prototype
* @type {Texture}
* @readonly
* @private
*/
texture: {
get: function () {
return this._clippingPlanesTexture;
},
},
/**
* Returns a Number encapsulating the state for this VoxelBoundsCollection.
*
* Clipping mode is encoded in the sign of the number, which is just the plane count.
* If this value changes, then shader regeneration is necessary.
*
* @memberof VoxelBoundsCollection.prototype
* @returns {number} A Number that describes the VoxelBoundsCollection's state.
* @readonly
* @private
*/
clippingPlanesState: {
get: function () {
return this._unionClippingRegions
? this._planes.length
: -this._planes.length;
},
},
});
/**
* Adds the specified {@link ClippingPlane} to the collection to be used to selectively disable rendering
* on the outside of each plane. Use {@link VoxelBoundsCollection#unionClippingRegions} to modify
* how modify the clipping behavior of multiple planes.
*
* @param {ClippingPlane} plane The ClippingPlane to add to the collection.
*
* @see VoxelBoundsCollection#unionClippingRegions
* @see VoxelBoundsCollection#remove
* @see VoxelBoundsCollection#removeAll
*/
VoxelBoundsCollection.prototype.add = function (plane) {
const newPlaneIndex = this._planes.length;
plane.index = newPlaneIndex;
this._planes.push(plane);
this.planeAdded.raiseEvent(plane, newPlaneIndex);
};
/**
* Returns the plane in the collection at the specified index. Indices are zero-based
* and increase as planes are added. Removing a plane shifts all planes after
* it to the left, changing their indices. This function is commonly used with
* {@link VoxelBoundsCollection#length} to iterate over all the planes
* in the collection.
*
* @param {number} index The zero-based index of the plane.
* @returns {ClippingPlane} The ClippingPlane at the specified index.
*
* @see VoxelBoundsCollection#length
*/
VoxelBoundsCollection.prototype.get = function (index) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("index", index);
//>>includeEnd('debug');
return this._planes[index];
};
function indexOf(planes, plane) {
for (let i = 0; i < planes.length; ++i) {
if (Plane.equals(planes[i], plane)) {
return i;
}
}
return -1;
}
/**
* Checks whether this collection contains a ClippingPlane equal to the given ClippingPlane.
*
* @param {ClippingPlane} [clippingPlane] The ClippingPlane to check for.
* @returns {boolean} true if this collection contains the ClippingPlane, false otherwise.
*
* @see VoxelBoundsCollection#get
*/
VoxelBoundsCollection.prototype.contains = function (clippingPlane) {
return indexOf(this._planes, clippingPlane) !== -1;
};
/**
* Removes the first occurrence of the given ClippingPlane from the collection.
*
* @param {ClippingPlane} clippingPlane
* @returns {boolean} <code>true</code> if the plane was removed; <code>false</code> if the plane was not found in the collection.
*
* @see VoxelBoundsCollection#add
* @see VoxelBoundsCollection#contains
* @see VoxelBoundsCollection#removeAll
*/
VoxelBoundsCollection.prototype.remove = function (clippingPlane) {
const planes = this._planes;
const index = indexOf(planes, clippingPlane);
if (index === -1) {
return false;
}
// Unlink this VoxelBoundsCollection from the ClippingPlane
if (clippingPlane instanceof ClippingPlane) {
clippingPlane.onChangeCallback = undefined;
clippingPlane.index = -1;
}
// Shift and update indices
const length = planes.length - 1;
for (let i = index; i < length; ++i) {
const planeToKeep = planes[i + 1];
planes[i] = planeToKeep;
if (planeToKeep instanceof ClippingPlane) {
planeToKeep.index = i;
}
}
planes.length = length;
this.planeRemoved.raiseEvent(clippingPlane, index);
return true;
};
/**
* Removes all planes from the collection.
*
* @see VoxelBoundsCollection#add
* @see VoxelBoundsCollection#remove
*/
VoxelBoundsCollection.prototype.removeAll = function () {
// Dereference this VoxelBoundsCollection from all ClippingPlanes
const planes = this._planes;
for (let i = 0; i < planes.length; ++i) {
const plane = planes[i];
if (plane instanceof ClippingPlane) {
plane.onChangeCallback = undefined;
plane.index = -1;
}
this.planeRemoved.raiseEvent(plane, i);
}
this._planes = [];
};
const scratchPlane = new Plane(Cartesian3.fromElements(1.0, 0.0, 0.0), 0.0);
// Pack starting at the beginning of the buffer to allow partial update
function transformAndPackPlanes(clippingPlaneCollection, transform) {
const float32View = clippingPlaneCollection._float32View;
const planes = clippingPlaneCollection._planes;
let floatIndex = 0;
for (let i = 0; i < planes.length; ++i) {
const { normal, distance } = transformPlane(
planes[i],
transform,
scratchPlane,
);
float32View[floatIndex] = normal.x;
float32View[floatIndex + 1] = normal.y;
float32View[floatIndex + 2] = normal.z;
float32View[floatIndex + 3] = distance;
floatIndex += 4; // each plane is 4 floats
}
}
const scratchPlaneCartesian4 = new Cartesian4();
const scratchTransformedNormal = new Cartesian3();
function transformPlane(plane, transform, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("plane", plane);
Check.typeOf.object("transform", transform);
//>>includeEnd('debug');
const { normal, distance } = plane;
const planeAsCartesian4 = Cartesian4.fromElements(
normal.x,
normal.y,
normal.z,
distance,
scratchPlaneCartesian4,
);
let transformedPlane = Matrix4.multiplyByVector(
transform,
planeAsCartesian4,
scratchPlaneCartesian4,
);
// Convert the transformed plane to Hessian Normal Form
const transformedNormal = Cartesian3.fromCartesian4(
transformedPlane,
scratchTransformedNormal,
);
transformedPlane = Cartesian4.divideByScalar(
transformedPlane,
Cartesian3.magnitude(transformedNormal),
scratchPlaneCartesian4,
);
return Plane.fromCartesian4(transformedPlane, result);
}
function computeTextureResolution(pixelsNeeded, result) {
result.x = Math.min(pixelsNeeded, ContextLimits.maximumTextureSize);
result.y = Math.ceil(pixelsNeeded / result.x);
return result;
}
const textureResolutionScratch = new Cartesian2();
/**
* Called when {@link Viewer} or {@link CesiumWidget} render the scene to
* build the resources for clipping planes.
* <p>
* Do not call this function directly.
* </p>
*/
VoxelBoundsCollection.prototype.update = function (frameState, transform) {
let clippingPlanesTexture = this._clippingPlanesTexture;
// Compute texture requirements for current planes
// In RGBA FLOAT, a plane is 4 floats packed to a single RGBA pixel.
const pixelsNeeded = this.length;
if (defined(clippingPlanesTexture)) {
const currentPixelCount =
clippingPlanesTexture.width * clippingPlanesTexture.height;
// Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.
// Optimization note: this isn't exactly the classic resizeable array algorithm
// * not necessarily checking for resize after each add/remove operation
// * random-access deletes instead of just pops
// * alloc ops likely more expensive than demonstrable via big-O analysis
if (
currentPixelCount < pixelsNeeded ||
pixelsNeeded < 0.25 * currentPixelCount
) {
clippingPlanesTexture.destroy();
clippingPlanesTexture = undefined;
this._clippingPlanesTexture = undefined;
}
}
// If there are no bound planes, there's nothing to update.
if (this.length === 0) {
return;
}
if (!defined(clippingPlanesTexture)) {
const requiredResolution = computeTextureResolution(
pixelsNeeded,
textureResolutionScratch,
);
// Allocate twice as much space as needed to avoid frequent texture reallocation.
// Allocate in the Y direction, since texture may be as wide as context texture support.
requiredResolution.y *= 2;
clippingPlanesTexture = new Texture({
context: frameState.context,
width: requiredResolution.x,
height: requiredResolution.y,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.FLOAT,
sampler: Sampler.NEAREST,
flipY: false,
});
this._float32View = new Float32Array(
requiredResolution.x * requiredResolution.y * 4,
);
this._clippingPlanesTexture = clippingPlanesTexture;
}
const { width, height } = clippingPlanesTexture;
transformAndPackPlanes(this, transform);
clippingPlanesTexture.copyFrom({
source: {
width: width,
height: height,
arrayBufferView: this._float32View,
},
});
};
/**
* Function for getting the clipping plane collection's texture resolution.
* If the VoxelBoundsCollection hasn't been updated, returns the resolution that will be
* allocated based on the current plane count.
*
* @param {VoxelBoundsCollection} clippingPlaneCollection The clipping plane collection
* @param {Context} context The rendering context
* @param {Cartesian2} result A Cartesian2 for the result.
* @returns {Cartesian2} The required resolution.
* @private
*/
VoxelBoundsCollection.getTextureResolution = function (
clippingPlaneCollection,
context,
result,
) {
const texture = clippingPlaneCollection.texture;
if (defined(texture)) {
result.x = texture.width;
result.y = texture.height;
return result;
}
const pixelsNeeded = clippingPlaneCollection.length;
const requiredResolution = computeTextureResolution(pixelsNeeded, result);
// Allocate twice as much space as needed to avoid frequent texture reallocation.
requiredResolution.y *= 2;
return requiredResolution;
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see VoxelBoundsCollection#destroy
*/
VoxelBoundsCollection.prototype.isDestroyed = function () {
return false;
};
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* <br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* voxelBounds = voxelBounds && voxelBounds.destroy();
*
* @see VoxelBoundsCollection#isDestroyed
*/
VoxelBoundsCollection.prototype.destroy = function () {
this._clippingPlanesTexture =
this._clippingPlanesTexture && this._clippingPlanesTexture.destroy();
return destroyObject(this);
};
export default VoxelBoundsCollection;

View File

@ -5,6 +5,8 @@ import Check from "../Core/Check.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import VoxelBoundsCollection from "./VoxelBoundsCollection.js";
import ClippingPlane from "./ClippingPlane.js";
/**
* A box {@link VoxelShape}.
@ -20,41 +22,10 @@ import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
* @private
*/
function VoxelBoxShape() {
/**
* An oriented bounding box containing the bounded shape.
* The update function must be called before accessing this value.
* @private
* @type {OrientedBoundingBox}
* @readonly
*/
this.orientedBoundingBox = new OrientedBoundingBox();
/**
* A bounding sphere containing the bounded shape.
* The update function must be called before accessing this value.
* @private
* @type {BoundingSphere}
* @readonly
*/
this.boundingSphere = new BoundingSphere();
/**
* A transformation matrix containing the bounded shape.
* The update function must be called before accessing this value.
* @private
* @type {Matrix4}
* @readonly
*/
this.boundTransform = new Matrix4();
/**
* A transformation matrix containing the shape, ignoring the bounds.
* The update function must be called before accessing this value.
* @private
* @type {Matrix4}
* @readonly
*/
this.shapeTransform = new Matrix4();
this._orientedBoundingBox = new OrientedBoundingBox();
this._boundingSphere = new BoundingSphere();
this._boundTransform = new Matrix4();
this._shapeTransform = new Matrix4();
/**
* The minimum bounds of the shape.
@ -71,49 +42,166 @@ function VoxelBoxShape() {
this._maxBounds = VoxelBoxShape.DefaultMaxBounds.clone();
/**
* The minimum render bounds of the shape.
* @type {Cartesian3}
* @private
* @type {Object<string, any>}
* @readonly
*/
this.shaderUniforms = {
renderMinBounds: new Cartesian3(),
renderMaxBounds: new Cartesian3(),
boxUvToShapeUvScale: new Cartesian3(),
boxUvToShapeUvTranslate: new Cartesian3(),
};
this._renderMinBounds = VoxelBoxShape.DefaultMinBounds.clone();
/**
* The maximum render bounds of the shape.
* @type {Cartesian3}
* @private
*/
this._renderMaxBounds = VoxelBoxShape.DefaultMaxBounds.clone();
const { DefaultMinBounds, DefaultMaxBounds } = VoxelBoxShape;
const boundPlanes = [
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()),
DefaultMinBounds.x,
),
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_Y, new Cartesian3()),
DefaultMinBounds.y,
),
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()),
DefaultMinBounds.z,
),
new ClippingPlane(Cartesian3.UNIT_X, -DefaultMaxBounds.x),
new ClippingPlane(Cartesian3.UNIT_Y, -DefaultMaxBounds.y),
new ClippingPlane(Cartesian3.UNIT_Z, -DefaultMaxBounds.z),
];
this._renderBoundPlanes = new VoxelBoundsCollection({ planes: boundPlanes });
this._shaderUniforms = {
boxEcToXyz: new Matrix3(),
boxLocalToShapeUvScale: new Cartesian3(),
boxLocalToShapeUvTranslate: new Cartesian3(),
};
this._shaderDefines = {
BOX_INTERSECTION_INDEX: undefined,
};
this._shaderMaximumIntersectionsLength = 0; // not known until update
}
Object.defineProperties(VoxelBoxShape.prototype, {
/**
* An oriented bounding box containing the bounded shape.
*
* @memberof VoxelBoxShape.prototype
* @type {OrientedBoundingBox}
* @readonly
* @private
*/
orientedBoundingBox: {
get: function () {
return this._orientedBoundingBox;
},
},
/**
* A collection of planes used for the render bounds
* @memberof VoxelBoxShape.prototype
* @type {VoxelBoundsCollection}
* @readonly
* @private
*/
renderBoundPlanes: {
get: function () {
return this._renderBoundPlanes;
},
},
/**
* A bounding sphere containing the bounded shape.
*
* @memberof VoxelBoxShape.prototype
* @type {BoundingSphere}
* @readonly
* @private
*/
boundingSphere: {
get: function () {
return this._boundingSphere;
},
},
/**
* A transformation matrix containing the bounded shape.
*
* @memberof VoxelBoxShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
boundTransform: {
get: function () {
return this._boundTransform;
},
},
/**
* A transformation matrix containing the shape, ignoring the bounds.
*
* @memberof VoxelBoxShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
shapeTransform: {
get: function () {
return this._shapeTransform;
},
},
/**
* @memberof VoxelBoxShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
this.shaderDefines = {
BOX_INTERSECTION_INDEX: undefined,
BOX_HAS_SHAPE_BOUNDS: undefined,
};
shaderUniforms: {
get: function () {
return this._shaderUniforms;
},
},
/**
* @memberof VoxelBoxShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderDefines: {
get: function () {
return this._shaderDefines;
},
},
/**
* The maximum number of intersections against the shape for any ray direction.
* @private
* @memberof VoxelBoxShape.prototype
* @type {number}
* @readonly
* @private
*/
this.shaderMaximumIntersectionsLength = 0; // not known until update
}
shaderMaximumIntersectionsLength: {
get: function () {
return this._shaderMaximumIntersectionsLength;
},
},
});
const scratchCenter = new Cartesian3();
const scratchScale = new Cartesian3();
const scratchRotation = new Matrix3();
const scratchClipMinBounds = new Cartesian3();
const scratchClipMaxBounds = new Cartesian3();
const scratchRenderMinBounds = new Cartesian3();
const scratchRenderMaxBounds = new Cartesian3();
const transformLocalToUv = Matrix4.fromRotationTranslation(
Matrix3.fromUniformScale(0.5, new Matrix3()),
new Cartesian3(0.5, 0.5, 0.5),
new Matrix4(),
);
/**
* Update the shape's state.
@ -148,13 +236,13 @@ VoxelBoxShape.prototype.update = function (
minBounds,
clipMinBounds,
clipMaxBounds,
scratchRenderMinBounds,
this._renderMinBounds,
);
const renderMaxBounds = Cartesian3.clamp(
maxBounds,
clipMinBounds,
clipMaxBounds,
scratchRenderMaxBounds,
this._renderMaxBounds,
);
// Box is not visible if:
@ -177,29 +265,39 @@ VoxelBoxShape.prototype.update = function (
return false;
}
this.shapeTransform = Matrix4.clone(modelMatrix, this.shapeTransform);
// Update the render bounds planes
const renderBoundPlanes = this._renderBoundPlanes;
renderBoundPlanes.get(0).distance = renderMinBounds.x;
renderBoundPlanes.get(1).distance = renderMinBounds.y;
renderBoundPlanes.get(2).distance = renderMinBounds.z;
renderBoundPlanes.get(3).distance = -renderMaxBounds.x;
renderBoundPlanes.get(4).distance = -renderMaxBounds.y;
renderBoundPlanes.get(5).distance = -renderMaxBounds.z;
this.orientedBoundingBox = getBoxChunkObb(
this._shapeTransform = Matrix4.clone(modelMatrix, this._shapeTransform);
this._orientedBoundingBox = getBoxChunkObb(
renderMinBounds,
renderMaxBounds,
this.shapeTransform,
this.orientedBoundingBox,
this._shapeTransform,
this._orientedBoundingBox,
);
// All of the box bounds go from -1 to +1, so the model matrix scale can be
// used as the oriented bounding box half axes.
this.boundTransform = Matrix4.fromRotationTranslation(
this.orientedBoundingBox.halfAxes,
this.orientedBoundingBox.center,
this.boundTransform,
this._boundTransform = Matrix4.fromRotationTranslation(
this._orientedBoundingBox.halfAxes,
this._orientedBoundingBox.center,
this._boundTransform,
);
this.boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this.orientedBoundingBox,
this.boundingSphere,
this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this._orientedBoundingBox,
this._boundingSphere,
);
const { shaderUniforms, shaderDefines } = this;
const shaderUniforms = this._shaderUniforms;
const shaderDefines = this._shaderDefines;
// To keep things simple, clear the defines every time
for (const key in shaderDefines) {
@ -214,57 +312,94 @@ VoxelBoxShape.prototype.update = function (
shaderDefines["BOX_INTERSECTION_INDEX"] = intersectionCount;
intersectionCount += 1;
shaderUniforms.renderMinBounds = Matrix4.multiplyByPoint(
transformLocalToUv,
renderMinBounds,
shaderUniforms.renderMinBounds,
);
shaderUniforms.renderMaxBounds = Matrix4.multiplyByPoint(
transformLocalToUv,
renderMaxBounds,
shaderUniforms.renderMaxBounds,
);
shaderDefines["BOX_HAS_SHAPE_BOUNDS"] = true;
// Compute scale and translation to transform from UV space to bounded UV space
const min = minBounds;
const max = maxBounds;
// Go from UV space to bounded UV space:
// delerp(posUv, minBoundsUv, maxBoundsUv)
// (posUv - minBoundsUv) / (maxBoundsUv - minBoundsUv)
// posUv / (maxBoundsUv - minBoundsUv) - minBoundsUv / (maxBoundsUv - minBoundsUv)
// scale = 1.0 / (maxBoundsUv - minBoundsUv)
// scale = 1.0 / ((maxBounds * 0.5 + 0.5) - (minBounds * 0.5 + 0.5))
// scale = 2.0 / (maxBounds - minBounds)
// offset = -minBoundsUv / ((maxBounds * 0.5 + 0.5) - (minBounds * 0.5 + 0.5))
// offset = -2.0 * (minBounds * 0.5 + 0.5) / (maxBounds - minBounds)
// offset = -scale * (minBounds * 0.5 + 0.5)
shaderUniforms.boxUvToShapeUvScale = Cartesian3.fromElements(
2.0 / (min.x === max.x ? 1.0 : max.x - min.x),
2.0 / (min.y === max.y ? 1.0 : max.y - min.y),
2.0 / (min.z === max.z ? 1.0 : max.z - min.z),
shaderUniforms.boxUvToShapeUvScale,
const boxLocalToShapeUvScale = Cartesian3.fromElements(
boundScale(min.x, max.x),
boundScale(min.y, max.y),
boundScale(min.z, max.z),
shaderUniforms.boxLocalToShapeUvScale,
);
shaderUniforms.boxLocalToShapeUvTranslate = Cartesian3.negate(
Cartesian3.multiplyComponents(
boxLocalToShapeUvScale,
min,
shaderUniforms.boxLocalToShapeUvTranslate,
),
shaderUniforms.boxLocalToShapeUvTranslate,
);
shaderUniforms.boxUvToShapeUvTranslate = Cartesian3.fromElements(
-shaderUniforms.boxUvToShapeUvScale.x * (min.x * 0.5 + 0.5),
-shaderUniforms.boxUvToShapeUvScale.y * (min.y * 0.5 + 0.5),
-shaderUniforms.boxUvToShapeUvScale.z * (min.z * 0.5 + 0.5),
shaderUniforms.boxUvToShapeUvTranslate,
);
this.shaderMaximumIntersectionsLength = intersectionCount;
this._shaderMaximumIntersectionsLength = intersectionCount;
return true;
};
function boundScale(minBound, maxBound) {
return CesiumMath.equalsEpsilon(minBound, maxBound, CesiumMath.EPSILON7)
? 1.0
: 1.0 / (maxBound - minBound);
}
const scratchTransformPositionWorldToLocal = new Matrix4();
/**
* Update any view-dependent transforms.
* @private
* @param {FrameState} frameState The frame state.
*/
VoxelBoxShape.prototype.updateViewTransforms = function (frameState) {
const shaderUniforms = this._shaderUniforms;
const transformPositionWorldToLocal = Matrix4.inverse(
this._shapeTransform,
scratchTransformPositionWorldToLocal,
);
const transformDirectionWorldToLocal = Matrix4.getMatrix3(
transformPositionWorldToLocal,
shaderUniforms.boxEcToXyz,
);
const rotateViewToWorld = frameState.context.uniformState.inverseViewRotation;
Matrix3.multiply(
transformDirectionWorldToLocal,
rotateViewToWorld,
shaderUniforms.boxEcToXyz,
);
};
/**
* Convert a local coordinate to the shape's UV space.
* @private
* @param {Cartesian3} positionLocal The local coordinate to convert.
* @param {Cartesian3} result The Cartesian3 to store the result in.
* @returns {Cartesian3} The converted UV coordinate.
*/
VoxelBoxShape.prototype.convertLocalToShapeUvSpace = function (
positionLocal,
result,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("positionLocal", positionLocal);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
const { boxLocalToShapeUvScale, boxLocalToShapeUvTranslate } =
this._shaderUniforms;
return Cartesian3.add(
Cartesian3.multiplyComponents(
positionLocal,
boxLocalToShapeUvScale,
result,
),
boxLocalToShapeUvTranslate,
result,
);
};
const scratchTileMinBounds = new Cartesian3();
const scratchTileMaxBounds = new Cartesian3();
/**
* Computes an oriented bounding box for a specified tile.
* The update function must be called before calling this function.
* @private
* @param {number} tileLevel The tile's level.
* @param {number} tileX The tile's x coordinate.
@ -309,7 +444,7 @@ VoxelBoxShape.prototype.computeOrientedBoundingBoxForTile = function (
return getBoxChunkObb(
tileMinBounds,
tileMaxBounds,
this.shapeTransform,
this._shapeTransform,
result,
);
};
@ -318,7 +453,6 @@ const sampleSizeScratch = new Cartesian3();
/**
* Computes an oriented bounding box for a specified sample within a specified tile.
* The update function must be called before calling this function.
* @private
* @param {SpatialNode} spatialNode The spatial node containing the sample
* @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding
@ -385,7 +519,7 @@ VoxelBoxShape.prototype.computeOrientedBoundingBoxForSample = function (
return getBoxChunkObb(
sampleMinBounds,
sampleMaxBounds,
this.shapeTransform,
this._shapeTransform,
result,
);
};
@ -412,6 +546,7 @@ VoxelBoxShape.DefaultMaxBounds = Object.freeze(
new Cartesian3(+1.0, +1.0, +1.0),
);
const scratchBoxScale = new Cartesian3();
/**
* Computes an {@link OrientedBoundingBox} for a subregion of the shape.
*
@ -437,7 +572,7 @@ function getBoxChunkObb(minimumBounds, maximumBounds, matrix, result) {
result.center = Matrix4.getTranslation(matrix, result.center);
result.halfAxes = Matrix4.getMatrix3(matrix, result.halfAxes);
} else {
let scale = Matrix4.getScale(matrix, scratchScale);
let scale = Matrix4.getScale(matrix, scratchBoxScale);
const localCenter = Cartesian3.midpoint(
minimumBounds,
maximumBounds,
@ -448,7 +583,7 @@ function getBoxChunkObb(minimumBounds, maximumBounds, matrix, result) {
scale.x * 0.5 * (maximumBounds.x - minimumBounds.x),
scale.y * 0.5 * (maximumBounds.y - minimumBounds.y),
scale.z * 0.5 * (maximumBounds.z - minimumBounds.z),
scratchScale,
scratchBoxScale,
);
const rotation = Matrix4.getRotation(matrix, scratchRotation);
result.halfAxes = Matrix3.setScale(rotation, scale, result.halfAxes);

View File

@ -4,9 +4,11 @@ import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import CesiumMath from "../Core/Math.js";
import Check from "../Core/Check.js";
import ClippingPlane from "./ClippingPlane.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import VoxelBoundsCollection from "./VoxelBoundsCollection.js";
/**
* A cylinder {@link VoxelShape}.
@ -22,41 +24,10 @@ import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
* @private
*/
function VoxelCylinderShape() {
/**
* An oriented bounding box containing the bounded shape.
* The update function must be called before accessing this value.
* @private
* @type {OrientedBoundingBox}
* @readonly
*/
this.orientedBoundingBox = new OrientedBoundingBox();
/**
* A bounding sphere containing the bounded shape.
* The update function must be called before accessing this value.
* @private
* @type {BoundingSphere}
* @readonly
*/
this.boundingSphere = new BoundingSphere();
/**
* A transformation matrix containing the bounded shape.
* The update function must be called before accessing this value.
* @private
* @type {Matrix4}
* @readonly
*/
this.boundTransform = new Matrix4();
/**
* A transformation matrix containing the shape, ignoring the bounds.
* The update function must be called before accessing this value.
* @private
* @type {Matrix4}
* @readonly
*/
this.shapeTransform = new Matrix4();
this._orientedBoundingBox = new OrientedBoundingBox();
this._boundingSphere = new BoundingSphere();
this._boundTransform = new Matrix4();
this._shapeTransform = new Matrix4();
/**
* The minimum bounds of the shape, corresponding to minimum radius, angle, and height.
@ -72,61 +43,160 @@ function VoxelCylinderShape() {
*/
this._maxBounds = VoxelCylinderShape.DefaultMaxBounds.clone();
/**
* @private
* @type {Object<string, any>}
* @readonly
*/
this.shaderUniforms = {
const { DefaultMinBounds, DefaultMaxBounds } = VoxelCylinderShape;
const boundPlanes = [
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()),
DefaultMinBounds.z,
),
new ClippingPlane(Cartesian3.UNIT_Z, -DefaultMaxBounds.z),
];
this._renderBoundPlanes = new VoxelBoundsCollection({ planes: boundPlanes });
this._shaderUniforms = {
cameraShapePosition: new Cartesian3(),
cylinderEcToRadialTangentUp: new Matrix3(),
cylinderRenderRadiusMinMax: new Cartesian2(),
cylinderRenderAngleMinMax: new Cartesian2(),
cylinderRenderHeightMinMax: new Cartesian2(),
cylinderUvToShapeUvRadius: new Cartesian2(),
cylinderUvToShapeUvAngle: new Cartesian2(),
cylinderUvToShapeUvHeight: new Cartesian2(),
cylinderShapeUvAngleMinMax: new Cartesian2(),
cylinderShapeUvAngleRangeZeroMid: 0.0,
cylinderLocalToShapeUvRadius: new Cartesian2(),
cylinderLocalToShapeUvAngle: new Cartesian2(),
cylinderLocalToShapeUvHeight: new Cartesian2(),
cylinderShapeUvAngleRangeOrigin: 0.0,
};
/**
* @private
* @type {Object<string, any>}
* @readonly
*/
this.shaderDefines = {
this._shaderDefines = {
CYLINDER_HAS_SHAPE_BOUNDS_ANGLE: undefined,
CYLINDER_HAS_RENDER_BOUNDS_RADIUS_MIN: undefined,
CYLINDER_HAS_RENDER_BOUNDS_RADIUS_FLAT: undefined,
CYLINDER_HAS_RENDER_BOUNDS_ANGLE: undefined,
CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_EQUAL_ZERO: undefined,
CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_UNDER_HALF: undefined,
CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_OVER_HALF: undefined,
CYLINDER_HAS_SHAPE_BOUNDS_RADIUS: undefined,
CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT: undefined,
CYLINDER_HAS_SHAPE_BOUNDS_ANGLE: undefined,
CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY: undefined,
CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY: undefined,
CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED: undefined,
CYLINDER_INTERSECTION_INDEX_RADIUS_MAX: undefined,
CYLINDER_INTERSECTION_INDEX_RADIUS_MIN: undefined,
CYLINDER_INTERSECTION_INDEX_ANGLE: undefined,
};
this._shaderMaximumIntersectionsLength = 0; // not known until update
}
Object.defineProperties(VoxelCylinderShape.prototype, {
/**
* An oriented bounding box containing the bounded shape.
*
* @memberof VoxelCylinderShape.prototype
* @type {OrientedBoundingBox}
* @readonly
* @private
*/
orientedBoundingBox: {
get: function () {
return this._orientedBoundingBox;
},
},
/**
* A collection of planes used for the render bounds
* @memberof VoxelCylinderShape.prototype
* @type {VoxelBoundsCollection}
* @readonly
* @private
*/
renderBoundPlanes: {
get: function () {
return this._renderBoundPlanes;
},
},
/**
* A bounding sphere containing the bounded shape.
*
* @memberof VoxelCylinderShape.prototype
* @type {BoundingSphere}
* @readonly
* @private
*/
boundingSphere: {
get: function () {
return this._boundingSphere;
},
},
/**
* A transformation matrix containing the bounded shape.
*
* @memberof VoxelCylinderShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
boundTransform: {
get: function () {
return this._boundTransform;
},
},
/**
* A transformation matrix containing the shape, ignoring the bounds.
*
* @memberof VoxelCylinderShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
shapeTransform: {
get: function () {
return this._shapeTransform;
},
},
/**
* @memberof VoxelCylinderShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderUniforms: {
get: function () {
return this._shaderUniforms;
},
},
/**
* @memberof VoxelCylinderShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderDefines: {
get: function () {
return this._shaderDefines;
},
},
/**
* The maximum number of intersections against the shape for any ray direction.
* @private
* @memberof VoxelCylinderShape.prototype
* @type {number}
* @readonly
* @private
*/
this.shaderMaximumIntersectionsLength = 0; // not known until update
}
shaderMaximumIntersectionsLength: {
get: function () {
return this._shaderMaximumIntersectionsLength;
},
},
});
const scratchScale = new Cartesian3();
const scratchClipMinBounds = new Cartesian3();
const scratchClipMaxBounds = new Cartesian3();
const scratchRenderMinBounds = new Cartesian3();
const scratchRenderMaxBounds = new Cartesian3();
const scratchTransformPositionWorldToLocal = new Matrix4();
const scratchCameraPositionLocal = new Cartesian3();
const scratchCameraRadialPosition = new Cartesian2();
/**
* Update the shape's state.
@ -158,15 +228,15 @@ VoxelCylinderShape.prototype.update = function (
maxBounds = Cartesian3.clone(maxBounds, this._maxBounds);
const { DefaultMinBounds, DefaultMaxBounds } = VoxelCylinderShape;
const defaultAngleRange = DefaultMaxBounds.y - DefaultMinBounds.y;
const defaultAngleRangeHalf = 0.5 * defaultAngleRange;
const defaultAngleRange = DefaultMaxBounds.y - DefaultMinBounds.y; // == 2 * PI
const defaultAngleRangeHalf = 0.5 * defaultAngleRange; // == PI
const epsilonZeroScale = CesiumMath.EPSILON10;
const epsilonAngleDiscontinuity = CesiumMath.EPSILON3; // 0.001 radians = 0.05729578 degrees
const epsilonAngle = CesiumMath.EPSILON10;
// Clamp the bounds to the valid range
minBounds.x = Math.max(0.0, minBounds.x);
// TODO: require maxBounds.x >= minBounds.x ?
maxBounds.x = Math.max(0.0, maxBounds.x);
minBounds.y = CesiumMath.negativePiToPi(minBounds.y);
maxBounds.y = CesiumMath.negativePiToPi(maxBounds.y);
@ -174,6 +244,10 @@ VoxelCylinderShape.prototype.update = function (
clipMinBounds.y = CesiumMath.negativePiToPi(clipMinBounds.y);
clipMaxBounds.y = CesiumMath.negativePiToPi(clipMaxBounds.y);
// TODO: what does this do with partial volumes crossing the antimeridian?
// We could have minBounds.y = +PI/2 and maxBounds.y = -PI/2.
// Then clipMinBounds.y = +PI/4 and clipMaxBounds.y = -PI/4.
// This maximumByComponent would cancel the clipping.
const renderMinBounds = Cartesian3.maximumByComponent(
minBounds,
clipMinBounds,
@ -205,57 +279,35 @@ VoxelCylinderShape.prototype.update = function (
return false;
}
this.shapeTransform = Matrix4.clone(modelMatrix, this.shapeTransform);
// Update the render bounds planes
const renderBoundPlanes = this._renderBoundPlanes;
renderBoundPlanes.get(0).distance = renderMinBounds.z;
renderBoundPlanes.get(1).distance = -renderMaxBounds.z;
this.orientedBoundingBox = getCylinderChunkObb(
this._shapeTransform = Matrix4.clone(modelMatrix, this._shapeTransform);
this._orientedBoundingBox = getCylinderChunkObb(
renderMinBounds,
renderMaxBounds,
this.shapeTransform,
this.orientedBoundingBox,
this._shapeTransform,
this._orientedBoundingBox,
);
this.boundTransform = Matrix4.fromRotationTranslation(
this.orientedBoundingBox.halfAxes,
this.orientedBoundingBox.center,
this.boundTransform,
this._boundTransform = Matrix4.fromRotationTranslation(
this._orientedBoundingBox.halfAxes,
this._orientedBoundingBox.center,
this._boundTransform,
);
this.boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this.orientedBoundingBox,
this.boundingSphere,
this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this._orientedBoundingBox,
this._boundingSphere,
);
const shapeIsDefaultRadius =
minBounds.x === DefaultMinBounds.x && maxBounds.x === DefaultMaxBounds.x;
const shapeIsAngleReversed = maxBounds.y < minBounds.y;
const shapeAngleRange =
maxBounds.y - minBounds.y + shapeIsAngleReversed * defaultAngleRange;
const shapeIsAngleRegular =
shapeAngleRange > defaultAngleRangeHalf + epsilonAngle &&
shapeAngleRange < defaultAngleRange - epsilonAngle;
const shapeIsAngleFlipped =
shapeAngleRange < defaultAngleRangeHalf - epsilonAngle;
const shapeIsAngleRangeHalf =
shapeAngleRange >= defaultAngleRangeHalf - epsilonAngle &&
shapeAngleRange <= defaultAngleRangeHalf + epsilonAngle;
const shapeHasAngle =
shapeIsAngleRegular || shapeIsAngleFlipped || shapeIsAngleRangeHalf;
const shapeIsMinAngleDiscontinuity = CesiumMath.equalsEpsilon(
minBounds.y,
DefaultMinBounds.y,
undefined,
epsilonAngleDiscontinuity,
);
const shapeIsMaxAngleDiscontinuity = CesiumMath.equalsEpsilon(
maxBounds.y,
DefaultMaxBounds.y,
undefined,
epsilonAngleDiscontinuity,
);
const shapeIsDefaultHeight =
minBounds.z === DefaultMinBounds.z && maxBounds.z === DefaultMaxBounds.z;
const renderIsDefaultMinRadius = renderMinBounds.x === DefaultMinBounds.x;
const renderIsAngleReversed = renderMaxBounds.y < renderMinBounds.y;
const renderAngleRange =
renderMaxBounds.y -
@ -271,7 +323,8 @@ VoxelCylinderShape.prototype.update = function (
const renderHasAngle =
renderIsAngleRegular || renderIsAngleFlipped || renderIsAngleRangeZero;
const { shaderUniforms, shaderDefines } = this;
const shaderUniforms = this._shaderUniforms;
const shaderDefines = this._shaderDefines;
// To keep things simple, clear the defines every time
for (const key in shaderDefines) {
@ -286,7 +339,11 @@ VoxelCylinderShape.prototype.update = function (
shaderDefines["CYLINDER_INTERSECTION_INDEX_RADIUS_MAX"] = intersectionCount;
intersectionCount += 1;
if (!renderIsDefaultMinRadius) {
if (shapeAngleRange < defaultAngleRange - epsilonAngle) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE"] = true;
}
if (renderMinBounds.x !== DefaultMinBounds.x) {
shaderDefines["CYLINDER_HAS_RENDER_BOUNDS_RADIUS_MIN"] = true;
shaderDefines["CYLINDER_INTERSECTION_INDEX_RADIUS_MIN"] = intersectionCount;
intersectionCount += 1;
@ -300,65 +357,32 @@ VoxelCylinderShape.prototype.update = function (
if (renderMinBounds.x === renderMaxBounds.x) {
shaderDefines["CYLINDER_HAS_RENDER_BOUNDS_RADIUS_FLAT"] = true;
}
if (!shapeIsDefaultRadius) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_RADIUS"] = true;
// delerp(radius, minRadius, maxRadius)
// (radius - minRadius) / (maxRadius - minRadius)
// radius / (maxRadius - minRadius) - minRadius / (maxRadius - minRadius)
// scale = 1.0 / (maxRadius - minRadius)
// offset = -minRadius / (maxRadius - minRadius)
// offset = minRadius / (minRadius - maxRadius)
const radiusRange = maxBounds.x - minBounds.x;
let scale = 0.0;
let offset = 1.0;
if (radiusRange !== 0.0) {
scale = 1.0 / radiusRange;
offset = -minBounds.x / radiusRange;
}
shaderUniforms.cylinderUvToShapeUvRadius = Cartesian2.fromElements(
scale,
offset,
shaderUniforms.cylinderUvToShapeUvRadius,
);
const radiusRange = maxBounds.x - minBounds.x;
let radialScale = 0.0;
let radialOffset = 1.0;
if (radiusRange !== 0.0) {
radialScale = 1.0 / radiusRange;
radialOffset = -minBounds.x * radialScale;
}
if (!shapeIsDefaultHeight) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT"] = true;
// delerp(heightUv, minHeightUv, maxHeightUv)
// (heightUv - minHeightUv) / (maxHeightUv - minHeightUv)
// heightUv / (maxHeightUv - minHeightUv) - minHeightUv / (maxHeightUv - minHeightUv)
// scale = 1.0 / (maxHeightUv - minHeightUv)
// scale = 1.0 / ((maxHeight * 0.5 + 0.5) - (minHeight * 0.5 + 0.5))
// scale = 2.0 / (maxHeight - minHeight)
// offset = -minHeightUv / (maxHeightUv - minHeightUv)
// offset = -minHeightUv / ((maxHeight * 0.5 + 0.5) - (minHeight * 0.5 + 0.5))
// offset = -2.0 * (minHeight * 0.5 + 0.5) / (maxHeight - minHeight)
// offset = -(minHeight + 1.0) / (maxHeight - minHeight)
// offset = (minHeight + 1.0) / (minHeight - maxHeight)
const heightRange = maxBounds.z - minBounds.z;
let scale = 0.0;
let offset = 1.0;
if (heightRange !== 0.0) {
scale = 2.0 / heightRange;
offset = -(minBounds.z + 1.0) / heightRange;
}
shaderUniforms.cylinderUvToShapeUvHeight = Cartesian2.fromElements(
scale,
offset,
shaderUniforms.cylinderUvToShapeUvHeight,
);
}
shaderUniforms.cylinderRenderHeightMinMax = Cartesian2.fromElements(
renderMinBounds.z,
renderMaxBounds.z,
shaderUniforms.cylinderRenderHeightMinMax,
shaderUniforms.cylinderLocalToShapeUvRadius = Cartesian2.fromElements(
radialScale,
radialOffset,
shaderUniforms.cylinderLocalToShapeUvRadius,
);
if (shapeIsAngleReversed) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED"] = true;
const heightRange = maxBounds.z - minBounds.z; // Default 2.0
let heightScale = 0.0;
let heightOffset = 1.0;
if (heightRange !== 0.0) {
heightScale = 1.0 / heightRange;
heightOffset = -minBounds.z * heightScale;
}
shaderUniforms.cylinderLocalToShapeUvHeight = Cartesian2.fromElements(
heightScale,
heightOffset,
shaderUniforms.cylinderLocalToShapeUvHeight,
);
if (renderHasAngle) {
shaderDefines["CYLINDER_HAS_RENDER_BOUNDS_ANGLE"] = true;
@ -382,64 +406,151 @@ VoxelCylinderShape.prototype.update = function (
);
}
if (shapeHasAngle) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE"] = true;
if (shapeIsMinAngleDiscontinuity) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY"] = true;
}
if (shapeIsMaxAngleDiscontinuity) {
shaderDefines["CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY"] = true;
}
const uvMinAngle = (minBounds.y - DefaultMinBounds.y) / defaultAngleRange;
const uvMaxAngle = (maxBounds.y - DefaultMinBounds.y) / defaultAngleRange;
const uvAngleRangeZero = 1.0 - shapeAngleRange / defaultAngleRange;
const uvMinAngle = (minBounds.y - DefaultMinBounds.y) / defaultAngleRange;
const uvMaxAngle = (maxBounds.y - DefaultMinBounds.y) / defaultAngleRange;
const uvAngleRangeZero = 1.0 - shapeAngleRange / defaultAngleRange;
// Translate the origin of UV angles (in [0,1]) to the center of the unoccupied space
const uvAngleRangeOrigin = (uvMaxAngle + 0.5 * uvAngleRangeZero) % 1.0;
shaderUniforms.cylinderShapeUvAngleRangeOrigin = uvAngleRangeOrigin;
shaderUniforms.cylinderShapeUvAngleMinMax = Cartesian2.fromElements(
uvMinAngle,
uvMaxAngle,
shaderUniforms.cylinderShapeUvAngleMinMax,
if (shapeAngleRange <= epsilonAngle) {
shaderUniforms.cylinderLocalToShapeUvAngle = Cartesian2.fromElements(
0.0,
1.0,
shaderUniforms.cylinderLocalToShapeUvAngle,
);
} else {
const scale = defaultAngleRange / shapeAngleRange;
const shiftedMinAngle = uvMinAngle - uvAngleRangeOrigin;
const offset = -scale * (shiftedMinAngle - Math.floor(shiftedMinAngle));
shaderUniforms.cylinderLocalToShapeUvAngle = Cartesian2.fromElements(
scale,
offset,
shaderUniforms.cylinderLocalToShapeUvAngle,
);
shaderUniforms.cylinderShapeUvAngleRangeZeroMid =
(uvMaxAngle + 0.5 * uvAngleRangeZero) % 1.0;
// delerp(angleUv, uvMinAngle, uvMaxAngle)
// (angelUv - uvMinAngle) / (uvMaxAngle - uvMinAngle)
// angleUv / (uvMaxAngle - uvMinAngle) - uvMinAngle / (uvMaxAngle - uvMinAngle)
// scale = 1.0 / (uvMaxAngle - uvMinAngle)
// scale = 1.0 / (((maxAngle - pi) / (2.0 * pi)) - ((minAngle - pi) / (2.0 * pi)))
// scale = 2.0 * pi / (maxAngle - minAngle)
// offset = -uvMinAngle / (uvMaxAngle - uvMinAngle)
// offset = -((minAngle - pi) / (2.0 * pi)) / (((maxAngle - pi) / (2.0 * pi)) - ((minAngle - pi) / (2.0 * pi)))
// offset = -(minAngle - pi) / (maxAngle - minAngle)
if (shapeAngleRange <= epsilonAngle) {
shaderUniforms.cylinderUvToShapeUvAngle = Cartesian2.fromElements(
0.0,
1.0,
shaderUniforms.cylinderUvToShapeUvAngle,
);
} else {
const scale = defaultAngleRange / shapeAngleRange;
const offset = -(minBounds.y - DefaultMinBounds.y) / shapeAngleRange;
shaderUniforms.cylinderUvToShapeUvAngle = Cartesian2.fromElements(
scale,
offset,
shaderUniforms.cylinderUvToShapeUvAngle,
);
}
}
this.shaderMaximumIntersectionsLength = intersectionCount;
this._shaderMaximumIntersectionsLength = intersectionCount;
return true;
};
const scratchRotateRtuToLocal = new Matrix3();
const scratchRtuRotation = new Matrix3();
const scratchTransformPositionViewToLocal = new Matrix4();
/**
* Update any view-dependent transforms.
* @private
* @param {FrameState} frameState The frame state.
*/
VoxelCylinderShape.prototype.updateViewTransforms = function (frameState) {
const shaderUniforms = this._shaderUniforms;
// 1. Update camera position in cylindrical coordinates
const transformPositionWorldToLocal = Matrix4.inverse(
this._shapeTransform,
scratchTransformPositionWorldToLocal,
);
const cameraPositionLocal = Matrix4.multiplyByPoint(
transformPositionWorldToLocal,
frameState.camera.positionWC,
scratchCameraPositionLocal,
);
shaderUniforms.cameraShapePosition = Cartesian3.fromElements(
Cartesian2.magnitude(cameraPositionLocal),
Math.atan2(cameraPositionLocal.y, cameraPositionLocal.x),
cameraPositionLocal.z,
shaderUniforms.cameraShapePosition,
);
// 2. Find radial, tangent, and up components at camera position
const cameraRadialDirection = Cartesian2.normalize(
Cartesian2.fromCartesian3(cameraPositionLocal, scratchCameraRadialPosition),
scratchCameraRadialPosition,
);
// As row vectors, the radial, tangent, and up vectors constitute a rotation matrix from local to RTU.
const rotateLocalToRtu = Matrix3.fromRowMajorArray(
[
cameraRadialDirection.x,
cameraRadialDirection.y,
0.0,
-cameraRadialDirection.y,
cameraRadialDirection.x,
0.0,
0.0,
0.0,
1.0,
],
scratchRotateRtuToLocal,
);
// 3. Get rotation from eye to local coordinates
const transformPositionViewToWorld =
frameState.context.uniformState.inverseView;
const transformPositionViewToLocal = Matrix4.multiplyTransformation(
transformPositionWorldToLocal,
transformPositionViewToWorld,
scratchTransformPositionViewToLocal,
);
const transformDirectionViewToLocal = Matrix4.getMatrix3(
transformPositionViewToLocal,
scratchRtuRotation,
);
// 4. Multiply to get rotation from eye to RTU coordinates
shaderUniforms.cylinderEcToRadialTangentUp = Matrix3.multiply(
rotateLocalToRtu,
transformDirectionViewToLocal,
shaderUniforms.cylinderEcToRadialTangentUp,
);
};
/**
* Convert a UV coordinate to the shape's UV space.
* @private
* @param {Cartesian3} positionLocal The local coordinate to convert.
* @param {Cartesian3} result The Cartesian3 to store the result in.
* @returns {Cartesian3} The converted UV coordinate.
*/
VoxelCylinderShape.prototype.convertLocalToShapeUvSpace = function (
positionLocal,
result,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("positionLocal", positionLocal);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
let radius = Math.hypot(positionLocal.x, positionLocal.y);
let angle = Math.atan2(positionLocal.y, positionLocal.x);
let height = positionLocal.z;
const {
cylinderLocalToShapeUvRadius,
cylinderLocalToShapeUvAngle,
cylinderShapeUvAngleRangeOrigin,
cylinderLocalToShapeUvHeight,
} = this._shaderUniforms;
radius =
radius * cylinderLocalToShapeUvRadius.x + cylinderLocalToShapeUvRadius.y;
// Convert angle to a "UV" in [0,1] with 0 defined at the center of the unoccupied space.
angle = (angle + Math.PI) / (2.0 * Math.PI);
angle -= cylinderShapeUvAngleRangeOrigin;
angle = angle - Math.floor(angle);
// Scale and shift so [0,1] covers the occupied space.
angle = angle * cylinderLocalToShapeUvAngle.x + cylinderLocalToShapeUvAngle.y;
height =
height * cylinderLocalToShapeUvHeight.x + cylinderLocalToShapeUvHeight.y;
return Cartesian3.fromElements(radius, angle, height, result);
};
const scratchMinBounds = new Cartesian3();
const scratchMaxBounds = new Cartesian3();
/**
* Computes an oriented bounding box for a specified tile.
* The update function must be called before calling this function.
* @private
* @param {number} tileLevel The tile's level.
* @param {number} tileX The tile's x coordinate.
@ -484,7 +595,7 @@ VoxelCylinderShape.prototype.computeOrientedBoundingBoxForTile = function (
return getCylinderChunkObb(
tileMinBounds,
tileMaxBounds,
this.shapeTransform,
this._shapeTransform,
result,
);
};
@ -493,7 +604,6 @@ const sampleSizeScratch = new Cartesian3();
/**
* Computes an oriented bounding box for a specified sample within a specified tile.
* The update function must be called before calling this function.
* @private
* @param {SpatialNode} spatialNode The spatial node containing the sample
* @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding
@ -557,7 +667,7 @@ VoxelCylinderShape.prototype.computeOrientedBoundingBoxForSample = function (
return getCylinderChunkObb(
sampleMinBounds,
sampleMaxBounds,
this.shapeTransform,
this._shapeTransform,
result,
);
};
@ -639,6 +749,7 @@ function computeLooseOrientedBoundingBox(matrix, result) {
return OrientedBoundingBox.fromPoints(corners, result);
}
const scratchBoxScale = new Cartesian3();
/**
* Computes an {@link OrientedBoundingBox} for a subregion of the shape.
*
@ -720,7 +831,7 @@ function getCylinderChunkObb(chunkMinBounds, chunkMaxBounds, matrix, result) {
extentX,
extentY,
extentZ,
scratchScale,
scratchBoxScale,
);
const scaleMatrix = Matrix4.fromScale(scale, scratchScaleMatrix);

View File

@ -1,6 +1,8 @@
import defined from "../Core/defined.js";
import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import Check from "../Core/Check.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import CesiumMath from "../Core/Math.js";
@ -8,6 +10,7 @@ import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import Rectangle from "../Core/Rectangle.js";
import Transforms from "../Core/Transforms.js";
/**
* An ellipsoid {@link VoxelShape}.
@ -23,41 +26,10 @@ import Rectangle from "../Core/Rectangle.js";
* @private
*/
function VoxelEllipsoidShape() {
/**
* An oriented bounding box containing the bounded shape.
* The update function must be called before accessing this value.
* @type {OrientedBoundingBox}
* @readonly
* @private
*/
this.orientedBoundingBox = new OrientedBoundingBox();
/**
* A bounding sphere containing the bounded shape.
* The update function must be called before accessing this value.
* @type {BoundingSphere}
* @readonly
* @private
*/
this.boundingSphere = new BoundingSphere();
/**
* A transformation matrix containing the bounded shape.
* The update function must be called before accessing this value.
* @type {Matrix4}
* @readonly
* @private
*/
this.boundTransform = new Matrix4();
/**
* A transformation matrix containing the shape, ignoring the bounds.
* The update function must be called before accessing this value.
* @type {Matrix4}
* @readonly
* @private
*/
this.shapeTransform = new Matrix4();
this._orientedBoundingBox = new OrientedBoundingBox();
this._boundingSphere = new BoundingSphere();
this._boundTransform = new Matrix4();
this._shapeTransform = new Matrix4();
/**
* @type {Rectangle}
@ -95,31 +67,25 @@ function VoxelEllipsoidShape() {
*/
this._rotation = new Matrix3();
/**
* @type {Object<string, any>}
* @readonly
* @private
*/
this.shaderUniforms = {
ellipsoidRadiiUv: new Cartesian3(),
this._shaderUniforms = {
cameraPositionCartographic: new Cartesian3(),
ellipsoidEcToEastNorthUp: new Matrix3(),
ellipsoidRadii: new Cartesian3(),
eccentricitySquared: 0.0,
evoluteScale: new Cartesian2(),
ellipsoidInverseRadiiSquaredUv: new Cartesian3(),
ellipsoidCurvatureAtLatitude: new Cartesian2(),
ellipsoidInverseRadiiSquared: new Cartesian3(),
ellipsoidRenderLongitudeMinMax: new Cartesian2(),
ellipsoidShapeUvLongitudeRangeOrigin: 0.0,
ellipsoidShapeUvLongitudeMinMaxMid: new Cartesian3(),
ellipsoidUvToShapeUvLongitude: new Cartesian2(),
ellipsoidUvToShapeUvLatitude: new Cartesian2(),
ellipsoidLocalToShapeUvLongitude: new Cartesian2(),
ellipsoidLocalToShapeUvLatitude: new Cartesian2(),
ellipsoidRenderLatitudeSinMinMax: new Cartesian2(),
ellipsoidInverseHeightDifferenceUv: 0.0,
ellipsoidInverseHeightDifference: 0.0,
clipMinMaxHeight: new Cartesian2(),
};
/**
* @type {Object<string, any>}
* @readonly
* @private
*/
this.shaderDefines = {
this._shaderDefines = {
ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE: undefined,
ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_RANGE_EQUAL_ZERO: undefined,
ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_RANGE_UNDER_HALF: undefined,
@ -142,14 +108,103 @@ function VoxelEllipsoidShape() {
ELLIPSOID_INTERSECTION_INDEX_HEIGHT_MIN: undefined,
};
this._shaderMaximumIntersectionsLength = 0; // not known until update
}
Object.defineProperties(VoxelEllipsoidShape.prototype, {
/**
* An oriented bounding box containing the bounded shape.
*
* @memberof VoxelEllipsoidShape.prototype
* @type {OrientedBoundingBox}
* @readonly
* @private
*/
orientedBoundingBox: {
get: function () {
return this._orientedBoundingBox;
},
},
/**
* A bounding sphere containing the bounded shape.
*
* @memberof VoxelEllipsoidShape.prototype
* @type {BoundingSphere}
* @readonly
* @private
*/
boundingSphere: {
get: function () {
return this._boundingSphere;
},
},
/**
* A transformation matrix containing the bounded shape.
*
* @memberof VoxelEllipsoidShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
boundTransform: {
get: function () {
return this._boundTransform;
},
},
/**
* A transformation matrix containing the shape, ignoring the bounds.
*
* @memberof VoxelEllipsoidShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
shapeTransform: {
get: function () {
return this._shapeTransform;
},
},
/**
* @memberof VoxelEllipsoidShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderUniforms: {
get: function () {
return this._shaderUniforms;
},
},
/**
* @memberof VoxelEllipsoidShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderDefines: {
get: function () {
return this._shaderDefines;
},
},
/**
* The maximum number of intersections against the shape for any ray direction.
* @memberof VoxelEllipsoidShape.prototype
* @type {number}
* @readonly
* @private
*/
this.shaderMaximumIntersectionsLength = 0; // not known until update
}
shaderMaximumIntersectionsLength: {
get: function () {
return this._shaderMaximumIntersectionsLength;
},
},
});
const scratchActualMinBounds = new Cartesian3();
const scratchShapeMinBounds = new Cartesian3();
@ -159,7 +214,6 @@ const scratchClipMaxBounds = new Cartesian3();
const scratchRenderMinBounds = new Cartesian3();
const scratchRenderMaxBounds = new Cartesian3();
const scratchScale = new Cartesian3();
const scratchRotationScale = new Matrix3();
const scratchShapeOuterExtent = new Cartesian3();
const scratchRenderOuterExtent = new Cartesian3();
const scratchRenderRectangle = new Rectangle();
@ -250,7 +304,6 @@ VoxelEllipsoidShape.prototype.update = function (
),
scratchShapeOuterExtent,
);
const shapeMaxExtent = Cartesian3.maximumComponent(shapeOuterExtent);
const renderOuterExtent = Cartesian3.add(
radii,
@ -300,31 +353,31 @@ VoxelEllipsoidShape.prototype.update = function (
scratchRenderRectangle,
);
this.orientedBoundingBox = getEllipsoidChunkObb(
this._orientedBoundingBox = getEllipsoidChunkObb(
renderRectangle,
renderMinBounds.z,
renderMaxBounds.z,
this._ellipsoid,
this._translation,
this._rotation,
this.orientedBoundingBox,
this._orientedBoundingBox,
);
this.shapeTransform = Matrix4.fromRotationTranslation(
Matrix3.setScale(this._rotation, shapeOuterExtent, scratchRotationScale),
this._shapeTransform = Matrix4.fromRotationTranslation(
this._rotation,
this._translation,
this.shapeTransform,
this._shapeTransform,
);
this.boundTransform = Matrix4.fromRotationTranslation(
this.orientedBoundingBox.halfAxes,
this.orientedBoundingBox.center,
this.boundTransform,
this._boundTransform = Matrix4.fromRotationTranslation(
this._orientedBoundingBox.halfAxes,
this._orientedBoundingBox.center,
this._boundTransform,
);
this.boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this.orientedBoundingBox,
this.boundingSphere,
this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this._orientedBoundingBox,
this._boundingSphere,
);
// Longitude
@ -415,7 +468,8 @@ VoxelEllipsoidShape.prototype.update = function (
shapeIsLatitudeMinOverHalf;
const shapeHasLatitude = shapeHasLatitudeMax || shapeHasLatitudeMin;
const { shaderUniforms, shaderDefines } = this;
const shaderUniforms = this._shaderUniforms;
const shaderDefines = this._shaderDefines;
// To keep things simple, clear the defines every time
for (const key in shaderDefines) {
@ -424,30 +478,28 @@ VoxelEllipsoidShape.prototype.update = function (
}
}
// The ellipsoid radii scaled to [0,1]. The max ellipsoid radius will be 1.0 and others will be less.
shaderUniforms.ellipsoidRadiiUv = Cartesian3.divideByScalar(
shaderUniforms.ellipsoidRadii = Cartesian3.clone(
shapeOuterExtent,
shapeMaxExtent,
shaderUniforms.ellipsoidRadiiUv,
shaderUniforms.ellipsoidRadii,
);
const { x: radiiUvX, z: radiiUvZ } = shaderUniforms.ellipsoidRadiiUv;
const axisRatio = radiiUvZ / radiiUvX;
const { x: radiiX, z: radiiZ } = shaderUniforms.ellipsoidRadii;
const axisRatio = radiiZ / radiiX;
shaderUniforms.eccentricitySquared = 1.0 - axisRatio * axisRatio;
shaderUniforms.evoluteScale = Cartesian2.fromElements(
(radiiUvX * radiiUvX - radiiUvZ * radiiUvZ) / radiiUvX,
(radiiUvZ * radiiUvZ - radiiUvX * radiiUvX) / radiiUvZ,
(radiiX * radiiX - radiiZ * radiiZ) / radiiX,
(radiiZ * radiiZ - radiiX * radiiX) / radiiZ,
shaderUniforms.evoluteScale,
);
// Used to compute geodetic surface normal.
shaderUniforms.ellipsoidInverseRadiiSquaredUv = Cartesian3.divideComponents(
shaderUniforms.ellipsoidInverseRadiiSquared = Cartesian3.divideComponents(
Cartesian3.ONE,
Cartesian3.multiplyComponents(
shaderUniforms.ellipsoidRadiiUv,
shaderUniforms.ellipsoidRadiiUv,
shaderUniforms.ellipsoidInverseRadiiSquaredUv,
shaderUniforms.ellipsoidRadii,
shaderUniforms.ellipsoidRadii,
shaderUniforms.ellipsoidInverseRadiiSquared,
),
shaderUniforms.ellipsoidInverseRadiiSquaredUv,
shaderUniforms.ellipsoidInverseRadiiSquared,
);
// Keep track of how many intersections there are going to be.
@ -460,16 +512,16 @@ VoxelEllipsoidShape.prototype.update = function (
intersectionCount += 1;
shaderUniforms.clipMinMaxHeight = Cartesian2.fromElements(
(renderMinBounds.z - shapeMaxBounds.z) / shapeMaxExtent,
(renderMaxBounds.z - shapeMaxBounds.z) / shapeMaxExtent,
renderMinBounds.z - shapeMaxBounds.z,
renderMaxBounds.z - shapeMaxBounds.z,
shaderUniforms.clipMinMaxHeight,
);
// The percent of space that is between the inner and outer ellipsoid.
const thickness = (shapeMaxBounds.z - shapeMinBounds.z) / shapeMaxExtent;
shaderUniforms.ellipsoidInverseHeightDifferenceUv = 1.0 / thickness;
const thickness = shapeMaxBounds.z - shapeMinBounds.z;
shaderUniforms.ellipsoidInverseHeightDifference = 1.0 / thickness;
if (shapeMinBounds.z === shapeMaxBounds.z) {
shaderUniforms.ellipsoidInverseHeightDifferenceUv = 0.0;
shaderUniforms.ellipsoidInverseHeightDifference = 0.0;
}
// Intersects a wedge for the min and max longitude.
@ -501,36 +553,39 @@ VoxelEllipsoidShape.prototype.update = function (
if (shapeHasLongitude) {
shaderDefines["ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE"] = true;
const shapeIsLongitudeReversed = shapeMaxBounds.x < shapeMinBounds.x;
const uvShapeMinLongitude =
(shapeMinBounds.x - DefaultMinBounds.x) / defaultLongitudeRange;
const uvShapeMaxLongitude =
(shapeMaxBounds.x - DefaultMinBounds.x) / defaultLongitudeRange;
const uvLongitudeRangeZero =
1.0 - shapeLongitudeRange / defaultLongitudeRange;
// Translate the origin of UV angles (in [0,1]) to the center of the unoccupied space
const uvLongitudeRangeOrigin =
(uvShapeMaxLongitude + 0.5 * uvLongitudeRangeZero) % 1.0;
shaderUniforms.ellipsoidShapeUvLongitudeRangeOrigin =
uvLongitudeRangeOrigin;
const shapeIsLongitudeReversed = shapeMaxBounds.x < shapeMinBounds.x;
if (shapeIsLongitudeReversed) {
shaderDefines["ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED"] =
true;
}
// delerp(longitudeUv, minLongitudeUv, maxLongitudeUv)
// (longitudeUv - minLongitudeUv) / (maxLongitudeUv - minLongitudeUv)
// longitudeUv / (maxLongitudeUv - minLongitudeUv) - minLongitudeUv / (maxLongitudeUv - minLongitudeUv)
// scale = 1.0 / (maxLongitudeUv - minLongitudeUv)
// scale = 1.0 / (((maxLongitude - pi) / (2.0 * pi)) - ((minLongitude - pi) / (2.0 * pi)))
// scale = 2.0 * pi / (maxLongitude - minLongitude)
// offset = -minLongitudeUv / (maxLongitudeUv - minLongitudeUv)
// offset = -((minLongitude - pi) / (2.0 * pi)) / (((maxLongitude - pi) / (2.0 * pi)) - ((minLongitude - pi) / (2.0 * pi)))
// offset = -(minLongitude - pi) / (maxLongitude - minLongitude)
if (shapeLongitudeRange <= epsilonLongitude) {
shaderUniforms.ellipsoidUvToShapeUvLongitude = Cartesian2.fromElements(
shaderUniforms.ellipsoidLocalToShapeUvLongitude = Cartesian2.fromElements(
0.0,
1.0,
shaderUniforms.ellipsoidUvToShapeUvLongitude,
shaderUniforms.ellipsoidLocalToShapeUvLongitude,
);
} else {
const scale = defaultLongitudeRange / shapeLongitudeRange;
const shiftedMinLongitude = uvShapeMinLongitude - uvLongitudeRangeOrigin;
const offset =
-(shapeMinBounds.x - DefaultMinBounds.x) / shapeLongitudeRange;
shaderUniforms.ellipsoidUvToShapeUvLongitude = Cartesian2.fromElements(
-scale * (shiftedMinLongitude - Math.floor(shiftedMinLongitude));
shaderUniforms.ellipsoidLocalToShapeUvLongitude = Cartesian2.fromElements(
scale,
offset,
shaderUniforms.ellipsoidUvToShapeUvLongitude,
shaderUniforms.ellipsoidLocalToShapeUvLongitude,
);
}
}
@ -630,45 +685,92 @@ VoxelEllipsoidShape.prototype.update = function (
if (shapeHasLatitude) {
shaderDefines["ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE"] = true;
// delerp(latitudeUv, minLatitudeUv, maxLatitudeUv)
// (latitudeUv - minLatitudeUv) / (maxLatitudeUv - minLatitudeUv)
// latitudeUv / (maxLatitudeUv - minLatitudeUv) - minLatitudeUv / (maxLatitudeUv - minLatitudeUv)
// scale = 1.0 / (maxLatitudeUv - minLatitudeUv)
// scale = 1.0 / (((maxLatitude - pi) / (2.0 * pi)) - ((minLatitude - pi) / (2.0 * pi)))
// scale = 2.0 * pi / (maxLatitude - minLatitude)
// offset = -minLatitudeUv / (maxLatitudeUv - minLatitudeUv)
// offset = -((minLatitude - -pi) / (2.0 * pi)) / (((maxLatitude - pi) / (2.0 * pi)) - ((minLatitude - pi) / (2.0 * pi)))
// offset = -(minLatitude - -pi) / (maxLatitude - minLatitude)
// offset = (-pi - minLatitude) / (maxLatitude - minLatitude)
if (shapeLatitudeRange < epsilonLatitude) {
shaderUniforms.ellipsoidUvToShapeUvLatitude = Cartesian2.fromElements(
shaderUniforms.ellipsoidLocalToShapeUvLatitude = Cartesian2.fromElements(
0.0,
1.0,
shaderUniforms.ellipsoidUvToShapeUvLatitude,
shaderUniforms.ellipsoidLocalToShapeUvLatitude,
);
} else {
const defaultLatitudeRange = DefaultMaxBounds.y - DefaultMinBounds.y;
const scale = defaultLatitudeRange / shapeLatitudeRange;
const offset =
(DefaultMinBounds.y - shapeMinBounds.y) / shapeLatitudeRange;
shaderUniforms.ellipsoidUvToShapeUvLatitude = Cartesian2.fromElements(
shaderUniforms.ellipsoidLocalToShapeUvLatitude = Cartesian2.fromElements(
scale,
offset,
shaderUniforms.ellipsoidUvToShapeUvLatitude,
shaderUniforms.ellipsoidLocalToShapeUvLatitude,
);
}
}
this.shaderMaximumIntersectionsLength = intersectionCount;
this._shaderMaximumIntersectionsLength = intersectionCount;
return true;
};
const scratchCameraPositionCartographic = new Cartographic();
const surfacePositionScratch = new Cartesian3();
const enuTransformScratch = new Matrix4();
const enuRotationScratch = new Matrix3();
/**
* Update any view-dependent transforms.
* @private
* @param {FrameState} frameState The frame state.
*/
VoxelEllipsoidShape.prototype.updateViewTransforms = function (frameState) {
const shaderUniforms = this._shaderUniforms;
const ellipsoid = this._ellipsoid;
// TODO: incorporate modelMatrix or shapeTransform here?
const cameraWC = frameState.camera.positionWC;
const cameraPositionCartographic = ellipsoid.cartesianToCartographic(
cameraWC,
scratchCameraPositionCartographic,
);
Cartesian3.fromElements(
cameraPositionCartographic.longitude,
cameraPositionCartographic.latitude,
cameraPositionCartographic.height,
shaderUniforms.cameraPositionCartographic,
);
// TODO: incorporate modelMatrix here?
const surfacePosition = Cartesian3.fromRadians(
cameraPositionCartographic.longitude,
cameraPositionCartographic.latitude,
0.0,
ellipsoid,
surfacePositionScratch,
);
shaderUniforms.ellipsoidCurvatureAtLatitude = ellipsoid.getLocalCurvature(
surfacePosition,
shaderUniforms.ellipsoidCurvatureAtLatitude,
);
const enuToWorld = Transforms.eastNorthUpToFixedFrame(
surfacePosition,
ellipsoid,
enuTransformScratch,
);
const rotateEnuToWorld = Matrix4.getRotation(enuToWorld, enuRotationScratch);
const rotateWorldToView = frameState.context.uniformState.viewRotation;
const rotateEnuToView = Matrix3.multiply(
rotateWorldToView,
rotateEnuToWorld,
enuRotationScratch,
);
// Inverse is the transpose since it's a pure rotation.
shaderUniforms.ellipsoidEcToEastNorthUp = Matrix3.transpose(
rotateEnuToView,
shaderUniforms.ellipsoidEcToEastNorthUp,
);
};
const scratchRectangle = new Rectangle();
/**
* Computes an oriented bounding box for a specified tile.
* The update function must be called before calling this function.
* @private
* @param {number} tileLevel The tile's level.
* @param {number} tileX The tile's x coordinate.
@ -732,13 +834,194 @@ VoxelEllipsoidShape.prototype.computeOrientedBoundingBoxForTile = function (
);
};
const scratchQuadrantPosition = new Cartesian2();
const scratchInverseRadii = new Cartesian2();
const scratchEllipseTrigs = new Cartesian2();
const scratchEllipseGuess = new Cartesian2();
const scratchEvolute = new Cartesian2();
const scratchQ = new Cartesian2();
/**
* Find the nearest point on an ellipse and its radius.
* @param {Cartesian2} position
* @param {Cartesian2} radii
* @param {Cartesian2} evoluteScale
* @param {Cartesian3} result The Cartesian3 to store the result in. .x and .y components contain the nearest point on the ellipse, .z contains the local radius of curvature.
* @returns {Cartesian3} The nearest point on the ellipse and its radius.
* @private
*/
function nearestPointAndRadiusOnEllipse(position, radii, evoluteScale, result) {
// Map to the first quadrant
const p = Cartesian2.abs(position, scratchQuadrantPosition);
const inverseRadii = Cartesian2.fromElements(
1.0 / radii.x,
1.0 / radii.y,
scratchInverseRadii,
);
// We describe the ellipse parametrically: v = radii * vec2(cos(t), sin(t))
// but store the cos and sin of t in a vec2 for efficiency.
// Initial guess: t = pi/4
let tTrigs = Cartesian2.fromElements(
Math.SQRT1_2,
Math.SQRT1_2,
scratchEllipseTrigs,
);
// TODO: too much duplication. Move v and evolute declarations inside loop?
// Initial guess of point on ellipsoid
let v = Cartesian2.multiplyComponents(radii, tTrigs, scratchEllipseGuess);
// Center of curvature of the ellipse at v
let evolute = Cartesian2.fromElements(
evoluteScale.x * tTrigs.x * tTrigs.x * tTrigs.x,
evoluteScale.y * tTrigs.y * tTrigs.y * tTrigs.y,
scratchEvolute,
);
for (let i = 0; i < 3; ++i) {
// Find the (approximate) intersection of p - evolute with the ellipsoid.
const distance = Cartesian2.magnitude(
Cartesian2.subtract(v, evolute, scratchQ),
);
const direction = Cartesian2.normalize(
Cartesian2.subtract(p, evolute, scratchQ),
scratchQ,
);
const q = Cartesian2.multiplyByScalar(direction, distance, scratchQ);
// Update the estimate of t
tTrigs = Cartesian2.multiplyComponents(
Cartesian2.add(q, evolute, scratchEllipseTrigs),
inverseRadii,
scratchEllipseTrigs,
);
tTrigs = Cartesian2.normalize(
Cartesian2.clamp(
tTrigs,
Cartesian2.ZERO,
Cartesian2.ONE,
scratchEllipseTrigs,
),
scratchEllipseTrigs,
);
v = Cartesian2.multiplyComponents(radii, tTrigs, scratchEllipseGuess);
evolute = Cartesian2.fromElements(
evoluteScale.x * tTrigs.x * tTrigs.x * tTrigs.x,
evoluteScale.y * tTrigs.y * tTrigs.y * tTrigs.y,
scratchEvolute,
);
}
// Map back to the original quadrant
return Cartesian3.fromElements(
Math.sign(position.x) * v.x,
Math.sign(position.y) * v.y,
Cartesian2.magnitude(Cartesian2.subtract(v, evolute, scratchQ)),
result,
);
}
const scratchEllipseRadii = new Cartesian2();
const scratchEllipsePosition = new Cartesian2();
const scratchSurfacePointAndRadius = new Cartesian3();
const scratchNormal2d = new Cartesian2();
/**
* Convert a UV coordinate to the shape's UV space.
* @private
* @param {Cartesian3} positionLocal The local position to convert.
* @param {Cartesian3} result The Cartesian3 to store the result in.
* @returns {Cartesian3} The converted UV coordinate.
*/
VoxelEllipsoidShape.prototype.convertLocalToShapeUvSpace = function (
positionLocal,
result,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("positionLocal", positionLocal);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
let longitude = Math.atan2(positionLocal.y, positionLocal.x);
const {
ellipsoidRadii,
evoluteScale,
ellipsoidInverseRadiiSquared,
ellipsoidInverseHeightDifference,
ellipsoidShapeUvLongitudeRangeOrigin,
ellipsoidLocalToShapeUvLongitude,
ellipsoidLocalToShapeUvLatitude,
} = this._shaderUniforms;
const distanceFromZAxis = Math.hypot(positionLocal.x, positionLocal.y);
const posEllipse = Cartesian2.fromElements(
distanceFromZAxis,
positionLocal.z,
scratchEllipsePosition,
);
const surfacePointAndRadius = nearestPointAndRadiusOnEllipse(
posEllipse,
Cartesian2.fromElements(
ellipsoidRadii.x,
ellipsoidRadii.z,
scratchEllipseRadii,
),
evoluteScale,
scratchSurfacePointAndRadius,
);
const normal2d = Cartesian2.normalize(
Cartesian2.fromElements(
surfacePointAndRadius.x * ellipsoidInverseRadiiSquared.x,
surfacePointAndRadius.y * ellipsoidInverseRadiiSquared.z,
scratchNormal2d,
),
scratchNormal2d,
);
let latitude = Math.atan2(normal2d.y, normal2d.x);
const heightSign =
Cartesian2.magnitude(posEllipse) <
Cartesian2.magnitude(surfacePointAndRadius)
? -1.0
: 1.0;
const heightVector = Cartesian2.subtract(
posEllipse,
surfacePointAndRadius,
scratchEllipsePosition,
);
let height = heightSign * Cartesian2.magnitude(heightVector);
const {
ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE,
ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE,
} = this._shaderDefines;
longitude = (longitude + Math.PI) / (2.0 * Math.PI);
if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)) {
longitude -= ellipsoidShapeUvLongitudeRangeOrigin;
longitude = longitude - Math.floor(longitude);
// Scale and shift so [0, 1] covers the occupied space.
longitude =
longitude * ellipsoidLocalToShapeUvLongitude.x +
ellipsoidLocalToShapeUvLongitude.y;
}
latitude = (latitude + Math.PI / 2.0) / Math.PI;
if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)) {
// Scale and shift so [0, 1] covers the occupied space.
latitude =
latitude * ellipsoidLocalToShapeUvLatitude.x +
ellipsoidLocalToShapeUvLatitude.y;
}
height = 1.0 + height * ellipsoidInverseHeightDifference;
return Cartesian3.fromElements(longitude, latitude, height, result);
};
const sampleSizeScratch = new Cartesian3();
const scratchTileMinBounds = new Cartesian3();
const scratchTileMaxBounds = new Cartesian3();
/**
* Computes an oriented bounding box for a specified sample within a specified tile.
* The update function must be called before calling this function.
* @private
* @param {SpatialNode} spatialNode The spatial node containing the sample
* @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding

View File

@ -130,6 +130,14 @@ function VoxelPrimitive(options) {
*/
this._paddingAfter = new Cartesian3();
/**
* This member is not known until the provider is ready.
*
* @type {number}
* @private
*/
this._availableLevels = 1;
/**
* This member is not known until the provider is ready.
*
@ -332,24 +340,24 @@ function VoxelPrimitive(options) {
this._clock = options.clock;
// Transforms and other values that are computed when the shape changes
/**
* @type {Matrix4}
* @private
*/
this._transformPositionLocalToWorld = new Matrix4();
/**
* @type {Matrix4}
* @private
*/
this._transformPositionWorldToUv = new Matrix4();
/**
* @type {Matrix3}
* @private
*/
this._transformDirectionWorldToUv = new Matrix3();
this._transformPositionWorldToLocal = new Matrix4();
/**
* Transforms a plane in Hessian normal form from local space to view space.
* @type {Matrix4}
* @private
*/
this._transformPositionUvToWorld = new Matrix4();
this._transformPlaneLocalToView = new Matrix4();
/**
* @type {Matrix3}
@ -440,14 +448,16 @@ function VoxelPrimitive(options) {
inputDimensions: new Cartesian3(),
paddingBefore: new Cartesian3(),
paddingAfter: new Cartesian3(),
transformPositionViewToUv: new Matrix4(),
transformPositionUvToView: new Matrix4(),
transformPositionViewToLocal: new Matrix4(),
transformDirectionViewToLocal: new Matrix3(),
cameraPositionUv: new Cartesian3(),
cameraDirectionUv: new Cartesian3(),
cameraPositionLocal: new Cartesian3(),
cameraDirectionLocal: new Cartesian3(),
cameraTileCoordinates: new Cartesian4(),
cameraTileUv: new Cartesian3(),
ndcSpaceAxisAlignedBoundingBox: new Cartesian4(),
clippingPlanesTexture: undefined,
clippingPlanesMatrix: new Matrix4(),
renderBoundPlanesTexture: undefined,
stepSize: 0,
pickColor: new Color(),
};
@ -631,11 +641,7 @@ function initialize(primitive, provider) {
// Create the shape object, and update it so it is valid for VoxelTraversal
const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType);
primitive._shape = new ShapeConstructor();
primitive._shapeVisible = updateShapeAndTransforms(
primitive,
primitive._shape,
provider,
);
primitive._shapeVisible = updateShapeAndTransforms(primitive);
}
Object.defineProperties(VoxelPrimitive.prototype, {
@ -1137,20 +1143,10 @@ Object.defineProperties(VoxelPrimitive.prototype, {
const scratchIntersect = new Cartesian4();
const scratchNdcAabb = new Cartesian4();
const scratchTransformPositionWorldToLocal = new Matrix4();
const scratchTransformPositionLocalToWorld = new Matrix4();
const scratchTransformPositionLocalToProjection = new Matrix4();
const transformPositionLocalToUv = Matrix4.fromRotationTranslation(
Matrix3.fromUniformScale(0.5, new Matrix3()),
new Cartesian3(0.5, 0.5, 0.5),
new Matrix4(),
);
const transformPositionUvToLocal = Matrix4.fromRotationTranslation(
Matrix3.fromUniformScale(2.0, new Matrix3()),
new Cartesian3(-1.0, -1.0, -1.0),
new Matrix4(),
);
const scratchCameraPositionShapeUv = new Cartesian3();
const scratchCameraTileCoordinates = new Cartesian4();
/**
* Updates the voxel primitive.
@ -1160,6 +1156,7 @@ const transformPositionUvToLocal = Matrix4.fromRotationTranslation(
*/
VoxelPrimitive.prototype.update = function (frameState) {
const provider = this._provider;
const uniforms = this._uniforms;
// Update the custom shader in case it has texture uniforms.
this._customShader.update(frameState);
@ -1185,10 +1182,9 @@ VoxelPrimitive.prototype.update = function (frameState) {
// frame because the member variables can be modified externally via the
// getters.
const shapeDirty = checkTransformAndBounds(this, provider);
const shape = this._shape;
if (shapeDirty) {
this._shapeVisible = updateShapeAndTransforms(this, shape, provider);
if (checkShapeDefines(this, shape)) {
this._shapeVisible = updateShapeAndTransforms(this);
if (checkShapeDefines(this)) {
this._shaderDirty = true;
}
}
@ -1196,6 +1192,8 @@ VoxelPrimitive.prototype.update = function (frameState) {
return;
}
this._shape.updateViewTransforms(frameState);
// Update the traversal and prepare for rendering.
const keyframeLocation = getKeyframeLocation(
provider.timeIntervalCollection,
@ -1243,7 +1241,6 @@ VoxelPrimitive.prototype.update = function (frameState) {
}
const leafNodeTexture = traversal.leafNodeTexture;
const uniforms = this._uniforms;
if (defined(leafNodeTexture)) {
uniforms.octreeLeafNodeTexture = traversal.leafNodeTexture;
uniforms.octreeLeafNodeTexelSizeUv = Cartesian2.clone(
@ -1262,7 +1259,7 @@ VoxelPrimitive.prototype.update = function (frameState) {
// Calculate the NDC-space AABB to "scissor" the fullscreen quad
const transformPositionWorldToProjection =
context.uniformState.viewProjection;
const orientedBoundingBox = shape.orientedBoundingBox;
const { orientedBoundingBox } = this._shape;
const ndcAabb = orientedBoundingBoxToNdcAabb(
orientedBoundingBox,
transformPositionWorldToProjection,
@ -1286,17 +1283,17 @@ VoxelPrimitive.prototype.update = function (frameState) {
uniforms.ndcSpaceAxisAlignedBoundingBox,
);
const transformPositionViewToWorld = context.uniformState.inverseView;
uniforms.transformPositionViewToUv = Matrix4.multiplyTransformation(
this._transformPositionWorldToUv,
const transformPositionViewToLocal = Matrix4.multiplyTransformation(
this._transformPositionWorldToLocal,
transformPositionViewToWorld,
uniforms.transformPositionViewToUv,
uniforms.transformPositionViewToLocal,
);
const transformPositionWorldToView = context.uniformState.view;
uniforms.transformPositionUvToView = Matrix4.multiplyTransformation(
transformPositionWorldToView,
this._transformPositionUvToWorld,
uniforms.transformPositionUvToView,
this._transformPlaneLocalToView = Matrix4.transpose(
transformPositionViewToLocal,
this._transformPlaneLocalToView,
);
const transformDirectionViewToWorld =
context.uniformState.inverseViewRotation;
uniforms.transformDirectionViewToLocal = Matrix3.multiply(
@ -1304,32 +1301,85 @@ VoxelPrimitive.prototype.update = function (frameState) {
transformDirectionViewToWorld,
uniforms.transformDirectionViewToLocal,
);
uniforms.cameraPositionUv = Matrix4.multiplyByPoint(
this._transformPositionWorldToUv,
uniforms.cameraPositionLocal = Matrix4.multiplyByPoint(
this._transformPositionWorldToLocal,
frameState.camera.positionWC,
uniforms.cameraPositionUv,
uniforms.cameraPositionLocal,
);
uniforms.cameraDirectionUv = Matrix3.multiplyByVector(
this._transformDirectionWorldToUv,
uniforms.cameraDirectionLocal = Matrix3.multiplyByVector(
this._transformDirectionWorldToLocal,
frameState.camera.directionWC,
uniforms.cameraDirectionUv,
uniforms.cameraDirectionLocal,
);
uniforms.cameraDirectionUv = Cartesian3.normalize(
uniforms.cameraDirectionUv,
uniforms.cameraDirectionUv,
const cameraTileCoordinates = getTileCoordinates(
this,
uniforms.cameraPositionLocal,
scratchCameraTileCoordinates,
);
uniforms.cameraTileCoordinates = Cartesian4.fromElements(
Math.floor(cameraTileCoordinates.x),
Math.floor(cameraTileCoordinates.y),
Math.floor(cameraTileCoordinates.z),
cameraTileCoordinates.w,
uniforms.cameraTileCoordinates,
);
uniforms.cameraTileUv = Cartesian3.fromElements(
cameraTileCoordinates.x - Math.floor(cameraTileCoordinates.x),
cameraTileCoordinates.y - Math.floor(cameraTileCoordinates.y),
cameraTileCoordinates.z - Math.floor(cameraTileCoordinates.z),
uniforms.cameraTileUv,
);
uniforms.stepSize = this._stepSizeMultiplier;
updateRenderBoundPlanes(this, frameState);
// Render the primitive
const command = frameState.passes.pick
? this._drawCommandPick
: frameState.passes.pickVoxel
? this._drawCommandPickVoxel
: this._drawCommand;
command.boundingVolume = shape.boundingSphere;
command.boundingVolume = this._shape.boundingSphere;
frameState.commandList.push(command);
};
function updateRenderBoundPlanes(primitive, frameState) {
const uniforms = primitive._uniforms;
const { renderBoundPlanes } = primitive._shape;
if (!defined(renderBoundPlanes)) {
return;
}
renderBoundPlanes.update(frameState, primitive._transformPlaneLocalToView);
uniforms.renderBoundPlanesTexture = renderBoundPlanes.texture;
}
/**
* Converts a position in local space to tile coordinates.
*
* @param {VoxelPrimitive} primitive The primitive to get the tile coordinates for.
* @param {Cartesian3} positionLocal The position in local space to convert to tile coordinates.
* @param {Cartesian4} result The result object to store the tile coordinates.
* @returns {Cartesian4} The tile coordinates of the supplied position.
* @private
*/
function getTileCoordinates(primitive, positionLocal, result) {
const shapeUv = primitive._shape.convertLocalToShapeUvSpace(
positionLocal,
scratchCameraPositionShapeUv,
);
const availableLevels = primitive._availableLevels;
const numTiles = 2 ** (availableLevels - 1);
return Cartesian4.fromElements(
shapeUv.x * numTiles,
shapeUv.y * numTiles,
shapeUv.z * numTiles,
availableLevels - 1,
result,
);
}
const scratchExaggerationScale = new Cartesian3();
const scratchExaggerationCenter = new Cartesian3();
const scratchCartographicCenter = new Cartographic();
@ -1337,7 +1387,6 @@ const scratchExaggerationTranslation = new Cartesian3();
/**
* Update the exaggerated bounds of a primitive to account for vertical exaggeration
* Currently only applies to Ellipsoid shape type
* @param {VoxelPrimitive} primitive
* @param {FrameState} frameState
* @private
@ -1513,6 +1562,7 @@ function initFromProvider(primitive, provider, context) {
primitive._inputDimensions,
uniforms.inputDimensions,
);
primitive._availableLevels = provider.availableLevels ?? 1;
// Create the VoxelTraversal, and set related uniforms
const keyframeCount = provider.keyframeCount ?? 1;
@ -1586,12 +1636,11 @@ function updateBound(primitive, newBoundKey, oldBoundKey) {
/**
* Update the shape and related transforms
* @param {VoxelPrimitive} primitive
* @param {VoxelShape} shape
* @param {VoxelProvider} provider
* @returns {boolean} True if the shape is visible
* @private
*/
function updateShapeAndTransforms(primitive, shape, provider) {
function updateShapeAndTransforms(primitive) {
const shape = primitive._shape;
const visible = shape.update(
primitive._compoundModelMatrix,
primitive._exaggeratedMinBounds,
@ -1603,29 +1652,16 @@ function updateShapeAndTransforms(primitive, shape, provider) {
return false;
}
const transformPositionLocalToWorld = shape.shapeTransform;
const transformPositionWorldToLocal = Matrix4.inverse(
transformPositionLocalToWorld,
scratchTransformPositionWorldToLocal,
primitive._transformPositionLocalToWorld = Matrix4.clone(
shape.shapeTransform,
primitive._transformPositionLocalToWorld,
);
// Set member variables when the shape is dirty
primitive._transformPositionWorldToUv = Matrix4.multiplyTransformation(
transformPositionLocalToUv,
transformPositionWorldToLocal,
primitive._transformPositionWorldToUv,
);
primitive._transformDirectionWorldToUv = Matrix4.getMatrix3(
primitive._transformPositionWorldToUv,
primitive._transformDirectionWorldToUv,
);
primitive._transformPositionUvToWorld = Matrix4.multiplyTransformation(
transformPositionLocalToWorld,
transformPositionUvToLocal,
primitive._transformPositionUvToWorld,
primitive._transformPositionWorldToLocal = Matrix4.inverse(
primitive._transformPositionLocalToWorld,
primitive._transformPositionWorldToLocal,
);
primitive._transformDirectionWorldToLocal = Matrix4.getMatrix3(
transformPositionWorldToLocal,
primitive._transformPositionWorldToLocal,
primitive._transformDirectionWorldToLocal,
);
@ -1679,17 +1715,16 @@ function setTraversalUniforms(traversal, uniforms) {
/**
* Track changes in shape-related shader defines
* @param {VoxelPrimitive} primitive
* @param {VoxelShape} shape
* @returns {boolean} True if any of the shape defines changed, requiring a shader rebuild
* @private
*/
function checkShapeDefines(primitive, shape) {
const shapeDefines = shape.shaderDefines;
const shapeDefinesChanged = Object.keys(shapeDefines).some(
(key) => shapeDefines[key] !== primitive._shapeDefinesOld[key],
function checkShapeDefines(primitive) {
const { shaderDefines } = primitive._shape;
const shapeDefinesChanged = Object.keys(shaderDefines).some(
(key) => shaderDefines[key] !== primitive._shapeDefinesOld[key],
);
if (shapeDefinesChanged) {
primitive._shapeDefinesOld = clone(shapeDefines, true);
primitive._shapeDefinesOld = clone(shaderDefines, true);
}
return shapeDefinesChanged;
}
@ -1761,12 +1796,12 @@ function updateClippingPlanes(primitive, frameState) {
const uniforms = primitive._uniforms;
uniforms.clippingPlanesTexture = clippingPlanes.texture;
// Compute the clipping plane's transformation to uv space and then take the inverse
// Compute the clipping plane's transformation to local space and then take the inverse
// transpose to properly transform the hessian normal form of the plane.
// transpose(inverse(worldToUv * clippingPlaneLocalToWorld))
// transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToUv))
// transpose(inverse(clippingPlaneLocalToWorld) * uvToWorld)
// transpose(inverse(worldToLocal * clippingPlaneLocalToWorld))
// transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToLocal))
// transpose(inverse(clippingPlaneLocalToWorld) * localToWorld)
uniforms.clippingPlanesMatrix = Matrix4.transpose(
Matrix4.multiplyTransformation(
@ -1774,7 +1809,7 @@ function updateClippingPlanes(primitive, frameState) {
clippingPlanes.modelMatrix,
uniforms.clippingPlanesMatrix,
),
primitive._transformPositionUvToWorld,
primitive._transformPositionLocalToWorld,
uniforms.clippingPlanesMatrix,
),
uniforms.clippingPlanesMatrix,

View File

@ -8,15 +8,15 @@ import VoxelFS from "../Shaders/Voxels/VoxelFS.js";
import VoxelVS from "../Shaders/Voxels/VoxelVS.js";
import IntersectionUtils from "../Shaders/Voxels/IntersectionUtils.js";
import IntersectDepth from "../Shaders/Voxels/IntersectDepth.js";
import IntersectClippingPlanes from "../Shaders/Voxels/IntersectClippingPlanes.js";
import IntersectPlane from "../Shaders/Voxels/IntersectPlane.js";
import IntersectLongitude from "../Shaders/Voxels/IntersectLongitude.js";
import IntersectBox from "../Shaders/Voxels/IntersectBox.js";
import IntersectCylinder from "../Shaders/Voxels/IntersectCylinder.js";
import IntersectEllipsoid from "../Shaders/Voxels/IntersectEllipsoid.js";
import Intersection from "../Shaders/Voxels/Intersection.js";
import convertUvToBox from "../Shaders/Voxels/convertUvToBox.js";
import convertUvToCylinder from "../Shaders/Voxels/convertUvToCylinder.js";
import convertUvToEllipsoid from "../Shaders/Voxels/convertUvToEllipsoid.js";
import convertLocalToBoxUv from "../Shaders/Voxels/convertLocalToBoxUv.js";
import convertLocalToCylinderUv from "../Shaders/Voxels/convertLocalToCylinderUv.js";
import convertLocalToEllipsoidUv from "../Shaders/Voxels/convertLocalToEllipsoidUv.js";
import Octree from "../Shaders/Voxels/Octree.js";
import Megatexture from "../Shaders/Voxels/Megatexture.js";
import VoxelMetadataOrder from "./VoxelMetadataOrder.js";
@ -84,6 +84,12 @@ function VoxelRenderResources(primitive) {
this.clippingPlanes = clippingPlanes;
this.clippingPlanesLength = clippingPlanesLength;
const renderBoundPlanes = primitive._shape.renderBoundPlanes;
const renderBoundPlanesLength = renderBoundPlanes?.length ?? 0;
this.renderBoundPlanes = renderBoundPlanes;
this.renderBoundPlanesLength = renderBoundPlanesLength;
// Build shader
shaderBuilder.addVertexLines([VoxelVS]);
@ -116,8 +122,10 @@ function VoxelRenderResources(primitive) {
"#line 0",
Octree,
VoxelUtils,
IntersectionUtils,
Megatexture,
IntersectionUtils,
IntersectPlane,
IntersectDepth,
]);
if (clippingPlanesLength > 0) {
@ -138,9 +146,8 @@ function VoxelRenderResources(primitive) {
ShaderDestination.FRAGMENT,
);
}
shaderBuilder.addFragmentLines([IntersectClippingPlanes]);
}
shaderBuilder.addFragmentLines([IntersectDepth]);
if (primitive._depthTest) {
shaderBuilder.addDefine(
"DEPTH_TEST",
@ -151,20 +158,20 @@ function VoxelRenderResources(primitive) {
if (shapeType === "BOX") {
shaderBuilder.addFragmentLines([
convertUvToBox,
convertLocalToBoxUv,
IntersectBox,
Intersection,
]);
} else if (shapeType === "CYLINDER") {
shaderBuilder.addFragmentLines([
convertUvToCylinder,
convertLocalToCylinderUv,
IntersectLongitude,
IntersectCylinder,
Intersection,
]);
} else if (shapeType === "ELLIPSOID") {
shaderBuilder.addFragmentLines([
convertUvToEllipsoid,
convertLocalToEllipsoidUv,
IntersectLongitude,
IntersectEllipsoid,
Intersection,

View File

@ -21,7 +21,6 @@ function VoxelShape() {
Object.defineProperties(VoxelShape.prototype, {
/**
* An oriented bounding box containing the bounded shape.
* The update function must be called before accessing this value.
*
* @memberof VoxelShape.prototype
* @type {OrientedBoundingBox}
@ -34,7 +33,6 @@ Object.defineProperties(VoxelShape.prototype, {
/**
* A bounding sphere containing the bounded shape.
* The update function must be called before accessing this value.
*
* @memberof VoxelShape.prototype
* @type {BoundingSphere}
@ -47,7 +45,6 @@ Object.defineProperties(VoxelShape.prototype, {
/**
* A transformation matrix containing the bounded shape.
* The update function must be called before accessing this value.
*
* @memberof VoxelShape.prototype
* @type {Matrix4}
@ -60,7 +57,6 @@ Object.defineProperties(VoxelShape.prototype, {
/**
* A transformation matrix containing the shape, ignoring the bounds.
* The update function must be called before accessing this value.
*
* @memberof VoxelShape.prototype
* @type {Matrix4}
@ -72,6 +68,7 @@ Object.defineProperties(VoxelShape.prototype, {
},
/**
* @memberof VoxelShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
@ -81,6 +78,7 @@ Object.defineProperties(VoxelShape.prototype, {
},
/**
* @memberof VoxelShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
@ -91,6 +89,7 @@ Object.defineProperties(VoxelShape.prototype, {
/**
* The maximum number of intersections against the shape for any ray direction.
* @memberof VoxelShape.prototype
* @type {number}
* @readonly
* @private
@ -110,9 +109,26 @@ Object.defineProperties(VoxelShape.prototype, {
*/
VoxelShape.prototype.update = DeveloperError.throwInstantiationError;
/**
* Update any view-dependent transforms.
* @private
* @param {FrameState} frameState The frame state.
*/
VoxelShape.prototype.updateViewTransforms =
DeveloperError.throwInstantiationError;
/**
* Converts a local coordinate to the shape's UV space.
* @private
* @param {Cartesian3} positionLocal The local coordinate to convert.
* @param {Cartesian3} result The Cartesian3 to store the result in.
* @returns {Cartesian3} The converted UV coordinate.
*/
VoxelShape.prototype.convertLocalToShapeUvSpace =
DeveloperError.throwInstantiationError;
/**
* Computes an oriented bounding box for a specified tile.
* The update function must be called before calling this function.
* @private
* @param {number} tileLevel The tile's level.
* @param {number} tileX The tile's x coordinate.
@ -126,7 +142,6 @@ VoxelShape.prototype.computeOrientedBoundingBoxForTile =
/**
* Computes an oriented bounding box for a specified sample within a specified tile.
* The update function must be called before calling this function.
* @private
* @param {SpatialNode} spatialNode The spatial node containing the sample
* @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding

View File

@ -1,14 +1,18 @@
import defined from "../Core/defined.js";
import PrimitiveType from "../Core/PrimitiveType.js";
import BlendingState from "./BlendingState.js";
import Cartesian2 from "../Core/Cartesian2.js";
import ClippingPlaneCollection from "./ClippingPlaneCollection.js";
import CullFace from "./CullFace.js";
import getClippingFunction from "./getClippingFunction.js";
import defined from "../Core/defined.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import Pass from "../Renderer/Pass.js";
import PrimitiveType from "../Core/PrimitiveType.js";
import processVoxelProperties from "./processVoxelProperties.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderDestination from "../Renderer/ShaderDestination.js";
import VoxelBoundsCollection from "./VoxelBoundsCollection.js";
import VoxelRenderResources from "./VoxelRenderResources.js";
import processVoxelProperties from "./processVoxelProperties.js";
const textureResolutionScratch = new Cartesian2();
/**
* @function
@ -23,27 +27,40 @@ function buildVoxelDrawCommands(primitive, context) {
processVoxelProperties(renderResources, primitive);
const { shaderBuilder, clippingPlanes, clippingPlanesLength } =
renderResources;
const {
shaderBuilder,
clippingPlanes,
clippingPlanesLength,
renderBoundPlanes,
renderBoundPlanesLength,
} = renderResources;
if (clippingPlanesLength > 0) {
// Extract the getClippingPlane function from the getClippingFunction string.
// This is a bit of a hack.
const functionId = "getClippingPlane";
const entireFunction = getClippingFunction(clippingPlanes, context);
const functionSignatureBegin = 0;
const functionSignatureEnd = entireFunction.indexOf(")") + 1;
const functionBodyBegin =
entireFunction.indexOf("{", functionSignatureEnd) + 1;
const functionBodyEnd = entireFunction.indexOf("}", functionBodyBegin);
const functionSignature = entireFunction.slice(
functionSignatureBegin,
functionSignatureEnd,
const functionSignature = `vec4 ${functionId}(highp sampler2D packedPlanes, int planeNumber)`;
const textureResolution = ClippingPlaneCollection.getTextureResolution(
clippingPlanes,
context,
textureResolutionScratch,
);
const functionBody = entireFunction.slice(
functionBodyBegin,
functionBodyEnd,
const functionBody = getPlaneFunctionBody(textureResolution);
shaderBuilder.addFunction(
functionId,
functionSignature,
ShaderDestination.FRAGMENT,
);
shaderBuilder.addFunctionLines(functionId, [functionBody]);
}
if (renderBoundPlanesLength > 0) {
const functionId = "getBoundPlane";
const functionSignature = `vec4 ${functionId}(highp sampler2D packedPlanes, int planeNumber)`;
const textureResolution = VoxelBoundsCollection.getTextureResolution(
renderBoundPlanes,
context,
textureResolutionScratch,
);
const functionBody = getPlaneFunctionBody(textureResolution);
shaderBuilder.addFunction(
functionId,
functionSignature,
@ -133,4 +150,28 @@ function buildVoxelDrawCommands(primitive, context) {
primitive._drawCommandPickVoxel = drawCommandPickVoxel;
}
function getPlaneFunctionBody(textureResolution) {
const width = textureResolution.x;
const height = textureResolution.y;
const pixelWidth = 1.0 / width;
const pixelHeight = 1.0 / height;
let pixelWidthString = `${pixelWidth}`;
if (pixelWidthString.indexOf(".") === -1) {
pixelWidthString += ".0";
}
let pixelHeightString = `${pixelHeight}`;
if (pixelHeightString.indexOf(".") === -1) {
pixelHeightString += ".0";
}
return `int pixY = planeNumber / ${width};
int pixX = planeNumber - (pixY * ${width});
// Sample from center of pixel
float u = (float(pixX) + 0.5) * ${pixelWidthString};
float v = (float(pixY) + 0.5) * ${pixelHeightString};
return texture(packedPlanes, vec2(u, v));`;
}
export default buildVoxelDrawCommands;

View File

@ -88,7 +88,7 @@ async function createGooglePhotorealistic3DTileset(apiOptions, tilesetOptions) {
}
const resource = new Resource({
url: `${GoogleMaps.mapTilesApiEndpoint}3dtiles/root.json`,
url: `${GoogleMaps.mapTilesApiEndpoint}v1/3dtiles/root.json`,
queryParameters: {
key: key,
},

View File

@ -38,66 +38,61 @@ function getClippingFunction(clippingPlaneCollection, context) {
}
function clippingFunctionUnion(clippingPlanesLength) {
const functionString =
`${
"float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)\n" +
"{\n" +
" vec4 position = czm_windowToEyeCoordinates(fragCoord);\n" +
" vec3 clipNormal = vec3(0.0);\n" +
" vec3 clipPosition = vec3(0.0);\n" +
" float clipAmount;\n" + // For union planes, we want to get the min distance. So we set the initial value to the first plane distance in the loop below.
" float pixelWidth = czm_metersPerPixel(position);\n" +
" bool breakAndDiscard = false;\n" +
" for (int i = 0; i < "
}${clippingPlanesLength}; ++i)\n` +
` {\n` +
` vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);\n` +
` clipNormal = clippingPlane.xyz;\n` +
` clipPosition = -clippingPlane.w * clipNormal;\n` +
` float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;\n` +
` clipAmount = czm_branchFreeTernary(i == 0, amount, min(amount, clipAmount));\n` +
` if (amount <= 0.0)\n` +
` {\n` +
` breakAndDiscard = true;\n` +
` break;\n` + // HLSL compiler bug if we discard here: https://bugs.chromium.org/p/angleproject/issues/detail?id=1945#c6
` }\n` +
` }\n` +
` if (breakAndDiscard) {\n` +
` discard;\n` +
` }\n` +
` return clipAmount;\n` +
`}\n`;
return functionString;
return `float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)
{
vec4 position = czm_windowToEyeCoordinates(fragCoord);
vec3 clipNormal = vec3(0.0);
vec3 clipPosition = vec3(0.0);
float clipAmount;
float pixelWidth = czm_metersPerPixel(position);
bool breakAndDiscard = false;
for (int i = 0; i < ${clippingPlanesLength}; ++i)
{
vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);
clipNormal = clippingPlane.xyz;
clipPosition = -clippingPlane.w * clipNormal;
float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;
clipAmount = czm_branchFreeTernary(i == 0, amount, min(amount, clipAmount));
if (amount <= 0.0)
{
breakAndDiscard = true;
// HLSL compiler bug if we discard here: https://bugs.chromium.org/p/angleproject/issues/detail?id=1945#c6
break;
}
}
if (breakAndDiscard) {
discard;
}
return clipAmount;
}
`;
}
function clippingFunctionIntersect(clippingPlanesLength) {
const functionString =
`${
"float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)\n" +
"{\n" +
" bool clipped = true;\n" +
" vec4 position = czm_windowToEyeCoordinates(fragCoord);\n" +
" vec3 clipNormal = vec3(0.0);\n" +
" vec3 clipPosition = vec3(0.0);\n" +
" float clipAmount = 0.0;\n" +
" float pixelWidth = czm_metersPerPixel(position);\n" +
" for (int i = 0; i < "
}${clippingPlanesLength}; ++i)\n` +
` {\n` +
` vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);\n` +
` clipNormal = clippingPlane.xyz;\n` +
` clipPosition = -clippingPlane.w * clipNormal;\n` +
` float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;\n` +
` clipAmount = max(amount, clipAmount);\n` +
` clipped = clipped && (amount <= 0.0);\n` +
` }\n` +
` if (clipped)\n` +
` {\n` +
` discard;\n` +
` }\n` +
` return clipAmount;\n` +
`}\n`;
return functionString;
return `float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)
{
bool clipped = true;
vec4 position = czm_windowToEyeCoordinates(fragCoord);
vec3 clipNormal = vec3(0.0);
vec3 clipPosition = vec3(0.0);
float clipAmount = 0.0;
float pixelWidth = czm_metersPerPixel(position);
for (int i = 0; i < ${clippingPlanesLength}; ++i)
{
vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);
clipNormal = clippingPlane.xyz;
clipPosition = -clippingPlane.w * clipNormal;
float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;
clipAmount = max(amount, clipAmount);
clipped = clipped && (amount <= 0.0);
}
if (clipped)
{
discard;
}
return clipAmount;
}
`;
}
function getClippingPlaneFloat(width, height) {
@ -113,19 +108,17 @@ function getClippingPlaneFloat(width, height) {
pixelHeightString += ".0";
}
const functionString =
`${
"vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)\n" +
"{\n" +
" int pixY = clippingPlaneNumber / "
}${width};\n` +
` int pixX = clippingPlaneNumber - (pixY * ${width});\n` +
` float u = (float(pixX) + 0.5) * ${pixelWidthString};\n` + // sample from center of pixel
` float v = (float(pixY) + 0.5) * ${pixelHeightString};\n` +
` vec4 plane = texture(packedClippingPlanes, vec2(u, v));\n` +
` return czm_transformPlane(plane, transform);\n` +
`}\n`;
return functionString;
return `vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)
{
int pixY = clippingPlaneNumber / ${width};
int pixX = clippingPlaneNumber - (pixY * ${width});
// Sample from center of pixel
float u = (float(pixX) + 0.5) * ${pixelWidthString};
float v = (float(pixY) + 0.5) * ${pixelHeightString};
vec4 plane = texture(packedClippingPlanes, vec2(u, v));
return czm_transformPlane(plane, transform);
}
`;
}
function getClippingPlaneUint8(width, height) {
@ -141,23 +134,21 @@ function getClippingPlaneUint8(width, height) {
pixelHeightString += ".0";
}
const functionString =
`${
"vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)\n" +
"{\n" +
" int clippingPlaneStartIndex = clippingPlaneNumber * 2;\n" + // clipping planes are two pixels each
" int pixY = clippingPlaneStartIndex / "
}${width};\n` +
` int pixX = clippingPlaneStartIndex - (pixY * ${width});\n` +
` float u = (float(pixX) + 0.5) * ${pixelWidthString};\n` + // sample from center of pixel
` float v = (float(pixY) + 0.5) * ${pixelHeightString};\n` +
` vec4 oct32 = texture(packedClippingPlanes, vec2(u, v)) * 255.0;\n` +
` vec2 oct = vec2(oct32.x * 256.0 + oct32.y, oct32.z * 256.0 + oct32.w);\n` +
` vec4 plane;\n` +
` plane.xyz = czm_octDecode(oct, 65535.0);\n` +
` plane.w = czm_unpackFloat(texture(packedClippingPlanes, vec2(u + ${pixelWidthString}, v)));\n` +
` return czm_transformPlane(plane, transform);\n` +
`}\n`;
return functionString;
return `vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)
{
int clippingPlaneStartIndex = clippingPlaneNumber * 2;
int pixY = clippingPlaneStartIndex / ${width};
int pixX = clippingPlaneStartIndex - (pixY * ${width});
// Sample from center of pixel
float u = (float(pixX) + 0.5) * ${pixelWidthString};
float v = (float(pixY) + 0.5) * ${pixelHeightString};
vec4 oct32 = texture(packedClippingPlanes, vec2(u, v)) * 255.0;
vec2 oct = vec2(oct32.x * 256.0 + oct32.y, oct32.z * 256.0 + oct32.w);
vec4 plane;
plane.xyz = czm_octDecode(oct, 65535.0);
plane.w = czm_unpackFloat(texture(packedClippingPlanes, vec2(u + ${pixelWidthString}, v)));
return czm_transformPlane(plane, transform);
}
`;
}
export default getClippingFunction;

View File

@ -5,43 +5,30 @@
#define BOX_INTERSECTION_INDEX ### // always 0
*/
uniform vec3 u_renderMinBounds;
uniform vec3 u_renderMaxBounds;
uniform sampler2D u_renderBoundPlanesTexture;
RayShapeIntersection intersectBox(in Ray ray, in vec3 minBound, in vec3 maxBound)
{
// Consider the box as the intersection of the space between 3 pairs of parallel planes
// Compute the distance along the ray to each plane
vec3 t0 = (minBound - ray.pos) / ray.dir;
vec3 t1 = (maxBound - ray.pos) / ray.dir;
// Identify candidate entries/exits based on distance from ray.pos
vec3 entries = min(t0, t1);
vec3 exits = max(t0, t1);
vec3 directions = sign(ray.dir);
// The actual intersection points are the furthest entry and the closest exit
float lastEntry = maxComponent(entries);
bvec3 isLastEntry = equal(entries, vec3(lastEntry));
vec3 entryNormal = -1.0 * vec3(isLastEntry) * directions;
vec4 entry = vec4(entryNormal, lastEntry);
float firstExit = minComponent(exits);
bvec3 isFirstExit = equal(exits, vec3(firstExit));
vec3 exitNormal = vec3(isLastEntry) * directions;
vec4 exit = vec4(exitNormal, firstExit);
if (entry.w > exit.w) {
entry.w = NO_HIT;
exit.w = NO_HIT;
RayShapeIntersection intersectBoundPlanes(in Ray ray) {
vec4 lastEntry = vec4(ray.dir, -INF_HIT);
vec4 firstExit = vec4(-ray.dir, +INF_HIT);
for (int i = 0; i < 6; i++) {
vec4 boundPlane = getBoundPlane(u_renderBoundPlanesTexture, i);
vec4 intersection = intersectPlane(ray, boundPlane);
if (dot(ray.dir, boundPlane.xyz) < 0.0) {
lastEntry = intersection.w > lastEntry.w ? intersection : lastEntry;
} else {
firstExit = intersection.w < firstExit.w ? intersection: firstExit;
}
}
return RayShapeIntersection(entry, exit);
if (lastEntry.w < firstExit.w) {
return RayShapeIntersection(lastEntry, firstExit);
} else {
return RayShapeIntersection(vec4(-ray.dir, NO_HIT), vec4(ray.dir, NO_HIT));
}
}
void intersectShape(in Ray ray, inout Intersections ix)
void intersectShape(in Ray rayUV, in Ray rayEC, inout Intersections ix)
{
RayShapeIntersection intersection = intersectBox(ray, u_renderMinBounds, u_renderMaxBounds);
RayShapeIntersection intersection = intersectBoundPlanes(rayEC);
setShapeIntersection(ix, BOX_INTERSECTION_INDEX, intersection);
}

View File

@ -19,32 +19,30 @@
// Cylinder uniforms
uniform vec2 u_cylinderRenderRadiusMinMax;
uniform vec2 u_cylinderRenderHeightMinMax;
#if defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE)
uniform vec2 u_cylinderRenderAngleMinMax;
#endif
/**
* Find the intersection of a ray with the volume defined by two planes of constant z
*/
RayShapeIntersection intersectHeightBounds(in Ray ray, in vec2 minMaxHeight, in bool convex)
{
float zPosition = ray.pos.z;
float zDirection = ray.dir.z;
uniform sampler2D u_renderBoundPlanesTexture;
float tmin = (minMaxHeight.x - zPosition) / zDirection;
float tmax = (minMaxHeight.y - zPosition) / zDirection;
RayShapeIntersection intersectBoundPlanes(in Ray ray) {
vec4 lastEntry = vec4(ray.dir, -INF_HIT);
vec4 firstExit = vec4(-ray.dir, +INF_HIT);
for (int i = 0; i < 2; i++) {
vec4 boundPlane = getBoundPlane(u_renderBoundPlanesTexture, i);
vec4 intersection = intersectPlane(ray, boundPlane);
if (dot(ray.dir, boundPlane.xyz) < 0.0) {
lastEntry = intersection.w > lastEntry.w ? intersection : lastEntry;
} else {
firstExit = intersection.w < firstExit.w ? intersection: firstExit;
}
}
// Normals point outside the volume
float signFlip = convex ? 1.0 : -1.0;
vec4 intersectMin = vec4(0.0, 0.0, -1.0 * signFlip, tmin);
vec4 intersectMax = vec4(0.0, 0.0, 1.0 * signFlip, tmax);
bool topEntry = zDirection < 0.0;
vec4 entry = topEntry ? intersectMax : intersectMin;
vec4 exit = topEntry ? intersectMin : intersectMax;
return RayShapeIntersection(entry, exit);
if (lastEntry.w < firstExit.w) {
return RayShapeIntersection(lastEntry, firstExit);
} else {
return RayShapeIntersection(vec4(-ray.dir, NO_HIT), vec4(ray.dir, NO_HIT));
}
}
/**
@ -70,8 +68,11 @@ RayShapeIntersection intersectCylinder(in Ray ray, in float radius, in bool conv
float t1 = (-b - determinant) / a;
float t2 = (-b + determinant) / a;
float signFlip = convex ? 1.0 : -1.0;
vec4 intersect1 = vec4(normalize(position + t1 * direction) * signFlip, 0.0, t1);
vec4 intersect2 = vec4(normalize(position + t2 * direction) * signFlip, 0.0, t2);
vec3 normal1 = vec3((position + t1 * direction) * signFlip, 0.0);
vec3 normal2 = vec3((position + t2 * direction) * signFlip, 0.0);
// Return normals in eye coordinates
vec4 intersect1 = vec4(normalize(czm_normal * normal1), t1);
vec4 intersect2 = vec4(normalize(czm_normal * normal2), t2);
return RayShapeIntersection(intersect1, intersect2);
}
@ -80,21 +81,16 @@ RayShapeIntersection intersectCylinder(in Ray ray, in float radius, in bool conv
* Find the intersection of a ray with a right cylindrical solid of given
* radius and height bounds. NOTE: The shape is assumed to be convex.
*/
RayShapeIntersection intersectBoundedCylinder(in Ray ray, in float radius, in vec2 minMaxHeight)
RayShapeIntersection intersectBoundedCylinder(in Ray ray, in Ray rayEC, in float radius)
{
RayShapeIntersection cylinderIntersection = intersectCylinder(ray, radius, true);
RayShapeIntersection heightBoundsIntersection = intersectHeightBounds(ray, minMaxHeight, true);
RayShapeIntersection heightBoundsIntersection = intersectBoundPlanes(rayEC);
return intersectIntersections(ray, cylinderIntersection, heightBoundsIntersection);
}
void intersectShape(Ray ray, inout Intersections ix)
void intersectShape(in Ray ray, in Ray rayEC, inout Intersections ix)
{
// Position is converted from [0,1] to [-1,+1] because shape intersections assume unit space is [-1,+1].
// Direction is scaled as well to be in sync with position.
ray.pos = ray.pos * 2.0 - 1.0;
ray.dir *= 2.0;
RayShapeIntersection outerIntersect = intersectBoundedCylinder(ray, u_cylinderRenderRadiusMinMax.y, u_cylinderRenderHeightMinMax);
RayShapeIntersection outerIntersect = intersectBoundedCylinder(ray, rayEC, u_cylinderRenderRadiusMinMax.y);
setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_RADIUS_MAX, outerIntersect);

View File

@ -5,8 +5,6 @@
#define DEPTH_INTERSECTION_INDEX ###
*/
uniform mat4 u_transformPositionViewToUv;
void intersectDepth(in vec2 screenCoord, in Ray ray, inout Intersections ix) {
float logDepthOrDepth = czm_unpackDepth(texture(czm_globeDepthTexture, screenCoord));
float entry;
@ -15,8 +13,7 @@ void intersectDepth(in vec2 screenCoord, in Ray ray, inout Intersections ix) {
// Calculate how far the ray must travel before it hits the depth buffer.
vec4 eyeCoordinateDepth = czm_screenToEyeCoordinates(screenCoord, logDepthOrDepth);
eyeCoordinateDepth /= eyeCoordinateDepth.w;
vec3 depthPositionUv = vec3(u_transformPositionViewToUv * eyeCoordinateDepth);
entry = dot(depthPositionUv - ray.pos, ray.dir);
entry = dot(eyeCoordinateDepth.xyz - ray.pos, ray.dir);
exit = +INF_HIT;
} else {
// There's no depth at this location.

View File

@ -26,7 +26,7 @@
#endif
uniform float u_eccentricitySquared;
uniform vec2 u_ellipsoidRenderLatitudeSinMinMax;
uniform vec2 u_clipMinMaxHeight;
uniform vec2 u_clipMinMaxHeight; // Values are negative: clipHeight - maxShapeHeight
RayShapeIntersection intersectZPlane(in Ray ray, in float z) {
float t = -ray.pos.z / ray.dir.z;
@ -44,11 +44,11 @@ RayShapeIntersection intersectZPlane(in Ray ray, in float z) {
}
}
RayShapeIntersection intersectHeight(in Ray ray, in float relativeHeight, in bool convex)
RayShapeIntersection intersectHeight(in Ray ray, in float height, in bool convex)
{
// Scale the ray by the ellipsoid axes to make it a unit sphere
// Note: approximating ellipsoid + height as an ellipsoid
vec3 radiiCorrection = u_ellipsoidRadiiUv / (u_ellipsoidRadiiUv + relativeHeight);
vec3 radiiCorrection = vec3(1.0) / (u_ellipsoidRadii + height);
vec3 position = ray.pos * radiiCorrection;
vec3 direction = ray.dir * radiiCorrection;
@ -74,10 +74,14 @@ RayShapeIntersection intersectHeight(in Ray ray, in float relativeHeight, in boo
float tmax = max(t1, t2);
float directionScale = convex ? 1.0 : -1.0;
vec3 d1 = directionScale * normalize(position + tmin * direction);
vec3 d2 = directionScale * normalize(position + tmax * direction);
vec3 d1 = directionScale * (position + tmin * direction);
vec3 d2 = directionScale * (position + tmax * direction);
return RayShapeIntersection(vec4(d1, tmin), vec4(d2, tmax));
// Return normals in eye coordinates. Use spherical approximation for the normal.
vec3 normal1 = normalize(czm_normal * d1);
vec3 normal2 = normalize(czm_normal * d2);
return RayShapeIntersection(vec4(normal1, tmin), vec4(normal2, tmax));
}
/**
@ -151,16 +155,13 @@ float getLatitudeConeShift(in float sinLatitude) {
// Find prime vertical radius of curvature:
// the distance along the ellipsoid normal to the intersection with the z-axis
float x2 = u_eccentricitySquared * sinLatitude * sinLatitude;
float primeVerticalRadius = inversesqrt(1.0 - x2);
float primeVerticalRadius = u_ellipsoidRadii.x * inversesqrt(1.0 - x2);
// Compute a shift from the origin to the intersection of the cone with the z-axis
return primeVerticalRadius * u_eccentricitySquared * sinLatitude;
}
void intersectFlippedCone(in Ray ray, in float cosHalfAngle, out RayShapeIntersection intersections[2]) {
// Undo the scaling from ellipsoid to sphere
ray.pos = ray.pos * u_ellipsoidRadiiUv;
ray.dir = ray.dir * u_ellipsoidRadiiUv;
// Shift the ray to account for the latitude cone not being centered at the Earth center
ray.pos.z += getLatitudeConeShift(cosHalfAngle);
@ -206,9 +207,6 @@ void intersectFlippedCone(in Ray ray, in float cosHalfAngle, out RayShapeInterse
}
RayShapeIntersection intersectRegularCone(in Ray ray, in float cosHalfAngle, in bool convex) {
// Undo the scaling from ellipsoid to sphere
ray.pos = ray.pos * u_ellipsoidRadiiUv;
ray.dir = ray.dir * u_ellipsoidRadiiUv;
// Shift the ray to account for the latitude cone not being centered at the Earth center
ray.pos.z += getLatitudeConeShift(cosHalfAngle);
@ -245,13 +243,7 @@ RayShapeIntersection intersectRegularCone(in Ray ray, in float cosHalfAngle, in
}
}
void intersectShape(in Ray ray, inout Intersections ix) {
// Position is converted from [0,1] to [-1,+1] because shape intersections assume unit space is [-1,+1].
// Direction is scaled as well to be in sync with position.
ray.pos = ray.pos * 2.0 - 1.0;
ray.dir *= 2.0;
// Outer ellipsoid
void intersectShape(in Ray ray, in Ray rayEC, inout Intersections ix) { // Outer ellipsoid
RayShapeIntersection outerIntersect = intersectHeight(ray, u_clipMinMaxHeight.y, true);
setShapeIntersection(ix, ELLIPSOID_INTERSECTION_INDEX_HEIGHT_MAX, outerIntersect);

View File

@ -1,6 +1,14 @@
// See IntersectionUtils.glsl for the definitions of Ray, NO_HIT, INF_HIT,
// RayShapeIntersection
vec4 transformNormalToEC(in vec4 intersection) {
return vec4(normalize(czm_normal * intersection.xyz), intersection.w);
}
RayShapeIntersection transformNormalsToEC(in RayShapeIntersection ix) {
return RayShapeIntersection(transformNormalToEC(ix.entry), transformNormalToEC(ix.exit));
}
vec4 intersectLongitude(in Ray ray, in float angle, in bool positiveNormal) {
float normalSign = positiveNormal ? 1.0 : -1.0;
vec2 planeNormal = vec2(-sin(angle), cos(angle)) * normalSign;
@ -32,8 +40,8 @@ RayShapeIntersection intersectHalfSpace(in Ray ray, in float angle, in bool posi
void intersectFlippedWedge(in Ray ray, in vec2 minMaxAngle, out RayShapeIntersection intersections[2])
{
intersections[0] = intersectHalfSpace(ray, minMaxAngle.x, false);
intersections[1] = intersectHalfSpace(ray, minMaxAngle.y, true);
intersections[0] = transformNormalsToEC(intersectHalfSpace(ray, minMaxAngle.x, false));
intersections[1] = transformNormalsToEC(intersectHalfSpace(ray, minMaxAngle.y, true));
}
bool hitPositiveHalfPlane(in Ray ray, in vec4 intersection, in bool positiveNormal) {
@ -46,14 +54,18 @@ bool hitPositiveHalfPlane(in Ray ray, in vec4 intersection, in bool positiveNorm
void intersectHalfPlane(in Ray ray, in float angle, out RayShapeIntersection intersections[2]) {
vec4 intersection = intersectLongitude(ray, angle, true);
vec4 farSide = vec4(normalize(ray.dir), INF_HIT);
bool hitPositiveSide = hitPositiveHalfPlane(ray, intersection, true);
if (hitPositiveHalfPlane(ray, intersection, true)) {
farSide = transformNormalToEC(farSide);
if (hitPositiveSide) {
intersection = transformNormalToEC(intersection);
intersections[0].entry = -1.0 * farSide;
intersections[0].exit = vec4(-1.0 * intersection.xy, 0.0, intersection.w);
intersections[0].exit = vec4(-1.0 * intersection.xyz, intersection.w);
intersections[1].entry = intersection;
intersections[1].exit = farSide;
} else {
vec4 miss = vec4(normalize(ray.dir), NO_HIT);
vec4 miss = vec4(normalize(czm_normal * ray.dir), NO_HIT);
intersections[0].entry = -1.0 * farSide;
intersections[0].exit = farSide;
intersections[1].entry = miss;
@ -88,15 +100,15 @@ RayShapeIntersection intersectRegularWedge(in Ray ray, in vec2 minMaxAngle)
if (exitFromInside && enterFromOutside) {
// Ray crosses both faces of negative wedge, exiting then entering the positive shape
return RayShapeIntersection(first, last);
return transformNormalsToEC(RayShapeIntersection(first, last));
} else if (!exitFromInside && enterFromOutside) {
// Ray starts inside wedge. last is in shadow wedge, and first is actually the entry
return RayShapeIntersection(-1.0 * farSide, first);
return transformNormalsToEC(RayShapeIntersection(-1.0 * farSide, first));
} else if (exitFromInside && !enterFromOutside) {
// First intersection was in the shadow wedge, so last is actually the exit
return RayShapeIntersection(last, farSide);
return transformNormalsToEC(RayShapeIntersection(last, farSide));
} else { // !exitFromInside && !enterFromOutside
// Both intersections were in the shadow wedge
return RayShapeIntersection(miss, miss);
return transformNormalsToEC(RayShapeIntersection(miss, miss));
}
}

View File

@ -22,6 +22,7 @@ vec4 intersectPlane(in Ray ray, in vec4 plane) {
return vec4(n, t);
}
#ifdef CLIPPING_PLANES
void intersectClippingPlanes(in Ray ray, inout Intersections ix) {
vec4 backSide = vec4(-ray.dir, -INF_HIT);
vec4 farSide = vec4(ray.dir, +INF_HIT);
@ -30,7 +31,7 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) {
#if (CLIPPING_PLANES_COUNT == 1)
// Union and intersection are the same when there's one clipping plane, and the code
// is more simplified.
vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, 0, u_clippingPlanesMatrix);
vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, 0);
vec4 intersection = intersectPlane(ray, planeUv);
bool reflects = dot(ray.dir, intersection.xyz) < 0.0;
clippingVolume.entry = reflects ? backSide : intersection;
@ -40,7 +41,7 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) {
vec4 firstTransmission = vec4(ray.dir, +INF_HIT);
vec4 lastReflection = vec4(-ray.dir, -INF_HIT);
for (int i = 0; i < CLIPPING_PLANES_COUNT; i++) {
vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i, u_clippingPlanesMatrix);
vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i);
vec4 intersection = intersectPlane(ray, planeUv);
if (dot(ray.dir, planeUv.xyz) > 0.0) {
firstTransmission = intersection.w <= firstTransmission.w ? intersection : firstTransmission;
@ -58,7 +59,7 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) {
vec4 lastTransmission = vec4(ray.dir, -INF_HIT);
vec4 firstReflection = vec4(-ray.dir, +INF_HIT);
for (int i = 0; i < CLIPPING_PLANES_COUNT; i++) {
vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i, u_clippingPlanesMatrix);
vec4 planeUv = getClippingPlane(u_clippingPlanesTexture, i);
vec4 intersection = intersectPlane(ray, planeUv);
if (dot(ray.dir, planeUv.xyz) > 0.0) {
lastTransmission = intersection.w > lastTransmission.w ? intersection : lastTransmission;
@ -76,3 +77,4 @@ void intersectClippingPlanes(in Ray ray, inout Intersections ix) {
setShapeIntersection(ix, CLIPPING_PLANES_INTERSECTION_INDEX, clippingVolume);
#endif
}
#endif

View File

@ -11,9 +11,9 @@
#define INTERSECTION_COUNT ###
*/
RayShapeIntersection intersectScene(in vec2 screenCoord, in Ray ray, out Intersections ix) {
RayShapeIntersection intersectScene(in vec2 screenCoord, in Ray ray, in Ray rayEC, out Intersections ix) {
// Do a ray-shape intersection to find the exact starting and ending points.
intersectShape(ray, ix);
intersectShape(ray, rayEC, ix);
// Exit early if the positive shape was completely missed or behind the ray.
RayShapeIntersection intersection = getFirstIntersection(ix);
@ -28,7 +28,7 @@ RayShapeIntersection intersectScene(in vec2 screenCoord, in Ray ray, out Interse
#endif
// Depth
intersectDepth(screenCoord, ray, ix);
intersectDepth(screenCoord, rayEC, ix);
// Find the first intersection that's in front of the ray
#if (INTERSECTION_COUNT > 1)

View File

@ -29,6 +29,11 @@ struct TraversalData {
int parentOctreeIndex;
};
struct TileAndUvCoordinate {
ivec4 tileCoords;
vec3 tileUv;
};
struct SampleData {
int megatextureIndex;
ivec4 tileCoords;
@ -79,23 +84,20 @@ int getOctreeParentIndex(in int octreeIndex) {
return parentOctreeIndex;
}
/**
* Convert a position in the uv-space of the tileset bounding shape
* into the uv-space of a tile within the tileset
*/
vec3 getTileUv(in vec3 shapePosition, in ivec4 octreeCoords) {
// PERFORMANCE_IDEA: use bit-shifting (only in WebGL2)
float dimAtLevel = exp2(float(octreeCoords.w));
return shapePosition * dimAtLevel - vec3(octreeCoords.xyz);
vec3 getTileUv(in TileAndUvCoordinate tileAndUv, in ivec4 octreeCoords) {
int levelDifference = tileAndUv.tileCoords.w - octreeCoords.w;
float scalar = exp2(-1.0 * float(levelDifference));
vec3 originShift = vec3(tileAndUv.tileCoords.xyz - (octreeCoords.xyz << levelDifference)) * scalar;
return tileAndUv.tileUv * scalar + originShift;
}
vec3 getClampedTileUv(in vec3 shapePosition, in ivec4 octreeCoords) {
vec3 tileUv = getTileUv(shapePosition, octreeCoords);
vec3 getClampedTileUv(in TileAndUvCoordinate tileAndUv, in ivec4 octreeCoords) {
vec3 tileUv = getTileUv(tileAndUv, octreeCoords);
return clamp(tileUv, vec3(0.0), vec3(1.0));
}
void addSampleCoordinates(in vec3 shapePosition, inout SampleData sampleData) {
vec3 tileUv = getClampedTileUv(shapePosition, sampleData.tileCoords);
void addSampleCoordinates(in TileAndUvCoordinate tileAndUv, inout SampleData sampleData) {
vec3 tileUv = getClampedTileUv(tileAndUv, sampleData.tileCoords);
vec3 inputCoordinate = tileUv * vec3(u_dimensions);
#if defined(PADDING)
@ -162,32 +164,25 @@ void getOctreeLeafSampleDatas(in OctreeNodeData data, in ivec4 octreeCoords, out
}
#endif
OctreeNodeData traverseOctreeDownwards(in vec3 shapePosition, inout TraversalData traversalData) {
float sizeAtLevel = exp2(-1.0 * float(traversalData.octreeCoords.w));
vec3 start = vec3(traversalData.octreeCoords.xyz) * sizeAtLevel;
vec3 end = start + vec3(sizeAtLevel);
OctreeNodeData traverseOctreeDownwards(in ivec4 tileCoordinate, inout TraversalData traversalData) {
OctreeNodeData childData;
for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) {
// Find out which octree child contains the position
// 0 if before center, 1 if after
vec3 center = 0.5 * (start + end);
vec3 childCoord = step(center, shapePosition);
// tileCoordinate.xyz is defined at the level of detail tileCoordinate.w.
// Find the corresponding coordinate at the level traversalData.octreeCoords.w
int level = traversalData.octreeCoords.w + 1;
int levelDifference = tileCoordinate.w - level;
ivec3 coordinateAtLevel = tileCoordinate.xyz >> levelDifference;
traversalData.octreeCoords = ivec4(coordinateAtLevel, level);
// Get octree coords for the next level down
ivec4 octreeCoords = traversalData.octreeCoords;
traversalData.octreeCoords = ivec4(octreeCoords.xyz * 2 + ivec3(childCoord), octreeCoords.w + 1);
childData = getOctreeChildData(traversalData.parentOctreeIndex, ivec3(childCoord));
ivec3 childCoordinate = coordinateAtLevel & 1;
childData = getOctreeChildData(traversalData.parentOctreeIndex, childCoordinate);
if (childData.flag != OCTREE_FLAG_INTERNAL) {
// leaf tile - stop traversing
break;
}
// interior tile - keep going deeper
start = mix(start, center, childCoord);
end = mix(center, end, childCoord);
traversalData.parentOctreeIndex = childData.data;
}
@ -198,50 +193,50 @@ OctreeNodeData traverseOctreeDownwards(in vec3 shapePosition, inout TraversalDat
* Transform a given position to an octree tile coordinate and a position within that tile,
* and find the corresponding megatexture index and texture coordinates
*/
void traverseOctreeFromBeginning(in vec3 shapePosition, out TraversalData traversalData, out SampleData sampleDatas[SAMPLE_COUNT]) {
void traverseOctreeFromBeginning(in TileAndUvCoordinate tileAndUv, out TraversalData traversalData, out SampleData sampleDatas[SAMPLE_COUNT]) {
traversalData.octreeCoords = ivec4(0);
traversalData.parentOctreeIndex = 0;
OctreeNodeData nodeData = getOctreeNodeData(vec2(0.0));
if (nodeData.flag != OCTREE_FLAG_LEAF) {
nodeData = traverseOctreeDownwards(shapePosition, traversalData);
nodeData = traverseOctreeDownwards(tileAndUv.tileCoords, traversalData);
}
#if (SAMPLE_COUNT == 1)
getOctreeLeafSampleData(nodeData, traversalData.octreeCoords, sampleDatas[0]);
addSampleCoordinates(shapePosition, sampleDatas[0]);
addSampleCoordinates(tileAndUv, sampleDatas[0]);
#else
getOctreeLeafSampleDatas(nodeData, traversalData.octreeCoords, sampleDatas);
addSampleCoordinates(shapePosition, sampleDatas[0]);
addSampleCoordinates(shapePosition, sampleDatas[1]);
addSampleCoordinates(tileAndUv, sampleDatas[0]);
addSampleCoordinates(tileAndUv, sampleDatas[1]);
#endif
}
bool inRange(in vec3 v, in vec3 minVal, in vec3 maxVal) {
return clamp(v, minVal, maxVal) == v;
bool insideTile(in ivec4 tileCoordinate, in ivec4 octreeCoords) {
int levelDifference = tileCoordinate.w - octreeCoords.w;
if (levelDifference < 0) {
return false;
}
ivec3 coordinateAtLevel = tileCoordinate.xyz >> levelDifference;
return coordinateAtLevel == octreeCoords.xyz;
}
bool insideTile(in vec3 shapePosition, in ivec4 octreeCoords) {
vec3 tileUv = getTileUv(shapePosition, octreeCoords);
bool inside = inRange(tileUv, vec3(0.0), vec3(1.0));
// Assume (!) the position is always inside the root tile.
return inside || octreeCoords.w == 0;
}
void traverseOctreeFromExisting(in vec3 shapePosition, inout TraversalData traversalData, inout SampleData sampleDatas[SAMPLE_COUNT]) {
if (insideTile(shapePosition, traversalData.octreeCoords)) {
void traverseOctreeFromExisting(in TileAndUvCoordinate tileAndUv, inout TraversalData traversalData, inout SampleData sampleDatas[SAMPLE_COUNT]) {
ivec4 tileCoords = tileAndUv.tileCoords;
if (insideTile(tileCoords, traversalData.octreeCoords)) {
for (int i = 0; i < SAMPLE_COUNT; i++) {
addSampleCoordinates(shapePosition, sampleDatas[i]);
addSampleCoordinates(tileAndUv, sampleDatas[i]);
}
return;
}
// Go up tree until we find a parent tile containing shapePosition
// Go up tree until we find a parent tile containing tileCoords.
// Assumes all parents are available all they way up to the root.
for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) {
traversalData.octreeCoords.xyz /= 2;
traversalData.octreeCoords.w -= 1;
if (insideTile(shapePosition, traversalData.octreeCoords)) {
if (insideTile(tileCoords, traversalData.octreeCoords)) {
break;
}
@ -249,14 +244,14 @@ void traverseOctreeFromExisting(in vec3 shapePosition, inout TraversalData trave
}
// Go down tree
OctreeNodeData nodeData = traverseOctreeDownwards(shapePosition, traversalData);
OctreeNodeData nodeData = traverseOctreeDownwards(tileCoords, traversalData);
#if (SAMPLE_COUNT == 1)
getOctreeLeafSampleData(nodeData, traversalData.octreeCoords, sampleDatas[0]);
addSampleCoordinates(shapePosition, sampleDatas[0]);
addSampleCoordinates(tileAndUv, sampleDatas[0]);
#else
getOctreeLeafSampleDatas(nodeData, traversalData.octreeCoords, sampleDatas);
addSampleCoordinates(shapePosition, sampleDatas[0]);
addSampleCoordinates(shapePosition, sampleDatas[1]);
addSampleCoordinates(tileAndUv, sampleDatas[0]);
addSampleCoordinates(tileAndUv, sampleDatas[1]);
#endif
}

View File

@ -1,9 +1,9 @@
// See Intersection.glsl for the definition of intersectScene
// See IntersectionUtils.glsl for the definition of nextIntersection
// See convertUvToBox.glsl, convertUvToCylinder.glsl, or convertUvToEllipsoid.glsl
// for the definition of convertUvToShapeUvSpace. The appropriate function is
// selected based on the VoxelPrimitive shape type, and added to the shader in
// Scene/VoxelRenderResources.js.
// See convertLocalToBoxUv.glsl, convertLocalToCylinderUv.glsl, or convertLocalToEllipsoidUv.glsl
// for the definitions of convertLocalToShapeSpaceDerivative and getTileAndUvCoordinate.
// The appropriate functions are selected based on the VoxelPrimitive shape type,
// and added to the shader in Scene/VoxelRenderResources.js.
// See Octree.glsl for the definitions of TraversalData, SampleData,
// traverseOctreeFromBeginning, and traverseOctreeFromExisting
// See Megatexture.glsl for the definition of accumulatePropertiesFromMegatexture
@ -15,10 +15,10 @@
#define ALPHA_ACCUM_MAX 0.98 // Must be > 0.0 and <= 1.0
#endif
uniform mat4 u_transformPositionUvToView;
uniform mat4 u_transformPositionViewToLocal;
uniform mat3 u_transformDirectionViewToLocal;
uniform vec3 u_cameraPositionUv;
uniform vec3 u_cameraDirectionUv;
uniform vec3 u_cameraPositionLocal;
uniform vec3 u_cameraDirectionLocal;
uniform float u_stepSize;
#if defined(PICKING)
@ -63,16 +63,15 @@ RayShapeIntersection getVoxelIntersection(in vec3 tileUv, in vec3 sampleSizeAlon
}
vec4 getStepSize(in SampleData sampleData, in Ray viewRay, in RayShapeIntersection shapeIntersection, in mat3 jacobianT, in float currentT) {
// The Jacobian is computed in a space where the shape spans [-1, 1].
// But the ray is marched in a space where the shape fills [0, 1].
// So we need to scale the Jacobian by 2.
vec3 gradient = 2.0 * viewRay.rawDir * jacobianT;
vec3 gradient = viewRay.dir * jacobianT;
vec3 sampleSizeAlongRay = getSampleSize(sampleData.tileCoords.w) / gradient;
RayShapeIntersection voxelIntersection = getVoxelIntersection(sampleData.tileUv, sampleSizeAlongRay);
// Transform normal from shape space to Cartesian space
vec3 voxelNormal = normalize(jacobianT * voxelIntersection.entry.xyz);
// Transform normal from shape space to Cartesian space to eye space
vec3 voxelNormal = jacobianT * voxelIntersection.entry.xyz;
voxelNormal = normalize(czm_normal * voxelNormal);
// Compare with the shape intersection, to choose the appropriate normal
vec4 voxelEntry = vec4(voxelNormal, currentT + voxelIntersection.entry.w);
vec4 entry = intersectionMax(shapeIntersection.entry, voxelEntry);
@ -114,37 +113,40 @@ int getSampleIndex(in SampleData sampleData) {
}
/**
* Compute the view ray at the current fragment, in the local UV coordinates of the shape.
* Compute the view ray at the current fragment, in the local coordinates of the shape.
*/
Ray getViewRayUv() {
Ray getViewRayLocal() {
vec4 eyeCoordinates = czm_windowToEyeCoordinates(gl_FragCoord);
vec3 viewDirUv;
vec3 viewPosUv;
vec3 origin;
vec3 direction;
if (czm_orthographicIn3D == 1.0) {
eyeCoordinates.z = 0.0;
viewPosUv = (u_transformPositionViewToUv * eyeCoordinates).xyz;
viewDirUv = normalize(u_cameraDirectionUv);
origin = (u_transformPositionViewToLocal * eyeCoordinates).xyz;
direction = u_cameraDirectionLocal;
} else {
viewPosUv = u_cameraPositionUv;
viewDirUv = normalize(u_transformDirectionViewToLocal * eyeCoordinates.xyz);
origin = u_cameraPositionLocal;
direction = u_transformDirectionViewToLocal * normalize(eyeCoordinates.xyz);
}
#if defined(SHAPE_ELLIPSOID)
// viewDirUv has been scaled to a space where the ellipsoid is a sphere.
// Undo this scaling to get the raw direction.
vec3 rawDir = viewDirUv * u_ellipsoidRadiiUv;
return Ray(viewPosUv, viewDirUv, rawDir);
#else
return Ray(viewPosUv, viewDirUv, viewDirUv);
#endif
return Ray(origin, direction);
}
Ray getViewRayEC() {
vec4 eyeCoordinates = czm_windowToEyeCoordinates(gl_FragCoord);
vec3 viewPosEC = (czm_orthographicIn3D == 1.0)
? vec3(eyeCoordinates.xy, 0.0)
: vec3(0.0);
vec3 viewDirEC = normalize(eyeCoordinates.xyz);
return Ray(viewPosEC, viewDirEC);
}
void main()
{
Ray viewRayUv = getViewRayUv();
Ray viewRayLocal = getViewRayLocal();
Ray viewRayEC = getViewRayEC();
Intersections ix;
vec2 screenCoord = (gl_FragCoord.xy - czm_viewport.xy) / czm_viewport.zw; // [0,1]
RayShapeIntersection shapeIntersection = intersectScene(screenCoord, viewRayUv, ix);
RayShapeIntersection shapeIntersection = intersectScene(screenCoord, viewRayLocal, viewRayEC, ix);
// Exit early if the scene was completely missed.
if (shapeIntersection.entry.w == NO_HIT) {
discard;
@ -152,20 +154,17 @@ void main()
float currentT = shapeIntersection.entry.w;
float endT = shapeIntersection.exit.w;
vec3 positionUv = viewRayUv.pos + currentT * viewRayUv.dir;
PointJacobianT pointJacobian = convertUvToShapeUvSpaceDerivative(positionUv);
vec3 positionEC = viewRayEC.pos + currentT * viewRayEC.dir;
TileAndUvCoordinate tileAndUv = getTileAndUvCoordinate(positionEC);
vec3 positionLocal = viewRayLocal.pos + currentT * viewRayLocal.dir;
mat3 jacobianT = convertLocalToShapeSpaceDerivative(positionLocal);
// Traverse the tree from the start position
TraversalData traversalData;
SampleData sampleDatas[SAMPLE_COUNT];
traverseOctreeFromBeginning(pointJacobian.point, traversalData, sampleDatas);
vec4 step = getStepSize(sampleDatas[0], viewRayUv, shapeIntersection, pointJacobian.jacobianT, currentT);
#if defined(JITTER)
float noise = hash(screenCoord); // [0,1]
currentT += noise * step.w;
positionUv += noise * step.w * viewRayUv.dir;
#endif
traverseOctreeFromBeginning(tileAndUv, traversalData, sampleDatas);
vec4 step = getStepSize(sampleDatas[0], viewRayLocal, shapeIntersection, jacobianT, currentT);
FragmentInput fragmentInput;
#if defined(STATISTICS)
@ -182,10 +181,11 @@ void main()
// Prepare the custom shader inputs
copyPropertiesToMetadata(properties, fragmentInput.metadata);
fragmentInput.attributes.positionEC = vec3(u_transformPositionUvToView * vec4(positionUv, 1.0));
fragmentInput.attributes.normalEC = normalize(czm_normal * step.xyz);
fragmentInput.attributes.positionEC = positionEC;
// Re-normalize normals: some shape intersections may have been scaled to encode positive/negative shapes
fragmentInput.attributes.normalEC = normalize(step.xyz);
fragmentInput.voxel.viewDirUv = viewRayUv.dir;
fragmentInput.voxel.viewDirUv = viewRayLocal.dir;
fragmentInput.voxel.travelDistance = step.w;
fragmentInput.voxel.stepCount = stepCount;
@ -233,13 +233,15 @@ void main()
}
#endif
}
positionUv = viewRayUv.pos + currentT * viewRayUv.dir;
positionEC = viewRayEC.pos + currentT * viewRayEC.dir;
tileAndUv = getTileAndUvCoordinate(positionEC);
positionLocal = viewRayLocal.pos + currentT * viewRayLocal.dir;
jacobianT = convertLocalToShapeSpaceDerivative(positionLocal);
// Traverse the tree from the current ray position.
// This is similar to traverseOctreeFromBeginning but is faster when the ray is in the same tile as the previous step.
pointJacobian = convertUvToShapeUvSpaceDerivative(positionUv);
traverseOctreeFromExisting(pointJacobian.point, traversalData, sampleDatas);
step = getStepSize(sampleDatas[0], viewRayUv, shapeIntersection, pointJacobian.jacobianT, currentT);
traverseOctreeFromExisting(tileAndUv, traversalData, sampleDatas);
step = getStepSize(sampleDatas[0], viewRayLocal, shapeIntersection, jacobianT, currentT);
}
// Convert the alpha from [0,ALPHA_ACCUM_MAX] to [0,1]

View File

@ -1,22 +1,8 @@
struct Ray {
vec3 pos;
vec3 dir;
vec3 rawDir;
};
#if defined(JITTER)
/**
* Generate a pseudo-random value for a given 2D screen coordinate.
* Similar to https://www.shadertoy.com/view/4djSRW with a modified hashscale.
*/
float hash(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * 50.0);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.x + p3.y) * p3.z);
}
#endif
float minComponent(in vec3 v) {
return min(min(v.x, v.y), v.z);
}
@ -24,8 +10,3 @@ float minComponent(in vec3 v) {
float maxComponent(in vec3 v) {
return max(max(v.x, v.y), v.z);
}
struct PointJacobianT {
vec3 point;
mat3 jacobianT;
};

View File

@ -0,0 +1,30 @@
uniform vec3 u_boxLocalToShapeUvScale;
uniform vec3 u_boxLocalToShapeUvTranslate;
uniform ivec4 u_cameraTileCoordinates;
uniform vec3 u_cameraTileUv;
uniform mat3 u_boxEcToXyz;
mat3 convertLocalToShapeSpaceDerivative(in vec3 positionLocal) {
// For BOX, local space = shape space, so the Jacobian is the identity matrix.
return mat3(1.0);
}
vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) {
return shapeUv / u_boxLocalToShapeUvScale;
}
vec3 convertEcToDeltaTile(in vec3 positionEC) {
vec3 dPosition = u_boxEcToXyz * positionEC;
return u_boxLocalToShapeUvScale * dPosition * float(1 << u_cameraTileCoordinates.w);
}
TileAndUvCoordinate getTileAndUvCoordinate(in vec3 positionEC) {
vec3 deltaTileCoordinate = convertEcToDeltaTile(positionEC);
vec3 tileUvSum = u_cameraTileUv + deltaTileCoordinate;
ivec3 tileCoordinate = u_cameraTileCoordinates.xyz + ivec3(floor(tileUvSum));
tileCoordinate = min(max(ivec3(0), tileCoordinate), ivec3((1 << u_cameraTileCoordinates.w) - 1));
ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz;
vec3 tileUv = clamp(tileUvSum - vec3(tileCoordinateChange), 0.0, 1.0);
return TileAndUvCoordinate(ivec4(tileCoordinate, u_cameraTileCoordinates.w), tileUv);
}

View File

@ -0,0 +1,97 @@
uniform vec2 u_cylinderLocalToShapeUvRadius; // x = scale, y = offset
uniform vec2 u_cylinderLocalToShapeUvHeight; // x = scale, y = offset
uniform vec2 u_cylinderLocalToShapeUvAngle; // x = scale, y = offset
uniform float u_cylinderShapeUvAngleRangeOrigin;
uniform mat3 u_cylinderEcToRadialTangentUp;
uniform ivec4 u_cameraTileCoordinates;
uniform vec3 u_cameraTileUv;
uniform vec3 u_cameraShapePosition; // (radial distance, angle, height) of camera in shape space
mat3 convertLocalToShapeSpaceDerivative(in vec3 position) {
vec3 radial = normalize(vec3(position.xy, 0.0));
vec3 z = vec3(0.0, 0.0, 1.0);
vec3 east = normalize(vec3(-position.y, position.x, 0.0));
return mat3(radial, east / length(position.xy), z);
}
vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) {
float radius = shapeUv.x / u_cylinderLocalToShapeUvRadius.x;
float angle = shapeUv.y * czm_twoPi / u_cylinderLocalToShapeUvAngle.x;
float height = shapeUv.z / u_cylinderLocalToShapeUvHeight.x;
return vec3(radius, angle, height);
}
/**
* Computes the change in polar coordinates given a change in position.
* @param {vec2} dPosition The change in position in Cartesian coordinates.
* @param {float} cameraRadialDistance The radial distance of the camera from the origin.
* @return {vec2} The change in polar coordinates (radial distance, angle).
*/
vec2 computePolarChange(in vec2 dPosition, in float cameraRadialDistance) {
float dAngle = atan(dPosition.y, cameraRadialDistance + dPosition.x);
// Find the direction of the radial axis at the output angle, in Cartesian coordinates
vec2 outputRadialAxis = vec2(cos(dAngle), sin(dAngle));
float sinHalfAngle = sin(dAngle / 2.0);
float versine = 2.0 * sinHalfAngle * sinHalfAngle;
float dRadial = dot(dPosition, outputRadialAxis) - cameraRadialDistance * versine;
return vec2(dRadial, dAngle);
}
vec3 convertEcToDeltaShape(in vec3 positionEC) {
// 1. Rotate to radial, tangent, and up coordinates
vec3 rtu = u_cylinderEcToRadialTangentUp * positionEC;
// 2. Compute change in angular and radial coordinates.
vec2 dPolar = computePolarChange(rtu.xy, u_cameraShapePosition.x);
return vec3(dPolar.xy, rtu.z);
}
vec3 convertEcToDeltaTile(in vec3 positionEC) {
vec3 deltaShape = convertEcToDeltaShape(positionEC);
// Convert to tileset coordinates in [0, 1]
float dx = u_cylinderLocalToShapeUvRadius.x * deltaShape.x;
float dy = deltaShape.y / czm_twoPi;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE)
// Wrap to ensure dy is not crossing through the unoccupied angle range, where
// angle to tile coordinate conversions would be more complicated
float cameraUvAngle = (u_cameraShapePosition.y + czm_pi) / czm_twoPi;
float cameraUvAngleShift = fract(cameraUvAngle - u_cylinderShapeUvAngleRangeOrigin);
float rawOutputUvAngle = cameraUvAngleShift + dy;
float rotation = floor(rawOutputUvAngle);
dy -= rotation;
#endif
dy *= u_cylinderLocalToShapeUvAngle.x;
float dz = u_cylinderLocalToShapeUvHeight.x * deltaShape.z;
// Convert to tile coordinate changes
return vec3(dx, dy, dz) * float(1 << u_cameraTileCoordinates.w);
}
TileAndUvCoordinate getTileAndUvCoordinate(in vec3 positionEC) {
vec3 deltaTileCoordinate = convertEcToDeltaTile(positionEC);
vec3 tileUvSum = u_cameraTileUv + deltaTileCoordinate;
ivec3 tileCoordinate = u_cameraTileCoordinates.xyz + ivec3(floor(tileUvSum));
int maxTileCoordinate = (1 << u_cameraTileCoordinates.w) - 1;
tileCoordinate.x = min(max(0, tileCoordinate.x), maxTileCoordinate);
tileCoordinate.z = min(max(0, tileCoordinate.z), maxTileCoordinate);
#if (!defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE))
ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz;
if (tileCoordinate.y < 0) {
tileCoordinate.y += (maxTileCoordinate + 1);
} else if (tileCoordinate.y > maxTileCoordinate) {
tileCoordinate.y -= (maxTileCoordinate + 1);
}
#else
tileCoordinate.y = min(max(0, tileCoordinate.y), maxTileCoordinate);
ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz;
#endif
vec3 tileUv = tileUvSum - vec3(tileCoordinateChange);
tileUv.x = clamp(tileUv.x, 0.0, 1.0);
#if (!defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE))
// If there is only one tile spanning 2*PI angle, the coordinate wraps around
tileUv.y = (u_cameraTileCoordinates.w == 0) ? fract(tileUv.y) : clamp(tileUv.y, 0.0, 1.0);
#else
tileUv.y = clamp(tileUv.y, 0.0, 1.0);
#endif
tileUv.z = clamp(tileUv.z, 0.0, 1.0);
return TileAndUvCoordinate(ivec4(tileCoordinate, u_cameraTileCoordinates.w), tileUv);
}

View File

@ -0,0 +1,193 @@
/* Ellipsoid defines (set in Scene/VoxelEllipsoidShape.js)
#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY
#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY
#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE
#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED
#define ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE
*/
uniform vec3 u_cameraPositionCartographic; // (longitude, latitude, height) in radians and meters
uniform vec2 u_ellipsoidCurvatureAtLatitude;
uniform mat3 u_ellipsoidEcToEastNorthUp;
uniform vec3 u_ellipsoidRadii;
uniform vec2 u_evoluteScale; // (radii.x ^ 2 - radii.z ^ 2) * vec2(1.0, -1.0) / radii;
uniform vec3 u_ellipsoidInverseRadiiSquared;
#if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY) || defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY) || defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED)
uniform vec3 u_ellipsoidShapeUvLongitudeMinMaxMid;
#endif
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)
uniform vec2 u_ellipsoidLocalToShapeUvLongitude; // x = scale, y = offset
uniform float u_ellipsoidShapeUvLongitudeRangeOrigin;
#endif
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)
uniform vec2 u_ellipsoidLocalToShapeUvLatitude; // x = scale, y = offset
#endif
uniform float u_ellipsoidInverseHeightDifference;
uniform ivec4 u_cameraTileCoordinates;
uniform vec3 u_cameraTileUv;
// robust iterative solution without trig functions
// https://github.com/0xfaded/ellipse_demo/issues/1
// https://stackoverflow.com/questions/22959698/distance-from-given-point-to-given-ellipse
// Extended to return radius of curvature along with the point
vec3 nearestPointAndRadiusOnEllipse(vec2 pos, vec2 radii) {
vec2 p = abs(pos);
vec2 inverseRadii = 1.0 / radii;
// We describe the ellipse parametrically: v = radii * vec2(cos(t), sin(t))
// but store the cos and sin of t in a vec2 for efficiency.
// Initial guess: t = pi/4
vec2 tTrigs = vec2(0.7071067811865476);
// Initial guess of point on ellipsoid
vec2 v = radii * tTrigs;
// Center of curvature of the ellipse at v
vec2 evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs;
const int iterations = 3;
for (int i = 0; i < iterations; ++i) {
// Find the (approximate) intersection of p - evolute with the ellipsoid.
vec2 q = normalize(p - evolute) * length(v - evolute);
// Update the estimate of t.
tTrigs = (q + evolute) * inverseRadii;
tTrigs = normalize(clamp(tTrigs, 0.0, 1.0));
v = radii * tTrigs;
evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs;
}
return vec3(v * sign(pos), length(v - evolute));
}
mat3 convertLocalToShapeSpaceDerivative(in vec3 position) {
vec3 east = normalize(vec3(-position.y, position.x, 0.0));
// Convert the 3D position to a 2D position relative to the ellipse (radii.x, radii.z)
// (assume radii.y == radii.x) and find the nearest point on the ellipse and its normal
float distanceFromZAxis = length(position.xy);
vec2 posEllipse = vec2(distanceFromZAxis, position.z);
vec3 surfacePointAndRadius = nearestPointAndRadiusOnEllipse(posEllipse, u_ellipsoidRadii.xz);
vec2 surfacePoint = surfacePointAndRadius.xy;
vec2 normal2d = normalize(surfacePoint * u_ellipsoidInverseRadiiSquared.xz);
vec3 north = vec3(-normal2d.y * normalize(position.xy), abs(normal2d.x));
float heightSign = length(posEllipse) < length(surfacePoint) ? -1.0 : 1.0;
float height = heightSign * length(posEllipse - surfacePoint);
vec3 up = normalize(cross(east, north));
return mat3(east / distanceFromZAxis, north / (surfacePointAndRadius.z + height), up);
}
vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) {
// Convert from [0, 1] to radians [-pi, pi]
float longitude = shapeUv.x * czm_twoPi;
#if defined (ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)
longitude /= u_ellipsoidLocalToShapeUvLongitude.x;
#endif
// Convert from [0, 1] to radians [-pi/2, pi/2]
float latitude = shapeUv.y * czm_pi;
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)
latitude /= u_ellipsoidLocalToShapeUvLatitude.x;
#endif
float height = shapeUv.z / u_ellipsoidInverseHeightDifference;
return vec3(longitude, latitude, height);
}
vec3 convertEcToDeltaShape(in vec3 positionEC) {
vec3 enu = u_ellipsoidEcToEastNorthUp * positionEC;
// 1. Compute the change in longitude from the camera to the ENU point
// First project the camera and ENU positions to the equatorial XY plane,
// positioning the camera on the +x axis, so that enu.x projects along the +y axis
float cosLatitude = cos(u_cameraPositionCartographic.y);
float sinLatitude = sin(u_cameraPositionCartographic.y);
float primeVerticalRadius = 1.0 / u_ellipsoidCurvatureAtLatitude.x;
vec2 cameraXY = vec2((primeVerticalRadius + u_cameraPositionCartographic.z) * cosLatitude, 0.0);
// Note precision loss in positionXY.x if length(enu) << length(cameraXY)
vec2 positionXY = cameraXY + vec2(-enu.y * sinLatitude + enu.z * cosLatitude, enu.x);
float dLongitude = atan(positionXY.y, positionXY.x);
// 2. Find the longitude component of positionXY, by rotating about Z until the y component is zero.
// Use the versine to compute the change in x directly from the change in angle:
// versine(angle) = 2 * sin^2(angle/2)
float sinHalfLongitude = sin(dLongitude / 2.0);
float dx = length(positionXY) * 2.0 * sinHalfLongitude * sinHalfLongitude;
// Rotate longitude component back to ENU North and Up, and remove from enu
enu += vec3(-enu.x, -dx * sinLatitude, dx * cosLatitude);
// 3. Compute the change in latitude from the camera to the ENU point.
// First project the camera and ENU positions to the meridional ZX plane,
// positioning the camera on the +Z axis, so that enu.y maps to the +X axis.
float meridionalRadius = 1.0 / u_ellipsoidCurvatureAtLatitude.y;
vec2 cameraZX = vec2(meridionalRadius + u_cameraPositionCartographic.z, 0.0);
vec2 positionZX = cameraZX + vec2(enu.z, enu.y);
float dLatitude = atan(positionZX.y, positionZX.x);
// 4. Compute the change in height above the ellipsoid
// Find the change in enu.z associated with rotating the point to the latitude of the camera
float sinHalfLatitude = sin(dLatitude / 2.0);
float dz = length(positionZX) * 2.0 * sinHalfLatitude * sinHalfLatitude;
// The remaining change in enu.z is the change in height above the ellipsoid
float dHeight = enu.z + dz;
return vec3(dLongitude, dLatitude, dHeight);
}
vec3 convertEcToDeltaTile(in vec3 positionEC) {
vec3 deltaShape = convertEcToDeltaShape(positionEC);
// Convert to tileset coordinates in [0, 1]
float dx = deltaShape.x / czm_twoPi;
#if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE))
// Wrap to ensure dx is not crossing through the unoccupied angle range, where
// angle to tile coordinate conversions would be more complicated
float cameraUvLongitude = (u_cameraPositionCartographic.x + czm_pi) / czm_twoPi;
float cameraUvLongitudeShift = fract(cameraUvLongitude - u_ellipsoidShapeUvLongitudeRangeOrigin);
float rawOutputUvLongitude = cameraUvLongitudeShift + dx;
float rotation = floor(rawOutputUvLongitude);
dx -= rotation;
dx *= u_ellipsoidLocalToShapeUvLongitude.x;
#endif
float dy = deltaShape.y / czm_pi;
#if (defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE))
dy *= u_ellipsoidLocalToShapeUvLatitude.x;
#endif
float dz = u_ellipsoidInverseHeightDifference * deltaShape.z;
// Convert to tile coordinate changes
return vec3(dx, dy, dz) * float(1 << u_cameraTileCoordinates.w);
}
TileAndUvCoordinate getTileAndUvCoordinate(in vec3 positionEC) {
vec3 deltaTileCoordinate = convertEcToDeltaTile(positionEC);
vec3 tileUvSum = u_cameraTileUv + deltaTileCoordinate;
ivec3 tileCoordinate = u_cameraTileCoordinates.xyz + ivec3(floor(tileUvSum));
int maxTileCoordinate = (1 << u_cameraTileCoordinates.w) - 1;
tileCoordinate.y = min(max(0, tileCoordinate.y), maxTileCoordinate);
tileCoordinate.z = min(max(0, tileCoordinate.z), maxTileCoordinate);
#if (!defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE))
ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz;
if (tileCoordinate.x < 0) {
tileCoordinate.x += (maxTileCoordinate + 1);
} else if (tileCoordinate.x > maxTileCoordinate) {
tileCoordinate.x -= (maxTileCoordinate + 1);
}
#else
tileCoordinate.x = min(max(0, tileCoordinate.x), maxTileCoordinate);
ivec3 tileCoordinateChange = tileCoordinate - u_cameraTileCoordinates.xyz;
#endif
vec3 tileUv = tileUvSum - vec3(tileCoordinateChange);
#if (!defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE))
// If there is only one tile spanning 2*PI angle, the coordinate wraps around
tileUv.x = (u_cameraTileCoordinates.w == 0) ? fract(tileUv.x) : clamp(tileUv.x, 0.0, 1.0);
#else
tileUv.x = clamp(tileUv.x, 0.0, 1.0);
#endif
tileUv.y = clamp(tileUv.y, 0.0, 1.0);
tileUv.z = clamp(tileUv.z, 0.0, 1.0);
return TileAndUvCoordinate(ivec4(tileCoordinate, u_cameraTileCoordinates.w), tileUv);
}

View File

@ -1,45 +0,0 @@
/* Box defines (set in Scene/VoxelBoxShape.js)
#define BOX_HAS_SHAPE_BOUNDS
*/
#if defined(BOX_HAS_SHAPE_BOUNDS)
uniform vec3 u_boxUvToShapeUvScale;
uniform vec3 u_boxUvToShapeUvTranslate;
#endif
PointJacobianT convertUvToShapeSpaceDerivative(in vec3 positionUv) {
// For BOX, UV space = shape space, so we can use positionUv as-is,
// and the Jacobian is the identity matrix, except that a step of 1
// only spans half the shape space [-1, 1], so the identity is scaled.
return PointJacobianT(positionUv, mat3(0.5));
}
vec3 convertShapeToShapeUvSpace(in vec3 positionShape) {
#if defined(BOX_HAS_SHAPE_BOUNDS)
return positionShape * u_boxUvToShapeUvScale + u_boxUvToShapeUvTranslate;
#else
return positionShape;
#endif
}
PointJacobianT convertUvToShapeUvSpaceDerivative(in vec3 positionUv) {
PointJacobianT pointJacobian = convertUvToShapeSpaceDerivative(positionUv);
pointJacobian.point = convertShapeToShapeUvSpace(pointJacobian.point);
return pointJacobian;
}
vec3 convertShapeUvToUvSpace(in vec3 shapeUv) {
#if defined(BOX_HAS_SHAPE_BOUNDS)
return (shapeUv - u_boxUvToShapeUvTranslate) / u_boxUvToShapeUvScale;
#else
return shapeUv;
#endif
}
vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) {
#if defined(BOX_HAS_SHAPE_BOUNDS)
return shapeUv / u_boxUvToShapeUvScale;
#else
return shapeUv;
#endif
}

View File

@ -1,99 +0,0 @@
/* Cylinder defines (set in Scene/VoxelCylinderShape.js)
#define CYLINDER_HAS_SHAPE_BOUNDS_RADIUS
#define CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT
#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE
#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY
#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY
#define CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED
*/
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_RADIUS)
uniform vec2 u_cylinderUvToShapeUvRadius; // x = scale, y = offset
#endif
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT)
uniform vec2 u_cylinderUvToShapeUvHeight; // x = scale, y = offset
#endif
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE)
uniform vec2 u_cylinderUvToShapeUvAngle; // x = scale, y = offset
#endif
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY) || defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY)
uniform vec2 u_cylinderShapeUvAngleMinMax;
#endif
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY) || defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY) || defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED)
uniform float u_cylinderShapeUvAngleRangeZeroMid;
#endif
PointJacobianT convertUvToShapeSpaceDerivative(in vec3 positionUv) {
// Convert from Cartesian UV space [0, 1] to Cartesian local space [-1, 1]
vec3 position = positionUv * 2.0 - 1.0;
float radius = length(position.xy); // [0, 1]
vec3 radial = normalize(vec3(position.xy, 0.0));
// Shape space height is defined within [0, 1]
float height = positionUv.z; // [0, 1]
vec3 z = vec3(0.0, 0.0, 1.0);
float angle = atan(position.y, position.x);
vec3 east = normalize(vec3(-position.y, position.x, 0.0));
vec3 point = vec3(radius, angle, height);
mat3 jacobianT = mat3(radial, east / length(position.xy), z);
return PointJacobianT(point, jacobianT);
}
vec3 convertShapeToShapeUvSpace(in vec3 positionShape) {
float radius = positionShape.x;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_RADIUS)
radius = radius * u_cylinderUvToShapeUvRadius.x + u_cylinderUvToShapeUvRadius.y;
#endif
float angle = (positionShape.y + czm_pi) / czm_twoPi;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE)
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_MAX_REVERSED)
// Comparing against u_cylinderShapeUvAngleMinMax has precision problems. u_cylinderShapeUvAngleRangeZeroMid is more conservative.
angle += float(angle < u_cylinderShapeUvAngleRangeZeroMid);
#endif
// Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity.
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MIN_DISCONTINUITY)
angle = angle > u_cylinderShapeUvAngleRangeZeroMid ? u_cylinderShapeUvAngleMinMax.x : angle;
#elif defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE_MAX_DISCONTINUITY)
angle = angle < u_cylinderShapeUvAngleRangeZeroMid ? u_cylinderShapeUvAngleMinMax.y : angle;
#endif
angle = angle * u_cylinderUvToShapeUvAngle.x + u_cylinderUvToShapeUvAngle.y;
#endif
float height = positionShape.z;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT)
height = height * u_cylinderUvToShapeUvHeight.x + u_cylinderUvToShapeUvHeight.y;
#endif
return vec3(radius, angle, height);
}
PointJacobianT convertUvToShapeUvSpaceDerivative(in vec3 positionUv) {
PointJacobianT pointJacobian = convertUvToShapeSpaceDerivative(positionUv);
pointJacobian.point = convertShapeToShapeUvSpace(pointJacobian.point);
return pointJacobian;
}
vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) {
float radius = shapeUv.x;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_RADIUS)
radius /= u_cylinderUvToShapeUvRadius.x;
#endif
float angle = shapeUv.y * czm_twoPi;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_ANGLE)
angle /= u_cylinderUvToShapeUvAngle.x;
#endif
float height = shapeUv.z;
#if defined(CYLINDER_HAS_SHAPE_BOUNDS_HEIGHT)
height /= u_cylinderUvToShapeUvHeight.x;
#endif
return vec3(radius, angle, height);
}

View File

@ -1,139 +0,0 @@
/* Ellipsoid defines (set in Scene/VoxelEllipsoidShape.js)
#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY
#define ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY
#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE
#define ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED
#define ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE
*/
uniform vec3 u_ellipsoidRadiiUv; // [0,1]
uniform vec2 u_evoluteScale; // (radiiUv.x ^ 2 - radiiUv.z ^ 2) * vec2(1.0, -1.0) / radiiUv;
uniform vec3 u_ellipsoidInverseRadiiSquaredUv;
#if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY) || defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY) || defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED)
uniform vec3 u_ellipsoidShapeUvLongitudeMinMaxMid;
#endif
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)
uniform vec2 u_ellipsoidUvToShapeUvLongitude; // x = scale, y = offset
#endif
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)
uniform vec2 u_ellipsoidUvToShapeUvLatitude; // x = scale, y = offset
#endif
uniform float u_ellipsoidInverseHeightDifferenceUv;
// robust iterative solution without trig functions
// https://github.com/0xfaded/ellipse_demo/issues/1
// https://stackoverflow.com/questions/22959698/distance-from-given-point-to-given-ellipse
// Extended to return radius of curvature along with the point
vec3 nearestPointAndRadiusOnEllipse(vec2 pos, vec2 radii) {
vec2 p = abs(pos);
vec2 inverseRadii = 1.0 / radii;
// We describe the ellipse parametrically: v = radii * vec2(cos(t), sin(t))
// but store the cos and sin of t in a vec2 for efficiency.
// Initial guess: t = pi/4
vec2 tTrigs = vec2(0.7071067811865476);
// Initial guess of point on ellipsoid
vec2 v = radii * tTrigs;
// Center of curvature of the ellipse at v
vec2 evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs;
const int iterations = 3;
for (int i = 0; i < iterations; ++i) {
// Find the (approximate) intersection of p - evolute with the ellipsoid.
vec2 q = normalize(p - evolute) * length(v - evolute);
// Update the estimate of t.
tTrigs = (q + evolute) * inverseRadii;
tTrigs = normalize(clamp(tTrigs, 0.0, 1.0));
v = radii * tTrigs;
evolute = u_evoluteScale * tTrigs * tTrigs * tTrigs;
}
return vec3(v * sign(pos), length(v - evolute));
}
PointJacobianT convertUvToShapeSpaceDerivative(in vec3 positionUv) {
// Convert from UV space [0, 1] to local space [-1, 1]
vec3 position = positionUv * 2.0 - 1.0;
// Undo the scaling from ellipsoid to sphere
position = position * u_ellipsoidRadiiUv;
float longitude = atan(position.y, position.x);
vec3 east = normalize(vec3(-position.y, position.x, 0.0));
// Convert the 3D position to a 2D position relative to the ellipse (radii.x, radii.z)
// (assume radii.y == radii.x) and find the nearest point on the ellipse and its normal
float distanceFromZAxis = length(position.xy);
vec2 posEllipse = vec2(distanceFromZAxis, position.z);
vec3 surfacePointAndRadius = nearestPointAndRadiusOnEllipse(posEllipse, u_ellipsoidRadiiUv.xz);
vec2 surfacePoint = surfacePointAndRadius.xy;
vec2 normal2d = normalize(surfacePoint * u_ellipsoidInverseRadiiSquaredUv.xz);
float latitude = atan(normal2d.y, normal2d.x);
vec3 north = vec3(-normal2d.y * normalize(position.xy), abs(normal2d.x));
float heightSign = length(posEllipse) < length(surfacePoint) ? -1.0 : 1.0;
float height = heightSign * length(posEllipse - surfacePoint);
vec3 up = normalize(cross(east, north));
vec3 point = vec3(longitude, latitude, height);
mat3 jacobianT = mat3(east / distanceFromZAxis, north / (surfacePointAndRadius.z + height), up);
return PointJacobianT(point, jacobianT);
}
vec3 convertShapeToShapeUvSpace(in vec3 positionShape) {
// Longitude: shift & scale to [0, 1]
float longitude = (positionShape.x + czm_pi) / czm_twoPi;
// Correct the angle when max < min
// Technically this should compare against min longitude - but it has precision problems so compare against the middle of empty space.
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE_MIN_MAX_REVERSED)
longitude += float(longitude < u_ellipsoidShapeUvLongitudeMinMaxMid.z);
#endif
// Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity.
#if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MIN_DISCONTINUITY)
longitude = longitude > u_ellipsoidShapeUvLongitudeMinMaxMid.z ? u_ellipsoidShapeUvLongitudeMinMaxMid.x : longitude;
#endif
#if defined(ELLIPSOID_HAS_RENDER_BOUNDS_LONGITUDE_MAX_DISCONTINUITY)
longitude = longitude < u_ellipsoidShapeUvLongitudeMinMaxMid.z ? u_ellipsoidShapeUvLongitudeMinMaxMid.y : longitude;
#endif
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)
longitude = longitude * u_ellipsoidUvToShapeUvLongitude.x + u_ellipsoidUvToShapeUvLongitude.y;
#endif
// Latitude: shift and scale to [0, 1]
float latitude = (positionShape.y + czm_piOverTwo) / czm_pi;
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)
latitude = latitude * u_ellipsoidUvToShapeUvLatitude.x + u_ellipsoidUvToShapeUvLatitude.y;
#endif
// Height: scale to the range [0, 1]
float height = 1.0 + positionShape.z * u_ellipsoidInverseHeightDifferenceUv;
return vec3(longitude, latitude, height);
}
PointJacobianT convertUvToShapeUvSpaceDerivative(in vec3 positionUv) {
PointJacobianT pointJacobian = convertUvToShapeSpaceDerivative(positionUv);
pointJacobian.point = convertShapeToShapeUvSpace(pointJacobian.point);
return pointJacobian;
}
vec3 scaleShapeUvToShapeSpace(in vec3 shapeUv) {
// Convert from [0, 1] to radians [-pi, pi]
float longitude = shapeUv.x * czm_twoPi;
#if defined (ELLIPSOID_HAS_SHAPE_BOUNDS_LONGITUDE)
longitude /= u_ellipsoidUvToShapeUvLongitude.x;
#endif
// Convert from [0, 1] to radians [-pi/2, pi/2]
float latitude = shapeUv.y * czm_pi;
#if defined(ELLIPSOID_HAS_SHAPE_BOUNDS_LATITUDE)
latitude /= u_ellipsoidUvToShapeUvLatitude.x;
#endif
float height = shapeUv.z / u_ellipsoidInverseHeightDifferenceUv;
return vec3(longitude, latitude, height);
}

View File

@ -1,15 +0,0 @@
import { defaultValue } from "../../index.js";
describe("Core/defaultValue", function () {
it("Works with first parameter undefined", function () {
expect(defaultValue(undefined, 5)).toEqual(5);
});
it("Works with first parameter null", function () {
expect(defaultValue(null, 5)).toEqual(5);
});
it("Works with first parameter not undefined and not null", function () {
expect(defaultValue(1, 5)).toEqual(1);
});
});

View File

@ -0,0 +1,194 @@
import {
Math as CesiumMath,
Rectangle,
Request,
RequestScheduler,
Resource,
WebMercatorTilingScheme,
Imagery,
ImageryLayer,
ImageryProvider,
ImageryState,
Azure2DImageryProvider,
} from "../../index.js";
import pollToPromise from "../../../../Specs/pollToPromise.js";
describe("Scene/Azure2DImageryProvider", function () {
afterEach(function () {
Resource._Implementations.createImage =
Resource._DefaultImplementations.createImage;
});
it("conforms to ImageryProvider interface", function () {
expect(Azure2DImageryProvider).toConformToInterface(ImageryProvider);
});
it("requires the subscription key to be specified", function () {
expect(function () {
return new Azure2DImageryProvider({
tilesetId: "a-tileset-id",
});
}).toThrowDeveloperError(
"options.subscriptionKey is required, actual value was undefined",
);
});
it("requires tilesetId to be specified", function () {
expect(function () {
return new Azure2DImageryProvider({
subscriptionKey: "a-subscription-key",
});
}).toThrowDeveloperError(
"options.tilesetId is required, actual value was undefined",
);
});
it("requestImage returns a promise for an image and loads it for cross-origin use", function () {
const provider = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
tilesetId: "a-tileset-id",
});
expect(provider.url).toEqual(
"https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=a-tileset-id&zoom={z}&x={x}&y={y}&subscription-key=test-subscriptionKey",
);
expect(provider.tileWidth).toEqual(256);
expect(provider.tileHeight).toEqual(256);
expect(provider.maximumLevel).toBe(22);
expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme);
expect(provider.rectangle).toEqual(new WebMercatorTilingScheme().rectangle);
spyOn(Resource._Implementations, "createImage").and.callFake(
function (request, crossOrigin, deferred) {
// Just return any old image.
Resource._DefaultImplementations.createImage(
new Request({ url: "Data/Images/Red16x16.png" }),
crossOrigin,
deferred,
);
},
);
return provider.requestImage(0, 0, 0).then(function (image) {
expect(Resource._Implementations.createImage).toHaveBeenCalled();
expect(image).toBeImageOrImageBitmap();
});
});
it("rectangle passed to constructor does not affect tile numbering", function () {
const rectangle = new Rectangle(0.1, 0.2, 0.3, 0.4);
const provider = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
tilesetId: "a-tileset-id",
rectangle: rectangle,
});
expect(provider.tileWidth).toEqual(256);
expect(provider.tileHeight).toEqual(256);
expect(provider.maximumLevel).toBe(22);
expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme);
expect(provider.rectangle).toEqualEpsilon(rectangle, CesiumMath.EPSILON14);
expect(provider.tileDiscardPolicy).toBeUndefined();
spyOn(Resource._Implementations, "createImage").and.callFake(
function (request, crossOrigin, deferred) {
expect(request.url).toContain("zoom=0&x=0&y=0");
// Just return any old image.
Resource._DefaultImplementations.createImage(
new Request({ url: "Data/Images/Red16x16.png" }),
crossOrigin,
deferred,
);
},
);
return provider.requestImage(0, 0, 0).then(function (image) {
expect(Resource._Implementations.createImage).toHaveBeenCalled();
expect(image).toBeImageOrImageBitmap();
});
});
it("uses maximumLevel passed to constructor", function () {
const provider = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
tilesetId: "a-tileset-id",
maximumLevel: 5,
});
expect(provider.maximumLevel).toEqual(5);
});
it("uses minimumLevel passed to constructor", function () {
const provider = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
tilesetId: "a-tileset-id",
minimumLevel: 1,
});
expect(provider.minimumLevel).toEqual(1);
});
it("turns the supplied credit into a logo", function () {
const creditText = "Thanks to our awesome made up source of this imagery!";
const providerWithCredit = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
tilesetId: "a-tileset-id",
credit: creditText,
});
expect(providerWithCredit.credit.html).toEqual(creditText);
});
it("raises error event when image cannot be loaded", function () {
const provider = new Azure2DImageryProvider({
subscriptionKey: "test-subscriptionKey",
tilesetId: "a-tileset-id",
});
const layer = new ImageryLayer(provider);
let tries = 0;
provider.errorEvent.addEventListener(function (error) {
expect(error.timesRetried).toEqual(tries);
++tries;
if (tries < 3) {
error.retry = true;
}
setTimeout(function () {
RequestScheduler.update();
}, 1);
});
Resource._Implementations.createImage = function (
request,
crossOrigin,
deferred,
) {
if (tries === 2) {
// Succeed after 2 tries
Resource._DefaultImplementations.createImage(
new Request({ url: "Data/Images/Red16x16.png" }),
crossOrigin,
deferred,
);
} else {
// fail
setTimeout(function () {
deferred.reject();
}, 1);
}
};
const imagery = new Imagery(layer, 0, 0, 0);
imagery.addReference();
layer._requestImagery(imagery);
RequestScheduler.update();
return pollToPromise(function () {
return imagery.state === ImageryState.RECEIVED;
}).then(function () {
expect(imagery.image).toBeImageOrImageBitmap();
expect(tries).toEqual(2);
imagery.releaseReference();
});
});
});

View File

@ -11,6 +11,7 @@ import {
import Cesium3DTilesTester from "../../../../Specs/Cesium3DTilesTester.js";
import createScene from "../../../../Specs/createScene.js";
import pollToPromise from "../../../../Specs/pollToPromise.js";
describe(
"Scene/GaussianSplat3DTileContent",
@ -96,6 +97,7 @@ describe(
},
);
});
it("Create and destroy GaussianSplat3DTileContent", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
@ -116,6 +118,69 @@ describe(
expect(tile.isDestroyed()).toBe(true);
expect(tile.content).toBeUndefined();
});
it("Load multiple instances of Gaussian splat tileset and validate transformed attributes", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
tilesetUrl,
options,
);
scene.camera.lookAt(
tileset.boundingSphere.center,
new HeadingPitchRange(0.0, -1.57, tileset.boundingSphere.radius),
);
const tileset2 = await Cesium3DTilesTester.loadTileset(
scene,
tilesetUrl,
options,
);
const tile = await Cesium3DTilesTester.waitForTileContentReady(
scene,
tileset.root,
);
scene.camera.lookAt(
tileset2.boundingSphere.center,
new HeadingPitchRange(0.0, -1.57, tileset2.boundingSphere.radius),
);
const tile2 = await Cesium3DTilesTester.waitForTileContentReady(
scene,
tileset2.root,
);
const content = tile.content;
const content2 = tile2.content;
expect(content).toBeDefined();
expect(content instanceof GaussianSplat3DTileContent).toBe(true);
expect(content2).toBeDefined();
expect(content2 instanceof GaussianSplat3DTileContent).toBe(true);
await pollToPromise(function () {
scene.renderForSpecs();
return (
tile.content._transformed === true &&
tile2.content._transformed === true
);
});
const positions1 = tile.content._positions;
const positions2 = tile2.content._positions;
expect(positions1.every((p, i) => p === positions2[i])).toBe(true);
const rotations1 = tile.content._rotations;
const rotations2 = tile2.content._rotations;
expect(rotations1.every((r, i) => r === rotations2[i])).toBe(true);
const scales1 = tile.content._scales;
const scales2 = tile2.content._scales;
expect(scales1.every((s, i) => s === scales2[i])).toBe(true);
});
},
"WebGL",
);

View File

@ -0,0 +1,226 @@
import {
Math as CesiumMath,
Rectangle,
Request,
RequestScheduler,
Resource,
WebMercatorTilingScheme,
Imagery,
ImageryLayer,
ImageryProvider,
ImageryState,
Google2DImageryProvider,
} from "../../index.js";
import pollToPromise from "../../../../Specs/pollToPromise.js";
describe("Scene/Google2DImageryProvider", function () {
beforeEach(function () {
RequestScheduler.clearForSpecs();
spyOn(
Google2DImageryProvider.prototype,
"getViewportCredits",
).and.returnValue(Promise.resolve(""));
});
afterEach(function () {
Resource._Implementations.createImage =
Resource._DefaultImplementations.createImage;
});
it("conforms to ImageryProvider interface", function () {
expect(Google2DImageryProvider).toConformToInterface(ImageryProvider);
});
it("requires the session token to be specified", function () {
expect(function () {
return new Google2DImageryProvider({});
}).toThrowDeveloperError();
});
it("requires the tileWidth to be specified", function () {
expect(function () {
return new Google2DImageryProvider({
session: "a-session-token",
});
}).toThrowDeveloperError();
});
it("requires the key to be specified", function () {
expect(function () {
return new Google2DImageryProvider({
session: "a-session-token",
tileHeight: 256,
tileWidth: 256,
});
}).toThrowDeveloperError();
});
it("fromIonAssetId throws if assetId is not provided", async function () {
await expectAsync(
Google2DImageryProvider.fromIonAssetId(),
).toBeRejectedWithDeveloperError(
"options.assetId is required, actual value was undefined",
);
});
it("requestImage returns a promise for an image and loads it for cross-origin use", function () {
const provider = new Google2DImageryProvider({
session: "test-session-token",
key: "test-key",
tileWidth: 256,
tileHeight: 256,
});
expect(provider.url).toEqual(
"https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session=test-session-token&key=test-key",
);
expect(provider.tileWidth).toEqual(256);
expect(provider.tileHeight).toEqual(256);
expect(provider.maximumLevel).toBe(22);
expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme);
expect(provider.rectangle).toEqual(new WebMercatorTilingScheme().rectangle);
spyOn(Resource._Implementations, "createImage").and.callFake(
function (request, crossOrigin, deferred) {
// Just return any old image.
Resource._DefaultImplementations.createImage(
new Request({ url: "Data/Images/Red16x16.png" }),
crossOrigin,
deferred,
);
},
);
return provider.requestImage(0, 0, 0).then(function (image) {
expect(Resource._Implementations.createImage).toHaveBeenCalled();
expect(image).toBeImageOrImageBitmap();
});
});
it("rectangle passed to constructor does not affect tile numbering", function () {
const rectangle = new Rectangle(0.1, 0.2, 0.3, 0.4);
const provider = new Google2DImageryProvider({
session: "test-session-token",
key: "test-key",
tileWidth: 256,
tileHeight: 256,
rectangle: rectangle,
});
expect(provider.tileWidth).toEqual(256);
expect(provider.tileHeight).toEqual(256);
expect(provider.maximumLevel).toBe(22);
expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme);
expect(provider.rectangle).toEqualEpsilon(rectangle, CesiumMath.EPSILON14);
expect(provider.tileDiscardPolicy).toBeUndefined();
spyOn(Resource._Implementations, "createImage").and.callFake(
function (request, crossOrigin, deferred) {
expect(request.url).toContain("/0/0/0");
// Just return any old image.
Resource._DefaultImplementations.createImage(
new Request({ url: "Data/Images/Red16x16.png" }),
crossOrigin,
deferred,
);
},
);
return provider.requestImage(0, 0, 0).then(function (image) {
expect(Resource._Implementations.createImage).toHaveBeenCalled();
expect(image).toBeImageOrImageBitmap();
});
});
it("uses maximumLevel passed to constructor", function () {
const provider = new Google2DImageryProvider({
session: "test-session-token",
key: "test-key",
tileWidth: 256,
tileHeight: 256,
maximumLevel: 5,
});
expect(provider.maximumLevel).toEqual(5);
});
it("uses minimumLevel passed to constructor", function () {
const provider = new Google2DImageryProvider({
session: "test-session-token",
key: "test-key",
tileWidth: 256,
tileHeight: 256,
minimumLevel: 1,
});
expect(provider.minimumLevel).toEqual(1);
});
it("turns the supplied credit into a logo", function () {
const creditText = "Thanks to our awesome made up source of this imagery!";
const providerWithCredit = new Google2DImageryProvider({
session: "test-session-token",
key: "test-key",
tileWidth: 256,
tileHeight: 256,
credit: creditText,
});
expect(providerWithCredit.credit.html).toEqual(creditText);
});
it("raises error event when image cannot be loaded", function () {
const provider = new Google2DImageryProvider({
session: "test-session-token",
key: "test-key",
tileWidth: 256,
tileHeight: 256,
});
const layer = new ImageryLayer(provider);
let tries = 0;
provider.errorEvent.addEventListener(function (error) {
expect(error.timesRetried).toEqual(tries);
++tries;
if (tries < 3) {
error.retry = true;
}
setTimeout(function () {
RequestScheduler.update();
}, 1);
});
Resource._Implementations.createImage = function (
request,
crossOrigin,
deferred,
) {
if (tries === 2) {
// Succeed after 2 tries
Resource._DefaultImplementations.createImage(
new Request({ url: "Data/Images/Red16x16.png" }),
crossOrigin,
deferred,
);
} else {
// fail
setTimeout(function () {
deferred.reject();
}, 1);
}
};
const imagery = new Imagery(layer, 0, 0, 0);
imagery.addReference();
layer._requestImagery(imagery);
RequestScheduler.update();
return pollToPromise(function () {
return imagery.state === ImageryState.RECEIVED;
}).then(function () {
expect(imagery.image).toBeImageOrImageBitmap();
expect(tries).toEqual(2);
imagery.releaseReference();
});
});
});

View File

@ -2446,7 +2446,6 @@ describe("Scene/LabelCollection", function () {
});
expect(l._clampedPosition).toBeDefined();
expect(l._glyphs[0].billboard._clampedPosition).toBeDefined();
l.heightReference = HeightReference.NONE;
expect(l._clampedPosition).toBeUndefined();
@ -2458,6 +2457,7 @@ describe("Scene/LabelCollection", function () {
heightReference: HeightReference.CLAMP_TO_GROUND,
text: "t",
position: Cartesian3.fromDegrees(-72.0, 40.0),
showBackground: true,
});
await pollToPromise(() => {
@ -2465,7 +2465,7 @@ describe("Scene/LabelCollection", function () {
return labelsWithHeight.ready;
});
const billboard = l._glyphs[0].billboard;
const billboard = l._backgroundBillboard;
expect(billboard._removeCallbackFunc).toBeDefined();
const spy = spyOn(billboard, "_removeCallbackFunc");
labelsWithHeight.remove(l);

View File

@ -90,17 +90,17 @@ describe("Scene/VoxelEllipsoidShape", function () {
).toEqualEpsilon(expectedOrientedBoundingBox.center, CesiumMath.EPSILON12);
const expectedShapeTransform = Matrix4.fromRowMajorArray([
(scale.x + maxHeight) * Math.cos(angle),
-(scale.x + maxHeight) * Math.sin(angle),
Math.cos(angle),
-Math.sin(angle),
0.0,
expectedOrientedBoundingBox.center.x,
(scale.y + maxHeight) * Math.sin(angle),
(scale.y + maxHeight) * Math.cos(angle),
Math.sin(angle),
Math.cos(angle),
0.0,
expectedOrientedBoundingBox.center.y,
0.0,
0.0,
scale.z + maxHeight,
1.0,
expectedOrientedBoundingBox.center.z,
0.0,
0.0,

View File

@ -52,7 +52,7 @@ describe("Scene/buildVoxelDrawCommands", function () {
const { shaderProgram } = primitive._drawCommand;
const fragmentShaderText = shaderProgram._fragmentShaderText;
const clippingFunctionSignature =
"vec4 getClippingPlane(highp sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)";
"vec4 getClippingPlane(highp sampler2D packedPlanes, int planeNumber)";
expect(fragmentShaderText.includes(clippingFunctionSignature)).toBe(true);
});

View File

@ -1,6 +1,6 @@
{
"name": "@cesium/engine",
"version": "20.0.1",
"version": "21.0.0",
"description": "CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.",
"keywords": [
"3D",

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,36 @@
import * as Cesium from "cesium";
Cesium.Ion.defaultServer = "https://api.ion-staging.cesium.com";
Cesium.Ion.defaultAccessToken = "";
const assetId = 1683;
const azure = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(assetId),
);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
viewer.imageryLayers.add(azure);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
});

View File

@ -0,0 +1,8 @@
legacyId: Azure 2D Tiles.html
title: Azure 2D Tiles
description: Global imagery data from Azure Maps.
development: true
labels:
- Imagery
- Development
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,54 @@
import * as Cesium from "cesium";
const assetId = 3830184;
const base = Cesium.ImageryLayer.fromProviderAsync(
Cesium.Google2DImageryProvider.fromIonAssetId({
assetId,
mapType: "satellite",
}),
);
const overlay = Cesium.ImageryLayer.fromProviderAsync(
Cesium.Google2DImageryProvider.fromIonAssetId({
assetId,
overlayLayerType: "layerRoadmap",
styles: [
{
stylers: [{ hue: "#00ffe6" }, { saturation: -20 }],
},
{
featureType: "road",
elementType: "geometry",
stylers: [{ lightness: 100 }, { visibility: "simplified" }],
},
],
}),
);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
viewer.imageryLayers.add(base);
viewer.imageryLayers.add(overlay);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
});

View File

@ -0,0 +1,7 @@
legacyId: Google 2D Tiles with Roadmap Styles.html
title: Google 2D Tiles with Custom Styles
description: Imagery tiles from Google Maps with additional parameters to create overlays and custom styles.
labels:
- Imagery
- Showcases
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,33 @@
import * as Cesium from "cesium";
const assetId = 3830184;
const google = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(assetId),
);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
viewer.imageryLayers.add(google);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
});

View File

@ -0,0 +1,7 @@
legacyId: Google 2D Tiles.html
title: Google 2D Tiles
description: Global imagery data from Google Maps.
labels:
- Imagery
- Showcases
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,63 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
timeline: false,
sceneModePicker: false,
navigationHelpButton: false,
homeButton: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1),
});
viewer.geocoder.viewModel.keepExpanded = true;
const menuOptions = [];
const dropdownOptions = [
{ label: "Google Maps 2D Contour", assetId: 3830186 },
{ label: "Google Maps 2D Labels Only", assetId: 3830185 },
{ label: "Google Maps 2D Roadmap", assetId: 3830184 },
{ label: "Google Maps 2D Satellite", assetId: 3830182 },
{ label: "Google Maps 2D Satellite with Labels", assetId: 3830183 },
{ label: "Bing Maps Aerial", assetId: 2 },
{ label: "Bing Maps Aerial with Labels", assetId: 3 },
{ label: "Bing Maps Road", assetId: 4 },
{ label: "Bing Maps Labels Only", assetId: 2411391 },
{ label: "Sentinel-2", assetId: 3954 },
];
function showLayer(assetId) {
viewer.imageryLayers.removeAll(true);
const layer = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(assetId),
);
viewer.imageryLayers.add(layer);
}
dropdownOptions.forEach((opt) => {
const option = {
text: opt.label,
onselect: function () {
showLayer(opt.assetId);
},
};
menuOptions.push(option);
});
Sandcastle.addToolbarMenu(menuOptions);
showLayer(3830186);
viewer.scene.camera.flyTo({
duration: 0,
destination: new Cesium.Rectangle.fromDegrees(
//Philly
-75.280266,
39.867004,
-74.955763,
40.137992,
),
});

View File

@ -0,0 +1,6 @@
legacyId: Imagery Assets available from ion.html
title: Imagery Assets available from ion
description: Global imagery assets available from Cesium ion.
labels:
- Showcases
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -35,16 +35,16 @@ function ProceduralMultiTileVoxelProvider(shape) {
this.names = ["color"];
this.types = [Cesium.MetadataType.VEC4];
this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
this._levelCount = 3;
this.availableLevels = 3;
this.globalTransform = globalTransform;
}
ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) {
const { tileLevel, tileX, tileY, tileZ } = options;
if (tileLevel >= this._levelCount) {
if (tileLevel >= this.availableLevels) {
return Promise.reject(
`No tiles available beyond level ${this._levelCount}`,
`No tiles available beyond level ${this.availableLevels - 1}`,
);
}
@ -129,6 +129,7 @@ function createPrimitive(provider) {
customShader: customShader,
});
voxelPrimitive.nearestSampling = true;
voxelPrimitive.stepSize = 0.7;
viewer.scene.primitives.add(voxelPrimitive);
camera.flyToBoundingSphere(voxelPrimitive.boundingSphere, {

View File

@ -92,14 +92,14 @@ function ProceduralMultiTileVoxelProvider(shape) {
this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
this.globalTransform = globalTransform;
this._levelCount = 2;
this._allVoxelData = new Array(this._levelCount);
this.availableLevels = 2;
this._allVoxelData = new Array(this.availableLevels);
const allVoxelData = this._allVoxelData;
const channelCount = Cesium.MetadataType.getComponentCount(this.types[0]);
const { dimensions } = this;
for (let level = 0; level < this._levelCount; level++) {
for (let level = 0; level < this.availableLevels; level++) {
const dimAtLevel = Math.pow(2, level);
const voxelCountX = dimensions.x * dimAtLevel;
const voxelCountY = dimensions.y * dimAtLevel;
@ -127,9 +127,9 @@ function ProceduralMultiTileVoxelProvider(shape) {
ProceduralMultiTileVoxelProvider.prototype.requestData = function (options) {
const { tileLevel, tileX, tileY, tileZ } = options;
if (tileLevel >= this._levelCount) {
if (tileLevel >= this.availableLevels) {
return Promise.reject(
`No tiles available beyond level ${this._levelCount - 1}`,
`No tiles available beyond level ${this.availableLevels - 1}`,
);
}

View File

@ -1,7 +1,7 @@
{
"name": "@cesium/sandcastle",
"private": true,
"version": "0.0.2",
"version": "0.0.3",
"type": "module",
"files": [
"scripts/buildGallery.js"

View File

@ -1,3 +1,5 @@
import process from "process";
const config = {
root: ".",
sourceUrl: "https://github.com/CesiumGS/cesium/blob/main/packages/sandcastle",
@ -19,7 +21,7 @@ const config = {
labels: [],
development: false,
},
includeDevelopment: true,
includeDevelopment: !process.env.PROD,
},
};

View File

@ -483,7 +483,10 @@ function App() {
synchronizeColorScheme
>
<div className="banner">
<Anchor href="https://sandcastle.cesium.com" tone="accent">
<Anchor
href="https://cesium.com/downloads/cesiumjs/releases/1.134/Apps/Sandcastle/index.html"
tone="accent"
>
Looking for the old Sandcastle? It's still here (for a little while)
</Anchor>
</div>

View File

@ -126,11 +126,9 @@
padding: var(--stratakit-space-x2);
gap: var(--stratakit-space-x2);
overflow-y: auto;
&:focus-visible {
/* small margin to allow focus highlight to be visible all the way around */
margin: 0 3px 4px;
border-radius: var(--stratakit-ext-radius-xs);
}
/* small margin to allow focus highlight to be visible all the way around */
margin: 0 3px 4px;
border-radius: var(--stratakit-ext-radius-xs);
}
.empty-list div {

View File

@ -194,6 +194,21 @@ export function useGalleryItemStore() {
return isGalleryLoaded ? () => loadFromUrl(items, legacyIds) : null;
}, [items, legacyIds]);
const [isFirstSearch, setFirstSearch] = useState(true);
const setSearchTermWrapper = useCallback(
(newSearchTerm: string | null) => {
// the default label filter for Showcases can be confusing when it doesn't
// search everything after page load. Remove the filter on the first search only
// to ensure we search everything
if (isFirstSearch) {
setSearchFilter(null);
setFirstSearch(false);
}
setSearchTerm(newSearchTerm);
},
[setSearchTerm, isFirstSearch, setSearchFilter],
);
return {
items,
galleryLoaded,
@ -203,7 +218,7 @@ export function useGalleryItemStore() {
searchFilter,
searchTerm,
isSearchPending,
setSearchTerm,
setSearchTerm: setSearchTermWrapper,
setSearchFilter,
searchResults: memoizedSearchResults,

View File

@ -43,6 +43,20 @@ export function loadFromUrl(
) {
const searchParams = new URLSearchParams(window.location.search);
const codeParam = searchParams.get("code");
if (codeParam) {
// This is a legacy support type url that was used by ion.
// Ideally we use the #c= param as that results in shorter urls
// The code query parameter is a Base64 encoded JSON string with `code` and `html` properties.
const json = JSON.parse(window.atob(codeParam.replaceAll(" ", "+")));
return {
title: "New Sandcastle",
code: json.code,
html: json.html,
};
}
if (window.location.hash.indexOf("#c=") === 0) {
const base64String = window.location.hash.substr(3);
const { code, html } = decodeBase64Data(base64String);

View File

@ -83,10 +83,28 @@ function SandcastleEditor({
const {
settings: { fontFamily, fontSize, fontLigatures },
} = useContext(SettingsContext);
const documentRef = useRef(document);
useEffect(() => {
internalEditorRef.current?.updateOptions({
fontFamily: availableFonts[fontFamily]?.cssValue ?? "Droid Sans Mono",
});
const cssName = availableFonts[fontFamily]?.cssValue ?? "Droid Sans Mono";
const fontFace = [...documentRef.current.fonts].find(
(font) => font.family === cssName && font.weight === "400",
);
if (fontFace?.status !== "loaded") {
// Monaco needs to check the size of the font for things like cursor position
// and variable highlighting. If it does this check before the font has loaded
// it will get the wrong size and may be horribly offset especially with ligatures
// https://github.com/microsoft/monaco-editor/issues/392
documentRef.current.fonts.load(`1rem ${cssName}`).then(() => {
internalEditorRef.current?.updateOptions({
fontFamily: cssName,
});
monaco.editor.remeasureFonts();
});
} else {
internalEditorRef.current?.updateOptions({
fontFamily: cssName,
});
}
}, [fontFamily]);
useEffect(() => {
internalEditorRef.current?.updateOptions({

View File

@ -302,6 +302,79 @@ of the world.\nhttp://www.openstreetmap.org",
}),
);
providerViewModels.push(
new ProviderViewModel({
name: "Google Maps Satellite",
iconUrl: buildModuleUrl(
"Widgets/Images/ImageryProviders/googleSatellite.png",
),
tooltip: "Imagery from Google Maps",
category: "Cesium ion",
creationFunction: function () {
return IonImageryProvider.fromAssetId(3830182);
},
}),
);
providerViewModels.push(
new ProviderViewModel({
name: "Google Maps Satellite with Labels",
iconUrl: buildModuleUrl(
"Widgets/Images/ImageryProviders/googleSatelliteLabels.png",
),
tooltip: "Imagery with place labels from Google Maps",
category: "Cesium ion",
creationFunction: function () {
return IonImageryProvider.fromAssetId(3830183);
},
}),
);
providerViewModels.push(
new ProviderViewModel({
name: "Google Maps Roadmap",
iconUrl: buildModuleUrl(
"Widgets/Images/ImageryProviders/googleRoadmap.png",
),
tooltip:
"Labeled roads and other features on a base landscape from Google Maps",
category: "Cesium ion",
creationFunction: function () {
return IonImageryProvider.fromAssetId(3830184);
},
}),
);
providerViewModels.push(
new ProviderViewModel({
name: "Google Maps Labels Only",
iconUrl: buildModuleUrl(
"Widgets/Images/ImageryProviders/googleLabels.png",
),
tooltip:
"Place labels from Google Maps to combine with other imagery such as Sentinel-2",
category: "Cesium ion",
creationFunction: function () {
return IonImageryProvider.fromAssetId(3830185);
},
}),
);
providerViewModels.push(
new ProviderViewModel({
name: "Google Maps Contour",
iconUrl: buildModuleUrl(
"Widgets/Images/ImageryProviders/googleContour.png",
),
tooltip:
"Hillshade mapping, contour lines, natural features (roadmap features hidden) from Google Maps",
category: "Cesium ion",
creationFunction: function () {
return IonImageryProvider.fromAssetId(3830186);
},
}),
);
return providerViewModels;
}
export default createDefaultImageryProviderViewModels;

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,6 +1,6 @@
{
"name": "@cesium/widgets",
"version": "13.1.1",
"version": "13.2.0",
"description": "A widgets library for use with CesiumJS. CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.",
"keywords": [
"3D",
@ -28,7 +28,7 @@
"node": ">=20.19.0"
},
"dependencies": {
"@cesium/engine": "^20.0.1",
"@cesium/engine": "^21.0.0",
"nosleep.js": "^0.12.0"
},
"type": "module",