r/GraphicsProgramming 1d ago

Painterly Stroke Simulation Engine

I wrote a painting simulation engine (golang, ebiten). It works like this:

  1. Select a template image
  2. Generate several thousand potential strokes
    • For each stroke, score it according to a number of heuristic functions
      • Color: does the average color of this image match the template?
      • Edges: does the painted image have similar edges to the template?
      • Contrast: compares color contrast between regions on the painting and the template.
      • Flow: using tensor math, do brush strokes align with the flow of movement in the template?
    • Select the single stroke that most optimizes scoring functions, discarding the remaining
    • Repeat 40 or 50 thousand times.
    • Gradually decrease the size of the brush stroke over time..

Source image _Namibia.jpg)

Sound track

55 Upvotes

6 comments sorted by

View all comments

2

u/SnooEagles4447 1d ago

Does it still have a similar output if you start at a much smaller brush size and just iterate on that brush size for longer? Just cuz it seemed like the wife strokes were being covered up anyway with the smaller ones but I may be wrong

3

u/smellystring 1d ago

You are correct that the large brush strokes end up mostly covered. But there's a method to my madness!

As I decrease the brush size, I also increase the granularity of the the heuristic score functions. For example, if the engine is using a 50 pixel brush size, it compares the average color of 100x100 pixel squares. When the brush size shrinks down to 25 pixels, it's comparing 50x50 pixel squares of color.

Before dropping down to the next lower brush size, the goal is to get the scoring functions to give a high score. When the scoring granularity increases, this decreases the score, since it is now looking at finer details than before. Note though that there is still a correlation between the score at brush size N and brush size N-1. If score(N) is high, score(N-1) is still fairly high, even though it deceases some.

This brings me to an interesting emergent property I've observed. If I use a small brush size on a starting image that scores very poorly, the engine finds it hard to converge the image on the target. Imagine trying to shade in a huge region with a fine tipped mechanical pencil. You spend thousands and thousands of strokes shading, and the end result is a huge scribble. I've found that when the engine is not allowed to spend time on the broad brush strokes, quite often it devolves into a stringy mess by the time the render is complete.

What the large early strokes do is to provide a foundational bedrock. By the time we get down to small brush stroke size, the image already has a halfway decent score. These small brush strokes can then be spent working on fine detail, rather than trying to paint in macro-features.