UNPKG

tangram

Version:
841 lines (720 loc) 32.3 kB
// Rendering styles import StyleParser from './style_parser'; import FeatureSelection from '../selection/selection'; import gl from '../gl/constants'; // import GL constants since workers can't access GL context import ShaderProgram from '../gl/shader_program'; import VBOMesh from '../gl/vbo_mesh'; import Texture from '../gl/texture'; import Material from '../lights/material'; import Light from '../lights/light'; import {RasterTileSource} from '../sources/raster'; import log from '../utils/log'; import mergeObjects from '../utils/merge'; import Thread from '../utils/thread'; import WorkerBroker from '../utils/worker_broker'; import makeWireframeForTriangleElementData from '../builders/wireframe'; import debugSettings from '../utils/debug_settings'; import selection_fragment_source from '../selection/selection_fragment.glsl'; import rasters_source from './raster/raster_globals.glsl'; // Base class export var Style = { init ({ generation, styles, sources = {}, introspection } = {}) { this.setGeneration(generation); this.styles = styles; // styles for scene this.sources = sources; // data sources for scene this.defines = (Object.prototype.hasOwnProperty.call(this, 'defines') && this.defines) || {}; // #defines to be injected into the shaders this.shaders = (Object.prototype.hasOwnProperty.call(this, 'shaders') && this.shaders) || {}; // shader customization (uniforms, defines, blocks, etc.) this.introspection = introspection || false; this.selection = this.selection || this.introspection || false; // flag indicating if this style supports feature selection this.compile_setup = false; // one-time setup steps for program compilation this.program = null; // GL program reference (for main render pass) this.selection_program = null; // GL program reference for feature selection render pass this.feature_style = {}; // style for feature currently being parsed, shared to lessen GC/memory thrash this.vertex_template = []; // shared single-vertex template, filled out by each style this.tile_data = {}; this.stencil_proxy_tiles = true; // applied to proxy tiles w/non-opaque blend mode to avoid compounding alpha this.variants = {}; // mesh variants by variant key this.vertex_layouts = {}; // vertex layouts by variant key // Default world coords to wrap every 100,000 meters, can turn off by setting this to 'false' this.defines.TANGRAM_WORLD_POSITION_WRAP = 100000; // Blending // `opaque` styles are drawn first/under other styles, without alpha blending this.blend = this.blend || 'opaque'; // default to opaque if (this.blend !== 'opaque') { // non-opaque styles can customize their blend order, which will determine which features // are drawn first/under each other if (this.blend_order == null) { this.blend_order = this.default_blend_orders[this.blend]; } } else { // `opaque` ignores blend order, always render first/under this.blend_order = this.default_blend_orders[this.blend]; } this.defines[`TANGRAM_BLEND_${this.blend.toUpperCase()}`] = true; this.removeShaderBlock('setup'); // clear before material injection // If the style defines its own material, replace the inherited material instance if (!(this.material instanceof Material)) { if (!Material.isValid(this.material)) { this.material = StyleParser.defaults.material; } this.material = new Material(this.material); } this.material.inject(this); // Set lighting mode: fragment, vertex, or none (specified as 'false') Light.setMode(this.lighting, this); // Setup raster samplers if needed this.setupRasters(); // Setup shader definitions for custom attributes this.setupCustomAttributes(); this.initialized = true; }, destroy () { if (this.program) { this.program.destroy(); this.program = null; } if (this.selection_program) { this.selection_program.destroy(); this.selection_program = null; } WorkerBroker.removeTarget(this.main_thread_target); this.gl = null; this.initialized = false; }, reset () { }, baseStyle () { return this.base || this.name; }, setGeneration (generation) { // Scene generation id this style was created for this.generation = generation; // Provide a hook for this object to be called from worker threads this.main_thread_target = ['Style', this.name, this.generation].join('_'); if (Thread.is_main) { WorkerBroker.addTarget(this.main_thread_target, this); } }, /*** Style parsing and geometry construction ***/ // Returns an object to hold feature data (for a tile or other object) startData (tile) { this.tile_data[tile.id] = this.tile_data[tile.id] || { meshes: {}, uniforms: {}, textures: [] }; }, // Finalizes an object holding feature data (for a tile or other object) async endData (tile) { var tile_data = this.tile_data[tile.id]; this.tile_data[tile.id] = null; if (tile_data && Object.keys(tile_data.meshes).length > 0) { for (let variant in tile_data.meshes) { let mesh = tile_data.meshes[variant]; // Remove empty mesh variants if (mesh.vertex_data.vertex_count === 0) { delete tile_data.meshes[variant]; continue; } // Only keep final byte buffer mesh.vertex_data.end(); mesh.vertex_elements = mesh.vertex_data.element_buffer; mesh.vertex_data = mesh.vertex_data.vertex_buffer; // convert from instance to raw typed array } // Load raster tiles passed from data source // Blocks mesh completion to avoid flickering return this.buildRasterTextures(tile, tile_data); } else { return null; // don't send tile data back if doesn't have geometry } }, // Has mesh data for a given tile? hasDataForTile (tile) { return this.tile_data[tile.id] != null; }, getTileMesh (tile, variant) { const meshes = this.tile_data[tile.id].meshes; if (meshes[variant.key] == null) { const vertex_layout = this.vertexLayoutForMeshVariant(variant); meshes[variant.key] = { variant, vertex_data: vertex_layout.createVertexData() }; } return meshes[variant.key]; }, vertexLayoutForMeshVariant (/*variant*/) { // styles must implement }, meshVariantTypeForDraw (/*draw*/) { // styles must implement }, addFeature (feature, draw, context) { let tile = context.tile; if (tile.generation !== this.generation) { return; } if (!this.tile_data[tile.id]) { this.startData(tile); } let style = this.parseFeature(feature, draw, context); if (!style) { return; // skip feature } if (this.buildGeometry(feature.geometry, style, context) > 0) { feature.generation = this.generation; // track scene generation that feature was rendered for } }, buildGeometry (geometry, style, context) { let geom_count; if (geometry.type === 'Polygon') { geom_count = this.buildPolygons([geometry.coordinates], style, context); } else if (geometry.type === 'MultiPolygon') { geom_count = this.buildPolygons(geometry.coordinates, style, context); } else if (geometry.type === 'LineString') { geom_count = this.buildLines([geometry.coordinates], style, context); } else if (geometry.type === 'MultiLineString') { geom_count = this.buildLines(geometry.coordinates, style, context); } else if (geometry.type === 'Point') { geom_count = this.buildPoints([geometry.coordinates], style, context); } else if (geometry.type === 'MultiPoint') { geom_count = this.buildPoints(geometry.coordinates, style, context); } // Optionally collect per-layer stats if (geom_count > 0 && debugSettings.layer_stats) { let tile = context.tile; tile.debug.layers = tile.debug.layers || { list: {}, tree: {} }; let list = tile.debug.layers.list; let tree = tile.debug.layers.tree; let ftree = {}; // tree of layers for this feature context.layers.forEach(layer => { addLayerDebugEntry(list, layer, 1, geom_count, {[this.name]: geom_count}, {[this.baseStyle()]: geom_count}); let node = tree; let fnode = ftree; let levels = layer.split(':'); for (let i=0; i < levels.length; i++) { let level = levels[i]; node[level] = node[level] || { features: 0, geoms: 0, styles: {}, base: {} }; if (fnode[level] == null) { // only count each layer level once per feature fnode[level] = {}; addLayerDebugEntry(node, level, 1, geom_count, {[this.name]: geom_count}, {[this.baseStyle()]: geom_count}); } if (i < levels.length - 1) { node[level].layers = node[level].layers || {}; } node = node[level].layers; fnode = fnode[level]; } }); } return geom_count; }, parseFeature (feature, draw, context) { try { var style = this.feature_style; // Calculate order style.order = this.parseOrder(draw.order, context); if (style.order == null && this.blend !== 'overlay') { let msg = `Layer '${draw.layers.join(', ')}', draw group '${draw.group}': `; msg += '\'order\' parameter is required unless blend mode is \'overlay\''; if (draw.order != null) { msg += '; \'order\' was set to a dynamic value (e.g. string tied to feature property, '; msg += 'or JS function), but evaluated to null for one or more features'; } log({ level: 'warn', once: true }, msg); return; } // Subclass implementation style = this._parseFeature(feature, draw, context); if (!style) { return; // skip feature } // Custom attributes if (this.shaders.attributes) { style.attributes = style.attributes || {}; for (const aname in this.shaders.attributes) { style.attributes[aname] = StyleParser.evalCachedProperty( draw.attributes && draw.attributes[aname], context); // set attribute value to zero for null/undefined/non-numeric values if (typeof style.attributes[aname] !== 'number') { style.attributes[aname] = 0; } } } // Feature selection (only if feature is marked as interactive, and style supports it) if (this.selection) { style.interactive = StyleParser.evalProperty(draw.interactive, context); } else { style.interactive = false; } if (style.interactive === true) { style.selection_color = FeatureSelection.makeColor(feature, context.tile, context); } else { style.selection_color = FeatureSelection.defaultColor; } return style; } catch (error) { log('error', 'Style.parseFeature: style parsing error', feature, style, error.stack); } }, _parseFeature (/*feature, draw, context*/) { return this.feature_style; }, preprocess (draw) { // Preprocess first time if (!draw.preprocessed) { // Apply draw defaults if (this.draw) { // Merge each property separately to avoid modifying `draw` instance identity for (let param in this.draw) { let val = this.draw[param]; if (typeof val === 'object' && !Array.isArray(val)) { // nested param (e.g. `outline`) draw[param] = mergeObjects({}, val, draw[param]); } else if (draw[param] == null) { // simple param (single scalar value or array) draw[param] = val; } } } if (!this.selection) { draw.interactive = false; // always disable feature selection for when style doesn't support it } else if (this.introspection) { draw.interactive = true; // always enable feature selection for introspection } draw = this._preprocess(draw); // optional subclass implementation if (!draw) { return; } // Custom attributes if (this.shaders.attributes) { draw.attributes = draw.attributes || {}; for (const aname in this.shaders.attributes) { draw.attributes[aname] = StyleParser.createPropertyCache( draw.attributes[aname] != null ? draw.attributes[aname] : 0); } } draw.preprocessed = true; } return draw; }, // optionally implemented by subclass _preprocess (draw) { return draw; }, // Parse an order value parseOrder (order, context) { // Calculate order if it was not cached if (typeof order !== 'number') { return StyleParser.calculateOrder(order, context); } return order; }, // Expand final precision for half-layers (for outlines) scaleOrder (order) { return order * 2; }, // Parse a color of choose a default if acceptable, return undefined if color missing parseColor(color, context) { // Need either a color, or a shader block for 'color' or 'filter' if (color) { return StyleParser.evalCachedColorProperty(color, context); } else if (this.shaders.blocks.color || this.shaders.blocks.filter) { return StyleParser.defaults.color; } }, // Build functions are no-ops until overriden buildPolygons () { return 0; }, buildLines () { return 0; }, buildPoints () { return 0; }, /*** GL state and rendering ***/ setGL (gl) { this.gl = gl; this.max_texture_size = Texture.getMaxTextureSize(this.gl); }, makeMesh (vertex_data, vertex_elements, options = {}) { let vertex_layout = this.vertexLayoutForMeshVariant(options.variant); if (debugSettings.wireframe) { // In wireframe debug mode, transform mesh into lines vertex_elements = makeWireframeForTriangleElementData(vertex_elements); return new VBOMesh(this.gl, vertex_data, vertex_elements, vertex_layout, { ...options, draw_mode: this.gl.LINES }); } return new VBOMesh(this.gl, vertex_data, vertex_elements, vertex_layout, options); }, render (mesh) { return mesh.render(); }, // Get a specific program, compiling if necessary getProgram (key = 'program') { this.compileSetup(); const program = this[key]; if (!program || program.error) { return; } if (!program.compiled) { log('debug', `Compiling style '${this.name}', program key '${key}'`); try { program.compile(); } catch (e) { log('error', `Style: error compiling program for style '${this.name}' (program key '${key}')`, this, e.stack, e.type, e.shader_errors); throw e; // re-throw so users can be notified via event subscriptions } } return program; }, // One-time setup for compiling style's programs compileSetup () { if (this.compile_setup) { return; } if (!this.gl) { throw(new Error(`style.compile(): skipping for ${this.name} because no GL context`)); } // Build defines & for selection (need to create a new object since the first is stored as a reference by the program) var defines = this.buildDefineList(); if (this.selection) { var selection_defines = Object.assign({}, defines); selection_defines.TANGRAM_FEATURE_SELECTION = true; } // Shader blocks var blocks = (this.shaders && this.shaders.blocks); var block_scopes = (this.shaders && this.shaders.block_scopes); // Uniforms var uniforms = Object.assign({}, this.shaders && this.shaders.uniforms); for (let u in uniforms) { // validate uniforms if (uniforms[u] == null) { log({ level: 'warn', once: true }, `Style '${this.name}' has invalid uniform '${u}': uniform values must be non-null`); } } // Accept a single extension, or an array of extensions var extensions = (this.shaders && this.shaders.extensions); if (typeof extensions === 'string') { extensions = [extensions]; } // Create shaders this.program = new ShaderProgram( this.gl, this.vertex_shader_src, this.fragment_shader_src, { name: this.name, defines, uniforms, blocks, block_scopes, extensions } ); if (this.selection) { this.selection_program = new ShaderProgram( this.gl, this.vertex_shader_src, selection_fragment_source, { name: (this.name + ' (selection)'), defines: selection_defines, uniforms, blocks, block_scopes, extensions } ); } else { this.selection_program = null; } this.compile_setup = true; }, // Add a shader block addShaderBlock (key, block, scope = null) { this.shaders.blocks = this.shaders.blocks || {}; this.shaders.blocks[key] = this.shaders.blocks[key] || []; this.shaders.blocks[key].push(block); this.shaders.block_scopes = this.shaders.block_scopes || {}; this.shaders.block_scopes[key] = this.shaders.block_scopes[key] || []; this.shaders.block_scopes[key].push(scope); }, // Remove all shader blocks for key removeShaderBlock (key) { if (this.shaders.blocks) { this.shaders.blocks[key] = null; } if (this.shaders.block_scopes) { this.shaders.block_scopes[key] = null; } }, replaceShaderBlock (key, block, scope = null) { this.removeShaderBlock(key); this.addShaderBlock(key, block, scope); }, /** TODO: could probably combine and generalize this with similar method in ShaderProgram * (list of define objects that inherit from each other) */ buildDefineList () { // Add any custom defines to built-in style defines var defines = {}; // create a new object to avoid mutating a prototype value that may be shared with other styles if (this.defines != null) { for (var d in this.defines) { defines[d] = this.defines[d]; } } if (this.shaders != null && this.shaders.defines != null) { for (d in this.shaders.defines) { defines[d] = this.shaders.defines[d]; } } return defines; }, // Determines if 'raster' parameter is set to a valid value hasRasters () { return (['color', 'normal', 'custom'].indexOf(this.raster) > -1); }, // Setup raster access in shaders setupRasters () { if (!this.hasRasters()) { return; } // Enable raster textures and configure how first raster is applied if (this.raster === 'color') { this.defines.TANGRAM_RASTER_TEXTURE_COLOR = true; } else if (this.raster === 'normal') { this.defines.TANGRAM_RASTER_TEXTURE_NORMAL = true; } // else custom raster (samplers will be made available but not automatically applied) // A given style may be built with multiple data sources, each of which may attach // a variable number of raster sources (0 to N, where N is the max number of raster sources // defined for the scene). This means we don't know *which* or *how many* rasters will be // bound now, at initial compile-time; we only know this at geometry build-time. To ensure // that we can bind as many raster sources as needed, we declare our uniform arrays to hold // the maximum number of possible sources. At render time, only the necessary number of rasters // are bound (the remaining slots aren't intended to be accessed). let num_raster_sources = Object.keys(this.sources) .filter(s => this.sources[s] instanceof RasterTileSource) .length; this.defines.TANGRAM_NUM_RASTER_SOURCES = `${num_raster_sources}`; // force to string to avoid auto-float conversion if (num_raster_sources > 0) { // Track how many raster sources have alpha masking (used for handling transparency outside raster image) const num_masked_rasters = Object.keys(this.sources) .filter(s => this.sources[s].mask_alpha) .length; this.defines.TANGRAM_HAS_MASKED_RASTERS = (num_masked_rasters > 0); this.defines.TANGRAM_ALL_MASKED_RASTERS = (num_masked_rasters === num_raster_sources); // Use model position of tile's coordinate zoom for raster tile texture UVs this.defines.TANGRAM_MODEL_POSITION_BASE_ZOOM_VARYING = true; // Uniforms and samplers this.replaceShaderBlock('raster', rasters_source, 'Raster'); } }, // Load raster tile textures and set uniforms async buildRasterTextures (tile, tile_data) { // skip if style doesn't support rasters if (!this.hasRasters()) { return tile_data; } // skip if source didn't attach any rasters to tile if (tile.rasters.length === 0) { return tile_data; } // Load textures on main thread and return when done // We want to block the building of a raster tile mesh until its texture is loaded, // to avoid flickering while loading (texture will render as black) let textures; try { textures = await WorkerBroker.postMessage( `${this.main_thread_target}.loadTextures`, { coords: tile.coords, source: tile.source, rasters: tile.rasters, min: tile.min, max: tile.max } ); } catch (e) { // error thrown if style has been removed from main thread return tile_data; } if (!textures || textures.length < 1) { // no textures found (unexpected) // TODO: warning return tile_data; } else if (textures.some(t => !t.loaded)) { // some textures failed, throw out style for this tile return null; } // Enable alpha masking if needed (for transparency outside raster image, on first raster only) tile_data.uniforms['u_raster_mask_alpha'] = (this.sources[tile.rasters[0]].mask_alpha === true); // Set texture uniforms (returned after loading from main thread) const u_samplers = tile_data.uniforms['u_rasters'] = []; const u_sizes = tile_data.uniforms['u_raster_sizes'] = []; const u_offsets = tile_data.uniforms['u_raster_offsets'] = []; textures.forEach(t => { const i = t.index; u_samplers[i] = t.name; tile_data.textures.push(t.name); u_sizes[i] = [t.width, t.height]; // Tile geometry may be at a higher zoom than the raster tile texture, // (e.g. an overzoomed raster tile), in which case we need to adjust the // raster texture UVs to offset to the appropriate starting point for // this geometry tile. if (tile.coords.z > t.coords.z) { let dz = tile.coords.z - t.coords.z; // # of levels raster source is overzoomed let dz2 = Math.pow(2, dz); u_offsets[i] = [ (((tile.coords.x % dz2) + dz2) % dz2) / dz2, // double-modulo to handle negative (wrapped) tile coords (dz2 - 1 - (tile.coords.y % dz2)) / dz2, // GL texture coords are +Y up 1 / dz2 ]; } else { u_offsets[i] = [0, 0, 1]; } }); return tile_data; }, // Determine which raster tile textures need to load for this tile, load them and return metadata to worker // Called on main thread async loadTextures (tile) { let configs = {}; // texture configs to pass to texture builder, keyed by texture name let index = {}; // index into raster sampler array, keyed by texture name let queue = []; // Find raster textures that need to be loaded tile.rasters.map(r => this.sources[r]).filter(x => x).forEach((source, i) => { if (source instanceof RasterTileSource) { queue.push(source.tileTexture(tile, this).then(config => { configs[config.name] = config; index[config.name] = i; })); } }); await Promise.all(queue); // Create and load raster textures await Texture.createFromObject(this.gl, configs); let textures = await Promise.all(Object.keys(configs) .map(t => Texture.textures[t] && Texture.textures[t].load()) .filter(x => x) ); textures.forEach(t => t.retain()); // Take a subset of texture metadata, and decorate with raster-specific info // NB: only return name and size of textures loaded, because we can't send actual texture objects to worker return textures.map(t => ({ name: t.name, width: t.width, height: t.height, loaded: t.loaded, index: index[t.name], // raster sampler index coords: configs[t.name].coords // tile coords of raster tile })); }, // Setup shader definitions for custom attributes setupCustomAttributes() { if (this.shaders.attributes) { for (const [aname, attrib] of Object.entries(this.shaders.attributes)) { // alias each custom attribute to the internal attribute name in vertex shader, // and internal varying name in fragment shader (if varying is enabled) if (attrib.type === 'float') { if (attrib.varying !== false) { this.addShaderBlock('attributes', ` #ifdef TANGRAM_VERTEX_SHADER attribute float a_${aname}; varying float v_${aname}; #define ${aname} a_${aname} #else varying float v_${aname}; #define ${aname} v_${aname} #endif `); this.addShaderBlock('setup', `#ifdef TANGRAM_VERTEX_SHADER\nv_${aname} = a_${aname};\n#endif`); } else { this.addShaderBlock('attributes', ` #ifdef TANGRAM_VERTEX_SHADER attribute float a_${aname}; #define ${aname} a_${aname} #endif `); } } } } }, // Add custom attributes to a list of attributes for initializing a vertex layout addCustomAttributesToAttributeList(attribs) { if (this.shaders.attributes) { for (const [aname, attrib] of Object.entries(this.shaders.attributes)) { if (attrib.type === 'float') { attribs.push({ name: `a_${aname}`, size: 1, type: gl.FLOAT, normalized: false }); } } } return attribs; }, // Add current feature values for custom attributes to vertex template addCustomAttributesToVertexTemplate(draw, index) { if (this.shaders.attributes) { for (let aname in this.shaders.attributes) { this.vertex_template[index++] = draw.attributes[aname] != null ? draw.attributes[aname] : 0; } } }, // Setup any GL state for rendering setup () { this.setUniforms(); this.material.setupProgram(ShaderProgram.current); }, // Set style uniforms on currently bound program setUniforms () { var program = ShaderProgram.current; if (!program) { return; } program.setUniforms(this.shaders?.uniforms, true); // reset texture unit to 0 }, // Render state settings by blend mode render_states: { opaque: { depth_test: true, depth_write: true }, translucent: { depth_test: true, depth_write: true }, add: { depth_test: true, depth_write: false }, multiply: { depth_test: true, depth_write: false }, inlay: { depth_test: true, depth_write: false }, overlay: { depth_test: false, depth_write: false } }, // Default sort order for blend modes default_blend_orders: { opaque: Number.MIN_SAFE_INTEGER, add: 1, multiply: 2, inlay: 3, translucent: 4, overlay: 5 }, getBlendOrderForDraw (draw) { // Allow draw block to override blend_order for non-opaque blend styles return ((this.blend !== 'opaque' && draw.blend_order != null) ? draw.blend_order : this.blend_order); } }; // add feature and geometry counts for a single layer export function addLayerDebugEntry (target, layer, faeture_count, geom_count, styles, bases) { target[layer] = target[layer] || { features: 0, geoms: 0, styles: {}, base: {} }; target[layer].features += faeture_count; // feature count target[layer].geoms += geom_count; // geometry count // geometry count by style for (let style in styles) { target[layer].styles[style] = target[layer].styles[style] || 0; target[layer].styles[style] += styles[style]; } // geometry count by base style for (let style in bases) { target[layer].base[style] = target[layer].base[style] || 0; target[layer].base[style] += bases[style]; } }