Skip to content

physics_ammo_rope

对应 three.js 示例地址

仅需关注 init 函数的内容,其他部分都是示例小程序所使用的描述配置。

js
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import AmmoLib from "three/examples/jsm/libs/ammo.wasm.js";

/** @type {import("@minisheeep/mp-three-examples").OfficialExampleInfo} */
const exampleInfo = {
  name: "physics_ammo_rope",
  useLoaders: [],
  needArrowControls: true,
  info: [
    [
      {
        tag: "text",
        content: "Ammo.js physics soft body rope demo"
      }
    ]
  ],
  init: ({ window, canvas, GUI, Stats, needToDispose, useFrame }) => {
    let stats;
    let camera, controls, scene, renderer;
    let textureLoader;
    const clock = new THREE.Clock();
    const gravityConstant = -9.8;
    let collisionConfiguration;
    let dispatcher;
    let broadphase;
    let solver;
    let softBodySolver;
    let physicsWorld;
    const rigidBodies = [];
    const margin = 0.05;
    let hinge;
    let rope;
    let transformAux1;
    let armMovement = 0;
    let Ammo;
    AmmoLib.call({}).then(function(AmmoLib2) {
      Ammo = AmmoLib2;
      init();
    });
    function init() {
      initGraphics();
      initPhysics();
      createObjects();
      initInput();
    }
    function initGraphics() {
      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.2, 2e3);
      scene = new THREE.Scene();
      scene.background = new THREE.Color(12571109);
      camera.position.set(-7, 5, 8);
      renderer = new THREE.WebGLRenderer({ antialias: true, canvas });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setAnimationLoop(animate);
      renderer.shadowMap.enabled = true;
      controls = new OrbitControls(camera, renderer.domElement);
      controls.target.set(0, 2, 0);
      controls.update();
      textureLoader = new THREE.TextureLoader();
      const ambientLight = new THREE.AmbientLight(12303291);
      scene.add(ambientLight);
      const light = new THREE.DirectionalLight(16777215, 3);
      light.position.set(-10, 10, 5);
      light.castShadow = true;
      const d = 10;
      light.shadow.camera.left = -10;
      light.shadow.camera.right = d;
      light.shadow.camera.top = d;
      light.shadow.camera.bottom = -10;
      light.shadow.camera.near = 2;
      light.shadow.camera.far = 50;
      light.shadow.mapSize.x = 1024;
      light.shadow.mapSize.y = 1024;
      scene.add(light);
      stats = new Stats(renderer);
      window.addEventListener("resize", onWindowResize);
      needToDispose(renderer, scene, controls);
    }
    function initPhysics() {
      collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
      dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
      broadphase = new Ammo.btDbvtBroadphase();
      solver = new Ammo.btSequentialImpulseConstraintSolver();
      softBodySolver = new Ammo.btDefaultSoftBodySolver();
      physicsWorld = new Ammo.btSoftRigidDynamicsWorld(
        dispatcher,
        broadphase,
        solver,
        collisionConfiguration,
        softBodySolver
      );
      physicsWorld.setGravity(new Ammo.btVector3(0, gravityConstant, 0));
      physicsWorld.getWorldInfo().set_m_gravity(new Ammo.btVector3(0, gravityConstant, 0));
      transformAux1 = new Ammo.btTransform();
    }
    function createObjects() {
      const pos = new THREE.Vector3();
      const quat = new THREE.Quaternion();
      pos.set(0, -0.5, 0);
      quat.set(0, 0, 0, 1);
      const ground = createParalellepiped(
        40,
        1,
        40,
        0,
        pos,
        quat,
        new THREE.MeshPhongMaterial({ color: 16777215 })
      );
      ground.castShadow = true;
      ground.receiveShadow = true;
      textureLoader.load("textures/grid.png", function(texture) {
        texture.colorSpace = THREE.SRGBColorSpace;
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(40, 40);
        ground.material.map = texture;
        ground.material.needsUpdate = true;
      });
      const ballMass = 1.2;
      const ballRadius = 0.6;
      const ball = new THREE.Mesh(
        new THREE.SphereGeometry(ballRadius, 20, 20),
        new THREE.MeshPhongMaterial({ color: 2105376 })
      );
      ball.castShadow = true;
      ball.receiveShadow = true;
      const ballShape = new Ammo.btSphereShape(ballRadius);
      ballShape.setMargin(margin);
      pos.set(-3, 2, 0);
      quat.set(0, 0, 0, 1);
      createRigidBody(ball, ballShape, ballMass, pos, quat);
      ball.userData.physicsBody.setFriction(0.5);
      const brickMass = 0.5;
      const brickLength = 1.2;
      const brickDepth = 0.6;
      const brickHeight = brickLength * 0.5;
      const numBricksLength = 6;
      const numBricksHeight = 8;
      const z0 = -6 * brickLength * 0.5;
      pos.set(0, brickHeight * 0.5, z0);
      quat.set(0, 0, 0, 1);
      for (let j = 0; j < numBricksHeight; j++) {
        const oddRow = j % 2 == 1;
        pos.z = z0;
        if (oddRow) {
          pos.z -= 0.25 * brickLength;
        }
        const nRow = oddRow ? numBricksLength + 1 : numBricksLength;
        for (let i = 0; i < nRow; i++) {
          let brickLengthCurrent = brickLength;
          let brickMassCurrent = brickMass;
          if (oddRow && (i == 0 || i == nRow - 1)) {
            brickLengthCurrent *= 0.5;
            brickMassCurrent *= 0.5;
          }
          const brick = createParalellepiped(
            brickDepth,
            brickHeight,
            brickLengthCurrent,
            brickMassCurrent,
            pos,
            quat,
            createMaterial()
          );
          brick.castShadow = true;
          brick.receiveShadow = true;
          if (oddRow && (i == 0 || i == nRow - 2)) {
            pos.z += 0.75 * brickLength;
          } else {
            pos.z += brickLength;
          }
        }
        pos.y += brickHeight;
      }
      const ropeNumSegments = 10;
      const ropeLength = 4;
      const ropeMass = 3;
      const ropePos = ball.position.clone();
      ropePos.y += ballRadius;
      const segmentLength = ropeLength / ropeNumSegments;
      const ropeGeometry = new THREE.BufferGeometry();
      const ropeMaterial = new THREE.LineBasicMaterial({ color: 0 });
      const ropePositions = [];
      const ropeIndices = [];
      for (let i = 0; i < ropeNumSegments + 1; i++) {
        ropePositions.push(ropePos.x, ropePos.y + i * segmentLength, ropePos.z);
      }
      for (let i = 0; i < ropeNumSegments; i++) {
        ropeIndices.push(i, i + 1);
      }
      ropeGeometry.setIndex(new THREE.BufferAttribute(new Uint16Array(ropeIndices), 1));
      ropeGeometry.setAttribute(
        "position",
        new THREE.BufferAttribute(new Float32Array(ropePositions), 3)
      );
      ropeGeometry.computeBoundingSphere();
      rope = new THREE.LineSegments(ropeGeometry, ropeMaterial);
      rope.castShadow = true;
      rope.receiveShadow = true;
      scene.add(rope);
      const softBodyHelpers = new Ammo.btSoftBodyHelpers();
      const ropeStart = new Ammo.btVector3(ropePos.x, ropePos.y, ropePos.z);
      const ropeEnd = new Ammo.btVector3(ropePos.x, ropePos.y + ropeLength, ropePos.z);
      const ropeSoftBody = softBodyHelpers.CreateRope(
        physicsWorld.getWorldInfo(),
        ropeStart,
        ropeEnd,
        ropeNumSegments - 1,
        0
      );
      const sbConfig = ropeSoftBody.get_m_cfg();
      sbConfig.set_viterations(10);
      sbConfig.set_piterations(10);
      ropeSoftBody.setTotalMass(ropeMass, false);
      Ammo.castObject(ropeSoftBody, Ammo.btCollisionObject).getCollisionShape().setMargin(margin * 3);
      physicsWorld.addSoftBody(ropeSoftBody, 1, -1);
      rope.userData.physicsBody = ropeSoftBody;
      ropeSoftBody.setActivationState(4);
      const armMass = 2;
      const armLength = 3;
      const pylonHeight = ropePos.y + ropeLength;
      const baseMaterial = new THREE.MeshPhongMaterial({ color: 6316128 });
      pos.set(ropePos.x, 0.1, ropePos.z - armLength);
      quat.set(0, 0, 0, 1);
      const base = createParalellepiped(1, 0.2, 1, 0, pos, quat, baseMaterial);
      base.castShadow = true;
      base.receiveShadow = true;
      pos.set(ropePos.x, 0.5 * pylonHeight, ropePos.z - armLength);
      const pylon = createParalellepiped(0.4, pylonHeight, 0.4, 0, pos, quat, baseMaterial);
      pylon.castShadow = true;
      pylon.receiveShadow = true;
      pos.set(ropePos.x, pylonHeight + 0.2, ropePos.z - 0.5 * armLength);
      const arm = createParalellepiped(0.4, 0.4, armLength + 0.4, armMass, pos, quat, baseMaterial);
      arm.castShadow = true;
      arm.receiveShadow = true;
      const influence = 1;
      ropeSoftBody.appendAnchor(0, ball.userData.physicsBody, true, influence);
      ropeSoftBody.appendAnchor(ropeNumSegments, arm.userData.physicsBody, true, influence);
      const pivotA = new Ammo.btVector3(0, pylonHeight * 0.5, 0);
      const pivotB = new Ammo.btVector3(0, -0.2, -3 * 0.5);
      const axis = new Ammo.btVector3(0, 1, 0);
      hinge = new Ammo.btHingeConstraint(
        pylon.userData.physicsBody,
        arm.userData.physicsBody,
        pivotA,
        pivotB,
        axis,
        axis,
        true
      );
      physicsWorld.addConstraint(hinge, true);
    }
    function createParalellepiped(sx, sy, sz, mass, pos, quat, material) {
      const threeObject = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz, 1, 1, 1), material);
      const shape = new Ammo.btBoxShape(new Ammo.btVector3(sx * 0.5, sy * 0.5, sz * 0.5));
      shape.setMargin(margin);
      createRigidBody(threeObject, shape, mass, pos, quat);
      return threeObject;
    }
    function createRigidBody(threeObject, physicsShape, mass, pos, quat) {
      threeObject.position.copy(pos);
      threeObject.quaternion.copy(quat);
      const transform = new Ammo.btTransform();
      transform.setIdentity();
      transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
      transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
      const motionState = new Ammo.btDefaultMotionState(transform);
      const localInertia = new Ammo.btVector3(0, 0, 0);
      physicsShape.calculateLocalInertia(mass, localInertia);
      const rbInfo = new Ammo.btRigidBodyConstructionInfo(
        mass,
        motionState,
        physicsShape,
        localInertia
      );
      const body = new Ammo.btRigidBody(rbInfo);
      threeObject.userData.physicsBody = body;
      scene.add(threeObject);
      if (mass > 0) {
        rigidBodies.push(threeObject);
        body.setActivationState(4);
      }
      physicsWorld.addRigidBody(body);
    }
    function createRandomColor() {
      return Math.floor(Math.random() * (1 << 24));
    }
    function createMaterial() {
      return new THREE.MeshPhongMaterial({ color: createRandomColor() });
    }
    function initInput() {
      const window2 = globalThis["window"];
      window2.addEventListener("keydown", function(event) {
        switch (event.code) {
          case "ArrowLeft":
            armMovement = -1;
            break;
          case "ArrowRight":
            armMovement = 1;
            break;
        }
      });
      window2.addEventListener("keyup", function(event) {
        switch (event.code) {
          case "ArrowLeft":
          case "ArrowRight":
            armMovement = 0;
            break;
        }
      });
    }
    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }
    function animate() {
      render();
      stats.update();
    }
    function render() {
      const deltaTime = clock.getDelta();
      updatePhysics(deltaTime);
      renderer.render(scene, camera);
    }
    function updatePhysics(deltaTime) {
      hinge.enableAngularMotor(true, 1.5 * armMovement, 50);
      physicsWorld.stepSimulation(deltaTime, 10);
      const softBody = rope.userData.physicsBody;
      const ropePositions = rope.geometry.attributes.position.array;
      const numVerts = ropePositions.length / 3;
      const nodes = softBody.get_m_nodes();
      let indexFloat = 0;
      for (let i = 0; i < numVerts; i++) {
        const node = nodes.at(i);
        const nodePos = node.get_m_x();
        ropePositions[indexFloat++] = nodePos.x();
        ropePositions[indexFloat++] = nodePos.y();
        ropePositions[indexFloat++] = nodePos.z();
      }
      rope.geometry.attributes.position.needsUpdate = true;
      for (let i = 0, il = rigidBodies.length; i < il; i++) {
        const objThree = rigidBodies[i];
        const objPhys = objThree.userData.physicsBody;
        const ms = objPhys.getMotionState();
        if (ms) {
          ms.getWorldTransform(transformAux1);
          const p = transformAux1.getOrigin();
          const q = transformAux1.getRotation();
          objThree.position.set(p.x(), p.y(), p.z());
          objThree.quaternion.set(q.x(), q.y(), q.z(), q.w());
        }
      }
    }
  }
};
export {
  exampleInfo as default
};
ts
import { Loader, TypedArray } from 'three';
/**
 * 官网示例的多端使用封装把版本
 * */
export interface OfficialExampleInfo extends MiniProgramMeta {
  /*** 示例名称(保持和官网一致)*/
  name: string;
  /** main */
  init: (context: LoadContext) => void;
}

export interface LoadContext {
  window: EventTarget & { innerWidth: number; innerHeight: number; devicePixelRatio: number };
  /** HTMLCanvasElement */
  canvas: any;
  /** https://www.npmjs.com/package/lil-gui */
  GUI: any;
  /**
   * https://www.npmjs.com/package/stats.js
   * 也可以使用其他受支持的版本
   * */
  Stats: any;
  /** 收集需要 dispose 的对象(官方示例没有处理这部分)*/
  needToDispose: (...objs: any[]) => void | ((fromFn: () => any[]) => void);

  /**基于 raq 的通用封装 */
  useFrame(animateFunc: (/** ms */ delta: number) => void): { cancel: () => void };

  /** 显示加载模态框 */
  requestLoading(text?: string): Promise<void>;

  /** 隐藏加载模态框*/
  cancelLoading(): void;

  /** 保存文件的通用封装*/
  saveFile(
    fileName: string,
    data: ArrayBuffer | TypedArray | DataView | string
  ): Promise<string | null>;

  /** 示例使用 DracoDecoder 时的资源路径 */
  DecoderPath: {
    GLTF: string;
    STANDARD: string;
  };

  /** 为资源路径拼上 CDN 前缀 */
  withCDNPrefix(path: string): string;

  /**
   * 在小程序中应使用 import { VideoTexture } from '@minisheep/three-platform-adapter/override/jsm/textures/VideoTexture.js';
   * 正常情况(web) 可直接使用 THREE.VideoTexture
   * */
  getVideoTexture(videoOptions: VideoOptions): Promise<[{ isVideoTexture: true }, video: any]>;

  /**
   * 在小程序中应使用 import { CameraTexture } from '@minisheep/three-platform-adapter/override/jsm/textures/CameraTexture.js';
   * 正常情况(web) 可参考示例 webgl_materials_video_webcam
   * */
  getCameraTexture(): { isVideoTexture: true };

  /** 用于动态修改 info 中的占位符*/
  bindInfoText(template: `$${string}$`, initValue?: string): { value: string };

  /** 分屏控件对应的事件回调 */
  onSlideStart(handle: () => void): void;
  /** 分屏控件对应的事件回调 */
  onSlideEnd(handle: () => void): void;
  /** 分屏控件对应的事件回调 */
  onSlideChange(handle: (offset: number, boxSize: number) => void): void;
}

export type VideoOptions = {
  src: string;
  /** 相当于 HTMLVideoElement 的 naturalWidth (小程序中获取不到)*/
  width: number;
  /** 相当于 HTMLVideoElement 的 naturalHeight (小程序中获取不到)*/
  height: number;
  loop?: boolean;
  autoplay?: boolean;
  muted?: boolean;
};

/** 示例小程序中使用的一些配置 */
export interface MiniProgramMeta {
  /** 用于统计加载相关信息 */
  useLoaders: Record<string, Loader>;
  /** 通用 info */
  info: TagItem[][];
  /** 特殊 info */
  infoPanel?: {
    left?: [string, string][];
    right?: [string, string][];
  };
  /** 分屏控件配置 */
  needSlider?: {
    /** 方向 */
    direction?: 'horizontal' | 'vertical';
    /** 初始偏移 0-100 */
    initPosition?: number;
  };
  /** 操作摇杆控件 */
  needArrowControls?: boolean;
  /** 默认需要的画布类型 */
  canvasType?: '2d' | 'webgl' | 'webgl2';
  /** 为保持效果一致所需要的画布样式 */
  canvasStyle?: {
    bgColor?: string;
    width?: number | string;
    height?: number | string;
  };
  /** 部分示例需要在加载前进行一些提示 */
  initAfterConfirm?: {
    /**
     * 提示类型
     * @default 'default'
     * */
    type?: 'warning' | 'default';
    text: string[];
  };
}

export interface BaseTag<T extends string> {
  tag: T;
  content: string;
}

export interface ATag extends BaseTag<'a'> {
  link: string;
}

export type TextTag = BaseTag<'text'>;

export type TagItem = TextTag | ATag;