7
\$\begingroup\$

I am experimenting with Perlin Noise and random map generation. I have a 2D numpy ndarray full of 16-bit floats called map_list that I call from the singleton ST. It has 900 rows with 1600 elements each. I am iterating through it to display different colored pixels to represent terrain at different points in the map. Array values in different ranges produce pixels of different colors. The total possible range is [0, 1] because I am normalizing the values.

Is there a way to display the aforementioned pixels faster than I am capable of now?

"""

This file holds functions that modify pyGame surfaces.

"""

from __future__ import division
from singleton import ST
from pygame import gfxdraw


def display_map(surface):
    """
    This takes in a pyGame surface, and draws colored tiles on it according to
    the values in ST.map_list. The higher the value, the lighter the shade of
    the tile.

    :param surface: A pyGame surface.
    """

    x_pos = 0
    y_pos = 0

    for y in range(len(ST.map_list)):

        for x in range(len(ST.map_list[y])):
            noise_value = ST.map_list[y][x]
            shade = int(noise_value * 255)
            color = __color_tiles(noise_value, shade)

            gfxdraw.pixel(surface, x_pos, y_pos, color)
            x_pos += ST.TILE_SIZE

        x_pos = 0
        y_pos += ST.TILE_SIZE


def __color_tiles(noise_value, shade):
    """
    Treat this function as private. It should only be called by functions and
    methods within this file. It returns a 3-element 1D tuple that represents
    the rgb color values to display the tile as.

    :param noise_value: The noise value at a specific point in ST.map_list.
    :param shade: How dark or light the tile should be.
    :return: tuple
    """

    if noise_value < ST.WATER_LEVEL:
        rgb = (shade, shade, 255)
    elif noise_value > ST.MOUNTAIN_LEVEL:
        rgb = (shade, shade, shade)
    else:
        rgb = (shade, 255, shade)

    return rgb

What it generates

enter image description here

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

If you have a lot of data in a rectangle, and you need to go fast, the answer is always the same: import numpy as np

Pygame has added some functions that support numpy arrays for exactly this reason. So you can fairly painlessly convert from an appropriately sized numpy array to a surface. You could even go back again, if you wanted.

Here's some code that does something similar to what you appear to be doing. I stripped out the stuff that you didn't provide source for, and focused on just doing the high/low/mid cutoff you seem to want. Also note, I'm using python 3, so there will likely be some slight gotchas in the syntax:

# https://codereview.stackexchange.com/questions/215575/plotting-terrain-pixels-with-pygame-based-on-random-numpy-array
"""

This file holds functions that modify pyGame surfaces.

"""

import numpy as np
import pygame

screen_size = (180, 320)

def display_map(noise):  # NOTE: I expect noise to be a 2-d np.ndarray
    ''' Return a surface with terrain mapped onto it. '''

    CHANNELS = 3  # use 4 for alpha, I guess
    RED = 0
    GREEN = 1
    BLUE = 2
    WATER_LEVEL = 0.20
    MOUNTAIN_LEVEL = 0.75

    # NOTE: numpy automagically "vectorizes" things like this. 
    # array times scalar means a[i,j] * scalar, for all i,j
    shade = (noise * 255).astype(np.ubyte)

    # NOTE: dstack "stacks" however-many 2d arrays along the "depth" axis
    # producing a 3d array where each [i,j] is (X,X,X)
    rgb = np.dstack([shade] * 3)

    # NOTE: (WATER_LEVEL <= noise) produces a 2d boolean array, where 
    # result[i,j] = (WATER_LEVEL <= noise[i,j]), kind of like the scalar
    # multiply above. The '&' operator is overloaded for boolean 'and'.
    # The upshot is that this assignment only happens where the boolean
    # array is 'True'
    rgb[(WATER_LEVEL <= noise) & (noise <= MOUNTAIN_LEVEL), GREEN] = 255
    rgb[(noise < WATER_LEVEL), BLUE] = 255

    # NOTE: pygame.surfarray was added mainly to talk to numpy, I believe.
    surf = pygame.surfarray.make_surface(rgb)
    return surf

pygame.init()
running = True
display = pygame.display.set_mode(screen_size)

noise = np.random.random_sample(screen_size)
terrain = display_map(noise)

while running:
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            running = False

    display.blit(terrain, (0, 0))
    pygame.display.update()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.