Skip to content

physics_ammo_break

对应 three.js 示例地址

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

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

/** @type {import("@minisheeep/mp-three-examples").OfficialExampleInfo} */
const exampleInfo = {
  name: "physics_ammo_break",
  useLoaders: [],
  info: [
    [
      {
        tag: "text",
        content: "Physics threejs demo with convex objects breaking in real time"
      }
    ],
    [
      {
        tag: "text",
        content: "Press mouse to throw balls and move the camera."
      }
    ]
  ],
  init: ({ window, canvas, GUI, Stats, needToDispose, useFrame }) => {
    let stats;
    let camera, controls, scene, renderer;
    let textureLoader;
    const clock = new THREE.Clock();
    const mouseCoords = new THREE.Vector2();
    const raycaster = new THREE.Raycaster();
    const ballMaterial = new THREE.MeshPhongMaterial({ color: 2105376 });
    let collisionConfiguration;
    let dispatcher;
    let broadphase;
    let solver;
    let physicsWorld;
    const margin = 0.05;
    const convexBreaker = new ConvexObjectBreaker();
    const rigidBodies = [];
    const pos = new THREE.Vector3();
    const quat = new THREE.Quaternion();
    let transformAux1;
    let tempBtVec3_1;
    const objectsToRemove = [];
    for (let i = 0; i < 500; i++) {
      objectsToRemove[i] = null;
    }
    let numObjectsToRemove = 0;
    const impactPoint = new THREE.Vector3();
    const impactNormal = new THREE.Vector3();
    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(-14, 8, 16);
      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, 18, 5);
      light.castShadow = true;
      const d = 14;
      light.shadow.camera.left = -14;
      light.shadow.camera.right = d;
      light.shadow.camera.top = d;
      light.shadow.camera.bottom = -14;
      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.btDefaultCollisionConfiguration();
      dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
      broadphase = new Ammo.btDbvtBroadphase();
      solver = new Ammo.btSequentialImpulseConstraintSolver();
      physicsWorld = new Ammo.btDiscreteDynamicsWorld(
        dispatcher,
        broadphase,
        solver,
        collisionConfiguration
      );
      physicsWorld.setGravity(new Ammo.btVector3(0, -7.8, 0));
      transformAux1 = new Ammo.btTransform();
      tempBtVec3_1 = new Ammo.btVector3(0, 0, 0);
    }
    function createObject(mass, halfExtents, pos2, quat2, material) {
      const object = new THREE.Mesh(
        new THREE.BoxGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2),
        material
      );
      object.position.copy(pos2);
      object.quaternion.copy(quat2);
      convexBreaker.prepareBreakableObject(
        object,
        mass,
        new THREE.Vector3(),
        new THREE.Vector3(),
        true
      );
      createDebrisFromBreakableObject(object);
    }
    function createObjects() {
      pos.set(0, -0.5, 0);
      quat.set(0, 0, 0, 1);
      const ground = createParalellepipedWithPhysics(
        40,
        1,
        40,
        0,
        pos,
        quat,
        new THREE.MeshPhongMaterial({ color: 16777215 })
      );
      ground.receiveShadow = true;
      textureLoader.load("textures/grid.png", function(texture) {
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(40, 40);
        ground.material.map = texture;
        ground.material.needsUpdate = true;
      });
      const towerMass = 1e3;
      const towerHalfExtents = new THREE.Vector3(2, 5, 2);
      pos.set(-8, 5, 0);
      quat.set(0, 0, 0, 1);
      createObject(towerMass, towerHalfExtents, pos, quat, createMaterial(11546644));
      pos.set(8, 5, 0);
      quat.set(0, 0, 0, 1);
      createObject(towerMass, towerHalfExtents, pos, quat, createMaterial(11547156));
      const bridgeMass = 100;
      const bridgeHalfExtents = new THREE.Vector3(7, 0.2, 1.5);
      pos.set(0, 10.2, 0);
      quat.set(0, 0, 0, 1);
      createObject(bridgeMass, bridgeHalfExtents, pos, quat, createMaterial(11778149));
      const stoneMass = 120;
      const stoneHalfExtents = new THREE.Vector3(1, 2, 0.15);
      const numStones = 8;
      quat.set(0, 0, 0, 1);
      for (let i = 0; i < numStones; i++) {
        pos.set(0, 2, 15 * (0.5 - i / (numStones + 1)));
        createObject(stoneMass, stoneHalfExtents, pos, quat, createMaterial(11579568));
      }
      const mountainMass = 860;
      const mountainHalfExtents = new THREE.Vector3(4, 5, 4);
      pos.set(5, mountainHalfExtents.y * 0.5, -7);
      quat.set(0, 0, 0, 1);
      const mountainPoints = [];
      mountainPoints.push(
        new THREE.Vector3(mountainHalfExtents.x, -mountainHalfExtents.y, mountainHalfExtents.z)
      );
      mountainPoints.push(
        new THREE.Vector3(-mountainHalfExtents.x, -mountainHalfExtents.y, mountainHalfExtents.z)
      );
      mountainPoints.push(
        new THREE.Vector3(mountainHalfExtents.x, -mountainHalfExtents.y, -mountainHalfExtents.z)
      );
      mountainPoints.push(
        new THREE.Vector3(-mountainHalfExtents.x, -mountainHalfExtents.y, -mountainHalfExtents.z)
      );
      mountainPoints.push(new THREE.Vector3(0, mountainHalfExtents.y, 0));
      const mountain = new THREE.Mesh(new ConvexGeometry(mountainPoints), createMaterial(11548692));
      mountain.position.copy(pos);
      mountain.quaternion.copy(quat);
      convexBreaker.prepareBreakableObject(
        mountain,
        mountainMass,
        new THREE.Vector3(),
        new THREE.Vector3(),
        true
      );
      createDebrisFromBreakableObject(mountain);
    }
    function createParalellepipedWithPhysics(sx, sy, sz, mass, pos2, quat2, material) {
      const object = 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(object, shape, mass, pos2, quat2);
      return object;
    }
    function createDebrisFromBreakableObject(object) {
      object.castShadow = true;
      object.receiveShadow = true;
      const shape = createConvexHullPhysicsShape(object.geometry.attributes.position.array);
      shape.setMargin(margin);
      const body = createRigidBody(
        object,
        shape,
        object.userData.mass,
        null,
        null,
        object.userData.velocity,
        object.userData.angularVelocity
      );
      const btVecUserData = new Ammo.btVector3(0, 0, 0);
      btVecUserData.threeObject = object;
      body.setUserPointer(btVecUserData);
    }
    function removeDebris(object) {
      scene.remove(object);
      physicsWorld.removeRigidBody(object.userData.physicsBody);
    }
    function createConvexHullPhysicsShape(coords) {
      const shape = new Ammo.btConvexHullShape();
      for (let i = 0, il = coords.length; i < il; i += 3) {
        tempBtVec3_1.setValue(coords[i], coords[i + 1], coords[i + 2]);
        const lastOne = i >= il - 3;
        shape.addPoint(tempBtVec3_1, lastOne);
      }
      return shape;
    }
    function createRigidBody(object, physicsShape, mass, pos2, quat2, vel, angVel) {
      if (pos2) {
        object.position.copy(pos2);
      } else {
        pos2 = object.position;
      }
      if (quat2) {
        object.quaternion.copy(quat2);
      } else {
        quat2 = object.quaternion;
      }
      const transform = new Ammo.btTransform();
      transform.setIdentity();
      transform.setOrigin(new Ammo.btVector3(pos2.x, pos2.y, pos2.z));
      transform.setRotation(new Ammo.btQuaternion(quat2.x, quat2.y, quat2.z, quat2.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);
      body.setFriction(0.5);
      if (vel) {
        body.setLinearVelocity(new Ammo.btVector3(vel.x, vel.y, vel.z));
      }
      if (angVel) {
        body.setAngularVelocity(new Ammo.btVector3(angVel.x, angVel.y, angVel.z));
      }
      object.userData.physicsBody = body;
      object.userData.collided = false;
      scene.add(object);
      if (mass > 0) {
        rigidBodies.push(object);
        body.setActivationState(4);
      }
      physicsWorld.addRigidBody(body);
      return body;
    }
    function createRandomColor() {
      return Math.floor(Math.random() * (1 << 24));
    }
    function createMaterial(color) {
      color = color || createRandomColor();
      return new THREE.MeshPhongMaterial({ color });
    }
    function initInput() {
      canvas.addEventListener("pointerdown", function(event) {
        mouseCoords.set(
          event.clientX / window.innerWidth * 2 - 1,
          -(event.clientY / window.innerHeight) * 2 + 1
        );
        raycaster.setFromCamera(mouseCoords, camera);
        const ballMass = 35;
        const ballRadius = 0.4;
        const ball = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 14, 10), ballMaterial);
        ball.castShadow = true;
        ball.receiveShadow = true;
        const ballShape = new Ammo.btSphereShape(ballRadius);
        ballShape.setMargin(margin);
        pos.copy(raycaster.ray.direction);
        pos.add(raycaster.ray.origin);
        quat.set(0, 0, 0, 1);
        const ballBody = createRigidBody(ball, ballShape, ballMass, pos, quat);
        pos.copy(raycaster.ray.direction);
        pos.multiplyScalar(24);
        ballBody.setLinearVelocity(new Ammo.btVector3(pos.x, pos.y, pos.z));
      });
    }
    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) {
      physicsWorld.stepSimulation(deltaTime, 10);
      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());
          objThree.userData.collided = false;
        }
      }
      for (let i = 0, il = dispatcher.getNumManifolds(); i < il; i++) {
        const contactManifold = dispatcher.getManifoldByIndexInternal(i);
        const rb0 = Ammo.castObject(contactManifold.getBody0(), Ammo.btRigidBody);
        const rb1 = Ammo.castObject(contactManifold.getBody1(), Ammo.btRigidBody);
        const threeObject0 = Ammo.castObject(rb0.getUserPointer(), Ammo.btVector3).threeObject;
        const threeObject1 = Ammo.castObject(rb1.getUserPointer(), Ammo.btVector3).threeObject;
        if (!threeObject0 && !threeObject1) {
          continue;
        }
        const userData0 = threeObject0 ? threeObject0.userData : null;
        const userData1 = threeObject1 ? threeObject1.userData : null;
        const breakable0 = userData0 ? userData0.breakable : false;
        const breakable1 = userData1 ? userData1.breakable : false;
        const collided0 = userData0 ? userData0.collided : false;
        const collided1 = userData1 ? userData1.collided : false;
        if (!breakable0 && !breakable1 || collided0 && collided1) {
          continue;
        }
        let contact = false;
        let maxImpulse = 0;
        for (let j = 0, jl = contactManifold.getNumContacts(); j < jl; j++) {
          const contactPoint = contactManifold.getContactPoint(j);
          if (contactPoint.getDistance() < 0) {
            contact = true;
            const impulse = contactPoint.getAppliedImpulse();
            if (impulse > maxImpulse) {
              maxImpulse = impulse;
              const pos2 = contactPoint.get_m_positionWorldOnB();
              const normal = contactPoint.get_m_normalWorldOnB();
              impactPoint.set(pos2.x(), pos2.y(), pos2.z());
              impactNormal.set(normal.x(), normal.y(), normal.z());
            }
            break;
          }
        }
        if (!contact) continue;
        const fractureImpulse = 250;
        if (breakable0 && !collided0 && maxImpulse > fractureImpulse) {
          const debris = convexBreaker.subdivideByImpact(
            threeObject0,
            impactPoint,
            impactNormal,
            1,
            2,
            1.5
          );
          const numObjects = debris.length;
          for (let j = 0; j < numObjects; j++) {
            const vel = rb0.getLinearVelocity();
            const angVel = rb0.getAngularVelocity();
            const fragment = debris[j];
            fragment.userData.velocity.set(vel.x(), vel.y(), vel.z());
            fragment.userData.angularVelocity.set(angVel.x(), angVel.y(), angVel.z());
            createDebrisFromBreakableObject(fragment);
          }
          objectsToRemove[numObjectsToRemove++] = threeObject0;
          userData0.collided = true;
        }
        if (breakable1 && !collided1 && maxImpulse > fractureImpulse) {
          const debris = convexBreaker.subdivideByImpact(
            threeObject1,
            impactPoint,
            impactNormal,
            1,
            2,
            1.5
          );
          const numObjects = debris.length;
          for (let j = 0; j < numObjects; j++) {
            const vel = rb1.getLinearVelocity();
            const angVel = rb1.getAngularVelocity();
            const fragment = debris[j];
            fragment.userData.velocity.set(vel.x(), vel.y(), vel.z());
            fragment.userData.angularVelocity.set(angVel.x(), angVel.y(), angVel.z());
            createDebrisFromBreakableObject(fragment);
          }
          objectsToRemove[numObjectsToRemove++] = threeObject1;
          userData1.collided = true;
        }
      }
      for (let i = 0; i < numObjectsToRemove; i++) {
        removeDebris(objectsToRemove[i]);
      }
      numObjectsToRemove = 0;
    }
  }
};
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;