@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,586 lines (1,390 loc) • 94.2 kB
JavaScript
import AttributeCompression from "./AttributeCompression.js";
import barycentricCoordinates from "./barycentricCoordinates.js";
import BoundingSphere from "./BoundingSphere.js";
import Cartesian2 from "./Cartesian2.js";
import Cartesian3 from "./Cartesian3.js";
import Cartesian4 from "./Cartesian4.js";
import Cartographic from "./Cartographic.js";
import ComponentDatatype from "./ComponentDatatype.js";
import defined from "./defined.js";
import DeveloperError from "./DeveloperError.js";
import EncodedCartesian3 from "./EncodedCartesian3.js";
import GeographicProjection from "./GeographicProjection.js";
import Geometry from "./Geometry.js";
import GeometryAttribute from "./GeometryAttribute.js";
import GeometryType from "./GeometryType.js";
import IndexDatatype from "./IndexDatatype.js";
import Intersect from "./Intersect.js";
import IntersectionTests from "./IntersectionTests.js";
import CesiumMath from "./Math.js";
import Matrix3 from "./Matrix3.js";
import Matrix4 from "./Matrix4.js";
import Plane from "./Plane.js";
import PrimitiveType from "./PrimitiveType.js";
import Tipsify from "./Tipsify.js";
/**
* Content pipeline functions for geometries.
*
* @namespace GeometryPipeline
*
* @see Geometry
*/
const GeometryPipeline = {};
function addTriangle(lines, index, i0, i1, i2) {
lines[index++] = i0;
lines[index++] = i1;
lines[index++] = i1;
lines[index++] = i2;
lines[index++] = i2;
lines[index] = i0;
}
function trianglesToLines(triangles) {
const count = triangles.length;
const size = (count / 3) * 6;
const lines = IndexDatatype.createTypedArray(count, size);
let index = 0;
for (let i = 0; i < count; i += 3, index += 6) {
addTriangle(lines, index, triangles[i], triangles[i + 1], triangles[i + 2]);
}
return lines;
}
function triangleStripToLines(triangles) {
const count = triangles.length;
if (count >= 3) {
const size = (count - 2) * 6;
const lines = IndexDatatype.createTypedArray(count, size);
addTriangle(lines, 0, triangles[0], triangles[1], triangles[2]);
let index = 6;
for (let i = 3; i < count; ++i, index += 6) {
addTriangle(
lines,
index,
triangles[i - 1],
triangles[i],
triangles[i - 2],
);
}
return lines;
}
return new Uint16Array();
}
function triangleFanToLines(triangles) {
if (triangles.length > 0) {
const count = triangles.length - 1;
const size = (count - 1) * 6;
const lines = IndexDatatype.createTypedArray(count, size);
const base = triangles[0];
let index = 0;
for (let i = 1; i < count; ++i, index += 6) {
addTriangle(lines, index, base, triangles[i], triangles[i + 1]);
}
return lines;
}
return new Uint16Array();
}
/**
* Converts a geometry's triangle indices to line indices. If the geometry has an <code>indices</code>
* and its <code>primitiveType</code> is <code>TRIANGLES</code>, <code>TRIANGLE_STRIP</code>,
* <code>TRIANGLE_FAN</code>, it is converted to <code>LINES</code>; otherwise, the geometry is not changed.
* <p>
* This is commonly used to create a wireframe geometry for visual debugging.
* </p>
*
* @param {Geometry} geometry The geometry to modify.
* @returns {Geometry} The modified <code>geometry</code> argument, with its triangle indices converted to lines.
*
* @exception {DeveloperError} geometry.primitiveType must be TRIANGLES, TRIANGLE_STRIP, or TRIANGLE_FAN.
*
* @example
* geometry = Cesium.GeometryPipeline.toWireframe(geometry);
*/
GeometryPipeline.toWireframe = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
//>>includeEnd('debug');
const indices = geometry.indices;
if (defined(indices)) {
switch (geometry.primitiveType) {
case PrimitiveType.TRIANGLES:
geometry.indices = trianglesToLines(indices);
break;
case PrimitiveType.TRIANGLE_STRIP:
geometry.indices = triangleStripToLines(indices);
break;
case PrimitiveType.TRIANGLE_FAN:
geometry.indices = triangleFanToLines(indices);
break;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError(
"geometry.primitiveType must be TRIANGLES, TRIANGLE_STRIP, or TRIANGLE_FAN.",
);
//>>includeEnd('debug');
}
geometry.primitiveType = PrimitiveType.LINES;
}
return geometry;
};
/**
* Creates a new {@link Geometry} with <code>LINES</code> representing the provided
* attribute (<code>attributeName</code>) for the provided geometry. This is used to
* visualize vector attributes like normals, tangents, and bitangents.
*
* @param {Geometry} geometry The <code>Geometry</code> instance with the attribute.
* @param {string} [attributeName='normal'] The name of the attribute.
* @param {number} [length=10000.0] The length of each line segment in meters. This can be negative to point the vector in the opposite direction.
* @returns {Geometry} A new <code>Geometry</code> instance with line segments for the vector.
*
* @exception {DeveloperError} geometry.attributes must have an attribute with the same name as the attributeName parameter.
*
* @example
* const geometry = Cesium.GeometryPipeline.createLineSegmentsForVectors(instance.geometry, 'bitangent', 100000.0);
*/
GeometryPipeline.createLineSegmentsForVectors = function (
geometry,
attributeName,
length,
) {
attributeName = attributeName ?? "normal";
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
if (!defined(geometry.attributes.position)) {
throw new DeveloperError("geometry.attributes.position is required.");
}
if (!defined(geometry.attributes[attributeName])) {
throw new DeveloperError(
`geometry.attributes must have an attribute with the same name as the attributeName parameter, ${attributeName}.`,
);
}
//>>includeEnd('debug');
length = length ?? 10000.0;
const positions = geometry.attributes.position.values;
const vectors = geometry.attributes[attributeName].values;
const positionsLength = positions.length;
const newPositions = new Float64Array(2 * positionsLength);
let j = 0;
for (let i = 0; i < positionsLength; i += 3) {
newPositions[j++] = positions[i];
newPositions[j++] = positions[i + 1];
newPositions[j++] = positions[i + 2];
newPositions[j++] = positions[i] + vectors[i] * length;
newPositions[j++] = positions[i + 1] + vectors[i + 1] * length;
newPositions[j++] = positions[i + 2] + vectors[i + 2] * length;
}
let newBoundingSphere;
const bs = geometry.boundingSphere;
if (defined(bs)) {
newBoundingSphere = new BoundingSphere(bs.center, bs.radius + length);
}
return new Geometry({
attributes: {
position: new GeometryAttribute({
componentDatatype: ComponentDatatype.DOUBLE,
componentsPerAttribute: 3,
values: newPositions,
}),
},
primitiveType: PrimitiveType.LINES,
boundingSphere: newBoundingSphere,
});
};
/**
* Creates an object that maps attribute names to unique locations (indices)
* for matching vertex attributes and shader programs.
*
* @param {Geometry} geometry The geometry, which is not modified, to create the object for.
* @returns {object} An object with attribute name / index pairs.
*
* @example
* const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry);
* // Example output
* // {
* // 'position' : 0,
* // 'normal' : 1
* // }
*/
GeometryPipeline.createAttributeLocations = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
//>>includeEnd('debug');
// There can be a WebGL performance hit when attribute 0 is disabled, so
// assign attribute locations to well-known attributes.
const semantics = [
"position",
"positionHigh",
"positionLow",
// From VertexFormat.position - after 2D projection and high-precision encoding
"position3DHigh",
"position3DLow",
"position2DHigh",
"position2DLow",
// From Primitive
"pickColor",
// From VertexFormat
"normal",
"st",
"tangent",
"bitangent",
// For shadow volumes
"extrudeDirection",
// From compressing texture coordinates and normals
"compressedAttributes",
];
const attributes = geometry.attributes;
const indices = {};
let j = 0;
let i;
const len = semantics.length;
// Attribute locations for well-known attributes
for (i = 0; i < len; ++i) {
const semantic = semantics[i];
if (defined(attributes[semantic])) {
indices[semantic] = j++;
}
}
// Locations for custom attributes
for (const name in attributes) {
if (attributes.hasOwnProperty(name) && !defined(indices[name])) {
indices[name] = j++;
}
}
return indices;
};
/**
* Reorders a geometry's attributes and <code>indices</code> to achieve better performance from the GPU's pre-vertex-shader cache.
*
* @param {Geometry} geometry The geometry to modify.
* @returns {Geometry} The modified <code>geometry</code> argument, with its attributes and indices reordered for the GPU's pre-vertex-shader cache.
*
* @exception {DeveloperError} Each attribute array in geometry.attributes must have the same number of attributes.
*
*
* @example
* geometry = Cesium.GeometryPipeline.reorderForPreVertexCache(geometry);
*
* @see GeometryPipeline.reorderForPostVertexCache
*/
GeometryPipeline.reorderForPreVertexCache = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
//>>includeEnd('debug');
const numVertices = Geometry.computeNumberOfVertices(geometry);
const indices = geometry.indices;
if (defined(indices)) {
const indexCrossReferenceOldToNew = new Int32Array(numVertices);
for (let i = 0; i < numVertices; i++) {
indexCrossReferenceOldToNew[i] = -1;
}
// Construct cross reference and reorder indices
const indicesIn = indices;
const numIndices = indicesIn.length;
const indicesOut = IndexDatatype.createTypedArray(numVertices, numIndices);
let intoIndicesIn = 0;
let intoIndicesOut = 0;
let nextIndex = 0;
let tempIndex;
while (intoIndicesIn < numIndices) {
tempIndex = indexCrossReferenceOldToNew[indicesIn[intoIndicesIn]];
if (tempIndex !== -1) {
indicesOut[intoIndicesOut] = tempIndex;
} else {
tempIndex = indicesIn[intoIndicesIn];
indexCrossReferenceOldToNew[tempIndex] = nextIndex;
indicesOut[intoIndicesOut] = nextIndex;
++nextIndex;
}
++intoIndicesIn;
++intoIndicesOut;
}
geometry.indices = indicesOut;
// Reorder attributes
const attributes = geometry.attributes;
for (const property in attributes) {
if (
attributes.hasOwnProperty(property) &&
defined(attributes[property]) &&
defined(attributes[property].values)
) {
const attribute = attributes[property];
const elementsIn = attribute.values;
let intoElementsIn = 0;
const numComponents = attribute.componentsPerAttribute;
const elementsOut = ComponentDatatype.createTypedArray(
attribute.componentDatatype,
nextIndex * numComponents,
);
while (intoElementsIn < numVertices) {
const temp = indexCrossReferenceOldToNew[intoElementsIn];
if (temp !== -1) {
for (let j = 0; j < numComponents; j++) {
elementsOut[numComponents * temp + j] =
elementsIn[numComponents * intoElementsIn + j];
}
}
++intoElementsIn;
}
attribute.values = elementsOut;
}
}
}
return geometry;
};
/**
* Reorders a geometry's <code>indices</code> to achieve better performance from the GPU's
* post vertex-shader cache by using the Tipsify algorithm. If the geometry <code>primitiveType</code>
* is not <code>TRIANGLES</code> or the geometry does not have an <code>indices</code>, this function has no effect.
*
* @param {Geometry} geometry The geometry to modify.
* @param {number} [cacheCapacity=24] The number of vertices that can be held in the GPU's vertex cache.
* @returns {Geometry} The modified <code>geometry</code> argument, with its indices reordered for the post-vertex-shader cache.
*
* @exception {DeveloperError} cacheCapacity must be greater than two.
*
*
* @example
* geometry = Cesium.GeometryPipeline.reorderForPostVertexCache(geometry);
*
* @see GeometryPipeline.reorderForPreVertexCache
* @see {@link http://gfx.cs.princ0eton.edu/pubs/Sander_2007_%3ETR/tipsy.pdf|Fast Triangle Reordering for Vertex Locality and Reduced Overdraw}
* by Sander, Nehab, and Barczak
*/
GeometryPipeline.reorderForPostVertexCache = function (
geometry,
cacheCapacity,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
//>>includeEnd('debug');
const indices = geometry.indices;
if (geometry.primitiveType === PrimitiveType.TRIANGLES && defined(indices)) {
const numIndices = indices.length;
let maximumIndex = 0;
for (let j = 0; j < numIndices; j++) {
if (indices[j] > maximumIndex) {
maximumIndex = indices[j];
}
}
geometry.indices = Tipsify.tipsify({
indices: indices,
maximumIndex: maximumIndex,
cacheSize: cacheCapacity,
});
}
return geometry;
};
function copyAttributesDescriptions(attributes) {
const newAttributes = {};
for (const attribute in attributes) {
if (
attributes.hasOwnProperty(attribute) &&
defined(attributes[attribute]) &&
defined(attributes[attribute].values)
) {
const attr = attributes[attribute];
newAttributes[attribute] = new GeometryAttribute({
componentDatatype: attr.componentDatatype,
componentsPerAttribute: attr.componentsPerAttribute,
normalize: attr.normalize,
values: [],
});
}
}
return newAttributes;
}
function copyVertex(destinationAttributes, sourceAttributes, index) {
for (const attribute in sourceAttributes) {
if (
sourceAttributes.hasOwnProperty(attribute) &&
defined(sourceAttributes[attribute]) &&
defined(sourceAttributes[attribute].values)
) {
const attr = sourceAttributes[attribute];
for (let k = 0; k < attr.componentsPerAttribute; ++k) {
destinationAttributes[attribute].values.push(
attr.values[index * attr.componentsPerAttribute + k],
);
}
}
}
}
/**
* Splits a geometry into multiple geometries, if necessary, to ensure that indices in the
* <code>indices</code> fit into unsigned shorts. This is used to meet the WebGL requirements
* when unsigned int indices are not supported.
* <p>
* If the geometry does not have any <code>indices</code>, this function has no effect.
* </p>
*
* @param {Geometry} geometry The geometry to be split into multiple geometries.
* @returns {Geometry[]} An array of geometries, each with indices that fit into unsigned shorts.
*
* @exception {DeveloperError} geometry.primitiveType must equal to PrimitiveType.TRIANGLES, PrimitiveType.LINES, or PrimitiveType.POINTS
* @exception {DeveloperError} All geometry attribute lists must have the same number of attributes.
*
* @example
* const geometries = Cesium.GeometryPipeline.fitToUnsignedShortIndices(geometry);
*/
GeometryPipeline.fitToUnsignedShortIndices = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
if (
defined(geometry.indices) &&
geometry.primitiveType !== PrimitiveType.TRIANGLES &&
geometry.primitiveType !== PrimitiveType.LINES &&
geometry.primitiveType !== PrimitiveType.POINTS
) {
throw new DeveloperError(
"geometry.primitiveType must equal to PrimitiveType.TRIANGLES, PrimitiveType.LINES, or PrimitiveType.POINTS.",
);
}
//>>includeEnd('debug');
const geometries = [];
// If there's an index list and more than 64K attributes, it is possible that
// some indices are outside the range of unsigned short [0, 64K - 1]
const numberOfVertices = Geometry.computeNumberOfVertices(geometry);
if (
defined(geometry.indices) &&
numberOfVertices >= CesiumMath.SIXTY_FOUR_KILOBYTES
) {
let oldToNewIndex = [];
let newIndices = [];
let currentIndex = 0;
let newAttributes = copyAttributesDescriptions(geometry.attributes);
const originalIndices = geometry.indices;
const numberOfIndices = originalIndices.length;
let indicesPerPrimitive;
if (geometry.primitiveType === PrimitiveType.TRIANGLES) {
indicesPerPrimitive = 3;
} else if (geometry.primitiveType === PrimitiveType.LINES) {
indicesPerPrimitive = 2;
} else if (geometry.primitiveType === PrimitiveType.POINTS) {
indicesPerPrimitive = 1;
}
for (let j = 0; j < numberOfIndices; j += indicesPerPrimitive) {
for (let k = 0; k < indicesPerPrimitive; ++k) {
const x = originalIndices[j + k];
let i = oldToNewIndex[x];
if (!defined(i)) {
i = currentIndex++;
oldToNewIndex[x] = i;
copyVertex(newAttributes, geometry.attributes, x);
}
newIndices.push(i);
}
if (
currentIndex + indicesPerPrimitive >=
CesiumMath.SIXTY_FOUR_KILOBYTES
) {
geometries.push(
new Geometry({
attributes: newAttributes,
indices: newIndices,
primitiveType: geometry.primitiveType,
boundingSphere: geometry.boundingSphere,
boundingSphereCV: geometry.boundingSphereCV,
}),
);
// Reset for next vertex-array
oldToNewIndex = [];
newIndices = [];
currentIndex = 0;
newAttributes = copyAttributesDescriptions(geometry.attributes);
}
}
if (newIndices.length !== 0) {
geometries.push(
new Geometry({
attributes: newAttributes,
indices: newIndices,
primitiveType: geometry.primitiveType,
boundingSphere: geometry.boundingSphere,
boundingSphereCV: geometry.boundingSphereCV,
}),
);
}
} else {
// No need to split into multiple geometries
geometries.push(geometry);
}
return geometries;
};
const scratchProjectTo2DCartesian3 = new Cartesian3();
const scratchProjectTo2DCartographic = new Cartographic();
/**
* Projects a geometry's 3D <code>position</code> attribute to 2D, replacing the <code>position</code>
* attribute with separate <code>position3D</code> and <code>position2D</code> attributes.
* <p>
* If the geometry does not have a <code>position</code>, this function has no effect.
* </p>
*
* @param {Geometry} geometry The geometry to modify.
* @param {string} attributeName The name of the attribute.
* @param {string} attributeName3D The name of the attribute in 3D.
* @param {string} attributeName2D The name of the attribute in 2D.
* @param {object} [projection=new GeographicProjection()] The projection to use.
* @returns {Geometry} The modified <code>geometry</code> argument with <code>position3D</code> and <code>position2D</code> attributes.
*
* @exception {DeveloperError} geometry must have attribute matching the attributeName argument.
* @exception {DeveloperError} The attribute componentDatatype must be ComponentDatatype.DOUBLE.
* @exception {DeveloperError} Could not project a point to 2D.
*
* @example
* geometry = Cesium.GeometryPipeline.projectTo2D(geometry, 'position', 'position3D', 'position2D');
*/
GeometryPipeline.projectTo2D = function (
geometry,
attributeName,
attributeName3D,
attributeName2D,
projection,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
if (!defined(attributeName)) {
throw new DeveloperError("attributeName is required.");
}
if (!defined(attributeName3D)) {
throw new DeveloperError("attributeName3D is required.");
}
if (!defined(attributeName2D)) {
throw new DeveloperError("attributeName2D is required.");
}
if (!defined(geometry.attributes[attributeName])) {
throw new DeveloperError(
`geometry must have attribute matching the attributeName argument: ${attributeName}.`,
);
}
if (
geometry.attributes[attributeName].componentDatatype !==
ComponentDatatype.DOUBLE
) {
throw new DeveloperError(
"The attribute componentDatatype must be ComponentDatatype.DOUBLE.",
);
}
//>>includeEnd('debug');
const attribute = geometry.attributes[attributeName];
projection = defined(projection) ? projection : new GeographicProjection();
const ellipsoid = projection.ellipsoid;
// Project original values to 2D.
const values3D = attribute.values;
const projectedValues = new Float64Array(values3D.length);
let index = 0;
for (let i = 0; i < values3D.length; i += 3) {
const value = Cartesian3.fromArray(
values3D,
i,
scratchProjectTo2DCartesian3,
);
const lonLat = ellipsoid.cartesianToCartographic(
value,
scratchProjectTo2DCartographic,
);
//>>includeStart('debug', pragmas.debug);
if (!defined(lonLat)) {
throw new DeveloperError(
`Could not project point (${value.x}, ${value.y}, ${value.z}) to 2D.`,
);
}
//>>includeEnd('debug');
const projectedLonLat = projection.project(
lonLat,
scratchProjectTo2DCartesian3,
);
projectedValues[index++] = projectedLonLat.x;
projectedValues[index++] = projectedLonLat.y;
projectedValues[index++] = projectedLonLat.z;
}
// Rename original cartesians to ellipsoid cartesians.
geometry.attributes[attributeName3D] = attribute;
// Replace original cartesians with 2D projected cartesians
geometry.attributes[attributeName2D] = new GeometryAttribute({
componentDatatype: ComponentDatatype.DOUBLE,
componentsPerAttribute: 3,
values: projectedValues,
});
delete geometry.attributes[attributeName];
return geometry;
};
const encodedResult = {
high: 0.0,
low: 0.0,
};
/**
* Encodes floating-point geometry attribute values as two separate attributes to improve
* rendering precision.
* <p>
* This is commonly used to create high-precision position vertex attributes.
* </p>
*
* @param {Geometry} geometry The geometry to modify.
* @param {string} attributeName The name of the attribute.
* @param {string} attributeHighName The name of the attribute for the encoded high bits.
* @param {string} attributeLowName The name of the attribute for the encoded low bits.
* @returns {Geometry} The modified <code>geometry</code> argument, with its encoded attribute.
*
* @exception {DeveloperError} geometry must have attribute matching the attributeName argument.
* @exception {DeveloperError} The attribute componentDatatype must be ComponentDatatype.DOUBLE.
*
* @example
* geometry = Cesium.GeometryPipeline.encodeAttribute(geometry, 'position3D', 'position3DHigh', 'position3DLow');
*/
GeometryPipeline.encodeAttribute = function (
geometry,
attributeName,
attributeHighName,
attributeLowName,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
if (!defined(attributeName)) {
throw new DeveloperError("attributeName is required.");
}
if (!defined(attributeHighName)) {
throw new DeveloperError("attributeHighName is required.");
}
if (!defined(attributeLowName)) {
throw new DeveloperError("attributeLowName is required.");
}
if (!defined(geometry.attributes[attributeName])) {
throw new DeveloperError(
`geometry must have attribute matching the attributeName argument: ${attributeName}.`,
);
}
if (
geometry.attributes[attributeName].componentDatatype !==
ComponentDatatype.DOUBLE
) {
throw new DeveloperError(
"The attribute componentDatatype must be ComponentDatatype.DOUBLE.",
);
}
//>>includeEnd('debug');
const attribute = geometry.attributes[attributeName];
const values = attribute.values;
const length = values.length;
const highValues = new Float32Array(length);
const lowValues = new Float32Array(length);
for (let i = 0; i < length; ++i) {
EncodedCartesian3.encode(values[i], encodedResult);
highValues[i] = encodedResult.high;
lowValues[i] = encodedResult.low;
}
const componentsPerAttribute = attribute.componentsPerAttribute;
geometry.attributes[attributeHighName] = new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: componentsPerAttribute,
values: highValues,
});
geometry.attributes[attributeLowName] = new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: componentsPerAttribute,
values: lowValues,
});
delete geometry.attributes[attributeName];
return geometry;
};
let scratchCartesian3 = new Cartesian3();
function transformPoint(matrix, attribute) {
if (defined(attribute)) {
const values = attribute.values;
const length = values.length;
for (let i = 0; i < length; i += 3) {
Cartesian3.unpack(values, i, scratchCartesian3);
Matrix4.multiplyByPoint(matrix, scratchCartesian3, scratchCartesian3);
Cartesian3.pack(scratchCartesian3, values, i);
}
}
}
function transformVector(matrix, attribute) {
if (defined(attribute)) {
const values = attribute.values;
const length = values.length;
for (let i = 0; i < length; i += 3) {
Cartesian3.unpack(values, i, scratchCartesian3);
Matrix3.multiplyByVector(matrix, scratchCartesian3, scratchCartesian3);
scratchCartesian3 = Cartesian3.normalize(
scratchCartesian3,
scratchCartesian3,
);
Cartesian3.pack(scratchCartesian3, values, i);
}
}
}
const inverseTranspose = new Matrix4();
const normalMatrix = new Matrix3();
/**
* Transforms a geometry instance to world coordinates. This changes
* the instance's <code>modelMatrix</code> to {@link Matrix4.IDENTITY} and transforms the
* following attributes if they are present: <code>position</code>, <code>normal</code>,
* <code>tangent</code>, and <code>bitangent</code>.
*
* @param {GeometryInstance} instance The geometry instance to modify.
* @returns {GeometryInstance} The modified <code>instance</code> argument, with its attributes transforms to world coordinates.
*
* @example
* Cesium.GeometryPipeline.transformToWorldCoordinates(instance);
*/
GeometryPipeline.transformToWorldCoordinates = function (instance) {
//>>includeStart('debug', pragmas.debug);
if (!defined(instance)) {
throw new DeveloperError("instance is required.");
}
//>>includeEnd('debug');
const modelMatrix = instance.modelMatrix;
if (Matrix4.equals(modelMatrix, Matrix4.IDENTITY)) {
// Already in world coordinates
return instance;
}
const attributes = instance.geometry.attributes;
// Transform attributes in known vertex formats
transformPoint(modelMatrix, attributes.position);
transformPoint(modelMatrix, attributes.prevPosition);
transformPoint(modelMatrix, attributes.nextPosition);
if (
defined(attributes.normal) ||
defined(attributes.tangent) ||
defined(attributes.bitangent)
) {
Matrix4.inverse(modelMatrix, inverseTranspose);
Matrix4.transpose(inverseTranspose, inverseTranspose);
Matrix4.getMatrix3(inverseTranspose, normalMatrix);
transformVector(normalMatrix, attributes.normal);
transformVector(normalMatrix, attributes.tangent);
transformVector(normalMatrix, attributes.bitangent);
}
const boundingSphere = instance.geometry.boundingSphere;
if (defined(boundingSphere)) {
instance.geometry.boundingSphere = BoundingSphere.transform(
boundingSphere,
modelMatrix,
boundingSphere,
);
}
instance.modelMatrix = Matrix4.clone(Matrix4.IDENTITY);
return instance;
};
function findAttributesInAllGeometries(instances, propertyName) {
const length = instances.length;
const attributesInAllGeometries = {};
const attributes0 = instances[0][propertyName].attributes;
let name;
for (name in attributes0) {
if (
attributes0.hasOwnProperty(name) &&
defined(attributes0[name]) &&
defined(attributes0[name].values)
) {
const attribute = attributes0[name];
let numberOfComponents = attribute.values.length;
let inAllGeometries = true;
// Does this same attribute exist in all geometries?
for (let i = 1; i < length; ++i) {
const otherAttribute = instances[i][propertyName].attributes[name];
if (
!defined(otherAttribute) ||
attribute.componentDatatype !== otherAttribute.componentDatatype ||
attribute.componentsPerAttribute !==
otherAttribute.componentsPerAttribute ||
attribute.normalize !== otherAttribute.normalize
) {
inAllGeometries = false;
break;
}
numberOfComponents += otherAttribute.values.length;
}
if (inAllGeometries) {
attributesInAllGeometries[name] = new GeometryAttribute({
componentDatatype: attribute.componentDatatype,
componentsPerAttribute: attribute.componentsPerAttribute,
normalize: attribute.normalize,
values: ComponentDatatype.createTypedArray(
attribute.componentDatatype,
numberOfComponents,
),
});
}
}
}
return attributesInAllGeometries;
}
const tempScratch = new Cartesian3();
function combineGeometries(instances, propertyName) {
const length = instances.length;
let name;
let i;
let j;
let k;
const m = instances[0].modelMatrix;
const haveIndices = defined(instances[0][propertyName].indices);
const primitiveType = instances[0][propertyName].primitiveType;
//>>includeStart('debug', pragmas.debug);
for (i = 1; i < length; ++i) {
if (!Matrix4.equals(instances[i].modelMatrix, m)) {
throw new DeveloperError("All instances must have the same modelMatrix.");
}
if (defined(instances[i][propertyName].indices) !== haveIndices) {
throw new DeveloperError(
"All instance geometries must have an indices or not have one.",
);
}
if (instances[i][propertyName].primitiveType !== primitiveType) {
throw new DeveloperError(
"All instance geometries must have the same primitiveType.",
);
}
}
//>>includeEnd('debug');
// Find subset of attributes in all geometries
const attributes = findAttributesInAllGeometries(instances, propertyName);
let values;
let sourceValues;
let sourceValuesLength;
// Combine attributes from each geometry into a single typed array
for (name in attributes) {
if (attributes.hasOwnProperty(name)) {
values = attributes[name].values;
k = 0;
for (i = 0; i < length; ++i) {
sourceValues = instances[i][propertyName].attributes[name].values;
sourceValuesLength = sourceValues.length;
for (j = 0; j < sourceValuesLength; ++j) {
values[k++] = sourceValues[j];
}
}
}
}
// Combine index lists
let indices;
if (haveIndices) {
let numberOfIndices = 0;
for (i = 0; i < length; ++i) {
numberOfIndices += instances[i][propertyName].indices.length;
}
const numberOfVertices = Geometry.computeNumberOfVertices(
new Geometry({
attributes: attributes,
primitiveType: PrimitiveType.POINTS,
}),
);
const destIndices = IndexDatatype.createTypedArray(
numberOfVertices,
numberOfIndices,
);
let destOffset = 0;
let offset = 0;
for (i = 0; i < length; ++i) {
const sourceIndices = instances[i][propertyName].indices;
const sourceIndicesLen = sourceIndices.length;
for (k = 0; k < sourceIndicesLen; ++k) {
destIndices[destOffset++] = offset + sourceIndices[k];
}
offset += Geometry.computeNumberOfVertices(instances[i][propertyName]);
}
indices = destIndices;
}
// Create bounding sphere that includes all instances
let center = new Cartesian3();
let radius = 0.0;
let bs;
for (i = 0; i < length; ++i) {
bs = instances[i][propertyName].boundingSphere;
if (!defined(bs)) {
// If any geometries have an undefined bounding sphere, then so does the combined geometry
center = undefined;
break;
}
Cartesian3.add(bs.center, center, center);
}
if (defined(center)) {
Cartesian3.divideByScalar(center, length, center);
for (i = 0; i < length; ++i) {
bs = instances[i][propertyName].boundingSphere;
const tempRadius =
Cartesian3.magnitude(
Cartesian3.subtract(bs.center, center, tempScratch),
) + bs.radius;
if (tempRadius > radius) {
radius = tempRadius;
}
}
}
return new Geometry({
attributes: attributes,
indices: indices,
primitiveType: primitiveType,
boundingSphere: defined(center)
? new BoundingSphere(center, radius)
: undefined,
});
}
/**
* Combines geometry from several {@link GeometryInstance} objects into one geometry.
* This concatenates the attributes, concatenates and adjusts the indices, and creates
* a bounding sphere encompassing all instances.
* <p>
* If the instances do not have the same attributes, a subset of attributes common
* to all instances is used, and the others are ignored.
* </p>
* <p>
* This is used by {@link Primitive} to efficiently render a large amount of static data.
* </p>
*
* @private
*
* @param {GeometryInstance[]} [instances] The array of {@link GeometryInstance} objects whose geometry will be combined.
* @returns {Geometry} A single geometry created from the provided geometry instances.
*
* @exception {DeveloperError} All instances must have the same modelMatrix.
* @exception {DeveloperError} All instance geometries must have an indices or not have one.
* @exception {DeveloperError} All instance geometries must have the same primitiveType.
*
*
* @example
* for (let i = 0; i < instances.length; ++i) {
* Cesium.GeometryPipeline.transformToWorldCoordinates(instances[i]);
* }
* const geometries = Cesium.GeometryPipeline.combineInstances(instances);
*
* @see GeometryPipeline.transformToWorldCoordinates
*/
GeometryPipeline.combineInstances = function (instances) {
//>>includeStart('debug', pragmas.debug);
if (!defined(instances) || instances.length < 1) {
throw new DeveloperError(
"instances is required and must have length greater than zero.",
);
}
//>>includeEnd('debug');
const instanceGeometry = [];
const instanceSplitGeometry = [];
const length = instances.length;
for (let i = 0; i < length; ++i) {
const instance = instances[i];
if (defined(instance.geometry)) {
instanceGeometry.push(instance);
} else if (
defined(instance.westHemisphereGeometry) &&
defined(instance.eastHemisphereGeometry)
) {
instanceSplitGeometry.push(instance);
}
}
const geometries = [];
if (instanceGeometry.length > 0) {
geometries.push(combineGeometries(instanceGeometry, "geometry"));
}
if (instanceSplitGeometry.length > 0) {
geometries.push(
combineGeometries(instanceSplitGeometry, "westHemisphereGeometry"),
);
geometries.push(
combineGeometries(instanceSplitGeometry, "eastHemisphereGeometry"),
);
}
return geometries;
};
const normal = new Cartesian3();
const v0 = new Cartesian3();
const v1 = new Cartesian3();
const v2 = new Cartesian3();
/**
* Computes per-vertex normals for a geometry containing <code>TRIANGLES</code> by averaging the normals of
* all triangles incident to the vertex. The result is a new <code>normal</code> attribute added to the geometry.
* This assumes a counter-clockwise winding order.
*
* @param {Geometry} geometry The geometry to modify.
* @returns {Geometry} The modified <code>geometry</code> argument with the computed <code>normal</code> attribute.
*
* @exception {DeveloperError} geometry.indices length must be greater than 0 and be a multiple of 3.
* @exception {DeveloperError} geometry.primitiveType must be {@link PrimitiveType.TRIANGLES}.
*
* @example
* Cesium.GeometryPipeline.computeNormal(geometry);
*/
GeometryPipeline.computeNormal = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
if (
!defined(geometry.attributes.position) ||
!defined(geometry.attributes.position.values)
) {
throw new DeveloperError(
"geometry.attributes.position.values is required.",
);
}
if (!defined(geometry.indices)) {
throw new DeveloperError("geometry.indices is required.");
}
if (geometry.indices.length < 2 || geometry.indices.length % 3 !== 0) {
throw new DeveloperError(
"geometry.indices length must be greater than 0 and be a multiple of 3.",
);
}
if (geometry.primitiveType !== PrimitiveType.TRIANGLES) {
throw new DeveloperError(
"geometry.primitiveType must be PrimitiveType.TRIANGLES.",
);
}
//>>includeEnd('debug');
const indices = geometry.indices;
const attributes = geometry.attributes;
const vertices = attributes.position.values;
const numVertices = attributes.position.values.length / 3;
const numIndices = indices.length;
const normalsPerVertex = new Array(numVertices);
const normalsPerTriangle = new Array(numIndices / 3);
const normalIndices = new Array(numIndices);
let i;
for (i = 0; i < numVertices; i++) {
normalsPerVertex[i] = {
indexOffset: 0,
count: 0,
currentCount: 0,
};
}
let j = 0;
for (i = 0; i < numIndices; i += 3) {
const i0 = indices[i];
const i1 = indices[i + 1];
const i2 = indices[i + 2];
const i03 = i0 * 3;
const i13 = i1 * 3;
const i23 = i2 * 3;
v0.x = vertices[i03];
v0.y = vertices[i03 + 1];
v0.z = vertices[i03 + 2];
v1.x = vertices[i13];
v1.y = vertices[i13 + 1];
v1.z = vertices[i13 + 2];
v2.x = vertices[i23];
v2.y = vertices[i23 + 1];
v2.z = vertices[i23 + 2];
normalsPerVertex[i0].count++;
normalsPerVertex[i1].count++;
normalsPerVertex[i2].count++;
Cartesian3.subtract(v1, v0, v1);
Cartesian3.subtract(v2, v0, v2);
normalsPerTriangle[j] = Cartesian3.cross(v1, v2, new Cartesian3());
j++;
}
let indexOffset = 0;
for (i = 0; i < numVertices; i++) {
normalsPerVertex[i].indexOffset += indexOffset;
indexOffset += normalsPerVertex[i].count;
}
j = 0;
let vertexNormalData;
for (i = 0; i < numIndices; i += 3) {
vertexNormalData = normalsPerVertex[indices[i]];
let index = vertexNormalData.indexOffset + vertexNormalData.currentCount;
normalIndices[index] = j;
vertexNormalData.currentCount++;
vertexNormalData = normalsPerVertex[indices[i + 1]];
index = vertexNormalData.indexOffset + vertexNormalData.currentCount;
normalIndices[index] = j;
vertexNormalData.currentCount++;
vertexNormalData = normalsPerVertex[indices[i + 2]];
index = vertexNormalData.indexOffset + vertexNormalData.currentCount;
normalIndices[index] = j;
vertexNormalData.currentCount++;
j++;
}
const normalValues = new Float32Array(numVertices * 3);
for (i = 0; i < numVertices; i++) {
const i3 = i * 3;
vertexNormalData = normalsPerVertex[i];
Cartesian3.clone(Cartesian3.ZERO, normal);
if (vertexNormalData.count > 0) {
for (j = 0; j < vertexNormalData.count; j++) {
Cartesian3.add(
normal,
normalsPerTriangle[normalIndices[vertexNormalData.indexOffset + j]],
normal,
);
}
// We can run into an issue where a vertex is used with 2 primitives that have opposite winding order.
if (
Cartesian3.equalsEpsilon(Cartesian3.ZERO, normal, CesiumMath.EPSILON10)
) {
Cartesian3.clone(
normalsPerTriangle[normalIndices[vertexNormalData.indexOffset]],
normal,
);
}
}
// We end up with a zero vector probably because of a degenerate triangle
if (
Cartesian3.equalsEpsilon(Cartesian3.ZERO, normal, CesiumMath.EPSILON10)
) {
// Default to (0,0,1)
normal.z = 1.0;
}
Cartesian3.normalize(normal, normal);
normalValues[i3] = normal.x;
normalValues[i3 + 1] = normal.y;
normalValues[i3 + 2] = normal.z;
}
geometry.attributes.normal = new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: 3,
values: normalValues,
});
return geometry;
};
const normalScratch = new Cartesian3();
const normalScale = new Cartesian3();
const tScratch = new Cartesian3();
/**
* Computes per-vertex tangents and bitangents for a geometry containing <code>TRIANGLES</code>.
* The result is new <code>tangent</code> and <code>bitangent</code> attributes added to the geometry.
* This assumes a counter-clockwise winding order.
* <p>
* Based on <a href="http://www.terathon.com/code/tangent.html">Computing Tangent Space Basis Vectors
* for an Arbitrary Mesh</a> by Eric Lengyel.
* </p>
*
* @param {Geometry} geometry The geometry to modify.
* @returns {Geometry} The modified <code>geometry</code> argument with the computed <code>tangent</code> and <code>bitangent</code> attributes.
*
* @exception {DeveloperError} geometry.indices length must be greater than 0 and be a multiple of 3.
* @exception {DeveloperError} geometry.primitiveType must be {@link PrimitiveType.TRIANGLES}.
*
* @example
* Cesium.GeometryPipeline.computeTangentAndBiTangent(geometry);
*/
GeometryPipeline.computeTangentAndBitangent = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
//>>includeEnd('debug');
const attributes = geometry.attributes;
const indices = geometry.indices;
//>>includeStart('debug', pragmas.debug);
if (!defined(attributes.position) || !defined(attributes.position.values)) {
throw new DeveloperError(
"geometry.attributes.position.values is required.",
);
}
if (!defined(attributes.normal) || !defined(attributes.normal.values)) {
throw new DeveloperError("geometry.attributes.normal.values is required.");
}
if (!defined(attributes.st) || !defined(attributes.st.values)) {
throw new DeveloperError("geometry.attributes.st.values is required.");
}
if (!defined(indices)) {
throw new DeveloperError("geometry.indices is required.");
}
if (indices.length < 2 || indices.length % 3 !== 0) {
throw new DeveloperError(
"geometry.indices length must be greater than 0 and be a multiple of 3.",
);
}
if (geometry.primitiveType !== PrimitiveType.TRIANGLES) {
throw new DeveloperError(
"geometry.primitiveType must be PrimitiveType.TRIANGLES.",
);
}
//>>includeEnd('debug');
const vertices = geometry.attributes.position.values;
const normals = geometry.attributes.normal.values;
const st = geometry.attributes.st.values;
const numVertices = geometry.attributes.position.values.length / 3;
const numIndices = indices.length;
const tan1 = new Array(numVertices * 3);
let i;
for (i = 0; i < tan1.length; i++) {
tan1[i] = 0;
}
let i03;
let i13;
let i23;
for (i = 0; i < numIndices; i += 3) {
const i0 = indices[i];
const i1 = indices[i + 1];
const i2 = indices[i + 2];
i03 = i0 * 3;
i13 = i1 * 3;
i23 = i2 * 3;
const i02 = i0 * 2;
const i12 = i1 * 2;
const i22 = i2 * 2;
const ux = vertices[i03];
const uy = vertices[i03 + 1];
const uz = vertices[i03 + 2];
const wx = st[i02];
const wy = st[i02 + 1];
const t1 = st[i12 + 1] - wy;
const t2 = st[i22 + 1] - wy;
const r = 1.0 / ((st[i12] - wx) * t2 - (st[i22] - wx) * t1);
const sdirx = (t2 * (vertices[i13] - ux) - t1 * (vertices[i23] - ux)) * r;
const sdiry =
(t2 * (vertices[i13 + 1] - uy) - t1 * (vertices[i23 + 1] - uy)) * r;
const sdirz =
(t2 * (vertices[i13 + 2] - uz) - t1 * (vertices[i23 + 2] - uz)) * r;
tan1[i03] += sdirx;
tan1[i03 + 1] += sdiry;
tan1[i03 + 2] += sdirz;
tan1[i13] += sdirx;
tan1[i13 + 1] += sdiry;
tan1[i13 + 2] += sdirz;
tan1[i23] += sdirx;
tan1[i23 + 1] += sdiry;
tan1[i23 + 2] += sdirz;
}
const tangentValues = new Float32Array(numVertices * 3);
const bitangentValues = new Float32Array(numVertices * 3);
for (i = 0; i < numVertices; i++) {
i03 = i * 3;
i13 = i03 + 1;
i23 = i03 + 2;
const n = Cartesian3.fromArray(normals, i03, normalScratch);
const t = Cartesian3.fromArray(tan1, i03, tScratch);
const scalar = Cartesian3.dot(n, t);
Cartesian3.multiplyByScalar(n, scalar, normalScale);
Cartesian3.normalize(Cartesian3.subtract(t, normalScale, t), t);
tangentValues[i03] = t.x;
tangentValues[i13] = t.y;
tangentValues[i23] = t.z;
Cartesian3.normalize(Cartesian3.cross(n, t, t), t);
bitangentValues[i03] = t.x;
bitangentValues[i13] = t.y;
bitangentValues[i23] = t.z;
}
geometry.attributes.tangent = new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: 3,
values: tangentValues,
});
geometry.attributes.bitangent = new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: 3,
values: bitangentValues,
});
return geometry;
};
const scratchCartesian2 = new Cartesian2();
const toEncode1 = new Cartesian3();
const toEncode2 = new Cartesian3();
const toEncode3 = new Cartesian3();
let encodeResult2 = new Cartesian2();
/**
* Compresses and packs geometry normal attribute values to save memory.
*
* @param {Geometry} geometry The geometry to modify.
* @returns {Geometry} The modified <code>geometry</code> argument, with its normals compressed and packed.
*
* @example
* geometry = Cesium.GeometryPipeline.compressVertices(geometry);
*/
GeometryPipeline.compressVertices = function (geometry) {
//>>includeStart('debug', pragmas.debug);
if (!defined(geometry)) {
throw new DeveloperError("geometry is required.");
}
//>>includeEnd('debug');
const extrudeAttribute = geometry.attributes.extrudeDirection;
let i;
let numVertices;
if (defined(extrudeAttribute)) {
//only shadow volumes use extrudeDirection, and shadow volumes use vertexFormat: POSITION_ONLY so we don't need to check other attributes
const extrudeDirections = extrudeAttribute.values;
numVertices = extrudeDirections.length / 3.0;
const compressedDirections = new Float32Array(numVertices * 2);
let i2 = 0;
for (i = 0; i < numVertices; ++i) {
Cartesian3.fromArray(extrudeDirections, i * 3.0, toEncode1);
if (Cartesian3.equals(toEncode1, Cartesian3.ZERO)) {
i2 += 2;
continue;
}
encodeResult2 = AttributeCompression.octEncodeInRange(
toEncode1,
65535,
encodeResult2,
);
compressedDirections[i2++] = encodeResult2.x;
compressedDirections[i2++] = encodeResult2.y;
}
geometry.attributes.compressedAttributes = new GeometryAttribute({
componentDatatype: ComponentDatatype.FLOAT,
componentsPerAttribute: 2,
values: compressedDirections,
});
delete geometry.attributes.extrudeDirection;
return geometry;
}
const normalAttribute = geometry.attributes.normal;
const stAttribute = geometry.attributes.st;
const hasNormal = defined(normalAttribute);
const hasSt = defined(stAttribute);
if (!hasNormal && !hasSt) {
return geometry;
}
const tangentAttribute = geometry.attributes.tangent;
const bitangentAttribute = geometry.attributes.bitangent;
const hasTangent = defined(tangentAttribute);
const hasBitangent = defined(bitangentAttribute);
let normals;
let st;
let tangents;
let bitangents;
if (hasNormal) {
normals = normalAttribute.values;
}
if (hasSt) {
st = stAttribute.values;
}
if (hasTangent) {
tangents = tangentAttribute.values;
}
if (hasBitangent) {
bitangents = bitangentAttribute.values;
}
const length = hasNormal ? normals.length : st.length;
const numComponents = hasNormal ? 3.0 : 2.0;
numVertices = length / numComponents;
let compressedLength = numVertices;
let numCompressedComponents = hasSt && hasNormal ? 2.0 : 1.0;
numCompressedComponents += hasTangent || hasBitangent ? 1.0 : 0.0;
compressedLength *= numCompressedComponents;
const compressedAttributes = new Float32Array(compressedLength);
let normalIndex = 0;
for (i = 0; i < numVertices; ++i) {
if (hasSt) {
Cartesian2.fromArray(st, i * 2.0, scratchCartesian2);
compressedAttributes[normalIndex++] =
AttributeCompression.compressTextureCoordinates(scratchCartesian2);
}
const index = i * 3.0;
if (hasNormal && defined(tangents) && defined(bitangents)) {
Cartesian3.fromArray(normals, index, toEncode1);
Cartesian3.fromArray(tangents, index, toEncode2);
Cartesian3.fromArray(bitangents, index, toEncode3);
AttributeCompression.octPack(
toEncode1,
toEncode2,
toEncode3,
scratchCartesian2,
);
compressedAttributes[normalIndex++] = scratchCartesian2.x;
compressedAttributes[normalIndex++] = scratchCartesian2.y;