Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/engine/Source/Scene/Model/GeometryPipelineStage.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,30 @@ GeometryPipelineStage.process = function (
);
}

shaderBuilder.addUniform("vec3", "u_ellipsoidRadii");
shaderBuilder.addUniform("vec3", "u_ellipsoidRadiiSquared");

const ellipsoid = frameState.mapProjection.ellipsoid;
const uniformMap = renderResources.uniformMap;

uniformMap.u_ellipsoidRadii = function () {
return ellipsoid.radii;
};
uniformMap.u_ellipsoidRadiiSquared = function () {
return ellipsoid.radiiSquared;
};

handleBitangents(shaderBuilder, primitive.attributes);

if (primitive.primitiveType === PrimitiveType.POINTS) {
shaderBuilder.addDefine("PRIMITIVE_TYPE_POINTS");
}

shaderBuilder.addVertexLines(GeometryStageVS);
if (model._customGeometryStageVS) {
shaderBuilder.addVertexLines(model._customGeometryStageVS);
} else {
shaderBuilder.addVertexLines(GeometryStageVS);
}
shaderBuilder.addFragmentLines(GeometryStageFS);
};

Expand Down
4 changes: 4 additions & 0 deletions packages/engine/Source/Scene/Model/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ import ModelImagery from "./ModelImagery.js";
* @privateParam {boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen.
* @privateParam {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this model.
* @privateParam {boolean} [options.projectTo2D=false] Whether to accurately project the model's positions in 2D. If this is true, the model will be projected accurately to 2D, but it will use more memory to do so. If this is false, the model will use less memory and will still render in 2D / CV mode, but its positions may be inaccurate. This disables minimumPixelSize and prevents future modification to the model matrix. This also cannot be set after the model has loaded.
* @privateParam {boolean} [options.customGeometryStageVS] Whether to override the default VS geometry stage, to let the user define its own VS geometry stage.
* @privateParam {boolean} [options.enablePick=false] Whether to allow CPU picking with <code>pick</code> when not using WebGL 2 or above. If using WebGL 2 or above, this option will be ignored. If using WebGL 1 and this is true, the <code>pick</code> operation will work correctly, but it will use more memory to do so. If running with WebGL 1 and this is false, the model will use less memory, but <code>pick</code> will always return <code>undefined</code>. This cannot be set after the model has loaded.
* @privateParam {string|number} [options.featureIdLabel="featureId_0"] Label of the feature ID set to use for picking and styling. For EXT_mesh_features, this is the feature ID's label property, or "featureId_N" (where N is the index in the featureIds array) when not specified. EXT_feature_metadata did not have a label field, so such feature ID sets are always labeled "featureId_N" where N is the index in the list of all feature Ids, where feature ID attributes are listed before feature ID textures. If featureIdLabel is an integer N, it is converted to the string "featureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
* @privateParam {string|number} [options.instanceFeatureIdLabel="instanceFeatureId_0"] Label of the instance feature ID set used for picking and styling. If instanceFeatureIdLabel is set to an integer N, it is converted to the string "instanceFeatureId_N" automatically. If both per-primitive and per-instance feature IDs are present, the instance feature IDs take priority.
Expand Down Expand Up @@ -479,6 +480,7 @@ function Model(options) {
this._sceneMode = undefined;
this._projectTo2D = options.projectTo2D ?? false;
this._enablePick = options.enablePick ?? false;
this._customGeometryStageVS = options.customGeometryStageVS;

this._fogRenderable = undefined;

Expand Down Expand Up @@ -3055,6 +3057,7 @@ Model.fromGltfAsync = async function (options) {
loadIndicesForWireframe: options.enableDebugWireframe,
loadPrimitiveOutline: options.enableShowOutline,
loadForClassification: defined(options.classificationType),
customGeometryStageVS: options.customGeometryStageVS,
};

const basePath = options.basePath ?? "";
Expand Down Expand Up @@ -3321,6 +3324,7 @@ function makeModelOptions(loader, modelType, options) {
showCreditsOnScreen: options.showCreditsOnScreen,
splitDirection: options.splitDirection,
projectTo2D: options.projectTo2D,
customGeometryStageVS: options.customGeometryStageVS,
enablePick: options.enablePick,
featureIdLabel: options.featureIdLabel,
instanceFeatureIdLabel: options.instanceFeatureIdLabel,
Expand Down
86 changes: 84 additions & 2 deletions packages/engine/Source/Shaders/Model/GeometryStageVS.glsl
Original file line number Diff line number Diff line change
@@ -1,18 +1,100 @@
vec3 projectToGeographic(vec3 cartographic)
{
float lon = cartographic.y;
float lat = cartographic.x;
float altitude = cartographic.z;

float plate_x = u_ellipsoidRadii.x * lat;
float plate_y = u_ellipsoidRadii.x * lon;

return vec3(altitude, plate_x, plate_y);
}

vec3 cartesianToCartographic(vec3 cartesian) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still processing the math here, but wondering if something here might be helpful (or if we can even just use these functions directly). It looks conceptually similar.

If we can use the above, I wonder if isSphere is actually a fast path, given the expense of trig functions on the GPU. Might be worth doing some performance measurements there to see. Maybe it's faster to just treat everything as an ellipsoid and use that non-trig iterative method.

// Correct the incoming (y, z, x) coordinate order to standard ECEF (x, y, z).
vec3 ecef = vec3(cartesian.z, cartesian.x, cartesian.y);

// Check if the ellipsoid is a sphere, which allows for a much faster, direct calculation.
bool isSphere = (u_ellipsoidRadii.x == u_ellipsoidRadii.y) && (u_ellipsoidRadii.x == u_ellipsoidRadii.z);

if (isSphere) {
float mag = length(ecef);
// Avoid division by zero at the center
if (mag < czm_epsilon6) {
return vec3(0.0, 0.0, -u_ellipsoidRadii.x);
}
float longitude = atan(ecef.y, ecef.x);
float latitude = asin(ecef.z / mag);
float height = mag - u_ellipsoidRadii.x;
return vec3(longitude, latitude, height);
}

// --- Ellipsoidal Calculation (Iterative Method) ---

// The vector from the center to the point on the XY plane
vec2 p = ecef.xy;
float pLength = length(p);

// If the point is near the Z-axis, the calculation is straightforward
if (pLength < czm_epsilon6) {
// Longitude is undefined at the poles, but 0.0 is a standard convention.
float longitude = 0.0;
float latitude = (ecef.z >= 0.0) ? czm_pi / 2.0 : -czm_pi / 2.0;
float height = abs(ecef.z) - u_ellipsoidRadii.z;
return vec3(longitude, latitude, height);
}

// Key ellipsoid parameters
float a = u_ellipsoidRadii.x;
float c = u_ellipsoidRadii.z;
float a2 = u_ellipsoidRadiiSquared.x;
float c2 = u_ellipsoidRadiiSquared.z;
float e2 = (a2 - c2) / a2; // First eccentricity squared

// Initial guess for latitude is the geocentric latitude
float latitude = atan(ecef.z, pLength);
float N; // Radius of curvature in the prime vertical

// Iteratively refine the latitude. 3-4 iterations are sufficient for high precision.
for (int i = 0; i < 4; ++i) {
float sinLat = sin(latitude);
N = a / sqrt(1.0 - e2 * sinLat * sinLat);
latitude = atan((ecef.z + N * e2 * sinLat) / pLength);
}

// With the final latitude, calculate longitude and height
float longitude = atan(ecef.y, ecef.x);
float sinLat = sin(latitude);
float cosLat = cos(latitude);
N = a / sqrt(1.0 - e2 * sinLat * sinLat);
float height = (pLength / cosLat) - N;

return vec3(longitude, latitude, height);
}

vec4 geometryStage(inout ProcessedAttributes attributes, mat4 modelView, mat3 normal)
{
vec4 computedPosition;

// Compute positions in different coordinate systems
vec3 positionMC = attributes.positionMC;
v_positionMC = positionMC;
v_positionEC = (modelView * vec4(positionMC, 1.0)).xyz;

#if defined(USE_2D_POSITIONS) || defined(USE_2D_INSTANCING)
vec3 position2D = attributes.position2D;
vec3 positionEC = (u_modelView2D * vec4(position2D, 1.0)).xyz;
computedPosition = czm_projection * vec4(positionEC, 1.0);
#else
computedPosition = czm_projection * vec4(v_positionEC, 1.0);
if(czm_morphTime != 1.){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit- given that, the vast majority of the time, we operate in 3D mode, it would be slightly faster to invert this check so that it short-circuits more often.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Looking back at this, I see that this is within the USE_2D_POSITIONS macro, so is it already always 2D mode? If we remove the CPU-side projection implementation, per my overall-review comment, then we would remove this macro too, I think, and we'd need the czm_morphTime check you added.)

vec3 cartographic = cartesianToCartographic(positionMC);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This probably reveals some misunderstanding I have but) - why does this operate on the model space position instead of world or eye space?

vec3 position = projectToGeographic(cartographic);

vec3 positionEC = (czm_view * vec4(position, 1.0)).xyz;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need to write out to v_positionEC in this branch of the if-statement?

Copy link
Contributor Author

@mickae1 mickae1 Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't know, If it is used in other pipeline, or used in fragment shader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefix v_ typically means it's a vertex output attribute - so it would get used in the fragment shader. In this case, you can see that here.

computedPosition = czm_projection * vec4(positionEC, 1.0);
}else{
v_positionEC = (modelView * vec4(positionMC, 1.0)).xyz;
computedPosition = czm_projection * vec4(v_positionEC, 1.0);
}
#endif

// Sometimes the custom shader and/or style needs this
Expand Down
Loading