返回

Matter.js 多边形物体“挤压”问题解决方案及代码示例

javascript

Matter.js 中多边形物体“挤压”问题的解决方案

在Matter.js物理引擎中,使用多个凸多边形组合成一个凹多边形物体时,有时会出现这些子多边形“挤压”在一起的现象。本文将深入分析此问题产生的原因,并提供几种有效的解决方案。

问题分析

Matter.js 提供了 Bodies.fromVertices 方法,允许开发者通过传入一组顶点坐标来创建刚体。 当处理凹多边形时,通常需要将其分割成多个凸多边形。 问题在于,仅仅将这些凸多边形的顶点传入 Bodies.fromVertices 方法创建刚体,然后使用约束将它们连接起来,并不能保证这些刚体按照预期的方式组合成一个整体。 这是因为,Matter.js 在创建刚体时,会根据顶点计算刚体的质心和形状,而没有考虑这些刚体之间的相对位置关系。当多个刚体质心位置过于接近时,就会出现“挤压”现象。

此外,仅仅通过一个约束将所有刚体连接到一点,会导致各个部分在力的作用下发生不自然的旋转和形变,加剧“挤压”问题。

解决方案

以下提供几种解决 Matter.js 中多边形物体“挤压”问题的方案:

1. 手动计算并设置子多边形的质心偏移

这种方法通过手动计算每个子多边形相对于整体形状的质心偏移量,并在创建刚体时进行设置,从而确保各个子多边形在正确的位置上组合。

  • 原理: 计算每个子多边形相对于父对象(即整体凹多边形)的质心偏移。然后在创建每个子多边形刚体时,应用这个偏移量。

  • 步骤:
    1. 计算整体凹多边形的质心。
    2. 计算每个凸多边形的质心。
    3. 计算每个凸多边形质心相对于整体质心的偏移量。
    4. 使用 Bodies.fromVertices 创建每个凸多边形的刚体。
    5. 通过 Body.setPosition 方法,将每个刚体的位置设置为其原始质心位置加上计算出的偏移量。
    6. 使用 Composite.addBody 方法将每个子多边形添加到物理世界中,然后使用Composites.chain 将它们链接起来。

  • 代码示例:

    import Matter from 'matter-js';
    
    function createCompositeBodyFromVertices(engine, verticesParts, options = {}) {
        const bodies = [];
        const bodyPositions = [];
        const composite = Matter.Composite.create();
    
        // Calculate overall centroid of all parts combined
        const combinedVertices = verticesParts.flat();
        const totalCentroid = calculateCentroid(combinedVertices);
    
        verticesParts.forEach(vertices => {
            // Calculate centroid of a single part
            const partCentroid = calculateCentroid(vertices);
    
            // Create body
            const body = Matter.Bodies.fromVertices(0, 0, vertices, options);
    
            // Position offset from total centroid to current part centroid
            const positionOffset = {
                x: partCentroid.x - totalCentroid.x,
                y: partCentroid.y - totalCentroid.y,
            };
    
            // Save calculated properties to arrays
            bodies.push(body);
            bodyPositions.push(positionOffset);
    
            // Translate body so vertices are around (0, 0) for later calculations,
            // before adding the body to a composite
            Matter.Body.setPosition(body, { x: -partCentroid.x, y: -partCentroid.y });
            Matter.Composite.addBody(composite, body);
        });
    
        // Add all bodies at right place into composite at total centroid.
        for (let i = 0; i < bodies.length; i++) {
            const body = bodies[i];
            const positionOffset = bodyPositions[i];
            Matter.Body.translate(body, positionOffset);
            Matter.Body.setPosition(body, { x: body.position.x + totalCentroid.x, y: body.position.y + totalCentroid.y });
        }
    
        Matter.Composites.chain(composite, 0.5, 0, -0.5, 0, { stiffness: 1, length: 2, render: { type: 'line' } });
    
        Matter.Composite.add(engine.world, composite);
        return composite;
    }
    
    // Helper function to calculate the centroid of a set of vertices
    function calculateCentroid(vertices) {
        let signedArea = 0;
        let centerX = 0;
        let centerY = 0;
    
        for (let i = 0; i < vertices.length; i++) {
            const vertex = vertices[i];
            const nextVertex = vertices[(i + 1) % vertices.length];
            const areaComponent = vertex.x * nextVertex.y - nextVertex.x * vertex.y;
            signedArea += areaComponent;
            centerX += (vertex.x + nextVertex.x) * areaComponent;
            centerY += (vertex.y + nextVertex.y) * areaComponent;
        }
    
        signedArea *= 0.5;
        centerX /= (6 * signedArea);
        centerY /= (6 * signedArea);
    
        return { x: centerX, y: centerY };
    }
    
    const engine = Matter.Engine.create();
    const render = Matter.Render.create({
        engine: engine,
        canvas: document.getElementById('canvas'), // your canvas element
        options: {
            width: 800,
            height: 600
        }
    });
    const rampVertices = [[{ x: 190, y: 145 }, { x: 261, y: 217 }, { x: 263, y: 291 }, { x: 189, y: 291 }, { x: 117, y: 219 }],
                       [{ x: 117, y: 219 }, { x: 189, y: 291 }, { x: 21, y: 310 }, { x: 0, y: 218 }],
                       [{ x: 21, y: 310 }, { x: 43, y: 386 }, { x: 0, y: 427 }, { x: 0, y: 218 }],
                      ]; // Replace with your actual vertices
    
    const ramp = createCompositeBodyFromVertices(engine, rampVertices);
     Matter.Composite.add(engine.world,ramp);
     Matter.Render.run(render);
     Matter.Runner.run(Matter.Runner.create(),engine);
    
  • 安全建议:

    • 确保顶点数据的准确性,错误的顶点数据会导致质心计算错误,进而导致物体位置不正确。
    • 对于复杂的凹多边形,分割成尽可能少的凸多边形,以减少计算量和潜在的误差。

2. 使用 Matter.js 插件 matter-decomposer

matter-decomposer 是一个 Matter.js 插件,专门用于将凹多边形分解为凸多边形,并自动处理子多边形的组合问题。

  • 原理: 该插件使用 poly-decomp 库进行多边形分解,并自动计算子多边形的相对位置和旋转,生成一个可以直接添加到 Matter.js 世界的复合刚体。

  • 步骤:
    1. 安装 matter-decomposerpoly-decomp 库:

    npm install matter-decomposer poly-decomp
    
  2. 引入 `matter-decomposer` 并将其注册到 Matter.js 中:

      ```javascript
      import Matter from 'matter-js';
      import decomp from 'matter-decomposer';

      Matter.use(decomp);
      ```
  3. 使用  `Bodies.fromVertices` 方法创建刚体时,将  `plugin`  选项设置为  `decomp`:
       ```javascript
       const ramp = Matter.Bodies.fromVertices(150, 70, vertices,{}, true, 0.01, 10,decomp);

      Matter.Composite.add(engine.world, ramp);
       ```
  • 代码示例:

    import Matter from 'matter-js';
    import decomp from 'matter-decomposer';
    
    Matter.use(decomp);
    
    const engine = Matter.Engine.create();
    const render = Matter.Render.create({
        engine: engine,
        canvas: document.getElementById('canvas'), // 替换成你的canvas元素
        options: {
            width: 800,
            height: 600