crashcat

repository·main·Indexed 19 days ago

https://github.com/isaac-mason/crashcat

A pure JavaScript 3D physics engine for games, simulations, and creative websites. crashcat is engine-agnostic and tree-shakeable, supporting rigid body dynamics, convex shapes, triangle meshes, and a variety of constraints (hinge, slider, distance, etc.). It features Continuous Collision Detection (CCD), broadphase spatial acceleration using dynamic BVH, and a two-tier layer system for efficient collision filtering. Version 0.0.5.

Tokens
44.6K
Snippets
118
Records
156
Agent score
65%

What's inside crashcat

  1. Overview of crashcat features

    main

    crashcat is a pure JavaScript physics engine designed for games, simulations, and creative websites. It is built to be highly tree-shakeable, allowing developers to only include the features they use. It is engine-agnostic and can be integrated with libraries like Babylon.js, PlayCanvas, Three.js, or custom engines.

    Core Capabilities:

    • Rigid Body Simulation: Full support for rigid body dynamics.
    • Shape Support: Convex shapes, triangle meshes, and custom shapes.
    • Constraints: Includes hinge, slider, distance, point, fixed, cone, swing-twist, and six-dof constraints, all supporting motors and springs.
    • Collision Detection: Continuous Collision Detection (CCD) for high-speed objects and broadphase spatial acceleration using dynamic BVH.
    • Advanced Features: Flexible collision filtering, rigid body sleeping, sensor rigid bodies, and hooks for intercepting/modifying physics events.
  2. Configure Collision Filtering with Groups and Masks

    main

    Collision filtering allows you to control which bodies collide using 32-bit bitmasks. This works in conjunction with objectLayer filtering; both must pass for a collision to occur.

    A collision occurs only if: (groupA & maskB) != 0 AND (groupB & maskA) != 0

    Use the bitmask.createFlags helper to define named groups easily.

    const GROUPS = bitmask.createFlags(['player', 'enemy', 'debris', 'projectile'] as const);
    
    // player collides with enemies and projectiles, but not debris
    const playerBody = rigidBody.create(world, {
        shape: sphere.create({ radius: 1 }),
        motionType: MotionType.DYNAMIC,
        objectLayer: OBJECT_LAYER_MOVING,
        collisionGroups: GROUPS.player,
        collisionMask: GROUPS.enemy | GROUPS.projectile,
    });
  3. crashcat Units and Coordinate System

    main

    crashcat uses SI units and OpenGL conventions. It is critical to scale your objects correctly to ensure realistic physics behavior.

    ConceptUnit / Convention
    Lengthmeters (m)
    Masskilograms (kg)
    Timeseconds (s)
    Forcenewtons (N)
    Gravity-9.81 m/s² (default)
    Coordinate SystemOpenGL right-handed (positive y is "up")
    Triangle Windingcounter-clockwise (CCW) for front face

    Warning on Scale: Large values for halfExtents result in massive objects. For example, a box with halfExtents: [100, 100, 100] is a 200-meter cube, which will appear to fall very slowly due to its scale.

  4. Achieve determinism in physics simulations

    main

    While updateWorld has implementation considerations for determinism, you must ensure the following to achieve consistent results:

    1. Exact same initial state: All world settings, bodies, constraints, and shapes must match exactly.
    2. Exact same order of operations: Adding and removing bodies/constraints must happen in the same order every time.
    3. Same update sequence: Call updateWorld with identical delta times and in the same order relative to other game logic.

    Avoid: Non-deterministic data structures (like Set or Map without careful handling), varying delta time steps, or environment-dependent math functions like Math.sin.

  5. Use Kinematic Character Controllers (KCC)

    main

    For player characters requiring precise movement, use the built-in kcc API. KCCs provide features like sliding along walls, stair stepping, slope handling, and interaction with moving platforms.

    Note on Visibility: By default, a KCC is not a rigid body and is not visible to raycasts or collision queries. If you need the character to be detectable (e.g., for AI line-of-sight or sensors), use the kcc API to create an inner rigid body that follows the KCC's movement.

  6. When to use crashcat vs WASM physics engines

    main

    Crashcat is a pure JavaScript physics engine. Choose it over WASM-based engines (like Rapier or JoltPhysics.js) if:

    • Bundle size is a priority: Crashcat is highly tree-shakeable and avoids the megabyte-sized payloads and initialization latency of WASM.
    • Frequent JS callbacks are required: If your logic relies heavily on collision callbacks, contact modification, or custom character behavior, crashcat avoids the expensive WASM $\leftrightarrow$ JavaScript boundary crossings.
    • Simplicity is preferred: It uses standard JavaScript objects that are easy to inspect, debug, and serialize without manual memory management.
    • Moderate complexity: It can handle hundreds of dynamic bodies at 60 Hz on typical desktops.

    Choose WASM instead if you require absolute maximum performance for extremely large, complex simulations or need multithreading capabilities.

  7. Manage multiple physics worlds

    main
    While the shape and constraints registry is global, you can create multiple independent physics worlds. This allows for scenarios like a spaceship simulation in one world (with specific gravity/scale) and the characters inside that ship in a separate world.
  8. How Broadphase and Object Layers work

    main

    crashcat uses a two-tier layer system to manage collision detection efficiently:

    1. Broadphase Layers

    Broadphase layers partition space using spatial acceleration structures (dynamic BVH trees). Each broadphase layer has its own tree. Bodies in different broadphase layers do not check for collisions against each other via the same tree, which improves performance.

    • Usage: Use addBroadphaseLayer(worldSettings) to create a layer.
    • Strategy: A common approach is to have a "moving" layer and a "not moving" layer. Advanced users might separate by update frequency (e.g., static terrain vs. dynamic debris).
    • Constraint: Each body belongs to exactly one broadphase layer.

    2. Object Layers

    Object layers control collision filtering (which specific types of objects hit each other). Every object layer must belong to exactly one broadphase layer.

    • Usage: Use addObjectLayer(worldSettings, broadphaseLayer) to create a layer.
    • Collision Rules: Use enableCollision(worldSettings, layerA, layerB) to define if objects in layerA can collide with objects in layerB.
    • Example: You can define that "projectiles" hit "enemies" but do not hit other "projectiles".
    const worldSettings = createWorldSettings();
    
    // Define Broadphase
    const LAYER_MOVING_BP = addBroadphaseLayer(worldSettings);
    const LAYER_STATIC_BP = addBroadphaseLayer(worldSettings);
    
    // Define Object Layers
    const PLAYER_OBJ = addObjectLayer(worldSettings, LAYER_MOVING_BP);
    const WALL_OBJ = addObjectLayer(worldSettings, LAYER_STATIC_BP);
    
    // Define Collision Rules
    enableCollision(worldSettings, PLAYER_OBJ, WALL_OBJ);
  9. Specify constraint attachment points in World or Local space

    main

    When creating constraints, you can define attachment points (pointA, pointB) relative to the world or the body's center of mass:

    • ConstraintSpace.WORLD: Attachment points are absolute world coordinates.
    • ConstraintSpace.LOCAL: Attachment points are offsets relative to each body's center of mass.
    // world space - specify attachment points in world coordinates
    const worldConstraint = pointConstraint.create(world, {
        bodyIdA: bodyA.id,
        bodyIdB: bodyB.id,
        pointA: [0, 5, 0], // absolute world position
        pointB: [0, 3, 0], // absolute world position
        space: ConstraintSpace.WORLD, // default
    });
    
    // local space - specify attachment points relative to body center of mass
    const localConstraint = pointConstraint.create(world, {
        bodyIdA: bodyA.id,
        bodyIdB: bodyB.id,
        pointA: [0, 0.5, 0], // offset from bodyA's center
        pointB: [0, -0.5, 0], // offset from bodyB's center
        space: ConstraintSpace.LOCAL,
    });
  10. Use a Listener to react to physics events

    main

    A Listener allows you to react to and modify physics events during world updates. You pass a Listener object to the updateWorld() function to receive callbacks for various collision and contact events.

    Common callbacks include:

    • onContactAdded: Called when a new contact is detected.
    • onContactPersisted: Called when a contact from the previous frame remains active.
    • onContactRemoved: Called when a contact is no longer active. Warning: Because bodies may be destroyed during the update, only the body IDs are safe to use in this callback.
    • onBodyPairValidate: Runs before narrowphase collision detection. Use this for custom filtering logic (e.g., faction systems) to avoid expensive collision calculations.
    • onContactValidate: Called after collision detection but before adding the contact constraint. Use this for logic that requires contact information (like one-way platforms). Note that rejecting contacts here is more expensive than using onBodyPairValidate.
    • onContactAdded (with settings): Can be used to modify contact properties like combinedFriction and combinedRestitution.
    // create a listener to react to physics events
    const listener: Listener = {
        onContactAdded: (bodyA, bodyB, manifold, settings) => {
            // called when a new contact is detected
            console.log('contact added between', bodyA.id, 'and', bodyB.id);
        },
        onContactPersisted: (bodyA, bodyB, manifold, settings) => {
            // called when a contact from last frame is still active
        },
        onContactRemoved: (bodyIdA, bodyIdB, subShapeIdA, subShapeIdB) => {
            // called when a contact is no longer active
            // WARNING: bodies may be destroyed, only body IDs are safe to use
        },
    };
    
    // pass listener to updateWorld
    updateWorld(world, listener, 1 / 60);
  11. Use Dynamic Character Controllers

    main
    For characters that should behave like physics objects (e.g., ragdolls, simple AI), use a regular dynamic rigid body. To prevent the character from tipping over, apply constraints on its rotation to keep it upright. This approach is computationally cheaper than KCC but offers less precise movement control.
  12. Serialize the physics world state

    main

    A physics world in crashcat is a JSON-serializable object. You can use JSON.stringify and JSON.parse on the entire world state, including bodies, shapes, constraints, and settings. This is useful for saving/loading game states for debugging or persistence.

    Warning: Object references (e.g., sharing a single shape instance across multiple bodies) will not survive serialization.