From 69086c23c73444eef0a578e5173504f9b05dc61a Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Thu, 14 Sep 2023 14:01:23 -0400 Subject: [PATCH 01/12] feat: add class based relative points coords/radius --- src/data/GraphPoints.ts | 233 +++++++++++++++------- src/data/shaders/GraphPoints.fs.glsl | 37 ++++ src/data/shaders/GraphPoints.test.vs.glsl | 23 --- src/data/shaders/GraphPoints.vs.glsl | 10 + src/graph/LayerRenderable.ts | 12 +- src/graph/edges/bundle/ClusterBundle.ts | 4 +- src/graph/edges/gravity/Gravity.ts | 2 +- src/graph/edges/path/CurvedPath.ts | 4 +- src/graph/edges/path/StraightPath.ts | 2 +- src/graph/edges/straight/Straight.ts | 2 +- src/graph/labels/point/PointLabel.ts | 2 +- src/graph/nodes/circle/Circle.ts | 2 +- src/renderer/DataTexture.ts | 98 +++++++++ 13 files changed, 326 insertions(+), 105 deletions(-) create mode 100644 src/data/shaders/GraphPoints.fs.glsl delete mode 100755 src/data/shaders/GraphPoints.test.vs.glsl create mode 100755 src/data/shaders/GraphPoints.vs.glsl diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index 0b2cb4f..0fd0a7d 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -1,13 +1,14 @@ -import PicoGL, {App, Texture} from 'picogl'; -import testVS from './shaders/GraphPoints.test.vs.glsl'; -import testFS from './shaders/noop.fs.glsl'; +import PicoGL, {App, Framebuffer, Texture} from 'picogl'; +import pointsVS from './shaders/GraphPoints.vs.glsl'; +import pointsFS from './shaders/GraphPoints.fs.glsl'; import {GLDataTypes} from '../renderer/Renderable'; -import {DataMappings, concatenateData, packData, printDataGL} from './DataTools'; -import {vec3} from 'gl-matrix'; +import {DataMappings, concatenateData, packData} from './DataTools'; +import {vec2, vec3} from 'gl-matrix'; import {DataTexture} from '../renderer/DataTexture'; export interface PointData { id?: number | string; + class?: number | string; x: number; y: number; z?: number; @@ -18,6 +19,7 @@ export type PointDataMappings = DataMappings; const kDefaultMappings: PointDataMappings = { id: (entry: any, i) => 'id' in entry ? entry.id : i, + class: (entry: any) => entry.class ?? null, x: (entry: any) => entry.x, y: (entry: any) => entry.y, z: (entry: any) => 'z' in entry ? entry.z : 0.0, @@ -42,15 +44,17 @@ export class GraphPoints extends DataTexture { return (new this(context, points)); } - private _dataBuffer: ArrayBuffer; - public get dataBuffer(): ArrayBuffer { - return this._dataBuffer; - } + private _frameBuffer: Framebuffer; + private _colorTarget: Texture; - private _dataView: DataView; - public get dataView(): DataView { - return this._dataView; - } + private _classBuffer: ArrayBuffer; + private _classView: DataView; + private _classTexture: Texture; + private _pointBuffer: ArrayBuffer; + private _pointView: DataView; + private _pointTexture: Texture; + + private _dataArrayBuffer: Float32Array; private _length: number = 0; public get length(): number { @@ -76,8 +80,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, classBuffer] = this.packData(data, mappings, true, true); + + this._classBuffer = classBuffer; + this._classView = new DataView(this._classBuffer); + + 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 +96,25 @@ 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._classBuffer = null; + this._pointBuffer = null; this.map = null; } public update(): void { if (this.dirty) { - const float32 = new Float32Array(this._dataBuffer); - this._texture.data(float32); + const classInt32 = new Int32Array(this._classBuffer); + this._classTexture.data(classInt32); + const pointFloat32 = new Float32Array(this._pointBuffer); + this._pointTexture.data(pointFloat32); + + this._texture = this.processData(this.context); } this.dirty = false; } @@ -112,11 +124,12 @@ export class GraphPoints extends DataTexture { } public getPointByIndex(index: number): [number, number, number, number] { + let startIndex = index * 4; 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[startIndex++], + this._dataArrayBuffer[startIndex++], + this._dataArrayBuffer[startIndex++], + this._dataArrayBuffer[startIndex], ]; } @@ -125,14 +138,16 @@ export class GraphPoints extends DataTexture { } 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 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 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 setView = new Float32Array(classBuffer); + this._classView.setInt32(index * 4, setView[0], true); this.dirty = true; } @@ -143,32 +158,73 @@ export class GraphPoints extends DataTexture { public addPoints(data: unknown[], mappings: Partial = {}): void { this.resizeTexture(this._length + data.length); + const [pointBuffer, classBuffer] = 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 class buffer + const classBytes = new Uint32Array(classBuffer); + const classBytesOld = new Uint32Array(this._classBuffer, 0, this._length); + this._classBuffer = new ArrayBuffer(this.capacity * 4); // 4 bytes for 1 float + this._classView = new DataView(this._classBuffer); + const classBytesMerge = new Uint32Array(this._classBuffer); + classBytesMerge.set(classBytesOld); + classBytesMerge.set(classBytes, classBytesOld.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 { - return this.context.createTexture2D(width, height, { - internalFormat: PicoGL.RGBA32F, - }); + protected resizeTexture(capacity: number): void { + if (this.capacity < capacity) { + const textureWidth = Math.pow(2, Math.ceil(Math.log2(Math.ceil(Math.sqrt(capacity))))); + const textureHeight = Math.pow(2, Math.ceil(Math.log2(Math.ceil(capacity / textureWidth)))); + this.textureSize = vec2.fromValues(textureWidth, textureHeight); + + // resize / create class texture + if (this._classTexture) { + this._classTexture.resize(textureWidth, textureHeight); + } else { + this._classTexture = this.context.createTexture2D(textureWidth, textureHeight, { + internalFormat: PicoGL.R32I, + magFilter: PicoGL.NEAREST, + minFilter: PicoGL.NEAREST, + }); + } + + // resize / create point texture + if (this._pointTexture) { + this._pointTexture.resize(textureWidth, textureHeight); + } else { + this._pointTexture = this.context.createTexture2D(textureWidth, textureHeight, { + internalFormat: PicoGL.RGBA32F, + magFilter: PicoGL.NEAREST, + minFilter: PicoGL.NEAREST, + }); + } + + // resize / create color target and initialize frame buffer if needed + if (this._colorTarget) { + this._colorTarget.resize(textureWidth, textureHeight); + } else { + this._frameBuffer = this.context.createFramebuffer(); + this._colorTarget = this.context.createTexture2D(textureWidth, textureHeight, { + internalFormat: PicoGL.RGBA32F, + }); + } + } } - protected packData(data: unknown[], mappings: Partial, potLength: boolean, addMapEntry: boolean): ArrayBuffer { + 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, kGLTypes, 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 +235,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 classData = packData(data, dataMappings, { class: PicoGL.INT }, potLength, (i, entry) => { + if(entry.class === null) { + entry.class = -1; + } else { + entry.class = this.map.get(entry.class) ?? -1; + } + }); + + return [pointData, classData]; } - 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): Texture { + 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); - drawCall.draw(); - context.disable(PicoGL.RASTERIZER_DISCARD); + // reset necessary context flags + this.context.disable(PicoGL.BLEND); - printDataGL(context, pointsTarget, 6, { - position: [PicoGL.FLOAT, PicoGL.FLOAT, PicoGL.FLOAT], - radius: PicoGL.FLOAT, - yolo: PicoGL.FLOAT, + // 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._colorTarget); + context.drawFramebuffer(this._frameBuffer) + .clearColor(0, 0, 0, 0) + .clear() + .depthMask(false); + + // create and initiate draw call + context.createDrawCall(program, pointsVAO) + .primitive(PicoGL.TRIANGLE_STRIP) + .texture('uPointTexture', this._pointTexture) + .texture('uClassTexture', this._classTexture) + .draw(); + + // read points texture into stored buffer for point coordinates readback + this.readTextureAsync(this._frameBuffer.colorAttachments[0]).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]); + + return this._frameBuffer.colorAttachments[0]; } } diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl new file mode 100644 index 0000000..a9b1200 --- /dev/null +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -0,0 +1,37 @@ +#version 300 es +precision highp float; +precision highp isampler2D; + +in vec2 vUv; + +out vec4 fragColor; + +uniform sampler2D uPointTexture; +uniform isampler2D uClassTexture; + +vec4 valueForIndex(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); +} + +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); +} + +void main() { + vec2 texSize = vec2(textureSize(uPointTexture, 0).xy); + ivec2 coords = ivec2(vUv * texSize); + fragColor = texelFetch(uPointTexture, coords, 0); + + int i = 0; + int classIndex = texelFetch(uClassTexture, coords, 0).x; + while(classIndex != -1 && i++ < 500) { + fragColor += valueForIndex(uPointTexture, classIndex); + classIndex = ivalueForIndex(uClassTexture, classIndex).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/graph/LayerRenderable.ts b/src/graph/LayerRenderable.ts index e4a2dd7..0836db9 100755 --- a/src/graph/LayerRenderable.ts +++ b/src/graph/LayerRenderable.ts @@ -73,7 +73,11 @@ 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); + this.updatePoints(); + } protected initialize(...args: any[]): void; protected initialize( @@ -94,6 +98,12 @@ export abstract class LayerRenderable extends PointsReaderEmitter< super.initialize(context, points, data, mappings); } + protected updatePoints(): void { + if(this.dataTexture !== this.localUniforms.uGraphPoints) { + this.localUniforms.uGraphPoints = this.dataTexture; + } + } + protected configureRenderContext(context: App, renderMode: RenderMode): void { context.depthRange(this.nearDepth, this.farDepth); 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..e207713 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,52 @@ 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) { + gl.bindFramebuffer(gl.FRAMEBUFFER, fbRead); + const buffer = new Float32Array(textureWidth * textureHeight * 4); + gl.readPixels(0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, buffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + return buffer; + } + + 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) { + gl.bindFramebuffer(gl.FRAMEBUFFER, fbRead); + const buffer = new Float32Array(textureWidth * textureHeight * 4); + await readPixelsAsync(gl as WebGL2RenderingContext, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, buffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + return buffer; + } + + return new Float32Array(); + } } From d209c2074c23d00bfa7450af08ce377e235c9e7a Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Thu, 14 Sep 2023 14:26:27 -0400 Subject: [PATCH 02/12] feat: getPoint functions allow pulling from both relative points data and absolute points data --- src/data/GraphPoints.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index 0fd0a7d..b4a3027 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -123,18 +123,25 @@ export class GraphPoints extends DataTexture { return this.map.get(id); } - public getPointByIndex(index: number): [number, number, number, number] { - let startIndex = index * 4; + 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._dataArrayBuffer[startIndex++], - this._dataArrayBuffer[startIndex++], - this._dataArrayBuffer[startIndex++], - this._dataArrayBuffer[startIndex], + 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 { From b317eaa54604a7ffcecf2fc98af0ddd92a4aaa36 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Thu, 14 Sep 2023 14:55:55 -0400 Subject: [PATCH 03/12] fix: GraphPoints shaders use valueForIndex functions --- src/data/shaders/GraphPoints.fs.glsl | 16 ++-------------- src/renderer/shaders/valueForIndex.glsl | 8 ++++++++ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl index a9b1200..071f12c 100644 --- a/src/data/shaders/GraphPoints.fs.glsl +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -1,6 +1,6 @@ #version 300 es precision highp float; -precision highp isampler2D; +precision lowp isampler2D; in vec2 vUv; @@ -9,19 +9,7 @@ out vec4 fragColor; uniform sampler2D uPointTexture; uniform isampler2D uClassTexture; -vec4 valueForIndex(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); -} - -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: import(../../renderer/shaders/valueForIndex.glsl) void main() { vec2 texSize = vec2(textureSize(uPointTexture, 0).xy); 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) From 89e2f95c324f56275868ea242f61b94cb963ffdf Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Fri, 15 Sep 2023 11:33:13 -0400 Subject: [PATCH 04/12] fix: clean up --- src/data/GraphPoints.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index b4a3027..58fd203 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -26,13 +26,17 @@ const kDefaultMappings: PointDataMappings = { 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 = { + class: PicoGL.INT, +}; + export class GraphPoints extends DataTexture { public static createGraphFromNodes(context: App, nodes: unknown[][], mappings: Partial = {}): R { let pointIndex = 0; @@ -102,8 +106,13 @@ export class GraphPoints extends DataTexture { super.destroy(); this.map.clear(); + this._classTexture.delete(); + this._pointTexture.delete(); + this._colorTarget.delete(); + this._classBuffer = null; this._pointBuffer = null; + this._dataArrayBuffer = null; this.map = null; } @@ -153,8 +162,8 @@ export class GraphPoints extends DataTexture { this._pointView.setFloat32(index * 16 + 8, pointView[2], true); this._pointView.setFloat32(index * 16 + 12, pointView[3], true); - const setView = new Float32Array(classBuffer); - this._classView.setInt32(index * 4, setView[0], true); + const classView = new Float32Array(classBuffer); + this._classView.setInt32(index * 4, classView[0], true); this.dirty = true; } @@ -201,8 +210,6 @@ export class GraphPoints extends DataTexture { } else { this._classTexture = this.context.createTexture2D(textureWidth, textureHeight, { internalFormat: PicoGL.R32I, - magFilter: PicoGL.NEAREST, - minFilter: PicoGL.NEAREST, }); } @@ -212,8 +219,6 @@ export class GraphPoints extends DataTexture { } else { this._pointTexture = this.context.createTexture2D(textureWidth, textureHeight, { internalFormat: PicoGL.RGBA32F, - magFilter: PicoGL.NEAREST, - minFilter: PicoGL.NEAREST, }); } @@ -231,7 +236,7 @@ export class GraphPoints extends DataTexture { protected packData(data: unknown[], mappings: Partial, potLength: boolean, addMapEntry: boolean): [ArrayBuffer, ArrayBuffer] { const dataMappings: PointDataMappings = Object.assign({}, kDefaultMappings, mappings); - const pointData = 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); @@ -242,7 +247,7 @@ 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 classData = packData(data, dataMappings, { class: PicoGL.INT }, potLength, (i, entry) => { + const classData = packData(data, dataMappings, kGLTypesClass, potLength, (i, entry) => { if(entry.class === null) { entry.class = -1; } else { From 3c0339a07188d98c6fbfb0d0bf3494a07cb623c6 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Fri, 15 Sep 2023 14:54:35 -0400 Subject: [PATCH 05/12] feat: add option to change behaviour of point classes --- src/data/GraphPoints.ts | 42 ++++++++++++++++++++++++---- src/data/shaders/GraphPoints.fs.glsl | 12 +++++++- src/data/shaders/classMode.glsl | 2 ++ src/grafer/GraferController.ts | 13 ++++++++- 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/data/shaders/classMode.glsl diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index 58fd203..e4a1aa4 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -1,11 +1,21 @@ import PicoGL, {App, Framebuffer, Texture} from 'picogl'; import pointsVS from './shaders/GraphPoints.vs.glsl'; import pointsFS from './shaders/GraphPoints.fs.glsl'; -import {GLDataTypes} from '../renderer/Renderable'; +import {GLDataTypes, setDrawCallUniforms} from '../renderer/Renderable'; import {DataMappings, concatenateData, packData} from './DataTools'; import {vec2, vec3} from 'gl-matrix'; import {DataTexture} from '../renderer/DataTexture'; +export enum ClassModes { + NONE, + ADD, +} + +export interface PointOptions { + positionClassMode?: ClassModes + radiusClassMode?: ClassModes +} + export interface PointData { id?: number | string; class?: number | string; @@ -58,6 +68,10 @@ export class GraphPoints extends DataTexture { private _pointView: DataView; private _pointTexture: Texture; + private _localUniforms = { + uPositionClassMode: ClassModes.ADD, + uRadiusClassMode: ClassModes.NONE, + }; private _dataArrayBuffer: Float32Array; private _length: number = 0; @@ -65,6 +79,20 @@ export class GraphPoints extends DataTexture { return this._length; } + public get positionClassMode(): ClassModes { + return this._localUniforms.uPositionClassMode; + } + public set positionClassMode(value: ClassModes) { + this._localUniforms.uPositionClassMode = value; + } + + public get radiusClassMode(): ClassModes { + return this._localUniforms.uRadiusClassMode; + } + public set radiusClassMode(value: ClassModes) { + this._localUniforms.uRadiusClassMode = value; + } + private map: Map; protected dirty: boolean = false; @@ -288,11 +316,13 @@ export class GraphPoints extends DataTexture { .depthMask(false); // create and initiate draw call - context.createDrawCall(program, pointsVAO) - .primitive(PicoGL.TRIANGLE_STRIP) - .texture('uPointTexture', this._pointTexture) - .texture('uClassTexture', this._classTexture) - .draw(); + const drawCall = context.createDrawCall(program, pointsVAO) + .primitive(PicoGL.TRIANGLE_STRIP); + setDrawCallUniforms(drawCall, Object.assign({}, this._localUniforms, { + uPointTexture: this._pointTexture, + uClassTexture: this._classTexture, + })); + drawCall.draw(); // read points texture into stored buffer for point coordinates readback this.readTextureAsync(this._frameBuffer.colorAttachments[0]).then(texArrayBuffer => { diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl index 071f12c..3d65ad9 100644 --- a/src/data/shaders/GraphPoints.fs.glsl +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -9,7 +9,11 @@ out vec4 fragColor; uniform sampler2D uPointTexture; uniform isampler2D uClassTexture; +uniform uint uPositionClassMode; +uniform uint uRadiusClassMode; + #pragma glslify: import(../../renderer/shaders/valueForIndex.glsl) +#pragma glslify: import(./classMode.glsl) void main() { vec2 texSize = vec2(textureSize(uPointTexture, 0).xy); @@ -19,7 +23,13 @@ void main() { int i = 0; int classIndex = texelFetch(uClassTexture, coords, 0).x; while(classIndex != -1 && i++ < 500) { - fragColor += valueForIndex(uPointTexture, classIndex); + vec4 point = valueForIndex(uPointTexture, classIndex); + if(uPositionClassMode == MODE_ADD) { + fragColor.xyz += point.xyz; + } + if(uRadiusClassMode == MODE_ADD) { + fragColor.w += point.w; + } classIndex = ivalueForIndex(uClassTexture, classIndex).x; } } \ No newline at end of file diff --git a/src/data/shaders/classMode.glsl b/src/data/shaders/classMode.glsl new file mode 100644 index 0000000..d54da11 --- /dev/null +++ b/src/data/shaders/classMode.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]; + } + } + } } } From 6a3836f0a9f6745f78bac6a1b62589a28f803fea Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Mon, 18 Sep 2023 17:18:59 -0400 Subject: [PATCH 06/12] feat: add option to specify maximum point hierarchy depth --- src/data/GraphPoints.ts | 9 +++++++++ src/data/shaders/GraphPoints.fs.glsl | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index e4a1aa4..ad56b36 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -14,6 +14,7 @@ export enum ClassModes { export interface PointOptions { positionClassMode?: ClassModes radiusClassMode?: ClassModes + maxHierarchyDepth?: number } export interface PointData { @@ -71,6 +72,7 @@ export class GraphPoints extends DataTexture { private _localUniforms = { uPositionClassMode: ClassModes.ADD, uRadiusClassMode: ClassModes.NONE, + uMaxHierarchyDepth: 100, }; private _dataArrayBuffer: Float32Array; @@ -93,6 +95,13 @@ export class GraphPoints extends DataTexture { this._localUniforms.uRadiusClassMode = 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; diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl index 3d65ad9..f6d7113 100644 --- a/src/data/shaders/GraphPoints.fs.glsl +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -11,6 +11,7 @@ uniform isampler2D uClassTexture; uniform uint uPositionClassMode; uniform uint uRadiusClassMode; +uniform uint uMaxHierarchyDepth; #pragma glslify: import(../../renderer/shaders/valueForIndex.glsl) #pragma glslify: import(./classMode.glsl) @@ -20,9 +21,9 @@ void main() { ivec2 coords = ivec2(vUv * texSize); fragColor = texelFetch(uPointTexture, coords, 0); - int i = 0; + uint i = 0u; int classIndex = texelFetch(uClassTexture, coords, 0).x; - while(classIndex != -1 && i++ < 500) { + while(classIndex != -1 && i++ < uMaxHierarchyDepth) { vec4 point = valueForIndex(uPointTexture, classIndex); if(uPositionClassMode == MODE_ADD) { fragColor.xyz += point.xyz; From 93a745cfc95aecd2a443b0abdab32855beebb3cd Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Tue, 19 Sep 2023 12:13:19 -0400 Subject: [PATCH 07/12] fix: remove colortarget texture and use datatexture texture --- src/data/GraphPoints.ts | 34 +++++++++++++--------------------- src/graph/LayerRenderable.ts | 7 ------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index ad56b36..1725585 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -3,7 +3,7 @@ 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 {vec2, vec3} from 'gl-matrix'; +import {vec3} from 'gl-matrix'; import {DataTexture} from '../renderer/DataTexture'; export enum ClassModes { @@ -60,7 +60,6 @@ export class GraphPoints extends DataTexture { } private _frameBuffer: Framebuffer; - private _colorTarget: Texture; private _classBuffer: ArrayBuffer; private _classView: DataView; @@ -145,7 +144,6 @@ export class GraphPoints extends DataTexture { this._classTexture.delete(); this._pointTexture.delete(); - this._colorTarget.delete(); this._classBuffer = null; this._pointBuffer = null; @@ -160,7 +158,7 @@ export class GraphPoints extends DataTexture { const pointFloat32 = new Float32Array(this._pointBuffer); this._pointTexture.data(pointFloat32); - this._texture = this.processData(this.context); + this.processData(this.context); } this.dirty = false; } @@ -199,7 +197,7 @@ export class GraphPoints extends DataTexture { this._pointView.setFloat32(index * 16 + 8, pointView[2], true); this._pointView.setFloat32(index * 16 + 12, pointView[3], true); - const classView = new Float32Array(classBuffer); + const classView = new Int32Array(classBuffer); this._classView.setInt32(index * 4, classView[0], true); this.dirty = true; @@ -235,11 +233,17 @@ export class GraphPoints extends DataTexture { 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 resizeTexture(capacity: number): void { if (this.capacity < capacity) { - const textureWidth = Math.pow(2, Math.ceil(Math.log2(Math.ceil(Math.sqrt(capacity))))); - const textureHeight = Math.pow(2, Math.ceil(Math.log2(Math.ceil(capacity / textureWidth)))); - this.textureSize = vec2.fromValues(textureWidth, textureHeight); + super.resizeTexture(capacity); + const [textureWidth, textureHeight] = this.textureSize; // resize / create class texture if (this._classTexture) { @@ -258,16 +262,6 @@ export class GraphPoints extends DataTexture { internalFormat: PicoGL.RGBA32F, }); } - - // resize / create color target and initialize frame buffer if needed - if (this._colorTarget) { - this._colorTarget.resize(textureWidth, textureHeight); - } else { - this._frameBuffer = this.context.createFramebuffer(); - this._colorTarget = this.context.createTexture2D(textureWidth, textureHeight, { - internalFormat: PicoGL.RGBA32F, - }); - } } } @@ -318,7 +312,7 @@ export class GraphPoints extends DataTexture { // bind frame buffer to context context.readFramebuffer(this._frameBuffer); - this._frameBuffer.colorTarget(0, this._colorTarget); + this._frameBuffer.colorTarget(0, this._texture); context.drawFramebuffer(this._frameBuffer) .clearColor(0, 0, 0, 0) .clear() @@ -346,7 +340,5 @@ export class GraphPoints extends DataTexture { // switch back to canvas frame buffer and restore original viewport size context.defaultDrawFramebuffer(); context.viewport(...savedViewport as [number, number, number, number]); - - return this._frameBuffer.colorAttachments[0]; } } diff --git a/src/graph/LayerRenderable.ts b/src/graph/LayerRenderable.ts index 0836db9..21fb1a8 100755 --- a/src/graph/LayerRenderable.ts +++ b/src/graph/LayerRenderable.ts @@ -76,7 +76,6 @@ export abstract class LayerRenderable extends PointsReaderEmitter< public render(context: App, mode: RenderMode, uniforms: RenderUniforms): void; public render(context: App, mode: RenderMode): void { this.configureRenderContext(context, mode); - this.updatePoints(); } protected initialize(...args: any[]): void; @@ -98,12 +97,6 @@ export abstract class LayerRenderable extends PointsReaderEmitter< super.initialize(context, points, data, mappings); } - protected updatePoints(): void { - if(this.dataTexture !== this.localUniforms.uGraphPoints) { - this.localUniforms.uGraphPoints = this.dataTexture; - } - } - protected configureRenderContext(context: App, renderMode: RenderMode): void { context.depthRange(this.nearDepth, this.farDepth); From b92e6abce46ebd2327a1345f131219bc68f3293e Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Tue, 19 Sep 2023 12:46:49 -0400 Subject: [PATCH 08/12] fix: renamed class to hierarchy or parent --- src/data/GraphPoints.ts | 98 ++++++++++++++-------------- src/data/shaders/GraphPoints.fs.glsl | 14 ++-- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index 1725585..203e2bc 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -6,20 +6,20 @@ import {DataMappings, concatenateData, packData} from './DataTools'; import {vec3} from 'gl-matrix'; import {DataTexture} from '../renderer/DataTexture'; -export enum ClassModes { +export enum HierarchyTypes { NONE, ADD, } export interface PointOptions { - positionClassMode?: ClassModes - radiusClassMode?: ClassModes + positionClassMode?: HierarchyTypes + radiusClassMode?: HierarchyTypes maxHierarchyDepth?: number } export interface PointData { id?: number | string; - class?: number | string; + parentId?: number | string; x: number; y: number; z?: number; @@ -30,7 +30,7 @@ export type PointDataMappings = DataMappings; const kDefaultMappings: PointDataMappings = { id: (entry: any, i) => 'id' in entry ? entry.id : i, - class: (entry: any) => entry.class ?? null, + 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, @@ -45,7 +45,7 @@ const kGLTypesPoint: GLDataTypes = { }; const kGLTypesClass: GLDataTypes = { - class: PicoGL.INT, + parentId: PicoGL.INT, }; export class GraphPoints extends DataTexture { @@ -61,16 +61,16 @@ export class GraphPoints extends DataTexture { private _frameBuffer: Framebuffer; - private _classBuffer: ArrayBuffer; - private _classView: DataView; - private _classTexture: Texture; + private _parentBuffer: ArrayBuffer; + private _parentView: DataView; + private _parentTexture: Texture; private _pointBuffer: ArrayBuffer; private _pointView: DataView; private _pointTexture: Texture; private _localUniforms = { - uPositionClassMode: ClassModes.ADD, - uRadiusClassMode: ClassModes.NONE, + uPositionHierarchyType: HierarchyTypes.ADD, + uRadiusHierarchyType: HierarchyTypes.NONE, uMaxHierarchyDepth: 100, }; private _dataArrayBuffer: Float32Array; @@ -80,18 +80,18 @@ export class GraphPoints extends DataTexture { return this._length; } - public get positionClassMode(): ClassModes { - return this._localUniforms.uPositionClassMode; + public get positionHierarchyType(): HierarchyTypes { + return this._localUniforms.uPositionHierarchyType; } - public set positionClassMode(value: ClassModes) { - this._localUniforms.uPositionClassMode = value; + public set positionHierarchyType(value: HierarchyTypes) { + this._localUniforms.uPositionHierarchyType = value; } - public get radiusClassMode(): ClassModes { - return this._localUniforms.uRadiusClassMode; + public get radiusHierarchyType(): HierarchyTypes { + return this._localUniforms.uRadiusHierarchyType; } - public set radiusClassMode(value: ClassModes) { - this._localUniforms.uRadiusClassMode = value; + public set radiusHierarchyType(value: HierarchyTypes) { + this._localUniforms.uRadiusHierarchyType = value; } public get maxHierarchyDepth(): number { @@ -120,10 +120,10 @@ export class GraphPoints extends DataTexture { }; this.bbCenter = vec3.create(); - const [pointBuffer, classBuffer] = this.packData(data, mappings, true, true); + const [pointBuffer, parentBuffer] = this.packData(data, mappings, true, true); - this._classBuffer = classBuffer; - this._classView = new DataView(this._classBuffer); + this._parentBuffer = parentBuffer; + this._parentView = new DataView(this._parentBuffer); this._pointBuffer = pointBuffer; this._pointView = new DataView(this._pointBuffer); @@ -142,10 +142,10 @@ export class GraphPoints extends DataTexture { super.destroy(); this.map.clear(); - this._classTexture.delete(); + this._parentTexture.delete(); this._pointTexture.delete(); - this._classBuffer = null; + this._parentBuffer = null; this._pointBuffer = null; this._dataArrayBuffer = null; this.map = null; @@ -153,8 +153,8 @@ export class GraphPoints extends DataTexture { public update(): void { if (this.dirty) { - const classInt32 = new Int32Array(this._classBuffer); - this._classTexture.data(classInt32); + const parentInt32 = new Int32Array(this._parentBuffer); + this._parentTexture.data(parentInt32); const pointFloat32 = new Float32Array(this._pointBuffer); this._pointTexture.data(pointFloat32); @@ -197,8 +197,8 @@ export class GraphPoints extends DataTexture { this._pointView.setFloat32(index * 16 + 8, pointView[2], true); this._pointView.setFloat32(index * 16 + 12, pointView[3], true); - const classView = new Int32Array(classBuffer); - this._classView.setInt32(index * 4, classView[0], true); + const parentView = new Int32Array(classBuffer); + this._parentView.setInt32(index * 4, parentView[0], true); this.dirty = true; } @@ -209,7 +209,7 @@ export class GraphPoints extends DataTexture { public addPoints(data: unknown[], mappings: Partial = {}): void { this.resizeTexture(this._length + data.length); - const [pointBuffer, classBuffer] = this.packData(data, mappings, false, true); + const [pointBuffer, parentBuffer] = this.packData(data, mappings, false, true); // create and populate points buffer const pointBytes = new Uint32Array(pointBuffer); @@ -220,14 +220,14 @@ export class GraphPoints extends DataTexture { pointBytesMerge.set(pointBytesOld); pointBytesMerge.set(pointBytes, pointBytesOld.length); - // create and populate class buffer - const classBytes = new Uint32Array(classBuffer); - const classBytesOld = new Uint32Array(this._classBuffer, 0, this._length); - this._classBuffer = new ArrayBuffer(this.capacity * 4); // 4 bytes for 1 float - this._classView = new DataView(this._classBuffer); - const classBytesMerge = new Uint32Array(this._classBuffer); - classBytesMerge.set(classBytesOld); - classBytesMerge.set(classBytes, classBytesOld.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); this._length += data.length; this.dirty = true; @@ -245,11 +245,11 @@ export class GraphPoints extends DataTexture { super.resizeTexture(capacity); const [textureWidth, textureHeight] = this.textureSize; - // resize / create class texture - if (this._classTexture) { - this._classTexture.resize(textureWidth, textureHeight); + // resize / create parent texture + if (this._parentTexture) { + this._parentTexture.resize(textureWidth, textureHeight); } else { - this._classTexture = this.context.createTexture2D(textureWidth, textureHeight, { + this._parentTexture = this.context.createTexture2D(textureWidth, textureHeight, { internalFormat: PicoGL.R32I, }); } @@ -278,18 +278,18 @@ 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 classData = packData(data, dataMappings, kGLTypesClass, potLength, (i, entry) => { - if(entry.class === null) { - entry.class = -1; + const parentData = packData(data, dataMappings, kGLTypesClass, potLength, (i, entry) => { + if(entry.parentId === null) { + entry.parentId = -1; } else { - entry.class = this.map.get(entry.class) ?? -1; + entry.parentId = this.map.get(entry.parentId) ?? -1; } }); - return [pointData, classData]; + return [pointData, parentData]; } - protected processData(context: App): Texture { + protected processData(context: App): void { const {gl} = context; // resize viewport to data texture size and save original viewport size @@ -323,12 +323,12 @@ export class GraphPoints extends DataTexture { .primitive(PicoGL.TRIANGLE_STRIP); setDrawCallUniforms(drawCall, Object.assign({}, this._localUniforms, { uPointTexture: this._pointTexture, - uClassTexture: this._classTexture, + uParentTexture: this._parentTexture, })); drawCall.draw(); // read points texture into stored buffer for point coordinates readback - this.readTextureAsync(this._frameBuffer.colorAttachments[0]).then(texArrayBuffer => { + this.readTextureAsync(this._texture).then(texArrayBuffer => { this._dataArrayBuffer = texArrayBuffer; }); diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl index f6d7113..9138562 100644 --- a/src/data/shaders/GraphPoints.fs.glsl +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -7,10 +7,10 @@ in vec2 vUv; out vec4 fragColor; uniform sampler2D uPointTexture; -uniform isampler2D uClassTexture; +uniform isampler2D uParentTexture; -uniform uint uPositionClassMode; -uniform uint uRadiusClassMode; +uniform uint uPositionHierarchyType; +uniform uint uRadiusHierarchyType; uniform uint uMaxHierarchyDepth; #pragma glslify: import(../../renderer/shaders/valueForIndex.glsl) @@ -22,15 +22,15 @@ void main() { fragColor = texelFetch(uPointTexture, coords, 0); uint i = 0u; - int classIndex = texelFetch(uClassTexture, coords, 0).x; + int classIndex = texelFetch(uParentTexture, coords, 0).x; while(classIndex != -1 && i++ < uMaxHierarchyDepth) { vec4 point = valueForIndex(uPointTexture, classIndex); - if(uPositionClassMode == MODE_ADD) { + if(uPositionHierarchyType == MODE_ADD) { fragColor.xyz += point.xyz; } - if(uRadiusClassMode == MODE_ADD) { + if(uRadiusHierarchyType == MODE_ADD) { fragColor.w += point.w; } - classIndex = ivalueForIndex(uClassTexture, classIndex).x; + classIndex = ivalueForIndex(uParentTexture, classIndex).x; } } \ No newline at end of file From 2bf10a4a012467a80d388dfb6e44dcf0e51eb792 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Tue, 19 Sep 2023 13:45:01 -0400 Subject: [PATCH 09/12] feat: add documentation associated with relative points --- docs/api/grafer-points-data.md | 13 +++++++++++++ docs/api/hierarchy-types.md | 15 +++++++++++++++ src/data/GraphPoints.ts | 4 ++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 docs/api/hierarchy-types.md 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/src/data/GraphPoints.ts b/src/data/GraphPoints.ts index 203e2bc..722f47b 100755 --- a/src/data/GraphPoints.ts +++ b/src/data/GraphPoints.ts @@ -12,8 +12,8 @@ export enum HierarchyTypes { } export interface PointOptions { - positionClassMode?: HierarchyTypes - radiusClassMode?: HierarchyTypes + positionHierarchyType?: HierarchyTypes + radiusHierarchyType?: HierarchyTypes maxHierarchyDepth?: number } From 3f35255826e6e781d0f4ab3ae0cd10b268f02dae Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Tue, 19 Sep 2023 15:47:58 -0400 Subject: [PATCH 10/12] fix: remove redundant bind frame buffer call --- src/renderer/DataTexture.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/DataTexture.ts b/src/renderer/DataTexture.ts index e207713..45bc21c 100644 --- a/src/renderer/DataTexture.ts +++ b/src/renderer/DataTexture.ts @@ -114,12 +114,11 @@ export abstract class DataTexture { gl.TEXTURE_2D, texture.texture, 0); const canRead = gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE; if(canRead) { - gl.bindFramebuffer(gl.FRAMEBUFFER, fbRead); const buffer = new Float32Array(textureWidth * textureHeight * 4); gl.readPixels(0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, buffer); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); return buffer; } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); return new Float32Array(); } @@ -138,12 +137,11 @@ export abstract class DataTexture { gl.TEXTURE_2D, texture.texture, 0); const canRead = gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE; if(canRead) { - gl.bindFramebuffer(gl.FRAMEBUFFER, fbRead); const buffer = new Float32Array(textureWidth * textureHeight * 4); await readPixelsAsync(gl as WebGL2RenderingContext, 0, 0, textureWidth, textureHeight, gl.RGBA, gl.FLOAT, buffer); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); return buffer; } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); return new Float32Array(); } From 13b442c13d4f9ef1435c093c0f8a96f6dca9b133 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Fri, 22 Sep 2023 12:58:40 -0400 Subject: [PATCH 11/12] fix: pr comments --- src/data/shaders/GraphPoints.fs.glsl | 10 +++++----- .../shaders/{classMode.glsl => hierarchyType.glsl} | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename src/data/shaders/{classMode.glsl => hierarchyType.glsl} (100%) diff --git a/src/data/shaders/GraphPoints.fs.glsl b/src/data/shaders/GraphPoints.fs.glsl index 9138562..3f2c3e7 100644 --- a/src/data/shaders/GraphPoints.fs.glsl +++ b/src/data/shaders/GraphPoints.fs.glsl @@ -14,7 +14,7 @@ uniform uint uRadiusHierarchyType; uniform uint uMaxHierarchyDepth; #pragma glslify: import(../../renderer/shaders/valueForIndex.glsl) -#pragma glslify: import(./classMode.glsl) +#pragma glslify: import(./hierarchyType.glsl) void main() { vec2 texSize = vec2(textureSize(uPointTexture, 0).xy); @@ -22,15 +22,15 @@ void main() { fragColor = texelFetch(uPointTexture, coords, 0); uint i = 0u; - int classIndex = texelFetch(uParentTexture, coords, 0).x; - while(classIndex != -1 && i++ < uMaxHierarchyDepth) { - vec4 point = valueForIndex(uPointTexture, classIndex); + 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; } - classIndex = ivalueForIndex(uParentTexture, classIndex).x; + parentIndex = ivalueForIndex(uParentTexture, parentIndex).x; } } \ No newline at end of file diff --git a/src/data/shaders/classMode.glsl b/src/data/shaders/hierarchyType.glsl similarity index 100% rename from src/data/shaders/classMode.glsl rename to src/data/shaders/hierarchyType.glsl From abf69c976e4a2b4188cf5e5773261f1a25fe20c5 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Fri, 22 Sep 2023 13:10:46 -0400 Subject: [PATCH 12/12] feat: added small example of hierarchy usage --- examples/src/basic/hierarchy.ts | 23 +++++++++++++++++++++++ examples/src/basic/mod.ts | 1 + 2 files changed, 24 insertions(+) create mode 100644 examples/src/basic/hierarchy.ts 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';