Skip to content

Repository files navigation

verlet

Simple physics approximation using Verlet Integration approach.

Inspired by this video. Almost exactly the same but I wanted to play with it myself :).

You can see the demo here: https://www.youtube.com/watch?v=vgMczxau7VM and here https://www.youtube.com/watch?v=vgMczxau7VM.

It is also possible to generate video that results into some predefined image (verlet_video project): https://youtu.be/NFWb60gZgKY

Building

git clone https://github.com/Sunday111/yae
git clone https://github.com/Sunday111/verlet
cd verlet
../yae/yae build

If yae is installed on PATH, run yae build from the project root instead. Machine-specific CMake overrides go in an ignored local-config.json next to yae_project.json.

verlet_video

verlet_video runs the simulation with every object coloured by where it will eventually come to rest, so the settled pile reproduces a target image.

The picture is --image, and any PNG or JPEG will do:

yae run verlet_video -- --image ~/pictures/van.jpg --klvk-diagnostics video.json

It works out the colours itself: on startup it simulates settle_frames frames with no rendering, reads back where each object ended up, samples the image there, then resets the solver and emitters and runs again with those colours. Both passes run single threaded from the same state, so object i lands where the first pass said it would, and a run is reproducible frame for frame.

Option Default What it is
--preset VerletAppPreset.json next to the executable Window size, object budget, and emitters. Written by the Save Preset button.
--image content/target_image.png The picture to reproduce. Sampled at each settled position.
--positions simulate instead Read settled positions from a dump written by the Save positions button rather than simulating them.

Recording is klvk's, not this project's — pass a diagnostic configuration containing a video block:

yae run verlet_video -- --klvk-diagnostics video.json
{
  "version": 1,
  "presentation": "offscreen",
  "framebuffer_size": [1920, 1080],
  "clock": {"mode": "fixed", "step_ns": 16666667},
  "video": {"path": "verlet.mp4", "encoding": "h264", "compression_level": 3, "include_ui": false},
  "exit": {"frame": 1800},
  "application": {"settle_frames": 1800}
}

Video requires offscreen presentation, a fixed clock, and even framebuffer dimensions — an odd width or height fails at startup rather than producing a broken file. The clock step is the output frame rate, and exit decides how long the recording runs. Encoding is av1, h264, or mpeg4, on cpu or gpu; see klvk's readme for the full set of options.

application.settle_frames is how long the first pass simulates before reading positions back, so it is the frame the picture appears on. It defaults to 3600 and is a separate decision from exit.frame, which is where the recording stops: settling earlier than the end holds the finished picture on screen for the remaining frames, and the two are equal only when the picture is meant to land on the very last one.

Pause stops the simulation while everything else keeps going: the view still renders, the camera still moves and the tools still work, so a frozen pile can be looked at and painted into before being let go. Next frame runs exactly one step and stays paused, which with a fixed timestep is exactly 1/60 s and its eight substeps. Space toggles the pause and the right arrow steps, both ignored while a text field has the keyboard.

World space

Screen size decides how much world there is. One world unit is kPixelsPerWorldUnit pixels, so an object covers the same few pixels at every resolution and a bigger window simulates a bigger world rather than the same world drawn larger.

Nothing in a preset is written in world units, so none of it has to be recomputed for a different resolution. Emitters are placed in relative coordinates, where -1 and 1 are the edges of the world on each axis and the origin is its centre: {"X": 0.99, "Y": 0.4} is a point just inside the right wall, forty percent of the way up. A relative length — a radial emitter's radius — is measured against the shorter half of the world, so a ring stays a ring at any aspect ratio.

SpeedFactor stays in world units on purpose, and a flat emitter's Spacing is counted in object diameters: both are about objects, and objects are the same size whatever the world.

The object budget is stated one of two ways, and a preset carries exactly one of them:

Key Meaning
MaxObjectsCount A literal number of objects. The same preset fills a small window and looks sparse in a large one.
MaxObjectsSaturation A share of what the world holds, from 0 to 1, where 1 is objects packed as tightly as circles go. Means the same thing at any resolution.

Saturation is converted to a count from the world's area whenever the world changes, so it follows a resize on its own. The Limit by saturation checkbox switches between the two and carries the current budget across, and the one in force is the one written back by Save Preset.

A worked example ships in content/, with its recording configuration beside it. fill_2244x6864.json is a tall 2244x6864 world lined with three flat emitters, one along each surface bounding the top 30%. They fill it to saturation 1.0 in 28 seconds, the picture is composed at 33 seconds, and the recording runs on to 38 seconds so the finished image holds on screen.

yae run verlet_video -- \
    --klvk-diagnostics src/verlet_video/content/fill_2244x6864_video.json \
    --preset src/verlet_video/content/fill_2244x6864.json \
    --image src/verlet_video/content/target_image.png

A preset's WindowSize sizes the window an interactive session opens; a render takes its resolution from the configuration's framebuffer_size instead. The two agree in the example, but they no longer have to: everything the preset places is relative, so the same file renders at any resolution and describes the same picture.

Emitters

Radial spawns outward from a ring, along a sector of it. Position is relative and Radius is relative to the shorter half of the world; Radius and SectorDegrees together decide how many objects leave per tick, and PhaseDegrees points the sector, measuring zero as straight up.

Flat spawns from a straight surface, all of it moving the same way. It is given as its two ends, Start and End, both relative — which is how a surface says "the whole top edge" without naming a size.

Direction is a vector, and LocalDirection says how to read it. Read locally, it is in the surface's own frame: x runs Start to End and y is the surface's left normal, so a surface points objects out of itself without naming a compass direction. Read globally it is simply a direction in the world, the same for every surface alike.

Spacing is the gap between neighbouring objects, in object diameters: 0 packs them touching, 1 leaves a whole object's width between them. It is not relative to the world, because it is a distance between objects and objects are the same size whatever the world.

A flat emitter fills a rectangle evenly, where a radial one is a point source that builds a cone and spreads only through collisions.

Burst fills the remaining object budget in one tick, placing stationary particles on an even grid across the object bounds. It emits once per reset; Rearm allows another burst. A preset entry is {"Type":"Burst","Burst":{}}. Choose a world large enough for the requested count if initial overlaps are unwanted.

CPU benchmark

The headless Google Benchmark target measures VerletSolver::Update() from verlet_lib. The library's Burst emitter creates 100,000 stationary particles in a 400 × 300 world before timing starts. Each benchmark runs 300 consecutive frames, with eight physics substeps per frame, using 1, 8, or 32 solver workers. Reported real time is elapsed time per frame, including worker execution. Each repetition starts with a fresh simulation.

The benchmark is opt-in and is not run by the normal build or test suite:

yae build verlet_bench
yae run verlet_bench -- --benchmark_repetitions=3

Use Google Benchmark's --benchmark_filter='BurstSimulation/8/' to select eight workers, or --benchmark_out=benchmark.json --benchmark_out_format=json to save results. Spawning and teardown are excluded; all 300 simulation frames are timed, including the initial fall and collisions.

For native CPU tuning, put this machine-specific override in local-config.json, then rebuild:

{"cmake_definitions":{"CMAKE_CXX_FLAGS_RELEASE":"-O3 -DNDEBUG -march=native"}}

Such binaries target the build machine's CPU. For comparisons, use identical compiler settings, particle counts, world dimensions, frame counts, and worker counts, and run repeated measurements without concurrent builds.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages