You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 04:18:12