Responsive image

Here is my new Poly Maker 2D tool that actually does nothing important and unfortunately doesn't change people's life. But! It was fun to write it and play with results. Inspired by Poly Maker tool written by Paul Lewis. I believe the original tool was written with JavaScripts and WebGL. As there were no sources, I decided to write my own real time version with Python and PyQt. So fork it, break it or improve it if you wish.

Bezier curve

Responsive image
Bezier curve with 20 steps of interpolation

A Bezier curve is a parametric curve that is used in computer graphics to draw procedural shapes. It is based on some entry data - some start points that are used to calculate all in-betweens. So I started with 4 points manually placed on the canvas. There are different ways to create a Bezier curve, I created a cubic one where minimum 4 points define the curve and the curve just passes through the first and the last points. The whole Bezier curve description can be found here, but if you want to save your time, here are the equations that help you calculate a Bezier curve points cordinates.

Responsive image
Bezier curve points coordinates (for each point t)
Responsive image
Bernstein basis polynomials of degree n
Responsive image
Binomial coefficient

The following code is a Python implementation of these equations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import math

def binomial(i, n):
    """Binomial coefficient"""
    return math.factorial(n) / float(math.factorial(i) * math.factorial(n - i))
    
def bernstein(t, i, n):
    """Bernstein basis polynomial"""
    return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i))

def bezier(t, points):
    """Per each iteration calculate curve point coordinates"""
    n = len(points) - 1
    x = 0
    y = 0
    for i, pos in enumerate(points):
        b = bernstein(t, i, n)
        x += pos[0] * b
        y += pos[1] * b
        
    return round(x,4), round(y,4)

def bezierCurve(n, points):
    """Bezier curve points generator"""
    for i in range(n):
        t = i / float(n - 1)
        yield bezier(t, points)

Perpendicular points

The next step was creation of perpendicular points. This is an important step as it gives us a grid of points that will be used for Delauney triangulation algorithm. The tool works in real time, that means when you move any control point, you will get the whole polymesh recalculated. Every time we move our CPs (pink squared dots), all perpendicular points should be recalculated. And this is where Linear Algebra helps. I just turned Bezier curve line segments into 3d vectors and found cross-products for each of those vectors and a vector B = [0,0,1] that points right to us from the screen. Then I normalized calculated perpendicular vectors and used them to offset a newly added points.

Responsive image
A grid of points perpendicular to the curve line segments

Delauney triangulation

In order to connect points into triangles I used matplotlib module that has everything we need to calculate a set of triangles using the given set of points. It uses Delauney triangulation algorithm and works pretty fast.

Responsive image
The grid of points is triangulated

But it's not exactly what I want. So I added a jitter effect in order to move points using some random directions and offsets.

Responsive image
Jitter effect has been applied to the points

Coloring the polygons

For coloring the polygons the first thing I did was adding some color gradient based on the Y coordinates of each triangle's centroid. It worked but I wanted something more advanced like grabbing colors from a random image. So I googled some samples and found a few interesting ones. In order to be able to recalculate triangles colors when I move my control points I decided to use a QPixmap and QImage classes. First thing I did was calculating the width and the height of my lowpoly mesh and resize a given image so it's size could match the size of the mesh. Next thing was mapping triangles centroids coordinates to the image so I could grab a pixel color. Then I used this color as a brush color for painting polygons. And it worked. If you run the project and set any image - you will see how colors of the polygons are changing when we move Control Points.

Responsive image
Coloring the triangles

Adding shadow didn't take too much time as it was similar to the coloring procedure. I've just calculated the width of the lowpoly mesh and resized my QGraphicsPixmapItem object that keeps a reference to the shadow image file. The height of the shadow is fixed and positioned at the fixed Y position.

Responsive image
Added a shadow effect

Using another sample image

Responsive image
Mona Lisa. Tirangulated and colored.