From 22c46778d9b8a8abb5b361f2b380617391cb6c73 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Tue, 27 May 2025 21:55:27 +0200 Subject: [PATCH] change to template-based shaders, move fractal chooser to menus, add burning ship and newton fractal, add escape radius to more fractals --- README.md | 2 +- game/burning_ship.py | 95 ++++++ game/julia.py | 4 +- game/mandelbrot.py | 4 +- game/newton_fractal.py | 95 ++++++ game/shader.py | 358 ++++++++++++++--------- game/play.py => menus/fractal_chooser.py | 14 + menus/main.py | 3 +- utils/constants.py | 24 +- 9 files changed, 453 insertions(+), 146 deletions(-) create mode 100644 game/burning_ship.py create mode 100644 game/newton_fractal.py rename game/play.py => menus/fractal_chooser.py (73%) diff --git a/README.md b/README.md index c744bc7..2d9e24e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ Fractal viewer in Python using compute shaders and the Arcade and Pyglet modules. -Currently supports multiple types of Julia, the Sierpinsky Carpet and Mandelbrot. +Currently supports Julia, multi-Julia, Mandelbrot, Multibrot, Burning Ship, Newton Fractal and the Sierpinsky Carpet. diff --git a/game/burning_ship.py b/game/burning_ship.py new file mode 100644 index 0000000..0ea3cef --- /dev/null +++ b/game/burning_ship.py @@ -0,0 +1,95 @@ +import arcade, arcade.gui, pyglet, json + +from PIL import Image + +from game.shader import create_iter_calc_shader +from utils.constants import menu_background_color, button_style, burning_ship_initial_real_min, burning_ship_initial_real_max, burning_ship_initial_imag_min, burning_ship_initial_imag_max +from utils.preload import button_texture, button_hovered_texture + +class BurningShipViewer(arcade.gui.UIView): + def __init__(self, pypresence_client): + super().__init__() + + self.pypresence_client = pypresence_client + self.real_min = burning_ship_initial_real_min + self.real_max = burning_ship_initial_real_max + self.imag_min = burning_ship_initial_imag_min + self.imag_max = burning_ship_initial_imag_max + + with open("settings.json", "r") as file: + self.settings_dict = json.load(file) + + self.max_iter = self.settings_dict.get("burning_ship_max_iter", 200) + self.zoom = 1.0 + + def on_show_view(self): + super().on_show_view() + + self.shader_program, self.burning_ship_image = create_iter_calc_shader("burning_ship", self.window.width, self.window.height, self.settings_dict.get("burning_ship_precision", "Single").lower(), 2, int(self.settings_dict.get("burning_ship_escape_radius", 2))) + + self.burning_ship_sprite = pyglet.sprite.Sprite(img=self.burning_ship_image) + + self.create_image() + + self.pypresence_client.update(state='Viewing Burning Ship', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time) + + self.setup_ui() + + def main_exit(self): + from menus.main import Main + self.window.show_view(Main(self.pypresence_client)) + + def setup_ui(self): + self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) + + self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="center", anchor_y="top") + self.zoom_label = self.info_box.add(arcade.gui.UILabel(text=f"Zoom: {self.zoom}", font_name="Protest Strike", font_size=16)) + self.max_iter_label = self.info_box.add(arcade.gui.UILabel(text=f"Max Iterations: {self.max_iter}", font_name="Protest Strike", font_size=16)) + + self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) + self.back_button.on_click = lambda event: self.main_exit() + self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5) + + def zoom_at(self, center_x, center_y, zoom_factor): + center_real = self.real_min + (center_x / self.width) * (self.real_max - self.real_min) + center_imag = self.imag_min + (center_y / self.height) * (self.imag_max - self.imag_min) + + new_real_range = (self.real_max - self.real_min) / zoom_factor + new_imag_range = (self.imag_max - self.imag_min) / zoom_factor + + self.real_min = center_real - new_real_range / 2 + self.real_max = center_real + new_real_range / 2 + self.imag_min = center_imag - new_imag_range / 2 + self.imag_max = center_imag + new_imag_range / 2 + + def create_image(self): + with self.shader_program: + self.shader_program['u_maxIter'] = int(self.max_iter) + self.shader_program['u_resolution'] = (self.window.width, self.window.height) + self.shader_program['u_real_range'] = (self.real_min, self.real_max) + self.shader_program['u_imag_range'] = (self.imag_min, self.imag_max) + self.shader_program.dispatch(self.burning_ship_image.width, self.burning_ship_image.height, 1, barrier=pyglet.gl.GL_ALL_BARRIER_BITS) + + def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> bool | None: + super().on_mouse_press(x, y, button, modifiers) + + if button == arcade.MOUSE_BUTTON_LEFT: + zoom = self.settings_dict.get("burning_ship_zoom_increase", 2) + elif button == arcade.MOUSE_BUTTON_RIGHT: + zoom = 1 / self.settings_dict.get("burning_ship_zoom_increase", 2) + else: + return + + self.zoom *= zoom + + self.zoom_label.text = f"Zoom: {self.zoom}" + + self.zoom_at(self.window.mouse.data["x"], self.window.mouse.data["y"], zoom) + self.create_image() + + self.pypresence_client.update(state='Viewing Burning Ship', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time) + + def on_draw(self): + self.window.clear() + self.burning_ship_sprite.draw() + self.ui.draw() diff --git a/game/julia.py b/game/julia.py index 427c8c3..722ea48 100644 --- a/game/julia.py +++ b/game/julia.py @@ -2,7 +2,7 @@ import arcade, arcade.gui, pyglet, json from PIL import Image -from game.shader import create_julia_shader +from game.shader import create_iter_calc_shader from utils.constants import menu_background_color, button_style from utils.preload import button_texture, button_hovered_texture @@ -37,7 +37,7 @@ class JuliaViewer(arcade.gui.UIView): def on_show_view(self): super().on_show_view() - self.shader_program, self.julia_image = create_julia_shader(self.window.width, self.window.height, self.settings_dict.get("julia_precision", "Single").lower(), self.settings_dict.get("julia_escape_radius", 2), self.settings_dict.get("julia_type", "Classic swirling"), int(self.settings_dict.get("julia_n", 2))) + self.shader_program, self.julia_image = create_iter_calc_shader("julia", self.window.width, self.window.height, self.settings_dict.get("julia_precision", "Single").lower(), int(self.settings_dict.get("julia_n", 2)), self.settings_dict.get("julia_escape_radius", 2), self.settings_dict.get("julia_type", "Classic swirling")) self.julia_sprite = pyglet.sprite.Sprite(img=self.julia_image) diff --git a/game/mandelbrot.py b/game/mandelbrot.py index 163893a..229cc76 100644 --- a/game/mandelbrot.py +++ b/game/mandelbrot.py @@ -2,7 +2,7 @@ import arcade, arcade.gui, pyglet, json from PIL import Image -from game.shader import create_mandelbrot_shader +from game.shader import create_iter_calc_shader from utils.constants import menu_background_color, button_style, mandelbrot_initial_real_min, mandelbrot_initial_real_max, mandelbrot_initial_imag_min, mandelbrot_initial_imag_max from utils.preload import button_texture, button_hovered_texture @@ -25,7 +25,7 @@ class MandelbrotViewer(arcade.gui.UIView): def on_show_view(self): super().on_show_view() - self.shader_program, self.mandelbrot_image = create_mandelbrot_shader(self.window.width, self.window.height, self.settings_dict.get("mandelbrot_precision", "Single").lower()) + self.shader_program, self.mandelbrot_image = create_iter_calc_shader("mandelbrot", self.window.width, self.window.height, self.settings_dict.get("mandelbrot_precision", "Single").lower(), int(self.settings_dict.get("mandelbrot_n", 2)), int(self.settings_dict.get("mandelbrot_escape_radius", 2))) self.mandelbrot_sprite = pyglet.sprite.Sprite(img=self.mandelbrot_image) diff --git a/game/newton_fractal.py b/game/newton_fractal.py new file mode 100644 index 0000000..8639064 --- /dev/null +++ b/game/newton_fractal.py @@ -0,0 +1,95 @@ +import arcade, arcade.gui, pyglet, json + +from PIL import Image + +from game.shader import create_iter_calc_shader +from utils.constants import menu_background_color, button_style, newton_fractal_initial_real_min, newton_fractal_initial_real_max, newton_fractal_initial_imag_min, newton_fractal_initial_imag_max +from utils.preload import button_texture, button_hovered_texture + +class NewtonFractalViewer(arcade.gui.UIView): + def __init__(self, pypresence_client): + super().__init__() + + self.pypresence_client = pypresence_client + self.real_min = newton_fractal_initial_real_min + self.real_max = newton_fractal_initial_real_max + self.imag_min = newton_fractal_initial_imag_min + self.imag_max = newton_fractal_initial_imag_max + + with open("settings.json", "r") as file: + self.settings_dict = json.load(file) + + self.max_iter = self.settings_dict.get("newton_fractal_max_iter", 200) + self.zoom = 1.0 + + def on_show_view(self): + super().on_show_view() + + self.shader_program, self.newton_fractal_image = create_iter_calc_shader("newton_fractal", self.window.width, self.window.height, self.settings_dict.get("newton_fractal_precision", "Single").lower(), 2, int(self.settings_dict.get("newton_fractal_escape_radius", 2))) + + self.newton_fractal_sprite = pyglet.sprite.Sprite(img=self.newton_fractal_image) + + self.create_image() + + self.pypresence_client.update(state='Viewing Newton Fractal', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time) + + self.setup_ui() + + def main_exit(self): + from menus.main import Main + self.window.show_view(Main(self.pypresence_client)) + + def setup_ui(self): + self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) + + self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10, vertical=False), anchor_x="center", anchor_y="top") + self.zoom_label = self.info_box.add(arcade.gui.UILabel(text=f"Zoom: {self.zoom}", font_name="Protest Strike", font_size=16)) + self.max_iter_label = self.info_box.add(arcade.gui.UILabel(text=f"Max Iterations: {self.max_iter}", font_name="Protest Strike", font_size=16)) + + self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) + self.back_button.on_click = lambda event: self.main_exit() + self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5) + + def zoom_at(self, center_x, center_y, zoom_factor): + center_real = self.real_min + (center_x / self.width) * (self.real_max - self.real_min) + center_imag = self.imag_min + (center_y / self.height) * (self.imag_max - self.imag_min) + + new_real_range = (self.real_max - self.real_min) / zoom_factor + new_imag_range = (self.imag_max - self.imag_min) / zoom_factor + + self.real_min = center_real - new_real_range / 2 + self.real_max = center_real + new_real_range / 2 + self.imag_min = center_imag - new_imag_range / 2 + self.imag_max = center_imag + new_imag_range / 2 + + def create_image(self): + with self.shader_program: + self.shader_program['u_maxIter'] = int(self.max_iter) + self.shader_program['u_resolution'] = (self.window.width, self.window.height) + self.shader_program['u_real_range'] = (self.real_min, self.real_max) + self.shader_program['u_imag_range'] = (self.imag_min, self.imag_max) + self.shader_program.dispatch(self.newton_fractal_image.width, self.newton_fractal_image.height, 1, barrier=pyglet.gl.GL_ALL_BARRIER_BITS) + + def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> bool | None: + super().on_mouse_press(x, y, button, modifiers) + + if button == arcade.MOUSE_BUTTON_LEFT: + zoom = self.settings_dict.get("newton_fractal_zoom_increase", 2) + elif button == arcade.MOUSE_BUTTON_RIGHT: + zoom = 1 / self.settings_dict.get("newton_fractal_zoom_increase", 2) + else: + return + + self.zoom *= zoom + + self.zoom_label.text = f"Zoom: {self.zoom}" + + self.zoom_at(self.window.mouse.data["x"], self.window.mouse.data["y"], zoom) + self.create_image() + + self.pypresence_client.update(state='Viewing Newton Fractal', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time) + + def on_draw(self): + self.window.clear() + self.newton_fractal_sprite.draw() + self.ui.draw() diff --git a/game/shader.py b/game/shader.py index 68f9f5f..b81d52e 100644 --- a/game/shader.py +++ b/game/shader.py @@ -1,156 +1,245 @@ import pyglet from utils.constants import c_for_julia_type -mandelbrot_compute_source = """#version 430 core +newton_coloring = """vec4 getColor(int color_number) {{ + vec4 value = vec4(0.0, 0.0, 0.0, 1.0); + if (color_number == 0) {{ + value.r = 1.0; + }} + else if (color_number == 1) {{ + value.g = 1.0; + }} + else if (color_number == 2) {{ + value.b = 1.0; + }} + return value; +}} +""" + +polynomial_coloring = """vec4 getColor(int iters) {{ + vec4 value = vec4(0.0, 0.0, 0.0, 1.0); + if (iters != u_maxIter) {{ + float t = float(iters) / float(u_maxIter); + value.r = 9.0 * (1.0 - t) * t * t * t; + value.g = 15.0 * (1.0 - t) * (1.0 - t) * t * t; + value.b = 8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t; + }} + return value; +}} +""" + +fire_coloring = """vec4 getColor(int iters) {{ + vec4 value = vec4(0.0, 0.0, 0.0, 1.0); + if (iters != u_maxIter) {{ + float t = float(iters) / float(u_maxIter); + value.r = 3.0 * t; + value.g = 2.0 * t * t; + value.b = t * t * t; + }} + return value; +}} +""" + +iter_fractal_template = """#version 430 core uniform int u_maxIter; uniform vec2 u_resolution; uniform vec2 u_real_range; uniform vec2 u_imag_range; - layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; layout(location = 0, rgba32f) uniform image2D img_output; -int mandelbrot({vec2type} c, int maxIter) { - {vec2type} z = {vec2type}(0.0, 0.0); - for (int n = 0; n < maxIter; n++) { - if (dot(z, z) > 4.0) { - return n; - } - z = {vec2type}( - z.x * z.x - z.y * z.y + c.x, - 2.0 * z.x * z.y + c.y - ); - } - return maxIter; -} +{coloring_func} +{iter_calc_func} -{vec2type} map_pixel({floattype} x, {floattype} y, {vec2type} resolution, {vec2type} real_range, {vec2type} imag_range) { +{vec2type} map_pixel({floattype} x, {floattype} y, {vec2type} resolution, {vec2type} real_range, {vec2type} imag_range) {{ {floattype} real = real_range.x + (x / resolution.x) * (real_range.y - real_range.x); {floattype} imag = imag_range.x + (y / resolution.y) * (imag_range.y - imag_range.x); return {vec2type}(real, imag); -} +}} -void main() { +void main() {{ ivec2 texel_coord = ivec2(gl_GlobalInvocationID.xy); - {vec2type} c = map_pixel({floattype}(texel_coord.x), {floattype}(texel_coord.y), u_resolution, u_real_range, u_imag_range); - int iters = mandelbrot(c, u_maxIter); - - vec4 value = vec4(0.0, 0.0, 0.0, 1.0); - - if (iters != u_maxIter) { - float t = float(iters) / float(u_maxIter); - - value.r = 9.0 * (1.0 - t) * t * t * t; - value.g = 15.0 * (1.0 - t) * (1.0 - t) * t * t; - value.b = 8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t; - } - + {vec2type} pos = map_pixel({floattype}(texel_coord.x), {floattype}(texel_coord.y), u_resolution, u_real_range, u_imag_range); + int iters = calculate_iters(pos); + vec4 value = getColor(iters); imageStore(img_output, texel_coord, value); -} +}} """ sierpinsky_carpet_compute_source = """#version 430 core - uniform int u_depth; uniform int u_zoom; uniform vec2 u_center; layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; layout(location = 0, rgba32f) uniform image2D img_output; - -void main() { +void main() {{ {vec2type} centered = {vec2type}(gl_GlobalInvocationID.xy) - u_center; {vec2type} zoomed = centered / u_zoom; {vec2type} final_coord = zoomed + u_center; - ivec2 coord = ivec2(final_coord); - bool isHole = false; - - for (int i = 0; i < u_depth; ++i) { - if (coord.x % 3 == 1 && coord.y % 3 == 1) { + for (int i = 0; i < u_depth; ++i) {{ + if (coord.x % 3 == 1 && coord.y % 3 == 1) {{ isHole = true; break; - } + }} coord /= 3; - } - + }} vec4 color = isHole ? vec4(0, 0, 0, 1) : vec4(1, 1, 1, 1); imageStore(img_output, ivec2(gl_GlobalInvocationID.xy), color); -} - +}} """ -julia_template = """#version 430 core - -uniform int u_maxIter; -uniform vec2 u_resolution; -uniform vec2 u_real_range; -uniform vec2 u_imag_range; - -layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; -layout(location = 0, rgba32f) uniform image2D img_output; - -{vec2type} map_pixel({floattype} x, {floattype} y, {vec2type} resolution, {vec2type} real_range, {vec2type} imag_range) { - {floattype} real = real_range.x + (x / resolution.x) * (real_range.y - real_range.x); - {floattype} imag = imag_range.x + (y / resolution.y) * (imag_range.y - imag_range.x); - return {vec2type}(real, imag); -} - -void main() { - ivec2 texel_coord = ivec2(gl_GlobalInvocationID.xy); - - float R = {escape_radius}; - int n = {julia_n}; - {vec2type} c = {vec2type}{julia_c}; - - {vec2type} z = map_pixel({floattype}(texel_coord.x), {floattype}(texel_coord.y), u_resolution, u_real_range, u_imag_range); +normal_julia_calc = """int calculate_iters({vec2type} z) {{ int iters = 0; - - {julia_calc} - - vec4 value = vec4(0.0, 0.0, 0.0, 1.0); - - if (iters != u_maxIter) { - float t = float(iters) / float(u_maxIter); - - value.r = 9.0 * (1.0 - t) * t * t * t; - value.g = 15.0 * (1.0 - t) * (1.0 - t) * t * t; - value.b = 8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t; - } - - imageStore(img_output, texel_coord, value); -} -""" - -normal_julia_calc = """while (z.x * z.x + z.y * z.y < R*R && iters < u_maxIter) - { + float R = {escape_radius}; + int n = {multi_n}; + {vec2type} c = {vec2type}{julia_c}; + while (dot(z, z) < R * R && iters < u_maxIter){{ {floattype} xtemp = z.x * z.x - z.y * z.y; z.y = 2 * z.x * z.y + c.y; z.x = xtemp + c.x; - - iters = iters + 1; - } + iters++; + }} + return iters; +}} """ -multi_julia_calc = """while ((z.x * z.x + z.y * z.y) < R*R && iters < u_maxIter) { - {floattype} xtmp = pow((z.x * z.x + z.y * z.y), (n / 2)) * cos(n * atan(z.y, z.x)) + c.x; - z.y = pow((z.x * z.x + z.y * z.y), (n / 2)) * sin(n * atan(z.y, z.x)) + c.y; - z.x = xtmp; +multi_julia_calc = """int calculate_iters(float z) {{ + int iters = 0; + float R = {escape_radius}; + float n = float({multi_n}); + float c = float({julia_c}); - iters = iters + 1; - } + while (dot(z, z) < R * R && iters < u_maxIter) {{ + float r = length(z); + float theta = atan(z.y, z.x); + float r_pow = pow(r, n); + + z = vec2(r_pow * cos(n * theta), r_pow * sin(n * theta)) + c; + + iters++; + }} + + return iters; +}} +""" + +mandelbrot_calc = """int calculate_iters({vec2type} c) {{ + int iters = 0; + {vec2type} z = {vec2type}(0.0, 0.0); + float R = {escape_radius}; + + while (dot(z, z) < R * R && iters < u_maxIter) {{ + z = {vec2type}( + z.x * z.x - z.y * z.y + c.x, + 2.0 * z.x * z.y + c.y + ); + + iters++; + }} + + return iters; +}} +""" + +multibrot_calc = """int calculate_iters(vec2 c) {{ + int iters = 0; + vec2 z = vec2(0.0); + float n = {multi_n}; + float R = {escape_radius}; + + while (dot(z, z) < R * R && iters < u_maxIter) {{ + float r = length(z); + float theta = atan(z.y, z.x); + + float r_n = pow(r, n); + float theta_n = n * theta; + + z = r_n * vec2(cos(theta_n), sin(theta_n)) + c; + + iters++; + }} + + return iters; +}} +""" + +burning_ship_calc = """int calculate_iters({vec2type} c) {{ + int iters = 0; + {vec2type} z = {vec2type}(0.0, 0.0); + float R = {escape_radius}; + + while (dot(z, z) < R * R && iters < u_maxIter) {{ + {floattype} xtemp = z.x * z.x - z.y * z.y + c.x; + z.y = abs(2.0 * z.x * z.y) + c.y; + z.x = xtemp; + + iters++; + }} + + return iters; +}} +""" + +newton_fractal_calc = """vec2 cmul(vec2 a, vec2 b) {{ + return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +}} + +vec2 cdiv(vec2 a, vec2 b) {{ + float denom = b.x * b.x + b.y * b.y; + return vec2((a.x * b.x + a.y * b.y) / denom, (a.y * b.x - a.x * b.y) / denom); +}} + +vec2 cpow(vec2 z, int power) {{ + vec2 result = vec2(1.0, 0.0); + for (int i = 0; i < power; ++i) {{ + result = cmul(result, z); + }} + return result; +}} + +vec2 func(vec2 z) {{ + return cpow(z, 3) - vec2(1.0, 0.0); +}} + +vec2 derivative(vec2 z) {{ + return 3.0 * cmul(z, z); +}} + +int calculate_iters(vec2 z) {{ + float tolerance = 0.000001; + vec2 roots[3] = vec2[]( + vec2(1, 0), + vec2(-0.5, 0.866025404), + vec2(-0.5, -0.866025404) + ); + + for (int iters = 0; iters < u_maxIter; iters++) {{ + z -= cdiv(func(z), derivative(z)); + + for (int i = 0; i < 3; i++) {{ + vec2 difference = z - roots[i]; + if (abs(difference.x) < tolerance && abs(difference.y) < tolerance) {{ + return i; + }} + }} + }} + + return -1; +}} """ def create_sierpinsky_carpet_shader(width, height, precision="single"): shader_source = sierpinsky_carpet_compute_source - if precision == "single": - shader_source = shader_source.replace("{vec2type}", "vec2").replace("{floattype}", "float") - elif precision == "double": - shader_source = shader_source.replace("{vec2type}", "dvec2").replace("{floattype}", "double") - else: - raise TypeError("Invalid Precision") + replacements = { + "vec2type": "dvec2" if precision == "double" else "vec2", + "floattype": "double" if precision == "double" else "float" + } + + shader_source = shader_source.format_map(replacements) shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source) @@ -161,27 +250,40 @@ def create_sierpinsky_carpet_shader(width, height, precision="single"): return shader_program, sierpinsky_carpet_image -def create_julia_shader(width, height, precision="single", escape_radius=2, julia_type="Classic swirling", julia_n=2): - shader_source = julia_template +def create_iter_calc_shader(fractal_type, width, height, precision="single", multi_n=2, escape_radius=2, julia_type="Classic swirling"): + shader_source = iter_fractal_template - if julia_n == 2: - shader_source = shader_source.replace("{julia_calc}", normal_julia_calc) + replacements = { + "multi_n": str(multi_n), + "julia_c": str(c_for_julia_type[julia_type]), + "escape_radius": str(escape_radius), + "vec2type": "dvec2" if int(multi_n) == 2 and precision == "double" else "vec2", + "floattype": "double" if int(multi_n) == 2 and precision == "double" else "float" + } - if precision == "single": - shader_source = shader_source.replace("{vec2type}", "vec2").replace("{floattype}", "float") - elif precision == "double": - shader_source = shader_source.replace("{vec2type}", "dvec2").replace("{floattype}", "double") + replacements["coloring_func"] = polynomial_coloring.format_map(replacements) - else: - shader_source = shader_source.replace("{julia_calc}", multi_julia_calc) - shader_source = shader_source.replace("{vec2type}", "vec2").replace("{floattype}", "float") # pow and atan only support floats + if fractal_type == "mandelbrot": + if int(multi_n) == 2: + replacements["iter_calc_func"] = mandelbrot_calc.format_map(replacements) + else: + replacements["iter_calc_func"] = multibrot_calc.format_map(replacements) - shader_source = shader_source.replace("{julia_n}", str(julia_n)) + elif fractal_type == "julia": + if int(multi_n) == 2: + replacements["iter_calc_func"] = normal_julia_calc.format_map(replacements) + else: + replacements["iter_calc_func"] = multi_julia_calc.format_map(replacements) - julia_c = c_for_julia_type[julia_type] - shader_source = shader_source.replace("{julia_c}", str(julia_c)) + elif fractal_type == "burning_ship": + replacements["coloring_func"] = fire_coloring.format_map(replacements) + replacements["iter_calc_func"] = burning_ship_calc.format_map(replacements) - shader_source = shader_source.replace("{escape_radius}", str(escape_radius)) + elif fractal_type == "newton_fractal": + replacements["coloring_func"] = newton_coloring.format_map(replacements) + replacements["iter_calc_func"] = newton_fractal_calc.format_map(replacements) + + shader_source = shader_source.format_map(replacements) shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source) @@ -191,23 +293,3 @@ def create_julia_shader(width, height, precision="single", escape_radius=2, juli julia_image.bind_image_texture(unit=uniform_location) return shader_program, julia_image - - -def create_mandelbrot_shader(width, height, precision="single"): - shader_source = mandelbrot_compute_source - - if precision == "single": - shader_source = shader_source.replace("{vec2type}", "vec2").replace("{floattype}", "float") - elif precision == "double": - shader_source = shader_source.replace("{vec2type}", "dvec2").replace("{floattype}", "double") - else: - raise TypeError("Invalid Precision") - - shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source) - - mandelbrot_image = pyglet.image.Texture.create(width, height, internalformat=pyglet.gl.GL_RGBA32F) - - uniform_location = shader_program['img_output'] - mandelbrot_image.bind_image_texture(unit=uniform_location) - - return shader_program, mandelbrot_image diff --git a/game/play.py b/menus/fractal_chooser.py similarity index 73% rename from game/play.py rename to menus/fractal_chooser.py index 96484cf..dcb62a6 100644 --- a/game/play.py +++ b/menus/fractal_chooser.py @@ -32,6 +32,12 @@ class FractalChooser(arcade.gui.UIView): self.julia_button = self.grid.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Julia', style=button_style, width=200, height=200), row=0, column=2) self.julia_button.on_click = lambda event: self.julia() + self.burning_ship_button = self.grid.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Burning Ship', style=button_style, width=200, height=200), row=1, column=0) + self.burning_ship_button.on_click = lambda event: self.burning_ship() + + self.newton_fractal_button = self.grid.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Newton Fractal', style=button_style, width=200, height=200), row=1, column=1) + self.newton_fractal_button.on_click = lambda event: self.newton_fractal() + def main_exit(self): from menus.main import Main self.window.show_view(Main(self.pypresence_client)) @@ -47,3 +53,11 @@ class FractalChooser(arcade.gui.UIView): def julia(self): from game.julia import JuliaViewer self.window.show_view(JuliaViewer(self.pypresence_client)) + + def burning_ship(self): + from game.burning_ship import BurningShipViewer + self.window.show_view(BurningShipViewer(self.pypresence_client)) + + def newton_fractal(self): + from game.newton_fractal import NewtonFractalViewer + self.window.show_view(NewtonFractalViewer(self.pypresence_client)) diff --git a/menus/main.py b/menus/main.py index f70da65..382b1a8 100644 --- a/menus/main.py +++ b/menus/main.py @@ -1,5 +1,4 @@ import arcade, arcade.gui, asyncio, pypresence, time, copy, json -from game.play import FractalChooser from utils.preload import button_texture, button_hovered_texture from utils.constants import big_button_style, discord_presence_id from utils.utils import FakePyPresence @@ -60,7 +59,7 @@ class Main(arcade.gui.UIView): self.settings_button.on_click = lambda event: self.settings() def play(self): - from game.play import FractalChooser + from menus.fractal_chooser import FractalChooser self.window.show_view(FractalChooser(self.pypresence_client)) def settings(self): diff --git a/utils/constants.py b/utils/constants.py index b35a294..eb74171 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -12,6 +12,16 @@ mandelbrot_initial_real_max = 1.0 mandelbrot_initial_imag_min = -1.0 mandelbrot_initial_imag_max = 1.0 +burning_ship_initial_real_min = -2.0 +burning_ship_initial_real_max = 1.5 +burning_ship_initial_imag_min = -2.0 +burning_ship_initial_imag_max = 1.0 + +newton_fractal_initial_real_min = -2.0 +newton_fractal_initial_real_max = 2.0 +newton_fractal_initial_imag_min = -2.0 +newton_fractal_initial_imag_max = 2.0 + c_for_julia_type = { "Classic swirling": (-0.7, 0.27015), "Douady rabbit": (-0.123, 0.745), @@ -35,9 +45,17 @@ slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'pr settings = { "Mandelbrot": { "Float Precision": {"type": "option", "options": ["Single", "Double"], "config_key": "mandelbrot_precision", "default": "Single"}, + "N": {"type": "slider", "min": 1, "max": 10, "config_key": "mandelbrot_n", "default": 2, "step": 1}, + "Escape Radius": {"type": "slider", "min": 1, "max": 10, "config_key": "mandelbrot_escape_radius", "default": 2, "step": 0.1}, "Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "mandelbrot_zoom_increase", "default": 2}, "Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "mandelbrot_max_iter", "default": 200, "step": 100} }, + "Burning Ship": { + "Float Precision": {"type": "option", "options": ["Single", "Double"], "config_key": "burning_ship_precision", "default": "Single"}, + "Escape Radius": {"type": "slider", "min": 1, "max": 10, "config_key": "burning_ship_escape_radius", "default": 2, "step": 0.1}, + "Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "burning_ship_zoom_increase", "default": 2}, + "Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "burning_ship_max_iter", "default": 200, "step": 100} + }, "Sierpinsky Carpet": { "Float Precision": {"type": "option", "options": ["Single", "Double"], "config_key": "sierpinsky_precision", "default": "Single"}, "Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "sierpinsky_zoom_increase", "default": 2}, @@ -49,7 +67,11 @@ settings = { "N": {"type": "slider", "min": 1, "max": 10, "config_key": "julia_n", "default": 2, "step": 1}, "Escape Radius": {"type": "slider", "min": 1, "max": 10, "config_key": "julia_escape_radius", "default": 2, "step": 0.1}, "Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "julia_zoom_increase", "default": 2}, - "Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "julia_max_iter", "default": 200, "step": 100} + "Max Iterations": {"type": "slider", "min": 100, "max": 4000, "config_key": "julia_max_iter", "default": 200, "step": 100} + }, + "Newton Fractal": { + "Zoom Increase Per Click": {"type": "slider", "min": 2, "max": 100, "config_key": "newton_fractal_zoom_increase", "default": 2}, + "Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "newton_fractal_max_iter", "default": 200, "step": 100} }, "Graphics": { "Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},