diff --git a/src/render/draw_background.js b/src/render/draw_background.js index 4fa1799d15f..bfbc9db4a2a 100644 --- a/src/render/draw_background.js +++ b/src/render/draw_background.js @@ -16,8 +16,8 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Style const image = layer.paint['background-pattern']; const opacity = layer.paint['background-opacity']; - const isOpaque = !image && color[3] === 1 && opacity === 1; - if (painter.isOpaquePass !== isOpaque) return; + const pass = (!image && color[3] === 1 && opacity === 1) ? 'opaque' : 'translucent'; + if (painter.renderPass !== pass) return; gl.disable(gl.STENCIL_TEST); diff --git a/src/render/draw_circle.js b/src/render/draw_circle.js index 27969739fb7..f74ee17315b 100644 --- a/src/render/draw_circle.js +++ b/src/render/draw_circle.js @@ -11,7 +11,8 @@ import type TileCoord from '../source/tile_coord'; module.exports = drawCircles; function drawCircles(painter: Painter, sourceCache: SourceCache, layer: CircleStyleLayer, coords: Array) { - if (painter.isOpaquePass) return; + const pass = 'translucent'; + if (painter.renderPass !== pass) return; const gl = painter.gl; diff --git a/src/render/draw_fill.js b/src/render/draw_fill.js index 74836fb4ce1..c4a6616d58b 100644 --- a/src/render/draw_fill.js +++ b/src/render/draw_fill.js @@ -13,15 +13,14 @@ function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLa const gl = painter.gl; gl.enable(gl.STENCIL_TEST); - const isOpaque = - !layer.paint['fill-pattern'] && + const pass = (!layer.paint['fill-pattern'] && layer.isPaintValueFeatureConstant('fill-color') && layer.isPaintValueFeatureConstant('fill-opacity') && layer.paint['fill-color'][3] === 1 && - layer.paint['fill-opacity'] === 1; + layer.paint['fill-opacity'] === 1) ? 'opaque' : 'translucent'; // Draw fill - if (painter.isOpaquePass === isOpaque) { + if (painter.renderPass === pass) { // Once we switch to earcut drawing we can pull most of the WebGL setup // outside of this coords loop. painter.setDepthSublayer(1); @@ -29,7 +28,7 @@ function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLa } // Draw stroke - if (!painter.isOpaquePass && layer.paint['fill-antialias']) { + if (painter.renderPass === 'translucent' && layer.paint['fill-antialias']) { painter.lineWidth(2); painter.depthMask(false); diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index 96e76015a10..6ad0ba97c34 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -3,6 +3,7 @@ const glMatrix = require('@mapbox/gl-matrix'); const pattern = require('./pattern'); const mat3 = glMatrix.mat3; +const mat4 = glMatrix.mat4; const vec3 = glMatrix.vec3; import type Painter from './painter'; @@ -13,18 +14,47 @@ import type TileCoord from '../source/tile_coord'; module.exports = draw; function draw(painter: Painter, source: SourceCache, layer: FillExtrusionStyleLayer, coords: Array) { - if (painter.isOpaquePass) return; + if (painter.renderPass === '3d') { + const gl = painter.gl; + + gl.disable(gl.STENCIL_TEST); + gl.enable(gl.DEPTH_TEST); + + painter.clearColor(); + painter.depthMask(true); + + for (let i = 0; i < coords.length; i++) { + drawExtrusion(painter, source, layer, coords[i]); + } + } else if (painter.renderPass === 'translucent') { + drawExtrusionTexture(painter, layer); + } +} + +function drawExtrusionTexture(painter, layer) { + const renderedTexture = painter._prerenderedTextures[layer.id]; + if (!renderedTexture) return; const gl = painter.gl; + const program = painter.useProgram('extrusionTexture'); + gl.disable(gl.STENCIL_TEST); - gl.enable(gl.DEPTH_TEST); + gl.disable(gl.DEPTH_TEST); - painter.clearColor(); - painter.depthMask(true); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, renderedTexture.texture); - for (let i = 0; i < coords.length; i++) { - drawExtrusion(painter, source, layer, coords[i]); - } + gl.uniform1f(program.u_opacity, layer.paint['fill-extrusion-opacity']); + gl.uniform1i(program.u_image, 0); + + const matrix = mat4.create(); + mat4.ortho(matrix, 0, painter.width, painter.height, 0, 0, 1); + gl.uniformMatrix4fv(program.u_matrix, false, matrix); + + gl.uniform2f(program.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight); + + renderedTexture.vao.bind(gl, program, renderedTexture.buffer); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } function drawExtrusion(painter, source, layer, coord) { diff --git a/src/render/draw_fill_extrusion_texture.js b/src/render/draw_fill_extrusion_texture.js deleted file mode 100644 index db5580eb45e..00000000000 --- a/src/render/draw_fill_extrusion_texture.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow - -const Buffer = require('../data/buffer'); -const VertexArrayObject = require('./vertex_array_object'); -const PosArray = require('../data/pos_array'); -const mat4 = require('@mapbox/gl-matrix').mat4; - -import type Painter from './painter'; -import type StyleLayer from '../style/style_layer'; - -module.exports = draw; - -function draw(painter: Painter, layer: StyleLayer, texture: WebGLTexture) { - const gl = painter.gl; - const program = painter.useProgram('extrusionTexture'); - - gl.disable(gl.STENCIL_TEST); - gl.disable(gl.DEPTH_TEST); - - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture); - - gl.uniform1f(program.u_opacity, layer.paint['fill-extrusion-opacity']); - gl.uniform1i(program.u_image, 0); - - const matrix = mat4.create(); - mat4.ortho(matrix, 0, painter.width, painter.height, 0, 0, 1); - gl.uniformMatrix4fv(program.u_matrix, false, matrix); - - gl.uniform2f(program.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight); - - const array = new PosArray(); - array.emplaceBack(0, 0); - array.emplaceBack(1, 0); - array.emplaceBack(0, 1); - array.emplaceBack(1, 1); - const buffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX); - - const vao = new VertexArrayObject(); - vao.bind(gl, program, buffer); - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); -} diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 946377d366f..af4252261de 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -9,7 +9,8 @@ import type LineStyleLayer from '../style/style_layer/line_style_layer'; import type TileCoord from '../source/tile_coord'; module.exports = function drawLine(painter: Painter, sourceCache: SourceCache, layer: LineStyleLayer, coords: Array) { - if (painter.isOpaquePass) return; + const pass = 'translucent'; + if (painter.renderPass !== pass) return; painter.setDepthSublayer(0); painter.depthMask(false); diff --git a/src/render/draw_raster.js b/src/render/draw_raster.js index 849c3f95fe0..9935e4b073b 100644 --- a/src/render/draw_raster.js +++ b/src/render/draw_raster.js @@ -10,7 +10,8 @@ import type TileCoord from '../source/tile_coord'; module.exports = drawRaster; function drawRaster(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array) { - if (painter.isOpaquePass) return; + const pass = 'translucent'; + if (painter.renderPass !== pass) return; const gl = painter.gl; diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index 31c2490f446..9aa7909fca1 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -16,7 +16,8 @@ import type TileCoord from '../source/tile_coord'; module.exports = drawSymbols; function drawSymbols(painter: Painter, sourceCache: SourceCache, layer: SymbolStyleLayer, coords: Array) { - if (painter.isOpaquePass) return; + const pass = 'translucent'; + if (painter.renderPass !== pass) return; const drawAcrossEdges = !layer.layout['text-allow-overlap'] && diff --git a/src/render/painter.js b/src/render/painter.js index ba5b3e080c0..f9fb3f1c8f0 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -22,7 +22,6 @@ const draw = { line: require('./draw_line'), fill: require('./draw_fill'), 'fill-extrusion': require('./draw_fill_extrusion'), - 'fill-extrusion-texture': require('./draw_fill_extrusion_texture'), raster: require('./draw_raster'), background: require('./draw_background'), debug: require('./draw_debug') @@ -38,6 +37,8 @@ import type LineAtlas from './line_atlas'; import type SpriteAtlas from '../symbol/sprite_atlas'; import type GlyphSource from '../symbol/glyph_source'; +export type RenderPass = '3d' | 'opaque' | 'translucent'; + type PainterOptions = { showOverdrawInspector: boolean, showTileBoundaries: boolean, @@ -84,7 +85,7 @@ class Painter { spriteAtlas: SpriteAtlas; glyphSource: GlyphSource; depthRange: number; - isOpaquePass: boolean; + renderPass: RenderPass; currentLayer: number; id: string; _showOverdrawInspector: boolean; @@ -124,8 +125,8 @@ class Painter { this.height = height * browser.devicePixelRatio; gl.viewport(0, 0, this.width, this.height); - for (let i = 0; i < this.viewportTextures.length; i++) { - this.gl.deleteTexture(this.viewportTextures[i].texture); + for (const texture of this.viewportTextures) { + this.gl.deleteTexture(texture.texture); } this.viewportTextures = []; @@ -273,9 +274,87 @@ class Painter { this.frameHistory.record(Date.now(), this.transform.zoom, style.getTransition().duration); - this.isOpaquePass = false; - this.render3D(); + const layerIds = this.style._order; + + // 3D pass + // We first bind an offscreen framebuffer and render each 3D layer to + // its own texture, which we'll use later in the translucent pass to + // render to the main framebuffer. By doing this before we render to + // the main framebuffer we won't have to do an expensive framebuffer + // restore mid-render pass. + // The most important distinction of the 3D pass is that we use the + // depth buffer in an entirely different way (to represent 3D space) + // than we do in the 2D pass (to preserve layer order). + this.renderPass = '3d'; + { + const gl = this.gl; + // We'll wait and only attach the framebuffer if we think we're + // rendering something. + let fboAttached = false; + + let sourceCache; + let coords = []; + + for (let i = 0; i < layerIds.length; i++) { + const layer = this.style._layers[layerIds[i]]; + + if (!layer.has3DPass()) continue; + + if (layer.paint['fill-extrusion-opacity'] === 0 || layer.isHidden(this.transform.zoom)) { + // Don't bother creating a texture if we're not going + // to render anything: emplace a null value as a marker + // for the regular translucent render pass. + this._prerenderedTextures[layer.id] = null; + } else { + if (layer.source !== (sourceCache && sourceCache.id)) { + sourceCache = this.style.sourceCaches[layer.source]; + coords = []; + + if (sourceCache) { + if (sourceCache.prepare) sourceCache.prepare(); + this.clearStencil(); + coords = sourceCache.getVisibleCoordinates(); + } + + coords.reverse(); + } + + if (!coords.length) { + this._prerenderedTextures[layer.id] = null; + continue; + } + + if (!fboAttached) { + // This is the first 3D layer we're rendering: attach + // the framebuffer now. + this._setup3DFramebuffer(); + // Wait to flip the boolean until after we attach the first + // texture and clear the depth buffer a few lines down. + } + + let renderTarget = this.viewportTextures.pop(); + if (!renderTarget) { + renderTarget = new RenderTexture(this); + } + renderTarget.attachToFramebuffer(); + + if (!fboAttached) { + this.clearDepth(); + fboAttached = true; + } + + this.renderLayer(this, (sourceCache: any), layer, coords); + + renderTarget.detachFromFramebuffer(); + + this._prerenderedTextures[layer.id] = renderTarget; + } + } + if (fboAttached) gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } + + // Clear buffers in preparation for drawing to the main framebuffer this.prepareBuffers(); this.clearColor(); this.clearDepth(); @@ -284,43 +363,22 @@ class Painter { this.depthRange = (style._order.length + 2) * this.numSublayers * this.depthEpsilon; - this.isOpaquePass = true; - this.renderPass(); - this.isOpaquePass = false; - this.renderPass(); + // Opaque pass + // Draw opaque layers top-to-bottom first. + this.renderPass = 'opaque'; + { + let sourceCache; + let coords = []; - if (this.options.showTileBoundaries) { - const sourceCache = this.style.sourceCaches[Object.keys(this.style.sourceCaches)[0]]; - if (sourceCache) { - draw.debug(this, sourceCache, sourceCache.getVisibleCoordinates()); + this.currentLayer = layerIds.length - 1; + + if (!this._showOverdrawInspector) { + this.gl.disable(this.gl.BLEND); } - } - } - render3D() { - // If this style has 3D (fill-extrusion) layers, we render them first - // to offscreen textures so that we don't have to do an expensive - // framebuffer restore in the middle of the translucent renderPass. - if (!this.style._order3D.length) return; + for (this.currentLayer; this.currentLayer >= 0; this.currentLayer--) { + const layer = this.style._layers[layerIds[this.currentLayer]]; - const gl = this.gl; - // We'll wait and only attach the framebuffer if we think we're - // rendering something. - let fboAttached = false; - - let sourceCache; - let coords = []; - - for (let i = 0; i < this.style._order3D.length; i++) { - const layerId = this.style._order3D[i]; - const layer = this.style._layers[layerId]; - - if (layer.paint['fill-extrusion-opacity'] === 0 || layer.isHidden(this.transform.zoom)) { - // Don't bother creating a texture if we're not going - // to render anything: emplace a null value as a marker - // for the regular translucent render pass. - this._prerenderedTextures[layerId] = null; - } else { if (layer.source !== (sourceCache && sourceCache.id)) { sourceCache = this.style.sourceCaches[layer.source]; coords = []; @@ -329,42 +387,56 @@ class Painter { if (sourceCache.prepare) sourceCache.prepare(); this.clearStencil(); coords = sourceCache.getVisibleCoordinates(); + if (sourceCache.getSource().isTileClipped) { + this._renderTileClippingMasks(coords); + } } - - coords.reverse(); } - if (!coords.length) { - this._prerenderedTextures[layerId] = null; - continue; - } + this.renderLayer(this, (sourceCache: any), layer, coords); + } + } - if (!fboAttached) { - this._setup3DFramebuffer(); - // Wait to flip the boolean until after we attach the first - // texture and clear the depth buffer a few lines down. - } + // Translucent pass + // Draw all other layers bottom-to-top. + this.renderPass = 'translucent'; + { + let sourceCache; + let coords = []; - let renderTarget = this.viewportTextures.pop(); - if (!renderTarget) { - renderTarget = new RenderTexture(this); - } - renderTarget.attachToFramebuffer(); + this.gl.enable(this.gl.BLEND); - if (!fboAttached) { - this.clearDepth(); - fboAttached = true; - } + this.currentLayer = 0; - this.renderLayer(this, (sourceCache: any), layer, coords); + for (this.currentLayer; this.currentLayer < layerIds.length; this.currentLayer++) { + const layer = this.style._layers[layerIds[this.currentLayer]]; - renderTarget.detachFromFramebuffer(); + if (layer.source !== (sourceCache && sourceCache.id)) { + sourceCache = this.style.sourceCaches[layer.source]; + coords = []; - this._prerenderedTextures[layerId] = renderTarget; + if (sourceCache) { + if (sourceCache.prepare) sourceCache.prepare(); + this.clearStencil(); + coords = sourceCache.getVisibleCoordinates(); + if (sourceCache.getSource().isTileClipped) { + this._renderTileClippingMasks(coords); + } + } + + coords.reverse(); + } + + this.renderLayer(this, (sourceCache: any), layer, coords); } } - if (fboAttached) gl.bindFramebuffer(gl.FRAMEBUFFER, null); + if (this.options.showTileBoundaries) { + const sourceCache = this.style.sourceCaches[Object.keys(this.style.sourceCaches)[0]]; + if (sourceCache) { + draw.debug(this, sourceCache, sourceCache.getVisibleCoordinates()); + } + } } _setup3DFramebuffer() { @@ -385,58 +457,6 @@ class Painter { } } - renderPass() { - const layerIds = this.style._order; - - let sourceCache; - let coords = []; - - this.currentLayer = this.isOpaquePass ? layerIds.length - 1 : 0; - - if (this.isOpaquePass) { - if (!this._showOverdrawInspector) { - this.gl.disable(this.gl.BLEND); - } - } else { - this.gl.enable(this.gl.BLEND); - } - - for (let i = 0; i < layerIds.length; i++) { - const layer = this.style._layers[layerIds[this.currentLayer]]; - - if (layer.source !== (sourceCache && sourceCache.id)) { - sourceCache = this.style.sourceCaches[layer.source]; - coords = []; - - if (sourceCache) { - if (sourceCache.prepare) sourceCache.prepare(); - this.clearStencil(); - coords = sourceCache.getVisibleCoordinates(); - if (sourceCache.getSource().isTileClipped) { - this._renderTileClippingMasks(coords); - } - } - - if (!this.isOpaquePass) { - coords.reverse(); - } - } - - if (!this.isOpaquePass && layer.type === 'fill-extrusion') { - const renderTarget = this._prerenderedTextures[layer.id]; - if (renderTarget) { - this.id = layer.id; - draw['fill-extrusion-texture'](this, layer, renderTarget.texture); - this.viewportTextures.push(renderTarget); - delete this._prerenderedTextures[layer.id]; - } - } else { - this.renderLayer(this, (sourceCache: any), layer, coords); - } - this.currentLayer += this.isOpaquePass ? -1 : 1; - } - } - depthMask(mask: boolean) { if (mask !== this._depthMask) { this._depthMask = mask; diff --git a/src/render/render_texture.js b/src/render/render_texture.js index e186923935d..7017c1242bc 100644 --- a/src/render/render_texture.js +++ b/src/render/render_texture.js @@ -1,10 +1,16 @@ // @flow +const Buffer = require('../data/buffer'); +const VertexArrayObject = require('./vertex_array_object'); +const PosArray = require('../data/pos_array'); + import type Painter from './painter'; class RenderTexture { gl: WebGLRenderingContext; texture: WebGLTexture; + buffer: Buffer; + vao: VertexArrayObject; constructor(painter: Painter) { const gl = this.gl = painter.gl; @@ -18,6 +24,14 @@ class RenderTexture { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, painter.width, painter.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.bindTexture(gl.TEXTURE_2D, null); + + const array = new PosArray(); + array.emplaceBack(0, 0); + array.emplaceBack(1, 0); + array.emplaceBack(0, 1); + array.emplaceBack(1, 1); + this.buffer = Buffer.fromStructArray(array, Buffer.BufferType.VERTEX); + this.vao = new VertexArrayObject(); } attachToFramebuffer() { diff --git a/src/style/style.js b/src/style/style.js index 1b228807189..26f4fb08663 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -80,7 +80,6 @@ class Style extends Evented { _layers: {[string]: StyleLayer}; _order: Array; - _order3D: Array; sourceCaches: {[string]: SourceCache}; zoomHistory: ZoomHistory | {}; _loaded: boolean; @@ -105,7 +104,6 @@ class Style extends Evented { this._layers = {}; this._order = []; - this._order3D = []; this.sourceCaches = {}; this.zoomHistory = {}; this._loaded = false; @@ -233,7 +231,6 @@ class Style extends Evented { const layers = deref(this.stylesheet.layers); this._order = layers.map((layer) => layer.id); - this._order3D = layers.filter((layer) => layer.type === 'fill-extrusion').map((layer) => layer.id); this._layers = {}; for (let layer of layers) { diff --git a/src/style/style_layer.js b/src/style/style_layer.js index 9f55d8248bf..1c3eff2f34d 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -373,6 +373,10 @@ class StyleLayer extends Evented { style: {glyphs: true, sprite: true} })); } + + has3DPass() { + return false; + } } module.exports = StyleLayer; diff --git a/src/style/style_layer/fill_extrusion_style_layer.js b/src/style/style_layer/fill_extrusion_style_layer.js index 3680e3021f4..d6c0111646c 100644 --- a/src/style/style_layer/fill_extrusion_style_layer.js +++ b/src/style/style_layer/fill_extrusion_style_layer.js @@ -19,6 +19,10 @@ class FillExtrusionStyleLayer extends StyleLayer { createBucket(parameters: BucketParameters) { return new FillExtrusionBucket(parameters); } + + has3DPass() { + return true; + } } module.exports = FillExtrusionStyleLayer;