From 71a9ccd083de1d97aa7ddfc002a62d1603e22cbb Mon Sep 17 00:00:00 2001 From: ajh123 Date: Mon, 25 Dec 2023 17:26:45 +0000 Subject: [PATCH 01/14] feat: add imgui + make f3 screen use imgui --- community/keyboard_mouse.py | 96 ++++++++++++++++++++----------------- community/main.py | 65 ++++++++++++++++--------- community/start.bat | 1 + 3 files changed, 94 insertions(+), 68 deletions(-) diff --git a/community/keyboard_mouse.py b/community/keyboard_mouse.py index c1146707..e79753bb 100644 --- a/community/keyboard_mouse.py +++ b/community/keyboard_mouse.py @@ -2,6 +2,7 @@ import controller import math +import imgui class Keyboard_Mouse(controller.Controller): def __init__(self, game): @@ -15,60 +16,65 @@ def __init__(self, game): self.game.on_key_release = self.on_key_release def on_mouse_press(self, x, y, button, modifiers): - if not self.game.mouse_captured: - self.game.mouse_captured = True - self.game.set_exclusive_mouse(True) + if not imgui.get_io().want_capture_mouse: + if not self.game.mouse_captured: + self.game.mouse_captured = True + self.game.set_exclusive_mouse(True) - return + return - if button == pyglet.window.mouse.RIGHT: self.interact(self.InteractMode.PLACE) - elif button == pyglet.window.mouse.LEFT: self.interact(self.InteractMode.BREAK) - elif button == pyglet.window.mouse.MIDDLE: self.interact(self.InteractMode.PICK) + if button == pyglet.window.mouse.RIGHT: self.interact(self.InteractMode.PLACE) + elif button == pyglet.window.mouse.LEFT: self.interact(self.InteractMode.BREAK) + elif button == pyglet.window.mouse.MIDDLE: self.interact(self.InteractMode.PICK) def on_mouse_motion(self, x, y, delta_x, delta_y): - if self.game.mouse_captured: - sensitivity = 0.004 + if not imgui.get_io().want_capture_mouse: + if self.game.mouse_captured: + sensitivity = 0.004 - self.game.player.rotation[0] += delta_x * sensitivity - self.game.player.rotation[1] += delta_y * sensitivity + self.game.player.rotation[0] += delta_x * sensitivity + self.game.player.rotation[1] += delta_y * sensitivity - self.game.player.rotation[1] = max(-math.tau / 4, min(math.tau / 4, self.game.player.rotation[1])) + self.game.player.rotation[1] = max(-math.tau / 4, min(math.tau / 4, self.game.player.rotation[1])) def on_mouse_drag(self, x, y, delta_x, delta_y, buttons, modifiers): - self.on_mouse_motion(x, y, delta_x, delta_y) + if not imgui.get_io().want_capture_mouse: + self.on_mouse_motion(x, y, delta_x, delta_y) def on_key_press(self, key, modifiers): - if not self.game.mouse_captured: - return - - if key == pyglet.window.key.D: self.start_move(self.MoveMode.RIGHT) - elif key == pyglet.window.key.A: self.start_move(self.MoveMode.LEFT) - elif key == pyglet.window.key.W: self.start_move(self.MoveMode.FORWARD) - elif key == pyglet.window.key.S: self.start_move(self.MoveMode.BACKWARD) - elif key == pyglet.window.key.SPACE : self.start_move(self.MoveMode.UP) - elif key == pyglet.window.key.LSHIFT: self.start_move(self.MoveMode.DOWN) - - elif key == pyglet.window.key.LCTRL : self.start_modifier(self.ModifierMode.SPRINT) - - elif key == pyglet.window.key.F: self.misc(self.MiscMode.FLY) - elif key == pyglet.window.key.G: self.misc(self.MiscMode.RANDOM) - elif key == pyglet.window.key.O: self.misc(self.MiscMode.SAVE) - elif key == pyglet.window.key.R: self.misc(self.MiscMode.TELEPORT) - elif key == pyglet.window.key.ESCAPE: self.misc(self.MiscMode.ESCAPE) - elif key == pyglet.window.key.F6: self.misc(self.MiscMode.SPEED_TIME) - elif key == pyglet.window.key.F11: self.misc(self.MiscMode.FULLSCREEN) - elif key == pyglet.window.key.F3: self.misc(self.MiscMode.TOGGLE_F3) - elif key == pyglet.window.key.F10: self.misc(self.MiscMode.TOGGLE_AO) + if not imgui.get_io().want_capture_keyboard: + if not self.game.mouse_captured: + return + + if key == pyglet.window.key.D: self.start_move(self.MoveMode.RIGHT) + elif key == pyglet.window.key.A: self.start_move(self.MoveMode.LEFT) + elif key == pyglet.window.key.W: self.start_move(self.MoveMode.FORWARD) + elif key == pyglet.window.key.S: self.start_move(self.MoveMode.BACKWARD) + elif key == pyglet.window.key.SPACE : self.start_move(self.MoveMode.UP) + elif key == pyglet.window.key.LSHIFT: self.start_move(self.MoveMode.DOWN) + + elif key == pyglet.window.key.LCTRL : self.start_modifier(self.ModifierMode.SPRINT) + + elif key == pyglet.window.key.F: self.misc(self.MiscMode.FLY) + elif key == pyglet.window.key.G: self.misc(self.MiscMode.RANDOM) + elif key == pyglet.window.key.O: self.misc(self.MiscMode.SAVE) + elif key == pyglet.window.key.R: self.misc(self.MiscMode.TELEPORT) + elif key == pyglet.window.key.ESCAPE: self.misc(self.MiscMode.ESCAPE) + elif key == pyglet.window.key.F6: self.misc(self.MiscMode.SPEED_TIME) + elif key == pyglet.window.key.F11: self.misc(self.MiscMode.FULLSCREEN) + elif key == pyglet.window.key.F3: self.misc(self.MiscMode.TOGGLE_F3) + elif key == pyglet.window.key.F10: self.misc(self.MiscMode.TOGGLE_AO) def on_key_release(self, key, modifiers): - if not self.game.mouse_captured: - return - - if key == pyglet.window.key.D: self.end_move(self.MoveMode.RIGHT) - elif key == pyglet.window.key.A: self.end_move(self.MoveMode.LEFT) - elif key == pyglet.window.key.W: self.end_move(self.MoveMode.FORWARD) - elif key == pyglet.window.key.S: self.end_move(self.MoveMode.BACKWARD) - elif key == pyglet.window.key.SPACE : self.end_move(self.MoveMode.UP) - elif key == pyglet.window.key.LSHIFT: self.end_move(self.MoveMode.DOWN) - - elif key == pyglet.window.key.LCTRL : self.end_modifier(self.ModifierMode.SPRINT) \ No newline at end of file + if not imgui.get_io().want_capture_keyboard: + if not self.game.mouse_captured: + return + + if key == pyglet.window.key.D: self.end_move(self.MoveMode.RIGHT) + elif key == pyglet.window.key.A: self.end_move(self.MoveMode.LEFT) + elif key == pyglet.window.key.W: self.end_move(self.MoveMode.FORWARD) + elif key == pyglet.window.key.S: self.end_move(self.MoveMode.BACKWARD) + elif key == pyglet.window.key.SPACE : self.end_move(self.MoveMode.UP) + elif key == pyglet.window.key.LSHIFT: self.end_move(self.MoveMode.DOWN) + + elif key == pyglet.window.key.LCTRL : self.end_modifier(self.ModifierMode.SPRINT) \ No newline at end of file diff --git a/community/main.py b/community/main.py index aaf34b2c..91f3a878 100644 --- a/community/main.py +++ b/community/main.py @@ -26,6 +26,10 @@ import keyboard_mouse from collections import deque +import imgui +from imgui.integrations.pyglet import create_renderer + + class InternalConfig: def __init__(self, options): self.RENDER_DISTANCE = options.RENDER_DISTANCE @@ -58,12 +62,6 @@ def __init__(self, **args): # F3 Debug Screen self.show_f3 = False - self.f3 = pyglet.text.Label("", x = 10, y = self.height - 10, - font_size = 16, - color = (255, 255, 255, 255), - width = self.width // 3, - multiline = True - ) self.system_info = f"""Python: {platform.python_implementation()} {platform.python_version()} System: {platform.machine()} {platform.system()} {platform.release()} {platform.version()} CPU: {platform.processor()} @@ -151,6 +149,11 @@ def __init__(self, **args): # GPU command syncs self.fences = deque() + + # ui stuff + imgui.create_context() + self.impl = create_renderer(self) + self.delta_time = 0 def toggle_fullscreen(self): self.set_fullscreen(not self.fullscreen) @@ -163,8 +166,11 @@ def on_close(self): super().on_close() - def update_f3(self, delta_time): + def update_f3(self): """Update the F3 debug screen content""" + imgui.set_next_window_position(5, 5) + imgui.set_next_window_bg_alpha(0.175) + imgui.get_io().ini_file_name = None player_chunk_pos = world.get_chunk_position(self.player.position) player_local_pos = world.get_local_position(self.player.position) @@ -172,11 +178,21 @@ def update_f3(self, delta_time): visible_chunk_count = len(self.world.visible_chunks) quad_count = sum(chunk.mesh_quad_count for chunk in self.world.chunks.values()) visible_quad_count = sum(chunk.mesh_quad_count for chunk in self.world.visible_chunks) - self.f3.text = \ -f""" -{round(1 / delta_time)} FPS ({self.world.chunk_update_counter} Chunk Updates) {"inf" if not self.options.VSYNC else "vsync"}{"ao" if self.options.SMOOTH_LIGHTING else ""} + + if imgui.begin( + "F3 Debug Screen", + self.show_f3, + flags= + imgui.WINDOW_NO_DECORATION | + imgui.WINDOW_ALWAYS_AUTO_RESIZE | + imgui.WINDOW_NO_SAVED_SETTINGS| + imgui.WINDOW_NO_FOCUS_ON_APPEARING | + imgui.WINDOW_NO_NAV + ): + imgui.text(f""" +{round(1 / self.delta_time)} FPS ({self.world.chunk_update_counter} Chunk Updates) {"inf" if not self.options.VSYNC else "vsync"}{"ao" if self.options.SMOOTH_LIGHTING else ""} C: {visible_chunk_count} / {chunk_count} pC: {self.world.pending_chunk_update_count} pU: {len(self.world.chunk_building_queue)} aB: {chunk_count} -Client Singleplayer @{round(delta_time * 1000)} ms tick {round(1 / delta_time)} TPS +Client Singleplayer @{round(self.delta_time * 1000)} ms tick {round(1 / self.delta_time)} TPS XYZ: ( X: {round(self.player.position[0], 3)} / Y: {round(self.player.position[1], 3)} / Z: {round(self.player.position[2], 3)} ) Block: {self.player.rounded_position[0]} {self.player.rounded_position[1]} {self.player.rounded_position[2]} @@ -190,12 +206,16 @@ def update_f3(self, delta_time): Vertex Data: {round(quad_count * 28 * ctypes.sizeof(gl.GLfloat) / 1048576, 3)} MiB ({quad_count} Quads) Visible Quads: {visible_quad_count} Buffer Uploading: Direct (glBufferSubData) -""" + """) + imgui.end() + + def update_ui(self): + if self.show_f3: + self.update_f3() def update(self, delta_time): """Every tick""" - if self.show_f3: - self.update_f3(delta_time) + self.impl.process_inputs() if not self.media_player.source and len(self.music) > 0: if not self.media_player.standby: @@ -213,6 +233,7 @@ def update(self, delta_time): self.player.update(delta_time) self.world.tick(delta_time) + self.delta_time = delta_time def on_draw(self): gl.glEnable(gl.GL_DEPTH_TEST) @@ -228,10 +249,6 @@ def on_draw(self): self.world.prepare_rendering() self.world.draw() - # Draw the F3 Debug screen - if self.show_f3: - self.f3.draw() - # CPU - GPU Sync if not self.options.SMOOTH_FPS: # self.fences.append(gl.glFenceSync(gl.GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) @@ -239,6 +256,12 @@ def on_draw(self): pass else: gl.glFinish() + + # Handle UI + imgui.new_frame() + self.update_ui() + imgui.render() + self.impl.render(imgui.get_draw_data()) # input functions @@ -248,8 +271,7 @@ def on_resize(self, width, height): self.player.view_width = width self.player.view_height = height - self.f3.y = self.height - 10 - self.f3.width = self.width // 3 + class Game: def __init__(self): @@ -262,7 +284,6 @@ def run(self): pyglet.app.run(interval = 0) - def init_logger(): log_folder = "logs/" log_filename = f"{time.time()}.log" @@ -278,8 +299,6 @@ def init_logger(): format="[%(asctime)s] [%(processName)s/%(threadName)s/%(levelname)s] (%(module)s.py/%(funcName)s) %(message)s") - - def main(): init_logger() game = Game() diff --git a/community/start.bat b/community/start.bat index 863f00b5..2024b1ca 100644 --- a/community/start.bat +++ b/community/start.bat @@ -3,4 +3,5 @@ py -m pip install --upgrade nbtlib py -m pip install --upgrade base36 py -m pip install --upgrade pyglm py -m pip install --upgrade numpy +py -m pip install --upgrade imgui[pyglet] py main.py \ No newline at end of file From 65f51e1e4645e6450e28acb520111af86e2d61f7 Mon Sep 17 00:00:00 2001 From: ajh123 Date: Mon, 25 Dec 2023 18:12:06 +0000 Subject: [PATCH 02/14] feat: initial scene system --- community/controller.py | 6 +- community/joystick.py | 2 +- community/keyboard_mouse.py | 22 ++-- community/main.py | 213 +++++++++++++++++++++++------------- 4 files changed, 152 insertions(+), 91 deletions(-) diff --git a/community/controller.py b/community/controller.py index a3a433c2..e0989721 100644 --- a/community/controller.py +++ b/community/controller.py @@ -57,12 +57,12 @@ def misc(self, mode): elif mode == self.MiscMode.SAVE: self.game.world.save.save() elif mode == self.MiscMode.ESCAPE: - self.game.mouse_captured = False - self.game.set_exclusive_mouse(False) + self.game.window.mouse_captured = False + self.game.window.set_exclusive_mouse(False) elif mode == self.MiscMode.SPEED_TIME: self.game.world.speed_daytime() elif mode == self.MiscMode.FULLSCREEN: - self.game.toggle_fullscreen() + self.game.window.toggle_fullscreen() elif mode == self.MiscMode.FLY: self.game.player.flying = not self.game.player.flying elif mode == self.MiscMode.TELEPORT: diff --git a/community/joystick.py b/community/joystick.py index 455b136a..76057c45 100644 --- a/community/joystick.py +++ b/community/joystick.py @@ -41,7 +41,7 @@ def init_joysticks(self, joysticks): joystick.open(exclusive=True) def update_controller(self): - if not self.game.mouse_captured or not self.joysticks: + if not self.game.window.mouse_captured or not self.joysticks: return self.game.player.rotation[0] += self.joystick_look[0] * self.camera_sensitivity diff --git a/community/keyboard_mouse.py b/community/keyboard_mouse.py index e79753bb..2bf9026a 100644 --- a/community/keyboard_mouse.py +++ b/community/keyboard_mouse.py @@ -8,18 +8,18 @@ class Keyboard_Mouse(controller.Controller): def __init__(self, game): super().__init__(game) - self.game.on_mouse_press = self.on_mouse_press - self.game.on_mouse_motion = self.on_mouse_motion - self.game.on_mouse_drag = self.on_mouse_drag + self.game.window.on_mouse_press = self.on_mouse_press + self.game.window.on_mouse_motion = self.on_mouse_motion + self.game.window.on_mouse_drag = self.on_mouse_drag - self.game.on_key_press = self.on_key_press - self.game.on_key_release = self.on_key_release + self.game.window.on_key_press = self.on_key_press + self.game.window.on_key_release = self.on_key_release def on_mouse_press(self, x, y, button, modifiers): if not imgui.get_io().want_capture_mouse: - if not self.game.mouse_captured: - self.game.mouse_captured = True - self.game.set_exclusive_mouse(True) + if not self.game.window.mouse_captured: + self.game.window.mouse_captured = True + self.game.window.set_exclusive_mouse(True) return @@ -29,7 +29,7 @@ def on_mouse_press(self, x, y, button, modifiers): def on_mouse_motion(self, x, y, delta_x, delta_y): if not imgui.get_io().want_capture_mouse: - if self.game.mouse_captured: + if self.game.window.mouse_captured: sensitivity = 0.004 self.game.player.rotation[0] += delta_x * sensitivity @@ -43,7 +43,7 @@ def on_mouse_drag(self, x, y, delta_x, delta_y, buttons, modifiers): def on_key_press(self, key, modifiers): if not imgui.get_io().want_capture_keyboard: - if not self.game.mouse_captured: + if not self.game.window.mouse_captured: return if key == pyglet.window.key.D: self.start_move(self.MoveMode.RIGHT) @@ -67,7 +67,7 @@ def on_key_press(self, key, modifiers): def on_key_release(self, key, modifiers): if not imgui.get_io().want_capture_keyboard: - if not self.game.mouse_captured: + if not self.game.window.mouse_captured: return if key == pyglet.window.key.D: self.end_move(self.MoveMode.RIGHT) diff --git a/community/main.py b/community/main.py index 91f3a878..7dafac05 100644 --- a/community/main.py +++ b/community/main.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import platform import ctypes import logging @@ -47,37 +49,34 @@ def __init__(self, options): self.ANTIALIASING = options.ANTIALIASING -class Window(pyglet.window.Window): - def __init__(self, **args): - super().__init__(**args) +class Scene(): + def __init__(self, window: Window) -> None: + self.window = window - # Options - self.options = InternalConfig(options) + def on_close(self): + pass - if self.options.INDIRECT_RENDERING and not gl.gl_info.have_version(4, 2): - raise RuntimeError("""Indirect Rendering is not supported on your hardware - This feature is only supported on OpenGL 4.2+, but your driver doesnt seem to support it, - Please disable "INDIRECT_RENDERING" in options.py""") - - # F3 Debug Screen + def on_draw(self): + pass - self.show_f3 = False - self.system_info = f"""Python: {platform.python_implementation()} {platform.python_version()} -System: {platform.machine()} {platform.system()} {platform.release()} {platform.version()} -CPU: {platform.processor()} -Display: {gl.gl_info.get_renderer()} -{gl.gl_info.get_version()}""" + def on_resize(self, width, height): + pass - logging.info(f"System Info: {self.system_info}") - # create shader + def update(self, delta_time): + pass - logging.info("Compiling Shaders") - if not self.options.COLORED_LIGHTING: - self.shader = shader.Shader("shaders/alpha_lighting/vert.glsl", "shaders/alpha_lighting/frag.glsl") - else: - self.shader = shader.Shader("shaders/colored_lighting/vert.glsl", "shaders/colored_lighting/frag.glsl") - self.shader_sampler_location = self.shader.find_uniform(b"u_TextureArraySampler") - self.shader.use() + def update_ui(self): + pass + + +class GameScene(Scene): + def __init__(self, window: Window) -> None: + super().__init__(window) + logging.info("Loading game scene") + + # F3 Debug Screen + + self.show_f3 = False # create textures logging.info("Creating Texture Array") @@ -85,18 +84,17 @@ def __init__(self, **args): # create world - self.world = world.World(self.shader, None, self.texture_manager, self.options) + self.world = world.World(self.window.shader, None, self.texture_manager, self.window.options) # player stuff logging.info("Setting up player & camera") - self.player = player.Player(self.world, self.shader, self.width, self.height) + self.player = player.Player(self.world, self.window.shader, self.window.width, self.window.height) self.world.player = self.player # pyglet stuff pyglet.clock.schedule(self.player.update_interpolation) pyglet.clock.schedule_interval(self.update, 1 / 60) - self.mouse_captured = False # misc stuff @@ -106,18 +104,7 @@ def __init__(self, **args): gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.world.texture_manager.texture_array) - gl.glUniform1i(self.shader_sampler_location, 0) - - # enable cool stuff - - gl.glEnable(gl.GL_DEPTH_TEST) - gl.glEnable(gl.GL_CULL_FACE) - gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) - - if self.options.ANTIALIASING: - gl.glEnable(gl.GL_MULTISAMPLE) - gl.glEnable(gl.GL_SAMPLE_ALPHA_TO_COVERAGE) - gl.glSampleCoverage(0.5, gl.GL_TRUE) + gl.glUniform1i(self.window.shader_sampler_location, 0) # controls stuff self.controls = [0, 0, 0] @@ -147,21 +134,10 @@ def __init__(self, **args): self.media_player.next_time = 0 - # GPU command syncs - self.fences = deque() - - # ui stuff - imgui.create_context() - self.impl = create_renderer(self) - self.delta_time = 0 - - def toggle_fullscreen(self): - self.set_fullscreen(not self.fullscreen) - def on_close(self): logging.info("Deleting media player") self.media_player.delete() - for fence in self.fences: + for fence in self.window.fences: gl.glDeleteSync(fence) super().on_close() @@ -190,18 +166,18 @@ def update_f3(self): imgui.WINDOW_NO_NAV ): imgui.text(f""" -{round(1 / self.delta_time)} FPS ({self.world.chunk_update_counter} Chunk Updates) {"inf" if not self.options.VSYNC else "vsync"}{"ao" if self.options.SMOOTH_LIGHTING else ""} +{round(1 / self.window.delta_time)} FPS ({self.world.chunk_update_counter} Chunk Updates) {"inf" if not self.window.options.VSYNC else "vsync"}{"ao" if self.window.options.SMOOTH_LIGHTING else ""} C: {visible_chunk_count} / {chunk_count} pC: {self.world.pending_chunk_update_count} pU: {len(self.world.chunk_building_queue)} aB: {chunk_count} -Client Singleplayer @{round(self.delta_time * 1000)} ms tick {round(1 / self.delta_time)} TPS +Client Singleplayer @{round(self.window.delta_time * 1000)} ms tick {round(1 / self.window.delta_time)} TPS XYZ: ( X: {round(self.player.position[0], 3)} / Y: {round(self.player.position[1], 3)} / Z: {round(self.player.position[2], 3)} ) Block: {self.player.rounded_position[0]} {self.player.rounded_position[1]} {self.player.rounded_position[2]} Chunk: {player_local_pos[0]} {player_local_pos[1]} {player_local_pos[2]} in {player_chunk_pos[0]} {player_chunk_pos[1]} {player_chunk_pos[2]} Light: {max(self.world.get_light(self.player.rounded_position), self.world.get_skylight(self.player.rounded_position))} ({self.world.get_skylight(self.player.rounded_position)} sky, {self.world.get_light(self.player.rounded_position)} block) -{self.system_info} +{self.window.system_info} -Renderer: {"OpenGL 3.3 VAOs" if not self.options.INDIRECT_RENDERING else "OpenGL 4.0 VAOs Indirect"} {"Conditional" if self.options.ADVANCED_OPENGL else ""} +Renderer: {"OpenGL 3.3 VAOs" if not self.window.options.INDIRECT_RENDERING else "OpenGL 4.0 VAOs Indirect"} {"Conditional" if self.window.options.ADVANCED_OPENGL else ""} Buffers: {chunk_count} Vertex Data: {round(quad_count * 28 * ctypes.sizeof(gl.GLfloat) / 1048576, 3)} MiB ({quad_count} Quads) Visible Quads: {visible_quad_count} @@ -209,14 +185,7 @@ def update_f3(self): """) imgui.end() - def update_ui(self): - if self.show_f3: - self.update_f3() - def update(self, delta_time): - """Every tick""" - self.impl.process_inputs() - if not self.media_player.source and len(self.music) > 0: if not self.media_player.standby: self.media_player.standby = True @@ -226,36 +195,132 @@ def update(self, delta_time): self.media_player.queue(random.choice(self.music)) self.media_player.play() - if not self.mouse_captured: + if not self.window.mouse_captured: self.player.input = [0, 0, 0] self.joystick_controller.update_controller() self.player.update(delta_time) self.world.tick(delta_time) - self.delta_time = delta_time + + def update_ui(self): + if self.show_f3: + self.update_f3() def on_draw(self): gl.glEnable(gl.GL_DEPTH_TEST) - self.shader.use() + self.window.shader.use() self.player.update_matrices() - while len(self.fences) > self.options.MAX_CPU_AHEAD_FRAMES: - fence = self.fences.popleft() + while len(self.window.fences) > self.window.options.MAX_CPU_AHEAD_FRAMES: + fence = self.window.fences.popleft() gl.glClientWaitSync(fence, gl.GL_SYNC_FLUSH_COMMANDS_BIT, 2147483647) gl.glDeleteSync(fence) - self.clear() + self.window.clear() self.world.prepare_rendering() self.world.draw() # CPU - GPU Sync - if not self.options.SMOOTH_FPS: + if not self.window.options.SMOOTH_FPS: # self.fences.append(gl.glFenceSync(gl.GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) # Broken in pyglet 2; glFenceSync is missing pass else: gl.glFinish() + + def on_resize(self, width, height): + logging.info(f"Resize {width} * {height}") + gl.glViewport(0, 0, width, height) + + self.player.view_width = width + self.player.view_height = height + + +class MenuScene(Scene): + def __init__(self, window: Window) -> None: + super().__init__(window) + + def update(self, delta_time): + pass + + def update_ui(self): + pass + + def on_draw(self): + pass + + +class Window(pyglet.window.Window): + def __init__(self, **args): + super().__init__(**args) + + # Options + self.options = InternalConfig(options) + + if self.options.INDIRECT_RENDERING and not gl.gl_info.have_version(4, 2): + raise RuntimeError("""Indirect Rendering is not supported on your hardware + This feature is only supported on OpenGL 4.2+, but your driver doesnt seem to support it, + Please disable "INDIRECT_RENDERING" in options.py""") + + self.system_info = f"""Python: {platform.python_implementation()} {platform.python_version()} +System: {platform.machine()} {platform.system()} {platform.release()} {platform.version()} +CPU: {platform.processor()} +Display: {gl.gl_info.get_renderer()} +{gl.gl_info.get_version()}""" + + logging.info(f"System Info: {self.system_info}") + # create shader + + logging.info("Compiling Shaders") + if not self.options.COLORED_LIGHTING: + self.shader = shader.Shader("shaders/alpha_lighting/vert.glsl", "shaders/alpha_lighting/frag.glsl") + else: + self.shader = shader.Shader("shaders/colored_lighting/vert.glsl", "shaders/colored_lighting/frag.glsl") + self.shader_sampler_location = self.shader.find_uniform(b"u_TextureArraySampler") + self.shader.use() + + # set scene + self.scene = GameScene(self) + self.mouse_captured = False + + # enable cool stuff + + gl.glEnable(gl.GL_DEPTH_TEST) + gl.glEnable(gl.GL_CULL_FACE) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + + if self.options.ANTIALIASING: + gl.glEnable(gl.GL_MULTISAMPLE) + gl.glEnable(gl.GL_SAMPLE_ALPHA_TO_COVERAGE) + gl.glSampleCoverage(0.5, gl.GL_TRUE) + + # GPU command syncs + self.fences = deque() + + # ui stuff + imgui.create_context() + self.impl = create_renderer(self) + self.delta_time = 1 + + def toggle_fullscreen(self): + self.set_fullscreen(not self.fullscreen) + + def on_close(self): + self.scene.on_close() + super().on_close() + + def update_ui(self): + self.scene.update_ui() + + def update(self, delta_time): + """Every tick""" + self.impl.process_inputs() + self.delta_time = delta_time + self.scene.update(delta_time) + + def on_draw(self): + self.scene.on_draw() # Handle UI imgui.new_frame() @@ -266,11 +331,7 @@ def on_draw(self): # input functions def on_resize(self, width, height): - logging.info(f"Resize {width} * {height}") - gl.glViewport(0, 0, width, height) - - self.player.view_width = width - self.player.view_height = height + self.scene.on_resize(width, height) class Game: From 2c4ab06e1d2558ffa9a49d5ecc159114044e79fe Mon Sep 17 00:00:00 2001 From: ajh123 Date: Mon, 25 Dec 2023 20:31:04 +0000 Subject: [PATCH 03/14] feat(ui): add main menu --- community/main.py | 135 +++++++++++++++++++++++++++++------ community/texture_manager.py | 12 +++- community/textures/logo.png | Bin 0 -> 26544 bytes 3 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 community/textures/logo.png diff --git a/community/main.py b/community/main.py index 7dafac05..0665356e 100644 --- a/community/main.py +++ b/community/main.py @@ -82,14 +82,24 @@ def __init__(self, window: Window) -> None: logging.info("Creating Texture Array") self.texture_manager = texture_manager.TextureManager(16, 16, 256) + # create shader + + logging.info("Compiling Shaders") + if not self.options.COLORED_LIGHTING: + self.shader = shader.Shader("shaders/alpha_lighting/vert.glsl", "shaders/alpha_lighting/frag.glsl") + else: + self.shader = shader.Shader("shaders/colored_lighting/vert.glsl", "shaders/colored_lighting/frag.glsl") + self.shader_sampler_location = self.shader.find_uniform(b"u_TextureArraySampler") + self.shader.use() + # create world - self.world = world.World(self.window.shader, None, self.texture_manager, self.window.options) + self.world = world.World(self.shader, None, self.texture_manager, self.window.options) # player stuff logging.info("Setting up player & camera") - self.player = player.Player(self.world, self.window.shader, self.window.width, self.window.height) + self.player = player.Player(self.world, self.shader, self.window.width, self.window.height) self.world.player = self.player # pyglet stuff @@ -104,7 +114,7 @@ def __init__(self, window: Window) -> None: gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.world.texture_manager.texture_array) - gl.glUniform1i(self.window.shader_sampler_location, 0) + gl.glUniform1i(self.shader_sampler_location, 0) # controls stuff self.controls = [0, 0, 0] @@ -135,13 +145,12 @@ def __init__(self, window: Window) -> None: self.media_player.next_time = 0 def on_close(self): + super().on_close(self) logging.info("Deleting media player") self.media_player.delete() for fence in self.window.fences: gl.glDeleteSync(fence) - super().on_close() - def update_f3(self): """Update the F3 debug screen content""" imgui.set_next_window_position(5, 5) @@ -161,7 +170,7 @@ def update_f3(self): flags= imgui.WINDOW_NO_DECORATION | imgui.WINDOW_ALWAYS_AUTO_RESIZE | - imgui.WINDOW_NO_SAVED_SETTINGS| + imgui.WINDOW_NO_SAVED_SETTINGS | imgui.WINDOW_NO_FOCUS_ON_APPEARING | imgui.WINDOW_NO_NAV ): @@ -186,6 +195,7 @@ def update_f3(self): imgui.end() def update(self, delta_time): + super().update(delta_time) if not self.media_player.source and len(self.music) > 0: if not self.media_player.standby: self.media_player.standby = True @@ -204,12 +214,14 @@ def update(self, delta_time): self.world.tick(delta_time) def update_ui(self): + super().update_ui() if self.show_f3: self.update_f3() def on_draw(self): + super().on_draw() gl.glEnable(gl.GL_DEPTH_TEST) - self.window.shader.use() + self.shader.use() self.player.update_matrices() while len(self.window.fences) > self.window.options.MAX_CPU_AHEAD_FRAMES: @@ -230,6 +242,7 @@ def on_draw(self): gl.glFinish() def on_resize(self, width, height): + super().on_resize(width, height) logging.info(f"Resize {width} * {height}") gl.glViewport(0, 0, width, height) @@ -240,15 +253,102 @@ def on_resize(self, width, height): class MenuScene(Scene): def __init__(self, window: Window) -> None: super().__init__(window) + self.texture_manager = texture_manager.TextureManager(0, 0, 0) - def update(self, delta_time): - pass + self.icon = self.texture_manager.load_texture("dirt") + self.logo = self.texture_manager.load_texture("logo") + + def on_draw(self): + super().on_draw() + gl.glEnable(gl.GL_TEXTURE_2D) + self.window.clear() def update_ui(self): - pass + super().update_ui() + io = imgui.get_io() + imgui.set_next_window_size(io.display_size.x, io.display_size.y) + imgui.set_next_window_position(0, 0) + imgui.push_style_var(imgui.STYLE_WINDOW_BORDERSIZE, 0.0) + imgui.push_style_var(imgui.STYLE_WINDOW_PADDING, (0.0, 0.0)) - def on_draw(self): - pass + if imgui.begin( + "MC PY", + True, + flags= + imgui.WINDOW_NO_DECORATION | + imgui.WINDOW_ALWAYS_AUTO_RESIZE | + imgui.WINDOW_NO_SAVED_SETTINGS | + imgui.WINDOW_NO_NAV + ): + # Get the draw list + draw_list = imgui.get_window_draw_list() + + # Tile the image across the window + for x in range(0, int(io.display_size.x), self.icon[1]): + for y in range(0, int(io.display_size.y), self.icon[2]): + draw_list.add_image(self.icon[0].id, (x, y), (x + self.icon[1], y + self.icon[2])) + + # Draw a semi-transparent overlay to darken the background + overlay_color = imgui.get_color_u32_rgba(0, 0, 0, 0.75) + draw_list.add_rect_filled(0, 0, io.display_size.x, io.display_size.y, overlay_color) + + + # Calculate the position to horizontally center the image + image_width = self.logo[1] / 2.5 + image_height = self.logo[2] / 2.5 + + # Set the cursor position to the calculated center position + imgui.set_cursor_pos(((io.display_size.x - image_width) / 2, 50)) + + # Draw the image at the calculated position + imgui.image(self.logo[0].id, image_width, image_height, (0, 1), (1, 0)) + + # Calculate the position to horizontally center the buttons + button_width = 300 # Adjust button width as needed + button_height = 30 # Adjust button height as needed + button_x = (io.display_size.x - button_width) / 2 + button_y = imgui.get_cursor_pos().y + image_height - 50 + + # Set the cursor position to the calculated center position for buttons + imgui.set_cursor_pos((button_x, button_y)) + + # Add buttons under the logo + if imgui.button("Singleplayer", width=button_width, height=button_height): + # Handle button 1 click + pass + + imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 10)) + + if imgui.button("Multiplayer", width=button_width, height=button_height): + # Handle button 2 click + pass + + imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 10)) + + if imgui.button("Play tutorial level", width=button_width, height=button_height): + # Handle button 2 click + pass + + imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 20)) + + if imgui.button("Options", width=button_width, height=button_height): + # Handle button 2 click + pass + + text = "Copyright MC PY contributors. MC PY is licensed under the MIT." + # Calculate the position to render the text in the bottom right + text_width, text_height = imgui.calc_text_size(text) + text_x = io.display_size.x - text_width + text_y = io.display_size.y - text_height + + # Set the cursor position to the calculated position for the text + imgui.set_cursor_pos((text_x, text_y)) + + # Render the text in the bottom right + imgui.text(text) + imgui.end() + imgui.pop_style_var() + imgui.pop_style_var() class Window(pyglet.window.Window): @@ -270,18 +370,9 @@ def __init__(self, **args): {gl.gl_info.get_version()}""" logging.info(f"System Info: {self.system_info}") - # create shader - - logging.info("Compiling Shaders") - if not self.options.COLORED_LIGHTING: - self.shader = shader.Shader("shaders/alpha_lighting/vert.glsl", "shaders/alpha_lighting/frag.glsl") - else: - self.shader = shader.Shader("shaders/colored_lighting/vert.glsl", "shaders/colored_lighting/frag.glsl") - self.shader_sampler_location = self.shader.find_uniform(b"u_TextureArraySampler") - self.shader.use() # set scene - self.scene = GameScene(self) + self.scene = MenuScene(self) self.mouse_captured = False # enable cool stuff diff --git a/community/texture_manager.py b/community/texture_manager.py index 5088997b..6b4000d0 100644 --- a/community/texture_manager.py +++ b/community/texture_manager.py @@ -1,3 +1,4 @@ +import ctypes import options import pyglet import logging @@ -43,4 +44,13 @@ def add_texture(self, texture): 0, 0, self.textures.index(texture), self.texture_width, self.texture_height, 1, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, - texture_image.get_data("RGBA", texture_image.width * 4)) \ No newline at end of file + texture_image.get_data("RGBA", texture_image.width * 4)) + + def load_texture(self, texture): + logging.debug(f"Loading texture textures/{texture}.png") + + image = pyglet.image.load(f"textures/{texture}.png") + width = image.width + height = image.height + + return image.get_texture(), width, height diff --git a/community/textures/logo.png b/community/textures/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..82a764547f0a7e2c0cfc3b2c57baf1f6f3379d6a GIT binary patch literal 26544 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 literal 0 HcmV?d00001 From 83c9927674fc2feb9110a426e2feddba7e7e23b4 Mon Sep 17 00:00:00 2001 From: ajh123 Date: Tue, 26 Dec 2023 16:26:52 +0000 Subject: [PATCH 04/14] docs: update readme for community --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 8e392052..2655fbd8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,19 @@ It more generally extends the project with functionality I've yet to cover in a Characteristic contributions are contributions which *add* something to the code. Contributions which *fix* something are still merged on the source of all episodes. +The community version requires some additional dependencies, below is a list of them all: + +- `pyglet` +- `nbtlib` +- `base36` +- `pyglm` +- `numpy` +- `imgui[pyglet]` + +These can be installed with the standard pip. + The community has several features and options that can be toggled in `options.py`: + - Render Distance: At what distance (in chunks) should chunks stop being rendered - FOV: Camera field of view @@ -59,6 +71,7 @@ The community has several features and options that can be toggled in `options.p - Mipmap (minification filtering): Texture filtering used on higher distances. Default is `GL_NEAREST` (no filtering) (more info in `options.py`) - Colored lighting: Uses an alternative shader program to achieve a more colored lighting; it aims to look similar to Beta 1.8+ (no performance loss should be incurred) - Antialiasing: Experimental feature +- A main menu / UI system: Uses PyImGUI to create a menu system which appears when the game loads instead of loading the world straight away. ## List of projects based on this From 8a78eeabdeb192d10c786fd31acc290190e0d42d Mon Sep 17 00:00:00 2001 From: ajh123 Date: Tue, 26 Dec 2023 16:29:21 +0000 Subject: [PATCH 05/14] docs: fix markdownlint warnings --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2655fbd8..2c57e021 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # MCPY Source code for each episode of my Minecraft clone in Python YouTube tutorial series. @@ -75,9 +76,9 @@ The community has several features and options that can be toggled in `options.p ## List of projects based on this -- **Nim implementation:** https://github.com/phargobikcin/nim-minecraft-clone -- **Java implementation:** https://github.com/StartForKillerMC/JavaMinecraft -- **C++ implementation:** https://github.com/Jukitsu/CppMinecraft-clone -- **Odin implementation:** https://github.com/anthony-63/lvo -- **Lua implementation:** https://github.com/brennop/lunarcraft -- **Javascript implementation:** https://github.com/drakeerv/js-minecraft-clone ([Demo](https://drakeerv.github.io/js-minecraft-clone/)) +- **Nim implementation:** +- **Java implementation:** +- **C++ implementation:** +- **Odin implementation:** +- **Lua implementation:** +- **Javascript implementation:** ([Demo](https://drakeerv.github.io/js-minecraft-clone/)) From f3f5cc8e3ba9a5eef7d80a5ad6a652deaa6754ec Mon Sep 17 00:00:00 2001 From: ajh123 Date: Tue, 26 Dec 2023 16:31:22 +0000 Subject: [PATCH 06/14] fix(docs): correct disabled rules --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c57e021..77994be4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # MCPY Source code for each episode of my Minecraft clone in Python YouTube tutorial series. From 5e46afd25461c85e22b5c63d23e1e53d7e508e8a Mon Sep 17 00:00:00 2001 From: ajh123 Date: Tue, 26 Dec 2023 16:40:23 +0000 Subject: [PATCH 07/14] refactor(requirements): replace start.bat with requirements.txt + update readme --- README.md | 13 ++----------- community/requirements.txt | 6 ++++++ community/start.bat | 7 ------- requirements.txt | 5 +++++ 4 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 community/requirements.txt delete mode 100644 community/start.bat create mode 100644 requirements.txt diff --git a/README.md b/README.md index 77994be4..202a90ad 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ And for Debian-based Linux distros: The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11, and the `pyglm` module is necessary for the `community` directory. You can install them with PIP by issuing: ```console -pip install --user pyglet nbtlib base36 pyglm +pip install --user -r requirements.txt`. ``` ## Running @@ -44,16 +44,7 @@ It more generally extends the project with functionality I've yet to cover in a Characteristic contributions are contributions which *add* something to the code. Contributions which *fix* something are still merged on the source of all episodes. -The community version requires some additional dependencies, below is a list of them all: - -- `pyglet` -- `nbtlib` -- `base36` -- `pyglm` -- `numpy` -- `imgui[pyglet]` - -These can be installed with the standard pip. +The community version requires some additional dependencies, these can be installed with `pip install --user -r community/requirements.txt`. The community has several features and options that can be toggled in `options.py`: diff --git a/community/requirements.txt b/community/requirements.txt new file mode 100644 index 00000000..8cf946e3 --- /dev/null +++ b/community/requirements.txt @@ -0,0 +1,6 @@ +pyglet >= 2.0.10 +nbtlib >= 2.0.4 +base36 >= 0.1.1 +pyglm >= 2.7.1 +numpy >= 1.26.2 +imgui[pyglet] >= 2.0.0 diff --git a/community/start.bat b/community/start.bat deleted file mode 100644 index 2024b1ca..00000000 --- a/community/start.bat +++ /dev/null @@ -1,7 +0,0 @@ -py -m pip install --upgrade pyglet -py -m pip install --upgrade nbtlib -py -m pip install --upgrade base36 -py -m pip install --upgrade pyglm -py -m pip install --upgrade numpy -py -m pip install --upgrade imgui[pyglet] -py main.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e17955f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pyglet >= 2.0.10 +nbtlib >= 2.0.4 +base36 >= 0.1.1 +pyglm >= 2.7.1 +numpy >= 1.26.2 From 5dd88ddfb2a2c5d786d8ba60a56ad2abe2f6a781 Mon Sep 17 00:00:00 2001 From: ajh123 Date: Tue, 26 Dec 2023 16:43:45 +0000 Subject: [PATCH 08/14] refactor(docs): make dependencies more clearer in readme --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 202a90ad..72e85470 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,20 @@ And for Debian-based Linux distros: [Setup: Linux](https://youtu.be/TtkTkfwwefA?list=PL6_bLxRDFzoKjaa3qCGkwR5L_ouSreaVP) -The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11, and the `pyglm` module is necessary for the `community` directory. You can install them with PIP by issuing: +The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11, the `pyglm` and PyImGUI modules are necessary for the `community` directory. You can install them with PIP by issuing: + +- Without community additions: ```console pip install --user -r requirements.txt`. ``` +- With community additions: + +```console +pip install --user -r community/requirements.txt`. +``` + ## Running Run the following command in the directory of any episode to run the result from that episode: @@ -44,8 +52,6 @@ It more generally extends the project with functionality I've yet to cover in a Characteristic contributions are contributions which *add* something to the code. Contributions which *fix* something are still merged on the source of all episodes. -The community version requires some additional dependencies, these can be installed with `pip install --user -r community/requirements.txt`. - The community has several features and options that can be toggled in `options.py`: - Render Distance: At what distance (in chunks) should chunks stop being rendered From 25ab8f52c798a9305e869e777d63ac1b4b85516e Mon Sep 17 00:00:00 2001 From: Samuel Hulme <41990982+ajh123@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:27:13 +0000 Subject: [PATCH 09/14] refactor(docs): include @obiwac's suggesion for readme Co-authored-by: Aymeric Wibo --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72e85470..5f349fc5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ And for Debian-based Linux distros: [Setup: Linux](https://youtu.be/TtkTkfwwefA?list=PL6_bLxRDFzoKjaa3qCGkwR5L_ouSreaVP) -The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11, the `pyglm` and PyImGUI modules are necessary for the `community` directory. You can install them with PIP by issuing: +The `pyglet` module is a necessary dependency for all episodes, the `nbtlib` & `base36` modules are necessary dependencies for all episodes starting with 11. + +The `pyglm` and PyImGUI modules are necessary for the `community` directory. You can install them with PIP by issuing: - Without community additions: From 2f3a6118bebc0b6ded33b221f669e79d464569e9 Mon Sep 17 00:00:00 2001 From: Samuel Hulme <41990982+ajh123@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:29:08 +0000 Subject: [PATCH 10/14] refactor(docs): include @obiwac's suggesion for requreiments Co-authored-by: Aymeric Wibo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f349fc5..8c3c573d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The `pyglm` and PyImGUI modules are necessary for the `community` directory. You - Without community additions: ```console -pip install --user -r requirements.txt`. +pip install --user pyglet nbtlib base36 pyglm ``` - With community additions: From 2e489a9947f2cbab56f97086647c516090113ba4 Mon Sep 17 00:00:00 2001 From: Samuel Hulme <41990982+ajh123@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:30:29 +0000 Subject: [PATCH 11/14] refactor(docs): include @obiwac's suggesion for requreiments Co-authored-by: Aymeric Wibo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c3c573d..692aad28 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ pip install --user pyglet nbtlib base36 pyglm - With community additions: ```console -pip install --user -r community/requirements.txt`. +pip install --user -r community/requirements.txt ``` ## Running From de26b9d59a9604287380ab74ed1d0156f4a81dcd Mon Sep 17 00:00:00 2001 From: ajh123 Date: Wed, 27 Dec 2023 09:37:02 +0000 Subject: [PATCH 12/14] refactor: use MCPY instead of MC PY --- community/main.py | 4 ++-- community/textures/logo.png | Bin 26544 -> 23300 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/community/main.py b/community/main.py index 0665356e..0e0dd6b6 100644 --- a/community/main.py +++ b/community/main.py @@ -272,7 +272,7 @@ def update_ui(self): imgui.push_style_var(imgui.STYLE_WINDOW_PADDING, (0.0, 0.0)) if imgui.begin( - "MC PY", + "MCPY", True, flags= imgui.WINDOW_NO_DECORATION | @@ -335,7 +335,7 @@ def update_ui(self): # Handle button 2 click pass - text = "Copyright MC PY contributors. MC PY is licensed under the MIT." + text = "Copyright MCPY contributors. MCPY is licensed under the MIT." # Calculate the position to render the text in the bottom right text_width, text_height = imgui.calc_text_size(text) text_x = io.display_size.x - text_width diff --git a/community/textures/logo.png b/community/textures/logo.png index 82a764547f0a7e2c0cfc3b2c57baf1f6f3379d6a..176f3b58acaef09b49ca615794a2f43663939097 100644 GIT binary patch literal 23300 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 I8usD;1J}n)#Q*>R literal 26544 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 From a97698f60d47c2b9ad4ef1f3ad2a60f702e044c8 Mon Sep 17 00:00:00 2001 From: ajh123 Date: Wed, 27 Dec 2023 11:50:48 +0000 Subject: [PATCH 13/14] feat(menu): load world with tutorial level button --- community/main.py | 48 +++++++++++++++++++++++++++++++++------------- community/save.py | 2 +- community/world.py | 4 ++-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/community/main.py b/community/main.py index 0e0dd6b6..0164a665 100644 --- a/community/main.py +++ b/community/main.py @@ -70,7 +70,7 @@ def update_ui(self): class GameScene(Scene): - def __init__(self, window: Window) -> None: + def __init__(self, window: Window, path) -> None: super().__init__(window) logging.info("Loading game scene") @@ -85,7 +85,7 @@ def __init__(self, window: Window) -> None: # create shader logging.info("Compiling Shaders") - if not self.options.COLORED_LIGHTING: + if not self.window.options.COLORED_LIGHTING: self.shader = shader.Shader("shaders/alpha_lighting/vert.glsl", "shaders/alpha_lighting/frag.glsl") else: self.shader = shader.Shader("shaders/colored_lighting/vert.glsl", "shaders/colored_lighting/frag.glsl") @@ -94,7 +94,7 @@ def __init__(self, window: Window) -> None: # create world - self.world = world.World(self.shader, None, self.texture_manager, self.window.options) + self.world = world.World(path, self.shader, None, self.texture_manager, self.window.options) # player stuff @@ -104,7 +104,6 @@ def __init__(self, window: Window) -> None: # pyglet stuff pyglet.clock.schedule(self.player.update_interpolation) - pyglet.clock.schedule_interval(self.update, 1 / 60) # misc stuff @@ -145,7 +144,7 @@ def __init__(self, window: Window) -> None: self.media_player.next_time = 0 def on_close(self): - super().on_close(self) + super().on_close() logging.info("Deleting media player") self.media_player.delete() for fence in self.window.fences: @@ -326,8 +325,8 @@ def update_ui(self): imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 10)) if imgui.button("Play tutorial level", width=button_width, height=button_height): - # Handle button 2 click - pass + scene = GameScene(self.window, "save") + self.window.push_scene(scene) imgui.set_cursor_pos((button_x, imgui.get_cursor_pos().y + 20)) @@ -372,7 +371,9 @@ def __init__(self, **args): logging.info(f"System Info: {self.system_info}") # set scene - self.scene = MenuScene(self) + self.scenes = [MenuScene(self)] + self.current_scene = self.scenes[0] + self.go_next_scene = False self.mouse_captured = False # enable cool stuff @@ -393,25 +394,27 @@ def __init__(self, **args): imgui.create_context() self.impl = create_renderer(self) self.delta_time = 1 + pyglet.clock.schedule_interval(self.update, 1 / 60) def toggle_fullscreen(self): self.set_fullscreen(not self.fullscreen) def on_close(self): - self.scene.on_close() + self.current_scene.on_close() super().on_close() def update_ui(self): - self.scene.update_ui() + self.current_scene.update_ui() def update(self, delta_time): """Every tick""" self.impl.process_inputs() self.delta_time = delta_time - self.scene.update(delta_time) + self.current_scene.update(delta_time) + self.try_next_scene() def on_draw(self): - self.scene.on_draw() + self.current_scene.on_draw() # Handle UI imgui.new_frame() @@ -422,7 +425,26 @@ def on_draw(self): # input functions def on_resize(self, width, height): - self.scene.on_resize(width, height) + self.current_scene.on_resize(width, height) + + # scene functions + + def push_scene(self, scene): + self.scenes.append(scene) + self.go_next_scene = True + + def pop_scene(self): + return self.scenes.pop(len(self.scenes) - 1) + + def try_next_scene(self): + if self.go_next_scene: + try: + scene = self.scenes[len(self.scenes) - 1] + if scene != self.current_scene: + self.current_scene = self.pop_scene() + self.go_next_scene = False + except IndexError: + pass # do nothing because there are no other scenes to switch to. class Game: diff --git a/community/save.py b/community/save.py index 3483accb..cb4c301f 100644 --- a/community/save.py +++ b/community/save.py @@ -6,7 +6,7 @@ import glm class Save: - def __init__(self, world, path = "save"): + def __init__(self, world, path): self.world = world self.path = path diff --git a/community/world.py b/community/world.py index f9b2b8f1..e8659b9a 100644 --- a/community/world.py +++ b/community/world.py @@ -35,7 +35,7 @@ def get_local_position(position): class World: - def __init__(self, shader, player, texture_manager, options): + def __init__(self, path, shader, player, texture_manager, options): self.options = options self.shader = shader self.player = player @@ -134,7 +134,7 @@ def __init__(self, shader, player, texture_manager, options): # load the world - self.save = save.Save(self) + self.save = save.Save(self, path) self.chunks = {} self.sorted_chunks = [] From 5c3dc8acb875b13a3add5aebd3a568d60fa96f0c Mon Sep 17 00:00:00 2001 From: ajh123 Date: Wed, 27 Dec 2023 12:13:29 +0000 Subject: [PATCH 14/14] fix: use pyopengl for imgui images --- community/main.py | 16 +++++----- community/requirements.txt | 2 ++ community/texture_manager.py | 61 ++++++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/community/main.py b/community/main.py index 0164a665..810bb840 100644 --- a/community/main.py +++ b/community/main.py @@ -254,8 +254,8 @@ def __init__(self, window: Window) -> None: super().__init__(window) self.texture_manager = texture_manager.TextureManager(0, 0, 0) - self.icon = self.texture_manager.load_texture("dirt") - self.logo = self.texture_manager.load_texture("logo") + self.icon = self.texture_manager.load_texture("dirt", use_pyglet_gl=False) + self.logo = self.texture_manager.load_texture("logo", use_pyglet_gl=False) def on_draw(self): super().on_draw() @@ -283,9 +283,9 @@ def update_ui(self): draw_list = imgui.get_window_draw_list() # Tile the image across the window - for x in range(0, int(io.display_size.x), self.icon[1]): - for y in range(0, int(io.display_size.y), self.icon[2]): - draw_list.add_image(self.icon[0].id, (x, y), (x + self.icon[1], y + self.icon[2])) + for x in range(0, int(io.display_size.x), self.icon.width): + for y in range(0, int(io.display_size.y), self.icon.height): + draw_list.add_image(self.icon.id, (x, y), (x + self.icon.width, y + self.icon.height), self.logo.uv0, self.logo.uv1) # Draw a semi-transparent overlay to darken the background overlay_color = imgui.get_color_u32_rgba(0, 0, 0, 0.75) @@ -293,14 +293,14 @@ def update_ui(self): # Calculate the position to horizontally center the image - image_width = self.logo[1] / 2.5 - image_height = self.logo[2] / 2.5 + image_width = self.logo.width / 2.5 + image_height = self.logo.height / 2.5 # Set the cursor position to the calculated center position imgui.set_cursor_pos(((io.display_size.x - image_width) / 2, 50)) # Draw the image at the calculated position - imgui.image(self.logo[0].id, image_width, image_height, (0, 1), (1, 0)) + imgui.image(self.logo.id, image_width, image_height, self.logo.uv0, self.logo.uv1) # Calculate the position to horizontally center the buttons button_width = 300 # Adjust button width as needed diff --git a/community/requirements.txt b/community/requirements.txt index 8cf946e3..2b49805e 100644 --- a/community/requirements.txt +++ b/community/requirements.txt @@ -4,3 +4,5 @@ base36 >= 0.1.1 pyglm >= 2.7.1 numpy >= 1.26.2 imgui[pyglet] >= 2.0.0 +pillow >= 9.3.0 +PyOpenGL >= 3.1.6 \ No newline at end of file diff --git a/community/texture_manager.py b/community/texture_manager.py index 6b4000d0..f3e6d826 100644 --- a/community/texture_manager.py +++ b/community/texture_manager.py @@ -1,9 +1,20 @@ -import ctypes import options import pyglet import logging import pyglet.gl as gl +from PIL import Image +import OpenGL.GL as pgl + + +class GLImage: + def __init__(self, gl_id, width, height, uv0, uv1) -> None: + self.id = gl_id + self.width = width + self.height = height + self.uv0 = uv0 + self.uv1 = uv1 + class TextureManager: def __init__(self, texture_width, texture_height, max_textures): @@ -46,11 +57,49 @@ def add_texture(self, texture): gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, texture_image.get_data("RGBA", texture_image.width * 4)) - def load_texture(self, texture): + def load_texture(self, texture, use_pyglet_gl = True): logging.debug(f"Loading texture textures/{texture}.png") - image = pyglet.image.load(f"textures/{texture}.png") - width = image.width - height = image.height + if use_pyglet_gl: + image = pyglet.image.load(f"textures/{texture}.png") + width = image.width + height = image.height + + return GLImage(image.get_texture().id, width, height, (0, 1), (1, 0)) + else: + # Load the image using PIL + image = Image.open(f"textures/{texture}.png") + img_data = list(image.getdata()) + + # Create a texture ID + texture_id = pgl.glGenTextures(1) + + # Bind the texture + pgl.glBindTexture(pgl.GL_TEXTURE_2D, texture_id) + + # Set texture parameters + pgl.glTexParameteri(pgl.GL_TEXTURE_2D, pgl.GL_TEXTURE_MIN_FILTER, pgl.GL_LINEAR) + pgl.glTexParameteri(pgl.GL_TEXTURE_2D, pgl.GL_TEXTURE_MAG_FILTER, pgl.GL_LINEAR) + + # Convert image data to bytes + img_bytes = [] + for rgba in img_data: + img_bytes.extend(rgba) + + # Upload the texture data + pgl.glTexImage2D( + pgl.GL_TEXTURE_2D, + 0, + pgl.GL_RGBA, + image.width, + image.height, + 0, + pgl.GL_RGBA, + pgl.GL_UNSIGNED_BYTE, + (pgl.GLubyte * len(img_bytes))(*img_bytes) + ) + + # Unbind the texture + pgl.glBindTexture(pgl.GL_TEXTURE_2D, 0) - return image.get_texture(), width, height + return GLImage(texture_id, image.width, image.height, (0, -1), (1, 0))