@pixi/3d
    Preparing search index...

    Math

    PixiJS 3D has its own math classes, all exported from @pixi/3d: Vector3 for points and directions, Quaternion for rotations, Matrix4 for transforms, and shapes such as Ray, Plane, Box and Sphere for hit tests and bounds. Their methods, and those of Vector2 and Vector4, follow the same few rules.

    A method such as add, normalize or applyMatrix4 changes the object you call it on and returns it, so calls chain. Most such methods also take an out object as their last argument. Pass one, and the result goes there instead. The object you called it on stays as it was:

    import { Vector3 } from '@pixi/3d';

    const a = new Vector3(1, 0, 0);
    const b = new Vector3(0, 1, 0);
    const sum = new Vector3();

    a.add(b); // a is now (1, 1, 0)
    a.add(b, sum); // sum is (1, 2, 0), and a is still (1, 1, 0)

    PixiJS's 2D math extras work the other way round. There, point.add(other) returns a new point. Here, enemy.position.add(step) moves the enemy.

    A method whose result is another kind of object, such as Ray#intersectPlane or Box#getCenter, takes an out too. Without one, it creates a new object on each call. A node is any object in the 3D scene. Methods of nodes and views that return a point or a ray, such as getGlobalPosition and screenToRay, work the same way.

    A method that reads a point takes a PointData3D, which is any object with x, y and z, such as a node's position or a plain { x: 0, y: 1, z: 0 }.

    Each object you create in a frame and then drop, whether with new Vector3() or from a method, is garbage. The browser collects it later, and that can make the frame rate stutter. In a ticker callback or a pointer listener, create what you need once, outside it, and pass it as out. Here an enemy chases the player at two units a second. Both have the same parent, so their positions are in the same space:

    const toPlayer = new Vector3();

    app.ticker.add((ticker) => {
    player.position.subtract(enemy.position, toPlayer).normalize(); // the direction, length 1
    enemy.position.addScaledVector(toPlayer, 2 * (ticker.deltaMS / 1000));
    });

    A node's position, rotation and scale are ObservableVector3s, and its quaternion is an ObservableQuaternion. They have the same methods as Vector3 and Quaternion. To keep a copy, use new Vector3().copyFrom(node.position) rather than clone. A clone is still tied to the node, so each write to it marks the node changed.

    A Quaternion holds a rotation as four numbers that you don't work out yourself. setFromAxisAngle builds a turn by an angle around an axis, given as a direction of length 1. Multiplying two quaternions combines their turns. Scene graph and transforms covers when a node uses its quaternion and when its rotation.

    On a node, multiply adds a turn around the node's own axes, which turn with the node, and premultiply adds it around the parent's axes. In a loaded model, each node's quaternion holds the rotation from the file, so multiply that quaternion to turn the node further. To lean a car's body, copy its quaternion once after loading and start each frame again from that copy, so the lean doesn't build up from frame to frame:

    const body = car.getChildByLabel('body', true)!;
    const rest = new Quaternion().copyFrom(body.quaternion); // the turn the file gave the body
    const roll = new Quaternion();
    const forward = { x: 0, y: 0, z: 1 };

    app.ticker.add(() => {
    roll.setFromAxisAngle(forward, lean); // lean: an angle in radians that your code works out
    body.quaternion.copyFrom(rest).multiply(roll); // around the body's own z axis
    });

    For a turn that keeps going, such as a spinning wheel, build each frame's turn from the time the frame took and multiply it in: spin.setFromAxisAngle(axle, speed * (ticker.deltaMS / 1000)), then wheel.quaternion.multiply(spin).

    slerp moves part of the way from one rotation toward another, the shorter way round. If you ease a car's angle toward a new heading instead, you have to keep the gap between -180° and 180°, or the car spins the long way round. A slerp needs no such check:

    const up = { x: 0, y: 1, z: 0 };
    const heading = new Quaternion();

    app.ticker.add((ticker) => {
    heading.setFromAxisAngle(up, targetYaw);
    // close the same share of the gap each second, whatever the frame rate
    car.quaternion.slerp(heading, 1 - Math.exp(-3 * (ticker.deltaMS / 1000)));
    });

    If you turn a node with multiply, slerp or another quaternion method, set its other turns through the quaternion too, rather than through rotation. For example, set its starting turn with setFromAxisAngle.

    Most code needs no matrix of its own. Each node keeps its worldTransform, the matrix that places it in the world. The view updates that matrix when it draws, so it can be out of date, such as right after you add or move the node. getGlobalTransform computes an up-to-date one. Scene graph says when to use each. toGlobal and toLocal move points between spaces.

    You build your own Matrix4 to place each mesh in a BatchedMesh3D, which draws many meshes from one node. Batched meshes does this with compose. You also build one to move a shape between spaces, as Shapes shows. Two things to know:

    • In a.multiply(b), b applies to a point first.
    • getInverse(m) sets the matrix you call it on to the inverse of m, the matrix that undoes m.
    • A Ray starts at its origin and runs along its direction. The sphere tests and distances assume the direction has length 1, so call normalize() on it after you set it yourself. screenToRay builds a ray through a point on the screen. To find the node under that point, use pointer events or raycast. Interaction covers both.
    • A Plane is a flat surface that goes on for ever. View3D uses one to find the ground under the pointer.
    • A Box is lined up with the axes, from its min corner to its max corner. getLocalBounds returns a node's bounds as a box.
    • A Sphere is a centre and a radius. A mesh's boundingSphere is one. Meshes shows how to test it against the camera's frustum, the volume the camera sees.

    A shape keeps the vectors you pass to its constructor, so new Sphere(enemy.position, 2) shares the enemy's position, and moving the sphere moves the enemy. Pass new vectors instead, such as new Vector3().copyFrom(enemy.position).

    Each shape is in some space. A ray from screenToRay and a mesh's boundingSphere are in world space, while getLocalBounds and a geometry's bounds3D are in the node's own space. Move one into the other's space before you test them:

    • applyMatrix4 with the node's world matrix takes a shape from the node's space to world space. A box grows when the matrix turns it, since Box#applyMatrix4 fits a new box, lined up with the world axes, around the turned one. Pass a box to write into, as in mesh.geometry.bounds3D.applyMatrix4(mesh.worldTransform, worldBounds). Without one, applyMatrix4 changes the geometry's own bounds, which pointer events, raycast and every mesh that shares the geometry read.
    • For an exact test, move the ray into the node's space instead, with ray.applyMatrix4(inverse.getInverse(mesh.worldTransform), localRay). Here inverse is a Matrix4 and localRay is a Ray, and you create both once. Writing into localRay keeps ray in world space for the next node. Ray#applyMatrix4 keeps the direction at length 1, so on a scaled node a distance along the moved ray isn't a world distance. To compare hits on several nodes, take each hit point back to world space with applyMatrix4(mesh.worldTransform).

    seededRandom returns a function to call in place of Math.random. The same seed gives the same numbers in the same order, so a scene laid out with it comes out the same on every load. The seed is a string. Here each level scatters its trees the same way every time:

    const random = seededRandom(`level-${level}`);

    for (const tree of trees) {
    tree.position.set((random() - 0.5) * 200, 0, (random() - 0.5) * 200);
    tree.rotation.y = random() * Math.PI * 2;
    }

    A particle system takes a seeded random function through its createRandom option. Pass a function that creates one, such as () => seededRandom('stream') in Bursts on demand. Particles covers what a seed makes repeat.