diff --git a/modules/AdministrationGUI/pom.xml b/modules/AdministrationGUI/pom.xml index cf3ba10b0e..53526597ad 100644 --- a/modules/AdministrationGUI/pom.xml +++ b/modules/AdministrationGUI/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. AdministrationGUI org.janelia.workstation adminstration - 9.24.1 + 9.25.1 nbm diff --git a/modules/ColorDepthSearch/pom.xml b/modules/ColorDepthSearch/pom.xml index 3b08a0ab54..78e77c1972 100644 --- a/modules/ColorDepthSearch/pom.xml +++ b/modules/ColorDepthSearch/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. ColorDepthSearch org.janelia.workstation colordepth - 9.24.1 + 9.25.1 nbm diff --git a/modules/CommonGUI/pom.xml b/modules/CommonGUI/pom.xml index e47c7e887c..1c3bf9dc23 100644 --- a/modules/CommonGUI/pom.xml +++ b/modules/CommonGUI/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. CommonGUI org.janelia.workstation common-gui - 9.24.1 + 9.25.1 nbm diff --git a/modules/CommonLibraries/pom.xml b/modules/CommonLibraries/pom.xml index 683306634b..5b43beb287 100644 --- a/modules/CommonLibraries/pom.xml +++ b/modules/CommonLibraries/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. CommonLibraries org.janelia.workstation libraries - 9.24.1 + 9.25.1 nbm diff --git a/modules/Core/pom.xml b/modules/Core/pom.xml index e60003effc..ea05de06f8 100644 --- a/modules/Core/pom.xml +++ b/modules/Core/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Core org.janelia.workstation core - 9.24.1 + 9.25.1 nbm diff --git a/modules/DarculaLAF/pom.xml b/modules/DarculaLAF/pom.xml index 24294cf7dc..7d765a7a7b 100644 --- a/modules/DarculaLAF/pom.xml +++ b/modules/DarculaLAF/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. DarculaLAF org.janelia.workstation darcula - 9.24.1 + 9.25.1 nbm diff --git a/modules/DataBrowser/pom.xml b/modules/DataBrowser/pom.xml index 1045abef64..c613bee52a 100644 --- a/modules/DataBrowser/pom.xml +++ b/modules/DataBrowser/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. DataBrowser org.janelia.workstation browser - 9.24.1 + 9.25.1 nbm diff --git a/modules/GLViewerTools/pom.xml b/modules/GLViewerTools/pom.xml index 92ccc6f23e..601b5b05e5 100644 --- a/modules/GLViewerTools/pom.xml +++ b/modules/GLViewerTools/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. GLViewerTools org.janelia.workstation gltools - 9.24.1 + 9.25.1 nbm diff --git a/modules/Geometry3d/pom.xml b/modules/Geometry3d/pom.xml index caca62cdfc..5b12d3f126 100644 --- a/modules/Geometry3d/pom.xml +++ b/modules/Geometry3d/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Geometry3d org.janelia.workstation geometry3d - 9.24.1 + 9.25.1 nbm diff --git a/modules/HortaTracer/pom.xml b/modules/HortaTracer/pom.xml index 08ff9db7fb..bf094f41b8 100644 --- a/modules/HortaTracer/pom.xml +++ b/modules/HortaTracer/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. HortaTracer org.janelia.workstation horta - 9.24.1 + 9.25.1 nbm @@ -78,7 +78,7 @@ org.aind jomezarr - 1.4.3 + 1.4.4 dev.zarr diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java b/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java index db23750d8c..22980a039d 100644 --- a/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java @@ -1123,6 +1123,17 @@ public boolean loadDroppedOmeZarr(String sourceName) { if (!haveSetBoundingBox[0] && !source.getResolutions().isEmpty()) { haveSetBoundingBox[0] = true; + // Metadata is loaded, so the channel count is known: size the + // shared color model to match, plus one extra slot for the + // synthetic tracing/unmix channel (mirrors the legacy 2-channel + // TetVolumeActor convention), and rebuild the slider UI. + if (getImageColorModel().getChannelCount() != source.getChannelCount() + 1) { + getImageColorModel().reset(65535, source.getChannelCount() + 1); + getImageColorModel().getChannel(source.getChannelCount()) + .setColor(new Color(0f, 0.5f, 1.0f)); // unmixed channel in Economo blue + initColorModel(); + } + TmModelManager.getInstance().setSampleBoundingBox(source.getBoundingBox3d()); TmModelManager.getInstance().setVoxelCenter(source.getVoxelCenter()); @@ -2370,6 +2381,13 @@ void setKtxSource(KtxOctreeBlockTileSource ktxSource) { if (ktxSource != null) { omeZarrSource = null; setVolumeSource(null); + // TetVolumeActor only supports exactly 3 channels; reset the shared color + // model in case a previous OME-Zarr multichannel session resized it larger, + // otherwise TetVolumeActor.display() throws UnsupportedOperationException. + if (getImageColorModel().getChannelCount() != 3) { + getImageColorModel().reset(65535, 3); + initColorModel(); + } } } diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/ViewLoader.java b/modules/HortaTracer/src/main/java/org/janelia/horta/ViewLoader.java index d959873080..6ebdfedeb6 100644 --- a/modules/HortaTracer/src/main/java/org/janelia/horta/ViewLoader.java +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/ViewLoader.java @@ -1,5 +1,6 @@ package org.janelia.horta; +import java.awt.Color; import java.io.IOException; import java.net.URL; @@ -138,6 +139,18 @@ private OmeZarrBlockTileSource createOmeZarrSource(URL renderedOmeZarrUrl, TmSam if (!haveFirstDataset[0] && !source.getResolutions().isEmpty()) { haveFirstDataset[0] = true; try { + // OME-Zarr metadata is now loaded, so the channel count is known. + // Size the shared color model to match, plus one extra slot for the + // synthetic tracing/unmix channel (mirrors the legacy 2-channel + // TetVolumeActor convention of a dedicated slider for that channel), + // then re-apply the persisted/workspace color model at the correct + // channel count (this also rebuilds the slider UI via ColorModelUpdateEvent). + if (nttc.getImageColorModel().getChannelCount() != source.getChannelCount() + 1) { + nttc.getImageColorModel().reset(65535, source.getChannelCount() + 1); + nttc.getImageColorModel().getChannel(source.getChannelCount()) + .setColor(new Color(0f, 0.5f, 1.0f)); // unmixed channel in Economo blue + nttc.initColorModel(); + } if (nttc.doesUpdateVolumeCache()) { loader.loadTransientOmeZarrTileAtCurrentFocus(nttc.getOmeZarrSource()); } else { diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/actors/OmeZarrVolumeMeshActor.java b/modules/HortaTracer/src/main/java/org/janelia/horta/actors/OmeZarrVolumeMeshActor.java index 2ee1fd1947..96c16c202b 100644 --- a/modules/HortaTracer/src/main/java/org/janelia/horta/actors/OmeZarrVolumeMeshActor.java +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/actors/OmeZarrVolumeMeshActor.java @@ -13,6 +13,7 @@ import org.janelia.horta.blocks.OmeZarrBlockResolution; import org.janelia.horta.blocks.OmeZarrBlockTileKey; import org.janelia.horta.blocks.OmeZarrBlockTileSource; +import org.janelia.horta.volume.OmeZarrVolumeMipMaterial; import org.janelia.horta.volume.VolumeMipMaterial; import org.janelia.workstation.controller.model.color.ImageColorModel; @@ -36,8 +37,8 @@ public class OmeZarrVolumeMeshActor extends MeshActor implements SortableBlockAc private double distance; - public OmeZarrVolumeMeshActor(OmeZarrBlockTileSource source, OmeZarrBlockTileKey tile, VolumeMipMaterial.VolumeState volumeState, int colorChannel) throws IOException { - super(new OmeZarrMesh(tile), new OmeZarrVolumeMeshActor.MeshMaterial(source, tile, source.getColorModel(), volumeState, colorChannel), null); + public OmeZarrVolumeMeshActor(OmeZarrBlockTileSource source, OmeZarrBlockTileKey tile, VolumeMipMaterial.VolumeState volumeState) throws IOException { + super(new OmeZarrMesh(tile), new OmeZarrVolumeMeshActor.MeshMaterial(source, tile, source.getColorModel(), volumeState), null); this.meshMaterial = (OmeZarrVolumeMeshActor.MeshMaterial) getMaterial(); @@ -99,18 +100,25 @@ public BlockTileResolution getResolution() { return resolution; } - private static class MeshMaterial extends VolumeMipMaterial { - private MeshMaterial(OmeZarrBlockTileSource source, OmeZarrBlockTileKey tile, ImageColorModel imageColorModel, VolumeState volumeState, int colorChannel) throws IOException { - super(safeLoadData(source, tile, colorChannel), imageColorModel); + private static class MeshMaterial extends OmeZarrVolumeMipMaterial { + private MeshMaterial(OmeZarrBlockTileSource source, OmeZarrBlockTileKey tile, ImageColorModel imageColorModel, VolumeMipMaterial.VolumeState volumeState) throws IOException { + super(safeLoadData(source, tile), imageColorModel); setVolumeState(volumeState); } - private static Texture3d safeLoadData(OmeZarrBlockTileSource source, OmeZarrBlockTileKey tile, int colorChannel) throws IOException { - Texture3d brick = source.loadBrick(tile, colorChannel); - if (brick == null) { - throw new IOException("Load was interrupted"); + // Load every channel of the block into its own single-component texture, preserving + // OME-Zarr's per-channel separation; the shader combines them at render time. + private static Texture3d[] safeLoadData(OmeZarrBlockTileSource source, OmeZarrBlockTileKey tile) throws IOException { + int channelCount = Math.max(1, Math.min(source.getChannelCount(), OmeZarrVolumeMipMaterial.MAX_CHANNELS)); + Texture3d[] textures = new Texture3d[channelCount]; + for (int c = 0; c < channelCount; ++c) { + Texture3d brick = source.loadBrick(tile, c); + if (brick == null) { + throw new IOException("Load was interrupted"); + } + textures[c] = brick; } - return brick; + return textures; } } } diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockLoadRunner.java b/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockLoadRunner.java index 6f2ce11a07..069895be63 100644 --- a/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockLoadRunner.java +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockLoadRunner.java @@ -44,7 +44,7 @@ private void loadFromBlockSource() { try { state = OmeZarrBlockLoadRunner.State.LOADING; OmeZarrVolumeActor parentActor = OmeZarrVolumeActor.getInstance(); - blockActor = new OmeZarrVolumeMeshActor(omeZarrBlockTileSource, omeZarrOctreeBlockTileKey, parentActor.getVolumeState(), 0); + blockActor = new OmeZarrVolumeMeshActor(omeZarrBlockTileSource, omeZarrOctreeBlockTileKey, parentActor.getVolumeState()); state = OmeZarrBlockLoadRunner.State.LOADED; setChanged(); long endTime = System.currentTimeMillis(); diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileKey.java b/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileKey.java index d7839e4fae..793e571e9e 100644 --- a/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileKey.java +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileKey.java @@ -167,10 +167,19 @@ public double getResolutionMicrometers() { private static final ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, true, Transparency.OPAQUE, DataBuffer.TYPE_USHORT); public Texture3d loadBrick(AutoContrastParameters parameters) { + return loadBrick(parameters, 0); + } + + public Texture3d loadBrick(AutoContrastParameters parameters, int channel) { Texture3d texture = new Texture3d(); try { - WritableRaster[] slices = TCZYXRasterZStack.fromDataset(dataset, readShape, readOffset, 1, parameters != null, parameters, null); + // readShape/readOffset are TCZYX; index 1 is the channel. Read one channel at a time + // into a single-component texture, leaving the key itself channel-agnostic. + int[] channelOffset = readOffset.clone(); + channelOffset[1] = channel; + + WritableRaster[] slices = TCZYXRasterZStack.fromDataset(dataset, readShape, channelOffset, 1, parameters != null, parameters, null); texture.loadRasterSlices(slices, colorModel); diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileSource.java b/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileSource.java index b8b6803f1c..d9e38e68e2 100644 --- a/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileSource.java +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/blocks/OmeZarrBlockTileSource.java @@ -41,6 +41,8 @@ public class OmeZarrBlockTileSource implements BlockTileSource resolutions = new ArrayList<>(); private OmeZarrBlockResolution maximumResolution = null; + private int channelCount = 1; + private final ImageColorModel imageColorModel; private final boolean useAutoContrast; @@ -128,6 +130,9 @@ private OmeZarrBlockTileSource init(OmeZarrReaderProgressObserver progressObserv boundingBox3d = new BoundingBox3d(new Vec3(origin.getX(), origin.getY(), origin.getZ()), new Vec3(outerCorner.getX(), outerCorner.getY(), outerCorner.getZ())); voxelCenter = boundingBox3d.getCenter(); + // TCZYX: channel count is the C dimension; constant across resolution levels. + channelCount = Math.max(1, shapeIndex.getC()); + haveExtents = true; } @@ -239,13 +244,16 @@ ConstVector3 getBlockSize(OmeZarrBlockResolution resolution) { resolution.getChunkSize()[2] * resolution.getResolutionMicrometers()); } + public int getChannelCount() { + return channelCount; + } + public Texture3d loadBrick(OmeZarrBlockTileKey tile, int colorChannel) { - // setColorChannelIndex(colorChannel); - return loadBrick(tile); + return tile.loadBrick(useAutoContrast ? autoContrastParameters : null, colorChannel); } public Texture3d loadBrick(OmeZarrBlockTileKey tile) { - return tile.loadBrick(useAutoContrast ? autoContrastParameters : null); + return loadBrick(tile, 0); } /* private void createTileKeysForDataset(OmeZarrDataset dataset, OmeZarrReaderProgressObserver progressReceiver) { diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/volume/OmeZarrVolumeMipMaterial.java b/modules/HortaTracer/src/main/java/org/janelia/horta/volume/OmeZarrVolumeMipMaterial.java new file mode 100644 index 0000000000..cf7257f9c0 --- /dev/null +++ b/modules/HortaTracer/src/main/java/org/janelia/horta/volume/OmeZarrVolumeMipMaterial.java @@ -0,0 +1,440 @@ +package org.janelia.horta.volume; + +import java.io.IOException; +import javax.media.opengl.GL2ES2; +import javax.media.opengl.GL3; +import org.apache.commons.io.IOUtils; +import org.janelia.geometry3d.*; +import org.janelia.workstation.controller.model.color.ChannelColorModel; +import org.janelia.workstation.controller.model.color.ImageColorModel; +import org.janelia.geometry3d.camera.BasicViewSlab; +import org.janelia.geometry3d.camera.ConstViewSlab; +import org.janelia.gltools.BasicShaderProgram; +import org.janelia.gltools.MeshActor; +import org.janelia.gltools.ShaderProgram; +import org.janelia.gltools.ShaderStep; +import org.janelia.gltools.material.BasicMaterial; +import org.janelia.gltools.material.DepthSlabClipper; +import org.janelia.gltools.texture.Texture2d; +import org.janelia.gltools.texture.Texture3d; +import org.openide.util.Exceptions; + +/** + * Multi-channel volume MIP material for OME-Zarr. + * + * Forked from {@link VolumeMipMaterial} so the KTX {@code BrickActor} path (which also extends + * VolumeMipMaterial) is left untouched. Differs in that each channel is its own single-component + * {@link Texture3d}, bound to a separate texture unit, and the channels are combined jointly in the + * fragment shader (OmeZarrVolumeMipFrag.glsl) rather than packed into one texture. + */ +public class OmeZarrVolumeMipMaterial extends BasicMaterial + implements DepthSlabClipper { + + // Must match MAX_CHANNELS in OmeZarrVolumeMipFrag.glsl + public static final int MAX_CHANNELS = 8; + + // The texture unit reserved for the opaque depth texture (after the channel units). + private static final int DEPTH_TEXTURE_UNIT = MAX_CHANNELS; + + private final Texture3d[] channelTextures; + private final int channelCount; + + private Texture2d opaqueDepthTexture = null; + + private int channelTextureIndex = -1; + private int channelCountIndex = -1; + private int opaqueDepthTextureIndex = -1; + private int cameraPositionInTextureCoordinatesIndex = -1; + private int levelOfDetailIndex = -1; + private int nearSlabPlaneIndex = -1; + private int farSlabPlaneIndex = -1; + private int volumeMicrometersIndex = -1; + private int tcToCameraIndex = -1; + private int opaqueZNearFarIndex = -1; + private int filteringOrderIndex = -1; + + private int channelColorIndex = -1; + private int channelMinIndex = -1; + private int channelMaxIndex = -1; + private int channelGammaIndex = -1; + private int channelVisibleIndex = -1; + private int unmixMinIndex = -1; + private int unmixScaleIndex = -1; + + private float[] opaqueZNearFar = {1e-2f, 1e4f}; // absolute clip in camera space + + private final ImageColorModel colorMap; + + protected final ShaderProgram mipShader = new OmeZarrVolumeMipShader(0); + protected final ShaderProgram occShader = new OmeZarrVolumeMipShader(1); + protected final ShaderProgram isoShader = new OmeZarrVolumeMipShader(2); + protected final ShaderProgram[] shaderPrograms = new ShaderProgram[]{ + mipShader, + occShader, + isoShader + }; + + private boolean uniformIndicesAreDirty = true; + private VolumeMipMaterial.VolumeState volumeState = new VolumeMipMaterial.VolumeState(); + + // Relative clip in camera space + private float relativeZNear = 0.92f; + private float relativeZFar = 1.08f; + + public OmeZarrVolumeMipMaterial(Texture3d[] channelTextures, ImageColorModel colorMap) { + this.colorMap = colorMap; + this.channelTextures = channelTextures; + this.channelCount = Math.min(channelTextures.length, MAX_CHANNELS); + + for (Texture3d t : channelTextures) { + if (t == null) + continue; + t.setGenerateMipmaps(true); + t.setMinFilter(GL3.GL_LINEAR_MIPMAP_NEAREST); + t.setMagFilter(GL3.GL_LINEAR); + } + + shaderProgram = mipShader; + + setShadingStyle(Shading.FLAT); + } + + @Override + public void setRelativeSlabThickness(float zNear, float zFar) { + this.relativeZNear = zNear; + this.relativeZFar = zFar; + } + + /** @return the first channel texture (used by caches that key off a representative texture). */ + public Texture3d getTexture() { + return channelTextures.length > 0 ? channelTextures[0] : null; + } + + public Texture3d[] getChannelTextures() { + return channelTextures; + } + + public VolumeMipMaterial.VolumeState getVolumeState() { + return volumeState; + } + + public void setVolumeState(VolumeMipMaterial.VolumeState volumeState) { + if (this.volumeState == volumeState) + return; + this.volumeState = volumeState; + this.uniformIndicesAreDirty = true; + } + + @Override + protected void activateCull(GL3 gl) { + gl.glEnable(GL3.GL_CULL_FACE); + gl.glCullFace(GL3.GL_FRONT); + } + + public int getFilteringOrder() { + return volumeState.filteringOrder; + } + + public void setFilteringOrder(int filteringOrder) { + volumeState.filteringOrder = filteringOrder; + } + + public int getProjectionMode() { + return volumeState.projectionMode; + } + + public void setProjectionMode(int projectionMode) { + if (volumeState.projectionMode == projectionMode) + return; + volumeState.projectionMode = projectionMode; + updateShaderProgram(); + uniformIndicesAreDirty = true; + } + + private void updateShaderProgram() { + if (shaderProgram != shaderPrograms[volumeState.projectionMode]) { + shaderProgram = shaderPrograms[volumeState.projectionMode]; + uniformIndicesAreDirty = true; + } + } + + @Override + protected void displayMesh(GL3 gl, MeshActor mesh, AbstractCamera camera, Matrix4 modelViewMatrix) { + if (uniformIndicesAreDirty) + updateUniformIndices(gl); + + if (mesh.getGeometry() instanceof VolumeTextureMesh) { + VolumeTextureMesh mg = (VolumeTextureMesh) mesh.getGeometry(); + Matrix4 world_X_tc = mg.getTransformWorldToTexCoord(); + Matrix4 camera_X_world = modelViewMatrix.inverse(); + Matrix4 camera_X_tc = new Matrix4(camera_X_world).multiply(world_X_tc); + Vector4 tc_camera = camera_X_tc.multiply(new Vector4(0, 0, 0, 1)); + gl.glUniform3fv(cameraPositionInTextureCoordinatesIndex, 1, tc_camera.toArray(), 0); + + // level-of-detail + float meshResolution = mg.getMinResolution(); + float screenResolution = + camera.getVantage().getSceneUnitsPerViewportHeight() + / camera.getViewport().getHeightPixels(); + float levelOfDetail = -(float) (Math.log(meshResolution / screenResolution) / Math.log(2.0)); + levelOfDetail += 0.5f; + levelOfDetail = Math.max(levelOfDetail, 0); + levelOfDetail = (float) Math.floor(levelOfDetail); + gl.glUniform1i(levelOfDetailIndex, (int) levelOfDetail); + + float cameraFocusDistance = 0.0f; + if (camera instanceof PerspectiveCamera) { + cameraFocusDistance = ((PerspectiveCamera) camera).getCameraFocusDistance(); + } + float absZNear = cameraFocusDistance * relativeZNear; + float absZFar = cameraFocusDistance * relativeZFar; + Vector4 nearSlabPlane_camera = new Vector4(0, 0, 1, absZNear); + Vector4 farSlabPlane_camera = new Vector4(0, 0, 1, absZFar); + Matrix4 planeXform = camera_X_tc.inverse().transpose(); + gl.glUniform4fv(nearSlabPlaneIndex, 1, planeXform.multiply(nearSlabPlane_camera).toArray(), 0); + gl.glUniform4fv(farSlabPlaneIndex, 1, planeXform.multiply(farSlabPlane_camera).toArray(), 0); + + // Per-channel transfer functions, driven by the ImageColorModel. + setChannelUniforms(gl); + + Vector4 micrometerVolumes = world_X_tc.multiply(new Vector4(1, 1, 1, 0)); + float[] volMic = new float[]{ + 1.0f / Math.abs(micrometerVolumes.get(0)), + 1.0f / Math.abs(micrometerVolumes.get(1)), + 1.0f / Math.abs(micrometerVolumes.get(2))}; + gl.glUniform3fv(volumeMicrometersIndex, 1, volMic, 0); + + gl.glUniformMatrix4fv(tcToCameraIndex, 1, false, camera_X_tc.inverse().asArray(), 0); + + opaqueZNearFar[0] = absZNear; + opaqueZNearFar[1] = absZFar; + gl.glUniform2fv(opaqueZNearFarIndex, 1, opaqueZNearFar, 0); + } + + super.displayMesh(gl, mesh, camera, modelViewMatrix); + } + + private void setChannelUniforms(GL3 gl) { + int n = channelCount; + gl.glUniform1i(channelCountIndex, n); + + if (colorMap == null) + return; + + // Only read as many ImageColorModel channels as actually exist, to avoid the + // index-out-of-bounds that produced PR #40's GLException when the model was not + // yet sized to the data. + int modelChannels = colorMap.getChannelCount(); + + float[] colors = new float[MAX_CHANNELS * 3]; + float[] mins = new float[MAX_CHANNELS]; + float[] maxes = new float[MAX_CHANNELS]; + float[] gammas = new float[MAX_CHANNELS]; + float[] visible = new float[MAX_CHANNELS]; + + for (int c = 0; c < n; ++c) { + if (c >= modelChannels) { + // No color model for this channel yet: render hidden until the model catches up. + maxes[c] = 1f; + gammas[c] = 1f; + continue; + } + ChannelColorModel chan = colorMap.getChannel(c); + colors[c * 3] = chan.getColor().getRed() / 255.0f; + colors[c * 3 + 1] = chan.getColor().getGreen() / 255.0f; + colors[c * 3 + 2] = chan.getColor().getBlue() / 255.0f; + mins[c] = (float) chan.getNormalizedMinimum(); + maxes[c] = (float) chan.getNormalizedMaximum(); + gammas[c] = (float) chan.getGamma(); + visible[c] = chan.isVisible() ? 1f : 0f; + } + + // Synthetic tracing/unmix channel display parameters, in the slot right after the real + // channels (mirrors the legacy TetVolumeActor convention of a dedicated channel-index-2 + // slider for the unmixed channel). The shader combines it alongside the real channels + // (see combineChannels()/integrate_intensity() in OmeZarrVolumeMipFrag.glsl). + if (n < MAX_CHANNELS) { + if (n < modelChannels) { + ChannelColorModel tracingChan = colorMap.getChannel(n); + colors[n * 3] = tracingChan.getColor().getRed() / 255.0f; + colors[n * 3 + 1] = tracingChan.getColor().getGreen() / 255.0f; + colors[n * 3 + 2] = tracingChan.getColor().getBlue() / 255.0f; + mins[n] = (float) tracingChan.getNormalizedMinimum(); + maxes[n] = (float) tracingChan.getNormalizedMaximum(); + gammas[n] = (float) tracingChan.getGamma(); + visible[n] = tracingChan.isVisible() ? 1f : 0f; + } else { + maxes[n] = 1f; + gammas[n] = 1f; + } + } + + gl.glUniform3fv(channelColorIndex, MAX_CHANNELS, colors, 0); + gl.glUniform1fv(channelMinIndex, MAX_CHANNELS, mins, 0); + gl.glUniform1fv(channelMaxIndex, MAX_CHANNELS, maxes, 0); + gl.glUniform1fv(channelGammaIndex, MAX_CHANNELS, gammas, 0); + gl.glUniform1fv(channelVisibleIndex, MAX_CHANNELS, visible, 0); + + // Per-channel unmixing weights (sized to MAX_CHANNELS; channels beyond n stay zero so they + // do not contribute to the synthetic tracing channel). + float[] unmixMin = new float[MAX_CHANNELS]; + float[] unmixScale = new float[MAX_CHANNELS]; + float[] unmixMins = colorMap.getUnmixChannelMinimums(n); + float[] unmixScales = colorMap.getUnmixChannelScales(n); + for (int c = 0; c < n; ++c) { + unmixMin[c] = unmixMins[c]; + unmixScale[c] = unmixScales[c]; + } + if (unmixMinIndex >= 0) + gl.glUniform1fv(unmixMinIndex, MAX_CHANNELS, unmixMin, 0); + if (unmixScaleIndex >= 0) + gl.glUniform1fv(unmixScaleIndex, MAX_CHANNELS, unmixScale, 0); + } + + @Override + protected void displayWithMatrices(GL3 gl, MeshActor mesh, AbstractCamera camera, Matrix4 modelViewMatrix) { + Viewport vp = camera.getViewport(); + ConstViewSlab slab = new BasicViewSlab(vp.getzNearRelative() / 10.0f, vp.getzFarRelative() + 100.0f); + try { + camera.pushInternalViewSlab(slab); + if (modelViewMatrix == null) + modelViewMatrix = new Matrix4(camera.getViewMatrix()); + gl.glUniformMatrix4fv(modelViewIndex, 1, false, modelViewMatrix.asArray(), 0); + + Matrix4 projectionMatrix = camera.getProjectionMatrix(); + gl.glUniformMatrix4fv(projectionIndex, 1, false, projectionMatrix.asArray(), 0); + + displayNoMatrices(gl, mesh, camera, modelViewMatrix); + } finally { + camera.popInternalViewSlab(); + } + } + + @Override + public void dispose(GL3 gl) { + for (ShaderProgram p : new ShaderProgram[]{isoShader, mipShader, occShader}) { + p.dispose(gl); + } + super.dispose(gl); + for (Texture3d t : channelTextures) { + if (t != null) + t.dispose(gl); + } + } + + @Override + public void load(GL3 gl, AbstractCamera camera) { + updateShaderProgram(); + super.load(gl, camera); + + if (uniformIndicesAreDirty) + updateUniformIndices(gl); + + // Bind each channel texture to its own texture unit and point the sampler array at them. + int[] samplerUnits = new int[MAX_CHANNELS]; + for (int c = 0; c < MAX_CHANNELS; ++c) { + Texture3d t = (c < channelCount) ? channelTextures[c] : channelTextures[0]; + int unit = (c < channelCount) ? c : 0; // unused samplers alias unit 0 (guarded by channelCount) + if (t != null) { + if (volumeState.filteringOrder <= 0) { + t.setMagFilter(GL3.GL_NEAREST); + t.setMinFilter(GL3.GL_NEAREST_MIPMAP_NEAREST); + } else { + t.setMagFilter(GL3.GL_LINEAR); + t.setMinFilter(GL3.GL_LINEAR_MIPMAP_NEAREST); + } + if (c < channelCount) + t.bind(gl, c); + } + samplerUnits[c] = unit; + } + gl.glUniform1iv(channelTextureIndex, MAX_CHANNELS, samplerUnits, 0); + + gl.glUniform1i(filteringOrderIndex, volumeState.filteringOrder); + + // 2D depth texture from the opaque render pass, on a unit past the channel units. + if (opaqueDepthTexture != null) { + opaqueDepthTexture.bind(gl, DEPTH_TEXTURE_UNIT); + gl.glUniform1i(opaqueDepthTextureIndex, DEPTH_TEXTURE_UNIT); + } + } + + @Override + public void unload(GL3 gl) { + super.unload(gl); + for (int c = 0; c < channelCount; ++c) { + if (channelTextures[c] != null) + channelTextures[c].unbind(gl); + } + if (opaqueDepthTexture != null) + opaqueDepthTexture.unbind(gl); + } + + @Override + public boolean usesNormals() { + return false; + } + + @Override + public void init(GL3 gl) { + updateShaderProgram(); + super.init(gl); + updateUniformIndices(gl); + for (Texture3d t : channelTextures) { + if (t != null) + t.init(gl); + } + } + + private void updateUniformIndices(GL3 gl) { + int s = shaderProgram.getProgramHandle(); + + cameraPositionInTextureCoordinatesIndex = gl.glGetUniformLocation(s, "camPosInTc"); + channelTextureIndex = gl.glGetUniformLocation(s, "channelTexture"); + channelCountIndex = gl.glGetUniformLocation(s, "channelCount"); + opaqueDepthTextureIndex = gl.glGetUniformLocation(s, "opaqueDepthTexture"); + levelOfDetailIndex = gl.glGetUniformLocation(s, "levelOfDetail"); + nearSlabPlaneIndex = gl.glGetUniformLocation(s, "nearSlabPlane"); + farSlabPlaneIndex = gl.glGetUniformLocation(s, "farSlabPlane"); + volumeMicrometersIndex = gl.glGetUniformLocation(s, "volumeMicrometers"); + filteringOrderIndex = gl.glGetUniformLocation(s, "filteringOrder"); + modelViewIndex = gl.glGetUniformLocation(s, "modelViewMatrix"); + projectionIndex = gl.glGetUniformLocation(s, "projectionMatrix"); + tcToCameraIndex = gl.glGetUniformLocation(s, "tcToCamera"); + opaqueZNearFarIndex = gl.glGetUniformLocation(s, "opaqueZNearFar"); + + channelColorIndex = gl.glGetUniformLocation(s, "channelColor"); + channelMinIndex = gl.glGetUniformLocation(s, "channelMin"); + channelMaxIndex = gl.glGetUniformLocation(s, "channelMax"); + channelGammaIndex = gl.glGetUniformLocation(s, "channelGamma"); + channelVisibleIndex = gl.glGetUniformLocation(s, "channelVisible"); + unmixMinIndex = gl.glGetUniformLocation(s, "unmixMin"); + unmixScaleIndex = gl.glGetUniformLocation(s, "unmixScale"); + + uniformIndicesAreDirty = false; + } + + @Override + public void setOpaqueDepthTexture(Texture2d opaqueDepthTexture) { + this.opaqueDepthTexture = opaqueDepthTexture; + } + + private static class OmeZarrVolumeMipShader extends BasicShaderProgram { + public OmeZarrVolumeMipShader(int projectionMode) { + try { + getShaderSteps().add(new ShaderStep(GL2ES2.GL_VERTEX_SHADER, + getClass().getResourceAsStream( + "/org/janelia/gltools/material/shader/VolumeMipVrtx.glsl")) + ); + String projectionDefine = "#define PROJECTION_MODE " + projectionMode + "\n"; + String basicFragShaderString = IOUtils.toString(getClass().getResourceAsStream( + "/org/janelia/horta/shader/OmeZarrVolumeMipFrag.glsl"), "UTF-8"); + String fragShaderString = basicFragShaderString.replace( + "#define PROJECTION_MODE PROJECTION_MAXIMUM", projectionDefine); + getShaderSteps().add(new ShaderStep(GL2ES2.GL_FRAGMENT_SHADER, fragShaderString)); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } + } +} diff --git a/modules/HortaTracer/src/main/resources/org/janelia/horta/shader/OmeZarrVolumeMipFrag.glsl b/modules/HortaTracer/src/main/resources/org/janelia/horta/shader/OmeZarrVolumeMipFrag.glsl new file mode 100644 index 0000000000..ef47484377 --- /dev/null +++ b/modules/HortaTracer/src/main/resources/org/janelia/horta/shader/OmeZarrVolumeMipFrag.glsl @@ -0,0 +1,549 @@ +#version 330 + +// Use #defines to compile separate shaders for each projection type +#define PROJECTION_MAXIMUM 0 +#define PROJECTION_OCCLUDING 1 +#define PROJECTION_ISOSURFACE 2 +// REPLACE THE FOLLOWING LINE WITH CORRECT PROJECTION DEFINE +#define PROJECTION_MODE PROJECTION_MAXIMUM + +/** + * Multi-channel ray-casting volume shader for OME-Zarr. + * + * Forked from VolumeMipFrag.glsl. Where that shader packs (up to) two channels into a single + * sampler3D, this one binds one single-component sampler3D per channel (channelTexture[c]) and + * combines an arbitrary number of channels jointly in a single ray-march pass, mirroring the + * channel-combination and in-shader unmixing of TetVolumeFrag330.glsl. + * + * GLSL 3.30 restriction: sampler arrays may only be indexed with a constant expression, so the + * per-channel texture fetches are unrolled with literal indices (see FETCH_CHANNEL). Ordinary + * float/vec uniform arrays (channelColor, channelMin, ...) may be indexed with a loop variable. + */ + +// Must match MAX_CHANNELS in OmeZarrVolumeMipMaterial.java +#define MAX_CHANNELS 8 + +// primary render target: final blended RGBA color +layout(location = 0) out vec4 colorOut; +// secondary render target for picking / tracing: core intensity and packed opacity+depth +layout(location = 1) out vec2 pickId; + +// One single-component volume texture per channel. +uniform sampler3D channelTexture[MAX_CHANNELS]; +uniform int channelCount = 1; + +// Per-channel transfer function, driven by the ImageColorModel (see setChannelUniforms()). +uniform vec3 channelColor[MAX_CHANNELS]; // display color of each channel +uniform float channelMin[MAX_CHANNELS]; // normalized black level +uniform float channelMax[MAX_CHANNELS]; // normalized white level +uniform float channelGamma[MAX_CHANNELS]; // gamma +uniform float channelVisible[MAX_CHANNELS]; // 1.0 visible, 0.0 hidden + +// In-shader unmixing: the synthetic tracing channel is a signed linear combination of all channels, +// tracing = sum_c unmixScale[c] * (raw[c] - unmixMin[c]) + blackLevel. A negative scale subtracts a +// channel's bleed-through. Defaults (set from the legacy 2-channel params on the Java side) reproduce +// the original behavior: scale 0.5 on channels 0 and 1, zero elsewhere. +uniform float unmixMin[MAX_CHANNELS]; +uniform float unmixScale[MAX_CHANNELS]; + +uniform vec3 camPosInTc; // camera position, in texture coordinate frame +uniform int levelOfDetail = 0; // volume texture LOD +uniform vec3 volumeMicrometers = vec3(256, 256, 200); +uniform float canonicalOccludingPathLengthUm = 1.0; // micrometers +uniform float fadeThicknessInTexels = 1000.0; + +// Homogeneous clip plane equations, in texture coordinates +uniform vec4 nearSlabPlane; +uniform vec4 farSlabPlane; + +// put surface normals in eye space for isosurface projection +uniform mat4 tcToCamera = mat4(1); + +// clip using depth buffer from opaque pass +uniform sampler2D opaqueDepthTexture; +uniform vec2 opaqueZNearFar = vec2(1e-2, 1e4); + +#define FILTER_NEAREST 0 +#define FILTER_TRILINEAR 1 +#define FILTER_TRICUBIC 3 // Expensive beautiful rendering option, for slow, high quality rendering passes +uniform int filteringOrder = 3; // 0: NEAREST; 1: TRILINEAR; 2: 3: TRICUBIC + +in vec3 fragTexCoord; // texture coordinate at mesh surface of volume + +// --------------------------------------------------------------------------- +// Tricubic filtering helpers (single component) +// --------------------------------------------------------------------------- + +// Catmull-Rom spline actually passes through control points +vec4 cubic(float x) +{ + const float s = 0.5; // potentially adjustable parameter + float x2 = x * x; + float x3 = x2 * x; + vec4 w; + w.x = -s*x3 + 2*s*x2 - s*x + 0; + w.y = (2-s)*x3 + (s-3)*x2 + 1; + w.z = (s-2)*x3 + (3-2*s)*x2 + s*x + 0; + w.w = s*x3 - s*x2 + 0; + return w; +} + +// Fast cubic interpolation using a source that is already linearly interpolated. +// Adapted from https://groups.google.com/forum/#!topic/comp.graphics.api.opengl/kqrujgJfTxo +float filterFastCubic3D(sampler3D tex, vec3 texcoord, vec3 texscale, int lod) +{ + float fx = fract(texcoord.x); + float fy = fract(texcoord.y); + float fz = fract(texcoord.z); + + texcoord.x -= fx; + texcoord.y -= fy; + texcoord.z -= fz; + + vec4 xcubic = cubic(fx); + vec4 ycubic = cubic(fy); + vec4 zcubic = cubic(fz); + + vec3 c0 = texcoord - vec3(0.5, 0.5, 0.5); + vec3 s0 = vec3(xcubic.x + xcubic.y, ycubic.x + ycubic.y, zcubic.x + zcubic.y); + vec3 offset0 = c0 + vec3(xcubic.y, ycubic.y, zcubic.y) / s0; + + float sample000 = textureLod(tex, vec3(offset0.x, offset0.y, offset0.z) * texscale, lod).r; + vec3 c1 = texcoord + vec3(1.5, 1.5, 1.5); + vec3 s1 = vec3(xcubic.z + xcubic.w, ycubic.z + ycubic.w, zcubic.z + zcubic.w); + vec3 offset1 = c1 + vec3(xcubic.w, ycubic.w, zcubic.w) / s1; + float sample100 = textureLod(tex, vec3(offset1.x, offset0.y, offset0.z) * texscale, lod).r; + float sx = s0.x / (s0.x + s1.x); + float sampleX00 = mix(sample100, sample000, sx); + float sample010 = textureLod(tex, vec3(offset0.x, offset1.y, offset0.z) * texscale, lod).r; + float sample110 = textureLod(tex, vec3(offset1.x, offset1.y, offset0.z) * texscale, lod).r; + float sy = s0.y / (s0.y + s1.y); + float sampleX10 = mix(sample110, sample010, sx); + float sampleXY0 = mix(sampleX10, sampleX00, sy); + float sample001 = textureLod(tex, vec3(offset0.x, offset0.y, offset1.z) * texscale, lod).r; + float sample101 = textureLod(tex, vec3(offset1.x, offset0.y, offset1.z) * texscale, lod).r; + float sampleX01 = mix(sample101, sample001, sx); + float sz = s0.z / (s0.z + s1.z); + float sample011 = textureLod(tex, vec3(offset0.x, offset1.y, offset1.z) * texscale, lod).r; + float sample111 = textureLod(tex, vec3(offset1.x, offset1.y, offset1.z) * texscale, lod).r; + float sampleX11 = mix(sample111, sample011, sx); + float sampleXY1 = mix(sampleX11, sampleX01, sy); + + return mix(sampleXY1, sampleXY0, sz); +} + +float rampstep(float edge0, float edge1, float x) { + return clamp((x - edge0)/(edge1 - edge0), 0.0, 1.0); +} + +// --------------------------------------------------------------------------- +// Per-channel sampling and combination +// --------------------------------------------------------------------------- + +// Fetch a single channel's raw normalized intensity at a texel position. +float sampleChannelRaw(sampler3D tex, vec3 texelPos, vec3 textureScale) { + if (filteringOrder == FILTER_TRICUBIC) + return filterFastCubic3D(tex, texelPos, textureScale, levelOfDetail); + return textureLod(tex, texelPos * textureScale, levelOfDetail).r; +} + +// Sample all active channels into raw[]. Sampler arrays require constant indices in GLSL 330, +// so this is unrolled; channels beyond channelCount are left at zero. +#define FETCH_CHANNEL(i) raw[i] = (i < channelCount) ? sampleChannelRaw(channelTexture[i], texelPos, textureScale) : 0.0; +void fetchChannels(vec3 texelPos, vec3 textureScale, out float raw[MAX_CHANNELS]) { + FETCH_CHANNEL(0) FETCH_CHANNEL(1) FETCH_CHANNEL(2) FETCH_CHANNEL(3) + FETCH_CHANNEL(4) FETCH_CHANNEL(5) FETCH_CHANNEL(6) FETCH_CHANNEL(7) +} + +// Apply one channel's transfer function: black/white contrast stretch, gamma, visibility. +float rescaleChannel(int c, float raw) { + float r = (raw - channelMin[c]) / max(channelMax[c] - channelMin[c], 1e-5); + r = clamp(r, 0.0, 1.0); + r = pow(r, channelGamma[c]); + return r * channelVisible[c]; +} + +// Combine all channels into a display color (screen combine of each channel's color) and an +// aggregate opacity (1 - product of per-channel transparencies), mirroring TetVolumeFrag330. +// Also includes the synthetic tracing/unmix channel at index channelCount (see +// tracingChannelFromRaw()), so it gets its own color/contrast/visibility and can contribute to +// opacity on its own, exactly like a real channel -- mirrors the legacy TetVolumeActor behavior +// where the unmixed channel was jointly rescaled and combined with the real channels. +vec4 combineChannels(in float raw[MAX_CHANNELS]) { + vec3 transparency = vec3(1.0); + float opacityTransparency = 1.0; + int totalChannels = min(channelCount + 1, MAX_CHANNELS); + for (int c = 0; c < totalChannels; ++c) { + float r = rescaleChannel(c, raw[c]); + transparency *= (vec3(1.0) - clamp(r * channelColor[c], 0.0, 1.0)); + opacityTransparency *= (1.0 - r); + } + vec3 color = clamp(vec3(1.0) - transparency, 0.0, 1.0); + float opacity = clamp(1.0 - opacityTransparency, 0.0, 1.0); + return vec4(color, opacity); +} + +// Synthesize the tracing channel as a signed linear combination of all measured channels. +float tracingChannelFromRaw(in float raw[MAX_CHANNELS]) { + if (channelCount < 2) + return raw[0]; + float result = 0.0; + float minRebased = 1.0; + int dominant = 0; // channel with the largest weight, whose black level we restore + for (int c = 0; c < channelCount; ++c) { + float rebased = raw[c] - unmixMin[c]; + minRebased = min(minRebased, rebased); + result += unmixScale[c] * rebased; + if (unmixScale[c] > unmixScale[dominant]) + dominant = c; + } + if (minRebased < -0.05) + return 0.0; // below threshold -> no data + result += unmixMin[dominant]; + return clamp(result, 0.0, 1.0); +} + +// --------------------------------------------------------------------------- +// Isosurface normal (computed from channel 0 as a representative scalar field) +// --------------------------------------------------------------------------- + +vec3 calculateNormalInScreenSpace(vec3 uvw, vec3 textureScale) +{ + const float delta = 0.75; + const float downScale = 0.4; + vec3 v0, v1; + v0.x = downScale * sampleChannelRaw(channelTexture[0], uvw + vec3(delta, 0, 0), textureScale); + v1.x = downScale * sampleChannelRaw(channelTexture[0], uvw - vec3(delta, 0, 0), textureScale); + v0.y = downScale * sampleChannelRaw(channelTexture[0], uvw + vec3(0, delta, 0), textureScale); + v1.y = downScale * sampleChannelRaw(channelTexture[0], uvw - vec3(0, delta, 0), textureScale); + v0.z = downScale * sampleChannelRaw(channelTexture[0], uvw + vec3(0, 0, delta), textureScale); + v1.z = downScale * sampleChannelRaw(channelTexture[0], uvw - vec3(0, 0, delta), textureScale); + vec3 result = normalize(v1 - v0); + result = (tcToCamera * vec4(result, 0)).xyz; + return normalize(result); +} + +// --------------------------------------------------------------------------- +// Ray-casting machinery (geometry only; unchanged from VolumeMipFrag.glsl, +// with the reference sampler being channelTexture[0]) +// --------------------------------------------------------------------------- + +// Accumulated values, for eventual display. Channels are integrated independently (element-wise +// max for MIP, per-channel under-operator for occluding) and only combined into a display color in +// save_color(), mirroring TetVolumeFrag330's integrate-then-colorize approach so that each channel's +// contribution survives the blend rather than being collapsed per voxel. +struct IntegratedIntensity +{ + float channel[MAX_CHANNELS]; // per-channel accumulated raw intensity (MIP/occluding) + vec3 isoColor; // surface color, isosurface mode only + float opacity; + float coreIntensity; // brightest tracing-channel intensity yet seen + float coreRayParameter; // location of coreIntensity +}; + +struct CoreStatus +{ + bool inLocalBody; + float firstBodyRayParam; + float finalBodyRayParam; +}; + +struct RayBounds { float minRayParameter; float maxRayParameter; }; + +struct RayParameters { + vec3 rayOriginInTexels; + vec3 rayDirectionInTexels; + vec3 rayBoxCorner; + vec3 forwardMask; + vec3 textureScale; +}; + +struct ViewSlab { float minRayParam; float maxRayParam; }; + +struct VoxelRayState { + float previousVoxelMiddleRayParameter; + float entryRayParameter; + float middleRayParameter; + float exitRayParameter; +}; + +float advance_to_voxel_edge(in float previousEdge, in RayParameters rayParameters) +{ + const float minStep = 0.01; + float t = previousEdge + minStep; + vec3 x0 = rayParameters.rayOriginInTexels; + vec3 x1 = rayParameters.rayDirectionInTexels; + vec3 currentTexelPos = (x0 + t*x1); + vec3 currentTexel = floor(currentTexelPos + rayParameters.rayBoxCorner) + - rayParameters.rayBoxCorner; + vec3 forwardMask = rayParameters.forwardMask; + vec3 candidateEdges = currentTexel + forwardMask; + vec3 candidateSteps = -(x0 - candidateEdges)/x1; + float nextEdge = min(candidateSteps.x, min(candidateSteps.y, candidateSteps.z)); + nextEdge = max(nextEdge, previousEdge + minStep); + return nextEdge; +} + +VoxelRayState find_first_voxel(in RayBounds rayBounds, in RayParameters rayParameters) { + float t1 = rayBounds.minRayParameter; + float t3 = advance_to_voxel_edge(t1, rayParameters); + float t2 = (t1 + t3)/2.0; + return VoxelRayState(t1, t1, t2, t3); +} + +RayBounds initialize_ray_bounds(in RayParameters rayParameters, in ViewSlab viewSlab) +{ + float tMin = viewSlab.minRayParam; + float tMax = viewSlab.maxRayParam; + + vec3 texelMax = rayParameters.forwardMask / rayParameters.textureScale; + vec3 reverseMask = vec3(1) - rayParameters.forwardMask; + vec3 texelMin = reverseMask / rayParameters.textureScale; + vec3 x0 = rayParameters.rayOriginInTexels; + vec3 x1 = rayParameters.rayDirectionInTexels; + vec3 vtMin = -(x0 - texelMin)/x1; + float texCoordTMin = max(max(vtMin.x, vtMin.y), vtMin.z); + vec3 vtMax = -(x0 - texelMax)/x1; + float texCoordTMax = min(min(vtMax.x, vtMax.y), vtMax.z); + + tMin = max(tMin, texCoordTMin); + tMax = min(tMax, texCoordTMax); + + // Clip by depth buffer from already-rendered opaque objects, such as neuron models + vec2 depthTc = gl_FragCoord.xy / textureSize(opaqueDepthTexture, 0); + float z_buf = texture(opaqueDepthTexture, depthTc).x; + float zNear = opaqueZNearFar.x; + float zFar = opaqueZNearFar.y; + float z_eye = 2*zFar*zNear / (zFar + zNear - (zFar - zNear)*(2*z_buf - 1)); + vec4 depth_plane_eye = vec4(0, 0, 1, z_eye); + vec4 depth_plane_tc = transpose(tcToCamera)*depth_plane_eye; + vec4 depth_plane_texels = vec4(depth_plane_tc.xyz*rayParameters.textureScale, depth_plane_tc.w); + float tDepth = -dot(depth_plane_texels, vec4(x0,1)) / dot(depth_plane_texels, vec4(x1,0)); + if ((z_buf != 0) && (z_buf < 0.9999)) { + tMax = min(tDepth, tMax); + } + + return RayBounds(tMin, tMax); +} + +RayParameters initialize_ray_parameters() { + ivec3 texelsPerVolume = textureSize(channelTexture[0], levelOfDetail); + + vec3 originInTexels = fragTexCoord * texelsPerVolume; + vec3 directionInTexels = normalize( (fragTexCoord - camPosInTc) * texelsPerVolume ); + + vec3 rayBoxCorner; + if (filteringOrder == 0) + rayBoxCorner = vec3(0, 0, 0); + else + rayBoxCorner = vec3(0.5, 0.5, 0.5); + + vec3 forwardMask = ceil(directionInTexels * 0.99); + vec3 textureScale = vec3(1,1,1) / texelsPerVolume; + + return RayParameters(originInTexels, directionInTexels, rayBoxCorner, forwardMask, textureScale); +} + +ViewSlab initialize_view_slab(RayParameters rayParams) +{ + vec3 x0 = rayParams.rayOriginInTexels; + vec3 x1 = rayParams.rayDirectionInTexels; + vec4 nearSlabTexels = vec4(nearSlabPlane.xyz*rayParams.textureScale, nearSlabPlane.w); + vec4 farSlabTexels = vec4(farSlabPlane.xyz*rayParams.textureScale, farSlabPlane.w); + float tMinSlab = -dot(nearSlabTexels, vec4(x0,1)) / dot(nearSlabTexels, vec4(x1,0)); + float tMaxSlab = -dot(farSlabTexels, vec4(x0,1)) / dot(farSlabTexels, vec4(x1,0)); + return ViewSlab(tMinSlab, tMaxSlab); +} + +// Integrate the latest sample into the per-channel accumulator. Channels are kept separate here +// (combined into a color only in save_color) so that, for MIP, each channel's maximum along the ray +// survives independently rather than being collapsed to a single winning voxel. +void integrate_intensity( + in float localRaw[MAX_CHANNELS], // raw per-channel intensity at this voxel + in float localOpacity, // aggregate opacity for this voxel (already faded) + in float tracingIntensity, + inout IntegratedIntensity integratedIntensity, + inout CoreStatus core, + in ViewSlab viewSlab, + in VoxelRayState voxelRayState, + in RayParameters rayParams) +{ + float rayParameter = voxelRayState.middleRayParameter; + + // Track the location of the tracing-channel core (MIP criterion, any projection mode) + if (tracingIntensity > integratedIntensity.coreIntensity) { + integratedIntensity.coreIntensity = tracingIntensity; + core.inLocalBody = true; + core.firstBodyRayParam = core.finalBodyRayParam = rayParameter; + integratedIntensity.coreRayParameter = rayParameter; + } + else if (core.inLocalBody) { + if (tracingIntensity == integratedIntensity.coreIntensity) { + core.finalBodyRayParam = rayParameter; + integratedIntensity.coreRayParameter = mix(core.firstBodyRayParam, core.finalBodyRayParam, 0.5); + } + else { + core.inLocalBody = false; + } + } + + #if PROJECTION_MODE == PROJECTION_ISOSURFACE + if (integratedIntensity.opacity > 0.5) + return; + const float isoThreshold = 0.5; + if (localOpacity <= isoThreshold) + return; // surface not intersected + integratedIntensity.opacity = 1.0; + float surfaceRayParam = voxelRayState.entryRayParameter; + vec3 x0 = rayParams.rayOriginInTexels; + vec3 x1 = rayParams.rayDirectionInTexels; + vec3 surfaceTexel = x0 + surfaceRayParam * x1; + vec3 normal = calculateNormalInScreenSpace(surfaceTexel, rayParams.textureScale); + normal = 0.5 * (normal + vec3(1, 1, 1)); + integratedIntensity.isoColor = normal; + return; + #endif + + if (localOpacity <= 0) + return; + + // fade intensity at front and back, for smoother clipping + float rd = (rayParameter - viewSlab.minRayParam) + / (viewSlab.maxRayParam - viewSlab.minRayParam); + float fadeInterval = fadeThicknessInTexels / (viewSlab.maxRayParam - viewSlab.minRayParam); + fadeInterval = min(0.40, fadeInterval); + float fade = rampstep(0.0, fadeInterval, rd); + fade = min(fade, rampstep(1.0, 1.0 - fadeInterval, rd)); + localOpacity *= fade; + + #if PROJECTION_MODE == PROJECTION_MAXIMUM + // Maximum intensity projection: keep each channel's brightest value independently. + // Includes the synthetic tracing channel at index channelCount (see cast_volume_ray()). + int totalChannelsMax = min(channelCount + 1, MAX_CHANNELS); + for (int c = 0; c < totalChannelsMax; ++c) + integratedIntensity.channel[c] = max(integratedIntensity.channel[c], localRaw[c]); + integratedIntensity.opacity = max(integratedIntensity.opacity, localOpacity); + #elif PROJECTION_MODE == PROJECTION_OCCLUDING + // Front-to-back "under" operator, applied per channel with a shared aggregate opacity. + float a_src = localOpacity; + float segmentLengthInRayParam = voxelRayState.exitRayParameter - voxelRayState.entryRayParameter; + vec3 fineVoxelMicrometers = volumeMicrometers / textureSize(channelTexture[0], 0); + vec3 voxelMicrometers = fineVoxelMicrometers; + float umPerRayParam = dot(abs(rayParams.rayDirectionInTexels), voxelMicrometers); + float segmentLengthInUm = segmentLengthInRayParam * umPerRayParam; + float transmittance = 1.0 - a_src; + float exponent = segmentLengthInUm / canonicalOccludingPathLengthUm; + transmittance = pow(transmittance, exponent); + a_src = 1.0 - transmittance; + + float a_dest = integratedIntensity.opacity; // already-integrated (in front) values + float a_out = 1.0 - (1.0 - a_src)*(1.0 - a_dest); + float kFront = (a_out > 0.0) ? a_dest / a_out : 0.0; + // Includes the synthetic tracing channel at index channelCount (see cast_volume_ray()). + int totalChannelsOcc = min(channelCount + 1, MAX_CHANNELS); + for (int c = 0; c < totalChannelsOcc; ++c) { + float blended = integratedIntensity.channel[c]*kFront + localRaw[c]*(1.0 - kFront); + integratedIntensity.channel[c] = clamp(blended, 0.0, 1.0); + } + integratedIntensity.opacity = a_out; + #endif +} + +bool ray_complete( + in VoxelRayState state, + in RayBounds bounds, + in IntegratedIntensity intensity, + in CoreStatus core) +{ + if (state.exitRayParameter >= bounds.maxRayParameter) + return true; + if ((intensity.opacity >= 0.99) && (! core.inLocalBody)) + return true; + return false; +} + +void step_ray(in RayParameters rayParams, inout VoxelRayState voxel) { + float t0 = voxel.exitRayParameter; + float t1 = advance_to_voxel_edge(t0, rayParams); + voxel.entryRayParameter = t0; + voxel.exitRayParameter = t1; + voxel.previousVoxelMiddleRayParameter = voxel.middleRayParameter; + voxel.middleRayParameter = mix(t0, t1, 0.5); +} + +IntegratedIntensity cast_volume_ray(in RayParameters rayParameters, in ViewSlab viewSlab) +{ + RayBounds rayBounds = initialize_ray_bounds(rayParameters, viewSlab); + VoxelRayState voxelRayState = find_first_voxel(rayBounds, rayParameters); + + // Struct contains an array member, so it cannot use a constructor; initialize explicitly. + IntegratedIntensity integratedIntensity; + for (int c = 0; c < MAX_CHANNELS; ++c) + integratedIntensity.channel[c] = 0.0; + integratedIntensity.isoColor = vec3(0); + integratedIntensity.opacity = 0.0; + integratedIntensity.coreIntensity = 0.0; + integratedIntensity.coreRayParameter = 0.0; + + CoreStatus coreStatus = CoreStatus(false, 0, 0); + int stepCount = 0; + const int maxStepCount = 800; + while(true) { + if (ray_complete(voxelRayState, rayBounds, integratedIntensity, coreStatus)) + return integratedIntensity; + + // Sample every channel at the voxel centroid + float t = voxelRayState.middleRayParameter; + vec3 texelPos = rayParameters.rayOriginInTexels + t * rayParameters.rayDirectionInTexels; + float raw[MAX_CHANNELS]; + fetchChannels(texelPos, rayParameters.textureScale, raw); + + float tracingIntensity = tracingChannelFromRaw(raw); + // Feed the synthetic tracing channel into the shared per-channel array (at the slot right + // after the real channels) so it gets its own color/contrast/visibility and can + // contribute to opacity, mirroring the legacy TetVolumeActor behavior. + if (channelCount < MAX_CHANNELS) + raw[channelCount] = tracingIntensity; + + float localOpacity = combineChannels(raw).a; + + integrate_intensity( + raw, + localOpacity, + tracingIntensity, + integratedIntensity, + coreStatus, viewSlab, voxelRayState, rayParameters); + + step_ray(rayParameters, voxelRayState); + stepCount += 1; + if (stepCount >= maxStepCount) + return integratedIntensity; + } + return integratedIntensity; +} + +void save_color(in IntegratedIntensity i, in ViewSlab slab) +{ + #if PROJECTION_MODE == PROJECTION_ISOSURFACE + vec3 color = i.isoColor; + #else + // Combine the per-channel accumulated intensities into a single display color now that + // integration is complete (screen-combine of each channel's transfer-function color). + vec3 color = combineChannels(i.channel).rgb; + #endif + colorOut = vec4(color, i.opacity); + + float relativeDepth = (i.coreRayParameter - slab.minRayParam) / (slab.maxRayParam - slab.minRayParam); + relativeDepth = clamp(1.0 - relativeDepth, 0, 0.999); + uint opacityInt = uint(clamp(int(i.opacity * 0x7f), 0, 0x7f)); + pickId = vec2(i.coreIntensity, opacityInt + relativeDepth); +} + +void main() { + RayParameters rayParams = initialize_ray_parameters(); + ViewSlab viewSlab = initialize_view_slab(rayParams); + IntegratedIntensity integratedIntensity = cast_volume_ray(rayParams, viewSlab); + if (integratedIntensity.opacity <= 0.005) + discard; + save_color(integratedIntensity, viewSlab); +} diff --git a/modules/IntegrationAPIs/pom.xml b/modules/IntegrationAPIs/pom.xml index 5259e0bd08..80866712a5 100644 --- a/modules/IntegrationAPIs/pom.xml +++ b/modules/IntegrationAPIs/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. IntegrationAPIs org.janelia.workstation integration - 9.24.1 + 9.25.1 nbm diff --git a/modules/Jogl213Lib/pom.xml b/modules/Jogl213Lib/pom.xml index 0ed07e9106..c0dd3c178d 100644 --- a/modules/Jogl213Lib/pom.xml +++ b/modules/Jogl213Lib/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Jogl213Lib org.janelia.workstation jogl213lib - 9.24.1 + 9.25.1 nbm diff --git a/modules/Jogl224Lib/pom.xml b/modules/Jogl224Lib/pom.xml index 6ff30bb7fe..9d6efb256d 100644 --- a/modules/Jogl224Lib/pom.xml +++ b/modules/Jogl224Lib/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Jogl224Lib org.janelia.workstation jogl224lib - 9.24.1 + 9.25.1 nbm diff --git a/modules/LMDataBrowser/pom.xml b/modules/LMDataBrowser/pom.xml index 9e00205616..7900f23ad4 100644 --- a/modules/LMDataBrowser/pom.xml +++ b/modules/LMDataBrowser/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. LMDataBrowser org.janelia.workstation lmdatabrowser - 9.24.1 + 9.25.1 nbm diff --git a/modules/LargeVolumeViewer/pom.xml b/modules/LargeVolumeViewer/pom.xml index f447ff6ab5..dfb8a3a5f6 100644 --- a/modules/LargeVolumeViewer/pom.xml +++ b/modules/LargeVolumeViewer/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. LargeVolumeViewer org.janelia.workstation large_volume_viewer - 9.24.1 + 9.25.1 nbm diff --git a/modules/N5Viewer/pom.xml b/modules/N5Viewer/pom.xml index c60d28daf6..92d10fb384 100644 --- a/modules/N5Viewer/pom.xml +++ b/modules/N5Viewer/pom.xml @@ -5,14 +5,14 @@ workstation org.janelia - 9.24.1 + 9.25.1 ../.. N5Viewer org.janelia.workstation n5viewer - 9.24.1 + 9.25.1 nbm diff --git a/modules/SceneWindow/pom.xml b/modules/SceneWindow/pom.xml index 1d303d9057..9a6439bed2 100644 --- a/modules/SceneWindow/pom.xml +++ b/modules/SceneWindow/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. SceneWindow org.janelia.workstation scenegraph - 9.24.1 + 9.25.1 nbm diff --git a/modules/SiteHortaCloud/pom.xml b/modules/SiteHortaCloud/pom.xml index 29c41f93f1..fe5eda4684 100644 --- a/modules/SiteHortaCloud/pom.xml +++ b/modules/SiteHortaCloud/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. SiteHortaCloud org.janelia.workstation sitehortacloud - 9.24.1 + 9.25.1 nbm diff --git a/modules/SiteJRC/pom.xml b/modules/SiteJRC/pom.xml index e7ddf70850..b6024d2fd6 100644 --- a/modules/SiteJRC/pom.xml +++ b/modules/SiteJRC/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. SiteJRC org.janelia.workstation sitejrc - 9.24.1 + 9.25.1 nbm diff --git a/modules/Viewer3D/pom.xml b/modules/Viewer3D/pom.xml index 77f578550b..6d026f2d54 100644 --- a/modules/Viewer3D/pom.xml +++ b/modules/Viewer3D/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Viewer3D org.janelia.workstation viewer3d - 9.24.1 + 9.25.1 nbm diff --git a/modules/ViewerController/pom.xml b/modules/ViewerController/pom.xml index 1b0fc00148..8c3d64f546 100644 --- a/modules/ViewerController/pom.xml +++ b/modules/ViewerController/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. ViewerController org.janelia.workstation viewercontroller - 9.24.1 + 9.25.1 nbm diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/color_slider/SliderPanel.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/color_slider/SliderPanel.java index dc65efb312..dec6f81598 100644 --- a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/color_slider/SliderPanel.java +++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/color_slider/SliderPanel.java @@ -47,10 +47,6 @@ public enum VIEW {Horta, LVV}; private static final String IMAGES_LOCK = "lock.png"; private static final String IMAGES_LOCK_UNLOCK = "lock_unlock.png"; - private ColorChannelWidget colorChannelWidget_0; - private ColorChannelWidget colorChannelWidget_1; - private ColorChannelWidget colorChannelWidget_2; - private ColorChannelWidget colorChannelWidget_3; private ColorChannelWidget[] colorWidgets; private JToggleButton lockBlackButton; private JToggleButton lockGrayButton; @@ -101,16 +97,11 @@ public final synchronized void setImageColorModel( ImageColorModel imageColorMod this.remove( ccw ); } } - colorChannelWidget_0 = new ColorChannelWidget(0, imageColorModel); - colorChannelWidget_1 = new ColorChannelWidget(1, imageColorModel); - colorChannelWidget_2 = new ColorChannelWidget(2, imageColorModel); - colorChannelWidget_3 = new ColorChannelWidget(3, imageColorModel); - colorWidgets = new ColorChannelWidget[] { - colorChannelWidget_0, - colorChannelWidget_1, - colorChannelWidget_2, - colorChannelWidget_3 - }; + int channelCount = imageColorModel.getChannelCount(); + colorWidgets = new ColorChannelWidget[channelCount]; + for (int i = 0; i < channelCount; i++) { + colorWidgets[i] = new ColorChannelWidget(i, imageColorModel); + } if ( visibilityListener != null && imageColorModel != null ) { imageColorModel.removeColorModelListener(visibilityListener); } diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/color/ImageColorModel.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/color/ImageColorModel.java index 4dcda13a9b..7a5e01cd10 100644 --- a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/color/ImageColorModel.java +++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/color/ImageColorModel.java @@ -81,6 +81,50 @@ public float[] getUnmixParameters() { public void setUnmixParameters(float[] unmixParameters) { this.unmix = unmixParameters; } + + // Optional per-channel unmixing weights, for combining an arbitrary number of channels into the + // synthetic tracing channel. When unset (null), they are derived from the legacy 2-channel + // unmix vec4 so existing behavior is unchanged. See getUnmixChannel*() below. + private float[] unmixChannelMin = null; + private float[] unmixChannelScale = null; + + /** + * Per-channel black levels for unmixing, sized to channelCount. Falls back to the legacy + * {@code unmix} parameters (min on channels 0/1, zero elsewhere) when not explicitly set. + */ + @JsonIgnore + public float[] getUnmixChannelMinimums(int channelCount) { + float[] result = new float[channelCount]; + for (int c = 0; c < channelCount; c++) { + if (unmixChannelMin != null && c < unmixChannelMin.length) + result[c] = unmixChannelMin[c]; + else if (c < 2 && unmix != null && unmix.length >= 2) + result[c] = unmix[c]; // legacy min0, min1 + } + return result; + } + + /** + * Per-channel unmixing weights, sized to channelCount. Falls back to the legacy {@code unmix} + * parameters (scale on channels 0/1, zero elsewhere) when not explicitly set. + */ + @JsonIgnore + public float[] getUnmixChannelScales(int channelCount) { + float[] result = new float[channelCount]; + for (int c = 0; c < channelCount; c++) { + if (unmixChannelScale != null && c < unmixChannelScale.length) + result[c] = unmixChannelScale[c]; + else if (c < 2 && unmix != null && unmix.length >= 4) + result[c] = unmix[c + 2]; // legacy scale0, scale1 + } + return result; + } + + public void setUnmixChannelParameters(float[] mins, float[] scales) { + this.unmixChannelMin = mins; + this.unmixChannelScale = scales; + fireUnmixingParametersChanged(); + } /** * return a string that represents the user-adjustable state of diff --git a/modules/ViewerInfoPanel/pom.xml b/modules/ViewerInfoPanel/pom.xml index 9fc7dabb4a..571b439c9a 100644 --- a/modules/ViewerInfoPanel/pom.xml +++ b/modules/ViewerInfoPanel/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. ViewerInfoPanel org.janelia.workstation infopanel - 9.24.1 + 9.25.1 nbm diff --git a/modules/application/pom.xml b/modules/application/pom.xml index 94fecede9c..41f9115e81 100644 --- a/modules/application/pom.xml +++ b/modules/application/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Janelia Workstation org.janelia.workstation application - 9.24.1 + 9.25.1 nbm-application diff --git a/modules/application_horta/pom.xml b/modules/application_horta/pom.xml index d07e39b7b3..2ac8ca32e4 100644 --- a/modules/application_horta/pom.xml +++ b/modules/application_horta/pom.xml @@ -5,14 +5,14 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. Horta org.janelia.workstation application_horta - 9.24.1 + 9.25.1 nbm-application diff --git a/modules/branding/pom.xml b/modules/branding/pom.xml index e578103cfa..73491c745d 100644 --- a/modules/branding/pom.xml +++ b/modules/branding/pom.xml @@ -5,13 +5,13 @@ org.janelia workstation - 9.24.1 + 9.25.1 ../.. org.janelia.workstation branding - 9.24.1 + 9.25.1 nbm Branding diff --git a/pom.xml b/pom.xml index 66551226f8..72e31d9ce4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ Janelia Workstation org.janelia workstation - 9.24.1 + 9.25.1 pom @@ -75,7 +75,7 @@ RELEASE82 - 3.85 + 3.86 1.8