Author – Oasis team – Tong Lun

The introduction

Physics engines are a very important part of game development. In everyone’s concept, it seems that physics has not been used in many games, Newton’s theorem I learned in junior high school has long been forgotten, so what is the role of physics engine? In fact, in physics engines typically used in games, the physics process is sparse rather than dense, i.e. the physics engine does not consider fluids such as air and focuses on rigid objects. Therefore, a physics engine consists of two parts, namely:

  1. The trigger

  2. Physical response

In most games, there is trigger logic, for example, when a character runs forward, hits an obstacle and automatically jumps over it. In fact, the process involves several steps:

  1. Call the animation of the run

  2. Collision detection for each frame detects the character colliding with an obstacle, triggering an event

  1. The event calls a new animation, jump

  2. An event is emitted after the collision is over

  1. Call the falling animation

Among the front-end game engines, the most commonly used physics engines are ammo.js, which uses WebIDL to compile Bullet into WASM, and Cannon.js, which is a pure JavaScript physics engine. In terms of features, both are not as powerful as PhysX, which is widely used in the game industry. In Oasis, we use Embind, which is also Emscripten toolkit, to compile PhysX4.1 into WASM, and encapsulates physical components based on this to provide a series of physical capabilities including trigger, Raycast, sweep, rigid body, constraint body, vehicle, cloth and so on. In Oasis physics first: PhysX cross-platform compilation with PVD based on WebAssembly, we have described how to compile PhysX written in C++ into a.wasm binary and an easy to import JavaScript glue file. In this article, we will introduce how TO apply PhysX in Oasis and design a scalable physical component architecture for it so that more physical backends can be added in the future.

Overall architecture based on subcontracting

Since the.wasm file of PhysX is close to 2 megabytes, and there are many scenarios in which users do not need to deal with very complex physical events, but only need trigger and ray detection methods, it is not appropriate to rely on PhysX completely in some scenarios. Therefore, Oasis does subcontracting in the physical back end, that is, applying PhysX in scenarios requiring complex physical responses, and using the existing capabilities of the engine in simple scenarios, but both are encapsulated with an interface that allows seamless switching on demand. The final physical component of Oasis presents the following subcontracting structure:

There are three levels

  1. @ Core /physics: Engine holds a PhysicsManager object that implements Raycast and manages the creation and destruction of physical components.
  2. Design /physics: Pure interface package, bridge core package and concrete physics implementation
  1. @physics-XXX: various physics implementation packages that conform to the @design/physics interface
  • 3.1. Physics-physx: Physical components based on PhysX

  • 3.2. Physics-lite: Lightweight physics components, currently including colliders and raycast methods included in the Oasis 0.5 milestone

Based on physical subcontracting, this paper will introduce the implementation and call logic related to Oasis around PhysX’s own architecture.

Load the WASM

An optional parameter of type IPhysics was added to Engine initialization because of the multi-backend design, which optionally loads the physical package during Engine initialization. For code that requires physical components, write the code as:

const engine = new WebGLEngine("canvas", LitePhysics);
Copy the code

Since.wasm files must be loaded asynchronously and must be initialized before all physical components are initialized, PhysXPhyscis provides an initialization function, and all engine initialization logic must be written in the callback function, for example:

PhysXPhysics.init().then(() => {
  const engine = new WebGLEngine("canvas", PhysXPhysics);
})
Copy the code

The IPhysics passed into the engine is saved, forcing all member methods to be static through the decorator StaticInterfaceImplement, so that physical components can call on static methods directly to initialize them. For third-party physical packages implemented by users, the following constraints are also recommended:

@StaticInterfaceImplement<IPhysics>()
export class PhysXPhysics {}
Copy the code

The type system

In the realization process of Oasis physical components, the type system of PhysX is mainly referred to to organize interface design in @Design /physics. We then construct the concrete implementation and user component implementation in @core/physics based on the interface. In fact, this architecture is widely used in major physics engines such as Bullet, Box2d, etc. Therefore, this paper will focus on the type system of PhysX and show how they are organized together:

The figure above shows the specific logic combined by PhysX and Oasis. There are two levels of information in the figure:

  1. The red label represents the type of PhysX. From the construction of PxFoundation, PxShape, PxActor and PxScene are gradually constructed, and then the Simulate method or Raycast method is invoked in PxScene. In addition, the blue line represents the construction order of PhysX objects.
  2. The green TAB represents the type that the Oasis engine encapsulates, a ColliderShape for PxShape, a Collider for PxActor, and a PxScene for PhyscisManager. In addition, the red line represents the order in which Oasis invokes the engine algorithms, primarily by calling the SIMULATE method in the PxScene and then starting the events in the script.

One of the most fundamental abstractions to the above logic is that there are two types of Collider in a physical scene, Dynamic and Static. Each Collider is a container for a ColliderShape, and can therefore be combined to form a complex Collider shape. * Therefore, a standard creation code for physical components is as follows:

    const capsuleShape = new CapsuleColliderShape();
    capsuleShape.radius = radius;
    capsuleShape.height = height;

    const capsuleCollider = cubeEntity.addComponent(StaticCollider);
    capsuleCollider.addShape(capsuleShape);
Copy the code

Based on this, we replanned the interface of physical components and made the following design:

ColliderShape

The first step in constructing a physical component is to create a ColliderShape. In Oasis, a ColliderShape represents the shape of the collider, including Plane, Box, Sphere, and Capsule. Each ColliderShape can set a local position and scale. Since a Collider can bind to multiple Shapes and belong to a collection of Shapes, the position and rotation properties of all Shapes are relative to the Collider.

The interface layer IColliderShape

In line with the above principles, our interface layer contains the following methods:

export interface IColliderShape {
  /**
   * Set unique id of the collider shape.
   * @param id - The unique index
   */
  setUniqueID(id: number): void;

  /**
   * Set local position.
   * @param position - The local position
   */
  setPosition(position: Vector3): void;

  /**
   * Set world scale of shape.
   * @param scale - The scale
   */
  setWorldScale(scale: Vector3): void;
}
Copy the code

Each ColliderShape has a unique ID that Bridges the ColliderShape object in the physics engine with the Entity in the Oasis engine. In PhysX, events are triggered around Shape. Therefore, by using this ID, we can quickly know which two entities have collision events, so that corresponding components, such as scripts, can be executed.

Collider

This adds up to creating a Collider component, which is the only component the user needs to create on a physical system. Collider itself has no shape, but comes in two types:

  1. StaticCollider: corresponding to a PxStaticActor that doesn’t move over time and is used as a trigger or StaticCollider.
  2. DynamicCollider: Corresponds to a PxDynamicActor that moves over time, is controlled by the user, or responds to physical motion.

Position and rotation can also be set on a Collider, but in Oasis these properties are not visible to the user. The rendering engine automatically synchronizes with the physics engine, and the user can only change the relative position on the ColliderShape or adjust the attitude of the Entity to control Collider movement.

The interface layer ICollider

Although the Collider’s stance is not exposed to the developer, the interface is still needed in the interface layer:

export interface ICollider {
  /**
   * Set global transform of collider.
   * @param position - The global position
   * @param rotation - The global rotation
   */
  setWorldTransform(position: Vector3, rotation: Quaternion): void;

  /**
   * Get global transform of collider.
   * @param outPosition - The global position
   * @param outRotation - The global rotation
   */
  getWorldTransform(outPosition: Vector3, outRotation: Quaternion): void;

  /**
   * Add collider shape on collider.
   * @param shape - The collider shape attached
   */
  addShape(shape: IColliderShape): void;

  /**
   * Remove collider shape on collider.
   * @param shape - The collider shape attached
   */
  removeShape(shape: IColliderShape): void;
}
Copy the code

PhysicsManager

Once a Collider has been created, it needs to be added to the physical scene. PhysicsManager manages the physical scene and updates the physical scene based on the actors it holds, such as constrains motion or triggers collision detection events. In Oasis, PhysicsMananger is constructed by default and is automatically added to PhysicsMananger for management when a Collider component is created. Also, PhysicsManage R includes radiographic detection methods:

engine.physicsManager.raycast(ray, Number.MAX_VALUE, Layer.Everything, hit);
Copy the code

The interface layer IPhysicsMananger

This interface defines the manager class for the physical scene, which includes:

export interface IPhysicsManager { /** * Add ICollider into the manager. * @param collider - StaticCollider or DynamicCollider. */ addCollider(collider: ICollider): void; /** * Remove ICollider. * @param collider - StaticCollider or DynamicCollider. */ removeCollider(collider: ICollider): void; /** * Call on every frame to update pose of objects. * @param elapsedTime - Step time of update. */ update(elapsedTime: number): void; /** * Casts a ray through the Scene and returns the first hit. * @param ray - The ray * @param distance - The max distance the ray should check * @param outHitResult - If true is returned, outHitResult will contain more detailed collision information * @returns Returns True if the ray intersects with a collider, otherwise false */ raycast( ray: Ray, distance: number, outHitResult? : (shapeUniqueID: number, distance: number, point: Vector3, normal: Vector3) => void ): boolean; }Copy the code

Physical and render synchronization logic

After this milestone refactoring, physical events are separated from the execution of other events to clarify the relationship between physical events and other scripted events:

if (scene) {
  componentsManager.callScriptOnStart();
  if (this.physicsManager) {
    componentsManager.callColliderOnUpdate();
    this.physicsManager.update(deltaTime);
    componentsManager.callColliderOnLateUpdate();
  }
  componentsManager.callScriptOnUpdate(deltaTime);
  componentsManager.callAnimationUpdate(deltaTime);
  componentsManager.callScriptOnLateUpdate(deltaTime);

  this._render(scene);
}
Copy the code

In general, the physics-related update logic is divided into three steps:

  1. CallColliderOnUpdate: First, if the Entity is manually updated by the user, such as executing script logic, then the new pose needs to be synchronized to the physics engine for summary purposes.
  2. Physicsmanager.update: Next, perform the physics engine’s update logic, including collision detection, triggering events, completing physical responses, and so on.
  1. CallColliderOnLateUpdate: Finally, for the DynamicCollider, its attitude is affected by the physical response, so it needs to resynchronize its attitude to the Entity so that the rendered object can move as well.

conclusion

This article briefly introduces the realization of physical components based on PhysX, which is divided into three layers. Users can realize their own physical bottom layer by implementing the interface in @Design/Physics. In fact, this interface oriented architecture model will also be applicable when new rendering apis such as WebGPU are introduced in the future, so that the engine and the underlying API can be completely separated and eventually run across platforms.

In terms of subcontracting logic, we designed ColliderShape, Collider, PhysicsManager and other component interfaces, implemented PhysX based physical back end, and provided a lightweight physical back end @physics-Lite to support lightweight scenarios.

In the next milestone, we’ll further enhance DynamicCollider, add physical responses, physical constraints, and add character controllers. It will be easy to develop pinball, shooting applications with these components, so stay tuned. Meanwhile, the Oasis Physics series will continue to introduce more content around PhysX and the physics engine. In Oasis Physics 3, we will continue to build on the Physics architecture introduced in this article, using the @physics-Lite package as an example to cover the technical details of the Physics engine implementation.

Write in the last

Welcome to star our Github warehouse. You can also pay attention to our subsequent V0.7 planning at any time, and give us your needs and questions in issues. Developers can join our tacks group to tease us and discuss a few questions:

Whether you’re in rendering, TA, Web front-end or gaming, if you’re looking for an oasis like us, please send your resume to [email protected]. Job description: www.yuque.com/oasis-engin…