如何优化p5.js重力网格模拟性能?是否必须使用Shader?
点阵网格重力模拟性能优化问题
我用p5.js开发了一个点阵网格的重力模拟效果,但性能表现不佳,经排查发现性能瓶颈主要源于ellipse调用。了解到有人讨论Shader技术,想咨询是否必须通过Shader来优化该模拟的性能?
实现代码
JavaScript 代码
const settings = { fps: 60, dotColor: '#bab6ab', dotSize: 2, maxGravForce: 12, maxEllasticForce: 20, columns: 120, columnGap: 2, rows: 60, rowGap: 1, }; let running = false; const clamp = (num, min, max) => { return Math.min(Math.max(num, min), max); }; /** * force = G*m1*m2 / (distance * distance) */ const calculateGravity = (point1, point2) => { const G = 1000; // grav constant const m1 = 1; const m2 = 1; // calculate distance const dx = point2.x - point1.x; const dy = point2.y - point1.y; const distance = Math.sqrt(dx * dx + dy * dy); // calculate force magnitude const forceMagnitude = G * m1 * m2 / Math.pow(distance, 1.05); // calculate force direction (unit vector) const ux = dx / distance; const uy = dy / distance; // calculate vector components const fx = clamp(forceMagnitude * ux, -settings.maxGravForce, settings.maxGravForce); const fy = clamp(forceMagnitude * uy, -settings.maxGravForce, settings.maxGravForce); // create and return the force vector return createVector(fx, fy); }; const calculateElasticity = (point1, point2) => { // calculate distance const dx = point2.x - point1.x; const dy = point2.y - point1.y; const distance = Math.sqrt(dx * dx + dy * dy); // calculate force magnitude let forceMagnitude = 0; if (distance <= 2.62) { // 2.62 is where these two functions intersect // y = x forceMagnitude = Math.abs(distance); } else { // exponential forceMagnitude = (Math.pow(2, (0.5 * distance) - 2)) + 2; } // calculate force direction (unit vector) const ux = distance !== 0 ? (dx / distance) : 0; const uy = distance !== 0 ? (dy / distance) : 0; // calculate vector components const fx = clamp(forceMagnitude * ux, -settings.maxEllasticForce, settings.maxEllasticForce); const fy = clamp(forceMagnitude * uy, -settings.maxEllasticForce, settings.maxEllasticForce); // create and return the force vector return createVector(fx, fy); }; class Entity { constructor(position, debug) { this.position = createVector(position.x, position.y); this.velocity = createVector(0, 0); this.anchor = createVector(position.x, position.y); this.debug = debug || false; } getPosition() { return this.position; } getAnchor() { return this.anchor; } applyForce(force) { this.velocity.add(force); } applyAnchorDamping() { // the closer we are to the anchor, the more we reduce the speed const point1 = this.position; const point2 = this.anchor; // Define a minimum distance to avoid extreme damping close to the anchor const minDistance = 8; // You can adjust this value const safeDistance = Math.max(point1.dist(point2), minDistance); // Define the damping strength const dampingStrength = 0.95; // You can adjust this value (closer to 1 means less damping) // Calculate the damping factor // As the entity gets closer to the anchor, the damping factor decreases const dampingFactor = Math.pow(dampingStrength, safeDistance); // if (this.debug) { // console.log('anchor damping', point1.dist(point2).toFixed(2), dampingFactor) // } // Apply damping to velocity this.velocity.mult(dampingFactor); } applyMouseDamping(mousePoint, debug) { const point1 = this.position; const point2 = mousePoint; const distance = point1.dist(point2); if (distance > 30) { return; } const dampingFactor = clamp(0.05 * distance + 0.0, 0, 1); // if (this.debug) { // console.log('mouse damping', point1.dist(point2).toFixed(2), dampingFactor) // } this.velocity.mult(dampingFactor); } applyVelocity() { this.position.add(this.velocity); } show() { noStroke(); fill(settings.dotColor); ellipse(this.position.x, this.position.y, settings.dotSize, settings.dotSize, 6); } } function throttle(cb, delay) { let flag = null; return (...args) => { if (flag === null) { cb(...args); flag = setTimeout(() => { flag = null; }, delay); } }; } let entities = []; function createEntities() { entities = []; const gridWidth = (settings.columns * settings.dotSize) + ((settings.columns - 1) * settings.columnGap); const gridHeight = (settings.rows * settings.dotSize) + ((settings.rows - 1) * settings.rowGap); for (let i = 0; i < settings.columns; i++) { for (let j = 0; j < settings.rows; j++) { let x = i * (settings.dotSize + settings.columnGap); let y = j * (settings.dotSize + settings.rowGap); // normalize to center x = x - (gridWidth / 2); y = y - (gridHeight / 2); entities.push( new Entity( { x, y }, i === 0 && j === 0, ), ); } } } const createEntitiesThrottled = throttle(createEntities, 250); function setup() { frameRate(settings.fps); createCanvas(windowWidth, windowHeight, WEBGL); createEntities(); // noLoop(); } function draw() { // console.log(frameRate()) clear(); // background('#DCDAD4') for (let i = 0; i < entities.length; i++) { const entity = entities[i]; const mousePosition = mouseX !== 0 && mouseY !== 0 ? createVector(mouseX - (windowWidth / 2), mouseY - (windowHeight / 2)) : null; if (mousePosition) { const mouseForce = calculateGravity( entity.getPosition(), mousePosition, ); // if (entity.debug) { // console.log(mouseForce.toString()) // } entity.applyForce(mouseForce); } const anchorForce = calculateElasticity( entity.getPosition(), entity.getAnchor(), ); entity.applyForce(anchorForce); entity.applyAnchorDamping(); if (mousePosition) { entity.applyMouseDamping(mousePosition); } entity.applyVelocity(); entity.show(); } } function windowResized() { resizeCanvas(windowWidth, windowHeight); createEntitiesThrottled(); }
HTML 代码
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
优化方案:并非必须使用Shader
你不需要立刻切换到Shader,先试试这些轻量优化手段,很多情况下就能解决性能问题:
1. 替换ellipse为更高效的绘制方式
ellipse是相对耗时的矢量绘制函数,尤其是在WEBGL模式下批量调用时。可以用以下替代方案:
- 使用
point():你的点大小是2,point()在WEBGL中会渲染成正方形点,视觉效果接近,且绘制成本极低。修改Entity的show方法:show() { stroke(settings.dotColor); strokeWeight(settings.dotSize); point(this.position.x, this.position.y); } - 使用批量顶点绘制:把所有点的坐标收集后,用
beginShape(POINTS)+vertex()+endShape()一次性绘制,避免多次调用绘制函数的开销。
2. 减少状态切换
当前每个Entity.show()都调用noStroke()和fill(),这些状态切换会增加GPU开销。可以把样式设置提到循环外,统一配置:
function draw() { clear(); noStroke(); fill(settings.dotColor); for (let i = 0; i < entities.length; i++) { const entity = entities[i]; // ... 计算力和更新位置的代码 ... entity.show(); } }
3. 优化计算逻辑
虽然你说瓶颈在ellipse,但计算部分也可以优化:
- 预计算鼠标位置:当前每次循环都创建
mousePosition,可以提到循环外,避免重复创建向量对象:function draw() { clear(); const mousePosition = (mouseX !== 0 && mouseY !== 0) ? createVector(mouseX - width/2, mouseY - height/2) : null; for (let i = 0; i < entities.length; i++) { const entity = entities[i]; if (mousePosition) { const mouseForce = calculateGravity(entity.position, mousePosition); entity.applyForce(mouseForce); } // ... 其他逻辑 ... entity.show(); } }
4. 当上述方案不够时,再考虑Shader
如果你的点阵数量继续增加(比如超过10000个),或者需要更复杂的视觉效果,Shader会是更好的选择。用Shader可以把所有点的绘制甚至物理计算都放到GPU上并行处理,大幅提升性能。但这需要掌握基础的WebGL着色器知识,是进阶优化方案。
总结:先尝试前三种轻量优化,大部分情况下就能达到60fps的要求,Shader不是必须的。
内容的提问来源于stack exchange,提问作者Cameron
相关产品推荐
相关产品推荐

