streetscape.gl学习笔记(二)

在第一篇中streetscape.gl学习笔记(一),大致对LogViewer (React Component)有所了解,现就看看它的核心组成组件Core3DViewer。

Core3DViewer大体认识

Core3DViewer组件主要由DeckGL、StaticMap、ObjectLabelsOverlay三个子组件组成。这里DeckGL、StaticMap又分别对应uber的另外两个开源产品deck.glreact-map-gl
在这里再多聊两句,这两个组件是通过两个Canvas进行组合,而且默认是监听deck.gl的交互事件。因此,我就遇到了这个问题,在streetscape.gl中我同时想和地图上的元素进行交互(例如:在地下停车场的场景中,我想查看地图上停车位的基本信息情况)时,就受到限制。Can streetscape.gl listen to mapbox's own event

好开始上源码。

export default class Core3DViewer extends PureComponent {
  static propTypes = {
    // Props from loader
    frame: PropTypes.object,
    metadata: PropTypes.object,

    // Rendering options
    showMap: PropTypes.bool,
    showTooltip: PropTypes.bool,
    mapboxApiAccessToken: PropTypes.string,
    mapStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
    xvizStyles: PropTypes.object,
    car: PropTypes.object,
    viewMode: PropTypes.object,
    streamFilter: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.array,
      PropTypes.object,
      PropTypes.func
    ]),
    customLayers: PropTypes.array,
    renderObjectLabel: PropTypes.func,
    getTransformMatrix: PropTypes.func,

    // Callbacks
    onMapLoad: PropTypes.func,
    onDeckLoad: PropTypes.func,
    onHover: PropTypes.func,
    onClick: PropTypes.func,
    onContextMenu: PropTypes.func,
    onViewStateChange: PropTypes.func,

    // Debug info listener
    debug: PropTypes.func,

    // States
    viewState: PropTypes.object,
    viewOffset: PropTypes.object,
    objectStates: PropTypes.object
  };
...
}

首先是定义属性,

  • frame、metadata属性是为了接收来自父组件透传过来的XVIZ log数据
  • showMap、showTooltip、mapboxApiAccessToken、mapStyle 、xvizStyles、car、viewMode、streamFilter、customLayers、renderObjectLabel、getTransformMatrix这些属性是为了地图上渲染元素及显示样式所定义的属性,
  • onMapLoad: PropTypes.func,
    onDeckLoad: PropTypes.func,
    onHover: PropTypes.func,
    onClick: PropTypes.func,
    onContextMenu: PropTypes.func,
    onViewStateChange: PropTypes.func,
    地图交互事件所定义的属性
  • debug调试属性
  • viewState、viewOffset、objectStates属性分别记录当前组件所对应的视图状态和目标物状态,以便和其他组件进行交互
  static defaultProps = {
    car: DEFAULT_CAR,
    viewMode: VIEW_MODE.PERSPECTIVE,
    xvizStyles: {},
    customLayers: [],
    onMapLoad: noop,
    onDeckLoad: noop,
    onViewStateChange: noop,
    onHover: noop,
    onClick: noop,
    onContextMenu: noop,
    showMap: true,
    showTooltip: false
  };

这部分是定义属性的初始值,如果你有车辆的模型文件(json格式)可以在这里替换。


替换后的效果.png
  constructor(props) {
    super(props);
    this.state = {
      styleParser: this._getStyleParser(props)
    };
  }

构造函数中,在state中添加一个styleParser,其类型是XVIZStyleParser,用于各stream流的样式获取。
接下来从render函数入手,看看deck.gl是怎么绘制和交互的

  render() {
    const {
      mapboxApiAccessToken,
      frame,
      metadata,
      objectStates,
      renderObjectLabel,
      getTransformMatrix,
      style,
      mapStyle,
      viewMode,
      showMap
    } = this.props;
    const {styleParser} = this.state;
    return (
      <DeckGL
        width="100%"
        height="100%"
        ref={this.deckRef}
        effects={[LIGHTS]}
        views={getViews(viewMode)}
        viewState={this._getViewState()}
        layers={this._getLayers()}
        layerFilter={this._layerFilter}
        getCursor={this._getCursor}
        onLoad={this._onDeckLoad}
        onHover={this._onLayerHover}
        onClick={this._onLayerClick}
        onViewStateChange={this._onViewStateChange}
        _onMetrics={this._onMetrics}
      >
        {showMap && (
          <StaticMap
            reuseMap={true}
            attributionControl={false}
            mapboxApiAccessToken={mapboxApiAccessToken}
            mapStyle={mapStyle}
            visible={frame && frame.origin && !viewMode.firstPerson}
            onLoad={this._onMapLoad}
          />
        )}

        <ObjectLabelsOverlay
          objectSelection={objectStates.selected}
          frame={frame}
          metadata={metadata}
          renderObjectLabel={renderObjectLabel}
          xvizStyleParser={styleParser}
          style={style}
          getTransformMatrix={getTransformMatrix}
        />

        {this.props.children}
      </DeckGL>
    );
  }

我们关注<DeckGL/>中的属性赋值,这里最重要的几个是views、viewState、layers,让我们来看看这几个属性是干嘛的,以及程序是怎么给它赋值的。

views

在搜索到views属性时,Deck.gl推荐先了解一下Views and Projections

view是什么,能否实现多个视图,并同步视图状态,写到这实在写不下去,自己研究还不够透彻,继续看streetscape.gl和deck.gl的API和示例。
以下是multi-view的效果

image.png

自己改造后的地下停车场效果
image.png

有研究的小伙伴可以留言交流,另外作者最近又接触了carla,有感兴趣的童鞋也可以交流。


接着往下写,这两天研究了view、view state、viewport,大致理解了一些。写下来防止自己忘了。

View

view可以理解为一个视图窗口,它的组成成员包括,id、在canvas中的x、y坐标位置,视图窗口的长宽(width、height)、特定摄像头参数(例如:摄像头的视野、近平面还是远平面、透视还是正交投影等参数)、视图能够进行的哪些操作controller。deck.gl允许定义多个view,可以将屏幕切分成多个视图。这些视图在操作时能够同步互动也可以独立。
我们来看看streetscape.gl中定义了哪些view。
在/modules/core/utils/constants.js中定义了VIEW_MODE

export const VIEW_MODE = {
  TOP_DOWN: {
    name: 'top-down',
    initialViewState: {},
    orthographic: true,
    tracked: {
      position: true
    }
  },
  PERSPECTIVE: {
    name: 'perspective',
    initialViewState: {
      maxPitch: 85,
      pitch: 60
    },
    tracked: {
      position: true,
      heading: true
    }
  },
  DRIVER: {
    name: 'driver',
    initialProps: {
      maxPitch: 0
    },
    firstPerson: {
      position: [0, 0, 1.5]
    },
    mapInteraction: {
      dragPan: false,
      scrollZoom: false
    }
  }
};

从中可以看出,定义了三种viewmode,并定义相应的controller交互有哪些,初始的viewstate,以及跟踪哪些tracked。
在应用中通过传递viewMode属性给core-3d-viewer,进而初始化view。在core-3d-viewer.js中,获取View如下:

export function getViews(viewMode) {
  const {name, orthographic, firstPerson, mapInteraction} = viewMode;

  const controllerProps = {...mapInteraction, keyboard: false};

  if (firstPerson) {
    return new FirstPersonView({
      id: name,
      fovy: 75,
      near: 1,
      far: 10000,
      focalDistance: 6,
      controller: controllerProps
    });
  }

  return new MapView({
    id: name,
    orthographic,
    controller: controllerProps
  });
}

从中可以看出,getViews生成两类View,分别是FirstPersonView和MapView。针对不同的View,deck.gl Views and Projections
有相关介绍,可以了解。

image.png

View State

View State定义了特定View所对应的当前视图状态(例如:摄像头当前位置、方向、当前放缩级别等等),当视图是可以和用户进行交互时,用户在视图内每平移、旋转、放缩,都会更新view state。
在core-3d-viewer.js中,获取View State如下:

 _getViewState() {
    const {viewMode, frame, viewState, viewOffset} = this.props;
    
    const trackedPosition = frame
      ? {
          longitude: frame.trackPosition[0],
          latitude: frame.trackPosition[1],
          // This is due to a bug in deck.gl where coordinateOrigin.z is not applied
          // Remove when deck is fixed
          altitude: frame.trackPosition[2] - frame.origin[2],
          bearing: 90 - frame.heading
        }
      : null;
    return getViewStates({viewState, viewMode, trackedPosition, offset: viewOffset});
  }

从属性中获取viewMode、当前数据帧frame、当前的viewState以及viewOffset,
从数据帧frame中获取当前车辆的位置、高程、航向角,
利用getViewStates函数进行viewState的计算。
再看看getViewStates:

// Creates viewports that contains information about car position and heading
export function getViewStates({viewState, trackedPosition, viewMode, offset}) {
  const {name, firstPerson, tracked = {}} = viewMode;

  const viewStates = {};

  if (firstPerson) {
    if (trackedPosition) {
      const bearing = trackedPosition.bearing;
      viewState = {
        ...viewState,
        ...firstPerson,
        longitude: trackedPosition.longitude,
        latitude: trackedPosition.latitude,
        bearing: bearing + offset.bearing
      };
    }
    viewStates[name] = viewState;
  } else {
    viewState = {...viewState, transitionDuration: 0};
    offset = {...offset};

    // Track car position & heading
    if (tracked.position && trackedPosition) {
      viewState.longitude = trackedPosition.longitude;
      viewState.latitude = trackedPosition.latitude;
    } else {
      offset.x = 0;
      offset.y = 0;
    }
    if (tracked.heading && trackedPosition) {
      viewState.bearing = trackedPosition.bearing;
    } else {
      offset.bearing = 0;
    }
    // Put the tracked object on the ground
    // TODO - support flying vehicle
    if (trackedPosition) {
      viewState.position = [0, 0, trackedPosition.altitude];
    }
    viewStates[name] = offsetViewState(viewState, offset);
  }
  return viewStates;
}

这段代码第一行注释告诉我们,该函数是计算除包含车辆位置和航向角的viewports。
这段也是我最难理解的,我试着解释看看。
程序进来是一个if判断,我们重点看else这段。
当viewmode是top-down或者perspective时,进入到这段。

viewState = {...viewState, transitionDuration: 0};

首先继承自传参过来的viewState,并添加了transitionDuration属性,设置其为0。也就意味着viewState之间的过渡是立即的。在View State Transitions
中有相应的介绍。
接着通过判断viewmode中跟踪的tracked以及当前传入的trackedPosition设置viewState

    // Track car position & heading
    if (tracked.position && trackedPosition) {
      viewState.longitude = trackedPosition.longitude;
      viewState.latitude = trackedPosition.latitude;
    } else {
      offset.x = 0;
      offset.y = 0;
    }
    if (tracked.heading && trackedPosition) {
      viewState.bearing = trackedPosition.bearing;
    } else {
      offset.bearing = 0;
    }
    // Put the tracked object on the ground
    // TODO - support flying vehicle
    if (trackedPosition) {
      viewState.position = [0, 0, trackedPosition.altitude];
    }

从判断语句可以看出,当viewmode为perspective模式时,viewState会根据车辆的位置和航向角实时调整,当viewmode为top-down模式时,viewState只会根据车辆的位置实时调整。
最后,也是最复杂的来了。通过设置好的viewState和offset,获得对应模式下的viewStates。

 viewStates[name] = offsetViewState(viewState, offset);

来看看offsetViewState这个函数

// Adjust lng/lat to position the car 1/4 from screen bottom
function offsetViewState(viewState, offset) {
  const shiftedViewState = {
    ...viewState,
    bearing: viewState.bearing + offset.bearing
  };

  const helperViewport = new WebMercatorViewport(shiftedViewState);

  const pos = [viewState.width / 2 + offset.x, viewState.height / 2 + offset.y];
  
  const lngLat = [viewState.longitude, viewState.latitude];
  
  const [longitude, latitude] = helperViewport.getLocationAtPoint({
    lngLat,
    pos
  });
  return {
    ...shiftedViewState,
    longitude,
    latitude
  };
}

还是看看注释,是将经纬度坐标调整到车离屏幕底部1/4的位置。
这段代码让我弄不明白的是,哪里看得出是让小车离距离屏幕底部1/4处。

const pos = [viewState.width / 2 + offset.x, viewState.height / 2 + offset.y];

这边也只是取1/2,我打印出viewState的width和height,全是1,因为在view中没有设置width和height,所以采用的默认值1。如果有小伙伴理解这段请指点迷津

  const [longitude, latitude] = helperViewport.getLocationAtPoint({
    lngLat,
    pos
  });

该函数我理解为将当前车辆的经纬度位置匹配到view所指定的像素位置,而返回的视图中心点经纬度坐标。
getLocationAtPoint可以从deck.gl源码中找到

  /**
   * Get the map center that place a given [lng, lat] coordinate at screen
   * point [x, y]
   *
   * @param {Array} lngLat - [lng,lat] coordinates
   *   Specifies a point on the sphere.
   * @param {Array} pos - [x,y] coordinates
   *   Specifies a point on the screen.
   * @return {Array} [lng,lat] new map center.
   */
  getMapCenterByLngLatPosition({lngLat, pos}) {
    const fromLocation = pixelsToWorld(pos, this.pixelUnprojectionMatrix);
    const toLocation = this.projectFlat(lngLat);

    const translate = vec2.add([], toLocation, vec2.negate([], fromLocation));
    const newCenter = vec2.add([], this.center, translate);

    return this.unprojectFlat(newCenter);
  }

  // Legacy method name
  getLocationAtPoint({lngLat, pos}) {
    return this.getMapCenterByLngLatPosition({lngLat, pos});
  }

getLocationAtPoint是一个零时的方法名,实际调用的是getMapCenterByLngLatPosition这个方法,从该方法的命名及首行注释可以看出,该方法是将指定的经纬度放到指定的像素坐标上所获得的地图中心点坐标。

ViewPort

上段代码中,我们看到了一个viewport

const helperViewport = new WebMercatorViewport(shiftedViewState);

从deck.gl文档中了解到viewport
viewport本质上是一个地理空间摄像头,且集成了很多功能,能够将3D坐标正反投影到屏幕坐标上。
viewport class专注于数学运算例如坐标转换,计算视图矩阵或投影矩阵以及webgl顶点着色器所需要的uniforms

image.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容