Как быстро отобразить набор Julia с помощью Pygame?

#python #pygame #rendering #fractals

#python #pygame #рендеринг #фракталы

Вопрос:

В настоящее время я играю со скриптом, генерирующим наборы Julia и набор Мандельброта, а затем использую pygame для отображения точек.

По сути, экран отображается в меньшей системе координат, где он ограничен -2,5, 2,5 по оси x и -1, 1 по оси y. Затем каждый из пикселей в этом отображенном диапазоне передается функции, чтобы проверить, есть ли эквивалент его комплексного числа в данном наборе. Эта функция возвращает количество итераций, которое потребовалось, чтобы вычислить, есть ли число в наборе или нет (или максимальное количество итераций).

Затем, для каждого пикселя, я знаю, в какой цвет его раскрасить, основываясь на этом показателе итерации, и визуализирую каждый из пикселей один за другим. Эта часть процесса действительно интенсивна и занимает ~ 30 секунд на рендеринг, но может занять гораздо больше времени в зависимости от сложности набора.

Вот код для определения того, есть ли переданное комплексное число и комплексная координата в наборе Julia, это вообще не займет много времени для вычисления при проверке 1920 * 1080 пикселей:

 max_iter = 45


def julia(z, c):
    n = 0
    while abs(z) <= 2 and n < max_iter:
        z = z * z   c
        n  = 1
    return n
  

Вот код, который я использую для рендеринга pygame, в этом определенно заключается проблема:

 size_ = 1920, 1080
re_ = -2.5, 2.5
im_ = -1, 1
surf = pygame.Surface(size)
colour_gradient1 = [c, c1, c2, c3, ...] # This is some list of colours generated by a gradient function

for x in range(0, size_[0]):
    for y in range(0, size_[1]):
        z = complex(re_[0]   (x / size_[0]) * (re_[1] - re_[0]),
                    im_[0]   (y / size_[1]) * (im_[1] - im_[0]))
        m = julia(z, c)
        colour = colour_gradient1[m]
        pygame.draw.rect(surf,
                         colour,
                         (x, y, 1, 1))
  

Я думаю, я понимаю, почему это требует высокой производительности, поскольку и pygame, и python на самом деле не оптимизированы для отображения содержимого на экране подобным образом. В настоящее время я пытаюсь изучить C и я лучше понимаю его для подобных вещей.

Я также экспериментировал с функцией масштабирования, где я мог бы выделить поле с помощью мыши, и скрипт отобразил бы эту выделенную область, но при реализации этого возникла проблема. Поскольку увеличенные фракталы усложнялись, скрипту требовалось слишком много времени для использования этой функции.

Итак, мой вопрос, есть ли лучший способ отобразить что-то подобное в режиме, близком к реальному времени, используя python и, возможно, pygame? Я открыт для использования другого пакета, но если это возможно через pygame, это было бы идеально.

Ниже прилагается пара изображений сгенерированных наборов: Это пример одного из сгенерированных фракталов.
Это пример одного из сгенерированных фракталов.
Это пример одного из сгенерированных фракталов.

Комментарии:

1. Красивые наборы Julia. Вы должны быть довольны ими.

Ответ №1:

Алгоритмы генерации фракталов всегда замедляются по мере увеличения масштаба, потому что по мере углубления требуется все больше итераций на пиксель (или до того, как будет достигнуто спасение).

Это никогда не будет особенно быстрым на интерпретируемом языке. Конечно, вы можете настроить его, чтобы немного увеличить скорость, но это никогда не будет «в реальном времени» (скажем, < 1 секунды / изображение) для всех уровней масштабирования.

Если вы хотите продолжить работу на Python, вам придется просто согласиться с самим собой, что это никогда не будет быстро.

Однако. Однако вы могли разделить генерацию каждого квадранта на отдельные процессы, каждый из которых выполнялся бы на своем собственном процессоре / ядре. Это ускорит работу на N / ядер.

Есть некоторые оптимизации, которые можно выполнить с обнаружением симметрии в изображении и вычислением только, скажем, половины пикселей, потому что другая сторона является его зеркалом (например, горизонтальная ось через увеличенный набор Мандельброта). Вероятно, вы могли бы обратиться к исходному тексту почтенной программы Fractint для получения примеров этого.

Кроме того: я написал один из них (рисование набора Мандельброта) на C, используя библиотеку nVidia CUDA, которая распределяет вычисления по процессорам мощностью 1200 мач на видеокарте (используя ноутбук среднего класса 2018). Хотя это работало довольно быстро для достаточно больших изображений или сильно «увеличенных» фракталов, это все равно замедляло работу. Здесь требуется слишком много обработки чисел.

раздел Мандельброта

Комментарии:

1. Спасибо за подробный ответ. Я не рассматривал возможность разделения процесса для каждого ядра моего процессора. Это не будет полезно на моем ноутбуке, поскольку он единственный двухъядерный, но на моем ПК 8, так что это может хорошо повысить производительность. Идея симметрии также интересна, я обязательно попробую это и посмотрю, что из этого получится.

Ответ №2:

(Этот вопрос, наконец, заставил меня установить PyOpenGL. Так что спасибо!)

Насколько я видел, перебор каждого пикселя по отдельности
никогда не даст хорошей производительности (не в C / C / Assembly /).
Поможет векторизация (в процессоре). Что действительно поможет,
так это использование способности графического процессора параллельно применять одну операцию (/ kernel)
ко всему многомерному массиву элементов.

В частности: использование фрагментного шейдера для вычисления
цвета каждого пикселя. Но это означает использование графического API,
такого как OpenGL (/Vulkan / Direct3D /), или GPGPU / Compute API, такого
как OpenCL (/ CUDA /).

Если результирующее изображение используется в графическом конвейере,
тогда оно может оставаться на графическом процессоре и отображаться непосредственно
оттуда. Если результирующее изображение необходимо использовать, например, в
графическом интерфейсе, сохранить на диске или подобном, его необходимо перенести с
GPU на CPU (возможно, преобразование в текстуру, чтение фреймбуфера,
использование закадровых буферов или другие параметры, которые я не знаю).

 import numpy as np

from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.GLUT import *

# Vertex shader: Pass through (no model-view-projection).
vsSrc = '''
#version 300 es

layout (location = 0) in vec4 posIn;

void main()
{
    gl_Position = posIn;
}
'''

# Fragment shader: Compute fractal color, per-pixel.
# en.wikipedia.org/wiki/Mandelbrot_set#Computer_drawings
fsSrc = '''
#version 300 es

precision mediump float;

out vec4 colorOut;

vec2 mapLinear(
  vec2 val,
  vec2 srcMin, vec2 srcMax,
  vec2 dstMin, vec2 dstMax
) {
    vec2 valNorm = (val - srcMin) / (srcMax - srcMin);
    return valNorm * (dstMax - dstMin)   dstMin;
}

void main()
{
  // Debugging: Return fixed color; see which pixels get it.
  //colorOut = vec4(0.0, 0.5, 0.0, 1.0);
  //return;
  
  // Originally, origin is top-left. Convert to Cartesian.
  vec2 pixelMin = vec2(0.0f, 720.0f);
  vec2 pixelMax = vec2(1280.0f, 0.0f);
  
  vec2 mbMin = vec2(-2.5f, -1.0f);
  vec2 mbMax = vec2(1.0f, 1.0f);
  vec2 mbExtent = mbMax - mbMin;
  vec2 mbCenter = mbMin   (mbExtent / 2.0f);
  
  vec2 fragMapped = mapLinear(
    gl_FragCoord.xy, pixelMin, pixelMax, mbMin, mbMax
  );
  
  float real = 0.0f;
  float imag = 0.0f;
  int iter = 0;
  const int maxIter = 500;
  while (
    ((real*real   imag*imag) < 4.0f) amp;amp; 
    (iter < maxIter)
  ) {
    float realTemp = real*real - imag*imag   fragMapped.x;
    imag = 2.0f*real*imag   fragMapped.y;
    real = realTemp;
      iter;
  }
  
  // Using generated colors, instead of indexing a palette.
  // (Don't remember anymore where this came from,
  // or if it was a heuristic.)
  vec3 chosenColor;
  float iterNorm = float(iter) / float(maxIter);
  if (iterNorm > 0.5f) {
    float iterNormInverse = 1.0f - iterNorm;
    chosenColor = vec3(
      0.0f, iterNormInverse, iterNormInverse
    );
  }
  else {
    chosenColor = vec3(0.0f, iterNorm, iterNorm);
  }
  
  colorOut = vec4(chosenColor.xyz, 1.0f);
}
'''

def compileFractalShader():
  vs = shaders.compileShader(vsSrc, GL_VERTEX_SHADER)
  fs = shaders.compileShader(fsSrc, GL_FRAGMENT_SHADER)
  return shaders.compileProgram(vs, fs)

# Geometry: Just 2 triangles, covering the display surface.
# (So that the fragment shader runs for all surface pixels.)
def drawTriangles():
  topLeftTriangle = (
    1.0, 1.0, 0.0,
    -1.0, -1.0, 0.0,
    -1.0, 1.0, 0.0
  )
  bottomRightTriangle = (
    1.0, 1.0, 0.0,
    -1.0, -1.0, 0.0,
    1.0, -1.0, 0.0
  )
  verts = np.array(
    topLeftTriangle   bottomRightTriangle,
    dtype=np.float32
  )
  
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, verts)
  glEnableVertexAttribArray(0)
  
  glDrawArrays(GL_TRIANGLES, 0, 6)

def printShaderException(e):
  errorMsg, shaderSrc, shaderType = e.args
  print('Shader error message:')
  for line in errorMsg.split('\n'): print(line)
  print('--')
  #print('Shader source:')
  #for line in shaderSrc[0].split(b'n'): print(line)
  #print('--')
  print('Shader type:', shaderType)

WIDTH = 1280
HEIGHT = 720

glutInit()
glutInitWindowSize(WIDTH, HEIGHT)
glutCreateWindow('Fractals with fragment shaders.')

# Create shaders, after creating a window / opengl-context:
try: fractalShader = compileFractalShader()
except RuntimeError as e: 
  printShaderException(e)
  exit()

glViewport(0, 0, WIDTH, HEIGHT)

glClearColor(0.5, 0.0, 0.5, 1.0)

def display():
  glClear(GL_COLOR_BUFFER_BIT)
  with fractalShader: drawTriangles()
  glutSwapBuffers()
glutDisplayFunc(display)

glutMainLoop()

  

введите описание изображения здесь

Это совершенно неоптимизировано.
Также, как писал Кингсли, масштабирование (здесь не показано)
замедляло работу даже в графическом процессоре (но: неоптимизировано).

Комментарии:

1. Спасибо, я ценю пример OpenGL. Я уже пробовал использовать PyOpenGL раньше, но у меня возникли проблемы с поиском хороших руководств / документов. Я мог бы попробовать воссоздать то, что я сделал, используя OpenGL, и посмотреть, что из этого получится. Ваш код станет хорошей отправной точкой, так что еще раз спасибо @volothud.

2. @rhyso98 Рад помочь (предполагая, что я это сделал :)). Если что-то не совсем работает (вполне возможны проблемы с конфигурацией OpenGL *), отправьте мне сообщение, и я попытаюсь посмотреть, смогу ли я разобраться в этом. (* Пришлось самому разобраться с несколькими проблемами, чтобы это сработало. Вот почему он использует OpenGL.GL , вместо OpenGL.GLES3 or OpenGL.GLES2 , как первоначально планировалось.)