diff --git a/docs/api/grafer-points-data.md b/docs/api/grafer-points-data.md index 670b561..e722166 100644 --- a/docs/api/grafer-points-data.md +++ b/docs/api/grafer-points-data.md @@ -14,6 +14,7 @@ An array of point objects to be loaded into Grafer. The PointData property list | Property | Type | Description | | :--- | :--- | :--- | | id | string - *optional* | Name of the point. Will be used as an ID when referencing point in node, edge, and label data. Will default to its index in the PointData array if left out. | +| parentId | string - *optional* | ID of the point which this point is the child of. Used to enable relative positioning of points and relative radius. Will default to *No Parent* if left out. | x | number | X-Coordinate of the point. | | y | number | Y-Coordinate of the point. | | z | number - *optional* | Z-Coordinate of the point. Will default to 0 if left out. | @@ -27,7 +28,19 @@ Data [mappings](../guides/mappings.md) are used to compute properties at runtime | Property | Type | Description | | :--- | :--- | :--- | | id | (datum: PointData) => string - *optional* | | +| parentId | (datum: PointData) => string - *optional* | | | x | (datum: PointData) => number - *optional* | | | y | (datum: PointData) => number - *optional* | | | z | (datum: PointData) => number - *optional* | | | radius | (datum: PointData) => number - *optional* | | + +### `options` +###### { [key: string]: any } - *optional* + +An object containing configuration options for the points. + +| Property | Type | Description | +| :--- | :--- | :--- | +| positionHierarchyType | HierarchyTypes | Changes how point hierarchies changes the point positions. See [HierarchyTypes](./hierarchy-types.md) for more information. | +| radiusHierarchyType | HierarchyTypes | Changes how point hierarchies changes the point radius. See [HierarchyTypes](./hierarchy-types.md) for more information. | +| maxHierarchyDepth | number | Sets the maximum hierarchy depth that any point will have. Defaults to 100. | diff --git a/docs/api/hierarchy-types.md b/docs/api/hierarchy-types.md new file mode 100644 index 0000000..1fb84c6 --- /dev/null +++ b/docs/api/hierarchy-types.md @@ -0,0 +1,15 @@ +# HierarchyTypes + +An enum specifying how a points hierarchy influences a given point data property. + +
+ +## Properties + +### `NONE` + +The hierarchy has no effect on a given data property. + +### `ADD` + +The value of a given point data property is the sum of all the values associated with that data property up the hierarchy. This type can be used to allow for relative point positioning as an example. diff --git a/examples/src/basic/hierarchy.ts b/examples/src/basic/hierarchy.ts new file mode 100644 index 0000000..341085d --- /dev/null +++ b/examples/src/basic/hierarchy.ts @@ -0,0 +1,23 @@ +import {html, render} from 'lit-html'; +import {GraferController} from '../../../src/mod'; + +export async function hierarchy(container: HTMLElement): Promise { + render(html``, container); + const canvas = document.querySelector('.grafer_container') as HTMLCanvasElement; + + const points = { + data: [ + { id: 0, x: 0, y: 0 }, + { id: 1, x: 2, y: 0, parentId: 0 }, + { id: 2, x: 2, y: 0, parentId: 1 }, + ], + }; + const nodes = { + ...points, + mappings: { + point: (d: any): number => d.id, + }, + }; + + new GraferController(canvas, { points, layers: [{nodes}] }); +} diff --git a/examples/src/basic/mod.ts b/examples/src/basic/mod.ts index 72bdeb0..8232df7 100755 --- a/examples/src/basic/mod.ts +++ b/examples/src/basic/mod.ts @@ -4,3 +4,4 @@ export * from './nodeColors'; export * from './edgeColors'; export * from './nodeRadius'; export * from './nodeID'; +export * from './hierarchy'; diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index 0b2cb4f..722f47b 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -1,13 +1,25 @@ -import PicoGL, {App, Texture} from 'picogl'; -import testVS from './shaders/GraphPoints.test.vs.glsl'; -import testFS from './shaders/noop.fs.glsl'; -import {GLDataTypes} from '../renderer/Renderable'; -import {DataMappings, concatenateData, packData, printDataGL} from './DataTools'; +import PicoGL, {App, Framebuffer, Texture} from 'picogl'; +import pointsVS from './shaders/GraphPoints.vs.glsl'; +import pointsFS from './shaders/GraphPoints.fs.glsl'; +import {GLDataTypes, setDrawCallUniforms} from '../renderer/Renderable'; +import {DataMappings, concatenateData, packData} from './DataTools'; import {vec3} from 'gl-matrix'; import {DataTexture} from '../renderer/DataTexture'; +export enum HierarchyTypes { + NONE, + ADD, +} + +export interface PointOptions { + positionHierarchyType?: HierarchyTypes + radiusHierarchyType?: HierarchyTypes + maxHierarchyDepth?: number +} + export interface PointData { id?: number | string; + parentId?: number | string; x: number; y: number; z?: number; @@ -18,19 +30,24 @@ export type PointDataMappings = DataMappings; const kDefaultMappings: PointDataMappings = { id: (entry: any, i) => 'id' in entry ? entry.id : i, + parentId: (entry: any) => entry.parentId ?? null, x: (entry: any) => entry.x, y: (entry: any) => entry.y, z: (entry: any) => 'z' in entry ? entry.z : 0.0, radius: (entry: any) => 'radius' in entry ? entry.radius : 0.0, }; -const kGLTypes: GLDataTypes = { +const kGLTypesPoint: GLDataTypes = { x: PicoGL.FLOAT, y: PicoGL.FLOAT, z: PicoGL.FLOAT, radius: PicoGL.FLOAT, }; +const kGLTypesClass: GLDataTypes = { + parentId: PicoGL.INT, +}; + export class GraphPoints extends DataTexture { public static createGraphFromNodes(context: App, nodes: unknown[][], mappings: Partial = {}): R { let pointIndex = 0; @@ -42,21 +59,48 @@ export class GraphPoints extends DataTexture { return (new this(context, points)); } - private _dataBuffer: ArrayBuffer; - public get dataBuffer(): ArrayBuffer { - return this._dataBuffer; - } + private _frameBuffer: Framebuffer; - private _dataView: DataView; - public get dataView(): DataView { - return this._dataView; - } + private _parentBuffer: ArrayBuffer; + private _parentView: DataView; + private _parentTexture: Texture; + private _pointBuffer: ArrayBuffer; + private _pointView: DataView; + private _pointTexture: Texture; + + private _localUniforms = { + uPositionHierarchyType: HierarchyTypes.ADD, + uRadiusHierarchyType: HierarchyTypes.NONE, + uMaxHierarchyDepth: 100, + }; + private _dataArrayBuffer: Float32Array; private _length: number = 0; public get length(): number { return this._length; } + public get positionHierarchyType(): HierarchyTypes { + return this._localUniforms.uPositionHierarchyType; + } + public set positionHierarchyType(value: HierarchyTypes) { + this._localUniforms.uPositionHierarchyType = value; + } + + public get radiusHierarchyType(): HierarchyTypes { + return this._localUniforms.uRadiusHierarchyType; + } + public set radiusHierarchyType(value: HierarchyTypes) { + this._localUniforms.uRadiusHierarchyType = value; + } + + public get maxHierarchyDepth(): number { + return this._localUniforms.uMaxHierarchyDepth; + } + public set maxHierarchyDepth(value: number) { + this._localUniforms.uMaxHierarchyDepth = value; + } + private map: Map; protected dirty: boolean = false; @@ -76,8 +120,13 @@ export class GraphPoints extends DataTexture { }; this.bbCenter = vec3.create(); - this._dataBuffer = this.packData(data, mappings, true, true); - this._dataView = new DataView(this._dataBuffer); + const [pointBuffer, parentBuffer] = this.packData(data, mappings, true, true); + + this._parentBuffer = parentBuffer; + this._parentView = new DataView(this._parentBuffer); + + this._pointBuffer = pointBuffer; + this._pointView = new DataView(this._pointBuffer); const diagonalVec = vec3.sub(vec3.create(), this.bb.max, this.bb.min); this.bbDiagonal = vec3.length(diagonalVec); @@ -87,22 +136,29 @@ export class GraphPoints extends DataTexture { // set the dirty flag so the texture is updated next time it is requested this.dirty = true; - - // this.testFeedback(context); } public destroy(): void { super.destroy(); this.map.clear(); - this._dataBuffer = null; + this._parentTexture.delete(); + this._pointTexture.delete(); + + this._parentBuffer = null; + this._pointBuffer = null; + this._dataArrayBuffer = null; this.map = null; } public update(): void { if (this.dirty) { - const float32 = new Float32Array(this._dataBuffer); - this._texture.data(float32); + const parentInt32 = new Int32Array(this._parentBuffer); + this._parentTexture.data(parentInt32); + const pointFloat32 = new Float32Array(this._pointBuffer); + this._pointTexture.data(pointFloat32); + + this.processData(this.context); } this.dirty = false; } @@ -111,28 +167,38 @@ export class GraphPoints extends DataTexture { return this.map.get(id); } - public getPointByIndex(index: number): [number, number, number, number] { + public getPointByIndex(index: number, isRelative = false): [number, number, number, number] { + if(isRelative) { + return [ + this._pointView.getFloat32(index * 16, true), + this._pointView.getFloat32(index * 16 + 4, true), + this._pointView.getFloat32(index * 16 + 8, true), + this._pointView.getFloat32(index * 16 + 12, true), + ]; + } return [ - this._dataView.getFloat32(index * 16, true), - this._dataView.getFloat32(index * 16 + 4, true), - this._dataView.getFloat32(index * 16 + 8, true), - this._dataView.getFloat32(index * 16 + 12, true), + this._dataArrayBuffer[index], + this._dataArrayBuffer[index + 1], + this._dataArrayBuffer[index + 2], + this._dataArrayBuffer[index + 3], ]; } - public getPointByID(id: number | string): [number, number, number, number] { - return this.getPointByIndex(this.getPointIndex(id)); + public getPointByID(id: number | string, isRelative = false): [number, number, number, number] { + return this.getPointByIndex(this.getPointIndex(id), isRelative); } public setPointByIndex(index: number, data: unknown, mappings: Partial = {}): void { - const setBuffer = this.packData([data], mappings, false, false); - const setView = new Float32Array(setBuffer); + const [pointBuffer, classBuffer] = this.packData([data], mappings, false, false); - const dataView = this._dataView; - dataView.setFloat32(index * 16, setView[0], true); - dataView.setFloat32(index * 16 + 4, setView[1], true); - dataView.setFloat32(index * 16 + 8, setView[2], true); - dataView.setFloat32(index * 16 + 12, setView[3], true); + const pointView = new Float32Array(pointBuffer); + this._pointView.setFloat32(index * 16, pointView[0], true); + this._pointView.setFloat32(index * 16 + 4, pointView[1], true); + this._pointView.setFloat32(index * 16 + 8, pointView[2], true); + this._pointView.setFloat32(index * 16 + 12, pointView[3], true); + + const parentView = new Int32Array(classBuffer); + this._parentView.setInt32(index * 4, parentView[0], true); this.dirty = true; } @@ -143,32 +209,65 @@ export class GraphPoints extends DataTexture { public addPoints(data: unknown[], mappings: Partial = {}): void { this.resizeTexture(this._length + data.length); + const [pointBuffer, parentBuffer] = this.packData(data, mappings, false, true); + + // create and populate points buffer + const pointBytes = new Uint32Array(pointBuffer); + const pointBytesOld = new Uint32Array(this._pointBuffer, 0, this._length * 4); + this._pointBuffer = new ArrayBuffer(this.capacity * 16); // 16 bytes for 4 floats + this._pointView = new DataView(this._pointBuffer); + const pointBytesMerge = new Uint32Array(this._pointBuffer); + pointBytesMerge.set(pointBytesOld); + pointBytesMerge.set(pointBytes, pointBytesOld.length); + + // create and populate parent buffer + const parentBytes = new Uint32Array(parentBuffer); + const parentBytesOld = new Uint32Array(this._parentBuffer, 0, this._length); + this._parentBuffer = new ArrayBuffer(this.capacity * 4); // 4 bytes for 1 float + this._parentView = new DataView(this._parentBuffer); + const parentBytesMerge = new Uint32Array(this._parentBuffer); + parentBytesMerge.set(parentBytesOld); + parentBytesMerge.set(parentBytes, parentBytesOld.length); - const mergeBuffer = new ArrayBuffer(this.capacity * 16); // 16 bytes for 4 floats - const mergeBytes = new Uint8Array(mergeBuffer); - - const dataBuffer = this.packData(data, mappings, false, true); - const dataBytes = new Uint8Array(dataBuffer); - const oldBytes = new Uint8Array(this._dataBuffer, 0, this._length * 16); - - mergeBytes.set(oldBytes); - mergeBytes.set(dataBytes, oldBytes.length); - - this._dataBuffer = mergeBuffer; - this._dataView = new DataView(this._dataBuffer); this._length += data.length; this.dirty = true; } protected createTexture(width: number, height: number): Texture { + this._frameBuffer = this.context.createFramebuffer(); return this.context.createTexture2D(width, height, { internalFormat: PicoGL.RGBA32F, }); } - protected packData(data: unknown[], mappings: Partial, potLength: boolean, addMapEntry: boolean): ArrayBuffer { + protected resizeTexture(capacity: number): void { + if (this.capacity < capacity) { + super.resizeTexture(capacity); + const [textureWidth, textureHeight] = this.textureSize; + + // resize / create parent texture + if (this._parentTexture) { + this._parentTexture.resize(textureWidth, textureHeight); + } else { + this._parentTexture = this.context.createTexture2D(textureWidth, textureHeight, { + internalFormat: PicoGL.R32I, + }); + } + + // resize / create point texture + if (this._pointTexture) { + this._pointTexture.resize(textureWidth, textureHeight); + } else { + this._pointTexture = this.context.createTexture2D(textureWidth, textureHeight, { + internalFormat: PicoGL.RGBA32F, + }); + } + } + } + + protected packData(data: unknown[], mappings: Partial, potLength: boolean, addMapEntry: boolean): [ArrayBuffer, ArrayBuffer] { const dataMappings: PointDataMappings = Object.assign({}, kDefaultMappings, mappings); - return packData(data, dataMappings, kGLTypes, potLength, (i, entry) => { + const pointData = packData(data, dataMappings, kGLTypesPoint, potLength, (i, entry) => { if(addMapEntry) this.map.set(entry.id, this._length + i); this.bb.min[0] = Math.min(this.bb.min[0], entry.x - entry.radius); @@ -179,34 +278,67 @@ export class GraphPoints extends DataTexture { this.bb.max[1] = Math.max(this.bb.max[1], entry.y + entry.radius); this.bb.max[2] = Math.max(this.bb.max[2], entry.z); }); + const parentData = packData(data, dataMappings, kGLTypesClass, potLength, (i, entry) => { + if(entry.parentId === null) { + entry.parentId = -1; + } else { + entry.parentId = this.map.get(entry.parentId) ?? -1; + } + }); + + return [pointData, parentData]; } - private testFeedback(context: App): void { - const program = context.createProgram(testVS, testFS, { transformFeedbackVaryings: [ 'vPosition', 'vRadius', 'vYolo' ], transformFeedbackMode: PicoGL.INTERLEAVED_ATTRIBS }); - const pointsTarget = context.createVertexBuffer(PicoGL.FLOAT, 4, 40); - const pointsIndices = context.createVertexBuffer(PicoGL.UNSIGNED_BYTE, 1, new Uint8Array([ - 0, - 1, - 2, - 3, - 4, - 5, - ])); + protected processData(context: App): void { + const {gl} = context; - const transformFeedback = context.createTransformFeedback().feedbackBuffer(0, pointsTarget); - const vertexArray = context.createVertexArray().vertexAttributeBuffer(0, pointsIndices); + // resize viewport to data texture size and save original viewport size + const savedViewport = gl.getParameter(gl.VIEWPORT); + context.viewport(0, 0, ...this.textureSize as [number, number]); - const drawCall = context.createDrawCall(program, vertexArray).transformFeedback(transformFeedback); - drawCall.primitive(PicoGL.POINTS); - drawCall.texture('uDataTexture', this.texture); - context.enable(PicoGL.RASTERIZER_DISCARD); + // reset necessary context flags + this.context.disable(PicoGL.BLEND); + + // create program with single mesh covering clip space + const program = context.createProgram(pointsVS, pointsFS); + const verticesVBO = context.createVertexBuffer(PicoGL.FLOAT, 2, new Float32Array([ + -1, -1, + 1, -1, + -1, 1, + 1, 1, + ])); + const pointsVAO = context.createVertexArray() + .vertexAttributeBuffer(0, verticesVBO); + + // bind frame buffer to context + context.readFramebuffer(this._frameBuffer); + this._frameBuffer.colorTarget(0, this._texture); + context.drawFramebuffer(this._frameBuffer) + .clearColor(0, 0, 0, 0) + .clear() + .depthMask(false); + + // create and initiate draw call + const drawCall = context.createDrawCall(program, pointsVAO) + .primitive(PicoGL.TRIANGLE_STRIP); + setDrawCallUniforms(drawCall, Object.assign({}, this._localUniforms, { + uPointTexture: this._pointTexture, + uParentTexture: this._parentTexture, + })); drawCall.draw(); - context.disable(PicoGL.RASTERIZER_DISCARD); - printDataGL(context, pointsTarget, 6, { - position: [PicoGL.FLOAT, PicoGL.FLOAT, PicoGL.FLOAT], - radius: PicoGL.FLOAT, - yolo: PicoGL.FLOAT, + // read points texture into stored buffer for point coordinates readback + this.readTextureAsync(this._texture).then(texArrayBuffer => { + this._dataArrayBuffer = texArrayBuffer; }); + + // debug print out + // console.log(this.readTexture(this._pointTexture)); + // this._dataArrayBuffer = this.readTexture(this._frameBuffer.colorAttachments[0]); + // console.log(this._dataArrayBuffer); + + // switch back to canvas frame buffer and restore original viewport size + context.defaultDrawFramebuffer(); + context.viewport(...savedViewport as [number, number, number, number]); } } diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl new file mode 100644 index 0000000..3f2c3e7 --- /dev/null +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -0,0 +1,36 @@ +#version 300 es +precision highp float; +precision lowp isampler2D; + +in vec2 vUv; + +out vec4 fragColor; + +uniform sampler2D uPointTexture; +uniform isampler2D uParentTexture; + +uniform uint uPositionHierarchyType; +uniform uint uRadiusHierarchyType; +uniform uint uMaxHierarchyDepth; + +#pragma glslify: import(../../renderer/shaders/valueForIndex.glsl) +#pragma glslify: import(./hierarchyType.glsl) + +void main() { + vec2 texSize = vec2(textureSize(uPointTexture, 0).xy); + ivec2 coords = ivec2(vUv * texSize); + fragColor = texelFetch(uPointTexture, coords, 0); + + uint i = 0u; + int parentIndex = texelFetch(uParentTexture, coords, 0).x; + while(parentIndex != -1 && i++ < uMaxHierarchyDepth) { + vec4 point = valueForIndex(uPointTexture, parentIndex); + if(uPositionHierarchyType == MODE_ADD) { + fragColor.xyz += point.xyz; + } + if(uRadiusHierarchyType == MODE_ADD) { + fragColor.w += point.w; + } + parentIndex = ivalueForIndex(uParentTexture, parentIndex).x; + } +} \ No newline at end of file diff --git a/src/data/shaders/GraphPoints.test.vs.glsl b/src/data/shaders/GraphPoints.test.vs.glsl deleted file mode 100755 index af7045f..0000000 --- a/src/data/shaders/GraphPoints.test.vs.glsl +++ /dev/null @@ -1,23 +0,0 @@ -#version 300 es - -layout(location=0) in uint aIndex; - -uniform sampler2D uDataTexture; - -flat out vec3 vPosition; -flat out float vRadius; -flat out float vYolo; - -vec4 getValueByIndexFromTexture(sampler2D tex, int index) { - int texWidth = textureSize(tex, 0).x; - int col = index % texWidth; - int row = index / texWidth; - return texelFetch(tex, ivec2(col, row), 0); -} - -void main() { - vec4 value = getValueByIndexFromTexture(uDataTexture, int(aIndex)); - vPosition = value.xyz; - vRadius = value.w; - vYolo = value.w / 10.0; -} diff --git a/src/data/shaders/GraphPoints.vs.glsl b/src/data/shaders/GraphPoints.vs.glsl new file mode 100755 index 0000000..b5fdde8 --- /dev/null +++ b/src/data/shaders/GraphPoints.vs.glsl @@ -0,0 +1,10 @@ +#version 300 es + +layout(location=0) in vec4 aVertex; + +out vec2 vUv; + +void main() { + vUv = (aVertex.xy + 1.) / 2.; + gl_Position = aVertex; +} diff --git a/src/data/shaders/hierarchyType.glsl b/src/data/shaders/hierarchyType.glsl new file mode 100644 index 0000000..d54da11 --- /dev/null +++ b/src/data/shaders/hierarchyType.glsl @@ -0,0 +1,2 @@ +#define MODE_NONE 0u +#define MODE_ADD 1u diff --git a/src/grafer/GraferController.ts b/src/grafer/GraferController.ts index c33b255..c692c66 100755 --- a/src/grafer/GraferController.ts +++ b/src/grafer/GraferController.ts @@ -1,5 +1,5 @@ import {Viewport, ViewportOptions} from '../renderer/Viewport'; -import {PointDataMappings} from '../data/GraphPoints'; +import {PointOptions, PointDataMappings} from '../data/GraphPoints'; import {nodes as GraphNodes, edges as GraphEdges, labels as GraphLabels, Graph} from '../graph/mod'; import {Layer} from '../graph/Layer'; import {DragTruck} from '../UX/mouse/drag/DragTruck'; @@ -31,6 +31,7 @@ export type GraferLabelsType = keyof typeof GraphLabels.types; export interface GraferDataInput { data: unknown[], mappings?: Partial, + options?: PointOptions, } export type GraferPointsData = GraferDataInput; @@ -413,6 +414,16 @@ export class GraferController extends EventEmitter { const mappings = Object.assign({}, pointsRadiusMapping, data.points.mappings); this._viewport.graph = new Graph(this._viewport.context, data.points.data, mappings); this._viewport.graph.picking = new PickingManager(this._viewport.context, this._viewport.mouseHandler); + + if ('options' in data.points) { + const options = data.points.options; + const keys = Object.keys(options); + for (const key of keys) { + if (key in this._viewport.graph) { + this._viewport.graph[key] = options[key]; + } + } + } } } diff --git a/src/graph/LayerRenderable.ts b/src/graph/LayerRenderable.ts index e4a2dd7..21fb1a8 100755 --- a/src/graph/LayerRenderable.ts +++ b/src/graph/LayerRenderable.ts @@ -73,7 +73,10 @@ export abstract class LayerRenderable extends PointsReaderEmitter< super(...args); } - public abstract render(context: App, mode: RenderMode, uniforms: RenderUniforms): void; + public render(context: App, mode: RenderMode, uniforms: RenderUniforms): void; + public render(context: App, mode: RenderMode): void { + this.configureRenderContext(context, mode); + } protected initialize(...args: any[]): void; protected initialize( diff --git a/src/graph/edges/bundle/ClusterBundle.ts b/src/graph/edges/bundle/ClusterBundle.ts index 21b4d2a..d3df03c 100755 --- a/src/graph/edges/bundle/ClusterBundle.ts +++ b/src/graph/edges/bundle/ClusterBundle.ts @@ -153,11 +153,11 @@ export class ClusterBundle extends Edges { } public render(context:App, mode: RenderMode, uniforms: RenderUniforms): void { - this.configureRenderContext(context, mode); + super.render(context, mode, uniforms); switch (mode) { case RenderMode.PICKING: diff --git a/src/graph/edges/path/CurvedPath.ts b/src/graph/edges/path/CurvedPath.ts index d3c1a92..acccd2d 100755 --- a/src/graph/edges/path/CurvedPath.ts +++ b/src/graph/edges/path/CurvedPath.ts @@ -155,11 +155,11 @@ export class CurvedPath extends Edges } public render(context:App, mode: RenderMode, uniforms: RenderUniforms): void { + super.render(context, mode, uniforms); + setDrawCallUniforms(this.drawCall, uniforms); setDrawCallUniforms(this.drawCall, this.localUniforms); - this.configureRenderContext(context, mode); - switch (mode) { case RenderMode.PICKING: setDrawCallUniforms(this.pickingDrawCall, uniforms); diff --git a/src/graph/edges/path/StraightPath.ts b/src/graph/edges/path/StraightPath.ts index 475d396..070c9b5 100644 --- a/src/graph/edges/path/StraightPath.ts +++ b/src/graph/edges/path/StraightPath.ts @@ -115,7 +115,7 @@ export class StraightPath extends Edges { } public render(context:App, mode: RenderMode, uniforms: RenderUniforms): void { - this.configureRenderContext(context, mode); + super.render(context, mode, uniforms); switch (mode) { case RenderMode.PICKING: diff --git a/src/graph/labels/point/PointLabel.ts b/src/graph/labels/point/PointLabel.ts index 5362f9a..27682c5 100755 --- a/src/graph/labels/point/PointLabel.ts +++ b/src/graph/labels/point/PointLabel.ts @@ -225,7 +225,7 @@ export class PointLabel extends Nodes { } public render(context: App, mode: RenderMode, uniforms: RenderUniforms): void { - this.configureRenderContext(context, mode); + super.render(context, mode, uniforms); switch (mode) { case RenderMode.PICKING: diff --git a/src/graph/nodes/circle/Circle.ts b/src/graph/nodes/circle/Circle.ts index 0a2272e..f541e06 100755 --- a/src/graph/nodes/circle/Circle.ts +++ b/src/graph/nodes/circle/Circle.ts @@ -104,7 +104,7 @@ export class Circle extends Nodes { } public render(context:App, mode: RenderMode, uniforms: RenderUniforms): void { - this.configureRenderContext(context, mode); + super.render(context, mode, uniforms); switch (mode) { case RenderMode.PICKING: diff --git a/src/renderer/DataTexture.ts b/src/renderer/DataTexture.ts index 4e64fc4..45bc21c 100644 --- a/src/renderer/DataTexture.ts +++ b/src/renderer/DataTexture.ts @@ -1,6 +1,56 @@ import {App, Texture} from 'picogl'; import {vec2} from 'gl-matrix'; +// utility functions to allow textures to be pulled off of gpu asynchronously +// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#use_non-blocking_async_data_readback +function clientWaitAsync(gl: WebGL2RenderingContext, sync: WebGLSync, flags: number, interval_ms: number): Promise { + return new Promise((resolve, reject) => { + function test(): void { + const res = gl.clientWaitSync(sync, flags, 0); + if (res === gl.WAIT_FAILED) { + reject(); + return; + } + if (res === gl.TIMEOUT_EXPIRED) { + setTimeout(test, interval_ms); + return; + } + resolve(); + } + test(); + }); +} +async function getBufferSubDataAsync( + gl: WebGL2RenderingContext, + target: number, + buffer: WebGLBuffer, + srcByteOffset: number, + dstBuffer: ArrayBufferView, + dstOffset?: number, + length?: number +): Promise { + const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); + gl.flush(); + + await clientWaitAsync(gl, sync, 0, 10); + gl.deleteSync(sync); + + gl.bindBuffer(target, buffer); + gl.getBufferSubData(target, srcByteOffset, dstBuffer, dstOffset, length); + gl.bindBuffer(target, null); +} +async function readPixelsAsync(gl: WebGL2RenderingContext, x: number, y: number, w: number, h: number, format: number, type: number, dest: ArrayBufferView): Promise { + const buf = gl.createBuffer(); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf); + gl.bufferData(gl.PIXEL_PACK_BUFFER, dest.byteLength, gl.STREAM_READ); + gl.readPixels(x, y, w, h, format, type, 0); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + + await getBufferSubDataAsync(gl, gl.PIXEL_PACK_BUFFER, buf, 0, dest); + + gl.deleteBuffer(buf); +} + export abstract class DataTexture { protected context: App; @@ -49,4 +99,50 @@ export abstract class DataTexture { } } } + + protected readTexture(texture: Texture): Float32Array { + const gl = texture.gl; + const [textureWidth, textureHeight] = this.textureSize; + const fbRead = gl.createFramebuffer(); + + // make this the current frame buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, fbRead); + + // attach the texture to the framebuffer. + gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, texture.texture, 0); + const canRead = gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE; + if(canRead) { + const buffer = new Float32Array(textureWidth * textureHeight * 4); + gl.readPixels(0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, buffer); + return buffer; + } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return new Float32Array(); + } + + protected async readTextureAsync(texture: Texture): Promise { + const gl = texture.gl; + const [textureWidth, textureHeight] = this.textureSize; + const fbRead = gl.createFramebuffer(); + + // make this the current frame buffer + gl.bindFramebuffer(gl.FRAMEBUFFER, fbRead); + + // attach the texture to the framebuffer. + gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, texture.texture, 0); + const canRead = gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE; + if(canRead) { + const buffer = new Float32Array(textureWidth * textureHeight * 4); + await readPixelsAsync(gl as WebGL2RenderingContext, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, buffer); + return buffer; + } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return new Float32Array(); + } } diff --git a/src/renderer/shaders/valueForIndex.glsl b/src/renderer/shaders/valueForIndex.glsl index 0a2d5ea..3bcc290 100755 --- a/src/renderer/shaders/valueForIndex.glsl +++ b/src/renderer/shaders/valueForIndex.glsl @@ -1,4 +1,5 @@ precision lowp usampler2D; +precision lowp isampler2D; vec4 valueForIndex(sampler2D tex, int index) { int texWidth = textureSize(tex, 0).x; @@ -21,4 +22,11 @@ uint uivalueForIndex(usampler2D tex, int index) { return texelFetch(tex, ivec2(col, row), 0)[0]; } +ivec4 ivalueForIndex(isampler2D tex, int index) { + int texWidth = textureSize(tex, 0).x; + int col = index % texWidth; + int row = index / texWidth; + return texelFetch(tex, ivec2(col, row), 0); +} + #pragma glslify: export(valueForIndex)