Skip to content

physics_ammo_volume

对应 three.js 示例地址

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

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

/** @type {import("@minisheeep/mp-three-examples").OfficialExampleInfo} */
const exampleInfo = {
  name: "physics_ammo_volume",
  useLoaders: [],
  info: [
    [
      {
        tag: "text",
        content: "Ammo.js physics soft body volume demo"
      }
    ],
    [
      {
        tag: "text",
        content: "Click to throw a ball"
      }
    ]
  ],
  init: ({ window, canvas, GUI, Stats, needToDispose, useFrame }) => {
    let stats;
    let camera, controls, scene, renderer;
    let textureLoader;
    const clock = new THREE.Clock();
    let clickRequest = false;
    const mouseCoords = new THREE.Vector2();
    const raycaster = new THREE.Raycaster();
    const ballMaterial = new THREE.MeshPhongMaterial({ color: 2105376 });
    const pos = new THREE.Vector3();
    const quat = new THREE.Quaternion();
    const gravityConstant = -9.8;
    let physicsWorld;
    const rigidBodies = [];
    const softBodies = [];
    const margin = 0.05;
    let transformAux1;
    let softBodyHelpers;
    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 = 20;
      light.shadow.camera.left = -20;
      light.shadow.camera.right = d;
      light.shadow.camera.top = d;
      light.shadow.camera.bottom = -20;
      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() {
      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();
      softBodyHelpers = new Ammo.btSoftBodyHelpers();
    }
    function createObjects() {
      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 volumeMass = 15;
      const sphereGeometry = new THREE.SphereGeometry(1.5, 40, 25);
      sphereGeometry.translate(5, 5, 0);
      createSoftVolume(sphereGeometry, volumeMass, 250);
      const boxGeometry = new THREE.BoxGeometry(1, 1, 5, 4, 4, 20);
      boxGeometry.translate(-2, 5, 0);
      createSoftVolume(boxGeometry, volumeMass, 120);
      pos.set(3, 1, 0);
      quat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), 30 * Math.PI / 180);
      const obstacle = createParalellepiped(
        10,
        1,
        4,
        0,
        pos,
        quat,
        new THREE.MeshPhongMaterial({ color: 6316128 })
      );
      obstacle.castShadow = true;
      obstacle.receiveShadow = true;
    }
    function processGeometry(bufGeometry) {
      const posOnlyBufGeometry = new THREE.BufferGeometry();
      posOnlyBufGeometry.setAttribute("position", bufGeometry.getAttribute("position"));
      posOnlyBufGeometry.setIndex(bufGeometry.getIndex());
      const indexedBufferGeom = BufferGeometryUtils.mergeVertices(posOnlyBufGeometry);
      mapIndices(bufGeometry, indexedBufferGeom);
    }
    function isEqual(x1, y1, z1, x2, y2, z2) {
      const delta = 1e-6;
      return Math.abs(x2 - x1) < delta && Math.abs(y2 - y1) < delta && Math.abs(z2 - z1) < delta;
    }
    function mapIndices(bufGeometry, indexedBufferGeom) {
      const vertices = bufGeometry.attributes.position.array;
      const idxVertices = indexedBufferGeom.attributes.position.array;
      const indices = indexedBufferGeom.index.array;
      const numIdxVertices = idxVertices.length / 3;
      const numVertices = vertices.length / 3;
      bufGeometry.ammoVertices = idxVertices;
      bufGeometry.ammoIndices = indices;
      bufGeometry.ammoIndexAssociation = [];
      for (let i = 0; i < numIdxVertices; i++) {
        const association = [];
        bufGeometry.ammoIndexAssociation.push(association);
        const i3 = i * 3;
        for (let j = 0; j < numVertices; j++) {
          const j3 = j * 3;
          if (isEqual(
            idxVertices[i3],
            idxVertices[i3 + 1],
            idxVertices[i3 + 2],
            vertices[j3],
            vertices[j3 + 1],
            vertices[j3 + 2]
          )) {
            association.push(j3);
          }
        }
      }
    }
    function createSoftVolume(bufferGeom, mass, pressure) {
      processGeometry(bufferGeom);
      const volume = new THREE.Mesh(bufferGeom, new THREE.MeshPhongMaterial({ color: 16777215 }));
      volume.castShadow = true;
      volume.receiveShadow = true;
      volume.frustumCulled = false;
      scene.add(volume);
      textureLoader.load("textures/colors.png", function(texture) {
        volume.material.map = texture;
        volume.material.needsUpdate = true;
      });
      const volumeSoftBody = softBodyHelpers.CreateFromTriMesh(
        physicsWorld.getWorldInfo(),
        bufferGeom.ammoVertices,
        bufferGeom.ammoIndices,
        bufferGeom.ammoIndices.length / 3,
        true
      );
      const sbConfig = volumeSoftBody.get_m_cfg();
      sbConfig.set_viterations(40);
      sbConfig.set_piterations(40);
      sbConfig.set_collisions(17);
      sbConfig.set_kDF(0.1);
      sbConfig.set_kDP(0.01);
      sbConfig.set_kPR(pressure);
      volumeSoftBody.get_m_materials().at(0).set_m_kLST(0.9);
      volumeSoftBody.get_m_materials().at(0).set_m_kAST(0.9);
      volumeSoftBody.setTotalMass(mass, false);
      Ammo.castObject(volumeSoftBody, Ammo.btCollisionObject).getCollisionShape().setMargin(margin);
      physicsWorld.addSoftBody(volumeSoftBody, 1, -1);
      volume.userData.physicsBody = volumeSoftBody;
      volumeSoftBody.setActivationState(4);
      softBodies.push(volume);
    }
    function createParalellepiped(sx, sy, sz, mass, pos2, quat2, 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, pos2, quat2);
      return threeObject;
    }
    function createRigidBody(threeObject, physicsShape, mass, pos2, quat2) {
      threeObject.position.copy(pos2);
      threeObject.quaternion.copy(quat2);
      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);
      threeObject.userData.physicsBody = body;
      scene.add(threeObject);
      if (mass > 0) {
        rigidBodies.push(threeObject);
        body.setActivationState(4);
      }
      physicsWorld.addRigidBody(body);
      return body;
    }
    function initInput() {
      canvas.addEventListener("pointerdown", function(event) {
        if (!clickRequest) {
          mouseCoords.set(
            event.clientX / window.innerWidth * 2 - 1,
            -(event.clientY / window.innerHeight) * 2 + 1
          );
          clickRequest = true;
        }
      });
    }
    function processClick() {
      if (clickRequest) {
        raycaster.setFromCamera(mouseCoords, camera);
        const ballMass = 3;
        const ballRadius = 0.4;
        const ball = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 18, 16), 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);
        ballBody.setFriction(0.5);
        pos.copy(raycaster.ray.direction);
        pos.multiplyScalar(14);
        ballBody.setLinearVelocity(new Ammo.btVector3(pos.x, pos.y, pos.z));
        clickRequest = false;
      }
    }
    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);
      processClick();
      renderer.render(scene, camera);
    }
    function updatePhysics(deltaTime) {
      physicsWorld.stepSimulation(deltaTime, 10);
      for (let i = 0, il = softBodies.length; i < il; i++) {
        const volume = softBodies[i];
        const geometry = volume.geometry;
        const softBody = volume.userData.physicsBody;
        const volumePositions = geometry.attributes.position.array;
        const volumeNormals = geometry.attributes.normal.array;
        const association = geometry.ammoIndexAssociation;
        const numVerts = association.length;
        const nodes = softBody.get_m_nodes();
        for (let j = 0; j < numVerts; j++) {
          const node = nodes.at(j);
          const nodePos = node.get_m_x();
          const x = nodePos.x();
          const y = nodePos.y();
          const z = nodePos.z();
          const nodeNormal = node.get_m_n();
          const nx = nodeNormal.x();
          const ny = nodeNormal.y();
          const nz = nodeNormal.z();
          const assocVertex = association[j];
          for (let k = 0, kl = assocVertex.length; k < kl; k++) {
            let indexVertex = assocVertex[k];
            volumePositions[indexVertex] = x;
            volumeNormals[indexVertex] = nx;
            indexVertex++;
            volumePositions[indexVertex] = y;
            volumeNormals[indexVertex] = ny;
            indexVertex++;
            volumePositions[indexVertex] = z;
            volumeNormals[indexVertex] = nz;
          }
        }
        geometry.attributes.position.needsUpdate = true;
        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;