Smudge Tweaks testers needed

Tags: #<Tag:0x00007f0a40de8c98>


So as the issue with the switching bucket order bothered me too much and since I also don’t own a pen with tilt support, I tried some tweaking in the engine myself. What I did is basically watch out for sudden painting direction turns (angle difference > x) and reset the whole smudge_buckets array in case. It’s very hacky and kind of unstable though.

I like the resulting strokes however (see video). Does anyone have an idea how to achieve this in a good way?


Resetting is kind of a shame, you might have some lovely colors in those smudge buckets. :_p I wonder if you could invert the array instead? so 0–>255, 1–>254, etc?

Although, this solution kind of permanently links smudge_buckets with direction, too, which isn’t very flexible. It is kind of hard to think of better use-cases for smudge_buckets besides offsets, but I made a brush that was pretty interesting linking smudge_bucket with gridmap input, so that particular buckets are used for particular x,y coordinate intervals (say, 0,0, 5, 5, 10, 10, etc all use one smudge bucket). That resulted in a neat background texture.

It’s also interesting to link smudge_buckets with pressure. Think of a brush that has been dipped in a lot of paints over time, when you press really hard you might bend the bristle enough that an earlier color is recalled. So, a dirty brush if you will :-). You can combine both offsets+pressure if you do something like a squared graph for the offsets, assigning several buckets for each “bristle”. Then it’s a simple pressure map to increase the smudge_bucket from say, 0 to 5 w/ pressure. Then each bristle will get 0-5 added to it to switch the smudge_buckets.


Thanks for the tips! Sadly I still can’t think of a clean solution for my issue with the flat brush. In the meantime I experimented a bit more and made a round brush with manually placed dabs that has no rotation and that I like quite much (see picture).

While creating some brushes, I was wondering, is there an angular offset that only follows the “Offset Angle” setting and not any pen rotation? For this purpose I currently use “Angular Offset Side Asc”, which seems to react the way I expect (probably because I don’t have tilt support).

I also managed to create a 10x10 square brush using the gridmap as offsets with 100 resulting separate buckets. It’s kind of tricky but it works :sun_with_face:. All in all these changes are pretty great and I look forward to seeing them in the official mypaint release soon!


Haha that’s awesome. I’m glad someone else is messing around with this stuff. It’s amazing how combining a few settings can do so many things. In some ways not having custom dab masks is a curse, but in other ways it is more flexible than other brush engines. I especially like that MyPaint doesn’t use bezier curves on the graphs-- so much more flexible being able to square things off.

Hmmm… So maybe an “Offset Side Static” and “Offset Side Mirrored Static” with the angle based on the current view rotation (in addition to the Offset Angle setting)? That would really easy.

That sounds neat. Feel free to share your brushes somewhere! (if you want!) ;-). I don’t know if you tried my “half tone” brushes based on the gridmap, but it is especially tricky to get it to work right when resizing the brush. You have to manipulate the gridmap scale along w/ the base brush radius input, and it takes a lot of fine tuning as you know :stuck_out_tongue:

I can’t think of a good solution for the “offsets follow direction” problem, and the smudge buckets. It feels like the SHOULD be a simple solution but it requires a higher order thinking than I’m equipped with at the moment :stuck_out_tongue:


@abn5x I’ve updated the code so it has a lightweight spectral upsampling option (smudge spectral setting). No need for the big rgb.txt nonsense.
Now you can smudge any kind of blue+yellow and it will be green.

Download windows binaries here:

The caveat still exists that you have to use the brush color on the brush at least somewhat. That is, you can’t just put blue and yellow next to each other and then switch to a pure blending-only colorless tool to get the colors to mix. So, not a big deal but I’m working on a solution for that.

There is a not-insignificant performance penalty for spectral mixing, but at least there really isn’t any memory or other issues with using it. My next step is to reduce the spectral wavelengths from 36 components to something less, maybe 5-10 and see if that still works. That would bump performance up by 3X-4X.


WOW, I’ll definitely try your new version, when I finish my exams, to see how the performance goes, got a laptop with an intel 7700hq and 32gb and a PC with intel 3930k and 32gb. And you’re doing an amazing job, I still don’t get how in the world big name companies like Corel or Adobe, or even Celsys (Clip Studio Paint) didn’t come with such things, which us the artist, have been asking for ages(or at least the finnicky ones like me :stuck_out_tongue: ). You trully Rock!


just tried the new code, it works flawlesly :smiley: i don’t see any decrease in performance from the last version


Thanks for testing! Nice sketch, too! My desktop is fine but my old celeron-based laptop does not like it too much, but hopefully optimizing later on can help.


Not related to the smudge-tweaks, but would it make sense to merge those 3 settings (Angular Offset Side, +Asc, +Static) into one (e.g. Angular Offset) and just control the angle via Offset Angle using either Direction, Ascension, Random or any other input? Or would you lose some control by doing so?


Hah, that makes a lot of sense, at least for the regular non-mirrored offset setting. However for the mirrored one I don’t know if that works. I tried to get my feather brushes to work with just the “Angular Offset Mirrored Asc”, which is basically static if you don’t have a tilt pen as you discovered already. But, because the dabs are “mirrored” it seems to throw a wrench in there. Actually, I only just recently pushed a commit to make the mirrored offsets truly “mirrored” the way a reflection in a mirror would be:

like this:

///////   (offset angle of ~30 degrees)
-------->  (direction or ascension angle)

instead of:


So, it seems like mirroring requires a special handling compared to the normal one. Maybe it is doable? It would be nice to collapse them all into two settings instead of 4 (or 6 if we include a static option besides ascension). We’re also hoping to collapse Direction and Direction 360 into one input, since they seem to be compatible as well as redundant. The trick is doing this without breaking a ton of brushes out there.


So I tried the subtractive, spectral and pow settings (1 / 1 / 2.4) and I kind of like how colors blend with it. The right side of the image below shows the settings activated while painting on a dark saturated red:

When using the spectral setting however, I noticed quite a performance loss (resulting in laggy strokes). I see you are currently working on the spectral setting, is this going to improve?

When using the pow setting (e.g. 2.4) while smudging on very dark gound with the same active color (e.g. pure black) I noticed some slight brightening up to dark gray which is a bit annoying while painting dark areas.

I also tried to just use subtractive smudge (1.0) but without both pow and the spectral setting I seem to get some weird hue and value shifts. Do these settings rely on each other?


As soon as I get it fully working, I want to try to reduce the spectral primaries from 36 down to something like 5 or 10 and see if that works. That should bump performance up a lot. But yeah, it’s going to be slower. There are some settings that can dramatically improve smudge performance, in general, though. For instance, if you increase the smudge length setting (and even use the smudge length multiplier to lengthen this exponentially) you can reduce the amount of times the canvas is sampled ( thus reducing triggering smudge code). This is practically required for brushes with lots of small dabs, like we are doing with the flat brushes. On the plus side, extending the smudge length lets you “smear” paint much much farther (while simultaneously increasing performance). So, definitely try my brush pack flat brush that I’ve tuned a bit and see if it’s better. I don’t have any lag on my desktop but it is a pretty beefy Intel core i7.

That’s not right. Hmm… Are you using the latest commit from 3 days ago? I switched out the spectral curves with some new ones for no particular reason other than to try using curves generated straight from python’s colour-science library instead of the Scott Burns curves. That might have screwed up something since. I’ll try to reproduce this and I might go back to Scott Burns curves. In fact, yeah, I think I will for a few other reasons.

Well, the settings are definitely intertwined with color science (of which I still have a LOT to learn). I didn’t want to hard-code it so that subtractive mode always uses spectral and a pow 2.4, but it could be that that is the best configuration since it is the most physically plausible model. I kind of like keeping it flexible so you can really see how a power function affects blending, etc etc. Especially in the brush editor “live update” mode.

Stay tuned, I hope (in the next day or so) to have the canvas sampling code updated to improve spectral blending even more, resulting in more saturated color blending. Right now whenever you sample the canvas for a color (this is smudge length setting), MyPaint sums up the pixels from a small area of the canvas and averages them together-- essentially an additive function. So if you have blue and yellow pixels near each other and you sample that, you’ll get a grey or even white color.

There are two ways around this that seem feasible. One is to use a kmeans clustering algorithm to find the “dominant color”. I’ve implemented this in the CIECAM PR on the mypaint side, for the color picker. Seems to work pretty well. If you pick a region that has yellow and blue pixels, you’ll generally get back a yellow or a blue color that actually exists on the canvas instead of a greyish color. I’m thinking this can help avoid progressively duller and duller colors if your process involves a lot of color picking.

The other way around this is what I’m working on in the libmypaint side, which is to do the same kind of spectral subtractive color blending for all the pixels in the sampled area. So, yellow and blue pixels would be sampled as a green color instead of grey, and so on. Unfortunately this means even more processing overhead. . . but I don’t think it’s insurmountable, especially if I can reduce the spectral primaries from 36 to 5 or 10 like I was saying. Sorry for the wall of text. Please try my brushes and see if the performance is still bad :slight_smile:


I just pulled the latest changes, I am currently on the smudge_tweaks branch but the broblem is still there. I also tried it with your Flat2#1 brush. The difference is barely noticable though (#000000 turns to #060606).

I just tried them and they don’t seem to be affected by the sprectral setting as much as mine, even if I turn up the dabs per actual radius. I guess my brushes can still be optimized a bit :slight_smile:, I currently use about 70 dabs per actual radius. A major point might also be the brush radius and the smudge radius setting.

Here is a link to one of my current brushes if you want to take a look:


I also wanted to ask this for a while. Ever since i built MyPaint manually i get the following error when trying to turn on the live update mode:

Mypaint version: 1.3.0-alpha+git.791da343
System information: Linux-4.16.12-1-ARCH-x86_64-with-arch
Using: Python 3.6.5, GTK 3.22.30, GdkPixbuf 2.36.12, Cairo 1.15.12, GLib 2.56.1
Traceback (most recent call last):
  File "/usr/local/lib/mypaint/gui/", line 1030, _live_update_idle_cb(self=<brusheditor.BrushEditorWindow object at 0x7f231...brusheditor+BrushEditorWindow at 0x5647cd34ba40)>)
            doc =
            self._live_update_idle_cb_id = None
  variables: {'doc.redo_last_stroke_with_different_brush': ('local', <bound method Document.redo_last_stroke_with_different_brush of <Document nlayers=1 bbox=Rect(0, 0, 0, 0) paintonly=False>>), 'self._brush': ('local', <lib.brush.BrushInfo object at 0x7f23196cd438>)}
  File "/usr/local/lib/mypaint/lib/", line 992, redo_last_stroke_with_different_brush(self=<Document nlayers=1 bbox=Rect(0, 0, 0, 0) paintonly=False>, brushinfo=<lib.brush.BrushInfo object>)
  variables: {'cmd.update': ('local', <bound method Brushwork.update of <Brushwork 0x7f2310078a58 1.108s None>>), 'brushinfo': ('local', <lib.brush.BrushInfo object at 0x7f23196cd438>)}
  File "/usr/local/lib/mypaint/lib/", line 362, update(self=<Brushwork 0x7f2310078a58 1.108s None>, brushinfo=<lib.brush.BrushInfo object>)
            stroke = self._stroke_seq.copy_using_different_brush(brushinfo)
            self._stroke_seq = stroke
  variables: {'layer.render_stroke': ('local', <bound method StrokemappedPaintingLayer.render_stroke of <PaintingLayer 'Ebene'>>), 'stroke': ('local', <lib.stroke.Stroke object at 0x7f23196dab00>)}
  File "/usr/local/lib/mypaint/lib/layer/", line 1507, render_stroke(self=<PaintingLayer 'Ebene'>, stroke=<lib.stroke.Stroke object>)
            self.autosave_dirty = True
  variables: {'stroke.render': ('local', <bound method Stroke.render of <lib.stroke.Stroke object at 0x7f23196dab00>>), 'self._surface': ('local', <lib.tiledsurface.MyPaintSurface object at 0x7f231843d208>)}
  File "/usr/local/lib/mypaint/lib/", line 89, render(self=<lib.stroke.Stroke object>, surface=<lib.tiledsurface.MyPaintSurface object>)
            version, data = self.stroke_data[0], self.stroke_data[1:]
            assert version == b'2'
            data = np.fromstring(data, dtype='float64')
  variables: {'version': ('local', 50)}

Did I do something wrong? I followed the basic build instructions… :see_no_evil:
(I use your fork of libmypaint and the original mypaint’s master branch)


I noticed you are running python 3.6. Can you try building with python2? I think you can just run python2 demo if you have python 2.7 installed. I don’t think python 3 is ready for production yet w/ mypaint.

Ok so if you pull the latest commit I have updated the spectral smudge to get a fully spectral color from canvas that isn’t munged up by additive averaging. If your smudge radius isn’t too big it’s not really noticeably slower to me on my desktop.

I checked out your brush and tweaked a bunch of settings and it’s pretty fast now, I can zoom out a bit and make the brush big/small by mapping settings to the base_brush_radius. I keep the dab size small via this setting, add some jitter when the size is bigger, and extend the smudge length multiplier as well. This way you can smear even big brushes and zoomed way out. I put it here for now so you can download it: Would you mind if I tweaked your brush more and added it to my collection? I like how you’ve mapped the dab angle to the direction and made that circle shape w/ the angle offset.

There’s really just one more major place I need to jam in the spectral upsampling-- the stamping/brush modes. This is where brush opacity, pixel feathering, and hardness all introduce additional additive mixing paths. This is why my brushes pretty much all have 100% opacity, hardness 1.0 and pixel feathering 0.0. If you use these settings you might notice the edges of the brush are messed up or different. Anyway, that’s next on the list. . . .


Thanks, sudo python2.7 managed_install did the trick for me. This makes it soooooo much easier to modify and test brushes :sun_with_face: …Might me a good thing to mention on the mypaint README.

I intentionally setup the radius of my brush so that the indicator circle matches the resulting stroke size. I think this might also directly affect the settings dabs per actual radius and smudge radius.

Sure, go ahead! I might share a set of 2 or 3 brushes myself once all your smudge tweaks are finished :slight_smile:. I also noticed you managed to give those dabs a constant size independent of the brush size. I also tried that once, but did not like how the visual density gets lower with increasing brush sizes. I guess you’d need brush dynamics for the dabs per actual radius setting to achieve that…

You might also like this brush’s dab placement:
It’s the one called “toniw_smudge_5” in my Dropbox folder. I used a little hack to get a second random input by randomizing the stroke duration setting. Did you ever have the need for a second independent random input?

(Feel free to reuse any of these :frog:)


Long ago I tried just editing the brushsettings.json “constant = false” for these settings and it didn’t work. So, I took a look again after you said that… and it was pretty simple to switch it to dynamic. Fetch latest commit and try it out :-). Oh, you might need to do a git fetch; git reset --hard origin/smudge_tweaks since I dropped an earlier commit. I’ve barely tested but seems really cool… you can change the density via speed, etc. very interesting.

That is pretty slick, going to have to download and see how you did that haha :slight_smile:

Yes, actually I’ve had a need for a 2nd stroke settings/input and a 2nd custom input as well :-D. Shouldn’t be hard to copy/paste them really. . . :stuck_out_tongue:


This should be fixed now. So, all the info I’ve come across says that you need a minimum value for the weighted geometric mean, something bigger than 0.0 for black. I was using the smallest number that would work for an 8 bit integer system, but mypaint is 15 bit and I’m actually using floating points for the mixing… so it seems like I can use a much smaller number (called an epsilon). Now you can blend black and black and still get a pure black. I was worried that this would make black too powerful, but it seems to be fine.


Great! Seems to work fine with pure black and very dark colors. While playing with pure black I noticed that I had to press harder to actually change the color of pure black compared to other colors:

The shown brush has 100% opacity and hardness, no pixel feather, pow 2.4. This seems to happen with the subtractive setting.

… not really a big problem, but maybe it’s easy to fix :slight_smile:


Thanks for testing! Hmm maybe I did go too small w/ the epsilon. I just reverted it back ot 0.0001 but I made it easily adjustable. If you edit helpers.h on line 15 you can change it:

#define WGM_EPSILON 0.0001

Brain is fried trying to conceptually understand what the minimum value should be… the mypaint model is 15 bit but it gets really confusing because the color GUI editor is all 8 bit (hence 0-255). So, when you use the color picker I think your actual brush color that you paint with can be a different value than what is represented in the GUI. That is, it can be in-between 0.5/255, etc. But that really shouldn’t matter anyway since we’re obviously at 6/255.

Can you retry your test with a few different values for the WGM_EPSILON?