Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 10 additions & 18 deletions packages/@tissuumaps-core/src/assets/shaders/points.vert
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#version 300 es

// maximum number of objects
#define MAX_N_OBJECTS 256u
#define MAX_N_OBJECTS 2048u

// marker atlas configuration
#define MARKER_ATLAS_GRID_SIZE 4u
Expand All @@ -13,7 +13,7 @@
#define DISCARD gl_PointSize = 0.0; gl_Position = vec4(2.0, 2.0, 0.0, 1.0); v_color = vec4(0.0); v_marker = uvec3(0); return;

// uniforms
uniform float u_globalPointSizeFactor;
uniform float u_worldPointSizeFactor;
uniform mat3x2 u_worldToViewportMatrix;
uniform vec2 u_viewportSize; // in world units
uniform vec2 u_canvasSize; // in browser pixels
Expand All @@ -32,13 +32,12 @@ layout(std140) uniform ObjectsUBO {
// is aligned to 4N. Since mat2x4 has 2 columns, its
// base alignment is 2 * 4N = 8N = 32 bytes.
mat2x4 transposedDataToWorldMatrices[MAX_N_OBJECTS];
vec4 objectPointSizeFactors[MAX_N_OBJECTS / 4u];
};

// vertex attributes
layout(location = 0) in float a_x; // in data units
layout(location = 1) in float a_y; // in data units
layout(location = 2) in float a_size; // in data units
layout(location = 2) in float a_size; // in world units
layout(location = 3) in uint a_color; // packed 8-bit RGBA
layout(location = 4) in uint a_marker; // marker index
layout(location = 5) in uint a_object; // object index
Expand Down Expand Up @@ -71,41 +70,34 @@ void main() {
DISCARD;
}

// compute object-specific parameters
float objectPointSizeFactor = objectPointSizeFactors[a_object / 4u][a_object % 4u];
mat3x2 dataToWorldMatrix = mat3x2(transpose(transposedDataToWorldMatrices[a_object]));
float dataToWorldScale = (length(dataToWorldMatrix[0]) + length(dataToWorldMatrix[1])) / 2.0;
float canvasPixelRatio = dot(u_canvasSize / u_viewportSize, vec2(0.5));

// compute point size in device pixels and discard points with non-positive size
float worldPointSize = a_size * objectPointSizeFactor * u_globalPointSizeFactor * dataToWorldScale;
float canvasPixelRatio = dot(u_canvasSize / u_viewportSize, vec2(0.5));
float worldPointSize = a_size * u_worldPointSizeFactor;
float canvasPointSize = worldPointSize * canvasPixelRatio; // in browser pixels
float devicePointSize = canvasPointSize * u_devicePixelRatio; // in device pixels
if(devicePointSize <= 0.0) {
DISCARD;
}
gl_PointSize = devicePointSize;

// compute point position in normalized device coordinates (NDCs) and discard points outside the viewport
mat3x2 dataToWorldMatrix = mat3x2(transpose(transposedDataToWorldMatrices[a_object]));
vec2 worldPosition = dataToWorldMatrix * vec3(a_x, a_y, 1.0);
vec2 viewportPosition = u_worldToViewportMatrix * vec3(worldPosition, 1.0); // in [0, 1]
vec2 ndcPosition = (2.0 * viewportPosition - 1.0) * vec2(1.0, -1.0); // in [-1, 1], y flipped
vec2 ndcPointSize = 2.0 * worldPointSize / u_viewportSize;
if(ndcPosition.x + 0.5 * ndcPointSize.x < -1.0 || ndcPosition.x - 0.5 * ndcPointSize.x > 1.0 || ndcPosition.y + 0.5 * ndcPointSize.y < -1.0 || ndcPosition.y - 0.5 * ndcPointSize.y > 1.0) {
DISCARD;
}
gl_Position = vec4(ndcPosition, 0.0, 1.0);

// unpack color and discard fully transparent points
vec4 color = unpackColor(a_color);
if(color.a == 0.0) {
DISCARD;
}
v_color = color;

// get marker atlas coordinates
uvec3 marker = markerAtlasCoords(a_marker);

// set outputs
gl_PointSize = devicePointSize;
gl_Position = vec4(ndcPosition, 0.0, 1.0);
v_color = color;
v_marker = marker;
v_marker = markerAtlasCoords(a_marker);
}
59 changes: 46 additions & 13 deletions packages/@tissuumaps-core/src/controllers/WebGLPointsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@ import { deepEqual } from "fast-equals";
import markersUrl from "../assets/markers/markers.png?url";
import pointsFragmentShader from "../assets/shaders/points.frag?raw";
import pointsVertexShader from "../assets/shaders/points.vert?raw";
import {
getActiveConfigSource,
isConstantConfig,
isFromConfig,
isGroupByConfig,
} from "../model/configs";
import {
defaultPointColor,
defaultPointMarker,
defaultPointOpacity,
defaultPointSize,
defaultPointVisibility,
defaultSizeUnit,
} from "../model/constants";
import { type Layer } from "../model/layer";
import { type Points, type PointsLayerConfig } from "../model/points";
import {
type Color,
type CoordinateSpace,
type DrawOptions,
Marker,
type ValueMap,
Expand All @@ -27,7 +35,7 @@ import { WebGLUtils } from "../utils/WebGLUtils";
import { WebGLControllerBase } from "./WebGLControllerBase";

export class WebGLPointsController extends WebGLControllerBase {
private static readonly _maxNumObjects = 256; // see vertex shader
private static readonly _maxNumObjects = 2048; // see vertex shader
private static readonly _attribLocations = {
X: 0,
Y: 1,
Expand All @@ -42,7 +50,7 @@ export class WebGLPointsController extends WebGLControllerBase {

private readonly _program: WebGLProgram;
private readonly _uniformLocations: {
globalPointSizeFactor: WebGLUniformLocation;
worldPointSizeFactor: WebGLUniformLocation;
worldToViewportMatrix: WebGLUniformLocation;
viewportSize: WebGLUniformLocation;
canvasSize: WebGLUniformLocation;
Expand Down Expand Up @@ -76,10 +84,10 @@ export class WebGLPointsController extends WebGLControllerBase {
);
// get uniform locations
this._uniformLocations = {
globalPointSizeFactor: WebGLUtils.getUniformLocation(
worldPointSizeFactor: WebGLUtils.getUniformLocation(
this._gl,
this._program,
"u_globalPointSizeFactor",
"u_worldPointSizeFactor",
),
worldToViewportMatrix: WebGLUtils.getUniformLocation(
this._gl,
Expand Down Expand Up @@ -125,7 +133,7 @@ export class WebGLPointsController extends WebGLControllerBase {
this._gl,
this._gl.UNIFORM_BUFFER,
this._buffers.objectsUBO,
WebGLPointsController._maxNumObjects * 9 * Float32Array.BYTES_PER_ELEMENT,
WebGLPointsController._maxNumObjects * 8 * Float32Array.BYTES_PER_ELEMENT,
this._gl.DYNAMIC_DRAW,
);
// create and configure VAO
Expand Down Expand Up @@ -177,7 +185,7 @@ export class WebGLPointsController extends WebGLControllerBase {
this._buffers.object,
WebGLPointsController._attribLocations.OBJECT,
1,
this._gl.UNSIGNED_BYTE,
this._gl.UNSIGNED_SHORT,
);
this._gl.bindVertexArray(null);
}
Expand Down Expand Up @@ -262,7 +270,7 @@ export class WebGLPointsController extends WebGLControllerBase {
WebGLPointsController._bindingPoints.OBJECTS_UBO,
);
this._gl.uniform1f(
this._uniformLocations.globalPointSizeFactor,
this._uniformLocations.worldPointSizeFactor,
drawOptions.pointSizeFactor,
);
this._gl.uniformMatrix3x2fv(
Expand Down Expand Up @@ -347,7 +355,7 @@ export class WebGLPointsController extends WebGLControllerBase {
this._gl,
this._gl.ARRAY_BUFFER,
this._buffers.object,
n * Uint8Array.BYTES_PER_ELEMENT,
n * Uint16Array.BYTES_PER_ELEMENT,
this._gl.STATIC_DRAW,
);
this._currentBufferSize = n;
Expand Down Expand Up @@ -415,7 +423,7 @@ export class WebGLPointsController extends WebGLControllerBase {
signal?.throwIfAborted();
let offset = 0;
const objectsUBOData = new Float32Array(
WebGLPointsController._maxNumObjects * 9,
WebGLPointsController._maxNumObjects * 8,
);
const newBufferSliceStates: PointsBufferSliceState[] = [];
for (let i = 0; i < refs.length; i++) {
Expand Down Expand Up @@ -494,13 +502,40 @@ export class WebGLPointsController extends WebGLControllerBase {
ref.points.pointSize,
)
) {
let activeUnit: CoordinateSpace;
const activeSource = getActiveConfigSource(ref.points.pointSize);
if (
activeSource === "constant" &&
isConstantConfig(ref.points.pointSize)
) {
activeUnit = ref.points.pointSize.constant.unit ?? defaultSizeUnit;
} else if (
activeSource === "from" &&
isFromConfig(ref.points.pointSize)
) {
activeUnit = ref.points.pointSize.from.unit ?? defaultSizeUnit;
} else if (
activeSource === "groupBy" &&
isGroupByConfig(ref.points.pointSize)
) {
activeUnit = ref.points.pointSize.groupBy.unit ?? defaultSizeUnit;
} else {
activeUnit = defaultSizeUnit;
}
let sizeFactor = ref.points.pointSizeFactor * ref.layer.pointSizeFactor;
if (activeUnit === "data") {
sizeFactor *= ref.layerConfig.transform.scale;
}
if (activeUnit === "data" || activeUnit === "layer") {
sizeFactor *= ref.layer.transform.scale;
}
const sizeData = await LoadUtils.loadSizeData(
ref.data.getIndex(),
ref.points.pointSize,
sizeMaps,
defaultPointSize,
loadTable,
{ signal },
{ signal, sizeFactor },
);
signal?.throwIfAborted();
WebGLUtils.loadBuffer(
Expand Down Expand Up @@ -582,7 +617,7 @@ export class WebGLPointsController extends WebGLControllerBase {
this._gl,
this._gl.ARRAY_BUFFER,
this._buffers.object,
new Uint8Array(numPoints).fill(i),
new Uint16Array(numPoints).fill(i),
{ offset },
);
}
Expand Down Expand Up @@ -619,8 +654,6 @@ export class WebGLPointsController extends WebGLControllerBase {
),
i * 8,
);
objectsUBOData[WebGLPointsController._maxNumObjects * 8 + i] =
ref.points.pointSizeFactor * ref.layer.pointSizeFactor;
offset += numPoints;
}
WebGLUtils.loadBuffer(
Expand Down
80 changes: 50 additions & 30 deletions packages/@tissuumaps-core/src/model/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type Config<TSource extends string> = {
* If a source is explicitly prioritized using {@link Config.source}, that source is returned.
*
* Otherwise, the active source is determined by checking for the presence of configuration-specific fields in the following order:
* - {@link ValueConfig}
* - {@link ConstantConfig}
* - {@link FromConfig}
* - {@link GroupByConfig}
* - {@link RandomConfig}
Expand All @@ -32,8 +32,8 @@ export function getActiveConfigSource<TSource extends string>(
if (config.source !== undefined) {
return config.source;
}
if (isValueConfig(config)) {
return "value" as TSource;
if (isConstantConfig(config)) {
return "constant" as TSource;
}
if (isFromConfig(config)) {
return "from" as TSource;
Expand All @@ -47,23 +47,25 @@ export function getActiveConfigSource<TSource extends string>(
return undefined;
}

/** Configuration to use a single value */
export type ValueConfig<TValue> = Config<"value"> & {
/** Specification of a single value */
value: NonNullable<TValue>;
/** Configuration to use a constant value */
export type ConstantConfig<
TValue,
TConstantExtra = unknown,
> = Config<"constant"> & {
/** Specification of a constant value */
constant: { value: NonNullable<TValue> } & TConstantExtra;
};

/**
* Determines whether the given object is a {@link ValueConfig}
* Determines whether the given object is a {@link ConstantConfig}
*
* @param obj - The object to check
* @returns Whether the object is an (active) {@link ValueConfig}
* @returns Whether the object is an (active) {@link ConstantConfig}
*/
export function isValueConfig<TValue>(
export function isConstantConfig<TValue, TConstantExtra = unknown>(
obj: unknown,
): obj is ValueConfig<TValue> {
const valueConfig = obj as ValueConfig<TValue>;
return valueConfig.source === "value" || valueConfig.value !== undefined;
): obj is ConstantConfig<TValue, TConstantExtra> {
return (obj as ConstantConfig<TValue, TConstantExtra>).constant !== undefined;
}

/** Configuration to load values from a table column */
Expand Down Expand Up @@ -155,7 +157,7 @@ export function isRandomConfig<TRandom>(
* Table values correspond to marker indices (see {@link Marker})
*/
export type MarkerConfig =
| ValueConfig<Marker>
| ConstantConfig<Marker>
| FromConfig
| GroupByConfig<Marker>;

Expand All @@ -164,27 +166,45 @@ export type MarkerConfig =
*
* Table values correspond to sizes in the specified {@link SizeConfig.unit}
*/
export type SizeConfig = (
| ValueConfig<number>
| FromConfig
| GroupByConfig<number>
) & {
/**
* Coordinate space in which the sizes are specified
*
* @defaultValue {@link "./constants".defaultSizeUnit}
*/

unit?: CoordinateSpace;
};
export type SizeConfig =
| ConstantConfig<
number,
{
/**
* Coordinate space in which the size values are specified
*
* @defaultValue {@link "./constants".defaultSizeUnit}
*/
unit?: CoordinateSpace;
}
>
| FromConfig<{
/**
* Coordinate space in which the size values are specified
*
* @defaultValue {@link "./constants".defaultSizeUnit}
*/
unit?: CoordinateSpace;
}>
| GroupByConfig<
number,
{
/**
* Coordinate space in which the size values are specified
*
* @defaultValue {@link "./constants".defaultSizeUnit}
*/
unit?: CoordinateSpace;
}
>;

/**
* Color configuration
*
* Numerical table values are linearly mapped to colors in the specified {@link ColorConfig.from.palette} using the specified {@link ColorConfig.from.range}.
*/
export type ColorConfig =
| ValueConfig<Color>
| ConstantConfig<Color>
| FromConfig<{
/**
* Value range that is linearly mapped to {@link ColorConfig.from.palette}
Expand All @@ -210,7 +230,7 @@ export type ColorConfig =
* Numerical table values are interpreted as booleans, where `0` is `false` and any other value is `true`.
*/
export type VisibilityConfig =
| ValueConfig<boolean>
| ConstantConfig<boolean>
| FromConfig
| GroupByConfig<boolean>;

Expand All @@ -220,6 +240,6 @@ export type VisibilityConfig =
* Numerical table values are interpreted as opacities between `0` (fully transparent) and `1` (fully opaque).
*/
export type OpacityConfig =
| ValueConfig<number>
| ConstantConfig<number>
| FromConfig
| GroupByConfig<number>;
4 changes: 2 additions & 2 deletions packages/@tissuumaps-core/src/model/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import {
*/
export const labelsDefaults = {
labelColor: { random: { palette: defaultRandomLabelColorPalette } },
labelVisibility: { value: defaultLabelVisibility },
labelOpacity: { value: defaultLabelOpacity },
labelVisibility: { constant: { value: defaultLabelVisibility } },
labelOpacity: { constant: { value: defaultLabelOpacity } },
} as const satisfies Partial<RawLabels>;

/**
Expand Down
Loading
Loading