Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "merge" blend mode #6936

Merged
merged 25 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
71e511e
WIP add new blend mode
MichaelBuessemeyer Mar 15, 2023
3490baf
WIP add cover blend mode
MichaelBuessemeyer Mar 21, 2023
9710973
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Mar 21, 2023
3f871db
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 14, 2023
ce1fff2
fix uint24 rendering in merge mode
MichaelBuessemeyer Apr 14, 2023
c22b91c
remove unused imports
MichaelBuessemeyer Apr 14, 2023
4821ce5
rename to render mode to blend mode
MichaelBuessemeyer Apr 20, 2023
11020f5
improve in line comments
MichaelBuessemeyer Apr 20, 2023
1f5b217
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 20, 2023
0dbf947
decide whether a color is valid by alpha < 0.
MichaelBuessemeyer Apr 20, 2023
6b4da6a
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 27, 2023
820bd9d
multiple fixes in cover blend mode
MichaelBuessemeyer Apr 27, 2023
2ca4a81
fix reading blendmode from backend on inital load
MichaelBuessemeyer Apr 27, 2023
f9cdd4d
Merge branch 'master' into cover-blend-mode
MichaelBuessemeyer Apr 27, 2023
ebadcb8
refactoring & make changes ready for review
MichaelBuessemeyer Apr 28, 2023
134acf8
Merge branch 'cover-blend-mode' of github.com:scalableminds/webknosso…
MichaelBuessemeyer Apr 28, 2023
30b268f
add changelog entry
MichaelBuessemeyer Apr 28, 2023
f9fe4b9
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer Apr 28, 2023
777c923
add blend mode setting to docs
MichaelBuessemeyer Apr 28, 2023
fd9a454
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer May 4, 2023
a23718e
Apply suggestions from code review
MichaelBuessemeyer May 4, 2023
9ff0aa4
Merge branch 'cover-blend-mode' of github.com:scalableminds/webknosso…
MichaelBuessemeyer May 4, 2023
56e971a
refactor shader code for blending
MichaelBuessemeyer May 4, 2023
96cb1b2
Merge branch 'master' of github.com:scalableminds/webknossos into cov…
MichaelBuessemeyer May 4, 2023
c4df6c2
Merge branch 'master' into cover-blend-mode
MichaelBuessemeyer May 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released

### Added
- Added segment groups so that segments can be organized in a hierarchy (similar to skeletons). [#6966](https://github.com/scalableminds/webknossos/pull/6966)
- Added a new "cover" blend mode which renders the visible layers on top of each other. The new blend mode can be selected in the Data Rendering settings in the right settings tab. [#6936](https://github.com/scalableminds/webknossos/pull/6936)
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
- In addition to drag and drop, the selected tree(s) in the Skeleton tab can also be moved into another group by right-clicking the target group and selecting "Move selected tree(s) here". [#7005](https://github.com/scalableminds/webknossos/pull/7005)

### Changed
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/tracing_ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ Note, not all control/viewport settings are available in every annotation mode.
#### Data Rendering
- `Hardware Utilization`: Adjusts the quality level used for rendering data. Changing this setting influences how many data is downloaded from the server as well as how much pressure is put on the user's graphics card. Tune this value to your network connection and hardware power. After changing the setting, the page has to be refreshed.
- `Loading Strategy`: You can choose between two different loading strategies. When using "best quality first" it will take a bit longer until you see data, because the highest quality is loaded. Alternatively, "Progressive quality" can be chosen which will improve the quality progressively while loading. As a result, initial data will be visible faster, but it will take more time until the best quality is shown.
- `Blend Mode`: You can switch between two modes of blending the color layer. The default (Additive) simply sums up all color values of all visible layers. The cover mode renders all color layers on top of each other. Thus the top most layer covers the layers below. This blend mode is especially useful for datasets using multi modality layers. Here is an example for such a dataset by Bosch et al. [1]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it true that this only affects color layers? Segmentation layers are handled differently?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Segmentation layers are handled differently?

Right now, that's exactly the case. The segmentation layer color pattern is added on top of the color value computed for the color layers.

Imo including the segmentation layer in that blending calculation might make things more complicated because in case a person has a color layer with opacity 100%, this would mean that this person could not see the segmentation. As this is currently the case (one can always see the segmentation if there is one), this might be confusing. Additionally, including the segmentation layer in the blending might require more configuration changes from a user for a typical use case for the "cover" blend mode: First change the blend mode to cover, then activate / deactivate the layer the user wants to see and finally tweak the opacities for the color layers so that the segmentation is still visible in the desired way the user wants.
Leaving the segmentation layer out of the blend mode calculation potentially removes the last step of the steps described above.

But I am open for discussion on this. I'll try to schedule a small meeting for this :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: We already discussed a solution:

  1. Keep the segmentation layer special right now.
  2. Add another pr that enabled editing the layer order
  3. Add another pr that enables setting the blend mode for every single layer including the segmentation layer. The default for segmentation layer will be cover, the default for color layers will be additive.


|Additive Blend Mode | &nbsp;&nbsp;&nbsp;&nbsp;Cover Blend Mode &nbsp; &nbsp;|
philippotto marked this conversation as resolved.
Show resolved Hide resolved
|:-------------------------:|:-------------------------:|
|![](./images/blend-mode-example-additive-bosch-et-al.png)|![](./images/blend-mode-example-cover-bosch-et-al.png)|

- `4 Bit`: Toggles data download from the server using only 4 bit instead of 8 bit for each voxel. Use this to reduce the amount of necessary internet bandwidth for WEBKNOSSOS. Useful for showcasing data on the go over cellular networks, e.g 4G.
- `Interpolation`: When interpolation is enabled, bilinear filtering is applied while rendering pixels between two voxels. As a result, data may look "smoother" (or blurry when being zoomed in very far). Without interpolation, data may look more "crisp" (or pixelated when being zoomed in very far).
- `Render Missing Data Black`: If a dataset does not contain data at a specific position, WEBKNOSSOS can either render these voxels in "black" or it can try to render data from another magnification.
Expand All @@ -138,3 +144,7 @@ The status bar at the bottom of the screen serves three functions:
1. It shows context-sensitive mouse and keyboard control hints. Depending on your selected annotation tool amd any pressed modifier keys (Shift, CMD, CTRL, ALT, etc) it provides useful interaction hints and shortcuts.
2. It provides useful information based on your mouse positioning and which objects it hovers over. This includes the current mouse position in the dataset coordinate space, any segment ID that you hover over, and the currently rendered magnification level (MipMap image pyramid) used for displaying any data.
3. When working with skeletons, the active node and tree IDs are listed. Use the little pencil icon to select/mark a specific ID as active if required. For more on the active node ID, [see the section on skeleton annotations](./skeleton_annotation.md#nodes_and_trees).


##### References
[1] Bosch, C., Ackels, T., Pacureanu, A. et al. Functional and multiscale 3D structural investigation of brain tissue through correlative in vivo physiology, synchrotron microtomography and volume electron microscopy. Nat Commun 13, 2923 (2022). https://doi.org/10.1038/s41467-022-30199-6
3 changes: 3 additions & 0 deletions frontend/javascripts/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const settings: Partial<Record<keyof RecommendedConfiguration, string>> =
gpuMemoryFactor: "Hardware Utilization",
overwriteMode: "Volume Annotation Overwrite Mode",
useLegacyBindings: "Classic Controls",
blendMode: "Blend Mode",
renderWatermark: "Logo in Screenshots",
};
export const settingsTooltips: Partial<Record<keyof RecommendedConfiguration, string>> = {
Expand Down Expand Up @@ -73,6 +74,8 @@ export const settingsTooltips: Partial<Record<keyof RecommendedConfiguration, st
mouseRotateValue: "Rotation speed when using the mouse to drag the rotation.",
zoom: "Zoom in or out in the data viewports",
displayScalebars: "Show a scale in the lower-right corner of each viewport",
blendMode:
"Set the blend mode for the dataset. The additive mode (default) adds the data values of all layers. The cover mode only shows the data of the top most layer in the list.",
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
renderWatermark: "Show a WEBKNOSSOS logo in the lower-left corner of each screenshot.",
};
export const layerViewConfigurations: Partial<Record<keyof DatasetLayerConfiguration, string>> = {
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,8 @@ export enum LOG_LEVELS {
ERROR = "ERROR",
CRITICAL = "CRITICAL",
}

export enum BLEND_MODES {
Additive = "Additive",
Cover = "Cover",
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as THREE from "three";
import _ from "lodash";
import type { OrthoView, Vector3 } from "oxalis/constants";
import { BLEND_MODES, OrthoView, Vector3 } from "oxalis/constants";
import {
ViewModeValues,
OrthoViewValues,
Expand Down Expand Up @@ -216,6 +216,7 @@ class PlaneMaterialFactory {
activeCellIdLow: {
value: new THREE.Vector4(0, 0, 0, 0),
},
blendMode: { value: 1.0 },
};

const activeMagIndices = getActiveMagIndicesForLayers(Store.getState());
Expand Down Expand Up @@ -567,6 +568,19 @@ class PlaneMaterialFactory {
true,
),
);
this.storePropertyUnsubscribers.push(
listenToStoreProperty(
(storeState) => storeState.datasetConfiguration.blendMode,
(blendMode) => {
if (blendMode === BLEND_MODES.Additive) {
this.uniforms.blendMode.value = 1.0;
} else {
this.uniforms.blendMode.value = 0.0;
}
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
},
true,
),
);
this.storePropertyUnsubscribers.push(
listenToStoreProperty(
(storeState) => storeState.dataset.dataSource.dataLayers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function maybePadRgbData(src: Uint8Array | Float32Array, elementClass: ElementCl
tmpPaddingBuffer[idx++] = src[srcIdx++];
tmpPaddingBuffer[idx++] = src[srcIdx++];
tmpPaddingBuffer[idx++] = src[srcIdx++];
idx++;
tmpPaddingBuffer[idx++] = 255;
}

return tmpPaddingBuffer;
Expand Down
10 changes: 7 additions & 3 deletions frontend/javascripts/oxalis/shaders/filtering.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const getMaybeFilteredColor: ShaderModule = {
export const getMaybeFilteredColorOrFallback: ShaderModule = {
requirements: [getMaybeFilteredColor],
code: `
vec4 getMaybeFilteredColorOrFallback(
vec4[2] getMaybeFilteredColorOrFallback(
float layerIndex,
float d_texture_width,
float packingDegree,
Expand All @@ -113,14 +113,18 @@ export const getMaybeFilteredColorOrFallback: ShaderModule = {
vec4 fallbackColor,
bool supportsPrecomputedBucketAddress
) {
vec4[2] returnValue;
vec4 color = getMaybeFilteredColor(layerIndex, d_texture_width, packingDegree, worldPositionUVW, suppressBilinearFiltering, supportsPrecomputedBucketAddress);

vec4 usedFallbackColor = vec4(0.0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the semantics of this var is a simple boolean, right? in that case, maybe define a struct with a vec4 and a bool?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, I never knew that glsl has structs 🤯. Thanks for the input 🙏

if (color.a < 0.0) {
// Render gray for not-yet-existing data
color = fallbackColor;
usedFallbackColor = vec4(1.0);
}

return color;
returnValue[0] = color;
returnValue[1] = usedFallbackColor;
return returnValue;
}

vec4[2] getSegmentIdOrFallback(
Expand Down
39 changes: 31 additions & 8 deletions frontend/javascripts/oxalis/shaders/main_data_shaders.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ uniform vec3 globalPosition;
uniform vec3 activeSegmentPosition;
uniform float zoomValue;
uniform bool useBilinearFiltering;
uniform float blendMode;
uniform vec3 globalMousePosition;
uniform bool isMouseInCanvas;
uniform float brushSizeInPixel;
Expand Down Expand Up @@ -169,7 +170,7 @@ void main() {
gl_FragColor = vec4(bucketPosition, activeMagIdx) / 255.;
return;
}
vec3 data_color = vec3(0.0);
vec4 data_color = vec4(0.0);

<% _.each(segmentationLayerNames, function(segmentationName, layerIndex) { %>
vec4 <%= segmentationName%>_id_low = vec4(0.);
Expand All @@ -193,7 +194,7 @@ void main() {

vec3 transformedCoordUVW = transDim((<%= name %>_transform * vec4(transDim(worldCoordUVW), 1.0)).xyz);
if (!isOutsideOfBoundingBox(transformedCoordUVW)) {
color_value =
vec4[2] maybe_filtered_color_and_used_fallback =
getMaybeFilteredColorOrFallback(
<%= formatNumberAsGLSLFloat(layerIndex) %>,
<%= name %>_data_texture_width,
Expand All @@ -202,8 +203,11 @@ void main() {
false,
fallbackGray,
!<%= name %>_has_transform
).xyz;

);
float used_fallback = maybe_filtered_color_and_used_fallback[1].r;
// For uint24 the alpha channel is always 0.0 :/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh sorry, forgot to remove this comment. This is no longer true thanks to the changes in texture_bucket_manager.ts.

vec4 maybe_filtered_color = maybe_filtered_color_and_used_fallback[0];
color_value = maybe_filtered_color_and_used_fallback[0].rgb;
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
<% if (textureLayerInfos[name].packingDegree === 2.0) { %>
// Workaround for 16-bit color layers
color_value = vec3(color_value.g * 256.0 + color_value.r);
Expand All @@ -222,14 +226,33 @@ void main() {
color_value = abs(color_value - <%= name %>_is_inverted);
// Catch the case where max == min would causes a NaN value and use black as a fallback color.
color_value = mix(color_value, vec3(0.0), is_max_and_min_equal);
// Multiply with color and alpha for <%= name %>
data_color += color_value * <%= name %>_alpha * <%= name %>_color;
color_value = color_value * <%= name %>_alpha * <%= name %>_color;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelBuessemeyer can you give this entire code block a bit of love? variable naming could be more consistent and I'd also hope that the code for the different blend mode could be extracted into helper functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the different blend mode could be extracted into helper functions?

That is a very good suggestion: I added a new glsl module called blending and extracted the methods there. I also renamed all variable to snail case and all methods & struct names to camelCase. Sadly, the name is very inconsistent in the shaders and thus I couldn't find a clear pattern what naming scheme / style to use. I also discussed this shortly with daniel (the naming style)

// Marking the color as invalid by setting alpha to 0.0 if the fallback color has been used
// so that it does not cover other colors.
vec4 layer_color = vec4(color_value, used_fallback == 1.0 ? 0.0 : maybe_filtered_color.a * <%= name %>_alpha);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, I'd simply rename color_value to layer_value and then don't define a new variable (as it's not necessary to have both at the same time, I think).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that the calculates done with color_value are only for the rgb channels and not for the alpha channel. But the blending needs a color with an alpha channel. Thus the calculations on the color are done with vec3 color_value and from that a full vec4 layer_color is calculated and then used for the blending calculation.

If I would remove the layer_color and rename color_value to layer_value, I would still have to construct a vec4 combining the color_value with the alpha channel in the blending methods (now extracted into own methods). So the construction of an additional vec4 cannot be avoided imo.

// Additive blendMode == 1: Simply adding up all layer colors.
vec4 additive_color = data_color + layer_color;
// Cover blendMode == 0: Applying alpha blending to merge the layers where the top most layer has priority.
// See https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending for details.
float mixed_alpha_factor = (1.0 - data_color.a) * layer_color.a;
float mixed_alpha = mixed_alpha_factor + data_color.a;
vec3 cover_color_rgb = data_color.a * data_color.rgb + mixed_alpha_factor * layer_color.rgb;
// Catching edge case where mixed_alpha is 0.0 and therefore the cover_color would have nan values.
float is_mixed_alpha_zero = float(mixed_alpha == 0.0);
vec4 cover_color = vec4(cover_color_rgb / (mixed_alpha + is_mixed_alpha_zero), mixed_alpha);
cover_color = mix(cover_color, vec4(0.0), is_mixed_alpha_zero);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daniel-wer I diverged from the original approach with the if statement to guard against the division by zero. Instead I used a neat tick that was already used a few code lines above where a simple 1.0 is added to the potentially dangerous divisor in case the divisor (mixed_alpha) is equal to zero.

I simply saw how it was handled in the code lines above and think that it is even easier to ready than the if expression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sadly I cannot comment the lines I mean in the comment above. The trick is already applied in the lines 218 - 228.

// Do not overwrite data_color if the layer color is only the fallback color.
float is_current_color_valid = float(used_fallback != 1.0);
cover_color = mix(data_color, cover_color, is_current_color_valid);
// Choose color depending on blendMode
data_color = mix(cover_color, additive_color, float(blendMode == 1.0));
}
}
<% }) %>
data_color = clamp(data_color, 0.0, 1.0);
data_color.a = 1.0;

gl_FragColor = vec4(data_color, 1.0);
gl_FragColor = data_color;

<% if (hasSegmentation) { %>
<% _.each(segmentationLayerNames, function(segmentationName, layerIndex) { %>
Expand All @@ -246,7 +269,7 @@ void main() {
float hoverAlphaIncrement = isHoveredCell && <%= segmentationName%>_alpha > 0.0 ? 0.2 : 0.0;
float proofreadingAlphaIncrement = isActiveCell && isProofreading && <%= segmentationName%>_alpha > 0.0 ? 0.4 : 0.0;
gl_FragColor = vec4(mix(
data_color,
data_color.rgb,
convertCellIdToRGB(<%= segmentationName%>_id_high, <%= segmentationName%>_id_low),
<%= segmentationName%>_alpha + max(hoverAlphaIncrement, proofreadingAlphaIncrement)
), 1.0);
Expand Down
3 changes: 2 additions & 1 deletion frontend/javascripts/oxalis/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import type {
OrthoViewWithoutTD,
InterpolationMode,
} from "oxalis/constants";
import { ControlModeEnum } from "oxalis/constants";
import { BLEND_MODES, ControlModeEnum } from "oxalis/constants";
import type { Matrix4x4 } from "libs/mjs";
import type { SkeletonTracingStats } from "oxalis/model/accessors/skeletontracing_accessor";
import type { UpdateAction } from "oxalis/model/sagas/update_actions";
Expand Down Expand Up @@ -293,6 +293,7 @@ export type DatasetConfiguration = {
readonly renderMissingDataBlack: boolean;
readonly loadingStrategy: LoadingStrategy;
readonly segmentationPatternOpacity: number;
readonly blendMode: BLEND_MODES;
};
export type PartialDatasetConfiguration = Partial<
DatasetConfiguration & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { setZoomStepAction } from "oxalis/model/actions/flycam_actions";
import messages, { settingsTooltips, settings as settingsLabels } from "messages";
import { userSettings } from "types/schemas/user_settings.schema";
import type { ViewMode } from "oxalis/constants";
import Constants from "oxalis/constants";
import Constants, { BLEND_MODES } from "oxalis/constants";
import { api } from "oxalis/singletons";
import Toast from "libs/toast";
import { ExclamationCircleOutlined } from "@ant-design/icons";
Expand Down Expand Up @@ -311,6 +311,21 @@ class ControlsAndRenderingSettingsTab extends PureComponent<ControlsAndRendering
},
]}
/>
<DropdownSetting
label={<Tooltip title={settingsTooltips.blendMode}>{settingsLabels.blendMode}</Tooltip>}
value={this.props.datasetConfiguration.blendMode}
onChange={this.onChangeDataset.blendMode}
options={[
{
value: BLEND_MODES.Additive,
label: "Additive",
},
{
value: BLEND_MODES.Cover,
label: "Cover",
},
]}
/>
<SwitchSetting
label={<Tooltip title={settingsTooltips.fourBit}>{settingsLabels.fourBit}</Tooltip>}
value={this.props.datasetConfiguration.fourBit}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BLEND_MODES } from "oxalis/constants";
import { type DatasetLayerConfiguration, type DatasetConfiguration } from "oxalis/store";

export function getDefaultLayerViewConfiguration(
Expand Down Expand Up @@ -70,6 +71,7 @@ export const defaultDatasetViewConfigurationWithoutNull: DatasetConfiguration =
loadingStrategy: "PROGRESSIVE_QUALITY",
segmentationPatternOpacity: 40,
layers: {},
blendMode: BLEND_MODES.Additive,
};
export const defaultDatasetViewConfiguration = {
...defaultDatasetViewConfigurationWithoutNull,
Expand Down Expand Up @@ -99,6 +101,9 @@ export const baseDatasetViewConfiguration = {
minimum: 0,
maximum: 100,
},
blendMode: {
enum: Object.values(BLEND_MODES),
},
};
export const datasetViewConfiguration = {
...baseDatasetViewConfiguration,
Expand Down