1
\$\begingroup\$

I was trying to make a simple noise visualizer using the noise, pygame and pygame_gui libraries. I wanted it to have UI features like buttons, drop down menus, sliders, etc. And the pygame_gui library is perfect for this.

The code is clearly not very clean, so any suggestions are well appreciated.

noise_visualizer.py

from random import randint, uniform
import sys

import pygame
import pygame_gui
from pygame_gui import UIManager
from pygame_gui.elements.ui_button import UIButton
from pygame_gui.elements.ui_drop_down_menu import UIDropDownMenu
from pygame_gui.elements.ui_horizontal_slider import UIHorizontalSlider
from pygame_gui.elements.ui_label import UILabel
from pygame_gui.elements.ui_panel import UIPanel
from pygame_gui.elements.ui_text_entry_line import UITextEntryLine

from constants import *
import core


# --------------------------------------------------------------------------- #

pygame.init()
screen = pygame.display.set_mode((750, 500))
pygame.display.set_caption("Noise Visualizer")
clock = pygame.time.Clock()
running = True

apply_color = APPLY_COLOR
use_falloff = USE_FALLOFF
map_surf = pygame.Surface((500, 500))

# --------------------------------------------------------------------------- #

ui_manager = UIManager((750, 500))

panel = UIPanel(
    relative_rect=pygame.Rect((MAP_SIZE, 0, *PANEL_SIZE)),
    manager=ui_manager,
    starting_layer_height=0,
    margins={
        "left": 5,
        "right": 5,
        "top": 5,
        "bottom": 5
    }
)

UILabel(
    relative_rect=pygame.Rect((0, 0, 240, 27)),
    manager=ui_manager,
    container=panel,
    text="Map type"
)

map_type_menu = UIDropDownMenu(
    relative_rect=pygame.Rect((0, 27*1, 240, 27)),
    manager=ui_manager,
    container=panel,
    options_list=MAP_OPTIONS,
    starting_option=MAP_OPTIONS[0]
)

UILabel(
    relative_rect=pygame.Rect((0, 27*2, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Tile Size"
)

tile_size_entry = UITextEntryLine(
    relative_rect=pygame.Rect((0, 27*3, 120, 27)),
    manager=ui_manager,
    container=panel
)
tile_size_entry.set_text(str(TILE_SIZE))
tile_size_entry.set_allowed_characters("numbers")

UILabel(
    relative_rect=pygame.Rect((120, 27*2, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Seed"
)

seed_entry = UITextEntryLine(
    relative_rect=pygame.Rect((120, 27*3, 120, 27)),
    manager=ui_manager,
    container=panel
)
seed_entry.set_text(str(SEED))
seed_entry.set_allowed_characters("numbers")

UILabel(
    relative_rect=pygame.Rect((0, 27*4, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Apply Colors"
)

apply_color_button = UIButton(
    relative_rect=pygame.Rect((120, 27*4, 120, 27)),
    manager=ui_manager,
    container=panel,
    text=str(apply_color)
)

UILabel(
    relative_rect=pygame.Rect((0, 27*5, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Falloff Map"
)

use_falloff_button = UIButton(
    relative_rect=pygame.Rect((120, 27*5, 120, 27)),
    manager=ui_manager,
    container=panel,
    text=str(use_falloff)
)

UILabel(
    relative_rect=pygame.Rect((0, 27*6, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Scale"
)

scale_entry = UITextEntryLine(
    relative_rect=pygame.Rect((120, 27*6, 120, 27)),
    manager=ui_manager,
    container=panel
)
scale_entry.set_text(str(SCALE))

scale_slider = UIHorizontalSlider(
    relative_rect=pygame.Rect((0, 27*7, 240, 27)),
    manager=ui_manager,
    container=panel,
    start_value=SCALE,
    value_range=SCALE_RANGE
)

UILabel(
    relative_rect=pygame.Rect((0, 27*8, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Octaves"
)

octaves_entry = UITextEntryLine(
    relative_rect=pygame.Rect((120, 27*8, 120, 27)),
    manager=ui_manager,
    container=panel
)
octaves_entry.set_text(str(OCTAVES))
octaves_entry.set_allowed_characters("numbers")

octaves_slider = UIHorizontalSlider(
    relative_rect=pygame.Rect((0, 27*9, 240, 27)),
    manager=ui_manager,
    container=panel,
    start_value=OCTAVES,
    value_range=OCTAVES_RANGE
)

UILabel(
    relative_rect=pygame.Rect((0, 27*10, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Persistence"
)

persistence_entry = UITextEntryLine(
    relative_rect=pygame.Rect((120, 27*10, 120, 27)),
    manager=ui_manager,
    container=panel
)
persistence_entry.set_text(str(PERSISTENCE))

persistence_slider = UIHorizontalSlider(
    relative_rect=pygame.Rect((0, 27*11, 240, 27)),
    manager=ui_manager,
    container=panel,
    start_value=PERSISTENCE,
    value_range=PERSISTENCE_RANGE
)

UILabel(
    relative_rect=pygame.Rect((0, 27*12, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Lacunarity"
)

lacunarity_entry = UITextEntryLine(
    relative_rect=pygame.Rect((120, 27*12, 120, 27)),
    manager=ui_manager,
    container=panel
)
lacunarity_entry.set_text(str(LACUNARITY))

lacunarity_slider = UIHorizontalSlider(
    relative_rect=pygame.Rect((0, 27*13, 240, 27)),
    manager=ui_manager,
    container=panel,
    start_value=LACUNARITY,
    value_range=LACUNARITY_RANGE
)

generate_rand_button = UIButton(
    relative_rect=pygame.Rect((60, 27*14, 120, 27)),
    manager=ui_manager,
    container=panel,
    text="Random"
)

# --------------------------------------------------------------------------- #

def regenerate_map():
    global apply_color
    global use_falloff

    map_type = map_type_menu.selected_option
    tile_size = int(tile_size_entry.get_text())
    seed = int(seed_entry.get_text())

    scale = int(scale_entry.get_text())
    octaves = int(octaves_entry.get_text())
    persistence = float(persistence_entry.get_text())
    lacunarity = float(lacunarity_entry.get_text())

    if map_type == "Random Noise Map":
        noise_map = core.random_noise(500//tile_size, seed)
        use_falloff_button.enable()
        scale_entry.disable()
        scale_slider.disable()
        octaves_entry.disable()
        octaves_slider.disable()
        persistence_entry.disable()
        persistence_slider.disable()
        lacunarity_entry.disable()
        lacunarity_slider.disable()
    elif map_type == "Perlin Noise Map":
        noise_map = core.perlin_noise(500//tile_size, seed, scale, octaves, persistence, lacunarity)
        use_falloff_button.enable()
        scale_entry.enable()
        scale_slider.enable()
        octaves_entry.enable()
        octaves_slider.enable()
        persistence_entry.enable()
        persistence_slider.enable()
        lacunarity_entry.enable()
        lacunarity_slider.enable()
    elif map_type == "Simplex Noise Map":
        noise_map = core.simplex_noise(500//tile_size, seed, scale, octaves, persistence, lacunarity)
        use_falloff_button.enable()
        scale_entry.enable()
        scale_slider.enable()
        octaves_entry.enable()
        octaves_slider.enable()
        persistence_entry.enable()
        persistence_slider.enable()
        lacunarity_entry.enable()
        lacunarity_slider.enable()
    elif map_type == "Falloff Map":
        noise_map = core.generate_falloff_map(500//tile_size)
        use_falloff_button.disable()
        scale_entry.disable()
        scale_slider.disable()
        octaves_entry.disable()
        octaves_slider.disable()
        persistence_entry.disable()
        persistence_slider.disable()
        lacunarity_entry.disable()
        lacunarity_slider.disable()

    map_size = len(noise_map)
    if use_falloff and map_type != "Falloff Map":
        falloff_map = core.generate_falloff_map(map_size)
        for x in range(map_size):
            for y in range(map_size):
                noise_map[x][y] = max(0, min(noise_map[x][y] - falloff_map[x][y], 1))
    if apply_color:
        color_map = core.generate_color_map(noise_map, REGION_COLORS)
        for x in range(map_size):
            for y in range(map_size):
                color = color_map[x][y]
                map_surf.fill(color, ((x*tile_size, y*tile_size), (tile_size, tile_size)))
    else:
        for x in range(map_size):
            for y in range(map_size):
                color = BLACK.lerp(WHITE, noise_map[x][y])
                map_surf.fill(color, ((x*tile_size, y*tile_size), (tile_size, tile_size)))


def randomize():
    map_type = map_type_menu.selected_option
    if map_type != "Falloff Map":
        seed_entry.set_text(str(randint(*SEED_RANGE)))
        if map_type != "Random Noise Map":
            scale_entry.set_text(str(randint(*SCALE_RANGE)))
            octaves_entry.set_text(str(randint(*OCTAVES_RANGE)))
            persistence_entry.set_text(str(uniform(*PERSISTENCE_RANGE)))
            lacunarity_entry.set_text(str(uniform(*LACUNARITY_RANGE)))

# --------------------------------------------------------------------------- #

regenerate_map()

while running:
    ui_manager.draw_ui(screen)
    screen.blit(map_surf, (0, 0))

    for event in pygame.event.get():
        ui_manager.process_events(event)
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.USEREVENT:
            if (event.user_type == pygame_gui.UI_DROP_DOWN_MENU_CHANGED or event.user_type == pygame_gui.UI_TEXT_ENTRY_FINISHED):
                regenerate_map()
            elif event.user_type == pygame_gui.UI_HORIZONTAL_SLIDER_MOVED:
                if event.ui_element == scale_slider:
                    scale_entry.set_text(str(scale_slider.get_current_value()))
                elif event.ui_element == octaves_slider:
                    octaves_entry.set_text(str(octaves_slider.get_current_value()))
                elif event.ui_element == persistence_slider:
                    persistence_entry.set_text(str(persistence_slider.get_current_value()))
                elif event.ui_element == lacunarity_slider:
                    lacunarity_entry.set_text(str(lacunarity_slider.get_current_value()))

                regenerate_map()
            elif event.user_type == pygame_gui.UI_BUTTON_PRESSED:
                if event.ui_element == apply_color_button:
                    if apply_color:
                        apply_color = False
                        apply_color_button.set_text(str(False))
                    else:
                        apply_color = True
                        apply_color_button.set_text(str(True))
                elif event.ui_element == use_falloff_button:
                    if use_falloff:
                        use_falloff = False
                        use_falloff_button.set_text(str(False))
                    else:
                        use_falloff = True
                        use_falloff_button.set_text(str(True))
                elif event.ui_element == generate_rand_button:
                    randomize()

                regenerate_map()

    time_delta = clock.tick(60) / 1000.0
    ui_manager.update(time_delta)
    pygame.display.update()

constants.py

import pygame

MAP_SIZE = 500

BLACK = pygame.Color((0, 0, 0))
WHITE = pygame.Color((255, 255, 255))
BLUE = pygame.Color((0, 0, 255))
YELLOW = pygame.Color((255, 255, 0))
GREEN = pygame.Color((0, 255, 0))
GRAY = pygame.Color((55, 55, 55))

REGION_COLORS = {
    0.45: BLUE,
    0.55: YELLOW,
    0.6: GREEN,
    0.8: GRAY,
    1: WHITE
}

PANEL_SIZE = (250, 500)
MARGIN = 5
ELEMENT_HEIGHT = 27

SEED = 0
MAP_OPTIONS = [
    "Random Noise Map",
    "Perlin Noise Map",
    "Simplex Noise Map",
    "Falloff Map"
]
TILE_SIZE = 5
APPLY_COLOR = False
USE_FALLOFF = False

SCALE = 15
OCTAVES = 8
PERSISTENCE = 0.5
LACUNARITY = 2.0

SEED_RANGE = (0, 100)
SCALE_RANGE = (1, 30)
OCTAVES_RANGE = (1, 8)
PERSISTENCE_RANGE = (0.0, 5.0)
LACUNARITY_RANGE = (0.0, 5.0)

core.py

import random

import noise
import numpy as np
import pygame


def random_noise(size, seed):
    random.seed(seed)
    map_ = np.zeros((size, size))
    for x in range(size):
        for y in range(size):
            map_[x][y] = random.random()

    return map_


def perlin_noise(size, seed, scale, octaves, persistence, lacunarity):
    noise_map = np.zeros((size, size))
    for x in range(size):
        for y in range(size):
            noise_map[x][y] = noise.pnoise3(x/scale, y/scale, seed, octaves=octaves, persistence=persistence, lacunarity=lacunarity)
            noise_map[x][y] = (noise_map[x][y] + 1) / 2

    return noise_map


def simplex_noise(size, seed, scale, octaves, persistence, lacunarity):
    noise_map = np.zeros((size, size))
    for x in range(size):
        for y in range(size):
            noise_map[x][y] = noise.snoise3(x/scale, y/scale, seed, octaves=octaves, persistence=persistence, lacunarity=lacunarity)
            noise_map[x][y] = (noise_map[x][y] + 1) / 2

    return noise_map


def generate_color_map(noise_map, region_colors):
    size = len(noise_map)
    color_map = np.empty((size, size), dtype=pygame.Color)
    for x in range(size):
        for y in range(size):
            for key, value in region_colors.items():
                if noise_map[x][y] <= key:
                    color_map[x][y] = value
                    break

    return color_map


def generate_falloff_map(map_size, a=3, b=2.2):
    falloff_map = np.zeros((map_size, map_size))
    for i in range(map_size):
        for j in range(map_size):
            x = i / map_size * 2 - 1
            y = j / map_size * 2 - 1

            value = max(abs(x), abs(y))
            falloff_map[i][j] = pow(value, a) / (pow(value, a) + pow(b - b*value, a))

    return falloff_map


if __name__ == '__main__':
    m = simplex_noise(5, 5, 5)
    print(m)
```
\$\endgroup\$
1
  • \$\begingroup\$ This won't run? simplex_noise is being called with the wrong number of arguments. \$\endgroup\$
    – Reinderien
    Jul 16, 2021 at 2:20

1 Answer 1

1
\$\begingroup\$
  • Your core has what looks to be an abandoned main guard that calls simplex_noise with an incorrect number of arguments. This should probably just be deleted.
  • Consider adding PEP484 type hints to all of your function signatures, like def random_noise(size: int, seed: int) -> np.ndarray:
  • The x/y loops in your noise functions should probably be converted into Numpy vectorized operations
  • In noise_visualizer, there are two big chunks of global code that need to be moved into functions - one at the top, and one at the bottom; to be called perhaps "setup" and "main_loop" and invoked from a main guard at the bottom
  • Your reliance on a large collection of globals in that module means that it's non-reentrant. There are several ways around this - you can do a bunch of setup in a function, define local closure functions and then return references to those functions; or you can make classes. But as it is now, it's effectively impossible to run two instances of your program in the same process, which hinders testing.
\$\endgroup\$

Your Answer

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

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