diff --git a/game/julia.py b/game/julia.py new file mode 100644 index 0000000..e99d530 --- /dev/null +++ b/game/julia.py @@ -0,0 +1,95 @@ +import arcade, arcade.gui, pyglet, json + +from PIL import Image + +from game.shader import create_julia_shader +from utils.constants import menu_background_color, button_style +from utils.preload import button_texture, button_hovered_texture + +class JuliaViewer(arcade.gui.UIView): + def __init__(self, pypresence_client): + super().__init__() + + self.pypresence_client = pypresence_client + + with open("settings.json", "r") as file: + self.settings_dict = json.load(file) + + self.max_iter = self.settings_dict.get("julia_max_iter", 200) + self.zoom = 1.0 + self.real_min = -self.settings_dict.get("julia_escape_radius", 2) + self.real_max = self.settings_dict.get("julia_escape_radius", 2) + self.imag_min = -self.settings_dict.get("julia_escape_radius", 2) + self.imag_max = self.settings_dict.get("julia_escape_radius", 2) + + 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 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")) + + self.julia_sprite = pyglet.sprite.Sprite(img=self.julia_image) + + self.create_image() + + self.pypresence_client.update(state='Viewing Julia', 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 create_image(self): + with self.shader_program: + self.shader_program['u_maxIter'] = 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.julia_image.width, self.julia_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("julia_zoom_increase", 2) + elif button == arcade.MOUSE_BUTTON_RIGHT: + zoom = 1 / self.settings_dict.get("julia_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 Julia', details=f'Zoom: {self.zoom}\nMax Iterations: {self.max_iter}', start=self.pypresence_client.start_time) + + def on_draw(self): + self.window.clear() + self.julia_sprite.draw() + self.ui.draw() diff --git a/game/mandelbrot.py b/game/mandelbrot.py index a062374..87aae2e 100644 --- a/game/mandelbrot.py +++ b/game/mandelbrot.py @@ -3,7 +3,7 @@ import arcade, arcade.gui, pyglet, json from PIL import Image from game.shader import create_mandelbrot_shader -from utils.constants import menu_background_color, button_style, initial_real_min, initial_real_max, initial_imag_min, initial_imag_max +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 class MandelbrotViewer(arcade.gui.UIView): @@ -11,10 +11,10 @@ class MandelbrotViewer(arcade.gui.UIView): super().__init__() self.pypresence_client = pypresence_client - self.real_min = initial_real_min - self.real_max = initial_real_max - self.imag_min = initial_imag_min - self.imag_max = initial_imag_max + self.real_min = mandelbrot_initial_real_min + self.real_max = mandelbrot_initial_real_max + self.imag_min = mandelbrot_initial_imag_min + self.imag_max = mandelbrot_initial_imag_max with open("settings.json", "r") as file: self.settings_dict = json.load(file) diff --git a/game/play.py b/game/play.py index 7c8b640..96484cf 100644 --- a/game/play.py +++ b/game/play.py @@ -29,6 +29,9 @@ class FractalChooser(arcade.gui.UIView): self.sierpinsky_carpet_button = self.grid.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Sierpinsky Carpet', style=button_style, width=200, height=200), row=0, column=1) self.sierpinsky_carpet_button.on_click = lambda event: self.sierpinsky_carpet() + 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() + def main_exit(self): from menus.main import Main self.window.show_view(Main(self.pypresence_client)) @@ -40,3 +43,7 @@ class FractalChooser(arcade.gui.UIView): def sierpinsky_carpet(self): from game.sierpinsky_carpet import SierpinskyCarpetViewer self.window.show_view(SierpinskyCarpetViewer(self.pypresence_client)) + + def julia(self): + from game.julia import JuliaViewer + self.window.show_view(JuliaViewer(self.pypresence_client)) diff --git a/game/shader.py b/game/shader.py index 86a1220..5050aa5 100644 --- a/game/shader.py +++ b/game/shader.py @@ -1,4 +1,6 @@ import pyglet +from utils.constants import c_for_julia_type + mandelbrot_compute_source = """#version 430 core uniform int u_maxIter; @@ -54,11 +56,17 @@ 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() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + {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) { @@ -75,9 +83,66 @@ void main() { """ +julia_compute_source = """#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); + + int R = {escape_radius}; + {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); + + int iters = 0; + + while ((z.x * z.x + z.y * z.y) < pow(R, 2) && 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; + } + + vec4 value = vec4(0.0, 0.0, 0.0, 1.0); + + if (iters != u_maxIter) { + float t = float(iters) / float(u_maxIter); + float pow_amount = 0.7; + t = pow(t, pow_amount); + + 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); +} +""" + 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") + shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source) sierpinsky_carpet_image = pyglet.image.Texture.create(width, height, internalformat=pyglet.gl.GL_RGBA32F) @@ -87,6 +152,31 @@ 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"): + shader_source = julia_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") + + julia_c = c_for_julia_type[julia_type] + shader_source = shader_source.replace("{julia_c}", str(julia_c)) + + shader_source = shader_source.replace("{escape_radius}", str(escape_radius)) + + shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source) + + julia_image = pyglet.image.Texture.create(width, height, internalformat=pyglet.gl.GL_RGBA32F) + + uniform_location = shader_program['img_output'] + 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 diff --git a/game/sierpinsky_carpet.py b/game/sierpinsky_carpet.py index d918c85..db900ea 100644 --- a/game/sierpinsky_carpet.py +++ b/game/sierpinsky_carpet.py @@ -22,7 +22,7 @@ class SierpinskyCarpetViewer(arcade.gui.UIView): def on_show_view(self): super().on_show_view() - self.shader_program, self.sierpinsky_carpet_image = create_sierpinsky_carpet_shader(self.window.width, self.window.height, self.settings_dict.get("precision", "Single").lower()) + self.shader_program, self.sierpinsky_carpet_image = create_sierpinsky_carpet_shader(self.window.width, self.window.height, self.settings_dict.get("sierpinsky_precision", "Single").lower()) self.sierpinsky_carpet_sprite = pyglet.sprite.Sprite(img=self.sierpinsky_carpet_image) @@ -50,9 +50,8 @@ class SierpinskyCarpetViewer(arcade.gui.UIView): def create_image(self): with self.shader_program: self.shader_program['u_depth'] = self.depth - #self.shader_program['u_zoom'] = int(self.zoom) - #self.shader_program['u_resolution'] = self.window.size - #self.shader_program['u_center'] = self.click_center + self.shader_program['u_zoom'] = int(self.zoom) + self.shader_program['u_center'] = self.click_center self.shader_program.dispatch(self.sierpinsky_carpet_image.width, self.sierpinsky_carpet_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: @@ -73,7 +72,7 @@ class SierpinskyCarpetViewer(arcade.gui.UIView): self.create_image() - self.pypresence_client.update(state='Viewing Sierpinsky Carpet', details=f'Zoom: {self.zoom}\nMax Iterations: {self.depth}', start=self.pypresence_client.start_time) + self.pypresence_client.update(state='Viewing Sierpinsky Carpet', details=f'Zoom: {self.zoom}\nDepth: {self.depth}', start=self.pypresence_client.start_time) def on_draw(self): self.window.clear() diff --git a/utils/constants.py b/utils/constants.py index 40acf2b..96fc01a 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -7,10 +7,17 @@ menu_background_color = (30, 30, 47) log_dir = 'logs' discord_presence_id = 1365949409254441000 -initial_real_min = -2.0 -initial_real_max = 1.0 -initial_imag_min = -1.0 -initial_imag_max = 1.0 +mandelbrot_initial_real_min = -2.0 +mandelbrot_initial_real_max = 1.0 +mandelbrot_initial_imag_min = -1.0 +mandelbrot_initial_imag_max = 1.0 + +c_for_julia_type = { + "Classic swirling": (-0.7, 0.27015), + "Douady rabbit": (-0.123, 0.745), + "Nebula-style": (0.285, 0), + "Snowflake": (-0.8, 0.156) +} button_style = {'normal': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'press': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK)} @@ -32,9 +39,17 @@ settings = { "Max Iterations": {"type": "slider", "min": 100, "max": 10000, "config_key": "mandelbrot_max_iter", "default": 200} }, "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}, "Depth": {"type": "slider", "min": 2, "max": 10000, "config_key": "sierpinsky_depth", "default": 10} }, + "Julia": { + "Type": {"type": "option", "options": ["Classic swirling", "Douady rabbit", "Nebula-style", "Snowflake"], "config_key": "julia_type", "default": "Classic swirling"}, + "Float Precision": {"type": "option", "options": ["Single", "Double"], "config_key": "julia_precision", "default": "Single"}, + "Escape Radius": {"type": "slider", "min": 1, "max": 10, "config_key": "julia_escape_radius", "default": 2}, + "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} + }, "Graphics": { "Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"}, "Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},