Openlayers源码实践系列:探索layer的渲染机制——从分析OpenLayers 6 的WebGLPointsLayer动画效果实现说起

前言

最新的OpenLayers 6.1.1 中提供了一个基于WebGL用于渲染大量点要素并且支持类expressions语法描述的动画渲染。OpenLayers6之前的老版本是不支持动画渲染的,如果想要实现动画要素效果,需要利用OpenLayers的render机制来实现动画帧的渲染。本文借由分析WebGLPointsLayer动画的实现,探索了一下OpenLayers的图层渲染机制。

从ol.layer.WebGLPointsLayer开始

书归正传,先看一下 WebGLPointsLayer 的动画渲染是怎么做到的。先看看官方实例:

Filtering features with WebGL

……
import WebGLPointsLayer from 'ol/layer/WebGLPoints';
……
var map = new Map({
        layers: [
                new TileLayer({
                        source: new Stamen({
                                layer: 'toner'
                        })
                }),
                new WebGLPointsLayer({
                        style: style,
                        source: vectorSource,
                        disableHitDetection: true
                })
        ],
        target: document.getElementById('map'),
        view: new View({
                center: [0, 0],
                zoom: 2
        })
});

// animate the map
function animate() {
        map.render();
        window.requestAnimationFrame(animate);
}
animate();

WebGLPointsLayer类的声明与使用和其它layer的用法表面上都差不多,不同的就是它的style使用的是类expressions的表达式。
在地图构造完成之后,通过一个递归过程来实现对地图的不间断重渲染:

function animate() {
        map.render();
        window.requestAnimationFrame(animate);
}

在这里我们发现了一个关键的API:

ol.Map类的render()

在实例中,这个API是显式调用的,同时ol.Map类的对象在调用addLayer()时,会触发change:layergroup事件,调用名为handleLayerGroupChanged_()的回调函数,ol.Map并没有重载这些代码,而是完整继承了父类ol.PluggableMap的代码,下面是相关代码:

    this.addEventListener(getChangeEventType(MapProperty.LAYERGROUP), this.handleLayerGroupChanged_);
……
/**
  * @private
  */
handleLayerGroupChanged_() {
        if (this.layerGroupPropertyListenerKeys_) {
                this.layerGroupPropertyListenerKeys_.forEach(unlistenByKey);
                this.layerGroupPropertyListenerKeys_ = null;
        }
        const layerGroup = this.getLayerGroup();
        if (layerGroup) {
                this.layerGroupPropertyListenerKeys_ = [
                        listen(
                                layerGroup, ObjectEventType.PROPERTYCHANGE,
                                this.render, this),
                        listen(
                                layerGroup, EventType.CHANGE,
                                this.render, this)
                ];
        }
        this.render();
}

ol.Map.render() 地图渲染过程

这一节研究一下ol.Map类中render的实现,首先看一下ol.Map类的全部代码:

class Map extends PluggableMap {

        /**
         * @param {import("./PluggableMap.js").MapOptions} options Map options.
         */
        constructor(options) {
                options = assign({}, options);
                if (!options.controls) {
                        options.controls = defaultControls();
                }
                if (!options.interactions) {
                        options.interactions = defaultInteractions();
                }

                super(options);
        }

        createRenderer() {
                return new CompositeMapRenderer(this);
        }
}


export default Map;

只有寥寥数行?实际上ol.Map只是对ol.PluggableMap进行了简单的扩展,所以我们直接去看ol.PluggableMap里面关于render()的实现:

/**
 * Request a map rendering (at the next animation frame).
 * @api
 */
render() {
        if (this.renderer_ && this.animationDelayKey_ === undefined) {
                this.animationDelayKey_ = requestAnimationFrame(this.animationDelay_);
        }
}

这一段的意思是如果没有定义render_和animationDelayKey_两个属性,则去调用requestAnimationFrame(callback)这个API,并且为animationDelayKey_赋值;作为参数传入requestAnimationFrame(callback)的animationDelay_(),在下一次浏览器刷新的时候将会被调用。向requestAnimationFrame(callback)传递的参数,是animationDelay_()这个函数,它的定义如下:

    /**
     * @private
     */
    this.animationDelay_ = function() {
      this.animationDelayKey_ = undefined;
      this.renderFrame_(Date.now());
    }.bind(this);

它先是将animationDelayKey_的值清空,然后调用了一次renderFrame_(time)。

在这里就能看得出来,关于地图渲染的核心逻辑是在renderFrame_(time)这个API里实现的:

ol.Map.renderFrame_(time) 地图帧的渲染

首先快速浏览一下它的代码:

/**
   * @param {number} time Time.
   * @private
   */
renderFrame_(time) {
        const size = this.getSize();
        const view = this.getView();
        const previousFrameState = this.frameState_;
        /** @type {?FrameState} */
        let frameState = null;
        if (size !== undefined && hasArea(size) && view && view.isDef()) {
                const viewHints = view.getHints(this.frameState_ ? this.frameState_.viewHints : undefined);
                const viewState = view.getState();
                frameState = {
                        animate: false,
                        coordinateToPixelTransform: this.coordinateToPixelTransform_,
                        declutterItems: previousFrameState ? previousFrameState.declutterItems : [],
                        extent: getForViewAndSize(viewState.center, viewState.resolution, viewState.rotation, size),
                        index: this.frameIndex_++,
                        layerIndex: 0,
                        layerStatesArray: this.getLayerGroup().getLayerStatesArray(),
                        pixelRatio: this.pixelRatio_,
                        pixelToCoordinateTransform: this.pixelToCoordinateTransform_,
                        postRenderFunctions: [],
                        size: size,
                        tileQueue: this.tileQueue_,
                        time: time,
                        usedTiles: {},
                        viewState: viewState,
                        viewHints: viewHints,
                        wantedTiles: {}
                };
        }

        this.frameState_ = frameState;
        this.renderer_.renderFrame(frameState);

        if (frameState) {
                if (frameState.animate) {
                        this.render();
                }
                Array.prototype.push.apply(this.postRenderFunctions_, frameState.postRenderFunctions);

                if (previousFrameState) {
                        const moveStart = !this.previousExtent_ ||
                                (!isEmpty(this.previousExtent_) &&
                                        !equals(frameState.extent, this.previousExtent_));
                        if (moveStart) {
                                this.dispatchEvent(
                                        new MapEvent(MapEventType.MOVESTART, this, previousFrameState));
                                this.previousExtent_ = createOrUpdateEmpty(this.previousExtent_);
                        }
                }

                const idle = this.previousExtent_ &&
                        !frameState.viewHints[ViewHint.ANIMATING] &&
                        !frameState.viewHints[ViewHint.INTERACTING] &&
                        !equals(frameState.extent, this.previousExtent_);

                if (idle) {
                        this.dispatchEvent(new MapEvent(MapEventType.MOVEEND, this, frameState));
                        clone(frameState.extent, this.previousExtent_);
                }
        }

        this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState));

        this.postRenderTimeoutHandle_ = setTimeout(this.handlePostRender.bind(this), 0);

}

代码行数比较多,但是不难读懂,这一大段做了四件事情:

  • 构造frameState;
  • 使用renderer_对象进行当前帧的渲染;
  • 进行view的交互动画(缩放、旋转、平移等)的预处理,并发射movestart事件,渲染view交互动画,并在结束之后发射moveend事件;
  • 处理完所有渲染工作后,发射postrender事件,并在handlePostRender()的调用中处理自己的渲染后收尾工作,例如预加载下一层瓦片等(此处代码不做详细分析,感兴趣的朋友请自行阅读源码);

这里要讲的重点是renderer_对象:

ol.PluggableMap的渲染器renderer_

再次回到代码,看看renderer_属性是在哪里被初始化的:

/**
 * @private
 */
handleTargetChanged_() {
        // target may be undefined, null, a string or an Element.
        // If it's a string we convert it to an Element before proceeding.
        // If it's not now an Element we remove the viewport from the DOM.
        // If it's an Element we append the viewport element to it.
        let targetElement;
        if (this.getTarget()) {
                targetElement = this.getTargetElement();
        }
……
        if (!targetElement) {
              ……
        } else {
                targetElement.appendChild(this.viewport_);
                if (!this.renderer_) {
                        this.renderer_ = this.createRenderer();
                }
      ……
        }
……
}

rederer_是地图指定的渲染器对象,在handleTargetChanged_()这个API中被初始化,而handleTargetChanged_()是由target属性发生变化时的change:target时间触发的回调函数:

    this.addEventListener(getChangeEventType(MapProperty.TARGET), this.handleTargetChanged_);

也就是说,当PluggableMap的target属性被赋值的时候,会触发这个事件并调用回调函数,此时如果renderer_属性未被赋值,则会由createRenderer()方法返回一个渲染器的对象句柄进行赋值,渲染器对象renderer_的具体类型由createRenderer()方法决定。

ol.Map.renderer_.renderFrame(frameState)做了哪些事

createRenderer()是一个抽象方法,在ol.Map中被重载,返回的是一个CompositeMapRenderer(ol.renderer.composite)类型的对象。
composite类型的渲染器对象调用的renderFrame(frameState)做了哪些事?继续看代码(ol/renderer/composite.js):

/**
 * @inheritDoc
 */
renderFrame(frameState) {
        if (!frameState) {
                if (this.renderedVisible_) {
                        this.element_.style.display = 'none';
                        this.renderedVisible_ = false;
                }
                return;
        }
        this.calculateMatrices2D(frameState);
        this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState);
        const layerStatesArray = frameState.layerStatesArray.sort(function (a, b) {
                return a.zIndex - b.zIndex;
        });
        const viewState = frameState.viewState;
        this.children_.length = 0;
        let previousElement = null;
        for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
                const layerState = layerStatesArray[i];
                frameState.layerIndex = i;
                if (!inView(layerState, viewState) ||
                        (layerState.sourceState != SourceState.READY && layerState.sourceState != SourceState.UNDEFINED)) {
                        continue;
                }
                const layer = layerState.layer;
                const element = layer.render(frameState, previousElement);
                if (!element) {
                        continue;
                }
                if (element !== previousElement) {
                        this.children_.push(element);
                        previousElement = element;
                }
        }
        super.renderFrame(frameState);
        replaceChildren(this.element_, this.children_);
        this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState);
        if (!this.renderedVisible_) {
                this.element_.style.display = '';
                this.renderedVisible_ = true;
        }
        this.scheduleExpireIconCache(frameState);
}

它具体做了这样几件事:


这里是重点

  • 使用继承自父类ol.renderer.MapRenderer的calculateMatrices2D(frameState)构造好渲染所需的canvas上下文参数,包括屏幕坐标到投影坐标的映射、变换等等;
  • 发射一个precompose事件;
  • 取得一个按照z-Index属性排好序的layer列表,并循环渲染列表内的layer,对每一个layer调用其内置的render函数进行渲染,并返回一个canvas类型的DOM元素,将该元素加到children_数组里,最后一并渲染到地图上;
  • 调用父类的renderFrame;
  • 发射一个postrender事件;
  • 清理图层中style样式包含的图片缓存;

ol.layer.Layer的内置render()

回到开篇讲到的WebGLPointsLayer,我们来看一下它的内置render函数。
可以看到和ol.Map类似,WebGLPointsLayer简单重载了ol.layer.Layer类,并主要重载了createRenderer(),在createRenderer()里实现了对WebGLPointsLayer类型layer的渲染。
WebGLPointsLayer的render函数直接继承自父类ol.layer.Layer:

/**
 * In charge to manage the rendering of the layer. One layer type is
 * bounded with one layer renderer.
 * @param {?import("../PluggableMap.js").FrameState} frameState Frame state.
 * @param {HTMLElement} target Target which the renderer may (but need not) use
 * for rendering its content.
 * @return {HTMLElement} The rendered element.
 */
render(frameState, target) {
        const layerRenderer = this.getRenderer();
        if (layerRenderer.prepareFrame(frameState)) {
                return layerRenderer.renderFrame(frameState, target);
        }
}

首先通过getRenderer()获得一个renderer句柄:

  /**
   * Get the renderer for this layer.
   * @return {import("../renderer/Layer.js").default} The layer renderer.
   */
  getRenderer() {
    if (!this.renderer_) {
      this.renderer_ = this.createRenderer();
    }
    return this.renderer_;
  }
……
       createRenderer() {
         return new WebGLPointsLayerRenderer(this, {
           vertexShader: this.parseResult_.builder.getSymbolVertexShader(),
           fragmentShader: this.parseResult_.builder.getSymbolFragmentShader(),
           hitVertexShader: !this.hitDetectionDisabled_ &&
             this.parseResult_.builder.getSymbolVertexShader(true),
           hitFragmentShader: !this.hitDetectionDisabled_ &&
             this.parseResult_.builder.getSymbolFragmentShader(true),
           uniforms: this.parseResult_.uniforms,
           attributes: this.parseResult_.attributes
         });
       }
……

之后调用prepareFrame(frameState),在WebGLPointsLayer中对应的是WebGLPointsLayerRenderer中的prepareFrame(frameState),主要完成一些渲染前的数据准备:

/**
 * @inheritDoc
 */
prepareFrame(frameState) {
        const layer = this.getLayer();
        const vectorSource = layer.getSource();
        const viewState = frameState.viewState;
        const viewNotMoving = !frameState.viewHints[ViewHint.ANIMATING] && !frameState.viewHints[ViewHint.INTERACTING];
        const extentChanged = !equals(this.previousExtent_, frameState.extent);
        const sourceChanged = this.sourceRevision_ < vectorSource.getRevision();

        if (sourceChanged) {
                this.sourceRevision_ = vectorSource.getRevision();
        }

        if (viewNotMoving && (extentChanged || sourceChanged)) {
                const projection = viewState.projection;
                const resolution = viewState.resolution;

                const renderBuffer = layer instanceof BaseVector ? layer.getRenderBuffer() : 0;
                const extent = buffer(frameState.extent, renderBuffer * resolution);
                vectorSource.loadFeatures(extent, resolution, projection);

                this.rebuildBuffers_(frameState);
                this.previousExtent_ = frameState.extent.slice();
        }

        // apply the current projection transform with the invert of the one used to fill buffers
        this.helper.makeProjectionTransform(frameState, this.currentTransform_);
        multiplyTransform(this.currentTransform_, this.invertRenderTransform_);

        this.helper.useProgram(this.program_);
        this.helper.prepareDraw(frameState);

        // write new data
        this.helper.bindBuffer(this.verticesBuffer_);
        this.helper.bindBuffer(this.indicesBuffer_);

        this.helper.enableAttributes(this.attributes);

        return true;
}

最后调用的是renderFrame(frameState, target),并将结果(canvas)返回给Map容器:


/**
 * @inheritDoc
 */
renderFrame(frameState) {
        const renderCount = this.indicesBuffer_.getSize();
        this.helper.drawElements(0, renderCount);
        this.helper.finalizeDraw(frameState);
        const canvas = this.helper.getCanvas();

        const layerState = frameState.layerStatesArray[frameState.layerIndex];
        const opacity = layerState.opacity;
        if (opacity !== parseFloat(canvas.style.opacity)) {
                canvas.style.opacity = opacity;
        }

        if (this.hitDetectionEnabled_) {
                this.renderHitDetection(frameState);
                this.hitRenderTarget_.clearCachedData();
        }

        return canvas;
}

至此,整个渲染的流程就算走完了,为了不打乱思路,这个过程中省略了很多细节,只为理解OpenLayers的渲染机制。稍作整理,大致的流程如下:

在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战斗中的老胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值