-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
518 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import {Vector3} from 'math.gl'; | ||
import {CullingVolume, Plane} from '@math.gl/culling'; | ||
import {Ellipsoid} from '@math.gl/geospatial'; | ||
|
||
const scratchPlane = new Plane(); | ||
const scratchPosition = new Vector3(); | ||
const cullingVolume = new CullingVolume([ | ||
new Plane(), | ||
new Plane(), | ||
new Plane(), | ||
new Plane(), | ||
new Plane(), | ||
new Plane() | ||
]); | ||
|
||
// Extracts a frame state appropriate for tile culling from a deck.gl viewport | ||
// TODO - this could likely be generalized and merged back into deck.gl for other culling scenarios | ||
export function getFrameState(viewport, frameNumber) { | ||
// Traverse and and request. Update _selectedTiles so that we know what to render. | ||
const {cameraDirection, cameraUp, height} = viewport; | ||
const {metersPerPixel} = viewport.distanceScales; | ||
|
||
const viewportCenterCartographic = [viewport.longitude, viewport.latitude, 0]; | ||
// TODO - Ellipsoid.eastNorthUpToFixedFrame() breaks on raw array, create a Vector. | ||
// TODO - Ellipsoid.eastNorthUpToFixedFrame() takes a cartesian, is that intuitive? | ||
const viewportCenterCartesian = Ellipsoid.WGS84.cartographicToCartesian( | ||
viewportCenterCartographic, | ||
new Vector3() | ||
); | ||
const enuToFixedTransform = Ellipsoid.WGS84.eastNorthUpToFixedFrame(viewportCenterCartesian); | ||
|
||
const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition); | ||
const cameraPositionCartesian = Ellipsoid.WGS84.cartographicToCartesian( | ||
cameraPositionCartographic, | ||
new Vector3() | ||
); | ||
|
||
// These should still be normalized as the transform has scale 1 (goes from meters to meters) | ||
const cameraDirectionCartesian = new Vector3( | ||
enuToFixedTransform.transformAsVector(new Vector3(cameraDirection).scale(metersPerPixel)) | ||
).normalize(); | ||
const cameraUpCartesian = new Vector3( | ||
enuToFixedTransform.transformAsVector(new Vector3(cameraUp).scale(metersPerPixel)) | ||
).normalize(); | ||
|
||
commonSpacePlanesToWGS84(viewport); | ||
|
||
// TODO: make a file/class for frameState and document what needs to be attached to this so that traversal can function | ||
return { | ||
camera: { | ||
position: cameraPositionCartesian, | ||
direction: cameraDirectionCartesian, | ||
up: cameraUpCartesian | ||
}, | ||
height, | ||
cullingVolume, | ||
frameNumber, // TODO: This can be the same between updates, what number is unique for between updates? | ||
sseDenominator: 1.15 // Assumes fovy = 60 degrees | ||
}; | ||
} | ||
|
||
function commonSpacePlanesToWGS84(viewport) { | ||
// Extract frustum planes based on current view. | ||
const viewportCenterCartographic = [viewport.longitude, viewport.latitude, 0]; | ||
const viewportCenterCartesian = Ellipsoid.WGS84.cartographicToCartesian( | ||
viewportCenterCartographic, | ||
new Vector3() | ||
); | ||
|
||
const frustumPlanes = viewport.getFrustumPlanes(); | ||
let i = 0; | ||
for (const dir in frustumPlanes) { | ||
const plane = frustumPlanes[dir]; | ||
const distanceToCenter = plane.normal.dot(viewport.center); | ||
scratchPosition | ||
.copy(plane.normal) | ||
.scale(plane.distance - distanceToCenter) | ||
.add(viewport.center); | ||
const cartographicPos = viewport.unprojectPosition(scratchPosition); | ||
|
||
const cartesianPos = Ellipsoid.WGS84.cartographicToCartesian(cartographicPos, new Vector3()); | ||
|
||
scratchPlane.normal | ||
.copy(cartesianPos) | ||
.subtract(viewportCenterCartesian) | ||
.scale(-1) // Want the normal to point into the frustum since that's what culling expects | ||
.normalize(); | ||
scratchPlane.distance = Math.abs(scratchPlane.normal.dot(cartesianPos)); | ||
|
||
cullingVolume.planes[i].normal.copy(scratchPlane.normal); | ||
cullingVolume.planes[i].distance = scratchPlane.distance; | ||
i = i + 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
// NOTE: This is a copy of the deck.gl layer | ||
// Use this for easy debugging | ||
/* global fetch */ | ||
|
||
import {COORDINATE_SYSTEM, CompositeLayer} from '@deck.gl/core'; | ||
import {PointCloudLayer} from '@deck.gl/layers'; | ||
import {ScenegraphLayer} from '@deck.gl/mesh-layers'; | ||
|
||
import {Tileset3D, _getIonTilesetMetadata} from '@loaders.gl/3d-tiles'; | ||
|
||
import {getFrameState} from './get-frame-state'; | ||
|
||
const defaultProps = { | ||
getPointColor: [0, 0, 0], | ||
pointSize: 1.0, | ||
opacity: 1.0, | ||
|
||
data: null, | ||
_ionAssetId: null, | ||
_ionAccessToken: null, | ||
loadOptions: {throttleRequests: true}, | ||
|
||
onTilesetLoad: tileset3d => {}, | ||
onTileLoad: tileHeader => {}, | ||
onTileUnload: tileHeader => {}, | ||
onTileLoadFail: (tile, message, url) => {} | ||
}; | ||
|
||
export default class Tile3DLayer extends CompositeLayer { | ||
initializeState() { | ||
this.state = { | ||
layerMap: {}, | ||
tileset3d: null | ||
}; | ||
} | ||
|
||
shouldUpdateState({changeFlags}) { | ||
return changeFlags.somethingChanged; | ||
} | ||
|
||
async updateState({props, oldProps}) { | ||
if (props.data && props.data !== oldProps.data) { | ||
await this._loadTileset(props.data); | ||
} else if ( | ||
(props._ionAccessToken || props._ionAssetId) && | ||
(props._ionAccessToken !== oldProps._ionAccessToken || | ||
props._ionAssetId !== oldProps._ionAssetId) | ||
) { | ||
await this._loadTilesetFromIon(props._ionAccessToken, props._ionAssetId); | ||
} | ||
|
||
const {tileset3d} = this.state; | ||
await this._updateTileset(tileset3d); | ||
} | ||
|
||
async _loadTileset(tilesetUrl, fetchOptions, ionMetadata) { | ||
const response = await fetch(tilesetUrl, fetchOptions); | ||
const tilesetJson = await response.json(); | ||
|
||
const loadOptions = this.getLoadOptions(); | ||
|
||
const tileset3d = new Tileset3D(tilesetJson, tilesetUrl, { | ||
onTileLoad: tileHeader => { | ||
this.props.onTileLoad(tileHeader); | ||
this._updateTileset(tileset3d); | ||
this.setNeedsUpdate(); | ||
}, | ||
onTileUnload: this.props.onTileUnload, | ||
onTileLoadFail: this.props.onTileLoadFail, | ||
// TODO: explicit passing should not be needed, registerLoaders should suffice | ||
fetchOptions, | ||
...ionMetadata, | ||
...loadOptions | ||
}); | ||
|
||
this.setState({ | ||
tileset3d, | ||
layerMap: {} | ||
}); | ||
|
||
if (tileset3d) { | ||
this.props.onTilesetLoad(tileset3d); | ||
} | ||
} | ||
|
||
async _loadTilesetFromIon(ionAccessToken, ionAssetId) { | ||
const ionMetadata = await _getIonTilesetMetadata(ionAccessToken, ionAssetId); | ||
const {url, headers} = ionMetadata; | ||
return await this._loadTileset(url, {headers}, ionMetadata); | ||
} | ||
|
||
_updateTileset(tileset3d) { | ||
const {timeline, viewport} = this.context; | ||
if (!timeline || !viewport || !tileset3d) { | ||
return; | ||
} | ||
|
||
// use Date.now() as frame identifier for now and later used to filter layers for rendering | ||
const frameState = getFrameState(viewport, Date.now()); | ||
tileset3d.update(frameState); | ||
this._updateLayerMap(frameState.frameNumber); | ||
} | ||
|
||
// `Layer` instances is created and added to the map if it doesn't exist yet. | ||
_updateLayerMap(frameNumber) { | ||
const {tileset3d, layerMap} = this.state; | ||
|
||
// create layers for new tiles | ||
const {selectedTiles} = tileset3d; | ||
const tilesWithoutLayer = selectedTiles.filter(tile => !layerMap[tile.fullUri]); | ||
|
||
for (const tile of tilesWithoutLayer) { | ||
// TODO - why do we call this here? Being "selected" should automatically add it to cache? | ||
tileset3d.addTileToCache(tile); | ||
|
||
layerMap[tile.fullUri] = { | ||
layer: this._create3DTileLayer(tile), | ||
tile | ||
}; | ||
} | ||
|
||
// update layer visibility | ||
this._selectLayers(frameNumber); | ||
} | ||
|
||
// Grab only those layers who were selected this frame. | ||
_selectLayers(frameNumber) { | ||
const {layerMap} = this.state; | ||
const layerMapValues = Object.values(layerMap); | ||
|
||
for (const value of layerMapValues) { | ||
const {tile} = value; | ||
let {layer} = value; | ||
|
||
if (tile.selectedFrame === frameNumber) { | ||
if (layer && layer.props && !layer.props.visible) { | ||
// Still has GPU resource but visibility is turned off so turn it back on so we can render it. | ||
layer = layer.clone({visible: true}); | ||
layerMap[tile.fullUri].layer = layer; | ||
} | ||
} else if (tile.contentUnloaded) { | ||
// Was cleaned up from tileset cache. We no longer need to track it. | ||
delete layerMap[tile.fullUri]; | ||
} else if (layer && layer.props && layer.props.visible) { | ||
// Still in tileset cache but doesn't need to render this frame. Keep the GPU resource bound but don't render it. | ||
layer = layer.clone({visible: false}); | ||
layerMap[tile.fullUri].layer = layer; | ||
} | ||
} | ||
|
||
this.setState({layers: Object.values(layerMap).map(layer => layer.layer)}); | ||
} | ||
|
||
_create3DTileLayer(tileHeader) { | ||
if (!tileHeader.content) { | ||
return null; | ||
} | ||
|
||
switch (tileHeader.content.type) { | ||
case 'pnts': | ||
return this._createPointCloudTileLayer(tileHeader); | ||
case 'i3dm': | ||
case 'b3dm': | ||
return this._create3DModelTileLayer(tileHeader); | ||
default: | ||
throw new Error(`Tile3DLayer: Failed to render layer of type ${tileHeader.content.type}`); | ||
} | ||
} | ||
|
||
_create3DModelTileLayer(tileHeader) { | ||
const {gltf} = tileHeader.content; | ||
const {instances, cartographicOrigin, modelMatrix} = tileHeader.content; | ||
|
||
const SubLayerClass = this.getSubLayerClass('scenegraph', ScenegraphLayer); | ||
|
||
return new SubLayerClass( | ||
{ | ||
_lighting: 'pbr' | ||
}, | ||
this.getSubLayerProps({ | ||
id: 'scenegraph' | ||
}), | ||
{ | ||
id: `${this.id}-scenegraph-${tileHeader.fullUri}`, | ||
// Fix for ScenegraphLayer.modelMatrix, under flag in deck 7.3 to avoid breaking existing code | ||
data: instances || [{}], | ||
scenegraph: gltf, | ||
|
||
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS, | ||
coordinateOrigin: cartographicOrigin, | ||
modelMatrix, | ||
_composeModelMatrix: true, | ||
getTransformMatrix: instance => instance.modelMatrix, | ||
getPosition: instance => [0, 0, 0] | ||
} | ||
); | ||
} | ||
|
||
_createPointCloudTileLayer(tileHeader) { | ||
const { | ||
attributes, | ||
pointCount, | ||
constantRGBA, | ||
cartographicOrigin, | ||
modelMatrix | ||
} = tileHeader.content; | ||
const {positions, normals, colors} = attributes; | ||
|
||
if (!positions) { | ||
return null; | ||
} | ||
|
||
const {pointSize, getPointColor} = this.props; | ||
const SubLayerClass = this.getSubLayerClass('pointcloud', PointCloudLayer); | ||
|
||
return new SubLayerClass( | ||
{ | ||
pointSize | ||
}, | ||
this.getSubLayerProps({ | ||
id: 'pointcloud' | ||
}), | ||
{ | ||
id: `${this.id}-pointcloud-${tileHeader.fullUri}`, | ||
data: { | ||
header: { | ||
vertexCount: pointCount | ||
}, | ||
attributes: { | ||
POSITION: positions, | ||
NORMAL: normals, | ||
COLOR_0: colors | ||
} | ||
}, | ||
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS, | ||
coordinateOrigin: cartographicOrigin, | ||
modelMatrix, | ||
|
||
getColor: constantRGBA || getPointColor | ||
} | ||
); | ||
} | ||
|
||
renderLayers() { | ||
return this.state.layers; | ||
} | ||
} | ||
|
||
Tile3DLayer.layerName = 'Tile3DLayer'; | ||
Tile3DLayer.defaultProps = defaultProps; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.