diff --git a/src/chunk_manager/backend.ts b/src/chunk_manager/backend.ts index df56bb02bb..8f5c905adf 100644 --- a/src/chunk_manager/backend.ts +++ b/src/chunk_manager/backend.ts @@ -450,8 +450,11 @@ function startChunkDownload(chunk: Chunk) { (error: any) => { if (chunk.downloadAbortController === downloadAbortController) { chunk.downloadAbortController = undefined; - chunk.downloadFailed(error); - console.log(`Error retrieving chunk ${chunk}: ${error}`); + if (error === "retry") { + } else { + chunk.downloadFailed(error); + console.log(`Error retrieving chunk ${chunk}: ${error}`); + } } }, ); diff --git a/src/chunk_manager/base.ts b/src/chunk_manager/base.ts index 81b3c912ab..1bd99013a0 100644 --- a/src/chunk_manager/base.ts +++ b/src/chunk_manager/base.ts @@ -110,6 +110,10 @@ export interface ChunkSourceParametersConstructor { RPC_ID: string; } +export interface ChunkSourceStateConstructor { + new (): T; +} + export class LayerChunkProgressInfo { numVisibleChunksNeeded = 0; numVisibleChunksAvailable = 0; diff --git a/src/chunk_manager/frontend.ts b/src/chunk_manager/frontend.ts index 3df76ce075..b70bee5538 100644 --- a/src/chunk_manager/frontend.ts +++ b/src/chunk_manager/frontend.ts @@ -16,6 +16,7 @@ import type { ChunkSourceParametersConstructor, + ChunkSourceStateConstructor, LayerChunkProgressInfo, } from "#src/chunk_manager/base.js"; import { @@ -493,22 +494,30 @@ export interface ChunkSource { export function WithParameters< Parameters, + State, TBase extends ChunkSourceConstructor, >( Base: TBase, parametersConstructor: ChunkSourceParametersConstructor, + state?: ChunkSourceStateConstructor, ) { + state; type WithParametersOptions = InstanceType["OPTIONS"] & { parameters: Parameters; + state?: State; }; @registerSharedObjectOwner(parametersConstructor.RPC_ID) class C extends Base { declare OPTIONS: WithParametersOptions; parameters: Parameters; + state: State; constructor(...args: any[]) { super(...args); const options: WithParametersOptions = args[1]; this.parameters = options.parameters; + if (options.state) { + this.state = options.state; + } } initializeCounterpart(rpc: RPC, options: any) { options.parameters = this.parameters; diff --git a/src/datasource/graphene/backend.ts b/src/datasource/graphene/backend.ts index b8dec5afb5..15fdc97f07 100644 --- a/src/datasource/graphene/backend.ts +++ b/src/datasource/graphene/backend.ts @@ -27,13 +27,14 @@ import { getGrapheneFragmentKey, GRAPHENE_MESH_NEW_SEGMENT_RPC_ID, ChunkedGraphSourceParameters, - MeshSourceParameters, + MeshSourceParametersWithFocus, CHUNKED_GRAPH_LAYER_RPC_ID, CHUNKED_GRAPH_RENDER_LAYER_UPDATE_SOURCES_RPC_ID, RENDER_RATIO_LIMIT, isBaseSegmentId, parseGrapheneError, getHttpSource, + startLayerForBBox, } from "#src/datasource/graphene/base.js"; import { decodeManifestChunk } from "#src/datasource/precomputed/backend.js"; import { WithSharedKvStoreContextCounterpart } from "#src/kvstore/backend.js"; @@ -98,7 +99,7 @@ function downloadFragmentWithSharding( function downloadFragment( fragmentKvStore: KvStoreWithPath, fragmentId: string, - parameters: MeshSourceParameters, + parameters: MeshSourceParametersWithFocus, signal: AbortSignal, ): Promise { if (parameters.sharding) { @@ -123,7 +124,7 @@ async function decodeDracoFragmentChunk( @registerSharedObject() export class GrapheneMeshSource extends WithParameters( WithSharedKvStoreContextCounterpart(MeshSource), - MeshSourceParameters, + MeshSourceParametersWithFocus, ) { manifestRequestCount = new Map(); newSegments = new Uint64Set(); @@ -136,6 +137,13 @@ export class GrapheneMeshSource extends WithParameters( this.parameters.fragmentUrl, ); + focusBoundingBox: SharedWatchableValue; + + constructor(rpc: RPC, options: MeshSourceParametersWithFocus) { + super(rpc, options); + this.focusBoundingBox = rpc.get(options.focusBoundingBox); + } + addNewSegment(segment: bigint) { const { newSegments } = this; newSegments.add(segment); @@ -146,12 +154,41 @@ export class GrapheneMeshSource extends WithParameters( } async download(chunk: ManifestChunk, signal: AbortSignal) { + const { + focusBoundingBox: { value: focusBoundingBox }, + } = this; + const { chunkSize, nBitsForLayerId } = this.parameters; + + const unregister = this.registerDisposer( + this.focusBoundingBox.changed.add(() => { + chunk.downloadAbortController?.abort("retry"); + unregister(); + if (chunk.newRequestedState !== ChunkState.NEW) { + // re-download manifest + this.chunkManager.queueManager.updateChunkState( + chunk, + ChunkState.QUEUED, + ); + } + }), + ); + const { parameters, newSegments, manifestRequestCount } = this; - if (isBaseSegmentId(chunk.objectId, parameters.nBitsForLayerId)) { + if (isBaseSegmentId(chunk.objectId, nBitsForLayerId)) { return decodeManifestChunk(chunk, { fragments: [] }); } + const { fetchOkImpl, baseUrl } = this.manifestHttpSource; - const manifestPath = `/manifest/${chunk.objectId}:${parameters.lod}?verify=1&prepend_seg_ids=1`; + let manifestPath = `/manifest/${chunk.objectId}:${parameters.lod}?verify=1&prepend_seg_ids=1`; + if (focusBoundingBox) { + const rank = focusBoundingBox.length / 2; + const startLayer = startLayerForBBox(focusBoundingBox, chunkSize); + const boundsStr = Array.from( + new Array(rank), + (_, i) => `${focusBoundingBox[i]}-${focusBoundingBox[i + rank]}`, + ).join("_"); + manifestPath += `&bounds=${boundsStr}&start_layer=${startLayer}`; + } const response = await ( await fetchOkImpl(baseUrl + manifestPath, { signal }) ).json(); diff --git a/src/datasource/graphene/base.ts b/src/datasource/graphene/base.ts index 844cddf352..145cc54f8e 100644 --- a/src/datasource/graphene/base.ts +++ b/src/datasource/graphene/base.ts @@ -27,7 +27,7 @@ import type { DataType, } from "#src/sliceview/base.js"; import { makeSliceViewChunkSpecification } from "#src/sliceview/base.js"; -import type { mat4 } from "#src/util/geom.js"; +import type { mat4, vec3 } from "#src/util/geom.js"; import type { FetchOk, HttpError } from "#src/util/http_request.js"; export const PYCG_APP_VERSION = 1; @@ -59,10 +59,14 @@ export class MeshSourceParameters { lod: number; sharding: Array | undefined; nBitsForLayerId: number; - + chunkSize: vec3; static RPC_ID = "graphene/MeshSource"; } +export class MeshSourceParametersWithFocus extends MeshSourceParameters { + focusBoundingBox: number; +} + export class MultiscaleMeshMetadata { transform: mat4; lodScaleMultiplier: number; @@ -172,3 +176,17 @@ export function getHttpSource( } return { fetchOkImpl, baseUrl: joinBaseUrlAndPath(baseUrl, path) }; } + +export const startLayerForBBox = ( + focusBoundingBox: Float32Array, + chunkSize: vec3, +) => { + const rank = 3; // TODO need to change vec3 otherwise it doesn't make sense to make this a variable + let minChunks = Number.POSITIVE_INFINITY; + for (let i = 0; i < rank; i++) { + const length = focusBoundingBox[i + rank] - focusBoundingBox[i]; + const numChunks = Math.ceil(length / chunkSize[i]); + minChunks = Math.min(minChunks, numChunks); + } + return 2 + Math.max(0, Math.floor(Math.log2(minChunks / 1))); +}; diff --git a/src/datasource/graphene/frontend.ts b/src/datasource/graphene/frontend.ts index d3fc71fa18..c34d331fbe 100644 --- a/src/datasource/graphene/frontend.ts +++ b/src/datasource/graphene/frontend.ts @@ -22,6 +22,7 @@ import { } from "#src/annotation/annotation_layer_state.js"; import type { AnnotationReference, + AxisAlignedBoundingBox, Line, Point, } from "#src/annotation/index.js"; @@ -176,6 +177,7 @@ import { verifyEnumString, verifyFiniteFloat, verifyFinitePositiveFloat, + verifyFloatArray, verifyInt, verifyNonnegativeInt, verifyObject, @@ -192,6 +194,12 @@ import type { Trackable } from "#src/util/trackable.js"; import { makeDeleteButton } from "#src/widget/delete_button.js"; import type { DependentViewContext } from "#src/widget/dependent_view_widget.js"; import { makeIcon } from "#src/widget/icon.js"; +import { + addLayerControlToOptionsTab, + registerLayerControl, +} from "#src/widget/layer_control.js"; +import { RPC } from "#src/worker_rpc.js"; +import { rangeLayerControl } from "#src/widget/layer_control_range.js"; function vec4FromVec3(vec: vec3, alpha = 0) { const res = vec4.clone([...vec]); @@ -211,14 +219,120 @@ const BLUE_COLOR_SEGMENT_PACKED = BigInt(packColor(BLUE_COLOR_SEGMENT)); const TRANSPARENT_COLOR_PACKED = BigInt(packColor(TRANSPARENT_COLOR)); const MULTICUT_OFF_COLOR = vec4.fromValues(0, 0, 0, 0.5); +class GrapheneState extends RefCounted implements Trackable { + changed = new NullarySignal(); + + public multicutState = new MulticutState(); + public mergeState = new MergeState(); + + public focusBoundingBox = new TrackableValue( + undefined, + (val) => { + const x = verifyFloatArray(val); + return new Float32Array(x); + }, + ); + public focusBoundingBoxSize = new TrackableValue(0, verifyNonnegativeInt); + public focusTrackGlobalPosition = new TrackableBoolean(true); + + constructor() { + super(); + this.registerDisposer( + this.multicutState.changed.add(() => { + this.changed.dispatch(); + }), + ); + this.registerDisposer( + this.mergeState.changed.add(() => { + this.changed.dispatch(); + }), + ); + this.registerDisposer( + this.focusBoundingBox.changed.add(() => { + this.changed.dispatch(); + }), + ); + this.registerDisposer( + this.focusBoundingBoxSize.changed.add(() => { + this.changed.dispatch(); + }), + ); + this.registerDisposer( + this.focusTrackGlobalPosition.changed.add(() => { + this.changed.dispatch(); + }), + ); + } + + reset() { + this.multicutState.reset(); + this.mergeState.reset(); + this.focusBoundingBox.reset(); + this.focusBoundingBoxSize.reset(); + this.focusTrackGlobalPosition.reset(); + } + + toJSON() { + return { + [MULTICUT_JSON_KEY]: this.multicutState.toJSON(), + [MERGE_JSON_KEY]: this.mergeState.toJSON(), + [FOCUS_BOUNDING_BOX_JSON_KEY]: + this.focusBoundingBox.value === undefined + ? undefined + : Array.from(this.focusBoundingBox.value), + [FOCUS_BOUNDING_BOX_SIZE_JSON_KEY]: this.focusBoundingBoxSize.toJSON(), + [FOCUS_TRACK_GLOBAL_POSITION_JSON_KEY]: + this.focusTrackGlobalPosition.toJSON(), + }; + } + + restoreState(x: any) { + verifyOptionalObjectProperty(x, MULTICUT_JSON_KEY, (value) => { + this.multicutState.restoreState(value); + }); + verifyOptionalObjectProperty(x, MERGE_JSON_KEY, (value) => { + this.mergeState.restoreState(value); + }); + verifyOptionalObjectProperty(x, FOCUS_BOUNDING_BOX_JSON_KEY, (value) => { + this.focusBoundingBox.restoreState(value); + }); + verifyOptionalObjectProperty( + x, + FOCUS_BOUNDING_BOX_SIZE_JSON_KEY, + (value) => { + this.focusBoundingBoxSize.restoreState(value); + }, + ); + verifyOptionalObjectProperty( + x, + FOCUS_TRACK_GLOBAL_POSITION_JSON_KEY, + (value) => { + this.focusTrackGlobalPosition.restoreState(value); + }, + ); + } +} + class GrapheneMeshSource extends WithParameters( WithSharedKvStoreContext(MeshSource), MeshSourceParameters, + GrapheneState, ) { getFragmentKey(objectKey: string | null, fragmentId: string) { objectKey; return getGrapheneFragmentKey(fragmentId); } + + initializeCounterpart(rpc: RPC, options: MeshSourceParameters): void { + const focusBoundingBox = SharedWatchableValue.makeFromExisting( + rpc!, + this.state.focusBoundingBox, + ); + super.initializeCounterpart(rpc, { + ...options, + focusBoundingBox: focusBoundingBox.rpcId!, + }); + } } class AppInfo { @@ -514,10 +628,12 @@ function parseGrapheneShardingParameters( function getShardedMeshSource( sharedKvStoreContext: SharedKvStoreContext, parameters: MeshSourceParameters, + state: GrapheneState, ) { return sharedKvStoreContext.chunkManager.getChunkSource(GrapheneMeshSource, { sharedKvStoreContext, parameters, + state, }); } @@ -526,7 +642,9 @@ async function getMeshSource( url: string, fragmentUrl: string, nBitsForLayerId: number, + chunkSize: vec3, options: ProgressOptions, + state: GrapheneState, ) { const { metadata, segmentPropertyMap } = await getMeshMetadata( sharedKvStoreContext, @@ -539,10 +657,11 @@ async function getMeshSource( lod: 0, sharding: metadata?.sharding, nBitsForLayerId, + chunkSize, }; const transform = metadata?.transform || mat4.create(); return { - source: getShardedMeshSource(sharedKvStoreContext, parameters), + source: getShardedMeshSource(sharedKvStoreContext, parameters, state), transform, segmentPropertyMap, }; @@ -654,7 +773,9 @@ async function getVolumeDataSource( ), ), info.graph.nBitsForLayerId, + info.graph.chunkSize, options, + state, ); const subsourceToModelSubspaceTransform = getSubsourceToModelSubspaceTransform(info); @@ -823,43 +944,9 @@ const SINK_JSON_KEY = "sink"; const SOURCE_JSON_KEY = "source"; const MERGED_ROOT_JSON_KEY = "mergedRoot"; const LOCKED_JSON_KEY = "locked"; - -class GrapheneState implements Trackable { - changed = new NullarySignal(); - - public multicutState = new MulticutState(); - public mergeState = new MergeState(); - - constructor() { - this.multicutState.changed.add(() => { - this.changed.dispatch(); - }); - this.mergeState.changed.add(() => { - this.changed.dispatch(); - }); - } - - reset() { - this.multicutState.reset(); - this.mergeState.reset(); - } - - toJSON() { - return { - [MULTICUT_JSON_KEY]: this.multicutState.toJSON(), - [MERGE_JSON_KEY]: this.mergeState.toJSON(), - }; - } - - restoreState(x: any) { - verifyOptionalObjectProperty(x, MULTICUT_JSON_KEY, (value) => { - this.multicutState.restoreState(value); - }); - verifyOptionalObjectProperty(x, MERGE_JSON_KEY, (value) => { - this.mergeState.restoreState(value); - }); - } -} +const FOCUS_BOUNDING_BOX_JSON_KEY = "focusBoundingBox"; +const FOCUS_BOUNDING_BOX_SIZE_JSON_KEY = "focusBoundingBoxSize"; +const FOCUS_TRACK_GLOBAL_POSITION_JSON_KEY = "focusTrackGlobalPosition"; export interface SegmentSelection { segmentId: bigint; @@ -1069,6 +1156,10 @@ class MulticutState extends RefCounted implements Trackable { } } +const clamp = (val: number, min: number, max: number) => { + return Math.max(min, Math.min(max, val)); +}; + class GraphConnection extends SegmentationGraphSourceConnection { public annotationLayerStates: AnnotationLayerState[] = []; public mergeAnnotationState: AnnotationLayerState; @@ -1080,6 +1171,86 @@ class GraphConnection extends SegmentationGraphSourceConnection { public state: GrapheneState, ) { super(graph, layer.displayState.segmentationGroupState.value); + + const updateMeshBounds = () => { + const { + focusBoundingBox: { value: focusBoundingBox }, + } = state; + if (!focusBoundingBox) { + layer.displayState.bounds.value = undefined; + return; + } + const { rank } = this.chunkSource; + const { resolution } = this.chunkSource.info.scales[0]; + const bounds = new Float32Array(rank * 2); + for (let i = 0; i < rank; i++) { + bounds[i] = focusBoundingBox[i] * resolution[i]; + bounds[i + rank] = focusBoundingBox[i + rank] * resolution[i]; + } + layer.displayState.bounds.value = bounds; + }; + updateMeshBounds(); + this.registerDisposer(state.focusBoundingBox.changed.add(updateMeshBounds)); + + const updateFocusBoundingBox = () => { + const { + focusBoundingBoxSize: { value: focusBoundingBoxSize }, + } = state; + if (focusBoundingBoxSize === 0) { + state.focusBoundingBox.value = undefined; + return; + } + const { rank } = this.chunkSource; + const { resolution, size, voxelOffset } = this.chunkSource.info.scales[0]; + const boundingBox = new Float32Array(rank * 2); + + let center: Float32Array = new Float32Array(rank); + + const currentBoundingBox = state.focusBoundingBox.value; + if (state.focusTrackGlobalPosition.value) { + const centerPosition = getGlobalPositionInLayerCoordinates( + layer.managedLayer.manager.root.globalPosition.value, + this.layer, + ); + if (!centerPosition) { + return; + } + center = centerPosition; + } else if (currentBoundingBox) { + for (let i = 0; i < rank; i++) { + center[i] = + (currentBoundingBox[i] + currentBoundingBox[i + rank]) / 2; + } + } + for (let i = 0; i < rank; i++) { + const boundingBoxLength = + (focusBoundingBoxSize ** 3 * resolution[0]) / resolution[i]; + boundingBox[i] = clamp( + Math.round(center[i] - boundingBoxLength / 2), + voxelOffset[i], + voxelOffset[i] + size[i], + ); + boundingBox[i + rank] = clamp( + Math.round(center[i] + boundingBoxLength / 2), + voxelOffset[i], + voxelOffset[i] + size[i], + ); + } + state.focusBoundingBox.value = boundingBox; + }; + + this.registerDisposer( + layer.managedLayer.manager.root.globalPosition.changed.add(() => { + if (state.focusTrackGlobalPosition.value) { + updateFocusBoundingBox(); + } + }), + ); + this.registerDisposer( + state.focusBoundingBoxSize.changed.add(updateFocusBoundingBox), + ); + updateFocusBoundingBox(); + const segmentsState = layer.displayState.segmentationGroupState.value; segmentsState.selectedSegments.changed.add( (segmentIds: bigint[] | bigint | null, add: boolean) => { @@ -1120,7 +1291,55 @@ class GraphConnection extends SegmentationGraphSourceConnection { ); synchronizeAnnotationSource(multicutState.sinks, redGroup); synchronizeAnnotationSource(multicutState.sources, blueGroup); - annotationLayerStates.push(redGroup, blueGroup); + + const focusArea = makeColoredAnnotationState( + layer, + loadedSubsource, + "focus area", + vec3.fromValues(0.0, 1.0, 0.8), + ); + + const focusAreaChunk = makeColoredAnnotationState( + layer, + loadedSubsource, + "focus area chunk", + vec3.fromValues(1.0, 0.0, 0.4), + ); + + const updateFocusBoundingBoxAnnotation = () => { + const annotationSource = focusArea.source; + for (const annotation of annotationSource) { + annotationSource.delete(annotationSource.getReference(annotation.id)); + } + const annotationSource2 = focusAreaChunk.source; + for (const annotation of annotationSource2) { + annotationSource2.delete(annotationSource2.getReference(annotation.id)); + } + const { + focusBoundingBox: { value: focusBoundingBox }, + } = state; + if (focusBoundingBox === undefined) return; + const { rank } = this.chunkSource; + + { + const pointA = new Float32Array(focusBoundingBox.slice(0, rank)); + const pointB = new Float32Array(focusBoundingBox.slice(rank)); + const annotation: AxisAlignedBoundingBox = { + id: "", + pointA, + pointB, + type: AnnotationType.AXIS_ALIGNED_BOUNDING_BOX, + properties: [], + }; + annotationSource.add(annotation); + } + }; + + updateFocusBoundingBoxAnnotation(); + + state.focusBoundingBox.changed.add(updateFocusBoundingBoxAnnotation); + + annotationLayerStates.push(redGroup, blueGroup, focusArea, focusAreaChunk); if (layer.tool.value instanceof MergeSegmentsPlaceLineTool) { layer.tool.value = undefined; @@ -1746,6 +1965,24 @@ class GrapheneGraphSource extends SegmentationGraphSource { parent.style.display = "contents"; const toolbox = document.createElement("div"); toolbox.className = "neuroglancer-segmentation-toolbox"; + parent.appendChild( + addLayerControlToOptionsTab( + tab, + layer, + tab.visibility, + focusBoundingBoxSizeControl, + ), + ); + { + const checkbox = tab.registerDisposer( + new TrackableBooleanCheckbox(this.state.focusTrackGlobalPosition), + ); + const label = document.createElement("label"); + label.appendChild(document.createTextNode("Focus track global position")); + label.title = "todo"; + label.appendChild(checkbox.element); + parent.appendChild(label); + } toolbox.appendChild( makeToolButton(context, layer.toolBinder, { toolJson: GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID, @@ -2025,7 +2262,7 @@ const synchronizeAnnotationSource = ( } }; -function getMousePositionInLayerCoordinates( +function getGlobalPositionInLayerCoordinates( unsnappedPosition: Float32Array, layer: SegmentationUserLayer, ): Float32Array | undefined { @@ -2057,7 +2294,7 @@ const getPoint = ( mouseState: MouseSelectionState, ) => { if (mouseState.updateUnconditionally()) { - return getMousePositionInLayerCoordinates( + return getGlobalPositionInLayerCoordinates( mouseState.unsnappedPosition, layer, ); @@ -2065,6 +2302,36 @@ const getPoint = ( return undefined; }; +const GRAPHENE_FOCUS_BOUNDING_BOX_SIZE_JSON_KEY = + "grapheneFocusBoundingBoxSize"; + +const focusBoundingBoxSizeControl = { + label: "Focus Bounding Box Size", + toolJson: GRAPHENE_FOCUS_BOUNDING_BOX_SIZE_JSON_KEY, + title: "", + ...rangeLayerControl((layer) => { + const { + graphConnection: { value: graphConnection }, + } = layer; + if (graphConnection && graphConnection instanceof GraphConnection) { + graphConnection.state.focusBoundingBoxSize; + return { + value: graphConnection.state.focusBoundingBoxSize, + options: { + min: 0, + max: 100, + step: 1, + }, + }; + } + return { + value: new WatchableValue(0), + }; + }), +}; + +registerLayerControl(SegmentationUserLayer, focusBoundingBoxSizeControl); + const MULTICUT_SEGMENTS_INPUT_EVENT_MAP = EventActionMap.fromObject({ "at:shift?+control+mousedown0": { action: "set-anchor" }, "at:shift?+keyg": { action: "swap-group" }, diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 74bf5cd841..657a198462 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -518,6 +518,8 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { ); } + bounds: WatchableValue = new WatchableValue(undefined); + segmentSelectionState = new SegmentSelectionState(); selectedAlpha = trackableAlphaValue(0.5); saturation = trackableAlphaValue(1.0); diff --git a/src/mesh/frontend.ts b/src/mesh/frontend.ts index 73c146a2ab..28bc1cb24f 100644 --- a/src/mesh/frontend.ts +++ b/src/mesh/frontend.ts @@ -54,7 +54,10 @@ import { SegmentationLayerSharedObject, } from "#src/segmentation_display_state/frontend.js"; import type { WatchableValueInterface } from "#src/trackable_value.js"; -import { makeCachedDerivedWatchableValue } from "#src/trackable_value.js"; +import { + makeCachedDerivedWatchableValue, + WatchableValue, +} from "#src/trackable_value.js"; import type { Borrowed, RefCounted } from "#src/util/disposable.js"; import type { vec4 } from "#src/util/geom.js"; import { @@ -349,19 +352,29 @@ export class MeshShaderManager { } makeGetter(layer: RefCounted & { gl: GL; displayState: MeshDisplayState }) { - const silhouetteRenderingEnabled = layer.registerDisposer( - makeCachedDerivedWatchableValue( - (x) => x > 0, - [layer.displayState.silhouetteRendering], - ), + const parameters = makeCachedDerivedWatchableValue( + (silhouetteRendering: number, bounds: Float32Array | undefined) => { + return { + silhouetteRenderingEnabled: silhouetteRendering > 0, + bounds, + }; + }, + [ + layer.displayState.silhouetteRendering, + layer.displayState.bounds || new WatchableValue(undefined), + ], ); return parameterizedEmitterDependentShaderGetter(layer, layer.gl, { memoizeKey: `mesh/MeshShaderManager/${this.fragmentRelativeVertices}/${this.vertexPositionFormat}`, - parameters: silhouetteRenderingEnabled, - defineShader: (builder, silhouetteRenderingEnabled) => { + parameters, + defineShader: (builder, { silhouetteRenderingEnabled, bounds }) => { + const cullingEnabled = bounds !== undefined; this.vertexPositionHandler.defineShader(builder); builder.addAttribute("highp vec2", "aVertexNormal"); builder.addVarying("highp vec4", "vColor"); + if (cullingEnabled) { + builder.addVarying("highp float", "vDiscard"); + } builder.addUniform("highp vec4", "uLightDirection"); builder.addUniform("highp vec4", "uColor"); builder.addUniform("highp mat3", "uNormalMatrix"); @@ -400,8 +413,41 @@ vColor = vec4(lightingFactor * uColor.rgb, uColor.a); vColor *= pow(1.0 - absCosAngle, uSilhouettePower); `; } + if (cullingEnabled) { + vertexMain += ` +if ((vertexPosition.x < ${bounds[0].toFixed(1)}f) || + (vertexPosition.y < ${bounds[1].toFixed(1)}f) || + (vertexPosition.z < ${bounds[2].toFixed(1)}f) || + (vertexPosition.x > ${bounds[3].toFixed(1)}f) || + (vertexPosition.y > ${bounds[4].toFixed(1)}f) || + (vertexPosition.z > ${bounds[5].toFixed(1)}f)) { + vDiscard = 1.0f; +} +`; + // alternate idea but culling doesn't seem to have a significant performance impact + // vertexMain += ` + // vDiscard += clamp(${bounds[0].toFixed(1)}f - vertexPosition.x, 0.0f, 1.0f); + // vDiscard += clamp(vertexPosition.x - ${bounds[3].toFixed(1)}f, 0.0f, 1.0f); + + // vDiscard += clamp(${bounds[1].toFixed(1)}f - vertexPosition.y, 0.0f, 1.0f); + // vDiscard += clamp(vertexPosition.y - ${bounds[4].toFixed(1)}f, 0.0f, 1.0f); + + // vDiscard += clamp(${bounds[2].toFixed(1)}f - vertexPosition.z, 0.0f, 1.0f); + // vDiscard += clamp(vertexPosition.z - ${bounds[5].toFixed(1)}f, 0.0f, 1.0f); + // `; + } builder.setVertexMain(vertexMain); - builder.setFragmentMain("emit(vColor, uPickID);"); + let fragmentMain = ` +emit(vColor, uPickID); +`; + if (cullingEnabled) { + fragmentMain += ` +if (vDiscard > 0.0f) { + discard; +} +`; + } + builder.setFragmentMain(fragmentMain); }, }); } @@ -409,6 +455,7 @@ vColor *= pow(1.0 - absCosAngle, uSilhouettePower); export interface MeshDisplayState extends SegmentationDisplayState3D { silhouetteRendering: WatchableValueInterface; + bounds?: WatchableValueInterface; } export class MeshLayer extends PerspectiveViewRenderLayer { diff --git a/src/util/json.ts b/src/util/json.ts index 5c4d21563f..b8e0826690 100644 --- a/src/util/json.ts +++ b/src/util/json.ts @@ -706,6 +706,16 @@ export function verifyIntegerArray(a: unknown) { return a; } +export function verifyFloatArray(a: unknown) { + if (!Array.isArray(a)) { + throw new Error(`Expected array, received: ${JSON.stringify(a)}.`); + } + for (const x of a) { + verifyFloat(x); + } + return a; +} + export function verifyBoolean(x: any) { if (typeof x !== "boolean") { throw new Error(`Expected boolean, received: ${JSON.stringify(x)}`);