Skip to content

Commit

Permalink
added(display): The dynamic coloring of the horizon is now possible w…
Browse files Browse the repository at this point in the history
…ith [LayerStyle.skyColor](https://heremaps.github.io/xyz-maps/docs/interfaces/core.layerstyle.html#skycolor), including support for gradients for smooth transitions from sky to horizon

improved(display): refined depth clipping to enhance visual quality at high pitch angles

Signed-off-by: Tim Deubler <tim.deubler@here.com>
  • Loading branch information
TerminalTim committed Nov 22, 2024
1 parent c069bb6 commit e2cc8b5
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 163 deletions.
16 changes: 16 additions & 0 deletions packages/core/src/styles/LayerStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import {Feature} from '../features/Feature';
import {Style} from './GenericStyle';
import {LinearGradient} from './HeatmapStyle';

/**
* A StyleExpression is a JSON array representing an expression that returns the desired value
Expand Down Expand Up @@ -344,6 +345,21 @@ export interface LayerStyle {
*/
backgroundColor?: Color | StyleZoomRange<Color> | ((zoomlevel: number) => Color) | StyleExpression<Color>;

/**
* Defines the sky color of the map.
* - The sky becomes visible when the map is pitched to higher angles.
* - When multiple layers with different sky colors are used, the sky color from the bottommost layer is applied.
* - Supports various formats, including:
* - `Color`: A single color applied across all zoom levels.
* - `StyleZoomRange<Color>`: Specifies a range of colors based on zoom levels.
* - `(zoomlevel: number) => Color`: A function that dynamically determines the color based on the current zoom level.
* - `StyleExpression<Color>`: An expression to compute color based on style parameters.
* - `LinearGradient`: Applies a gradient effect for smooth transitions in sky color.
* - When using `LinearGradient`, stops at `0` represent the color near the horizon (close to the ground), and stops at `1` represent the color in the sky at the top of the screen.
* - The full gradient (0 to 1) becomes fully visible only when the map pitch is set to maximum (85 degrees).
*/
skyColor?: Color | StyleZoomRange<Color> | ((zoomlevel: number) => Color) | StyleExpression<Color> | LinearGradient;

/**
* The `lights` property specifies a collection of light sources that can be used to illuminate features within the layer.
* It is a map where each key is a unique light group name, and the value is an array of light objects.
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/styles/XYZLayerStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
StyleGroupMap,
StyleValueFunction
} from '../styles/LayerStyle';
import {Expression, ExpressionMode, ExpressionParser} from '@here/xyz-maps-common';
import {Expression, ExpressionMode, ExpressionParser, Color as Colors} from '@here/xyz-maps-common';
import {TileLayer} from '../layers/TileLayer';

const isTypedArray = (() => {
Expand Down Expand Up @@ -106,6 +106,8 @@ export class XYZLayerStyle implements LayerStyle {

private _style: LayerStyle;

skyColor: Colors.Color;

/**
*
* @param styleJSON
Expand Down
6 changes: 3 additions & 3 deletions packages/display/src/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,16 +477,16 @@ export class Map {
if (pitch !== UNDEF) {
const maxPitch = this._cfg.maxPitch;
pitch = Math.max(0, Math.min(maxPitch, Math.round(pitch % 360 * 10) / 10));
const rad = -pitch * Math.PI / 180;
const rad = pitch * Math.PI / 180;
const rotX = this._rx;

if (rotX != rad) {
this._rx = rad;
this.updateGrid();
this._l.trigger('pitch', ['pitch', pitch, -rotX * 180 / Math.PI], true);
this._l.trigger('pitch', ['pitch', pitch, rotX * 180 / Math.PI], true);
}
}
return -this._rx * 180 / Math.PI;
return this._rx * 180 / Math.PI;
};

/**
Expand Down
44 changes: 12 additions & 32 deletions packages/display/src/displays/BasicDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {createZoomRangeFunction, parseColorMap} from './styleTools';
type RGBA = ColorUtils.RGBA;

const CREATE_IF_NOT_EXISTS = true;
const MAX_PITCH_GRID = 60 / 180 * Math.PI;

function toggleLayerEventListener(toggle: string, layer: any, listeners: any) {
toggle = toggle + 'EventListener';
Expand Down Expand Up @@ -348,9 +347,8 @@ abstract class Display {
}

setSize(w: number, h: number) {
var display = this;
var dpr = display.dpr;
var canvas = display.canvas;
const display = this;
const {dpr, canvas} = display;

display.w = w;
display.h = h;
Expand Down Expand Up @@ -434,24 +432,8 @@ abstract class Display {
return [this.w / 2, this.h / 2];
}

private clipGridHeight(maxPitch: number) {
const {rz, rx, s, _gridClip, centerWorld} = this;
// cache result for the current map transform
if (_gridClip.rx != rx || _gridClip.rz != rz
// || _gridClip.s != s
) {
_gridClip.rz = rz;
_gridClip.rx = rx;
// _gridClip.s = s;
// this.setTransform(s, rz, -maxPitch);
this.setView(centerWorld, s, rz, -maxPitch);

const topAtMaxPitch = this.unproject(this.w / 2, 0);
// this.setTransform(s, rz, rx);
this.setView(centerWorld, s, rz, rx);
_gridClip.top = this.project(topAtMaxPitch[0], topAtMaxPitch[1])[1];
}
return _gridClip.top;
protected pitchMapOffsetY() {
return 0;
}

updateGrid(tileGridZoom: number, zoomLevel: number, screenOffsetX: number, screenOffsetY: number) {
Expand All @@ -472,25 +454,23 @@ abstract class Display {
const mapWidthPixel = this.w;
const mapHeightPixel = this.h;
const displayWidth = mapWidthPixel;

// Be sure to also handle tiles that are not part of the actual viewport but whose data is still visible because of high altitude.
const displayHeight = Math.max(mapHeightPixel, this.getCamGroundPositionScreen()[1]);
const grid = this.grid;
let height = 0;

// if map is pitched too much, we clip the grid at the top
if (-this.rx > MAX_PITCH_GRID) {
height = this.clipGridHeight(MAX_PITCH_GRID);
}
const maxGridPitchOffset = this.pitchMapOffsetY();

// optimize gird if screen is rotated
let rotatedScreenPixels = [
display.unproject(0, height),
display.unproject(displayWidth - 1, height),
// Calculate the world pixel bounds of the grid based on effective display height
let gridWorldPixel = [
display.unproject(0, maxGridPitchOffset),
display.unproject(displayWidth - 1, maxGridPitchOffset),
display.unproject(displayWidth - 1, displayHeight - 1),
display.unproject(0, displayHeight - 1)
];

grid.init(centerWorldPixel, rotZRad, mapWidthPixel, mapHeightPixel, rotatedScreenPixels);
// Initialize the grid with the adjusted bounds
this.grid.init(centerWorldPixel, rotZRad, mapWidthPixel, mapHeightPixel, gridWorldPixel);

const layers = this.layers;
const tileSizes = layers.reset(tileGridZoom/* + Math.log(this.s) / Math.LN2*/);
Expand Down
92 changes: 77 additions & 15 deletions packages/display/src/displays/webgl/Display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,28 @@ import {createBuffer} from './buffer/createBuffer';
import {createImageBuffer} from './buffer/createImageBuffer';

import {transformMat4} from 'gl-matrix/vec3';
import {invert} from 'gl-matrix/mat4';
import {Layer, Layers, ScreenTile} from '../Layers';
import GLTile from './GLTile';
import {FeatureFactory} from './buffer/FeatureFactory';
import {CollisionHandler} from './CollisionHandler';
import {GeometryBuffer} from './buffer/GeometryBuffer';
import {AmbientLight, CustomLayer, DirectionalLight, TileLayer, XYZLayerStyle} from '@here/xyz-maps-core';
import {
CustomLayer,
LinearGradient,
TileLayer,
XYZLayerStyle,
Color
} from '@here/xyz-maps-core';
import {PASS} from './program/GLStates';
import {Raycaster} from './Raycaster';
import {defaultLightUniforms, initLightUniforms, ProcessedLights} from './lights';
import {UniformMap} from './program/Program';
import {Color as Colors} from '@here/xyz-maps-common';

const {toRGB} = Colors;

export const MAX_PITCH_GRID = 60 / 180 * Math.PI;

const PREVIEW_LOOK_AHEAD_LEVELS: [number, number] = [3, 9];

Expand Down Expand Up @@ -118,7 +130,15 @@ class WebGlDisplay extends BasicDisplay {
private worldCenter: number[] = [0, 0];
private worldSize: number;
private _zSortedTileBuffers: { tileBuffers: BufferData[], min3dZIndex: number, maxZIndex: number };

// Normalized maximum horizon Y-coordinate.
// This value represents the maximum vertical offset of the grid's horizon when maximum possible pitch applied.
// The offset is normalized by dividing it by the total screen height.
private maxHorizonY: number;
// Represents the world-space Y-coordinate of the topmost edge of the grid visible at the maximum pitch.
// maxPitchGridTopWorld is used to calculate `horizonY` when the map is pitched beyond the maximum grid pitch.
private maxPitchGridTopWorld: number[];
// vertical offset from top of the screen to the "horizon line" in screen pixels.
protected horizonY: number;
constructor(mapEl: HTMLElement, renderTileSize: number, devicePixelRatio: number | string, renderOptions?: RenderOptions) {
super(
mapEl,
Expand Down Expand Up @@ -203,11 +223,28 @@ class WebGlDisplay extends BasicDisplay {
this.render.processedLight[displayLayer.index] = processedLightSets;
}

addLayer(layer: TileLayer | CustomLayer, index: number, styles?: XYZLayerStyle): Layer {
private initSky(skyColor: Color | LinearGradient) {
let color;
if (this.factory.gradients.isGradient(skyColor)) {
color = this.factory.gradients.getTexture((<unknown>skyColor as LinearGradient));
} else {
color = toRGB(skyColor as Color);
}

this.render.setSkyColor(color);
}

addLayer(layer: TileLayer | CustomLayer, index: number, styles: XYZLayerStyle): Layer {
const displayLayer = super.addLayer(layer, index, styles);
// if (displayLayer) {
// this.initLights(displayLayer);
// }


if (displayLayer?.index == 0) {
// styles.skyColor

this.initSky(styles.skyColor || [1, 0, 0, 1]);
}
// this.initLights(displayLayer);

return displayLayer;
}

Expand All @@ -218,12 +255,12 @@ class WebGlDisplay extends BasicDisplay {
return super.removeLayer(layer);
}

unproject(x: number, y: number, z?): number[] {
const invScreenMat = this.render.invScreenMat;
unproject(x: number, y: number, z?: number, inverseMatrix?: Float32Array): number[] {
inverseMatrix ||= this.render.invScreenMat;

if (typeof z == 'number') {
const p = [x, y, z];
transformMat4(p, p, invScreenMat);
transformMat4(p, p, inverseMatrix);
p[2] *= -1;
return p;
}
Expand All @@ -234,8 +271,8 @@ class WebGlDisplay extends BasicDisplay {
const p0 = [x, y, 0];
const p1 = [x, y, 1];

transformMat4(p0, p0, invScreenMat);
transformMat4(p1, p1, invScreenMat);
transformMat4(p0, p0, inverseMatrix);
transformMat4(p1, p1, inverseMatrix);

const z0 = p0[2];
const z1 = p1[2];
Expand All @@ -246,21 +283,34 @@ class WebGlDisplay extends BasicDisplay {
}

// from unprojected screen pixels to projected screen pixels
project(x: number, y: number, z: number = 0, sx = this.sx, sy = this.sy): [number, number, number] {
project(
x: number,
y: number,
z: number = 0,
sx = this.sx,
sy = this.sy,
matrix: Float32Array | Float64Array = this.render.screenMat
): [number, number, number] {
// x -= screenOffsetX;
// y -= screenOffsetY;
// const p = [x, y, 0];
// const s = this.s;
// const p = [x * s, y * s, 0];
const p = [x - sx, y - sy, -z];
return transformMat4(p, p, this.render.screenMat);
return transformMat4(p, p, matrix);
// transformMat4(p, p, this.render.vPMats);
// return fromClipSpace(p, this.w, this.h);
}

setSize(w: number, h: number) {
super.setSize(w, h);
setSize(width: number, height: number) {
super.setSize(width, height);
this.initRenderer();

const matrix = this.render.updateMapGridMatrix(MAX_PITCH_GRID, width, height);
const inverseMatrix = invert([], matrix);
this.maxPitchGridTopWorld = this.unproject(0, 1, null, inverseMatrix);

this.maxHorizonY = this.pitchMapOffsetY(85 / 180 * Math.PI) / height;
}

setTransform(scale: number, rotZ: number, rotX: number) {
Expand Down Expand Up @@ -527,6 +577,15 @@ class WebGlDisplay extends BasicDisplay {
return this.project(cameraWorld[0], cameraWorld[1]);
}

protected pitchMapOffsetY(pitch: number = this.rx) {
const {w, h} = this;
const [x, maxPitchGridOffset] = this.maxPitchGridTopWorld;
const matrix = this.render.updateMapGridMatrix(pitch, w, h);
const y = this.project(x, maxPitchGridOffset, 0, 0, 0, matrix)[1];
this.horizonY = (1 - y) * h / 2;
return this.horizonY;
}

protected viewport(dirty?: boolean) {
const display = this;
const {buckets, layers, render} = display;
Expand All @@ -549,6 +608,9 @@ class WebGlDisplay extends BasicDisplay {
const layerBuffers = this.initLayerBuffers(layers);
const {tileBuffers, min3dZIndex, maxZIndex} = layerBuffers;


render.drawSky(this.horizonY, this.h, this.maxHorizonY);

render.zIndexLength = maxZIndex;

// fill the depthbuffer with real depth values for the ground plane.
Expand Down
Loading

0 comments on commit e2cc8b5

Please sign in to comment.