itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
350 lines (335 loc) • 13.4 kB
JavaScript
import * as THREE from 'three';
import { Coordinates } from '@itowns/geographic';
import placeObjectOnGround from "./placeObjectOnGround.js";
const FAST_READ_Z = 0;
const PRECISE_READ_Z = 1;
/**
* Utility module to retrieve elevation at a given coordinates. The returned
* value is read in the elevation textures used by the graphics card to render
* the tiles (globe or plane). This implies that the return value may change
* depending on the current tile resolution.
*
* @module DEMUtils
*/
export default {
/**
* Gives the elevation value of a {@link TiledGeometryLayer}, at a specific
* {@link Coordinates}.
*
* @param {TiledGeometryLayer} layer - The tile layer owning the elevation
* textures we're going to query. This is typically a `GlobeLayer` or
* `PlanarLayer` (accessible through `view.tileLayer`).
* @param {Coordinates} coord - The coordinates that we're interested in.
* @param {number} [method=FAST_READ_Z] - There are two available methods:
* `FAST_READ_Z` (default) or `PRECISE_READ_Z`. The first one is faster,
* while the second one is slower but gives better precision.
* @param {TileMesh[]} [tileHint] - Optional array of tiles to speed up the
* process. You can give candidates tiles likely to contain `coord`.
* Otherwise the lookup process starts from the root of `layer`.
*
* @return {number} If found, a value in meters is returned; otherwise
* `undefined`.
*/
getElevationValueAt(layer, coord) {
let method = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : FAST_READ_Z;
let tileHint = arguments.length > 3 ? arguments[3] : undefined;
const result = _readZ(layer, method, coord, tileHint || layer.level0Nodes);
if (result) {
return result.coord.z;
}
},
/**
* @typedef Terrain
* @type {Object}
*
* @property {Coordinates} coord - Pick coordinate with the elevation in coord.z.
* @property {THREE.Texture} texture - the picked elevation texture.
* The texture where the `z` value has been read from
* @property {TileMesh} tile - the picked tile and the tile containing the texture
*/
/**
* Gives a {@link Terrain} object, at a specific {@link Coordinates}. The returned
* object is as follow:
* - `coord`, Coordinate, coord.z is the value in meters of the elevation at the coordinates
* - `texture`, the texture where the `z` value has been read from
* - `tile`, the tile containing the texture
* @example
* // place mesh on the ground
* const coord = new Coordinates('EPSG:4326', 6, 45);
* const result = DEMUtils.getTerrainObjectAt(view.tileLayer, coord)
* mesh.position.copy(result.coord.as(view.referenceCrs));
* view.scene.add(mesh);
* mesh.updateMatrixWorld();
*
*
* @param {TiledGeometryLayer} layer - The tile layer owning the elevation
* textures we're going to query. This is typically a `GlobeLayer` or
* `PlanarLayer` (accessible through `view.tileLayer`).
* @param {Coordinates} coord - The coordinates that we're interested in.
* @param {number} [method=FAST_READ_Z] - There are two available methods:
* `FAST_READ_Z` (default) or `PRECISE_READ_Z`. The first one is faster,
* while the second one is slower but gives better precision.
* @param {TileMesh[]} [tileHint] - Optional array of tiles to speed up the
* process. You can give candidates tiles likely to contain `coord`.
* Otherwise the lookup process starts from the root of `layer`.
* @param {Object} [cache] - Object to cache previous result and speed up the next `getTerrainObjectAt`` use.
*
* @return {Terrain} - The {@link Terrain} object.
*/
getTerrainObjectAt(layer, coord) {
let method = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : FAST_READ_Z;
let tileHint = arguments.length > 3 ? arguments[3] : undefined;
let cache = arguments.length > 4 ? arguments[4] : undefined;
return _readZ(layer, method, coord, tileHint || layer.level0Nodes, cache);
},
FAST_READ_Z,
PRECISE_READ_Z,
placeObjectOnGround
};
function tileAt(pt, tile) {
if (tile.extent) {
if (!tile.extent.isPointInside(pt)) {
return undefined;
}
for (let i = 0; i < tile.children.length; i++) {
const t = tileAt(pt, tile.children[i]);
if (t) {
return t;
}
}
const tileLayer = tile.material.getElevationLayer();
if (tileLayer && tileLayer.level >= 0) {
return tile;
}
return undefined;
}
}
let _canvas;
function _readTextureValueAt(metadata, texture) {
for (var _len = arguments.length, uv = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
uv[_key - 2] = arguments[_key];
}
for (let i = 0; i < uv.length; i += 2) {
uv[i] = THREE.MathUtils.clamp(uv[i], 0, texture.image.width - 1);
uv[i + 1] = THREE.MathUtils.clamp(uv[i + 1], 0, texture.image.height - 1);
}
if (texture.image.data) {
// read a single value
if (uv.length === 2) {
const v = texture.image.data[uv[1] * texture.image.width + uv[0]];
return v != metadata.noDataValue ? v : undefined;
}
// or read multiple values
const result = [];
for (let i = 0; i < uv.length; i += 2) {
const v = texture.image.data[uv[i + 1] * texture.image.width + uv[i]];
result.push(v != metadata.noDataValue ? v : undefined);
}
return result;
} else {
if (!_canvas) {
_canvas = document.createElement('canvas');
_canvas.width = 2;
_canvas.height = 2;
}
let minx = Infinity;
let miny = Infinity;
let maxx = -Infinity;
let maxy = -Infinity;
for (let i = 0; i < uv.length; i += 2) {
minx = Math.min(uv[i], minx);
miny = Math.min(uv[i + 1], miny);
maxx = Math.max(uv[i], maxx);
maxy = Math.max(uv[i + 1], maxy);
}
const dw = maxx - minx + 1;
const dh = maxy - miny + 1;
_canvas.width = Math.max(_canvas.width, dw);
_canvas.height = Math.max(_canvas.height, dh);
const ctx = _canvas.getContext('2d', {
willReadFrequently: true
});
ctx.drawImage(texture.image, minx, miny, dw, dh, 0, 0, dw, dh);
const d = ctx.getImageData(0, 0, dw, dh);
const result = [];
for (let i = 0; i < uv.length; i += 2) {
const ox = uv[i] - minx;
const oy = uv[i + 1] - miny;
// d is 4 bytes per pixel
const v = THREE.MathUtils.lerp(metadata.colorTextureElevationMinZ, metadata.colorTextureElevationMaxZ, d.data[4 * oy * dw + 4 * ox] / 255);
result.push(v != metadata.noDataValue ? v : undefined);
}
if (uv.length === 2) {
return result[0];
} else {
return result;
}
}
}
function _convertUVtoTextureCoords(texture, u, v) {
const width = texture.image.width;
const height = texture.image.height;
const up = Math.max(0, u * width - 0.5);
const vp = Math.max(0, v * height - 0.5);
const u1 = Math.floor(up);
const u2 = Math.ceil(up);
const v1 = Math.floor(vp);
const v2 = Math.ceil(vp);
return {
u1,
u2,
v1,
v2,
wu: up - u1,
wv: vp - v1
};
}
function _readTextureValueNearestFiltering(metadata, texture, vertexU, vertexV) {
const coords = _convertUVtoTextureCoords(texture, vertexU, vertexV);
const u = coords.wu <= 0 ? coords.u1 : coords.u2;
const v = coords.wv <= 0 ? coords.v1 : coords.v2;
return _readTextureValueAt(metadata, texture, u, v);
}
function _lerpWithUndefinedCheck(x, y, t) {
if (x == undefined) {
return y;
} else if (y == undefined) {
return x;
} else {
return THREE.MathUtils.lerp(x, y, t);
}
}
export function readTextureValueWithBilinearFiltering(metadata, texture, vertexU, vertexV) {
const coords = _convertUVtoTextureCoords(texture, vertexU, vertexV);
const [z11, z21, z12, z22] = _readTextureValueAt(metadata, texture, coords.u1, coords.v1, coords.u2, coords.v1, coords.u1, coords.v2, coords.u2, coords.v2);
// horizontal filtering
const zu1 = _lerpWithUndefinedCheck(z11, z21, coords.wu);
const zu2 = _lerpWithUndefinedCheck(z12, z22, coords.wu);
// then vertical filtering
return _lerpWithUndefinedCheck(zu1, zu2, coords.wv);
}
function _readZFast(layer, texture, uv) {
const elevationLayer = layer.attachedLayers.filter(l => l.isElevationLayer)[0];
return _readTextureValueNearestFiltering(elevationLayer, texture, uv.x, uv.y);
}
const bary = new THREE.Vector3();
function _readZCorrect(layer, texture, uv, tileDimensions, tileOwnerDimensions) {
// We need to emulate the vertex shader code that does 2 thing:
// - interpolate (u, v) between triangle vertices: u,v will be multiple of 1/nsegments
// (for now assume nsegments == 16)
// - read elevation texture at (u, v) for
// Determine u,v based on the vertices count.
// 'modulo' is the gap (in [0, 1]) between 2 successive vertices in the geometry
// e.g if you have 5 vertices, the only possible values for u (or v) are: 0, 0.25, 0.5, 0.75, 1
// so modulo would be 0.25
// note: currently the number of segments is hard-coded to 16 (see TileProvider) => 17 vertices
const modulo = tileDimensions.x / tileOwnerDimensions.x / (17 - 1);
let u = Math.floor(uv.x / modulo) * modulo;
let v = Math.floor(uv.y / modulo) * modulo;
if (u == 1) {
u -= modulo;
}
if (v == 1) {
v -= modulo;
}
// Build 4 vertices, 3 of them will be our triangle:
// 11---21
// | / |
// | / |
// | / |
// 21---22
const u1 = u;
const u2 = u + modulo;
const v1 = v;
const v2 = v + modulo;
// Our multiple z-value will be weigh-blended, depending on the distance of the real point
// so lu (resp. lv) are the weight. When lu -> 0 (resp. 1) the final value -> z at u1 (resp. u2)
const lu = (uv.x - u) / modulo;
const lv = (uv.y - v) / modulo;
// Determine if we're going to read the vertices from the top-left or lower-right triangle
// (low-right = on the line 21-22 or under the diagonal lu = 1 - lv)
const tri = new THREE.Triangle(new THREE.Vector3(u1, v2), new THREE.Vector3(u2, v1), lv == 1 || lu / (1 - lv) >= 1 ? new THREE.Vector3(u2, v2) : new THREE.Vector3(u1, v1));
// bary holds the respective weight of each vertices of the triangles
tri.getBarycoord(new THREE.Vector3(uv.x, uv.y), bary);
const elevationLayer = layer.attachedLayers.filter(l => l.isElevationLayer)[0];
// read the 3 interesting values
const z1 = readTextureValueWithBilinearFiltering(elevationLayer, texture, tri.a.x, tri.a.y);
const z2 = readTextureValueWithBilinearFiltering(elevationLayer, texture, tri.b.x, tri.b.y);
const z3 = readTextureValueWithBilinearFiltering(elevationLayer, texture, tri.c.x, tri.c.y);
// Blend with bary
return z1 * bary.x + z2 * bary.y + z3 * bary.z;
}
const temp = {
v: new THREE.Vector3(),
coord1: new Coordinates('EPSG:4978'),
coord2: new Coordinates('EPSG:4978'),
offset: new THREE.Vector2()
};
const dimension = new THREE.Vector2();
function offsetInExtent(point, extent) {
let target = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : new THREE.Vector2();
if (point.crs != extent.crs) {
throw new Error(`Unsupported mix: ${point.crs} and ${extent.crs}`);
}
extent.planarDimensions(dimension);
const originX = (point.x - extent.west) / dimension.x;
const originY = (extent.north - point.y) / dimension.y;
return target.set(originX, originY);
}
function _readZ(layer, method, coord, nodes, cache) {
const pt = coord.as(layer.extent.crs, temp.coord1);
let tileWithValidElevationTexture = null;
// first check in cache
if (cache?.tile?.material) {
tileWithValidElevationTexture = tileAt(pt, cache.tile);
}
for (let i = 0; !tileWithValidElevationTexture && i < nodes.length; i++) {
tileWithValidElevationTexture = tileAt(pt, nodes[i]);
}
if (!tileWithValidElevationTexture) {
// failed to find a tile, abort
return;
}
const tile = tileWithValidElevationTexture;
const tileLayer = tile.material.getElevationLayer();
const src = tileLayer.textures[0];
// check cache value if existing
if (cache) {
if (cache.id === src.id && cache.version === src.version) {
return {
coord: pt,
texture: src,
tile
};
}
}
// Assuming that tiles are split in 4 children, we lookup the parent that
// really owns this texture
const stepsUpInHierarchy = Math.round(Math.log2(1.0 / tileLayer.offsetScales[0].z));
for (let i = 0; i < stepsUpInHierarchy; i++) {
tileWithValidElevationTexture = tileWithValidElevationTexture.parent;
}
// offset = offset from top-left
offsetInExtent(pt, tileWithValidElevationTexture.extent, temp.offset);
// At this point we have:
// - tileWithValidElevationTexture.texture.image which is the current image
// used for rendering
// - offset which is the offset in this texture for the coordinate we're
// interested in
// We now have 2 options:
// - the fast one: read the value of tileWithValidElevationTexture.texture.image
// at (offset.x, offset.y) and we're done
// - the correct one: emulate the vertex shader code
if (method == PRECISE_READ_Z) {
pt.z = _readZCorrect(layer, src, temp.offset, tile.extent.planarDimensions(), tileWithValidElevationTexture.extent.planarDimensions());
} else {
pt.z = _readZFast(layer, src, temp.offset);
}
if (pt.z != undefined) {
return {
coord: pt,
texture: src,
tile
};
}
}