npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@dgreenheck/three-pinata

v2.0.1

Published

Three.js library for fracturing and slicing meshes in real time.

Downloads

544

Readme

three-pinata

NPM Version NPM Downloads GitHub Repo stars X (formerly Twitter) Follow YouTube Channel Subscribers

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-pinata

Requires 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 mesh
  • outerMaterial - Material for the original outer surfaces
  • innerMaterial - 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 configuration
    • onFragment?: (fragment: DestructibleMesh, index: number) => void - Optional callback for each fragment
    • onComplete?: () => 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 configuration
    • onSlice?: (piece: DestructibleMesh, index: number) => void - Optional callback for each piece
    • onComplete?: () => 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 configuration
    • onSlice?: (piece: DestructibleMesh, index: number) => void - Optional callback for each piece
    • onComplete?: () => 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 around
  • impactRadius?: number - Radius around impact point for fragment density
  • projectionAxis?: "x" | "y" | "z" | "auto" - For 2.5D mode: projection axis (default: "auto")
  • projectionNormal?: THREE.Vector3 - For 2.5D mode: optional projection plane normal
  • useApproximation?: 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:

  1. Select mesh in Edit mode
  2. Mesh > Clean Up > Make Manifold
  3. 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 wrapper
  • demo/src/physics/PhysicsBody.ts - Physics body wrapper
  • demo/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:lib

Generates:

  • three-pinata.es.js - ES Module
  • three-pinata.umd.js - UMD Module
  • Type declarations

Running the Demo

npm install
npm run dev

Open http://localhost:5173/

License

This project is licensed under the MIT License.

Acknowledgments

Support

For bugs or feature requests, create an issue in the issue tracker.

Contributing

Contributions welcome! Please submit a Pull Request.