Page 1 of 1

High quality sprites with color gradient and anti-aliasing

#1 DK3250  Icon User is online

  • Pythonian
  • member icon

Reputation: 321
  • View blog
  • Posts: 1,056
  • Joined: 27-December 13

Posted 16 November 2016 - 02:22 PM

High quality sprites with color gradient and anti-aliasing

This tutorial requires pygame and python 3.x

In an earlier tutorial, I've shown how to anti-alias a simple circle/ball figure. http://www.dreaminco...-anti-aliasing/
With such simple figures the anti-aliasing mathematics can be boiled down to work even if the sprite moves on a multicolored background.

In this tutorial, we will make anti-aliasing of more complicated figures, but only on monochrome backgrounds.

If you look on a professional game like Candy Chrush, the sprites (the figures) are smooth and soft in their appearance with little artificial reflections giving the impression of 3D surfaces. If the sprites in your games are much more dull and flat, this tutorial may be just what you are looking for.

The strategy for making a nice sprite is:
1. Draw the sprite in monochrome (one color) - or import a monochrome figure drawn elsewhere. The figure must be oversized compared to the final result by a factor of 2 or 4 on each side.
If your final sprite is 50X50 pixels, your figure must be 100X100 or 200X200 pixels.
I prefer a quite large start picture, simply because I find it easier to make nice, precise drawings on a large canvas.
2. Modify the picture by adding reflections and/or a color gradient.
In this tutorial I use a color gradient as it can be applied to almost any figure using fairly simple mathematics.
3. Anti-alias the figure by averaging the color values of all 2X2 picture elements and use the average as color in a picture of half the dimension (or quarter the size).
Point 3 may be repeated to get to sufficient small final sprites.

Let's look at the individual steps.

Drawing a monochrome sprite

The drawing part is a challenge to me; I am really not very artistic, so I tend to draw using basic shapes like circles, lines and polygons; figures I can get from the pygame.draw.<figure>() functions.

If you are comfortable using a more free-style drawing tool, you may be able to make even more impressive sprites, and to combine step 1 and 2 in the procedure mentioned above.

Anyway, let me show how I've made a heart using basic pygame figures. I use five graphic parts to generate a heart. In the first picture below the five parts are shown in five different colors; this is for demonstration only, in the final code all parts should be of the same color. The second picture show the final result.
The code for making the five picture elements is shown in the function ”draw_figure()” in the code below.

Attached Image Attached Image

If you want to make more advanced drawings, I recommend a free tool: Piskel With Piskel, and an artistic nerve, you can do really nice sprites - and skip parts of this tutorial.

Adding a color gradient

First a center for our gradient is chosen.
The color gradient is made by pixel for pixel inspection of the surface holding the monochrome figure. If the color is the monochrome, it is changed to the gradient color which in turn is dependent of the pixel's distance to the gradient center.

When I originally made the gradient functions, I used a circle with diameter 255. To ensure a steep color gradient in small figures (such that the visual impression is the same no matter the size), the distance to gradient center is normalized by multiplication with (255/size) - size is the 'dimension' of your surface; typically the average of the two axis lengths.

The gradient function itself just returns a color depending of the normalized distance. I normally use an almost white color near the center, and then reduce the color intensity as the distance grows. The color elements (r, g, b ) are not reduced equally, but such that the gradients ”color target” are favored. As the color elements are reduced, we need to ensure they don't become negative; for this I use the max() function.

Try to play with the individual values of the gradient function, the possibilities are endless.

Resize and anti-alias

Now we only need to resize and anti-alias. To this we evaluate four pixels in a 2X2 grid and calculate the average of each color element (r, g, b ). This average is then used to color one pixel in a surface with half the dimension length. Then on to the next four pixels, and so on.

This procedure may be repeated to get to sufficient small sprites.

The anti-aliasing effect occurs when four pixels include both background and monochrome. You will get a 25%, 50%, 75% saturation depending of the situation. If you run two resizes the smoothening becomes even better.

This anti aliasing technique can be applied to all sprites and patterns; striped, dotted or otherwise multicolored. The anti aliasing will work just as well on the internal color boundaries as on the boundary to the background.

Closing words

This whole procedure is a bit time consuming so you don't want to do it in the middle of a game. But a few seconds during game initialization is normally acceptable.
During game initialization you make all the sprites your game needs, and later you just show/move them around.
Tutorial, demonstrating how a color gradient and anti aliasing 
can be applied to a 'home made' sprite.

By DK3250, November 2016
import pygame, sys, math, random


X = 400
Y = 400

BLACK = (0, 0, 0)
RED = (225, 50, 50)
WHITE = (255, 255, 255)

size = 320

screen = pygame.display.set_mode((X, Y))


def red_1(d2):
    """ A gradient function - values can be modified for change of effect """
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), max(200-int(d2*2), 0)]

def magenta_1(d2):
    return [max(255-int(d2*0.6), 0), max(200-int(d2*1.5), 0), max(255-int(d2*0.6), 0)]

def draw_figure():
    Drawing of a monochrome figure
    Here by use of basic pygame.draw.<figure> functions
    Could also be an import of a monochrome picture
    surf = pygame.surface.Surface((size, size))

    help_surf = pygame.surface.Surface((size, size))
    help_surf.fill(BLACK), MONO, (38, 47), 265, 0)
    pygame.draw.polygon(surf, BLACK, ((size-1, 78), (129, size-1), (0, size-1), (0, 0), (size-1, 0)), 0), MONO, (282, 47), 265, 0)
    pygame.draw.polygon(help_surf, BLACK, ((0, 78), (191, size-1), (size-1, size-1), (size-1, 0), (0, 0)), 0)
    surf.blit(help_surf, (0, 0)), MONO, (96, 100), 72, 0), MONO, (224, 100), 72, 0)
    pygame.draw.polygon(surf, MONO, ((28, 118), (160, 280), (288, 118)), 0)

    return surf

def make_gradient(gradient_function):
    In-place conversion of a monochrome figure to a gradiented one.
    Normalized distance from center of gradient is calculated
    and used as input for the gradient function.

    The gradient function defines the modified color range.
    x1, y1 = 200, 120  # center of gradient
    for j in range(size):
        for i in range(size):
            if surf.get_at((i, j)) == MONO:
                d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / size
                color = gradient_function(d2)  # call gradient function
                surf.set_at((i,j), color)

def resize(surf, size):
    Anti-aliasing by average of color code in four pixels
    with subsequent use of the average in a smaller surface
    new_surf = pygame.surface.Surface((size//2, size//2))
    for j in range(0, size, 2):
        for i in range(0, size, 2):
            r1, g1, b1, a1 = surf.get_at((i, j))
            r2, g2, b2, a2 = surf.get_at((i+1, j))
            r3, g3, b3, a3 = surf.get_at((i, j+1))
            r4, g4, b4, a4 = surf.get_at((i+1, j+1))

            r = (r1 + r2 + r3 + r4) / 4
            g = (g1 + g2 + g3 + g4) / 4
            b = (b1 + b2 + b3 + b4) / 4

            new_surf.set_at((i//2, j//2), (r, g, b, 255))

    new_size = size // 2
    return new_surf, new_size


surf = draw_figure()
surf, size = resize(surf, size)
surf, size = resize(surf, size)
screen.blit(surf, (10, 10))

Is This A Good Question/Topic? 1
  • +

Replies To: High quality sprites with color gradient and anti-aliasing

#2 noles123  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 8
  • Joined: 19-October 17

Posted 19 October 2017 - 11:52 AM

hi, i am a little bit confused
why in this code you must use 'size = 720'? and in the draw figure you also use size at 'surface' and 'draw', i wonder to know why
Was This Post Helpful? 0
  • +
  • -

#3 DK3250  Icon User is online

  • Pythonian
  • member icon

Reputation: 321
  • View blog
  • Posts: 1,056
  • Joined: 27-December 13

Posted 19 October 2017 - 02:33 PM

In line 20 I use 'size = 320'. This value is chosen such that it divides nicely with 2, 4, (and 8, 16) as explained in the text.

This variable named 'size' is then used in various functions as you mention. In this way the function becomes generic i.e. able to handle all sizes and the variable only need to be defined in one single line.

I hope this answers your question...
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1