diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index c21e3177bd7..de5bf0c852f 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -38,29 +38,29 @@ jobs: run: yarn typecheck - name: Check for cyclic dependencies in frontend run: yarn check-cyclic-dependencies - - name: Run frontend tests - run: yarn run vitest run --config vitest_spec.config.ts --coverage.enabled true - - name: Download Coverage Artifacts - # Can not use actions/download-artifact@v4 because it does not support downloading artifacts from other GA runs - run: | - gh_last_success_run_id=$(gh run list --workflow "CI Pipeline" --json conclusion,headBranch,databaseId --branch master --jq 'first(.[] | select(.conclusion | contains("success"))) | .databaseId') - [ -z "$gh_last_success_run_id" ] && echo "No successful run found" && exit 1 || true - gh run download $gh_last_success_run_id -n $ARTIFACT_NAME -D $OUTPUT_DIR - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ARTIFACT_NAME: vitest-coverage-master - OUTPUT_DIR: coverage-master - - name: 'Report Coverage' - uses: davelosert/vitest-coverage-report-action@v2 - with: - comment-on: none # alternative: pr - json-summary-compare-path: coverage-master/coverage-summary.json - - name: "Upload Coverage" - if: github.ref == 'refs/heads/master' - uses: actions/upload-artifact@v4 - with: - name: vitest-coverage-master - path: coverage + # - name: Run frontend tests + # run: yarn run vitest run --config vitest_spec.config.ts --coverage.enabled true + # - name: Download Coverage Artifacts + # # Can not use actions/download-artifact@v4 because it does not support downloading artifacts from other GA runs + # run: | + # gh_last_success_run_id=$(gh run list --workflow "CI Pipeline" --json conclusion,headBranch,databaseId --branch master --jq 'first(.[] | select(.conclusion | contains("success"))) | .databaseId') + # [ -z "$gh_last_success_run_id" ] && echo "No successful run found" && exit 1 || true + # gh run download $gh_last_success_run_id -n $ARTIFACT_NAME -D $OUTPUT_DIR + # env: + # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # ARTIFACT_NAME: vitest-coverage-master + # OUTPUT_DIR: coverage-master + # - name: 'Report Coverage' + # uses: davelosert/vitest-coverage-report-action@v2 + # with: + # comment-on: none # alternative: pr + # json-summary-compare-path: coverage-master/coverage-summary.json + # - name: "Upload Coverage" + # if: github.ref == 'refs/heads/master' + # uses: actions/upload-artifact@v4 + # with: + # name: vitest-coverage-master + # path: coverage backend-tests: runs-on: ubuntu-22.04 diff --git a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts index 7376e095036..5619a6334fc 100644 --- a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts +++ b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts @@ -42,6 +42,7 @@ const datasetNames = [ "Multi-Channel-Test", "connectome_file_test_dataset", "kiwi", // This dataset is rotated and translated. + "test-extreme-anisotropy", ]; type DatasetName = string; @@ -63,6 +64,7 @@ const viewOverrides: Record = { connectome_file_test_dataset: '{"position":[102,109,60],"mode":"orthogonal","zoomStep":0.734,"stateByLayer":{"segmentation":{"connectomeInfo":{"connectomeName":"connectome","agglomerateIdsToImport":[1]}}}}', kiwi: "1191,1112,21,0,8.746", + "test-extreme-anisotropy": "100,100,75,0,6.727", }; const datasetConfigOverrides: Record = { ROI2017_wkw: { diff --git a/frontend/javascripts/test/screenshots/test-extreme-anisotropy.png b/frontend/javascripts/test/screenshots/test-extreme-anisotropy.png new file mode 100644 index 00000000000..c969cceedd2 Binary files /dev/null and b/frontend/javascripts/test/screenshots/test-extreme-anisotropy.png differ diff --git a/frontend/javascripts/viewer/constants.ts b/frontend/javascripts/viewer/constants.ts index d44687ba0fe..d99fe9c0317 100644 --- a/frontend/javascripts/viewer/constants.ts +++ b/frontend/javascripts/viewer/constants.ts @@ -269,12 +269,14 @@ export enum TreeTypeEnum { export type TreeType = keyof typeof TreeTypeEnum; export const NODE_ID_REF_REGEX = /#([0-9]+)/g; export const POSITION_REF_REGEX = /#\(([0-9]+,[0-9]+,[0-9]+)\)/g; -const VIEWPORT_WIDTH = 376; +const VIEWPORT_WIDTH = 1; +const ARBITRARY_VIEWPORT_WIDTH = 376; // ARBITRARY_CAM_DISTANCE has to be calculated such that with cam // angle 45°, the plane of width Constants.VIEWPORT_WIDTH fits exactly in the // viewport. -export const ARBITRARY_CAM_DISTANCE = VIEWPORT_WIDTH / 2 / Math.tan(((Math.PI / 180) * 45) / 2); +export const ARBITRARY_CAM_DISTANCE = + ARBITRARY_VIEWPORT_WIDTH / 2 / Math.tan(((Math.PI / 180) * 45) / 2); export const ensureSmallerEdge = false; export const Unicode = { @@ -307,6 +309,7 @@ const Constants = { BUCKET_WIDTH: 32, BUCKET_SIZE: 32 ** 3, VIEWPORT_WIDTH, + ARBITRARY_VIEWPORT_WIDTH, DEFAULT_NAVBAR_HEIGHT: 48, BANNER_HEIGHT: 38, // For reference, the area of a large brush size (let's say, 300px) corresponds to diff --git a/frontend/javascripts/viewer/geometries/arbitrary_plane.ts b/frontend/javascripts/viewer/geometries/arbitrary_plane.ts index c8a5319379b..8119989d827 100644 --- a/frontend/javascripts/viewer/geometries/arbitrary_plane.ts +++ b/frontend/javascripts/viewer/geometries/arbitrary_plane.ts @@ -122,7 +122,12 @@ class ArbitraryPlane { const textureMaterial = this.materialFactory.setup().getMaterial(); this.plane = adaptPlane( new Mesh( - new PlaneGeometry(constants.VIEWPORT_WIDTH, constants.VIEWPORT_WIDTH, 1, 1), + new PlaneGeometry( + constants.ARBITRARY_VIEWPORT_WIDTH, + constants.ARBITRARY_VIEWPORT_WIDTH, + 1, + 1, + ), textureMaterial, ), ); @@ -176,7 +181,12 @@ class ArbitraryPlane { debuggerMaterial.transparent = true; shaderEditor.addMaterial(99, debuggerMaterial); const debuggerPlane = new Mesh( - new PlaneGeometry(constants.VIEWPORT_WIDTH, constants.VIEWPORT_WIDTH, 50, 50), + new PlaneGeometry( + constants.ARBITRARY_VIEWPORT_WIDTH, + constants.ARBITRARY_VIEWPORT_WIDTH, + 50, + 50, + ), debuggerMaterial, ); return debuggerPlane; diff --git a/frontend/javascripts/viewer/geometries/plane.ts b/frontend/javascripts/viewer/geometries/plane.ts index efe349f1cf8..fb095bb61a6 100644 --- a/frontend/javascripts/viewer/geometries/plane.ts +++ b/frontend/javascripts/viewer/geometries/plane.ts @@ -24,6 +24,7 @@ import PlaneMaterialFactory, { type PlaneShaderMaterial, } from "viewer/geometries/materials/plane_material_factory"; import { listenToStoreProperty } from "viewer/model/helpers/listener_helpers"; +import { getBaseVoxelInUnit } from "viewer/model/scaleinfo"; // A subdivision of 100 means that there will be 100 segments per axis // and thus 101 vertices per axis (i.e., the vertex shader is executed 101**2). @@ -68,10 +69,6 @@ class Plane { this.planeID = planeID; this.displayCrosshair = true; this.lastScaleFactors = [-1, -1]; - // VIEWPORT_WIDTH means that the plane should be that many voxels wide in the - // dimension with the highest mag. In all other dimensions, the plane - // is smaller in voxels, so that it is squared in nm. - // --> scaleInfo.baseVoxel this.baseRotation = new Euler(0, 0, 0); this.bindToEvents(); this.createMeshes(); @@ -163,8 +160,12 @@ class Plane { } this.lastScaleFactors[0] = xFactor; this.lastScaleFactors[1] = yFactor; - // Account for the dataset scale to match one world space coordinate to one dataset scale unit. - const scaleVector: Vector3 = V3.multiply([xFactor, yFactor, 1], this.datasetScaleFactor); + // Scale to base voxel space which is the same coordinate space the cameras use + const baseVoxelUnit = getBaseVoxelInUnit(this.datasetScaleFactor); + const scaleVector: Vector3 = V3.multiply( + [xFactor, yFactor, 1], + [baseVoxelUnit, baseVoxelUnit, baseVoxelUnit], + ); this.getMeshes().map((mesh) => mesh.scale.set(...scaleVector)); } diff --git a/frontend/javascripts/viewer/model/bucket_data_handling/bucket_picker_strategies/flight_bucket_picker.ts b/frontend/javascripts/viewer/model/bucket_data_handling/bucket_picker_strategies/flight_bucket_picker.ts index 53a0cc8d2f6..fab75c48cd3 100644 --- a/frontend/javascripts/viewer/model/bucket_data_handling/bucket_picker_strategies/flight_bucket_picker.ts +++ b/frontend/javascripts/viewer/model/bucket_data_handling/bucket_picker_strategies/flight_bucket_picker.ts @@ -49,7 +49,7 @@ export default function determineBucketsForFlight( abortLimit?: number, ): void { const queryMatrix = M4x4.scale1(1, matrix); - const width = constants.VIEWPORT_WIDTH; + const width = constants.ARBITRARY_VIEWPORT_WIDTH; const halfWidth = width / 2; const cameraVertex: Vector3 = [0, 0, -sphericalCapRadius]; const fallbackZoomStep = logZoomStep + 1; diff --git a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts index 55e090a7563..0bb7ab7ddeb 100644 --- a/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts +++ b/frontend/javascripts/viewer/model/reducers/flycam_reducer.ts @@ -100,11 +100,17 @@ function rotateReducer( }); } +// export function getMatrixScale(voxelSize: Vector3): Vector3 { +// const scale = [1 / voxelSize[0], 1 / voxelSize[1], 1 / voxelSize[2]]; +// const maxScale = Math.max(scale[0], scale[1], scale[2]); +// const multi = 1 / maxScale; +// return [multi * scale[0], multi * scale[1], multi * scale[2]]; +// } + export function getMatrixScale(voxelSize: Vector3): Vector3 { - const scale = [1 / voxelSize[0], 1 / voxelSize[1], 1 / voxelSize[2]]; - const maxScale = Math.max(scale[0], scale[1], scale[2]); - const multi = 1 / maxScale; - return [multi * scale[0], multi * scale[1], multi * scale[2]]; + const baseVoxelSize = 1; + // const baseVoxelSize = Math.min(...voxelSize); + return [baseVoxelSize / voxelSize[0], baseVoxelSize / voxelSize[1], baseVoxelSize / voxelSize[2]]; } function resetMatrix(matrix: Matrix4x4, voxelSize: Vector3) { diff --git a/frontend/javascripts/viewer/model/scaleinfo.ts b/frontend/javascripts/viewer/model/scaleinfo.ts index e99d6fc5c9f..d51388aafa3 100644 --- a/frontend/javascripts/viewer/model/scaleinfo.ts +++ b/frontend/javascripts/viewer/model/scaleinfo.ts @@ -2,9 +2,10 @@ import { UnitsMap } from "libs/format_utils"; import type { VoxelSize } from "types/api_types"; import { LongUnitToShortUnitMap, type UnitShort, type Vector3 } from "viewer/constants"; -export function getBaseVoxelInUnit(voxelSizeFactor: Vector3): number { - // base voxel should be a cube with highest mag - return Math.min(...voxelSizeFactor); +export function getBaseVoxelInUnit(_voxelSizeFactor: Vector3): number { + // base voxel should be a cube with highest resolution + return 1; + // return Math.min(...voxelSizeFactor); } export function voxelToVolumeInUnit( diff --git a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts index 6c8f019c046..99dcebbeec0 100644 --- a/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts +++ b/frontend/javascripts/viewer/shaders/main_data_shaders.glsl.ts @@ -174,6 +174,7 @@ flat in vec2 index; flat in uint outputMagIdx[<%= globalLayerCount %>]; flat in uint outputSeed[<%= globalLayerCount %>]; flat in float outputAddress[<%= globalLayerCount %>]; +flat in float useBucketBorderVertexOptimization; in vec4 worldCoord; in vec4 modelCoord; in mat4 savedModelMatrix; @@ -438,6 +439,8 @@ flat out vec2 index; flat out uint outputMagIdx[<%= globalLayerCount %>]; flat out uint outputSeed[<%= globalLayerCount %>]; flat out float outputAddress[<%= globalLayerCount %>]; +// bool varyings are not supported +flat out float useBucketBorderVertexOptimization; uniform bool is3DViewBeingRendered; uniform vec3 representativeMagForVertexAlignment; @@ -476,6 +479,8 @@ void main() { <% } }) %> + useBucketBorderVertexOptimization = 1.0; + vUv = uv; modelCoord = vec4(position, 1.0); savedModelMatrix = modelMatrix; @@ -486,6 +491,7 @@ void main() { // The same goes when all layers of the dataset are transformed. // This shouldn't really impact the performance as isFlycamRotated is a uniform. if(isFlycamRotated || !<%= isOrthogonal %> || doAllLayersHaveTransforms) { + useBucketBorderVertexOptimization = 0.0; return; } // Remember the original z position, since it can subtly diverge in the @@ -522,6 +528,14 @@ void main() { vec2 d = transDim(vec3(bucketWidth) * representativeMagForVertexAlignment).xy; vec3 voxelSizeFactorUVW = transDim(voxelSizeFactor); + vec2 viewportWidthInVoxelsUV = abs(worldCoordBottomRight.xy - worldCoordTopLeft.xy) / voxelSizeFactorUVW.xy; + // If the plane subdivision vertices cannot possibly cover all bucket borders, the optimization must not be used. + // Otherwise, rendering artifacts will occur (partially rendered planes). + if ((d * PLANE_SUBDIVISION).x < viewportWidthInVoxelsUV.x || (d * PLANE_SUBDIVISION).y < viewportWidthInVoxelsUV.y) { + useBucketBorderVertexOptimization = 0.0; + return; + } + vec3 voxelSizeFactorInvertedUVW = transDim(voxelSizeFactorInverted); vec3 transWorldCoord = transDim(worldCoord.xyz); diff --git a/frontend/javascripts/viewer/shaders/segmentation.glsl.ts b/frontend/javascripts/viewer/shaders/segmentation.glsl.ts index b95c242172c..2b6a342be2d 100644 --- a/frontend/javascripts/viewer/shaders/segmentation.glsl.ts +++ b/frontend/javascripts/viewer/shaders/segmentation.glsl.ts @@ -225,7 +225,7 @@ export const convertCellIdToRGB: ShaderModule = { float zoomAdaption = ceil(zoomValue); vec3 worldCoordUVW = coordScaling * getUnrotatedWorldCoordUVW() / zoomAdaption; - float baseVoxelSize = min(min(voxelSizeFactor.x, voxelSizeFactor.y), voxelSizeFactor.z); + float baseVoxelSize = 1.; // min(min(voxelSizeFactor.x, voxelSizeFactor.y), voxelSizeFactor.z); vec3 anisotropyFactorUVW = transDim(voxelSizeFactor) / baseVoxelSize; worldCoordUVW.x = worldCoordUVW.x * anisotropyFactorUVW.x; worldCoordUVW.y = worldCoordUVW.y * anisotropyFactorUVW.y; @@ -333,7 +333,7 @@ export const getBrushOverlay: ShaderModule = { // Compute the anisotropy of the dataset so that the brush looks the same in // each viewport - float baseVoxelSize = min(min(voxelSizeFactor.x, voxelSizeFactor.y), voxelSizeFactor.z); + float baseVoxelSize = 1.; // min(min(voxelSizeFactor.x, voxelSizeFactor.y), voxelSizeFactor.z); vec3 anisotropyFactorUVW = transDim(voxelSizeFactor) / baseVoxelSize; float dist = length((floor(worldCoordUVW.xy) - transDim(flooredMousePos).xy) * anisotropyFactorUVW.xy); @@ -361,7 +361,7 @@ export const getProofreadingCrossHairOverlay: ShaderModule = { // Compute the anisotropy of the dataset so that the cross hair looks the same in // each viewport - float baseVoxelSize = min(min(voxelSizeFactor.x, voxelSizeFactor.y), voxelSizeFactor.z); + float baseVoxelSize = 1.; // min(min(voxelSizeFactor.x, voxelSizeFactor.y), voxelSizeFactor.z); vec3 anisotropyFactorUVW = transDim(voxelSizeFactor) / baseVoxelSize; // Compute the distance in screen coordinate space to show a zoom-independent cross hair diff --git a/frontend/javascripts/viewer/shaders/texture_access.glsl.ts b/frontend/javascripts/viewer/shaders/texture_access.glsl.ts index bf2bb98024d..1ea7b633f92 100644 --- a/frontend/javascripts/viewer/shaders/texture_access.glsl.ts +++ b/frontend/javascripts/viewer/shaders/texture_access.glsl.ts @@ -204,7 +204,7 @@ export const getColorForCoords: ShaderModule = { // To avoid rare rendering artifacts, don't use the precomputed // bucket address when being at the border of buckets. - bool beSafe = isFlycamRotated || !<%= isOrthogonal %>; + bool beSafe = useBucketBorderVertexOptimization < 0.5; renderedMagIdx = outputMagIdx[globalLayerIndex]; vec3 coords = floor(getAbsoluteCoords(worldPositionUVW, renderedMagIdx, globalLayerIndex)); vec3 absoluteBucketPosition = div(coords, bucketWidth); diff --git a/unreleased_changes/9114.md b/unreleased_changes/9114.md new file mode 100644 index 00000000000..ffa0725c75f --- /dev/null +++ b/unreleased_changes/9114.md @@ -0,0 +1,2 @@ +### Fixed +- Fixed rendering issues for extremely anisotropic datasets.