@dgreenheck/three-pinata
v2.0.1
Published
Three.js library for fracturing and slicing meshes in real time.
Downloads
544
Readme
three-pinata
Real-time mesh fracturing and slicing for Three.js
Three-pinata is a library that enables you to fracture and slice 3D meshes in real-time directly in the browser. Whether you're building a destruction physics game, an interactive art piece, or a scientific visualization, three-pinata provides the tools you need to break things beautifully.
Features
- Voronoi Fracturing - Natural-looking fracture patterns with 3D and 2.5D modes
- Impact-Based Fracturing - Concentrate fragments around impact points for realistic destruction
- Refracturing - Fragments can be fractured again for progressive destruction (generation tracking handled externally)
- Plane Slicing - Slice meshes along arbitrary planes in local or world space
- Dual Materials - Separate materials for outer surfaces and internal fracture faces
- Custom Seed Points - Full control over fracture patterns with custom Voronoi seeds
- UV Mapping - Automatic UV generation for internal faces with configurable scale and offset
Live Demo
Check out the interactive demo: https://three-pinata-demo.vercel.app/
Installation
npm install @dgreenheck/three-pinataRequires Three.js >= 0.158.0 as a peer dependency.
Quick Start
import * as THREE from "three";
import { DestructibleMesh, FractureOptions } from "@dgreenheck/three-pinata";
// Setup scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Create materials
const outerMaterial = new THREE.MeshStandardMaterial({ color: 0x4a90e2 });
const innerMaterial = new THREE.MeshStandardMaterial({ color: 0xff6b6b });
// Create destructible mesh with both materials
const geometry = new THREE.SphereGeometry(1, 32, 32);
const mesh = new DestructibleMesh(geometry, outerMaterial, innerMaterial);
scene.add(mesh);
// Fracture the mesh
const options = new FractureOptions({
fractureMethod: "voronoi",
fragmentCount: 50,
voronoiOptions: {
mode: "3D",
},
});
const fragments = mesh.fracture(options, (fragment) => {
// Setup each fragment (material already set automatically)
scene.add(fragment);
});
// Hide original mesh
mesh.visible = false;
// Render
renderer.render(scene, camera);API Reference
Classes
DestructibleMesh
Extends THREE.Mesh with built-in fracturing and slicing capabilities.
Constructor:
new DestructibleMesh(
geometry?: THREE.BufferGeometry,
outerMaterial?: THREE.Material,
innerMaterial?: THREE.Material
)Parameters:
geometry- The geometry for the meshouterMaterial- Material for the original outer surfacesinnerMaterial- Material for newly created internal fracture/slice faces (optional, defaults to outerMaterial)
Methods:
fracture(options, onFragment?, onComplete?)
Fractures the mesh into fragments.
- Parameters:
options: FractureOptions- Fracture configurationonFragment?: (fragment: DestructibleMesh, index: number) => void- Optional callback for each fragmentonComplete?: () => void- Optional callback when fracturing is complete
- Returns:
DestructibleMesh[]- Array of fragment meshes
slice(sliceNormal, sliceOrigin, options?, onSlice?, onComplete?)
Slices the mesh along a plane (local space).
- Parameters:
sliceNormal: THREE.Vector3- Slice plane normal (local space)sliceOrigin: THREE.Vector3- Point on slice plane (local space)options?: SliceOptions- Slice configurationonSlice?: (piece: DestructibleMesh, index: number) => void- Optional callback for each pieceonComplete?: () => void- Optional callback when slicing is complete
- Returns:
DestructibleMesh[]- Array of fragment meshes
sliceWorld(worldNormal, worldOrigin, options?, onSlice?, onComplete?)
Slices the mesh along a plane (world space).
- Parameters:
worldNormal: THREE.Vector3- Slice plane normal (world space)worldOrigin: THREE.Vector3- Point on slice plane (world space)options?: SliceOptions- Slice configurationonSlice?: (piece: DestructibleMesh, index: number) => void- Optional callback for each pieceonComplete?: () => void- Optional callback when slicing is complete
- Returns:
DestructibleMesh[]- Array of fragment meshes
dispose()
Disposes the mesh geometry and material to free up memory.
- Parameters: None
- Returns:
void
Options
FractureOptions
Configuration for fracturing operations. Supports both Voronoi and simple plane-based fracturing.
Constructor:
new FractureOptions({
fractureMethod?: "voronoi" | "simple";
fragmentCount?: number;
voronoiOptions?: VoronoiOptions;
fracturePlanes?: { x: boolean; y: boolean; z: boolean };
textureScale?: THREE.Vector2;
textureOffset?: THREE.Vector2;
seed?: number;
})Properties:
fractureMethod: "voronoi" | "simple"- Fracture method to use (default: "voronoi")"voronoi": Natural-looking fracture using Voronoi tessellation (requires voronoiOptions)"simple": Simple plane-based fracturing (fast, lower quality)
fragmentCount: number- Number of fragments to create (default: 50). Note that actual fragment count may be higher when fracturing non-convex meshes.voronoiOptions?: VoronoiOptions- Voronoi-specific options (required when fractureMethod is "voronoi")fracturePlanes: { x: boolean; y: boolean; z: boolean }- Simple fracture: which axes to fracture along (default: all true)textureScale: THREE.Vector2- UV scale for internal faces (default: 1,1)textureOffset: THREE.Vector2- UV offset for internal faces (default: 0,0)seed?: number- Random seed for reproducibility
VoronoiOptions
Voronoi-specific fracture configuration (used within FractureOptions).
Interface:
{
mode: "3D" | "2.5D";
seedPoints?: THREE.Vector3[];
impactPoint?: THREE.Vector3;
impactRadius?: number;
projectionAxis?: "x" | "y" | "z" | "auto";
projectionNormal?: THREE.Vector3;
useApproximation?: boolean;
approximationNeighborCount?: number;
}Properties:
mode: "3D" | "2.5D"- Voronoi fracture mode (required)"3D": Full 3D Voronoi tessellation - most realistic, slower"2.5D": 2D Voronoi projected through mesh - faster, good for flat objects
seedPoints?: THREE.Vector3[]- Custom seed points for Voronoi cells in local space (auto-generated if not provided)impactPoint?: THREE.Vector3- Impact location in local space to concentrate fragments aroundimpactRadius?: number- Radius around impact point for fragment densityprojectionAxis?: "x" | "y" | "z" | "auto"- For 2.5D mode: projection axis (default: "auto")projectionNormal?: THREE.Vector3- For 2.5D mode: optional projection plane normaluseApproximation?: boolean- Use K-nearest neighbor approximation for performance (default: false)- Warning: May cause fragment overlap when enabled
approximationNeighborCount?: number- Neighbors to consider when using approximation (default: 12)
SliceOptions
Configuration for slicing operations.
Constructor:
new SliceOptions();Properties:
textureScale: THREE.Vector2- UV scale for internal faces (default: 1,1)textureOffset: THREE.Vector2- UV offset for internal faces (default: 0,0)
Usage Examples
Basic Fracturing
import { DestructibleMesh, FractureOptions } from "@dgreenheck/three-pinata";
const outerMaterial = new THREE.MeshStandardMaterial({ color: 0x4a90e2 });
const innerMaterial = new THREE.MeshStandardMaterial({ color: 0xff6b6b });
const mesh = new DestructibleMesh(geometry, outerMaterial, innerMaterial);
scene.add(mesh);
const options = new FractureOptions({
fractureMethod: "voronoi",
fragmentCount: 16,
voronoiOptions: {
mode: "3D",
},
});
const fragments = mesh.fracture(options);
fragments.forEach((fragment) => scene.add(fragment));
mesh.visible = false;Fracturing with Impact Point
const options = new FractureOptions({
fractureMethod: "voronoi",
fragmentCount: 16,
voronoiOptions: {
mode: "3D",
impactPoint: new THREE.Vector3(0, 1, 0), // Local space
impactRadius: 0.5, // Smaller = more concentrated
},
});
const fragments = mesh.fracture(options);Refracturing
Fragments can be fractured again for progressive destruction. Generation tracking is handled externally using userData:
// Configuration
const maxGeneration = 3;
const fragmentCounts = {
1: 32, // First fracture: 32 fragments
2: 16, // Second fracture: 16 fragments
3: 8, // Third fracture: 8 fragments
};
// Initial fracture
const mesh = new DestructibleMesh(geometry, outerMaterial, innerMaterial);
mesh.userData.generation = 0; // Track generation externally
const options1 = new FractureOptions({
fractureMethod: "voronoi",
fragmentCount: fragmentCounts[1],
voronoiOptions: {
mode: "3D",
},
});
const fragments = mesh.fracture(options1, (fragment) => {
// Track generation in each fragment
fragment.userData.generation = 1;
scene.add(fragment);
});
// Later, refracture a fragment when clicked
function onFragmentClick(fragment: DestructibleMesh) {
const currentGeneration = fragment.userData.generation || 0;
// Check generation limit (external refracture control)
if (currentGeneration >= maxGeneration) {
return; // Max generation reached
}
// Determine fragment count for next generation
const nextGeneration = currentGeneration + 1;
const fragmentCount = fragmentCounts[nextGeneration];
const options = new FractureOptions({
fractureMethod: "voronoi",
fragmentCount: fragmentCount,
voronoiOptions: {
mode: "3D",
},
});
const newFragments = fragment.fracture(options, (newFragment) => {
// Track generation externally
newFragment.userData.generation = nextGeneration;
scene.add(newFragment);
});
// Clean up old fragment
scene.remove(fragment);
fragment.geometry.dispose();
}This external approach gives you complete control over refracture behavior, allowing you to implement custom strategies like:
- Progressive weakening (fewer fragments per generation)
- Energy-based refracturing (only if impact force exceeds threshold)
- Material-based limits (glass refractures more than wood)
- Performance-based throttling (stop refracturing when FPS drops)
Simple Fracturing
const options = new FractureOptions({
fractureMethod: "simple",
fragmentCount: 50,
fracturePlanes: { x: true, y: true, z: true },
});
const fragments = mesh.fracture(options);Slicing
// Create mesh with outer and inner materials
const mesh = new DestructibleMesh(geometry, outerMaterial, innerMaterial);
const sliceNormal = new THREE.Vector3(0, 1, 0); // Horizontal cut
const sliceOrigin = new THREE.Vector3(0, 0, 0); // At origin
const options = new SliceOptions();
const pieces = mesh.slice(sliceNormal, sliceOrigin, options);
// Materials are already set automatically
pieces.forEach((piece) => scene.add(piece));
mesh.visible = false;Using Callbacks
const mesh = new DestructibleMesh(geometry, outerMaterial, innerMaterial);
const fragments = mesh.fracture(
options,
(fragment, index) => {
// Called for each fragment (materials already set)
fragment.castShadow = true;
// Add physics, apply forces, etc.
physics.add(fragment, { type: "dynamic" });
},
() => {
// Called when complete
console.log("Fracturing complete!");
},
);Dual Materials
Fragments support two materials - one for the original surface, one for internal fracture faces.
Recommended approach: Pass both materials to the constructor:
const outerMaterial = new THREE.MeshStandardMaterial({ color: 0xff6644 });
const innerMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd });
// Materials are automatically inherited by fragments
const mesh = new DestructibleMesh(geometry, outerMaterial, innerMaterial);
const fragments = mesh.fracture(options, (fragment) => {
// Material array [outer, inner] is already set automatically
scene.add(fragment);
});Alternative: Manually set materials on each fragment:
const mesh = new DestructibleMesh(geometry, outerMaterial);
const fragments = mesh.fracture(options, (fragment) => {
// Manually assign material array: [outer, inner]
fragment.material = [outerMaterial, innerMaterial];
scene.add(fragment);
});The fragment geometries include two material groups:
- Group 0 (materialIndex 0): Original outer surface faces
- Group 1 (materialIndex 1): Newly created internal fracture faces
Important Requirements
Manifold/Watertight Meshes
The library requires manifold (watertight) meshes - 3D models that form a completely closed, solid volume with no holes or self-intersecting geometry.
Valid meshes:
- Sphere, cube, cylinder, torus
- Closed character models
- Properly modeled objects with no gaps
Invalid meshes:
- Planes or single-sided surfaces
- Meshes with holes or missing faces
- Open-ended cylinders or boxes
- Overlapping geometry
How to check/fix in Blender:
- Select mesh in Edit mode
Mesh > Clean Up > Make Manifold- Use "3D Print Toolbox" addon to check for issues
Why this matters:
- Fracturing algorithms need to determine "inside" vs "outside"
- Non-manifold meshes create ambiguity leading to missing faces, holes, and visual artifacts
- Physics colliders will behave unpredictably with non-manifold fragments
Physics Integration
The library handles geometry only. For realistic destruction, integrate with a physics engine like Rapier.
Basic Physics Pattern
import RAPIER from "@dimforge/rapier3d";
// Initialize physics world
await RAPIER.init();
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
// Add physics to fragments
const fragments = mesh.fracture(options, (fragment) => {
// Create rigid body
const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(
fragment.position.x,
fragment.position.y,
fragment.position.z,
);
const rigidBody = world.createRigidBody(rigidBodyDesc);
// Create convex hull collider
const vertices = fragment.geometry.getAttribute("position").array;
const colliderDesc = RAPIER.ColliderDesc.convexHull(vertices)
.setRestitution(0.3)
.setFriction(0.5);
world.createCollider(colliderDesc, rigidBody);
});
// Update physics each frame
function animate() {
world.step();
// Sync Three.js objects with physics...
}For a complete implementation, see:
demo/src/physics/PhysicsWorld.ts- Complete physics wrapperdemo/src/physics/PhysicsBody.ts- Physics body wrapperdemo/src/scenes/SmashingScene.ts- Real-world example
Performance Tips
- Fragment Count: 10-50 fragments is optimal. 100+ may cause lag on slower devices
- 2.5D vs 3D: Use 2.5D mode when possible - significantly faster
- Pre-fracture: Fracture ahead of time and keep fragments hidden for instant destruction
- Approximation: For high fragment counts (>50), enable
useApproximation(may cause overlap) - Physics: More fragments = more physics bodies. Despawn fragments after they settle
Limitations
- Manifold Requirement: Meshes must be watertight (no holes or self-intersecting geometry)
- Memory: Each fragment is a new geometry. Plan accordingly for many destructible objects
- Physics Required: Library only handles geometry - you must add physics integration
Building
npm run build:libGenerates:
three-pinata.es.js- ES Modulethree-pinata.umd.js- UMD Module- Type declarations
Running the Demo
npm install
npm run devOpen http://localhost:5173/
License
This project is licensed under the MIT License.
Acknowledgments
- Original OpenFracture Unity library
- Three.js for 3D rendering
- Rapier for physics in the demo
Support
For bugs or feature requests, create an issue in the issue tracker.
Contributing
Contributions welcome! Please submit a Pull Request.
