st-tech/ppf-contact-solverPublic

A contact solver for physics-based simulations involving 👚 shells, 🪵 solids, 🪢 rods, 🧱 rigid bodies and ⏳ sand.

AI summary: A scalable, GPU-accelerated contact solver for high-fidelity physics simulations involving complex materials.

Stars
4.5K
+6 today
Forks
337
Watchers
37
Open issues
92
Open PRs
0
Contributors
~2
Commits
460
Branches
2

PythonApache-2.0Created Nov 21, 2024Last push 1d agoLatest release addon-2026-09-22-2204+19 stars this week+60 this month

Quick answers

What is ppf-contact-solver?
A scalable, GPU-accelerated contact solver for high-fidelity physics simulations involving complex materials.
What does ppf-contact-solver do?
This project provides a robust, highly scalable physics engine focused specifically on resolving complex contacts and collisions between diverse material types, including cloth (shells), rigid bodies, soft solids, and granular materials (sand). Originally developed for demanding fashion e-commerce simulations at ZOZO, it employs advanced Finite Element Method (FEM) techniques and symbolic force jacobians to ensure penetration-free stability without explosive jitter. It operates entirely on the GPU in single precision, offering massive parallelism and efficient caching for scenes with hundreds of millions of contacts. It avoids extending triangles beyond strict upper bounds, preventing rubbery physics artifacts.
Who is ppf-contact-solver for?
Technical artists, graphics programmers, and physics researchers requiring extreme-fidelity collision detection and simulation. It requires modern NVIDIA GPU hardware and familiarity with 3D simulation concepts.
How do I get started with ppf-contact-solver?
docker run --rm -it --gpus all -p 8080:8080 -p 9090:9090 -e WEB_PORT=8080 ghcr.io/st-tech/ppf-contact-solver-compiled:latest
How popular is ppf-contact-solver on GitHub?
st-tech/ppf-contact-solver has 4,517 stars and 337 forks on GitHub, and gained 19 stars in the last 7 days.
What license does ppf-contact-solver use?
st-tech/ppf-contact-solver is released under the Apache-2.0 license.

Star history

since Jul 29, 2026
02K4KJul 2026Aug 2026Sep 2026Oct 2026
4.5K stars as of Oct 3, 2026. Measured daily since Jul 29, 2026; GitHub no longer exposes earlier star timestamps.

Contribution activity

commits per day, last 52 weeks
SepOctNovDecJanFebMarAprMayJunJulAugSepMonWedFri2025-09-28: 0 commits2025-09-29: 0 commits2025-09-30: 0 commits2025-10-01: 0 commits2025-10-02: 0 commits2025-10-03: 12 commits2025-10-04: 2 commits2025-10-05: 1 commit2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 2 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 2 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 2 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-08: 0 commits2025-11-09: 0 commits2025-11-10: 1 commit2025-11-11: 0 commits2025-11-12: 8 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 5 commits2025-11-19: 1 commit2025-11-20: 4 commits2025-11-21: 1 commit2025-11-22: 1 commit2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 5 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 4 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 3 commits2025-12-18: 7 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 3 commits2025-12-22: 3 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 6 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 1 commit2025-12-29: 3 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 2 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 1 commit2026-02-03: 0 commits2026-02-04: 0 commits2026-02-05: 0 commits2026-02-06: 0 commits2026-02-07: 0 commits2026-02-08: 0 commits2026-02-09: 0 commits2026-02-10: 0 commits2026-02-11: 0 commits2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 0 commits2026-02-15: 0 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 0 commits2026-02-19: 0 commits2026-02-20: 0 commits2026-02-21: 0 commits2026-02-22: 0 commits2026-02-23: 0 commits2026-02-24: 0 commits2026-02-25: 0 commits2026-02-26: 0 commits2026-02-27: 1 commit2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 0 commits2026-03-04: 0 commits2026-03-05: 0 commits2026-03-06: 0 commits2026-03-07: 0 commits2026-03-08: 0 commits2026-03-09: 0 commits2026-03-10: 0 commits2026-03-11: 0 commits2026-03-12: 0 commits2026-03-13: 0 commits2026-03-14: 0 commits2026-03-15: 0 commits2026-03-16: 0 commits2026-03-17: 1 commit2026-03-18: 0 commits2026-03-19: 0 commits2026-03-20: 0 commits2026-03-21: 0 commits2026-03-22: 2 commits2026-03-23: 3 commits2026-03-24: 0 commits2026-03-25: 0 commits2026-03-26: 0 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 0 commits2026-03-30: 0 commits2026-03-31: 0 commits2026-04-01: 0 commits2026-04-02: 0 commits2026-04-03: 0 commits2026-04-04: 0 commits2026-04-05: 0 commits2026-04-06: 0 commits2026-04-07: 0 commits2026-04-08: 0 commits2026-04-09: 0 commits2026-04-10: 0 commits2026-04-11: 0 commits2026-04-12: 0 commits2026-04-13: 0 commits2026-04-14: 0 commits2026-04-15: 0 commits2026-04-16: 0 commits2026-04-17: 0 commits2026-04-18: 0 commits2026-04-19: 0 commits2026-04-20: 0 commits2026-04-21: 0 commits2026-04-22: 0 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 0 commits2026-04-28: 0 commits2026-04-29: 0 commits2026-04-30: 8 commits2026-05-01: 4 commits2026-05-02: 1 commit2026-05-03: 3 commits2026-05-04: 0 commits2026-05-05: 0 commits2026-05-06: 0 commits2026-05-07: 1 commit2026-05-08: 0 commits2026-05-09: 0 commits2026-05-10: 0 commits2026-05-11: 0 commits2026-05-12: 1 commit2026-05-13: 1 commit2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 0 commits2026-05-18: 7 commits2026-05-19: 12 commits2026-05-20: 0 commits2026-05-21: 1 commit2026-05-22: 3 commits2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 2 commits2026-05-26: 0 commits2026-05-27: 6 commits2026-05-28: 7 commits2026-05-29: 7 commits2026-05-30: 0 commits2026-05-31: 1 commit2026-06-01: 2 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 0 commits2026-06-05: 0 commits2026-06-06: 0 commits2026-06-07: 0 commits2026-06-08: 0 commits2026-06-09: 1 commit2026-06-10: 0 commits2026-06-11: 0 commits2026-06-12: 0 commits2026-06-13: 0 commits2026-06-14: 0 commits2026-06-15: 0 commits2026-06-16: 0 commits2026-06-17: 0 commits2026-06-18: 0 commits2026-06-19: 0 commits2026-06-20: 0 commits2026-06-21: 0 commits2026-06-22: 0 commits2026-06-23: 0 commits2026-06-24: 0 commits2026-06-25: 0 commits2026-06-26: 0 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 0 commits2026-06-30: 1 commit2026-07-01: 1 commit2026-07-02: 0 commits2026-07-03: 0 commits2026-07-04: 0 commits2026-07-05: 0 commits2026-07-06: 0 commits2026-07-07: 1 commit2026-07-08: 0 commits2026-07-09: 2 commits2026-07-10: 0 commits2026-07-11: 0 commits2026-07-12: 0 commits2026-07-13: 1 commit2026-07-14: 1 commit2026-07-15: 0 commits2026-07-16: 0 commits2026-07-17: 0 commits2026-07-18: 0 commits2026-07-19: 0 commits2026-07-20: 0 commits2026-07-21: 0 commits2026-07-22: 0 commits2026-07-23: 0 commits2026-07-24: 0 commits2026-07-25: 0 commits2026-07-26: 1 commit2026-07-27: 0 commits2026-07-28: 0 commits2026-07-29: 0 commits2026-07-30: 0 commits2026-07-31: 0 commits2026-08-01: 0 commits2026-08-02: 0 commits2026-08-03: 4 commits2026-08-04: 1 commit2026-08-05: 0 commits2026-08-06: 0 commits2026-08-07: 1 commit2026-08-08: 0 commits2026-08-09: 0 commits2026-08-10: 0 commits2026-08-11: 0 commits2026-08-12: 1 commit2026-08-13: 4 commits2026-08-14: 2 commits2026-08-15: 0 commits2026-08-16: 0 commits2026-08-17: 1 commit2026-08-18: 0 commits2026-08-19: 1 commit2026-08-20: 0 commits2026-08-21: 0 commits2026-08-22: 0 commits2026-08-23: 0 commits2026-08-24: 0 commits2026-08-25: 0 commits2026-08-26: 0 commits2026-08-27: 0 commits2026-08-28: 0 commits2026-08-29: 0 commits2026-08-30: 0 commits2026-08-31: 0 commits2026-09-01: 0 commits2026-09-02: 0 commits2026-09-03: 0 commits2026-09-04: 0 commits2026-09-05: 0 commits2026-09-06: 0 commits2026-09-07: 1 commit2026-09-08: 2 commits2026-09-09: 3 commits2026-09-10: 0 commits2026-09-11: 1 commit2026-09-12: 0 commits2026-09-13: 0 commits2026-09-14: 0 commits2026-09-15: 4 commits2026-09-16: 2 commits2026-09-17: 0 commits2026-09-18: 0 commits2026-09-19: 6 commits2026-09-20: 3 commits2026-09-21: 18 commits2026-09-22: 14 commits2026-09-23: 1 commit2026-09-24: 0 commits2026-09-25: 0 commits2026-09-26: 0 commits
233 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Actively maintained

    Pushed within 48 hours

  • Permissive license

    Apache-2.0

  • Continuous integration

    Automated checks passing

What ppf-contact-solver does

This project provides a robust, highly scalable physics engine focused specifically on resolving complex contacts and collisions between diverse material types, including cloth (shells), rigid bodies, soft solids, and granular materials (sand). Originally developed for demanding fashion e-commerce simulations at ZOZO, it employs advanced Finite Element Method (FEM) techniques and symbolic force jacobians to ensure penetration-free stability without explosive jitter. It operates entirely on the GPU in single precision, offering massive parallelism and efficient caching for scenes with hundreds of millions of contacts. It avoids extending triangles beyond strict upper bounds, preventing rubbery physics artifacts.

Technical artists, graphics programmers, and physics researchers requiring extreme-fidelity collision detection and simulation. It requires modern NVIDIA GPU hardware and familiarity with 3D simulation concepts.

  • Multi-Material Support: Accurately simulates interactions between cloth, rigid bodies, soft bodies, and sand.
  • High Stability: Uses advanced mathematical solvers and strict bounds to prevent snagging and explosive collisions.
  • Blender Integration: Includes an add-on for setting up, remotely simulating, and visualizing physics directly in Blender.
  • Massive GPU Acceleration: Leverages single-precision CUDA processing for highly parallel computation of extreme contact scenarios.
  • Production Tested: Originally developed and stress-tested for high-fidelity fashion e-commerce applications.
  • Docker Deployment: Offers pre-compiled Docker images for rapid, isolated deployment across Linux and Windows.

Where teams use it

Cloth Simulation

Accurately modeling how garments drape, fold, and interact with character bodies in high-end 3D animation.

Granular Dynamics

Simulating complex interactions of sand or dirt with solid objects in visual effects production.

Scientific Visualization

Modeling highly detailed mechanical interactions and deformations for engineering analysis.

Remote Simulation Workflows

Using cloud instances via Docker to run heavy simulations remotely while viewing results in local Blender clients.

Getting started: docker run --rm -it --gpus all -p 8080:8080 -p 9090:9090 -e WEB_PORT=8080 ghcr.io/st-tech/ppf-contact-solver-compiled:latest

README

main branch

ZOZO's Contact Solver 🫶

A contact solver for physics-based simulations involving 👚 shells, 🪵 solids, 🪢 rods, 🧱 rigid bodies and ⏳ sand. Started as an in-house physics engine at ZOZO, Inc., the largest fashion e-commerce company in Japan, and now evolving with the community. All open-source.

Getting Started All Examples All Examples (Windows Native) Python API Docs Docker Build Build Release Blender CI solver_logo

🤖 LLM Transparency: We highly respect that readers expect to hear the author's original voice and tone, which we work to retain throughout. Our use of LLMs is clarified in (Markdown).

📌 TL;DR

If you simply want to use this through Blender, watch this CGMatter YouTube video. The release page is here. Our video tutorials page is here.

👀 Quick Look

🎨 Simulate remotely from our Blender add-on (screenshots taken on macOS; you can also run locally on a modern NVIDIA GPU on Windows or Linux, or on an Apple silicon Mac)

blender-addon-trailer-2026.mp4

🚀 Or run it locally: ./ppf-contact-solver (macOS/Linux), double click start.bat (Windows), or a Docker command (Linux/Windows)

glance-terminal

🌐 Click the URL and explore our examples

glance-jupyter

✨ Highlights

  • 💪 100% Penetration-Free Upon Success: No snagging intersections (Details).
  • 🦀 Rust-First: We minimize the use of C++ to maximize safety. No performance compromised.
  • ⏲ Scalable: An extreme case includes beyond 180M contacts. Not just one million.
  • 🚲 Cache Efficient: All on the GPU runs in single precision. No double precision.
  • 🥼 Not Rubbery: Any single triangle never extends beyond strict upper bounds (e.g., 1%).
  • 📐 Finite Element Method: We use FEM for deformables and symbolic force jacobians.
  • 📊 Parameters Calibrated: Fabric presets match real measurements (Report).
  • ⚔️ Highly Stressed: We run GitHub Actions to run stress tests 10 times in a row.
  • 🐛 Community Tested: Users report bugs and we fix them (Issues).
  • 🚀 Massively Parallel: Both contact and elasticity solvers are run on the GPU.
  • 🖥️ Cross Platform: Both the add-on and the solver engine run natively on macOS, Windows, and Linux.
  • 🎛️ Cross Architecture: We natively support Apple silicon (Metal), NVIDIA (CUDA), and AMD (ROCm) GPUs.
  • 🧮 SIMD-Optimized CPU Backend: Our CPU code runs natively on both x86_64 and ARM64.
  • 🐳 Docker Sealed: All can be deployed fast. The image is ~1GB.
  • 🌐 JupyterLab Included: Open your browser and run examples right away (Video).
  • 🐍 Documented Python APIs: Our Python code is fully docstringed and lintable (Video).
  • ☁️ Cloud-Ready: Our solver can be seamlessly deployed on major cloud platforms.
  • 🎨 Blender Add-on: Simulate remotely and fetch results locally, even on macOS.
  • 🤖 MCP Support: Let a LLM run simulations for you using natural language.
  • ✨ Stay Clean: You can remove all traces after use.
  • 📜 Permissive License: Apache 2.0 allows commercial and proprietary use.

🚧 Gentle Disclaimer and Limitations

  • ⏳ Offline use: Not real time, though some examples reach interactive rates.
  • 🐢 Not the fastest: Other recent work reports faster results.
  • 📉 Not differentiable: No gradients, so inverse design and learning are out of scope.
  • 🧪 Not production ready (yet): Many bugs remain, including undiscovered ones. We are actively working to make it production-proof. Known bugs are tracked in Issues.
  • 🐌 CPU and Metal backends are slow: Meant for evaluation, learning, and small examples. Larger scenes need a powerful GPU.
  • 🟥 AMD GPUs untested: The ROCm backend has never run on real AMD hardware. We rely on community bug reports.
  • 🛠️ Add-on setup takes effort: Not one click. The solver backend is deployed separately, locally or remotely.
  • 👤 Development pace: Maintained by Ryoichi Ando alone, with limited time.

🔖 Table of Contents

📚 Advanced Contents

📝 Change History

📚 For the complete change history, see articles/changes.md.

🎓 Technical Materials

📘 A Cubic Barrier with Elasticity-Inclusive Dynamic Stiffness
siga2024-main-mini.mp4
📌 Reference Implementation

The main branch is undergoing frequent updates and will deviate from the paper. To retain consistency with the paper, we have created a new branch sigasia-2024.

  • 🛠️ Only maintenance updates are planned for this branch.
  • 🚫 General users should not use this branch as it is not optimized for best performance.
  • 🚫 All algorithmic changes listed in this (Markdown) are excluded from this branch.
  • 📦 We also provide a pre-compiled Docker image: ghcr.io/st-tech/ppf-contact-solver-compiled-sigasia-2024:latest of this branch.
  • 🌐 Template Link for vast.ai

⚡️ Requirements

A GPU is recommended, but not required. The right backend is picked for you when a run starts.

Hardware Backend Runs on Good to know
🟩 NVIDIA GPU CUDA 12.8+ Linux (x86_64, arm64), Windows (x86_64) RTX 4090 / 5090 for large scenes; 3090 / 4070 / 5070 for small to medium ones
🟥 AMD GPU ROCm Linux (x86_64), Windows (x86_64) gfx9 through gfx12
🍎 Apple silicon Metal macOS M1 / A14 generation or newer
💻 Any CPU CPU (x86_64, arm64) macOS, Windows, Linux SIMD-optimized using AVX2 and NEON instructions

You will also need:

  • 🐳 A Docker environment (see below), supported only on x86_64 with an NVIDIA GPU (CUDA)
  • 🎨 Blender 5+ (only if you intend to use the Blender add-on)

💨 Getting Started

Whether you plan to use the Blender add-on or the JupyterLab interface, the solver engine itself must first be deployed. The steps below apply to both.

⚠️ Do not run warmup.py locally. If you do, you are very likely to hit failures and find it difficult to cleanup.

🤔 If you are cautious, you can review the build workflow to verify safety yourself. We try to maximize transparency; we never build locally and upload.

🍎 macOS Native

For Apple silicon Macs, a self-contained folder carrying the Metal and CPU backends. No Python, Homebrew, or Xcode installation is needed, and nothing is installed outside the folder.

  1. Download the latest release from GitHub Releases and unzip
  2. Open Terminal and change into the unzipped folder
  3. Run ./ppf-contact-solver

JupyterLab frontend will auto-start. You should be able to access it at http://localhost:8080. Keep the terminal window open; Ctrl+C shuts it down.

🪟 Windows Native Executable

For Windows 10/11 users, a self-contained executable (several hundred MB) is available. No Python, Docker, or CUDA Toolkit installation is needed. All should simply work out of the box (Video). The x86_64 archive carries the CUDA, ROCm, and CPU backends, so one download serves an NVIDIA machine and an AMD one, while a Windows on ARM machine runs the CPU backend.

  1. Install the latest driver for your GPU: NVIDIA (Link) or AMD (Link), skipped for a CPU-only run
  2. Download the latest release from GitHub Releases and unzip
  3. Double click start.bat

JupyterLab frontend will auto-start. You should be able to access it at http://localhost:8080.

🐧 Linux Native

A self-contained directory for x86_64 and aarch64 machines running glibc 2.28 or newer. The x86_64 archive carries the CUDA, ROCm, and CPU backends, and the aarch64 one carries CUDA and the CPU backend. No Python, CUDA Toolkit, or ROCm installation is needed.

  1. Install the latest driver for your GPU: NVIDIA (Link) or AMD (Link), skipped for a CPU-only run
  2. Download the latest release from GitHub Releases and unzip
  3. Run ./ppf-contact-solver

JupyterLab frontend will auto-start. You should be able to access it at http://localhost:8080. Keep the terminal window open; Ctrl+C shuts it down.

🐳 Docker (Linux and Windows)

Install a NVIDIA driver (Link) on your host system and follow the instructions below specific to the operating system to get a Docker running:

🐧 Linux 🪟 Windows
Install the Docker engine from here (Link). Also, install the NVIDIA Container Toolkit (Link). Just to make sure that the Container Toolkit is loaded, run sudo service docker restart. Install the Docker Desktop (Link). You may need to log out or reboot after the installation. After logging back in, launch Docker Desktop to ensure that Docker is running.

Next, run the following command to start the container. If no edits are needed, just copy and paste:

🪟 Windows (PowerShell)
$MY_WEB_PORT = 8080      # JupyterLab port on your side
$MY_BLENDER_PORT = 9090  # Solver port for the Blender add-on
$IMAGE_NAME = "ghcr.io/st-tech/ppf-contact-solver-compiled:latest"
docker run --rm -it `
  --name ppf-contact-solver `
  --gpus all `
  -p ${MY_WEB_PORT}:${MY_WEB_PORT} `
  -p ${MY_BLENDER_PORT}:${MY_BLENDER_PORT} `
  -e WEB_PORT=${MY_WEB_PORT} `
  $IMAGE_NAME # Image size ~1GB
🐧 Linux (Bash/Zsh)
MY_WEB_PORT=8080      # JupyterLab port on your side
MY_BLENDER_PORT=9090  # Solver port for the Blender add-on
IMAGE_NAME=ghcr.io/st-tech/ppf-contact-solver-compiled:latest
docker run --rm -it \
  --name ppf-contact-solver \
  --gpus all \
  -p ${MY_WEB_PORT}:${MY_WEB_PORT} \
  -p ${MY_BLENDER_PORT}:${MY_BLENDER_PORT} \
  -e WEB_PORT=${MY_WEB_PORT} \
  $IMAGE_NAME # Image size ~1GB

The image download shall be started. Our image is hosted on GitHub Container Registry (~1GB). JupyterLab will then auto-start. Eventually you should be seeing:

==== JupyterLab Launched! 🚀 ====
     http://localhost:8080
    Press Ctrl+C to shutdown
================================

Next, open your browser and navigate to http://localhost:8080. The port 8080 can change if you change the MY_WEB_PORT variable. Keep your terminal window open. Now you are ready to go! 🎉

🛑 Shutting Down

To shut down the container, just press Ctrl+C in the terminal. The container will be removed and all traces will be cleaned up. 🧹

If you wish to keep the container running in the background, replace --rm with -d. To shutdown the container and remove it, run docker stop ppf-contact-solver && docker rm ppf-contact-solver.

🔧 Advanced Installation

If you wish to build the docker image from scratch, please refer to the cleaner installation guide (Markdown).

🐍 How To Use

We provide two frontends: a Blender add-on and a JupyterLab interface. The Blender add-on lets you build scenes and run simulations entirely within Blender's UI, while JupyterLab lets you script everything in Python from your browser. Both communicate with the same solver engine, so pick whichever you like.

In both cases, you can interact with the simulator on your laptop while the actual simulation runs on a remote headless server over the internet. This means that you don't have to own NVIDIA hardware, but can rent it at vast.ai for less than $0.5 per hour. That said, if you do have a modern NVIDIA GPU on a local Windows or Linux machine, or an Apple silicon Mac, you can also run the solver directly on it. Actually, this (Video) was recorded on a vast.ai instance. The experience is good! 👍

🎨 Blender Add-on

Our Blender add-on aims to offer a familiar UI that best feels like everything works locally, but under the hood, it communicates with a remote server where all simulations run, and then the results are fetched back.

This provides a unique experience where users can leverage powerful remote GPUs while working seamlessly in their local Blender environment. Remarkably, our Blender add-on works even on macOS systems 😊, unlike other CUDA-based physics simulator add-ons that require local NVIDIA GPUs. More importantly, you can work on a laptop without worrying about draining the battery fast. 🔋

Follow this page How to Install to learn how to install the add-on. For a thorough walk through workflow, we refer to our documentation below:

Read Blender Add-on Documentation

🎬 Prefer video? CGMatter walks through the full setup in this video tutorial:

ZoZos Contact Solver - The ultimate Blender cloth simulator

Here are some highlights:

📖 Docs Look

We maintain a full docs site with workflow guides and recorded walkthroughs for the add-on:

Workflow documentation page on the docs site, showing the Blender / solver coordinate system explanation. Video Tutorials page on the docs site, showing a grid of recorded walkthroughs.
Workflow documentation page. (Link) Video tutorials page. (Link)
🖼️ UI Look

Here are a couple of screenshots of the add-on running inside Blender:

The kite scene set up inside Blender using the add-on. The zebra scene set up inside Blender using the add-on.
Kite scene set up in Blender. (full-size) Zebra scene set up in Blender. (full-size)

Blender add-on overview

🤖 From Natural Language to Simulation (via MCP)

We expose all of the add-on's tools through an MCP server, so any LLM (Claude, Codex, etc.) can drive the whole pipeline from a natural language prompt. Scene building, parameter tweaks, and running the simulation all happen without UI clicks. Here are two examples:

Codex terminal on the left driving Blender on the right through the MCP server, building a bowl-and-spheres scene from a natural language prompt. A cloth sheet draped over a sphere, produced from a single natural language prompt through the MCP server.
Codex (left) driving Blender (right) through the add-on's MCP server. A prompt: drape a sheet over a sphere and make an animation video mp4 render 300 frames.
🐍 From a Python Script to Simulation

You can also drive the entire pipeline from a Python script inside Blender's scripting editor. This is handy for procedural scene setup and batch variant generation. Below is a full example that drapes a sheet over a sphere:

import addon_utils
import importlib
import bpy

# Look up the add-on module under whichever extension repository Blender
# installed it into and grab the public solver API.
addon = next(m for m in addon_utils.modules() if m.__name__.endswith(".ppf_contact_solver"))
solver = importlib.import_module(f"{addon.__name__}.ops.api").solver

# Reset any prior state.
solver.clear()

# Create a sphere (the static collider) at the origin.
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=4, radius=0.5, location=(0, 0, 0))
bpy.context.object.name = "Sphere"

# Create a 2x2 sheet just above the sphere as a 64x64 grid.
bpy.ops.mesh.primitive_grid_add(x_subdivisions=64, y_subdivisions=64, size=2, location=(0, 0, 0.6))
sheet = bpy.context.object
sheet.name = "Sheet"

# Pin the two corners on the -x edge via a vertex group.
vg = sheet.vertex_groups.new(name="Corners")
corner_indices = [
    i for i, v in enumerate(sheet.data.vertices)
    if v.co.x < -0.99 and abs(abs(v.co.y) - 1.0) < 0.01
]
vg.add(corner_indices, 1.0, "REPLACE")

# Build solver groups.
cloth = solver.create_group("Cloth", type="SHELL")
cloth.add("Sheet")
cloth.param.enable_strain_limit = True
cloth.param.strain_limit_percent = 5.0
cloth.param.bend = 1

ball = solver.create_group("Ball", type="STATIC")
ball.add("Sphere")

# Pin the two sheet corners.
cloth.create_pin("Sheet", "Corners")

# Scene parameters.
solver.param.frame_count = 100
solver.param.step_size = 0.01

Here's how the script runs inside Blender (full-size):

python-scripting

For the full solver.* surface, see the Blender Python API guide.

🌏 Community Translations

Our add-on ships UI catalogs for 日本語, 简体中文, and 한국어, selectable from Edit → Preferences → Interface → Translation. narznarznarz polished our Korean catalog to match Blender's own Korean terminology (Discussion):

To install, download it (keep the name ko_KR.json) and overwrite the ko_KR.json in the installed add-on's i18n directory, which this prints in Blender's Python Console:

import addon_utils, os
addon = next(m for m in addon_utils.modules() if m.__name__.endswith(".ppf_contact_solver"))
print(os.path.join(os.path.dirname(addon.__file__), "i18n"))

Then restart Blender. Updating the add-on restores our catalog, so re-apply it after an update.

🌐 JupyterLab

Our frontend is accessible through a browser using our built-in JupyterLab interface. All is set up when you open it for the first time. Results can be interactively viewed through the browser and exported as needed. Our Python interface is designed with the following principles in mind:

  • 🛠️ In-Pipeline Tri/Tet Creation: Depending on external 3D/CAD softwares for triangulation or tetrahedralization makes dynamic resolution changes cumbersome. We provide handy .triangulate() and .tetrahedralize() calls to keep everything in-pipeline, allowing users to skip explicit mesh exports to 3D/CAD software.
  • 🚫 No Mesh Data Included: Preparing mesh data using external tools can be cumbersome. Our frontend minimizes this effort by allowing meshes to be created on the fly or downloaded when needed.
  • 🔗 Method Chaining: We adopt the method chaining style from JavaScript, making the API intuitive to understand and read smoothly.
  • 📦 Single Import for Everything: All frontend features are accessible by simply importing with from frontend import App.

Here's an example of draping five sheets over a sphere with two corners pinned. We have more examples in the examples directory. Please take a look! 👀

# import our frontend
from frontend import App

# make an app
app = App.create("drape")

# create a square mesh resolution 128 spanning the xz plane
V, F = app.mesh.square(res=128, ex=[1, 0, 0], ey=[0, 0, 1])

# add to the asset and name it "sheet"
app.asset.add.tri("sheet", V, F)

# create an icosphere mesh radius 0.5
V, F = app.mesh.icosphere(r=0.5, subdiv_count=4)

# add to the asset and name it "sphere"
app.asset.add.tri("sphere", V, F)

# create a scene
scene = app.scene.create()

# define gap between sheets
gap = 0.01

for i in range(5):

    # add the sheet asset to the scene with an vertical offset
    obj = scene.add("sheet").at(0, gap * i, 0)

    # pick two corners
    corner = obj.grab([1, 0, -1]) + obj.grab([-1, 0, -1])

    # pin the corners
    obj.pin(corner)

    # set the strict limit on maximum strain to 5% per triangle
    obj.param.set("strain-limit", 0.05)

# add a sphere mesh at a lower position with jitter and set it static collider
scene.add("sphere").at(0, -0.5 - gap, 0).jitter().pin()

# compile the scene and report stats
scene = scene.build().report()

# preview the initial scene, shows image left
scene.preview()

# create a new session with the compiled scene
session = app.session.create(scene)

# set session params
session.param.set("frames", 100).set("dt", 0.01)

# build this session
session = session.build()

# start the simulation and live-preview the results, shows image right
session.start().preview()

# also show streaming logs
session.stream()

# or interactively view the animation sequences
session.animate()

# export all simulated frames in (sequences of ply meshes + a video)
session.export.animation()

drape

📚 Python APIs and Parameters
  • Full API documentation is available on our GitHub Pages. The major APIs are documented using docstrings and compiled with Sphinx We have also included jupyter-lsp to provide interactive linting assistance and display docstrings as you type. See this video (Video) for an example. The behaviors can be changed through the settings.

  • A list of parameters used in param.set(key,value) is documented here: (Simulation Parameters) (Material Parameters).

⚠️ Please note that our Python APIs are subject to breaking changes as this repository undergoes frequent iterations. If you need APIs to be fixed, please fork.

🔍 Obtaining Logs

Logs for the simulation can also be queried through our Python APIs. Here's an example of how to get a list of recorded logs, fetch them, and compute the average.

# get a list of log names
logs = session.get.log.names()
print(logs)
assert "time-per-frame" in logs
assert "newton-steps" in logs

# get a list of time per video frame
msec_per_video = session.get.log.numbers("time-per-frame")

# compute the average time per video frame
print("avg per frame:", sum([n for _, n in msec_per_video]) / len(msec_per_video))

# get a list of newton steps
newton_steps = session.get.log.numbers("newton-steps")

# compute the average of consumed newton steps
print("avg newton steps:", sum([n for _, n in newton_steps]) / len(newton_steps))

# Last 8 lines. Omit for everything.
print("==== log stream ====")
for line in session.get.log.stdout(n_lines=8):
    print(line)

Below are some representatives. vid_time refers to the video time in seconds and is recorded as float. ms refers to the consumed simulation time in milliseconds recorded as int. vid_frame is the video frame count recorded as int.

Name Description Format
time-per-frame Time per video frame list[(vid_frame,ms)]
matrix-assembly Matrix assembly time list[(vid_time,ms)]
pcg-linsolve Linear system solve time list[(vid_time,ms)]
line-search Line search time list[(vid_time,ms)]
time-per-step Time per step list[(vid_time,ms)]
newton-steps Newton iterations per step list[(vid_time,count)]
num-contact Contact count list[(vid_time,count)]
max-sigma Max stretch list(vid_time,float)

The full list of log names and their descriptions is documented here: (GitHub Pages).

Note that some entries have multiple records at the same video time. This occurs because the same operation is executed multiple times within a single step during the inner Newton's iterations. For example, the linear system solve is performed at each Newton's step, so if multiple Newton's steps are executed, multiple linear system solve times appear in the record at the same video time.

If you would like to retrieve the raw log stream, you can do so by

# Last 8 lines. Omit for everything.
for line in session.get.log.stdout(n_lines=8):
    print(line)

This will output something like:

* dt: 1.000e-03
* max_sigma: 1.045e+00
* avg_sigma: 1.030e+00
------ newton step 1 ------
   ====== contact_matrix_assembly ======
   > dry_pass...0 msec
   > rebuild...7 msec
   > fillin_pass...0 msec

If you would like to read stderr, you can do so using session.get.log.stderr() (if it exists). This returns list[str]. All the log files are updated in real-time and can be fetched right after the simulation starts; you don't have to wait until it finishes.

🖼️ Catalogue

🎨 Blender Add-on Examples

These scenes are all built with our add-on. The simulation itself runs on a remote solver, or directly on your local machine if you have a modern NVIDIA GPU on Windows or Linux, or an Apple silicon Mac.

You set the geometry, constraints, and parameters from Blender's UI, and the saved .blend carries everything the add-on needs.

kite.blend (Video) crumple.blend (Video) puff.blend (Video)
press.blend (Video) zebra.blend (Video) curtain.blend (Video)

The simulated portion (objects, groups, pins, and solver parameters) is generated by a script you drop into Blender's Scripting editor. Cameras, lighting, and any non-simulated props are still set up in Blender's UI. Each script is linked above its thumbnail.

cards.py (Video) five-twist.py (Video) noodle.py (Video) woven.py (Video)

🌐 JupyterLab Examples

All these examples run on our Python frontend through JupyterLab. Click any notebook to see how the scene is built, or click the video link to watch the result.

woven.ipynb (Video) stack.ipynb (Video) trampoline.ipynb (Video) needle.ipynb (Video)
cards.ipynb (Video) codim.ipynb (Video) hang.ipynb (Video) trapped.ipynb (Video)
domino.ipynb (Video) noodle.ipynb (Video) drape.ipynb (Video) five-twist.ipynb (Video)
ribbon.ipynb (Video) curtain.ipynb (Video) fishingknot.ipynb (Video) friction.ipynb (Video)
belt.ipynb (Video) fitting.ipynb (Video) roller.ipynb (Video) yarn.ipynb (Video)
(README truncated)

View on GitHub

Recent activity

commits and pull requests

Recent open issues

view all

Discussions

all 31

Releases and announcements

45 total
  1. Blender Add-on 1.0.16 (addon-2026-09-22-2204)addon-2026-09-22-2204Sep 22, 202658 downloads

    manifest version: 1.0.16

  2. ZOZO's Contact Solver 2026-09-22-20-522026-09-22-20-52Sep 22, 202698 downloads

    **Full Changelog**: https://github.com/st-tech/ppf-contact-solver/compare/2026-09-21-21-32...2026-09-22-20-52

  3. Blender Add-on 1.0.16 (addon-2026-09-21-2300)addon-2026-09-21-2300Sep 21, 202629 downloads

    manifest version: 1.0.16

  4. ZOZO's Contact Solver 2026-09-21-21-322026-09-21-21-32Sep 21, 202633 downloads

    **Full Changelog**: https://github.com/st-tech/ppf-contact-solver/compare/addon-2026-09-19-2315...2026-09-21-21-32

  5. Blender Add-on 1.0.16 (addon-2026-09-19-2315)addon-2026-09-19-2315Sep 19, 202635 downloads

    manifest version: 1.0.16

Code frequency

additions and deletions
+214.8K-214.8KWeek of 2025-09-28: +21,610 linesWeek of 2025-09-28: -7,032 linesWeek of 2025-10-05: +1 linesWeek of 2025-10-05: -1 linesWeek of 2025-10-12: +2 linesWeek of 2025-10-12: -2 linesWeek of 2025-10-19: +18 linesWeek of 2025-10-19: -1 linesWeek of 2025-10-26: +0 linesWeek of 2025-10-26: -0 linesWeek of 2025-11-02: +69 linesWeek of 2025-11-02: -47 linesWeek of 2025-11-09: +11,155 linesWeek of 2025-11-09: -3,064 linesWeek of 2025-11-16: +437 linesWeek of 2025-11-16: -112 linesWeek of 2025-11-23: +205 linesWeek of 2025-11-23: -7 linesWeek of 2025-11-30: +0 linesWeek of 2025-11-30: -0 linesWeek of 2025-12-07: +997 linesWeek of 2025-12-07: -117 linesWeek of 2025-12-14: +16,713 linesWeek of 2025-12-14: -379 linesWeek of 2025-12-21: +15,856 linesWeek of 2025-12-21: -2,366 linesWeek of 2025-12-28: +2,150 linesWeek of 2025-12-28: -1,792 linesWeek of 2026-01-04: +0 linesWeek of 2026-01-04: -0 linesWeek of 2026-01-11: +9 linesWeek of 2026-01-11: -1 linesWeek of 2026-01-18: +0 linesWeek of 2026-01-18: -0 linesWeek of 2026-01-25: +0 linesWeek of 2026-01-25: -0 linesWeek of 2026-02-01: +17,502 linesWeek of 2026-02-01: -19,029 linesWeek of 2026-02-08: +0 linesWeek of 2026-02-08: -0 linesWeek of 2026-02-15: +0 linesWeek of 2026-02-15: -0 linesWeek of 2026-02-22: +31 linesWeek of 2026-02-22: -31 linesWeek of 2026-03-01: +0 linesWeek of 2026-03-01: -0 linesWeek of 2026-03-08: +0 linesWeek of 2026-03-08: -0 linesWeek of 2026-03-15: +31 linesWeek of 2026-03-15: -31 linesWeek of 2026-03-22: +25 linesWeek of 2026-03-22: -14 linesWeek of 2026-03-29: +0 linesWeek of 2026-03-29: -0 linesWeek of 2026-04-05: +0 linesWeek of 2026-04-05: -0 linesWeek of 2026-04-12: +0 linesWeek of 2026-04-12: -0 linesWeek of 2026-04-19: +0 linesWeek of 2026-04-19: -0 linesWeek of 2026-04-26: +92,779 linesWeek of 2026-04-26: -3,443 linesWeek of 2026-05-03: +81,214 linesWeek of 2026-05-03: -41,364 linesWeek of 2026-05-10: +22 linesWeek of 2026-05-10: -18 linesWeek of 2026-05-17: +4,173 linesWeek of 2026-05-17: -1,399 linesWeek of 2026-05-24: +8,336 linesWeek of 2026-05-24: -969 linesWeek of 2026-05-31: +2,997 linesWeek of 2026-05-31: -168 linesWeek of 2026-06-07: +3 linesWeek of 2026-06-07: -0 linesWeek of 2026-06-14: +0 linesWeek of 2026-06-14: -0 linesWeek of 2026-06-21: +0 linesWeek of 2026-06-21: -0 linesWeek of 2026-06-28: +44,534 linesWeek of 2026-06-28: -12,872 linesWeek of 2026-07-05: +3,457 linesWeek of 2026-07-05: -301 linesWeek of 2026-07-12: +9,597 linesWeek of 2026-07-12: -757 linesWeek of 2026-07-19: +0 linesWeek of 2026-07-19: -0 linesWeek of 2026-07-26: +12,697 linesWeek of 2026-07-26: -1,980 linesWeek of 2026-08-02: +34,359 linesWeek of 2026-08-02: -2,493 linesWeek of 2026-08-09: +3,087 linesWeek of 2026-08-09: -182 linesWeek of 2026-08-16: +2 linesWeek of 2026-08-16: -1 linesWeek of 2026-08-23: +0 linesWeek of 2026-08-23: -0 linesWeek of 2026-08-30: +0 linesWeek of 2026-08-30: -0 linesWeek of 2026-09-06: +45,581 linesWeek of 2026-09-06: -3,249 linesWeek of 2026-09-13: +214,835 linesWeek of 2026-09-13: -40,784 linesWeek of 2026-09-20: +7,331 linesWeek of 2026-09-20: -1,529 linesSep 28, 2025Sep 20, 2026
+651.8K lines added, -145.5K removed over the last year.

Commits per week

last 52 weeks
360Week of 2025-09-28: 14 commitsWeek of 2025-10-05: 1 commitsWeek of 2025-10-12: 2 commitsWeek of 2025-10-19: 2 commitsWeek of 2025-10-26: 0 commitsWeek of 2025-11-02: 2 commitsWeek of 2025-11-09: 9 commitsWeek of 2025-11-16: 12 commitsWeek of 2025-11-23: 5 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 4 commitsWeek of 2025-12-14: 10 commitsWeek of 2025-12-21: 12 commitsWeek of 2025-12-28: 4 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 2 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 1 commitsWeek of 2026-02-08: 0 commitsWeek of 2026-02-15: 0 commitsWeek of 2026-02-22: 1 commitsWeek of 2026-03-01: 0 commitsWeek of 2026-03-08: 0 commitsWeek of 2026-03-15: 1 commitsWeek of 2026-03-22: 5 commitsWeek of 2026-03-29: 0 commitsWeek of 2026-04-05: 0 commitsWeek of 2026-04-12: 0 commitsWeek of 2026-04-19: 0 commitsWeek of 2026-04-26: 13 commitsWeek of 2026-05-03: 4 commitsWeek of 2026-05-10: 2 commitsWeek of 2026-05-17: 23 commitsWeek of 2026-05-24: 22 commitsWeek of 2026-05-31: 3 commitsWeek of 2026-06-07: 1 commitsWeek of 2026-06-14: 0 commitsWeek of 2026-06-21: 0 commitsWeek of 2026-06-28: 2 commitsWeek of 2026-07-05: 3 commitsWeek of 2026-07-12: 2 commitsWeek of 2026-07-19: 0 commitsWeek of 2026-07-26: 1 commitsWeek of 2026-08-02: 6 commitsWeek of 2026-08-09: 7 commitsWeek of 2026-08-16: 2 commitsWeek of 2026-08-23: 0 commitsWeek of 2026-08-30: 0 commitsWeek of 2026-09-06: 7 commitsWeek of 2026-09-13: 12 commitsWeek of 2026-09-20: 36 commitsSep 28, 2025Sep 20, 2026
233 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 2 commitsSun 1:00 — 1 commitsSun 2:00 — 2 commitsSun 3:00 — 4 commitsSun 4:00 — 0 commitsSun 5:00 — 3 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 0 commitsSun 9:00 — 7 commitsSun 10:00 — 8 commitsSun 11:00 — 2 commitsSun 12:00 — 2 commitsSun 13:00 — 1 commitsSun 14:00 — 1 commitsSun 15:00 — 1 commitsSun 16:00 — 0 commitsSun 17:00 — 1 commitsSun 18:00 — 0 commitsSun 19:00 — 0 commitsSun 20:00 — 1 commitsSun 21:00 — 2 commitsSun 22:00 — 3 commitsSun 23:00 — 3 commitsMon 0:00 — 1 commitsMon 1:00 — 1 commitsMon 2:00 — 2 commitsMon 3:00 — 5 commitsMon 4:00 — 1 commitsMon 5:00 — 1 commitsMon 6:00 — 1 commitsMon 7:00 — 1 commitsMon 8:00 — 1 commitsMon 9:00 — 6 commitsMon 10:00 — 2 commitsMon 11:00 — 5 commitsMon 12:00 — 2 commitsMon 13:00 — 7 commitsMon 14:00 — 1 commitsMon 15:00 — 6 commitsMon 16:00 — 4 commitsMon 17:00 — 0 commitsMon 18:00 — 6 commitsMon 19:00 — 8 commitsMon 20:00 — 6 commitsMon 21:00 — 12 commitsMon 22:00 — 8 commitsMon 23:00 — 3 commitsTue 0:00 — 3 commitsTue 1:00 — 1 commitsTue 2:00 — 0 commitsTue 3:00 — 0 commitsTue 4:00 — 1 commitsTue 5:00 — 4 commitsTue 6:00 — 1 commitsTue 7:00 — 3 commitsTue 8:00 — 0 commitsTue 9:00 — 4 commitsTue 10:00 — 5 commitsTue 11:00 — 6 commitsTue 12:00 — 4 commitsTue 13:00 — 1 commitsTue 14:00 — 4 commitsTue 15:00 — 1 commitsTue 16:00 — 3 commitsTue 17:00 — 3 commitsTue 18:00 — 3 commitsTue 19:00 — 9 commitsTue 20:00 — 7 commitsTue 21:00 — 3 commitsTue 22:00 — 7 commitsTue 23:00 — 5 commitsWed 0:00 — 2 commitsWed 1:00 — 0 commitsWed 2:00 — 0 commitsWed 3:00 — 0 commitsWed 4:00 — 0 commitsWed 5:00 — 0 commitsWed 6:00 — 0 commitsWed 7:00 — 5 commitsWed 8:00 — 2 commitsWed 9:00 — 5 commitsWed 10:00 — 1 commitsWed 11:00 — 10 commitsWed 12:00 — 4 commitsWed 13:00 — 3 commitsWed 14:00 — 4 commitsWed 15:00 — 2 commitsWed 16:00 — 1 commitsWed 17:00 — 1 commitsWed 18:00 — 1 commitsWed 19:00 — 3 commitsWed 20:00 — 6 commitsWed 21:00 — 2 commitsWed 22:00 — 3 commitsWed 23:00 — 0 commitsThu 0:00 — 1 commitsThu 1:00 — 1 commitsThu 2:00 — 0 commitsThu 3:00 — 1 commitsThu 4:00 — 0 commitsThu 5:00 — 0 commitsThu 6:00 — 0 commitsThu 7:00 — 4 commitsThu 8:00 — 7 commitsThu 9:00 — 2 commitsThu 10:00 — 1 commitsThu 11:00 — 1 commitsThu 12:00 — 3 commitsThu 13:00 — 5 commitsThu 14:00 — 1 commitsThu 15:00 — 10 commitsThu 16:00 — 8 commitsThu 17:00 — 2 commitsThu 18:00 — 4 commitsThu 19:00 — 4 commitsThu 20:00 — 8 commitsThu 21:00 — 3 commitsThu 22:00 — 1 commitsThu 23:00 — 2 commitsFri 0:00 — 9 commitsFri 1:00 — 4 commitsFri 2:00 — 0 commitsFri 3:00 — 0 commitsFri 4:00 — 0 commitsFri 5:00 — 0 commitsFri 6:00 — 0 commitsFri 7:00 — 4 commitsFri 8:00 — 7 commitsFri 9:00 — 1 commitsFri 10:00 — 3 commitsFri 11:00 — 3 commitsFri 12:00 — 1 commitsFri 13:00 — 3 commitsFri 14:00 — 3 commitsFri 15:00 — 1 commitsFri 16:00 — 5 commitsFri 17:00 — 7 commitsFri 18:00 — 4 commitsFri 19:00 — 1 commitsFri 20:00 — 6 commitsFri 21:00 — 5 commitsFri 22:00 — 7 commitsFri 23:00 — 7 commitsSat 0:00 — 3 commitsSat 1:00 — 2 commitsSat 2:00 — 0 commitsSat 3:00 — 0 commitsSat 4:00 — 0 commitsSat 5:00 — 0 commitsSat 6:00 — 0 commitsSat 7:00 — 1 commitsSat 8:00 — 1 commitsSat 9:00 — 5 commitsSat 10:00 — 2 commitsSat 11:00 — 4 commitsSat 12:00 — 4 commitsSat 13:00 — 2 commitsSat 14:00 — 4 commitsSat 15:00 — 2 commitsSat 16:00 — 0 commitsSat 17:00 — 0 commitsSat 18:00 — 0 commitsSat 19:00 — 2 commitsSat 20:00 — 0 commitsSat 21:00 — 4 commitsSat 22:00 — 5 commitsSat 23:00 — 2 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.
DateListRankStars gained
May 24, 2026daily#20+108