NormalPainter : How it was made
NormalPainter is definitely an impressive little software. It was the first time I tackled the challenge of making an image editor and here I want to go through the process in making one. It took me about 5 days to complete (and 5 more to optimize and polish).
Basic libGDX project
This project uses libGDX and it follows a quite usual libGDX workflow having an App and Screens. There is only one screen active at the time, in this case either the editor screen or the light tester screen. We also initialized a simple camera and setup basic the InputListener for receiving keyboard and mouse events.
libGDX Pixmap
The backbone of this project really is the Pixmap from libGDX. It's a class that represents an image (map of pixels) which allows for edition in Java. It's a bit like a BufferedImage in traditional Java but much more convenient and allows to convert it into an OpenGL texture easily. When drawing with the mouse onto the screen, it basically draws circles when clicking or lines when drawing. Those operations are relatively simple, it simply iterates over the pixels that are being edited and applies the correct blending with the color. On top of that saving functions are already present in libGDX:
PixmapIO.writePNG(file, pixmap);
It's not that simple, however when drawing with opacity not at 100%. We need to avoid going over the opacity amount when passing multiple times at the same spot. To avoid this I needed another pixmap as some buffer, but then copying this pixmap all the time for preview caused performance issues so I had to create a "ranged" buffer, basically a buffer that knows which section of the map has been edited to avoid copying unneeded sections:
public class RangedGdxBuffer extends GdxPixmap { private int minX, maxX, minY, maxY; public RangedGdxBuffer(int width, int height) { super(width, height); initRange(); } public RangedGdxBuffer(FileHandle file) { super(file); initRange(); } public void initRange() { minX = getWidth(); maxX = 0; minY = getHeight(); maxY = 0; } @Override public void clear() { super.clear(); initRange(); } @Override public void copy(PixmapBuffer pixmap) { if(pixmap instanceof RangedGdxBuffer) { clear(); minX = ((RangedGdxBuffer)pixmap).getMinX(); maxX = ((RangedGdxBuffer)pixmap).getMaxX(); minY = ((RangedGdxBuffer)pixmap).getMinY(); maxY = ((RangedGdxBuffer)pixmap).getMaxY(); if(maxX - minX < 0 || maxY - minY < 0) return; drawPixmap((RangedGdxBuffer)pixmap, minX, minY, minX, minY, maxX - minX, maxY - minY); } else { super.copy(pixmap); fullRange(); } } public void fullRange() { minX = 0; maxX = getWidth(); minY = 0; maxY = getHeight(); } @Override public void fill() { super.fill(); fullRange(); } // ... @Override public void drawPixel(int x, int y, int color) { super.drawPixel(x, y, color); if(x < minX) minX = x; if(y < minY) minY = y; if(x + 1 > maxX) maxX = x + 1; if(y + 1 > maxY) maxY = y + 1; } }
As you can see it has an optimized copy() function that limits the cost of this buffer. This technique has been used throughout the projects everywhere where it was possible. For instance, there is another buffer for when editing using a mask which is also using this technique. Overall, this was worth it because we can now easily edit images up to 2000x2000 pixels!
gdx-controller and JPen
Now for the fun part. Implementing controller support was quite easy because of the gdx-controller library. All I had to do for controllers was to instantiate a new ControllerListener and then register it using
Controllers.addListener(listener);
At first JPen was a bit of a pen in the ... Once you figure it out, it's easy though. First, you need a JPenOwner and you make sure the PenProviderContructors include all of those
@Override public Collection<Constructor> getPenProviderConstructors() { return Arrays.asList( new XinputProvider.Constructor(), new WintabProvider.Constructor(), new CocoaProvider.Constructor()); }
You also need to give a PenClip, just create a dummy one that includes the whole screen:
public class DummyPenClip implements PenClip { @Override public void evalLocationOnScreen(Point point) {} @Override public boolean contains(Float aFloat) { return true; } }
Then, with that you are free to make a PenListener. The mechanics of those are a bit funky. When you receive a penLevelEvent, use that to assign the pen reference and when you receive a penTock, use the assigned pen and then unassign it again. Don't ask me why it works that way instead of just receiving the events with numerical values. Anyway, once you do that you can easily get the tilt using
pen.getLevelValue(Type.TILT_X), pen.getLevelValue(Type.TILT_Y)
With that we are all set! We can compute a direction from the joystick axis and from the tilt and convert that direction into a normal direction and then color. Display that color onto the top right indicator and we are good to go
The Light Tester
For the light tester, I basically reused the engine of my game to light up this scene. In the source code, you have total access to the LightingBatch I used, a special SpriteBatch which I custom made for my game with dynamic lighting. That's out of scope for this article, but if you want to see the way I implemented Dynamic Lighting in my game, feel free to check it out in the source code. The TL;DR would be, it basically just allows for custom attributes to work with the lighting shader. I then strapped this onto a new screen and made it so it switches to it when pressing F5. Voilà!
I hope this was somewhat helpful. I think pen tilt has lots of potential to be used in the creative workflow for other things than traditional painting. Someone with precise control over their pen tilt gets extra degrees of freedom to use as input for anything, this could possibly apply to 3D modeling, animation or anything that requires more input all at once really!!
Cheers,
Alexander Winter
Get NormalPainter
NormalPainter
Draw normal maps the smart way
More posts
- NormalPainter 1.6.1 : Increased convenienceOct 12, 2023
- NormalPainter 1.6.0 : New experimental featuresOct 10, 2023
- NormalPainter TutorialApr 21, 2021
- NormalPainter is out! Draw normal maps the smart wayApr 16, 2021
Leave a comment
Log in with itch.io to leave a comment.