Skip to content

Commit

Permalink
Merge branch 'feature/basic-zarr-prefetch' into feature/loader-worker
Browse files Browse the repository at this point in the history
  • Loading branch information
frasercl committed Jan 10, 2024
2 parents 8cafddf + 27db5d4 commit e1330a0
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 36 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aics/volume-viewer",
"version": "3.4.0",
"version": "3.5.3",
"description": "volume renderer for multichannel 8-bit intensity data stored as 3D arrays",
"main": "es/index.js",
"module": "es/index.js",
Expand Down
8 changes: 8 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
<input id="timeSlider" type="range" min="0" max="0" step="1" value="0"/><input type="number" id="timeValue" min="0" max="0" value="0"/>
</p>
<p>Pathtrace iterations: <span id="counter">0</span></p>
<p>
<label for="gammaMin">Gamma Min</label>
<input id="gammaMin" type="number" min="0" max="255" step="1" value="0"/>
<label for="gammaScale">Gamma Scale</label>
<input id="gammaScale" type="number" min="0" max="255" step="1" value="128"/>
<label for="gammaMax">Gamma Max</label>
<input id="gammaMax" type="number" min="0" max="255" step="1" value="255"/>
</p>
</body>

</html>
48 changes: 48 additions & 0 deletions public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,27 @@ async function loadTestData(testdata: TestDataSpec) {
loadVolume(loadSpec, myState.loader);
}

function gammaSliderToImageValues(sliderValues: [number, number, number]): [number, number, number] {
let min = Number(sliderValues[0]);
let mid = Number(sliderValues[1]);
let max = Number(sliderValues[2]);

if (mid > max || mid < min) {
mid = 0.5 * (min + max);
}
const div = 255;
min /= div;
max /= div;
mid /= div;
const diff = max - min;
const x = (mid - min) / diff;
let scale = 4 * x * x;
if ((mid - 0.5) * (mid - 0.5) < 0.0005) {
scale = 1.0;
}
return [min, max, scale];
}

function main() {
const el = document.getElementById("volume-viewer");
if (!el) {
Expand Down Expand Up @@ -1266,6 +1287,33 @@ function main() {
});
});

const gammaMin = document.getElementById("gammaMin") as HTMLInputElement;
const gammaMax = document.getElementById("gammaMax") as HTMLInputElement;
const gammaScale = document.getElementById("gammaScale") as HTMLInputElement;
gammaMin?.addEventListener("change", ({ currentTarget }) => {
const g = gammaSliderToImageValues([gammaMin.valueAsNumber, gammaScale.valueAsNumber, gammaMax.valueAsNumber]);
view3D.setGamma(myState.volume, g[0], g[1], g[2]);
});
gammaMin?.addEventListener("input", ({ currentTarget }) => {
const g = gammaSliderToImageValues([gammaMin.valueAsNumber, gammaScale.valueAsNumber, gammaMax.valueAsNumber]);
view3D.setGamma(myState.volume, g[0], g[1], g[2]);
});
gammaMax?.addEventListener("change", ({ currentTarget }) => {
const g = gammaSliderToImageValues([gammaMin.valueAsNumber, gammaScale.valueAsNumber, gammaMax.valueAsNumber]);
view3D.setGamma(myState.volume, g[0], g[1], g[2]);
});
gammaMax?.addEventListener("input", ({ currentTarget }) => {
const g = gammaSliderToImageValues([gammaMin.valueAsNumber, gammaScale.valueAsNumber, gammaMax.valueAsNumber]);
view3D.setGamma(myState.volume, g[0], g[1], g[2]);
});
gammaScale?.addEventListener("change", ({ currentTarget }) => {
const g = gammaSliderToImageValues([gammaMin.valueAsNumber, gammaScale.valueAsNumber, gammaMax.valueAsNumber]);
view3D.setGamma(myState.volume, g[0], g[1], g[2]);
});
gammaScale?.addEventListener("input", ({ currentTarget }) => {
const g = gammaSliderToImageValues([gammaMin.valueAsNumber, gammaScale.valueAsNumber, gammaMax.valueAsNumber]);
view3D.setGamma(myState.volume, g[0], g[1], g[2]);
});
setupGui();

loadTestData(TEST_DATA[(testDataSelect as HTMLSelectElement)?.value]);
Expand Down
8 changes: 5 additions & 3 deletions src/PathTracedVolume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export default class PathTracedVolume implements VolumeRenderImpl {

if (dirtyFlags & SettingsFlags.MATERIAL) {
this.pathTracingUniforms.gDensityScale.value = this.settings.density * 150.0;
this.updateMaterial();
}

// update bounds
Expand Down Expand Up @@ -574,7 +575,7 @@ export default class PathTracedVolume implements VolumeRenderImpl {
this.updateVolumeData4();
this.resetProgress();
this.updateLuts(channelColors, channelData);
this.updateMaterial(channelColors, channelData);
this.updateMaterial();
}

updateVolumeData4(): void {
Expand Down Expand Up @@ -644,12 +645,13 @@ export default class PathTracedVolume implements VolumeRenderImpl {

// image is a material interface that supports per-channel color, spec,
// emissive, glossiness
updateMaterial(channelColors: FuseChannel[], channelData: Channel[]): void {
updateMaterial(): void {
for (let c = 0; c < this.viewChannels.length; ++c) {
const i = this.viewChannels[c];
if (i > -1) {
// diffuse color is actually blended into the LUT now.
const combinedLut = channelData[i].combineLuts(channelColors[i].rgbColor);
const channelData = this.volume.getChannel(i);
const combinedLut = channelData.combineLuts(this.settings.diffuse[i]);
this.pathTracingUniforms.gLutTexture.value.image.data.set(combinedLut, c * LUT_ARRAY_LENGTH);
this.pathTracingUniforms.gLutTexture.value.needsUpdate = true;
this.pathTracingUniforms.gDiffuse.value[c] = new Vector3(1.0, 1.0, 1.0);
Expand Down
16 changes: 10 additions & 6 deletions src/Volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,11 @@ interface VolumeDataObserver {
*/
export default class Volume {
public imageInfo: ImageInfo;
public loadSpec: LoadSpec;
public loadSpec: Required<LoadSpec>;
public loader?: IVolumeLoader;
// `LoadSpec` representing the minimum data required to display what's in the viewer (subregion, channels, etc.).
// Used to intelligently issue load requests whenever required by a state change. Modify with `updateRequiredData`.
private loadSpecRequired: Required<LoadSpec>;
public loadSpecRequired: Required<LoadSpec>;
public channelLoadCallback?: PerChannelCallback;
public imageMetadata: Record<string, unknown>;
public name: string;
Expand Down Expand Up @@ -169,13 +169,16 @@ export default class Volume {
this.loaded = false;
this.imageInfo = imageInfo;
this.name = this.imageInfo.name;
this.loadSpec = loadSpec;
this.loadSpecRequired = {
this.loadSpec = {
// Fill in defaults for optional properties
multiscaleLevel: 0,
channels: Array.from({ length: this.imageInfo.numChannels }, (_val, idx) => idx),
...loadSpec,
subregion: loadSpec.subregion.clone(),
};
this.loadSpecRequired = {
...this.loadSpec,
channels: this.loadSpec.channels.slice(),
subregion: this.loadSpec.subregion.clone(),
};
this.loader = loader;
// imageMetadata to be filled in by Volume Loaders
Expand Down Expand Up @@ -241,7 +244,8 @@ export default class Volume {
this.loadSpecRequired = { ...this.loadSpecRequired, ...required };
let noReload =
this.loadSpec.time === this.loadSpecRequired.time &&
this.loadSpec.subregion.containsBox(this.loadSpecRequired.subregion);
this.loadSpec.subregion.containsBox(this.loadSpecRequired.subregion) &&
this.loadSpecRequired.channels.every((channel) => this.loadSpec.channels.includes(channel));

// An update to `subregion` should trigger a reload when the new subregion is not contained in the old one
// OR when the new subregion is smaller than the old one by enough that we can load a higher scale level.
Expand Down
26 changes: 20 additions & 6 deletions src/VolumeDrawable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export default class VolumeDrawable {
// @param {number} glevel 0..1
// @param {number} gmax 0..1, should be > gmin
setGamma(gmin: number, glevel: number, gmax: number): void {
if (this.settings.gammaMin === gmin || this.settings.gammaLevel === glevel || this.settings.gammaMax === gmax) {
if (this.settings.gammaMin === gmin && this.settings.gammaLevel === glevel && this.settings.gammaMax === gmax) {
return;
}
this.settings.gammaMin = gmin;
Expand Down Expand Up @@ -427,10 +427,12 @@ export default class VolumeDrawable {

updateMaterial(): void {
this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
}

updateLuts(): void {
this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
}

setVoxelSize(values: Vector3): void {
Expand Down Expand Up @@ -474,6 +476,11 @@ export default class VolumeDrawable {
],
};

this.settings.diffuse[newChannelIndex] = [
this.channelColors[newChannelIndex][0],
this.channelColors[newChannelIndex][1],
this.channelColors[newChannelIndex][2],
];
this.settings.specular[newChannelIndex] = [0, 0, 0];
this.settings.emissive[newChannelIndex] = [0, 0, 0];
this.settings.glossiness[newChannelIndex] = 0;
Expand All @@ -490,12 +497,17 @@ export default class VolumeDrawable {
// flip the color to the "null" value
this.fusion[channelIndex].rgbColor = enabled ? this.channelColors[channelIndex] : 0;
// if all are nulled out, then hide the volume element from the scene.
if (this.fusion.every((elem) => elem.rgbColor === 0)) {
this.settings.visible = false;
} else {
this.settings.visible = true;
}
this.settings.visible = !this.fusion.every((elem) => elem.rgbColor === 0);
this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);

// add or remove this channel from the list of required channels to load
const { channels } = this.volume.loadSpecRequired;
const channelRequired = channels.includes(channelIndex);
if (enabled && !channelRequired) {
this.volume.updateRequiredData({ channels: [...channels, channelIndex] });
} else if (!enabled && channelRequired) {
this.volume.updateRequiredData({ channels: channels.filter((i) => i !== channelIndex) });
}
}

isVolumeChannelEnabled(channelIndex: number): boolean {
Expand All @@ -510,6 +522,7 @@ export default class VolumeDrawable {
return;
}
this.channelColors[channelIndex] = colorrgb;
this.settings.diffuse[channelIndex] = colorrgb;
// if volume channel is zero'ed out, then don't update it until it is switched on again.
if (this.fusion[channelIndex].rgbColor !== 0) {
this.fusion[channelIndex].rgbColor = colorrgb;
Expand Down Expand Up @@ -545,6 +558,7 @@ export default class VolumeDrawable {
return;
}
this.updateChannelColor(channelIndex, colorrgb);
this.settings.diffuse[channelIndex] = colorrgb;
this.settings.specular[channelIndex] = specularrgb;
this.settings.emissive[channelIndex] = emissivergb;
this.settings.glossiness[channelIndex] = glossiness;
Expand Down
6 changes: 5 additions & 1 deletion src/VolumeRenderSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export enum SettingsFlags {
ROI = 0b000001000,
/** parameters: maskAlpha */
MASK_ALPHA = 0b000010000,
/** parameters: density, specular, emissive, glossiness */
/** parameters: density, diffuse, specular, emissive, glossiness */
MATERIAL = 0b000100000,
/** parameters: resolution, useInterpolation, pixelSamplingRate, primaryRayStepSize, secondaryRayStepSize*/
SAMPLING = 0b001000000,
Expand Down Expand Up @@ -64,6 +64,7 @@ export class VolumeRenderSettings {

// MATERIAL
public density: number;
public diffuse: [number, number, number][];
public specular: [number, number, number][];
public emissive: [number, number, number][];
public glossiness: number[];
Expand Down Expand Up @@ -115,11 +116,13 @@ export class VolumeRenderSettings {
// volume-dependent properties
if (volume) {
this.zSlice = Math.floor(volume.imageInfo.subregionSize.z / 2);
this.diffuse = new Array(volume.imageInfo.numChannels).fill([255, 255, 255]);
this.specular = new Array(volume.imageInfo.numChannels).fill([0, 0, 0]);
this.emissive = new Array(volume.imageInfo.numChannels).fill([0, 0, 0]);
this.glossiness = new Array(volume.imageInfo.numChannels).fill(0);
} else {
this.zSlice = 0;
this.diffuse = [[255, 255, 255]];
this.specular = [[0, 0, 0]];
this.emissive = [[0, 0, 0]];
this.glossiness = [0];
Expand All @@ -130,6 +133,7 @@ export class VolumeRenderSettings {

public resizeWithVolume(volume: Volume): void {
this.zSlice = Math.floor(volume.imageInfo.subregionSize.z / 2);
this.diffuse = new Array(volume.imageInfo.numChannels).fill([255, 255, 255]);
this.specular = new Array(volume.imageInfo.numChannels).fill([0, 0, 0]);
this.emissive = new Array(volume.imageInfo.numChannels).fill([0, 0, 0]);
this.glossiness = new Array(volume.imageInfo.numChannels).fill(0);
Expand Down
4 changes: 2 additions & 2 deletions src/loaders/IVolumeLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,13 @@ export abstract class ThreadableVolumeLoader implements IVolumeLoader {
onChannelLoaded?.(volume, channelIndex);
};

const spec = loadSpec || volume.loadSpec;
const spec = { ...loadSpec, ...volume.loadSpec };
const [adjustedImageInfo, adjustedLoadSpec] = await this.loadRawChannelData(volume.imageInfo, spec, onChannelData);

if (adjustedImageInfo) {
volume.imageInfo = adjustedImageInfo;
volume.updateDimensions();
}
volume.loadSpec = adjustedLoadSpec || spec;
volume.loadSpec = { ...adjustedLoadSpec, ...spec };
}
}
4 changes: 2 additions & 2 deletions src/loaders/OmeZarrLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
imageInfo: ImageInfo,
loadSpec: LoadSpec,
onData: RawChannelDataCallback
): Promise<[ImageInfo, LoadSpec]> {
): Promise<[ImageInfo, undefined]> {
// First, cancel any pending requests for this volume
if (this.loadSubscriber !== undefined) {
this.requestQueue.removeSubscriber(this.loadSubscriber, CHUNK_REQUEST_CANCEL_REASON);
Expand Down Expand Up @@ -599,7 +599,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
this.requestQueue.removeSubscriber(subscriber, CHUNK_REQUEST_CANCEL_REASON);
setTimeout(() => this.beginPrefetch(keys, level), 1000);
});
return Promise.resolve([updatedImageInfo, loadSpec]);
return Promise.resolve([updatedImageInfo, undefined]);
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/test/SubscribableRequestQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ describe("SubscribableRequestQueue", () => {
const id2 = queue.addSubscriber();
expect(id1).to.not.equal(id2);
});

it("never reuses ids of removed subscribers", () => {
const queue = new SubscribableRequestQueue();
const id1 = queue.addSubscriber();
const id2 = queue.addSubscriber();
queue.removeSubscriber(id1);
const id3 = queue.addSubscriber();
expect(id1).to.not.equal(id2);
expect(id3).to.not.equal(id2);
});
});

describe("addRequestToQueue", () => {
Expand Down Expand Up @@ -158,9 +168,11 @@ describe("SubscribableRequestQueue", () => {
expect(queue.hasRequest("test")).to.be.true;
expect(queue.isSubscribed(id, "test")).to.be.true;

queue.cancelRequest("test", id);
const cancelResult = queue.cancelRequest("test", id);
expect(cancelResult).to.be.true;
expect(queue.isSubscribed(id, "test")).to.be.false;
expect(await isRejected(promise)).to.be.true;
expect(queue.cancelRequest("test", id)).to.be.false;
});

it("does not cancel an underlying request if it is running", async () => {
Expand Down
Loading

0 comments on commit e1330a0

Please sign in to comment.