Add height reference behavior to ModelExperimental

This commit is contained in:
Janine Liu 2022-06-09 15:38:24 -04:00
parent b6cf89ee89
commit d69afa2de3
2 changed files with 211 additions and 56 deletions

View File

@ -8,6 +8,7 @@ import defined from "../../Core/defined.js";
import defaultValue from "../../Core/defaultValue.js";
import DeveloperError from "../../Core/DeveloperError.js";
import GltfLoader from "../GltfLoader.js";
import HeightReference from "../HeightReference.js";
import ImageBasedLighting from "../ImageBasedLighting.js";
import ModelExperimentalAnimationCollection from "./ModelExperimentalAnimationCollection.js";
import ModelExperimentalSceneGraph from "./ModelExperimentalSceneGraph.js";
@ -27,6 +28,7 @@ import I3dmLoader from "./I3dmLoader.js";
import PntsLoader from "./PntsLoader.js";
import Color from "../../Core/Color.js";
import SceneMode from "../SceneMode.js";
import SceneTransforms from "../SceneTransforms.js";
import ShadowMode from "../ShadowMode.js";
import SplitDirection from "../SplitDirection.js";
@ -42,6 +44,7 @@ import SplitDirection from "../SplitDirection.js";
*
* @param {Object} options Object with the following properties:
* @param {Resource} options.resource The Resource to the 3D model.
* @param {Boolean} [options.show=true] Whether or not to render the model.
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the model from model to world coordinates.
* @param {Number} [options.scale=1.0] A uniform scale applied to this model.
* @param {Number} [options.minimumPixelSize=0.0] The approximate minimum pixel size of the model regardless of zoom.
@ -55,7 +58,8 @@ import SplitDirection from "../SplitDirection.js";
* @param {Boolean} [options.allowPicking=true] When <code>true</code>, each primitive is pickable with {@link Scene#pick}.
* @param {CustomShader} [options.customShader] A custom shader. This will add user-defined GLSL code to the vertex and fragment shaders. Using custom shaders with a {@link Cesium3DTileStyle} may lead to undefined behavior.
* @param {Cesium3DTileContent} [options.content] The tile content this model belongs to. This property will be undefined if model is not loaded as part of a tileset.
* @param {Boolean} [options.show=true] Whether or not to render the model.
* @param {HeightReference} [options.heightReference=HeightReference.NONE] Determines how the model is drawn relative to terrain.
* @param {Scene} [options.scene] Must be passed in for models that use the height reference property.
* @param {Color} [options.color] A color that blends with the model's rendered color.
* @param {ColorBlendMode} [options.colorBlendMode=ColorBlendMode.HIGHLIGHT] Defines how the color blends with the model.
* @param {Number} [options.colorBlendAmount=0.5] Value used to determine the color strength when the <code>colorBlendMode</code> is <code>MIX</code>. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two.
@ -212,10 +216,31 @@ export default function ModelExperimental(options) {
// Keeps track of resources that need to be destroyed when the Model is destroyed.
this._modelResources = [];
// Computation of the model's bounding sphere and its initial radius is done in ModelExperimentalSceneGraph
// Computation of the model's bounding sphere and its initial radius is done
// in ModelExperimentalSceneGraph.
this._boundingSphere = new BoundingSphere();
this._initialRadius = undefined;
this._heightReference = defaultValue(
options.heightReference,
HeightReference.NONE
);
this._heightDirty = this._heightReference !== HeightReference.NONE;
this._removeUpdateHeightCallback = undefined;
this._clampedModelMatrix = undefined; // For use with height reference
const scene = options.scene;
if (defined(scene) && defined(scene.terrainProviderChanged)) {
this._terrainProviderChangedCallback = scene.terrainProviderChanged.addEventListener(
function () {
this._heightDirty = true;
},
this
);
}
this._scene = scene;
const pointCloudShading = new PointCloudShading(options.pointCloudShading);
this._attenuation = pointCloudShading.attenuation;
this._pointCloudShading = pointCloudShading;
@ -613,6 +638,28 @@ Object.defineProperties(ModelExperimental.prototype, {
},
},
/**
* The height reference of the model, which determines how the model is drawn
* relative to terrain.
*
* @memberof ModelExperimental.prototype
*
* @type {HeightReference}
* @default {HeightReference.NONE}
*
*/
heightReference: {
get: function () {
return this._heightReference;
},
set: function (value) {
if (value !== this._heightReference) {
this._heightDirty = true;
}
this._heightReference = value;
},
},
/**
* The structural metadata from the EXT_structural_metadata extension
*
@ -1209,12 +1256,32 @@ ModelExperimental.prototype.update = function (frameState) {
this._attenuation = this.pointCloudShading.attenuation;
}
const context = frameState.context;
const referenceMatrix = defaultValue(this.referenceMatrix, this.modelMatrix);
// Update the image-based lighting for this model to detect any changes in parameters.
// Update the image-based lighting for this model to load any texture uniforms
// it uses (specular maps) and to detect any changes in parameters.
this._imageBasedLighting.update(frameState);
const context = frameState.context;
this._defaultTexture = context.defaultTexture;
// Short-circuit if the model resources aren't ready.
if (!this._resourcesLoaded || frameState.mode === SceneMode.MORPHING) {
return;
}
// Some of the other features (e.g. image-based lighting, clipping planes)
// depend on the model matrix being updated for the current height reference,
// so update it first.
if (this._heightDirty) {
updateClamping(this);
this._heightDirty = false;
this._updateModelMatrix = true;
}
const modelMatrix = defined(this._clampedModelMatrix)
? this._clampedModelMatrix
: this.modelMatrix;
const referenceMatrix = defaultValue(this.referenceMatrix, modelMatrix);
if (
this._imageBasedLighting.useSphericalHarmonicCoefficients ||
this._imageBasedLighting.useSpecularEnvironmentMaps
@ -1241,6 +1308,8 @@ ModelExperimental.prototype.update = function (frameState) {
);
}
// This value will have been updated after calling imageBasedLighting.update()
// earlier in the function.
if (this._imageBasedLighting.shouldRegenerateShaders) {
this.resetDrawCommands();
}
@ -1276,13 +1345,6 @@ ModelExperimental.prototype.update = function (frameState) {
this._clippingPlanesState = currentClippingPlanesState;
}
this._defaultTexture = context.defaultTexture;
// short-circuit if the model resources aren't ready.
if (!this._resourcesLoaded || frameState.mode === SceneMode.MORPHING) {
return;
}
if (frameState.mode !== this._sceneMode) {
if (this._projectTo2D) {
this.resetDrawCommands();
@ -1312,22 +1374,13 @@ ModelExperimental.prototype.update = function (frameState) {
this.destroyResources();
this._sceneGraph.buildDrawCommands(frameState);
this._drawCommandsBuilt = true;
const model = this;
if (!model._ready) {
model._completeLoad(model, frameState);
// Don't render until the next frame after the ready promise is resolved
return;
}
}
// This is done without a dirty flag so that the model matrix can be updated in-place
// without needing to use a setter.
if (!Matrix4.equals(this.modelMatrix, this._modelMatrix)) {
//>>includeStart('debug', pragmas.debug);
if (frameState.mode !== SceneMode.SCENE3D && this._projectTo2D) {
if (frameState.mode === SceneMode.SCENE3D && this._projectTo2D) {
throw new DeveloperError(
"ModelExperimental.modelMatrix cannot be changed in 2D or Columbus View if projectTo2D is true."
);
@ -1335,23 +1388,34 @@ ModelExperimental.prototype.update = function (frameState) {
//>>includeEnd('debug');
this._updateModelMatrix = true;
this._modelMatrix = Matrix4.clone(this.modelMatrix, this._modelMatrix);
this._boundingSphere = BoundingSphere.transform(
this._sceneGraph.boundingSphere,
this.modelMatrix,
this._boundingSphere
);
}
if (this._updateModelMatrix || this._minimumPixelSize !== 0.0) {
this._clampedScale = defined(this._maximumScale)
? Math.min(this._scale, this._maximumScale)
: this._scale;
this._boundingSphere = BoundingSphere.transform(
this._sceneGraph.boundingSphere,
modelMatrix,
this._boundingSphere
);
this._boundingSphere.radius = this._initialRadius * this._clampedScale;
this._computedScale = getScale(this, frameState);
this._sceneGraph.updateModelMatrix(frameState);
this._computedScale = getScale(this, modelMatrix, frameState);
this._sceneGraph.updateModelMatrix(modelMatrix, frameState);
this._updateModelMatrix = false;
}
// This check occurs after the bounding sphere has been updated so that
// to account for the modifications from the clamp-to-ground setting.
const model = this;
if (!model._ready) {
model._completeLoad(model, frameState);
// Don't render until the next frame after the ready promise is resolved
return;
}
if (this._backFaceCullingDirty) {
this.sceneGraph.updateBackFaceCulling(this._backFaceCulling);
this._backFaceCullingDirty = false;
@ -1416,9 +1480,8 @@ function scaleInPixels(positionWC, radius, frameState) {
}
const scratchPosition = new Cartesian3();
const scratchCartographic = new Cartographic();
function getScale(model, frameState) {
function getScale(model, modelMatrix, frameState) {
let scale = model.scale;
if (model.minimumPixelSize !== 0.0 && !model._projectTo2D) {
@ -1428,25 +1491,14 @@ function getScale(model, frameState) {
context.drawingBufferWidth,
context.drawingBufferHeight
);
const m = model.modelMatrix;
scratchPosition.x = m[12];
scratchPosition.y = m[13];
scratchPosition.z = m[14];
scratchPosition.x = modelMatrix[12];
scratchPosition.y = modelMatrix[13];
scratchPosition.z = modelMatrix[14];
if (model._sceneMode !== SceneMode.SCENE3D) {
const projection = frameState.mapProjection;
const cartographic = projection.ellipsoid.cartesianToCartographic(
SceneTransforms.computeActualWgs84Position(
frameState,
scratchPosition,
scratchCartographic
);
projection.project(cartographic, scratchPosition);
// In 2D / CV mode, the map is a yz-plane in world space, so the coordinates
// need to be reordered accordingly.
Cartesian3.fromElements(
scratchPosition.z,
scratchPosition.x,
scratchPosition.y,
scratchPosition
);
}
@ -1474,6 +1526,89 @@ function getScale(model, frameState) {
: scale;
}
const scratchCartographic = new Cartographic();
function getUpdateHeightCallback(model, ellipsoid, cartoPosition) {
return function (clampedPosition) {
if (model.heightReference === HeightReference.RELATIVE_TO_GROUND) {
const clampedCart = ellipsoid.cartesianToCartographic(
clampedPosition,
scratchCartographic
);
clampedCart.height += cartoPosition.height;
ellipsoid.cartographicToCartesian(clampedCart, clampedPosition);
}
const clampedModelMatrix = model._clampedModelMatrix;
// Modify clamped model matrix to use new height
Matrix4.clone(model.modelMatrix, clampedModelMatrix);
clampedModelMatrix[12] = clampedPosition.x;
clampedModelMatrix[13] = clampedPosition.y;
clampedModelMatrix[14] = clampedPosition.z;
model._heightChanged = true;
};
}
function updateClamping(model) {
if (defined(model._removeUpdateHeightCallback)) {
model._removeUpdateHeightCallback();
model._removeUpdateHeightCallback = undefined;
}
const scene = model._scene;
if (
!defined(scene) ||
!defined(scene.globe) ||
model.heightReference === HeightReference.NONE
) {
//>>includeStart('debug', pragmas.debug);
if (model.heightReference !== HeightReference.NONE) {
throw new DeveloperError(
"Height reference is not supported without a scene and globe."
);
}
//>>includeEnd('debug');
model._clampedModelMatrix = undefined;
return;
}
const globe = scene.globe;
const ellipsoid = globe.ellipsoid;
// Compute cartographic position so we don't recompute every update
const modelMatrix = model.modelMatrix;
scratchPosition.x = modelMatrix[12];
scratchPosition.y = modelMatrix[13];
scratchPosition.z = modelMatrix[14];
const cartoPosition = ellipsoid.cartesianToCartographic(scratchPosition);
if (!defined(model._clampedModelMatrix)) {
model._clampedModelMatrix = Matrix4.clone(modelMatrix, new Matrix4());
}
// Install callback to handle updating of terrain tiles
const surface = globe._surface;
model._removeUpdateHeightCallback = surface.updateHeight(
cartoPosition,
getUpdateHeightCallback(model, ellipsoid, cartoPosition)
);
// Set the correct height now
const height = globe.getHeight(cartoPosition);
if (defined(height)) {
// Get callback with cartoPosition being the non-clamped position
const callback = getUpdateHeightCallback(model, ellipsoid, cartoPosition);
// Compute the clamped cartesian and call updateHeight callback
Cartographic.clone(cartoPosition, scratchCartographic);
scratchCartographic.height = height;
ellipsoid.cartographicToCartesian(scratchCartographic, scratchPosition);
callback(scratchPosition);
}
}
/**
* Gets whether or not clipping planes are enabled for this model.
*
@ -1536,6 +1671,17 @@ ModelExperimental.prototype.destroy = function () {
this.destroyResources();
this.destroyModelResources();
// Remove callbacks for height reference behavior.
if (defined(this._removeUpdateHeightCallback)) {
this._removeUpdateHeightCallback();
this._removeUpdateHeightCallback = undefined;
}
if (defined(this._terrainProviderChangedCallback)) {
this._terrainProviderChangedCallback();
this._terrainProviderChangedCallback = undefined;
}
// Only destroy the ClippingPlaneCollection if this is the owner.
const clippingPlaneCollection = this._clippingPlanes;
if (
@ -1596,6 +1742,7 @@ ModelExperimental.prototype.destroyModelResources = function () {
* @param {Object} options Object with the following properties:
* @param {String|Resource} options.url The url to the .gltf or .glb file.
* @param {String|Resource} [options.basePath=''] The base path that paths in the glTF JSON are relative to.
* @param {Boolean} [options.show=true] Whether or not to render the model.
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the model from model to world coordinates.
* @param {Number} [options.scale=1.0] A uniform scale applied to this model.
* @param {Number} [options.minimumPixelSize=0.0] The approximate minimum pixel size of the model regardless of zoom.
@ -1612,7 +1759,8 @@ ModelExperimental.prototype.destroyModelResources = function () {
* @param {Boolean} [options.allowPicking=true] When <code>true</code>, each primitive is pickable with {@link Scene#pick}.
* @param {CustomShader} [options.customShader] A custom shader. This will add user-defined GLSL code to the vertex and fragment shaders. Using custom shaders with a {@link Cesium3DTileStyle} may lead to undefined behavior.
* @param {Cesium3DTileContent} [options.content] The tile content this model belongs to. This property will be undefined if model is not loaded as part of a tileset.
* @param {Boolean} [options.show=true] Whether or not to render the model.
* @param {HeightReference} [options.heightReference=HeightReference.NONE] Determines how the model is drawn relative to terrain.
* @param {Scene} [options.scene] Must be passed in for models that use the height reference property.
* @param {Color} [options.color] A color that blends with the model's rendered color.
* @param {ColorBlendMode} [options.colorBlendMode=ColorBlendMode.HIGHLIGHT] Defines how the color blends with the model.
* @param {Number} [options.colorBlendAmount=0.5] Value used to determine the color strength when the <code>colorBlendMode</code> is <code>MIX</code>. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two.
@ -1808,6 +1956,7 @@ function makeModelOptions(loader, modelType, options) {
loader: loader,
type: modelType,
resource: options.resource,
show: options.show,
modelMatrix: options.modelMatrix,
scale: options.scale,
minimumPixelSize: options.minimumPixelSize,
@ -1820,7 +1969,8 @@ function makeModelOptions(loader, modelType, options) {
allowPicking: options.allowPicking,
customShader: options.customShader,
content: options.content,
show: options.show,
heightReference: options.heightReference,
scene: options.scene,
color: options.color,
colorBlendAmount: options.colorBlendAmount,
colorBlendMode: options.colorBlendMode,

View File

@ -221,13 +221,17 @@ function initialize(sceneGraph) {
const components = sceneGraph._components;
const scene = components.scene;
computeModelMatrix(sceneGraph);
// If the model has a height reference that modifies the model matrix,
// it will be accounted for in updateModelMatrix.
const modelMatrix = sceneGraph._model.modelMatrix;
computeModelMatrix(sceneGraph, modelMatrix);
const nodes = components.nodes;
const nodesLength = nodes.length;
// Initialize this array to be the same size as the nodes array in the model's file.
// This is so nodes can be stored by their index in the file, for future ease of access.
// Initialize this array to be the same size as the nodes array in
// the model's file. This is so nodes can be stored by their index
// in the file, for future ease of access.
sceneGraph._runtimeNodes = new Array(nodesLength);
const rootNodes = scene.nodes;
@ -276,12 +280,12 @@ function initialize(sceneGraph) {
}
}
function computeModelMatrix(sceneGraph) {
function computeModelMatrix(sceneGraph, modelMatrix) {
const components = sceneGraph._components;
const model = sceneGraph._model;
sceneGraph._computedModelMatrix = Matrix4.multiplyTransformation(
model.modelMatrix,
modelMatrix,
components.transform,
sceneGraph._computedModelMatrix
);
@ -605,9 +609,10 @@ ModelExperimentalSceneGraph.prototype.update = function (
};
ModelExperimentalSceneGraph.prototype.updateModelMatrix = function (
modelMatrix,
frameState
) {
computeModelMatrix(this);
computeModelMatrix(this, modelMatrix);
if (frameState.mode !== SceneMode.SCENE3D) {
computeModelMatrix2D(this, frameState);
}