在第一篇中streetscape.gl学习笔记(一),大致对LogViewer (React Component)有所了解,现就看看它的核心组成组件Core3DViewer。
Core3DViewer大体认识
Core3DViewer组件主要由DeckGL、StaticMap、ObjectLabelsOverlay三个子组件组成。这里DeckGL、StaticMap又分别对应uber的另外两个开源产品deck.gl和react-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格式)可以在这里替换。
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 (
{showMap && (
)}
{this.props.children}
);
}
我们关注
views
在搜索到views属性时,Deck.gl推荐先了解一下Views and Projections
view是什么,能否实现多个视图,并同步视图状态,写到这实在写不下去,自己研究还不够透彻,继续看streetscape.gl和deck.gl的API和示例。
以下是multi-view的效果
自己改造后的地下停车场效果
有研究的小伙伴可以留言交流,另外作者最近又接触了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
有相关介绍,可以了解。
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