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