Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

HARP-13329: Fix antimeridian cracks on ground planes. #2018

Merged
merged 5 commits into from
Jan 6, 2021
Merged
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
5 changes: 4 additions & 1 deletion @here/harp-geometry/lib/SubdivisionModifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ export abstract class SubdivisionModifier {
}
}

positionAttr.array = new Float32Array(position);
positionAttr.array =
positionAttr.array instanceof Float32Array
? new Float32Array(position)
: new Float64Array(position);
positionAttr.count = position.length / positionAttr.itemSize;
positionAttr.needsUpdate = true;

Expand Down
4 changes: 2 additions & 2 deletions @here/harp-mapview/lib/BackgroundDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FlatTheme, Theme } from "@here/harp-datasource-protocol";
import { TileKey, TilingScheme, webMercatorTilingScheme } from "@here/harp-geoutils";

import { DataSource } from "./DataSource";
import { TileGeometryCreator } from "./geometry/TileGeometryCreator";
import { addGroundPlane } from "./geometry/AddGroundPlane";
import { Tile } from "./Tile";

/**
Expand Down Expand Up @@ -77,7 +77,7 @@ export class BackgroundDataSource extends DataSource {
getTile(tileKey: TileKey): Tile | undefined {
const tile = new Tile(this, tileKey);
tile.forceHasGeometry(true);
TileGeometryCreator.instance.addGroundPlane(tile, BackgroundDataSource.GROUND_RENDER_ORDER);
addGroundPlane(tile, BackgroundDataSource.GROUND_RENDER_ORDER);

return tile;
}
Expand Down
2 changes: 1 addition & 1 deletion @here/harp-mapview/lib/BoundsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { assert } from "@here/harp-utils";
import { Frustum, Line3, Matrix4, PerspectiveCamera, Plane, Ray, Vector2, Vector3 } from "three";

import { TileCorners } from "./geometry/TileGeometryCreator";
import { TileCorners } from "./geometry/ProjectTilePlaneCorners";
import { CanvasSide, SphereHorizon } from "./SphereHorizon";
import { MapViewUtils } from "./Utils";

Expand Down
196 changes: 196 additions & 0 deletions @here/harp-mapview/lib/geometry/AddGroundPlane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

import { GeometryKind } from "@here/harp-datasource-protocol";
import {
EdgeLengthGeometrySubdivisionModifier,
SubdivisionMode
} from "@here/harp-geometry/lib/EdgeLengthGeometrySubdivisionModifier";
import { SphericalGeometrySubdivisionModifier } from "@here/harp-geometry/lib/SphericalGeometrySubdivisionModifier";
import { Projection, ProjectionType } from "@here/harp-geoutils";
import { MapMeshBasicMaterial, MapMeshStandardMaterial } from "@here/harp-materials";
import * as THREE from "three";

import { Tile } from "../Tile";
import { LodMesh } from "./LodMesh";
import { projectTilePlaneCorners } from "./ProjectTilePlaneCorners";
import { registerTileObject } from "./RegisterTileObject";

const tmpV = new THREE.Vector3();

/**
* Creates and adds a background plane mesh for the tile.
* @param tile - The tile to which the ground plane belongs.
* @param renderOrder - The plane render order.
* @param materialOrColor - The plane material or a color for a default material.
* @param createTexCoords - Whether to create texture coordinates.
* @param receiveShadow - Whether the plane should receive shadows.
* @param createMultiLod - Whether to generate multiple LODs for sphere projection.
* @internal
*/
export function addGroundPlane(
tile: Tile,
renderOrder: number,
materialOrColor: THREE.Material | THREE.Material[] | number = tile.mapView.clearColor,
createTexCoords: boolean = false,
receiveShadow: boolean = tile.mapView.shadowsEnabled,
createMultiLod: boolean = tile.mapView.enableMixedLod !== false
) {
const mesh = createGroundPlane(
tile,
createTexCoords,
receiveShadow,
materialOrColor,
createMultiLod
);
mesh.receiveShadow = receiveShadow;
mesh.renderOrder = renderOrder;
registerTileObject(tile, mesh, GeometryKind.Background, { pickable: false });
tile.objects.push(mesh);
}

function createGroundPlane(
tile: Tile,
createTexCoords: boolean,
receiveShadow: boolean,
materialOrColor: THREE.Material | THREE.Material[] | number,
createMultiLod: boolean
): THREE.Mesh {
const { dataSource, projection } = tile;
const sourceProjection = dataSource.getTilingScheme().projection;
const shouldSubdivide = projection.type === ProjectionType.Spherical;
const useLocalTargetCoords = !shouldSubdivide;

const material =
typeof materialOrColor === "number"
? createGroundPlaneMaterial(
new THREE.Color(materialOrColor),
receiveShadow,
projection.type === ProjectionType.Spherical
)
: materialOrColor;

const geometry = createGroundPlaneGeometry(
tile,
useLocalTargetCoords,
createTexCoords,
receiveShadow
);

if (!shouldSubdivide) {
return new THREE.Mesh(geometry, material);
Copy link
Member

Choose a reason for hiding this comment

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

You need to convert the Float64Array back to Float32Array also in this case.

Copy link
Collaborator Author

@atomicsulfate atomicsulfate Jan 6, 2021

Choose a reason for hiding this comment

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

If !shouldSubdivide, then useLocalTargetCoords == true. You can see in createPlaneGeometry() that in that case whe use FLoat32Array:

const bufferArray = useLocalTargetCoords ? new Float32Array(12) : new Float64Array(12);

Copy link
Member

Choose a reason for hiding this comment

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

I'm not such a fan of having the useLocalTargetCoords control the transformation to local coordinates in the createPlaneGeometry function, I think it makes it easier to read when this transformation is explicitly outside of the function, like it was previously with moveTileCenter, so that it isn't responsible for too many things. But because @FraukeF already approve, I won't block it.

}

const geometries: THREE.BufferGeometry[] = [];
const sphericalModifier = new SphericalGeometrySubdivisionModifier(
THREE.MathUtils.degToRad(10),
sourceProjection
);

if (!createMultiLod) {
sphericalModifier.modify(geometry);
toLocalTargetCoords(geometry, sourceProjection, tile);

return new THREE.Mesh(geometry, material);
}

// Use a [[LodMesh]] to adapt tesselation of tile depending on zoom level
for (let zoomLevelOffset = 0; zoomLevelOffset < 4; ++zoomLevelOffset) {
const subdivision = Math.pow(2, zoomLevelOffset);
const zoomLevelGeometry = geometry.clone();
if (subdivision > 1) {
const edgeModifier = new EdgeLengthGeometrySubdivisionModifier(
subdivision,
tile.geoBox,
SubdivisionMode.All,
sourceProjection
);
edgeModifier.modify(zoomLevelGeometry);
}
sphericalModifier.modify(zoomLevelGeometry);
toLocalTargetCoords(zoomLevelGeometry, sourceProjection, tile);
geometries.push(zoomLevelGeometry);
}
return new LodMesh(geometries, material);
}

function toLocalTargetCoords(geom: THREE.BufferGeometry, srcProjection: Projection, tile: Tile) {
const attr = geom.getAttribute("position") as THREE.BufferAttribute;
const oldArray = attr.array as Float64Array;
// Convert to single precision before rendering (WebGL does not support double
// precision).
const newArray = new Float32Array(oldArray.length);
for (let i = 0; i < attr.array.length; i += 1) {
tmpV.fromBufferAttribute(attr, i);
tile.projection.reprojectPoint(srcProjection, tmpV, tmpV).sub(tile.center);
tmpV.toArray(newArray, i * 3);
}
attr.array = newArray;
attr.needsUpdate = true;
}

function createGroundPlaneGeometry(
tile: Tile,
useLocalTargetCoords: boolean,
createTexCoords: boolean,
receiveShadow: boolean
): THREE.BufferGeometry {
const { dataSource, projection } = tile;
const sourceProjection = dataSource.getTilingScheme().projection;
const tmpV = new THREE.Vector3();

const geometry = new THREE.BufferGeometry();
const tileCorners = projectTilePlaneCorners(tile, sourceProjection);
const cornersArray = [tileCorners.sw, tileCorners.se, tileCorners.nw, tileCorners.ne];
if (useLocalTargetCoords) {
for (const corner of cornersArray) {
projection.reprojectPoint(sourceProjection, corner, corner).sub(tile.center);
}
}
// Use 64bits floats for world coordinates to avoid precision issues on coordinate
// tranformations. The array must be converted to single precision before rendering.
const bufferArray = useLocalTargetCoords ? new Float32Array(12) : new Float64Array(12);
const posAttr = new THREE.BufferAttribute(bufferArray, 3).copyVector3sArray(cornersArray);
geometry.setAttribute("position", posAttr);

if (receiveShadow) {
// Webmercator needs to have it negated to work correctly.
sourceProjection.surfaceNormal(tileCorners.sw, tmpV).negate();
const normAttr = new THREE.BufferAttribute(new Float32Array(12), 3).copyVector3sArray(
Array(4).fill(tmpV)
);
geometry.setAttribute("normal", normAttr);
}
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array([0, 1, 2, 2, 1, 3]), 1));

if (createTexCoords) {
const uvAttr = new THREE.BufferAttribute(new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), 2);
geometry.setAttribute("uv", uvAttr);
}

return geometry;
}

function createGroundPlaneMaterial(
color: THREE.Color,
receiveShadow: boolean,
depthWrite: boolean
): THREE.Material {
if (receiveShadow) {
return new MapMeshStandardMaterial({
color,
visible: true,
depthWrite,
removeDiffuseLight: true
});
} else {
return new MapMeshBasicMaterial({
color,
visible: true,
depthWrite
});
}
}
39 changes: 39 additions & 0 deletions @here/harp-mapview/lib/geometry/ProjectTilePlaneCorners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

import { GeoCoordinates, Projection } from "@here/harp-geoutils";
import * as THREE from "three";

import { Tile } from "../Tile";

/**
* Coordinates of a tile's corners.
* @internal
* @hidden
*/
export interface TileCorners {
se: THREE.Vector3;
sw: THREE.Vector3;
ne: THREE.Vector3;
nw: THREE.Vector3;
}

/**
* Returns the corners of the tile's geo bounding box projected using a given projection.
* @param tile - The tile whose corners will be projected.
* @param projection - The projection to be used.
* @returns The projected tile corners.
* @internal
* @hidden
*/
export function projectTilePlaneCorners(tile: Tile, projection: Projection): TileCorners {
const { east, west, north, south } = tile.geoBox;
const sw = projection.projectPoint(new GeoCoordinates(south, west), new THREE.Vector3());
const se = projection.projectPoint(new GeoCoordinates(south, east), new THREE.Vector3());
const nw = projection.projectPoint(new GeoCoordinates(north, west), new THREE.Vector3());
const ne = projection.projectPoint(new GeoCoordinates(north, east), new THREE.Vector3());
return { sw, se, nw, ne };
}
57 changes: 57 additions & 0 deletions @here/harp-mapview/lib/geometry/RegisterTileObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/

import { GeometryKind, GeometryKindSet } from "@here/harp-datasource-protocol";
import * as THREE from "three";

import { MapObjectAdapter, MapObjectAdapterParams } from "../MapObjectAdapter";
import { Tile } from "../Tile";

/**
* Adds a THREE object to the root of the tile and register [[MapObjectAdapter]].
*
* Sets the owning tiles datasource.name and the `tileKey` in the `userData` property of the
* object, such that the tile it belongs to can be identified during picking.
*
* @param tile - The {@link Tile} to add the object to.
* @param object - The object to add to the root of the tile.
* @param geometryKind - The kind of object. Can be used for filtering.
* @param mapAdapterParams - additional parameters for [[MapObjectAdapter]]
*/
export function registerTileObject(
tile: Tile,
object: THREE.Object3D,
geometryKind: GeometryKind | GeometryKindSet | undefined,
mapAdapterParams?: MapObjectAdapterParams
) {
const kind =
geometryKind instanceof Set
? Array.from((geometryKind as GeometryKindSet).values())
: Array.isArray(geometryKind)
? geometryKind
: [geometryKind];

MapObjectAdapter.create(object, {
dataSource: tile.dataSource,
kind,
level: tile.tileKey.level,
...mapAdapterParams
});

// TODO legacy fields, encoded directly in `userData to be removed
if (object.userData === undefined) {
object.userData = {};
}

const userData = object.userData;
userData.tileKey = tile.tileKey;
userData.dataSource = tile.dataSource.name;

userData.kind = kind;

// Force a visibility check of all objects.
tile.resetVisibilityCounter();
}
Loading