feat: 实现供水运营专题大屏BI可视化

- 新增OperationDashboard.vue全屏大屏组件
- 添加6个核心KPI指标卡片: 进水总量/出水总量/产销差率/营收额/平均水质/报警次数
- 实现6个ECharts图表: 供水趋势/水质分布/报警统计/管网空间/设备状态/营收分析
- 创建静态HTML版本operation-dashboard.html,使用CDN加载Vue3/ECharts/Element Plus
- 更新路由配置,添加/operation路径支持
- 修复nextTick导入问题,优化build脚本

🚧 开发者: bot_dev1
📝 任务: #38 [BI] 运营仪表盘 + 供水专题大屏
This commit is contained in:
2026-06-15 09:06:11 +08:00
parent 37bcc78eee
commit 85df71fc28
17426 changed files with 4091508 additions and 47 deletions
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
/**
* The alpha rendering mode of the material.
*
* @enum {string}
* @private
*/
const AlphaMode = {
/**
* The alpha value is ignored and the rendered output is fully opaque.
*
* @type {string}
* @constant
*/
OPAQUE: "OPAQUE",
/**
* The rendered output is either fully opaque or fully transparent depending on the alpha value and the specified alpha cutoff value.
*
* @type {string}
* @constant
*/
MASK: "MASK",
/**
* The rendered output is composited onto the destination with alpha blending.
*
* @type {string}
* @constant
*/
BLEND: "BLEND",
};
Object.freeze(AlphaMode);
export default AlphaMode;
+204
View File
@@ -0,0 +1,204 @@
import clone from "../Core/clone.js";
import combine from "../Core/combine.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import BlendingState from "./BlendingState.js";
import CullFace from "./CullFace.js";
/**
* An appearance defines the full GLSL vertex and fragment shaders and the
* render state used to draw a {@link Primitive}. All appearances implement
* this base <code>Appearance</code> interface.
*
* @alias Appearance
* @constructor
*
* @param {object} [options] Object with the following properties:
* @param {boolean} [options.translucent=true] When <code>true</code>, the geometry is expected to appear translucent so {@link Appearance#renderState} has alpha blending enabled.
* @param {boolean} [options.closed=false] When <code>true</code>, the geometry is expected to be closed so {@link Appearance#renderState} has backface culling enabled.
* @param {Material} [options.material=Material.ColorType] The material used to determine the fragment color.
* @param {string} [options.vertexShaderSource] Optional GLSL vertex shader source to override the default vertex shader.
* @param {string} [options.fragmentShaderSource] Optional GLSL fragment shader source to override the default fragment shader.
* @param {object} [options.renderState] Optional render state to override the default render state.
*
* @see MaterialAppearance
* @see EllipsoidSurfaceAppearance
* @see PerInstanceColorAppearance
* @see DebugAppearance
* @see PolylineColorAppearance
* @see PolylineMaterialAppearance
*
* @demo {@link https://sandcastle.cesium.com/index.html?id=geometry-and-appearances|Geometry and Appearances Demo}
*/
function Appearance(options) {
options = options ?? Frozen.EMPTY_OBJECT;
/**
* The material used to determine the fragment color. Unlike other {@link Appearance}
* properties, this is not read-only, so an appearance's material can change on the fly.
*
* @type Material
*
* @see {@link https://github.com/CesiumGS/cesium/wiki/Fabric|Fabric}
*/
this.material = options.material;
/**
* When <code>true</code>, the geometry is expected to appear translucent.
*
* @type {boolean}
*
* @default true
*/
this.translucent = options.translucent ?? true;
this._vertexShaderSource = options.vertexShaderSource;
this._fragmentShaderSource = options.fragmentShaderSource;
this._renderState = options.renderState;
this._closed = options.closed ?? false;
}
Object.defineProperties(Appearance.prototype, {
/**
* The GLSL source code for the vertex shader.
*
* @memberof Appearance.prototype
*
* @type {string}
* @readonly
*/
vertexShaderSource: {
get: function () {
return this._vertexShaderSource;
},
},
/**
* The GLSL source code for the fragment shader. The full fragment shader
* source is built procedurally taking into account the {@link Appearance#material}.
* Use {@link Appearance#getFragmentShaderSource} to get the full source.
*
* @memberof Appearance.prototype
*
* @type {string}
* @readonly
*/
fragmentShaderSource: {
get: function () {
return this._fragmentShaderSource;
},
},
/**
* The WebGL fixed-function state to use when rendering the geometry.
*
* @memberof Appearance.prototype
*
* @type {object}
* @readonly
*/
renderState: {
get: function () {
return this._renderState;
},
},
/**
* When <code>true</code>, the geometry is expected to be closed.
*
* @memberof Appearance.prototype
*
* @type {boolean}
* @readonly
*
* @default false
*/
closed: {
get: function () {
return this._closed;
},
},
});
/**
* Procedurally creates the full GLSL fragment shader source for this appearance
* taking into account {@link Appearance#fragmentShaderSource} and {@link Appearance#material}.
*
* @returns {string} The full GLSL fragment shader source.
*/
Appearance.prototype.getFragmentShaderSource = function () {
const parts = [];
if (this.flat) {
parts.push("#define FLAT");
}
if (this.faceForward) {
parts.push("#define FACE_FORWARD");
}
if (defined(this.material)) {
parts.push(this.material.shaderSource);
}
parts.push(this.fragmentShaderSource);
return parts.join("\n");
};
/**
* Determines if the geometry is translucent based on {@link Appearance#translucent} and {@link Material#isTranslucent}.
*
* @returns {boolean} <code>true</code> if the appearance is translucent.
*/
Appearance.prototype.isTranslucent = function () {
return (
(defined(this.material) && this.material.isTranslucent()) ||
(!defined(this.material) && this.translucent)
);
};
/**
* Creates a render state. This is not the final render state instance; instead,
* it can contain a subset of render state properties identical to the render state
* created in the context.
*
* @returns {object} The render state.
*/
Appearance.prototype.getRenderState = function () {
const translucent = this.isTranslucent();
const rs = clone(this.renderState, false);
if (translucent) {
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
} else {
rs.depthMask = true;
}
return rs;
};
/**
* @private
*/
Appearance.getDefaultRenderState = function (translucent, closed, existing) {
let rs = {
depthTest: {
enabled: true,
},
};
if (translucent) {
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
}
if (closed) {
rs.cull = {
enabled: true,
face: CullFace.BACK,
};
}
if (defined(existing)) {
rs = combine(existing, rs, true);
}
return rs;
};
export default Appearance;
+17
View File
@@ -0,0 +1,17 @@
// @ts-check
/**
* ArcGisBaseMapType enumerates the ArcGIS image tile layers that are supported by default.
*
* @enum {number}
* @see ArcGisMapServerImageryProvider
*/
const ArcGisBaseMapType = {
SATELLITE: 1,
OCEANS: 2,
HILLSHADE: 3,
};
Object.freeze(ArcGisBaseMapType);
export default ArcGisBaseMapType;
@@ -0,0 +1,875 @@
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 Credit from "../Core/Credit.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Event from "../Core/Event.js";
import GeographicProjection from "../Core/GeographicProjection.js";
import GeographicTilingScheme from "../Core/GeographicTilingScheme.js";
import CesiumMath from "../Core/Math.js";
import Rectangle from "../Core/Rectangle.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import WebMercatorProjection from "../Core/WebMercatorProjection.js";
import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
import ArcGisMapService from "./ArcGisMapService.js";
import DiscardMissingTileImagePolicy from "./DiscardMissingTileImagePolicy.js";
import ImageryLayerFeatureInfo from "./ImageryLayerFeatureInfo.js";
import ImageryProvider from "./ImageryProvider.js";
import ArcGisBaseMapType from "./ArcGisBaseMapType.js";
import DeveloperError from "../Core/DeveloperError.js";
/**
* @typedef {object} ArcGisMapServerImageryProvider.ConstructorOptions
*
* Initialization options for the ArcGisMapServerImageryProvider constructor
*
* @property {TileDiscardPolicy} [tileDiscardPolicy] The policy that determines if a tile
* is invalid and should be discarded. If this value is not specified, a default
* {@link DiscardMissingTileImagePolicy} is used for tiled map servers, and a
* {@link NeverTileDiscardPolicy} is used for non-tiled map servers. In the former case,
* we request tile 0,0 at the maximum tile level and check pixels (0,0), (200,20), (20,200),
* (80,110), and (160, 130). If all of these pixels are transparent, the discard check is
* disabled and no tiles are discarded. If any of them have a non-transparent color, any
* tile that has the same values in these pixel locations is discarded. The end result of
* these defaults should be correct tile discarding for a standard ArcGIS Server. To ensure
* that no tiles are discarded, construct and pass a {@link NeverTileDiscardPolicy} for this
* parameter.
* @property {boolean} [usePreCachedTilesIfAvailable=true] If true, the server's pre-cached
* tiles are used if they are available. Exporting Tiles is only supported with deprecated APIs.
* @property {string} [layers] A comma-separated list of the layers to show, or undefined if all layers should be shown.
* @property {boolean} [enablePickFeatures=true] If true, {@link ArcGisMapServerImageryProvider#pickFeatures} will invoke
* the Identify service on the MapServer and return the features included in the response. If false,
* {@link ArcGisMapServerImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable features)
* without communicating with the server. Set this property to false if you don't want this provider's features to
* be pickable. Can be overridden by setting the {@link ArcGisMapServerImageryProvider#enablePickFeatures} property on the object.
* @property {Rectangle} [rectangle=Rectangle.MAX_VALUE] The rectangle of the layer. This parameter is ignored when accessing
* a tiled layer.
* @property {TilingScheme} [tilingScheme=new GeographicTilingScheme()] The tiling scheme to use to divide the world into tiles.
* This parameter is ignored when accessing a tiled server.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If the tilingScheme is specified and used,
* this parameter is ignored and the tiling scheme's ellipsoid is used instead. If neither
* parameter is specified, the default ellipsoid is used.
* @property {Credit|string} [credit] A credit for the data source, which is displayed on the canvas. This parameter is ignored when accessing a tiled server.
* @property {number} [tileWidth=256] The width of each tile in pixels. This parameter is ignored when accessing a tiled server.
* @property {number} [tileHeight=256] The height of each tile in pixels. This parameter is ignored when accessing a tiled server.
* @property {number} [maximumLevel] The maximum tile level to request, or undefined if there is no maximum. This parameter is ignored when accessing
* a tiled server.
*
*
*/
/**
* Used to track creation details while fetching initial metadata
*
* @constructor
* @private
*
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} options An object describing initialization options
*/
function ImageryProviderBuilder(options) {
this.useTiles = options.usePreCachedTilesIfAvailable ?? true;
const ellipsoid = options.ellipsoid;
this.tilingScheme =
options.tilingScheme ??
new GeographicTilingScheme({ ellipsoid: ellipsoid });
this.rectangle = options.rectangle ?? this.tilingScheme.rectangle;
this.ellipsoid = ellipsoid;
let credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
this.credit = credit;
this.tileCredits = undefined;
this.tileDiscardPolicy = options.tileDiscardPolicy;
this.tileWidth = options.tileWidth ?? 256;
this.tileHeight = options.tileHeight ?? 256;
this.maximumLevel = options.maximumLevel;
}
/**
* Complete ArcGisMapServerImageryProvider creation based on builder values.
*
* @private
*
* @param {ArcGisMapServerImageryProvider} provider
*/
ImageryProviderBuilder.prototype.build = function (provider) {
provider._useTiles = this.useTiles;
provider._tilingScheme = this.tilingScheme;
provider._rectangle = this.rectangle;
provider._credit = this.credit;
provider._tileCredits = this.tileCredits;
provider._tileDiscardPolicy = this.tileDiscardPolicy;
provider._tileWidth = this.tileWidth;
provider._tileHeight = this.tileHeight;
provider._maximumLevel = this.maximumLevel;
// Install the default tile discard policy if none has been supplied.
if (this.useTiles && !defined(this.tileDiscardPolicy)) {
provider._tileDiscardPolicy = new DiscardMissingTileImagePolicy({
missingImageUrl: buildImageResource(provider, 0, 0, this.maximumLevel)
.url,
pixelsToCheck: [
new Cartesian2(0, 0),
new Cartesian2(200, 20),
new Cartesian2(20, 200),
new Cartesian2(80, 110),
new Cartesian2(160, 130),
],
disableCheckIfAllPixelsAreTransparent: true,
});
}
};
function metadataSuccess(data, imageryProviderBuilder) {
const tileInfo = data.tileInfo;
if (!defined(tileInfo)) {
imageryProviderBuilder.useTiles = false;
} else {
imageryProviderBuilder.tileWidth = tileInfo.rows;
imageryProviderBuilder.tileHeight = tileInfo.cols;
if (
tileInfo.spatialReference.wkid === 102100 ||
tileInfo.spatialReference.wkid === 102113
) {
imageryProviderBuilder.tilingScheme = new WebMercatorTilingScheme({
ellipsoid: imageryProviderBuilder.ellipsoid,
});
} else if (data.tileInfo.spatialReference.wkid === 4326) {
imageryProviderBuilder.tilingScheme = new GeographicTilingScheme({
ellipsoid: imageryProviderBuilder.ellipsoid,
});
} else {
const message = `Tile spatial reference WKID ${data.tileInfo.spatialReference.wkid} is not supported.`;
throw new RuntimeError(message);
}
imageryProviderBuilder.maximumLevel = data.tileInfo.lods.length - 1;
if (defined(data.fullExtent)) {
if (
defined(data.fullExtent.spatialReference) &&
defined(data.fullExtent.spatialReference.wkid)
) {
if (
data.fullExtent.spatialReference.wkid === 102100 ||
data.fullExtent.spatialReference.wkid === 102113
) {
const projection = new WebMercatorProjection();
const extent = data.fullExtent;
const sw = projection.unproject(
new Cartesian3(
Math.max(
extent.xmin,
-imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
Math.max(
extent.ymin,
-imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
0.0,
),
);
const ne = projection.unproject(
new Cartesian3(
Math.min(
extent.xmax,
imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
Math.min(
extent.ymax,
imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
0.0,
),
);
imageryProviderBuilder.rectangle = new Rectangle(
sw.longitude,
sw.latitude,
ne.longitude,
ne.latitude,
);
} else if (data.fullExtent.spatialReference.wkid === 4326) {
imageryProviderBuilder.rectangle = Rectangle.fromDegrees(
data.fullExtent.xmin,
data.fullExtent.ymin,
data.fullExtent.xmax,
data.fullExtent.ymax,
);
} else {
const extentMessage = `fullExtent.spatialReference WKID ${data.fullExtent.spatialReference.wkid} is not supported.`;
throw new RuntimeError(extentMessage);
}
}
} else {
imageryProviderBuilder.rectangle =
imageryProviderBuilder.tilingScheme.rectangle;
}
imageryProviderBuilder.useTiles = true;
}
if (defined(data.copyrightText) && data.copyrightText.length > 0) {
if (defined(imageryProviderBuilder.credit)) {
imageryProviderBuilder.tileCredits = [new Credit(data.copyrightText)];
} else {
imageryProviderBuilder.credit = new Credit(data.copyrightText);
}
}
}
function metadataFailure(resource, error) {
let message = `An error occurred while accessing ${resource.url}`;
if (defined(error) && defined(error.message)) {
message += `: ${error.message}`;
}
throw new RuntimeError(message);
}
async function requestMetadata(resource, imageryProviderBuilder) {
const jsonResource = resource.getDerivedResource({
queryParameters: {
f: "json",
},
});
try {
const data = await jsonResource.fetchJson();
metadataSuccess(data, imageryProviderBuilder);
} catch (error) {
metadataFailure(resource, error);
}
}
/**
* <div class="notice">
* This object is normally not instantiated directly, use {@link ArcGisMapServerImageryProvider.fromBasemapType} or {@link ArcGisMapServerImageryProvider.fromUrl}.
* </div>
*
* Provides tiled imagery hosted by an ArcGIS MapServer. By default, the server's pre-cached tiles are
* used, if available.
*
* <br/>
*
* An {@link https://developers.arcgis.com/documentation/mapping-apis-and-services/security| ArcGIS Access Token } is required to authenticate requests to an ArcGIS Image Tile service.
* To access secure ArcGIS resources, it's required to create an ArcGIS developer
* account or an ArcGIS online account, then implement an authentication method to obtain an access token.
*
* @alias ArcGisMapServerImageryProvider
* @constructor
*
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} [options] Object describing initialization options
*
* @see ArcGisMapServerImageryProvider.fromBasemapType
* @see ArcGisMapServerImageryProvider.fromUrl
*
* @example
* // Set the default access token for accessing ArcGIS Image Tile service
* Cesium.ArcGisMapService.defaultAccessToken = "<ArcGIS Access Token>";
*
* // Add a base layer from a default ArcGIS basemap
* const viewer = new Cesium.Viewer("cesiumContainer", {
* baseLayer: Cesium.ImageryLayer.fromProviderAsync(
* Cesium.ArcGisMapServerImageryProvider.fromBasemapType(
* Cesium.ArcGisBaseMapType.SATELLITE
* )
* ),
* });
*
* @example
* // Create an imagery provider from the url directly
* const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
* "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer", {
* token: "<ArcGIS Access Token>"
* });
*
* @see {@link https://developers.arcgis.com/rest/|ArcGIS Server REST API}
* @see {@link https://developers.arcgis.com/documentation/mapping-apis-and-services/security| ArcGIS Access Token }
*/
function ArcGisMapServerImageryProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._defaultAlpha = undefined;
this._defaultNightAlpha = undefined;
this._defaultDayAlpha = undefined;
this._defaultBrightness = undefined;
this._defaultContrast = undefined;
this._defaultHue = undefined;
this._defaultSaturation = undefined;
this._defaultGamma = undefined;
this._defaultMinificationFilter = undefined;
this._defaultMagnificationFilter = undefined;
this._tileDiscardPolicy = options.tileDiscardPolicy;
this._tileWidth = options.tileWidth ?? 256;
this._tileHeight = options.tileHeight ?? 256;
this._maximumLevel = options.maximumLevel;
this._tilingScheme =
options.tilingScheme ??
new GeographicTilingScheme({ ellipsoid: options.ellipsoid });
this._useTiles = options.usePreCachedTilesIfAvailable ?? true;
this._rectangle = options.rectangle ?? this._tilingScheme.rectangle;
this._layers = options.layers;
this._credit = options.credit;
this._tileCredits = undefined;
let credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
/**
* Gets or sets a value indicating whether feature picking is enabled. If true, {@link ArcGisMapServerImageryProvider#pickFeatures} will
* invoke the "identify" operation on the ArcGIS server and return the features included in the response. If false,
* {@link ArcGisMapServerImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable features)
* without communicating with the server.
* @type {boolean}
* @default true
*/
this.enablePickFeatures = options.enablePickFeatures ?? true;
this._errorEvent = new Event();
}
/**
* Creates an {@link ImageryProvider} which provides tiled imagery from an ArcGIS base map.
* @param {ArcGisBaseMapType} style The style of the ArcGIS base map imagery. Valid options are {@link ArcGisBaseMapType.SATELLITE}, {@link ArcGisBaseMapType.OCEANS}, and {@link ArcGisBaseMapType.HILLSHADE}.
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} [options] Object describing initialization options.
* @returns {Promise<ArcGisMapServerImageryProvider>} A promise that resolves to the created ArcGisMapServerImageryProvider.
*
* @example
* // Set the default access token for accessing ArcGIS Image Tile service
* Cesium.ArcGisMapService.defaultAccessToken = "<ArcGIS Access Token>";
*
* // Add a base layer from a default ArcGIS basemap
* const provider = await Cesium.ArcGisMapServerImageryProvider.fromBasemapType(
* Cesium.ArcGisBaseMapType.SATELLITE);
*
* @example
* // Add a base layer from a default ArcGIS Basemap
* const viewer = new Cesium.Viewer("cesiumContainer", {
* baseLayer: Cesium.ImageryLayer.fromProviderAsync(
* Cesium.ArcGisMapServerImageryProvider.fromBasemapType(
* Cesium.ArcGisBaseMapType.HILLSHADE, {
* token: "<ArcGIS Access Token>"
* }
* )
* ),
* });
*/
ArcGisMapServerImageryProvider.fromBasemapType = async function (
style,
options,
) {
//>>includeStart('debug', pragmas.debug);
Check.defined("style", style);
//>>includeEnd('debug');
options = options ?? Frozen.EMPTY_OBJECT;
let accessToken;
let server;
let warningCredit;
switch (style) {
case ArcGisBaseMapType.SATELLITE:
{
accessToken = options.token ?? ArcGisMapService.defaultAccessToken;
server = Resource.createIfNeeded(
ArcGisMapService.defaultWorldImageryServer,
);
server.appendForwardSlash();
const defaultTokenCredit =
ArcGisMapService.getDefaultTokenCredit(accessToken);
if (defined(defaultTokenCredit)) {
warningCredit = Credit.clone(defaultTokenCredit);
}
}
break;
case ArcGisBaseMapType.OCEANS:
{
accessToken = options.token ?? ArcGisMapService.defaultAccessToken;
server = Resource.createIfNeeded(
ArcGisMapService.defaultWorldOceanServer,
);
server.appendForwardSlash();
const defaultTokenCredit =
ArcGisMapService.getDefaultTokenCredit(accessToken);
if (defined(defaultTokenCredit)) {
warningCredit = Credit.clone(defaultTokenCredit);
}
}
break;
case ArcGisBaseMapType.HILLSHADE:
{
accessToken = options.token ?? ArcGisMapService.defaultAccessToken;
server = Resource.createIfNeeded(
ArcGisMapService.defaultWorldHillshadeServer,
);
server.appendForwardSlash();
const defaultTokenCredit =
ArcGisMapService.getDefaultTokenCredit(accessToken);
if (defined(defaultTokenCredit)) {
warningCredit = Credit.clone(defaultTokenCredit);
}
}
break;
default:
//>>includeStart('debug', pragmas.debug);
throw new DeveloperError(`Unsupported basemap type: ${style}`);
//>>includeEnd('debug');
}
return ArcGisMapServerImageryProvider.fromUrl(server, {
...options,
token: accessToken,
credit: warningCredit,
usePreCachedTilesIfAvailable: true, // ArcGIS Base Map Service Layers only support Tiled views
});
};
function buildImageResource(imageryProvider, x, y, level, request) {
let resource;
if (imageryProvider._useTiles) {
resource = imageryProvider._resource.getDerivedResource({
url: `tile/${level}/${y}/${x}`,
request: request,
});
} else {
const nativeRectangle =
imageryProvider._tilingScheme.tileXYToNativeRectangle(x, y, level);
const bbox = `${nativeRectangle.west},${nativeRectangle.south},${nativeRectangle.east},${nativeRectangle.north}`;
const query = {
bbox: bbox,
size: `${imageryProvider._tileWidth},${imageryProvider._tileHeight}`,
format: "png32",
transparent: true,
f: "image",
};
if (
imageryProvider._tilingScheme.projection instanceof GeographicProjection
) {
query.bboxSR = 4326;
query.imageSR = 4326;
} else {
query.bboxSR = 3857;
query.imageSR = 3857;
}
if (imageryProvider.layers) {
query.layers = `show:${imageryProvider.layers}`;
}
resource = imageryProvider._resource.getDerivedResource({
url: "export",
request: request,
queryParameters: query,
});
}
return resource;
}
Object.defineProperties(ArcGisMapServerImageryProvider.prototype, {
/**
* Gets the URL of the ArcGIS MapServer.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._resource._url;
},
},
/**
* Gets the ArcGIS token used to authenticate with the ArcGis MapServer service.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {string}
* @readonly
*/
token: {
get: function () {
return this._resource.queryParameters.token;
},
},
/**
* Gets the proxy used by this provider.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._resource.proxy;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return 0;
},
},
/**
* Gets the tiling scheme used by this provider.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._tilingScheme;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by this instance.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._rectangle;
},
},
/**
* 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 ArcGisMapServerImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._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 ArcGisMapServerImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._credit;
},
},
/**
* Gets a value indicating whether this imagery provider is using pre-cached tiles from the
* ArcGIS MapServer.
* @memberof ArcGisMapServerImageryProvider.prototype
*
* @type {boolean}
* @readonly
* @default true
*/
usingPrecachedTiles: {
get: function () {
return this._useTiles;
},
},
/**
* 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 ArcGisMapServerImageryProvider.prototype
*
* @type {boolean}
* @readonly
* @default true
*/
hasAlphaChannel: {
get: function () {
return true;
},
},
/**
* Gets the comma-separated list of layer IDs to show.
* @memberof ArcGisMapServerImageryProvider.prototype
*
* @type {string}
*/
layers: {
get: function () {
return this._layers;
},
},
});
/**
* Creates an {@link ImageryProvider} which provides tiled imagery hosted by an ArcGIS MapServer. By default, the server's pre-cached tiles are
* used, if available.
*
* @param {Resource|string} url The URL of the ArcGIS MapServer service.
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} [options] Object describing initialization options.
* @returns {Promise<ArcGisMapServerImageryProvider>} A promise that resolves to the created ArcGisMapServerImageryProvider.
*
* @example
* const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
* "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"
* );
*
* @exception {RuntimeError} metadata spatial reference specifies an unknown WKID
* @exception {RuntimeError} metadata fullExtent.spatialReference specifies an unknown WKID
*/
ArcGisMapServerImageryProvider.fromUrl = async function (url, options) {
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
//>>includeEnd('debug');
options = options ?? Frozen.EMPTY_OBJECT;
const resource = Resource.createIfNeeded(url);
resource.appendForwardSlash();
if (defined(options.token)) {
resource.setQueryParameters({
token: options.token,
});
}
const provider = new ArcGisMapServerImageryProvider(options);
provider._resource = resource;
const imageryProviderBuilder = new ImageryProviderBuilder(options);
const useTiles = options.usePreCachedTilesIfAvailable ?? true;
if (useTiles) {
await requestMetadata(resource, imageryProviderBuilder);
}
imageryProviderBuilder.build(provider);
return provider;
};
/**
* 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[]} The credits to be displayed when the tile is displayed.
*/
ArcGisMapServerImageryProvider.prototype.getTileCredits = function (
x,
y,
level,
) {
return this._tileCredits;
};
/**
* 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.
*/
ArcGisMapServerImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
return ImageryProvider.loadImage(
this,
buildImageResource(this, x, y, level, request),
);
};
/**
/**
* Asynchronously determines what features, if any, are located at a given longitude and latitude within
* a tile.
*
* @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 {Promise<ImageryLayerFeatureInfo[]>|undefined} A promise for the picked features that will resolve when the asynchronous
* picking completes. The resolved value is an array of {@link ImageryLayerFeatureInfo}
* instances. The array may be empty if no features are found at the given location.
*/
ArcGisMapServerImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
if (!this.enablePickFeatures) {
return undefined;
}
const rectangle = this._tilingScheme.tileXYToNativeRectangle(x, y, level);
let horizontal;
let vertical;
let sr;
if (this._tilingScheme.projection instanceof GeographicProjection) {
horizontal = CesiumMath.toDegrees(longitude);
vertical = CesiumMath.toDegrees(latitude);
sr = "4326";
} else {
const projected = this._tilingScheme.projection.project(
new Cartographic(longitude, latitude, 0.0),
);
horizontal = projected.x;
vertical = projected.y;
sr = "3857";
}
let layers = "visible";
if (defined(this._layers)) {
layers += `:${this._layers}`;
}
const query = {
f: "json",
tolerance: 2,
geometryType: "esriGeometryPoint",
geometry: `${horizontal},${vertical}`,
mapExtent: `${rectangle.west},${rectangle.south},${rectangle.east},${rectangle.north}`,
imageDisplay: `${this._tileWidth},${this._tileHeight},96`,
sr: sr,
layers: layers,
};
const resource = this._resource.getDerivedResource({
url: "identify",
queryParameters: query,
});
return resource.fetchJson().then(function (json) {
const result = [];
const features = json.results;
if (!defined(features)) {
return result;
}
for (let i = 0; i < features.length; ++i) {
const feature = features[i];
const featureInfo = new ImageryLayerFeatureInfo();
featureInfo.data = feature;
featureInfo.name = feature.value;
featureInfo.properties = feature.attributes;
featureInfo.configureDescriptionFromProperties(feature.attributes);
// If this is a point feature, use the coordinates of the point.
if (feature.geometryType === "esriGeometryPoint" && feature.geometry) {
const wkid =
feature.geometry.spatialReference &&
feature.geometry.spatialReference.wkid
? feature.geometry.spatialReference.wkid
: 4326;
if (wkid === 4326 || wkid === 4283) {
featureInfo.position = Cartographic.fromDegrees(
feature.geometry.x,
feature.geometry.y,
feature.geometry.z,
);
} else if (wkid === 102100 || wkid === 900913 || wkid === 3857) {
const projection = new WebMercatorProjection();
featureInfo.position = projection.unproject(
new Cartesian3(
feature.geometry.x,
feature.geometry.y,
feature.geometry.z,
),
);
}
}
result.push(featureInfo);
}
return result;
});
};
ArcGisMapServerImageryProvider._metadataCache = {};
export default ArcGisMapServerImageryProvider;
+80
View File
@@ -0,0 +1,80 @@
import Credit from "../Core/Credit.js";
import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
let defaultTokenCredit;
const defaultAccessToken =
"AAPTauP-rqeCeHTtrSMga9XbPxQ..ZMIZzf1tcwicoJi5E5T2mG0EO3qlIRkQNe5S-uA95RJKU0YkrwOT0AF-lSIyXXBFakDJIPaxsZ5vpnzlaW3kCYG2Tk53bY6OntMqn1EBYtDPyqz38iM4NU_bbDN3DuxUdkxYDWnxJXxxWe3bEeUANbvhKK76PU7N0wjZgtEYHYocspYXSHxbCEYo_eXrHDkrWpgkzcmQ3WpQqf2XprJ39pbqVSZbxskDh3zBm92XxUaiSmdf7rC1w3VoAT1_UarLJfAQ";
/**
* Default options for accessing the ArcGIS image tile service.
*
* An ArcGIS access token is required to access ArcGIS image tile layers.
* A default token is provided for evaluation purposes only.
* To obtain an access token, go to {@link https://developers.arcgis.com} and create a free account.
* More info can be found in the {@link https://developers.arcgis.com/documentation/mapping-apis-and-services/security/ | ArcGIS developer guide}.
*
* @see ArcGisMapServerImageryProvider
* @namespace ArcGisMapService
*/
const ArcGisMapService = {};
/**
* Gets or sets the default ArcGIS access token.
*
* @type {string}
*/
ArcGisMapService.defaultAccessToken = defaultAccessToken;
/**
* Gets or sets the URL of the ArcGIS World Imagery tile service.
*
* @type {string|Resource}
* @default https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer
*/
ArcGisMapService.defaultWorldImageryServer = new Resource({
url: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer",
});
/**
* Gets or sets the URL of the ArcGIS World Hillshade tile service.
*
* @type {string|Resource}
* @default https://ibasemaps-api.arcgis.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer
*/
ArcGisMapService.defaultWorldHillshadeServer = new Resource({
url: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer",
});
/**
* Gets or sets the URL of the ArcGIS World Oceans tile service.
*
* @type {string|Resource}
* @default https://ibasemaps-api.arcgis.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer
*/
ArcGisMapService.defaultWorldOceanServer = new Resource({
url: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer",
});
/**
*
* @param {string} providedKey
* @return {string|undefined}
*/
ArcGisMapService.getDefaultTokenCredit = function (providedKey) {
if (providedKey !== defaultAccessToken) {
return undefined;
}
if (!defined(defaultTokenCredit)) {
const defaultTokenMessage =
'<b> \
This application is using a default ArcGIS access token. Please assign <i>Cesium.ArcGisMapService.defaultAccessToken</i> \
with an API key from your ArcGIS Developer account before using the ArcGIS tile services. \
You can sign up for a free ArcGIS Developer account at <a href="https://developers.arcgis.com/">https://developers.arcgis.com/</a>.</b>';
defaultTokenCredit = new Credit(defaultTokenMessage, true);
}
return defaultTokenCredit;
};
export default ArcGisMapService;
+149
View File
@@ -0,0 +1,149 @@
import Cartesian3 from "../Core/Cartesian3.js";
import CesiumMath from "../Core/Math.js";
import DynamicAtmosphereLightingType from "./DynamicAtmosphereLightingType.js";
/**
* Common atmosphere settings used by 3D Tiles and models for rendering sky atmosphere, ground atmosphere, and fog.
*
* <p>
* This class is not to be confused with {@link SkyAtmosphere}, which is responsible for rendering the sky.
* </p>
* <p>
* While the atmosphere settings affect the color of fog, see {@link Fog} to control how fog is rendered.
* </p>
*
* @alias Atmosphere
* @constructor
*
* @example
* // Turn on dynamic atmosphere lighting using the sun direction
* scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SUNLIGHT;
*
* @example
* // Turn on dynamic lighting using whatever light source is in the scene
* scene.light = new Cesium.DirectionalLight({
* direction: new Cesium.Cartesian3(1, 0, 0)
* });
* scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SCENE_LIGHT;
*
* @example
* // Adjust the color of the atmosphere effects.
* scene.atmosphere.hueShift = 0.4; // Cycle 40% around the color wheel
* scene.atmosphere.brightnessShift = 0.25; // Increase the brightness
* scene.atmosphere.saturationShift = -0.1; // Desaturate the colors
*
* @see SkyAtmosphere
* @see Globe
* @see Fog
*/
function Atmosphere() {
/**
* The intensity of the light that is used for computing the ground atmosphere color.
*
* @type {number}
* @default 10.0
*/
this.lightIntensity = 10.0;
/**
* The Rayleigh scattering coefficient used in the atmospheric scattering equations for the ground atmosphere.
*
* @type {Cartesian3}
* @default Cartesian3(5.5e-6, 13.0e-6, 28.4e-6)
*/
this.rayleighCoefficient = new Cartesian3(5.5e-6, 13.0e-6, 28.4e-6);
/**
* The Mie scattering coefficient used in the atmospheric scattering equations for the ground atmosphere.
*
* @type {Cartesian3}
* @default Cartesian3(21e-6, 21e-6, 21e-6)
*/
this.mieCoefficient = new Cartesian3(21e-6, 21e-6, 21e-6);
/**
* The Rayleigh scale height used in the atmospheric scattering equations for the ground atmosphere, in meters.
*
* @type {number}
* @default 10000.0
*/
this.rayleighScaleHeight = 10000.0;
/**
* The Mie scale height used in the atmospheric scattering equations for the ground atmosphere, in meters.
*
* @type {number}
* @default 3200.0
*/
this.mieScaleHeight = 3200.0;
/**
* The anisotropy of the medium to consider for Mie scattering.
* <p>
* Valid values are between -1.0 and 1.0.
* </p>
*
* @type {number}
* @default 0.9
*/
this.mieAnisotropy = 0.9;
/**
* The hue shift to apply to the atmosphere. Defaults to 0.0 (no shift).
* A hue shift of 1.0 indicates a complete rotation of the hues available.
*
* @type {number}
* @default 0.0
*/
this.hueShift = 0.0;
/**
* The saturation shift to apply to the atmosphere. Defaults to 0.0 (no shift).
* A saturation shift of -1.0 is monochrome.
*
* @type {number}
* @default 0.0
*/
this.saturationShift = 0.0;
/**
* The brightness shift to apply to the atmosphere. Defaults to 0.0 (no shift).
* A brightness shift of -1.0 is complete darkness, which will let space show through.
*
* @type {number}
* @default 0.0
*/
this.brightnessShift = 0.0;
/**
* When not DynamicAtmosphereLightingType.NONE, the selected light source will
* be used for dynamically lighting all atmosphere-related rendering effects.
*
* @type {DynamicAtmosphereLightingType}
* @default DynamicAtmosphereLightingType.NONE
*/
this.dynamicLighting = DynamicAtmosphereLightingType.NONE;
}
/**
* Returns <code>true</code> if the atmosphere shader requires a color correct step.
* @param {Atmosphere} atmosphere The atmosphere instance to check
* @returns {boolean} true if the atmosphere shader requires a color correct step
*/
Atmosphere.requiresColorCorrect = function (atmosphere) {
return !(
CesiumMath.equalsEpsilon(atmosphere.hueShift, 0.0, CesiumMath.EPSILON7) &&
CesiumMath.equalsEpsilon(
atmosphere.saturationShift,
0.0,
CesiumMath.EPSILON7,
) &&
CesiumMath.equalsEpsilon(
atmosphere.brightnessShift,
0.0,
CesiumMath.EPSILON7,
)
);
};
export default Atmosphere;
+202
View File
@@ -0,0 +1,202 @@
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 DeveloperError from "../Core/DeveloperError.js";
import Matrix2 from "../Core/Matrix2.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
/**
* An enum describing the attribute type for glTF and 3D Tiles.
*
* @enum {string}
*
* @private
*/
const AttributeType = {
/**
* The attribute is a single component.
*
* @type {string}
* @constant
*/
SCALAR: "SCALAR",
/**
* The attribute is a two-component vector.
*
* @type {string}
* @constant
*/
VEC2: "VEC2",
/**
* The attribute is a three-component vector.
*
* @type {string}
* @constant
*/
VEC3: "VEC3",
/**
* The attribute is a four-component vector.
*
* @type {string}
* @constant
*/
VEC4: "VEC4",
/**
* The attribute is a 2x2 matrix.
*
* @type {string}
* @constant
*/
MAT2: "MAT2",
/**
* The attribute is a 3x3 matrix.
*
* @type {string}
* @constant
*/
MAT3: "MAT3",
/**
* The attribute is a 4x4 matrix.
*
* @type {string}
* @constant
*/
MAT4: "MAT4",
};
/**
* Gets the scalar, vector, or matrix type for the attribute type.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {*} The math type.
*
* @private
*/
AttributeType.getMathType = function (attributeType) {
switch (attributeType) {
case AttributeType.SCALAR:
return Number;
case AttributeType.VEC2:
return Cartesian2;
case AttributeType.VEC3:
return Cartesian3;
case AttributeType.VEC4:
return Cartesian4;
case AttributeType.MAT2:
return Matrix2;
case AttributeType.MAT3:
return Matrix3;
case AttributeType.MAT4:
return Matrix4;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
/**
* Gets the number of components per attribute.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {number} The number of components.
*
* @private
*/
AttributeType.getNumberOfComponents = function (attributeType) {
switch (attributeType) {
case AttributeType.SCALAR:
return 1;
case AttributeType.VEC2:
return 2;
case AttributeType.VEC3:
return 3;
case AttributeType.VEC4:
case AttributeType.MAT2:
return 4;
case AttributeType.MAT3:
return 9;
case AttributeType.MAT4:
return 16;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
/**
* Get the number of attribute locations needed to fit this attribute. Most
* types require one, but matrices require multiple attribute locations.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {number} The number of attribute locations needed in the shader
*
* @private
*/
AttributeType.getAttributeLocationCount = function (attributeType) {
switch (attributeType) {
case AttributeType.SCALAR:
case AttributeType.VEC2:
case AttributeType.VEC3:
case AttributeType.VEC4:
return 1;
case AttributeType.MAT2:
return 2;
case AttributeType.MAT3:
return 3;
case AttributeType.MAT4:
return 4;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
/**
* Gets the GLSL type for the attribute type.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {string} The GLSL type for the attribute type.
*
* @private
*/
AttributeType.getGlslType = function (attributeType) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("attributeType", attributeType);
//>>includeEnd('debug');
switch (attributeType) {
case AttributeType.SCALAR:
return "float";
case AttributeType.VEC2:
return "vec2";
case AttributeType.VEC3:
return "vec3";
case AttributeType.VEC4:
return "vec4";
case AttributeType.MAT2:
return "mat2";
case AttributeType.MAT3:
return "mat3";
case AttributeType.MAT4:
return "mat4";
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
Object.freeze(AttributeType);
export default AttributeType;
+386
View File
@@ -0,0 +1,386 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import ClearCommand from "../Renderer/ClearCommand.js";
import FramebufferManager from "../Renderer/FramebufferManager.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
/**
* A post process stage that will get the luminance value at each pixel and
* uses parallel reduction to compute the average luminance in a 1x1 texture.
* This texture can be used as input for tone mapping.
*
* @constructor
* @private
*/
function AutoExposure() {
this._uniformMap = undefined;
this._command = undefined;
this._colorTexture = undefined;
this._depthTexture = undefined;
this._ready = false;
this._name = "czm_autoexposure";
this._logDepthChanged = undefined;
this._useLogDepth = undefined;
this._framebuffers = undefined;
this._previousLuminance = new FramebufferManager();
this._commands = undefined;
this._clearCommand = undefined;
this._minMaxLuminance = new Cartesian2();
/**
* Whether or not to execute this post-process stage when ready.
*
* @type {boolean}
*/
this.enabled = true;
this._enabled = true;
/**
* The minimum value used to clamp the luminance.
*
* @type {number}
* @default 0.1
*/
this.minimumLuminance = 0.1;
/**
* The maximum value used to clamp the luminance.
*
* @type {number}
* @default 10.0
*/
this.maximumLuminance = 10.0;
}
Object.defineProperties(AutoExposure.prototype, {
/**
* Determines if this post-process stage is ready to be executed. A stage is only executed when both <code>ready</code>
* and {@link AutoExposure#enabled} are <code>true</code>. A stage will not be ready while it is waiting on textures
* to load.
*
* @memberof AutoExposure.prototype
* @type {boolean}
* @readonly
*/
ready: {
get: function () {
return this._ready;
},
},
/**
* The unique name of this post-process stage for reference by other stages.
*
* @memberof AutoExposure.prototype
* @type {string}
* @readonly
*/
name: {
get: function () {
return this._name;
},
},
/**
* A reference to the texture written to when executing this post process stage.
*
* @memberof AutoExposure.prototype
* @type {Texture}
* @readonly
* @private
*/
outputTexture: {
get: function () {
const framebuffers = this._framebuffers;
if (!defined(framebuffers)) {
return undefined;
}
return framebuffers[framebuffers.length - 1].getColorTexture(0);
},
},
});
function destroyFramebuffers(autoexposure) {
const framebuffers = autoexposure._framebuffers;
if (!defined(framebuffers)) {
return;
}
const length = framebuffers.length;
for (let i = 0; i < length; ++i) {
framebuffers[i].destroy();
}
autoexposure._framebuffers = undefined;
autoexposure._previousLuminance.destroy();
autoexposure._previousLuminance = undefined;
}
function createFramebuffers(autoexposure, context) {
destroyFramebuffers(autoexposure);
let width = autoexposure._width;
let height = autoexposure._height;
const pixelDatatype = context.halfFloatingPointTexture
? PixelDatatype.HALF_FLOAT
: PixelDatatype.FLOAT;
const length = Math.ceil(Math.log(Math.max(width, height)) / Math.log(3.0));
const framebuffers = new Array(length);
for (let i = 0; i < length; ++i) {
width = Math.max(Math.ceil(width / 3.0), 1.0);
height = Math.max(Math.ceil(height / 3.0), 1.0);
framebuffers[i] = new FramebufferManager();
framebuffers[i].update(context, width, height, 1, pixelDatatype);
}
const lastTexture = framebuffers[length - 1].getColorTexture(0);
autoexposure._previousLuminance.update(
context,
lastTexture.width,
lastTexture.height,
1,
pixelDatatype,
);
autoexposure._framebuffers = framebuffers;
}
function destroyCommands(autoexposure) {
const commands = autoexposure._commands;
if (!defined(commands)) {
return;
}
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].shaderProgram.destroy();
}
autoexposure._commands = undefined;
}
function createUniformMap(autoexposure, index) {
let uniforms;
if (index === 0) {
uniforms = {
colorTexture: function () {
return autoexposure._colorTexture;
},
colorTextureDimensions: function () {
return autoexposure._colorTexture.dimensions;
},
};
} else {
const texture = autoexposure._framebuffers[index - 1].getColorTexture(0);
uniforms = {
colorTexture: function () {
return texture;
},
colorTextureDimensions: function () {
return texture.dimensions;
},
};
}
uniforms.minMaxLuminance = function () {
return autoexposure._minMaxLuminance;
};
uniforms.previousLuminance = function () {
return autoexposure._previousLuminance.getColorTexture(0);
};
return uniforms;
}
function getShaderSource(index, length) {
let source =
"uniform sampler2D colorTexture; \n" +
"in vec2 v_textureCoordinates; \n" +
"float sampleTexture(vec2 offset) { \n";
if (index === 0) {
source +=
" vec4 color = texture(colorTexture, v_textureCoordinates + offset); \n" +
" return czm_luminance(color.rgb); \n";
} else {
source +=
" return texture(colorTexture, v_textureCoordinates + offset).r; \n";
}
source += "}\n\n";
source +=
"uniform vec2 colorTextureDimensions; \n" +
"uniform vec2 minMaxLuminance; \n" +
"uniform sampler2D previousLuminance; \n" +
"void main() { \n" +
" float color = 0.0; \n" +
" float xStep = 1.0 / colorTextureDimensions.x; \n" +
" float yStep = 1.0 / colorTextureDimensions.y; \n" +
" int count = 0; \n" +
" for (int i = 0; i < 3; ++i) { \n" +
" for (int j = 0; j < 3; ++j) { \n" +
" vec2 offset; \n" +
" offset.x = -xStep + float(i) * xStep; \n" +
" offset.y = -yStep + float(j) * yStep; \n" +
" if (offset.x < 0.0 || offset.x > 1.0 || offset.y < 0.0 || offset.y > 1.0) { \n" +
" continue; \n" +
" } \n" +
" color += sampleTexture(offset); \n" +
" ++count; \n" +
" } \n" +
" } \n" +
" if (count > 0) { \n" +
" color /= float(count); \n" +
" } \n";
if (index === length - 1) {
source +=
" float previous = texture(previousLuminance, vec2(0.5)).r; \n" +
" color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n" +
" color = previous + (color - previous) / (60.0 * 1.5); \n" +
" color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n";
}
source += " out_FragColor = vec4(color); \n" + "} \n";
return source;
}
function createCommands(autoexposure, context) {
destroyCommands(autoexposure);
const framebuffers = autoexposure._framebuffers;
const length = framebuffers.length;
const commands = new Array(length);
for (let i = 0; i < length; ++i) {
commands[i] = context.createViewportQuadCommand(
getShaderSource(i, length),
{
framebuffer: framebuffers[i].framebuffer,
uniformMap: createUniformMap(autoexposure, i),
},
);
}
autoexposure._commands = commands;
}
/**
* A function that will be called before execute. Used to clear any textures attached to framebuffers.
* @param {Context} context The context.
* @private
*/
AutoExposure.prototype.clear = function (context) {
const framebuffers = this._framebuffers;
if (!defined(framebuffers)) {
return;
}
let clearCommand = this._clearCommand;
if (!defined(clearCommand)) {
clearCommand = this._clearCommand = new ClearCommand({
color: new Color(0.0, 0.0, 0.0, 0.0),
framebuffer: undefined,
});
}
const length = framebuffers.length;
for (let i = 0; i < length; ++i) {
framebuffers[i].clear(context, clearCommand);
}
};
/**
* A function that will be called before execute. Used to create WebGL resources and load any textures.
* @param {Context} context The context.
* @private
*/
AutoExposure.prototype.update = function (context) {
const width = context.drawingBufferWidth;
const height = context.drawingBufferHeight;
if (width !== this._width || height !== this._height) {
this._width = width;
this._height = height;
createFramebuffers(this, context);
createCommands(this, context);
if (!this._ready) {
this._ready = true;
}
}
this._minMaxLuminance.x = this.minimumLuminance;
this._minMaxLuminance.y = this.maximumLuminance;
const framebuffers = this._framebuffers;
const temp = framebuffers[framebuffers.length - 1];
framebuffers[framebuffers.length - 1] = this._previousLuminance;
this._commands[this._commands.length - 1].framebuffer =
this._previousLuminance.framebuffer;
this._previousLuminance = temp;
};
/**
* Executes the post-process stage. The color texture is the texture rendered to by the scene or from the previous stage.
* @param {Context} context The context.
* @param {Texture} colorTexture The input color texture.
* @private
*/
AutoExposure.prototype.execute = function (context, colorTexture) {
this._colorTexture = colorTexture;
const commands = this._commands;
if (!defined(commands)) {
return;
}
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].execute(context);
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <p>
* 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.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see AutoExposure#destroy
*/
AutoExposure.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.
* <p>
* 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.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see AutoExposure#isDestroyed
*/
AutoExposure.prototype.destroy = function () {
destroyFramebuffers(this);
destroyCommands(this);
return destroyObject(this);
};
export default AutoExposure;
+118
View File
@@ -0,0 +1,118 @@
import Check from "../Core/Check.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
/**
* An enum describing the x, y, and z axes and helper conversion functions.
*
* @enum {number}
*/
const Axis = {
/**
* Denotes the x-axis.
*
* @type {number}
* @constant
*/
X: 0,
/**
* Denotes the y-axis.
*
* @type {number}
* @constant
*/
Y: 1,
/**
* Denotes the z-axis.
*
* @type {number}
* @constant
*/
Z: 2,
};
/**
* Matrix used to convert from y-up to z-up
*
* @type {Matrix4}
* @constant
*/
Axis.Y_UP_TO_Z_UP = Matrix4.fromRotationTranslation(
// Rotation about PI/2 around the X-axis
Matrix3.fromArray([1, 0, 0, 0, 0, 1, 0, -1, 0]),
);
/**
* Matrix used to convert from z-up to y-up
*
* @type {Matrix4}
* @constant
*/
Axis.Z_UP_TO_Y_UP = Matrix4.fromRotationTranslation(
// Rotation about -PI/2 around the X-axis
Matrix3.fromArray([1, 0, 0, 0, 0, -1, 0, 1, 0]),
);
/**
* Matrix used to convert from x-up to z-up
*
* @type {Matrix4}
* @constant
*/
Axis.X_UP_TO_Z_UP = Matrix4.fromRotationTranslation(
// Rotation about -PI/2 around the Y-axis
Matrix3.fromArray([0, 0, 1, 0, 1, 0, -1, 0, 0]),
);
/**
* Matrix used to convert from z-up to x-up
*
* @type {Matrix4}
* @constant
*/
Axis.Z_UP_TO_X_UP = Matrix4.fromRotationTranslation(
// Rotation about PI/2 around the Y-axis
Matrix3.fromArray([0, 0, -1, 0, 1, 0, 1, 0, 0]),
);
/**
* Matrix used to convert from x-up to y-up
*
* @type {Matrix4}
* @constant
*/
Axis.X_UP_TO_Y_UP = Matrix4.fromRotationTranslation(
// Rotation about PI/2 around the Z-axis
Matrix3.fromArray([0, 1, 0, -1, 0, 0, 0, 0, 1]),
);
/**
* Matrix used to convert from y-up to x-up
*
* @type {Matrix4}
* @constant
*/
Axis.Y_UP_TO_X_UP = Matrix4.fromRotationTranslation(
// Rotation about -PI/2 around the Z-axis
Matrix3.fromArray([0, -1, 0, 1, 0, 0, 0, 0, 1]),
);
/**
* Gets the axis by name
*
* @param {string} name The name of the axis.
* @returns {number} The axis enum.
*/
Axis.fromName = function (name) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("name", name);
//>>includeEnd('debug');
return Axis[name];
};
Object.freeze(Axis);
export default Axis;
@@ -0,0 +1,393 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.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 {string} subscriptionKey The public subscription key for the imagery.
* @property {string} [url="https://atlas.microsoft.com/"] The Azure server url.
* @property {string} [tilesetId="microsoft.imagery"] The Azure tileset ID. Valid options are {@link microsoft.imagery}, {@link microsoft.base.road}, and {@link microsoft.base.labels.road}
* @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.
*/
/**
* Provides 2D image tiles from Azure.
*
* @alias Azure2DImageryProvider
* @constructor
* @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 ?? {};
const tilesetId = options.tilesetId ?? "microsoft.imagery";
this._maximumLevel = options.maximumLevel ?? 22;
this._minimumLevel = options.minimumLevel ?? 0;
this._subscriptionKey =
options.subscriptionKey ?? options["subscription-key"];
//>>includeStart('debug', pragmas.debug);
Check.defined("options.subscriptionKey", this._subscriptionKey);
//>>includeEnd('debug');
this._tilesetId = options.tilesetId;
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 += "/";
}
const tilesUrl = `${templateUrl}map/tile`;
this._viewportUrl = `${templateUrl}map/attribution`;
resource.url = tilesUrl;
resource.setQueryParameters({
"api-version": "2024-04-01",
tilesetId: tilesetId,
"subscription-key": this._subscriptionKey,
zoom: `{z}`,
x: `{x}`,
y: `{y}`,
});
this._resource = resource;
let credit;
if (defined(options.credit)) {
credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
}
const provider = new UrlTemplateImageryProvider({
...options,
maximumLevel: this._maximumLevel,
minimumLevel: this._minimumLevel,
url: resource,
credit: credit,
});
provider._resource = resource;
this._imageryProvider = provider;
// This will be defined for ion resources
this._tileCredits = resource.credits;
this._attributionsByLevel = undefined;
}
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) {
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.
*/
Azure2DImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
const promise = this._imageryProvider.requestImage(x, y, level, request);
// If the requestImage call returns undefined, it couldn't be scheduled this frame. Make sure to return undefined so this can be handled upstream.
if (!defined(promise)) {
return undefined;
}
// Asynchronously request and populate _attributionsByLevel if it hasn't been already. We do this here so that the promise can be properly awaited.
if (!defined(this._attributionsByLevel)) {
return Promise.all([promise, this.getViewportCredits()]).then(
(results) => results[0],
);
}
return promise;
};
/**
* 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;
};
/**
* Get attribution for imagery from Azure Maps to display in the credits
* @private
* @return {Promise<Map<Credit[]>>} The list of attribution sources to display in the credits.
*/
Azure2DImageryProvider.prototype.getViewportCredits = async function () {
const maximumLevel = this._maximumLevel;
const promises = [];
for (let level = 0; level < maximumLevel + 1; level++) {
promises.push(
fetchViewportAttribution(
this._resource,
this._viewportUrl,
this._subscriptionKey,
this._tilesetId,
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].join(",");
if (attributions) {
const levelCredits = new Credit(attributions);
credits.push(levelCredits);
}
attributionsByLevel.set(level, credits);
}
this._attributionsByLevel = attributionsByLevel;
return attributionsByLevel;
};
async function fetchViewportAttribution(resource, url, key, tilesetId, level) {
const viewportResource = resource.getDerivedResource({
url,
queryParameters: {
zoom: level,
bounds: "-180,-90,180,90",
},
data: JSON.stringify(Frozen.EMPTY_OBJECT),
});
const viewportJson = await viewportResource.fetchJson();
return viewportJson.copyrights;
}
// Exposed for tests
export default Azure2DImageryProvider;
+173
View File
@@ -0,0 +1,173 @@
import Check from "../Core/Check.js";
import deprecationWarning from "../Core/deprecationWarning.js";
import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Handles parsing of a Batched 3D Model.
*
* @namespace B3dmParser
* @private
*/
const B3dmParser = {};
B3dmParser._deprecationWarning = deprecationWarning;
const sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;
/**
* Parses the contents of a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel|Batched 3D Model}.
*
* @private
*
* @param {ArrayBuffer} arrayBuffer The array buffer containing the b3dm.
* @param {number} [byteOffset=0] The byte offset of the beginning of the b3dm in the array buffer.
* @returns {object} Returns an object with the batch length, feature table (binary and json), batch table (binary and json) and glTF parts of the b3dm.
*/
B3dmParser.parse = function (arrayBuffer, byteOffset) {
const byteStart = byteOffset ?? 0;
//>>includeStart('debug', pragmas.debug);
Check.defined("arrayBuffer", arrayBuffer);
//>>includeEnd('debug');
byteOffset = byteStart;
const uint8Array = new Uint8Array(arrayBuffer);
const view = new DataView(arrayBuffer);
byteOffset += sizeOfUint32; // Skip magic
const version = view.getUint32(byteOffset, true);
if (version !== 1) {
throw new RuntimeError(
`Only Batched 3D Model version 1 is supported. Version ${version} is not.`,
);
}
byteOffset += sizeOfUint32;
const byteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let featureTableJsonByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let featureTableBinaryByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let batchTableJsonByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let batchTableBinaryByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let batchLength;
// Legacy header #1: [batchLength] [batchTableByteLength]
// Legacy header #2: [batchTableJsonByteLength] [batchTableBinaryByteLength] [batchLength]
// Current header: [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength]
// If the header is in the first legacy format 'batchTableJsonByteLength' will be the start of the JSON string (a quotation mark) or the glTF magic.
// Accordingly its first byte will be either 0x22 or 0x67, and so the minimum uint32 expected is 0x22000000 = 570425344 = 570MB. It is unlikely that the feature table JSON will exceed this length.
// The check for the second legacy format is similar, except it checks 'batchTableBinaryByteLength' instead
if (batchTableJsonByteLength >= 570425344) {
// First legacy check
byteOffset -= sizeOfUint32 * 2;
batchLength = featureTableJsonByteLength;
batchTableJsonByteLength = featureTableBinaryByteLength;
batchTableBinaryByteLength = 0;
featureTableJsonByteLength = 0;
featureTableBinaryByteLength = 0;
B3dmParser._deprecationWarning(
"b3dm-legacy-header",
"This b3dm header is using the legacy format [batchLength] [batchTableByteLength]. The new format is [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] from https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel.",
);
} else if (batchTableBinaryByteLength >= 570425344) {
// Second legacy check
byteOffset -= sizeOfUint32;
batchLength = batchTableJsonByteLength;
batchTableJsonByteLength = featureTableJsonByteLength;
batchTableBinaryByteLength = featureTableBinaryByteLength;
featureTableJsonByteLength = 0;
featureTableBinaryByteLength = 0;
B3dmParser._deprecationWarning(
"b3dm-legacy-header",
"This b3dm header is using the legacy format [batchTableJsonByteLength] [batchTableBinaryByteLength] [batchLength]. The new format is [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] from https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel.",
);
}
let featureTableJson;
if (featureTableJsonByteLength === 0) {
featureTableJson = {
BATCH_LENGTH: batchLength ?? 0,
};
} else {
featureTableJson = getJsonFromTypedArray(
uint8Array,
byteOffset,
featureTableJsonByteLength,
);
byteOffset += featureTableJsonByteLength;
}
const featureTableBinary = new Uint8Array(
arrayBuffer,
byteOffset,
featureTableBinaryByteLength,
);
byteOffset += featureTableBinaryByteLength;
let batchTableJson;
let batchTableBinary;
if (batchTableJsonByteLength > 0) {
// PERFORMANCE_IDEA: is it possible to allocate this on-demand? Perhaps keep the
// arraybuffer/string compressed in memory and then decompress it when it is first accessed.
//
// We could also make another request for it, but that would make the property set/get
// API async, and would double the number of numbers in some cases.
batchTableJson = getJsonFromTypedArray(
uint8Array,
byteOffset,
batchTableJsonByteLength,
);
byteOffset += batchTableJsonByteLength;
if (batchTableBinaryByteLength > 0) {
// Has a batch table binary
batchTableBinary = new Uint8Array(
arrayBuffer,
byteOffset,
batchTableBinaryByteLength,
);
// Copy the batchTableBinary section and let the underlying ArrayBuffer be freed
batchTableBinary = new Uint8Array(batchTableBinary);
byteOffset += batchTableBinaryByteLength;
}
}
const gltfByteLength = byteStart + byteLength - byteOffset;
if (gltfByteLength === 0) {
throw new RuntimeError("glTF byte length must be greater than 0.");
}
let gltfView;
if (byteOffset % 4 === 0) {
gltfView = new Uint8Array(arrayBuffer, byteOffset, gltfByteLength);
} else {
// Create a copy of the glb so that it is 4-byte aligned
B3dmParser._deprecationWarning(
"b3dm-glb-unaligned",
"The embedded glb is not aligned to a 4-byte boundary.",
);
gltfView = new Uint8Array(
uint8Array.subarray(byteOffset, byteOffset + gltfByteLength),
);
}
return {
batchLength: batchLength,
featureTableJson: featureTableJson,
featureTableBinary: featureTableBinary,
batchTableJson: batchTableJson,
batchTableBinary: batchTableBinary,
gltf: gltfView,
};
};
export default B3dmParser;
+636
View File
@@ -0,0 +1,636 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import combine from "../Core/combine.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import PixelFormat from "../Core/PixelFormat.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
/**
* Creates a texture to look up per instance attributes for batched primitives. For example, store each primitive's pick color in the texture.
*
* @alias BatchTable
* @constructor
* @private
*
* @param {Context} context The context in which the batch table is created.
* @param {object[]} attributes An array of objects describing a per instance attribute. Each object contains a datatype, components per attributes, whether it is normalized and a function name
* to retrieve the value in the vertex shader.
* @param {number} numberOfInstances The number of instances in a batch table.
*
* @example
* // create the batch table
* const attributes = [{
* functionName : 'getShow',
* componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
* componentsPerAttribute : 1
* }, {
* functionName : 'getPickColor',
* componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
* componentsPerAttribute : 4,
* normalize : true
* }];
* const batchTable = new BatchTable(context, attributes, 5);
*
* // when creating the draw commands, update the uniform map and the vertex shader
* vertexShaderSource = batchTable.getVertexShaderCallback()(vertexShaderSource);
* const shaderProgram = ShaderProgram.fromCache({
* // ...
* vertexShaderSource : vertexShaderSource,
* });
*
* drawCommand.shaderProgram = shaderProgram;
* drawCommand.uniformMap = batchTable.getUniformMapCallback()(uniformMap);
*
* // use the attribute function names in the shader to retrieve the instance values
* // ...
* attribute float batchId;
*
* void main() {
* // ...
* float show = getShow(batchId);
* vec3 pickColor = getPickColor(batchId);
* // ...
* }
*/
function BatchTable(context, attributes, numberOfInstances) {
//>>includeStart('debug', pragmas.debug);
if (!defined(context)) {
throw new DeveloperError("context is required");
}
if (!defined(attributes)) {
throw new DeveloperError("attributes is required");
}
if (!defined(numberOfInstances)) {
throw new DeveloperError("numberOfInstances is required");
}
//>>includeEnd('debug');
this._attributes = attributes;
this._numberOfInstances = numberOfInstances;
if (attributes.length === 0) {
return;
}
// PERFORMANCE_IDEA: We may be able to arrange the attributes so they can be packing into fewer texels.
// Right now, an attribute with one component uses an entire texel when 4 single component attributes can
// be packed into a texel.
//
// Packing floats into unsigned byte textures makes the problem worse. A single component float attribute
// will be packed into a single texel leaving 3 texels unused. 4 texels are reserved for each float attribute
// regardless of how many components it has.
const pixelDatatype = getDatatype(attributes);
const textureFloatSupported = context.floatingPointTexture;
const packFloats =
pixelDatatype === PixelDatatype.FLOAT && !textureFloatSupported;
const offsets = createOffsets(attributes, packFloats);
const stride = getStride(offsets, attributes, packFloats);
const maxNumberOfInstancesPerRow = Math.floor(
ContextLimits.maximumTextureSize / stride,
);
const instancesPerWidth = Math.min(
numberOfInstances,
maxNumberOfInstancesPerRow,
);
const width = stride * instancesPerWidth;
const height = Math.ceil(numberOfInstances / instancesPerWidth);
const stepX = 1.0 / width;
const centerX = stepX * 0.5;
const stepY = 1.0 / height;
const centerY = stepY * 0.5;
this._textureDimensions = new Cartesian2(width, height);
this._textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
this._pixelDatatype = !packFloats
? pixelDatatype
: PixelDatatype.UNSIGNED_BYTE;
this._packFloats = packFloats;
this._offsets = offsets;
this._stride = stride;
this._texture = undefined;
const batchLength = 4 * width * height;
this._batchValues =
pixelDatatype === PixelDatatype.FLOAT && !packFloats
? new Float32Array(batchLength)
: new Uint8Array(batchLength);
this._batchValuesDirty = false;
}
Object.defineProperties(BatchTable.prototype, {
/**
* The attribute descriptions.
* @memberOf BatchTable.prototype
* @type {object[]}
* @readonly
*/
attributes: {
get: function () {
return this._attributes;
},
},
/**
* The number of instances.
* @memberOf BatchTable.prototype
* @type {number}
* @readonly
*/
numberOfInstances: {
get: function () {
return this._numberOfInstances;
},
},
});
function getDatatype(attributes) {
let foundFloatDatatype = false;
const length = attributes.length;
for (let i = 0; i < length; ++i) {
if (attributes[i].componentDatatype !== ComponentDatatype.UNSIGNED_BYTE) {
foundFloatDatatype = true;
break;
}
}
return foundFloatDatatype ? PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE;
}
function getAttributeType(attributes, attributeIndex) {
const componentsPerAttribute =
attributes[attributeIndex].componentsPerAttribute;
if (componentsPerAttribute === 2) {
return Cartesian2;
} else if (componentsPerAttribute === 3) {
return Cartesian3;
} else if (componentsPerAttribute === 4) {
return Cartesian4;
}
return Number;
}
function createOffsets(attributes, packFloats) {
const offsets = new Array(attributes.length);
let currentOffset = 0;
const attributesLength = attributes.length;
for (let i = 0; i < attributesLength; ++i) {
const attribute = attributes[i];
const componentDatatype = attribute.componentDatatype;
offsets[i] = currentOffset;
if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
currentOffset += 4;
} else {
++currentOffset;
}
}
return offsets;
}
function getStride(offsets, attributes, packFloats) {
const length = offsets.length;
const lastOffset = offsets[length - 1];
const lastAttribute = attributes[length - 1];
const componentDatatype = lastAttribute.componentDatatype;
if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
return lastOffset + 4;
}
return lastOffset + 1;
}
const scratchPackedFloatCartesian4 = new Cartesian4();
function getPackedFloat(array, index, result) {
let packed = Cartesian4.unpack(array, index, scratchPackedFloatCartesian4);
const x = Cartesian4.unpackFloat(packed);
packed = Cartesian4.unpack(array, index + 4, scratchPackedFloatCartesian4);
const y = Cartesian4.unpackFloat(packed);
packed = Cartesian4.unpack(array, index + 8, scratchPackedFloatCartesian4);
const z = Cartesian4.unpackFloat(packed);
packed = Cartesian4.unpack(array, index + 12, scratchPackedFloatCartesian4);
const w = Cartesian4.unpackFloat(packed);
return Cartesian4.fromElements(x, y, z, w, result);
}
function setPackedAttribute(value, array, index) {
let packed = Cartesian4.packFloat(value.x, scratchPackedFloatCartesian4);
Cartesian4.pack(packed, array, index);
packed = Cartesian4.packFloat(value.y, packed);
Cartesian4.pack(packed, array, index + 4);
packed = Cartesian4.packFloat(value.z, packed);
Cartesian4.pack(packed, array, index + 8);
packed = Cartesian4.packFloat(value.w, packed);
Cartesian4.pack(packed, array, index + 12);
}
const scratchGetAttributeCartesian4 = new Cartesian4();
/**
* Gets the value of an attribute in the table.
*
* @param {number} instanceIndex The index of the instance.
* @param {number} attributeIndex The index of the attribute.
* @param {undefined|Cartesian2|Cartesian3|Cartesian4} [result] The object onto which to store the result. The type is dependent on the attribute's number of components.
* @returns {number|Cartesian2|Cartesian3|Cartesian4} The attribute value stored for the instance.
*
* @exception {DeveloperError} instanceIndex is out of range.
* @exception {DeveloperError} attributeIndex is out of range.
*/
BatchTable.prototype.getBatchedAttribute = function (
instanceIndex,
attributeIndex,
result,
) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
throw new DeveloperError("instanceIndex is out of range.");
}
if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
throw new DeveloperError("attributeIndex is out of range");
}
//>>includeEnd('debug');
const attributes = this._attributes;
const offset = this._offsets[attributeIndex];
const stride = this._stride;
const index = 4 * stride * instanceIndex + 4 * offset;
let value;
if (
this._packFloats &&
attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE
) {
value = getPackedFloat(
this._batchValues,
index,
scratchGetAttributeCartesian4,
);
} else {
value = Cartesian4.unpack(
this._batchValues,
index,
scratchGetAttributeCartesian4,
);
}
const attributeType = getAttributeType(attributes, attributeIndex);
if (defined(attributeType.fromCartesian4)) {
return attributeType.fromCartesian4(value, result);
} else if (defined(attributeType.clone)) {
return attributeType.clone(value, result);
}
return value.x;
};
const setAttributeScratchValues = [
undefined,
undefined,
new Cartesian2(),
new Cartesian3(),
new Cartesian4(),
];
const setAttributeScratchCartesian4 = new Cartesian4();
/**
* Sets the value of an attribute in the table.
*
* @param {number} instanceIndex The index of the instance.
* @param {number} attributeIndex The index of the attribute.
* @param {number|Cartesian2|Cartesian3|Cartesian4} value The value to be stored in the table. The type of value will depend on the number of components of the attribute.
*
* @exception {DeveloperError} instanceIndex is out of range.
* @exception {DeveloperError} attributeIndex is out of range.
*/
BatchTable.prototype.setBatchedAttribute = function (
instanceIndex,
attributeIndex,
value,
) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
throw new DeveloperError("instanceIndex is out of range.");
}
if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
throw new DeveloperError("attributeIndex is out of range");
}
if (!defined(value)) {
throw new DeveloperError("value is required.");
}
//>>includeEnd('debug');
const attributes = this._attributes;
const result =
setAttributeScratchValues[
attributes[attributeIndex].componentsPerAttribute
];
const currentAttribute = this.getBatchedAttribute(
instanceIndex,
attributeIndex,
result,
);
const attributeType = getAttributeType(this._attributes, attributeIndex);
const entriesEqual = defined(attributeType.equals)
? attributeType.equals(currentAttribute, value)
: currentAttribute === value;
if (entriesEqual) {
return;
}
const attributeValue = setAttributeScratchCartesian4;
attributeValue.x = defined(value.x) ? value.x : value;
attributeValue.y = defined(value.y) ? value.y : 0.0;
attributeValue.z = defined(value.z) ? value.z : 0.0;
attributeValue.w = defined(value.w) ? value.w : 0.0;
const offset = this._offsets[attributeIndex];
const stride = this._stride;
const index = 4 * stride * instanceIndex + 4 * offset;
if (
this._packFloats &&
attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE
) {
setPackedAttribute(attributeValue, this._batchValues, index);
} else {
Cartesian4.pack(attributeValue, this._batchValues, index);
}
this._batchValuesDirty = true;
};
function createTexture(batchTable, context) {
const dimensions = batchTable._textureDimensions;
batchTable._texture = new Texture({
context: context,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: batchTable._pixelDatatype,
width: dimensions.x,
height: dimensions.y,
sampler: Sampler.NEAREST,
flipY: false,
});
}
function updateTexture(batchTable) {
const dimensions = batchTable._textureDimensions;
batchTable._texture.copyFrom({
source: {
width: dimensions.x,
height: dimensions.y,
arrayBufferView: batchTable._batchValues,
},
});
}
/**
* Creates/updates the batch table texture.
* @param {FrameState} frameState The frame state.
*
* @exception {RuntimeError} The floating point texture extension is required but not supported.
*/
BatchTable.prototype.update = function (frameState) {
if (
(defined(this._texture) && !this._batchValuesDirty) ||
this._attributes.length === 0
) {
return;
}
this._batchValuesDirty = false;
if (!defined(this._texture)) {
createTexture(this, frameState.context);
}
updateTexture(this);
};
/**
* Gets a function that will update a uniform map to contain values for looking up values in the batch table.
*
* @returns {BatchTable.updateUniformMapCallback} A callback for updating uniform maps.
*/
BatchTable.prototype.getUniformMapCallback = function () {
const that = this;
return function (uniformMap) {
if (that._attributes.length === 0) {
return uniformMap;
}
const batchUniformMap = {
batchTexture: function () {
return that._texture;
},
batchTextureDimensions: function () {
return that._textureDimensions;
},
batchTextureStep: function () {
return that._textureStep;
},
};
return combine(uniformMap, batchUniformMap);
};
};
function getGlslComputeSt(batchTable) {
const stride = batchTable._stride;
// GLSL batchId is zero-based: [0, numberOfInstances - 1]
if (batchTable._textureDimensions.y === 1) {
return (
`${
"uniform vec4 batchTextureStep; \n" +
"vec2 computeSt(float batchId) \n" +
"{ \n" +
" float stepX = batchTextureStep.x; \n" +
" float centerX = batchTextureStep.y; \n" +
" float numberOfAttributes = float("
}${stride}); \n` +
` return vec2(centerX + (batchId * numberOfAttributes * stepX), 0.5); \n` +
`} \n`
);
}
return (
`${
"uniform vec4 batchTextureStep; \n" +
"uniform vec2 batchTextureDimensions; \n" +
"vec2 computeSt(float batchId) \n" +
"{ \n" +
" float stepX = batchTextureStep.x; \n" +
" float centerX = batchTextureStep.y; \n" +
" float stepY = batchTextureStep.z; \n" +
" float centerY = batchTextureStep.w; \n" +
" float numberOfAttributes = float("
}${stride}); \n` +
` float xId = mod(batchId * numberOfAttributes, batchTextureDimensions.x); \n` +
` float yId = floor(batchId * numberOfAttributes / batchTextureDimensions.x); \n` +
` return vec2(centerX + (xId * stepX), centerY + (yId * stepY)); \n` +
`} \n`
);
}
function getComponentType(componentsPerAttribute) {
if (componentsPerAttribute === 1) {
return "float";
}
return `vec${componentsPerAttribute}`;
}
function getComponentSwizzle(componentsPerAttribute) {
if (componentsPerAttribute === 1) {
return ".x";
} else if (componentsPerAttribute === 2) {
return ".xy";
} else if (componentsPerAttribute === 3) {
return ".xyz";
}
return "";
}
function getGlslAttributeFunction(batchTable, attributeIndex) {
const attributes = batchTable._attributes;
const attribute = attributes[attributeIndex];
const componentsPerAttribute = attribute.componentsPerAttribute;
const functionName = attribute.functionName;
const functionReturnType = getComponentType(componentsPerAttribute);
const functionReturnValue = getComponentSwizzle(componentsPerAttribute);
const offset = batchTable._offsets[attributeIndex];
let glslFunction =
`${functionReturnType} ${functionName}(float batchId) \n` +
`{ \n` +
` vec2 st = computeSt(batchId); \n` +
` st.x += batchTextureStep.x * float(${offset}); \n`;
if (
batchTable._packFloats &&
attribute.componentDatatype !== PixelDatatype.UNSIGNED_BYTE
) {
glslFunction +=
"vec4 textureValue; \n" +
"textureValue.x = czm_unpackFloat(texture(batchTexture, st)); \n" +
"textureValue.y = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x, 0.0))); \n" +
"textureValue.z = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x * 2.0, 0.0))); \n" +
"textureValue.w = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x * 3.0, 0.0))); \n";
} else {
glslFunction += " vec4 textureValue = texture(batchTexture, st); \n";
}
glslFunction += ` ${functionReturnType} value = textureValue${functionReturnValue}; \n`;
if (
batchTable._pixelDatatype === PixelDatatype.UNSIGNED_BYTE &&
attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE &&
!attribute.normalize
) {
glslFunction += "value *= 255.0; \n";
} else if (
batchTable._pixelDatatype === PixelDatatype.FLOAT &&
attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE &&
attribute.normalize
) {
glslFunction += "value /= 255.0; \n";
}
glslFunction += " return value; \n" + "} \n";
return glslFunction;
}
/**
* Gets a function that will update a vertex shader to contain functions for looking up values in the batch table.
*
* @returns {BatchTable.updateVertexShaderSourceCallback} A callback for updating a vertex shader source.
*/
BatchTable.prototype.getVertexShaderCallback = function () {
const attributes = this._attributes;
if (attributes.length === 0) {
return function (source) {
return source;
};
}
let batchTableShader = "uniform highp sampler2D batchTexture; \n";
batchTableShader += `${getGlslComputeSt(this)}\n`;
const length = attributes.length;
for (let i = 0; i < length; ++i) {
batchTableShader += getGlslAttributeFunction(this, i);
}
return function (source) {
const mainIndex = source.indexOf("void main");
const beforeMain = source.substring(0, mainIndex);
const afterMain = source.substring(mainIndex);
return `${beforeMain}\n${batchTableShader}\n${afterMain}`;
};
};
/**
* 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 BatchTable#destroy
*/
BatchTable.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 /><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.
*
* @see BatchTable#isDestroyed
*/
BatchTable.prototype.destroy = function () {
this._texture = this._texture && this._texture.destroy();
return destroyObject(this);
};
/**
* A callback for updating uniform maps.
* @callback BatchTable.updateUniformMapCallback
*
* @param {object} uniformMap The uniform map.
* @returns {object} The new uniform map with properties for retrieving values from the batch table.
*/
/**
* A callback for updating a vertex shader source.
* @callback BatchTable.updateVertexShaderSourceCallback
*
* @param {string} vertexShaderSource The vertex shader source.
* @returns {string} The new vertex shader source with the functions for retrieving batch table values injected.
*/
export default BatchTable;
@@ -0,0 +1,504 @@
import AttributeType from "./AttributeType.js";
import Check from "../Core/Check.js";
import clone from "../Core/clone.js";
import combine from "../Core/combine.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import getBinaryAccessor from "./getBinaryAccessor.js";
import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js";
/**
* Object for handling the <code>3DTILES_batch_table_hierarchy</code> extension
*
* @param {object} options Object with the following properties:
* @param {object} options.extension The <code>3DTILES_batch_table_hierarchy</code> extension object.
* @param {Uint8Array} [options.binaryBody] The binary body of the batch table
*
* @alias BatchTableHierarchy
* @constructor
*
* @private
*/
function BatchTableHierarchy(options) {
this._classes = undefined;
this._classIds = undefined;
this._classIndexes = undefined;
this._parentCounts = undefined;
this._parentIndexes = undefined;
this._parentIds = undefined;
// Total memory used by the typed arrays
this._byteLength = 0;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.extension", options.extension);
//>>includeEnd('debug');
initialize(this, options.extension, options.binaryBody);
//>>includeStart('debug', pragmas.debug);
validateHierarchy(this);
//>>includeEnd('debug');
}
Object.defineProperties(BatchTableHierarchy.prototype, {
byteLength: {
get: function () {
return this._byteLength;
},
},
});
/**
* Parse the batch table hierarchy from the
* <code>3DTILES_batch_table_hierarchy</code> extension.
*
* @param {BatchTableHierarchy} hierarchy The hierarchy instance
* @param {object} hierarchyJson The JSON of the extension
* @param {Uint8Array} [binaryBody] The binary body of the batch table for accessing binary properties
* @private
*/
function initialize(hierarchy, hierarchyJson, binaryBody) {
let i;
let classId;
let binaryAccessor;
const instancesLength = hierarchyJson.instancesLength;
const classes = hierarchyJson.classes;
let classIds = hierarchyJson.classIds;
let parentCounts = hierarchyJson.parentCounts;
let parentIds = hierarchyJson.parentIds;
let parentIdsLength = instancesLength;
let byteLength = 0;
if (defined(classIds.byteOffset)) {
classIds.componentType =
classIds.componentType ?? ComponentDatatype.UNSIGNED_SHORT;
classIds.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(classIds);
classIds = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + classIds.byteOffset,
instancesLength,
);
byteLength += classIds.byteLength;
}
let parentIndexes;
if (defined(parentCounts)) {
if (defined(parentCounts.byteOffset)) {
parentCounts.componentType =
parentCounts.componentType ?? ComponentDatatype.UNSIGNED_SHORT;
parentCounts.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(parentCounts);
parentCounts = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + parentCounts.byteOffset,
instancesLength,
);
byteLength += parentCounts.byteLength;
}
parentIndexes = new Uint16Array(instancesLength);
parentIdsLength = 0;
for (i = 0; i < instancesLength; ++i) {
parentIndexes[i] = parentIdsLength;
parentIdsLength += parentCounts[i];
}
byteLength += parentIndexes.byteLength;
}
if (defined(parentIds) && defined(parentIds.byteOffset)) {
parentIds.componentType =
parentIds.componentType ?? ComponentDatatype.UNSIGNED_SHORT;
parentIds.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(parentIds);
parentIds = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + parentIds.byteOffset,
parentIdsLength,
);
byteLength += parentIds.byteLength;
}
const classesLength = classes.length;
for (i = 0; i < classesLength; ++i) {
const classInstancesLength = classes[i].length;
const properties = classes[i].instances;
const binaryProperties = Cesium3DTileBatchTable.getBinaryProperties(
classInstancesLength,
properties,
binaryBody,
);
byteLength += countBinaryPropertyMemory(binaryProperties);
classes[i].instances = combine(binaryProperties, properties);
}
const classCounts = new Array(classesLength).fill(0);
const classIndexes = new Uint16Array(instancesLength);
for (i = 0; i < instancesLength; ++i) {
classId = classIds[i];
classIndexes[i] = classCounts[classId];
++classCounts[classId];
}
byteLength += classIndexes.byteLength;
hierarchy._classes = classes;
hierarchy._classIds = classIds;
hierarchy._classIndexes = classIndexes;
hierarchy._parentCounts = parentCounts;
hierarchy._parentIndexes = parentIndexes;
hierarchy._parentIds = parentIds;
hierarchy._byteLength = byteLength;
}
function countBinaryPropertyMemory(binaryProperties) {
let byteLength = 0;
for (const name in binaryProperties) {
if (binaryProperties.hasOwnProperty(name)) {
byteLength += binaryProperties[name].typedArray.byteLength;
}
}
return byteLength;
}
//>>includeStart('debug', pragmas.debug);
const scratchValidateStack = [];
function validateHierarchy(hierarchy) {
const stack = scratchValidateStack;
stack.length = 0;
const classIds = hierarchy._classIds;
const instancesLength = classIds.length;
for (let i = 0; i < instancesLength; ++i) {
validateInstance(hierarchy, i, stack);
}
}
function validateInstance(hierarchy, instanceIndex, stack) {
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
const parentIndexes = hierarchy._parentIndexes;
const classIds = hierarchy._classIds;
const instancesLength = classIds.length;
if (!defined(parentIds)) {
// No need to validate if there are no parents
return;
}
if (instanceIndex >= instancesLength) {
throw new DeveloperError(
`Parent index ${instanceIndex} exceeds the total number of instances: ${instancesLength}`,
);
}
if (stack.indexOf(instanceIndex) > -1) {
throw new DeveloperError(
"Circular dependency detected in the batch table hierarchy.",
);
}
stack.push(instanceIndex);
const parentCount = defined(parentCounts) ? parentCounts[instanceIndex] : 1;
const parentIndex = defined(parentCounts)
? parentIndexes[instanceIndex]
: instanceIndex;
for (let i = 0; i < parentCount; ++i) {
const parentId = parentIds[parentIndex + i];
// Stop the traversal when the instance has no parent (its parentId equals itself), else continue the traversal.
if (parentId !== instanceIndex) {
validateInstance(hierarchy, parentId, stack);
}
}
stack.pop(instanceIndex);
}
//>>includeEnd('debug');
// The size of this array equals the maximum instance count among all loaded tiles, which has the potential to be large.
const scratchVisited = [];
const scratchStack = [];
let marker = 0;
function traverseHierarchyMultipleParents(
hierarchy,
instanceIndex,
endConditionCallback,
) {
const classIds = hierarchy._classIds;
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
const parentIndexes = hierarchy._parentIndexes;
const instancesLength = classIds.length;
// Ignore instances that have already been visited. This occurs in diamond inheritance situations.
// Use a marker value to indicate that an instance has been visited, which increments with each run.
// This is more efficient than clearing the visited array every time.
const visited = scratchVisited;
visited.length = Math.max(visited.length, instancesLength);
const visitedMarker = ++marker;
const stack = scratchStack;
stack.length = 0;
stack.push(instanceIndex);
while (stack.length > 0) {
instanceIndex = stack.pop();
if (visited[instanceIndex] === visitedMarker) {
// This instance has already been visited, stop traversal
continue;
}
visited[instanceIndex] = visitedMarker;
const result = endConditionCallback(hierarchy, instanceIndex);
if (defined(result)) {
// The end condition was met, stop the traversal and return the result
return result;
}
const parentCount = parentCounts[instanceIndex];
const parentIndex = parentIndexes[instanceIndex];
for (let i = 0; i < parentCount; ++i) {
const parentId = parentIds[parentIndex + i];
// Stop the traversal when the instance has no parent (its parentId equals itself)
// else add the parent to the stack to continue the traversal.
if (parentId !== instanceIndex) {
stack.push(parentId);
}
}
}
}
function traverseHierarchySingleParent(
hierarchy,
instanceIndex,
endConditionCallback,
) {
let hasParent = true;
while (hasParent) {
const result = endConditionCallback(hierarchy, instanceIndex);
if (defined(result)) {
// The end condition was met, stop the traversal and return the result
return result;
}
const parentId = hierarchy._parentIds[instanceIndex];
hasParent = parentId !== instanceIndex;
instanceIndex = parentId;
}
}
function traverseHierarchy(hierarchy, instanceIndex, endConditionCallback) {
// Traverse over the hierarchy and process each instance with the endConditionCallback.
// When the endConditionCallback returns a value, the traversal stops and that value is returned.
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
if (!defined(parentIds)) {
return endConditionCallback(hierarchy, instanceIndex);
} else if (defined(parentCounts)) {
return traverseHierarchyMultipleParents(
hierarchy,
instanceIndex,
endConditionCallback,
);
}
return traverseHierarchySingleParent(
hierarchy,
instanceIndex,
endConditionCallback,
);
}
/**
* Returns whether the feature has this property.
*
* @param {number} batchId the batch ID of the feature
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {boolean} Whether the feature has this property.
* @private
*/
BatchTableHierarchy.prototype.hasProperty = function (batchId, propertyId) {
const result = traverseHierarchy(
this,
batchId,
function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instances = hierarchy._classes[classId].instances;
if (defined(instances[propertyId])) {
return true;
}
},
);
return defined(result);
};
/**
* Returns whether any feature has this property.
*
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {boolean} Whether any feature has this property.
* @private
*/
BatchTableHierarchy.prototype.propertyExists = function (propertyId) {
const classes = this._classes;
const classesLength = classes.length;
for (let i = 0; i < classesLength; ++i) {
const instances = classes[i].instances;
if (defined(instances[propertyId])) {
return true;
}
}
return false;
};
/**
* Returns an array of property IDs.
*
* @param {number} batchId the batch ID of the feature
* @param {number} index The index of the entity.
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The property IDs.
* @private
*/
BatchTableHierarchy.prototype.getPropertyIds = function (batchId, results) {
results = defined(results) ? results : [];
results.length = 0;
traverseHierarchy(this, batchId, function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instances = hierarchy._classes[classId].instances;
for (const name in instances) {
if (instances.hasOwnProperty(name)) {
if (results.indexOf(name) === -1) {
results.push(name);
}
}
}
});
return results;
};
/**
* Returns a copy of the value of the property with the given ID.
*
* @param {number} batchId the batch ID of the feature
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
*/
BatchTableHierarchy.prototype.getProperty = function (batchId, propertyId) {
return traverseHierarchy(this, batchId, function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
const indexInClass = hierarchy._classIndexes[instanceIndex];
const propertyValues = instanceClass.instances[propertyId];
if (defined(propertyValues)) {
if (defined(propertyValues.typedArray)) {
return getBinaryProperty(propertyValues, indexInClass);
}
return clone(propertyValues[indexInClass], true);
}
});
};
function getBinaryProperty(binaryProperty, index) {
const typedArray = binaryProperty.typedArray;
const componentCount = binaryProperty.componentCount;
if (componentCount === 1) {
return typedArray[index];
}
return binaryProperty.type.unpack(typedArray, index * componentCount);
}
/**
* Sets the value of the property with the given ID. Only properties of the
* instance may be set; parent properties may not be set.
*
* @param {number} batchId The batchId of the feature
* @param {string} propertyId The case-sensitive ID of the property.
* @param {*} value The value of the property that will be copied.
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise.
*
* @exception {DeveloperError} when setting an inherited property
* @private
*/
BatchTableHierarchy.prototype.setProperty = function (
batchId,
propertyId,
value,
) {
const result = traverseHierarchy(
this,
batchId,
function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
const indexInClass = hierarchy._classIndexes[instanceIndex];
const propertyValues = instanceClass.instances[propertyId];
if (defined(propertyValues)) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex !== batchId) {
throw new DeveloperError(
`Inherited property "${propertyId}" is read-only.`,
);
}
//>>includeEnd('debug');
if (defined(propertyValues.typedArray)) {
setBinaryProperty(propertyValues, indexInClass, value);
} else {
propertyValues[indexInClass] = clone(value, true);
}
return true;
}
},
);
return defined(result);
};
function setBinaryProperty(binaryProperty, index, value) {
const typedArray = binaryProperty.typedArray;
const componentCount = binaryProperty.componentCount;
if (componentCount === 1) {
typedArray[index] = value;
} else {
binaryProperty.type.pack(value, typedArray, index * componentCount);
}
}
/**
* Check if a feature belongs to a class with the given name
*
* @param {number} batchId The batch ID of the feature
* @param {string} className The name of the class
* @return {boolean} <code>true</code> if the feature belongs to the class given by className, or <code>false</code> otherwise
* @private
*/
BatchTableHierarchy.prototype.isClass = function (batchId, className) {
// PERFORMANCE_IDEA : cache results in the ancestor classes to speed up this check if this area becomes a hotspot
// PERFORMANCE_IDEA : treat class names as integers for faster comparisons
const result = traverseHierarchy(
this,
batchId,
function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
if (instanceClass.name === className) {
return true;
}
},
);
return defined(result);
};
/**
* Get the name of the class a given feature belongs to
*
* @param {number} batchId The batch ID of the feature
* @return {string} The name of the class this feature belongs to
*/
BatchTableHierarchy.prototype.getClassName = function (batchId) {
const classId = this._classIds[batchId];
const instanceClass = this._classes[classId];
return instanceClass.name;
};
export default BatchTableHierarchy;
+574
View File
@@ -0,0 +1,574 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import createGuid from "../Core/createGuid.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import PixelFormat from "../Core/PixelFormat.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
/**
* An object that manages color, show/hide and picking textures for a batch
* table or feature table.
*
* @param {object} options Object with the following properties:
* @param {number} featuresLength The number of features in the batch table or feature table
* @param {Cesium3DTileContent|ModelFeatureTable} owner The owner of this batch texture. For 3D Tiles, this will be a {@link Cesium3DTileContent}. For glTF models, this will be a {@link ModelFeatureTable}.
* @param {object} [statistics] The statistics object to update with information about the batch texture.
* @param {Function} [colorChangedCallback] A callback function that is called whenever the color of a feature changes.
*
* @alias BatchTexture
* @constructor
*
* @private
*/
function BatchTexture(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("options.featuresLength", options.featuresLength);
Check.typeOf.object("options.owner", options.owner);
//>>includeEnd('debug');
this._id = createGuid();
const featuresLength = options.featuresLength;
// PERFORMANCE_IDEA: These parallel arrays probably generate cache misses in get/set color/show
// and use A LOT of memory. How can we use less memory?
this._showAlphaProperties = undefined; // [Show (0 or 255), Alpha (0 to 255)] property for each feature
this._batchValues = undefined; // Per-feature RGBA (A is based on the color's alpha and feature's show property)
this._batchValuesDirty = false;
this._batchTexture = undefined;
this._defaultTexture = undefined;
this._pickTexture = undefined;
this._pickIds = [];
// Dimensions for batch and pick textures
let textureDimensions;
let textureStep;
if (featuresLength > 0) {
// PERFORMANCE_IDEA: this can waste memory in the last row in the uncommon case
// when more than one row is needed (e.g., > 16K features in one tile)
const width = Math.min(featuresLength, ContextLimits.maximumTextureSize);
const height = Math.ceil(featuresLength / ContextLimits.maximumTextureSize);
const stepX = 1.0 / width;
const centerX = stepX * 0.5;
const stepY = 1.0 / height;
const centerY = stepY * 0.5;
textureDimensions = new Cartesian2(width, height);
textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
}
this._translucentFeaturesLength = 0;
this._featuresLength = featuresLength;
this._textureDimensions = textureDimensions;
this._textureStep = textureStep;
this._owner = options.owner;
this._statistics = options.statistics;
this._colorChangedCallback = options.colorChangedCallback;
}
Object.defineProperties(BatchTexture.prototype, {
/**
* Number of features that are translucent
*
* @memberof BatchTexture.prototype
* @type {number}
* @readonly
* @private
*/
translucentFeaturesLength: {
get: function () {
return this._translucentFeaturesLength;
},
},
/**
* Total size of all GPU resources used by this batch texture.
*
* @memberof BatchTexture.prototype
* @type {number}
* @readonly
* @private
*/
byteLength: {
get: function () {
let memory = 0;
if (defined(this._pickTexture)) {
memory += this._pickTexture.sizeInBytes;
}
if (defined(this._batchTexture)) {
memory += this._batchTexture.sizeInBytes;
}
return memory;
},
},
/**
* Dimensions of the underlying batch texture.
*
* @memberof BatchTexture.prototype
* @type {Cartesian2}
* @readonly
* @private
*/
textureDimensions: {
get: function () {
return this._textureDimensions;
},
},
/**
* Size of each texture and distance from side to center of a texel in
* each direction. Stored as (stepX, centerX, stepY, centerY)
*
* @memberof BatchTexture.prototype
* @type {Cartesian4}
* @readonly
* @private
*/
textureStep: {
get: function () {
return this._textureStep;
},
},
/**
* The underlying texture used for styling. The texels are accessed
* by batch ID, and the value is the color of this feature after accounting
* for show/hide settings.
*
* @memberof BatchTexture.prototype
* @type {Texture}
* @readonly
* @private
*/
batchTexture: {
get: function () {
return this._batchTexture;
},
},
/**
* The default texture to use when there are no batch values
*
* @memberof BatchTexture.prototype
* @type {Texture}
* @readonly
* @private
*/
defaultTexture: {
get: function () {
return this._defaultTexture;
},
},
/**
* The underlying texture used for picking. The texels are accessed by
* batch ID, and the value is the pick color.
*
* @memberof BatchTexture.prototype
* @type {Texture}
* @readonly
* @private
*/
pickTexture: {
get: function () {
return this._pickTexture;
},
},
});
BatchTexture.DEFAULT_COLOR_VALUE = Color.WHITE;
BatchTexture.DEFAULT_SHOW_VALUE = true;
function getByteLength(batchTexture) {
const dimensions = batchTexture._textureDimensions;
return dimensions.x * dimensions.y * 4;
}
function getBatchValues(batchTexture) {
if (!defined(batchTexture._batchValues)) {
// Default batch texture to RGBA = 255: white highlight (RGB) and show/alpha = true/255 (A).
const byteLength = getByteLength(batchTexture);
const bytes = new Uint8Array(byteLength).fill(255);
batchTexture._batchValues = bytes;
}
return batchTexture._batchValues;
}
function getShowAlphaProperties(batchTexture) {
if (!defined(batchTexture._showAlphaProperties)) {
const byteLength = 2 * batchTexture._featuresLength;
const bytes = new Uint8Array(byteLength).fill(255);
// [Show = true, Alpha = 255]
batchTexture._showAlphaProperties = bytes;
}
return batchTexture._showAlphaProperties;
}
function checkBatchId(batchId, featuresLength) {
if (!defined(batchId) || batchId < 0 || batchId >= featuresLength) {
throw new DeveloperError(
`batchId is required and between zero and featuresLength - 1 (${featuresLength}` -
+").",
);
}
}
/**
* Set whether a feature is visible.
*
* @param {number} batchId the ID of the feature
* @param {boolean} show <code>true</code> if the feature should be shown, <code>false</code> otherwise
* @private
*/
BatchTexture.prototype.setShow = function (batchId, show) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
Check.typeOf.bool("show", show);
//>>includeEnd('debug');
if (show && !defined(this._showAlphaProperties)) {
// Avoid allocating since the default is show = true
return;
}
const showAlphaProperties = getShowAlphaProperties(this);
const propertyOffset = batchId * 2;
const newShow = show ? 255 : 0;
if (showAlphaProperties[propertyOffset] !== newShow) {
showAlphaProperties[propertyOffset] = newShow;
const batchValues = getBatchValues(this);
// Compute alpha used in the shader based on show and color.alpha properties
const offset = batchId * 4 + 3;
batchValues[offset] = show ? showAlphaProperties[propertyOffset + 1] : 0;
this._batchValuesDirty = true;
}
};
/**
* Set the show for all features at once.
*
* @param {boolean} show <code>true</code> if the feature should be shown, <code>false</code> otherwise
* @private
*/
BatchTexture.prototype.setAllShow = function (show) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.bool("show", show);
//>>includeEnd('debug');
const featuresLength = this._featuresLength;
for (let i = 0; i < featuresLength; ++i) {
this.setShow(i, show);
}
};
/**
* Check the current show value for a feature
*
* @param {number} batchId the ID of the feature
* @return {boolean} <code>true</code> if the feature is shown, or <code>false</code> otherwise
* @private
*/
BatchTexture.prototype.getShow = function (batchId) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
//>>includeEnd('debug');
if (!defined(this._showAlphaProperties)) {
// Avoid allocating since the default is show = true
return true;
}
const offset = batchId * 2;
return this._showAlphaProperties[offset] === 255;
};
const scratchColorBytes = new Array(4);
/**
* Set the styling color of a feature
*
* @param {number} batchId the ID of the feature
* @param {Color} color the color to assign to this feature.
*
* @private
*/
BatchTexture.prototype.setColor = function (batchId, color) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
Check.typeOf.object("color", color);
//>>includeEnd('debug');
if (
Color.equals(color, BatchTexture.DEFAULT_COLOR_VALUE) &&
!defined(this._batchValues)
) {
// Avoid allocating since the default is white
return;
}
const newColor = color.toBytes(scratchColorBytes);
const newAlpha = newColor[3];
const batchValues = getBatchValues(this);
const offset = batchId * 4;
const showAlphaProperties = getShowAlphaProperties(this);
const propertyOffset = batchId * 2;
if (
batchValues[offset] !== newColor[0] ||
batchValues[offset + 1] !== newColor[1] ||
batchValues[offset + 2] !== newColor[2] ||
showAlphaProperties[propertyOffset + 1] !== newAlpha
) {
batchValues[offset] = newColor[0];
batchValues[offset + 1] = newColor[1];
batchValues[offset + 2] = newColor[2];
const wasTranslucent = showAlphaProperties[propertyOffset + 1] !== 255;
// Compute alpha used in the shader based on show and color.alpha properties
const show = showAlphaProperties[propertyOffset] !== 0;
batchValues[offset + 3] = show ? newAlpha : 0;
showAlphaProperties[propertyOffset + 1] = newAlpha;
// Track number of translucent features so we know if this tile needs
// opaque commands, translucent commands, or both for rendering.
const isTranslucent = newAlpha !== 255;
if (isTranslucent && !wasTranslucent) {
++this._translucentFeaturesLength;
} else if (!isTranslucent && wasTranslucent) {
--this._translucentFeaturesLength;
}
this._batchValuesDirty = true;
if (defined(this._colorChangedCallback)) {
this._colorChangedCallback(batchId, color);
}
}
};
/**
* Set the styling color for all features at once
*
* @param {Color} color the color to assign to all features.
*
* @private
*/
BatchTexture.prototype.setAllColor = function (color) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("color", color);
//>>includeEnd('debug');
const featuresLength = this._featuresLength;
for (let i = 0; i < featuresLength; ++i) {
this.setColor(i, color);
}
};
/**
* Get the current color of a feature
*
* @param {number} batchId The ID of the feature
* @param {Color} result A color object where the result will be stored.
* @return {Color} The color assigned to the selected feature
*
* @private
*/
BatchTexture.prototype.getColor = function (batchId, result) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
if (!defined(this._batchValues)) {
return Color.clone(BatchTexture.DEFAULT_COLOR_VALUE, result);
}
const batchValues = this._batchValues;
const offset = batchId * 4;
const showAlphaProperties = this._showAlphaProperties;
const propertyOffset = batchId * 2;
return Color.fromBytes(
batchValues[offset],
batchValues[offset + 1],
batchValues[offset + 2],
showAlphaProperties[propertyOffset + 1],
result,
);
};
/**
* Get the pick color of a feature. This feature is an RGBA encoding of the
* pick ID.
*
* @param {number} batchId The ID of the feature
* @return {PickId} The picking color assigned to this feature
*
* @private
*/
BatchTexture.prototype.getPickColor = function (batchId) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
//>>includeEnd('debug');
return this._pickIds[batchId];
};
function createTexture(batchTexture, context, bytes) {
const dimensions = batchTexture._textureDimensions;
return new Texture({
context: context,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
source: {
width: dimensions.x,
height: dimensions.y,
arrayBufferView: bytes,
},
flipY: false,
sampler: Sampler.NEAREST,
});
}
function createPickTexture(batchTexture, context) {
const featuresLength = batchTexture._featuresLength;
if (!defined(batchTexture._pickTexture) && featuresLength > 0) {
const pickIds = batchTexture._pickIds;
const byteLength = getByteLength(batchTexture);
const bytes = new Uint8Array(byteLength);
const owner = batchTexture._owner;
const statistics = batchTexture._statistics;
// PERFORMANCE_IDEA: we could skip the pick texture completely by allocating
// a continuous range of pickIds and then converting the base pickId + batchId
// to RGBA in the shader. The only consider is precision issues, which might
// not be an issue in WebGL 2.
for (let i = 0; i < featuresLength; ++i) {
const pickId = context.createPickId(owner.getFeature(i));
pickIds.push(pickId);
const pickColor = pickId.color;
const offset = i * 4;
bytes[offset] = Color.floatToByte(pickColor.red);
bytes[offset + 1] = Color.floatToByte(pickColor.green);
bytes[offset + 2] = Color.floatToByte(pickColor.blue);
bytes[offset + 3] = Color.floatToByte(pickColor.alpha);
}
batchTexture._pickTexture = createTexture(batchTexture, context, bytes);
// Make sure the tileset statistics are updated the frame when the pick
// texture is created.
if (defined(statistics)) {
statistics.batchTableByteLength += batchTexture._pickTexture.sizeInBytes;
}
}
}
function updateBatchTexture(batchTexture) {
const dimensions = batchTexture._textureDimensions;
// PERFORMANCE_IDEA: Instead of rewriting the entire texture, use fine-grained
// texture updates when less than, for example, 10%, of the values changed. Or
// even just optimize the common case when one feature show/color changed.
batchTexture._batchTexture.copyFrom({
source: {
width: dimensions.x,
height: dimensions.y,
arrayBufferView: batchTexture._batchValues,
},
});
}
BatchTexture.prototype.update = function (tileset, frameState) {
const context = frameState.context;
this._defaultTexture = context.defaultTexture;
const passes = frameState.passes;
if (passes.pick || passes.postProcess) {
createPickTexture(this, context);
}
if (this._batchValuesDirty) {
this._batchValuesDirty = false;
// Create batch texture on-demand
if (!defined(this._batchTexture)) {
this._batchTexture = createTexture(this, context, this._batchValues);
// Make sure the tileset statistics are updated the frame when the
// batch texture is created.
if (defined(this._statistics)) {
this._statistics.batchTableByteLength += this._batchTexture.sizeInBytes;
}
}
updateBatchTexture(this); // Apply per-feature show/color updates
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <p>
* 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.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see BatchTexture#destroy
* @private
*/
BatchTexture.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.
* <p>
* 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.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* e = e && e.destroy();
*
* @see BatchTexture#isDestroyed
* @private
*/
BatchTexture.prototype.destroy = function () {
this._batchTexture = this._batchTexture && this._batchTexture.destroy();
this._pickTexture = this._pickTexture && this._pickTexture.destroy();
const pickIds = this._pickIds;
const length = pickIds.length;
for (let i = 0; i < length; ++i) {
pickIds[i].destroy();
}
return destroyObject(this);
};
export default BatchTexture;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
/**
* Defines loading state of a image as any request is resolved and the WebGL resources are updated.
* @private
* @enum {number}
*/
const BillboardLoadState = Object.freeze({
/**
* There is no image data to load.
* @private
* @type {number}
* @constant
*/
NONE: 0,
/**
* Image data is in the process of downloading, or WebGL resources are being updated.
* @private
* @type {number}
* @constant
*/
LOADING: 2,
/**
* The image data has been downloaded and the WebGL resources have been created. It is ready for rendering.
* @private
* @type {number}
* @constant
*/
LOADED: 3,
/**
* There was an error while downloading an image or updating the WebGL resources.
* @private
* @type {number}
* @constant
*/
ERROR: 4,
/**
* Updating the WebGL resources failed, due to the resource being destroyed or another error.
* @private
* @type {number}
* @constant
*/
FAILED: 5,
});
export default BillboardLoadState;
+400
View File
@@ -0,0 +1,400 @@
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import BillboardLoadState from "./BillboardLoadState.js";
/**
* Tracks a reference to an image and it's loading state, as used in a BillboardCollection and stored in a texture atlas.
* @constructor
* @private
* @see BillboardCollection
* @see Billboard#image
* @alias BillboardTexture
* @param {BillboardCollection} billboardCollection The associated billboard collecion.
*/
function BillboardTexture(billboardCollection) {
//>>includeStart('debug', pragmas.debug);
Check.defined("billboardCollection", billboardCollection);
//>>includeEnd('debug');
this._billboardCollection = billboardCollection;
this._id = undefined;
this._loadState = BillboardLoadState.NONE;
this._loadError = undefined;
this._index = -1;
this._width = undefined;
this._height = undefined;
this._hasSubregion = false;
/**
* Used by billboardCollection to track whcih billboards to update.
* @type {boolean}
* @private
*/
this.dirty = false;
}
Object.defineProperties(BillboardTexture.prototype, {
/**
* If defined, this error was encountered during the loading process.
* @memberof BillboardTexture.prototype
* @type {Error|undefined}
* @readonly
* @private
*/
loadError: {
get: function () {
return this._loadError;
},
},
/**
* The current status of the image load. When <code>BillboardLoadState.LOADED</code>, this billboard is ready to render, i.e., the image
* has been downloaded and the WebGL resources are created.
* @memberof BillboardTexture.prototype
* @type {BillboardLoadState}
* @readonly
* @default BillboardLoadState.NONE
* @private
*/
loadState: {
get: function () {
return this._loadState;
},
},
/**
* When <code>true</code>, this texture is ready to render, i.e., the image
* has been downloaded and the WebGL resources are created.
* @memberof BillboardTexture.prototype
* @type {boolean}
* @readonly
* @default false
* @private
*/
ready: {
get: function () {
return this._loadState === BillboardLoadState.LOADED;
},
},
/**
* Returns <code>true</code> if there is image data associated with this instance.
* @memberof BillboardTexture.prototype
* @type {boolean}
* @readonly
* @private
*/
hasImage: {
get: function () {
return this._loadState !== BillboardLoadState.NONE;
},
},
/**
* A unique identifier for the image, or undefined if no image data has been associated with this instance.
* @memberof BillboardTexture.prototype
* @type {string|undefined}
* @readonly
* @private
*/
id: {
get: function () {
return this._id;
},
},
/**
* The width of the associated image. Before the instance is <code>ready</code>, this will be <code>undefined</code>.
* @memberof BillboardTexture.prototype
* @type {number|undefined}
* @readonly
* @private
*/
width: {
get: function () {
return this._width;
},
},
/**
* The height of the associated image. Before the instance is <code>ready</code>, this will be <code>undefined</code>.
* @memberof BillboardTexture.prototype
* @type {number|undefined}
* @readonly
* @private
*/
height: {
get: function () {
return this._height;
},
},
});
/**
* Releases reference to any associated image data.
* @private
*/
BillboardTexture.prototype.unload = async function () {
if (this._loadState === BillboardLoadState.NONE) {
return;
}
this._id = undefined;
this._loadError = undefined;
this._loadState = BillboardLoadState.NONE;
this._index = -1;
this._width = undefined;
this._height = undefined;
this.dirty = true;
};
/**
* Starts loading an image into the texture atlas.
* @see {TextureAtlas#addImage}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas,
* or a URL to an Image, or a Promise for an image, or a function that creates an image.
* @param {number} width A number specifying the width of the texture. If undefined, the image width will be used.
* @param {number} height A number specifying the height of the texture. If undefined, the image height will be used.
*/
BillboardTexture.prototype.loadImage = async function (
id,
image,
width,
height,
) {
if (this._id === id) {
// This image has already been loaded
return;
}
const collection = this._billboardCollection;
const cache = collection.billboardTextureCache;
let billboardTexture = cache.get(id);
if (
(defined(billboardTexture) &&
image.loadState === BillboardLoadState.LOADING) ||
image.loadState === BillboardLoadState.LOADED
) {
// Use the cached texture if it is in progress or successful.
BillboardTexture.clone(billboardTexture, this);
return;
}
// Otherwise, load if not yet assigned an image, and try the load again if anything failed during the last billboard creation
if (!defined(billboardTexture)) {
billboardTexture = new BillboardTexture(collection);
cache.set(id, billboardTexture);
}
billboardTexture._id = this._id = id;
billboardTexture._loadState = this._loadState = BillboardLoadState.LOADING;
billboardTexture._loadError = this._loadError = undefined;
let index;
const atlas = this._billboardCollection.textureAtlas;
try {
index = atlas.addImage(id, image, width, height);
if (index instanceof Promise) {
index = await index;
}
} catch (error) {
// There was an error loading the image
billboardTexture._loadState = BillboardLoadState.ERROR;
billboardTexture._loadError = error;
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._loadState = BillboardLoadState.ERROR;
this._loadError = error;
return;
}
if (!defined(index) || index === -1) {
// Resources destroyed or otherwise
billboardTexture._loadState = BillboardLoadState.FAILED;
billboardTexture._index = -1;
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._loadState = BillboardLoadState.FAILED;
this._index = -1;
return;
}
billboardTexture._index = index;
billboardTexture._loadState = BillboardLoadState.LOADED;
const rectangle = atlas.rectangles[index];
billboardTexture._width = rectangle.width;
billboardTexture._height = rectangle.height;
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._index = index;
this._loadState = BillboardLoadState.LOADED;
this._width = rectangle.width;
this._height = rectangle.height;
this.dirty = true;
};
/**
* Track a reference to a sub-region of an existing image.
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
*/
BillboardTexture.prototype.addImageSubRegion = function (id, subRegion) {
this._id = id;
this._loadError = undefined;
this._hasSubregion = true;
const atlas = this._billboardCollection.textureAtlas;
const indexOrPromise = atlas.addImageSubRegion(id, subRegion);
if (typeof indexOrPromise === "number") {
this.setImageSubRegion(indexOrPromise, subRegion);
return;
}
this.loadImageSubRegion(id, subRegion, indexOrPromise);
};
/**
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
* @param {Promise<number>} indexPromise A promise that resolves to the image region index.
*/
BillboardTexture.prototype.loadImageSubRegion = async function (
id,
subRegion,
indexPromise,
) {
let index;
try {
this._loadState = BillboardLoadState.LOADING;
index = await indexPromise;
} catch (error) {
// There was an error loading the referenced image
this._loadState = BillboardLoadState.ERROR;
this._loadError = error;
return;
}
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._loadState = BillboardLoadState.LOADED;
this.setImageSubRegion(index, subRegion);
};
/**
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {number} index The resolved index in the {@link TextureAtlas}
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
*/
BillboardTexture.prototype.setImageSubRegion = function (index, subRegion) {
if (this._index === index) {
return;
}
if (!defined(index) || index === -1) {
this._loadState = BillboardLoadState.FAILED;
this._index = -1;
this._width = undefined;
this._height = undefined;
return;
}
this._width = subRegion.width;
this._height = subRegion.height;
this._index = index;
this.dirty = true;
};
/**
* Get the texture coordinates for reading the loaded texture in shaders.
* @private
* @param {BoundingRectangle} [result] The modified result parameter or a new BoundingRectangle instance if one was not provided.
* @return {BoundingRectangle} The modified result parameter or a new BoundingRectangle instance if one was not provided.
*/
BillboardTexture.prototype.computeTextureCoordinates = function (result) {
const atlas = this._billboardCollection.textureAtlas;
return atlas.computeTextureCoordinates(this._index, result);
};
/**
* Clones an existing billboard texture, inlcuding any in-flight tracking, into the target billboard texture.
* @param {BillboardTexture} billboardTexture
* @param {BillboardTexture} target
* @returns {BillboardTexture} target
*/
BillboardTexture.clone = function (billboardTexture, target) {
target._id = billboardTexture._id;
target._loadState = billboardTexture._loadState;
target._loadError = undefined;
target._index = billboardTexture._index;
target._width = billboardTexture._width;
target._height = billboardTexture._height;
target._hasSubregion = billboardTexture._hasSubregion;
if (billboardTexture.ready) {
target.dirty = true;
return;
}
const completeLoad = async () => {
const id = billboardTexture._id;
const atlas = billboardTexture._billboardCollection.textureAtlas;
await atlas._indexPromiseById.get(id);
// Any errors should have already been handled
if (target._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
if (billboardTexture._hasSubregion) {
// Subregions must wait an additional frame to be ready
await Promise.resolve();
}
target._id = id;
target._loadState = billboardTexture._loadState;
target._loadError = billboardTexture._loadError;
target._index = billboardTexture._index;
target._width = billboardTexture._width;
target._height = billboardTexture._height;
target.dirty = true;
};
completeLoad();
return target;
};
export default BillboardTexture;
@@ -0,0 +1,769 @@
import buildModuleUrl from "../Core/buildModuleUrl.js";
import Check from "../Core/Check.js";
import Credit from "../Core/Credit.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Event from "../Core/Event.js";
import CesiumMath from "../Core/Math.js";
import Rectangle from "../Core/Rectangle.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import TileProviderError from "../Core/TileProviderError.js";
import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
import BingMapsStyle from "./BingMapsStyle.js";
import DiscardEmptyTilePolicy from "./DiscardEmptyTileImagePolicy.js";
import ImageryProvider from "./ImageryProvider.js";
/**
* @typedef {object} BingMapsImageryProvider.ConstructorOptions
*
* Initialization options for the BingMapsImageryProvider constructor
*
* @property {string} [key] The Bing Maps key for your application, which can be
* created at {@link https://www.bingmapsportal.com/}.
* @property {string} [tileProtocol] The protocol to use when loading tiles, e.g. 'http' or 'https'.
* By default, tiles are loaded using the same protocol as the page.
* @property {BingMapsStyle} [mapStyle=BingMapsStyle.AERIAL] The type of Bing Maps imagery to load.
* @property {string} [mapLayer] Additional display layer options as defined on {@link https://learn.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#template-parameters}
* @property {string} [culture=''] The culture to use when requesting Bing Maps imagery. Not
* all cultures are supported. See {@link http://msdn.microsoft.com/en-us/library/hh441729.aspx}
* for information on the supported cultures.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @property {TileDiscardPolicy} [tileDiscardPolicy] The policy that determines if a tile
* is invalid and should be discarded. By default, a {@link DiscardEmptyTileImagePolicy}
* will be used, with the expectation that the Bing Maps server will send a zero-length response for missing tiles.
* To ensure that no tiles are discarded, construct and pass a {@link NeverTileDiscardPolicy} for this parameter.
*/
/**
* Used to track creation details while fetching initial metadata
*
* @constructor
* @private
*
* @param {BingMapsImageryProvider.ConstructorOptions} options An object describing initialization options
*/
function ImageryProviderBuilder(options) {
this.tileWidth = undefined;
this.tileHeight = undefined;
this.maximumLevel = undefined;
this.imageUrlSubdomains = undefined;
this.imageUrlTemplate = undefined;
this.attributionList = undefined;
}
/**
* Complete BingMapsImageryProvider creation based on builder values.
*
* @private
*
* @param {BingMapsImageryProvider} provider
*/
ImageryProviderBuilder.prototype.build = function (provider) {
provider._tileWidth = this.tileWidth;
provider._tileHeight = this.tileHeight;
provider._maximumLevel = this.maximumLevel;
provider._imageUrlSubdomains = this.imageUrlSubdomains;
provider._imageUrlTemplate = this.imageUrlTemplate;
let attributionList = (provider._attributionList = this.attributionList);
if (!attributionList) {
attributionList = [];
}
provider._attributionList = attributionList;
for (
let attributionIndex = 0, attributionLength = attributionList.length;
attributionIndex < attributionLength;
++attributionIndex
) {
const attribution = attributionList[attributionIndex];
if (attribution.credit instanceof Credit) {
// If attribution.credit has already been created
// then we are using a cached value, which means
// none of the remaining processing needs to be done.
break;
}
attribution.credit = new Credit(attribution.attribution);
const coverageAreas = attribution.coverageAreas;
for (
let areaIndex = 0, areaLength = attribution.coverageAreas.length;
areaIndex < areaLength;
++areaIndex
) {
const area = coverageAreas[areaIndex];
const bbox = area.bbox;
area.bbox = new Rectangle(
CesiumMath.toRadians(bbox[1]),
CesiumMath.toRadians(bbox[0]),
CesiumMath.toRadians(bbox[3]),
CesiumMath.toRadians(bbox[2]),
);
}
}
};
function metadataSuccess(data, imageryProviderBuilder) {
if (data.resourceSets.length !== 1) {
throw new RuntimeError(
"metadata does not specify one resource in resourceSets",
);
}
const resource = data.resourceSets[0].resources[0];
imageryProviderBuilder.tileWidth = resource.imageWidth;
imageryProviderBuilder.tileHeight = resource.imageHeight;
imageryProviderBuilder.maximumLevel = resource.zoomMax - 1;
imageryProviderBuilder.imageUrlSubdomains = resource.imageUrlSubdomains;
imageryProviderBuilder.imageUrlTemplate = resource.imageUrl;
let validProviders = resource.imageryProviders;
if (defined(resource.imageryProviders)) {
// prevent issues with the imagery API from crashing the viewer when the expected properties are not there
// See https://github.com/CesiumGS/cesium/issues/12088
validProviders = resource.imageryProviders.filter((provider) =>
provider.coverageAreas?.some((area) => defined(area.bbox)),
);
}
imageryProviderBuilder.attributionList = validProviders;
}
function metadataFailure(metadataResource, error, provider) {
let message = `An error occurred while accessing ${metadataResource.url}`;
if (defined(error) && defined(error.message)) {
message += `: ${error.message}`;
}
TileProviderError.reportError(
undefined,
provider,
defined(provider) ? provider._errorEvent : undefined,
message,
undefined,
undefined,
undefined,
error,
);
throw new RuntimeError(message);
}
async function requestMetadata(
metadataResource,
imageryProviderBuilder,
provider,
) {
const cacheKey = metadataResource.url;
let promise = BingMapsImageryProvider._metadataCache[cacheKey];
if (!defined(promise)) {
promise = metadataResource.fetchJson();
BingMapsImageryProvider._metadataCache[cacheKey] = promise;
}
try {
const data = await promise;
return metadataSuccess(data, imageryProviderBuilder);
} catch (e) {
metadataFailure(metadataResource, e, provider);
}
}
/**
* <div class="notice">
* To construct a BingMapsImageryProvider, call {@link BingMapsImageryProvider.fromUrl}. Do not call the constructor directly.
* </div>
*
* Provides tiled imagery using the Bing Maps Imagery REST API.
*
* @alias BingMapsImageryProvider
* @constructor
*
* @param {BingMapsImageryProvider.ConstructorOptions} options Object describing initialization options
*
* @see BingMapsImageryProvider.fromUrl
* @see ArcGisMapServerImageryProvider
* @see GoogleEarthEnterpriseMapsProvider
* @see OpenStreetMapImageryProvider
* @see SingleTileImageryProvider
* @see TileMapServiceImageryProvider
* @see WebMapServiceImageryProvider
* @see WebMapTileServiceImageryProvider
* @see UrlTemplateImageryProvider
*
* @example
* const bing = await Cesium.BingMapsImageryProvider.fromUrl(
* "https://dev.virtualearth.net", {
* key: "get-yours-at-https://www.bingmapsportal.com/",
* mapStyle: Cesium.BingMapsStyle.AERIAL
* });
*
* @see {@link http://msdn.microsoft.com/en-us/library/ff701713.aspx|Bing Maps REST Services}
* @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing}
*/
function BingMapsImageryProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._defaultAlpha = undefined;
this._defaultNightAlpha = undefined;
this._defaultDayAlpha = undefined;
this._defaultBrightness = undefined;
this._defaultContrast = undefined;
this._defaultHue = undefined;
this._defaultSaturation = undefined;
this._defaultGamma = 1.0;
this._defaultMinificationFilter = undefined;
this._defaultMagnificationFilter = undefined;
this._mapStyle = options.mapStyle ?? BingMapsStyle.AERIAL;
this._mapLayer = options.mapLayer;
this._culture = options.culture ?? "";
this._key = options.key;
this._tileDiscardPolicy = options.tileDiscardPolicy;
if (!defined(this._tileDiscardPolicy)) {
this._tileDiscardPolicy = new DiscardEmptyTilePolicy();
}
this._proxy = options.proxy;
this._credit = new Credit(
`<a href="https://www.microsoft.com/en-us/maps/bing-maps/product"><img src="${BingMapsImageryProvider.logoUrl}" title="Bing Imagery"/></a>`,
);
this._tilingScheme = new WebMercatorTilingScheme({
numberOfLevelZeroTilesX: 2,
numberOfLevelZeroTilesY: 2,
ellipsoid: options.ellipsoid,
});
this._tileWidth = undefined;
this._tileHeight = undefined;
this._maximumLevel = undefined;
this._imageUrlTemplate = undefined;
this._imageUrlSubdomains = undefined;
this._attributionList = undefined;
this._errorEvent = new Event();
}
Object.defineProperties(BingMapsImageryProvider.prototype, {
/**
* Gets the name of the BingMaps server url hosting the imagery.
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._resource.url;
},
},
/**
* Gets the proxy used by this provider.
* @memberof BingMapsImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._resource.proxy;
},
},
/**
* Gets the Bing Maps key.
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
key: {
get: function () {
return this._key;
},
},
/**
* Gets the type of Bing Maps imagery to load.
* @memberof BingMapsImageryProvider.prototype
* @type {BingMapsStyle}
* @readonly
*/
mapStyle: {
get: function () {
return this._mapStyle;
},
},
/**
* Gets the additional map layer options as defined in {@link https://learn.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#template-parameters}/
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
mapLayer: {
get: function () {
return this._mapLayer;
},
},
/**
* The culture to use when requesting Bing Maps imagery. Not
* all cultures are supported. See {@link http://msdn.microsoft.com/en-us/library/hh441729.aspx}
* for information on the supported cultures.
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
culture: {
get: function () {
return this._culture;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof BingMapsImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof BingMapsImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof BingMapsImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested.
* @memberof BingMapsImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return 0;
},
},
/**
* Gets the tiling scheme used by this provider.
* @memberof BingMapsImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._tilingScheme;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by this instance.
* @memberof BingMapsImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._tilingScheme.rectangle;
},
},
/**
* 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 BingMapsImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._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 BingMapsImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof BingMapsImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._credit;
},
},
/**
* 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. Setting this property to false reduces memory usage
* and texture upload time.
* @memberof BingMapsImageryProvider.prototype
* @type {boolean}
* @readonly
*/
hasAlphaChannel: {
get: function () {
return defined(this.mapLayer);
},
},
});
/**
* Creates an {@link ImageryProvider} which provides tiled imagery using the Bing Maps Imagery REST API.
*
* @param {Resource|string} url The url of the Bing Maps server hosting the imagery.
* @param {BingMapsImageryProvider.ConstructorOptions} options Object describing initialization options
* @returns {Promise<BingMapsImageryProvider>} A promise that resolves to the created BingMapsImageryProvider
*
* @example
* const bing = await Cesium.BingMapsImageryProvider.fromUrl(
* "https://dev.virtualearth.net", {
* key: "get-yours-at-https://www.bingmapsportal.com/",
* mapStyle: Cesium.BingMapsStyle.AERIAL
* });
*
* @exception {RuntimeError} metadata does not specify one resource in resourceSets
*/
BingMapsImageryProvider.fromUrl = async function (url, options) {
options = options ?? Frozen.EMPTY_OBJECT;
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
Check.defined("options.key", options.key);
//>>includeEnd('debug');
let tileProtocol = options.tileProtocol;
// For backward compatibility reasons, the tileProtocol may end with
// a `:`. Remove it.
if (defined(tileProtocol)) {
if (
tileProtocol.length > 0 &&
tileProtocol[tileProtocol.length - 1] === ":"
) {
tileProtocol = tileProtocol.substr(0, tileProtocol.length - 1);
}
} else {
// use http if the document's protocol is http, otherwise use https
const documentProtocol = document.location.protocol;
tileProtocol = documentProtocol === "http:" ? "http" : "https";
}
const mapStyle = options.mapStyle ?? BingMapsStyle.AERIAL;
const resource = Resource.createIfNeeded(url);
resource.appendForwardSlash();
const queryParameters = {
incl: "ImageryProviders",
key: options.key,
uriScheme: tileProtocol,
};
if (defined(options.mapLayer)) {
queryParameters.mapLayer = options.mapLayer;
}
if (defined(options.culture)) {
queryParameters.culture = options.culture;
}
const metadataResource = resource.getDerivedResource({
url: `REST/v1/Imagery/Metadata/${mapStyle}`,
queryParameters: queryParameters,
});
const provider = new BingMapsImageryProvider(options);
provider._resource = resource;
const imageryProviderBuilder = new ImageryProviderBuilder(options);
await requestMetadata(metadataResource, imageryProviderBuilder);
imageryProviderBuilder.build(provider);
return provider;
};
const rectangleScratch = new Rectangle();
/**
* 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[]} The credits to be displayed when the tile is displayed.
*/
BingMapsImageryProvider.prototype.getTileCredits = function (x, y, level) {
const rectangle = this._tilingScheme.tileXYToRectangle(
x,
y,
level,
rectangleScratch,
);
const result = getRectangleAttribution(
this._attributionList,
level,
rectangle,
);
return result;
};
/**
* 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.
*/
BingMapsImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
const promise = ImageryProvider.loadImage(
this,
buildImageResource(this, x, y, level, request),
);
if (defined(promise)) {
return promise.catch(function (error) {
// One cause of an error here is that the image we tried to load was zero-length.
// This isn't actually a problem, since it indicates that there is no tile.
// So, in that case we return the EMPTY_IMAGE sentinel value for later discarding.
if (defined(error.blob) && error.blob.size === 0) {
return DiscardEmptyTilePolicy.EMPTY_IMAGE;
}
return Promise.reject(error);
});
}
return undefined;
};
/**
* 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.
*/
BingMapsImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
return undefined;
};
/**
* Converts a tiles (x, y, level) position into a quadkey used to request an image
* from a Bing Maps server.
*
* @param {number} x The tile's x coordinate.
* @param {number} y The tile's y coordinate.
* @param {number} level The tile's zoom level.
*
* @see {@link http://msdn.microsoft.com/en-us/library/bb259689.aspx|Bing Maps Tile System}
* @see BingMapsImageryProvider#quadKeyToTileXY
*/
BingMapsImageryProvider.tileXYToQuadKey = function (x, y, level) {
let quadkey = "";
for (let i = level; i >= 0; --i) {
const bitmask = 1 << i;
let digit = 0;
if ((x & bitmask) !== 0) {
digit |= 1;
}
if ((y & bitmask) !== 0) {
digit |= 2;
}
quadkey += digit;
}
return quadkey;
};
/**
* Converts a tile's quadkey used to request an image from a Bing Maps server into the
* (x, y, level) position.
*
* @param {string} quadkey The tile's quad key
*
* @see {@link http://msdn.microsoft.com/en-us/library/bb259689.aspx|Bing Maps Tile System}
* @see BingMapsImageryProvider#tileXYToQuadKey
*/
BingMapsImageryProvider.quadKeyToTileXY = function (quadkey) {
let x = 0;
let y = 0;
const level = quadkey.length - 1;
for (let i = level; i >= 0; --i) {
const bitmask = 1 << i;
const digit = +quadkey[level - i];
if ((digit & 1) !== 0) {
x |= bitmask;
}
if ((digit & 2) !== 0) {
y |= bitmask;
}
}
return {
x: x,
y: y,
level: level,
};
};
BingMapsImageryProvider._logoUrl = undefined;
Object.defineProperties(BingMapsImageryProvider, {
/**
* Gets or sets the URL to the Bing logo for display in the credit.
* @memberof BingMapsImageryProvider
* @type {string}
*/
logoUrl: {
get: function () {
if (!defined(BingMapsImageryProvider._logoUrl)) {
BingMapsImageryProvider._logoUrl = buildModuleUrl(
"Assets/Images/bing_maps_credit.png",
);
}
return BingMapsImageryProvider._logoUrl;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.defined("value", value);
//>>includeEnd('debug');
BingMapsImageryProvider._logoUrl = value;
},
},
});
function buildImageResource(imageryProvider, x, y, level, request) {
const imageUrl = imageryProvider._imageUrlTemplate;
const subdomains = imageryProvider._imageUrlSubdomains;
const subdomainIndex = (x + y + level) % subdomains.length;
return imageryProvider._resource.getDerivedResource({
url: imageUrl,
request: request,
templateValues: {
quadkey: BingMapsImageryProvider.tileXYToQuadKey(x, y, level),
subdomain: subdomains[subdomainIndex],
culture: imageryProvider._culture,
},
queryParameters: {
// this parameter tells the Bing servers to send a zero-length response
// instead of a placeholder image for missing tiles.
n: "z",
},
});
}
const intersectionScratch = new Rectangle();
function getRectangleAttribution(attributionList, level, rectangle) {
// Bing levels start at 1, while ours start at 0.
++level;
const result = [];
for (
let attributionIndex = 0, attributionLength = attributionList.length;
attributionIndex < attributionLength;
++attributionIndex
) {
const attribution = attributionList[attributionIndex];
const coverageAreas = attribution.coverageAreas;
let included = false;
for (
let areaIndex = 0, areaLength = attribution.coverageAreas.length;
!included && areaIndex < areaLength;
++areaIndex
) {
const area = coverageAreas[areaIndex];
if (level >= area.zoomMin && level <= area.zoomMax) {
const intersection = Rectangle.intersection(
rectangle,
area.bbox,
intersectionScratch,
);
if (defined(intersection)) {
included = true;
}
}
}
if (included) {
result.push(attribution.credit);
}
}
return result;
}
// Exposed for testing
BingMapsImageryProvider._metadataCache = {};
export default BingMapsImageryProvider;
+98
View File
@@ -0,0 +1,98 @@
// @ts-check
/**
* The types of imagery provided by Bing Maps.
*
* @enum {number}
*
* @see BingMapsImageryProvider
*/
const BingMapsStyle = {
/**
* Aerial imagery.
*
* @type {string}
* @constant
*/
AERIAL: "Aerial",
/**
* Aerial imagery with a road overlay.
*
* @type {string}
* @constant
* @deprecated See https://github.com/CesiumGS/cesium/issues/7128.
* Use `BingMapsStyle.AERIAL_WITH_LABELS_ON_DEMAND` instead
*/
AERIAL_WITH_LABELS: "AerialWithLabels",
/**
* Aerial imagery with a road overlay.
*
* @type {string}
* @constant
*/
AERIAL_WITH_LABELS_ON_DEMAND: "AerialWithLabelsOnDemand",
/**
* Roads without additional imagery.
*
* @type {string}
* @constant
* @deprecated See https://github.com/CesiumGS/cesium/issues/7128.
* Use `BingMapsStyle.ROAD_ON_DEMAND` instead
*/
ROAD: "Road",
/**
* Roads without additional imagery.
*
* @type {string}
* @constant
*/
ROAD_ON_DEMAND: "RoadOnDemand",
/**
* A dark version of the road maps.
*
* @type {string}
* @constant
*/
CANVAS_DARK: "CanvasDark",
/**
* A lighter version of the road maps.
*
* @type {string}
* @constant
*/
CANVAS_LIGHT: "CanvasLight",
/**
* A grayscale version of the road maps.
*
* @type {string}
* @constant
*/
CANVAS_GRAY: "CanvasGray",
/**
* Ordnance Survey imagery. This imagery is visible only for the London, UK area.
*
* @type {string}
* @constant
*/
ORDNANCE_SURVEY: "OrdnanceSurvey",
/**
* Collins Bart imagery.
*
* @type {string}
* @constant
*/
COLLINS_BART: "CollinsBart",
};
Object.freeze(BingMapsStyle);
export default BingMapsStyle;
+58
View File
@@ -0,0 +1,58 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines how two pixels' values are combined.
*
* @enum {number}
*/
const BlendEquation = {
/**
* Pixel values are added componentwise. This is used in additive blending for translucency.
*
* @type {number}
* @constant
*/
ADD: WebGLConstants.FUNC_ADD,
/**
* Pixel values are subtracted componentwise (source - destination). This is used in alpha blending for translucency.
*
* @type {number}
* @constant
*/
SUBTRACT: WebGLConstants.FUNC_SUBTRACT,
/**
* Pixel values are subtracted componentwise (destination - source).
*
* @type {number}
* @constant
*/
REVERSE_SUBTRACT: WebGLConstants.FUNC_REVERSE_SUBTRACT,
/**
* Pixel values are given to the minimum function (min(source, destination)).
*
* This equation operates on each pixel color component.
*
* @type {number}
* @constant
*/
MIN: WebGLConstants.MIN,
/**
* Pixel values are given to the maximum function (max(source, destination)).
*
* This equation operates on each pixel color component.
*
* @type {number}
* @constant
*/
MAX: WebGLConstants.MAX,
};
Object.freeze(BlendEquation);
export default BlendEquation;
+134
View File
@@ -0,0 +1,134 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines how blending factors are computed.
*
* @enum {number}
*/
const BlendFunction = {
/**
* The blend factor is zero.
*
* @type {number}
* @constant
*/
ZERO: WebGLConstants.ZERO,
/**
* The blend factor is one.
*
* @type {number}
* @constant
*/
ONE: WebGLConstants.ONE,
/**
* The blend factor is the source color.
*
* @type {number}
* @constant
*/
SOURCE_COLOR: WebGLConstants.SRC_COLOR,
/**
* The blend factor is one minus the source color.
*
* @type {number}
* @constant
*/
ONE_MINUS_SOURCE_COLOR: WebGLConstants.ONE_MINUS_SRC_COLOR,
/**
* The blend factor is the destination color.
*
* @type {number}
* @constant
*/
DESTINATION_COLOR: WebGLConstants.DST_COLOR,
/**
* The blend factor is one minus the destination color.
*
* @type {number}
* @constant
*/
ONE_MINUS_DESTINATION_COLOR: WebGLConstants.ONE_MINUS_DST_COLOR,
/**
* The blend factor is the source alpha.
*
* @type {number}
* @constant
*/
SOURCE_ALPHA: WebGLConstants.SRC_ALPHA,
/**
* The blend factor is one minus the source alpha.
*
* @type {number}
* @constant
*/
ONE_MINUS_SOURCE_ALPHA: WebGLConstants.ONE_MINUS_SRC_ALPHA,
/**
* The blend factor is the destination alpha.
*
* @type {number}
* @constant
*/
DESTINATION_ALPHA: WebGLConstants.DST_ALPHA,
/**
* The blend factor is one minus the destination alpha.
*
* @type {number}
* @constant
*/
ONE_MINUS_DESTINATION_ALPHA: WebGLConstants.ONE_MINUS_DST_ALPHA,
/**
* The blend factor is the constant color.
*
* @type {number}
* @constant
*/
CONSTANT_COLOR: WebGLConstants.CONSTANT_COLOR,
/**
* The blend factor is one minus the constant color.
*
* @type {number}
* @constant
*/
ONE_MINUS_CONSTANT_COLOR: WebGLConstants.ONE_MINUS_CONSTANT_COLOR,
/**
* The blend factor is the constant alpha.
*
* @type {number}
* @constant
*/
CONSTANT_ALPHA: WebGLConstants.CONSTANT_ALPHA,
/**
* The blend factor is one minus the constant alpha.
*
* @type {number}
* @constant
*/
ONE_MINUS_CONSTANT_ALPHA: WebGLConstants.ONE_MINUS_CONSTANT_ALPHA,
/**
* The blend factor is the saturated source alpha.
*
* @type {number}
* @constant
*/
SOURCE_ALPHA_SATURATE: WebGLConstants.SRC_ALPHA_SATURATE,
};
Object.freeze(BlendFunction);
export default BlendFunction;
+33
View File
@@ -0,0 +1,33 @@
// @ts-check
/**
* Determines how opaque and translucent parts of billboards, points, and labels are blended with the scene.
*
* @enum {number}
*/
const BlendOption = {
/**
* The billboards, points, or labels in the collection are completely opaque.
* @type {number}
* @constant
*/
OPAQUE: 0,
/**
* The billboards, points, or labels in the collection are completely translucent.
* @type {number}
* @constant
*/
TRANSLUCENT: 1,
/**
* The billboards, points, or labels in the collection are both opaque and translucent.
* @type {number}
* @constant
*/
OPAQUE_AND_TRANSLUCENT: 2,
};
Object.freeze(BlendOption);
export default BlendOption;
+78
View File
@@ -0,0 +1,78 @@
// @ts-check
import BlendEquation from "./BlendEquation.js";
import BlendFunction from "./BlendFunction.js";
/**
* The blending state combines {@link BlendEquation} and {@link BlendFunction} and the
* <code>enabled</code> flag to define the full blending state for combining source and
* destination fragments when rendering.
* <p>
* This is a helper when using custom render states with {@link Appearance#renderState}.
* </p>
*
* @namespace
*/
const BlendingState = {
/**
* Blending is disabled.
*
* @type {object}
* @constant
*/
DISABLED: Object.freeze({
enabled: false,
}),
/**
* Blending is enabled using alpha blending, <code>source(source.alpha) + destination(1 - source.alpha)</code>.
*
* @type {object}
* @constant
*/
ALPHA_BLEND: Object.freeze({
enabled: true,
equationRgb: BlendEquation.ADD,
equationAlpha: BlendEquation.ADD,
functionSourceRgb: BlendFunction.SOURCE_ALPHA,
functionSourceAlpha: BlendFunction.ONE,
functionDestinationRgb: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
functionDestinationAlpha: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
}),
/**
* Blending is enabled using alpha blending with premultiplied alpha, <code>source + destination(1 - source.alpha)</code>.
*
* @type {object}
* @constant
*/
PRE_MULTIPLIED_ALPHA_BLEND: Object.freeze({
enabled: true,
equationRgb: BlendEquation.ADD,
equationAlpha: BlendEquation.ADD,
functionSourceRgb: BlendFunction.ONE,
functionSourceAlpha: BlendFunction.ONE,
functionDestinationRgb: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
functionDestinationAlpha: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
}),
/**
* Blending is enabled using additive blending, <code>source(source.alpha) + destination</code>.
*
* @type {object}
* @constant
*/
ADDITIVE_BLEND: Object.freeze({
enabled: true,
equationRgb: BlendEquation.ADD,
equationAlpha: BlendEquation.ADD,
functionSourceRgb: BlendFunction.SOURCE_ALPHA,
functionSourceAlpha: BlendFunction.ONE,
functionDestinationRgb: BlendFunction.ONE,
functionDestinationAlpha: BlendFunction.ONE,
}),
};
Object.freeze(BlendingState);
export default BlendingState;
@@ -0,0 +1,169 @@
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
/**
* Utilities for parsing bounding volume semantics from 3D Tiles 1.1 metadata.
*
* @namespace BoundingVolumeSemantics
* @private
*/
const BoundingVolumeSemantics = {};
/**
* Parse the bounding volume-related semantics such as
* <code>TILE_BOUNDING_BOX</code> and <code>CONTENT_BOUNDING_BOX</code> from
* implicit tile or content metadata. Results are returned as a JSON object for
* use when transcoding tiles (see {@link Implicit3DTileContent}).
* <p>
* Bounding volumes are checked in the order box, region, then sphere. Only
* the first valid bounding volume is returned.
* </p>
* <p>
* This handles both tile and content bounding volumes, as the only difference
* is the prefix. e.g. <code>TILE_BOUNDING_BOX</code> and
* <code>CONTENT_BOUNDING_BOX</code> have the same memory layout.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata/Semantics|3D Metadata Semantic Reference} for the various bounding volumes and minimum/maximum heights.
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata object for looking up values by semantic. In practice, this will typically be a {@link ImplicitMetadataView}
* @return {object} An object containing the bounding volume, and any minimum or maximum height.
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
BoundingVolumeSemantics.parseAllBoundingVolumeSemantics = function (
prefix,
metadata,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
return {
boundingVolume: BoundingVolumeSemantics.parseBoundingVolumeSemantic(
prefix,
metadata,
),
minimumHeight: BoundingVolumeSemantics._parseMinimumHeight(
prefix,
metadata,
),
maximumHeight: BoundingVolumeSemantics._parseMaximumHeight(
prefix,
metadata,
),
};
};
/**
* Parse the bounding volume from tile or content metadata. If the metadata
* specify multiple bounding volumes, only the first one is returned. Bounding
* volumes are checked in the order box, region, then sphere.
* <p>
* This handles both tile and content bounding volumes, as the only difference
* is the prefix. e.g. <code>TILE_BOUNDING_BOX</code> and
* <code>CONTENT_BOUNDING_BOX</code> have the same memory layout.
* </p>
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata for looking up values
* @return {object} An object representing the JSON description of the tile or content bounding volume
* @private
*/
BoundingVolumeSemantics.parseBoundingVolumeSemantic = function (
prefix,
metadata,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
const boundingBoxSemantic = `${prefix}_BOUNDING_BOX`;
const boundingBox = metadata.getPropertyBySemantic(boundingBoxSemantic);
if (defined(boundingBox)) {
return {
box: boundingBox,
};
}
const boundingRegionSemantic = `${prefix}_BOUNDING_REGION`;
const boundingRegion = metadata.getPropertyBySemantic(boundingRegionSemantic);
if (defined(boundingRegion)) {
return {
region: boundingRegion,
};
}
const boundingSphereSemantic = `${prefix}_BOUNDING_SPHERE`;
const boundingSphere = metadata.getPropertyBySemantic(boundingSphereSemantic);
if (defined(boundingSphere)) {
// ARRAY with 4 elements is automatically converted to a Cartesian4
return {
sphere: boundingSphere,
};
}
return undefined;
};
/**
* Parse the minimum height from tile or content metadata. This is used for making
* tighter quadtree bounds for implicit tiling. This works for both
* <code>TILE_MINIMUM_HEIGHT</code> and <code>CONTENT_MINIMUM_HEIGHT</code>
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata for looking up values
* @return {number} The minimum height
* @private
*/
BoundingVolumeSemantics._parseMinimumHeight = function (prefix, metadata) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
const minimumHeightSemantic = `${prefix}_MINIMUM_HEIGHT`;
return metadata.getPropertyBySemantic(minimumHeightSemantic);
};
/**
* Parse the maximum height from tile or content metadata. This is used for
* making tighter quadtree bounds for implicit tiling. This works for both
* <code>TILE_MAXIMUM_HEIGHT</code> and <code>CONTENT_MAXIMUM_HEIGHT</code>
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata for looking up values
* @return {number} The maximum height
* @private
*/
BoundingVolumeSemantics._parseMaximumHeight = function (prefix, metadata) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
const maximumHeightSemantic = `${prefix}_MAXIMUM_HEIGHT`;
return metadata.getPropertyBySemantic(maximumHeightSemantic);
};
export default BoundingVolumeSemantics;
+74
View File
@@ -0,0 +1,74 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
const defaultDimensions = new Cartesian3(1.0, 1.0, 1.0);
/**
* A ParticleEmitter that emits particles within a box.
* Particles will be positioned randomly within the box and have initial velocities emanating from the center of the box.
*
* @alias BoxEmitter
* @constructor
*
* @param {Cartesian3} dimensions The width, height and depth dimensions of the box.
*/
function BoxEmitter(dimensions) {
dimensions = dimensions ?? defaultDimensions;
//>>includeStart('debug', pragmas.debug);
Check.defined("dimensions", dimensions);
Check.typeOf.number.greaterThanOrEquals("dimensions.x", dimensions.x, 0.0);
Check.typeOf.number.greaterThanOrEquals("dimensions.y", dimensions.y, 0.0);
Check.typeOf.number.greaterThanOrEquals("dimensions.z", dimensions.z, 0.0);
//>>includeEnd('debug');
this._dimensions = Cartesian3.clone(dimensions);
}
Object.defineProperties(BoxEmitter.prototype, {
/**
* The width, height and depth dimensions of the box in meters.
* @memberof BoxEmitter.prototype
* @type {Cartesian3}
* @default new Cartesian3(1.0, 1.0, 1.0)
*/
dimensions: {
get: function () {
return this._dimensions;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.defined("value", value);
Check.typeOf.number.greaterThanOrEquals("value.x", value.x, 0.0);
Check.typeOf.number.greaterThanOrEquals("value.y", value.y, 0.0);
Check.typeOf.number.greaterThanOrEquals("value.z", value.z, 0.0);
//>>includeEnd('debug');
Cartesian3.clone(value, this._dimensions);
},
},
});
const scratchHalfDim = new Cartesian3();
/**
* Initializes the given {Particle} by setting it's position and velocity.
*
* @private
* @param {Particle} particle The particle to initialize.
*/
BoxEmitter.prototype.emit = function (particle) {
const dim = this._dimensions;
const halfDim = Cartesian3.multiplyByScalar(dim, 0.5, scratchHalfDim);
const x = CesiumMath.randomBetween(-halfDim.x, halfDim.x);
const y = CesiumMath.randomBetween(-halfDim.y, halfDim.y);
const z = CesiumMath.randomBetween(-halfDim.z, halfDim.z);
particle.position = Cartesian3.fromElements(x, y, z, particle.position);
particle.velocity = Cartesian3.normalize(
particle.position,
particle.velocity,
);
};
export default BoxEmitter;
+75
View File
@@ -0,0 +1,75 @@
import BoundingRectangle from "../Core/BoundingRectangle.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import PixelFormat from "../Core/PixelFormat.js";
import Framebuffer from "../Renderer/Framebuffer.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import RenderState from "../Renderer/RenderState.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
import BrdfLutGeneratorFS from "../Shaders/BrdfLutGeneratorFS.js";
/**
* @private
*/
function BrdfLutGenerator() {
this._colorTexture = undefined;
this._drawCommand = undefined;
}
Object.defineProperties(BrdfLutGenerator.prototype, {
colorTexture: {
get: function () {
return this._colorTexture;
},
},
});
function createCommand(generator, context, framebuffer) {
const drawCommand = context.createViewportQuadCommand(BrdfLutGeneratorFS, {
framebuffer: framebuffer,
renderState: RenderState.fromCache({
viewport: new BoundingRectangle(0.0, 0.0, 256.0, 256.0),
}),
});
generator._drawCommand = drawCommand;
}
BrdfLutGenerator.prototype.update = function (frameState) {
if (!defined(this._colorTexture)) {
const context = frameState.context;
const colorTexture = new Texture({
context: context,
width: 256,
height: 256,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
sampler: Sampler.NEAREST,
});
this._colorTexture = colorTexture;
const framebuffer = new Framebuffer({
context: context,
colorTextures: [colorTexture],
destroyAttachments: false,
});
createCommand(this, context, framebuffer);
this._drawCommand.execute(context);
framebuffer.destroy();
this._drawCommand.shaderProgram =
this._drawCommand.shaderProgram &&
this._drawCommand.shaderProgram.destroy();
}
};
BrdfLutGenerator.prototype.isDestroyed = function () {
return false;
};
BrdfLutGenerator.prototype.destroy = function () {
this._colorTexture = this._colorTexture && this._colorTexture.destroy();
return destroyObject(this);
};
export default BrdfLutGenerator;
+130
View File
@@ -0,0 +1,130 @@
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import ResourceLoader from "./ResourceLoader.js";
import ResourceLoaderState from "./ResourceLoaderState.js";
/**
* Loads an embedded or external buffer.
* <p>
* Implements the {@link ResourceLoader} interface.
* </p>
*
* @private
*/
class BufferLoader extends ResourceLoader {
/**
* @param {object} options Object with the following properties:
* @param {Uint8Array} [options.typedArray] The typed array containing the embedded buffer contents. Mutually exclusive with options.resource.
* @param {Resource} [options.resource] The {@link Resource} pointing to the external buffer. Mutually exclusive with options.typedArray.
* @param {string} [options.cacheKey] The cache key of the resource.
*
* @exception {DeveloperError} One of options.typedArray and options.resource must be defined.
*/
constructor(options) {
super();
options = options ?? Frozen.EMPTY_OBJECT;
const typedArray = options.typedArray;
const resource = options.resource;
const cacheKey = options.cacheKey;
//>>includeStart('debug', pragmas.debug);
if (defined(typedArray) === defined(resource)) {
throw new DeveloperError(
"One of options.typedArray and options.resource must be defined.",
);
}
//>>includeEnd('debug');
this._typedArray = typedArray;
this._resource = resource;
this._cacheKey = cacheKey;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
}
/**
* The cache key of the resource.
*
*
* @type {string}
* @readonly
* @private
*/
get cacheKey() {
return this._cacheKey;
}
/**
* The typed array containing the embedded buffer contents.
*
*
* @type {Uint8Array}
* @readonly
* @private
*/
get typedArray() {
return this._typedArray;
}
/**
* Loads the resource.
* @returns {Promise<BufferLoader>} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
async load() {
if (defined(this._promise)) {
return this._promise;
}
if (defined(this._typedArray)) {
this._promise = Promise.resolve(this);
return this._promise;
}
this._promise = loadExternalBuffer(this);
return this._promise;
}
/**
* Exposed for testing
* @private
*/
static _fetchArrayBuffer(resource) {
return resource.fetchArrayBuffer();
}
/**
* Unloads the resource.
* @private
*/
unload() {
this._typedArray = undefined;
}
}
async function loadExternalBuffer(bufferLoader) {
const resource = bufferLoader._resource;
bufferLoader._state = ResourceLoaderState.LOADING;
try {
const arrayBuffer = await BufferLoader._fetchArrayBuffer(resource);
if (bufferLoader.isDestroyed()) {
return;
}
bufferLoader._typedArray = new Uint8Array(arrayBuffer);
bufferLoader._state = ResourceLoaderState.READY;
return bufferLoader;
} catch (error) {
if (bufferLoader.isDestroyed()) {
return;
}
bufferLoader._state = ResourceLoaderState.FAILED;
const errorMessage = `Failed to load external buffer: ${resource.url}`;
throw bufferLoader.getError(errorMessage, error);
}
}
export default BufferLoader;
+149
View File
@@ -0,0 +1,149 @@
// @ts-check
import BufferPrimitive from "./BufferPrimitive.js";
import Cartesian3 from "../Core/Cartesian3.js";
import assert from "../Core/assert.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
/** @import BufferPointCollection from "./BufferPointCollection.js"; */
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
const scratchCartesian = new Cartesian3();
/**
* View bound to the underlying buffer data of a {@link BufferPointCollection}.
*
* <p>BufferPoint instances are {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweights}:
* a single BufferPoint instance can be temporarily bound to any conceptual
* "point" in a BufferPointCollection, allowing very large collections to be
* iterated and updated with a minimal memory footprint.</p>
*
* Represented as one (1) position.
*
* @see BufferPointCollection
* @see BufferPointMaterial
* @see BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitive
*/
class BufferPoint extends BufferPrimitive {
/**
* @type {BufferPointCollection}
* @ignore
*/
_collection = null;
/** @ignore */
static Layout = {
...BufferPrimitive.Layout,
/**
* Offset in position array to current point vertex, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH,
/**
* @type {number}
* @ignore
*/
__BYTE_LENGTH: BufferPrimitive.Layout.__BYTE_LENGTH + 4,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source point to result.
*
* @param {BufferPoint} point
* @param {BufferPoint} result
* @return {BufferPoint}
* @override
*/
static clone(point, result) {
super.clone(point, result);
result.setPosition(point.getPosition(scratchCartesian));
return result;
}
/////////////////////////////////////////////////////////////////////////////
// GEOMETRY
/**
* Offset in collection position array to position of this point, number
* of VEC3 elements.
*
* @type {number}
* @readonly
* @ignore
*/
get vertexOffset() {
return this._getUint32(BufferPoint.Layout.POSITION_OFFSET_U32);
}
/**
* Count of positions (vertices) in this primitive. Always 1.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return 1;
}
/**
* Gets the position of this point.
*
* @param {Cartesian3} [result]
* @returns {Cartesian3}
*/
getPosition(result) {
const positionF64 = this._collection._positionView;
// @ts-expect-error TODO(tsd-jsdoc): See https://github.com/CesiumGS/cesium/pull/13302.
return Cartesian3.fromArray(positionF64, this.vertexOffset * 3, result);
}
/**
* Sets the position of this point.
*
* @param {Cartesian3} position
*/
setPosition(position) {
const collection = this._collection;
const vertexOffset = this.vertexOffset;
//>>includeStart('debug', pragmas.debug);
assert(vertexOffset < collection.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._positionView[vertexOffset * 3] = position.x;
collection._positionView[vertexOffset * 3 + 1] = position.y;
collection._positionView[vertexOffset * 3 + 2] = position.z;
this._dirty = true;
collection._makeDirtyBoundingVolume();
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the point. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
* @override
*/
toJSON() {
return {
...super.toJSON(),
position: Cartesian3.pack(this.getPosition(), []),
};
}
}
export default BufferPoint;
@@ -0,0 +1,151 @@
// @ts-check
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPoint from "./BufferPoint.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Frozen from "../Core/Frozen.js";
import renderPoints from "./renderBufferPointCollection.js";
import BufferPointMaterial from "./BufferPointMaterial.js";
/** @import BlendOption from "./BlendOption.js"; */
/** @import BoundingSphere from "../Core/BoundingSphere.js"; */
/** @import ComponentDatatype from "../Core/ComponentDatatype.js"; */
/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js"; */
/**
* @typedef {object} BufferPointOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPointMaterial} [material=BufferPointMaterial.DEFAULT_MATERIAL]
* @property {number} [featureId]
* @property {object} [pickObject]
* @property {Cartesian3} [position=Cartesian3.ZERO]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of points held in ArrayBuffer storage for performance and memory optimization.
*
* <p>Default buffer memory allocation is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.</p>
*
* @example
* const collection = new BufferPointCollection({primitiveCountMax: 1024});
*
* const point = new BufferPoint();
* const material = new BufferPointMaterial({color: Color.WHITE});
*
* // Create a new point, temporarily bound to 'point' local variable.
* collection.add({
* position: new Cartesian3(0.0, 0.0, 0.0),
* material
* }, point);
*
* // Iterate over all points in collection, temporarily binding 'point'
* // local variable to each, and updating point material.
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, point);
* point.setMaterial(material);
* }
*
* @see BufferPoint
* @see BufferPrimitiveCollection
* @extends BufferPrimitiveCollection<BufferPoint>
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPointCollection extends BufferPrimitiveCollection {
/**
* @param {object} options
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
* @param {boolean} [options.positionNormalized=false]
* @param {boolean} [options.show=true]
* @param {boolean} [options.allowPicking=false] When <code>true</code>, primitives are pickable with {@link Scene#pick}. When <code>false</code>, memory and initialization cost are lower.
* @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When
* unspecified, a bounding volume is computed automatically and updated when primitive positions change. When
* specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume
* manually, and updating it only as needed, will improve performance for larger dynamic collections.
* @param {boolean} [options.debugShowBoundingVolume=false]
* @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super({ ...options, vertexCountMax: options.primitiveCountMax });
}
_getCollectionClass() {
return BufferPointCollection;
}
_getPrimitiveClass() {
return BufferPoint;
}
_getMaterialClass() {
return BufferPointMaterial;
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @param {BufferPointCollection} collection
* @returns {BufferPointCollection}
* @override
* @ignore
*/
static _cloneEmpty(collection) {
return new BufferPointCollection({
primitiveCountMax: collection.primitiveCountMax,
positionDatatype: collection.positionDatatype,
positionNormalized: collection.positionNormalized,
});
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Adds a new point to the collection, with the specified options. A
* {@link BufferPoint} instance is linked to the new point, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPoint instance rather than
* allocating a new instance on each call.
*
* @param {BufferPointOptions} options
* @param {BufferPoint} result
* @returns {BufferPoint}
* @override
*/
add(options, result = new BufferPoint()) {
super.add(options, result);
result._setUint32(
BufferPoint.Layout.POSITION_OFFSET_U32,
this._positionCount++,
);
result.setPosition(options.position ?? Cartesian3.ZERO);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/**
* @param {FrameState} frameState
* @ignore
*/
update(frameState) {
super.update(frameState);
const passes = frameState.passes;
if (this.show && (passes.render || passes.pick)) {
this._renderContext = renderPoints(this, frameState, this._renderContext);
}
}
}
export default BufferPointCollection;
@@ -0,0 +1,95 @@
// @ts-check
import Frozen from "../Core/Frozen.js";
import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js";
/** @import Color from "../Core/Color.js"; */
/** @import BufferPoint from "./BufferPoint.js"; */
/**
* @typedef {object} BufferPointMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
* @property {number} [size=1.0] Size of point, 0-255px.
*/
/**
* Material description for a {@link BufferPoint}.
*
* <p>BufferPointMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPoint#setMaterial}. Subsequent changes to the
* material will not affect the point until setMaterial() is called again.</p>
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitiveMaterial
*/
class BufferPointMaterial extends BufferPrimitiveMaterial {
/** @ignore */
static Layout = {
...BufferPrimitiveMaterial.Layout,
SIZE_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH,
__BYTE_LENGTH: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 4,
};
/**
* @type {BufferPointMaterial}
* @ignore
*/
static DEFAULT_MATERIAL = Object.freeze(new BufferPointMaterial());
/**
* @param {BufferPointMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
/**
* Size of point, 0-255px.
* @type {number}
*/
this.size = options.size ?? 1;
}
/**
* @override
* @param {BufferPointMaterial} material
* @param {DataView} view
* @param {number} byteOffset
* @override
*/
static pack(material, view, byteOffset) {
super.pack(material, view, byteOffset);
view.setUint8(this.Layout.SIZE_U8 + byteOffset, material.size);
}
/**
* @override
* @param {DataView} view
* @param {number} byteOffset
* @param {BufferPointMaterial} result
* @returns {BufferPointMaterial}
* @override
*/
static unpack(view, byteOffset, result) {
super.unpack(view, byteOffset, result);
result.size = view.getUint8(this.Layout.SIZE_U8 + byteOffset);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the material. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
return { ...super.toJSON(), size: this.size };
}
}
export default BufferPointMaterial;
+517
View File
@@ -0,0 +1,517 @@
// @ts-check
import assert from "../Core/assert.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import BufferPrimitive from "./BufferPrimitive.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
/** @import { TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import BufferPolygonCollection from "./BufferPolygonCollection.js"; */
const { ERR_CAPACITY, ERR_RESIZE, ERR_OUT_OF_RANGE } =
BufferPrimitiveCollection.Error;
/**
* View bound to the underlying buffer data of a {@link BufferPolygonCollection}.
*
* <p>BufferPolygon instances are {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweights}:
* a single BufferPolygon instance can be temporarily bound to any conceptual
* "polygon" in a BufferPolygonCollection, allowing very large collections to be
* iterated and updated with a minimal memory footprint.</p>
*
* <p>Represented as one (1) external linear ring of three (3) or more positions.
* May optionally define one or more internal linear rings ("holes") within the
* polygon. Each hole is represented as a single index into the positions array,
* where the vertex at that index is the start of an internal linear ring that
* continues along the following vertices until reaching either the vertex
* index of the next hole, or the end of the vertex list. Stores a precomputed
* triangulation, represented as three vertex indices per triangle.</p>
*
* @see BufferPolygonCollection
* @see BufferPolygonMaterial
* @see BufferPrimitive
* @extends BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolygon extends BufferPrimitive {
/**
* @type {BufferPolygonCollection}
* @ignore
*/
_collection = null;
/** @ignore */
static Layout = {
...BufferPrimitive.Layout,
/**
* Offset in collection position array to first vertex in polygon, number
* of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH,
/**
* Count of positions (vertices) in this polygon, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 4,
/**
* Offset in collection holes array to first hole in polygon, number of
* integer elements.
* @type {number}
* @ignore
*/
HOLE_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 8,
/**
* Count of holes (indices) in this polygon.
* @type {number}
* @ignore
*/
HOLE_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 12,
/**
* Offset in collection triangles array to first triangle in polygon,
* number of VEC3 elements.
* @type {number}
* @ignore
*/
TRIANGLE_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 16,
/**
* Count of triangles in this polygon, number of VEC3 elements.
* @type {number}
* @ignore
*/
TRIANGLE_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 20,
/**
* @type {number}
* @ignore
*/
__BYTE_LENGTH: BufferPrimitive.Layout.__BYTE_LENGTH + 24,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source polygon to result. If the result polygon is not
* new (the last polygon in the collection) then source and result polygons
* must have the same vertex counts, hole counts, and triangle counts.
*
* @param {BufferPolygon} polygon
* @param {BufferPolygon} result
* @return {BufferPolygon}
* @override
*/
static clone(polygon, result) {
super.clone(polygon, result);
result.setPositions(polygon.getPositions());
result.setHoles(polygon.getHoles());
result.setTriangles(polygon.getTriangles());
return result;
}
/////////////////////////////////////////////////////////////////////////////
// GEOMETRY
/**
* Offset in collection position array to first vertex in polygon, number
* of VEC3 elements.
*
* @type {number}
* @readonly
* @ignore
*/
get vertexOffset() {
return this._getUint32(BufferPolygon.Layout.POSITION_OFFSET_U32);
}
/**
* Count of positions (vertices) in this polygon, including both outer ring and
* internal rings (holes), number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return this._getUint32(BufferPolygon.Layout.POSITION_COUNT_U32);
}
/**
* Returns an array view of this polygon's vertex positions. If 'result'
* argument is given, vertex positions are written to that array and returned.
* Otherwise, returns an ArrayView on collection memory — changes to this array
* will not trigger render updates, which requires `.setPositions()`.
*
* @param {TypedArray} [result]
* return {TypedArray}
*/
getPositions(result) {
return this._getPositionsRange(0, this.vertexCount, result);
}
/** @param {TypedArray} positions */
setPositions(positions) {
const collection = this._collection;
const vertexOffset = this.vertexOffset;
const srcCount = this.vertexCount;
const dstCount = positions.length / 3;
const collectionCount = collection.vertexCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._positionCount = collectionCount;
this._setUint32(BufferPolygon.Layout.POSITION_COUNT_U32, dstCount);
const positionView = collection._positionView;
for (let i = 0; i < dstCount; i++) {
positionView[(vertexOffset + i) * 3] = positions[i * 3];
positionView[(vertexOffset + i) * 3 + 1] = positions[i * 3 + 1];
positionView[(vertexOffset + i) * 3 + 2] = positions[i * 3 + 2];
}
this._dirty = true;
collection._makeDirtyBoundingVolume();
}
/**
* Offset in collection position array to first vertex in polygon's outer
* linear ring, number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get outerVertexOffset() {
return this.vertexOffset;
}
/**
* Count of positions (vertices) in this polygon's outer linear ring, number
* of VEC3 elements.
*
* @type {number}
* @readonly
*/
get outerVertexCount() {
if (this.holeCount > 0) {
return this.getHoles()[0];
}
return this.vertexCount;
}
/**
* Returns an array view of this polygon's outer linear ring vertex positions.
* If 'result' argument is given, vertex positions are written to that array
* and returned. Otherwise, returns an ArrayView on collection memory —
* changes to this array will not trigger render updates, which requires
* `.setPositions()`.
*
* @param {TypedArray} [result]
* @returns {TypedArray}
*/
getOuterPositions(result) {
return this._getPositionsRange(0, this.outerVertexCount, result);
}
/**
* Offset in collection holes array to first hole in polygon, number of
* integer elements.
*
* @type {number}
* @readonly
* @ignore
*/
get holeOffset() {
return this._getUint32(BufferPolygon.Layout.HOLE_OFFSET_U32);
}
/**
* Count of holes (indices) in this polygon.
*
* @type {number}
* @readonly
*/
get holeCount() {
return this._getUint32(BufferPolygon.Layout.HOLE_COUNT_U32);
}
/**
* Gets this polygon's hole indices, with each hole represented as a single
* offset into this polygon's positions array. Each hole implicitly
* continues along an internal linear ring from that vertex offset until
* reaching either the end of the positions array, or the next hole offset.
*
* If 'result' argument is given, hole indices are written to that array and
* returned. Otherwise, returns an ArrayView on collection memory — changes
* to this array will not trigger render updates, which requires `.setHoles()`.
*
* @param {TypedArray} [result]
* @returns {TypedArray}
*/
getHoles(result) {
const { holeOffset, holeCount } = this;
const holeIndexView = this._collection._holeIndexView;
if (!defined(result)) {
const byteOffset =
holeIndexView.byteOffset + holeOffset * holeIndexView.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
holeIndexView.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (holeIndexView.buffer),
byteOffset,
holeCount,
);
}
for (let i = 0; i < holeCount; i++) {
result[i] = holeIndexView[holeOffset + i];
}
return result;
}
/**
* Sets this polygon's hole indices, with holes represented as a single
* offset into this polygon's positions array. Each hole implicitly
* continues along an internal linear ring from that vertex offset until
* reaching either the end of the positions array, or the next hole offset.
*
* @param {TypedArray} holes
*/
setHoles(holes) {
const collection = this._collection;
const holeOffset = this.holeOffset;
const srcCount = this.holeCount;
const dstCount = holes.length;
const collectionCount = collection.holeCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.holeCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._holeCount = collectionCount;
this._setUint32(BufferPolygon.Layout.HOLE_COUNT_U32, dstCount);
const holeIndexView = collection._holeIndexView;
for (let i = 0; i < dstCount; i++) {
holeIndexView[holeOffset + i] = holes[i];
}
this._dirty = true;
}
/**
* Returns the number of (VEC3) vertices in the specified hole.
*
* @param {number} holeIndex
* @returns {number}
*/
getHoleVertexCount(holeIndex) {
const holes = this.getHoles();
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThanOrEquals("holeIndex", holeIndex, 0);
Check.typeOf.number.lessThan("holeIndex", holeIndex, holes.length);
//>>includeEnd('debug');
const holeVertexOffset = holes[holeIndex];
return holeIndex === holes.length - 1
? this.vertexCount - holeVertexOffset
: holes[holeIndex + 1] - holeVertexOffset;
}
/**
* Returns an array view of the inner linear ring vertex positions for the
* specified hole. If 'result' argument is given, vertex positions are written
* to that array and returned. Otherwise, returns an ArrayView on collection
* memory — changes to this array will not trigger render updates, which
* requires `.setPositions()`.
*
* @param {number} holeIndex
* @param {TypedArray} [result]
* return {TypedArray}
*/
getHolePositions(holeIndex, result) {
const holes = this.getHoles();
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThanOrEquals("holeIndex", holeIndex, 0);
Check.typeOf.number.lessThan("holeIndex", holeIndex, holes.length);
//>>includeEnd('debug');
const holeVertexOffset = holes[holeIndex];
const holeVertexCount = this.getHoleVertexCount(holeIndex);
return this._getPositionsRange(holeVertexOffset, holeVertexCount, result);
}
/**
* Internal helper for accessing vertex positions. 'vertexOffset' argument
* is relative to the start of the polygon's vertex block, with 0 being
* the first vertex in the polygon. If 'result' argument is given, the
* requested range of vertices are written to the result array and returned.
* Otherwise, returns an ArrayView on collection memory.
*
* @param {number} vertexOffset
* @param {number} vertexCount
* @param {TypedArray} [result]
* @returns {TypedArray}
* @private
*/
_getPositionsRange(vertexOffset, vertexCount, result) {
const collection = this._collection;
const positionView = this._collection._positionView;
const collectionVertexOffset = this.vertexOffset + vertexOffset;
//>>includeStart('debug', pragmas.debug);
assert(collectionVertexOffset >= 0, ERR_OUT_OF_RANGE);
assert(collectionVertexOffset < collection.vertexCount, ERR_OUT_OF_RANGE);
assert(vertexCount > 0, ERR_OUT_OF_RANGE);
assert(vertexCount <= this.vertexCount, ERR_OUT_OF_RANGE);
//>>includeEnd('debug');
if (!defined(result)) {
const byteOffset =
positionView.byteOffset +
collectionVertexOffset * 3 * positionView.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
positionView.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (positionView.buffer),
byteOffset,
vertexCount * 3,
);
}
for (let i = 0; i < vertexCount; i++) {
result[i * 3] = positionView[(collectionVertexOffset + i) * 3];
result[i * 3 + 1] = positionView[(collectionVertexOffset + i) * 3 + 1];
result[i * 3 + 2] = positionView[(collectionVertexOffset + i) * 3 + 2];
}
return result;
}
/**
* Offset in collection triangles array to first triangle in polygon,
* number of VEC3 elements.
* @type {number}
* @readonly
* @ignore
*/
get triangleOffset() {
return this._getUint32(BufferPolygon.Layout.TRIANGLE_OFFSET_U32);
}
/**
* Count of triangles in this polygon, number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get triangleCount() {
return this._getUint32(BufferPolygon.Layout.TRIANGLE_COUNT_U32);
}
/**
* Returns an array view of this polygon's triangle indices, represented as
* three vertex indices per triangle.
*
* If 'result' argument is given, triangle indices are written to that array
* and returned. Otherwise, returns an ArrayView on collection memory —
* changes to this array will not trigger render updates, which requires
* `.setTriangles()`.
*
* @param {TypedArray} [result]
* @returns {TypedArray}
*/
getTriangles(result) {
const { triangleOffset, triangleCount } = this;
const indices = this._collection._triangleIndexView;
if (!defined(result)) {
const byteOffset =
indices.byteOffset + triangleOffset * 3 * indices.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
indices.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (indices.buffer),
byteOffset,
triangleCount * 3,
);
}
for (let i = 0; i < triangleCount; i++) {
result[i * 3] = indices[(triangleOffset + i) * 3];
result[i * 3 + 1] = indices[(triangleOffset + i) * 3 + 1];
result[i * 3 + 2] = indices[(triangleOffset + i) * 3 + 2];
}
return result;
}
/**
* Sets this polygon's triangle indices, represented as three vertex indices
* per triangle.
*
* @param {TypedArray} indices
*/
setTriangles(indices) {
const collection = this._collection;
const triangleOffset = this.triangleOffset;
const srcCount = this.triangleCount;
const dstCount = indices.length / 3;
const collectionCount = collection.triangleCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.triangleCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._triangleCount += dstCount - srcCount;
this._setUint32(BufferPolygon.Layout.TRIANGLE_COUNT_U32, dstCount);
const dstIndices = collection._triangleIndexView;
for (let i = 0; i < dstCount; i++) {
dstIndices[(triangleOffset + i) * 3] = indices[i * 3];
dstIndices[(triangleOffset + i) * 3 + 1] = indices[i * 3 + 1];
dstIndices[(triangleOffset + i) * 3 + 2] = indices[i * 3 + 2];
}
this._dirty = true;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the polygon. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
* @override
*/
toJSON() {
return {
...super.toJSON(),
positions: Array.from(this.getPositions()),
holes: Array.from(this.getHoles()),
triangles: Array.from(this.getTriangles()),
};
}
}
export default BufferPolygon;
@@ -0,0 +1,380 @@
// @ts-check
import defined from "../Core/defined.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPolygon from "./BufferPolygon.js";
import Frozen from "../Core/Frozen.js";
import assert from "../Core/assert.js";
import IndexDatatype from "../Core/IndexDatatype.js";
import renderPolygons from "./renderBufferPolygonCollection.js";
import BufferPolygonMaterial from "./BufferPolygonMaterial.js";
/** @import BlendOption from "./BlendOption.js"; */
/** @import BoundingSphere from "../Core/BoundingSphere.js"; */
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js" */
/** @import ComponentDatatype from "../Core/ComponentDatatype.js"; */
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
/**
* @typedef {object} BufferPolygonOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPolygonMaterial} [material=BufferPolygonMaterial.DEFAULT_MATERIAL]
* @property {number} [featureId]
* @property {object} [pickObject]
* @property {TypedArray} [positions]
* @property {TypedArray} [holes]
* @property {TypedArray} [triangles]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of polygons held in ArrayBuffer storage for performance and memory optimization.
*
* <p>Default buffer memory allocation is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.</p>
*
* @example
* import earcut from "earcut";
*
* const collection = new BufferPolygonCollection({
* primitiveCountMax: 1024,
* vertexCountMax: 4096,
* holeCountMax: 1024,
* triangleCountMax: 2048,
* });
*
* const polygon = new BufferPolygon();
* const positions = [ ... ];
* const holes = [ ... ];
* const material = new BufferPolygonMaterial({color: Color.WHITE});
*
* // Create a new polygon, temporarily bound to 'polygon' local variable.
* collection.add({
* positions: new Float64Array(positions),
* holes: new Uint32Array(holes),
* triangles: new Uint32Array(earcut(positions, holes, 3)),
* material
* }, polygon);
*
* // Iterate over all polygons in collection, temporarily binding 'polygon'
* // local variable to each, and updating polygon material.
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, polygon);
* polygon.setMaterial(material);
* }
*
* @see BufferPolygon
* @see BufferPolygonMaterial
* @see BufferPrimitiveCollection
* @extends BufferPrimitiveCollection<BufferPolygon>
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolygonCollection extends BufferPrimitiveCollection {
/**
* @param {object} options
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.vertexCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.holeCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.triangleCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
* @param {boolean} [options.positionNormalized=false]
* @param {boolean} [options.show=true]
* @param {boolean} [options.allowPicking=true] When <code>true</code>, primitives are pickable with {@link Scene#pick}. When <code>false</code>, memory and initialization cost are lower.
* @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When
* unspecified, a bounding volume is computed automatically and updated when primitive positions change. When
* specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume
* manually, and updating it only as needed, will improve performance for larger dynamic collections.
* @param {boolean} [options.debugShowBoundingVolume=false]
* @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
/**
* @type {number}
* @ignore
*/
this._holeCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._holeCountMax =
options.holeCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {TypedArray}
* @ignore
*/
this._holeIndexView = null;
/**
* @type {number}
* @ignore
*/
this._triangleCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._triangleCountMax =
options.triangleCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {TypedArray}
* @ignore
*/
this._triangleIndexView = null;
this._allocateHoleIndexBuffer();
this._allocateTriangleIndexBuffer();
}
_getCollectionClass() {
return BufferPolygonCollection;
}
_getPrimitiveClass() {
return BufferPolygon;
}
_getMaterialClass() {
return BufferPolygonMaterial;
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @private
* @ignore
*/
_allocateHoleIndexBuffer() {
// @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
this._holeIndexView = IndexDatatype.createTypedArray(
this._positionCountMax,
this._holeCountMax,
);
}
/**
* @private
* @ignore
*/
_allocateTriangleIndexBuffer() {
// @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
this._triangleIndexView = IndexDatatype.createTypedArray(
this._positionCountMax,
this._triangleCountMax * 3,
);
}
/**
* Duplicates the contents of this collection into the result collection.
* Result collection is not resized, and must contain enough space for all
* primitives in the source collection. Existing polygons in the result
* collection will be overwritten.
*
* <p>Useful when allocating more space for a collection that has reached its
* capacity, and efficiently transferring polygons to the new collection.</p>
*
* @example
* const result = new BufferPolygonCollection({ ... }); // allocate larger 'result' collection
* BufferPolygonCollection.clone(collection, result); // copy polygons from 'collection' into 'result'
*
* @param {BufferPolygonCollection} collection
* @param {BufferPolygonCollection} result
* @returns {BufferPolygonCollection}
*/
static clone(collection, result) {
super.clone(collection, result);
//>>includeStart('debug', pragmas.debug);
assert(collection.holeCount <= result.holeCountMax, ERR_CAPACITY);
assert(collection.triangleCount <= result.triangleCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
this._copySubArray(
collection._holeIndexView,
result._holeIndexView,
collection.holeCount,
);
this._copySubArray(
collection._triangleIndexView,
result._triangleIndexView,
collection._triangleCount * 3,
);
result._holeCount = collection._holeCount;
result._triangleCount = collection._triangleCount;
return result;
}
/**
* @param {BufferPolygonCollection} collection
* @returns {BufferPolygonCollection}
* @override
* @ignore
*/
static _cloneEmpty(collection) {
return new BufferPolygonCollection({
primitiveCountMax: collection.primitiveCountMax,
vertexCountMax: collection.vertexCountMax,
holeCountMax: collection.holeCountMax,
triangleCountMax: collection.triangleCountMax,
positionDatatype: collection.positionDatatype,
positionNormalized: collection.positionNormalized,
});
}
/**
* @param {BufferPolygonCollection} src
* @param {BufferPolygonCollection} dst
* @override
* @ignore
*/
static _replaceBuffers(src, dst) {
super._replaceBuffers(src, dst);
dst._holeIndexView = src._holeIndexView;
dst._triangleIndexView = src._triangleIndexView;
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Adds a new polygon to the collection, with the specified options. A
* {@link BufferPolygon} instance is linked to the new polygon, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPolygon instance rather than
* allocating a new instance on each call.
*
* @param {BufferPolygonOptions} options
* @param {BufferPolygon} result
* @returns {BufferPolygon}
* @override
*/
add(options, result = new BufferPolygon()) {
super.add(options, result);
const vertexOffset = this._positionCount;
result._setUint32(BufferPolygon.Layout.POSITION_OFFSET_U32, vertexOffset);
result._setUint32(BufferPolygon.Layout.POSITION_COUNT_U32, 0);
const holeOffset = this._holeCount;
result._setUint32(BufferPolygon.Layout.HOLE_OFFSET_U32, holeOffset);
result._setUint32(BufferPolygon.Layout.HOLE_COUNT_U32, 0);
const triangleOffset = this._triangleCount;
result._setUint32(BufferPolygon.Layout.TRIANGLE_OFFSET_U32, triangleOffset);
result._setUint32(BufferPolygon.Layout.TRIANGLE_COUNT_U32, 0);
if (defined(options.positions)) {
result.setPositions(options.positions);
}
if (defined(options.holes)) {
result.setHoles(options.holes);
}
if (defined(options.triangles)) {
result.setTriangles(options.triangles);
}
return result;
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/**
* @param {FrameState} frameState
* @ignore
*/
update(frameState) {
super.update(frameState);
const passes = frameState.passes;
if (this.show && (passes.render || passes.pick)) {
this._renderContext = renderPolygons(
this,
frameState,
this._renderContext,
);
}
}
/////////////////////////////////////////////////////////////////////////////
// ACCESSORS
/**
* Total byte length of buffers owned by this collection. Includes any unused
* space allocated by {@link primitiveCountMax}, even if no polygons have
* yet been added in that space.
*
* @type {number}
* @readonly
* @override
*/
get byteLength() {
return (
super.byteLength +
this._holeIndexView.byteLength +
this._triangleIndexView.byteLength
);
}
/**
* Number of holes in collection. Must be <= {@link holeCountMax}.
*
* @type {number}
* @readonly
*/
get holeCount() {
return this._holeCount;
}
/**
* Maximum number of holes in collection. Must be >= {@link holeCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get holeCountMax() {
return this._holeCountMax;
}
/**
* Number of triangles in collection. Must be <= {@link triangleCountMax}.
*
* @type {number}
* @readonly
*/
get triangleCount() {
return this._triangleCount;
}
/**
* Maximum number of triangles in collection. Must be >= {@link triangleCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get triangleCountMax() {
return this._triangleCountMax;
}
}
export default BufferPolygonCollection;
@@ -0,0 +1,41 @@
// @ts-check
import Frozen from "../Core/Frozen.js";
import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js";
/** @import Color from "../Core/Color.js"; */
/** @import BufferPolygon from "./BufferPolygon.js"; */
/**
* @typedef {object} BufferPolygonMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
*/
/**
* Material description for a {@link BufferPolygon}.
*
* <p>BufferPolygonMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPolygon#setMaterial}. Subsequent changes to the
* material will not affect the polygon until setMaterial() is called again.</p>
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitiveMaterial
*/
class BufferPolygonMaterial extends BufferPrimitiveMaterial {
/**
* @type {BufferPolygonMaterial}
* @ignore
*/
static DEFAULT_MATERIAL = Object.freeze(new BufferPolygonMaterial());
/**
* @param {BufferPolygonMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
}
}
export default BufferPolygonMaterial;
+186
View File
@@ -0,0 +1,186 @@
// @ts-check
import BufferPrimitive from "./BufferPrimitive.js";
import assert from "../Core/assert.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import defined from "../Core/defined.js";
/** @import { TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import BufferPolylineCollection from "./BufferPolylineCollection.js"; */
const { ERR_RESIZE, ERR_CAPACITY } = BufferPrimitiveCollection.Error;
/**
* View bound to the underlying buffer data of a {@link BufferPolylineCollection}.
*
* <p>BufferPolyline instances are {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweights}:
* a single BufferPolyline instance can be temporarily bound to any conceptual
* "polyline" in a BufferPolylineCollection, allowing very large collections to be
* iterated and updated with a minimal memory footprint.</p>
*
* Represented as two (2) or more positions.
*
* @see BufferPolylineCollection
* @see BufferPolylineMaterial
* @see BufferPrimitive
* @extends BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolyline extends BufferPrimitive {
/**
* @type {BufferPolylineCollection}
* @ignore
*/
_collection = null;
/** @ignore */
static Layout = {
...BufferPrimitive.Layout,
/**
* Offset in position array to first vertex in polyline, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH,
/**
* Count of positions (vertices) in this polyline, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 4,
/**
* @type {number}
* @ignore
*/
__BYTE_LENGTH: BufferPrimitive.Layout.__BYTE_LENGTH + 8,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source polyline to result. If the result polyline is not
* new (the last polyline in the collection) then source and result polylines
* must have the same vertex counts.
*
* @param {BufferPolyline} polyline
* @param {BufferPolyline} result
* @return {BufferPolyline}
* @override
*/
static clone(polyline, result) {
super.clone(polyline, result);
result.setPositions(polyline.getPositions());
return result;
}
/////////////////////////////////////////////////////////////////////////////
// GEOMETRY
/**
* Offset in collection position array to first vertex in polyline, number
* of VEC3 elements.
*
* @type {number}
* @readonly
* @ignore
*/
get vertexOffset() {
return this._getUint32(BufferPolyline.Layout.POSITION_OFFSET_U32);
}
/**
* Count of positions (vertices) in this polyline, number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return this._getUint32(BufferPolyline.Layout.POSITION_COUNT_U32);
}
/**
* Returns an array view of this polyline's vertex positions. If 'result'
* argument is given, vertex positions are written to that array and returned.
* Otherwise, returns an ArrayView on collection memory — changes to this array
* will not trigger render updates, which requires `.setPositions()`.
*
* @param {TypedArray} [result]
* return {TypedArray}
*/
getPositions(result) {
const { vertexOffset, vertexCount } = this;
const positionView = this._collection._positionView;
if (!defined(result)) {
const byteOffset =
positionView.byteOffset +
vertexOffset * 3 * positionView.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
positionView.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (positionView.buffer),
byteOffset,
vertexCount * 3,
);
}
for (let i = 0; i < vertexCount; i++) {
result[i * 3] = positionView[(vertexOffset + i) * 3];
result[i * 3 + 1] = positionView[(vertexOffset + i) * 3 + 1];
result[i * 3 + 2] = positionView[(vertexOffset + i) * 3 + 2];
}
return result;
}
/** @param {TypedArray} positions */
setPositions(positions) {
const collection = this._collection;
const vertexOffset = this.vertexOffset;
const srcCount = this.vertexCount;
const dstCount = positions.length / 3;
const collectionCount = collection._positionCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._positionCount = collectionCount;
this._setUint32(BufferPolyline.Layout.POSITION_COUNT_U32, dstCount);
const positionView = collection._positionView;
for (let i = 0; i < dstCount; i++) {
positionView[(vertexOffset + i) * 3] = positions[i * 3];
positionView[(vertexOffset + i) * 3 + 1] = positions[i * 3 + 1];
positionView[(vertexOffset + i) * 3 + 2] = positions[i * 3 + 2];
}
this._dirty = true;
collection._makeDirtyBoundingVolume();
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the polyline. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
* @override
*/
toJSON() {
return {
...super.toJSON(),
positions: Array.from(this.getPositions()),
};
}
}
export default BufferPolyline;
@@ -0,0 +1,140 @@
// @ts-check
import defined from "../Core/defined.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPolyline from "./BufferPolyline.js";
import renderPolylines from "./renderBufferPolylineCollection.js";
import BufferPolylineMaterial from "./BufferPolylineMaterial.js";
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js" */
/**
* @typedef {object} BufferPolylineOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPolylineMaterial} [material=BufferPolylineMaterial.DEFAULT_MATERIAL]
* @property {number} [featureId]
* @property {object} [pickObject]
* @property {TypedArray} [positions]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of polylines held in ArrayBuffer storage for performance and memory optimization.
*
* <p>Default buffer memory allocation is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.</p>
*
* @example
* const collection = new BufferPolylineCollection({
* primitiveCountMax: 1024,
* vertexCountMax: 4096,
* });
*
* const polyline = new BufferPolyline();
* const material = new BufferPolylineMaterial({color: Color.WHITE});
*
* // Create a new polyline, temporarily bound to 'polyline' local variable.
* collection.add({
* positions: new Float64Array([ ... ]),
* material,
* }, polyline);
*
* // Iterate over all polylines in collection, temporarily binding 'polyline'
* // local variable to each, and updating polyline material.
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, polyline);
* polyline.setMaterial(material);
* }
*
* @see BufferPolyline
* @see BufferPolylineMaterial
* @see BufferPrimitiveCollection
* @extends BufferPrimitiveCollection<BufferPolyline>
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolylineCollection extends BufferPrimitiveCollection {
_getCollectionClass() {
return BufferPolylineCollection;
}
_getPrimitiveClass() {
return BufferPolyline;
}
_getMaterialClass() {
return BufferPolylineMaterial;
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @param {BufferPolylineCollection} collection
* @returns {BufferPolylineCollection}
* @override
* @ignore
*/
static _cloneEmpty(collection) {
return new BufferPolylineCollection({
primitiveCountMax: collection.primitiveCountMax,
vertexCountMax: collection.vertexCountMax,
positionDatatype: collection.positionDatatype,
positionNormalized: collection.positionNormalized,
});
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Adds a new polyline to the collection, with the specified options. A
* {@link BufferPolyline} instance is linked to the new polyline, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPolyline instance rather than
* allocating a new instance on each call.
*
* @param {BufferPolylineOptions} options
* @param {BufferPolyline} result
* @returns {BufferPolyline}
* @override
*/
add(options, result = new BufferPolyline()) {
super.add(options, result);
const vertexOffset = this._positionCount;
result._setUint32(BufferPolyline.Layout.POSITION_OFFSET_U32, vertexOffset);
result._setUint32(BufferPolyline.Layout.POSITION_COUNT_U32, 0);
if (defined(options.positions)) {
result.setPositions(options.positions);
}
return result;
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/**
* @param {FrameState} frameState
* @ignore
*/
update(frameState) {
super.update(frameState);
const passes = frameState.passes;
if (this.show && (passes.render || passes.pick)) {
this._renderContext = renderPolylines(
this,
frameState,
this._renderContext,
);
}
}
}
export default BufferPolylineCollection;
@@ -0,0 +1,93 @@
// @ts-check
import Frozen from "../Core/Frozen.js";
import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js";
/** @import Color from "../Core/Color.js"; */
/** @import BufferPolyline from "./BufferPolyline.js"; */
/**
* @typedef {object} BufferPolylineMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
* @property {number} [width=1.0] Width of line, 0-255px.
*/
/**
* Material description for a {@link BufferPolyline}.
*
* <p>BufferPolylineMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPolyline#setMaterial}. Subsequent changes to the
* material will not affect the polyline until setMaterial() is called again.</p>
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitiveMaterial
*/
class BufferPolylineMaterial extends BufferPrimitiveMaterial {
/** @ignore */
static Layout = {
...BufferPrimitiveMaterial.Layout,
WIDTH_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH,
__BYTE_LENGTH: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 4,
};
/**
* @type {BufferPolylineMaterial}
* @ignore
*/
static DEFAULT_MATERIAL = Object.freeze(new BufferPolylineMaterial());
/**
* @param {BufferPolylineMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
/**
* Width of polyline, 0–255px.
* @type {number}
*/
this.width = options.width ?? 1;
}
/**
* @param {BufferPolylineMaterial} material
* @param {DataView} view
* @param {number} byteOffset
* @override
*/
static pack(material, view, byteOffset) {
super.pack(material, view, byteOffset);
view.setUint8(this.Layout.WIDTH_U8 + byteOffset, material.width);
}
/**
* @param {DataView} view
* @param {number} byteOffset
* @param {BufferPolylineMaterial} result
* @returns {BufferPolylineMaterial}
* @override
*/
static unpack(view, byteOffset, result) {
super.unpack(view, byteOffset, result);
result.width = view.getUint8(this.Layout.WIDTH_U8 + byteOffset);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the material. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
return { ...super.toJSON(), width: this.width };
}
}
export default BufferPolylineMaterial;
+347
View File
@@ -0,0 +1,347 @@
// @ts-check
import assert from "../Core/assert.js";
/** @import BufferPrimitiveCollection from './BufferPrimitiveCollection.js'; */
/** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */
/**
* View bound to the underlying buffer data of a {@link BufferPrimitiveCollection}. Abstract.
*
* <p>BufferPrimitive instances are intended to be reused when iterating over large collections,
* and temporarily bound to a primitive index while performing read/write operations on that primitive,
* before being rebound to the next primitive, using the
* {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweight pattern}.</p>
*
* @see BufferPrimitiveCollection
* @see BufferPrimitiveMaterial
* @see BufferPoint
* @see BufferPolyline
* @see BufferPolygon
*
* @abstract
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPrimitive {
/**
* Collecton containing the primitive(s) for which this instance currently
* provides a view.
*
* @type {BufferPrimitiveCollection<BufferPrimitive>}
* @ignore
*/
_collection = null;
/**
* Index of the primitive for which this instance currently provides a view.
*
* @type {number}
* @ignore
*/
_index = -1;
/**
* Byte offset, into the collection's primitive buffer, of the primitive for
* which this instance currently provides a view.
*
* @type {number}
* @ignore
*/
_byteOffset = -1;
/**
* Binary layout for this primitive type in collection's primitive buffer.
* Each `Layout.MY_KEY_U32` entry is an offset in bytes, relative to the
* start offset of the primitive, with the suffix indicating the data type
* stored at that offset.
*
* The final entry, `__BYTE_LENGTH`, is not a pointer into a buffer — its
* literal value is the total byte length of one primitive in the primitive
* buffer, exclusive of other buffers.
*
* @ignore
*/
static Layout = {
/**
* Feature ID associated with the primitive; not required to be unique.
* @type {number}
* @ignore
*/
FEATURE_ID_U32: 0,
/**
* Boolean (0 or 1) flag indicating whether primitive is shown.
* @type {number}
* @ignore
*/
SHOW_U8: 4,
/**
* Boolean (0 or 1) flag indicating whether primitive is dirty.
* @type {number}
* @ignore
*/
DIRTY_U8: 5,
/**
* Pick ID (uint32) of primitive.
* @type {number}
* @ignore
*/
PICK_ID_U32: 8,
/**
* Byte length of one primitive in the primitive buffer, exclusive of
* other buffers. Literal value, not a pointer.
* @type {number}
* @ignore
*/
__BYTE_LENGTH: 12,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source primitive to result. If the result primitive is not
* new (the last primitive in the collection) then source and result primitives
* must have the same vertex counts.
*
* @param {BufferPrimitive} primitive
* @param {BufferPrimitive} result
* @returns {BufferPrimitive}
*/
static clone(primitive, result) {
const SrcMaterialClass = primitive._collection._getMaterialClass();
//>>includeStart('debug', pragmas.debug);
const DstMaterialClass = result._collection._getMaterialClass();
assert(SrcMaterialClass === DstMaterialClass, "Incompatible materials");
//>>includeEnd('debug');
result.featureId = primitive.featureId;
result.show = primitive.show;
result.setMaterial(primitive.getMaterial(new SrcMaterialClass()));
return result;
}
/**
* Returns true if this primitive's memory footprint is resizable. Only the
* newest (most recently created) primitive in a collection can be resized,
* to guarantee fast and stable performance.
*
* @returns {boolean}
* @protected
* @ignore
*/
_isResizable() {
return this._index === this._collection.primitiveCount - 1;
}
/////////////////////////////////////////////////////////////////////////////
// ACCESSORS
/**
* Feature ID associated with the primitive; not required to be unique.
* @type {number}
*/
get featureId() {
return this._getUint32(BufferPrimitive.Layout.FEATURE_ID_U32);
}
set featureId(featureId) {
this._setUint32(BufferPrimitive.Layout.FEATURE_ID_U32, featureId);
}
/**
* Whether primitive is shown.
* @type {boolean}
*/
get show() {
return this._getUint8(BufferPrimitive.Layout.SHOW_U8) === 1;
}
set show(show) {
this._setUint8(BufferPrimitive.Layout.SHOW_U8, show ? 1 : 0);
}
/**
* @param {BufferPrimitiveMaterial} result
* @returns {BufferPrimitiveMaterial}
*/
getMaterial(result) {
const collection = this._collection;
const MaterialClass = collection._getMaterialClass();
return MaterialClass.unpack(
collection._materialView,
this._index * MaterialClass.packedLength,
result,
);
}
/**
* @param {BufferPrimitiveMaterial} material
*/
setMaterial(material) {
const collection = this._collection;
const MaterialClass = collection._getMaterialClass();
MaterialClass.pack(
material,
collection._materialView,
this._index * MaterialClass.packedLength,
);
this._dirty = true;
return material;
}
/**
* Whether the primitive requires an update on next render. Renderers should
* _not_ iterate over all primitives each frame, but must instead inspect
* only the dirty range of the parent collection. This flag is managed
* automatically, by primitive setters and collection renderers.
*
* @type {boolean}
* @ignore
*
* @see BufferPrimitiveCollection#_dirtyOffset
* @see BufferPrimitiveCollection#_dirtyCount
*/
get _dirty() {
return this._getUint8(BufferPrimitive.Layout.DIRTY_U8) === 1;
}
set _dirty(dirty) {
// Avoid `._setUint8()` here, which would infinitely loop `._dirty = true`.
this._collection._primitiveView.setUint8(
this._byteOffset + BufferPrimitive.Layout.DIRTY_U8,
dirty ? 1 : 0,
);
// A 'dirty' primitive is responsible for notifying the collection. Applying
// updates and marking the primitive 'clean' will be handled by the collection,
// so we don't notify the collection here in that case.
if (dirty) {
this._collection._makeDirty(this._index);
}
}
/**
* Pick ID (uint32) of primitive.
* @type {number}
* @ignore
*/
get _pickId() {
return this._getUint32(BufferPrimitive.Layout.PICK_ID_U32);
}
set _pickId(pickId) {
this._setUint32(BufferPrimitive.Layout.PICK_ID_U32, pickId);
}
/////////////////////////////////////////////////////////////////////////////
// BUFFER ACCESSORS
/**
* @param {number} itemByteOffset
* @returns {number}
* @ignore
*/
_getUint8(itemByteOffset) {
return this._collection._primitiveView.getUint8(
this._byteOffset + itemByteOffset,
);
}
/**
* @param {number} itemByteOffset
* @param {number} itemValue
* @ignore
*/
_setUint8(itemByteOffset, itemValue) {
this._collection._primitiveView.setUint8(
this._byteOffset + itemByteOffset,
itemValue,
);
this._dirty = true;
}
/**
* @param {number} itemByteOffset
* @returns {number}
* @ignore
*/
_getUint32(itemByteOffset) {
return this._collection._primitiveView.getUint32(
this._byteOffset + itemByteOffset,
true,
);
}
/**
* @param {number} itemByteOffset
* @param {number} itemValue
* @ignore
*/
_setUint32(itemByteOffset, itemValue) {
this._collection._primitiveView.setUint32(
this._byteOffset + itemByteOffset,
itemValue,
true,
);
this._dirty = true;
}
/**
* @param {number} itemByteOffset
* @returns {number}
* @ignore
*/
_getFloat32(itemByteOffset) {
return this._collection._primitiveView.getFloat32(
this._byteOffset + itemByteOffset,
true,
);
}
/**
* @param {number} itemByteOffset
* @param {number} itemValue
* @ignore
*/
_setFloat32(itemByteOffset, itemValue) {
this._collection._primitiveView.setFloat32(
this._byteOffset + itemByteOffset,
itemValue,
true,
);
this._dirty = true;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the primitive. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
const collection = this._collection;
const MaterialClass = collection._getMaterialClass();
return {
featureId: this.featureId,
show: this.show,
dirty: this._dirty,
material: this.getMaterial(new MaterialClass()).toJSON(),
};
}
}
export default BufferPrimitive;
@@ -0,0 +1,869 @@
// @ts-check
import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian3 from "../Core/Cartesian3.js";
import DeveloperError from "../Core/DeveloperError.js";
import Frozen from "../Core/Frozen.js";
import Matrix4 from "../Core/Matrix4.js";
import assert from "../Core/assert.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import Check from "../Core/Check.js";
import AttributeCompression from "../Core/AttributeCompression.js";
import SceneMode from "./SceneMode.js";
import AttributeType from "./AttributeType.js";
import oneTimeWarning from "../Core/oneTimeWarning.js";
import BlendOption from "../Scene/BlendOption.js";
/** @import { Destroyable, TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import Context from "../Renderer/Context.js"; */
/** @import FrameState from "./FrameState.js"; */
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
/** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */
/** @import PickId from "../Renderer/PickId.js"; */
/**
* @typedef {object} BufferPrimitiveOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPrimitiveMaterial} [material]
* @property {number} [featureId]
* @property {object} [pickObject]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of primitives held in ArrayBuffer storage for performance and memory optimization.
*
* <p>To get the full performance benefit of using a BufferPrimitiveCollection containing "N" primitives,
* be careful to avoid allocating "N" instances of any related JavaScript object. {@link BufferPrimitive},
* {@link Color}, {@link Cartesian3}, and other objects can all be reused when working with large collections,
* using the {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweight pattern}.</p>
*
* @abstract
* @template T extends BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*
* @see BufferPrimitive
* @see BufferPrimitiveMaterial
* @see BufferPointCollection
* @see BufferPolylineCollection
* @see BufferPolygonCollection
*/
class BufferPrimitiveCollection {
/** @ignore */
static Error = {
ERR_RESIZE: "BufferPrimitive range cannot be resized after initialization.",
ERR_CAPACITY: "BufferPrimitiveCollection capacity exceeded.",
ERR_MULTIPLE_OF_FOUR:
"BufferPrimitive byte length must be a multiple of 4.",
ERR_OUT_OF_RANGE: "BufferPrimitive buffer access out of range.",
};
/**
* Resources managed by the collection's renderer. Collections may have multiple renderer
* implementations, so the collection should be ignorant of the renderer's implementation
* and context data. A collection only has one renderer active at a time.
*
* @type {Destroyable|null}
* @ignore
*/
_renderContext = null;
/**
* @param {object} options
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.vertexCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {boolean} [options.show=true]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
* @param {boolean} [options.positionNormalized=false] When <code>true</code>, integer position values are treated as normalized,
* where the full integer range maps to [-1, 1] (signed) or [0, 1] (unsigned). Only relevant for integer position datatypes
* (BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT).
* @param {boolean} [options.allowPicking=false] When <code>true</code>, primitives are pickable with {@link Scene#pick}. When <code>false</code>, memory and initialization cost are lower.
* @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When
* unspecified, a bounding volume is computed automatically and updated when primitive positions change. When
* specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume
* manually, and updating it only as needed, will improve performance for larger dynamic collections.
* @param {boolean} [options.debugShowBoundingVolume=false]
* @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
/**
* Determines if primitives in this collection will be shown.
* @type {boolean}
* @default true
*/
this.show = options.show ?? true;
/**
* Collection blend option; must be OPAQUE or TRANSLUCENT.
* @type {BlendOption}
* @readonly
* @ignore
*/
this._blendOption = options.blendOption ?? BlendOption.TRANSLUCENT;
/**
* Transforms geometry from model to world coordinates.
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
* @protected
*/
this._modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY);
/**
* @type {BoundingSphere}
* @readonly
* @protected
*/
this._boundingVolume = BoundingSphere.clone(
options.boundingVolume ?? new BoundingSphere(),
new BoundingSphere(),
);
/**
* @type {boolean}
* @readonly
* @protected
*/
this._boundingVolumeAutoUpdate = !defined(options.boundingVolume);
/**
* When <code>true</code>, primitives are pickable with {@link Scene#pick}.
* When <code>false</code>, memory and initialization cost are lower.
* @type {boolean}
* @readonly
* @ignore
* @default false
*/
this._allowPicking = options.allowPicking ?? false;
/**
* @type {Map<Context, PickId[]>}
* @readonly
* @ignore
*/
this._pickIds = new Map();
/**
* @type {object[]}
* @readonly
* @ignore
*/
this._pickObjects = [];
/**
* This property is for debugging only; it is not for production use nor is it optimized.
* <p>
* Draws the bounding sphere for each draw command in the primitive.
* </p>
*
* @type {boolean}
* @default false
*/
this.debugShowBoundingVolume = options.debugShowBoundingVolume ?? false;
/**
* @type {number}
* @protected
* @ignore
*/
this._primitiveCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._primitiveCountMax =
options.primitiveCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {DataView<ArrayBuffer>}
* @ignore
*/
this._primitiveView = null;
/**
* @type {number}
* @ignore
*/
this._positionCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._positionCountMax =
options.vertexCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {TypedArray}
* @ignore
*/
this._positionView = null;
/**
* @type {ComponentDatatype}
* @ignore
*/
this._positionDatatype =
options.positionDatatype ?? ComponentDatatype.DOUBLE;
/**
* When <code>true</code>, integer position values represent normalized floats
* in [-1, 1] (signed) or [0, 1] (unsigned). Only applicable to integer datatypes.
* @type {boolean}
* @ignore
*/
this._positionNormalized = options.positionNormalized ?? false;
/**
* @type {DataView<ArrayBuffer>}
* @ignore
*/
this._materialView = null;
// Potentially-dirty primitives are tracked as a contiguous range, with
// 'clean' primitives potentially within the range. Individual primitive
// 'dirty' flags are source-of-truth.
/**
* @type {number}
* @ignore
*/
this._dirtyOffset = 0;
/**
* @type {number}
* @ignore
*/
this._dirtyCount = 0;
/**
* @type {boolean}
* @ignore
*/
this._dirtyBoundingVolume = false;
this._allocatePrimitiveBuffer();
this._allocatePositionBuffer();
this._allocateMaterialBuffer();
}
/**
* Accessing `this.constructor` can cause JSDoc builds to fail, so use this
* protected getter function instead.
* @protected
* @return {*}
* @ignore
*/
_getCollectionClass() {
DeveloperError.throwInstantiationError();
}
/**
* @protected
* @return {*}
* @ignore
*/
_getPrimitiveClass() {
DeveloperError.throwInstantiationError();
}
/**
* @return {*}
* @ignore
*/
_getMaterialClass() {
DeveloperError.throwInstantiationError();
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @private
* @ignore
*/
_allocatePrimitiveBuffer() {
const layout = this._getPrimitiveClass().Layout;
//>>includeStart('debug', pragmas.debug);
const { ERR_MULTIPLE_OF_FOUR } = BufferPrimitiveCollection.Error;
assert(layout.__BYTE_LENGTH % 4 === 0, ERR_MULTIPLE_OF_FOUR);
//>>includeEnd('debug');
this._primitiveView = new DataView(
new ArrayBuffer(this._primitiveCountMax * layout.__BYTE_LENGTH),
);
}
/**
* @private
* @ignore
*/
_allocatePositionBuffer() {
// @ts-expect-error https://github.com/CesiumGS/cesium/issues/13420
this._positionView = ComponentDatatype.createTypedArray(
this._positionDatatype,
this._positionCountMax * 3,
);
}
/**
* @private
* @ignore
*/
_allocateMaterialBuffer() {
const MaterialClass = this._getMaterialClass();
this._materialView = new DataView(
new ArrayBuffer(this._primitiveCountMax * MaterialClass.packedLength),
);
}
/**
* Returns true if this object was destroyed; otherwise, false.
*
* @returns {boolean} True if this object was destroyed; otherwise, false.
*/
isDestroyed() {
return false;
}
/** Destroys collection and its GPU resources. */
destroy() {
this._pickObjects.length = 0;
for (const contextPickIds of this._pickIds.values()) {
for (const pickId of contextPickIds) {
pickId.destroy();
}
}
if (defined(this._renderContext)) {
this._renderContext.destroy();
this._renderContext = undefined;
this._dirtyOffset = 0;
this._dirtyCount = this.primitiveCount;
}
}
/**
* Sorts primitives of the collection.
*
* Because sorting changes the indices (but not the feature IDs) of primitives
* in the collection, the function also returns an array mapping from previous
* index to new index. When sorting repeatedly, the array can be reused and
* passed as the 'result' argument for each call.
*
* @param {Function} sortFn
* @param {Uint32Array} result
* @returns {Uint32Array} Mapping from previous index to new index.
*/
sort(sortFn, result = new Uint32Array(this.primitiveCount)) {
const PrimitiveClass = this._getPrimitiveClass();
const CollectionClass = this._getCollectionClass();
const { primitiveCount } = this;
const a = new PrimitiveClass();
const b = new PrimitiveClass();
// Mapping from NEW index to PREVIOUS index.
const dstSrcMap = new Uint32Array(primitiveCount);
for (let i = 0; i < primitiveCount; i++) {
dstSrcMap[i] = i;
}
dstSrcMap.sort((indexA, indexB) =>
sortFn(this.get(indexA, a), this.get(indexB, b)),
);
// Mapping from PREVIOUS index to NEW index.
for (let i = 0; i < primitiveCount; i++) {
result[dstSrcMap[i]] = i;
}
// Copy primitives to temporary collection, in sort order.
const tmp = CollectionClass._cloneEmpty(this);
for (let i = 0; i < primitiveCount; i++) {
const src = this.get(dstSrcMap[i], a);
const dst = tmp.add({}, b);
PrimitiveClass.clone(src, dst);
}
// Assign buffers from temporary collection onto this one.
CollectionClass._replaceBuffers(tmp, this);
this._dirtyOffset = 0;
this._dirtyCount = primitiveCount;
return result;
}
/**
* Duplicates the contents of this collection into the result collection.
* Result collection is not resized, and must contain enough space for all
* primitives in the source collection. Existing primitives in the result
* collection will be overwritten.
*
* <p>Useful when allocating more space for a collection that has reached its
* capacity, and efficiently transferring features to the new collection.</p>
*
* @example
* const result = new BufferPrimitiveCollection({ ... }); // allocate larger 'result' collection
* BufferPrimitiveCollection.clone(collection, result); // copy primitives from 'collection' into 'result'
*
* @param {BufferPrimitiveCollection<T>} collection
* @param {BufferPrimitiveCollection<T>} result
* @template T extends BufferPrimitive
*/
static clone(collection, result) {
//>>includeStart('debug', pragmas.debug);
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
assert(collection.primitiveCount <= result.primitiveCountMax, ERR_CAPACITY);
assert(collection.vertexCount <= result.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
const layout = collection._getPrimitiveClass().Layout;
const MaterialClass = collection._getMaterialClass();
const PrimitiveClass = collection._getPrimitiveClass();
this._copySubDataView(
collection._primitiveView,
result._primitiveView,
collection.primitiveCount * layout.__BYTE_LENGTH,
);
this._copySubArray(
collection._positionView,
result._positionView,
collection.vertexCount * 3,
);
this._copySubDataView(
collection._materialView,
result._materialView,
collection.primitiveCount * MaterialClass.packedLength,
);
result.show = collection.show;
result.debugShowBoundingVolume = collection.debugShowBoundingVolume;
result._primitiveCount = collection._primitiveCount;
result._positionCount = collection._positionCount;
// Unset PickIds.
const primitive = new PrimitiveClass();
for (let i = 0, il = result.primitiveCount; i < il; i++) {
result.get(i, primitive)._pickId = 0;
}
result._dirtyOffset = 0;
result._dirtyCount = result.primitiveCount;
collection.boundingVolume.clone(result.boundingVolume);
return result;
}
/**
* Returns an empty collection with the same buffer sizes as this collection.
* Internal utility for operations requiring a working copy of memory.
*
* @param {BufferPrimitiveCollection<T>} collection
* @returns {BufferPrimitiveCollection<T>}
* @template T extends BufferPrimitive
* @protected
* @abstract
* @ignore
*/
static _cloneEmpty(collection) {
DeveloperError.throwInstantiationError();
}
/**
* Assigns buffers from source collection to target collection, without
* validation or side effects. Callers must handle any validation, dirty
* flag updates, etc.
*
* @param {BufferPrimitiveCollection<T>} src
* @param {BufferPrimitiveCollection<T>} dst
* @template T extends BufferPrimitive
* @protected
* @ignore
*/
static _replaceBuffers(src, dst) {
dst._primitiveView = src._primitiveView;
dst._positionView = src._positionView;
dst._materialView = src._materialView;
}
/**
* Rebuilds collection bounding volume.
* @protected
* @ignore
*/
_updateBoundingVolume() {
// Exclude unused space in the position buffer.
let vertices = this._positionView.subarray(0, this._positionCount * 3);
if (this._positionNormalized) {
vertices = AttributeCompression.dequantize(
/** @type {Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array} */ (
vertices
),
this._positionDatatype,
AttributeType.VEC3,
this._positionCount,
);
}
BoundingSphere.fromVertices(
vertices,
Cartesian3.ZERO,
3,
this.boundingVolume,
);
BoundingSphere.transform(
this.boundingVolume,
this.modelMatrix,
this.boundingVolume,
);
this._dirtyBoundingVolume = false;
}
/**
* Updates PickIds for the given context.
* @param {Context} context
* @protected
* @ignore
*/
_updatePickIds(context) {
let pickIds = this._pickIds.get(context);
if (pickIds && pickIds.length === this._primitiveCount) {
return;
}
if (!pickIds) {
pickIds = [];
this._pickIds.set(context, pickIds);
}
const collection = this;
const PrimitiveClass = this._getPrimitiveClass();
const primitive = new PrimitiveClass();
// Fill in missing PickIDs for recently-added primitives.
for (let i = pickIds.length, il = this._primitiveCount; i < il; i++) {
this.get(i, primitive);
const pickObject = this._pickObjects[i] || {
collection: this,
index: i,
get primitive() {
// Cannot reuse primitives; scene.drillPick() appends to a list.
return collection.get(i, new PrimitiveClass());
},
};
const pickId = context.createPickId(pickObject);
primitive._pickId = pickId.key;
pickIds.push(pickId);
}
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Makes the given {@link BufferPrimitive} a view onto this collection's
* primitive at the given index, for use when reading/writing primitive
* properties. When iterating over a large collection, prefer to reuse
* the same BufferPrimitive instance throughout the loop — rebinding
* an existing instance to a different primitive is cheap, and avoids
* allocating in-memory objects for every object.
*
* @example
* const primitive = new BufferPrimitive();
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, primitive);
* primitive.setColor(Color.RED);
* }
*
* @param {number} index
* @param {BufferPrimitive} result
* @returns {BufferPrimitive} The BufferPrimitive instance passed as the
* 'result' argument, now bound to the specified primitive index.
*/
get(index, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThanOrEquals("index", index, 0);
Check.typeOf.number.lessThan("index", index, this._primitiveCount);
//>>includeEnd('debug');
result._collection = this;
result._index = index;
result._byteOffset = index * this._getPrimitiveClass().Layout.__BYTE_LENGTH;
return result;
}
/**
* Adds a new primitive to the collection, with the specified options. A
* {@link BufferPrimitive} instance is linked to the new primitive, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPrimitive instance rather than
* allocating a new instance on each call.
*
* @param {BufferPrimitiveOptions} options
* @param {BufferPrimitive} result
* @returns {BufferPrimitive}
*/
add(options = Frozen.EMPTY_OBJECT, result) {
//>>includeStart('debug', pragmas.debug);
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
assert(this.primitiveCount < this.primitiveCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
const MaterialClass = this._getMaterialClass();
const index = this._primitiveCount++;
result = this.get(index, result);
result.featureId = options.featureId ?? index;
result.show = options.show ?? true;
result.setMaterial(options.material ?? MaterialClass.DEFAULT_MATERIAL);
result._pickId = 0; // unset
result._dirty = true;
if (defined(options.pickObject)) {
this._pickObjects[index] = options.pickObject;
}
return result;
}
/**
* Marks primitive at given index as 'dirty', to be updated on next render.
* @param {number} index
* @ignore
*/
_makeDirty(index) {
if (this._dirtyCount === 0) {
this._dirtyCount = 1;
this._dirtyOffset = index;
} else if (index < this._dirtyOffset) {
this._dirtyCount += this._dirtyOffset - index;
this._dirtyOffset = index;
} else if (index + 1 > this._dirtyOffset + this._dirtyCount) {
this._dirtyCount = index + 1 - this._dirtyOffset;
}
}
/**
* Marks collection bounding volume as 'dirty', to be updated on next render,
* if automatic bounding volume updates are enabled.
* @ignore
*/
_makeDirtyBoundingVolume() {
if (this._boundingVolumeAutoUpdate) {
this._dirtyBoundingVolume = true;
}
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/** @param {object} frameState */
update(frameState) {
if (/** @type {FrameState} */ (frameState).mode !== SceneMode.SCENE3D) {
oneTimeWarning(
"bufferprim-scenemode",
"BufferPrimitiveCollection requires SceneMode.SCENE3D.",
);
}
if (this._dirtyBoundingVolume) {
this._updateBoundingVolume();
}
if (this._allowPicking && this._dirtyCount > 0) {
this._updatePickIds(/** @type {FrameState} */ (frameState).context);
}
}
/////////////////////////////////////////////////////////////////////////////
// ACCESSORS
/**
* Number of primitives in collection. Must be <= {@link primitiveCountMax}.
*
* @type {number}
* @readonly
*/
get primitiveCount() {
return this._primitiveCount;
}
/**
* Maximum number of primitives this collection can contain. Must be >=
* {@link primitiveCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get primitiveCountMax() {
return this._primitiveCountMax;
}
/**
* Total byte length of buffers owned by this collection. Includes any unused
* space allocated by {@link primitiveCountMax}, even if no primitives have
* yet been added in that space.
*
* @type {number}
* @readonly
*/
get byteLength() {
return (
this._primitiveView.byteLength +
this._positionView.byteLength +
this._materialView.byteLength
);
}
/**
* Number of vertices in collection. Must be <= {@link vertexCountMax}.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return this._positionCount;
}
/**
* Maximum number of vertices this collection can contain. Must be >=
* {@link vertexCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get vertexCountMax() {
return this._positionCountMax;
}
/**
* Transforms geometry from model to world coordinates.
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
*/
get modelMatrix() {
return this._modelMatrix;
}
/**
* World-space bounding volume for all primitives in the collection, including both
* shown and hidden primitives.
* @type {BoundingSphere}
* @readonly
*/
get boundingVolume() {
return this._boundingVolume;
}
/**
* The component datatype used to store position values.
* @type {ComponentDatatype}
* @readonly
*/
get positionDatatype() {
return this._positionDatatype;
}
/**
* When <code>true</code>, integer position values are treated as normalized
* values, where the full integer range maps to [-1, 1] (signed) or [0, 1]
* (unsigned).
* @type {boolean}
* @readonly
*/
get positionNormalized() {
return this._positionNormalized;
}
/////////////////////////////////////////////////////////////////////////////
// UTILS
/**
* @param {TypedArray} src
* @param {TypedArray} dst
* @param {number} count
* @protected
* @ignore
*/
static _copySubArray(src, dst, count) {
for (let i = 0; i < count; i++) {
dst[i] = src[i];
}
}
/**
* @param {DataView} src
* @param {DataView} dst
* @param {number} byteLength
* @protected
* @ignore
*/
static _copySubDataView(src, dst, byteLength) {
// No need to match the original array type, just copy in 4-byte chunks.
this._copySubArray(
new Uint32Array(src.buffer, src.byteOffset, src.byteLength / 4),
new Uint32Array(dst.buffer, dst.byteOffset, dst.byteLength / 4),
byteLength / 4,
);
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable array representing the collection. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @example
* console.table(collection.toJSON());
*
* @returns {Array<Object>} List of JSON-serializable objects, one for each
* primitive in the collection.
*/
toJSON() {
const PrimitiveClass = this._getPrimitiveClass();
const primitive = new PrimitiveClass();
const results = [];
for (let i = 0, il = this.primitiveCount; i < il; i++) {
results.push(this.get(i, primitive).toJSON());
}
return results;
}
}
/**
* Default capacity of buffers on new collections. A quantity of elements:
* number of vertices in the vertex buffer, primitives in the primitive
* buffer, etc. This value is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.
*
* @type {number}
* @static
* @constant
*/
BufferPrimitiveCollection.DEFAULT_CAPACITY = 1024;
export default BufferPrimitiveCollection;
@@ -0,0 +1,140 @@
// @ts-check
import Color from "../Core/Color.js";
import Frozen from "../Core/Frozen.js";
/** @import Packable from "../Core/Packable.js"; */
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
/**
* @typedef {object} BufferPrimitiveMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
*/
/**
* Material description for a {@link BufferPrimitive}. Abstract.
*
* <p>BufferPrimitiveMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPrimitive#setMaterial}. Subsequent changes to the
* material will not affect the primitive until setMaterial() is called again.</p>
*
* @see BufferPointMaterial
* @see BufferPolylineMaterial
* @see BufferPolygonMaterial
* @see Packable
*
* @abstract
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPrimitiveMaterial {
/** @ignore */
static Layout = {
COLOR_U32: 0,
OUTLINE_COLOR_U32: 4,
OUTLINE_WIDTH_U8: 8,
__BYTE_LENGTH: 12,
};
/**
* @type {BufferPrimitiveMaterial}
* @ignore
*/
static DEFAULT_MATERIAL;
/**
* @param {BufferPrimitiveMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
/**
* Color of fill.
* @type {Color}
*/
this.color = Color.clone(options.color ?? Color.WHITE);
/**
* Color of outline.
* @type {Color}
*/
this.outlineColor = Color.clone(options.outlineColor ?? Color.WHITE);
/**
* Width of outline, 0-255px.
* @type {number}
*/
this.outlineWidth = options.outlineWidth ?? 0;
}
/** @type {number} */
static get packedLength() {
return this.Layout.__BYTE_LENGTH;
}
/**
* Stores the provided material into the provided array.
*
* @param {BufferPrimitiveMaterial} material
* @param {DataView} view
* @param {number} byteOffset
*/
static pack(material, view, byteOffset) {
view.setUint32(
this.Layout.COLOR_U32 + byteOffset,
material.color.toRgba(),
true,
);
view.setUint32(
this.Layout.OUTLINE_COLOR_U32 + byteOffset,
material.outlineColor.toRgba(),
true,
);
view.setUint8(
this.Layout.OUTLINE_WIDTH_U8 + byteOffset,
material.outlineWidth,
);
}
/**
* Retrieves a material from a packed array.
*
* @param {DataView} view The packed array.
* @param {number} byteOffset Starting index of the element to be unpacked.
* @param {BufferPrimitiveMaterial} result Material into which results are unpacked.
* @returns {BufferPrimitiveMaterial} Modified result material, with results unpacked.
*/
static unpack(view, byteOffset, result) {
Color.fromRgba(
view.getUint32(this.Layout.COLOR_U32 + byteOffset, true),
result.color,
);
Color.fromRgba(
view.getUint32(this.Layout.OUTLINE_COLOR_U32 + byteOffset, true),
result.outlineColor,
);
result.outlineWidth = view.getUint8(
this.Layout.OUTLINE_WIDTH_U8 + byteOffset,
);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the material. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
return {
color: this.color.toCssHexString(),
outlineColor: this.outlineColor.toCssHexString(),
outlineWidth: this.outlineWidth,
};
}
}
export default BufferPrimitiveMaterial;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,631 @@
import Cartesian2 from "../Core/Cartesian2.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import KeyboardEventModifier from "../Core/KeyboardEventModifier.js";
import CesiumMath from "../Core/Math.js";
import ScreenSpaceEventHandler from "../Core/ScreenSpaceEventHandler.js";
import ScreenSpaceEventType from "../Core/ScreenSpaceEventType.js";
import CameraEventType from "./CameraEventType.js";
const keyboardModifierCombinations = [
KeyboardEventModifier.SHIFT,
KeyboardEventModifier.CTRL,
KeyboardEventModifier.ALT,
[KeyboardEventModifier.SHIFT, KeyboardEventModifier.CTRL],
[KeyboardEventModifier.SHIFT, KeyboardEventModifier.ALT],
[KeyboardEventModifier.CTRL, KeyboardEventModifier.ALT],
[
KeyboardEventModifier.SHIFT,
KeyboardEventModifier.CTRL,
KeyboardEventModifier.ALT,
],
];
function getKey(type, modifiers) {
if (!defined(modifiers)) {
return `${type}`;
}
const modifierList = Array.isArray(modifiers)
? modifiers.toSorted()
: [modifiers];
return `${type}+${modifierList.join("+")}`;
}
function clonePinchMovement(pinchMovement, result) {
Cartesian2.clone(
pinchMovement.distance.startPosition,
result.distance.startPosition,
);
Cartesian2.clone(
pinchMovement.distance.endPosition,
result.distance.endPosition,
);
Cartesian2.clone(
pinchMovement.angleAndHeight.startPosition,
result.angleAndHeight.startPosition,
);
Cartesian2.clone(
pinchMovement.angleAndHeight.endPosition,
result.angleAndHeight.endPosition,
);
}
function listenToPinch(aggregator, modifier, canvas) {
const key = getKey(CameraEventType.PINCH, modifier);
const update = aggregator._update;
const isDown = aggregator._isDown;
const eventStartPosition = aggregator._eventStartPosition;
const pressTime = aggregator._pressTime;
const releaseTime = aggregator._releaseTime;
update[key] = true;
isDown[key] = false;
eventStartPosition[key] = new Cartesian2();
let movement = aggregator._movement[key];
if (!defined(movement)) {
movement = aggregator._movement[key] = {};
}
movement.distance = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
};
movement.angleAndHeight = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
};
movement.prevAngle = 0.0;
aggregator._eventHandler.setInputAction(
function (event) {
aggregator._buttonsDown++;
isDown[key] = true;
pressTime[key] = new Date();
// Compute center position and store as start point.
Cartesian2.lerp(
event.position1,
event.position2,
0.5,
eventStartPosition[key],
);
},
ScreenSpaceEventType.PINCH_START,
modifier,
);
aggregator._eventHandler.setInputAction(
function () {
aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0);
isDown[key] = false;
releaseTime[key] = new Date();
},
ScreenSpaceEventType.PINCH_END,
modifier,
);
aggregator._eventHandler.setInputAction(
function (mouseMovement) {
if (isDown[key]) {
// Aggregate several input events into a single animation frame.
if (!update[key]) {
Cartesian2.clone(
mouseMovement.distance.endPosition,
movement.distance.endPosition,
);
Cartesian2.clone(
mouseMovement.angleAndHeight.endPosition,
movement.angleAndHeight.endPosition,
);
} else {
clonePinchMovement(mouseMovement, movement);
update[key] = false;
movement.prevAngle = movement.angleAndHeight.startPosition.x;
}
// Make sure our aggregation of angles does not "flip" over 360 degrees.
let angle = movement.angleAndHeight.endPosition.x;
const prevAngle = movement.prevAngle;
const TwoPI = Math.PI * 2;
while (angle >= prevAngle + Math.PI) {
angle -= TwoPI;
}
while (angle < prevAngle - Math.PI) {
angle += TwoPI;
}
movement.angleAndHeight.endPosition.x =
(-angle * canvas.clientWidth) / 12;
movement.angleAndHeight.startPosition.x =
(-prevAngle * canvas.clientWidth) / 12;
}
},
ScreenSpaceEventType.PINCH_MOVE,
modifier,
);
}
function listenToWheel(aggregator, modifier) {
const key = getKey(CameraEventType.WHEEL, modifier);
const pressTime = aggregator._pressTime;
const releaseTime = aggregator._releaseTime;
const update = aggregator._update;
update[key] = true;
let movement = aggregator._movement[key];
if (!defined(movement)) {
movement = aggregator._movement[key] = {};
}
let lastMovement = aggregator._lastMovement[key];
if (!defined(lastMovement)) {
lastMovement = aggregator._lastMovement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
movement.startPosition = new Cartesian2();
Cartesian2.clone(Cartesian2.ZERO, movement.startPosition);
movement.endPosition = new Cartesian2();
aggregator._eventHandler.setInputAction(
function (delta) {
const arcLength = 7.5 * CesiumMath.toRadians(delta);
pressTime[key] = releaseTime[key] = new Date();
movement.endPosition.x = 0.0;
movement.endPosition.y = arcLength;
Cartesian2.clone(movement.endPosition, lastMovement.endPosition);
lastMovement.valid = true;
update[key] = false;
},
ScreenSpaceEventType.WHEEL,
modifier,
);
}
function listenMouseButtonDownUp(aggregator, modifier, type) {
const key = getKey(type, modifier);
const isDown = aggregator._isDown;
const eventStartPosition = aggregator._eventStartPosition;
const pressTime = aggregator._pressTime;
isDown[key] = false;
eventStartPosition[key] = new Cartesian2();
let lastMovement = aggregator._lastMovement[key];
if (!defined(lastMovement)) {
lastMovement = aggregator._lastMovement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
let down;
let up;
if (type === CameraEventType.LEFT_DRAG) {
down = ScreenSpaceEventType.LEFT_DOWN;
up = ScreenSpaceEventType.LEFT_UP;
} else if (type === CameraEventType.RIGHT_DRAG) {
down = ScreenSpaceEventType.RIGHT_DOWN;
up = ScreenSpaceEventType.RIGHT_UP;
} else if (type === CameraEventType.MIDDLE_DRAG) {
down = ScreenSpaceEventType.MIDDLE_DOWN;
up = ScreenSpaceEventType.MIDDLE_UP;
}
aggregator._eventHandler.setInputAction(
function (event) {
aggregator._buttonsDown++;
lastMovement.valid = false;
isDown[key] = true;
pressTime[key] = new Date();
Cartesian2.clone(event.position, eventStartPosition[key]);
},
down,
modifier,
);
aggregator._eventHandler.setInputAction(
function () {
cancelMouseDownAction(getKey(type, undefined), aggregator);
for (const modifier of keyboardModifierCombinations) {
const cancelKey = getKey(type, modifier);
cancelMouseDownAction(cancelKey, aggregator);
}
},
up,
modifier,
);
}
function cancelMouseDownAction(cancelKey, aggregator) {
const releaseTime = aggregator._releaseTime;
const isDown = aggregator._isDown;
if (isDown[cancelKey]) {
aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0);
}
isDown[cancelKey] = false;
releaseTime[cancelKey] = new Date();
}
function cloneMouseMovement(mouseMovement, result) {
Cartesian2.clone(mouseMovement.startPosition, result.startPosition);
Cartesian2.clone(mouseMovement.endPosition, result.endPosition);
}
function refreshMouseDownStatus(type, modifier, aggregator) {
// first: Judge if the mouse is pressed
const isDown = aggregator._isDown;
let anyButtonIsDown = false;
const currentKey = getKey(type, modifier);
for (const [downKey, downValue] of Object.entries(isDown)) {
if (downKey.startsWith(type) && downValue && downKey !== currentKey) {
anyButtonIsDown = true;
cancelMouseDownAction(downKey, aggregator);
}
}
if (!anyButtonIsDown) {
return;
}
// second: If it is pressed, it will be transferred to the current modifier.
const pressTime = aggregator._pressTime;
let lastMovement = aggregator._lastMovement[currentKey];
if (!defined(lastMovement)) {
lastMovement = aggregator._lastMovement[currentKey] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
aggregator._buttonsDown++;
lastMovement.valid = false;
isDown[currentKey] = true;
pressTime[currentKey] = new Date();
}
function listenMouseMove(aggregator, modifier) {
const update = aggregator._update;
const movement = aggregator._movement;
const lastMovement = aggregator._lastMovement;
const isDown = aggregator._isDown;
for (const typeName in CameraEventType) {
if (CameraEventType.hasOwnProperty(typeName)) {
const type = CameraEventType[typeName];
if (defined(type)) {
const key = getKey(type, modifier);
update[key] = true;
if (!defined(aggregator._lastMovement[key])) {
aggregator._lastMovement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
if (!defined(aggregator._movement[key])) {
aggregator._movement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
};
}
}
}
}
aggregator._eventHandler.setInputAction(
function (mouseMovement) {
for (const typeName in CameraEventType) {
if (CameraEventType.hasOwnProperty(typeName)) {
const type = CameraEventType[typeName];
if (defined(type)) {
const key = getKey(type, modifier);
refreshMouseDownStatus(type, modifier, aggregator);
if (isDown[key]) {
if (!update[key]) {
Cartesian2.clone(
mouseMovement.endPosition,
movement[key].endPosition,
);
} else {
cloneMouseMovement(movement[key], lastMovement[key]);
lastMovement[key].valid = true;
cloneMouseMovement(mouseMovement, movement[key]);
update[key] = false;
}
}
}
}
}
Cartesian2.clone(
mouseMovement.endPosition,
aggregator._currentMousePosition,
);
},
ScreenSpaceEventType.MOUSE_MOVE,
modifier,
);
}
/**
* Aggregates input events. For example, suppose the following inputs are received between frames:
* left mouse button down, mouse move, mouse move, left mouse button up. These events will be aggregated into
* one event with a start and end position of the mouse.
*
* @alias CameraEventAggregator
* @constructor
*
* @param {HTMLCanvasElement} [canvas=document] The element to handle events for.
*
* @see ScreenSpaceEventHandler
*/
function CameraEventAggregator(canvas) {
//>>includeStart('debug', pragmas.debug);
if (!defined(canvas)) {
throw new DeveloperError("canvas is required.");
}
//>>includeEnd('debug');
this._eventHandler = new ScreenSpaceEventHandler(canvas);
this._update = {};
this._movement = {};
this._lastMovement = {};
this._isDown = {};
this._eventStartPosition = {};
this._pressTime = {};
this._releaseTime = {};
this._buttonsDown = 0;
this._currentMousePosition = new Cartesian2();
listenToWheel(this, undefined);
listenToPinch(this, undefined, canvas);
listenMouseButtonDownUp(this, undefined, CameraEventType.LEFT_DRAG);
listenMouseButtonDownUp(this, undefined, CameraEventType.RIGHT_DRAG);
listenMouseButtonDownUp(this, undefined, CameraEventType.MIDDLE_DRAG);
listenMouseMove(this, undefined);
for (const modifiers of keyboardModifierCombinations) {
listenToWheel(this, modifiers);
listenToPinch(this, modifiers, canvas);
listenMouseButtonDownUp(this, modifiers, CameraEventType.LEFT_DRAG);
listenMouseButtonDownUp(this, modifiers, CameraEventType.RIGHT_DRAG);
listenMouseButtonDownUp(this, modifiers, CameraEventType.MIDDLE_DRAG);
listenMouseMove(this, modifiers);
}
}
Object.defineProperties(CameraEventAggregator.prototype, {
/**
* Gets the current mouse position.
* @memberof CameraEventAggregator.prototype
* @type {Cartesian2}
*/
currentMousePosition: {
get: function () {
return this._currentMousePosition;
},
},
/**
* Gets whether any mouse button is down, a touch has started, or the wheel has been moved.
* @memberof CameraEventAggregator.prototype
* @type {boolean}
*/
anyButtonDown: {
get: function () {
const wheelMoved =
!this._update[getKey(CameraEventType.WHEEL)] ||
!this._update[
getKey(CameraEventType.WHEEL, KeyboardEventModifier.SHIFT)
] ||
!this._update[
getKey(CameraEventType.WHEEL, KeyboardEventModifier.CTRL)
] ||
!this._update[getKey(CameraEventType.WHEEL, KeyboardEventModifier.ALT)];
return this._buttonsDown > 0 || wheelMoved;
},
},
});
/**
* Gets if a mouse button down or touch has started and has been moved.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {boolean} Returns <code>true</code> if a mouse button down or touch has started and has been moved; otherwise, <code>false</code>
*/
CameraEventAggregator.prototype.isMoving = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return !this._update[key];
};
/**
* Gets the aggregated start and end position of the current event.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {object} An object with two {@link Cartesian2} properties: <code>startPosition</code> and <code>endPosition</code>.
*/
CameraEventAggregator.prototype.getMovement = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
const movement = this._movement[key];
return movement;
};
/**
* Gets the start and end position of the last move event (not the aggregated event).
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {object|undefined} An object with two {@link Cartesian2} properties: <code>startPosition</code> and <code>endPosition</code> or <code>undefined</code>.
*/
CameraEventAggregator.prototype.getLastMovement = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
const lastMovement = this._lastMovement[key];
if (lastMovement.valid) {
return lastMovement;
}
return undefined;
};
/**
* Gets whether the mouse button is down or a touch has started.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {boolean} Whether the mouse button is down or a touch has started.
*/
CameraEventAggregator.prototype.isButtonDown = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return this._isDown[key];
};
/**
* Gets the mouse position that started the aggregation.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {Cartesian2} The mouse position.
*/
CameraEventAggregator.prototype.getStartMousePosition = function (
type,
modifier,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
if (type === CameraEventType.WHEEL) {
return this._currentMousePosition;
}
const key = getKey(type, modifier);
return this._eventStartPosition[key];
};
/**
* Gets the time the button was pressed or the touch was started.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {Date} The time the button was pressed or the touch was started.
*/
CameraEventAggregator.prototype.getButtonPressTime = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return this._pressTime[key];
};
/**
* Gets the time the button was released or the touch was ended.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {Date} The time the button was released or the touch was ended.
*/
CameraEventAggregator.prototype.getButtonReleaseTime = function (
type,
modifier,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return this._releaseTime[key];
};
/**
* Signals that all of the events have been handled and the aggregator should be reset to handle new events.
*/
CameraEventAggregator.prototype.reset = function () {
for (const name in this._update) {
if (this._update.hasOwnProperty(name)) {
this._update[name] = true;
}
}
};
/**
* 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 CameraEventAggregator#destroy
*/
CameraEventAggregator.prototype.isDestroyed = function () {
return false;
};
/**
* Removes mouse listeners held by this object.
* <br /><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
* handler = handler && handler.destroy();
*
* @see CameraEventAggregator#isDestroyed
*/
CameraEventAggregator.prototype.destroy = function () {
this._eventHandler = this._eventHandler && this._eventHandler.destroy();
return destroyObject(this);
};
export default CameraEventAggregator;
+52
View File
@@ -0,0 +1,52 @@
// @ts-check
/**
* Enumerates the available input for interacting with the camera.
*
* @enum {number}
*/
const CameraEventType = {
/**
* A left mouse button press followed by moving the mouse and releasing the button.
*
* @type {number}
* @constant
*/
LEFT_DRAG: 0,
/**
* A right mouse button press followed by moving the mouse and releasing the button.
*
* @type {number}
* @constant
*/
RIGHT_DRAG: 1,
/**
* A middle mouse button press followed by moving the mouse and releasing the button.
*
* @type {number}
* @constant
*/
MIDDLE_DRAG: 2,
/**
* Scrolling the middle mouse button.
*
* @type {number}
* @constant
*/
WHEEL: 3,
/**
* A two-finger touch on a touch surface.
*
* @type {number}
* @constant
*/
PINCH: 4,
};
Object.freeze(CameraEventType);
export default CameraEventType;
+576
View File
@@ -0,0 +1,576 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import EasingFunction from "../Core/EasingFunction.js";
import CesiumMath from "../Core/Math.js";
import PerspectiveFrustum from "../Core/PerspectiveFrustum.js";
import PerspectiveOffCenterFrustum from "../Core/PerspectiveOffCenterFrustum.js";
import SceneMode from "./SceneMode.js";
/**
* Creates tweens for camera flights.
* <br /><br />
* Mouse interaction is disabled during flights.
*
* @private
*/
const CameraFlightPath = {};
function getAltitude(frustum, dx, dy) {
let near;
let top;
let right;
if (frustum instanceof PerspectiveFrustum) {
const tanTheta = Math.tan(0.5 * frustum.fovy);
near = frustum.near;
top = frustum.near * tanTheta;
right = frustum.aspectRatio * top;
return Math.max((dx * near) / right, (dy * near) / top);
} else if (frustum instanceof PerspectiveOffCenterFrustum) {
near = frustum.near;
top = frustum.top;
right = frustum.right;
return Math.max((dx * near) / right, (dy * near) / top);
}
return Math.max(dx, dy);
}
const scratchCart = new Cartesian3();
const scratchCart2 = new Cartesian3();
function createPitchFunction(
startPitch,
endPitch,
heightFunction,
pitchAdjustHeight,
) {
if (defined(pitchAdjustHeight) && heightFunction(0.5) > pitchAdjustHeight) {
const startHeight = heightFunction(0.0);
const endHeight = heightFunction(1.0);
const middleHeight = heightFunction(0.5);
const d1 = middleHeight - startHeight;
const d2 = middleHeight - endHeight;
return function (time) {
const altitude = heightFunction(time);
if (time <= 0.5) {
const t1 = (altitude - startHeight) / d1;
return CesiumMath.lerp(startPitch, -CesiumMath.PI_OVER_TWO, t1);
}
const t2 = (altitude - endHeight) / d2;
return CesiumMath.lerp(-CesiumMath.PI_OVER_TWO, endPitch, 1 - t2);
};
}
return function (time) {
return CesiumMath.lerp(startPitch, endPitch, time);
};
}
function createHeightFunction(
camera,
destination,
startHeight,
endHeight,
optionAltitude,
) {
let altitude = optionAltitude;
const maxHeight = Math.max(startHeight, endHeight);
if (!defined(altitude)) {
const start = camera.position;
const end = destination;
const up = camera.up;
const right = camera.right;
const frustum = camera.frustum;
const diff = Cartesian3.subtract(start, end, scratchCart);
const verticalDistance = Cartesian3.magnitude(
Cartesian3.multiplyByScalar(up, Cartesian3.dot(diff, up), scratchCart2),
);
const horizontalDistance = Cartesian3.magnitude(
Cartesian3.multiplyByScalar(
right,
Cartesian3.dot(diff, right),
scratchCart2,
),
);
altitude = Math.min(
getAltitude(frustum, verticalDistance, horizontalDistance) * 0.2,
1000000000.0,
);
}
if (maxHeight < altitude) {
const power = 8.0;
const factor = 1000000.0;
const s = -Math.pow((altitude - startHeight) * factor, 1.0 / power);
const e = Math.pow((altitude - endHeight) * factor, 1.0 / power);
return function (t) {
const x = t * (e - s) + s;
return -Math.pow(x, power) / factor + altitude;
};
}
return function (t) {
return CesiumMath.lerp(startHeight, endHeight, t);
};
}
function adjustAngleForLERP(startAngle, endAngle) {
if (
CesiumMath.equalsEpsilon(
startAngle,
CesiumMath.TWO_PI,
CesiumMath.EPSILON11,
)
) {
startAngle = 0.0;
}
if (endAngle > startAngle + Math.PI) {
startAngle += CesiumMath.TWO_PI;
} else if (endAngle < startAngle - Math.PI) {
startAngle -= CesiumMath.TWO_PI;
}
return startAngle;
}
const scratchStart = new Cartesian3();
function createUpdateCV(
scene,
duration,
destination,
heading,
pitch,
roll,
optionAltitude,
optionPitchAdjustHeight,
) {
const camera = scene.camera;
const start = Cartesian3.clone(camera.position, scratchStart);
const startPitch = camera.pitch;
const startHeading = adjustAngleForLERP(camera.heading, heading);
const startRoll = adjustAngleForLERP(camera.roll, roll);
const heightFunction = createHeightFunction(
camera,
destination,
start.z,
destination.z,
optionAltitude,
);
const pitchFunction = createPitchFunction(
startPitch,
pitch,
heightFunction,
optionPitchAdjustHeight,
);
function update(value) {
const time = value.time / duration;
camera.setView({
orientation: {
heading: CesiumMath.lerp(startHeading, heading, time),
pitch: pitchFunction(time),
roll: CesiumMath.lerp(startRoll, roll, time),
},
});
Cartesian2.lerp(start, destination, time, camera.position);
camera.position.z = heightFunction(time);
}
return update;
}
function useLongestFlight(startCart, destCart) {
if (startCart.longitude < destCart.longitude) {
startCart.longitude += CesiumMath.TWO_PI;
} else {
destCart.longitude += CesiumMath.TWO_PI;
}
}
function useShortestFlight(startCart, destCart) {
const diff = startCart.longitude - destCart.longitude;
if (diff < -CesiumMath.PI) {
startCart.longitude += CesiumMath.TWO_PI;
} else if (diff > CesiumMath.PI) {
destCart.longitude += CesiumMath.TWO_PI;
}
}
const scratchStartCart = new Cartographic();
const scratchEndCart = new Cartographic();
function createUpdate3D(
scene,
duration,
destination,
heading,
pitch,
roll,
optionAltitude,
optionFlyOverLongitude,
optionFlyOverLongitudeWeight,
optionPitchAdjustHeight,
) {
const camera = scene.camera;
const projection = scene.mapProjection;
const ellipsoid = projection.ellipsoid;
const startCart = Cartographic.clone(
camera.positionCartographic,
scratchStartCart,
);
const startPitch = camera.pitch;
const startHeading = adjustAngleForLERP(camera.heading, heading);
const startRoll = adjustAngleForLERP(camera.roll, roll);
const destCart = ellipsoid.cartesianToCartographic(
destination,
scratchEndCart,
);
startCart.longitude = CesiumMath.zeroToTwoPi(startCart.longitude);
destCart.longitude = CesiumMath.zeroToTwoPi(destCart.longitude);
let useLongFlight = false;
if (defined(optionFlyOverLongitude)) {
const hitLon = CesiumMath.zeroToTwoPi(optionFlyOverLongitude);
const lonMin = Math.min(startCart.longitude, destCart.longitude);
const lonMax = Math.max(startCart.longitude, destCart.longitude);
const hitInside = hitLon >= lonMin && hitLon <= lonMax;
if (defined(optionFlyOverLongitudeWeight)) {
// Distance inside (0...2Pi)
const din = Math.abs(startCart.longitude - destCart.longitude);
// Distance outside (0...2Pi)
const dot = CesiumMath.TWO_PI - din;
const hitDistance = hitInside ? din : dot;
const offDistance = hitInside ? dot : din;
if (
hitDistance < offDistance * optionFlyOverLongitudeWeight &&
!hitInside
) {
useLongFlight = true;
}
} else if (!hitInside) {
useLongFlight = true;
}
}
if (useLongFlight) {
useLongestFlight(startCart, destCart);
} else {
useShortestFlight(startCart, destCart);
}
const heightFunction = createHeightFunction(
camera,
destination,
startCart.height,
destCart.height,
optionAltitude,
);
const pitchFunction = createPitchFunction(
startPitch,
pitch,
heightFunction,
optionPitchAdjustHeight,
);
// Isolate scope for update function.
// to have local copies of vars used in lerp
// Othervise, if you call nex
// createUpdate3D (createAnimationTween)
// before you played animation, variables will be overwriten.
function isolateUpdateFunction() {
const startLongitude = startCart.longitude;
const destLongitude = destCart.longitude;
const startLatitude = startCart.latitude;
const destLatitude = destCart.latitude;
return function update(value) {
const time = value.time / duration;
const position = Cartesian3.fromRadians(
CesiumMath.lerp(startLongitude, destLongitude, time),
CesiumMath.lerp(startLatitude, destLatitude, time),
heightFunction(time),
ellipsoid,
);
camera.setView({
destination: position,
orientation: {
heading: CesiumMath.lerp(startHeading, heading, time),
pitch: pitchFunction(time),
roll: CesiumMath.lerp(startRoll, roll, time),
},
});
};
}
return isolateUpdateFunction();
}
function createUpdate2D(
scene,
duration,
destination,
heading,
pitch,
roll,
optionAltitude,
) {
const camera = scene.camera;
const start = Cartesian3.clone(camera.position, scratchStart);
const startHeading = adjustAngleForLERP(camera.heading, heading);
const startHeight = camera.frustum.right - camera.frustum.left;
const heightFunction = createHeightFunction(
camera,
destination,
startHeight,
destination.z,
optionAltitude,
);
function update(value) {
const time = value.time / duration;
camera.setView({
orientation: {
heading: CesiumMath.lerp(startHeading, heading, time),
},
});
Cartesian2.lerp(start, destination, time, camera.position);
const zoom = heightFunction(time);
const frustum = camera.frustum;
const ratio = frustum.top / frustum.right;
const incrementAmount = (zoom - (frustum.right - frustum.left)) * 0.5;
frustum.right += incrementAmount;
frustum.left -= incrementAmount;
frustum.top = ratio * frustum.right;
frustum.bottom = -frustum.top;
}
return update;
}
const scratchCartographic = new Cartographic();
const scratchDestination = new Cartesian3();
function emptyFlight(complete, cancel) {
return {
startObject: {},
stopObject: {},
duration: 0.0,
complete: complete,
cancel: cancel,
};
}
function wrapCallback(controller, cb) {
function wrapped() {
if (typeof cb === "function") {
cb();
}
controller.enableInputs = true;
}
return wrapped;
}
CameraFlightPath.createTween = function (scene, options) {
options = options ?? Frozen.EMPTY_OBJECT;
let destination = options.destination;
//>>includeStart('debug', pragmas.debug);
if (!defined(scene)) {
throw new DeveloperError("scene is required.");
}
if (!defined(destination)) {
throw new DeveloperError("destination is required.");
}
//>>includeEnd('debug');
const mode = scene.mode;
if (mode === SceneMode.MORPHING) {
return emptyFlight();
}
const convert = options.convert ?? true;
const projection = scene.mapProjection;
const ellipsoid = projection.ellipsoid;
const maximumHeight = options.maximumHeight;
const flyOverLongitude = options.flyOverLongitude;
const flyOverLongitudeWeight = options.flyOverLongitudeWeight;
const pitchAdjustHeight = options.pitchAdjustHeight;
let easingFunction = options.easingFunction;
if (convert && mode !== SceneMode.SCENE3D) {
ellipsoid.cartesianToCartographic(destination, scratchCartographic);
destination = projection.project(scratchCartographic, scratchDestination);
}
const camera = scene.camera;
const transform = options.endTransform;
if (defined(transform)) {
camera._setTransform(transform);
}
let duration = options.duration;
if (!defined(duration)) {
duration =
Math.ceil(Cartesian3.distance(camera.position, destination) / 1000000.0) +
2.0;
duration = Math.min(duration, 3.0);
}
const heading = options.heading ?? 0.0;
const pitch = options.pitch ?? -CesiumMath.PI_OVER_TWO;
const roll = options.roll ?? 0.0;
const controller = scene.screenSpaceCameraController;
controller.enableInputs = false;
const complete = wrapCallback(controller, options.complete);
const cancel = wrapCallback(controller, options.cancel);
const frustum = camera.frustum;
let empty = scene.mode === SceneMode.SCENE2D;
empty =
empty &&
Cartesian2.equalsEpsilon(camera.position, destination, CesiumMath.EPSILON6);
empty =
empty &&
CesiumMath.equalsEpsilon(
Math.max(frustum.right - frustum.left, frustum.top - frustum.bottom),
destination.z,
CesiumMath.EPSILON6,
);
empty =
empty ||
(scene.mode !== SceneMode.SCENE2D &&
Cartesian3.equalsEpsilon(
destination,
camera.position,
CesiumMath.EPSILON10,
));
empty =
empty &&
CesiumMath.equalsEpsilon(
CesiumMath.negativePiToPi(heading),
CesiumMath.negativePiToPi(camera.heading),
CesiumMath.EPSILON10,
) &&
CesiumMath.equalsEpsilon(
CesiumMath.negativePiToPi(pitch),
CesiumMath.negativePiToPi(camera.pitch),
CesiumMath.EPSILON10,
) &&
CesiumMath.equalsEpsilon(
CesiumMath.negativePiToPi(roll),
CesiumMath.negativePiToPi(camera.roll),
CesiumMath.EPSILON10,
);
if (empty) {
return emptyFlight(complete, cancel);
}
const updateFunctions = new Array(4);
updateFunctions[SceneMode.SCENE2D] = createUpdate2D;
updateFunctions[SceneMode.SCENE3D] = createUpdate3D;
updateFunctions[SceneMode.COLUMBUS_VIEW] = createUpdateCV;
if (duration <= 0.0) {
const newOnComplete = function () {
const update = updateFunctions[mode](
scene,
1.0,
destination,
heading,
pitch,
roll,
maximumHeight,
flyOverLongitude,
flyOverLongitudeWeight,
pitchAdjustHeight,
);
update({ time: 1.0 });
if (typeof complete === "function") {
complete();
}
};
return emptyFlight(newOnComplete, cancel);
}
const update = updateFunctions[mode](
scene,
duration,
destination,
heading,
pitch,
roll,
maximumHeight,
flyOverLongitude,
flyOverLongitudeWeight,
pitchAdjustHeight,
);
if (!defined(easingFunction)) {
const startHeight = camera.positionCartographic.height;
const endHeight =
mode === SceneMode.SCENE3D
? ellipsoid.cartesianToCartographic(destination).height
: destination.z;
if (startHeight > endHeight && startHeight > 11500.0) {
easingFunction = EasingFunction.CUBIC_OUT;
} else {
easingFunction = EasingFunction.QUINTIC_IN_OUT;
}
}
return {
duration: duration,
easingFunction: easingFunction,
startObject: {
time: 0.0,
},
stopObject: {
time: duration,
},
update: update,
complete: complete,
cancel: cancel,
};
};
export default CameraFlightPath;
@@ -0,0 +1,44 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
/**
* Simple abstraction for a group. This class exists to make the metadata API
* more consistent, i.e. metadata can be accessed via
* <code>content.group.metadata</code> much like tile metadata is accessed as
* <code>tile.metadata</code>.
*
* @param {object} options Object with the following properties:
* @param {GroupMetadata} options.metadata The metadata associated with this group.
*
* @alias Cesium3DContentGroup
* @constructor
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DContentGroup(options) {
options = options ?? Frozen.EMPTY_OBJECT;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.metadata", options.metadata);
//>>includeEnd('debug');
this._metadata = options.metadata;
}
Object.defineProperties(Cesium3DContentGroup.prototype, {
/**
* Get the metadata for this group
*
* @memberof Cesium3DContentGroup.prototype
*
* @type {GroupMetadata}
*
* @readonly
*/
metadata: {
get: function () {
return this._metadata;
},
},
});
export default Cesium3DContentGroup;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
// @ts-check
/**
* Defines how per-feature colors set from the Cesium API or declarative styling blend with the source colors from
* the original feature, e.g. glTF material or per-point color in the tile.
* <p>
* When <code>REPLACE</code> or <code>MIX</code> are used and the source color is a glTF material, the technique must assign the
* <code>_3DTILESDIFFUSE</code> semantic to the diffuse color parameter. Otherwise only <code>HIGHLIGHT</code> is supported.
* </p>
* <p>
* A feature whose color evaluates to white (1.0, 1.0, 1.0) is always rendered without color blending, regardless of the
* tileset's color blend mode.
* </p>
* <pre><code>
* "techniques": {
* "technique0": {
* "parameters": {
* "diffuse": {
* "semantic": "_3DTILESDIFFUSE",
* "type": 35666
* }
* }
* }
* }
* </code></pre>
*
* @enum {number}
*/
const Cesium3DTileColorBlendMode = {
/**
* Multiplies the source color by the feature color.
*
* @type {number}
* @constant
*/
HIGHLIGHT: 0,
/**
* Replaces the source color with the feature color.
*
* @type {number}
* @constant
*/
REPLACE: 1,
/**
* Blends the source color and feature color together.
*
* @type {number}
* @constant
*/
MIX: 2,
};
Object.freeze(Cesium3DTileColorBlendMode);
export default Cesium3DTileColorBlendMode;
@@ -0,0 +1,356 @@
// @ts-check
import DeveloperError from "../Core/DeveloperError.js";
/** @import Cartesian3 from "../Core/Cartesian3.js"; */
/** @import Cesium3DContentGroup from "./Cesium3DContentGroup.js"; */
/** @import Cesium3DTile from "./Cesium3DTile.js"; */
/** @import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; */
/** @import Cesium3DTileFeature from "./Cesium3DTileFeature.js"; */
/** @import Cesium3DTileStyle from "./Cesium3DTileStyle.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/** @import Color from "../Core/Color.js"; */
/** @import FrameState from "./FrameState.js"; */
/** @import ImplicitMetadataView from "./ImplicitMetadataView.js"; */
/** @import Ray from "../Core/Ray.js"; */
/**
* The content of a tile in a {@link Cesium3DTileset}.
* <p>
* Derived classes of this interface provide access to individual features in the tile.
* Access derived objects through {@link Cesium3DTile#content}.
* </p>
* <p>
* This type describes an interface and is not intended to be instantiated directly.
* </p>
*
* @interface
*/
class Cesium3DTileContent {
constructor() {
/**
* Gets or sets if any feature's property changed. Used to
* optimized applying a style when a feature's property changed.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {boolean}
*
* @protected
* @ignore
*/
this.featurePropertiesDirty = false;
}
/**
* Gets the number of features in the tile.
*
*
* @type {number}
* @readonly
* @constant
*/
featuresLength;
/**
* Gets the number of points in the tile.
* <p>
* Only applicable for tiles with Point Cloud content. This is different than {@link Cesium3DTileContent#featuresLength} which
* equals the number of groups of points as distinguished by the <code>BATCH_ID</code> feature table semantic.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/PointCloud#batched-points}
*
*
* @type {number}
* @readonly
* @constant
*/
pointsLength;
/**
* Gets the number of triangles in the tile.
*
*
* @type {number}
* @readonly
* @constant
*/
trianglesLength;
/**
* Gets the tile's geometry memory in bytes.
*
*
* @type {number}
* @readonly
* @constant
*/
geometryByteLength;
/**
* Gets the tile's texture memory in bytes.
*
*
* @type {number}
* @readonly
* @constant
*/
texturesByteLength;
/**
* Gets the amount of memory used by the batch table textures and any binary
* metadata properties not accounted for in geometryByteLength or
* texturesByteLength
*
*
* @type {number}
* @readonly
* @constant
*/
batchTableByteLength;
/**
* Gets the array of {@link Cesium3DTileContent} objects for contents that contain other contents, such as composite tiles. The inner contents may in turn have inner contents, such as a composite tile that contains a composite tile.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Composite|Composite specification}
*
*
* @type {Array<*>}
* @readonly
* @constant
*/
innerContents;
/**
* Returns true when the tile's content is ready to render; otherwise false
*
*
* @type {boolean}
* @readonly
* @constant
*/
ready;
/**
* Gets the tileset for this tile.
*
*
* @type {Cesium3DTileset}
* @readonly
* @constant
*/
tileset;
/**
* Gets the tile containing this content.
*
*
* @type {Cesium3DTile}
* @readonly
* @constant
*/
tile;
/**
* Gets the url of the tile's content.
*
* @type {string}
* @readonly
* @constant
*/
url;
/**
* Gets the batch table for this content.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {Cesium3DTileBatchTable}
* @readonly
* @constant
*
* @private
*/
batchTable;
/**
* Gets the metadata for this content, whether it is available explicitly or via
* implicit tiling. If there is no metadata, this property should be undefined.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {ImplicitMetadataView|undefined}
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
metadata;
/**
* Gets the group for this content if the content has metadata (3D Tiles 1.1) or
* if it uses the <code>3DTILES_metadata</code> extension. If neither are present,
* this property should be undefined.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {Cesium3DContentGroup|undefined}
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
group;
/**
* Returns whether the feature has this property.
*
* @param {number} batchId The batchId for the feature.
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} <code>true</code> if the feature has this property; otherwise, <code>false</code>.
*/
hasProperty(batchId, name) {
DeveloperError.throwInstantiationError();
}
/**
* Returns the {@link Cesium3DTileFeature} object for the feature with the
* given <code>batchId</code>. This object is used to get and modify the
* feature's properties.
* <p>
* Features in a tile are ordered by <code>batchId</code>, an index used to retrieve their metadata from the batch table.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/BatchTable}.
*
* @param {number} batchId The batchId for the feature.
* @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object.
*
* @exception {DeveloperError} batchId must be between zero and {@link Cesium3DTileContent#featuresLength} - 1.
*/
getFeature(batchId) {
DeveloperError.throwInstantiationError();
}
/**
* Called when {@link Cesium3DTileset#debugColorizeTiles} changes.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {boolean} enabled Whether to enable or disable debug settings.
* @param {Color} color Debug color.
* @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object.
* @private
*/
applyDebugSettings(enabled, color) {
DeveloperError.throwInstantiationError();
}
/**
* Apply a style to the content
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {Cesium3DTileStyle} style The style.
* @returns {void}
*
* @private
*/
applyStyle(style) {
DeveloperError.throwInstantiationError();
}
/**
* Called by the tile during tileset traversal to get the draw commands needed to render this content.
* When the tile's content is in the PROCESSING state, this creates WebGL resources to ultimately
* move to the READY state.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {Cesium3DTileset} tileset The tileset containing this tile.
* @param {FrameState} frameState The frame state.
* @returns {void}
*
* @private
*/
update(tileset, frameState) {
DeveloperError.throwInstantiationError();
}
/**
* Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
*
* @param {Ray} ray The ray to test for intersection.
* @param {FrameState} frameState The frame state.
* @param {Cartesian3|undefined} [result] The intersection or <code>undefined</code> if none was found.
* @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
*
* @private
*/
pick(ray, frameState, result) {
DeveloperError.throwInstantiationError();
}
/**
* 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.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see Cesium3DTileContent#destroy
*
* @private
*/
isDestroyed() {
DeveloperError.throwInstantiationError();
}
/**
* 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 /><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.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* content = content && content.destroy();
*
* @see Cesium3DTileContent#isDestroyed
*
* @returns {void}
*
* @private
*/
destroy() {
DeveloperError.throwInstantiationError();
}
}
export default Cesium3DTileContent;
@@ -0,0 +1,135 @@
import Composite3DTileContent from "./Composite3DTileContent.js";
import Geometry3DTileContent from "./Geometry3DTileContent.js";
import Implicit3DTileContent from "./Implicit3DTileContent.js";
import Model3DTileContent from "./Model/Model3DTileContent.js";
import Tileset3DTileContent from "./Tileset3DTileContent.js";
import Vector3DTileContent from "./Vector3DTileContent.js";
import VectorGltf3DTileContent from "./VectorGltf3DTileContent.js";
import GaussianSplat3DTileContent from "./GaussianSplat3DTileContent.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Maps a tile's magic field in its header to a new content object for the tile's payload.
*
* @private
*/
const Cesium3DTileContentFactory = {
b3dm: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Model3DTileContent.fromB3dm(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
pnts: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Model3DTileContent.fromPnts(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
i3dm: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Model3DTileContent.fromI3dm(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
cmpt: function (tileset, tile, resource, arrayBuffer, byteOffset) {
// Send in the factory in order to avoid a cyclical dependency
return Composite3DTileContent.fromTileType(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
Cesium3DTileContentFactory,
);
},
externalTileset: function (tileset, tile, resource, json) {
return Tileset3DTileContent.fromJson(tileset, tile, resource, json);
},
geom: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return new Geometry3DTileContent(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
vctr: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return new Vector3DTileContent(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
subt: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Implicit3DTileContent.fromSubtreeJson(
tileset,
tile,
resource,
undefined,
arrayBuffer,
byteOffset,
);
},
subtreeJson: function (tileset, tile, resource, json) {
return Implicit3DTileContent.fromSubtreeJson(tileset, tile, resource, json);
},
glb: function (tileset, tile, resource, arrayBuffer, byteOffset) {
const arrayBufferByteLength = arrayBuffer.byteLength;
if (arrayBufferByteLength < 12) {
throw new RuntimeError("Invalid glb content");
}
const dataView = new DataView(arrayBuffer, byteOffset);
const byteLength = dataView.getUint32(8, true);
const glb = new Uint8Array(arrayBuffer, byteOffset, byteLength);
if (
GaussianSplat3DTileContent.tilesetRequiresGaussianSplattingExt(tileset)
) {
return GaussianSplat3DTileContent.fromGltf(tileset, tile, resource, glb);
}
// @deprecated CESIUM_mesh_vector to be removed after v1.142 release.
if (
tileset.hasExtension("3DTILES_content_gltf_vector") ||
tileset.isGltfExtensionUsed("CESIUM_mesh_vector")
) {
return VectorGltf3DTileContent.fromGltf(tileset, tile, resource, glb);
}
return Model3DTileContent.fromGltf(tileset, tile, resource, glb);
},
gltf: function (tileset, tile, resource, json) {
if (
GaussianSplat3DTileContent.tilesetRequiresGaussianSplattingExt(tileset)
) {
return GaussianSplat3DTileContent.fromGltf(tileset, tile, resource, json);
}
// @deprecated CESIUM_mesh_vector to be removed after v1.142 release.
if (
tileset.hasExtension("3DTILES_content_gltf_vector") ||
tileset.isGltfExtensionUsed("CESIUM_mesh_vector")
) {
return VectorGltf3DTileContent.fromGltf(tileset, tile, resource, json);
}
return Model3DTileContent.fromGltf(tileset, tile, resource, json);
},
geoJson: function (tileset, tile, resource, json) {
return Model3DTileContent.fromGeoJson(tileset, tile, resource, json);
},
};
export default Cesium3DTileContentFactory;
@@ -0,0 +1,17 @@
// @ts-check
/**
* @private
*/
const Cesium3DTileContentState = {
UNLOADED: 0, // Has never been requested
LOADING: 1, // Is waiting on a pending request
PROCESSING: 2, // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data.
READY: 3, // Ready to render.
EXPIRED: 4, // Is expired and will be unloaded once new content is loaded.
FAILED: 5, // Request failed.
};
Object.freeze(Cesium3DTileContentState);
export default Cesium3DTileContentState;
@@ -0,0 +1,181 @@
/**
* An enum to indicate the different types of {@link Cesium3DTileContent}.
* For binary files, the enum value is the magic number of the binary file
* unless otherwise noted. For JSON files, the enum value is a unique name
* for internal use.
*
* @enum {string}
* @see Cesium3DTileContent
*
* @private
*/
const Cesium3DTileContentType = {
/**
* A Batched 3D Model. This is a binary format with
* magic number <code>b3dm</code>
*
* @type {string}
* @constant
* @private
*/
BATCHED_3D_MODEL: "b3dm",
/**
* An Instanced 3D Model. This is a binary format with magic number
* <code>i3dm</code>
*
* @type {string}
* @constant
* @private
*/
INSTANCED_3D_MODEL: "i3dm",
/**
* A Composite model. This is a binary format with magic number
* <code>cmpt</code>
*
* @type {string}
* @constant
* @private
*/
COMPOSITE: "cmpt",
/**
* A Point Cloud model. This is a binary format with magic number
* <code>pnts</code>
*
* @type {string}
* @constant
* @private
*/
POINT_CLOUD: "pnts",
/**
* Vector tiles. This is a binary format with magic number
* <code>vctr</code>
*
* @type {string}
* @constant
* @private
*/
VECTOR: "vctr",
/**
* Geometry tiles. This is a binary format with magic number
* <code>geom</code>
*
* @type {string}
* @constant
* @private
*/
GEOMETRY: "geom",
/**
* A glTF model in JSON + external BIN form. This is treated
* as a JSON format.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
GLTF: "gltf",
/**
* The binary form of a glTF file. Internally, the magic number is
* changed from <code>glTF</code> to <code>glb</code> to distinguish it from
* the JSON glTF format.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
GLTF_BINARY: "glb",
/**
* For implicit tiling, availability bitstreams are stored in binary subtree files.
* The magic number is <code>subt</code>
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
IMPLICIT_SUBTREE: "subt",
/**
* For implicit tiling. Subtrees can also be represented as JSON files.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
IMPLICIT_SUBTREE_JSON: "subtreeJson",
/**
* Contents can reference another tileset.json to use
* as an external tileset. This is a JSON-based format.
*
* @type {string}
* @constant
* @private
*/
EXTERNAL_TILESET: "externalTileset",
/**
* Multiple contents are handled separately from the other content types
* due to differences in request scheduling.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
MULTIPLE_CONTENT: "multipleContent",
/**
* GeoJSON content for <code>MAXAR_content_geojson</code> extension.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
GEOJSON: "geoJson",
/**
* Binary voxel content for <code>3DTILES_content_voxels</code> extension.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
VOXEL_BINARY: "voxl",
/**
* Binary voxel content for <code>3DTILES_content_voxels</code> extension.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
VOXEL_JSON: "voxelJson",
};
/**
* Check if a content is one of the supported binary formats. Otherwise,
* the caller can assume a JSON format.
* @param {Cesium3DTileContentType} contentType The content type of the content payload.
* @return {boolean} <code>true</code> if the content type is a binary format, or <code>false</code> if the content type is a JSON format.
* @private
*/
Cesium3DTileContentType.isBinaryFormat = function (contentType) {
switch (contentType) {
case Cesium3DTileContentType.BATCHED_3D_MODEL:
case Cesium3DTileContentType.INSTANCED_3D_MODEL:
case Cesium3DTileContentType.COMPOSITE:
case Cesium3DTileContentType.POINT_CLOUD:
case Cesium3DTileContentType.VECTOR:
case Cesium3DTileContentType.GEOMETRY:
case Cesium3DTileContentType.IMPLICIT_SUBTREE:
case Cesium3DTileContentType.VOXEL_BINARY:
case Cesium3DTileContentType.GLTF_BINARY:
return true;
default:
return false;
}
};
Object.freeze(Cesium3DTileContentType);
export default Cesium3DTileContentType;
@@ -0,0 +1,427 @@
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
/** @import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; */
/** @import Cesium3DTileContent from "./Cesium3DTileContent.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/**
* A feature of a {@link Cesium3DTileset}.
* <p>
* Provides access to a feature's properties stored in the tile's batch table, as well
* as the ability to show/hide a feature and change its highlight color via
* {@link Cesium3DTileFeature#show} and {@link Cesium3DTileFeature#color}, respectively.
* </p>
* <p>
* Modifications to a <code>Cesium3DTileFeature</code> object have the lifetime of the tile's
* content. If the tile's content is unloaded, e.g., due to it going out of view and needing
* to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any
* modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications.
* </p>
* <p>
* Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature}
* or picking using {@link Scene#pick}.
* </p>
*
* @example
* // On mouse over, display all the properties for a feature in the console log.
* handler.setInputAction(function(movement) {
* const feature = scene.pick(movement.endPosition);
* if (feature instanceof Cesium.Cesium3DTileFeature) {
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
* }
* }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
*/
class Cesium3DTileFeature {
/**
* @param {Cesium3DTileContent} content
* @param {number} batchId
*/
constructor(content, batchId) {
this._content = content;
this._batchId = batchId;
this._color = undefined; // for calling getColor
}
/**
* Gets or sets if the feature will be shown. This is set for all features
* when a style's show is evaluated.
*
* @type {boolean}
*
* @default true
*/
get show() {
return this._content.batchTable.getShow(this._batchId);
}
set show(value) {
this._content.batchTable.setShow(this._batchId, value);
}
/**
* Gets or sets the highlight color multiplied with the feature's color. When
* this is white, the feature's color is not changed. This is set for all features
* when a style's color is evaluated.
*
* @type {Color}
*
* @default {@link Color.WHITE}
*/
get color() {
if (!defined(this._color)) {
this._color = new Color();
}
return this._content.batchTable.getColor(this._batchId, this._color);
}
set color(value) {
this._content.batchTable.setColor(this._batchId, value);
}
/**
* Gets a typed array containing the ECEF positions of the polyline.
* Returns undefined if {@link Cesium3DTileset#vectorKeepDecodedPositions} is false
* or the feature is not a polyline in a vector tile.
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*
* @type {Float64Array}
*/
get polylinePositions() {
if (!defined(this._content.getPolylinePositions)) {
return undefined;
}
return this._content.getPolylinePositions(this._batchId);
}
/**
* Gets the content of the tile containing the feature.
*
* @type {Cesium3DTileContent}
*
* @readonly
* @private
*/
get content() {
return this._content;
}
/**
* Gets the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get tileset() {
return this._content.tileset;
}
/**
* All objects returned by {@link Scene#pick} have a <code>primitive</code> property. This returns
* the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get primitive() {
return this._content.tileset;
}
/**
* Get the feature ID associated with this feature. For 3D Tiles 1.0, the
* batch ID is returned. For EXT_mesh_features, this is the feature ID from
* the selected feature ID set.
*
* @type {number}
*
* @readonly
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
get featureId() {
return this._batchId;
}
/**
* @private
*/
get pickId() {
return this._content.batchTable.getPickColor(this._batchId);
}
/**
* Returns whether the feature contains this property. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} Whether the feature contains this property.
*/
hasProperty(name) {
return this._content.batchTable.hasProperty(this._batchId, name);
}
/**
* Returns an array of property IDs for the feature. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The IDs of the feature's properties.
*/
getPropertyIds(results) {
return this._content.batchTable.getPropertyIds(this._batchId, results);
}
/**
* Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @example
* // Display all the properties for a feature in the console log.
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
*/
getProperty(name) {
return this._content.batchTable.getProperty(this._batchId, name);
}
/**
* Returns a copy of the feature's property with the given name, examining all
* the metadata from 3D Tiles 1.0 formats, the EXT_structural_metadata and legacy
* EXT_feature_metadata glTF extensions, and the metadata present either in the
* tileset JSON (3D Tiles 1.1) or in the 3DTILES_metadata 3D Tiles extension.
* Metadata is checked against name from most specific to most general and the
* first match is returned. Metadata is checked in this order:
*
* <ol>
* <li>Batch table (structural metadata) property by semantic</li>
* <li>Batch table (structural metadata) property by property ID</li>
* <li>Content metadata property by semantic</li>
* <li>Content metadata property by property</li>
* <li>Tile metadata property by semantic</li>
* <li>Tile metadata property by property ID</li>
* <li>Subtree metadata property by semantic</li>
* <li>Subtree metadata property by property ID</li>
* <li>Group metadata property by semantic</li>
* <li>Group metadata property by property ID</li>
* <li>Tileset metadata property by semantic</li>
* <li>Tileset metadata property by property ID</li>
* <li>Otherwise, return undefined</li>
* </ol>
* <p>
* For 3D Tiles Next details, see the {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata|3DTILES_metadata Extension}
* for 3D Tiles, as well as the {@link https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_structural_metadata|EXT_structural_metadata Extension}
* for glTF. For the legacy glTF extension, see {@link https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_feature_metadata|EXT_feature_metadata Extension}
* </p>
*
* @param {Cesium3DTileContent} content The content for accessing the metadata
* @param {number} batchId The batch ID (or feature ID) of the feature to get a property for
* @param {string} name The semantic or property ID of the feature. Semantics are checked before property IDs in each granularity of metadata.
* @privateParam {Cesium3DTileBatchTable} [batchTable] Batch table in which to look up the feature property. If unspecified, `content.batchTable` is used.
* @return {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
static getPropertyInherited(
content,
batchId,
name,
batchTable = content.batchTable,
) {
if (defined(batchTable)) {
if (batchTable.hasPropertyBySemantic(batchId, name)) {
return batchTable.getPropertyBySemantic(batchId, name);
}
if (batchTable.hasProperty(batchId, name)) {
return batchTable.getProperty(batchId, name);
}
}
const contentMetadata = content.metadata;
if (defined(contentMetadata)) {
if (contentMetadata.hasPropertyBySemantic(name)) {
return contentMetadata.getPropertyBySemantic(name);
}
if (contentMetadata.hasProperty(name)) {
return contentMetadata.getProperty(name);
}
}
const tile = content.tile;
const tileMetadata = tile.metadata;
if (defined(tileMetadata)) {
if (tileMetadata.hasPropertyBySemantic(name)) {
return tileMetadata.getPropertyBySemantic(name);
}
if (tileMetadata.hasProperty(name)) {
return tileMetadata.getProperty(name);
}
}
let subtreeMetadata;
if (defined(tile.implicitSubtree)) {
subtreeMetadata = tile.implicitSubtree.metadata;
}
if (defined(subtreeMetadata)) {
if (subtreeMetadata.hasPropertyBySemantic(name)) {
return subtreeMetadata.getPropertyBySemantic(name);
}
if (subtreeMetadata.hasProperty(name)) {
return subtreeMetadata.getProperty(name);
}
}
const groupMetadata = defined(content.group)
? content.group.metadata
: undefined;
if (defined(groupMetadata)) {
if (groupMetadata.hasPropertyBySemantic(name)) {
return groupMetadata.getPropertyBySemantic(name);
}
if (groupMetadata.hasProperty(name)) {
return groupMetadata.getProperty(name);
}
}
const tilesetMetadata = content.tileset.metadata;
if (defined(tilesetMetadata)) {
if (tilesetMetadata.hasPropertyBySemantic(name)) {
return tilesetMetadata.getPropertyBySemantic(name);
}
if (tilesetMetadata.hasProperty(name)) {
return tilesetMetadata.getProperty(name);
}
}
return undefined;
}
/**
* Returns a copy of the value of the feature's property with the given name.
* If the feature is contained within a tileset that has metadata (3D Tiles 1.1)
* or uses the <code>3DTILES_metadata</code> extension, tileset, group and tile
* metadata is inherited.
* <p>
* To resolve name conflicts, this method resolves names from most specific to
* least specific by metadata granularity in the order: feature, tile, group,
* tileset. Within each granularity, semantics are resolved first, then other
* properties.
* </p>
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
*/
getPropertyInherited(name) {
return Cesium3DTileFeature.getPropertyInherited(
this._content,
this._batchId,
name,
);
}
/**
* Sets the value of the feature's property with the given name.
* <p>
* If a property with the given name doesn't exist, it is created.
* </p>
*
* @param {string} name The case-sensitive name of the property.
* @param {*} value The value of the property that will be copied.
*
* @exception {DeveloperError} Inherited batch table hierarchy property is read only.
*
* @example
* const height = feature.getProperty('Height'); // e.g., the height of a building
*
* @example
* const name = 'clicked';
* if (feature.getProperty(name)) {
* console.log('already clicked');
* } else {
* feature.setProperty(name, true);
* console.log('first click');
* }
*/
setProperty(name, value) {
this._content.batchTable.setProperty(this._batchId, name, value);
// PERFORMANCE_IDEA: Probably overkill, but maybe only mark the tile dirty if the
// property is in one of the style's expressions or - if it can be done quickly -
// if the new property value changed the result of an expression.
this._content.featurePropertiesDirty = true;
}
/**
* Returns whether the feature's class name equals <code>className</code>. Unlike {@link Cesium3DTileFeature#isClass}
* this function only checks the feature's exact class and not inherited classes.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class name equals <code>className</code>
*
* @private
*/
isExactClass(className) {
return this._content.batchTable.isExactClass(this._batchId, className);
}
/**
* Returns whether the feature's class or any inherited classes are named <code>className</code>.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class or inherited classes are named <code>className</code>
*
* @private
*/
isClass(className) {
return this._content.batchTable.isClass(this._batchId, className);
}
/**
* Returns the feature's class name.
* <p>
* This function returns <code>undefined</code> if no batch table hierarchy is present.
* </p>
*
* @returns {string} The feature's class name.
*
* @private
*/
getExactClassName() {
return this._content.batchTable.getExactClassName(this._batchId);
}
}
export default Cesium3DTileFeature;
@@ -0,0 +1,131 @@
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
/**
* @private
*/
function Cesium3DTileFeatureTable(featureTableJson, featureTableBinary) {
this.json = featureTableJson;
this.buffer = featureTableBinary;
this._cachedTypedArrays = {};
this.featuresLength = 0;
}
function getTypedArrayFromBinary(
featureTable,
semantic,
componentType,
componentLength,
count,
byteOffset,
) {
const cachedTypedArrays = featureTable._cachedTypedArrays;
let typedArray = cachedTypedArrays[semantic];
if (!defined(typedArray)) {
typedArray = ComponentDatatype.createArrayBufferView(
componentType,
featureTable.buffer.buffer,
featureTable.buffer.byteOffset + byteOffset,
count * componentLength,
);
cachedTypedArrays[semantic] = typedArray;
}
return typedArray;
}
function getTypedArrayFromArray(featureTable, semantic, componentType, array) {
const cachedTypedArrays = featureTable._cachedTypedArrays;
let typedArray = cachedTypedArrays[semantic];
if (!defined(typedArray)) {
typedArray = ComponentDatatype.createTypedArray(componentType, array);
cachedTypedArrays[semantic] = typedArray;
}
return typedArray;
}
Cesium3DTileFeatureTable.prototype.getGlobalProperty = function (
semantic,
componentType,
componentLength,
) {
const jsonValue = this.json[semantic];
if (!defined(jsonValue)) {
return undefined;
}
if (defined(jsonValue.byteOffset)) {
componentType = componentType ?? ComponentDatatype.UNSIGNED_INT;
componentLength = componentLength ?? 1;
return getTypedArrayFromBinary(
this,
semantic,
componentType,
componentLength,
1,
jsonValue.byteOffset,
);
}
return jsonValue;
};
Cesium3DTileFeatureTable.prototype.hasProperty = function (semantic) {
return defined(this.json[semantic]);
};
Cesium3DTileFeatureTable.prototype.getPropertyArray = function (
semantic,
componentType,
componentLength,
) {
const jsonValue = this.json[semantic];
if (!defined(jsonValue)) {
return undefined;
}
if (defined(jsonValue.byteOffset)) {
if (defined(jsonValue.componentType)) {
componentType = ComponentDatatype.fromName(jsonValue.componentType);
}
return getTypedArrayFromBinary(
this,
semantic,
componentType,
componentLength,
this.featuresLength,
jsonValue.byteOffset,
);
}
return getTypedArrayFromArray(this, semantic, componentType, jsonValue);
};
Cesium3DTileFeatureTable.prototype.getProperty = function (
semantic,
componentType,
componentLength,
featureId,
result,
) {
const jsonValue = this.json[semantic];
if (!defined(jsonValue)) {
return undefined;
}
const typedArray = this.getPropertyArray(
semantic,
componentType,
componentLength,
);
if (componentLength === 1) {
return typedArray[featureId];
}
for (let i = 0; i < componentLength; ++i) {
result[i] = typedArray[componentLength * featureId + i];
}
return result;
};
export default Cesium3DTileFeatureTable;
@@ -0,0 +1,18 @@
// @ts-check
/**
* Hint defining optimization support for a 3D tile
*
* @enum {number}
*
* @private
*/
const Cesium3DTileOptimizationHint = {
NOT_COMPUTED: -1,
USE_OPTIMIZATION: 1,
SKIP_OPTIMIZATION: 0,
};
Object.freeze(Cesium3DTileOptimizationHint);
export default Cesium3DTileOptimizationHint;
@@ -0,0 +1,115 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import Cesium3DTileOptimizationHint from "./Cesium3DTileOptimizationHint.js";
import TileBoundingRegion from "./TileBoundingRegion.js";
import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js";
/**
* Utility functions for computing optimization hints for a {@link Cesium3DTileset}.
*
* @namespace Cesium3DTileOptimizations
*
* @private
*/
const Cesium3DTileOptimizations = {};
const scratchAxis = new Cartesian3();
/**
* Evaluates support for the childrenWithinParent optimization. This is used to more tightly cull tilesets if
* children bounds are fully contained within the parent. Currently, support for the optimization only works for
* oriented bounding boxes, so both the child and parent tile must be either a {@link TileOrientedBoundingBox} or
* {@link TileBoundingRegion}. The purpose of this check is to prevent use of a culling optimization when the child
* bounds exceed those of the parent. If the child bounds are greater, it is more likely that the optimization will
* waste CPU cycles. Bounding spheres are not supported for the reason that the child bounds can very often be
* partially outside of the parent bounds.
*
* @param {Cesium3DTile} tile The tile to check.
* @returns {boolean} Whether the childrenWithinParent optimization is supported.
*/
Cesium3DTileOptimizations.checkChildrenWithinParent = function (tile) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("tile", tile);
//>>includeEnd('debug');
const children = tile.children;
const length = children.length;
// Check if the parent has an oriented bounding box.
const boundingVolume = tile.boundingVolume;
if (
boundingVolume instanceof TileOrientedBoundingBox ||
boundingVolume instanceof TileBoundingRegion
) {
const orientedBoundingBox = boundingVolume._orientedBoundingBox;
tile._optimChildrenWithinParent =
Cesium3DTileOptimizationHint.USE_OPTIMIZATION;
for (let i = 0; i < length; ++i) {
const child = children[i];
// Check if the child has an oriented bounding box.
const childBoundingVolume = child.boundingVolume;
if (
!(
childBoundingVolume instanceof TileOrientedBoundingBox ||
childBoundingVolume instanceof TileBoundingRegion
)
) {
// Do not support if the parent and child both do not have oriented bounding boxes.
tile._optimChildrenWithinParent =
Cesium3DTileOptimizationHint.SKIP_OPTIMIZATION;
break;
}
const childOrientedBoundingBox = childBoundingVolume._orientedBoundingBox;
// Compute the axis from the parent to the child.
const axis = Cartesian3.subtract(
childOrientedBoundingBox.center,
orientedBoundingBox.center,
scratchAxis,
);
const axisLength = Cartesian3.magnitude(axis);
Cartesian3.divideByScalar(axis, axisLength, axis);
// Project the bounding box of the parent onto the axis. Because the axis is a ray from the parent
// to the child, the projection parameterized along the ray will be (+/- proj1).
const proj1 =
Math.abs(orientedBoundingBox.halfAxes[0] * axis.x) +
Math.abs(orientedBoundingBox.halfAxes[1] * axis.y) +
Math.abs(orientedBoundingBox.halfAxes[2] * axis.z) +
Math.abs(orientedBoundingBox.halfAxes[3] * axis.x) +
Math.abs(orientedBoundingBox.halfAxes[4] * axis.y) +
Math.abs(orientedBoundingBox.halfAxes[5] * axis.z) +
Math.abs(orientedBoundingBox.halfAxes[6] * axis.x) +
Math.abs(orientedBoundingBox.halfAxes[7] * axis.y) +
Math.abs(orientedBoundingBox.halfAxes[8] * axis.z);
// Project the bounding box of the child onto the axis. Because the axis is a ray from the parent
// to the child, the projection parameterized along the ray will be (+/- proj2) + axis.length.
const proj2 =
Math.abs(childOrientedBoundingBox.halfAxes[0] * axis.x) +
Math.abs(childOrientedBoundingBox.halfAxes[1] * axis.y) +
Math.abs(childOrientedBoundingBox.halfAxes[2] * axis.z) +
Math.abs(childOrientedBoundingBox.halfAxes[3] * axis.x) +
Math.abs(childOrientedBoundingBox.halfAxes[4] * axis.y) +
Math.abs(childOrientedBoundingBox.halfAxes[5] * axis.z) +
Math.abs(childOrientedBoundingBox.halfAxes[6] * axis.x) +
Math.abs(childOrientedBoundingBox.halfAxes[7] * axis.y) +
Math.abs(childOrientedBoundingBox.halfAxes[8] * axis.z);
// If the child extends the parent's bounds, the optimization is not valid and we skip it.
if (proj1 <= proj2 + axisLength) {
tile._optimChildrenWithinParent =
Cesium3DTileOptimizationHint.SKIP_OPTIMIZATION;
break;
}
}
}
return (
tile._optimChildrenWithinParent ===
Cesium3DTileOptimizationHint.USE_OPTIMIZATION
);
};
export default Cesium3DTileOptimizations;
+83
View File
@@ -0,0 +1,83 @@
/**
* The pass in which a 3D Tileset is updated.
*
* @enum {number}
* @private
*/
const Cesium3DTilePass = {
RENDER: 0,
PICK: 1,
SHADOW: 2,
PRELOAD: 3,
PRELOAD_FLIGHT: 4,
REQUEST_RENDER_MODE_DEFER_CHECK: 5,
MOST_DETAILED_PRELOAD: 6,
MOST_DETAILED_PICK: 7,
NUMBER_OF_PASSES: 8,
};
const passOptions = new Array(Cesium3DTilePass.NUMBER_OF_PASSES);
passOptions[Cesium3DTilePass.RENDER] = Object.freeze({
pass: Cesium3DTilePass.RENDER,
isRender: true,
requestTiles: true,
ignoreCommands: false,
});
passOptions[Cesium3DTilePass.PICK] = Object.freeze({
pass: Cesium3DTilePass.PICK,
isRender: false,
requestTiles: false,
ignoreCommands: false,
});
passOptions[Cesium3DTilePass.SHADOW] = Object.freeze({
pass: Cesium3DTilePass.SHADOW,
isRender: false,
requestTiles: true,
ignoreCommands: false,
});
passOptions[Cesium3DTilePass.PRELOAD] = Object.freeze({
pass: Cesium3DTilePass.PRELOAD,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.PRELOAD_FLIGHT] = Object.freeze({
pass: Cesium3DTilePass.PRELOAD_FLIGHT,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.REQUEST_RENDER_MODE_DEFER_CHECK] = Object.freeze({
pass: Cesium3DTilePass.REQUEST_RENDER_MODE_DEFER_CHECK,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.MOST_DETAILED_PRELOAD] = Object.freeze({
pass: Cesium3DTilePass.MOST_DETAILED_PRELOAD,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.MOST_DETAILED_PICK] = Object.freeze({
pass: Cesium3DTilePass.MOST_DETAILED_PICK,
isRender: false,
requestTiles: false,
ignoreCommands: false,
});
Cesium3DTilePass.getPassOptions = function (pass) {
return passOptions[pass];
};
Object.freeze(Cesium3DTilePass);
export default Cesium3DTilePass;
@@ -0,0 +1,52 @@
import Check from "../Core/Check.js";
/**
* The state for a 3D Tiles update pass.
*
* @private
* @constructor
*/
function Cesium3DTilePassState(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options", options);
Check.typeOf.number("options.pass", options.pass);
//>>includeEnd('debug');
/**
* The pass.
*
* @type {Cesium3DTilePass}
*/
this.pass = options.pass;
/**
* An array of rendering commands to use instead of {@link FrameState.commandList} for the current pass.
*
* @type {DrawCommand[]}
*/
this.commandList = options.commandList;
/**
* A camera to use instead of {@link FrameState.camera} for the current pass.
*
* @type {Camera}
*/
this.camera = options.camera;
/**
* A culling volume to use instead of {@link FrameState.cullingVolume} for the current pass.
*
* @type {CullingVolume}
*/
this.cullingVolume = options.cullingVolume;
/**
* A read-only property that indicates whether the pass is ready, i.e. all tiles needed by the pass are loaded.
*
* @type {boolean}
* @readonly
* @default false
*/
this.ready = false;
}
export default Cesium3DTilePassState;
@@ -0,0 +1,778 @@
import Cartographic from "../Core/Cartographic.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import Cesium3DTileFeature from "./Cesium3DTileFeature.js";
import createBillboardPointCallback from "./createBillboardPointCallback.js";
/** @import Billboard from "./Billboard.js"; */
/** @import Cesium3DTileContent from "./Cesium3DTileContent.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/** @import DistanceDisplayCondition from "../Core/DistanceDisplayCondition.js"; */
/** @import HorizontalOrigin from "./HorizontalOrigin.js"; */
/** @import Label from "./Label.js"; */
/** @import NearFarScalar from "../Core/NearFarScalar.js"; */
/** @import Polyline from "./Polyline.js"; */
/** @import VerticalOrigin from "./VerticalOrigin.js"; */
/** @ignore */
const scratchCartographic = new Cartographic();
/**
* A point feature of a {@link Cesium3DTileset}.
* <p>
* Provides access to a feature's properties stored in the tile's batch table, as well
* as the ability to show/hide a feature and change its point properties
* </p>
* <p>
* Modifications to a <code>Cesium3DTilePointFeature</code> object have the lifetime of the tile's
* content. If the tile's content is unloaded, e.g., due to it going out of view and needing
* to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any
* modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications.
* </p>
* <p>
* Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature}
* or picking using {@link Scene#pick} and {@link Scene#pickPosition}.
* </p>
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*
* @example
* // On mouse over, display all the properties for a feature in the console log.
* handler.setInputAction(function(movement) {
* const feature = scene.pick(movement.endPosition);
* if (feature instanceof Cesium.Cesium3DTilePointFeature) {
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
* }
* }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
*/
class Cesium3DTilePointFeature {
static defaultColor = Color.WHITE;
static defaultPointOutlineColor = Color.BLACK;
static defaultPointOutlineWidth = 0.0;
static defaultPointSize = 8.0;
/**
* @param {Cesium3DTileContent} content
* @param {number} batchId
* @param {Billboard} billboard
* @param {Label} label
* @param {Polyline} polyline
*/
constructor(content, batchId, billboard, label, polyline) {
this._content = content;
this._billboard = billboard;
this._label = label;
this._polyline = polyline;
this._batchId = batchId;
this._billboardImage = undefined;
this._billboardColor = undefined;
this._billboardOutlineColor = undefined;
this._billboardOutlineWidth = undefined;
this._billboardSize = undefined;
this._pointSize = undefined;
this._color = undefined;
this._pointSize = undefined;
this._pointOutlineColor = undefined;
this._pointOutlineWidth = undefined;
this._heightOffset = undefined;
this._pickIds = new Array(3);
setBillboardImage(this);
}
/**
* Gets or sets if the feature will be shown. This is set for all features
* when a style's show is evaluated.
*
* @type {boolean}
*
* @default true
*/
get show() {
return this._label.show;
}
set show(value) {
this._label.show = value;
this._billboard.show = value;
this._polyline.show = value;
}
/**
* Gets or sets the color of the point of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {Color}
*/
get color() {
return this._color;
}
set color(value) {
this._color = Color.clone(value, this._color);
setBillboardImage(this);
}
/**
* Gets or sets the point size of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {number}
*/
get pointSize() {
return this._pointSize;
}
set pointSize(value) {
this._pointSize = value;
setBillboardImage(this);
}
/**
* Gets or sets the point outline color of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {Color}
*/
get pointOutlineColor() {
return this._pointOutlineColor;
}
set pointOutlineColor(value) {
this._pointOutlineColor = Color.clone(value, this._pointOutlineColor);
setBillboardImage(this);
}
/**
* Gets or sets the point outline width in pixels of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {number}
*/
get pointOutlineWidth() {
return this._pointOutlineWidth;
}
set pointOutlineWidth(value) {
this._pointOutlineWidth = value;
setBillboardImage(this);
}
/**
* Gets or sets the label color of this feature.
* <p>
* The color will be applied to the label if <code>labelText</code> is defined.
* </p>
*
* @type {Color}
*/
get labelColor() {
return this._label.fillColor;
}
set labelColor(value) {
this._label.fillColor = value;
this._polyline.show = this._label.show && value.alpha > 0.0;
}
/**
* Gets or sets the label outline color of this feature.
* <p>
* The outline color will be applied to the label if <code>labelText</code> is defined.
* </p>
*
* @type {Color}
*/
get labelOutlineColor() {
return this._label.outlineColor;
}
set labelOutlineColor(value) {
this._label.outlineColor = value;
}
/**
* Gets or sets the outline width in pixels of this feature.
* <p>
* The outline width will be applied to the point if <code>labelText</code> is defined.
* </p>
*
* @type {number}
*/
get labelOutlineWidth() {
return this._label.outlineWidth;
}
set labelOutlineWidth(value) {
this._label.outlineWidth = value;
}
/**
* Gets or sets the font of this feature.
* <p>
* Only applied when the <code>labelText</code> is defined.
* </p>
*
* @type {string}
*/
get font() {
return this._label.font;
}
set font(value) {
this._label.font = value;
}
/**
* Gets or sets the fill and outline style of this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {LabelStyle}
*/
get labelStyle() {
return this._label.style;
}
set labelStyle(value) {
this._label.style = value;
}
/**
* Gets or sets the text for this feature.
*
* @type {string}
*/
get labelText() {
return this._label.text;
}
set labelText(value) {
if (!defined(value)) {
value = "";
}
this._label.text = value;
}
/**
* Gets or sets the background color of the text for this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {Color}
*/
get backgroundColor() {
return this._label.backgroundColor;
}
set backgroundColor(value) {
this._label.backgroundColor = value;
}
/**
* Gets or sets the background padding of the text for this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {Cartesian2}
*/
get backgroundPadding() {
return this._label.backgroundPadding;
}
set backgroundPadding(value) {
this._label.backgroundPadding = value;
}
/**
* Gets or sets whether to display the background of the text for this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {boolean}
*/
get backgroundEnabled() {
return this._label.showBackground;
}
set backgroundEnabled(value) {
this._label.showBackground = value;
}
/**
* Gets or sets the near and far scaling properties for this feature.
*
* @type {NearFarScalar}
*/
get scaleByDistance() {
return this._label.scaleByDistance;
}
set scaleByDistance(value) {
this._label.scaleByDistance = value;
this._billboard.scaleByDistance = value;
}
/**
* Gets or sets the near and far translucency properties for this feature.
*
* @type {NearFarScalar}
*/
get translucencyByDistance() {
return this._label.translucencyByDistance;
}
set translucencyByDistance(value) {
this._label.translucencyByDistance = value;
this._billboard.translucencyByDistance = value;
}
/**
* Gets or sets the condition specifying at what distance from the camera that this feature will be displayed.
*
* @type {DistanceDisplayCondition}
*/
get distanceDisplayCondition() {
return this._label.distanceDisplayCondition;
}
set distanceDisplayCondition(value) {
this._label.distanceDisplayCondition = value;
this._polyline.distanceDisplayCondition = value;
this._billboard.distanceDisplayCondition = value;
}
/**
* Gets or sets the height offset in meters of this feature.
*
* @type {number}
*/
get heightOffset() {
return this._heightOffset;
}
set heightOffset(value) {
const offset = this._heightOffset ?? 0.0;
const ellipsoid = this._content.tileset.ellipsoid;
const cart = ellipsoid.cartesianToCartographic(
this._billboard.position,
scratchCartographic,
);
cart.height = cart.height - offset + value;
const newPosition = ellipsoid.cartographicToCartesian(cart);
this._billboard.position = newPosition;
this._label.position = this._billboard.position;
this._polyline.positions = [this._polyline.positions[0], newPosition];
this._heightOffset = value;
}
/**
* Gets or sets whether the anchor line is displayed.
* <p>
* Only applied when <code>heightOffset</code> is defined.
* </p>
*
* @type {boolean}
*/
get anchorLineEnabled() {
return this._polyline.show;
}
set anchorLineEnabled(value) {
this._polyline.show = value;
}
/**
* Gets or sets the color for the anchor line.
* <p>
* Only applied when <code>heightOffset</code> is defined.
* </p>
*
* @type {Color}
*/
get anchorLineColor() {
return this._polyline.material.uniforms.color;
}
set anchorLineColor(value) {
this._polyline.material.uniforms.color = Color.clone(
value,
this._polyline.material.uniforms.color,
);
}
/**
* Gets or sets the image of this feature.
*
* @type {string}
*/
get image() {
return this._billboardImage;
}
set image(value) {
const imageChanged = this._billboardImage !== value;
this._billboardImage = value;
if (imageChanged) {
setBillboardImage(this);
}
}
/**
* Gets or sets the distance where depth testing will be disabled.
*
* @type {number}
*/
get disableDepthTestDistance() {
return this._label.disableDepthTestDistance;
}
set disableDepthTestDistance(value) {
this._label.disableDepthTestDistance = value;
this._billboard.disableDepthTestDistance = value;
}
/**
* Gets or sets the horizontal origin of this point, which determines if the point is
* to the left, center, or right of its anchor position.
*
* @type {HorizontalOrigin}
*/
get horizontalOrigin() {
return this._billboard.horizontalOrigin;
}
set horizontalOrigin(value) {
this._billboard.horizontalOrigin = value;
}
/**
* Gets or sets the vertical origin of this point, which determines if the point is
* to the bottom, center, or top of its anchor position.
*
* @type {VerticalOrigin}
*/
get verticalOrigin() {
return this._billboard.verticalOrigin;
}
set verticalOrigin(value) {
this._billboard.verticalOrigin = value;
}
/**
* Gets or sets the horizontal origin of this point's text, which determines if the point's text is
* to the left, center, or right of its anchor position.
*
* @type {HorizontalOrigin}
*/
get labelHorizontalOrigin() {
return this._label.horizontalOrigin;
}
set labelHorizontalOrigin(value) {
this._label.horizontalOrigin = value;
}
/**
* Get or sets the vertical origin of this point's text, which determines if the point's text is
* to the bottom, center, top, or baseline of it's anchor point.
*
* @type {VerticalOrigin}
*/
get labelVerticalOrigin() {
return this._label.verticalOrigin;
}
set labelVerticalOrigin(value) {
this._label.verticalOrigin = value;
}
/**
* Gets the content of the tile containing the feature.
*
* @type {Cesium3DTileContent}
*
* @readonly
* @private
*/
get content() {
return this._content;
}
/**
* Gets the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get tileset() {
return this._content.tileset;
}
/**
* All objects returned by {@link Scene#pick} have a <code>primitive</code> property. This returns
* the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get primitive() {
return this._content.tileset;
}
/**
* @private
*/
get pickIds() {
const ids = this._pickIds;
ids[0] = this._billboard.pickId;
ids[1] = this._label.pickId;
ids[2] = this._polyline.pickId;
return ids;
}
/**
* Returns whether the feature contains this property. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} Whether the feature contains this property.
*/
hasProperty(name) {
return this._content.batchTable.hasProperty(this._batchId, name);
}
/**
* Returns an array of property IDs for the feature. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The IDs of the feature's properties.
*/
getPropertyIds(results) {
return this._content.batchTable.getPropertyIds(this._batchId, results);
}
/**
* Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @example
* // Display all the properties for a feature in the console log.
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId} : ${feature.getProperty(propertyId)}`);
* }
*/
getProperty(name) {
return this._content.batchTable.getProperty(this._batchId, name);
}
/**
* Returns a copy of the value of the feature's property with the given name.
* If the feature is contained within a tileset that has metadata (3D Tiles 1.1)
* or uses the <code>3DTILES_metadata</code> extension, tileset, group and tile metadata is
* inherited.
* <p>
* To resolve name conflicts, this method resolves names from most specific to
* least specific by metadata granularity in the order: feature, tile, group,
* tileset. Within each granularity, semantics are resolved first, then other
* properties.
* </p>
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
getPropertyInherited(name) {
return Cesium3DTileFeature.getPropertyInherited(
this._content,
this._batchId,
name,
);
}
/**
* Sets the value of the feature's property with the given name.
* <p>
* If a property with the given name doesn't exist, it is created.
* </p>
*
* @param {string} name The case-sensitive name of the property.
* @param {*} value The value of the property that will be copied.
*
* @exception {DeveloperError} Inherited batch table hierarchy property is read only.
*
* @example
* const height = feature.getProperty('Height'); // e.g., the height of a building
*
* @example
* const name = 'clicked';
* if (feature.getProperty(name)) {
* console.log('already clicked');
* } else {
* feature.setProperty(name, true);
* console.log('first click');
* }
*/
setProperty(name, value) {
this._content.batchTable.setProperty(this._batchId, name, value);
// PERFORMANCE_IDEA: Probably overkill, but maybe only mark the tile dirty if the
// property is in one of the style's expressions or - if it can be done quickly -
// if the new property value changed the result of an expression.
this._content.featurePropertiesDirty = true;
}
/**
* Returns whether the feature's class name equals <code>className</code>. Unlike {@link Cesium3DTilePointFeature#isClass}
* this function only checks the feature's exact class and not inherited classes.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class name equals <code>className</code>
*
* @private
*/
isExactClass(className) {
return this._content.batchTable.isExactClass(this._batchId, className);
}
/**
* Returns whether the feature's class or any inherited classes are named <code>className</code>.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class or inherited classes are named <code>className</code>
*
* @private
*/
isClass(className) {
return this._content.batchTable.isClass(this._batchId, className);
}
/**
* Returns the feature's class name.
* <p>
* This function returns <code>undefined</code> if no batch table hierarchy is present.
* </p>
*
* @returns {string} The feature's class name.
*
* @private
*/
getExactClassName() {
return this._content.batchTable.getExactClassName(this._batchId);
}
}
/**
* @param {Cesium3DTilePointFeature} feature
* @ignore
*/
function setBillboardImage(feature) {
const b = feature._billboard;
if (defined(feature._billboardImage) && feature._billboardImage !== b.image) {
b.image = feature._billboardImage;
return;
}
if (defined(feature._billboardImage)) {
return;
}
const newColor = feature._color ?? Cesium3DTilePointFeature.defaultColor;
const newOutlineColor =
feature._pointOutlineColor ??
Cesium3DTilePointFeature.defaultPointOutlineColor;
const newOutlineWidth =
feature._pointOutlineWidth ??
Cesium3DTilePointFeature.defaultPointOutlineWidth;
const newPointSize =
feature._pointSize ?? Cesium3DTilePointFeature.defaultPointSize;
const currentColor = feature._billboardColor;
const currentOutlineColor = feature._billboardOutlineColor;
const currentOutlineWidth = feature._billboardOutlineWidth;
const currentPointSize = feature._billboardSize;
if (
Color.equals(newColor, currentColor) &&
Color.equals(newOutlineColor, currentOutlineColor) &&
newOutlineWidth === currentOutlineWidth &&
newPointSize === currentPointSize
) {
return;
}
feature._billboardColor = Color.clone(newColor, feature._billboardColor);
feature._billboardOutlineColor = Color.clone(
newOutlineColor,
feature._billboardOutlineColor,
);
feature._billboardOutlineWidth = newOutlineWidth;
feature._billboardSize = newPointSize;
const centerAlpha = newColor.alpha;
const cssColor = newColor.toCssColorString();
const cssOutlineColor = newOutlineColor.toCssColorString();
const textureId = JSON.stringify([
cssColor,
newPointSize,
cssOutlineColor,
newOutlineWidth,
]);
b.setImage(
textureId,
createBillboardPointCallback(
centerAlpha,
cssColor,
cssOutlineColor,
newOutlineWidth,
newPointSize,
),
);
}
export default Cesium3DTilePointFeature;
@@ -0,0 +1,34 @@
// @ts-check
/**
* The refinement approach for a tile.
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification#refinement|Refinement}
* in the 3D Tiles spec.
* </p>
*
* @enum {number}
*
* @private
*/
const Cesium3DTileRefine = {
/**
* Render this tile and, if it doesn't meet the screen space error, also refine to its children.
*
* @type {number}
* @constant
*/
ADD: 0,
/**
* Render this tile or, if it doesn't meet the screen space error, refine to its descendants instead.
*
* @type {number}
* @constant
*/
REPLACE: 1,
};
Object.freeze(Cesium3DTileRefine);
export default Cesium3DTileRefine;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
import defined from "../Core/defined.js";
/**
* @private
*/
function Cesium3DTileStyleEngine() {
this._style = undefined; // The style provided by the user
this._styleDirty = false; // true when the style is reassigned
this._lastStyleTime = 0; // The "time" when the last style was assigned
}
Object.defineProperties(Cesium3DTileStyleEngine.prototype, {
style: {
get: function () {
return this._style;
},
set: function (value) {
if (value === this._style) {
return;
}
this._style = value;
this._styleDirty = true;
},
},
});
Cesium3DTileStyleEngine.prototype.makeDirty = function () {
this._styleDirty = true;
};
Cesium3DTileStyleEngine.prototype.resetDirty = function () {
this._styleDirty = false;
};
Cesium3DTileStyleEngine.prototype.applyStyle = function (tileset) {
if (!defined(tileset.root)) {
return;
}
if (defined(this._style) && !this._style._ready) {
return;
}
const styleDirty = this._styleDirty;
if (styleDirty) {
// Increase "time", so the style is applied to all visible tiles
++this._lastStyleTime;
}
const lastStyleTime = this._lastStyleTime;
const statistics = tileset._statistics;
// If a new style was assigned, loop through all the visible tiles; otherwise, loop through
// only the tiles that are newly visible, i.e., they are visible this frame, but were not
// visible last frame. In many cases, the newly selected tiles list will be short or empty.
const tiles = styleDirty
? tileset._selectedTiles
: tileset._selectedTilesToStyle;
// PERFORMANCE_IDEA: does mouse-over picking basically trash this? We need to style on
// pick, for example, because a feature's show may be false.
const length = tiles.length;
for (let i = 0; i < length; ++i) {
const tile = tiles[i];
if (tile.lastStyleTime !== lastStyleTime) {
// Apply the style to this tile if it wasn't already applied because:
// 1) the user assigned a new style to the tileset
// 2) this tile is now visible, but it wasn't visible when the style was first assigned
const content = tile.content;
tile.lastStyleTime = lastStyleTime;
content.applyStyle(this._style);
statistics.numberOfFeaturesStyled += content.featuresLength;
++statistics.numberOfTilesStyled;
}
}
};
export default Cesium3DTileStyleEngine;
@@ -0,0 +1,586 @@
// @ts-check
import DeveloperError from "../Core/DeveloperError.js";
import BufferPoint from "./BufferPoint.js";
import BufferPointCollection from "./BufferPointCollection.js";
import BufferPointMaterial from "./BufferPointMaterial.js";
import BufferPolygon from "./BufferPolygon.js";
import BufferPolygonCollection from "./BufferPolygonCollection.js";
import BufferPolygonMaterial from "./BufferPolygonMaterial.js";
import BufferPolyline from "./BufferPolyline.js";
import BufferPolylineCollection from "./BufferPolylineCollection.js";
import BufferPolylineMaterial from "./BufferPolylineMaterial.js";
import Cesium3DTileFeature from "./Cesium3DTileFeature.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
/** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */
/** @import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; */
/** @import Cesium3DTileContent from "./Cesium3DTileContent.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/** @import VectorGltf3DTileContent from "./VectorGltf3DTileContent.js"; */
const point = new BufferPoint();
const polyline = new BufferPolyline();
const polygon = new BufferPolygon();
const pointMaterial = new BufferPointMaterial();
const polylineMaterial = new BufferPolylineMaterial();
const polygonMaterial = new BufferPolygonMaterial();
/**
* A vector feature of a {@link Cesium3DTileset}.
* <p>
* Provides access to a feature's properties stored in the tile's batch table, as well
* as the ability to show/hide and style the feature
* </p>
* <p>
* Modifications to a <code>Cesium3DTileVectorFeature</code> object have the lifetime of the tile's
* content. If the tile's content is unloaded, e.g., due to it going out of view and needing
* to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any
* modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications.
* </p>
* <p>
* Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature}
* or picking using {@link Scene#pick} and {@link Scene#pickPosition}.
* </p>
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*
* @example
* // On mouse over, display all the properties for a feature in the console log.
* handler.setInputAction(function(movement) {
* const feature = scene.pick(movement.endPosition);
* if (feature instanceof Cesium.Cesium3DTileVectorFeature) {
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
* }
* }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
*
* @ignore
*/
class Cesium3DTileVectorFeature {
/** @private */
_color = new Color();
/** @private */
_outlineColor = new Color();
/**
* @param {VectorGltf3DTileContent} content
* @param {number} batchId
* @param {number} [batchTableId=0]
*/
constructor(content, batchId, batchTableId = 0) {
this._content = content;
this._batchId = batchId;
this._batchTableId = batchTableId;
/**
* For each collection index N, this map returns the indices of all
* primitive in the collection associated with this feature.
* @type {Map<number, number[]>}
* @private
*/
this._primitivesByCollection = new Map();
}
/**
* @param {number} collectionIndex
* @param {number} primitiveIndex
*/
addPrimitiveByCollection(collectionIndex, primitiveIndex) {
let primitiveIndices = this._primitivesByCollection.get(collectionIndex);
if (!primitiveIndices) {
primitiveIndices = [];
this._primitivesByCollection.set(collectionIndex, primitiveIndices);
}
primitiveIndices.push(primitiveIndex);
}
/**
* @type {boolean}
* @default true
*/
get show() {
for (const prim of this._iteratePrimitives()) {
if (prim.show) {
return true;
}
}
return false;
}
set show(value) {
for (const prim of this._iteratePrimitives()) {
prim.show = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get color() {
for (const material of this._iterateMaterials()) {
return Color.clone(material.color, this._color);
}
return Color.clone(Color.WHITE, this._color);
}
set color(value) {
for (const material of this._iterateMaterials()) {
Color.clone(value, material.color);
}
}
/**
* @type {number}
* @default 1
*/
get pointSize() {
for (const material of this._iteratePointMaterials()) {
return material.size;
}
return 1;
}
set pointSize(value) {
for (const material of this._iteratePointMaterials()) {
material.size = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get pointOutlineColor() {
for (const material of this._iteratePointMaterials()) {
return Color.clone(material.outlineColor, this._outlineColor);
}
return Color.clone(Color.WHITE, this._outlineColor);
}
set pointOutlineColor(value) {
for (const material of this._iteratePointMaterials()) {
Color.clone(value, material.outlineColor);
}
}
/**
* @type {number}
* @default 0
*/
get pointOutlineWidth() {
for (const material of this._iteratePointMaterials()) {
return material.outlineWidth;
}
return 0;
}
set pointOutlineWidth(value) {
for (const material of this._iteratePointMaterials()) {
material.outlineWidth = value;
}
}
/**
* @type {number}
* @default 1
*/
get lineWidth() {
for (const material of this._iteratePolylineMaterials()) {
return material.width;
}
return 1;
}
set lineWidth(value) {
for (const material of this._iteratePolylineMaterials()) {
material.width = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get lineOutlineColor() {
for (const material of this._iteratePolylineMaterials()) {
return Color.clone(material.outlineColor, this._outlineColor);
}
return Color.clone(Color.WHITE, this._outlineColor);
}
set lineOutlineColor(value) {
for (const material of this._iteratePolylineMaterials()) {
Color.clone(value, material.outlineColor);
}
}
/**
* @type {number}
* @default 0
*/
get lineOutlineWidth() {
for (const material of this._iteratePolylineMaterials()) {
return material.outlineWidth;
}
return 0;
}
set lineOutlineWidth(value) {
for (const material of this._iteratePolylineMaterials()) {
material.outlineWidth = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get polygonOutlineColor() {
for (const material of this._iteratePolygonMaterials()) {
return Color.clone(material.outlineColor, this._outlineColor);
}
return Color.clone(Color.WHITE, this._outlineColor);
}
set polygonOutlineColor(value) {
for (const material of this._iteratePolygonMaterials()) {
Color.clone(value, material.outlineColor);
}
}
/**
* @type {number}
* @default 0
*/
get polygonOutlineWidth() {
for (const material of this._iteratePolygonMaterials()) {
return material.outlineWidth;
}
return 0;
}
set polygonOutlineWidth(value) {
for (const material of this._iteratePolygonMaterials()) {
material.outlineWidth = value;
}
}
/**
* Gets the content of the tile containing the feature.
*
* @type {VectorGltf3DTileContent}
*
* @ignore
*/
get content() {
return this._content;
}
/**
* Gets the tileset containing the feature.
*
* @type {Cesium3DTileset}
*/
get tileset() {
return this._content.tileset;
}
/**
* All objects returned by {@link Scene#pick} have a <code>primitive</code> property. This returns
* the tileset containing the feature.
*
* @type {Cesium3DTileset}
*/
get primitive() {
return this._content.tileset;
}
/**
* Get the feature ID associated with this feature. Using EXT_mesh_features,
* this is the feature ID from the selected feature ID set.
*
* @type {number}
*
* @readonly
*/
get featureId() {
return this._batchId;
}
/**
* @type {Cesium3DTileBatchTable|undefined}
* @private
*/
get _batchTable() {
return this._content.batchTables[this._batchTableId];
}
/**
* @type {number[]}
* @ignore
*/
get pickIds() {
const pickIds = [];
for (const prim of this._iteratePrimitives()) {
pickIds.push(prim._pickId);
}
return pickIds;
}
/**
* Returns whether the feature contains this property. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} Whether the feature contains this property.
*/
hasProperty(name) {
if (!defined(this._batchTable)) {
return false;
}
return this._batchTable.hasProperty(this._batchId, name);
}
/**
* Returns an array of property IDs for the feature. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The IDs of the feature's properties.
*/
getPropertyIds(results) {
if (!defined(this._batchTable)) {
return [];
}
return this._batchTable.getPropertyIds(this._batchId, results);
}
/**
* Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @example
* // Display all the properties for a feature in the console log.
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId} : ${feature.getProperty(propertyId)}`);
* }
*/
getProperty(name) {
if (!defined(this._batchTable)) {
return undefined;
}
return this._batchTable.getProperty(this._batchId, name);
}
/**
* Returns a copy of the value of the feature's property with the given name.
* If the feature is contained within a tileset that has metadata (3D Tiles 1.1)
* or uses the <code>3DTILES_metadata</code> extension, tileset, group and tile metadata is
* inherited.
* <p>
* To resolve name conflicts, this method resolves names from most specific to
* least specific by metadata granularity in the order: feature, tile, group,
* tileset. Within each granularity, semantics are resolved first, then other
* properties.
* </p>
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
getPropertyInherited(name) {
return Cesium3DTileFeature.getPropertyInherited(
// @ts-expect-error Requires type checking in Cesium3DTileContent.
this._content,
this._batchId,
name,
this._batchTable,
);
}
/**
* Sets the value of the feature's property with the given name.
* <p>
* If a property with the given name doesn't exist, it is created.
* </p>
*
* @param {string} name The case-sensitive name of the property.
* @param {*} value The value of the property that will be copied.
*
* @exception {DeveloperError} Inherited batch table hierarchy property is read only.
*
* @example
* const height = feature.getProperty('Height'); // e.g., the height of a building
*
* @example
* const name = 'clicked';
* if (feature.getProperty(name)) {
* console.log('already clicked');
* } else {
* feature.setProperty(name, true);
* console.log('first click');
* }
*/
setProperty(name, value) {
throw new DeveloperError("Not implemented");
}
/**
* Returns whether the feature's class name equals <code>className</code>. Unlike {@link Cesium3DTileVectorFeature#isClass}
* this function only checks the feature's exact class and not inherited classes.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class name equals <code>className</code>
*
* @private
*/
isExactClass(className) {
if (!defined(this._batchTable)) {
return false;
}
return this._batchTable.isExactClass(this._batchId, className);
}
/**
* Returns whether the feature's class or any inherited classes are named <code>className</code>.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class or inherited classes are named <code>className</code>
*
* @private
*/
isClass(className) {
if (!defined(this._batchTable)) {
return false;
}
return this._batchTable.isClass(this._batchId, className);
}
/**
* Returns the feature's class name.
* <p>
* This function returns <code>undefined</code> if no batch table hierarchy is present.
* </p>
*
* @returns {string} The feature's class name.
*
* @private
*/
getExactClassName() {
if (!defined(this._batchTable)) {
return undefined;
}
return this._batchTable.getExactClassName(this._batchId);
}
/////////////////////////////////////////////////////////////////////////////
// INTERNAL ITERATORS
/**
* @returns {Iterable<BufferPrimitive>}
*/
*_iteratePrimitives() {
yield* this._iteratePrimitivesWith(BufferPointCollection, point);
yield* this._iteratePrimitivesWith(BufferPolylineCollection, polyline);
yield* this._iteratePrimitivesWith(BufferPolygonCollection, polygon);
}
/**
* @param {*} CollectionType
* @param {BufferPrimitive} result
* @returns {Iterable<BufferPrimitive>}
*/
*_iteratePrimitivesWith(CollectionType, result) {
const collections = this._content._collections;
for (let i = 0; i < collections.length; i++) {
const collection = collections[i];
const primitiveIndices = this._primitivesByCollection.get(i);
if (primitiveIndices && collection instanceof CollectionType) {
for (const primitiveIndex of primitiveIndices) {
collection.get(primitiveIndex, result);
yield result;
}
}
}
}
/** @returns {Iterable<BufferPrimitiveMaterial>} */
*_iterateMaterials() {
yield* this._iteratePointMaterials();
yield* this._iteratePolylineMaterials();
yield* this._iteratePolygonMaterials();
}
/** @returns {Iterable<BufferPointMaterial>} */
*_iteratePointMaterials() {
yield* /** @type {Iterable<BufferPointMaterial>} */ (
this._iterateMaterialsWith(BufferPointCollection, point, pointMaterial)
);
}
/** @returns {Iterable<BufferPolylineMaterial>} */
*_iteratePolylineMaterials() {
yield* /** @type {Iterable<BufferPolylineMaterial>} */ (
this._iterateMaterialsWith(
BufferPolylineCollection,
polyline,
polylineMaterial,
)
);
}
/** @returns {Iterable<BufferPolygonMaterial>} */
*_iteratePolygonMaterials() {
yield* /** @type {Iterable<BufferPolygonMaterial>} */ (
this._iterateMaterialsWith(
BufferPolygonCollection,
polygon,
polygonMaterial,
)
);
}
/**
* @param {*} CollectionType
* @param {BufferPrimitive} primitive
* @param {BufferPrimitiveMaterial} result
* @returns {Iterable<BufferPrimitiveMaterial>}
*/
*_iterateMaterialsWith(CollectionType, primitive, result) {
for (const prim of this._iteratePrimitivesWith(CollectionType, primitive)) {
prim.getMaterial(result);
yield result;
prim.setMaterial(result);
}
}
}
export default Cesium3DTileVectorFeature;
@@ -0,0 +1,760 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Cesium3DTilesetMetadata from "./Cesium3DTilesetMetadata.js";
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import hasExtension from "./hasExtension.js";
import ImplicitSubtree from "./ImplicitSubtree.js";
import ImplicitSubtreeCache from "./ImplicitSubtreeCache.js";
import ImplicitTileCoordinates from "./ImplicitTileCoordinates.js";
import ImplicitTileset from "./ImplicitTileset.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import MetadataSemantic from "./MetadataSemantic.js";
import MetadataType from "./MetadataType.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import preprocess3DTileContent from "./preprocess3DTileContent.js";
import Resource from "../Core/Resource.js";
import ResourceCache from "./ResourceCache.js";
import RuntimeError from "../Core/RuntimeError.js";
import VoxelContent from "./VoxelContent.js";
import VoxelMetadataOrder from "./VoxelMetadataOrder.js";
import VoxelShapeType from "./VoxelShapeType.js";
import CesiumMath from "../Core/Math.js";
import Quaternion from "../Core/Quaternion.js";
/**
* @typedef {object} Cesium3DTilesVoxelProvider.ConstructorOptions
*
* Initialization options for the Cesium3DTilesVoxelProvider constructor
*
* @property {string} className The class in the tileset schema describing voxel metadata.
* @property {string[]} names The metadata names.
* @property {MetadataType[]} types The metadata types.
* @property {MetadataComponentType[]} componentTypes The metadata component types.
* @property {VoxelShapeType} shape The {@link VoxelShapeType}.
* @property {Cartesian3} dimensions The number of voxels per dimension of a tile. This is the same for all tiles in the dataset.
* @property {Cartesian3} [paddingBefore=Cartesian3.ZERO] The number of padding voxels before the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
* @property {Cartesian3} [paddingAfter=Cartesian3.ZERO] The number of padding voxels after the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
* @property {Matrix4} [globalTransform=Matrix4.IDENTITY] A transform from local space to global space.
* @property {Matrix4} [shapeTransform=Matrix4.IDENTITY] A transform from shape space to local space.
* @property {Cartesian3} [minBounds] The minimum bounds.
* @property {Cartesian3} [maxBounds] The maximum bounds.
* @property {number[][]} [minimumValues] The metadata minimum values.
* @property {number[][]} [maximumValues] The metadata maximum values.
* @property {number} [maximumTileCount] The maximum number of tiles that exist for this provider. This value is used as a hint to the voxel renderer to allocate an appropriate amount of GPU memory. If this value is not known it can be undefined.
*/
/**
* A {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset.
* <p>
* Implements the {@link VoxelProvider} interface.
* </p>
* <div class="notice">
* This object is normally not instantiated directly, use {@link Cesium3DTilesVoxelProvider.fromUrl}.
* </div>
*
* @alias Cesium3DTilesVoxelProvider
* @constructor
* @augments VoxelProvider
*
* @param {Cesium3DTilesVoxelProvider.ConstructorOptions} options An object describing initialization options
*
* @see Cesium3DTilesVoxelProvider.fromUrl
* @see VoxelProvider
* @see VoxelPrimitive
* @see VoxelShapeType
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DTilesVoxelProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const {
className,
names,
types,
componentTypes,
shape,
dimensions,
paddingBefore = Cartesian3.ZERO.clone(),
paddingAfter = Cartesian3.ZERO.clone(),
globalTransform = Matrix4.IDENTITY.clone(),
shapeTransform = Matrix4.IDENTITY.clone(),
minBounds,
maxBounds,
minimumValues,
maximumValues,
maximumTileCount,
} = options;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("className", className);
Check.typeOf.object("names", names);
Check.typeOf.object("types", types);
Check.typeOf.object("componentTypes", componentTypes);
Check.typeOf.string("shape", shape);
Check.typeOf.object("dimensions", dimensions);
//>>includeEnd('debug');
this._shapeTransform = shapeTransform;
this._globalTransform = globalTransform;
this._shape = shape;
this._minBounds = minBounds;
this._maxBounds = maxBounds;
this._dimensions = dimensions;
this._paddingBefore = paddingBefore;
this._paddingAfter = paddingAfter;
this._className = className;
this._names = names;
this._types = types;
this._componentTypes = componentTypes;
this._metadataOrder =
shape === VoxelShapeType.ELLIPSOID
? VoxelMetadataOrder.Z_UP
: VoxelMetadataOrder.Y_UP;
this._minimumValues = minimumValues;
this._maximumValues = maximumValues;
this._maximumTileCount = maximumTileCount;
this._availableLevels = undefined;
this._implicitTileset = undefined;
this._subtreeCache = new ImplicitSubtreeCache();
}
Object.defineProperties(Cesium3DTilesVoxelProvider.prototype, {
/**
* A transform from local space to global space.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
*/
globalTransform: {
get: function () {
return this._globalTransform;
},
},
/**
* A transform from shape space to local space.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
*/
shapeTransform: {
get: function () {
return this._shapeTransform;
},
},
/**
* Gets the {@link VoxelShapeType}
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {VoxelShapeType}
* @readonly
*/
shape: {
get: function () {
return this._shape;
},
},
/**
* Gets the minimum bounds.
* If undefined, the shape's default minimum bounds will be used instead.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3|undefined}
* @readonly
*/
minBounds: {
get: function () {
return this._minBounds;
},
},
/**
* Gets the maximum bounds.
* If undefined, the shape's default maximum bounds will be used instead.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3|undefined}
* @readonly
*/
maxBounds: {
get: function () {
return this._maxBounds;
},
},
/**
* Gets the number of voxels per dimension of a tile. This is the same for all tiles in the dataset.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3}
* @readonly
*/
dimensions: {
get: function () {
return this._dimensions;
},
},
/**
* Gets the number of padding voxels before the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3}
* @default Cartesian3.ZERO
* @readonly
*/
paddingBefore: {
get: function () {
return this._paddingBefore;
},
},
/**
* Gets the number of padding voxels after the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3}
* @default Cartesian3.ZERO
* @readonly
*/
paddingAfter: {
get: function () {
return this._paddingAfter;
},
},
/**
* The metadata class for this tileset.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {string}
* @readonly
*/
className: {
get: function () {
return this._className;
},
},
/**
* Gets the metadata names.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {string[]}
* @readonly
*/
names: {
get: function () {
return this._names;
},
},
/**
* Gets the metadata types.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {MetadataType[]}
* @readonly
*/
types: {
get: function () {
return this._types;
},
},
/**
* Gets the metadata component types.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {MetadataComponentType[]}
* @readonly
*/
componentTypes: {
get: function () {
return this._componentTypes;
},
},
/**
* Gets the ordering of the metadata in the buffers.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {VoxelMetadataOrder}
* @readonly
* @private
*/
metadataOrder: {
get: function () {
return this._metadataOrder;
},
},
/**
* Gets the metadata minimum values.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number[][]|undefined}
* @readonly
*/
minimumValues: {
get: function () {
return this._minimumValues;
},
},
/**
* Gets the metadata maximum values.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number[][]|undefined}
* @readonly
*/
maximumValues: {
get: function () {
return this._maximumValues;
},
},
/**
* The maximum number of tiles that exist for this provider.
* This value is used as a hint to the voxel renderer to allocate an appropriate amount of GPU memory.
* If this value is not known it can be undefined.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumTileCount: {
get: function () {
return this._maximumTileCount;
},
},
/**
* The number of levels of detail containing available tiles in the tileset.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number|undefined}
* @readonly
*/
availableLevels: {
get: function () {
return this._availableLevels;
},
},
});
/**
* Creates a {@link Cesium3DTilesVoxelProvider} that fetches voxel data from a 3D Tiles tileset.
*
* @param {Resource|string} url The URL to a tileset JSON file
* @returns {Promise<Cesium3DTilesVoxelProvider>} The created provider
*
* @exception {RuntimeException} Root must have content
* @exception {RuntimeException} Root tile content must have 3DTILES_content_voxels extension
* @exception {RuntimeException} Root tile must have implicit tiling
* @exception {RuntimeException} Tileset must have a metadata schema
* @exception {RuntimeException} Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider
*
* @example
* try {
* const voxelProvider = await Cesium3DTilesVoxelProvider.fromUrl(
* "http://localhost:8002/tilesets/voxel/tileset.json"
* );
* const voxelPrimitive = new VoxelPrimitive({
* provider: voxelProvider,
* customShader: customShader,
* });
* scene.primitives.add(voxelPrimitive);
* } catch (error) {
* console.error(`Error creating voxel primitive: ${error}`);
* }
*
* @see {@link VoxelPrimitive}
*/
Cesium3DTilesVoxelProvider.fromUrl = async function (url) {
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
//>>includeEnd('debug');
const resource = Resource.createIfNeeded(url);
const tilesetJson = await resource.fetchJson();
validate(tilesetJson);
const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
await schemaLoader.load();
const { root } = tilesetJson;
const metadataJson = hasExtension(tilesetJson, "3DTILES_metadata")
? tilesetJson.extensions["3DTILES_metadata"]
: tilesetJson;
const tilesetMetadata = new Cesium3DTilesetMetadata({
metadataJson: metadataJson,
schema: schemaLoader.schema,
});
const voxel = root.content.extensions["3DTILES_content_voxels"];
const className = voxel.class;
const providerOptions = getAttributeInfo(tilesetMetadata, className);
Object.assign(providerOptions, getShape(root));
if (defined(root.transform)) {
providerOptions.globalTransform = Matrix4.unpack(root.transform);
} else {
providerOptions.globalTransform = Matrix4.clone(Matrix4.IDENTITY);
}
providerOptions.dimensions = Cartesian3.unpack(voxel.dimensions);
providerOptions.maximumTileCount = getTileCount(tilesetMetadata);
if (defined(voxel.padding)) {
providerOptions.paddingBefore = Cartesian3.unpack(voxel.padding.before);
providerOptions.paddingAfter = Cartesian3.unpack(voxel.padding.after);
}
const provider = new Cesium3DTilesVoxelProvider(providerOptions);
const implicitTileset = new ImplicitTileset(
resource,
root,
schemaLoader.schema,
);
provider._implicitTileset = implicitTileset;
provider._availableLevels = implicitTileset.availableLevels;
ResourceCache.unload(schemaLoader);
return provider;
};
function getTileCount(metadata) {
if (!defined(metadata.tileset)) {
return undefined;
}
return metadata.tileset.getPropertyBySemantic(
MetadataSemantic.TILESET_TILE_COUNT,
);
}
function validate(tileset) {
const root = tileset.root;
if (!defined(root.content)) {
throw new RuntimeError("Root must have content");
}
if (!hasExtension(root.content, "3DTILES_content_voxels")) {
throw new RuntimeError(
"Root tile content must have 3DTILES_content_voxels extension",
);
}
if (
!hasExtension(root, "3DTILES_implicit_tiling") &&
!defined(root.implicitTiling)
) {
throw new RuntimeError("Root tile must have implicit tiling");
}
if (
!defined(tileset.schema) &&
!defined(tileset.schemaUri) &&
!hasExtension(tileset, "3DTILES_metadata")
) {
throw new RuntimeError("Tileset must have a metadata schema");
}
}
function getShape(tile) {
const boundingVolume = tile.boundingVolume;
if (defined(boundingVolume.box)) {
return getBoxShape(boundingVolume.box);
} else if (defined(boundingVolume.region)) {
return getEllipsoidShape(boundingVolume.region);
} else if (hasExtension(boundingVolume, "3DTILES_bounding_volume_cylinder")) {
return getCylinderShape(
boundingVolume.extensions["3DTILES_bounding_volume_cylinder"],
);
}
throw new RuntimeError(
"Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider",
);
}
function getEllipsoidShape(region) {
const west = region[0];
const south = region[1];
const east = region[2];
const north = region[3];
const minHeight = region[4];
const maxHeight = region[5];
const shapeTransform = Matrix4.fromScale(Ellipsoid.WGS84.radii);
const minBounds = new Cartesian3(west, south, minHeight);
const maxBounds = new Cartesian3(east, north, maxHeight);
return {
shape: VoxelShapeType.ELLIPSOID,
minBounds: minBounds,
maxBounds: maxBounds,
shapeTransform: shapeTransform,
};
}
const scratchScale = new Cartesian3();
const scratchRotation = new Matrix3();
function getBoxShape(box) {
const obb = OrientedBoundingBox.unpack(box);
const scale = Matrix3.getScale(obb.halfAxes, scratchScale);
const rotation = Matrix3.getRotation(obb.halfAxes, scratchRotation);
return {
shape: VoxelShapeType.BOX,
minBounds: Cartesian3.negate(scale, new Cartesian3()),
maxBounds: Cartesian3.clone(scale),
shapeTransform: Matrix4.fromRotationTranslation(rotation, obb.center),
};
}
function getCylinderShape(cylinder) {
const {
minRadius,
maxRadius,
height,
minAngle = -CesiumMath.PI,
maxAngle = CesiumMath.PI,
translation = [0, 0, 0],
rotation = [0, 0, 0, 1],
} = cylinder;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("minRadius", minRadius);
Check.typeOf.number("maxRadius", maxRadius);
Check.typeOf.number("height", height);
Check.typeOf.number("minAngle", minAngle);
Check.typeOf.number("maxAngle", maxAngle);
Check.typeOf.object("translation", translation);
Check.typeOf.object("rotation", rotation);
//>>includeEnd('debug');
const minHeight = -0.5 * height + translation[2];
const maxHeight = 0.5 * height + translation[2];
const shapeTransform = Matrix4.fromTranslationQuaternionRotationScale(
Cartesian3.unpack(translation),
Quaternion.unpack(rotation),
Cartesian3.ONE,
);
return {
shape: VoxelShapeType.CYLINDER,
minBounds: Cartesian3.fromElements(minRadius, minAngle, minHeight),
maxBounds: Cartesian3.fromElements(maxRadius, maxAngle, maxHeight),
shapeTransform: shapeTransform,
};
}
function getMetadataSchemaLoader(tilesetJson, resource) {
const { schemaUri, schema } = tilesetJson;
if (!defined(schemaUri)) {
return ResourceCache.getSchemaLoader({ schema });
}
return ResourceCache.getSchemaLoader({
resource: resource.getDerivedResource({
url: schemaUri,
}),
});
}
function getAttributeInfo(metadata, className) {
const { schema, statistics } = metadata;
const classStatistics = statistics?.classes[className];
const properties = schema.classes[className].properties;
const propertyInfo = Object.entries(properties).map(([id, property]) => {
const { type, componentType } = property;
const min = classStatistics?.properties[id].min;
const max = classStatistics?.properties[id].max;
const componentCount = MetadataType.getComponentCount(type);
const minValue = copyArray(min, componentCount);
const maxValue = copyArray(max, componentCount);
return { id, type, componentType, minValue, maxValue };
});
const names = propertyInfo.map((info) => info.id);
const types = propertyInfo.map((info) => info.type);
const componentTypes = propertyInfo.map((info) => info.componentType);
const minimumValues = propertyInfo.map((info) => info.minValue);
const maximumValues = propertyInfo.map((info) => info.maxValue);
const hasMinimumValues = minimumValues.some(defined);
return {
className,
names,
types,
componentTypes,
minimumValues: hasMinimumValues ? minimumValues : undefined,
maximumValues: hasMinimumValues ? maximumValues : undefined,
};
}
function copyArray(values, length) {
// Copy input values into a new array of a specified length.
// If the input is not an array, its value will be copied into the first element
// of the returned array. If the input is an array shorter than the returned
// array, the extra elements in the returned array will be undefined. If the
// input is undefined, the return will be undefined.
if (!defined(values)) {
return;
}
const valuesArray = Array.isArray(values) ? values : [values];
return Array.from({ length }, (v, i) => valuesArray[i]);
}
/**
* Get the subtree at a given subtree coordinate
* @param {VoxelProvider} provider The voxel provider
* @param {ImplicitTileCoordinates} subtreeCoord The coordinate at which to retrieve the subtree
* @returns {Promise<ImplicitSubtree>} The subtree at the given coordinate
* @private
*/
async function getSubtree(provider, subtreeCoord) {
const implicitTileset = provider._implicitTileset;
const subtreeCache = provider._subtreeCache;
// First load the subtree to check if the tile is available.
// If the subtree has been requested previously it might still be in the cache
let subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
return subtree;
}
const subtreeRelative = implicitTileset.subtreeUriTemplate.getDerivedResource(
{
templateValues: subtreeCoord.getTemplateValues(),
},
);
const subtreeResource = implicitTileset.baseResource.getDerivedResource({
url: subtreeRelative.url,
});
const arrayBuffer = await subtreeResource.fetchArrayBuffer();
// Check one more time if the subtree is in the cache.
// This could happen if there are two in-flight tile requests from the same
// subtree and one finishes before the other.
subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
return subtree;
}
const preprocessed = preprocess3DTileContent(arrayBuffer);
subtree = await ImplicitSubtree.fromSubtreeJson(
subtreeResource,
preprocessed.jsonPayload,
preprocessed.binaryPayload,
implicitTileset,
subtreeCoord,
);
subtreeCache.addSubtree(subtree);
return subtree;
}
/**
* Requests the data for a given tile.
*
* @param {object} [options] Object with the following properties:
* @param {number} [options.tileLevel=0] The tile's level.
* @param {number} [options.tileX=0] The tile's X coordinate.
* @param {number} [options.tileY=0] The tile's Y coordinate.
* @param {number} [options.tileZ=0] The tile's Z coordinate.
* @privateparam {number} [options.keyframe=0] The requested keyframe.
* @returns {Promise<VoxelContent>|undefined} A promise resolving to a VoxelContent containing the data for the tile, or undefined if the request could not be scheduled this frame.
*/
Cesium3DTilesVoxelProvider.prototype.requestData = async function (options) {
options = options ?? Frozen.EMPTY_OBJECT;
const {
tileLevel = 0,
tileX = 0,
tileY = 0,
tileZ = 0,
keyframe = 0,
} = options;
if (keyframe !== 0) {
return Promise.reject(
`3D Tiles currently doesn't support time-dynamic data.`,
);
}
// 1. Load the subtree that the tile belongs to (possibly from the subtree cache)
// 2. Load the voxel content if available
// Can't use a scratch variable here because the object is used inside the promise chain.
const implicitTileset = this._implicitTileset;
const tileCoordinates = new ImplicitTileCoordinates({
subdivisionScheme: implicitTileset.subdivisionScheme,
subtreeLevels: implicitTileset.subtreeLevels,
level: tileLevel,
x: tileX,
y: tileY,
z: tileZ,
});
// Find the coordinates of the parent subtree containing tileCoordinates
// If tileCoordinates is a subtree child, use that subtree
// If tileCoordinates is a subtree root, use its parent subtree
const isSubtreeRoot =
tileCoordinates.isSubtreeRoot() && tileCoordinates.level > 0;
const subtreeCoord = isSubtreeRoot
? tileCoordinates.getParentSubtreeCoordinates()
: tileCoordinates.getSubtreeCoordinates();
const that = this;
const subtree = await getSubtree(that, subtreeCoord);
// NOTE: these two subtree methods are ONLY used by voxels!
const isAvailable = isSubtreeRoot
? subtree.childSubtreeIsAvailableAtCoordinates
: subtree.tileIsAvailableAtCoordinates;
const available = isAvailable.call(subtree, tileCoordinates);
if (!available) {
return Promise.reject(
`Tile is not available at level ${tileLevel}, x ${tileX}, y ${tileY}, z ${tileZ}.`,
);
}
const { contentUriTemplates, baseResource } = implicitTileset;
const gltfRelative = contentUriTemplates[0].getDerivedResource({
templateValues: tileCoordinates.getTemplateValues(),
});
const gltfResource = baseResource.getDerivedResource({
url: gltfRelative.url,
});
return VoxelContent.fromGltf(gltfResource);
};
export default Cesium3DTilesVoxelProvider;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,291 @@
import defined from "../Core/defined.js";
import ManagedArray from "../Core/ManagedArray.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js";
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* A tile does not refine until all children are loaded.
* This is the traditional replacement refinement approach and is called the base traversal.
*
* @alias Cesium3DTilesetBaseTraversal
* @constructor
*
* @private
*/
function Cesium3DTilesetBaseTraversal() {}
const traversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
const emptyTraversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
*/
Cesium3DTilesetBaseTraversal.selectTiles = function (tileset, frameState) {
tileset._requestedTiles.length = 0;
if (tileset.debugFreezeFrame) {
return;
}
tileset._selectedTiles.length = 0;
tileset._selectedTilesToStyle.length = 0;
tileset._emptyTiles.length = 0;
tileset.hasMixedContent = false;
const root = tileset.root;
Cesium3DTilesetTraversal.updateTile(root, frameState);
if (!root.isVisible) {
return;
}
if (
root.getScreenSpaceError(frameState, true) <=
tileset.memoryAdjustedScreenSpaceError
) {
return;
}
executeTraversal(root, frameState);
traversal.stack.trim(traversal.stackMaximumLength);
emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength);
// Update the priority for any requests found during traversal
// Update after traversal so that min and max values can be used to normalize priority values
const requestedTiles = tileset._requestedTiles;
for (let i = 0; i < requestedTiles.length; ++i) {
requestedTiles[i].updatePriority();
}
};
/**
* Mark a tile as selected if it has content available.
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function selectDesiredTile(tile, frameState) {
if (tile.contentAvailable) {
Cesium3DTilesetTraversal.selectTile(tile, frameState);
}
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {ManagedArray} stack
* @param {FrameState} frameState
* @returns {boolean}
*/
function updateAndPushChildren(tile, stack, frameState) {
const replace = tile.refine === Cesium3DTileRefine.REPLACE;
const { tileset, children } = tile;
const { updateTile, loadTile, touchTile } = Cesium3DTilesetTraversal;
for (let i = 0; i < children.length; ++i) {
updateTile(children[i], frameState);
}
// Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail
children.sort(Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera);
// For traditional replacement refinement only refine if all children are loaded.
// Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space.
const checkRefines = replace && tile.hasRenderableContent;
let refines = true;
let anyChildrenVisible = false;
// Determining min child
let minIndex = -1;
let minimumPriority = Number.MAX_VALUE;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
stack.push(child);
if (child._foveatedFactor < minimumPriority) {
minIndex = i;
minimumPriority = child._foveatedFactor;
}
anyChildrenVisible = true;
} else if (checkRefines || tileset.loadSiblings) {
// Keep non-visible children loaded since they are still needed before the parent can refine.
// Or loadSiblings is true so always load tiles regardless of visibility.
if (child._foveatedFactor < minimumPriority) {
minIndex = i;
minimumPriority = child._foveatedFactor;
}
loadTile(child, frameState);
touchTile(child, frameState);
}
if (checkRefines) {
let childRefines;
if (!child._inRequestVolume) {
childRefines = false;
} else if (!child.hasRenderableContent) {
childRefines = executeEmptyTraversal(child, frameState);
} else {
childRefines = child.contentAvailable;
}
refines = refines && childRefines;
}
}
if (!anyChildrenVisible) {
refines = false;
}
if (minIndex !== -1 && replace) {
// An ancestor will hold the _foveatedFactor and _distanceToCamera for descendants between itself and its highest priority descendant. Siblings of a min children along the way use this ancestor as their priority holder as well.
// Priority of all tiles that refer to the _foveatedFactor and _distanceToCamera stored in the common ancestor will be differentiated based on their _depth.
const minPriorityChild = children[minIndex];
minPriorityChild._wasMinPriorityChild = true;
const priorityHolder =
(tile._wasMinPriorityChild || tile === tileset.root) &&
minimumPriority <= tile._priorityHolder._foveatedFactor
? tile._priorityHolder
: tile; // This is where priority dependency chains are wired up or started anew.
priorityHolder._foveatedFactor = Math.min(
minPriorityChild._foveatedFactor,
priorityHolder._foveatedFactor,
);
priorityHolder._distanceToCamera = Math.min(
minPriorityChild._distanceToCamera,
priorityHolder._distanceToCamera,
);
for (let i = 0; i < children.length; ++i) {
children[i]._priorityHolder = priorityHolder;
}
}
return refines;
}
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* A tile does not refine until all children are loaded.
* This is the traditional replacement refinement approach and is called the base traversal.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function executeTraversal(root, frameState) {
const { tileset } = root;
const { canTraverse, loadTile, visitTile, touchTile } =
Cesium3DTilesetTraversal;
const stack = traversal.stack;
stack.push(root);
while (stack.length > 0) {
traversal.stackMaximumLength = Math.max(
traversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const parent = tile.parent;
const parentRefines = !defined(parent) || parent._refines;
tile._refines = canTraverse(tile)
? updateAndPushChildren(tile, stack, frameState) && parentRefines
: false;
const stoppedRefining = !tile._refines && parentRefines;
if (!tile.hasRenderableContent) {
// Add empty tile just to show its debug bounding volume
// If the tile has tileset content load the external tileset
tileset._emptyTiles.push(tile);
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
} else if (tile.refine === Cesium3DTileRefine.ADD) {
// Additive tiles are always loaded and selected
selectDesiredTile(tile, frameState);
loadTile(tile, frameState);
} else if (tile.refine === Cesium3DTileRefine.REPLACE) {
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
}
visitTile(tile, frameState);
touchTile(tile, frameState);
}
}
/**
* Depth-first traversal that checks if all nearest descendants with content are loaded.
* Ignores visibility.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
* @returns {boolean}
*/
function executeEmptyTraversal(root, frameState) {
const { canTraverse, updateTile, loadTile, touchTile } =
Cesium3DTilesetTraversal;
let allDescendantsLoaded = true;
const stack = emptyTraversal.stack;
stack.push(root);
while (stack.length > 0) {
emptyTraversal.stackMaximumLength = Math.max(
emptyTraversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const children = tile.children;
const childrenLength = children.length;
// Only traverse if the tile is empty - traversal stops at descendants with content
const traverse = !tile.hasRenderableContent && canTraverse(tile);
// Traversal stops but the tile does not have content yet
// There will be holes if the parent tries to refine to its children, so don't refine
if (!traverse && !tile.contentAvailable) {
allDescendantsLoaded = false;
}
updateTile(tile, frameState);
if (!tile.isVisible) {
// Load tiles that aren't visible since they are still needed for the parent to refine
loadTile(tile, frameState);
touchTile(tile, frameState);
}
if (traverse) {
for (let i = 0; i < childrenLength; ++i) {
const child = children[i];
stack.push(child);
}
}
}
return root.hasEmptyContent || allDescendantsLoaded;
}
export default Cesium3DTilesetBaseTraversal;
@@ -0,0 +1,80 @@
import defined from "../Core/defined.js";
import DoublyLinkedList from "../Core/DoublyLinkedList.js";
/**
* Stores tiles with content loaded.
*
* @private
*/
function Cesium3DTilesetCache() {
// [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache
// (sentinel, tail] -> tiles that were selected this frame
this._list = new DoublyLinkedList();
this._sentinel = this._list.add();
this._trimTiles = false;
}
Cesium3DTilesetCache.prototype.reset = function () {
// Move sentinel node to the tail so, at the start of the frame, all tiles
// may be potentially replaced. Tiles are moved to the right of the sentinel
// when they are selected so they will not be replaced.
this._list.splice(this._list.tail, this._sentinel);
};
Cesium3DTilesetCache.prototype.touch = function (tile) {
const node = tile.cacheNode;
if (defined(node)) {
this._list.splice(this._sentinel, node);
}
};
Cesium3DTilesetCache.prototype.add = function (tile) {
if (!defined(tile.cacheNode)) {
tile.cacheNode = this._list.add(tile);
}
};
Cesium3DTilesetCache.prototype.unloadTile = function (
tileset,
tile,
unloadCallback,
) {
const node = tile.cacheNode;
if (!defined(node)) {
return;
}
this._list.remove(node);
tile.cacheNode = undefined;
unloadCallback(tileset, tile);
};
Cesium3DTilesetCache.prototype.unloadTiles = function (
tileset,
unloadCallback,
) {
const trimTiles = this._trimTiles;
this._trimTiles = false;
const list = this._list;
// Traverse the list only to the sentinel since tiles/nodes to the
// right of the sentinel were used this frame.
//
// The sub-list to the left of the sentinel is ordered from LRU to MRU.
const sentinel = this._sentinel;
let node = list.head;
while (
node !== sentinel &&
(tileset.totalMemoryUsageInBytes > tileset.cacheBytes || trimTiles)
) {
const tile = node.item;
node = node.next;
this.unloadTile(tileset, tile, unloadCallback);
}
};
Cesium3DTilesetCache.prototype.trim = function () {
this._trimTiles = true;
};
export default Cesium3DTilesetCache;
@@ -0,0 +1,165 @@
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import JulianDate from "../Core/JulianDate.js";
import CesiumMath from "../Core/Math.js";
/**
* A heatmap colorizer in a {@link Cesium3DTileset}. A tileset can colorize its visible tiles in a heatmap style.
*
* @alias Cesium3DTilesetHeatmap
* @constructor
* @private
*/
function Cesium3DTilesetHeatmap(tilePropertyName) {
/**
* The tile variable to track for heatmap colorization.
* Tile's will be colorized relative to the other visible tile's values for this variable.
*
* @type {string}
*/
this.tilePropertyName = tilePropertyName;
// Members that are updated every time a tile is colorized
this._minimum = Number.MAX_VALUE;
this._maximum = -Number.MAX_VALUE;
// Members that are updated once every frame
this._previousMinimum = Number.MAX_VALUE;
this._previousMaximum = -Number.MAX_VALUE;
// If defined uses a reference minimum maximum to colorize by instead of using last frames minimum maximum of rendered tiles.
// For example, the _loadTimestamp can get a better colorization using setReferenceMinimumMaximum in order to take accurate colored timing diffs of various scenes.
this._referenceMinimum = {};
this._referenceMaximum = {};
}
/**
* Convert to a usable heatmap value (i.e. a number). Ensures that tile values that aren't stored as numbers can be used for colorization.
* @private
*/
function getHeatmapValue(tileValue, tilePropertyName) {
let value;
if (tilePropertyName === "_loadTimestamp") {
value = JulianDate.toDate(tileValue).getTime();
} else {
value = tileValue;
}
return value;
}
/**
* Sets the reference minimum and maximum for the variable name. Converted to numbers before they are stored.
*
* @param {object} minimum The minimum reference value.
* @param {object} maximum The maximum reference value.
* @param {string} tilePropertyName The tile variable that will use these reference values when it is colorized.
*/
Cesium3DTilesetHeatmap.prototype.setReferenceMinimumMaximum = function (
minimum,
maximum,
tilePropertyName,
) {
this._referenceMinimum[tilePropertyName] = getHeatmapValue(
minimum,
tilePropertyName,
);
this._referenceMaximum[tilePropertyName] = getHeatmapValue(
maximum,
tilePropertyName,
);
};
function getHeatmapValueAndUpdateMinimumMaximum(heatmap, tile) {
const tilePropertyName = heatmap.tilePropertyName;
if (defined(tilePropertyName)) {
const heatmapValue = getHeatmapValue(
tile[tilePropertyName],
tilePropertyName,
);
if (!defined(heatmapValue)) {
heatmap.tilePropertyName = undefined;
return heatmapValue;
}
heatmap._maximum = Math.max(heatmapValue, heatmap._maximum);
heatmap._minimum = Math.min(heatmapValue, heatmap._minimum);
return heatmapValue;
}
}
const heatmapColors = [
new Color(0.1, 0.1, 0.1, 1), // Dark Gray
new Color(0.153, 0.278, 0.878, 1), // Blue
new Color(0.827, 0.231, 0.49, 1), // Pink
new Color(0.827, 0.188, 0.22, 1), // Red
new Color(1.0, 0.592, 0.259, 1), // Orange
new Color(1.0, 0.843, 0.0, 1),
]; // Yellow
/**
* Colorize the tile in heat map style based on where it lies within the minimum maximum window.
* Heatmap colors are black, blue, pink, red, orange, yellow. 'Cold' or low numbers will be black and blue, 'Hot' or high numbers will be orange and yellow,
* @param {Cesium3DTile} tile The tile to colorize relative to last frame's minimum and maximum values of all visible tiles.
* @param {FrameState} frameState The frame state.
*/
Cesium3DTilesetHeatmap.prototype.colorize = function (tile, frameState) {
const tilePropertyName = this.tilePropertyName;
if (
!defined(tilePropertyName) ||
!tile.contentAvailable ||
tile._selectedFrame !== frameState.frameNumber
) {
return;
}
const heatmapValue = getHeatmapValueAndUpdateMinimumMaximum(this, tile);
const minimum = this._previousMinimum;
const maximum = this._previousMaximum;
if (minimum === Number.MAX_VALUE || maximum === -Number.MAX_VALUE) {
return;
}
// Shift the minimum maximum window down to 0
const shiftedMax = maximum - minimum + CesiumMath.EPSILON7; // Prevent divide by 0
const shiftedValue = CesiumMath.clamp(
heatmapValue - minimum,
0.0,
shiftedMax,
);
// Get position between minimum and maximum and convert that to a position in the color array
const zeroToOne = shiftedValue / shiftedMax;
const lastIndex = heatmapColors.length - 1.0;
const colorPosition = zeroToOne * lastIndex;
// Take floor and ceil of the value to get the two colors to lerp between, lerp using the fractional portion
const colorPositionFloor = Math.floor(colorPosition);
const colorPositionCeil = Math.ceil(colorPosition);
const t = colorPosition - colorPositionFloor;
const colorZero = heatmapColors[colorPositionFloor];
const colorOne = heatmapColors[colorPositionCeil];
// Perform the lerp
const finalColor = Color.clone(Color.WHITE);
finalColor.red = CesiumMath.lerp(colorZero.red, colorOne.red, t);
finalColor.green = CesiumMath.lerp(colorZero.green, colorOne.green, t);
finalColor.blue = CesiumMath.lerp(colorZero.blue, colorOne.blue, t);
tile._debugColor = finalColor;
};
/**
* Resets the tracked minimum maximum values for heatmap colorization. Happens right before tileset traversal.
*/
Cesium3DTilesetHeatmap.prototype.resetMinimumMaximum = function () {
// For heat map colorization
const tilePropertyName = this.tilePropertyName;
if (defined(tilePropertyName)) {
const referenceMinimum = this._referenceMinimum[tilePropertyName];
const referenceMaximum = this._referenceMaximum[tilePropertyName];
const useReference = defined(referenceMinimum) && defined(referenceMaximum);
this._previousMinimum = useReference ? referenceMinimum : this._minimum;
this._previousMaximum = useReference ? referenceMaximum : this._maximum;
this._minimum = Number.MAX_VALUE;
this._maximum = -Number.MAX_VALUE;
}
};
export default Cesium3DTilesetHeatmap;
@@ -0,0 +1,199 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import GroupMetadata from "./GroupMetadata.js";
import TilesetMetadata from "./TilesetMetadata.js";
/**
* An object containing metadata about a 3D Tileset.
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata|3DTILES_metadata Extension} for 3D Tiles.
* </p>
* <p>
* This object represents the tileset JSON (3D Tiles 1.1) or the <code>3DTILES_metadata</code> object that contains
* the schema ({@link MetadataSchema}), tileset metadata ({@link TilesetMetadata}), group metadata (dictionary of {@link GroupMetadata}), and metadata statistics (dictionary)
* </p>
*
* @param {object} options Object with the following properties:
* @param {object} options.metadataJson Either the tileset JSON (3D Tiles 1.1) or the <code>3DTILES_metadata</code> extension object that contains the tileset metadata.
* @param {MetadataSchema} options.schema The parsed schema.
*
* @alias Cesium3DTilesetMetadata
* @constructor
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DTilesetMetadata(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const metadataJson = options.metadataJson;
// The calling code is responsible for loading the schema.
// This keeps metadata parsing synchronous.
const schema = options.schema;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.metadataJson", metadataJson);
Check.typeOf.object("options.schema", schema);
//>>includeEnd('debug');
// An older schema stored the tileset metadata in the "tileset" property.
const metadata = metadataJson.metadata ?? metadataJson.tileset;
let tileset;
if (defined(metadata)) {
tileset = new TilesetMetadata({
tileset: metadata,
class: schema.classes[metadata.class],
});
}
let groupIds = [];
const groups = [];
const groupsJson = metadataJson.groups;
if (Array.isArray(groupsJson)) {
const length = groupsJson.length;
for (let i = 0; i < length; i++) {
const group = groupsJson[i];
groups.push(
new GroupMetadata({
group: group,
class: schema.classes[group.class],
}),
);
}
} else if (defined(groupsJson)) {
// An older version of group metadata stored groups in a dictionary
// instead of an array.
groupIds = Object.keys(groupsJson).sort();
const length = groupIds.length;
for (let i = 0; i < length; i++) {
const groupId = groupIds[i];
if (groupsJson.hasOwnProperty(groupId)) {
const group = groupsJson[groupId];
groups.push(
new GroupMetadata({
id: groupId,
group: groupsJson[groupId],
class: schema.classes[group.class],
}),
);
}
}
}
this._schema = schema;
this._groups = groups;
this._groupIds = groupIds;
this._tileset = tileset;
this._statistics = metadataJson.statistics;
this._extras = metadataJson.extras;
this._extensions = metadataJson.extensions;
}
Object.defineProperties(Cesium3DTilesetMetadata.prototype, {
/**
* Schema containing classes and enums.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {MetadataSchema}
* @readonly
* @private
*/
schema: {
get: function () {
return this._schema;
},
},
/**
* Metadata about groups of content.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {GroupMetadata[]}
* @readonly
* @private
*/
groups: {
get: function () {
return this._groups;
},
},
/**
* The IDs of the group metadata in the corresponding groups dictionary.
* Only populated if using the legacy schema.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {}
* @readonly
* @private
*/
groupIds: {
get: function () {
return this._groupIds;
},
},
/**
* Metadata about the tileset as a whole.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {TilesetMetadata}
* @readonly
* @private
*/
tileset: {
get: function () {
return this._tileset;
},
},
/**
* Statistics about the metadata.
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/blob/main/extensions/3DTILES_metadata/schema/statistics.schema.json|statistics schema reference}
* in the 3D Tiles spec for the full set of properties.
* </p>
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {object}
* @readonly
* @private
*/
statistics: {
get: function () {
return this._statistics;
},
},
/**
* Extra user-defined properties.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {*}
* @readonly
* @private
*/
extras: {
get: function () {
return this._extras;
},
},
/**
* An object containing extensions.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {object}
* @readonly
* @private
*/
extensions: {
get: function () {
return this._extensions;
},
},
});
export default Cesium3DTilesetMetadata;
@@ -0,0 +1,131 @@
import Intersect from "../Core/Intersect.js";
import ManagedArray from "../Core/ManagedArray.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js";
/**
* Traversal that loads all leaves that intersect the camera frustum.
* Used to determine ray-tileset intersections during a pickFromRayMostDetailed call.
*
* @alias Cesium3DTilesetMostDetailedTraversal
* @constructor
*
* @private
*/
function Cesium3DTilesetMostDetailedTraversal() {}
const traversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
* @returns {boolean} Whether the appropriate tile is ready for picking
*/
Cesium3DTilesetMostDetailedTraversal.selectTiles = function (
tileset,
frameState,
) {
tileset._selectedTiles.length = 0;
tileset._requestedTiles.length = 0;
tileset.hasMixedContent = false;
let ready = true;
const root = tileset.root;
root.updateVisibility(frameState);
if (!root.isVisible) {
return ready;
}
const { touchTile, visitTile } = Cesium3DTilesetTraversal;
const stack = traversal.stack;
stack.push(root);
while (stack.length > 0) {
traversal.stackMaximumLength = Math.max(
traversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const add = tile.refine === Cesium3DTileRefine.ADD;
const replace = tile.refine === Cesium3DTileRefine.REPLACE;
const traverse = canTraverse(tile);
if (traverse) {
updateAndPushChildren(tile, stack, frameState);
}
if (add || (replace && !traverse)) {
loadTile(tileset, tile);
touchTile(tile, frameState);
selectDesiredTile(tile, frameState);
if (tile.hasRenderableContent && !tile.contentAvailable) {
ready = false;
}
}
visitTile(tile, frameState);
}
traversal.stack.trim(traversal.stackMaximumLength);
return ready;
};
function canTraverse(tile) {
if (tile.children.length === 0) {
return false;
}
if (tile.hasTilesetContent || tile.hasImplicitContent) {
// Traverse external tileset to visit its root tile
// Don't traverse if the subtree is expired because it will be destroyed
return !tile.contentExpired;
}
if (tile.hasEmptyContent) {
return true;
}
return true; // Keep traversing until a leaf is hit
}
function updateAndPushChildren(tile, stack, frameState) {
const { children } = tile;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
child.updateVisibility(frameState);
if (child.isVisible) {
stack.push(child);
}
}
}
function loadTile(tileset, tile) {
if (tile.hasUnloadedRenderableContent || tile.contentExpired) {
tile._priority = 0.0; // Highest priority
tileset._requestedTiles.push(tile);
}
}
function selectDesiredTile(tile, frameState) {
if (
tile.contentAvailable &&
tile.contentVisibility(frameState) !== Intersect.OUTSIDE
) {
tile.tileset._selectedTiles.push(tile);
}
}
export default Cesium3DTilesetMostDetailedTraversal;
@@ -0,0 +1,420 @@
import defined from "../Core/defined.js";
import ManagedArray from "../Core/ManagedArray.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js";
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* Allows for skipping levels of the tree and rendering children and parent tiles simultaneously.
*
* @alias Cesium3DTilesetSkipTraversal
* @constructor
*
* @private
*/
function Cesium3DTilesetSkipTraversal() {}
const traversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
const descendantTraversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
const selectionTraversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
ancestorStack: new ManagedArray(),
ancestorStackMaximumLength: 0,
};
const descendantSelectionDepth = 2;
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
*/
Cesium3DTilesetSkipTraversal.selectTiles = function (tileset, frameState) {
tileset._requestedTiles.length = 0;
if (tileset.debugFreezeFrame) {
return;
}
tileset._selectedTiles.length = 0;
tileset._selectedTilesToStyle.length = 0;
tileset._emptyTiles.length = 0;
tileset.hasMixedContent = false;
const root = tileset.root;
Cesium3DTilesetTraversal.updateTile(root, frameState);
if (!root.isVisible) {
return;
}
if (
root.getScreenSpaceError(frameState, true) <=
tileset.memoryAdjustedScreenSpaceError
) {
return;
}
executeTraversal(root, frameState);
traverseAndSelect(root, frameState);
traversal.stack.trim(traversal.stackMaximumLength);
descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength);
selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength);
selectionTraversal.ancestorStack.trim(
selectionTraversal.ancestorStackMaximumLength,
);
// Update the priority for any requests found during traversal
// Update after traversal so that min and max values can be used to normalize priority values
const requestedTiles = tileset._requestedTiles;
for (let i = 0; i < requestedTiles.length; ++i) {
requestedTiles[i].updatePriority();
}
};
/**
* Mark descendant tiles for rendering, and update as needed
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function selectDescendants(root, frameState) {
const { updateTile, touchTile, selectTile } = Cesium3DTilesetTraversal;
const stack = descendantTraversal.stack;
stack.push(root);
while (stack.length > 0) {
descendantTraversal.stackMaximumLength = Math.max(
descendantTraversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const children = tile.children;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
if (child.contentAvailable) {
updateTile(child, frameState);
touchTile(child, frameState);
selectTile(child, frameState);
} else if (child._depth - root._depth < descendantSelectionDepth) {
// Continue traversing, but not too far
stack.push(child);
}
}
}
}
}
/**
* Mark a tile as selected if it has content available.
* If its content is not available, and we are skipping levels of detail,
* select an ancestor or descendant tile instead
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function selectDesiredTile(tile, frameState) {
// If this tile is not loaded attempt to select its ancestor instead
const loadedTile = tile.contentAvailable
? tile
: tile._ancestorWithContentAvailable;
if (defined(loadedTile)) {
// Tiles will actually be selected in traverseAndSelect
loadedTile._shouldSelect = true;
} else {
// If no ancestors are ready traverse down and select tiles to minimize empty regions.
// This happens often for immediatelyLoadDesiredLevelOfDetail where parent tiles are not necessarily loaded before zooming out.
selectDescendants(tile, frameState);
}
}
/**
* Update links to the ancestor tiles that have content
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function updateTileAncestorContentLinks(tile, frameState) {
tile._ancestorWithContent = undefined;
tile._ancestorWithContentAvailable = undefined;
const { parent } = tile;
if (!defined(parent)) {
return;
}
const parentHasContent =
!parent.hasUnloadedRenderableContent ||
parent._requestedFrame === frameState.frameNumber;
// ancestorWithContent is an ancestor that has content or has the potential to have
// content. Used in conjunction with tileset.skipLevels to know when to skip a tile.
tile._ancestorWithContent = parentHasContent
? parent
: parent._ancestorWithContent;
// ancestorWithContentAvailable is an ancestor that is rendered if a desired tile is not loaded
tile._ancestorWithContentAvailable = parent.contentAvailable
? parent
: parent._ancestorWithContentAvailable;
}
/**
* Determine if a tile has reached the limit of level of detail skipping.
* If so, it should _not_ be skipped: it should be loaded and rendered
*
* @private
* @param {Cesium3DTileset} tileset
* @param {Cesium3DTile} tile
* @returns {boolean} true if this tile should not be skipped
*/
function reachedSkippingThreshold(tileset, tile) {
const ancestor = tile._ancestorWithContent;
return (
!tileset.immediatelyLoadDesiredLevelOfDetail &&
(tile._priorityProgressiveResolutionScreenSpaceErrorLeaf ||
(defined(ancestor) &&
tile._screenSpaceError <
ancestor._screenSpaceError / tileset.skipScreenSpaceErrorFactor &&
tile._depth > ancestor._depth + tileset.skipLevels))
);
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {ManagedArray} stack
* @param {FrameState} frameState
* @returns {boolean}
*/
function updateAndPushChildren(tile, stack, frameState) {
const { tileset, children } = tile;
const { updateTile, loadTile, touchTile } = Cesium3DTilesetTraversal;
for (let i = 0; i < children.length; ++i) {
updateTile(children[i], frameState);
}
// Sort by distance to take advantage of early Z and reduce artifacts
children.sort(Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera);
let anyChildrenVisible = false;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
stack.push(child);
anyChildrenVisible = true;
} else if (tileset.loadSiblings) {
loadTile(child, frameState);
touchTile(child, frameState);
}
}
return anyChildrenVisible;
}
/**
* Determine if a tile is part of the base traversal.
* If not, this tile could be considered for level of detail skipping
*
* @private
* @param {Cesium3DTile} tile
* @param {number} baseScreenSpaceError
* @returns {boolean}
*/
function inBaseTraversal(tile, baseScreenSpaceError) {
const { tileset } = tile;
if (tileset.immediatelyLoadDesiredLevelOfDetail) {
return false;
}
if (!defined(tile._ancestorWithContent)) {
// Include root or near-root tiles in the base traversal so there is something to select up to
return true;
}
if (tile._screenSpaceError === 0.0) {
// If a leaf, use parent's SSE
return tile.parent._screenSpaceError > baseScreenSpaceError;
}
return tile._screenSpaceError > baseScreenSpaceError;
}
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* Tiles that have a greater screen space error than the base screen space error are part of the base traversal,
* all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree
* and rendering children and parent tiles simultaneously.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function executeTraversal(root, frameState) {
const { tileset } = root;
const baseScreenSpaceError = tileset.immediatelyLoadDesiredLevelOfDetail
? Number.MAX_VALUE
: Math.max(
tileset.baseScreenSpaceError,
tileset.memoryAdjustedScreenSpaceError,
);
const { canTraverse, loadTile, visitTile, touchTile } =
Cesium3DTilesetTraversal;
const stack = traversal.stack;
stack.push(root);
while (stack.length > 0) {
traversal.stackMaximumLength = Math.max(
traversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
updateTileAncestorContentLinks(tile, frameState);
const parent = tile.parent;
const parentRefines = !defined(parent) || parent._refines;
tile._refines = canTraverse(tile)
? updateAndPushChildren(tile, stack, frameState) && parentRefines
: false;
const stoppedRefining = !tile._refines && parentRefines;
if (!tile.hasRenderableContent) {
// Add empty tile just to show its debug bounding volume
// If the tile has tileset content load the external tileset
// If the tile cannot refine further select its nearest loaded ancestor
tileset._emptyTiles.push(tile);
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
} else if (tile.refine === Cesium3DTileRefine.ADD) {
// Additive tiles are always loaded and selected
selectDesiredTile(tile, frameState);
loadTile(tile, frameState);
} else if (tile.refine === Cesium3DTileRefine.REPLACE) {
if (inBaseTraversal(tile, baseScreenSpaceError)) {
// Always load tiles in the base traversal
// Select tiles that can't refine further
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
} else if (stoppedRefining) {
// In skip traversal, load and select tiles that can't refine further
selectDesiredTile(tile, frameState);
loadTile(tile, frameState);
} else if (reachedSkippingThreshold(tileset, tile)) {
// In skip traversal, load tiles that aren't skipped
loadTile(tile, frameState);
}
}
visitTile(tile, frameState);
touchTile(tile, frameState);
}
}
/**
* Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue.
* This is a preorder traversal so children tiles are selected before ancestor tiles.
*
* The reason for the preorder traversal is so that tiles can easily be marked with their
* selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed.
* This property is important for use in the stencil test because we want to render deeper tiles on top of their
* ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer.
*
* We want to select children before their ancestors because there is no guarantee on the relationship between
* the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top
* of ancestor regardless of true depth. The stencil tests used require children to be drawn first.
*
* NOTE: 3D Tiles uses 3 bits from the stencil buffer meaning this will not work when there is a chain of
* selected tiles that is deeper than 7. This is not very likely.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function traverseAndSelect(root, frameState) {
const { selectTile, canTraverse } = Cesium3DTilesetTraversal;
const { stack, ancestorStack } = selectionTraversal;
let lastAncestor;
stack.push(root);
while (stack.length > 0 || ancestorStack.length > 0) {
selectionTraversal.stackMaximumLength = Math.max(
selectionTraversal.stackMaximumLength,
stack.length,
);
selectionTraversal.ancestorStackMaximumLength = Math.max(
selectionTraversal.ancestorStackMaximumLength,
ancestorStack.length,
);
if (ancestorStack.length > 0) {
const waitingTile = ancestorStack.peek();
if (waitingTile._stackLength === stack.length) {
ancestorStack.pop();
if (waitingTile !== lastAncestor) {
waitingTile._finalResolution = false;
}
selectTile(waitingTile, frameState);
continue;
}
}
const tile = stack.pop();
if (!defined(tile)) {
// stack is empty but ancestorStack isn't
continue;
}
const traverse = canTraverse(tile);
if (tile._shouldSelect) {
if (tile.refine === Cesium3DTileRefine.ADD) {
selectTile(tile, frameState);
} else {
tile._selectionDepth = ancestorStack.length;
if (tile._selectionDepth > 0) {
tile.tileset.hasMixedContent = true;
}
lastAncestor = tile;
if (!traverse) {
selectTile(tile, frameState);
continue;
}
ancestorStack.push(tile);
tile._stackLength = stack.length;
}
}
if (traverse) {
const children = tile.children;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
stack.push(child);
}
}
}
}
}
export default Cesium3DTilesetSkipTraversal;
@@ -0,0 +1,195 @@
import defined from "../Core/defined.js";
import Model3DTileContent from "./Model/Model3DTileContent.js";
/**
* @private
*/
function Cesium3DTilesetStatistics() {
// Rendering statistics
this.selected = 0;
this.visited = 0;
// Loading statistics
this.numberOfCommands = 0;
this.numberOfAttemptedRequests = 0;
this.numberOfPendingRequests = 0;
this.numberOfTilesProcessing = 0;
this.numberOfTilesWithContentReady = 0; // Number of tiles with content loaded, does not include empty tiles
this.numberOfTilesTotal = 0; // Number of tiles in tileset JSON (and other tileset JSON files as they are loaded)
this.numberOfLoadedTilesTotal = 0; // Running total of loaded tiles for the lifetime of the session
// Features statistics
this.numberOfFeaturesSelected = 0; // Number of features rendered
this.numberOfFeaturesLoaded = 0; // Number of features in memory
this.numberOfPointsSelected = 0;
this.numberOfPointsLoaded = 0;
this.numberOfTrianglesSelected = 0;
// Styling statistics
this.numberOfTilesStyled = 0;
this.numberOfFeaturesStyled = 0;
// Optimization statistics
this.numberOfTilesCulledWithChildrenUnion = 0;
// Memory statistics
this.geometryByteLength = 0;
this.texturesByteLength = 0;
this.texturesReferenceCounterById = {};
this.batchTableByteLength = 0; // batch textures and any binary metadata properties not otherwise accounted for
}
Cesium3DTilesetStatistics.prototype.clear = function () {
this.selected = 0;
this.visited = 0;
this.numberOfCommands = 0;
this.numberOfAttemptedRequests = 0;
this.numberOfFeaturesSelected = 0;
this.numberOfPointsSelected = 0;
this.numberOfTrianglesSelected = 0;
this.numberOfTilesStyled = 0;
this.numberOfFeaturesStyled = 0;
this.numberOfTilesCulledWithChildrenUnion = 0;
};
/**
* Increment the counters for the points, triangles, and features
* that are currently selected for rendering.
*
* This will be called recursively for the given content and
* all its inner contents
*
* @param {Cesium3DTileContent} content
*/
Cesium3DTilesetStatistics.prototype.incrementSelectionCounts = function (
content,
) {
this.numberOfFeaturesSelected += content.featuresLength;
this.numberOfPointsSelected += content.pointsLength;
this.numberOfTrianglesSelected += content.trianglesLength;
// Recursive calls on all inner contents
const contents = content.innerContents;
if (defined(contents)) {
const length = contents.length;
for (let i = 0; i < length; ++i) {
this.incrementSelectionCounts(contents[i]);
}
}
};
/**
* Increment the counters for the number of features and points that
* are currently loaded, and the lengths (size in bytes) of the
* occupied memory.
*
* This will be called recursively for the given content and
* all its inner contents
*
* @param {Cesium3DTileContent} content
*/
Cesium3DTilesetStatistics.prototype.incrementLoadCounts = function (content) {
this.numberOfFeaturesLoaded += content.featuresLength;
this.numberOfPointsLoaded += content.pointsLength;
this.geometryByteLength += content.geometryByteLength;
this.batchTableByteLength += content.batchTableByteLength;
// When the content is not a `Model3DTileContent`, then its
// textures byte length is added directly
if (!(content instanceof Model3DTileContent)) {
this.texturesByteLength += content.texturesByteLength;
} else {
// When the content is a `Model3DTileContent`, then increment the
// reference counter for all its textures. The byte length of any
// newly tracked texture to the total textures byte length
const textureIds = content.getTextureIds();
for (const textureId of textureIds) {
const referenceCounter =
this.texturesReferenceCounterById[textureId] ?? 0;
if (referenceCounter === 0) {
const textureByteLength = content.getTextureByteLengthById(textureId);
this.texturesByteLength += textureByteLength;
}
this.texturesReferenceCounterById[textureId] = referenceCounter + 1;
}
}
// Recursive calls on all inner contents
const contents = content.innerContents;
if (defined(contents)) {
const length = contents.length;
for (let i = 0; i < length; ++i) {
this.incrementLoadCounts(contents[i]);
}
}
};
/**
* Decrement the counters for the number of features and points that
* are currently loaded, and the lengths (size in bytes) of the
* occupied memory.
*
* This will be called recursively for the given content and
* all its inner contents
*
* @param {Cesium3DTileContent} content
*/
Cesium3DTilesetStatistics.prototype.decrementLoadCounts = function (content) {
this.numberOfFeaturesLoaded -= content.featuresLength;
this.numberOfPointsLoaded -= content.pointsLength;
this.geometryByteLength -= content.geometryByteLength;
this.batchTableByteLength -= content.batchTableByteLength;
// When the content is not a `Model3DTileContent`, then its
// textures byte length is subtracted directly
if (!(content instanceof Model3DTileContent)) {
this.texturesByteLength -= content.texturesByteLength;
} else {
// When the content is a `Model3DTileContent`, then decrement the
// reference counter for all its textures. The byte length of any
// texture that is no longer references is subtracted from the
// total textures byte length
const textureIds = content.getTextureIds();
for (const textureId of textureIds) {
const referenceCounter = this.texturesReferenceCounterById[textureId];
if (referenceCounter === 1) {
delete this.texturesReferenceCounterById[textureId];
const textureByteLength = content.getTextureByteLengthById(textureId);
this.texturesByteLength -= textureByteLength;
} else {
this.texturesReferenceCounterById[textureId] = referenceCounter - 1;
}
}
}
// Recursive calls on all inner contents
const contents = content.innerContents;
if (defined(contents)) {
const length = contents.length;
for (let i = 0; i < length; ++i) {
this.decrementLoadCounts(contents[i]);
}
}
};
Cesium3DTilesetStatistics.clone = function (statistics, result) {
result.selected = statistics.selected;
result.visited = statistics.visited;
result.numberOfCommands = statistics.numberOfCommands;
result.numberOfAttemptedRequests = statistics.numberOfAttemptedRequests;
result.numberOfPendingRequests = statistics.numberOfPendingRequests;
result.numberOfTilesProcessing = statistics.numberOfTilesProcessing;
result.numberOfTilesWithContentReady =
statistics.numberOfTilesWithContentReady;
result.numberOfTilesTotal = statistics.numberOfTilesTotal;
result.numberOfFeaturesSelected = statistics.numberOfFeaturesSelected;
result.numberOfFeaturesLoaded = statistics.numberOfFeaturesLoaded;
result.numberOfPointsSelected = statistics.numberOfPointsSelected;
result.numberOfPointsLoaded = statistics.numberOfPointsLoaded;
result.numberOfTrianglesSelected = statistics.numberOfTrianglesSelected;
result.numberOfTilesStyled = statistics.numberOfTilesStyled;
result.numberOfFeaturesStyled = statistics.numberOfFeaturesStyled;
result.numberOfTilesCulledWithChildrenUnion =
statistics.numberOfTilesCulledWithChildrenUnion;
result.geometryByteLength = statistics.geometryByteLength;
result.texturesByteLength = statistics.texturesByteLength;
result.texturesReferenceCounterById = {
...statistics.texturesReferenceCounterById,
};
result.batchTableByteLength = statistics.batchTableByteLength;
};
export default Cesium3DTilesetStatistics;
@@ -0,0 +1,323 @@
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Intersect from "../Core/Intersect.js";
import Cesium3DTileOptimizationHint from "./Cesium3DTileOptimizationHint.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
* This type describes an interface and is not intended to be instantiated directly.
*
* @alias Cesium3DTilesetTraversal
* @constructor
* @abstract
*
* @see Cesium3DTilesetBaseTraversal
* @see Cesium3DTilesetSkipTraversal
* @see Cesium3DTilesetMostDetailedTraversal
*
* @private
*/
function Cesium3DTilesetTraversal() {}
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.selectTiles = function (tileset, frameState) {
DeveloperError.throwInstantiationError();
};
/**
* Sort by farthest child first since this is going on a stack
*
* @private
* @param {Cesium3DTile} a
* @param {Cesium3DTile} b
* @returns {number}
*/
Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera = function (a, b) {
if (b._distanceToCamera === 0 && a._distanceToCamera === 0) {
return b._centerZDepth - a._centerZDepth;
}
return b._distanceToCamera - a._distanceToCamera;
};
/**
* Determine if a tile can and should be traversed for children tiles that
* would contribute to rendering the current view
*
* @private
* @param {Cesium3DTile} tile
* @returns {boolean}
*/
Cesium3DTilesetTraversal.canTraverse = function (tile) {
if (tile.children.length === 0) {
return false;
}
if (tile.hasTilesetContent || tile.hasImplicitContent) {
// Traverse external tileset to visit its root tile
// Don't traverse if the subtree is expired because it will be destroyed
return !tile.contentExpired;
}
return tile._screenSpaceError > tile.tileset.memoryAdjustedScreenSpaceError;
};
/**
* Mark a tile as selected, and add it to the tileset's list of selected tiles
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.selectTile = function (tile, frameState) {
if (tile.contentVisibility(frameState) === Intersect.OUTSIDE) {
return;
}
tile._wasSelectedLastFrame = true;
const { content, tileset } = tile;
if (content.featurePropertiesDirty) {
// A feature's property in this tile changed, the tile needs to be re-styled.
content.featurePropertiesDirty = false;
tile.lastStyleTime = 0; // Force applying the style to this tile
tileset._selectedTilesToStyle.push(tile);
} else if (tile._selectedFrame < frameState.frameNumber - 1) {
// Tile is newly selected; it is selected this frame, but was not selected last frame.
tileset._selectedTilesToStyle.push(tile);
tile._wasSelectedLastFrame = false;
}
tile._selectedFrame = frameState.frameNumber;
tileset._selectedTiles.push(tile);
};
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.visitTile = function (tile, frameState) {
++tile.tileset._statistics.visited;
tile._visitedFrame = frameState.frameNumber;
};
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.touchTile = function (tile, frameState) {
if (tile._touchedFrame === frameState.frameNumber) {
// Prevents another pass from touching the frame again
return;
}
tile.tileset._cache.touch(tile);
tile._touchedFrame = frameState.frameNumber;
};
/**
* Add a tile to the list of requested tiles, if appropriate
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.loadTile = function (tile, frameState) {
const { tileset } = tile;
if (
tile._requestedFrame === frameState.frameNumber ||
(!tile.hasUnloadedRenderableContent && !tile.contentExpired)
) {
return;
}
if (!isOnScreenLongEnough(tile, frameState)) {
return;
}
const cameraHasNotStoppedMovingLongEnough =
frameState.camera.timeSinceMoved < tileset.foveatedTimeDelay;
if (tile.priorityDeferred && cameraHasNotStoppedMovingLongEnough) {
return;
}
tile._requestedFrame = frameState.frameNumber;
tileset._requestedTiles.push(tile);
};
/**
* Prevent unnecessary loads while camera is moving by getting the ratio of travel distance to tile size.
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
* @returns {boolean}
*/
function isOnScreenLongEnough(tile, frameState) {
const { tileset } = tile;
if (!tileset._cullRequestsWhileMoving) {
return true;
}
const { positionWCDeltaMagnitude, positionWCDeltaMagnitudeLastFrame } =
frameState.camera;
const deltaMagnitude =
positionWCDeltaMagnitude !== 0.0
? positionWCDeltaMagnitude
: positionWCDeltaMagnitudeLastFrame;
// How do n frames of this movement compare to the tile's physical size.
const diameter = Math.max(tile.boundingSphere.radius * 2.0, 1.0);
const movementRatio =
(tileset.cullRequestsWhileMovingMultiplier * deltaMagnitude) / diameter;
return movementRatio < 1.0;
}
/**
* Reset some of the tile's flags and re-evaluate visibility and priority
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.updateTile = function (tile, frameState) {
updateTileVisibility(tile, frameState);
tile.updateExpiration();
tile._wasMinPriorityChild = false;
tile._priorityHolder = tile;
updateMinimumMaximumPriority(tile);
// SkipLOD
tile._shouldSelect = false;
tile._finalResolution = true;
};
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function updateTileVisibility(tile, frameState) {
tile.updateVisibility(frameState);
if (!tile.isVisible) {
return;
}
const hasChildren = tile.children.length > 0;
if ((tile.hasTilesetContent || tile.hasImplicitContent) && hasChildren) {
// Use the root tile's visibility instead of this tile's visibility.
// The root tile may be culled by the children bounds optimization in which
// case this tile should also be culled.
const child = tile.children[0];
updateTileVisibility(child, frameState);
tile._visible = child._visible;
return;
}
if (meetsScreenSpaceErrorEarly(tile, frameState)) {
tile._visible = false;
return;
}
// Optimization - if none of the tile's children are visible then this tile isn't visible
const replace = tile.refine === Cesium3DTileRefine.REPLACE;
const useOptimization =
tile._optimChildrenWithinParent ===
Cesium3DTileOptimizationHint.USE_OPTIMIZATION;
if (replace && useOptimization && hasChildren) {
if (!anyChildrenVisible(tile, frameState)) {
++tile.tileset._statistics.numberOfTilesCulledWithChildrenUnion;
tile._visible = false;
return;
}
}
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
* @returns {boolean}
*/
function meetsScreenSpaceErrorEarly(tile, frameState) {
const { parent, tileset } = tile;
if (
!defined(parent) ||
parent.hasTilesetContent ||
parent.hasImplicitContent ||
parent.refine !== Cesium3DTileRefine.ADD
) {
return false;
}
// Use parent's geometric error with child's box to see if the tile already meet the SSE
return (
tile.getScreenSpaceError(frameState, true) <=
tileset.memoryAdjustedScreenSpaceError
);
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
* @returns {boolean}
*/
function anyChildrenVisible(tile, frameState) {
let anyVisible = false;
const children = tile.children;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
child.updateVisibility(frameState);
anyVisible = anyVisible || child.isVisible;
}
return anyVisible;
}
/**
* @private
* @param {Cesium3DTile} tile
*/
function updateMinimumMaximumPriority(tile) {
const minimumPriority = tile.tileset._minimumPriority;
const maximumPriority = tile.tileset._maximumPriority;
const priorityHolder = tile._priorityHolder;
maximumPriority.distance = Math.max(
priorityHolder._distanceToCamera,
maximumPriority.distance,
);
minimumPriority.distance = Math.min(
priorityHolder._distanceToCamera,
minimumPriority.distance,
);
maximumPriority.depth = Math.max(tile._depth, maximumPriority.depth);
minimumPriority.depth = Math.min(tile._depth, minimumPriority.depth);
maximumPriority.foveatedFactor = Math.max(
priorityHolder._foveatedFactor,
maximumPriority.foveatedFactor,
);
minimumPriority.foveatedFactor = Math.min(
priorityHolder._foveatedFactor,
minimumPriority.foveatedFactor,
);
maximumPriority.reverseScreenSpaceError = Math.max(
tile._priorityReverseScreenSpaceError,
maximumPriority.reverseScreenSpaceError,
);
minimumPriority.reverseScreenSpaceError = Math.min(
tile._priorityReverseScreenSpaceError,
minimumPriority.reverseScreenSpaceError,
);
}
export default Cesium3DTilesetTraversal;
+61
View File
@@ -0,0 +1,61 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
/**
* A ParticleEmitter that emits particles from a circle.
* Particles will be positioned within a circle and have initial velocities going along the z vector.
*
* @alias CircleEmitter
* @constructor
*
* @param {number} [radius=1.0] The radius of the circle in meters.
*/
function CircleEmitter(radius) {
radius = radius ?? 1.0;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThan("radius", radius, 0.0);
//>>includeEnd('debug');
this._radius = radius ?? 1.0;
}
Object.defineProperties(CircleEmitter.prototype, {
/**
* The radius of the circle in meters.
* @memberof CircleEmitter.prototype
* @type {number}
* @default 1.0
*/
radius: {
get: function () {
return this._radius;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThan("value", value, 0.0);
//>>includeEnd('debug');
this._radius = value;
},
},
});
/**
* Initializes the given {@link Particle} by setting it's position and velocity.
*
* @private
* @param {Particle} particle The particle to initialize.
*/
CircleEmitter.prototype.emit = function (particle) {
const theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
const rad = CesiumMath.randomBetween(0.0, this._radius);
const x = rad * Math.cos(theta);
const y = rad * Math.sin(theta);
const z = 0.0;
particle.position = Cartesian3.fromElements(x, y, z, particle.position);
particle.velocity = Cartesian3.clone(Cartesian3.UNIT_Z, particle.velocity);
};
export default CircleEmitter;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
/**
* Whether a classification affects terrain, 3D Tiles or both.
*
* @enum {number}
*/
const ClassificationType = {
/**
* Only terrain will be classified.
*
* @type {number}
* @constant
*/
TERRAIN: 0,
/**
* Only 3D Tiles will be classified.
*
* @type {number}
* @constant
*/
CESIUM_3D_TILE: 1,
/**
* Both terrain and 3D Tiles will be classified.
*
* @type {number}
* @constant
*/
BOTH: 2,
};
/**
* @private
*/
ClassificationType.NUMBER_OF_CLASSIFICATION_TYPES = 3;
Object.freeze(ClassificationType);
export default ClassificationType;
+184
View File
@@ -0,0 +1,184 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
/**
* A Plane in Hessian Normal form to be used with {@link ClippingPlaneCollection}.
* Compatible with mathematics functions in {@link Plane}
*
* @alias ClippingPlane
* @constructor
*
* @param {Cartesian3} normal The plane's normal (normalized).
* @param {number} distance The shortest distance from the origin to the plane. The sign of
* <code>distance</code> determines which side of the plane the origin
* is on. If <code>distance</code> is positive, the origin is in the half-space
* in the direction of the normal; if negative, the origin is in the half-space
* opposite to the normal; if zero, the plane passes through the origin.
*/
function ClippingPlane(normal, distance) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("normal", normal);
Check.typeOf.number("distance", distance);
//>>includeEnd('debug');
this._distance = distance;
this._normal = new UpdateChangedCartesian3(normal, this);
this.onChangeCallback = undefined;
this.index = -1; // to be set by ClippingPlaneCollection
}
Object.defineProperties(ClippingPlane.prototype, {
/**
* The shortest distance from the origin to the plane. The sign of
* <code>distance</code> determines which side of the plane the origin
* is on. If <code>distance</code> is positive, the origin is in the half-space
* in the direction of the normal; if negative, the origin is in the half-space
* opposite to the normal; if zero, the plane passes through the origin.
*
* @type {number}
* @memberof ClippingPlane.prototype
*/
distance: {
get: function () {
return this._distance;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (defined(this.onChangeCallback) && value !== this._distance) {
this.onChangeCallback(this.index);
}
this._distance = value;
},
},
/**
* The plane's normal.
*
* @type {Cartesian3}
* @memberof ClippingPlane.prototype
*/
normal: {
get: function () {
return this._normal;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("value", value);
//>>includeEnd('debug');
if (
defined(this.onChangeCallback) &&
!Cartesian3.equals(this._normal._cartesian3, value)
) {
this.onChangeCallback(this.index);
}
// Set without firing callback again
Cartesian3.clone(value, this._normal._cartesian3);
},
},
});
/**
* Create a ClippingPlane from a Plane object.
*
* @param {Plane} plane The plane containing parameters to copy
* @param {ClippingPlane} [result] The object on which to store the result
* @returns {ClippingPlane} The ClippingPlane generated from the plane's parameters.
*/
ClippingPlane.fromPlane = function (plane, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("plane", plane);
//>>includeEnd('debug');
if (!defined(result)) {
result = new ClippingPlane(plane.normal, plane.distance);
} else {
result.normal = plane.normal;
result.distance = plane.distance;
}
return result;
};
/**
* Clones the ClippingPlane without setting its ownership.
* @param {ClippingPlane} clippingPlane The ClippingPlane to be cloned
* @param {ClippingPlane} [result] The object on which to store the cloned parameters.
* @returns {ClippingPlane} a clone of the input ClippingPlane
*/
ClippingPlane.clone = function (clippingPlane, result) {
if (!defined(result)) {
return new ClippingPlane(clippingPlane.normal, clippingPlane.distance);
}
result.normal = clippingPlane.normal;
result.distance = clippingPlane.distance;
return result;
};
/**
* Wrapper on Cartesian3 that allows detection of Plane changes from "members of members," for example:
*
* const clippingPlane = new ClippingPlane(...);
* clippingPlane.normal.z = -1.0;
*
* @private
*/
function UpdateChangedCartesian3(normal, clippingPlane) {
this._clippingPlane = clippingPlane;
this._cartesian3 = Cartesian3.clone(normal);
}
Object.defineProperties(UpdateChangedCartesian3.prototype, {
x: {
get: function () {
return this._cartesian3.x;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (
defined(this._clippingPlane.onChangeCallback) &&
value !== this._cartesian3.x
) {
this._clippingPlane.onChangeCallback(this._clippingPlane.index);
}
this._cartesian3.x = value;
},
},
y: {
get: function () {
return this._cartesian3.y;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (
defined(this._clippingPlane.onChangeCallback) &&
value !== this._cartesian3.y
) {
this._clippingPlane.onChangeCallback(this._clippingPlane.index);
}
this._cartesian3.y = value;
},
},
z: {
get: function () {
return this._cartesian3.z;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (
defined(this._clippingPlane.onChangeCallback) &&
value !== this._cartesian3.z
) {
this._clippingPlane.onChangeCallback(this._clippingPlane.index);
}
this._cartesian3.z = value;
},
},
});
export default ClippingPlane;
@@ -0,0 +1,764 @@
import AttributeCompression from "../Core/AttributeCompression.js";
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 Color from "../Core/Color.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import Event from "../Core/Event.js";
import Intersect from "../Core/Intersect.js";
import Matrix4 from "../Core/Matrix4.js";
import PixelFormat from "../Core/PixelFormat.js";
import Plane from "../Core/Plane.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
import ClippingPlane from "./ClippingPlane.js";
/**
* Specifies a set of clipping planes. Clipping planes selectively disable rendering in a region on the
* outside of the specified list of {@link ClippingPlane} objects for a single gltf model, 3D Tileset, or the globe.
* <p>
* In general the clipping planes' coordinates are relative to the object they're attached to, so a plane with distance set to 0 will clip
* through the center of the object.
* </p>
* <p>
* For 3D Tiles, the root tile's transform is used to position the clipping planes. If a transform is not defined, the root tile's {@link Cesium3DTile#boundingSphere} is used instead.
* </p>
*
* @alias ClippingPlaneCollection
* @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 {boolean} [options.enabled=true] Determines whether the clipping planes are active.
* @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.
* @param {Color} [options.edgeColor=Color.WHITE] The color applied to highlight the edge along which an object is clipped.
* @param {number} [options.edgeWidth=0.0] The width, in pixels, of the highlight applied to the edge along which an object is clipped.
*
* @demo {@link https://sandcastle.cesium.com/?id=3d-tiles-clipping-planes|Clipping 3D Tiles and glTF models.}
* @demo {@link https://sandcastle.cesium.com/?id=terrain-clipping-planes|Clipping the Globe.}
*
* @example
* // This clipping plane's distance is positive, which means its normal
* // is facing the origin. This will clip everything that is behind
* // the plane, which is anything with y coordinate < -5.
* const clippingPlanes = new Cesium.ClippingPlaneCollection({
* planes : [
* new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 1.0, 0.0), 5.0)
* ],
* });
* // Create an entity and attach the ClippingPlaneCollection to the model.
* const entity = viewer.entities.add({
* position : Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, 10000),
* model : {
* uri : 'model.gltf',
* minimumPixelSize : 128,
* maximumScale : 20000,
* clippingPlanes : clippingPlanes
* }
* });
* viewer.zoomTo(entity);
*/
function ClippingPlaneCollection(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._planes = [];
// Do partial texture updates if just one plane is dirty.
// If many planes are dirty, refresh the entire texture.
this._dirtyIndex = -1;
this._multipleDirtyPlanes = false;
this._enabled = options.enabled ?? true;
/**
* 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(options.modelMatrix ?? Matrix4.IDENTITY);
/**
* The color applied to highlight the edge along which an object is clipped.
*
* @type {Color}
* @default Color.WHITE
*/
this.edgeColor = Color.clone(options.edgeColor ?? Color.WHITE);
/**
* The width, in pixels, of the highlight applied to the edge along which an object is clipped.
*
* @type {number}
* @default 0.0
*/
this.edgeWidth = options.edgeWidth ?? 0.0;
/**
* 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();
// If this ClippingPlaneCollection has an owner, only its owner should update or destroy it.
// This is because in a Cesium3DTileset multiple models may reference the tileset's ClippingPlaneCollection.
this._owner = undefined;
const unionClippingRegions = options.unionClippingRegions ?? false;
this._unionClippingRegions = unionClippingRegions;
this._testIntersection = unionClippingRegions
? unionIntersectFunction
: defaultIntersectFunction;
this._uint8View = undefined;
this._float32View = undefined;
this._clippingPlanesTexture = undefined;
// Add each ClippingPlane object.
const planes = options.planes;
if (defined(planes)) {
const planesLength = planes.length;
for (let i = 0; i < planesLength; ++i) {
this.add(planes[i]);
}
}
}
function unionIntersectFunction(value) {
return value === Intersect.OUTSIDE;
}
function defaultIntersectFunction(value) {
return value === Intersect.INSIDE;
}
Object.defineProperties(ClippingPlaneCollection.prototype, {
/**
* Returns the number of planes in this collection. This is commonly used with
* {@link ClippingPlaneCollection#get} to iterate over all the planes
* in the collection.
*
* @memberof ClippingPlaneCollection.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 ClippingPlaneCollection.prototype
* @type {boolean}
* @default false
*/
unionClippingRegions: {
get: function () {
return this._unionClippingRegions;
},
set: function (value) {
if (this._unionClippingRegions === value) {
return;
}
this._unionClippingRegions = value;
this._testIntersection = value
? unionIntersectFunction
: defaultIntersectFunction;
},
},
/**
* If true, clipping will be enabled.
*
* @memberof ClippingPlaneCollection.prototype
* @type {boolean}
* @default true
*/
enabled: {
get: function () {
return this._enabled;
},
set: function (value) {
if (this._enabled === value) {
return;
}
this._enabled = value;
},
},
/**
* Returns a texture containing packed, untransformed clipping planes.
*
* @memberof ClippingPlaneCollection.prototype
* @type {Texture}
* @readonly
* @private
*/
texture: {
get: function () {
return this._clippingPlanesTexture;
},
},
/**
* A reference to the ClippingPlaneCollection's owner, if any.
*
* @memberof ClippingPlaneCollection.prototype
* @readonly
* @private
*/
owner: {
get: function () {
return this._owner;
},
},
/**
* Returns a Number encapsulating the state for this ClippingPlaneCollection.
*
* 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 ClippingPlaneCollection.prototype
* @returns {number} A Number that describes the ClippingPlaneCollection's state.
* @readonly
* @private
*/
clippingPlanesState: {
get: function () {
return this._unionClippingRegions
? this._planes.length
: -this._planes.length;
},
},
});
function setIndexDirty(collection, index) {
// If there's already a different _dirtyIndex set, more than one plane has changed since update.
// Entire texture must be reloaded
collection._multipleDirtyPlanes =
collection._multipleDirtyPlanes ||
(collection._dirtyIndex !== -1 && collection._dirtyIndex !== index);
collection._dirtyIndex = index;
}
/**
* Adds the specified {@link ClippingPlane} to the collection to be used to selectively disable rendering
* on the outside of each plane. Use {@link ClippingPlaneCollection#unionClippingRegions} to modify
* how modify the clipping behavior of multiple planes.
*
* @param {ClippingPlane} plane The ClippingPlane to add to the collection.
*
* @see ClippingPlaneCollection#unionClippingRegions
* @see ClippingPlaneCollection#remove
* @see ClippingPlaneCollection#removeAll
*/
ClippingPlaneCollection.prototype.add = function (plane) {
const newPlaneIndex = this._planes.length;
const that = this;
plane.onChangeCallback = function (index) {
setIndexDirty(that, index);
};
plane.index = newPlaneIndex;
setIndexDirty(this, 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 ClippingPlaneCollection#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 ClippingPlaneCollection#length
*/
ClippingPlaneCollection.prototype.get = function (index) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("index", index);
//>>includeEnd('debug');
return this._planes[index];
};
function indexOf(planes, plane) {
const length = planes.length;
for (let i = 0; i < 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 ClippingPlaneCollection#get
*/
ClippingPlaneCollection.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 ClippingPlaneCollection#add
* @see ClippingPlaneCollection#contains
* @see ClippingPlaneCollection#removeAll
*/
ClippingPlaneCollection.prototype.remove = function (clippingPlane) {
const planes = this._planes;
const index = indexOf(planes, clippingPlane);
if (index === -1) {
return false;
}
// Unlink this ClippingPlaneCollection 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;
}
}
// Indicate planes texture is dirty
this._multipleDirtyPlanes = true;
planes.length = length;
this.planeRemoved.raiseEvent(clippingPlane, index);
return true;
};
/**
* Removes all planes from the collection.
*
* @see ClippingPlaneCollection#add
* @see ClippingPlaneCollection#remove
*/
ClippingPlaneCollection.prototype.removeAll = function () {
// Dereference this ClippingPlaneCollection from all ClippingPlanes
const planes = this._planes;
const planesCount = planes.length;
for (let i = 0; i < planesCount; ++i) {
const plane = planes[i];
if (plane instanceof ClippingPlane) {
plane.onChangeCallback = undefined;
plane.index = -1;
}
this.planeRemoved.raiseEvent(plane, i);
}
this._multipleDirtyPlanes = true;
this._planes = [];
};
const distanceEncodeScratch = new Cartesian4();
const oct32EncodeScratch = new Cartesian4();
function packPlanesAsUint8(clippingPlaneCollection, startIndex, endIndex) {
const uint8View = clippingPlaneCollection._uint8View;
const planes = clippingPlaneCollection._planes;
let byteIndex = 0;
for (let i = startIndex; i < endIndex; ++i) {
const plane = planes[i];
const oct32Normal = AttributeCompression.octEncodeToCartesian4(
plane.normal,
oct32EncodeScratch,
);
uint8View[byteIndex] = oct32Normal.x;
uint8View[byteIndex + 1] = oct32Normal.y;
uint8View[byteIndex + 2] = oct32Normal.z;
uint8View[byteIndex + 3] = oct32Normal.w;
const encodedDistance = Cartesian4.packFloat(
plane.distance,
distanceEncodeScratch,
);
uint8View[byteIndex + 4] = encodedDistance.x;
uint8View[byteIndex + 5] = encodedDistance.y;
uint8View[byteIndex + 6] = encodedDistance.z;
uint8View[byteIndex + 7] = encodedDistance.w;
byteIndex += 8;
}
}
// Pack starting at the beginning of the buffer to allow partial update
function packPlanesAsFloats(clippingPlaneCollection, startIndex, endIndex) {
const float32View = clippingPlaneCollection._float32View;
const planes = clippingPlaneCollection._planes;
let floatIndex = 0;
for (let i = startIndex; i < endIndex; ++i) {
const plane = planes[i];
const normal = plane.normal;
float32View[floatIndex] = normal.x;
float32View[floatIndex + 1] = normal.y;
float32View[floatIndex + 2] = normal.z;
float32View[floatIndex + 3] = plane.distance;
floatIndex += 4; // each plane is 4 floats
}
}
function computeTextureResolution(pixelsNeeded, result) {
const maxSize = ContextLimits.maximumTextureSize;
result.x = Math.min(pixelsNeeded, maxSize);
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>
*/
ClippingPlaneCollection.prototype.update = function (frameState) {
let clippingPlanesTexture = this._clippingPlanesTexture;
const context = frameState.context;
const useFloatTexture = ClippingPlaneCollection.useFloatTexture(context);
// 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 bytes or 2 pixels in RGBA.
const pixelsNeeded = useFloatTexture ? this.length : this.length * 2;
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 clipping 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;
if (useFloatTexture) {
clippingPlanesTexture = new Texture({
context: 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,
);
} else {
clippingPlanesTexture = new Texture({
context: context,
width: requiredResolution.x,
height: requiredResolution.y,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
sampler: Sampler.NEAREST,
flipY: false,
});
this._uint8View = new Uint8Array(
requiredResolution.x * requiredResolution.y * 4,
);
}
this._clippingPlanesTexture = clippingPlanesTexture;
this._multipleDirtyPlanes = true;
}
const dirtyIndex = this._dirtyIndex;
if (!this._multipleDirtyPlanes && dirtyIndex === -1) {
return;
}
if (!this._multipleDirtyPlanes) {
// partial updates possible
let offsetX = 0;
let offsetY = 0;
if (useFloatTexture) {
offsetY = Math.floor(dirtyIndex / clippingPlanesTexture.width);
offsetX = Math.floor(dirtyIndex - offsetY * clippingPlanesTexture.width);
packPlanesAsFloats(this, dirtyIndex, dirtyIndex + 1);
clippingPlanesTexture.copyFrom({
source: {
width: 1,
height: 1,
arrayBufferView: this._float32View,
},
xOffset: offsetX,
yOffset: offsetY,
});
} else {
offsetY = Math.floor((dirtyIndex * 2) / clippingPlanesTexture.width);
offsetX = Math.floor(
dirtyIndex * 2 - offsetY * clippingPlanesTexture.width,
);
packPlanesAsUint8(this, dirtyIndex, dirtyIndex + 1);
clippingPlanesTexture.copyFrom({
source: {
width: 2,
height: 1,
arrayBufferView: this._uint8View,
},
xOffset: offsetX,
yOffset: offsetY,
});
}
} else if (useFloatTexture) {
packPlanesAsFloats(this, 0, this._planes.length);
clippingPlanesTexture.copyFrom({
source: {
width: clippingPlanesTexture.width,
height: clippingPlanesTexture.height,
arrayBufferView: this._float32View,
},
});
} else {
packPlanesAsUint8(this, 0, this._planes.length);
clippingPlanesTexture.copyFrom({
source: {
width: clippingPlanesTexture.width,
height: clippingPlanesTexture.height,
arrayBufferView: this._uint8View,
},
});
}
this._multipleDirtyPlanes = false;
this._dirtyIndex = -1;
};
const scratchMatrix = new Matrix4();
const scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0);
/**
* Determines the type intersection with the planes of this ClippingPlaneCollection instance and the specified {@link TileBoundingVolume}.
* @private
*
* @param {object} tileBoundingVolume The volume to determine the intersection with the planes.
* @param {Matrix4} [transform] An optional, additional matrix to transform the plane to world coordinates.
* @returns {Intersect} {@link Intersect.INSIDE} if the entire volume is on the side of the planes
* the normal is pointing and should be entirely rendered, {@link Intersect.OUTSIDE}
* if the entire volume is on the opposite side and should be clipped, and
* {@link Intersect.INTERSECTING} if the volume intersects the planes.
*/
ClippingPlaneCollection.prototype.computeIntersectionWithBoundingVolume =
function (tileBoundingVolume, transform) {
const planes = this._planes;
const length = planes.length;
let modelMatrix = this.modelMatrix;
if (defined(transform)) {
modelMatrix = Matrix4.multiply(transform, modelMatrix, scratchMatrix);
}
// If the collection is not set to union the clipping regions, the volume must be outside of all planes to be
// considered completely clipped. If the collection is set to union the clipping regions, if the volume can be
// outside any the planes, it is considered completely clipped.
// Lastly, if not completely clipped, if any plane is intersecting, more calculations must be performed.
let intersection = Intersect.INSIDE;
if (!this.unionClippingRegions && length > 0) {
intersection = Intersect.OUTSIDE;
}
for (let i = 0; i < length; ++i) {
const plane = planes[i];
Plane.transform(plane, modelMatrix, scratchPlane); // ClippingPlane can be used for Plane math
const value = tileBoundingVolume.intersectPlane(scratchPlane);
if (value === Intersect.INTERSECTING) {
intersection = value;
} else if (this._testIntersection(value)) {
return value;
}
}
return intersection;
};
/**
* Sets the owner for the input ClippingPlaneCollection if there wasn't another owner.
* Destroys the owner's previous ClippingPlaneCollection if setting is successful.
*
* @param {ClippingPlaneCollection} [clippingPlaneCollection] A ClippingPlaneCollection (or undefined) being attached to an object
* @param {object} owner An Object that should receive the new ClippingPlaneCollection
* @param {string} key The Key for the Object to reference the ClippingPlaneCollection
* @private
*/
ClippingPlaneCollection.setOwner = function (
clippingPlaneCollection,
owner,
key,
) {
// Don't destroy the ClippingPlaneCollection if it is already owned by newOwner
if (clippingPlaneCollection === owner[key]) {
return;
}
// Destroy the existing ClippingPlaneCollection, if any
owner[key] = owner[key] && owner[key].destroy();
if (defined(clippingPlaneCollection)) {
//>>includeStart('debug', pragmas.debug);
if (defined(clippingPlaneCollection._owner)) {
throw new DeveloperError(
"ClippingPlaneCollection should only be assigned to one object",
);
}
//>>includeEnd('debug');
clippingPlaneCollection._owner = owner;
owner[key] = clippingPlaneCollection;
}
};
/**
* Function for checking if the context will allow clipping planes with floating point textures.
*
* @param {Context} context The Context that will contain clipped objects and clipping textures.
* @returns {boolean} <code>true</code> if floating point textures can be used for clipping planes.
* @private
*/
ClippingPlaneCollection.useFloatTexture = function (context) {
return context.floatingPointTexture;
};
/**
* Function for getting the clipping plane collection's texture resolution.
* If the ClippingPlaneCollection hasn't been updated, returns the resolution that will be
* allocated based on the current plane count.
*
* @param {ClippingPlaneCollection} 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
*/
ClippingPlaneCollection.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.useFloatTexture(context)
? clippingPlaneCollection.length
: clippingPlaneCollection.length * 2;
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 ClippingPlaneCollection#destroy
*/
ClippingPlaneCollection.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 /><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
* clippingPlanes = clippingPlanes && clippingPlanes.destroy();
*
* @see ClippingPlaneCollection#isDestroyed
*/
ClippingPlaneCollection.prototype.destroy = function () {
this._clippingPlanesTexture =
this._clippingPlanesTexture && this._clippingPlanesTexture.destroy();
return destroyObject(this);
};
export default ClippingPlaneCollection;
+300
View File
@@ -0,0 +1,300 @@
import Check from "../Core/Check.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import CesiumMath from "../Core/Math.js";
import PolygonGeometry from "../Core/PolygonGeometry.js";
import Rectangle from "../Core/Rectangle.js";
/**
* A geodesic polygon to be used with {@link ClippingPlaneCollection} for selectively hiding regions in a model, a 3D tileset, or the globe.
* @alias ClippingPolygon
* @constructor
*
* @param {object} options Object with the following properties:
* @param {Cartesian3[]} options.positions A list of three or more Cartesian coordinates defining the outer ring of the clipping polygon.
* @param {Ellipsoid} [options.ellipsoid=Ellipsoid.default]
*
* @example
* const positions = Cesium.Cartesian3.fromRadiansArray([
* -1.3194369277314022,
* 0.6988062530900625,
* -1.31941,
* 0.69879,
* -1.3193955980204217,
* 0.6988091578771254,
* -1.3193931220959367,
* 0.698743632490865,
* -1.3194358224045408,
* 0.6987471965556998,
* ]);
*
* const polygon = new Cesium.ClippingPolygon({
* positions: positions
* });
*/
function ClippingPolygon(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options", options);
Check.typeOf.object("options.positions", options.positions);
Check.typeOf.number.greaterThanOrEquals(
"options.positions.length",
options.positions.length,
3,
);
//>>includeEnd('debug');
this._ellipsoid = options.ellipsoid ?? Ellipsoid.default;
this._positions = copyArrayCartesian3(options.positions);
/**
* A copy of the input positions.
*
* This is used to detect modifications of the positions in
* <code>coputeRectangle</code>: The rectangle only has
* to be re-computed when these positions have changed.
*
* @type {Cartesian3[]|undefined}
* @private
*/
this._cachedPositions = undefined;
/**
* A cached version of the rectangle that is computed in
* <code>computeRectangle</code>.
*
* This is only re-computed when the positions have changed, as
* determined by comparing the <code>_positions</code> to the
* <code>_cachedPositions</code>
*
* @type {Rectangle|undefined}
* @private
*/
this._cachedRectangle = undefined;
}
/**
* Returns a deep copy of the given array.
*
* If the input is undefined, then <code>undefined</code> is returned.
*
* Otherwise, the result will be a copy of the given array, where
* each element is copied with <code>Cartesian3.clone</code>.
*
* @param {Cartesian3[]|undefined} input The input array
* @returns {Cartesian3[]|undefined} The copy
*/
function copyArrayCartesian3(input) {
if (!defined(input)) {
return undefined;
}
const n = input.length;
const output = Array(n);
for (let i = 0; i < n; i++) {
output[i] = Cartesian3.clone(input[i]);
}
return output;
}
/**
* Returns whether the given arrays are component-wise equal.
*
* When both arrays are undefined, then <code>true</code> is returned.
* When only one array is defined, or they are both defined but have
* different lengths, then <code>false</code> is returned.
*
* Otherwise, returns whether the corresponding elements of the arrays
* are equal, as of <code>Cartesian3.equals</code>.
*
* @param {Cartesian3[]|undefined} a The first array
* @param {Cartesian3[]|undefined} b The second array
* @returns {boolean} Whether the arrays are equal
*/
function equalsArrayCartesian3(a, b) {
if (!defined(a) && !defined(b)) {
return true;
}
if (defined(a) !== defined(b)) {
return false;
}
if (a.length !== b.length) {
return false;
}
const n = a.length;
for (let i = 0; i < n; i++) {
const ca = a[i];
const cb = b[i];
if (!Cartesian3.equals(ca, cb)) {
return false;
}
}
return true;
}
Object.defineProperties(ClippingPolygon.prototype, {
/**
* Returns the total number of positions in the polygon, include any holes.
*
* @memberof ClippingPolygon.prototype
* @type {number}
* @readonly
*/
length: {
get: function () {
return this._positions.length;
},
},
/**
* Returns the outer ring of positions.
*
* @memberof ClippingPolygon.prototype
* @type {Cartesian3[]}
* @readonly
*/
positions: {
get: function () {
return this._positions;
},
},
/**
* Returns the ellipsoid used to project the polygon onto surfaces when clipping.
*
* @memberof ClippingPolygon.prototype
* @type {Ellipsoid}
* @readonly
*/
ellipsoid: {
get: function () {
return this._ellipsoid;
},
},
});
/**
* Clones the ClippingPolygon without setting its ownership.
* @param {ClippingPolygon} polygon The ClippingPolygon to be cloned
* @param {ClippingPolygon} [result] The object on which to store the cloned parameters.
* @returns {ClippingPolygon} a clone of the input ClippingPolygon
*/
ClippingPolygon.clone = function (polygon, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("polygon", polygon);
//>>includeEnd('debug');
if (!defined(result)) {
return new ClippingPolygon({
positions: polygon.positions,
ellipsoid: polygon.ellipsoid,
});
}
result._ellipsoid = polygon.ellipsoid;
result._positions.length = 0;
result._positions.push(...polygon.positions);
return result;
};
/**
* Compares the provided ClippingPolygons and returns
* <code>true</code> if they are equal, <code>false</code> otherwise.
*
* @param {ClippingPolygon} left The first polygon.
* @param {ClippingPolygon} right The second polygon.
* @returns {boolean} <code>true</code> if left and right are equal, <code>false</code> otherwise.
*/
ClippingPolygon.equals = function (left, right) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("left", left);
Check.typeOf.object("right", right);
//>>includeEnd('debug');
return (
left.ellipsoid.equals(right.ellipsoid) && left.positions === right.positions
);
};
/**
* Computes a cartographic rectangle which encloses the polygon defined by the list of positions, including cases over the international date line and the poles.
*
* @param {Rectangle} [result] An object in which to store the result.
* @returns {Rectangle} The result rectangle
*/
ClippingPolygon.prototype.computeRectangle = function (result) {
if (equalsArrayCartesian3(this._positions, this._cachedPositions)) {
return Rectangle.clone(this._cachedRectangle, result);
}
const rectangle = PolygonGeometry.computeRectangleFromPositions(
this.positions,
this.ellipsoid,
undefined,
result,
);
this._cachedPositions = copyArrayCartesian3(this._positions);
this._cachedRectangle = Rectangle.clone(rectangle);
return rectangle;
};
const scratchRectangle = new Rectangle();
const spherePointScratch = new Cartesian3();
/**
* Computes a rectangle with the spherical extents that encloses the polygon defined by the list of positions, including cases over the international date line and the poles.
*
* @private
*
* @param {Rectangle} [result] An object in which to store the result.
* @returns {Rectangle} The result rectangle with spherical extents.
*/
ClippingPolygon.prototype.computeSphericalExtents = function (result) {
if (!defined(result)) {
result = new Rectangle();
}
const rectangle = this.computeRectangle(scratchRectangle);
let spherePoint = Cartographic.toCartesian(
Rectangle.southwest(rectangle),
this.ellipsoid,
spherePointScratch,
);
// Project into plane with vertical for latitude
let magXY = Math.sqrt(
spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y,
);
// Use fastApproximateAtan2 for alignment with shader
let sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
let sphereLongitude = CesiumMath.fastApproximateAtan2(
spherePoint.x,
spherePoint.y,
);
result.south = sphereLatitude;
result.west = sphereLongitude;
spherePoint = Cartographic.toCartesian(
Rectangle.northeast(rectangle),
this.ellipsoid,
spherePointScratch,
);
// Project into plane with vertical for latitude
magXY = Math.sqrt(
spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y,
);
// Use fastApproximateAtan2 for alignment with shader
sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
sphereLongitude = CesiumMath.fastApproximateAtan2(
spherePoint.x,
spherePoint.y,
);
result.north = sphereLatitude;
result.east = sphereLongitude;
return result;
};
export default ClippingPolygon;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
/**
* Specifies the type of the cloud that is added to a {@link CloudCollection} in {@link CloudCollection#add}.
*
* @enum {number}
*/
const CloudType = {
/**
* Cumulus cloud.
*
* @type {number}
* @constant
*/
CUMULUS: 0,
};
/**
* Validates that the provided cloud type is a valid {@link CloudType}
*
* @param {CloudType} cloudType The cloud type to validate.
* @returns {boolean} <code>true</code> if the provided cloud type is a valid value; otherwise, <code>false</code>.
*
* @example
* if (!Cesium.CloudType.validate(cloudType)) {
* throw new Cesium.DeveloperError('cloudType must be a valid value.');
* }
*/
CloudType.validate = function (cloudType) {
return cloudType === CloudType.CUMULUS;
};
Object.freeze(CloudType);
export default CloudType;
+36
View File
@@ -0,0 +1,36 @@
import CesiumMath from "../Core/Math.js";
/**
* Defines different modes for blending between a target color and a primitive's source color.
*
* HIGHLIGHT multiplies the source color by the target color
* REPLACE replaces the source color with the target color
* MIX blends the source color and target color together
*
* @enum {number}
*
* @see Model.colorBlendMode
*/
const ColorBlendMode = {
HIGHLIGHT: 0,
REPLACE: 1,
MIX: 2,
};
/**
* @private
*/
ColorBlendMode.getColorBlend = function (colorBlendMode, colorBlendAmount) {
if (colorBlendMode === ColorBlendMode.HIGHLIGHT) {
return 0.0;
} else if (colorBlendMode === ColorBlendMode.REPLACE) {
return 1.0;
} else if (colorBlendMode === ColorBlendMode.MIX) {
// The value 0.0 is reserved for highlight, so clamp to just above 0.0.
return CesiumMath.clamp(colorBlendAmount, CesiumMath.EPSILON4, 1.0);
}
};
Object.freeze(ColorBlendMode);
export default ColorBlendMode;
@@ -0,0 +1,365 @@
import Cartesian3 from "../Core/Cartesian3.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import getMagic from "../Core/getMagic.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Represents the contents of a
* {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Composite|Composite}
* tile in a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles} tileset.
* <p>
* Implements the {@link Cesium3DTileContent} interface.
* </p>
*
* @implements Cesium3DTileContent
* @private
*/
class Composite3DTileContent {
constructor(tileset, tile, resource, contents) {
this._tileset = tileset;
this._tile = tile;
this._resource = resource;
if (!defined(contents)) {
contents = [];
}
this._contents = contents;
this._metadata = undefined;
this._group = undefined;
this._ready = false;
}
get featurePropertiesDirty() {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
if (contents[i].featurePropertiesDirty) {
return true;
}
}
return false;
}
set featurePropertiesDirty(value) {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].featurePropertiesDirty = value;
}
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>featuresLength</code> for a tile in the composite.
*/
get featuresLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>pointsLength</code> for a tile in the composite.
*/
get pointsLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>trianglesLength</code> for a tile in the composite.
*/
get trianglesLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>geometryByteLength</code> for a tile in the composite.
*/
get geometryByteLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>texturesByteLength</code> for a tile in the composite.
*/
get texturesByteLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>batchTableByteLength</code> for a tile in the composite.
*/
get batchTableByteLength() {
return 0;
}
get innerContents() {
return this._contents;
}
/**
* Returns true when the tile's content is ready to render; otherwise false
*
*
* @type {boolean}
* @readonly
* @private
*/
get ready() {
return this._ready;
}
get tileset() {
return this._tileset;
}
get tile() {
return this._tile;
}
get url() {
return this._resource.getUrlComponent(true);
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* both stores the content metadata and propagates the content metadata to all of its children.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
get metadata() {
return this._metadata;
}
set metadata(value) {
this._metadata = value;
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].metadata = value;
}
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>undefined</code>. Instead call <code>batchTable</code> for a tile in the composite.
*/
get batchTable() {
return undefined;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* both stores the group metadata and propagates the group metadata to all of its children.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
get group() {
return this._group;
}
set group(value) {
this._group = value;
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].group = value;
}
}
static async fromTileType(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
factory,
) {
byteOffset = byteOffset ?? 0;
const uint8Array = new Uint8Array(arrayBuffer);
const view = new DataView(arrayBuffer);
byteOffset += sizeOfUint32; // Skip magic
const version = view.getUint32(byteOffset, true);
if (version !== 1) {
throw new RuntimeError(
`Only Composite Tile version 1 is supported. Version ${version} is not.`,
);
}
byteOffset += sizeOfUint32;
// Skip byteLength
byteOffset += sizeOfUint32;
const tilesLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
// For caching purposes, models within the composite tile must be
// distinguished. To do this, add a query parameter ?compositeIndex=i.
// Since composite tiles may contain other composite tiles, check for an
// existing prefix and separate them with underscores. e.g.
// ?compositeIndex=0_1_1
let prefix = resource.queryParameters.compositeIndex;
if (defined(prefix)) {
// We'll be adding another value at the end, so add an underscore.
prefix = `${prefix}_`;
} else {
// no prefix
prefix = "";
}
const promises = [];
promises.length = tilesLength;
for (let i = 0; i < tilesLength; ++i) {
const tileType = getMagic(uint8Array, byteOffset);
// Tile byte length is stored after magic and version
const tileByteLength = view.getUint32(
byteOffset + sizeOfUint32 * 2,
true,
);
const contentFactory = factory[tileType];
// Label which content within the composite this is
const compositeIndex = `${prefix}${i}`;
const childResource = resource.getDerivedResource({
queryParameters: {
compositeIndex: compositeIndex,
},
});
if (defined(contentFactory)) {
promises[i] = Promise.resolve(
contentFactory(tileset, tile, childResource, arrayBuffer, byteOffset),
);
} else {
throw new RuntimeError(
`Unknown tile content type, ${tileType}, inside Composite tile`,
);
}
byteOffset += tileByteLength;
}
const innerContents = await Promise.all(promises);
const content = new Composite3DTileContent(
tileset,
tile,
resource,
innerContents,
);
return content;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>false</code>. Instead call <code>hasProperty</code> for a tile in the composite.
*/
hasProperty(batchId, name) {
return false;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>undefined</code>. Instead call <code>getFeature</code> for a tile in the composite.
*/
getFeature(batchId) {
return undefined;
}
applyDebugSettings(enabled, color) {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].applyDebugSettings(enabled, color);
}
}
applyStyle(style) {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].applyStyle(style);
}
}
update(tileset, frameState) {
const contents = this._contents;
const length = contents.length;
let ready = true;
for (let i = 0; i < length; ++i) {
contents[i].update(tileset, frameState);
ready = ready && contents[i].ready;
}
if (!this._ready && ready) {
this._ready = true;
}
}
/**
* Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
*
* @param {Ray} ray The ray to test for intersection.
* @param {FrameState} frameState The frame state.
* @param {Cartesian3|undefined} [result] The intersection or <code>undefined</code> if none was found.
* @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
*
* @private
*/
pick(ray, frameState, result) {
if (!this._ready) {
return undefined;
}
let intersection;
let minDistance = Number.POSITIVE_INFINITY;
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
const candidate = contents[i].pick(ray, frameState, result);
if (!defined(candidate)) {
continue;
}
const distance = Cartesian3.distance(ray.origin, candidate);
if (distance < minDistance) {
intersection = candidate;
minDistance = distance;
}
}
if (!defined(intersection)) {
return undefined;
}
return result;
}
isDestroyed() {
return false;
}
destroy() {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].destroy();
}
return destroyObject(this);
}
}
const sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;
export default Composite3DTileContent;
@@ -0,0 +1,219 @@
import addAllToArray from "../Core/addAllToArray.js";
import clone from "../Core/clone.js";
import defined from "../Core/defined.js";
import Expression from "./Expression.js";
/**
* An expression for a style applied to a {@link Cesium3DTileset}.
* <p>
* Evaluates a conditions expression defined using the
* {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling|3D Tiles Styling language}.
* </p>
* <p>
* Implements the {@link StyleExpression} interface.
* </p>
*
* @alias ConditionsExpression
* @constructor
*
* @param {object} [conditionsExpression] The conditions expression defined using the 3D Tiles Styling language.
* @param {object} [defines] Defines in the style.
*
* @example
* const expression = new Cesium.ConditionsExpression({
* conditions : [
* ['${Area} > 10, 'color("#FF0000")'],
* ['${id} !== "1"', 'color("#00FF00")'],
* ['true', 'color("#FFFFFF")']
* ]
* });
* expression.evaluateColor(feature, result); // returns a Cesium.Color object
*/
function ConditionsExpression(conditionsExpression, defines) {
this._conditionsExpression = clone(conditionsExpression, true);
this._conditions = conditionsExpression.conditions;
this._runtimeConditions = undefined;
setRuntime(this, defines);
}
Object.defineProperties(ConditionsExpression.prototype, {
/**
* Gets the conditions expression defined in the 3D Tiles Styling language.
*
* @memberof ConditionsExpression.prototype
*
* @type {object}
* @readonly
*
* @default undefined
*/
conditionsExpression: {
get: function () {
return this._conditionsExpression;
},
},
});
function Statement(condition, expression) {
this.condition = condition;
this.expression = expression;
}
function setRuntime(expression, defines) {
const runtimeConditions = [];
const conditions = expression._conditions;
if (!defined(conditions)) {
return;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
const cond = String(statement[0]);
const condExpression = String(statement[1]);
runtimeConditions.push(
new Statement(
new Expression(cond, defines),
new Expression(condExpression, defines),
),
);
}
expression._runtimeConditions = runtimeConditions;
}
/**
* Evaluates the result of an expression, optionally using the provided feature's properties. If the result of
* the expression in the
* {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling|3D Tiles Styling language}
* is of type <code>Boolean</code>, <code>Number</code>, or <code>String</code>, the corresponding JavaScript
* primitive type will be returned. If the result is a <code>RegExp</code>, a Javascript <code>RegExp</code>
* object will be returned. If the result is a <code>Cartesian2</code>, <code>Cartesian3</code>, or <code>Cartesian4</code>,
* a {@link Cartesian2}, {@link Cartesian3}, or {@link Cartesian4} object will be returned. If the <code>result</code> argument is
* a {@link Color}, the {@link Cartesian4} value is converted to a {@link Color} and then returned.
*
* @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression.
* @param {object} [result] The object onto which to store the result.
* @returns {boolean|number|string|RegExp|Cartesian2|Cartesian3|Cartesian4|Color} The result of evaluating the expression.
*/
ConditionsExpression.prototype.evaluate = function (feature, result) {
const conditions = this._runtimeConditions;
if (!defined(conditions)) {
return undefined;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
if (statement.condition.evaluate(feature)) {
return statement.expression.evaluate(feature, result);
}
}
};
/**
* Evaluates the result of a Color expression, using the values defined by a feature.
* <p>
* This is equivalent to {@link ConditionsExpression#evaluate} but always returns a {@link Color} object.
* </p>
* @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression.
* @param {Color} [result] The object in which to store the result
* @returns {Color} The modified result parameter or a new Color instance if one was not provided.
*/
ConditionsExpression.prototype.evaluateColor = function (feature, result) {
const conditions = this._runtimeConditions;
if (!defined(conditions)) {
return undefined;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
if (statement.condition.evaluate(feature)) {
return statement.expression.evaluateColor(feature, result);
}
}
};
/**
* Gets the shader function for this expression.
* Returns undefined if the shader function can't be generated from this expression.
*
* @param {string} functionSignature Signature of the generated function.
* @param {object} variableSubstitutionMap Maps variable names to shader variable names.
* @param {object} shaderState Stores information about the generated shader function, including whether it is translucent.
* @param {string} returnType The return type of the generated function.
*
* @returns {string} The shader function.
*
* @private
*/
ConditionsExpression.prototype.getShaderFunction = function (
functionSignature,
variableSubstitutionMap,
shaderState,
returnType,
) {
const conditions = this._runtimeConditions;
if (!defined(conditions) || conditions.length === 0) {
return undefined;
}
let shaderFunction = "";
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
const condition = statement.condition.getShaderExpression(
variableSubstitutionMap,
shaderState,
);
const expression = statement.expression.getShaderExpression(
variableSubstitutionMap,
shaderState,
);
// Build the if/else chain from the list of conditions
shaderFunction +=
` ${i === 0 ? "if" : "else if"} (${condition})\n` +
` {\n` +
` return ${expression};\n` +
` }\n`;
}
shaderFunction =
`${returnType} ${functionSignature}\n` +
`{\n${shaderFunction} return ${returnType}(1.0);\n` + // Return a default value if no conditions are met
`}\n`;
return shaderFunction;
};
/**
* Gets the variables used by the expression.
*
* @returns {string[]} The variables used by the expression.
*
* @private
*/
ConditionsExpression.prototype.getVariables = function () {
let variables = [];
const conditions = this._runtimeConditions;
if (!defined(conditions) || conditions.length === 0) {
return variables;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
addAllToArray(variables, statement.condition.getVariables());
addAllToArray(variables, statement.expression.getVariables());
}
// Remove duplicates
variables = variables.filter(function (variable, index, variables) {
return variables.indexOf(variable) === index;
});
return variables;
};
export default ConditionsExpression;
+61
View File
@@ -0,0 +1,61 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
const defaultAngle = CesiumMath.toRadians(30.0);
/**
* A ParticleEmitter that emits particles within a cone.
* Particles will be positioned at the tip of the cone and have initial velocities going towards the base.
*
* @alias ConeEmitter
* @constructor
*
* @param {number} [angle=Cesium.Math.toRadians(30.0)] The angle of the cone in radians.
*/
function ConeEmitter(angle) {
this._angle = angle ?? defaultAngle;
}
Object.defineProperties(ConeEmitter.prototype, {
/**
* The angle of the cone in radians.
* @memberof CircleEmitter.prototype
* @type {number}
* @default Cesium.Math.toRadians(30.0)
*/
angle: {
get: function () {
return this._angle;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
this._angle = value;
},
},
});
/**
* Initializes the given {Particle} by setting it's position and velocity.
*
* @private
* @param {Particle} particle The particle to initialize
*/
ConeEmitter.prototype.emit = function (particle) {
const radius = Math.tan(this._angle);
// Compute a random point on the cone's base
const theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
const rad = CesiumMath.randomBetween(0.0, radius);
const x = rad * Math.cos(theta);
const y = rad * Math.sin(theta);
const z = 1.0;
particle.velocity = Cartesian3.fromElements(x, y, z, particle.velocity);
Cartesian3.normalize(particle.velocity, particle.velocity);
particle.position = Cartesian3.clone(Cartesian3.ZERO, particle.position);
};
export default ConeEmitter;
+184
View File
@@ -0,0 +1,184 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import MetadataEntity from "./MetadataEntity.js";
/**
* Metadata about the content of a 3D Tile. This represents the content metadata JSON (3D Tiles 1.1)
* or the <code>3DTILES_metadata</code> extension on a single {@link Cesium3DTileContent}
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata|3DTILES_metadata Extension} for 3D Tiles
* </p>
*
* @param {object} options Object with the following properties:
* @param {object} options.content Either the content metadata JSON (3D Tiles 1.1) or the extension JSON attached to the content.
* @param {MetadataClass} options.class The class that the content metadata conforms to.
*
* @alias ContentMetadata
* @constructor
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
function ContentMetadata(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const content = options.content;
const metadataClass = options.class;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.content", content);
Check.typeOf.object("options.class", metadataClass);
//>>includeEnd('debug');
this._class = metadataClass;
this._properties = content.properties;
this._extensions = content.extensions;
this._extras = content.extras;
}
Object.defineProperties(ContentMetadata.prototype, {
/**
* The class that properties conform to.
*
* @memberof ContentMetadata.prototype
* @type {MetadataClass}
* @readonly
* @private
*/
class: {
get: function () {
return this._class;
},
},
/**
* Extra user-defined properties.
*
* @memberof ContentMetadata.prototype
* @type {object}
* @readonly
* @private
*/
extras: {
get: function () {
return this._extras;
},
},
/**
* An object containing extensions.
*
* @memberof ContentMetadata.prototype
* @type {object}
* @readonly
* @private
*/
extensions: {
get: function () {
return this._extensions;
},
},
});
/**
* Returns whether the content has this property.
*
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {boolean} Whether the content has this property.
* @private
*/
ContentMetadata.prototype.hasProperty = function (propertyId) {
return MetadataEntity.hasProperty(propertyId, this._properties, this._class);
};
/**
* Returns whether the content has a property with the given semantic.
*
* @param {string} semantic The case-sensitive semantic of the property.
* @returns {boolean} Whether the content has a property with the given semantic.
* @private
*/
ContentMetadata.prototype.hasPropertyBySemantic = function (semantic) {
return MetadataEntity.hasPropertyBySemantic(
semantic,
this._properties,
this._class,
);
};
/**
* Returns an array of property IDs.
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The property IDs.
* @private
*/
ContentMetadata.prototype.getPropertyIds = function (results) {
return MetadataEntity.getPropertyIds(this._properties, this._class, results);
};
/**
* Returns a copy of the value of the property with the given ID.
* <p>
* If the property is normalized the normalized value is returned.
* </p>
*
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {*} The value of the property or <code>undefined</code> if the content does not have this property.
* @private
*/
ContentMetadata.prototype.getProperty = function (propertyId) {
return MetadataEntity.getProperty(propertyId, this._properties, this._class);
};
/**
* Sets the value of the property with the given ID.
* <p>
* If the property is normalized a normalized value must be provided to this function.
* </p>
*
* @param {string} propertyId The case-sensitive ID of the property.
* @param {*} value The value of the property that will be copied.
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise.
* @private
*/
ContentMetadata.prototype.setProperty = function (propertyId, value) {
return MetadataEntity.setProperty(
propertyId,
value,
this._properties,
this._class,
);
};
/**
* Returns a copy of the value of the property with the given semantic.
*
* @param {string} semantic The case-sensitive semantic of the property.
* @returns {*} The value of the property or <code>undefined</code> if the content does not have this semantic.
* @private
*/
ContentMetadata.prototype.getPropertyBySemantic = function (semantic) {
return MetadataEntity.getPropertyBySemantic(
semantic,
this._properties,
this._class,
);
};
/**
* Sets the value of the property with the given semantic.
*
* @param {string} semantic The case-sensitive semantic of the property.
* @param {*} value The value of the property that will be copied.
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise.
* @private
*/
ContentMetadata.prototype.setPropertyBySemantic = function (semantic, value) {
return MetadataEntity.setPropertyBySemantic(
semantic,
value,
this._properties,
this._class,
);
};
export default ContentMetadata;
+654
View File
@@ -0,0 +1,654 @@
import AssociativeArray from "../Core/AssociativeArray.js";
import buildModuleUrl from "../Core/buildModuleUrl.js";
import Check from "../Core/Check.js";
import Credit from "../Core/Credit.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import Uri from "urijs";
const mobileWidth = 576;
const lightboxHeight = 100;
const textColor = "#ffffff";
const highlightColor = "#48b";
/**
* Used to sort the credits by frequency of appearance
* when they are later displayed.
*
* @alias CreditDisplay.CreditDisplayElement
* @constructor
*
* @private
*/
function CreditDisplayElement(credit, count) {
this.credit = credit;
this.count = count ?? 1;
}
function contains(credits, credit) {
const len = credits.length;
for (let i = 0; i < len; i++) {
const existingCredit = credits[i];
if (Credit.equals(existingCredit, credit)) {
return true;
}
}
return false;
}
function swapCesiumCredit(creditDisplay) {
// We don't want to clutter the screen with the Cesium logo and the Cesium ion
// logo at the same time. Since the ion logo is required, we just replace the
// Cesium logo or add the logo if the Cesium one was removed.
const previousCredit = creditDisplay._previousCesiumCredit;
const currentCredit = creditDisplay._currentCesiumCredit;
if (Credit.equals(currentCredit, previousCredit)) {
return;
}
if (defined(previousCredit)) {
creditDisplay._cesiumCreditContainer.removeChild(previousCredit.element);
}
if (defined(currentCredit)) {
creditDisplay._cesiumCreditContainer.appendChild(currentCredit.element);
}
creditDisplay._previousCesiumCredit = currentCredit;
}
const delimiterClassName = "cesium-credit-delimiter";
function createDelimiterElement(delimiter) {
const delimiterElement = document.createElement("span");
delimiterElement.textContent = delimiter;
delimiterElement.className = delimiterClassName;
return delimiterElement;
}
function createCreditElement(element, elementWrapperTagName) {
// may need to wrap the credit in another element
if (defined(elementWrapperTagName)) {
const wrapper = document.createElement(elementWrapperTagName);
wrapper._creditId = element._creditId;
wrapper.appendChild(element);
element = wrapper;
}
return element;
}
function displayCredits(container, credits, delimiter, elementWrapperTagName) {
const childNodes = container.childNodes;
let domIndex = -1;
// Sort the credits such that more frequent credits appear first
credits.sort(function (credit1, credit2) {
return credit2.count - credit1.count;
});
for (let creditIndex = 0; creditIndex < credits.length; ++creditIndex) {
const credit = credits[creditIndex].credit;
if (defined(credit)) {
domIndex = creditIndex;
if (defined(delimiter)) {
// credits may be separated by delimiters
domIndex *= 2;
if (creditIndex > 0) {
const delimiterDomIndex = domIndex - 1;
if (childNodes.length <= delimiterDomIndex) {
container.appendChild(createDelimiterElement(delimiter));
} else {
const existingDelimiter = childNodes[delimiterDomIndex];
if (existingDelimiter.className !== delimiterClassName) {
container.replaceChild(
createDelimiterElement(delimiter),
existingDelimiter,
);
}
}
}
}
const element = credit.element;
// check to see if the correct credit is in the right place
if (childNodes.length <= domIndex) {
container.appendChild(
createCreditElement(element, elementWrapperTagName),
);
} else {
const existingElement = childNodes[domIndex];
if (existingElement._creditId !== credit._id) {
// not the right credit, swap it in
container.replaceChild(
createCreditElement(element, elementWrapperTagName),
existingElement,
);
}
}
}
}
// any remaining nodes in the container are unnecessary
++domIndex;
while (domIndex < childNodes.length) {
container.removeChild(childNodes[domIndex]);
}
}
function styleLightboxContainer(that) {
const lightboxCredits = that._lightboxCredits;
const width = that.viewport.clientWidth;
const height = that.viewport.clientHeight;
if (width !== that._lastViewportWidth) {
if (width < mobileWidth) {
lightboxCredits.className =
"cesium-credit-lightbox cesium-credit-lightbox-mobile";
lightboxCredits.style.marginTop = "0";
} else {
lightboxCredits.className =
"cesium-credit-lightbox cesium-credit-lightbox-expanded";
lightboxCredits.style.marginTop = `${Math.floor(
(height - lightboxCredits.clientHeight) * 0.5,
)}px`;
}
that._lastViewportWidth = width;
}
if (width >= mobileWidth && height !== that._lastViewportHeight) {
lightboxCredits.style.marginTop = `${Math.floor(
(height - lightboxCredits.clientHeight) * 0.5,
)}px`;
that._lastViewportHeight = height;
}
}
function appendCss(container) {
const style = /*css*/ `
.cesium-credit-lightbox-overlay {
display: none;
z-index: 1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(80, 80, 80, 0.8);
}
.cesium-credit-lightbox {
background-color: #303336;
color: ${textColor};
position: relative;
min-height: ${lightboxHeight}px;
margin: auto;
}
.cesium-credit-lightbox > ul > li a,
.cesium-credit-lightbox > ul > li a:visited,
.cesium-credit-wrapper a,
.cesium-credit-wrapper a:visited {
color: ${textColor};
}
.cesium-credit-lightbox > ul > li a:hover {
color: ${highlightColor};
}
.cesium-credit-lightbox.cesium-credit-lightbox-expanded {
border: 1px solid #444;
border-radius: 5px;
max-width: 470px;
}
.cesium-credit-lightbox.cesium-credit-lightbox-mobile {
height: 100%;
width: 100%;
}
.cesium-credit-lightbox-title {
padding: 20px 20px 0 20px;
}
.cesium-credit-lightbox-close {
font-size: 18pt;
cursor: pointer;
position: absolute;
top: 0;
right: 6px;
color: ${textColor};
}
.cesium-credit-lightbox-close:hover {
color: ${highlightColor};
}
.cesium-credit-lightbox > ul {
margin: 0;
padding: 12px 20px 12px 40px;
font-size: 13px;
}
.cesium-credit-lightbox > ul > li {
padding-bottom: 6px;
}
.cesium-credit-lightbox > ul > li * {
padding: 0;
margin: 0;
}
.cesium-credit-expand-link {
padding-left: 5px;
cursor: pointer;
text-decoration: underline;
color: ${textColor};
}
.cesium-credit-expand-link:hover {
color: ${highlightColor};
}
.cesium-credit-text {
color: ${textColor};
}
.cesium-credit-delimiter {
padding: 0 5px;
}
.cesium-credit-textContainer *,
.cesium-credit-logoContainer * {
display: inline;
}
.cesium-credit-textContainer a:hover {
color: ${highlightColor}
}
.cesium-credit-textContainer .cesium-credit-wrapper:first-of-type {
padding-left: 5px;
}
`;
function getShadowRoot(container) {
if (container.shadowRoot) {
return container.shadowRoot;
}
if (container.getRootNode) {
const root = container.getRootNode();
if (root instanceof ShadowRoot) {
return root;
}
}
return undefined;
}
const shadowRootOrDocumentHead = getShadowRoot(container) ?? document.head;
const styleElem = document.createElement("style");
styleElem.innerHTML = style;
shadowRootOrDocumentHead.appendChild(styleElem);
}
/**
* The credit display is responsible for displaying credits on screen.
*
* @param {HTMLElement} container The HTML element where credits will be displayed
* @param {string} [delimiter= '•'] The string to separate text credits
* @param {HTMLElement} [viewport=document.body] The HTML element that will contain the credits popup
*
* @alias CreditDisplay
* @constructor
*
* @example
* // Add a credit with a tooltip, image and link to display onscreen
* const credit = new Cesium.Credit(`<a href="https://cesium.com/" target="_blank"><img src="/images/cesium_logo.png" title="Cesium"/></a>`, true);
* viewer.creditDisplay.addStaticCredit(credit);
*
* @example
* // Add a credit with a plaintext link to display in the lightbox
* const credit = new Cesium.Credit('<a href="https://cesium.com/" target="_blank">Cesium</a>');
* viewer.creditDisplay.addStaticCredit(credit);
*/
function CreditDisplay(container, delimiter, viewport) {
//>>includeStart('debug', pragmas.debug);
Check.defined("container", container);
//>>includeEnd('debug');
const that = this;
viewport = viewport ?? document.body;
const lightbox = document.createElement("div");
lightbox.className = "cesium-credit-lightbox-overlay";
viewport.appendChild(lightbox);
const lightboxCredits = document.createElement("div");
lightboxCredits.className = "cesium-credit-lightbox";
lightbox.appendChild(lightboxCredits);
function hideLightbox(event) {
if (lightboxCredits.contains(event.target)) {
return;
}
that.hideLightbox();
}
lightbox.addEventListener("click", hideLightbox, false);
const title = document.createElement("div");
title.className = "cesium-credit-lightbox-title";
title.textContent = "Data provided by:";
lightboxCredits.appendChild(title);
const closeButton = document.createElement("a");
closeButton.onclick = this.hideLightbox.bind(this);
closeButton.innerHTML = "&times;";
closeButton.className = "cesium-credit-lightbox-close";
lightboxCredits.appendChild(closeButton);
const creditList = document.createElement("ul");
lightboxCredits.appendChild(creditList);
const cesiumCreditContainer = document.createElement("div");
cesiumCreditContainer.className = "cesium-credit-logoContainer";
cesiumCreditContainer.style.display = "inline";
container.appendChild(cesiumCreditContainer);
const screenContainer = document.createElement("div");
screenContainer.className = "cesium-credit-textContainer";
screenContainer.style.display = "inline";
container.appendChild(screenContainer);
const expandLink = document.createElement("a");
expandLink.className = "cesium-credit-expand-link";
expandLink.onclick = this.showLightbox.bind(this);
expandLink.textContent = "Data attribution";
container.appendChild(expandLink);
appendCss(container);
const cesiumCredit = Credit.clone(CreditDisplay.cesiumCredit);
this._delimiter = delimiter ?? "•";
this._screenContainer = screenContainer;
this._cesiumCreditContainer = cesiumCreditContainer;
this._lastViewportHeight = undefined;
this._lastViewportWidth = undefined;
this._lightboxCredits = lightboxCredits;
this._creditList = creditList;
this._lightbox = lightbox;
this._hideLightbox = hideLightbox;
this._expandLink = expandLink;
this._expanded = false;
this._staticCredits = [];
this._cesiumCredit = cesiumCredit;
this._previousCesiumCredit = undefined;
this._currentCesiumCredit = cesiumCredit;
this._creditDisplayElementPool = [];
this._creditDisplayElementIndex = 0;
this._currentFrameCredits = {
screenCredits: new AssociativeArray(),
lightboxCredits: new AssociativeArray(),
};
this._defaultCredit = undefined;
this.viewport = viewport;
/**
* The HTML element where credits will be displayed.
* @type {HTMLElement}
*/
this.container = container;
}
function setCredit(creditDisplay, credits, credit, count) {
count = count ?? 1;
let creditDisplayElement = credits.get(credit.id);
if (!defined(creditDisplayElement)) {
const pool = creditDisplay._creditDisplayElementPool;
const poolIndex = creditDisplay._creditDisplayElementPoolIndex;
if (poolIndex < pool.length) {
creditDisplayElement = pool[poolIndex];
creditDisplayElement.credit = credit;
creditDisplayElement.count = count;
} else {
creditDisplayElement = new CreditDisplayElement(credit, count);
pool.push(creditDisplayElement);
}
++creditDisplay._creditDisplayElementPoolIndex;
credits.set(credit.id, creditDisplayElement);
} else if (creditDisplayElement.count < Number.MAX_VALUE) {
creditDisplayElement.count += count;
}
}
/**
* Adds a {@link Credit} that will show on screen or in the lightbox until
* the next frame. This is mostly for internal use. Use {@link CreditDisplay.addStaticCredit} to add a persistent credit to the screen.
*
* @see CreditDisplay.addStaticCredit
*
* @param {Credit} credit The credit to display in the next frame.
*/
CreditDisplay.prototype.addCreditToNextFrame = function (credit) {
//>>includeStart('debug', pragmas.debug);
Check.defined("credit", credit);
//>>includeEnd('debug');
if (credit.isIon()) {
// If this is the an ion logo credit from the ion server
// Just use the default credit (which is identical) to avoid blinking
if (!defined(this._defaultCredit)) {
this._defaultCredit = Credit.clone(getDefaultCredit());
}
this._currentCesiumCredit = this._defaultCredit;
return;
}
let credits;
if (!credit.showOnScreen) {
credits = this._currentFrameCredits.lightboxCredits;
} else {
credits = this._currentFrameCredits.screenCredits;
}
setCredit(this, credits, credit);
};
/**
* Adds a {@link Credit} that will show on screen or in the lightbox until removed with {@link CreditDisplay.removeStaticCredit}.
*
* @param {Credit} credit The credit to added
*
* @example
* // Add a credit with a tooltip, image and link to display onscreen
* const credit = new Cesium.Credit(`<a href="https://cesium.com/" target="_blank"><img src="/images/cesium_logo.png" title="Cesium"/></a>`, true);
* viewer.creditDisplay.addStaticCredit(credit);
*
* @example
* // Add a credit with a plaintext link to display in the lightbox
* const credit = new Cesium.Credit('<a href="https://cesium.com/" target="_blank">Cesium</a>');
* viewer.creditDisplay.addStaticCredit(credit);
*/
CreditDisplay.prototype.addStaticCredit = function (credit) {
//>>includeStart('debug', pragmas.debug);
Check.defined("credit", credit);
//>>includeEnd('debug');
const staticCredits = this._staticCredits;
if (!contains(staticCredits, credit)) {
staticCredits.push(credit);
}
};
/**
* Removes a static credit shown on screen or in the lightbox.
*
* @param {Credit} credit The credit to be removed.
*/
CreditDisplay.prototype.removeStaticCredit = function (credit) {
//>>includeStart('debug', pragmas.debug);
Check.defined("credit", credit);
//>>includeEnd('debug');
const staticCredits = this._staticCredits;
const index = staticCredits.indexOf(credit);
if (index !== -1) {
staticCredits.splice(index, 1);
}
};
/**
* @private
*/
CreditDisplay.prototype.showLightbox = function () {
this._lightbox.style.display = "block";
this._expanded = true;
};
/**
* @private
*/
CreditDisplay.prototype.hideLightbox = function () {
this._lightbox.style.display = "none";
this._expanded = false;
};
/**
* Updates the credit display before a new frame is rendered.
*/
CreditDisplay.prototype.update = function () {
if (this._expanded) {
styleLightboxContainer(this);
}
};
/**
* Resets the credit display to a beginning of frame state, clearing out current credits.
*/
CreditDisplay.prototype.beginFrame = function () {
const currentFrameCredits = this._currentFrameCredits;
this._creditDisplayElementPoolIndex = 0;
const screenCredits = currentFrameCredits.screenCredits;
const lightboxCredits = currentFrameCredits.lightboxCredits;
screenCredits.removeAll();
lightboxCredits.removeAll();
const staticCredits = this._staticCredits;
for (let i = 0; i < staticCredits.length; ++i) {
const staticCredit = staticCredits[i];
const creditCollection = staticCredit.showOnScreen
? screenCredits
: lightboxCredits;
if (
staticCredit.isIon() &&
Credit.equals(CreditDisplay.cesiumCredit, this._cesiumCredit)
) {
// If this is an ion logo credit from the ion server,
// make sure to de-duplicate with the default ion credit
continue;
}
setCredit(this, creditCollection, staticCredit, Number.MAX_VALUE);
}
if (!Credit.equals(CreditDisplay.cesiumCredit, this._cesiumCredit)) {
this._cesiumCredit = Credit.clone(CreditDisplay.cesiumCredit);
}
this._currentCesiumCredit = this._cesiumCredit;
};
/**
* Sets the credit display to the end of frame state, displaying credits from the last frame in the credit container.
*/
CreditDisplay.prototype.endFrame = function () {
const screenCredits = this._currentFrameCredits.screenCredits.values;
displayCredits(
this._screenContainer,
screenCredits,
this._delimiter,
undefined,
);
const lightboxCredits = this._currentFrameCredits.lightboxCredits.values;
this._expandLink.style.display =
lightboxCredits.length > 0 ? "inline" : "none";
displayCredits(this._creditList, lightboxCredits, undefined, "li");
swapCesiumCredit(this);
};
/**
* Destroys the resources held by this object. Destroying an object allows for deterministic
* release of resources, instead of relying on the garbage collector to destroy this object.
* <br /><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.
*/
CreditDisplay.prototype.destroy = function () {
this._lightbox.removeEventListener("click", this._hideLightbox, false);
this.container.removeChild(this._cesiumCreditContainer);
this.container.removeChild(this._screenContainer);
this.container.removeChild(this._expandLink);
this.viewport.removeChild(this._lightbox);
return destroyObject(this);
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*/
CreditDisplay.prototype.isDestroyed = function () {
return false;
};
CreditDisplay._cesiumCredit = undefined;
CreditDisplay._cesiumCreditInitialized = false;
let defaultCredit;
function getDefaultCredit() {
if (!defined(defaultCredit)) {
let logo = buildModuleUrl("Assets/Images/ion-credit.png");
// When hosting in a WebView, the base URL scheme is file:// or ms-appx-web://
// which is stripped out from the Credit's <img> tag; use the full path instead
if (
logo.indexOf("http://") !== 0 &&
logo.indexOf("https://") !== 0 &&
logo.indexOf("data:") !== 0
) {
const logoUrl = new Uri(logo);
logo = logoUrl.path();
}
defaultCredit = new Credit(
`<a href="https://cesium.com/" target="_blank"><img src="${logo}" style="vertical-align: -7px" title="Cesium ion"/></a>`,
true,
);
}
if (!CreditDisplay._cesiumCreditInitialized) {
CreditDisplay._cesiumCredit = defaultCredit;
CreditDisplay._cesiumCreditInitialized = true;
}
return defaultCredit;
}
Object.defineProperties(CreditDisplay, {
/**
* Gets or sets the Cesium logo credit.
* @memberof CreditDisplay
* @type {Credit}
*/
cesiumCredit: {
get: function () {
getDefaultCredit();
return CreditDisplay._cesiumCredit;
},
set: function (value) {
CreditDisplay._cesiumCredit = value;
CreditDisplay._cesiumCreditInitialized = true;
},
},
});
CreditDisplay.CreditDisplayElement = CreditDisplayElement;
export default CreditDisplay;
+351
View File
@@ -0,0 +1,351 @@
import BoxGeometry from "../Core/BoxGeometry.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import GeometryPipeline from "../Core/GeometryPipeline.js";
import Matrix4 from "../Core/Matrix4.js";
import VertexFormat from "../Core/VertexFormat.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import CubeMap from "../Renderer/CubeMap.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import loadCubeMap from "../Renderer/loadCubeMap.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderProgram from "../Renderer/ShaderProgram.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import VertexArray from "../Renderer/VertexArray.js";
import SkyBoxFS from "../Shaders/SkyBoxFS.js";
import SkyBoxVS from "../Shaders/SkyBoxVS.js";
import CubeMapPanoramaVS from "../Shaders/CubeMapPanoramaVS.js";
import BlendingState from "./BlendingState.js";
import SceneMode from "./SceneMode.js";
import Pass from "../Renderer/Pass.js";
import Credit from "../Core/Credit.js";
/**
* @typedef {object} CubeMapPanorama.ConstructorOptions
*
* Initialization options for the CubeMapPanorama constructor
*
* @property {object} [options.sources] The source URL or <code>Image</code> object for each of the six cube map faces. See the example below.
* @property {Matrix3} [options.transform] A 3x3 transformation matrix that defines the panorama’s orientation. If not specified, the default orientation is defined using the True Equator Mean Equinox (TEME) axes.
* @property {boolean} [options.show=true] Determines if this primitive will be shown.
* @property {Credit|string} [options.credit] A credit for the panorama, which is displayed on the canvas.
*
*/
/**
* A {@link Panorama} that displays imagery in cube map format in a scene.
* <p>
* This is only supported in 3D. The cube map panorama is faded out when morphing to 2D or Columbus view. The size of
* the cube map panorama must not exceed {@link Scene#maximumSkyBoxSize}.
* </p>
*
* @alias CubeMapPanorama
* @constructor
*
* @param {CubeMapPanorama.ConstructorOptions} options Object describing initialization options
*
* @example
* const modelMatrix = Cesium.Matrix4.getMatrix3(
* Cesium.Transforms.localFrameToFixedFrameGenerator("north", "down")(
* Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
* Cesium.Ellipsoid.default
* ),
* new Cesium.Matrix3()
* );
*
*
* scene.primitives.add(new Cesium.CubeMapPanorama({
* sources : {
* positiveX : 'cubemap_px.png',
* negativeX : 'cubemap_nx.png',
* positiveY : 'cubemap_py.png',
* negativeY : 'cubemap_ny.png',
* positiveZ : 'cubemap_pz.png',
* negativeZ : 'cubemap_nz.png'
* }
* transform: modelMatrix,
* }));
*
* @see SkyBox
*
* @demo {@link https://sandcastle.cesium.com/index.html?id=panorama|Cesium Sandcastle Panorama}
*/
function CubeMapPanorama(options) {
/**
* The sources used to create the cube map faces: an object
* with <code>positiveX</code>, <code>negativeX</code>, <code>positiveY</code>,
* <code>negativeY</code>, <code>positiveZ</code>, and <code>negativeZ</code> properties.
* These can be either URLs or <code>Image</code> objects.
*
* @type {object}
* @default undefined
*/
this.sources = options.sources;
this._sources = undefined;
this._transform = options.transform;
/**
* Determines if the cube map panorama will be shown.
*
* @type {boolean}
* @default true
*/
this.show = options.show ?? true;
this._returnCommand = options.returnCommand ?? false;
this._addToPanoramaCommandList = !this._returnCommand;
this._command = new DrawCommand({
modelMatrix: Matrix4.clone(Matrix4.IDENTITY),
owner: this,
// render before everything else
pass: Pass.ENVIRONMENT,
});
this._cubeMap = undefined;
this._attributeLocations = undefined;
this._useHdr = undefined;
this._hasError = false;
this._error = undefined;
// Credit specified by the user.
let credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
this._credit = credit;
}
Object.defineProperties(CubeMapPanorama.prototype, {
/**
* Gets the transform of the panorama. If undefined, the default orientation uses the True Equator Mean Equinox (TEME) axes.
* @memberof CubeMapPanorama.prototype
* @type {Matrix3}
* @readonly
*/
transform: {
get: function () {
return this._transform;
},
},
/**
* Gets the credits of the panorama.
* @memberof CubeMapPanorama.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return defined(this._credit) ? this._credit : undefined;
},
},
});
/**
* Called when {@link Viewer} or {@link CesiumWidget} render the scene to
* get the draw commands needed to render this primitive.
* <p>
* Do not call this function directly. This is documented just to
* list the exceptions that may be propagated when the scene is rendered:
* </p>
*
* @exception {DeveloperError} this.sources is required and must have positiveX, negativeX, positiveY, negativeY, positiveZ, and negativeZ properties.
* @exception {DeveloperError} this.sources properties must all be the same type.
*/
CubeMapPanorama.prototype.update = function (frameState, useHdr) {
const that = this;
const { mode, passes, context, panoramaCommandList } = frameState;
if (!this.show) {
return undefined;
}
if (mode !== SceneMode.SCENE3D && mode !== SceneMode.MORPHING) {
return undefined;
}
// The cube map panorama is only rendered during the render pass; it is not pickable, it doesn't cast shadows, etc.
if (!passes.render) {
return undefined;
}
// Throw any errors that had previously occurred asynchronously so they aren't
// ignored when running. See https://github.com/CesiumGS/cesium/pull/12307
if (this._hasError) {
const error = this._error;
this._hasError = false;
this._error = undefined;
throw error;
}
if (this._sources !== this.sources) {
this._sources = this.sources;
const sources = this.sources;
//>>includeStart('debug', pragmas.debug);
Check.defined("this.sources", sources);
if (
Object.values(CubeMap.FaceName).some(
(faceName) => !defined(sources[faceName]),
)
) {
throw new DeveloperError(
"this.sources must have positiveX, negativeX, positiveY, negativeY, positiveZ, and negativeZ properties.",
);
}
const sourceType = typeof sources.positiveX;
if (
Object.values(CubeMap.FaceName).some(
(faceName) => typeof sources[faceName] !== sourceType,
)
) {
throw new DeveloperError(
"this.sources properties must all be the same type.",
);
}
//>>includeEnd('debug');
if (typeof sources.positiveX === "string") {
// Given urls for cube-map images. Load them.
loadCubeMap(context, this._sources)
.then(function (cubeMap) {
that._cubeMap = that._cubeMap && that._cubeMap.destroy();
that._cubeMap = cubeMap;
})
.catch((error) => {
// Defer throwing the error until the next call to update to prevent
// test from failing in `afterAll` if this is rejected after the test
// using the Skybox ends. See https://github.com/CesiumGS/cesium/pull/12307
this._hasError = true;
this._error = error;
});
} else {
this._cubeMap = this._cubeMap && this._cubeMap.destroy();
this._cubeMap = new CubeMap({
context: context,
source: sources,
});
}
this._addToPanoramaCommandList = true;
}
const command = this._command;
if (!defined(command.vertexArray)) {
command.uniformMap = {
u_cubeMap: function () {
return that._cubeMap;
},
u_cubeMapPanoramaTransform: function () {
return that._transform;
},
};
const geometry = BoxGeometry.createGeometry(
BoxGeometry.fromDimensions({
dimensions: new Cartesian3(2.0, 2.0, 2.0),
vertexFormat: VertexFormat.POSITION_ONLY,
}),
);
const attributeLocations = (this._attributeLocations =
GeometryPipeline.createAttributeLocations(geometry));
command.vertexArray = VertexArray.fromGeometry({
context: context,
geometry: geometry,
attributeLocations: attributeLocations,
bufferUsage: BufferUsage.STATIC_DRAW,
});
// no depth test/write
command.renderState = RenderState.fromCache({
depthTest: { enabled: false },
depthMask: false,
blending: BlendingState.ALPHA_BLEND,
});
this._addToPanoramaCommandList = true;
}
if (!defined(command.shaderProgram) || this._useHdr !== useHdr) {
const fs = new ShaderSource({
defines: [useHdr ? "HDR" : ""],
sources: [SkyBoxFS],
});
command.shaderProgram = ShaderProgram.fromCache({
context: context,
//vertexShaderSource: SkyBoxVS,
vertexShaderSource: defined(this._transform)
? CubeMapPanoramaVS
: SkyBoxVS,
fragmentShaderSource: fs,
attributeLocations: this._attributeLocations,
});
this._useHdr = useHdr;
this._addToPanoramaCommandList = true;
}
if (!defined(this._cubeMap)) {
return undefined;
}
if (this.show && defined(this._credit) && !this._returnCommand) {
const creditDisplay = frameState.creditDisplay;
creditDisplay.addCreditToNextFrame(this._credit);
}
if (this._returnCommand) {
return command;
}
if (this._addToPanoramaCommandList) {
panoramaCommandList.push(command);
this._addToPanoramaCommandList = false;
}
};
/**
* 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 CubeMapPanorama#destroy
*/
CubeMapPanorama.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 /><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
* cubeMapPanorama = cubeMapPanorama && cubeMapPanorama.destroy();
*
* @see CubeMapPanorama#isDestroyed
*/
CubeMapPanorama.prototype.destroy = function () {
const command = this._command;
command.vertexArray = command.vertexArray && command.vertexArray.destroy();
command.shaderProgram =
command.shaderProgram && command.shaderProgram.destroy();
this._cubeMap = this._cubeMap && this._cubeMap.destroy();
return destroyObject(this);
};
export default CubeMapPanorama;
+38
View File
@@ -0,0 +1,38 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines which triangles, if any, are culled.
*
* @enum {number}
*/
const CullFace = {
/**
* Front-facing triangles are culled.
*
* @type {number}
* @constant
*/
FRONT: WebGLConstants.FRONT,
/**
* Back-facing triangles are culled.
*
* @type {number}
* @constant
*/
BACK: WebGLConstants.BACK,
/**
* Both front-facing and back-facing triangles are culled.
*
* @type {number}
* @constant
*/
FRONT_AND_BACK: WebGLConstants.FRONT_AND_BACK,
};
Object.freeze(CullFace);
export default CullFace;
+347
View File
@@ -0,0 +1,347 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
/**
* <div class="notice">
* A cloud is created and its initial properties are set by calling {@link CloudCollection#add}.
* and {@link CloudCollection#remove}. Do not call the constructor directly.
* </div>
* A cumulus cloud billboard positioned in the 3D scene, that is created and rendered using a {@link CloudCollection}.
* <br /><br />
* <div align='center'>
* <img src='Images/CumulusCloud.png' width='400' height='300' /><br />
* Example cumulus clouds
* </div>
* @alias CumulusCloud
*
* @performance Similar to {@link Billboard}, reading a property, e.g., {@link CumulusCloud#show},
* takes constant time. Assigning to a property is constant time but results in
* CPU to GPU traffic when {@link CloudCollection#update} is called. The per-cloud traffic is
* the same regardless of how many properties were updated. If most clouds in a collection need to be
* updated, it may be more efficient to clear the collection with {@link CloudCollection#removeAll}
* and add new clouds instead of modifying each one.
*
* @see CloudCollection
* @see CloudCollection#add
*
* @internalConstructor
* @class
*
* @demo {@link https://sandcastle.cesium.com/index.html?id=cloud-parameters|Cesium Sandcastle Cloud Parameters Demo}
*/
function CumulusCloud(options, cloudCollection) {
options = options ?? Frozen.EMPTY_OBJECT;
this._show = options.show ?? true;
this._position = Cartesian3.clone(options.position ?? Cartesian3.ZERO);
if (!defined(options.scale) && defined(options.maximumSize)) {
this._maximumSize = Cartesian3.clone(options.maximumSize);
this._scale = new Cartesian2(this._maximumSize.x, this._maximumSize.y);
} else {
this._scale = Cartesian2.clone(options.scale ?? new Cartesian2(20.0, 12.0));
const defaultMaxSize = new Cartesian3(
this._scale.x,
this._scale.y,
Math.min(this._scale.x, this._scale.y) / 1.5,
);
this._maximumSize = Cartesian3.clone(options.maximumSize ?? defaultMaxSize);
}
this._slice = options.slice ?? -1.0;
this._color = Color.clone(options.color ?? Color.WHITE);
this._brightness = options.brightness ?? 1.0;
this._cloudCollection = cloudCollection;
this._index = -1; // Used by CloudCollection
}
const SHOW_INDEX = (CumulusCloud.SHOW_INDEX = 0);
const POSITION_INDEX = (CumulusCloud.POSITION_INDEX = 1);
const SCALE_INDEX = (CumulusCloud.SCALE_INDEX = 2);
const MAXIMUM_SIZE_INDEX = (CumulusCloud.MAXIMUM_SIZE_INDEX = 3);
const SLICE_INDEX = (CumulusCloud.SLICE_INDEX = 4);
const BRIGHTNESS_INDEX = (CumulusCloud.BRIGHTNESS_INDEX = 5);
const COLOR_INDEX = (CumulusCloud.COLOR_INDEX = 6);
CumulusCloud.NUMBER_OF_PROPERTIES = 7;
function makeDirty(cloud, propertyChanged) {
const cloudCollection = cloud._cloudCollection;
if (defined(cloudCollection)) {
cloudCollection._updateCloud(cloud, propertyChanged);
cloud._dirty = true;
}
}
Object.defineProperties(CumulusCloud.prototype, {
/**
* Determines if this cumulus cloud will be shown. Use this to hide or show a cloud, instead
* of removing it and re-adding it to the collection.
* @memberof CumulusCloud.prototype
* @type {boolean}
* @default true
*/
show: {
get: function () {
return this._show;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.bool("value", value);
//>>includeEnd('debug');
if (this._show !== value) {
this._show = value;
makeDirty(this, SHOW_INDEX);
}
},
},
/**
* Gets or sets the Cartesian position of this cumulus cloud.
* @memberof CumulusCloud.prototype
* @type {Cartesian3}
*/
position: {
get: function () {
return this._position;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug)
Check.typeOf.object("value", value);
//>>includeEnd('debug');
const position = this._position;
if (!Cartesian3.equals(position, value)) {
Cartesian3.clone(value, position);
makeDirty(this, POSITION_INDEX);
}
},
},
/**
* <p>Gets or sets the scale of the cumulus cloud billboard in meters.
* The <code>scale</code> property will affect the size of the billboard,
* but not the cloud's actual appearance.</p>
* <div align='center'>
* <table border='0' cellpadding='5'><tr>
* <td align='center'>
* <code>cloud.scale = new Cesium.Cartesian2(12, 8);</code><br/>
* <img src='Images/CumulusCloud.scalex12y8.png' width='250' height='158' />
* </td>
* <td align='center'>
* <code>cloud.scale = new Cesium.Cartesian2(24, 10);</code><br/>
* <img src='Images/CumulusCloud.scalex24y10.png' width='250' height='158' />
* </td>
* </tr></table>
* </div>
*
* <p>To modify the cloud's appearance, modify its <code>maximumSize</code>
* and <code>slice</code> properties.</p>
* @memberof CumulusCloud.prototype
* @type {Cartesian2}
*
* @see CumulusCloud#maximumSize
* @see CumulusCloud#slice
*/
scale: {
get: function () {
return this._scale;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug)
Check.typeOf.object("value", value);
//>>includeEnd('debug');
const scale = this._scale;
if (!Cartesian2.equals(scale, value)) {
Cartesian2.clone(value, scale);
makeDirty(this, SCALE_INDEX);
}
},
},
/**
* <p>Gets or sets the maximum size of the cumulus cloud rendered on the billboard.
* This defines a maximum ellipsoid volume that the cloud can appear in.
* Rather than guaranteeing a specific size, this specifies a boundary for the
* cloud to appear in, and changing it can affect the shape of the cloud.</p>
* <p>Changing the z-value of <code>maximumSize</code> has the most dramatic effect
* on the cloud's appearance because it changes the depth of the cloud, and thus the
* positions at which the cloud-shaping texture is sampled.</p>
* <div align='center'>
* <table border='0' cellpadding='5'>
* <tr>
* <td align='center'>
* <code>cloud.maximumSize = new Cesium.Cartesian3(14, 9, 10);</code><br/>
* <img src='Images/CumulusCloud.maximumSizex14y9z10.png' width='250' height='158' />
* </td>
* <td align='center'>
* <code>cloud.maximumSize.x = 25;</code><br/>
* <img src='Images/CumulusCloud.maximumSizex25.png' width='250' height='158' />
* </td>
* </tr>
* <tr>
* <td align='center'>
* <code>cloud.maximumSize.y = 5;</code><br/>
* <img src='Images/CumulusCloud.maximumSizey5.png' width='250' height='158' />
* </td>
* <td align='center'>
* <code>cloud.maximumSize.z = 17;</code><br/>
* <img src='Images/CumulusCloud.maximumSizez17.png' width='250' height='158' />
* </td>
* </tr>
* </table>
* </div>
*
* <p>To modify the billboard's actual size, modify the cloud's <code>scale</code> property.</p>
* @memberof CumulusCloud.prototype
* @type {Cartesian3}
*
* @see CumulusCloud#scale
*/
maximumSize: {
get: function () {
return this._maximumSize;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug)
Check.typeOf.object("value", value);
//>>includeEnd('debug');
const maximumSize = this._maximumSize;
if (!Cartesian3.equals(maximumSize, value)) {
Cartesian3.clone(value, maximumSize);
makeDirty(this, MAXIMUM_SIZE_INDEX);
}
},
},
/**
* Sets the color of the cloud
* @memberof CumulusCloud.prototype
* @type {Color}
* @default Color.WHITE
*/
color: {
get: function () {
return this._color;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug)
Check.typeOf.object("value", value);
//>>includeEnd('debug');
const color = this._color;
if (!Color.equals(color, value)) {
Color.clone(value, color);
makeDirty(this, COLOR_INDEX);
}
},
},
/**
* <p>Gets or sets the "slice" of the cloud that is rendered on the billboard, i.e.
* the specific cross-section of the cloud chosen for the billboard's appearance.
* Given a value between 0 and 1, the slice specifies how deeply into the cloud
* to intersect based on its maximum size in the z-direction.</p>
* <div align='center'>
* <table border='0' cellpadding='5'><tr>
* <td align='center'><code>cloud.slice = 0.32;</code><br/><img src='Images/CumulusCloud.slice0.32.png' width='250' height='158' /></td>
* <td align='center'><code>cloud.slice = 0.5;</code><br/><img src='Images/CumulusCloud.slice0.5.png' width='250' height='158' /></td>
* <td align='center'><code>cloud.slice = 0.6;</code><br/><img src='Images/CumulusCloud.slice0.6.png' width='250' height='158' /></td>
* </tr></table>
* </div>
*
* <br />
* <p>Due to the nature in which this slice is calculated,
* values below <code>0.2</code> may result in cross-sections that are too small,
* and the edge of the ellipsoid will be visible. Similarly, values above <code>0.7</code>
* will cause the cloud to appear smaller. Values outside the range <code>[0.1, 0.9]</code>
* should be avoided entirely because they do not produce desirable results.</p>
*
* <div align='center'>
* <table border='0' cellpadding='5'><tr>
* <td align='center'><code>cloud.slice = 0.08;</code><br/><img src='Images/CumulusCloud.slice0.08.png' width='250' height='158' /></td>
* <td align='center'><code>cloud.slice = 0.8;</code><br/><img src='Images/CumulusCloud.slice0.8.png' width='250' height='158' /></td>
* </tr></table>
* </div>
*
* <p>If <code>slice</code> is set to a negative number, the cloud will not render a cross-section.
* Instead, it will render the outside of the ellipsoid that is visible. For clouds with
* small values of `maximumSize.z`, this can produce good-looking results, but for larger
* clouds, this can result in a cloud that is undesirably warped to the ellipsoid volume.</p>
*
* <div align='center'>
* <table border='0' cellpadding='5'><tr>
* <td align='center'>
* <code>cloud.slice = -1.0;<br/>cloud.maximumSize.z = 18;</code><br/>
* <img src='Images/CumulusCloud.slice-1z18.png' width='250' height='158' />
* </td>
* <td align='center'>
* <code>cloud.slice = -1.0;<br/>cloud.maximumSize.z = 30;</code><br/>
* <img src='Images/CumulusCloud.slice-1z30.png' width='250' height='158' /></td>
* </tr></table>
* </div>
*
* @memberof CumulusCloud.prototype
* @type {number}
* @default -1.0
*/
slice: {
get: function () {
return this._slice;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug)
Check.typeOf.number("value", value);
//>>includeEnd('debug');
const slice = this._slice;
if (slice !== value) {
this._slice = value;
makeDirty(this, SLICE_INDEX);
}
},
},
/**
* Gets or sets the brightness of the cloud. This can be used to give clouds
* a darker, grayer appearance.
* <br /><br />
* <div align='center'>
* <table border='0' cellpadding='5'><tr>
* <td align='center'><code>cloud.brightness = 1.0;</code><br/><img src='Images/CumulusCloud.brightness1.png' width='250' height='158' /></td>
* <td align='center'><code>cloud.brightness = 0.6;</code><br/><img src='Images/CumulusCloud.brightness0.6.png' width='250' height='158' /></td>
* <td align='center'><code>cloud.brightness = 0.0;</code><br/><img src='Images/CumulusCloud.brightness0.png' width='250' height='158' /></td>
* </tr></table>
* </div>
* @memberof CumulusCloud.prototype
* @type {number}
* @default 1.0
*/
brightness: {
get: function () {
return this._brightness;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug)
Check.typeOf.number("value", value);
//>>includeEnd('debug');
const brightness = this._brightness;
if (brightness !== value) {
this._brightness = value;
makeDirty(this, BRIGHTNESS_INDEX);
}
},
},
});
CumulusCloud.prototype._destroy = function () {
this._cloudCollection = undefined;
};
export default CumulusCloud;
+266
View File
@@ -0,0 +1,266 @@
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Appearance from "./Appearance.js";
/**
* Visualizes a vertex attribute by displaying it as a color for debugging.
* <p>
* Components for well-known unit-length vectors, i.e., <code>normal</code>,
* <code>tangent</code>, and <code>bitangent</code>, are scaled and biased
* from [-1.0, 1.0] to (-1.0, 1.0).
* </p>
*
* @alias DebugAppearance
* @constructor
*
* @param {object} options Object with the following properties:
* @param {string} options.attributeName The name of the attribute to visualize.
* @param {boolean} [options.perInstanceAttribute=false] Boolean that determines whether this attribute is a per-instance geometry attribute.
* @param {string} [options.glslDatatype='vec3'] The GLSL datatype of the attribute. Supported datatypes are <code>float</code>, <code>vec2</code>, <code>vec3</code>, and <code>vec4</code>.
* @param {string} [options.vertexShaderSource] Optional GLSL vertex shader source to override the default vertex shader.
* @param {string} [options.fragmentShaderSource] Optional GLSL fragment shader source to override the default fragment shader.
* @param {object} [options.renderState] Optional render state to override the default render state.
*
* @exception {DeveloperError} options.glslDatatype must be float, vec2, vec3, or vec4.
*
* @example
* const primitive = new Cesium.Primitive({
* geometryInstances : // ...
* appearance : new Cesium.DebugAppearance({
* attributeName : 'normal'
* })
* });
*/
function DebugAppearance(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const attributeName = options.attributeName;
let perInstanceAttribute = options.perInstanceAttribute;
//>>includeStart('debug', pragmas.debug);
if (!defined(attributeName)) {
throw new DeveloperError("options.attributeName is required.");
}
//>>includeEnd('debug');
if (!defined(perInstanceAttribute)) {
perInstanceAttribute = false;
}
let glslDatatype = options.glslDatatype ?? "vec3";
const varyingName = `v_${attributeName}`;
let getColor;
// Well-known normalized vector attributes in VertexFormat
if (
attributeName === "normal" ||
attributeName === "tangent" ||
attributeName === "bitangent"
) {
getColor = `vec4 getColor() { return vec4((${varyingName} + vec3(1.0)) * 0.5, 1.0); }\n`;
} else {
// All other attributes, both well-known and custom
if (attributeName === "st") {
glslDatatype = "vec2";
}
switch (glslDatatype) {
case "float":
getColor = `vec4 getColor() { return vec4(vec3(${varyingName}), 1.0); }\n`;
break;
case "vec2":
getColor = `vec4 getColor() { return vec4(${varyingName}, 0.0, 1.0); }\n`;
break;
case "vec3":
getColor = `vec4 getColor() { return vec4(${varyingName}, 1.0); }\n`;
break;
case "vec4":
getColor = `vec4 getColor() { return ${varyingName}; }\n`;
break;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError(
"options.glslDatatype must be float, vec2, vec3, or vec4.",
);
//>>includeEnd('debug');
}
}
const vs =
`${
"in vec3 position3DHigh;\n" +
"in vec3 position3DLow;\n" +
"in float batchId;\n"
}${
perInstanceAttribute ? "" : `in ${glslDatatype} ${attributeName};\n`
}out ${glslDatatype} ${varyingName};\n` +
`void main()\n` +
`{\n` +
`vec4 p = czm_translateRelativeToEye(position3DHigh, position3DLow);\n${
perInstanceAttribute
? `${varyingName} = czm_batchTable_${attributeName}(batchId);\n`
: `${varyingName} = ${attributeName};\n`
}gl_Position = czm_modelViewProjectionRelativeToEye * p;\n` +
`}`;
const fs =
`in ${glslDatatype} ${varyingName};\n${getColor}\n` +
`void main()\n` +
`{\n` +
`out_FragColor = getColor();\n` +
`}`;
/**
* This property is part of the {@link Appearance} interface, but is not
* used by {@link DebugAppearance} since a fully custom fragment shader is used.
*
* @type Material
*
* @default undefined
*/
this.material = undefined;
/**
* When <code>true</code>, the geometry is expected to appear translucent.
*
* @type {boolean}
*
* @default false
*/
this.translucent = options.translucent ?? false;
this._vertexShaderSource = options.vertexShaderSource ?? vs;
this._fragmentShaderSource = options.fragmentShaderSource ?? fs;
this._renderState = Appearance.getDefaultRenderState(
false,
false,
options.renderState,
);
this._closed = options.closed ?? false;
// Non-derived members
this._attributeName = attributeName;
this._glslDatatype = glslDatatype;
}
Object.defineProperties(DebugAppearance.prototype, {
/**
* The GLSL source code for the vertex shader.
*
* @memberof DebugAppearance.prototype
*
* @type {string}
* @readonly
*/
vertexShaderSource: {
get: function () {
return this._vertexShaderSource;
},
},
/**
* The GLSL source code for the fragment shader. The full fragment shader
* source is built procedurally taking into account the {@link DebugAppearance#material}.
* Use {@link DebugAppearance#getFragmentShaderSource} to get the full source.
*
* @memberof DebugAppearance.prototype
*
* @type {string}
* @readonly
*/
fragmentShaderSource: {
get: function () {
return this._fragmentShaderSource;
},
},
/**
* The WebGL fixed-function state to use when rendering the geometry.
*
* @memberof DebugAppearance.prototype
*
* @type {object}
* @readonly
*/
renderState: {
get: function () {
return this._renderState;
},
},
/**
* When <code>true</code>, the geometry is expected to be closed.
*
* @memberof DebugAppearance.prototype
*
* @type {boolean}
* @readonly
*
* @default false
*/
closed: {
get: function () {
return this._closed;
},
},
/**
* The name of the attribute being visualized.
*
* @memberof DebugAppearance.prototype
*
* @type {string}
* @readonly
*/
attributeName: {
get: function () {
return this._attributeName;
},
},
/**
* The GLSL datatype of the attribute being visualized.
*
* @memberof DebugAppearance.prototype
*
* @type {string}
* @readonly
*/
glslDatatype: {
get: function () {
return this._glslDatatype;
},
},
});
/**
* Returns the full GLSL fragment shader source, which for {@link DebugAppearance} is just
* {@link DebugAppearance#fragmentShaderSource}.
*
* @function
*
* @returns {string} The full GLSL fragment shader source.
*/
DebugAppearance.prototype.getFragmentShaderSource =
Appearance.prototype.getFragmentShaderSource;
/**
* Determines if the geometry is translucent based on {@link DebugAppearance#translucent}.
*
* @function
*
* @returns {boolean} <code>true</code> if the appearance is translucent.
*/
DebugAppearance.prototype.isTranslucent = Appearance.prototype.isTranslucent;
/**
* Creates a render state. This is not the final render state instance; instead,
* it can contain a subset of render state properties identical to the render state
* created in the context.
*
* @function
*
* @returns {object} The render state.
*/
DebugAppearance.prototype.getRenderState = Appearance.prototype.getRenderState;
export default DebugAppearance;
@@ -0,0 +1,257 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Color from "../Core/Color.js";
import ColorGeometryInstanceAttribute from "../Core/ColorGeometryInstanceAttribute.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import FrustumGeometry from "../Core/FrustumGeometry.js";
import FrustumOutlineGeometry from "../Core/FrustumOutlineGeometry.js";
import GeometryInstance from "../Core/GeometryInstance.js";
import Matrix3 from "../Core/Matrix3.js";
import OrthographicFrustum from "../Core/OrthographicFrustum.js";
import OrthographicOffCenterFrustum from "../Core/OrthographicOffCenterFrustum.js";
import PerspectiveFrustum from "../Core/PerspectiveFrustum.js";
import PerspectiveOffCenterFrustum from "../Core/PerspectiveOffCenterFrustum.js";
import Quaternion from "../Core/Quaternion.js";
import PerInstanceColorAppearance from "./PerInstanceColorAppearance.js";
import Primitive from "./Primitive.js";
/**
* Draws the outline of the camera's view frustum.
*
* @alias DebugCameraPrimitive
* @constructor
*
* @param {object} options Object with the following properties:
* @param {Camera} options.camera The camera.
* @param {number[]} [options.frustumSplits] Distances to the near and far planes of the camera frustums. This overrides the camera's frustum near and far values.
* @param {Color} [options.color=Color.CYAN] The color of the debug outline.
* @param {boolean} [options.updateOnChange=true] Whether the primitive updates when the underlying camera changes.
* @param {boolean} [options.show=true] Determines if this primitive will be shown.
* @param {object} [options.id] A user-defined object to return when the instance is picked with {@link Scene#pick}.
*
* @example
* primitives.add(new Cesium.DebugCameraPrimitive({
* camera : camera,
* color : Cesium.Color.YELLOW
* }));
*/
function DebugCameraPrimitive(options) {
options = options ?? Frozen.EMPTY_OBJECT;
//>>includeStart('debug', pragmas.debug);
if (!defined(options.camera)) {
throw new DeveloperError("options.camera is required.");
}
//>>includeEnd('debug');
this._camera = options.camera;
this._frustumSplits = options.frustumSplits;
this._color = options.color ?? Color.CYAN;
this._updateOnChange = options.updateOnChange ?? true;
/**
* Determines if this primitive will be shown.
*
* @type {boolean}
* @default true
*/
this.show = options.show ?? true;
/**
* User-defined value returned when the primitive is picked.
*
* @type {*}
* @default undefined
*
* @see Scene#pick
*/
this.id = options.id;
this._id = undefined;
this._outlinePrimitives = [];
this._planesPrimitives = [];
}
const scratchRight = new Cartesian3();
const scratchRotation = new Matrix3();
const scratchOrientation = new Quaternion();
const scratchPerspective = new PerspectiveFrustum();
const scratchPerspectiveOffCenter = new PerspectiveOffCenterFrustum();
const scratchOrthographic = new OrthographicFrustum();
const scratchOrthographicOffCenter = new OrthographicOffCenterFrustum();
const scratchColor = new Color();
const scratchSplits = [1.0, 100000.0];
/**
* @private
*/
DebugCameraPrimitive.prototype.update = function (frameState) {
if (!this.show) {
return;
}
const planesPrimitives = this._planesPrimitives;
const outlinePrimitives = this._outlinePrimitives;
let i;
let length;
if (this._updateOnChange) {
// Recreate the primitive every frame
length = planesPrimitives.length;
for (i = 0; i < length; ++i) {
outlinePrimitives[i] =
outlinePrimitives[i] && outlinePrimitives[i].destroy();
planesPrimitives[i] =
planesPrimitives[i] && planesPrimitives[i].destroy();
}
planesPrimitives.length = 0;
outlinePrimitives.length = 0;
}
if (planesPrimitives.length === 0) {
const camera = this._camera;
const cameraFrustum = camera.frustum;
let frustum;
if (cameraFrustum instanceof PerspectiveFrustum) {
frustum = scratchPerspective;
} else if (cameraFrustum instanceof PerspectiveOffCenterFrustum) {
frustum = scratchPerspectiveOffCenter;
} else if (cameraFrustum instanceof OrthographicFrustum) {
frustum = scratchOrthographic;
} else {
frustum = scratchOrthographicOffCenter;
}
frustum = cameraFrustum.clone(frustum);
let numFrustums;
let frustumSplits = this._frustumSplits;
if (!defined(frustumSplits) || frustumSplits.length <= 1) {
// Use near and far planes if no splits created
frustumSplits = scratchSplits;
frustumSplits[0] = this._camera.frustum.near;
frustumSplits[1] = this._camera.frustum.far;
numFrustums = 1;
} else {
numFrustums = frustumSplits.length - 1;
}
const position = camera.positionWC;
const direction = camera.directionWC;
const up = camera.upWC;
let right = camera.rightWC;
right = Cartesian3.negate(right, scratchRight);
const rotation = scratchRotation;
Matrix3.setColumn(rotation, 0, right, rotation);
Matrix3.setColumn(rotation, 1, up, rotation);
Matrix3.setColumn(rotation, 2, direction, rotation);
const orientation = Quaternion.fromRotationMatrix(
rotation,
scratchOrientation,
);
planesPrimitives.length = outlinePrimitives.length = numFrustums;
for (i = 0; i < numFrustums; ++i) {
frustum.near = frustumSplits[i];
frustum.far = frustumSplits[i + 1];
planesPrimitives[i] = new Primitive({
geometryInstances: new GeometryInstance({
geometry: new FrustumGeometry({
origin: position,
orientation: orientation,
frustum: frustum,
_drawNearPlane: i === 0,
}),
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(
Color.fromAlpha(this._color, 0.1, scratchColor),
),
},
id: this.id,
pickPrimitive: this,
}),
appearance: new PerInstanceColorAppearance({
translucent: true,
flat: true,
}),
asynchronous: false,
});
outlinePrimitives[i] = new Primitive({
geometryInstances: new GeometryInstance({
geometry: new FrustumOutlineGeometry({
origin: position,
orientation: orientation,
frustum: frustum,
_drawNearPlane: i === 0,
}),
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(this._color),
},
id: this.id,
pickPrimitive: this,
}),
appearance: new PerInstanceColorAppearance({
translucent: false,
flat: true,
}),
asynchronous: false,
});
}
}
length = planesPrimitives.length;
for (i = 0; i < length; ++i) {
outlinePrimitives[i].update(frameState);
planesPrimitives[i].update(frameState);
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <p>
* 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.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see DebugCameraPrimitive#destroy
*/
DebugCameraPrimitive.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.
* <p>
* 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.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* p = p && p.destroy();
*
* @see DebugCameraPrimitive#isDestroyed
*/
DebugCameraPrimitive.prototype.destroy = function () {
const length = this._planesPrimitives.length;
for (let i = 0; i < length; ++i) {
this._outlinePrimitives[i] =
this._outlinePrimitives[i] && this._outlinePrimitives[i].destroy();
this._planesPrimitives[i] =
this._planesPrimitives[i] && this._planesPrimitives[i].destroy();
}
return destroyObject(this);
};
export default DebugCameraPrimitive;
+151
View File
@@ -0,0 +1,151 @@
import Color from "../Core/Color.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import ShaderProgram from "../Renderer/ShaderProgram.js";
import defined from "../Core/defined.js";
/**
* @private
*/
function DebugInspector() {
this._cachedShowFrustumsShaders = {};
}
function getAttributeLocations(shaderProgram) {
const attributeLocations = {};
const attributes = shaderProgram.vertexAttributes;
for (const a in attributes) {
if (attributes.hasOwnProperty(a)) {
attributeLocations[a] = attributes[a].index;
}
}
return attributeLocations;
}
function createDebugShowFrustumsShaderProgram(scene, shaderProgram) {
const context = scene.context;
const sp = shaderProgram;
const fs = sp.fragmentShaderSource.clone();
const targets = [];
fs.sources = fs.sources.map(function (source) {
source = ShaderSource.replaceMain(source, "czm_Debug_main");
const re = /out_FragData_(\d+)/g;
let match;
while ((match = re.exec(source)) !== null) {
if (targets.indexOf(match[1]) === -1) {
targets.push(match[1]);
}
}
return source;
});
const length = targets.length;
let newMain = "";
newMain += "uniform vec3 debugShowCommandsColor;\n";
newMain += "uniform vec3 debugShowFrustumsColor;\n";
newMain += "void main() \n" + "{ \n" + " czm_Debug_main(); \n";
// set debugShowCommandsColor to Color(1.0, 1.0, 1.0, 1.0) to stop rendering scene.debugShowCommands
// set debugShowFrustumsColor to Color(1.0, 1.0, 1.0, 1.0) to stop rendering scene.debugShowFrustums
let i;
if (length > 0) {
for (i = 0; i < length; ++i) {
newMain += ` out_FragData_${targets[i]}.rgb *= debugShowCommandsColor;\n`;
newMain += ` out_FragData_${targets[i]}.rgb *= debugShowFrustumsColor;\n`;
}
} else {
newMain += " out_FragColor.rgb *= debugShowCommandsColor;\n";
newMain += " out_FragColor.rgb *= debugShowFrustumsColor;\n";
}
newMain += "}";
fs.sources.push(newMain);
const attributeLocations = getAttributeLocations(sp);
return ShaderProgram.fromCache({
context: context,
vertexShaderSource: sp.vertexShaderSource,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
});
}
const scratchFrustumColor = new Color();
function createDebugShowFrustumsUniformMap(scene, command) {
// setup uniform for the shader
let debugUniformMap;
if (!defined(command.uniformMap)) {
debugUniformMap = {};
} else {
debugUniformMap = command.uniformMap;
}
if (
defined(debugUniformMap.debugShowCommandsColor) ||
defined(debugUniformMap.debugShowFrustumsColor)
) {
return debugUniformMap;
}
debugUniformMap.debugShowCommandsColor = function () {
if (!scene.debugShowCommands) {
return Color.WHITE;
}
if (!defined(command._debugColor)) {
command._debugColor = Color.fromRandom();
}
return command._debugColor;
};
debugUniformMap.debugShowFrustumsColor = function () {
if (!scene.debugShowFrustums) {
return Color.WHITE;
}
// Support up to three frustums. If a command overlaps all
// three, it's code is not changed.
scratchFrustumColor.red =
command.debugOverlappingFrustums & (1 << 0) ? 1.0 : 0.0;
scratchFrustumColor.green =
command.debugOverlappingFrustums & (1 << 1) ? 1.0 : 0.0;
scratchFrustumColor.blue =
command.debugOverlappingFrustums & (1 << 2) ? 1.0 : 0.0;
scratchFrustumColor.alpha = 1.0;
return scratchFrustumColor;
};
return debugUniformMap;
}
const scratchShowFrustumCommand = new DrawCommand();
DebugInspector.prototype.executeDebugShowFrustumsCommand = function (
scene,
command,
passState,
) {
// create debug command
const shaderProgramId = command.shaderProgram.id;
let debugShaderProgram = this._cachedShowFrustumsShaders[shaderProgramId];
if (!defined(debugShaderProgram)) {
debugShaderProgram = createDebugShowFrustumsShaderProgram(
scene,
command.shaderProgram,
);
this._cachedShowFrustumsShaders[shaderProgramId] = debugShaderProgram;
}
const debugCommand = DrawCommand.shallowClone(
command,
scratchShowFrustumCommand,
);
debugCommand.shaderProgram = debugShaderProgram;
debugCommand.uniformMap = createDebugShowFrustumsUniformMap(scene, command);
debugCommand.execute(scene.context, passState);
};
export default DebugInspector;
@@ -0,0 +1,220 @@
import ArcType from "../Core/ArcType.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Color from "../Core/Color.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import GeometryInstance from "../Core/GeometryInstance.js";
import Matrix4 from "../Core/Matrix4.js";
import PolylineGeometry from "../Core/PolylineGeometry.js";
import PolylineColorAppearance from "./PolylineColorAppearance.js";
import Primitive from "./Primitive.js";
/**
* Draws the axes of a reference frame defined by a matrix that transforms to world
* coordinates, i.e., Earth's WGS84 coordinates. The most prominent example is
* a primitives <code>modelMatrix</code>.
* <p>
* The X axis is red; Y is green; and Z is blue.
* </p>
* <p>
* This is for debugging only; it is not optimized for production use.
* </p>
*
* @alias DebugModelMatrixPrimitive
* @constructor
*
* @param {object} [options] Object with the following properties:
* @param {number} [options.length=10000000.0] The length of the axes in meters.
* @param {number} [options.width=2.0] The width of the axes in pixels.
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 matrix that defines the reference frame, i.e., origin plus axes, to visualize.
* @param {boolean} [options.show=true] Determines if this primitive will be shown.
* @param {object} [options.id] A user-defined object to return when the instance is picked with {@link Scene#pick}
*
* @example
* primitives.add(new Cesium.DebugModelMatrixPrimitive({
* modelMatrix : primitive.modelMatrix, // primitive to debug
* length : 100000.0,
* width : 10.0
* }));
*/
function DebugModelMatrixPrimitive(options) {
options = options ?? Frozen.EMPTY_OBJECT;
/**
* The length of the axes in meters.
*
* @type {number}
* @default 10000000.0
*/
this.length = options.length ?? 10000000.0;
this._length = undefined;
/**
* The width of the axes in pixels.
*
* @type {number}
* @default 2.0
*/
this.width = options.width ?? 2.0;
this._width = undefined;
/**
* Determines if this primitive will be shown.
*
* @type {boolean}
* @default true
*/
this.show = options.show ?? true;
/**
* The 4x4 matrix that defines the reference frame, i.e., origin plus axes, to visualize.
*
* @type {Matrix4}
* @default {@link Matrix4.IDENTITY}
*/
this.modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY);
this._modelMatrix = new Matrix4();
/**
* User-defined value returned when the primitive is picked.
*
* @type {*}
* @default undefined
*
* @see Scene#pick
*/
this.id = options.id;
this._id = undefined;
this._primitive = undefined;
}
/**
* @private
*/
DebugModelMatrixPrimitive.prototype.update = function (frameState) {
if (!this.show) {
return;
}
if (
!defined(this._primitive) ||
!Matrix4.equals(this._modelMatrix, this.modelMatrix) ||
this._length !== this.length ||
this._width !== this.width ||
this._id !== this.id
) {
this._modelMatrix = Matrix4.clone(this.modelMatrix, this._modelMatrix);
this._length = this.length;
this._width = this.width;
this._id = this.id;
if (defined(this._primitive)) {
this._primitive.destroy();
}
// Workaround projecting (0, 0, 0)
if (
this.modelMatrix[12] === 0.0 &&
this.modelMatrix[13] === 0.0 &&
this.modelMatrix[14] === 0.0
) {
this.modelMatrix[14] = 0.01;
}
const x = new GeometryInstance({
geometry: new PolylineGeometry({
positions: [Cartesian3.ZERO, Cartesian3.UNIT_X],
width: this.width,
vertexFormat: PolylineColorAppearance.VERTEX_FORMAT,
colors: [Color.RED, Color.RED],
arcType: ArcType.NONE,
}),
modelMatrix: Matrix4.multiplyByUniformScale(
this.modelMatrix,
this.length,
new Matrix4(),
),
id: this.id,
pickPrimitive: this,
});
const y = new GeometryInstance({
geometry: new PolylineGeometry({
positions: [Cartesian3.ZERO, Cartesian3.UNIT_Y],
width: this.width,
vertexFormat: PolylineColorAppearance.VERTEX_FORMAT,
colors: [Color.GREEN, Color.GREEN],
arcType: ArcType.NONE,
}),
modelMatrix: Matrix4.multiplyByUniformScale(
this.modelMatrix,
this.length,
new Matrix4(),
),
id: this.id,
pickPrimitive: this,
});
const z = new GeometryInstance({
geometry: new PolylineGeometry({
positions: [Cartesian3.ZERO, Cartesian3.UNIT_Z],
width: this.width,
vertexFormat: PolylineColorAppearance.VERTEX_FORMAT,
colors: [Color.BLUE, Color.BLUE],
arcType: ArcType.NONE,
}),
modelMatrix: Matrix4.multiplyByUniformScale(
this.modelMatrix,
this.length,
new Matrix4(),
),
id: this.id,
pickPrimitive: this,
});
this._primitive = new Primitive({
geometryInstances: [x, y, z],
appearance: new PolylineColorAppearance(),
asynchronous: false,
});
}
this._primitive.update(frameState);
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <p>
* 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.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see DebugModelMatrixPrimitive#destroy
*/
DebugModelMatrixPrimitive.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.
* <p>
* 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.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* p = p && p.destroy();
*
* @see DebugModelMatrixPrimitive#isDestroyed
*/
DebugModelMatrixPrimitive.prototype.destroy = function () {
this._primitive = this._primitive && this._primitive.destroy();
return destroyObject(this);
};
export default DebugModelMatrixPrimitive;
+78
View File
@@ -0,0 +1,78 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines the function used to compare two depths for the depth test.
*
* @enum {number}
*/
const DepthFunction = {
/**
* The depth test never passes.
*
* @type {number}
* @constant
*/
NEVER: WebGLConstants.NEVER,
/**
* The depth test passes if the incoming depth is less than the stored depth.
*
* @type {number}
* @constant
*/
LESS: WebGLConstants.LESS,
/**
* The depth test passes if the incoming depth is equal to the stored depth.
*
* @type {number}
* @constant
*/
EQUAL: WebGLConstants.EQUAL,
/**
* The depth test passes if the incoming depth is less than or equal to the stored depth.
*
* @type {number}
* @constant
*/
LESS_OR_EQUAL: WebGLConstants.LEQUAL,
/**
* The depth test passes if the incoming depth is greater than the stored depth.
*
* @type {number}
* @constant
*/
GREATER: WebGLConstants.GREATER,
/**
* The depth test passes if the incoming depth is not equal to the stored depth.
*
* @type {number}
* @constant
*/
NOT_EQUAL: WebGLConstants.NOTEQUAL,
/**
* The depth test passes if the incoming depth is greater than or equal to the stored depth.
*
* @type {number}
* @constant
*/
GREATER_OR_EQUAL: WebGLConstants.GEQUAL,
/**
* The depth test always passes.
*
* @type {number}
* @constant
*/
ALWAYS: WebGLConstants.ALWAYS,
};
Object.freeze(DepthFunction);
export default DepthFunction;
+238
View File
@@ -0,0 +1,238 @@
import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian3 from "../Core/Cartesian3.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import FeatureDetection from "../Core/FeatureDetection.js";
import Geometry from "../Core/Geometry.js";
import GeometryAttribute from "../Core/GeometryAttribute.js";
import OrthographicFrustum from "../Core/OrthographicFrustum.js";
import PrimitiveType from "../Core/PrimitiveType.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import Pass from "../Renderer/Pass.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderProgram from "../Renderer/ShaderProgram.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import VertexArray from "../Renderer/VertexArray.js";
import DepthPlaneFS from "../Shaders/DepthPlaneFS.js";
import DepthPlaneVS from "../Shaders/DepthPlaneVS.js";
import SceneMode from "./SceneMode.js";
import Ellipsoid from "../Core/Ellipsoid.js";
/**
* @private
*/
function DepthPlane(depthPlaneEllipsoidOffset) {
this._rs = undefined;
this._sp = undefined;
this._va = undefined;
this._command = undefined;
this._mode = undefined;
this._useLogDepth = false;
this._ellipsoidOffset = depthPlaneEllipsoidOffset ?? 0;
}
const depthQuadScratch = FeatureDetection.supportsTypedArrays()
? new Float32Array(12)
: [];
const scratchCartesian1 = new Cartesian3();
const scratchCartesian2 = new Cartesian3();
const scratchCartesian3 = new Cartesian3();
const scratchCartesian4 = new Cartesian3();
const scratchCartesian5 = new Cartesian3();
function computeDepthQuad(ellipsoid, frameState) {
const radii = ellipsoid.radii;
const camera = frameState.camera;
let center, eastOffset, northOffset;
if (camera.frustum instanceof OrthographicFrustum) {
center = Cartesian3.ZERO;
eastOffset = camera.rightWC;
northOffset = camera.upWC;
} else {
const p = camera.positionWC;
// Find the corresponding position in the scaled space of the ellipsoid.
const q = Cartesian3.multiplyComponents(
ellipsoid.oneOverRadii,
p,
scratchCartesian1,
);
const qUnit = Cartesian3.normalize(q, scratchCartesian2);
// Determine the east and north directions at q.
const eUnit = Cartesian3.normalize(
Cartesian3.cross(Cartesian3.UNIT_Z, q, scratchCartesian3),
scratchCartesian3,
);
const nUnit = Cartesian3.normalize(
Cartesian3.cross(qUnit, eUnit, scratchCartesian4),
scratchCartesian4,
);
const qMagnitude = Cartesian3.magnitude(q);
// Determine the radius of the 'limb' of the ellipsoid.
const wMagnitude = Math.sqrt(qMagnitude * qMagnitude - 1.0);
// Compute the center and offsets.
center = Cartesian3.multiplyByScalar(
qUnit,
1.0 / qMagnitude,
scratchCartesian1,
);
const scalar = wMagnitude / qMagnitude;
eastOffset = Cartesian3.multiplyByScalar(eUnit, scalar, scratchCartesian2);
northOffset = Cartesian3.multiplyByScalar(nUnit, scalar, scratchCartesian3);
}
// A conservative measure for the longitudes would be to use the min/max longitudes of the bounding frustum.
const upperLeft = Cartesian3.add(center, northOffset, scratchCartesian5);
Cartesian3.subtract(upperLeft, eastOffset, upperLeft);
Cartesian3.multiplyComponents(radii, upperLeft, upperLeft);
Cartesian3.pack(upperLeft, depthQuadScratch, 0);
const lowerLeft = Cartesian3.subtract(center, northOffset, scratchCartesian5);
Cartesian3.subtract(lowerLeft, eastOffset, lowerLeft);
Cartesian3.multiplyComponents(radii, lowerLeft, lowerLeft);
Cartesian3.pack(lowerLeft, depthQuadScratch, 3);
const upperRight = Cartesian3.add(center, northOffset, scratchCartesian5);
Cartesian3.add(upperRight, eastOffset, upperRight);
Cartesian3.multiplyComponents(radii, upperRight, upperRight);
Cartesian3.pack(upperRight, depthQuadScratch, 6);
const lowerRight = Cartesian3.subtract(
center,
northOffset,
scratchCartesian5,
);
Cartesian3.add(lowerRight, eastOffset, lowerRight);
Cartesian3.multiplyComponents(radii, lowerRight, lowerRight);
Cartesian3.pack(lowerRight, depthQuadScratch, 9);
return depthQuadScratch;
}
DepthPlane.prototype.update = function (frameState) {
this._mode = frameState.mode;
if (frameState.mode !== SceneMode.SCENE3D) {
return;
}
const context = frameState.context;
// Allow offsetting the ellipsoid radius to address rendering artifacts below ellipsoid zero elevation.
const radii = frameState.mapProjection.ellipsoid.radii;
const ellipsoid = new Ellipsoid(
radii.x + this._ellipsoidOffset,
radii.y + this._ellipsoidOffset,
radii.z + this._ellipsoidOffset,
);
const useLogDepth = frameState.useLogDepth;
if (!defined(this._command)) {
this._rs = RenderState.fromCache({
// Write depth, not color
cull: {
enabled: true,
},
depthTest: {
enabled: true,
},
colorMask: {
red: false,
green: false,
blue: false,
alpha: false,
},
});
this._command = new DrawCommand({
renderState: this._rs,
boundingVolume: new BoundingSphere(
Cartesian3.ZERO,
ellipsoid.maximumRadius,
),
pass: Pass.OPAQUE,
owner: this,
});
}
if (!defined(this._sp) || this._useLogDepth !== useLogDepth) {
this._useLogDepth = useLogDepth;
const vs = new ShaderSource({
sources: [DepthPlaneVS],
});
const fs = new ShaderSource({
sources: [DepthPlaneFS],
});
if (useLogDepth) {
fs.defines.push("LOG_DEPTH");
vs.defines.push("LOG_DEPTH");
}
this._sp = ShaderProgram.replaceCache({
shaderProgram: this._sp,
context: context,
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: {
position: 0,
},
});
this._command.shaderProgram = this._sp;
}
// update depth plane
const depthQuad = computeDepthQuad(ellipsoid, frameState);
// depth plane
if (!defined(this._va)) {
const geometry = new Geometry({
attributes: {
position: new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: 3,
values: depthQuad,
}),
},
indices: [0, 1, 2, 2, 1, 3],
primitiveType: PrimitiveType.TRIANGLES,
});
this._va = VertexArray.fromGeometry({
context: context,
geometry: geometry,
attributeLocations: {
position: 0,
},
bufferUsage: BufferUsage.DYNAMIC_DRAW,
});
this._command.vertexArray = this._va;
} else {
this._va.getAttribute(0).vertexBuffer.copyFromArrayView(depthQuad);
}
};
DepthPlane.prototype.execute = function (context, passState) {
if (this._mode === SceneMode.SCENE3D) {
this._command.execute(context, passState);
}
};
DepthPlane.prototype.isDestroyed = function () {
return false;
};
DepthPlane.prototype.destroy = function () {
this._sp = this._sp && this._sp.destroy();
this._va = this._va && this._va.destroy();
};
export default DepthPlane;
+685
View File
@@ -0,0 +1,685 @@
import { MetadataComponentType } from "@cesium/engine";
import defined from "../Core/defined.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import MetadataType from "./MetadataType.js";
import MetadataPickingPipelineStage from "./Model/MetadataPickingPipelineStage.js";
/**
* @private
*/
function DerivedCommand() {}
const fragDepthRegex = /\bgl_FragDepth\b/;
const discardRegex = /\bdiscard\b/;
function getDepthOnlyShaderProgram(context, shaderProgram) {
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"depthOnly",
);
if (defined(cachedShader)) {
return cachedShader;
}
let fs = shaderProgram.fragmentShaderSource;
let writesDepthOrDiscards = false;
const sources = fs.sources;
for (let i = 0; i < sources.length; ++i) {
if (fragDepthRegex.test(sources[i]) || discardRegex.test(sources[i])) {
writesDepthOrDiscards = true;
break;
}
}
const usesLogDepth = fs.defines.indexOf("LOG_DEPTH") >= 0;
if (!writesDepthOrDiscards && !usesLogDepth) {
const source = `void main()
{
out_FragColor = vec4(1.0);
}
`;
fs = new ShaderSource({
sources: [source],
});
} else if (!writesDepthOrDiscards && usesLogDepth) {
const source = `void main()
{
out_FragColor = vec4(1.0);
czm_writeLogDepth();
}
`;
fs = new ShaderSource({
defines: ["LOG_DEPTH"],
sources: [source],
});
}
return context.shaderCache.createDerivedShaderProgram(
shaderProgram,
"depthOnly",
{
vertexShaderSource: shaderProgram.vertexShaderSource,
fragmentShaderSource: fs,
attributeLocations: shaderProgram._attributeLocations,
},
);
}
function getDepthOnlyRenderState(scene, renderState) {
const cache = scene._depthOnlyRenderStateCache;
const cachedDepthOnlyState = cache[renderState.id];
if (defined(cachedDepthOnlyState)) {
return cachedDepthOnlyState;
}
const rs = RenderState.getState(renderState);
rs.depthMask = true;
rs.colorMask = {
red: false,
green: false,
blue: false,
alpha: false,
};
const depthOnlyState = RenderState.fromCache(rs);
cache[renderState.id] = depthOnlyState;
return depthOnlyState;
}
DerivedCommand.createDepthOnlyDerivedCommand = function (
scene,
command,
context,
result,
) {
// For a depth only pass, we bind a framebuffer with only a depth attachment (no color attachments),
// do not write color, and write depth. If the fragment shader doesn't modify the fragment depth
// or discard, the driver can replace the fragment shader with a pass-through shader. We're unsure if this
// actually happens so we modify the shader to use a pass-through fragment shader.
if (!defined(result)) {
result = {};
}
const shader = result.depthOnlyCommand?.shaderProgram;
const renderState = result.depthOnlyCommand?.renderState;
result.depthOnlyCommand = DrawCommand.shallowClone(
command,
result.depthOnlyCommand,
);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.depthOnlyCommand.shaderProgram = getDepthOnlyShaderProgram(
context,
command.shaderProgram,
);
result.depthOnlyCommand.renderState = getDepthOnlyRenderState(
scene,
command.renderState,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.depthOnlyCommand.shaderProgram = shader;
result.depthOnlyCommand.renderState = renderState;
}
return result;
};
const writeLogDepthRegex = /\s+czm_writeLogDepth\(/;
const vertexlogDepthRegex = /\s+czm_vertexLogDepth\(/;
function getLogDepthShaderProgram(context, shaderProgram) {
const disableLogDepthWrite =
shaderProgram.fragmentShaderSource.defines.indexOf("LOG_DEPTH_READ_ONLY") >=
0;
if (disableLogDepthWrite) {
return shaderProgram;
}
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"logDepth",
);
if (defined(cachedShader)) {
return cachedShader;
}
const attributeLocations = shaderProgram._attributeLocations;
const vs = shaderProgram.vertexShaderSource.clone();
const fs = shaderProgram.fragmentShaderSource.clone();
vs.defines = defined(vs.defines) ? vs.defines.slice(0) : [];
vs.defines.push("LOG_DEPTH");
fs.defines = defined(fs.defines) ? fs.defines.slice(0) : [];
fs.defines.push("LOG_DEPTH");
let writesLogDepth = false;
let sources = vs.sources;
for (let i = 0; i < sources.length; ++i) {
if (vertexlogDepthRegex.test(sources[i])) {
writesLogDepth = true;
break;
}
}
if (!writesLogDepth) {
for (let i = 0; i < sources.length; ++i) {
sources[i] = ShaderSource.replaceMain(sources[i], "czm_log_depth_main");
}
const logMain = `
void main()
{
czm_log_depth_main();
czm_vertexLogDepth();
}
`;
sources.push(logMain);
}
sources = fs.sources;
writesLogDepth = false;
for (let i = 0; i < sources.length; ++i) {
if (writeLogDepthRegex.test(sources[i])) {
writesLogDepth = true;
}
}
// This define indicates that a log depth value is written by the shader but doesn't use czm_writeLogDepth.
if (fs.defines.indexOf("LOG_DEPTH_WRITE") !== -1) {
writesLogDepth = true;
}
let logSource = "";
if (!writesLogDepth) {
for (let i = 0; i < sources.length; i++) {
sources[i] = ShaderSource.replaceMain(sources[i], "czm_log_depth_main");
}
logSource = `
void main()
{
czm_log_depth_main();
czm_writeLogDepth();
}
`;
}
sources.push(logSource);
return context.shaderCache.createDerivedShaderProgram(
shaderProgram,
"logDepth",
{
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
},
);
}
DerivedCommand.createLogDepthCommand = function (command, context, result) {
if (!defined(result)) {
result = {};
}
const shader = result.command?.shaderProgram;
result.command = DrawCommand.shallowClone(command, result.command);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.command.shaderProgram = getLogDepthShaderProgram(
context,
command.shaderProgram,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.command.shaderProgram = shader;
}
return result;
};
function getPickShaderProgram(context, shaderProgram, pickId) {
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"pick",
);
if (defined(cachedShader)) {
return cachedShader;
}
const attributeLocations = shaderProgram._attributeLocations;
const { sources, defines } = shaderProgram.fragmentShaderSource;
const hasFragData = sources.some((source) => source.includes("out_FragData"));
const outputColorVariable = hasFragData ? "out_FragData_0" : "out_FragColor";
const newMain = `void main ()
{
czm_non_pick_main();
if (${outputColorVariable}.a == 0.0) {
discard;
}
${outputColorVariable} = ${pickId};
} `;
const length = sources.length;
const newSources = new Array(length + 1);
for (let i = 0; i < length; ++i) {
newSources[i] = ShaderSource.replaceMain(sources[i], "czm_non_pick_main");
}
newSources[length] = newMain;
const fragmentShaderSource = new ShaderSource({
sources: newSources,
defines: defines,
});
return context.shaderCache.createDerivedShaderProgram(shaderProgram, "pick", {
vertexShaderSource: shaderProgram.vertexShaderSource,
fragmentShaderSource: fragmentShaderSource,
attributeLocations: attributeLocations,
});
}
function getPickRenderState(scene, renderState) {
const cache = scene.picking.pickRenderStateCache;
const cachedPickState = cache[renderState.id];
if (defined(cachedPickState)) {
return cachedPickState;
}
const rs = RenderState.getState(renderState);
rs.blending.enabled = false;
// Turns on depth writing for opaque and translucent passes
// Overlapping translucent geometry on the globe surface may exhibit z-fighting
// during the pick pass which may not match the rendered scene. Once
// terrain is on by default and ground primitives are used instead
// this will become less of a problem.
rs.depthMask = true;
const pickState = RenderState.fromCache(rs);
cache[renderState.id] = pickState;
return pickState;
}
DerivedCommand.createPickDerivedCommand = function (
scene,
command,
context,
result,
) {
if (!defined(result)) {
result = {};
}
const shader = result.pickCommand?.shaderProgram;
const renderState = result.pickCommand?.renderState;
result.pickCommand = DrawCommand.shallowClone(command, result.pickCommand);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.pickCommand.shaderProgram = getPickShaderProgram(
context,
command.shaderProgram,
command.pickId,
);
result.pickCommand.renderState = getPickRenderState(
scene,
command.renderState,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.pickCommand.shaderProgram = shader;
result.pickCommand.renderState = renderState;
}
return result;
};
/**
* Replaces the value of the specified 'define' directive identifier
* with the given value.
*
* The given defines are the parts of the define directives that are
* stored in the `ShaderSource`. For example, the defines may be
* `["EXAMPLE", "EXAMPLE_VALUE 123"]`
*
* Calling `replaceDefine(defines, "EXAMPLE", 999)` will result in
* the defines being
* `["EXAMPLE 999", "EXAMPLE_VALUE 123"]`
*
* @param {string[]} defines The define directive identifiers
* @param {string} defineName The name (identifier) of the define directive
* @param {any} newDefineValue The new value whose string representation
* will become the token string for the define directive
* @private
*/
function replaceDefine(defines, defineName, newDefineValue) {
const n = defines.length;
for (let i = 0; i < n; i++) {
const define = defines[i];
const tokens = define.trimStart().split(/\s+/);
if (tokens[0] === defineName) {
defines[i] = `${defineName} ${newDefineValue}`;
}
}
}
/**
* Returns the component count for the given class property, or
* its array length if it is an array.
*
* This will be
* `[1, 2, 3, 4]` for `[SCALAR, VEC2, VEC3, VEC4`] types,
* or the array length if it is an array.
*
* @param {MetadataClassProperty} classProperty The class property
* @returns {number} The component count
* @private
*/
function getComponentCount(classProperty) {
if (!classProperty.isArray) {
return MetadataType.getComponentCount(classProperty.type);
}
return classProperty.arrayLength;
}
/**
* Returns a shader statement that applies the inverse of the
* value transform to the given value, based on the given offset
* and scale.
*
* @param {string} input The input value
* @param {string} offset The offset
* @param {string} scale The scale
* @returns {string} The statement
*/
function unapplyValueTransform(input, offset, scale) {
return `((${input} - float(${offset})) / float(${scale}))`;
}
/**
* Returns a shader statement that applies the inverse of the
* normalization, based on the given component type
*
* @param {string} input The input value
* @param {string} componentType The component type
* @returns {string} The statement
*/
function unnormalize(input, componentType) {
const max = MetadataComponentType.getMaximum(componentType);
return `(${input}) / float(${max})`;
}
/**
* Creates a shader statement that returns the value of the specified
* property, normalized to the range [0, 1].
*
* @param {MetadataClassProperty} classProperty The class property
* @param {object} metadataProperty The metadata property, either
* a `PropertyTextureProperty` or a `PropertyAttributeProperty`
* @returns {string} The string
*/
function getSourceValueStringScalar(classProperty, metadataProperty) {
let result = `float(value)`;
// The 'hasValueTransform' indicates whether the property
// (or its class property) did define an 'offset' or 'scale'.
// Even when they had not been defined in the JSON, they are
// defined in the object, with default values.
if (metadataProperty.hasValueTransform) {
const offset = metadataProperty.offset;
const scale = metadataProperty.scale;
result = unapplyValueTransform(result, offset, scale);
}
if (!classProperty.normalized) {
result = unnormalize(result, classProperty.componentType);
}
return result;
}
/**
* Creates a shader statement that returns the value of the specified
* component of the given property, normalized to the range [0, 1].
*
* @param {MetadataClassProperty} classProperty The class property
* @param {object} metadataProperty The metadata property, either
* a `PropertyTextureProperty` or a `PropertyAttributeProperty`
* @param {string} componentName The name, in ["x", "y", "z", "w"]
* @returns {string} The string
*/
function getSourceValueStringComponent(
classProperty,
metadataProperty,
componentName,
) {
const valueString = `value.${componentName}`;
let result = `float(${valueString})`;
// The 'hasValueTransform' indicates whether the property
// (or its class property) did define an 'offset' or 'scale'.
// Even when they had not been defined in the JSON, they are
// defined in the object, with default values
// Note that in the 'PropertyTextureProperty' and the
// 'PropertyAttributeProperty', these values are
// stored as "object types" (like 'Cartesian2'), whereas
// in the 'MetadataClassProperty', they are stored as
// "array types", e.g. a `[number, number]`
if (metadataProperty.hasValueTransform) {
const offset = metadataProperty.offset[componentName];
const scale = metadataProperty.scale[componentName];
result = unapplyValueTransform(result, offset, scale);
}
if (!classProperty.normalized) {
result = unnormalize(result, classProperty.componentType);
}
return result;
}
/**
* Creates a new `ShaderProgram` from the given input that renders metadata
* values into the frame buffer, according to the given picked metadata info.
*
* This will update the `defines` of the fragment shader of the given shader
* program, by setting `METADATA_PICKING_ENABLED`, and updating the
* `METADATA_PICKING_VALUE_*` defines so that they reflect the components
* of the metadata that should be written into the RGBA (vec4) that
* ends up as the 'color' in the frame buffer.
*
* The RGBA values will eventually be converted back into an actual metadata
* value in `Picking.js`, by calling `MetadataPicking.decodeMetadataValues`.
*
* @param {Context} context The context
* @param {ShaderProgram} shaderProgram The shader program
* @param {PickedMetadataInfo} pickedMetadataInfo The picked metadata info
* @returns {ShaderProgram} The new shader program
* @private
*/
function getPickMetadataShaderProgram(
context,
shaderProgram,
pickedMetadataInfo,
) {
const schemaId = pickedMetadataInfo.schemaId;
const className = pickedMetadataInfo.className;
const propertyName = pickedMetadataInfo.propertyName;
const keyword = `pickMetadata-${schemaId}-${className}-${propertyName}`;
const shader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
keyword,
);
if (defined(shader)) {
return shader;
}
const metadataProperty = pickedMetadataInfo.metadataProperty;
const classProperty = pickedMetadataInfo.classProperty;
const glslType = classProperty.getGlslType();
// Define the components that will go into the output `metadataValues`.
// This will be the 'color' that is written into the frame buffer,
// meaning that the values should be in [0.0, 1.0], and will become
// values in [0, 255] in the frame buffer.
// By default, all of them are 0.0.
const sourceValueStrings = ["0.0", "0.0", "0.0", "0.0"];
const componentCount = getComponentCount(classProperty);
if (componentCount === 1) {
// When the property is a scalar, store the source value
// string directly in `metadataValues.x`
sourceValueStrings[0] = getSourceValueStringScalar(
classProperty,
metadataProperty,
);
} else {
// When the property is an array, store the array elements
// in `metadataValues.x/y/z/w`
const componentNames = ["x", "y", "z", "w"];
for (let i = 0; i < componentCount; i++) {
sourceValueStrings[i] = getSourceValueStringComponent(
classProperty,
metadataProperty,
componentNames[i],
);
}
}
const newDefines = shaderProgram.fragmentShaderSource.defines.slice();
newDefines.push(MetadataPickingPipelineStage.METADATA_PICKING_ENABLED);
// Replace the defines of the shader, using the type, property
// access, and value components that have been determined
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_TYPE,
glslType,
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_STRING,
`metadata.${propertyName}`,
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_X,
sourceValueStrings[0],
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Y,
sourceValueStrings[1],
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Z,
sourceValueStrings[2],
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_W,
sourceValueStrings[3],
);
const newFragmentShaderSource = new ShaderSource({
sources: shaderProgram.fragmentShaderSource.sources,
defines: newDefines,
});
const newShader = context.shaderCache.createDerivedShaderProgram(
shaderProgram,
keyword,
{
vertexShaderSource: shaderProgram.vertexShaderSource,
fragmentShaderSource: newFragmentShaderSource,
attributeLocations: shaderProgram._attributeLocations,
},
);
return newShader;
}
/**
* @private
*/
DerivedCommand.createPickMetadataDerivedCommand = function (
scene,
command,
context,
result,
) {
if (!defined(result)) {
result = {};
}
result.pickMetadataCommand = DrawCommand.shallowClone(
command,
result.pickMetadataCommand,
);
result.pickMetadataCommand.shaderProgram = getPickMetadataShaderProgram(
context,
command.shaderProgram,
command.pickedMetadataInfo,
);
result.pickMetadataCommand.renderState = getPickRenderState(
scene,
command.renderState,
);
result.shaderProgramId = command.shaderProgram.id;
return result;
};
function getHdrShaderProgram(context, shaderProgram) {
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"HDR",
);
if (defined(cachedShader)) {
return cachedShader;
}
const attributeLocations = shaderProgram._attributeLocations;
const vs = shaderProgram.vertexShaderSource.clone();
const fs = shaderProgram.fragmentShaderSource.clone();
vs.defines = defined(vs.defines) ? vs.defines.slice(0) : [];
vs.defines.push("HDR");
fs.defines = defined(fs.defines) ? fs.defines.slice(0) : [];
fs.defines.push("HDR");
return context.shaderCache.createDerivedShaderProgram(shaderProgram, "HDR", {
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
});
}
DerivedCommand.createHdrCommand = function (command, context, result) {
if (!defined(result)) {
result = {};
}
const shader = result.command?.shaderProgram;
result.command = DrawCommand.shallowClone(command, result.command);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.command.shaderProgram = getHdrShaderProgram(
context,
command.shaderProgram,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.command.shaderProgram = shader;
}
return result;
};
export default DerivedCommand;
@@ -0,0 +1,120 @@
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import CesiumMath from "../Core/Math.js";
import Matrix3 from "../Core/Matrix3.js";
import Quaternion from "../Core/Quaternion.js";
/**
* @private
*/
function DeviceOrientationCameraController(scene) {
//>>includeStart('debug', pragmas.debug);
if (!defined(scene)) {
throw new DeveloperError("scene is required.");
}
//>>includeEnd('debug');
this._scene = scene;
this._lastAlpha = undefined;
this._lastBeta = undefined;
this._lastGamma = undefined;
this._alpha = undefined;
this._beta = undefined;
this._gamma = undefined;
const that = this;
function callback(e) {
const alpha = e.alpha;
if (!defined(alpha)) {
that._alpha = undefined;
that._beta = undefined;
that._gamma = undefined;
return;
}
that._alpha = CesiumMath.toRadians(alpha);
that._beta = CesiumMath.toRadians(e.beta);
that._gamma = CesiumMath.toRadians(e.gamma);
}
window.addEventListener("deviceorientation", callback, false);
this._removeListener = function () {
window.removeEventListener("deviceorientation", callback, false);
};
}
const scratchQuaternion1 = new Quaternion();
const scratchQuaternion2 = new Quaternion();
const scratchMatrix3 = new Matrix3();
function rotate(camera, alpha, beta, gamma) {
const direction = camera.direction;
const right = camera.right;
const up = camera.up;
const bQuat = Quaternion.fromAxisAngle(direction, beta, scratchQuaternion2);
const gQuat = Quaternion.fromAxisAngle(right, gamma, scratchQuaternion1);
const rotQuat = Quaternion.multiply(gQuat, bQuat, gQuat);
const aQuat = Quaternion.fromAxisAngle(up, alpha, scratchQuaternion2);
Quaternion.multiply(aQuat, rotQuat, rotQuat);
const matrix = Matrix3.fromQuaternion(rotQuat, scratchMatrix3);
Matrix3.multiplyByVector(matrix, right, right);
Matrix3.multiplyByVector(matrix, up, up);
Matrix3.multiplyByVector(matrix, direction, direction);
}
DeviceOrientationCameraController.prototype.update = function () {
if (!defined(this._alpha)) {
return;
}
if (!defined(this._lastAlpha)) {
this._lastAlpha = this._alpha;
this._lastBeta = this._beta;
this._lastGamma = this._gamma;
}
const a = this._lastAlpha - this._alpha;
const b = this._lastBeta - this._beta;
const g = this._lastGamma - this._gamma;
rotate(this._scene.camera, -a, b, g);
this._lastAlpha = this._alpha;
this._lastBeta = this._beta;
this._lastGamma = this._gamma;
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*/
DeviceOrientationCameraController.prototype.isDestroyed = function () {
return false;
};
/**
* Destroys the resources held by this object. Destroying an object allows for deterministic
* release of resources, instead of relying on the garbage collector to destroy this object.
* <br /><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.
*/
DeviceOrientationCameraController.prototype.destroy = function () {
this._removeListener();
return destroyObject(this);
};
export default DeviceOrientationCameraController;
+49
View File
@@ -0,0 +1,49 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import DeveloperError from "../Core/DeveloperError.js";
/**
* A light that gets emitted in a single direction from infinitely far away.
*
* @param {object} options Object with the following properties:
* @param {Cartesian3} options.direction The direction in which light gets emitted.
* @param {Color} [options.color=Color.WHITE] The color of the light.
* @param {number} [options.intensity=1.0] The intensity of the light.
*
* @exception {DeveloperError} options.direction cannot be zero-length
*
* @alias DirectionalLight
* @constructor
*/
function DirectionalLight(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options", options);
Check.typeOf.object("options.direction", options.direction);
if (Cartesian3.equals(options.direction, Cartesian3.ZERO)) {
throw new DeveloperError("options.direction cannot be zero-length");
}
//>>includeEnd('debug');
/**
* The direction in which light gets emitted.
* @type {Cartesian3}
*/
this.direction = Cartesian3.clone(options.direction);
/**
* The color of the light.
* @type {Color}
* @default Color.WHITE
*/
this.color = Color.clone(options.color ?? Color.WHITE);
/**
* The intensity of the light.
* @type {number}
* @default 1.0
*/
this.intensity = options.intensity ?? 1.0;
}
export default DirectionalLight;

Some files were not shown because too many files have changed in this diff Show More