UNPKG

@itwin/core-frontend

Version:
235 lines • 12.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Tiles */ import { assert, ByteStream } from "@itwin/core-bentley"; import { Point2d, Point3d, Transform } from "@itwin/core-geometry"; import { BatchType, CompositeTileHeader, TileFormat } from "@itwin/core-common"; import { IModelApp } from "../../IModelApp"; import { GraphicBranch } from "../../render/GraphicBranch"; import { ScreenViewport } from "../../Viewport"; import { GltfWrapMode } from "../../common/gltf/GltfSchema"; import { B3dmReader, createDefaultViewFlagOverrides, GltfGraphicsReader, GltfReader, GltfReaderProps, I3dmReader, ImdlReader, readPointCloudTileContent, } from "../../tile/internal"; const defaultViewFlagOverrides = createDefaultViewFlagOverrides({}); const scratchTileCenterWorld = new Point3d(); const scratchTileCenterView = new Point3d(); /** Serves as a "handler" for a specific type of [[TileTree]]. Its primary responsibilities involve loading tile content. * @internal */ export class RealityTileLoader { _produceGeometry; _containsPointClouds = false; preloadRealityParentDepth; preloadRealityParentSkip; constructor(_produceGeometry) { this._produceGeometry = _produceGeometry; this.preloadRealityParentDepth = IModelApp.tileAdmin.contextPreloadParentDepth; this.preloadRealityParentSkip = IModelApp.tileAdmin.contextPreloadParentSkip; } computeTilePriority(tile, viewports, _users) { // ###TODO: Handle case where tile tree reference(s) have a transform different from tree's (background map with ground bias). return RealityTileLoader.computeTileLocationPriority(tile, viewports, tile.tree.iModelTransform); } get wantDeduplicatedVertices() { return false; } get _batchType() { return BatchType.Primary; } get _loadEdges() { return true; } getBatchIdMap() { return undefined; } get isContentUnbounded() { return false; } get containsPointClouds() { return this._containsPointClouds; } get parentsAndChildrenExclusive() { return true; } forceTileLoad(_tile) { return false; } get maximumScreenSpaceError() { return undefined; } processSelectedTiles(selected, _args) { return selected; } // NB: The isCanceled arg is chiefly for tests...in usual case it just returns false if the tile is no longer in 'loading' state. async loadTileContent(tile, data, system, isCanceled) { assert(data instanceof Uint8Array); const blob = data; const streamBuffer = ByteStream.fromUint8Array(blob); const realityTile = tile; return (this._produceGeometry && this._produceGeometry !== "no") ? this.loadGeometryFromStream(realityTile, streamBuffer, system) : this.loadGraphicsFromStream(realityTile, streamBuffer, system, isCanceled); } _getFormat(streamBuffer) { const position = streamBuffer.curPos; const format = streamBuffer.readUint32(); streamBuffer.curPos = position; return format; } async loadGeometryFromStream(tile, streamBuffer, system) { const format = this._getFormat(streamBuffer); if (format !== TileFormat.B3dm) return {}; const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot; const reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, undefined, this.getBatchIdMap()); if (reader) reader.defaultWrapMode = GltfWrapMode.ClampToEdge; const geom = reader?.readGltfAndCreateGeometry(tile.tree.iModelTransform); const xForm = tile.reprojectionTransform; if (tile.tree.reprojectGeometry && geom?.polyfaces && xForm) { const polyfaces = geom.polyfaces.map((pf) => pf.cloneTransformed(xForm)); return { geometry: { polyfaces } }; } else { return { geometry: geom }; } } async loadGraphicsFromStream(tile, streamBuffer, system, isCanceled) { const format = this._getFormat(streamBuffer); if (undefined === isCanceled) isCanceled = () => !tile.isLoading; const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot; let reader; const ecefTransform = tile.tree.iModel.isGeoLocated ? tile.tree.iModel.getEcefTransform() : Transform.createIdentity(); const tileData = { ecefTransform, range: tile.range, layerClassifiers: tile.tree.layerHandler?.layerClassifiers, }; switch (format) { case TileFormat.IModel: reader = ImdlReader.create({ stream: streamBuffer, iModel, modelId, is3d, system, isCanceled, }); break; case TileFormat.Pnts: this._containsPointClouds = true; const res = await readPointCloudTileContent(streamBuffer, iModel, modelId, is3d, tile, system); let graphic = res.graphic; const rtcCenter = res.rtcCenter; if (graphic && (rtcCenter || tile.transformToRoot && !tile.transformToRoot.isIdentity)) { const transformBranch = new GraphicBranch(true); transformBranch.add(graphic); let xform; if (!tile.transformToRoot && rtcCenter) xform = Transform.createTranslation(rtcCenter); else { if (rtcCenter) xform = Transform.createOriginAndMatrix(rtcCenter.plus(tile.transformToRoot.origin), tile.transformToRoot.matrix); else xform = tile.transformToRoot; } graphic = system.createBranch(transformBranch, xform); } return { graphic }; case TileFormat.B3dm: reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, isCanceled, this.getBatchIdMap(), this.wantDeduplicatedVertices, tileData); if (reader) { // glTF spec defaults wrap mode to "repeat" but many reality tiles omit the wrap mode and should not repeat. // The render system also currently only produces mip-maps for repeating textures, and we don't want mip-maps for reality tile textures. assert(reader instanceof GltfReader); reader.defaultWrapMode = GltfWrapMode.ClampToEdge; } break; case TileFormat.I3dm: reader = I3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, isCanceled, undefined, this.wantDeduplicatedVertices, tileData); break; case TileFormat.Gltf: const tree = tile.tree; const baseUrl = tree.baseUrl; const props = createReaderPropsWithBaseUrl(streamBuffer, yAxisUp, baseUrl); if (props) { reader = new GltfGraphicsReader(props, { iModel, gltf: props.glTF, contentRange: tile.contentRange, transform: tile.transformToRoot, hasChildren: !tile.isLeaf, pickableOptions: { id: modelId }, idMap: this.getBatchIdMap(), tileData }); } break; case TileFormat.Cmpt: const header = new CompositeTileHeader(streamBuffer); if (!header.isValid) return {}; const branch = new GraphicBranch(true); for (let i = 0; i < header.tileCount; i++) { const tilePosition = streamBuffer.curPos; streamBuffer.advance(8); // Skip magic and version. const tileBytes = streamBuffer.readUint32(); streamBuffer.curPos = tilePosition; const result = await this.loadGraphicsFromStream(tile, streamBuffer, system, isCanceled); if (result.graphic) branch.add(result.graphic); streamBuffer.curPos = tilePosition + tileBytes; } return { graphic: branch.isEmpty ? undefined : system.createBranch(branch, Transform.createIdentity()), isLeaf: tile.isLeaf }; default: assert(false, `unknown tile format ${format}`); break; } let content = {}; if (undefined !== reader) { try { content = await reader.read(); if (content.containsPointCloud) this._containsPointClouds = true; } catch { // Failure to load should prevent us from trying to load children content.isLeaf = true; } } return content; } get viewFlagOverrides() { return defaultViewFlagOverrides; } static computeTileLocationPriority(tile, viewports, location) { // Compute a priority value for tiles that are: // * Closer to the eye; // * Closer to the center of attention (center of the screen or zoom target). // This way, we can load in priority tiles that are more likely to be important. let center; let minDistance = 1.0; const currentInputState = IModelApp.toolAdmin.currentInputState; const now = Date.now(); const wheelEventRelevanceTimeout = 1000; // Wheel events older than this value will not be considered for (const viewport of viewports) { center = center ?? location.multiplyPoint3d(tile.center, scratchTileCenterWorld); const npc = viewport.worldToNpc(center, scratchTileCenterView); let focusPoint = new Point2d(0.5, 0.5); if (currentInputState.viewport === viewport && viewport instanceof ScreenViewport) { // Try to get a better target point from the last zoom target const { lastWheelEvent } = currentInputState; if (lastWheelEvent !== undefined && now - lastWheelEvent.time < wheelEventRelevanceTimeout) { const focusPointCandidate = Point2d.fromJSON(viewport.worldToNpc(lastWheelEvent.point)); if (focusPointCandidate.x > 0 && focusPointCandidate.x < 1 && focusPointCandidate.y > 0 && focusPointCandidate.y < 1) focusPoint = focusPointCandidate; } } // NB: In NPC coords, 0 = far plane, 1 = near plane. const distanceToEye = 1.0 - npc.z; const distanceToCenter = Math.min(npc.distanceXY(focusPoint) / 0.707, 1.0); // Math.sqrt(0.5) = 0.707 // Distance is a mix of the two previously computed values, still in range [0; 1] // We use this factor to determine how much the distance to the center of attention is important compared to distance to the eye const distanceToCenterWeight = 0.3; const distance = distanceToEye * (1.0 - distanceToCenterWeight) + distanceToCenter * distanceToCenterWeight; minDistance = Math.min(distance, minDistance); } return minDistance; } } /** Exposed strictly for testing purposes. * @internal */ export function createReaderPropsWithBaseUrl(streamBuffer, yAxisUp, baseUrl) { let url; if (baseUrl) { try { url = new URL(baseUrl); } catch { url = undefined; } } return GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp, url); } //# sourceMappingURL=RealityTileLoader.js.map