G'MIC Adventures #1: A fake 3D extrusion filter (2.5D)

Hello.
I’ve stumbled accross this page : https://www.reddit.com/r/GIMP/comments/1id2v7g/how_would_i_achieve_this_hole_text_effect_using/

and I found that the effect the OP is looking for is pretty cool:

And it seems also it’s not that easy to do in GIMP (I mean, like a one or two mouse click solution :slight_smile: ).

So, my plan is to create a new filter that just does that for G’MIC-Qt.

And for once, I thought it would be nice to write a little log on the progress of this filter’s implementation (perhaps it could be of interest to other filter creators?).
So I’ll try to post here the various stages in my process of creating a filter that tries to do this sort of thing (including the bugs :slight_smile: ). Sharing my impressions and my approach to the problem. It could be fun!

Not sure I will succeed, but let’s try!

2 Likes

Oh come on… Lol. You know you can.

Iteration #1

OK, so here’s the first way I’m going to approach the problem:
I’m going to try to transform an input image that just represents a monochrome shape (I’d say binary, but I want to be able to allow anti-aliasing in the input image).
Just like this one:
shape

(hopefully, this will work also with the negative version, where the “hole” is the text).

And we want to have kind of 3D rendering, but I want to do it without doing real 3D (in the sense of having vector 3D meshes). There are several reasons for this:

  • We want to be able to process input images with arbitrarily complex shapes, and I don’t want to end up having to manage 3d meshes with millions of primitives. This seems too complicated for the type of “pseudo-3d” rendering I want to generate.
  • It’s only “fake-3D”, so no perspective projection needed. My memories of the game “Crafton and Xunk” on the Amstrad CPC (Get Dexter! in English) tells me that 2.5D rendering is perfectly acceptable with just a few well-thought-out tricks.

So my first attempt is to add fake shadow/lightling to the input shape, rotate it a bit, and copy this modified shape as much as needed to create a fake 3D effect.
The fake shadow/lighting can be done easily just from the X&Y gradients of the input shapes.

Let’s try this:

foo :

  # Create 2d binary mask.
  0 t. "3D Text",0,0,${"font Acme,128"},1,1 r. 400,400,1,1,0,0,0.5,0.5

  # Add shadow/lighting.
  +g. xy +[-2,-1] 100%,100%,1,3,[255,128,0] rv[-2,-1] *. 200 +[-2,-1] c. 0,255 *. ..

  # Rotate.
  rotate[-2,-1] 25,1 a[-2,-1] z autocrop. s. z channels.. 0

  # Pseudo 3D rendering.
  400,400,1,3
  eval "
    M = crop(#0);
    S = crop(#1);
    for (y = 100, y>60, --y,
      draw(#-1,S,30,y,0,0,w#0,h#0,1,3,1,M);
    );
  "

I got this result:
iter1_000002

which is ugly but encouraging :slight_smile:
In particular, there are too much color variations in the extruded shape, so that’s something I need to fix.

But, hey, that was only the first iteration :slight_smile: Stay tuned!

1 Like

Uhm you might be a lil embarrassed about this, but you can do this with a few mouse clicks in gmic right now. You did make a 3D Extrusion feature in gmic…It seems to do what you are looking for here.

Is it not capable of doing what you are looking for? Maybe with a little tweaking?

I’m keeping my fingers crossed for a new filter, but in Gimp it’s very simple (text rotation + GEGL Long Shadow).

3D Text

Unless you mean a different result?

I think the main issue is G’MIC is raster graphic processing. Unless it has curve capability, I don’t see much point to do this. It won’t be that smooth. Which reminds me of something, I really wish we had a vector counterpart to G’MIC, I’d prefer not to do Python.

The extrusion filter in G’MIC is indeed a “real” 3D filter, with perspective projection.
It uses marching cubes to “convert” the initial shape into a 3D mesh, which gives this look:

It’s OK (maybe a bit smootheed at the edges though), but as I said in my first post, it’s probably not suited for too complex shapes (the 3D mesh will have many many primitives).

The filter I’m trying to implement now is not 3D, is much more 2.5D.
This should enable a quite sharp rendering, possibily with high resolution inputs.

Indeed, but see the color of the long shadow ? It’s monochrome, so does not take account of the lighting, nor the normals to the extruded surface. It’s too flat, doesn’t look like 3D at all. Compare with the image on the first post, where the different side orientations lead to different colors.
Can you do that with GIMP ?

Iteration #2:

(no results, just analysis of last night’s test)

Sleep is the best advice (what could be more relaxing than thinking about how to implement a filter in the evening, in bed, before going to sleep?).

So this morning, I have a fresh look at this filter. Based on yesterday’s quick experience, I can tell you that the tricky part is going to be calculating regular colors for the “sides” of the extruded object (so having accurate normal vectors).

Indeed, in our case, we want the input image to already be a bitmap image (unlike in the tutorial video in my first post, where most of the work is done with vector objects).
And “pixmap” means “discretization”, whereas we want normal vectors to be as continuous as possible, especially if one side of the object is “linear” (and unfortunately, a straight line discretized at any angle looks very much like a set of pixels connected with possibly complex patterns).

So, in a way, we’re going to have to do a little work that looks like vectorizing bitmaps*.
Of course, we don’t want to do a complete vectorization (that sounds a bit too much). But at least a basic vectorization of the input shape’s contour should enable me to calculate sufficiently smooth normal vectors on the edges (after filtering the extracted curves), and therefore sufficiently uniform colors for a nice 2.5D rendering!

So I’m going to use the edgel calculation (see command edgels), which seems to be the quickest and most elegant way of calculating smooth normal vectors on the shape’s contours.

Let’s see what happens! :smiley:

Iteration #3:

Looks like my intuition was not that bad!
Edgels are so efficient to get smooth normal vectors to 2D shapes, it’s indecent :slight_smile:
(a special thought for my colleague Sébastien, who did part of his thesis with these models, and who introduced me to their use).

So, I’ve been able to render a smooth colorization of the shape’s side, and once this is done, I use the same technique as before.
Here is what my filter prototype looks like right now:

foo :

  # Create 2d binary mask.
  0 t. "  3D Text\nIteration \#3",0,0,${"font Acme,128"},1,1 r. 500,500,1,1,0,0,0.5,0.5
  rotate. 25,1

  # Estimate continuous field of normal vectors.
  +ge[0] 20% l. {
    edgels -1
    foreach {
      s c,-2 channels. 0,1
      f. "arg0(i0,[1,0],[0,1],[-1,0],[0,-1])"
      b. y,6,2 orientation.
      a c
    }
    a y
  }

  # Render colored sides of shape object.
  {0,[w,h]},1,3
  eval.. "begin(col1 = [ 255,128,0 ]; col2 = [ 128,32,0 ]);
    col = max(0,i2)*col1 + max(0,i3)*col2;
    I(#-1,i0,i1) = col;
  "
  dilate. 5
  rm..

  # Render 2.5D extruded view.
  {0,[w,h]},1,3
  eval "
    M = crop(#0);
    S = crop(#1);
    for (y = 20, y>0, --y,
      print(y);
      draw(#-1,S,0,y,0,0,w#1,h#1,1,s#1,1,M);
    );
  "
  100%,100%,1,3,[255,200,0] j.. .,0,0,0,0,1,[0] rm. # Add top face
  c 0,255

And the resulting image:

text_it3

Now that starts to look pretty cool, no ?

And the good news, if you negate the input shape, it renders the text as a hole, as expected:

text_it3_2

I have a feeling we’re off to a pretty good start!

3 Likes

To me, it looks like it’s pretty much done…
Think you could add outlines when you’re finished? That would be cool.

That was my first thougt, too. But have a look at the top left corners, they look a little bit blurry:
grafik
I’d like to see a much more sharp edge there.

@David_Tschumperle I have the feeling, that the idea with the “negate the input shape” is somehow wrong.
Is it possible to render this image without the “yellop top layer” like an empty object. Perhaps use find edges and then render it with the outlines. Then you could use the top layer as mask to blend out the “not inner part” of the object. (I hope you understand what I’m tring to explayn.)
text_it3

Or is it just the font that has corners that are too rounded?

The image is too small on my screen, i can’t really tell. Might be the colors(yellow near orange) playing tricks?

Iteration 4:

I think the overall algorithm is OK. Now, I’m focusing on the (important) details to improve the rendering as much as I can.
So what I’ve done since iteration #3 is :

  • Add a resizing step to crush the perspective of the input shape, giving the result an even more 3D look.
  • Improved the rendering of the side colors, with more control on the color shading as a function of the estimated orientation of the normal vector (from very smooth to quite sharp transitions).
  • Improved the “dilation” of the side color images, so that lighter colors don’t bleed onto darker ones.

Well, that’s it for this iteration : a lot of trial and error, and not necessarily many clear-cut visual improvements.
But that’s the way it is: the further you go, the more you improve the details.

Source code of the iteration #4:

foo :

  # Create 2d binary mask.
  0 t. "  3D Text\nIteration \#4",0,0,${"font Acme,128,1"},1,1

  rotate. 25,1 r. 100%,70%,1,100%,2 shift. 0,-30
  negate

  # Estimate continuous field of normal vectors.
  +ge[0] 20% l. {
    edgels -1
    foreach {
      s c,-2 channels. 0,1
      f. "arg0(i0,[1,0],[0,1],[-1,0],[0,-1])"
      guided. 3,100
      orientation. a c
    }
    a y
  }

  # Render colored sides of shape object.
  {0,[w,h]},1,4
  eval.. "begin(col1 = [ 255,128,0 ]; col2 = [ 128,32,0 ]);
    a1 = max(0,i2)^5;
    a2 = max(0,i3)^5;
    col = i2<0 && i3<0?[ 0,0,0 ]:(a1*col1 + a2*col2)/max(1e-8,a1 + a2);
    I(#-1,i0,i1) = [ col,1 ];
  "
  rm..
  s. c,-3 . dilate.. 11 -[-2,-1] inpaint.. .,0,1 rm. # Smart dilation of shape side colors

  # Render 2.5D extruded view.
  N=50
  {0,[w,h]},1,3
  eval "
    M = crop(#0);
    S = crop(#1);
    repeat ($N,k,
      y = $N - k;
      draw(#-1,S,0,y,0,0,w#1,h#1,1,s#1,1,M);
    );
  "
  100%,100%,1,3,[255,200,0] j.. .,0,0,0,0,1,[0] rm. # Add top face
  c 0,255

And the results:

iter4

and with the inverted one:

iter4_2

I got these results with shape_gear :

gear

gear2_000002Preformatted text

Had to run it through oldschool 8bit though :stuck_out_tongue:
image

1 Like

Iteration #5:

Now that the algorithm has been implemented for most of its aspects, it is time to create the filter GUI!
I’ve named the filter 2.5D Extrusion, and will probably put it in category Rendering

Actually, there is almost nothing special to say here.

I wrote down the list of all parameters I’ve introduced into my rendering algorithm, and created a simple interface for setting them. Here’s how the filter interface looks after this 5th iteration!

(here, the input shape is a simple “Hello World” written in white over a transparent background).

This is the part of creating a filter that I personally enjoy the least, as imagining a graphical interface for a filter isn’t necessarily very varied or interesting (I much prefer to think about the algorithm itself and implement it! :slight_smile: ).

But at the same time, it’s motivating to think that a new filter will land soon in G’MIC-Qt and be used by potentially quite a few users (and even that it will come in handy one day or another, with a bit of luck). So I try to do it well anyway, with attention to detail (here, it took me almost as long as implementing the rendering algorithm itself!).

So now I’m going to push this filter into Rendering/, make an announcement on G’MIC’s Mastodon thread, and see if I get any feedback (and possibly bugs to fix or new features to implement, if it’s not too complicated).

Here we go!

EDIT: I forgot to copy/paste the current version of the filter code:

#@gui 2.5D Extrusion : fx_extrude25d,fx_extrude25d_preview(1)
#@gui : Depth (%) = float(10,0,50)
#@gui : Angle = point(80,60)_0
#@gui : Shear (%) = float(25,0,100)
#@gui : Negate = bool(0)
#@gui : Normal Smoothness = float(10,0,50)
#@gui : sep = separator()
#@gui : Color Shading (%) = float(50,0,100)
#@gui : Top Color = color(255,200,0)
#@gui : Right Color = color(255,128,0)
#@gui : Bottom Color = color(128,64,0)
#@gui : Left Color = color(64,0,0)
#@gui : sep = separator()
#@gui : note = note("<small>Author: <i>David Tschumperlé</i>.      Latest Update: <i>2025/01/31</i>.</small>")
__fx_extrude25d :
  depth,angx,angy,shear,negate,normal_smoothness,shading,\
  Rt,Gt,Bt,Rr,Gr,Br,Rb,Gb,Bb,Rl,Gl,Bl:=${1-19}
  angle:=rad2deg(atan2($angy-50,$angx-50))

fx_extrude25d :
  __fx_extrude25d $*
  foreach {
    w,h:=w,h
    shear_factor:=1-$shear%

    # Transform input layer as a binary shape (with possible anti-aliasing).
    if isin(s,2,4) channels. 100% else norm. fi
    if $angle rotate. $angle,1,1 fi
    if $shear r. 100%,{max(1,$shear_factor*h)},1,100% fi
    if $negate negate. fi
    if h>1 autocrop fi
    if w
      n. 0,1

      # Estimate continuous field of normal vectors.
      +ge[0] 20% l. {
        edgels -1
        foreach {
          s c,-2 channels. 0,1 f. "arg0(i0,[1,0],[0,1],[-1,0],[0,-1])"
          guided. $normal_smoothness,100 orientation. a c
        }
        a y
      }

      # Render colored sides of shape object.
      {0,[w,h]},1,4
        eval.. "begin(
          colr = [ "$Rr,$Gr,$Br" ];
          colb = [ "$Rb,$Gb,$Bb" ];
          coll = [ "$Rl,$Gl,$Bl" ];
          const N = max(1,20*(1 - $shading%^0.5));
        );
        ar = max(0,i2)^N;
        ab = max(0,i3)^N;
        al = max(0,-i2)^N;
        col = (ar*colr + ab*colb + al*coll)/max(1e-8,ar + ab + al);
        I(#-1,i0,i1) = [ col,1 ]"
      rm..
      s. c,-3 . dilate.. 11 -[-2,-1] inpaint.. .,0,1 rm. # Perform dilation of side colors
      to_a.

      # Render 2.5D extruded view.
      depth:=int(h*$depth%/max(0.01,$shear_factor))
      {0,[w,h+$depth]},1,4
      1,$depth,1,1,">begin(M = crop(#0); S = crop(#1)); draw(#-1,S,0,$depth - y,0,0,w#1,h#1,1,s#1,1,M)" rm.
      ($Rt^$Gt^$Bt^255) r. [0],[0],1,4 j.. .,0,0,0,0,1,[0] rm. # Add top face
      k. c 0,255
    fi
    if $_is_preview r. $_preview_area_width,$_preview_area_height,1,4,0,0,0.5,0.5
    else r. {[max($w,w),max($h,h)]},1,4,0,0,0.5,0.5
    fi
  }

fx_extrude25d_preview :
  __fx_extrude25d $* _is_preview=1 fx_extrude25d $*
  line 50%,50%,$angx%,$angy%,1,0x0F0F0F0F,0
  line 50%,50%,$angx%,$angy%,1,0xF0F0F0F0,255

(77 lines of code for implementing such a filter, that is what a call “short” :slight_smile: ).

Now committed to the official G’MIC repository:

Filter, as seen after a filter update (requires G’MIC 3.5.1+):

Here is what i noticed with my cupid test :


For some reason foot goes out of bounds when using negate


No output when shear is set to 100%

The rotation tool is nice, but i thought it would rotate the whole “block” too. Maybe a second handle?
Also i wonder if you could add an option to extend the surface to the image size (no cropping)?

Coll, thanks. It works, but I have a similar problem.
I have a layer with a lot of space around it:


But when I launch your plugin, it ignores it:

More ideas from playing with the filter:

  • an option to set the top color to transparent
  • an option for a button color
  • and I don’t like the color block at the buttom:
    grafik