d3-force-webgpu
Version:
Force-directed graph layout using velocity Verlet integration with WebGPU acceleration.
1,944 lines (1,639 loc) • 66 kB
JavaScript
// https://github.com/jamescarruthers/d3-force-webgpu v3.1.2 Copyright 2010-2021 Mike Bostock
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree'), require('d3-dispatch'), require('d3-timer')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree', 'd3-dispatch', 'd3-timer'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3));
}(this, (function (exports, d3Quadtree, d3Dispatch, d3Timer) { 'use strict';
function center(x, y) {
var nodes, strength = 1;
if (x == null) x = 0;
if (y == null) y = 0;
function force() {
var i,
n = nodes.length,
node,
sx = 0,
sy = 0;
for (i = 0; i < n; ++i) {
node = nodes[i], sx += node.x, sy += node.y;
}
for (sx = (sx / n - x) * strength, sy = (sy / n - y) * strength, i = 0; i < n; ++i) {
node = nodes[i], node.x -= sx, node.y -= sy;
}
}
force.initialize = function(_) {
nodes = _;
};
force.x = function(_) {
return arguments.length ? (x = +_, force) : x;
};
force.y = function(_) {
return arguments.length ? (y = +_, force) : y;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
return force;
}
function constant(x) {
return function() {
return x;
};
}
function jiggle(random) {
return (random() - 0.5) * 1e-6;
}
function x$2(d) {
return d.x + d.vx;
}
function y$2(d) {
return d.y + d.vy;
}
function collide(radius) {
var nodes,
radii,
random,
strength = 1,
iterations = 1;
if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);
function force() {
var i, n = nodes.length,
tree,
node,
xi,
yi,
ri,
ri2;
for (var k = 0; k < iterations; ++k) {
tree = d3Quadtree.quadtree(nodes, x$2, y$2).visitAfter(prepare);
for (i = 0; i < n; ++i) {
node = nodes[i];
ri = radii[node.index], ri2 = ri * ri;
xi = node.x + node.vx;
yi = node.y + node.vy;
tree.visit(apply);
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data, rj = quad.r, r = ri + rj;
if (data) {
if (data.index > node.index) {
var x = xi - data.x - data.vx,
y = yi - data.y - data.vy,
l = x * x + y * y;
if (l < r * r) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
l = (r - (l = Math.sqrt(l))) / l * strength;
node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
node.vy += (y *= l) * r;
data.vx -= x * (r = 1 - r);
data.vy -= y * r;
}
}
return;
}
return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
}
}
function prepare(quad) {
if (quad.data) return quad.r = radii[quad.data.index];
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r;
}
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
radii = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};
return force;
}
function index(d) {
return d.index;
}
function find(nodeById, nodeId) {
var node = nodeById.get(nodeId);
if (!node) throw new Error("node not found: " + nodeId);
return node;
}
function link(links) {
var id = index,
strength = defaultStrength,
strengths,
distance = constant(30),
distances,
nodes,
count,
bias,
random,
iterations = 1;
if (links == null) links = [];
function defaultStrength(link) {
return 1 / Math.min(count[link.source.index], count[link.target.index]);
}
function force(alpha) {
for (var k = 0, n = links.length; k < iterations; ++k) {
for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
link = links[i], source = link.source, target = link.target;
x = target.x + target.vx - source.x - source.vx || jiggle(random);
y = target.y + target.vy - source.y - source.vy || jiggle(random);
l = Math.sqrt(x * x + y * y);
l = (l - distances[i]) / l * alpha * strengths[i];
x *= l, y *= l;
target.vx -= x * (b = bias[i]);
target.vy -= y * b;
source.vx += x * (b = 1 - b);
source.vy += y * b;
}
}
}
function initialize() {
if (!nodes) return;
var i,
n = nodes.length,
m = links.length,
nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d])),
link;
for (i = 0, count = new Array(n); i < m; ++i) {
link = links[i], link.index = i;
if (typeof link.source !== "object") link.source = find(nodeById, link.source);
if (typeof link.target !== "object") link.target = find(nodeById, link.target);
count[link.source.index] = (count[link.source.index] || 0) + 1;
count[link.target.index] = (count[link.target.index] || 0) + 1;
}
for (i = 0, bias = new Array(m); i < m; ++i) {
link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
}
strengths = new Array(m), initializeStrength();
distances = new Array(m), initializeDistance();
}
function initializeStrength() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
strengths[i] = +strength(links[i], i, links);
}
}
function initializeDistance() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
distances[i] = +distance(links[i], i, links);
}
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.links = function(_) {
return arguments.length ? (links = _, initialize(), force) : links;
};
force.id = function(_) {
return arguments.length ? (id = _, force) : id;
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
};
force.distance = function(_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
};
return force;
}
// https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
const a = 1664525;
const c = 1013904223;
const m = 4294967296; // 2^32
function lcg() {
let s = 1;
return () => (s = (a * s + c) % m) / m;
}
function x$1(d) {
return d.x;
}
function y$1(d) {
return d.y;
}
var initialRadius$1 = 10,
initialAngle$1 = Math.PI * (3 - Math.sqrt(5));
function simulation(nodes) {
var simulation,
alpha = 1,
alphaMin = 0.001,
alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
alphaTarget = 0,
velocityDecay = 0.6,
forces = new Map(),
stepper = d3Timer.timer(step),
event = d3Dispatch.dispatch("tick", "end"),
random = lcg();
if (nodes == null) nodes = [];
function step() {
tick();
event.call("tick", simulation);
if (alpha < alphaMin) {
stepper.stop();
event.call("end", simulation);
}
}
function tick(iterations) {
var i, n = nodes.length, node;
if (iterations === undefined) iterations = 1;
for (var k = 0; k < iterations; ++k) {
alpha += (alphaTarget - alpha) * alphaDecay;
forces.forEach(function(force) {
force(alpha);
});
for (i = 0; i < n; ++i) {
node = nodes[i];
if (node.fx == null) node.x += node.vx *= velocityDecay;
else node.x = node.fx, node.vx = 0;
if (node.fy == null) node.y += node.vy *= velocityDecay;
else node.y = node.fy, node.vy = 0;
}
}
return simulation;
}
function initializeNodes() {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.index = i;
if (node.fx != null) node.x = node.fx;
if (node.fy != null) node.y = node.fy;
if (isNaN(node.x) || isNaN(node.y)) {
var radius = initialRadius$1 * Math.sqrt(0.5 + i), angle = i * initialAngle$1;
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
}
if (isNaN(node.vx) || isNaN(node.vy)) {
node.vx = node.vy = 0;
}
}
}
function initializeForce(force) {
if (force.initialize) force.initialize(nodes, random);
return force;
}
initializeNodes();
return simulation = {
tick: tick,
restart: function() {
return stepper.restart(step), simulation;
},
stop: function() {
return stepper.stop(), simulation;
},
nodes: function(_) {
return arguments.length ? (nodes = _, initializeNodes(), forces.forEach(initializeForce), simulation) : nodes;
},
alpha: function(_) {
return arguments.length ? (alpha = +_, simulation) : alpha;
},
alphaMin: function(_) {
return arguments.length ? (alphaMin = +_, simulation) : alphaMin;
},
alphaDecay: function(_) {
return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay;
},
alphaTarget: function(_) {
return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget;
},
velocityDecay: function(_) {
return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
},
randomSource: function(_) {
return arguments.length ? (random = _, forces.forEach(initializeForce), simulation) : random;
},
force: function(name, _) {
return arguments.length > 1 ? ((_ == null ? forces.delete(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
},
find: function(x, y, radius) {
var i = 0,
n = nodes.length,
dx,
dy,
d2,
node,
closest;
if (radius == null) radius = Infinity;
else radius *= radius;
for (i = 0; i < n; ++i) {
node = nodes[i];
dx = x - node.x;
dy = y - node.y;
d2 = dx * dx + dy * dy;
if (d2 < radius) closest = node, radius = d2;
}
return closest;
},
on: function(name, _) {
return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
}
};
}
function manyBody() {
var nodes,
node,
random,
alpha,
strength = constant(-30),
strengths,
distanceMin2 = 1,
distanceMax2 = Infinity,
theta2 = 0.81;
function force(_) {
var i, n = nodes.length, tree = d3Quadtree.quadtree(nodes, x$1, y$1).visitAfter(accumulate);
for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply);
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
strengths = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], strengths[node.index] = +strength(node, i, nodes);
}
function accumulate(quad) {
var strength = 0, q, c, weight = 0, x, y, i;
// For internal nodes, accumulate forces from child quadrants.
if (quad.length) {
for (x = y = i = 0; i < 4; ++i) {
if ((q = quad[i]) && (c = Math.abs(q.value))) {
strength += q.value, weight += c, x += c * q.x, y += c * q.y;
}
}
quad.x = x / weight;
quad.y = y / weight;
}
// For leaf nodes, accumulate forces from coincident quadrants.
else {
q = quad;
q.x = q.data.x;
q.y = q.data.y;
do strength += strengths[q.data.index];
while (q = q.next);
}
quad.value = strength;
}
function apply(quad, x1, _, x2) {
if (!quad.value) return true;
var x = quad.x - node.x,
y = quad.y - node.y,
w = x2 - x1,
l = x * x + y * y;
// Apply the Barnes-Hut approximation if possible.
// Limit forces for very close nodes; randomize direction if coincident.
if (w * w / theta2 < l) {
if (l < distanceMax2) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
node.vx += x * quad.value * alpha / l;
node.vy += y * quad.value * alpha / l;
}
return true;
}
// Otherwise, process points directly.
else if (quad.length || l >= distanceMax2) return;
// Limit forces for very close nodes; randomize direction if coincident.
if (quad.data !== node || quad.next) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
}
do if (quad.data !== node) {
w = strengths[quad.data.index] * alpha / l;
node.vx += x * w;
node.vy += y * w;
} while (quad = quad.next);
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.distanceMin = function(_) {
return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
};
force.distanceMax = function(_) {
return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
};
force.theta = function(_) {
return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
};
return force;
}
function radial(radius, x, y) {
var nodes,
strength = constant(0.1),
strengths,
radiuses;
if (typeof radius !== "function") radius = constant(+radius);
if (x == null) x = 0;
if (y == null) y = 0;
function force(alpha) {
for (var i = 0, n = nodes.length; i < n; ++i) {
var node = nodes[i],
dx = node.x - x || 1e-6,
dy = node.y - y || 1e-6,
r = Math.sqrt(dx * dx + dy * dy),
k = (radiuses[i] - r) * strengths[i] * alpha / r;
node.vx += dx * k;
node.vy += dy * k;
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
radiuses = new Array(n);
for (i = 0; i < n; ++i) {
radiuses[i] = +radius(nodes[i], i, nodes);
strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes);
}
}
force.initialize = function(_) {
nodes = _, initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};
force.x = function(_) {
return arguments.length ? (x = +_, force) : x;
};
force.y = function(_) {
return arguments.length ? (y = +_, force) : y;
};
return force;
}
function x(x) {
var strength = constant(0.1),
nodes,
strengths,
xz;
if (typeof x !== "function") x = constant(x == null ? 0 : +x);
function force(alpha) {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
xz = new Array(n);
for (i = 0; i < n; ++i) {
strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
}
}
force.initialize = function(_) {
nodes = _;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.x = function(_) {
return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x;
};
return force;
}
function y(y) {
var strength = constant(0.1),
nodes,
strengths,
yz;
if (typeof y !== "function") y = constant(y == null ? 0 : +y);
function force(alpha) {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha;
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
yz = new Array(n);
for (i = 0; i < n; ++i) {
strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
}
}
force.initialize = function(_) {
nodes = _;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.y = function(_) {
return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y;
};
return force;
}
// WebGPU Device Manager
// Handles device initialization, capability detection, and resource management
let gpuDevice = null;
let gpuAdapter = null;
let initPromise = null;
async function initWebGPU() {
if (gpuDevice) return gpuDevice;
if (initPromise) return initPromise;
initPromise = (async () => {
if (!navigator.gpu) {
throw new Error("WebGPU not supported in this browser");
}
gpuAdapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance"
});
if (!gpuAdapter) {
throw new Error("No WebGPU adapter found");
}
const requiredLimits = {
maxStorageBufferBindingSize: gpuAdapter.limits.maxStorageBufferBindingSize,
maxBufferSize: gpuAdapter.limits.maxBufferSize,
maxComputeWorkgroupsPerDimension: gpuAdapter.limits.maxComputeWorkgroupsPerDimension
};
gpuDevice = await gpuAdapter.requestDevice({
requiredLimits
});
gpuDevice.lost.then((info) => {
console.error("WebGPU device lost:", info.message);
gpuDevice = null;
gpuAdapter = null;
initPromise = null;
});
// Handle uncaptured errors
gpuDevice.onuncapturederror = (event) => {
console.error("WebGPU uncaptured error:", event.error);
};
return gpuDevice;
})();
return initPromise;
}
function isWebGPUAvailable() {
return typeof navigator !== "undefined" && !!navigator.gpu;
}
async function checkWebGPUSupport() {
if (!isWebGPUAvailable()) return false;
try {
const adapter = await navigator.gpu.requestAdapter();
return !!adapter;
} catch (e) {
return false;
}
}
// Utility to create a compute pipeline with error handling
async function createComputePipeline(device, shaderCode, entryPoint, bindGroupLayout) {
const shaderModule = device.createShaderModule({
code: shaderCode
});
const compilationInfo = await shaderModule.getCompilationInfo();
if (compilationInfo.messages.some(m => m.type === "error")) {
const errors = compilationInfo.messages.filter(m => m.type === "error");
throw new Error("Shader compilation failed: " + errors.map(e => e.message).join("\n"));
}
return device.createComputePipeline({
layout: bindGroupLayout ? device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }) : "auto",
compute: {
module: shaderModule,
entryPoint
}
});
}
// GPU Buffer Manager
// Handles creation and synchronization of GPU buffers for node/link data
class NodeBuffers {
constructor(device, nodeCount) {
this.device = device;
this.nodeCount = nodeCount;
this.isMapped = false;
// Each node has: x, y, vx, vy (4 floats = 16 bytes)
// We also need fx, fy for fixed positions (use NaN to indicate not fixed)
// And strength for many-body force
// Layout: [x, y, vx, vy, fx, fy, strength, radius] = 8 floats per node = 32 bytes
this.floatsPerNode = 8;
this.bytesPerNode = this.floatsPerNode * 4;
this.bufferSize = this.nodeCount * this.bytesPerNode;
// Main storage buffer (GPU read/write)
this.storageBuffer = device.createBuffer({
size: this.bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
label: "Node Storage Buffer"
});
// Staging buffer for reading back to CPU
this.stagingBuffer = device.createBuffer({
size: this.bufferSize,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
label: "Node Staging Buffer"
});
// CPU-side typed array for uploads
this.cpuData = new Float32Array(this.nodeCount * this.floatsPerNode);
}
// Upload node data from JS objects to GPU
uploadNodes(nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const offset = i * this.floatsPerNode;
this.cpuData[offset + 0] = node.x || 0;
this.cpuData[offset + 1] = node.y || 0;
this.cpuData[offset + 2] = node.vx || 0;
this.cpuData[offset + 3] = node.vy || 0;
this.cpuData[offset + 4] = node.fx != null ? node.fx : NaN;
this.cpuData[offset + 5] = node.fy != null ? node.fy : NaN;
this.cpuData[offset + 6] = node._strength != null ? node._strength : -30;
this.cpuData[offset + 7] = node.radius != null ? node.radius : 5;
}
this.device.queue.writeBuffer(this.storageBuffer, 0, this.cpuData);
}
// Read node positions back from GPU to JS objects
async downloadNodes(nodes) {
// Safety check - don't map if already mapped
if (this.isMapped) {
console.warn("Buffer already mapped, skipping download");
return;
}
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(this.storageBuffer, 0, this.stagingBuffer, 0, this.bufferSize);
this.device.queue.submit([commandEncoder.finish()]);
this.isMapped = true;
try {
await this.stagingBuffer.mapAsync(GPUMapMode.READ);
const data = new Float32Array(this.stagingBuffer.getMappedRange().slice(0));
this.stagingBuffer.unmap();
for (let i = 0; i < nodes.length; i++) {
const offset = i * this.floatsPerNode;
nodes[i].x = data[offset + 0];
nodes[i].y = data[offset + 1];
nodes[i].vx = data[offset + 2];
nodes[i].vy = data[offset + 3];
}
} finally {
this.isMapped = false;
}
}
// Update velocities only (after force computation)
async downloadVelocities(nodes) {
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(this.storageBuffer, 0, this.stagingBuffer, 0, this.bufferSize);
this.device.queue.submit([commandEncoder.finish()]);
await this.stagingBuffer.mapAsync(GPUMapMode.READ);
const data = new Float32Array(this.stagingBuffer.getMappedRange().slice(0));
this.stagingBuffer.unmap();
for (let i = 0; i < nodes.length; i++) {
const offset = i * this.floatsPerNode;
nodes[i].vx = data[offset + 2];
nodes[i].vy = data[offset + 3];
}
}
destroy() {
this.storageBuffer.destroy();
this.stagingBuffer.destroy();
}
}
class LinkBuffers {
constructor(device, linkCount) {
this.device = device;
this.linkCount = linkCount;
// Each link has: sourceIndex, targetIndex, distance, strength, bias (5 values)
// Use 8 floats for alignment: [sourceIdx, targetIdx, distance, strength, bias, pad, pad, pad]
this.floatsPerLink = 8;
this.bytesPerLink = this.floatsPerLink * 4;
this.bufferSize = Math.max(32, this.linkCount * this.bytesPerLink);
this.storageBuffer = device.createBuffer({
size: this.bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
label: "Link Storage Buffer"
});
this.cpuData = new Float32Array(this.linkCount * this.floatsPerLink);
}
uploadLinks(links, distances, strengths, bias) {
for (let i = 0; i < links.length; i++) {
const link = links[i];
const offset = i * this.floatsPerLink;
this.cpuData[offset + 0] = link.source.index;
this.cpuData[offset + 1] = link.target.index;
this.cpuData[offset + 2] = distances[i];
this.cpuData[offset + 3] = strengths[i];
this.cpuData[offset + 4] = bias[i];
// padding
this.cpuData[offset + 5] = 0;
this.cpuData[offset + 6] = 0;
this.cpuData[offset + 7] = 0;
}
this.device.queue.writeBuffer(this.storageBuffer, 0, this.cpuData);
}
destroy() {
this.storageBuffer.destroy();
}
}
class SimulationParamsBuffer {
constructor(device) {
this.device = device;
// Simulation parameters matching shader struct (24 floats = 96 bytes):
// 0: alpha: f32, 1: velocityDecay: f32, 2: nodeCount: u32, 3: linkCount: u32,
// 4: centerX: f32, 5: centerY: f32, 6: centerStrength: f32, 7: theta2: f32,
// 8: distanceMin2: f32, 9: distanceMax2: f32, 10: iterations: u32, 11: collisionRadius: f32,
// 12: collisionStrength: f32, 13: collisionIterations: u32, 14: forceXTarget: f32, 15: forceXStrength: f32,
// 16: forceYTarget: f32, 17: forceYStrength: f32, 18: radialX: f32, 19: radialY: f32,
// 20: radialRadius: f32, 21: radialStrength: f32, 22: _pad1: f32, 23: _pad2: f32
// 24 x 4 bytes = 96 bytes (aligned to 16 bytes)
this.bufferSize = 96;
this.storageBuffer = device.createBuffer({
size: this.bufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
label: "Simulation Params Buffer"
});
// Use ArrayBuffer with views to properly handle mixed f32/u32 types
this.arrayBuffer = new ArrayBuffer(96);
this.floatView = new Float32Array(this.arrayBuffer);
this.uintView = new Uint32Array(this.arrayBuffer);
}
update(params) {
// f32 values
this.floatView[0] = params.alpha || 1;
this.floatView[1] = params.velocityDecay || 0.6;
// u32 values (indices 2, 3)
this.uintView[2] = params.nodeCount || 0;
this.uintView[3] = params.linkCount || 0;
// f32 values
this.floatView[4] = params.centerX || 0;
this.floatView[5] = params.centerY || 0;
this.floatView[6] = params.centerStrength || 1;
this.floatView[7] = params.theta2 || 0.81;
this.floatView[8] = params.distanceMin2 || 1;
// Use a very large finite number instead of Infinity for GPU compatibility
const maxDist2 = params.distanceMax2;
this.floatView[9] = (maxDist2 === Infinity || maxDist2 > 1e30) ? 1e30 : maxDist2;
// u32 value
this.uintView[10] = params.iterations || 1;
// f32 value
this.floatView[11] = params.collisionRadius || 5;
this.floatView[12] = params.collisionStrength || 1;
// u32 value
this.uintView[13] = params.collisionIterations || 1;
// forceX params
this.floatView[14] = params.forceXTarget || 0;
this.floatView[15] = params.forceXStrength || 0.1;
// forceY params
this.floatView[16] = params.forceYTarget || 0;
this.floatView[17] = params.forceYStrength || 0.1;
// radial params
this.floatView[18] = params.radialX || 0;
this.floatView[19] = params.radialY || 0;
this.floatView[20] = params.radialRadius || 100;
this.floatView[21] = params.radialStrength || 0.1;
// padding
this.floatView[22] = 0;
this.floatView[23] = 0;
this.device.queue.writeBuffer(this.storageBuffer, 0, this.arrayBuffer);
}
destroy() {
this.storageBuffer.destroy();
}
}
// Shader source code as strings
// These are embedded directly to avoid async loading issues
const manyBodyShader = `
// Many-Body Force Compute Shader
// Implements N-body gravitational/repulsive force using tile-based algorithm
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: Params;
const TILE_SIZE: u32 = 256u;
var<workgroup> tile: array<vec4<f32>, 256>;
fn jiggle(seed: u32) -> f32 {
let s = (seed * 1103515245u + 12345u) & 0x7fffffffu;
return (f32(s) / f32(0x7fffffff) - 0.5) * 1e-6;
}
@compute @workgroup_size(256)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(workgroup_id) workgroup_id: vec3<u32>
) {
let i = global_id.x;
let nodeCount = params.nodeCount;
let isValid = i < nodeCount;
// Load node data (use defaults for out-of-bounds threads)
var myPos = vec2<f32>(0.0, 0.0);
var myVx: f32 = 0.0;
var myVy: f32 = 0.0;
if (isValid) {
let node = nodes[i];
myPos = vec2<f32>(node.x, node.y);
myVx = node.vx;
myVy = node.vy;
}
var forceX: f32 = 0.0;
var forceY: f32 = 0.0;
let alpha = params.alpha;
let distanceMin2 = params.distanceMin2;
let distanceMax2 = params.distanceMax2;
let numTiles = (nodeCount + TILE_SIZE - 1u) / TILE_SIZE;
for (var t: u32 = 0u; t < numTiles; t++) {
// All threads participate in loading the tile
let tileIdx = t * TILE_SIZE + local_id.x;
if (tileIdx < nodeCount) {
let other = nodes[tileIdx];
tile[local_id.x] = vec4<f32>(other.x, other.y, other.strength, 0.0);
} else {
tile[local_id.x] = vec4<f32>(0.0, 0.0, 0.0, 0.0);
}
// All threads must hit this barrier
workgroupBarrier();
// Only valid threads compute forces
if (isValid) {
let tileEnd = min(TILE_SIZE, nodeCount - t * TILE_SIZE);
for (var j: u32 = 0u; j < tileEnd; j++) {
let otherIdx = t * TILE_SIZE + j;
if (otherIdx != i) {
let other = tile[j];
var dx = other.x - myPos.x;
var dy = other.y - myPos.y;
var l2 = dx * dx + dy * dy;
if (l2 < distanceMax2) {
if (dx == 0.0) {
dx = jiggle(i * nodeCount + otherIdx);
l2 += dx * dx;
}
if (dy == 0.0) {
dy = jiggle(i * nodeCount + otherIdx + 1u);
l2 += dy * dy;
}
if (l2 < distanceMin2) {
l2 = sqrt(distanceMin2 * l2);
}
let strength = other.z;
let force = strength * alpha / l2;
forceX += dx * force;
forceY += dy * force;
}
}
}
}
// All threads must hit this barrier
workgroupBarrier();
}
// Only valid threads write results
if (isValid) {
nodes[i].vx = myVx + forceX;
nodes[i].vy = myVy + forceY;
}
}
`;
const linkShader = `
// Link Force Compute Shader
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Link {
sourceIdx: f32,
targetIdx: f32,
distance: f32,
strength: f32,
bias: f32,
_pad1: f32,
_pad2: f32,
_pad3: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<storage, read> links: array<Link>;
@group(0) @binding(2) var<uniform> params: Params;
fn jiggle(seed: u32) -> f32 {
let s = (seed * 1103515245u + 12345u) & 0x7fffffffu;
return (f32(s) / f32(0x7fffffff) - 0.5) * 1e-6;
}
@compute @workgroup_size(256)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let nodeIdx = global_id.x;
let nodeCount = params.nodeCount;
let linkCount = params.linkCount;
let alpha = params.alpha;
if (nodeIdx >= nodeCount) {
return;
}
let node = nodes[nodeIdx];
var dvx: f32 = 0.0;
var dvy: f32 = 0.0;
for (var i: u32 = 0u; i < linkCount; i++) {
let link = links[i];
let sourceIdx = u32(link.sourceIdx);
let targetIdx = u32(link.targetIdx);
if (sourceIdx != nodeIdx && targetIdx != nodeIdx) {
continue;
}
let srcNode = nodes[sourceIdx];
let dstNode = nodes[targetIdx];
var dx = dstNode.x + dstNode.vx - srcNode.x - srcNode.vx;
var dy = dstNode.y + dstNode.vy - srcNode.y - srcNode.vy;
if (dx == 0.0 && dy == 0.0) {
dx = jiggle(i * 2u);
dy = jiggle(i * 2u + 1u);
}
let l = sqrt(dx * dx + dy * dy);
let force = (l - link.distance) / l * alpha * link.strength;
let fx = dx * force;
let fy = dy * force;
if (nodeIdx == targetIdx) {
dvx -= fx * link.bias;
dvy -= fy * link.bias;
} else {
dvx += fx * (1.0 - link.bias);
dvy += fy * (1.0 - link.bias);
}
}
nodes[nodeIdx].vx = node.vx + dvx;
nodes[nodeIdx].vy = node.vy + dvy;
}
`;
const integrateShader = `
// Position Integration Compute Shader
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: Params;
fn isNaN(v: f32) -> bool {
return !(v == v);
}
@compute @workgroup_size(256)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let i = global_id.x;
let nodeCount = params.nodeCount;
if (i >= nodeCount) {
return;
}
var node = nodes[i];
let velocityDecay = params.velocityDecay;
if (isNaN(node.fx)) {
node.vx = node.vx * velocityDecay;
node.x = node.x + node.vx;
} else {
node.x = node.fx;
node.vx = 0.0;
}
if (isNaN(node.fy)) {
node.vy = node.vy * velocityDecay;
node.y = node.y + node.vy;
} else {
node.y = node.fy;
node.vy = 0.0;
}
nodes[i] = node;
}
`;
const collisionShader = `
// Collision Force Compute Shader
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: Params;
const TILE_SIZE: u32 = 256u;
var<workgroup> tile: array<vec4<f32>, 256>;
fn jiggle(seed: u32) -> f32 {
let s = (seed * 1103515245u + 12345u) & 0x7fffffffu;
return (f32(s) / f32(0x7fffffff) - 0.5) * 1e-6;
}
@compute @workgroup_size(256)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let i = global_id.x;
let nodeCount = params.nodeCount;
let isValid = i < nodeCount;
// Load node data (use defaults for out-of-bounds threads)
var myX: f32 = 0.0;
var myY: f32 = 0.0;
var myRadius: f32 = 0.0;
var myVx: f32 = 0.0;
var myVy: f32 = 0.0;
if (isValid) {
let node = nodes[i];
myX = node.x;
myY = node.y;
myRadius = node.radius;
myVx = node.vx;
myVy = node.vy;
}
var dvx: f32 = 0.0;
var dvy: f32 = 0.0;
let strength = params.collisionStrength;
let numTiles = (nodeCount + TILE_SIZE - 1u) / TILE_SIZE;
for (var t: u32 = 0u; t < numTiles; t++) {
// All threads participate in loading the tile
let tileIdx = t * TILE_SIZE + local_id.x;
if (tileIdx < nodeCount) {
let other = nodes[tileIdx];
tile[local_id.x] = vec4<f32>(other.x, other.y, other.radius, 0.0);
} else {
tile[local_id.x] = vec4<f32>(0.0, 0.0, 0.0, 0.0);
}
// All threads must hit this barrier
workgroupBarrier();
// Only valid threads compute collisions
if (isValid) {
let tileEnd = min(TILE_SIZE, nodeCount - t * TILE_SIZE);
for (var j: u32 = 0u; j < tileEnd; j++) {
let otherIdx = t * TILE_SIZE + j;
if (otherIdx != i) {
let other = tile[j];
let otherRadius = other.z;
let combinedRadius = myRadius + otherRadius;
var dx = myX - other.x;
var dy = myY - other.y;
var l2 = dx * dx + dy * dy;
let minDist2 = combinedRadius * combinedRadius;
if (l2 < minDist2) {
if (dx == 0.0) {
dx = jiggle(min(i, otherIdx) * nodeCount + max(i, otherIdx));
l2 += dx * dx;
}
if (dy == 0.0) {
dy = jiggle(min(i, otherIdx) * nodeCount + max(i, otherIdx) + 1u);
l2 += dy * dy;
}
let l = sqrt(l2);
let overlap = combinedRadius - l;
let totalRadius = myRadius + otherRadius;
let myWeight = otherRadius / totalRadius;
let impulse = overlap * strength * 0.5;
let nx = dx / l;
let ny = dy / l;
dvx += nx * impulse * myWeight;
dvy += ny * impulse * myWeight;
}
}
}
}
// All threads must hit this barrier
workgroupBarrier();
}
// Only valid threads write results
if (isValid) {
nodes[i].vx = myVx + dvx;
nodes[i].vy = myVy + dvy;
}
}
`;
const forceXShader = `
// X-Positioning Force Compute Shader
// Pushes nodes toward a target x position
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: Params;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let i = global_id.x;
if (i >= params.nodeCount) {
return;
}
let node = nodes[i];
let alpha = params.alpha;
let targetX = params.forceXTarget;
let strength = params.forceXStrength;
let dx = targetX - node.x;
nodes[i].vx = node.vx + dx * strength * alpha;
}
`;
const forceYShader = `
// Y-Positioning Force Compute Shader
// Pushes nodes toward a target y position
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: Params;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let i = global_id.x;
if (i >= params.nodeCount) {
return;
}
let node = nodes[i];
let alpha = params.alpha;
let targetY = params.forceYTarget;
let strength = params.forceYStrength;
let dy = targetY - node.y;
nodes[i].vy = node.vy + dy * strength * alpha;
}
`;
const forceRadialShader = `
// Radial Force Compute Shader
// Pushes nodes toward a target radius from a center point
struct Node {
x: f32,
y: f32,
vx: f32,
vy: f32,
fx: f32,
fy: f32,
strength: f32,
radius: f32,
}
struct Params {
alpha: f32,
velocityDecay: f32,
nodeCount: u32,
linkCount: u32,
centerX: f32,
centerY: f32,
centerStrength: f32,
theta2: f32,
distanceMin2: f32,
distanceMax2: f32,
iterations: u32,
collisionRadius: f32,
collisionStrength: f32,
collisionIterations: u32,
forceXTarget: f32,
forceXStrength: f32,
forceYTarget: f32,
forceYStrength: f32,
radialX: f32,
radialY: f32,
radialRadius: f32,
radialStrength: f32,
_pad1: f32,
_pad2: f32,
}
@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;
@group(0) @binding(1) var<uniform> params: Params;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let i = global_id.x;
if (i >= params.nodeCount) {
return;
}
let node = nodes[i];
let alpha = params.alpha;
let cx = params.radialX;
let cy = params.radialY;
let targetRadius = params.radialRadius;
let strength = params.radialStrength;
let dx = node.x - cx;
let dy = node.y - cy;
let r = sqrt(dx * dx + dy * dy);
if (r > 0.0) {
let k = (targetRadius - r) * strength * alpha / r;
nodes[i].vx = node.vx + dx * k;
nodes[i].vy = node.vy + dy * k;
}
}
`;
// GPU-Accelerated Force Simulation
const WORKGROUP_SIZE = 256;
var initialRadius = 10,
initialAngle = Math.PI * (3 - Math.sqrt(5));
function simulationGpu (nodes) {
var simulation,
alpha = 1,
alphaMin = 0.001,
alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
alphaTarget = 0,
velocityDecay = 0.6,
forces = new Map(),
stepper = d3Timer.timer(step),
event = d3Dispatch.dispatch("tick", "end"),
random = lcg();
// GPU resources
var gpuInitialized = false,
gpuDevice = null,
nodeBuffers = null,
paramsBuffer = null,
pipelines = {},
bindGroups = {},
gpuReadyPromise = null,
gpuReadyResolve = null,
gpuLoopRunning = false; // GPU compute loop running flag
// Force-specific GPU resources
var linkBuffers = null,
linkData = { links: [], distances: [], strengths: [], bias: [] };
// Force configurations (mirroring CPU forces)
var manyBodyConfig = {
enabled: false,
strength: -30,
theta: 0.9,
distanceMin: 1,
distanceMax: Infinity,
};
var centerConfig = {
enabled: false,
x: 0,
y: 0,
strength: 1,
};
var collisionConfig = {
enabled: false,
radius: 5,
strength: 1,
iterations: 1,
};
var forceXConfig = {
enabled: false,
x: 0,
strength: 0.1,
};
var forceYConfig = {
enabled: false,
y: 0,
strength: 0.1,
};
var radialConfig = {
enabled: false,
x: 0,
y: 0,
radius: 100,
strength: 0.1,
};
if (nodes == null) nodes = [];
// Create promise for gpuReady()
gpuReadyPromise = new Promise((resolve) => {
gpuReadyResolve = resolve;
});
async function initGPU() {
if (gpuInitialized) return true;
try {
gpuDevice = await initWebGPU();
await createPipelines();
gpuInitialized = true;
return true;
} catch (e) {
console.warn("WebGPU initialization failed, falling back to CPU:", e);
return false;
}
}
async function createPipelines() {
// Many-body force pipeline
pipelines.manyBody = await createComputePipeline(
gpuDevice,
manyBodyShader,
"main"
);
// Link force pipeline
pipelines.linkForce = await createComputePipeline(
gpuDevice,
linkShader,
"main"
);
// Integration pipeline
pipelines.integrate = await createComputePipeline(
gpuDevice,
integrateShader,
"main"
);
// Collision pipeline
pipelines.collision = await createComputePipeline(
gpuDevice,
collisionShader,
"main"
);
// ForceX pipeline
pipelines.forceX = await createComputePipeline(
gpuDevice,
forceXShader,
"main"
);
// ForceY pipeline
pipelines.forceY = await createComputePipeline(
gpuDevice,
forceYShader,
"main"
);
// Radial force pipeline
pipelines.radial = await createComputePipeline(
gpuDevice,
forceRadialShader,
"main"
);
}
function createBuffers() {
if (!gpuDevice || nodes.length === 0) return;
// Destroy old buffers
if (nodeBuffers) nodeBuffers.destroy();
if (paramsBuffer) paramsBuffer.destroy();
if (linkBuffers) linkBuffers.destroy();
nodeBuffers = new NodeBuffers(gpuDevice, nodes.length);
paramsBuffer = new SimulationParamsBuffer(gpuDevice);
// Create link buffers if we have links
if (linkData.links.length > 0) {
linkBuffers = new LinkBuffers(gpuDevice, linkData.links.length);
}
createBindGroups();
}
function createBindGroups() {
if (!gpuDevice || !nodeBuffers) return;
// Many-body bind group
bindGroups.manyBody = gpuDevice.createBindGroup({
layout: pipelines.manyBody.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
// Integration bind group
bindGroups.integrate = gpuDevice.createBindGroup({
layout: pipelines.integrate.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
// Collision bind group
bindGroups.collision = gpuDevice.createBindGroup({
layout: pipelines.collision.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
// Link bind group (if links exist)
if (linkBuffers) {
bindGroups.link = gpuDevice.createBindGroup({
layout: pipelines.linkForce.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: linkBuffers.storageBuffer } },
{ binding: 2, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
}
// ForceX bind group
bindGroups.forceX = gpuDevice.createBindGroup({
layout: pipelines.forceX.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
// ForceY bind group
bindGroups.forceY = gpuDevice.createBindGroup({
layout: pipelines.forceY.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
// Radial force bind group
bindGroups.radial = gpuDevice.createBindGroup({
layout: pipelines.radial.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: nodeBuffers.storageBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer.storageBuffer } },
],
});
}
function uploadToGPU() {
if (!nodeBuffers) return;
// Set per-node strengths for many-body force
for (let i = 0; i < nodes.length; i++) {
nodes[i]._strength = manyBodyConfig.strength;
nodes[i].radius = nodes[i].radius || collisionConfig.radius;
}
nodeBuffers.uploadNodes(nodes);
paramsBuffer.update({
alpha: alpha,
velocityDecay: velocityDecay,
nodeCount: nodes.length,
linkCount: linkData.links.length,
centerX: centerConfig.x,
centerY: centerConfig.y,
centerStrength: centerConfig.strength,
theta2: manyBodyConfig.theta * manyBodyConfig.theta,
distanceMin2: manyBodyConfig.distanceMin * manyBodyConfig.distanceMin,
distanceMax2: manyBodyConfig.distanceMax * manyBodyConfig.distanceMax,
iterations: 1,
collisionRadius: collisionConfig.radius,
collisionStrength: collisionConfig.strength,
collisionIterations: collisionConfig.iterations,
forceXTarget: forceXConfig.x,
forceXStrength: forceXConfig.enabled