diff --git a/packages/core/src/styles/ImageStyle.ts b/packages/core/src/styles/ImageStyle.ts index bbdf8416b..a84c2ebf0 100644 --- a/packages/core/src/styles/ImageStyle.ts +++ b/packages/core/src/styles/ImageStyle.ts @@ -51,6 +51,12 @@ export interface ImageStyle { */ src: string | StyleValueFunction | StyleZoomRange; + /** + * If specified, the Image provided by {@link src} is considered as an IconAtlas/TextureAtlas. + * The clipping region for the image must be defined by x, y, width and height. + */ + atlas?: {x: number, y: number, width: number, height: number}; + /** * Defines the opacity of the style. * The value must be between 0.0 (fully transparent) and 1.0 (fully opaque). @@ -165,7 +171,7 @@ export interface ImageStyle { repeat?: number | StyleValueFunction | StyleZoomRange; /** - * Sets the anchor point for styles of type "Circle" used with Line or Polygon geometry. + * Sets the anchor point for styles of type "Image" used with Line or Polygon geometry. * * Possible values for Line geometry are "Coordinate" and "Line". * - "Coordinate": the respective style is displayed at each coordinate of the polyline. @@ -175,7 +181,7 @@ export interface ImageStyle { * - "Center": the center of the bounding box of the polygon. * - "Centroid": the geometric centroid of the polygon geometry. * - * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default is "Line""Coordinate". + * @defaultValue For Polygon geometry the default is "Center". For Line geometry the default is "Line". */ anchor?: 'Line' | 'Coordinate' | 'Centroid' diff --git a/packages/core/src/styles/LineStyle.ts b/packages/core/src/styles/LineStyle.ts index 52c28b6d2..1442f3a48 100644 --- a/packages/core/src/styles/LineStyle.ts +++ b/packages/core/src/styles/LineStyle.ts @@ -110,6 +110,13 @@ export interface LineStyle { */ strokeDasharray?: number[] | StyleValueFunction | StyleZoomRange | 'none'; + /** + * Specifies the URL of the image to be rendered at the positions of the dashes. + * If strokeDashimage is defined, only the first dash and gap definition of the {@link strokeDasharry} pattern is used. + * The dashimage will be colored with the color defined in {@link stroke}. + */ + strokeDashimage?: string + /** * Define the starting position of a segment of the entire line in %. * A Segment allows to display and style parts of the entire line individually. diff --git a/packages/display/src/Map.ts b/packages/display/src/Map.ts index 931dc2132..c04a9fec6 100644 --- a/packages/display/src/Map.ts +++ b/packages/display/src/Map.ts @@ -45,6 +45,9 @@ import { import {FlightAnimator} from './animation/FlightAnimator'; import Copyright from './ui/copyright/Copyright'; import Logo from './ui/Logo'; +import {fillMap} from './displays/styleTools'; +import {toRGB} from './displays/webgl/color'; + const project = webMercator; // const alt2z = webMercator.alt2z; @@ -112,6 +115,9 @@ export class Map { return instances.slice(); } + static fillZoomMap = fillMap; + static toRGB = toRGB; + id: number; private readonly _el: HTMLElement; diff --git a/packages/display/src/displays/ImageResourceHandler.ts b/packages/display/src/displays/ImageLoader.ts similarity index 63% rename from packages/display/src/displays/ImageResourceHandler.ts rename to packages/display/src/displays/ImageLoader.ts index ced37e309..3fa5f2e4f 100644 --- a/packages/display/src/displays/ImageResourceHandler.ts +++ b/packages/display/src/displays/ImageLoader.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2022 HERE Europe B.V. + * Copyright (C) 2019-2023 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ function onLoad() { img._cbs = null; - if (size > MAX_IMAGE_SIZE) { + if (!img.skipAutoScale && size > MAX_IMAGE_SIZE) { // rescale image to fit max allowed source size const scale = MAX_IMAGE_SIZE / size; img = img._r[img.src] = createScaledImage(img, scale); @@ -59,19 +59,42 @@ function onLoad() { } } - -declare global { - interface HTMLImageElement { - ready: boolean - _cbs: { [key: string]: [(...args) => void, any[]] } - _r: ImgDataMap - } +export const loadImage = (url: string, skipAutoScale?): Promise => new Promise((resolve, reject) => { + let img = new Image(); + img.crossOrigin = 'Anonymous'; + // img.skipAutoScale = skipAutoScale; + img.addEventListener('load', () => { + let image: HTMLCanvasElement | HTMLImageElement = img; + const size = Math.max(img.width, img.height); + if (!skipAutoScale && size > MAX_IMAGE_SIZE) { + // rescale image to fit max allowed source size + const scale = MAX_IMAGE_SIZE / size; + image = createScaledImage(img, scale); + } else { + // workaround for chrome issue (bug!?) in case of base64 encoded svg image + // is send to texture with incorrect size. + _ctx.drawImage(img, 0, 0); + } + resolve(image); + }); + img.addEventListener('error', (err) => reject(err)); + img.src = url; +}); + + +interface Image extends HTMLImageElement{ + ready: boolean; + _cbs: { [key: string]: [(...args) => void, any[]] }; + _r: ImgDataMap; + skipAutoScale: boolean; } -type ImgDataMap = { [url: string]: HTMLImageElement }; -class ImageResourceHandler { - private imgData: ImgDataMap = {}; +type ImgDataMap = { [url: string]: Image }; + +export class ImageLoader { + imgData: ImgDataMap = {}; + private promises: any = {}; constructor() { } @@ -84,35 +107,31 @@ class ImageResourceHandler { return this.imgData[url]?.ready; }; - get(url: string, cb?: (img: HTMLImageElement, ...args) => void, cbID?: string, args?) { + get(url: string, cb?: (img: HTMLImageElement, ...args) => void, cbID?: string, args?, skipAutoScale?: boolean) { let resources = this.imgData; if (resources[url] == UNDEF) { - let img = resources[url] = new Image(); + let img = resources[url] = new Image() as Image; img.ready = false; img._cbs = {}; if (cb) { img._cbs[cbID] = [cb, args]; } img._r = resources; - img.crossOrigin = 'Anonymous'; + img.skipAutoScale = skipAutoScale; img.onload = onLoad; // if (img.decode) { // img.decode().then(() => onLoad(img)).catch((e) => {}); // } img.src = url; } else if (cb) { - // image is still getting loaded.. + // image is still getting loaded... if (!resources[url].ready) { resources[url]._cbs[cbID] = [cb, args]; } else { cb(resources[url], args); } } - return resources[url]; }; } - - -export default ImageResourceHandler; diff --git a/packages/display/src/displays/canvas/drawPoint.ts b/packages/display/src/displays/canvas/drawPoint.ts index 78b4e13e0..b89b3f9c4 100644 --- a/packages/display/src/displays/canvas/drawPoint.ts +++ b/packages/display/src/displays/canvas/drawPoint.ts @@ -18,9 +18,9 @@ */ import {getValue} from '../styleTools'; -import ImgResourceHandler from '../ImageResourceHandler'; +import {ImageLoader} from '../ImageLoader'; -const imgResources = new ImgResourceHandler(); +const imgResources = new ImageLoader(); const PI2 = 2 * Math.PI; const TO_RAD = Math.PI / 180; const MAX_CANVAS_SIZE = 512; diff --git a/packages/display/src/displays/styleTools.ts b/packages/display/src/displays/styleTools.ts index f7cfd111c..035d7e18d 100644 --- a/packages/display/src/displays/styleTools.ts +++ b/packages/display/src/displays/styleTools.ts @@ -128,7 +128,8 @@ const parseSizeValue = (size: string | number, float: boolean = false): [number, } } if (!float) { - value = value ^ 0; // no "float pixels" + value = Math.round(value as number || 0); // no "float pixels" + // value = value ^ 0; // no "float pixels" } return [value, unit]; }; @@ -496,9 +497,13 @@ const searchLerp = (map, search: number, parseSize: boolean = true) => { } return rawVal; }; -const fillMap = (map, searchMap, parseSizeValue: boolean) => { +export const fillMap = (searchMap, parseSizeValue: boolean, map = {}) => { + let fixedZoomMap = {}; + for (let zoom in searchMap) { + fixedZoomMap[Math.round(zoom)] = searchMap[zoom]; + } for (let zoom = 1; zoom <= 20; zoom++) { - map[zoom] = searchLerp(searchMap, zoom, parseSizeValue); + map[zoom] = searchLerp(fixedZoomMap, zoom, parseSizeValue); } return map; }; @@ -512,7 +517,7 @@ export const parseColorMap = (map: { [zoom: string]: RGBA }) => { }; export const createZoomRangeFunction = (map: StyleZoomRange, /* isFeatureContext?:boolean,*/ parseSizeValue?: boolean) => { - map = fillMap({}, map, parseSizeValue); + map = fillMap(map, parseSizeValue); // return new Function('f,zoom', `return (${JSON.stringify(map)})[zoom];`); const range = (feature, zoom: number) => { return map[zoom ?? feature]; @@ -544,7 +549,7 @@ const parseStyleGroup = (styleGroup: Style[]) => { const parseSizeValue = !allowedFloatProperties[name]; style[name] = createZoomRangeFunction(value, parseSizeValue); - // let map = fillMap({}, value, parseSizeValue); + // let map = fillMap(value, parseSizeValue, {}); // style[name] = createZoomRangeFunction(map); } } diff --git a/packages/display/src/displays/webgl/Atlas.ts b/packages/display/src/displays/webgl/Atlas.ts index 206451685..eb73547c3 100644 --- a/packages/display/src/displays/webgl/Atlas.ts +++ b/packages/display/src/displays/webgl/Atlas.ts @@ -83,7 +83,7 @@ class Atlas { return this.c.get(key); }; - init() { + private init() { let {texture, gl, maxSize, d} = this; if (!this.texture) { diff --git a/packages/display/src/displays/webgl/DashAtlas.ts b/packages/display/src/displays/webgl/DashAtlas.ts index 6f8449a22..63469602e 100644 --- a/packages/display/src/displays/webgl/DashAtlas.ts +++ b/packages/display/src/displays/webgl/DashAtlas.ts @@ -28,7 +28,7 @@ class DashAtlas { this.gl = gl; } - create(dashArray: DashArray) { + private create(dashArray: DashArray) { let size = dashArray.reduce((a, b) => a + b) * // double size for odd dasharray size to get repeating pattern @@ -62,7 +62,7 @@ class DashAtlas { }); } - get(dashArray: DashArray): SharedTexture { + get(dashArray: DashArray, dashImage?): SharedTexture { const id = String(dashArray); let dashData = this.data[id]; diff --git a/packages/display/src/displays/webgl/Display.ts b/packages/display/src/displays/webgl/Display.ts index 46c419cee..97d879b59 100644 --- a/packages/display/src/displays/webgl/Display.ts +++ b/packages/display/src/displays/webgl/Display.ts @@ -68,27 +68,27 @@ const stencilQuad = (quadkey: string, subQuadkey: string) => { export type TileBufferData = { - z: number; - tiled: true; - b: GeometryBuffer; - data: { - tile: ScreenTile; - preview?: [string, number, number, number, number, number, number, number, number]; - previewTile?: GLTile; - stencils?; - }; + z: number; + tiled: true; + b: GeometryBuffer; + data: { + tile: ScreenTile; + preview?: [string, number, number, number, number, number, number, number, number]; + previewTile?: GLTile; + stencils?; + }; }; type CustomBufferData = { - z: number; - tiled: boolean; - b: { - zLayer?: number; - zIndex?: number; - pass?: number; - flat: boolean; - }; - data: CustomLayer; + z: number; + tiled: boolean; + b: { + zLayer?: number; + zIndex?: number; + pass?: number; + flat: boolean; + }; + data: CustomLayer; }; type BufferData = CustomBufferData | TileBufferData; @@ -138,7 +138,7 @@ class WebGlDisplay extends BasicDisplay { this.rayCaster = new Raycaster(render.screenMat, render.invScreenMat); - this.factory = new FeatureFactory(render.gl, render.icons, this.collision, this.dpr); + this.factory = new FeatureFactory(render.gl, this.collision, this.dpr); } private refreshTile(quadkey: string, layerId: string) { @@ -206,15 +206,15 @@ 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] { - // x -= screenOffsetX; - // y -= screenOffsetY; - // const p = [x, y, 0]; - // const s = this.s; - // const p = [x * s, y * s, 0]; + // 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); - // transformMat4(p, p, this.render.vPMats); - // return fromClipSpace(p, this.w, this.h); + // transformMat4(p, p, this.render.vPMats); + // return fromClipSpace(p, this.w, this.h); } setSize(w: number, h: number) { @@ -224,14 +224,14 @@ class WebGlDisplay extends BasicDisplay { } setTransform(scale: number, rotZ: number, rotX: number) { - // if (this.s != scale || this.rz != rotZ || this.rx != rotX) - // { + // if (this.s != scale || this.rz != rotZ || this.rx != rotX) + // { const PI2 = 2 * Math.PI; rotZ = (rotZ + PI2) % PI2; this.s = scale; this.rz = rotZ; this.rx = rotX; - // } + // } } setView( diff --git a/packages/display/src/displays/webgl/GLRender.ts b/packages/display/src/displays/webgl/GLRender.ts index 5c2e43f64..165a888b8 100644 --- a/packages/display/src/displays/webgl/GLRender.ts +++ b/packages/display/src/displays/webgl/GLRender.ts @@ -20,7 +20,6 @@ import BasicRender from '../BasicRender'; import {CustomLayer, Tile, TileLayer} from '@here/xyz-maps-core'; import GLTile from './GLTile'; -import {IconManager} from './IconManager'; import {RGBA, toRGB} from './color'; import RectProgram from './program/Rect'; @@ -45,7 +44,20 @@ import {PASS} from './program/GLStates'; import {TileBufferData} from './Display'; import {transformMat4} from 'gl-matrix/vec3'; -import {clone, copy, create, identity, invert, lookAt, multiply, perspective, rotateX, rotateZ, scale, translate} from 'gl-matrix/mat4'; +import { + clone, + copy, + create, + identity, + invert, + lookAt, + multiply, + perspective, + rotateX, + rotateZ, + scale, + translate +} from 'gl-matrix/mat4'; import BasicTile from '../BasicTile'; import {Attribute} from './buffer/Attribute'; import {GLExtensions} from './GLExtensions'; @@ -77,7 +89,6 @@ export type RenderOptions = WebGLContextAttributes; export type BufferCache = WeakMap; export class GLRender implements BasicRender { - icons: IconManager; readonly vPMat: Float32Array; // projection matrix private readonly vMat: Float32Array; // view matrix private readonly invVPMat: Float32Array; // inverse projection matrix @@ -88,10 +99,10 @@ export class GLRender implements BasicRender { cameraWorld: Float64Array = new Float64Array(3); private tilePreviewTransform: { - m: Float32Array; // tile transformation matrix, - tx: number; // translate x - ty: number; // translate y - s: number; // scale + m: Float32Array; // tile transformation matrix, + tx: number; // translate x + ty: number; // translate y + s: number; // scale }; private zMeterToPixel: number; @@ -194,9 +205,9 @@ export class GLRender implements BasicRender { } setBackgroundColor(color: RGBA) { - // this.clearColor = color; + // this.clearColor = color; if (this.gl) { - this.gl.clearColor(color[0], color[1], color[2], 1.0); + this.gl.clearColor(color[0], color[1], color[2], color[3] || 1.0); } } @@ -249,10 +260,6 @@ export class GLRender implements BasicRender { this.depthBufferSize = 1 << gl.getParameter(gl.DEPTH_BITS); - const texUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); - - this.icons = new IconManager(gl, texUnits - 2); - const programConfig = this.programConfig = { Rect: {program: RectProgram}, Line: {program: LineProgram}, @@ -576,7 +583,7 @@ export class GLRender implements BasicRender { program: Program, buffer: GeometryBuffer, renderPass: PASS, - sharedUniforms = this.sharedUniforms + useStencil: boolean|number = true ) { const bufAttributes = buffer.getAttributes(); this.useProgram(program); @@ -586,12 +593,12 @@ export class GLRender implements BasicRender { program.initGeometryBuffer(buffer, renderPass, // only use stencil when needed... no need if map is untransformed // Boolean(this.rx || this.rz), - true, + useStencil, this.zIndex ); program.initAttributes(bufAttributes); - program.initUniforms(sharedUniforms); + program.initUniforms(this.sharedUniforms); program.initUniforms(buffer.uniforms); } @@ -628,7 +635,13 @@ export class GLRender implements BasicRender { sharedUniforms.u_matrix = pMat || this.vPMat; sharedUniforms.u_zMeterToPixel = this.zMeterToPixel / dZoom; - if (buffer.scissor) { + const uses2PassAlpha = buffer.pass & PASS.POST_ALPHA; + // 2 pass alpha requires stencil usage. otherwise only needed when buffer scissors. + let useStencil = (uses2PassAlpha ?? pass == PASS.POST_ALPHA) || buffer.scissor; + // for flat buffers we can use tile stenciling as well. + useStencil &&= buffer.isFlat() ? this.stencilVal : true; + + if (typeof useStencil == 'number') { this.drawStencil(x, y, dZoom/* ,buffer.type=='Image'*/); } @@ -642,9 +655,7 @@ export class GLRender implements BasicRender { // const depth = 1 - (1 + zIndex) / (1 << 16); gl.depthRange(buffer.flat ? depth : 0, depth); - - this.initProgram(program, buffer, pass, this.sharedUniforms); - + this.initProgram(program, buffer, pass, useStencil); if (isOnTopOf3d) { gl.disable(gl.DEPTH_TEST); @@ -666,7 +677,7 @@ export class GLRender implements BasicRender { }; private drawStencil(x: number, y: number, zoom: number/* , snapGrid: boolean*/) { - // return this.gl.stencilFunc(this.gl.ALWAYS, 0, 0); + // return this.gl.stencilFunc(this.gl.ALWAYS, 0, 0); const refVal = this.stencilVal; const {gl, stencilTile, sharedUniforms} = this; const program: Program = this.getProgram(stencilTile); @@ -684,7 +695,10 @@ export class GLRender implements BasicRender { sharedUniforms.u_topLeft[0] = x + position[0] * tileSize; sharedUniforms.u_topLeft[1] = y + position[1] * tileSize; - this.initProgram(program, stencilTile, this.pass); + this.initProgram(program, stencilTile, this.pass, false); + + gl.enable(gl.STENCIL_TEST); + program.draw(stencilTile); } gl.stencilFunc(gl.EQUAL, refVal, 0xff); @@ -697,13 +711,13 @@ export class GLRender implements BasicRender { private scissorSize: number; private initScissor(x: number, y: number, size: number, matrix: Float32Array) { - // return this.gl.scissor(0, 0, this.gl.canvas.width, this.gl.canvas.height); + // return this.gl.scissor(0, 0, this.gl.canvas.width, this.gl.canvas.height); const {gl} = this; const w = gl.canvas.width; const h = gl.canvas.height; if (this.scale > 4.0 // workaround: precision issues for 22+ zooms -> disable scissor - || -this.rx > MAX_PITCH_SCISSOR // high pitch, part of tile is "behind" the cam, plane "flips" -> skip scissor. + || -this.rx > MAX_PITCH_SCISSOR // high pitch, part of tile is "behind" the cam, plane "flips" -> skip scissor. ) { gl.scissor(0, 0, w, h); this.scissorX = null; @@ -785,7 +799,7 @@ export class GLRender implements BasicRender { const previewTransformMatrix = this.initPreviewMatrix(x, y, scale); if (buffer.scissor) { - this.initScissor( x + dx, y + dy, tileSize * scale, this.vPMat); + this.initScissor(x + dx, y + dy, tileSize * scale, this.vPMat); // this.initScissor(x - sx * scale, y - sy * scale, dWidth * scale, this.vPMat); // this.initScissor(px, py, tileSize, previewTransformMatrix); } @@ -820,7 +834,6 @@ export class GLRender implements BasicRender { } destroy(): void { - this.icons.destroy(); } prepare(INSTRUCTIONS: any, tile: Tile, layer: TileLayer, display: any, dTile: BasicTile, cb: () => void): void { diff --git a/packages/display/src/displays/webgl/IconAtlas.ts b/packages/display/src/displays/webgl/IconAtlas.ts new file mode 100644 index 000000000..96eec2654 --- /dev/null +++ b/packages/display/src/displays/webgl/IconAtlas.ts @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019-2022 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {ImageLoader} from '../ImageLoader'; +import {Atlas, ImageInfo} from './Atlas'; +import {Texture} from './Texture'; + +class IconAtlas { + private loader = new ImageLoader(); + private atlas: Atlas; + private textures: Map = new Map(); + private promises = {}; + private gl: WebGLRenderingContext; + + constructor(gl: WebGLRenderingContext) { + this.gl = gl; + this.atlas = new Atlas({gl, maxImgSize: 64}); + } + + getTexture(src): Texture { + return this.textures.get(src); + } + + load(src: string, imageInfo?): ImageInfo | Promise { + const {atlas, loader, promises} = this; + + if (!imageInfo) { + return atlas.get(src) || (promises[src] ||= new Promise((resolve, reject) => { + loader.get(src, (img) => { + delete promises[src]; + imageInfo = atlas.set(src, img); + this.textures.set(src, atlas.texture); + resolve(imageInfo); + }); + })); + } else { + return (promises[src] ||= new Promise((resolve, reject) => { + loader.get(src, (img) => { + delete promises[src]; + this.textures.set(src, new Texture(this.gl, img)); + resolve(imageInfo); + }); + })); + } + } + + destroy() { + this.atlas.destroy(); + } +} + +export {IconAtlas}; diff --git a/packages/display/src/displays/webgl/IconManager.ts b/packages/display/src/displays/webgl/IconManager.ts deleted file mode 100644 index b4671fc96..000000000 --- a/packages/display/src/displays/webgl/IconManager.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2019-2022 HERE Europe B.V. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * License-Filename: LICENSE - */ - -import ImageResourceHandler from '../ImageResourceHandler'; -import {Atlas, ImageInfo} from './Atlas'; -import {Texture} from './Texture'; - -class IconManager { - private images = new ImageResourceHandler(); - private atlas: Atlas; - - // onLoad: (info: ImageInfo) => void; - - constructor(gl: WebGLRenderingContext, tu: number) { - this.atlas = new Atlas({ - gl: gl, - maxImgSize: 64 - }); - } - - getTexture(): Texture { - return this.atlas.texture; - } - - // get(src: string, width?: number, height?: number, readyCb?: (atlasInfo: ImageInfo) => void): ImageInfo | false { - // const {atlas, images, onLoad} = this; - // let info = atlas.get(src); - // - // if (!info) { - // const img = images.get(src, (img) => { - // info = atlas.set(src, img); - // // let old = atlas.get(src); - // if (readyCb) { - // readyCb(info); - // } - // if (onLoad) { - // onLoad(info); - // } - // }); - // - // if (!img.ready) { - // // debugger; - // console.log('imgReady', img.ready, info); - // return false; - // // set empty dummy until real image is ready... - // // info = atlas.set(src, {width: width, height: height}); - // } - // } - // - // return info; - // } - - private promises = {}; - get(src: string, width?: number, height?: number, readyCb?: (atlasInfo: ImageInfo) => void): ImageInfo | Promise { - const {atlas, images, promises} = this; - return atlas.get(src) || (promises[src] ||= new Promise((resolve, reject)=>{ - images.get(src, (img) => { - delete promises[src]; - const info = atlas.set(src, img); - readyCb?.(info); - resolve(info); - }); - })); - } - - destroy() { - this.atlas.destroy(); - } -} - -export {IconManager}; diff --git a/packages/display/src/displays/webgl/TextureAtlasManager.ts b/packages/display/src/displays/webgl/TextureAtlasManager.ts new file mode 100644 index 000000000..1175e74a8 --- /dev/null +++ b/packages/display/src/displays/webgl/TextureAtlasManager.ts @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2019-2023 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import {ImageLoader} from '../ImageLoader'; +import {Atlas, ImageInfo} from './Atlas'; +import {Texture, TextureOptions} from './Texture'; + +export class TextureAtlasManager { + private loader = new ImageLoader(); + private atlas: Atlas; + private textures: Map = new Map(); + private promises: Map = new Map(); + private gl: WebGLRenderingContext; + + constructor(gl: WebGLRenderingContext) { + this.gl = gl; + this.atlas = new Atlas({gl, maxImgSize: 64}); + } + + getTexture(src): Texture { + const texture = this.textures.get(src); + texture.ref++; + return texture; + } + + load(src, textureOptions?: TextureOptions) { + const {loader, promises} = this; + return this.textures.get(src) ?? (promises[src] ||= new Promise((resolve, reject) => { + loader.get(src, (img) => { + delete promises[src]; + const texture = new Texture(this.gl, img, textureOptions); + texture.ref = Infinity; + // texture.onDestroyed = () => { + // console.log('deleting texture', src); + // this.textures.delete(src); + // }; + this.textures.set(src, texture); + resolve(texture); + }, '*', undefined, true); + })); + } + + loadAtlas(src: string, atlasInfo?: { x: number, y: number, width: number, height: number }): ImageInfo | Promise { + const {atlas, loader, promises} = this; + // let key = atlasInfo ? src+JSON.stringify(atlasInfo) : src; + if (!atlasInfo) { + return atlas.get(src) || (promises[src] ||= new Promise((resolve, reject) => { + loader.get(src, (img) => { + delete promises[src]; + let imageInfo = atlas.set(src, img); + this.textures.set(src, atlas.texture); + resolve(imageInfo); + }); + })); + } else { + if (this.textures.has(src)) { + return new ImageInfo(0, atlasInfo.x, atlasInfo.x + atlasInfo.width, atlasInfo.y, atlasInfo.y + atlasInfo.height); + } + return this.load(src); + } + } + + destroy() { + this.atlas.destroy(); + } +} diff --git a/packages/display/src/displays/webgl/buffer/FeatureFactory.ts b/packages/display/src/displays/webgl/buffer/FeatureFactory.ts index e87c39d1b..9d79d66bc 100644 --- a/packages/display/src/displays/webgl/buffer/FeatureFactory.ts +++ b/packages/display/src/displays/webgl/buffer/FeatureFactory.ts @@ -26,9 +26,8 @@ import {calcBBox, getTextString, getValue, parseSizeValue, Style, StyleGroup} fr import {defaultFont, wrapText} from '../../textUtils'; import {FontStyle, GlyphTexture} from '../GlyphTexture'; import {toRGB} from '../color'; -import {IconManager} from '../IconManager'; import {DashAtlas} from '../DashAtlas'; -import {CollisionData, CollisionHandler} from '../CollisionHandler'; +import {BBox, CollisionData, CollisionHandler} from '../CollisionHandler'; import {LineFactory} from './LineFactory'; import {TextBuffer} from './templates/TextBuffer'; @@ -37,7 +36,7 @@ import {PointBuffer} from './templates/PointBuffer'; import {PolygonBuffer} from './templates/PolygonBuffer'; import {ExtrudeBuffer} from './templates/ExtrudeBuffer'; import {toPresentationFormB} from '../arabic'; -import {Tile, Feature, GeoJSONCoordinate, TextStyle} from '@here/xyz-maps-core'; +import {Tile, Feature, GeoJSONCoordinate, TextStyle, ImageStyle} from '@here/xyz-maps-core'; import {TemplateBuffer} from './templates/TemplateBuffer'; import {addVerticalLine} from './addVerticalLine'; import {BoxBuffer} from './templates/BoxBuffer'; @@ -51,6 +50,8 @@ import {ModelBuffer} from './templates/ModelBuffer'; import {ImageInfo} from '../Atlas'; import {GradientFactory} from '../GradientFactory'; import {HeatmapBuffer} from './templates/HeatmapBuffer'; +import {TextureAtlasManager} from '../TextureAtlasManager'; +import {LineBuffer} from './templates/LineBuffer'; const DEFAULT_STROKE_WIDTH = 1; const DEFAULT_LINE_CAP = 'round'; @@ -60,64 +61,65 @@ const NONE = '*'; let UNDEF; export type CollisionGroup = { - id: string; - styleGrp: Style[]; - priority: number; - repeat: number; - bbox: number[]; - - feature?: Feature; - geomType?: string; - coordinates?: any; - - offsetX?: number; - offsetY?: number; - width?: number; - height?: number; + id: string; + styleGrp: Style[]; + priority: number; + repeat: number; + bbox: number[]; + + feature?: Feature; + geomType?: string; + coordinates?: any; + + offsetX?: number; + offsetY?: number; + width?: number; + height?: number; }; type DrawGroup = { - type: string; - zLayer: number; - depthTest: boolean; - shared: { - unit: string; - font: string; - fill: Float32Array; - // fill: Float32Array|LinearGradient; - opacity: number; - stroke: Float32Array; - strokeWidth: number; - strokeLinecap: string; - strokeLinejoin: string; - strokeDasharray: number[]; - width: number; - height: number; - depth: number; - rotation: number; - offsetX: number; - offsetY: number; - offsetZ: number; - offsetUnit: string; - alignment: string; - modelMode: number; - scaleByAltitude: boolean; - }; - buffer?: TemplateBuffer | TemplateBufferBucket; - extrudeStrokeIndex?: number[]; - pointerEvents?: boolean; + type: string; + zLayer: number; + depthTest: boolean; + shared: { + unit: string; + font: string; + fill: Float32Array; + // fill: Float32Array|LinearGradient; + opacity: number; + stroke: Float32Array; + strokeWidth: number; + strokeLinecap: string; + strokeLinejoin: string; + strokeDasharray: number[]; + strokeDashimage: string; + width: number; + height: number; + depth: number; + rotation: number; + offsetX: number; + offsetY: number; + offsetZ: number; + offsetUnit: string; + alignment: string; + modelMode: number; + scaleByAltitude: boolean; + }; + buffer?: TemplateBuffer | TemplateBufferBucket; + extrudeStrokeIndex?: number[]; + pointerEvents?: boolean; }; type ZDrawGroup = { - index: { [grpId: string]: number }; - groups: DrawGroup[]; + index: { [grpId: string]: number }; + groups: DrawGroup[]; }; export type GroupMap = { [zIndex: string]: ZDrawGroup }; export class FeatureFactory { private readonly gl: WebGLRenderingContext; - private icons: IconManager; + private atlasManager: TextureAtlasManager; private dpr: number; private dashes: DashAtlas; private tile: Tile; @@ -131,9 +133,9 @@ export class FeatureFactory { private waitAndRefresh: (p: Promise) => void; gradients: GradientFactory; - constructor(gl: WebGLRenderingContext, iconManager: IconManager, collisionHandler, devicePixelRatio: number) { + constructor(gl: WebGLRenderingContext, collisionHandler, devicePixelRatio: number) { this.gl = gl; - this.icons = iconManager; + this.atlasManager = new TextureAtlasManager(gl); this.dpr = devicePixelRatio; this.dashes = new DashAtlas(gl); this.collisions = collisionHandler; @@ -273,8 +275,8 @@ export class FeatureFactory { const src = getValue('src', style, feature, level); const width = getValue('width', style, feature, level); const height = getValue('height', style, feature, level) || width; - - const img = this.icons.get(src, width, height); + const atlas = (style as ImageStyle).atlas; + const img = this.atlasManager.loadAtlas(src, atlas); if ((>img).then) { return this.waitAndRefresh(>img); @@ -286,16 +288,16 @@ export class FeatureFactory { x, y, z, - img, - width, - height, - flexAttributes.a_size.data, - positionBuffer.data, - flexAttributes.a_texcoord.data, - rotationZ + img, + width, + height, + flexAttributes.a_size.data, + positionBuffer.data, + flexAttributes.a_texcoord.data, + rotationZ ); - groupBuffer.addUniform('u_texture', this.icons.getTexture()); - // group.texture = this.icons.getTexture(); + + groupBuffer.addUniform('u_texture', this.atlasManager.getTexture(src)); } else if (type == 'Circle' || type == 'Rect') { const pointBuffer = ((group.buffer as PointBuffer) ||= new PointBuffer(isFlat)); positionBuffer = pointBuffer.flexAttributes.a_position; @@ -332,7 +334,6 @@ export class FeatureFactory { return; } - collisionBufferStop = positionBuffer?.data.length; collisionBufferStart = collisionBufferStop - 12; } @@ -377,6 +378,7 @@ export class FeatureFactory { let strokeAlpha; let strokeWidth; let strokeDasharray; + let strokeDashimage; let strokeLinecap; let strokeLinejoin; let extrude; @@ -457,7 +459,7 @@ export class FeatureFactory { if ( opacity == UNDEF || - opacity >= 0.98 // no alpha visible -> no need to use more expensive alpha pass + opacity >= 0.98 // no alpha visible -> no need to use more expensive alpha pass ) { opacity = 1; } @@ -475,6 +477,7 @@ export class FeatureFactory { strokeAlpha = 1; strokeWidth = UNDEF; strokeDasharray = UNDEF; + strokeDashimage = UNDEF; strokeLinecap = UNDEF; strokeLinejoin = UNDEF; width = UNDEF; @@ -523,28 +526,35 @@ export class FeatureFactory { strokeLinecap = getValue('strokeLinecap', style, feature, level) || DEFAULT_LINE_CAP; strokeLinejoin = getValue('strokeLinejoin', style, feature, level) || DEFAULT_LINE_JOIN; + + const offset = getValue('offset', style, feature, level); + // store line offset/unit in shared offsetXY + [offsetX, offsetUnit] = parseSizeValue(offset); + + groupId = + (altitude ? 'AL' : 'L') + + sizeUnit + + offsetX + + offsetUnit + + strokeLinecap + + strokeLinejoin; strokeDasharray = getValue('strokeDasharray', style, feature, level); if (Array.isArray(strokeDasharray)) { if (!strokeDasharray.length || !strokeDasharray[0]) { strokeDasharray = UNDEF; } + + groupId += strokeDasharray; + + strokeDashimage = getValue('strokeDashimage', style, feature, level); + + if (strokeDashimage) { + groupId += strokeDashimage.slice(-8); + } } else { strokeDasharray = UNDEF; } - - const offset = getValue('offset', style, feature, level); - // store line offset/unit in shared offsetXY - [offsetX, offsetUnit] = parseSizeValue(offset); - - groupId = - (altitude ? 'AL' : 'L') + - sizeUnit + - offsetX + - offsetUnit + - strokeLinecap + - strokeLinejoin + - (strokeDasharray || NONE); } else { fill = getValue('fill', style, feature, level); @@ -582,7 +592,6 @@ export class FeatureFactory { if (alignment == UNDEF) { alignment = geomType == 'Point' ? 'viewport' : 'map'; } - groupId = 'T' + (font || NONE); } else if (type == 'Circle') { width = height = getValue('radius', style, feature, level); @@ -740,6 +749,7 @@ export class FeatureFactory { strokeLinecap, strokeLinejoin, strokeDasharray, + strokeDashimage, width, height, depth, @@ -801,22 +811,34 @@ export class FeatureFactory { } } else if (geomType == 'LineString') { if (type == 'Line') { + if (strokeDashimage) { + const dashImgTexture = this.atlasManager.load(strokeDashimage, {mipMaps: false}); + if ((>dashImgTexture).then) { + this.waitAndRefresh(>dashImgTexture); + return; + } + } + let vertexLength = this.lineFactory.createLine( - coordinates, - group, - tile, - tileSize, - removeTileBounds, - strokeDasharray, - strokeLinecap, - strokeLinejoin, - strokeWidth, - altitude, - offsetX, - getValue('from', style, feature, level), - getValue('to', style, feature, level) + coordinates, + group, + tile, + tileSize, + removeTileBounds, + strokeDasharray, + strokeLinecap, + strokeLinejoin, + strokeWidth, + altitude, + offsetX, + getValue('from', style, feature, level), + getValue('to', style, feature, level) ); + if (strokeDashimage) { + (group.buffer as LineBuffer).addUniform('u_dashTexture', this.atlasManager.getTexture(strokeDashimage)); + } + group.buffer.setIdOffset(feature.id); } else { let isText = type == 'Text'; diff --git a/packages/display/src/displays/webgl/buffer/addLineString.ts b/packages/display/src/displays/webgl/buffer/addLineString.ts index 0b187cd8a..03bd48aa6 100644 --- a/packages/display/src/displays/webgl/buffer/addLineString.ts +++ b/packages/display/src/displays/webgl/buffer/addLineString.ts @@ -31,11 +31,11 @@ const SCALE = 8191; const TILE_CLIP_MARGIN = 16; enum OutCode { - INSIDE = 0, // 0000 - LEFT = 1, // 0001 - RIGHT = 2, // 0010 - BOTTOM = 4, // 0100 - TOP = 8 // 1000 + INSIDE = 0, // 0000 + LEFT = 1, // 0001 + RIGHT = 2, // 0010 + BOTTOM = 4, // 0100 + TOP = 8 // 1000 } const computeOutCode = (x: number, y: number, xmin: number, xmax: number, ymin: number, ymax: number): OutCode => { @@ -155,7 +155,7 @@ const addLineString = ( cap: Cap, join: Join, strokeWidth: number, - lengthToVertex?: number[] | false, + lengthToVertex?: FlexArray | false, isRing?: boolean, offset?: number, relStart = 0, @@ -163,7 +163,6 @@ const addLineString = ( ): number => { strokeWidth *= .5; - // join = 'none' const clipOnTileEdges = !!height; const includeHeight = clipOnTileEdges; if (height === true && dimensions == 2) { @@ -204,10 +203,7 @@ const addLineString = ( if (lengthToVertex) { cap = 'butt'; - if (!offset) { - // keep meter join for offset dashed lines - join = 'none'; - } + join = 'miter'; } let outCode0 = computeOutCode(coordinates[0], coordinates[1], tileMin, tileMax, tileMin, tileMax); @@ -333,7 +329,7 @@ const addSegments = ( capStop: Cap, join: Join, strokeWidth: number, - lengthToVertex: number[] | false, + lengthToVertex: FlexArray | false, absStart?: number, absStop?: number, offset?: number, @@ -539,7 +535,9 @@ const addSegments = ( } else { // >2 -> >90deg // bisectorExceeds = bisectorLength > 2; - bisectorExceeds = bisectorLength > 3; + bisectorExceeds = lengthToVertex + ? true // prevent dasharray distortion + : bisectorLength > 3; // if angle is to sharp and bisector length goes to infinity we cut the cone // // bisectorLength > 10 behaves exactly like canvas2d.. // // ..but we cut earlier to prevent "cone explosion" @@ -574,6 +572,13 @@ const addSegments = ( p1Up = nUp; p2Up = nUp; + // nUp = [-nx << 1 | 0, ny << 1 | 1]; + // nDown = [nUp[0], nUp[1] ^ 1]; + // p1Down = [-nx << 1 | 0, nDown[1]]; + // p2Down = [-nx << 1 | 1, nDown[1]]; + // p1Up = [-nx << 1 | 0, nUp[1]]; + // p2Up = [-nx << 1 | 1, nUp[1]]; + if (join != 'none') { if (!last && curJoin == JOIN_MITER && vLength > 2 * dimensions /** 4**/) { p2Down = [ex << 1 | 0, ey << 1 | 0]; @@ -583,12 +588,12 @@ const addSegments = ( if (!first && !prevBisectorExceeds) { if (prevLeft) { p1Up = [prevEx << 1 | 1, prevEy << 1 | 1]; - if (join == 'miter') { + if (join == JOIN_MITER) { p1Down = [prevEx << 1 | 0, prevEy << 1 | 0]; // miter } } else { p1Down = [prevEx << 1 | 0, prevEy << 1 | 0]; - if (join == 'miter') { + if (join == JOIN_MITER) { p1Up = [prevEx << 1 | 1, prevEy << 1 | 1]; // miter } } @@ -675,6 +680,7 @@ const addSegments = ( } else { vertex.push(prevX2, prevY2, prevX2, prevY2, prevX2, prevY2); } + (lengthToVertex as FlexArray)?.push(prevLengthSoFar, prevLengthSoFar, prevLengthSoFar); } if (!first) { @@ -727,7 +733,7 @@ const addSegments = ( } else { vertex.push(prevX2, prevY2, prevX2, prevY2, prevX2, prevY2); } - + (lengthToVertex as FlexArray)?.push(prevLengthSoFar, prevLengthSoFar, prevLengthSoFar); // if (hasZ && join == JOIN_BEVEL) { // if (z1 != z2 && join == JOIN_BEVEL) { @@ -807,6 +813,7 @@ const addSegments = ( } else { vertex.push(prevX2, prevY2, prevX2, prevY2, prevX2, prevY2); } + (lengthToVertex as FlexArray)?.push(prevLengthSoFar, prevLengthSoFar, prevLengthSoFar); } } } @@ -864,13 +871,10 @@ const addSegments = ( addCap(capStop, x2, y2, includeHeight && z2, -nx, -ny, vertex, normal); } - if (lengthToVertex) { - const prevLengthSoFar = lengthSoFar - length; - lengthToVertex.push( - prevLengthSoFar, lengthSoFar, prevLengthSoFar, - prevLengthSoFar, lengthSoFar, lengthSoFar - ); - } + (lengthToVertex as FlexArray)?.push( + prevLengthSoFar, lengthSoFar, prevLengthSoFar, + prevLengthSoFar, lengthSoFar, lengthSoFar + ); } skipFirstSegment = false; diff --git a/packages/display/src/displays/webgl/buffer/createBuffer.ts b/packages/display/src/displays/webgl/buffer/createBuffer.ts index 719d8e7ee..8c6b7bb99 100644 --- a/packages/display/src/displays/webgl/buffer/createBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/createBuffer.ts @@ -197,7 +197,13 @@ const createBuffer = ( if (type == 'Line') { if (shared.strokeDasharray) { geoBuffer.type = 'DashedLine'; - geoBuffer.addUniform('u_texWidth', (geoBuffer.uniforms.u_pattern as Texture).width); + + geoBuffer.addUniform('u_hasDashTexture', !!(geoBuffer.uniforms.u_dashTexture)); + geoBuffer.addUniform('u_dashSize', [ + (geoBuffer.uniforms.u_pattern as Texture).width, + shared.strokeDasharray[0], + shared.strokeDasharray[1] + ]); } geoBuffer.addUniform('u_fill', stroke); @@ -207,10 +213,11 @@ const createBuffer = ( shared.offsetUnit == 'm' ? meterToPixel : 0 ]); - geoBuffer.addUniform('u_no_antialias', !grpBuffer.isFlat()); + geoBuffer.addUniform('u_no_antialias', false); + // geoBuffer.addUniform('u_no_antialias', !grpBuffer.isFlat()); geoBuffer.pass = PASS.ALPHA; - if (!geoBuffer.flat) { + if (!geoBuffer.flat || shared.strokeDasharray || hasAlphaColor) { geoBuffer.pass |= PASS.POST_ALPHA; } geoBuffer.depth = geoBuffer.blend = true; @@ -249,7 +256,9 @@ const createBuffer = ( geoBuffer.addUniform('u_opacity', shared.opacity); } - geoBuffer.addUniform('u_atlasScale', 1 / (geoBuffer.uniforms.u_texture as Texture).width); + const texture = geoBuffer.uniforms.u_texture as Texture; + geoBuffer.addUniform('u_texSize', [texture.width, texture.height]); + geoBuffer.addUniform('u_alignMap', shared.alignment == 'map'); geoBuffer.pass = PASS.ALPHA; diff --git a/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts b/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts index e08af7c6d..1d1265ab4 100644 --- a/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/debugTileBuffer.ts @@ -159,7 +159,7 @@ const createGridTextBuffer = (quadkey: string, gl: WebGLRenderingContext, font) textBuffer.scissor = false; // textBuffer.texture = glyphs; textBuffer.addUniform('u_texture', glyphs); - textBuffer.addUniform('u_atlasScale', 1 / glyphs.width); + textBuffer.addUniform('u_texSize', [glyphs.width, glyphs.height]); textBuffer.addUniform('u_opacity', 1.0); textBuffer.addUniform('u_alignMap', true); textBuffer.addUniform('u_fillColor', toRGB(font.fill)); diff --git a/packages/display/src/displays/webgl/buffer/templates/SymbolBuffer.ts b/packages/display/src/displays/webgl/buffer/templates/SymbolBuffer.ts index 7152b517c..5f208e3db 100644 --- a/packages/display/src/displays/webgl/buffer/templates/SymbolBuffer.ts +++ b/packages/display/src/displays/webgl/buffer/templates/SymbolBuffer.ts @@ -41,7 +41,7 @@ export class SymbolBuffer extends PointBuffer { }, // bit1 -> bit5 - rotation low (x), rotation height (y) // bit6 -> bit16 - texture coordinate - // 10 bit rotation, 2 * 12 bit texture coordinate + // 10 bit rotation, 2 * 11 bit texture coordinate (2048x2048) // MSB LSB // |TCX|TCX|TCX|TCX|TCX|TCX|TCX|TCX|TCX|TCX|TCX|ROTL|ROTL|ROTL|ROTL|ROTL| // |TCY|TCY|TCY|TCY|TCY|TCY|TCY|TCY|TCY|TCY|TCY|ROTH|ROTH|ROTH|ROTH|ROTH| diff --git a/packages/display/src/displays/webgl/color.ts b/packages/display/src/displays/webgl/color.ts index c3d9a7e89..d13cc08e1 100644 --- a/packages/display/src/displays/webgl/color.ts +++ b/packages/display/src/displays/webgl/color.ts @@ -171,7 +171,6 @@ const HTML_COLOR_NAMES: { [color: string]: RGBA | string } = { yellowgreen: '9acd32' }; - const hexStringToRGBA = (hexString: string): RGBA => { const length = hexString.length; if (length < 5) { @@ -202,7 +201,6 @@ const hexToRGBA = (hex: number, alpha?: boolean): RGBA => { ]; }; - for (let name in HTML_COLOR_NAMES) { HTML_COLOR_NAMES[name] = hexStringToRGBA(HTML_COLOR_NAMES[name] as string); } @@ -218,8 +216,7 @@ const parseRGBAString = (color: string): RGBA => { ]; }; - -export const toRGB = (color: string | RGBA | number): RGBA => { +export const toRGB = (color: string | RGBA | number, ignoreNumbers?: boolean): RGBA => { let rgba; if (color) { if (Array.isArray(color)) { @@ -228,7 +225,9 @@ export const toRGB = (color: string | RGBA | number): RGBA => { rgba[3] = 1; } } else if (typeof color == 'number') { - rgba = hexToRGBA(color); + if (!ignoreNumbers) { + rgba = hexToRGBA(color); + } } else if (color[0] == '#') { rgba = hexStringToRGBA(color.slice(1)); } else { @@ -244,3 +243,5 @@ export const toRGB = (color: string | RGBA | number): RGBA => { } return rgba || INVALID_COLOR; }; + +toRGB.INVALID_COLOR = INVALID_COLOR; diff --git a/packages/display/src/displays/webgl/glsl/icon_vertex.glsl b/packages/display/src/displays/webgl/glsl/icon_vertex.glsl index 0a8a88ae4..6936742ab 100644 --- a/packages/display/src/displays/webgl/glsl/icon_vertex.glsl +++ b/packages/display/src/displays/webgl/glsl/icon_vertex.glsl @@ -7,7 +7,7 @@ attribute vec2 a_texcoord; uniform mat4 u_matrix; uniform vec2 u_topLeft; uniform float u_scale; -uniform float u_atlasScale; +uniform vec2 u_texSize; uniform vec4 u_offset; uniform vec2 u_offsetZ; uniform bool u_alignMap; @@ -60,6 +60,6 @@ void main(void){ } // textcoords bit6->bit16 - v_texcoord = floor(a_texcoord / 32.0) * u_atlasScale; + v_texcoord = floor(a_texcoord / 32.0) / u_texSize; } } diff --git a/packages/display/src/displays/webgl/glsl/line_dash_fragment.glsl b/packages/display/src/displays/webgl/glsl/line_dash_fragment.glsl deleted file mode 100644 index 52878d0ef..000000000 --- a/packages/display/src/displays/webgl/glsl/line_dash_fragment.glsl +++ /dev/null @@ -1,17 +0,0 @@ -precision lowp float; - -uniform vec4 u_fill; -varying vec2 v_normal; -varying vec2 v_width; -varying float v_lengthSoFar; - -uniform sampler2D u_pattern; - -void main(void){ - - float width = length(v_normal) * (v_width.s + v_width.t); - float alpha = 1.0 - clamp(.5 * (width - v_width.s + v_width.t) / v_width.t, .0, 1.); - - gl_FragColor = u_fill; - gl_FragColor.a *= alpha * texture2D(u_pattern, vec2(fract(v_lengthSoFar))).r; -} diff --git a/packages/display/src/displays/webgl/glsl/line_fragment.glsl b/packages/display/src/displays/webgl/glsl/line_fragment.glsl index 297e7924a..0b190c4e1 100644 --- a/packages/display/src/displays/webgl/glsl/line_fragment.glsl +++ b/packages/display/src/displays/webgl/glsl/line_fragment.glsl @@ -1,20 +1,48 @@ precision lowp float; -uniform sampler2D u_pattern; uniform vec4 u_fill; varying vec2 v_normal; +varying vec2 v_dir; +uniform bool u_no_antialias; + +#ifdef DASHARRAY +uniform highp float u_scale; +uniform sampler2D u_pattern; varying float v_lengthSoFar; -varying vec2 texCoord; +uniform sampler2D u_dashTexture; +uniform bool u_hasDashTexture; +uniform vec3 u_dashSize; +#endif varying vec2 v_width; void main(void){ + highp float lineWidth = v_width.s + v_width.t;// including alias overhead; + highp float width = length(v_normal) * lineWidth; - float width = length(v_normal) * (v_width.s + v_width.t); - float alpha = 1.0 - clamp(.5 * (width - v_width.s + v_width.t) / v_width.t, .0, 1.); + if (width > lineWidth){ // discard round caps ("cones") + discard; + } + #ifdef DASHARRAY + if (u_hasDashTexture){ + // dash size + gap size + float totalDashSize = u_dashSize.y + u_dashSize.z; + // [dashsize: constant, gabsize: scaling, ->pattern: fix] + float u = fract(v_lengthSoFar/totalDashSize) * (1. + u_dashSize.z / u_dashSize.y) * u_scale; + // [dashsize: constant, gabsize: constant, ->pattern: floating] +// float u = fract(v_lengthSoFar/totalDashSize * u_scale) * (u_dashSize.z / u_dashSize.y); + gl_FragColor = u_fill * texture2D(u_dashTexture, vec2(u, v_dir.y)); + // gl_FragColor = vec4(u_fill.rgb, u_fill.a * texture2D(u_dashTexture, vec2(u, v_dir.y)).a); + } else { + float dash = texture2D(u_pattern, vec2(fract(v_lengthSoFar / u_dashSize.x * u_scale))).r; + gl_FragColor = u_fill * dash; + } + #else gl_FragColor = u_fill; + #endif - if (v_width.t == 0.0 && alpha < 1.0) discard; - - gl_FragColor.a *= alpha; + if (!u_no_antialias){ + // float antialiasAlpha = 1.0 - clamp(.5 * (width - lineWidth) / v_width.t, .0, 1.); + gl_FragColor.a *= 1.0 - clamp(.5 * width - .5 * v_width.s + .5, .0, 1.); + } } diff --git a/packages/display/src/displays/webgl/glsl/line_vertex.glsl b/packages/display/src/displays/webgl/glsl/line_vertex.glsl index 75697d6ff..61671ff84 100644 --- a/packages/display/src/displays/webgl/glsl/line_vertex.glsl +++ b/packages/display/src/displays/webgl/glsl/line_vertex.glsl @@ -8,12 +8,15 @@ uniform mat4 u_matrix; uniform highp vec2 u_strokeWidth; uniform highp float u_scale; uniform vec2 u_topLeft; -uniform float u_texWidth; varying vec2 v_normal; +#ifdef DASHARRAY varying float v_lengthSoFar; +#endif varying vec2 v_width; +varying vec2 v_dir; uniform vec2 u_offset; +uniform vec3 u_dashSize; uniform float u_tileScale; uniform bool u_no_antialias; uniform bool u_scaleByAltitude; @@ -24,7 +27,7 @@ void main(void){ float strokeWidth = toPixel(u_strokeWidth, u_scale); float alias = u_no_antialias - ? .0 + ? 0.0 : strokeWidth < 1. ? .65 : 1.; float width = (strokeWidth+alias) / u_scale; @@ -33,17 +36,22 @@ void main(void){ vec2 dir2 = mod(a_normal.zw, 2.0) * 2.0 - 1.0; vec2 aliasNormal = floor(a_normal.zw * .5) * N_SCALE; v_normal = dir2 * aliasNormal; + v_dir = mod(a_normal.xy, 2.0); // LSB is direction/normal vector [-1,+1] - vec2 dir = mod(a_normal.xy, 2.0) * 2.0 - 1.0; + vec2 dir = v_dir * 2.0 - 1.0; vec2 normal = floor(a_normal.xy * .5) * N_SCALE; - v_lengthSoFar = a_lengthSoFar / u_texWidth; + #ifdef DASHARRAY +// v_lengthSoFar = a_lengthSoFar / u_dashSize.x; + v_lengthSoFar = a_lengthSoFar; + #endif float lineOffset = toPixel(u_offset, u_scale); vec2 position = a_position.xy + normal * -lineOffset / u_scale; vec2 posCenterWorld = vec2(u_topLeft +position * u_tileScale); +// vec2 offset = dir.y * normal * width; vec2 offset = dir * normal * width; if (!u_scaleByAltitude){ diff --git a/packages/display/src/displays/webgl/glsl/text_vertex.glsl b/packages/display/src/displays/webgl/glsl/text_vertex.glsl index f4154d9f8..f06beaa7d 100644 --- a/packages/display/src/displays/webgl/glsl/text_vertex.glsl +++ b/packages/display/src/displays/webgl/glsl/text_vertex.glsl @@ -14,7 +14,7 @@ uniform float u_scale; uniform float u_rotate; uniform bool u_alignMap; uniform bool u_fixedView; -uniform float u_atlasScale; +uniform vec2 u_texSize; uniform bool u_scaleByAltitude; varying vec2 v_texcoord; @@ -37,7 +37,7 @@ void main(void) { vec2 rotLowHi = mod(a_texcoord, 32.0); float rotationZ = rotLowHi.y * 32.0 + rotLowHi.x; // texture coodrinates bit6->bit16 - v_texcoord = floor(a_texcoord / 32.0) * u_atlasScale; + v_texcoord = floor(a_texcoord / 32.0) / u_texSize; vec2 labelOffset = vec2(toPixel(u_offset.xy, u_scale), toPixel(u_offset.zw, u_scale)); diff --git a/packages/display/src/displays/webgl/program/DashedLine.ts b/packages/display/src/displays/webgl/program/DashedLine.ts index a84732a09..9353cf100 100644 --- a/packages/display/src/displays/webgl/program/DashedLine.ts +++ b/packages/display/src/displays/webgl/program/DashedLine.ts @@ -20,7 +20,7 @@ // @ts-ignore import vertexShader from '../glsl/line_vertex.glsl'; // @ts-ignore -import fragmentShader from '../glsl/line_dash_fragment.glsl'; +import fragmentShader from '../glsl/line_fragment.glsl'; import Program from './Program'; import {GLStates, PASS} from './GLStates'; @@ -37,7 +37,7 @@ class DashedLineProgram extends Program { }); constructor(gl: WebGLRenderingContext, devicePixelRation: number) { - super(gl, devicePixelRation); + super(gl, devicePixelRation, {DASHARRAY: true}); this.mode = gl.TRIANGLES; this.vertexShaderSrc = vertexShader; diff --git a/packages/display/src/displays/webgl/program/Program.ts b/packages/display/src/displays/webgl/program/Program.ts index d31e1a77d..4e0b85813 100644 --- a/packages/display/src/displays/webgl/program/Program.ts +++ b/packages/display/src/displays/webgl/program/Program.ts @@ -24,8 +24,7 @@ import introVertex from '../glsl/intro_vertex.glsl'; import {ArrayGrp, GeometryBuffer, IndexData, IndexGrp} from '../buffer/GeometryBuffer'; import {BufferCache} from '../GLRender'; import {Attribute} from '../buffer/Attribute'; -import {ConstantAttribute, FlexAttribute} from '../buffer/templates/TemplateBuffer'; -import {DEFAULT_HEATMAP_GRADIENT} from '../buffer/templates/HeatmapBuffer'; +import {ConstantAttribute} from '../buffer/templates/TemplateBuffer'; let UNDEF; @@ -78,7 +77,7 @@ class Program { private textureUnits: number = 0; private macros: { [name: string]: string | number | boolean } = { - // '#version 300 es',s + // '#version 300 es',s 'M_PI': 3.1415927410125732 }; @@ -360,13 +359,21 @@ class Program { const {gl} = this; const {groups, instances} = geoBuffer; - // console.log( - // this.name, - // 'DEPTH_TEST', gl.getParameter(gl.DEPTH_TEST), - // 'SCISSOR_TEST', gl.getParameter(gl.SCISSOR_TEST), - // 'STENCIL_TEST', gl.getParameter(gl.STENCIL_TEST), - // 'BLEND', gl.getParameter(gl.BLEND) - // ); + // // if (this.name == 'Extrude' || this.name == 'Image') { + // if (this.name == 'Line'||(this.name == 'Extrude'||this.name == 'Image') { + // const stencilFunc = {}; + // for (let func of ['NEVER', 'LESS', 'LEQUAL', 'GREATER', 'GEQUAL', 'EQUAL', 'NOTEQUAL', 'ALWAYS']) { + // stencilFunc[gl[func]] = func; + // } + // console.log( + // this._pass, + // this.name, + // 'DEPTH_TEST', gl.getParameter(gl.DEPTH_TEST), + // 'SCISSOR_TEST', gl.getParameter(gl.SCISSOR_TEST), + // 'STENCIL_TEST', gl.getParameter(gl.STENCIL_TEST) ? `${stencilFunc[gl.getParameter(gl.STENCIL_FUNC)]} ${gl.getParameter(gl.STENCIL_REF)}` : false, + // 'BLEND', gl.getParameter(gl.BLEND) + // ); + // } for (let grp of groups) { let mode = grp.mode != UNDEF ? grp.mode : this.mode; @@ -435,7 +442,7 @@ class Program { this.gl.blendFunc(sFactor, dFactor); } - initGeometryBuffer(geoBuffer: GeometryBuffer, pass: PASS, stencil: boolean, zIndex?: number) { + initGeometryBuffer(geoBuffer: GeometryBuffer, pass: PASS, useStencil: boolean|number = true, zIndex?: number) { const prog = this; const {gl} = prog; const opaquePass = pass == PASS.OPAQUE; @@ -454,7 +461,7 @@ class Program { depth = geoBuffer.depth; } - prog.setStates(scissor, blend, depth, stencil && scissor); + prog.setStates(scissor, blend, depth, useStencil && scissor); this.blendFunc(); @@ -468,7 +475,6 @@ class Program { } const isDepthPrePass = pass == PASS.ALPHA && geoBuffer.pass & PASS.POST_ALPHA; - const colorMask = isDepthPrePass ? NO_COLOR_MASK : geoBuffer.colorMask || DEFAULT_COLOR_MASK; @@ -484,13 +490,20 @@ class Program { gl.disable(gl.POLYGON_OFFSET_FILL); - if (pass == PASS.POST_ALPHA) { - // use additional pass with stencil buffer to avoid "overlapping alpha" of unclipped geometry + if (useStencil && pass == PASS.POST_ALPHA) { gl.enable(gl.STENCIL_TEST); - gl.stencilFunc(gl.GREATER, 1, 0xff); - gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); - // gl.stencilFunc(gl.EQUAL, 0, 0xff); - // gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR); + + if (typeof useStencil == 'number') { + gl.stencilFunc(gl.EQUAL, useStencil as number, 0xff); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.ZERO); + } else { + // flat geometry and tile stencil is being used. + // use additional pass with stencil buffer to avoid "overlapping alpha" of unclipped geometry + gl.stencilFunc(gl.GREATER, 1, 0xff); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); + // gl.stencilFunc(gl.EQUAL, 0, 0xff); + // gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR); + } } }