Skip to content

physics_ammo_cloth

对应 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_cloth",
  useLoaders: [],
  needArrowControls: true,
  info: [
    [
      {
        tag: "text",
        content: "Ammo.js physics soft body cloth 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 physicsWorld;
    const rigidBodies = [];
    const margin = 0.05;
    let hinge;
    let cloth;
    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(-12, 7, 4);
      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(-7, 10, 15);
      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;
      light.shadow.bias = -3e-3;
      scene.add(light);
      stats = new Stats(renderer);
      window.addEventListener("resize", onWindowResize);
      needToDispose(renderer, scene, controls);
    }
    function initPhysics() {
      const collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
      const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
      const broadphase = new Ammo.btDbvtBroadphase();
      const solver = new Ammo.btSequentialImpulseConstraintSolver();
      const 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 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 clothWidth = 4;
      const clothHeight = 3;
      const clothNumSegmentsZ = clothWidth * 5;
      const clothNumSegmentsY = clothHeight * 5;
      const clothPos = new THREE.Vector3(-3, 3, 2);
      const clothGeometry = new THREE.PlaneGeometry(
        clothWidth,
        clothHeight,
        clothNumSegmentsZ,
        clothNumSegmentsY
      );
      clothGeometry.rotateY(Math.PI * 0.5);
      clothGeometry.translate(
        clothPos.x,
        clothPos.y + clothHeight * 0.5,
        clothPos.z - clothWidth * 0.5
      );
      const clothMaterial = new THREE.MeshLambertMaterial({
        color: 16777215,
        side: THREE.DoubleSide
      });
      cloth = new THREE.Mesh(clothGeometry, clothMaterial);
      cloth.castShadow = true;
      cloth.receiveShadow = true;
      scene.add(cloth);
      textureLoader.load("textures/grid.png", function(texture) {
        texture.colorSpace = THREE.SRGBColorSpace;
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(clothNumSegmentsZ, clothNumSegmentsY);
        cloth.material.map = texture;
        cloth.material.needsUpdate = true;
      });
      const softBodyHelpers = new Ammo.btSoftBodyHelpers();
      const clothCorner00 = new Ammo.btVector3(clothPos.x, clothPos.y + clothHeight, clothPos.z);
      const clothCorner01 = new Ammo.btVector3(
        clothPos.x,
        clothPos.y + clothHeight,
        clothPos.z - clothWidth
      );
      const clothCorner10 = new Ammo.btVector3(clothPos.x, clothPos.y, clothPos.z);
      const clothCorner11 = new Ammo.btVector3(clothPos.x, clothPos.y, clothPos.z - clothWidth);
      const clothSoftBody = softBodyHelpers.CreatePatch(
        physicsWorld.getWorldInfo(),
        clothCorner00,
        clothCorner01,
        clothCorner10,
        clothCorner11,
        clothNumSegmentsZ + 1,
        clothNumSegmentsY + 1,
        0,
        true
      );
      const sbConfig = clothSoftBody.get_m_cfg();
      sbConfig.set_viterations(10);
      sbConfig.set_piterations(10);
      clothSoftBody.setTotalMass(0.9, false);
      Ammo.castObject(clothSoftBody, Ammo.btCollisionObject).getCollisionShape().setMargin(margin * 3);
      physicsWorld.addSoftBody(clothSoftBody, 1, -1);
      cloth.userData.physicsBody = clothSoftBody;
      clothSoftBody.setActivationState(4);
      const armMass = 2;
      const armLength = 3 + clothWidth;
      const pylonHeight = clothPos.y + clothHeight;
      const baseMaterial = new THREE.MeshPhongMaterial({ color: 6316128 });
      pos.set(clothPos.x, 0.1, clothPos.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(clothPos.x, 0.5 * pylonHeight, clothPos.z - armLength);
      const pylon = createParalellepiped(0.4, pylonHeight, 0.4, 0, pos, quat, baseMaterial);
      pylon.castShadow = true;
      pylon.receiveShadow = true;
      pos.set(clothPos.x, pylonHeight + 0.2, clothPos.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 = 0.5;
      clothSoftBody.appendAnchor(0, arm.userData.physicsBody, false, influence);
      clothSoftBody.appendAnchor(clothNumSegmentsZ, arm.userData.physicsBody, false, influence);
      const pivotA = new Ammo.btVector3(0, pylonHeight * 0.5, 0);
      const pivotB = new Ammo.btVector3(0, -0.2, -7 * 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) {
          // Q
          case "ArrowLeft":
            armMovement = -1;
            break;
          // A
          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, 0.8 * armMovement, 50);
      physicsWorld.stepSimulation(deltaTime, 10);
      const softBody = cloth.userData.physicsBody;
      const clothPositions = cloth.geometry.attributes.position.array;
      const numVerts = clothPositions.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();
        clothPositions[indexFloat++] = nodePos.x();
        clothPositions[indexFloat++] = nodePos.y();
        clothPositions[indexFloat++] = nodePos.z();
      }
      cloth.geometry.computeVertexNormals();
      cloth.geometry.attributes.position.needsUpdate = true;
      cloth.geometry.attributes.normal.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 {
  //为了减少官方代码的改动,实际上等同于 canvas
  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: 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;