From a33900621088791f47f3559b008b64f13ce83f79 Mon Sep 17 00:00:00 2001 From: Andres Mandado Date: Thu, 21 Jan 2021 18:23:22 +0100 Subject: [PATCH] HARP-13235: Set constantHeight as common parameter for all techniques. If datasource geometry is 3D and technique's constantHeight is set to true or defaults to true (for store levels lower than 12), then projected geometry height is scaled by target projection's scale factor. --- .../lib/DecodedTile.ts | 24 +++ .../lib/TechniqueDescriptors.ts | 2 +- .../lib/TechniqueParams.ts | 18 +- .../lib/OmvUtils.ts | 7 +- .../lib/VectorTileDataEmitter.ts | 90 +++++---- .../test/OmvDecodedTileEmitterTest.ts | 175 +++++++++++++++--- 6 files changed, 245 insertions(+), 71 deletions(-) diff --git a/@here/harp-datasource-protocol/lib/DecodedTile.ts b/@here/harp-datasource-protocol/lib/DecodedTile.ts index f74dc1cc36..43e23cd244 100644 --- a/@here/harp-datasource-protocol/lib/DecodedTile.ts +++ b/@here/harp-datasource-protocol/lib/DecodedTile.ts @@ -442,3 +442,27 @@ export function getFeatureText( return getFeatureName(env, propName, useAbbreviation, useIsoCode, languages); } + +/** + * Determine whether to scale heights by the projection scale factor for geometry + * using the given technique. + * @remarks Unless explicitly defined, the scale factor to convert meters to world space units + * won't be applied if the tile's level is less than a fixed storage level. + * @param context - Context for evaluation of technique attributes. + * @param technique - Technique to be evaluated. + * @param tileLevel - The level of the tile where the geometry is stored. + * @returns `true` if height must be scaled, `false` otherwise. + */ +export function scaleHeight( + context: Env | AttrEvaluationContext, + technique: Technique, + tileLevel: number +): boolean { + const SCALED_HEIGHT_MIN_STORAGE_LEVEL = 12; + const useConstantHeight = evaluateTechniqueAttr( + context, + technique.constantHeight, + tileLevel < SCALED_HEIGHT_MIN_STORAGE_LEVEL + ); + return !useConstantHeight; +} diff --git a/@here/harp-datasource-protocol/lib/TechniqueDescriptors.ts b/@here/harp-datasource-protocol/lib/TechniqueDescriptors.ts index b024f64965..7f28a74d07 100644 --- a/@here/harp-datasource-protocol/lib/TechniqueDescriptors.ts +++ b/@here/harp-datasource-protocol/lib/TechniqueDescriptors.ts @@ -127,6 +127,7 @@ const baseTechniqueParamsDescriptor: TechniqueDescriptor = // For now we chosen all, but it maybe not suitable for text or line marker techniques. attrTransparencyColor: "color", attrDescriptors: { + constantHeight: AttrScope.FeatureGeometry, enabled: AttrScope.FeatureGeometry, fadeFar: AttrScope.TechniqueRendering, fadeNear: AttrScope.TechniqueRendering, @@ -270,7 +271,6 @@ const extrudedPolygonTechniqueDescriptor = mergeTechniqueDescriptor; + + /** + * If `true`, geometry height won't be scaled on projection. Enable it for projections with + * variable scale factor (e.g. mercator) to avoid distortions in geometry with great heights and + * latitude spans. E.g. a large object with even height would look oblique to the ground plane + * on mercator unless this property is set to `true`. + * + * @defaultValue `true` for geometries stored at level less than `12`. + */ + constantHeight?: boolean; } export enum TextureCoordinateType { @@ -1256,14 +1266,6 @@ export interface ExtrudedPolygonTechniqueParams extends StandardTechniqueParams */ defaultColor?: DynamicProperty; - /** - * If `true`, the height of the extruded buildings will not be modified by the mercator - * projection distortion that happens around the poles. - * - * @defaultValue `true` for geometries stored at level less than `12`. - */ - constantHeight?: boolean; - /** * If `true`, wall geometry will is added along the tile boundaries. * diff --git a/@here/harp-vectortile-datasource/lib/OmvUtils.ts b/@here/harp-vectortile-datasource/lib/OmvUtils.ts index c53439422d..11108b3814 100644 --- a/@here/harp-vectortile-datasource/lib/OmvUtils.ts +++ b/@here/harp-vectortile-datasource/lib/OmvUtils.ts @@ -130,10 +130,14 @@ export function webMercatorTile2TargetWorld( decodeInfo: DecodeInfo, position: THREE.Vector2 | THREE.Vector3, target: THREE.Vector3, + scaleHeight: boolean, flipY: boolean = false ) { tile2world(extents, decodeInfo, position, flipY, target); decodeInfo.targetProjection.reprojectPoint(webMercatorProjection, target, target); + if (position instanceof THREE.Vector3 && scaleHeight) { + target.z *= decodeInfo.targetProjection.getScaleFactor(target); + } } export function webMercatorTile2TargetTile( @@ -141,8 +145,9 @@ export function webMercatorTile2TargetTile( decodeInfo: DecodeInfo, position: THREE.Vector2 | THREE.Vector3, target: THREE.Vector3, + scaleHeight: boolean, flipY: boolean = false ) { - webMercatorTile2TargetWorld(extents, decodeInfo, position, target, flipY); + webMercatorTile2TargetWorld(extents, decodeInfo, position, target, scaleHeight, flipY); target.sub(decodeInfo.center); } diff --git a/@here/harp-vectortile-datasource/lib/VectorTileDataEmitter.ts b/@here/harp-vectortile-datasource/lib/VectorTileDataEmitter.ts index 9255b28927..efd75f48ad 100644 --- a/@here/harp-vectortile-datasource/lib/VectorTileDataEmitter.ts +++ b/@here/harp-vectortile-datasource/lib/VectorTileDataEmitter.ts @@ -33,6 +33,7 @@ import { PathGeometry, PoiGeometry, PoiTechnique, + scaleHeight, StyleColor, Technique, TextGeometry, @@ -85,7 +86,6 @@ import { Ring } from "./Ring"; const logger = LoggerManager.instance.create("OmvDecodedTileEmitter"); const tempTileOrigin = new THREE.Vector3(); -const tempVertOrigin = new THREE.Vector3(); const tempVertNormal = new THREE.Vector3(); const tempFootDisp = new THREE.Vector3(); const tempRoofDisp = new THREE.Vector3(); @@ -331,6 +331,7 @@ export class VectorTileDataEmitter { } } + const scaleHeights = scaleHeight(context, technique, this.m_decodeInfo.tileKey.level); const featureId = getFeatureId(env.entries); for (const pos of tilePositions) { if (shouldCreateTextGeometries) { @@ -347,9 +348,21 @@ export class VectorTileDataEmitter { // Always store the position, otherwise the following POIs will be // misplaced. if (shouldCreateTextGeometries) { - webMercatorTile2TargetWorld(extents, this.m_decodeInfo, pos, tmpV3); + webMercatorTile2TargetWorld( + extents, + this.m_decodeInfo, + pos, + tmpV3, + scaleHeights + ); } else { - webMercatorTile2TargetTile(extents, this.m_decodeInfo, pos, tmpV3); + webMercatorTile2TargetTile( + extents, + this.m_decodeInfo, + pos, + tmpV3, + scaleHeights + ); } positions.push(tmpV3.x, tmpV3.y, tmpV3.z); objInfos.push(this.m_gatherFeatureAttributes ? env.entries : featureId); @@ -404,6 +417,7 @@ export class VectorTileDataEmitter { let texCoordinateType: TextureCoordinateType | undefined; const hasUntiledLines = geometry[0].untiledPositions !== undefined; + const scaleHeights = false; // No need to scale height, source data is 2D. // If true, special handling for dashes is required (round and diamond shaped dashes). let hasIndividualLineSegments = false; @@ -471,9 +485,21 @@ export class VectorTileDataEmitter { const pos1 = polyline.positions[i]; const pos2 = polyline.positions[i + 1]; - webMercatorTile2TargetWorld(extents, this.m_decodeInfo, pos1, tmpV3); + webMercatorTile2TargetWorld( + extents, + this.m_decodeInfo, + pos1, + tmpV3, + scaleHeights + ); worldLine.push(tmpV3.x, tmpV3.y, tmpV3.z); - webMercatorTile2TargetWorld(extents, this.m_decodeInfo, pos2, tmpV4); + webMercatorTile2TargetWorld( + extents, + this.m_decodeInfo, + pos2, + tmpV4, + scaleHeights + ); worldLine.push(tmpV4.x, tmpV4.y, tmpV4.z); if (computeTexCoords) { @@ -527,7 +553,13 @@ export class VectorTileDataEmitter { const lineUvs: number[] = []; const lineOffsets: number[] = []; polyline.positions.forEach(pos => { - webMercatorTile2TargetWorld(extents, this.m_decodeInfo, pos, tmpV3); + webMercatorTile2TargetWorld( + extents, + this.m_decodeInfo, + pos, + tmpV3, + scaleHeights + ); worldLine.push(tmpV3.x, tmpV3.y, tmpV3.z); if (computeTexCoords) { @@ -770,6 +802,8 @@ export class VectorTileDataEmitter { const env = context.env; this.processFeatureCommon(env); + const scaleHeights = false; // No need to scale height, source data is 2D. + techniques.forEach(technique => { if (technique === undefined) { return; @@ -906,7 +940,8 @@ export class VectorTileDataEmitter { extents, this.m_decodeInfo, tmpV2.copy(curr), - tmpV3 + tmpV3, + scaleHeights ); line.push(tmpV3.x, tmpV3.y, tmpV3.z); @@ -916,7 +951,8 @@ export class VectorTileDataEmitter { extents, this.m_decodeInfo, tmpV2.copy(next), - tmpV4 + tmpV4, + scaleHeights ); line.push(tmpV4.x, tmpV4.y, tmpV4.z); @@ -934,7 +970,8 @@ export class VectorTileDataEmitter { extents, this.m_decodeInfo, tmpV2.copy(next), - tmpV3 + tmpV3, + scaleHeights ); line.push(tmpV3.x, tmpV3.y, tmpV3.z); } @@ -1299,16 +1336,8 @@ export class VectorTileDataEmitter { const tileLevel = this.m_decodeInfo.tileKey.level; - const SCALED_EXTRUSION_MIN_STORAGE_LEVEL = 12; - - // Unless explicitly defined do not apply the projection - // scale factor to convert meters to world space units - // if the tile's level is less than `SCALED_EXTRUSION_MIN_STORAGE_LEVEL`. - const styleSetConstantHeight = evaluateTechniqueAttr( - context, - extrudedPolygonTechnique.constantHeight, - tileLevel < SCALED_EXTRUSION_MIN_STORAGE_LEVEL - ); + const scaleHeights = + isExtruded && scaleHeight(context, extrudedPolygonTechnique, tileLevel); this.m_decodeInfo.tileBounds.getCenter(tempTileOrigin); @@ -1533,25 +1562,18 @@ export class VectorTileDataEmitter { // Assemble the vertex buffer. for (let i = 0; i < vertices.length; i += vertexStride) { - webMercatorTile2TargetTile( + webMercatorTile2TargetWorld( extents, this.m_decodeInfo, tmpV2.set(vertices[i], vertices[i + 1]), tmpV3, + false, // no need to scale height (source data is 2D). true ); - let scaleFactor = 1.0; - if (isExtruded && styleSetConstantHeight !== true) { - tempVertOrigin.set( - tempTileOrigin.x + tmpV3.x, - tempTileOrigin.y + tmpV3.y, - tempTileOrigin.z + tmpV3.z - ); - scaleFactor = this.m_decodeInfo.targetProjection.getScaleFactor( - tempVertOrigin - ); - } + const scaleFactor = scaleHeights + ? this.m_decodeInfo.targetProjection.getScaleFactor(tmpV3) + : 1.0; this.m_maxGeometryHeight = Math.max( this.m_maxGeometryHeight, scaleFactor * height @@ -1562,11 +1584,9 @@ export class VectorTileDataEmitter { ); if (isSpherical) { - tempVertNormal - .set(tmpV3.x, tmpV3.y, tmpV3.z) - .add(this.center) - .normalize(); + tempVertNormal.set(tmpV3.x, tmpV3.y, tmpV3.z).normalize(); } + tmpV3.sub(this.center); tempFootDisp.copy(tempVertNormal).multiplyScalar(floorHeight * scaleFactor); positions.push( diff --git a/@here/harp-vectortile-datasource/test/OmvDecodedTileEmitterTest.ts b/@here/harp-vectortile-datasource/test/OmvDecodedTileEmitterTest.ts index 4b42a12d9d..b0582f55b4 100644 --- a/@here/harp-vectortile-datasource/test/OmvDecodedTileEmitterTest.ts +++ b/@here/harp-vectortile-datasource/test/OmvDecodedTileEmitterTest.ts @@ -18,6 +18,7 @@ import { GeoCoordinates, mercatorProjection, TileKey, + Vector3Like, webMercatorProjection } from "@here/harp-geoutils"; import { assert } from "chai"; @@ -38,26 +39,29 @@ class OmvDecodedTileEmitterTest extends VectorTileDataEmitter { } } -describe("OmvDecodedTileEmitter", function () { - function createTileEmitter(): { - tileEmitter: OmvDecodedTileEmitterTest; - styleSetEvaluator: StyleSetEvaluator; - } { - const tileKey = TileKey.fromRowColumnLevel(0, 0, 1); - const projection = mercatorProjection; +const extents = 4096; +const layer = "dummy"; - const decodeInfo = new DecodeInfo("test", projection, tileKey); - - const styleSet: StyleSet = [ +describe("OmvDecodedTileEmitter", function () { + function createTileEmitter( + decodeInfo: DecodeInfo = new DecodeInfo( + "test", + mercatorProjection, + TileKey.fromRowColumnLevel(0, 0, 1) + ), + styleSet: StyleSet = [ { - when: "1", + when: "layer == 'mock-layer'", technique: "standard", attr: { textureCoordinateType: TextureCoordinateType.TileSpace } } - ]; - + ] + ): { + tileEmitter: OmvDecodedTileEmitterTest; + styleSetEvaluator: StyleSetEvaluator; + } { const styleSetEvaluator = new StyleSetEvaluator({ styleSet }); const tileEmitter = new OmvDecodedTileEmitterTest( @@ -89,14 +93,8 @@ describe("OmvDecodedTileEmitter", function () { return buffer; } - it("Ring data conversion to polygon data: whole tile square shape", function () { - const tileKey = TileKey.fromRowColumnLevel(0, 0, 1); - const projection = mercatorProjection; - - const decodeInfo = new DecodeInfo("test", projection, tileKey); - + function tileGeoBoxToPolygonGeometry(decodeInfo: DecodeInfo): IPolygonGeometry[] { const geoBox = decodeInfo.geoBox; - const coordinates: GeoCoordinates[] = [ new GeoCoordinates(geoBox.south, geoBox.west), new GeoCoordinates(geoBox.south, geoBox.east), @@ -107,17 +105,142 @@ describe("OmvDecodedTileEmitter", function () { const tileLocalCoords = coordinates.map(p => { const projected = webMercatorProjection.projectPoint(p, new Vector3()); const result = new Vector2(); - const tileCoords = world2tile(4096, decodeInfo, projected, false, result); + const tileCoords = world2tile(extents, decodeInfo, projected, false, result); return tileCoords; }); - const polygons: IPolygonGeometry[] = [ - { - rings: [tileLocalCoords] + return [{ rings: [tileLocalCoords] }]; + } + + for (const { level, constantHeight, expectScaledHeight } of [ + { level: 12, constantHeight: undefined, expectScaledHeight: true }, + { level: 10, constantHeight: undefined, expectScaledHeight: false }, + { level: 12, constantHeight: true, expectScaledHeight: false }, + { level: 10, constantHeight: false, expectScaledHeight: true } + ]) { + const result = expectScaledHeight ? "scaled" : "not scaled"; + const tileKey = TileKey.fromRowColumnLevel(0, 0, level); + const decodeInfo = new DecodeInfo("test", mercatorProjection, tileKey); + + function getExpectedHeight(geoAltitude: number, worldCoords: Vector3Like) { + const scaleFactor = expectScaledHeight + ? decodeInfo.targetProjection.getScaleFactor(worldCoords) + : 1.0; + // Force conversion to single precision as in decoder so that results match. + return new Float32Array([geoAltitude * scaleFactor])[0]; + } + + it(`Point Height at level ${level} with constantHeight ${constantHeight} is ${result}`, function () { + const geoCoords = decodeInfo.geoBox.center.clone(); + geoCoords.altitude = 100; + const tileLocalCoords = world2tile( + extents, + decodeInfo, + webMercatorProjection.projectPoint(geoCoords), + false, + new Vector3() + ); + const worldCoords = decodeInfo.targetProjection.projectPoint(geoCoords); + + const { tileEmitter, styleSetEvaluator } = createTileEmitter(decodeInfo, [ + { + when: "1", + technique: "text", + attr: { text: "Test", constantHeight } + } + ]); + + const mockContext = { + env: new MapEnv({ layer }), + storageLevel: tileKey.level, + zoomLevel: tileKey.level + }; + + tileEmitter.processPointFeature( + layer, + extents, + [tileLocalCoords], + mockContext, + styleSetEvaluator.getMatchingTechniques(mockContext.env) + ); + + const { textGeometries } = tileEmitter.getDecodedTile(); + + assert.equal(textGeometries?.length, 1, "only one geometry created"); + + const buffer = new Float32Array(textGeometries![0].positions.buffer); + assert.equal(buffer.length, 3, "one position (3 coordinates)"); + + const actualHeight = buffer[2]; + assert.equal(actualHeight, getExpectedHeight(geoCoords.altitude, worldCoords)); + }); + + it(`Extruded polygon height at level ${level} with constantHeight ${constantHeight} is ${result}`, function () { + const polygons = tileGeoBoxToPolygonGeometry(decodeInfo); + const height = 100; + const { tileEmitter, styleSetEvaluator } = createTileEmitter(decodeInfo, [ + { + when: "1", + technique: "extruded-polygon", + attr: { + textureCoordinateType: TextureCoordinateType.TileSpace, + height, + constantHeight + } + } + ]); + + const mockContext = { + env: new MapEnv({ layer }), + storageLevel: level, + zoomLevel: level + }; + + tileEmitter.processPolygonFeature( + layer, + extents, + polygons, + mockContext, + styleSetEvaluator.getMatchingTechniques(mockContext.env), + undefined + ); + + const decodedTile = tileEmitter.getDecodedTile(); + + const { geometries } = decodedTile; + + assert.equal(geometries.length, 1, "only one geometry created"); + assert.equal(geometries[0].type, GeometryType.ExtrudedPolygon, "geometry is a polygon"); + const geometry = geometries[0]; + const posAttr = geometry.vertexAttributes![0]; + const array = new Float32Array(posAttr.buffer); + + const vertexCount = 8; + assert.equal(array.length / posAttr.itemCount, vertexCount); + + const worldVertices = []; + for (let i = 0; i < array.length; i += posAttr.itemCount) { + worldVertices.push(new Vector3().fromArray(array, i).add(decodeInfo.center)); } - ]; + worldVertices.sort((vl, vr) => vl.z - vr.z); // sort by height. + // First half must have 0 height + worldVertices.slice(0, vertexCount / 2).forEach(v => assert.equal(v.z, 0)); + + // Second half must have expected height + worldVertices + .slice(vertexCount / 2) + .forEach(v => assert.equal(v.z, getExpectedHeight(height, v))); + }); + } + + it("Ring data conversion to polygon data: whole tile square shape", function () { + const tileKey = TileKey.fromRowColumnLevel(0, 0, 1); + const projection = mercatorProjection; + + const decodeInfo = new DecodeInfo("test", projection, tileKey); + const polygons = tileGeoBoxToPolygonGeometry(decodeInfo); - const { tileEmitter, styleSetEvaluator } = createTileEmitter(); + const { tileEmitter, styleSetEvaluator } = createTileEmitter(decodeInfo); const storageLevel = 10; const mockContext = {