Skip to content

Commit 617ba93

Browse files
New: Add WebGL renderer (#109)
* New: Add WebGL renderer * New: Add naive WebGL force layout computation * New: Add WebGL improved node/shape geometry options * New: Add labels and node images * Fix: GPU drag behavior * Chore: Remove leftover comments * Fix: WebGL renderer style invalidating * Chore: Refactor code quality * Chore: Add WebGL example and docs
1 parent 01f3194 commit 617ba93

29 files changed

Lines changed: 3779 additions & 39 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ node_modules/
66

77
# IDEs and editors
88
.idea/
9+
.vscode/
910

1011
# Optional npm cache directory
1112
.npm

docs/view-default.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ interface IOrbViewSettings {
7171
radius: number;
7272
}
7373
};
74-
// For canvas rendering and events
74+
// For rendering and events
7575
render: {
76+
type: 'canvas' | 'webgl';
7677
devicePixelRatio: number | null;
7778
fps: number;
7879
minZoom: number;
@@ -319,6 +320,7 @@ Each layout type has its own option list you can tweak in order to change the la
319320
- `isSimulatingOnDataUpdate` - Whether to run simulation when graph data is updated. Enabled by default (`true`).
320321
- `isSimulatingOnSettingsUpdate` - Whether to re-run simulation when settings are updated. Enabled by default (`true`).
321322
- `isSimulatingOnUnstick` - Whether to re-run simulation when a node is unstuck. Enabled by default (`true`).
323+
- `useGPU` - Runs the force layout on the GPU (WebGL2) using a Barnes-Hut quadtree. Falls back to the CPU engine automatically if WebGL2 is unavailable. Disabled by default (`false`).
322324
- `alpha` - Fine-grained control over the simulation's annealing process.
323325
- `alpha` - Current alpha value. Default `1`.
324326
- `alphaMin` - Minimum alpha threshold below which simulation stops. Default `0.001`.
@@ -352,6 +354,13 @@ Each layout type has its own option list you can tweak in order to change the la
352354
Optional property `render` has several rendering options that you can tweak. Read more about them
353355
on [Styling guide](./styles.md).
354356

357+
#### Property `render.type`
358+
359+
Selects the rendering backend. `canvas` (default) draws with the HTML5 Canvas API, while `webgl`
360+
uses a WebGL2 renderer that offloads drawing to the GPU for better performance on large graphs.
361+
The backend is chosen when the view is created and cannot be changed afterwards — to switch, create
362+
a new view. See the `example-webgl-renderer.html` example for a live comparison.
363+
355364
#### Property `render.devicePixelRatio`
356365

357366
`devicePixelRatio` is useful when dealing with the difference between rendering on a standard
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<!DOCTYPE html>
2+
<html lang='en' type="module">
3+
<head>
4+
<base href=".">
5+
<meta charset='UTF-8'>
6+
<title>Orb | WebGL renderer & GPU layout</title>
7+
<script type="text/javascript" src="./orb.js"></script>
8+
</head>
9+
<style>
10+
html, body {
11+
height: 100%;
12+
margin: 0;
13+
}
14+
.controls {
15+
width: 70%;
16+
display: flex;
17+
gap: 1rem;
18+
flex-wrap: wrap;
19+
align-items: center;
20+
margin-bottom: 0.5rem;
21+
}
22+
.controls label {
23+
display: flex;
24+
gap: 0.35rem;
25+
align-items: center;
26+
}
27+
</style>
28+
<body>
29+
<div style="display: flex; flex-direction: column; align-items: center; height: 100%;">
30+
<h1>Example 9 - WebGL renderer & GPU layout</h1>
31+
<p style="width: 70%">
32+
Renders a generated graph and lets you switch between the Canvas and WebGL
33+
renderers and the CPU and GPU force-layout engines across different graph
34+
sizes. The GPU layout falls back to the CPU automatically if WebGL2 is
35+
unavailable.
36+
</p>
37+
38+
<div class="controls">
39+
<label>
40+
Renderer
41+
<select id="renderer">
42+
<option value="canvas">Canvas</option>
43+
<option value="webgl" selected>WebGL</option>
44+
</select>
45+
</label>
46+
<label>
47+
Layout engine
48+
<select id="engine">
49+
<option value="cpu">CPU</option>
50+
<option value="gpu" selected>GPU</option>
51+
</select>
52+
</label>
53+
<label>
54+
Nodes
55+
<select id="size">
56+
<option>100</option>
57+
<option>500</option>
58+
<option selected>2000</option>
59+
<option>5000</option>
60+
<option>10000</option>
61+
<option>20000</option>
62+
</select>
63+
</label>
64+
<button id="regenerate">Regenerate</button>
65+
</div>
66+
67+
<!--
68+
Make sure that your graph container has a defined width and height.
69+
Orb will expand to any available space, but won't be visible if it's parent container is collapsed.
70+
-->
71+
<div id='graph' style="flex: 1; width: 100%;"></div>
72+
</div>
73+
<script type="text/javascript">
74+
const container = document.getElementById('graph');
75+
const rendererSelect = document.getElementById('renderer');
76+
const engineSelect = document.getElementById('engine');
77+
const sizeSelect = document.getElementById('size');
78+
const regenerateButton = document.getElementById('regenerate');
79+
80+
// Generates a connected random graph: a spanning tree plus some extra edges.
81+
const generateGraph = (nodeCount) => {
82+
const nodes = [];
83+
const edges = [];
84+
for (let i = 0; i < nodeCount; i++) {
85+
nodes.push({ id: i });
86+
}
87+
let edgeId = 0;
88+
for (let i = 1; i < nodeCount; i++) {
89+
const target = Math.floor(Math.random() * i);
90+
edges.push({ id: edgeId++, start: i, end: target });
91+
}
92+
const extraEdges = Math.floor(nodeCount * 0.5);
93+
for (let i = 0; i < extraEdges; i++) {
94+
const start = Math.floor(Math.random() * nodeCount);
95+
const end = Math.floor(Math.random() * nodeCount);
96+
if (start !== end) {
97+
edges.push({ id: edgeId++, start, end });
98+
}
99+
}
100+
return { nodes, edges };
101+
};
102+
103+
let orb;
104+
105+
const build = () => {
106+
const type = rendererSelect.value;
107+
const useGPU = engineSelect.value === 'gpu';
108+
const nodeCount = Number(sizeSelect.value);
109+
110+
// The renderer type is fixed at construction time, so a switch rebuilds the view.
111+
if (orb) {
112+
orb.destroy();
113+
}
114+
orb = new Orb.OrbView(container, {
115+
render: { type },
116+
layout: { type: 'force', options: { useGPU } },
117+
});
118+
119+
orb.data.setDefaultStyle({
120+
getNodeStyle() {
121+
return {
122+
size: 6,
123+
color: '#1d5fa5',
124+
borderColor: '#0c447c',
125+
borderWidth: 0.5,
126+
};
127+
},
128+
getEdgeStyle() {
129+
return {
130+
width: 0.3,
131+
color: '#cccccc',
132+
};
133+
},
134+
});
135+
136+
orb.data.setup(generateGraph(nodeCount));
137+
orb.render(() => orb.recenter());
138+
};
139+
140+
rendererSelect.addEventListener('change', build);
141+
engineSelect.addEventListener('change', build);
142+
sizeSelect.addEventListener('change', build);
143+
regenerateButton.addEventListener('click', build);
144+
145+
build();
146+
</script>
147+
</body>
148+
</html>

examples/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,13 @@ <h1>Orb Examples</h1>
7777
Renders a simple graph with hierarchical layout (tree-like).
7878
</p>
7979
</div>
80+
<div>
81+
<a href="./example-webgl-renderer.html"><h3><li>Example 9 - WebGL renderer & GPU layout</li></h3></a>
82+
<p>
83+
Renders a generated graph and lets you switch between the Canvas and WebGL
84+
renderers and the CPU and GPU force-layout engines across different graph
85+
sizes.
86+
</p>
87+
</div>
8088
</body>
8189
</html>

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
{
2020
"name": "Toni Lastre",
2121
"url": "https://github.com/tonilastre"
22+
},
23+
{
24+
"name": "Oleksandr Ichenskyi",
25+
"url": "https://github.com/AlexIchenskiy"
2226
}
2327
],
2428
"author": "Memgraph Ltd. <contact@memgraph.com>",
@@ -109,4 +113,4 @@
109113
"main"
110114
]
111115
}
112-
}
116+
}

src/glsl.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
declare module '*.glsl' {
2+
const value: string;
3+
export default value;
4+
}
5+
6+
declare module '*.vert' {
7+
const value: string;
8+
export default value;
9+
}
10+
11+
declare module '*.frag' {
12+
const value: string;
13+
export default value;
14+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#version 300 es
2+
3+
precision highp float;
4+
5+
in vec2 vWorldPos;
6+
in vec2 vStart;
7+
in vec2 vEnd;
8+
in vec2 vControl;
9+
in float vHalfWidth;
10+
in float vLoopbackRadius;
11+
in float vArrowSize;
12+
in vec2 vArrowTip;
13+
in vec2 vArrowDir;
14+
in vec4 vColor;
15+
in vec4 vShadowColor;
16+
in float vShadowSize;
17+
in vec2 vShadowOffset;
18+
flat in int vEdgeType;
19+
20+
uniform bool uSimpleMode;
21+
22+
out vec4 fragColor;
23+
24+
float sdSegment(vec2 p, vec2 a, vec2 b) {
25+
vec2 pa = p - a;
26+
vec2 ba = b - a;
27+
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
28+
return length(pa - ba * h);
29+
}
30+
31+
float sdBezier(vec2 pos, vec2 A, vec2 B, vec2 C) {
32+
vec2 a = B - A;
33+
vec2 b = A - 2.0 * B + C;
34+
vec2 c = a * 2.0;
35+
vec2 d = A - pos;
36+
37+
float kk = 1.0 / max(dot(b, b), 0.0001);
38+
float kx = kk * dot(a, b);
39+
float ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
40+
float kz = kk * dot(d, a);
41+
42+
float p = ky - kx * kx;
43+
float q = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
44+
float p3 = p * p * p;
45+
float q2 = q * q;
46+
float h = q2 + 4.0 * p3;
47+
48+
float res;
49+
if (h >= 0.0) {
50+
h = sqrt(h);
51+
vec2 x = (vec2(h, -h) - q) / 2.0;
52+
vec2 uv = sign(x) * pow(abs(x), vec2(1.0 / 3.0));
53+
float t = clamp(uv.x + uv.y - kx, 0.0, 1.0);
54+
vec2 qo = d + (c + b * t) * t;
55+
res = dot(qo, qo);
56+
} else {
57+
float z = sqrt(-p);
58+
float v = acos(q / (p * z * 2.0)) / 3.0;
59+
float m = cos(v);
60+
float n = sin(v) * 1.732050808;
61+
vec3 t = clamp(vec3(m + m, -n - m, n - m) * z - kx, 0.0, 1.0);
62+
vec2 qx = d + (c + b * t.x) * t.x;
63+
float dx = dot(qx, qx);
64+
vec2 qy = d + (c + b * t.y) * t.y;
65+
float dy = dot(qy, qy);
66+
res = min(dx, dy);
67+
}
68+
69+
return sqrt(res);
70+
}
71+
72+
float sdArrow(vec2 p, vec2 tip, vec2 dir, float size) {
73+
if (size <= 0.0) return 1e6;
74+
75+
vec2 perp = vec2(-dir.y, dir.x);
76+
vec2 rel = p - tip;
77+
float along = dot(rel, -dir);
78+
float across = dot(rel, perp);
79+
80+
if (along < 0.0) return length(rel);
81+
if (along > size) {
82+
float hw = size * 0.4;
83+
float closest = clamp(across, -hw, hw);
84+
vec2 pt = tip - dir * size + perp * closest;
85+
return length(p - pt);
86+
}
87+
88+
float halfW = (along / size) * size * 0.4;
89+
float d = abs(across) - halfW;
90+
return d;
91+
}
92+
93+
void main() {
94+
if (uSimpleMode && vEdgeType == 0) {
95+
fragColor = vColor;
96+
return;
97+
}
98+
99+
float dist;
100+
if (vEdgeType == 0) {
101+
dist = sdSegment(vWorldPos, vStart, vEnd);
102+
} else if (vEdgeType == 1) {
103+
dist = sdBezier(vWorldPos, vStart, vControl, vEnd);
104+
} else {
105+
dist = abs(length(vWorldPos - vControl) - vLoopbackRadius);
106+
}
107+
108+
float edgeSdf = dist - vHalfWidth;
109+
float combinedSdf = edgeSdf;
110+
111+
if (vArrowSize > 0.0) {
112+
float arrowDist = sdArrow(vWorldPos, vArrowTip, vArrowDir, vArrowSize);
113+
combinedSdf = min(edgeSdf, arrowDist);
114+
}
115+
116+
float shadowAlpha = 0.0;
117+
if (vShadowSize > 0.0) {
118+
vec2 shadowPos = vWorldPos - vShadowOffset;
119+
float shadowDist;
120+
if (vEdgeType == 0) {
121+
shadowDist = sdSegment(shadowPos, vStart, vEnd);
122+
} else if (vEdgeType == 1) {
123+
shadowDist = sdBezier(shadowPos, vStart, vControl, vEnd);
124+
} else {
125+
shadowDist = abs(length(shadowPos - vControl) - vLoopbackRadius);
126+
}
127+
float shadowArrowDist = vArrowSize > 0.0
128+
? sdArrow(shadowPos, vArrowTip, vArrowDir, vArrowSize)
129+
: 1.0e6;
130+
float shadowCombined = min(shadowDist - vHalfWidth, shadowArrowDist);
131+
float t = max(shadowCombined, 0.0) / vShadowSize;
132+
shadowAlpha = exp(-t * t * 1.5) * 0.5 * vShadowColor.a;
133+
}
134+
135+
float aa = fwidth(combinedSdf);
136+
float edgeAlpha = 1.0 - smoothstep(-aa, aa, combinedSdf);
137+
vec4 edgeColor = vColor;
138+
edgeColor.a *= edgeAlpha;
139+
140+
float finalAlpha = edgeColor.a + shadowAlpha * (1.0 - edgeColor.a);
141+
142+
if (finalAlpha < 0.001) discard;
143+
144+
if (shadowAlpha > 0.0) {
145+
vec3 finalRGB = (edgeColor.rgb * edgeColor.a + vShadowColor.rgb * shadowAlpha * (1.0 - edgeColor.a)) / finalAlpha;
146+
fragColor = vec4(finalRGB, finalAlpha);
147+
} else {
148+
fragColor = edgeColor;
149+
}
150+
}

0 commit comments

Comments
 (0)