A 3D frame costs CPU time to tell the GPU what to draw, and GPU time to shade every pixel.
A draw call is one request from the CPU to the GPU to draw some triangles. Each one costs CPU time, so PixiJS 3D draws many meshes in one instanced draw call. The GPU draws the same geometry once per mesh, and each copy gets its own transform and material values. Meshes can share a draw call only when they have all three of these in common:
new CubeGeometry() calls make two geometries and two draws. Create a geometry once and pass it to every mesh that uses it.TextureSource and count as one. This differs from pixi's 2D batcher, which mixes several textures in one batch.Values don't change the shader. Colours, roughness, a clearcoat's strength and transforms are data the shader reads per mesh, so a hundred crates with a hundred colours draw in one call:
import { CubeGeometry, Material3D, Mesh3D } from '@pixi/3d';
const geometry = new CubeGeometry();
for (let i = 0; i < 100; i++) {
const crate = new Mesh3D({
geometry, // one geometry for every crate
material: new Material3D({ baseColor: Math.random() * 0xffffff, roughness: 0.6 }),
});
crate.position.set((i % 10) * 1.5, 0.5, Math.floor(i / 10) * 1.5);
view3d.root.addChild(crate);
}
On WebGL, a change of material never splits meshes that have all three in common. On WebGPU, each material among them takes its own call, so four materials cost four calls. They draw as one call again only once there are many materials. So share one material between meshes that look the same. To give each slot of a BatchedMesh3D its own colour in one call, give one material a value per slot.
Blended meshes, the ones with alphaMode: 'blend', draw back to front by their distance from the camera, so only meshes next to each other in that order can share a draw.
To count a frame's draws, add GraphDebugOverlay from @pixi/3d/extras, as Debugging shows.
For hundreds or thousands of copies, Choosing a node for many objects compares the nodes built for this, such as BatchedMesh3D, which draws copies without a scene node for each.
The region of space the camera sees is its frustum, which has six sides. Before it draws, a view skips each mesh whose bounding sphere, a sphere around its geometry, lies wholly beyond one of those sides. That's culling, and it's on by default, while pixi's 2D cullable is off until you turn it on.
The view never culls a mesh with dynamic geometry, such as a ParticleContainer3D. A BatchedMesh3D follows its own cull setting, which is off by default.
The default view3d.cull, 'gpu', suits most scenes. CullMode says when 'cpu' or 'off' suits better. Despite its name, 'gpu' culls on the CPU in some setups, such as under WebGL2, and there it costs CPU time for every mesh each frame. WebGL and WebGPU says which setups.
A skinned mesh is one that bones bend. The view tests it at its bind pose, the pose it was modelled in, so an animation that moves it far from that pose can make it vanish near the edge of the screen. If that happens, set view3d.cull to 'off', which turns culling off for every mesh in the view.
Cap the resolution when you start the app, and scale heavy counts down on phones and tablets, as the BatchedMesh3D example does:
import { isMobile } from 'pixi.js';
await app.init({
preference: 'webgpu',
resizeTo: window,
resolution: Math.min(window.devicePixelRatio, 2), // shade at most 2 x 2 pixels per CSS pixel
autoDensity: true,
});
const size = isMobile.any ? 25_000 : 100_000; // slots in a BatchedMesh3D, a quarter as many on a phone or tablet
antialias if a phone still drops frames at the cap. It smooths edges with several samples per pixel, though the shader still runs about once per pixel. The PixiJS performance tips suggest turning it off on older phones.'manual' to redraw only when you call refreshShadows, as Shadows shows. A directional light with cascades splits what the camera sees into slices by distance and draws a map for each. Keep cascades few, and use 'manual' on such a light only while the camera stays still. Without cascades, a shadow doesn't follow the camera, so 'manual' suits a light and casters that don't move, however the camera moves.'hard', a filter that reads the shadow map once per pixel, where the default 'pcf' reads it four times. Also lower the view's shadowQuality. Each step down quarters each shadow map's memory and the pixels drawn into it.alphaMode: 'mask' rather than 'blend' for anything with hard edges, such as leaves, fences and many sprites. A Sprite3D that makes its own material blends by default, and the view sorts blended meshes back to front every frame. Masked meshes draw with the solid ones and skip that sort, so they batch like solid meshes.out object for them to fill. Garbage made every frame can make the browser pause to collect it.preference: 'webgl'. A browser without WebGPU gets WebGL2, where culling runs on the CPU.