Adding hard lines to a gradient

I’ve created a shader that takes a greyscale heightmap and renders it like a contour map by breaking up the gradients into hard steps and adding lines between each step, like this:

I’m mostly happy with it, except that the the line widths can vary, blowing out into fat chunks where the gradient becomes “flat” or level.

First I chop the height value up into steps with this (where h is the float height value and _StepCount is the number of contour steps I want):

float steppedDepth = ceil(h * _StepCount) / _StepCount;
Works great! But then I add the lines with this hacky method (where _LineSize is half the desired line width in gradient values):

float lineVal = fmod(h * -_StepCount - _LineSize, 1) + fmod(h * _StepCount - _LineSize, 1);
It’s not a very good solution, so I was wondering if any shader wizards could suggest a better way to add fixed-width lines between input values…

what you need is to:

  • first step the line
  • do a sobel filter that discriminate the step you want (ie if you don’t want it on brown to blue)
  • multiply the sobel onto the step colors

TIL the term “sobel filter”. That looks perfect, I’ll give it a shot!

Thanks!

All you really need to do is scale the line width by the gradient of the height (which you can access through screen space derivative functions);

float width = fwidth (h) * _LineSize
float lineVal = fmod(h * -_StepCount - width, 1) + fmod(h * _StepCount - width, 1);

For something like this situation, fwidth() will produce poor results due to the derivatives not being constant. A proper sobel type filter will make much better looking line.

1 Like

Good point. For that I made reference to a shader I wrote for UA that does this, but I now realise I remembered it poorly - the height wasn’t sampled from the texture directly, rather the vertex coords were displaced and derivatives taken from that.

I tried the fwidth() solution, and it actually worked really well unless I’m zoomed all the way in; at that point the lines get noisy and break up in an odd way.
I think I can probably work around that though (by modifying the target width based on current zoom level), and avoid the cost overhead of the sobel solution.

Thanks a lot for your help!

You may not need a true Sobel. Just 4 samples in an X can look pretty good too!

I’m interested in this fwidth() thing though. I’ve never messed with derivatives in shaders and it seems like it could be useful. Thanks, Namey5!