From 6f21f9fa87962574796d5cb62a883599f3f6f4d2 Mon Sep 17 00:00:00 2001 From: Alberto Torres Date: Tue, 18 Mar 2025 17:51:01 +0100 Subject: [PATCH] Add support for MSAA, both in the OpenGL context and in any framebuffer. --- src/graphics/framebuffer.nim | 111 +++++++++++++++++++++++++++++++---- src/graphics/render.nim | 10 +++- src/myou_engine.nim | 6 +- src/platform/android.nim | 2 + src/platform/generic.nim | 2 + src/platform/glfm_wrap.nim | 2 + src/platform/glfw_wrap.nim | 3 + src/screen.nim | 32 +++++++++- src/types.nim | 10 +++- 9 files changed, 159 insertions(+), 19 deletions(-) diff --git a/src/graphics/framebuffer.nim b/src/graphics/framebuffer.nim index 3393b37..cc1ca34 100644 --- a/src/graphics/framebuffer.nim +++ b/src/graphics/framebuffer.nim @@ -48,6 +48,7 @@ proc newFramebuffer*(engine: MyouEngine, depth_tex_type: TextureType = Tex2D, layer_count = 1, texture: Texture = nil, + samples = 1, ): Framebuffer proc set_attachments(self: Framebuffer; layer, mipmap_level: int) proc set_texture*(self: Framebuffer, texture: Texture) @@ -57,10 +58,14 @@ proc clear*(self: Framebuffer, color = vec4(0,0,0,0), clear_color = true, clear_ proc disable*(self: Framebuffer) proc draw*(dest: Framebuffer, shader_mat: Material, inputs: seq[Texture], rect = none((int32,int32,int32,int32)), layer = -1, mipmap_level = 0) +proc blit_to*(self: Framebuffer, dest: Framebuffer, + src_rect = none((int32,int32,int32,int32)), + dst_rect = none((int32,int32,int32,int32))) proc get_framebuffer_status_string*(self: Framebuffer): string proc generate_mipmap*(self: Framebuffer, custom_shader: Material = nil) proc destroy*(self: Framebuffer, remove_from_context: bool = true) proc newMainFramebuffer*(engine: MyouEngine): Framebuffer +proc resize*(self: Framebuffer, width, height: int, samples = 0) # End forward declarations import std/strformat @@ -88,6 +93,7 @@ proc newFramebuffer*(engine: MyouEngine, depth_tex_type: TextureType = Tex2D, layer_count = 1, texture: Texture = nil, + samples = 1, ): Framebuffer = assert Tex3D notin [tex_type, depth_tex_type], @@ -104,11 +110,17 @@ proc newFramebuffer*(engine: MyouEngine, self.height = height self.layer_count = layer_count self.format = format + self.depth_format = depth_format self.depth_type = depth_type + self.samples = samples var tex_type = tex_type # self.filters_should_blend = false + if samples > 1: + assert depth_type != DepthTexture, + "You can't use DepthTexture with a multisample framebuffer" + if not depth_only: - if texture == nil: + if texture == nil and samples <= 1: if self.texture != nil: # TODO: rely on destructor instead self.texture.destroy() @@ -137,14 +149,24 @@ proc newFramebuffer*(engine: MyouEngine, glGenFramebuffers(1, fb.addr) self.framebuffer = fb glBindFramebuffer(GL_FRAMEBUFFER, fb) - self.set_attachments(0, 0) + if not depth_only and texture == nil and samples > 1: + glGenRenderbuffers(1, self.color_render_buffer.addr) + glBindRenderbuffer(GL_RENDERBUFFER, self.color_render_buffer) + glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, samples.GLsizei, format.toInternalFormat, width.GLsizei, height.GLsizei) + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.color_render_buffer) if depth_type == DepthRenderBuffer: glGenRenderbuffers(1, rb.addr) - self.render_buffer = rb + self.depth_render_buffer = rb glBindRenderbuffer(GL_RENDERBUFFER, rb) - glRenderbufferStorage(GL_RENDERBUFFER, depth_format.toInternalFormat, width.GLsizei, height.GLsizei) + if samples > 1: + glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, samples.GLsizei, depth_format.toInternalFormat, width.GLsizei, height.GLsizei) + else: + glRenderbufferStorage(GL_RENDERBUFFER, depth_format.toInternalFormat, width.GLsizei, height.GLsizei) glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb) + self.current_mipmap_level = -1 # force attachment with the call below + self.set_attachments(0, 0) self.is_complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE + doAssert self.is_complete, "Error creating framebuffer: " & self.get_framebuffer_status_string self.has_mipmap = false # self.last_viewport = nil if self.texture != nil: @@ -163,17 +185,24 @@ proc set_attachments(self: Framebuffer; layer, mipmap_level: int) = for i,tex in [self.texture, self.depth_texture]: if tex != nil: if tex.tex_type == Tex2DArray: + assert self.samples == 1, "unsupported" assert layer >= 0, "Texture array layer must be specified" glFramebufferTextureLayer(GL_FRAMEBUFFER, attachments[i], tex.storage.tex.GLuint, mipmap_level.GLint, layer.GLint) elif tex.tex_type == TexCube: assert layer >= 0, "Texture cube side must be specified" + assert self.samples == 1, "unsupported" glFramebufferTexture2D(GL_FRAMEBUFFER, attachments[i], (GL_TEXTURE_CUBE_MAP_POSITIVE_X.int + layer).GLenum, tex.storage.tex.GLuint, mipmap_level.GLint) elif self.current_mipmap_level != mipmap_level: - glFramebufferTexture2D(GL_FRAMEBUFFER, attachments[i], - tex.storage.target, tex.storage.tex.GLuint, mipmap_level.GLint) + if self.samples > 1: + glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, attachments[i], + tex.storage.target, tex.storage.tex.GLuint, mipmap_level.GLint, + self.samples.GLsizei) + else: + glFramebufferTexture2D(GL_FRAMEBUFFER, attachments[i], + tex.storage.target, tex.storage.tex.GLuint, mipmap_level.GLint) proc set_texture*(self: Framebuffer, texture: Texture) = if active_buffer == self: @@ -291,14 +320,27 @@ proc draw*(dest: Framebuffer, shader_mat: Material, inputs: seq[Texture], if ubo.name == "EffectShaderInputs": ubo.unbind() +proc blit_to*(self: Framebuffer, dest: Framebuffer, + src_rect = none((int32,int32,int32,int32)), + dst_rect = none((int32,int32,int32,int32))) = + dest.enable() + let (x1, y1, w1, h1) = src_rect.get((0'i32, 0'i32, self.width.int32, self.height.int32)) + let (x2, y2, w2, h2) = dst_rect.get((0'i32, 0'i32, dest.width.int32, dest.height.int32)) + glBindFramebuffer(GL_READ_FRAMEBUFFER, self.framebuffer); + glBlitFramebuffer(x1, y1, x1+w1, y1+h1, x2, y2, x2+w2, y2+h2, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + proc get_framebuffer_status_string*(self: Framebuffer): string = glBindFramebuffer(GL_FRAMEBUFFER, self.framebuffer) let status = glCheckFramebufferStatus(GL_FRAMEBUFFER) result = case status: - of GL_FRAMEBUFFER_COMPLETE: "GL_FRAMEBUFFER_COMPLETE" - of GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT" - of GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS" - of GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" + of GL_FRAMEBUFFER_COMPLETE: "COMPLETE" + of GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: "INCOMPLETE ATTACHMENT" + of GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: "INCOMPLETE DIMENSIONS" + of GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: "INCOMPLETE MISSING ATTACHMENT" + of GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: "INCOMPLETE MULTISAMPLE" + of GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: "INCOMPLETE LAYER TARGETS" else: &"Unknown status {status.GLint}" self.disable() @@ -351,8 +393,10 @@ proc destroy*(self: Framebuffer, remove_from_context: bool = true) = self.texture.destroy() if self.depth_texture != nil: self.depth_texture.destroy() - if self.render_buffer != 0: - glDeleteRenderbuffers(1, self.render_buffer.addr) + if self.color_render_buffer != 0: + glDeleteRenderbuffers(1, self.color_render_buffer.addr) + if self.depth_render_buffer != 0: + glDeleteRenderbuffers(1, self.depth_render_buffer.addr) glDeleteFramebuffers(1, self.framebuffer.addr) if remove_from_context: var index = self.engine.all_framebuffers.find(self) @@ -365,3 +409,46 @@ proc newMainFramebuffer*(engine: MyouEngine): Framebuffer = result.framebuffer = 0 result.is_complete = true result.use_sRGB = not engine.use_glsl_tone_mapping + result.samples = 1 + +proc resize*(self: Framebuffer, width, height: int, samples = 0) = + if self.framebuffer == 0: + # It's the main framebuffer, just update width and height + self.width = width + self.height = height + return + let samples = if samples != 0: samples else: self.samples + if width == self.width and height == self.height and samples == self.samples: + # No change needed + return + + var depth_only = false + var filter = Linear + var tex_type = Tex2D + var depth_filter = Linear + var depth_tex_type = Tex2D + if self.texture != nil: + filter = self.texture.filter + tex_type = self.texture.tex_type + elif self.color_render_buffer == 0: + depth_only = true + if self.depth_texture != nil: + depth_filter = self.depth_texture.filter + depth_tex_type = self.depth_texture.tex_type + + let new_FB = newFramebuffer( + self.engine, + width, height, self.format, + depth_type = self.depth_type, + depth_format = self.depth_format, + depth_only = depth_only, + filter = filter, + tex_type = tex_type, + depth_filter = depth_filter, + depth_tex_type = depth_tex_type, + layer_count = self.layer_count, + samples = samples, + ) + self[] = move new_FB[] + + \ No newline at end of file diff --git a/src/graphics/render.nim b/src/graphics/render.nim index 35df74c..cf9dae1 100644 --- a/src/graphics/render.nim +++ b/src/graphics/render.nim @@ -32,6 +32,7 @@ import ../types import ../platform/gl +import ../postprocessing/effect_shaders import arr_ref import vmath except Quat, quat @@ -231,12 +232,19 @@ proc draw_all*(self: RenderManager) = continue discard screen.platform_switch_screen() screen.pre_draw(screen) + let fb = if screen.framebuffer_multisample != nil: + screen.framebuffer_multisample + else: + screen.framebuffer for viewport in screen.viewports: let scene = viewport.camera.scene if not scene.enabled: continue # TODO: effect chains which contain effects and passes - self.draw_viewport(viewport, viewport.rect_pix, screen.framebuffer, @[0, 1]) + self.draw_viewport(viewport, viewport.rect_pix, fb, @[0, 1]) + if fb != screen.framebuffer: + fb.blit_to screen.framebuffer + screen.post_draw(screen) glUseProgram(0) glBindVertexArray(0) diff --git a/src/myou_engine.nim b/src/myou_engine.nim index 226761d..caf5e1a 100644 --- a/src/myou_engine.nim +++ b/src/myou_engine.nim @@ -65,6 +65,7 @@ proc newMyouEngine*( opengl_es = default_gl_es, glsl_version = "", use_glsl_tone_mapping = true, + context_msaa_samples = 1, ): MyouEngine proc get_builtin_shader_library*(use_cubemap_prefiltering = true): string proc get_builtin_shader_textures*(): Table[string, Texture] @@ -92,6 +93,7 @@ proc newMyouEngine*( opengl_es = default_gl_es, glsl_version = "", use_glsl_tone_mapping = true, + context_msaa_samples = 1, ): MyouEngine = ## Creates a Myou Engine instance. You need to call this before you can use ## the engine. You also need to call `run <#run,MyouEngine>`_ at the end of @@ -137,9 +139,9 @@ proc newMyouEngine*( # this will call result.renderer.initialize() now or later # but first it will ensure a screen can be created when not defined(nimdoc): - init_graphics(result, width, height, title, opengl_version, opengl_es) + init_graphics(result, width, height, title, opengl_version, opengl_es, context_msaa_samples) discard result.newScreen(width, height, title) - + registerBlendLoader(result) proc get_builtin_shader_library*(use_cubemap_prefiltering = true): string = diff --git a/src/platform/android.nim b/src/platform/android.nim index 80a4a90..3d9bbda 100644 --- a/src/platform/android.nim +++ b/src/platform/android.nim @@ -147,7 +147,9 @@ var max_messages = 0 proc init_graphics*(engine1: MyouEngine, width, height: int32, title: string, opengl_version = 330, opengl_es = false, + samples = 1, ) = + assert samples == 1, "Samples != 1 not supported on this platform yet" engine = engine1 diff --git a/src/platform/generic.nim b/src/platform/generic.nim index fd1bf73..4dbe1d9 100644 --- a/src/platform/generic.nim +++ b/src/platform/generic.nim @@ -114,7 +114,9 @@ var max_messages = 0 proc init_graphics*(engine: MyouEngine, width, height: int32, title: string, opengl_version = 330, opengl_es = false, + samples = 1, ) = + assert samples == 1, "Samples != 1 not supported on this platform yet" let major = opengl_version div 100 let minor = opengl_version mod 100 div 10 diff --git a/src/platform/glfm_wrap.nim b/src/platform/glfm_wrap.nim index fc207d7..7f09c72 100644 --- a/src/platform/glfm_wrap.nim +++ b/src/platform/glfm_wrap.nim @@ -227,7 +227,9 @@ var max_messages = 0 proc init_graphics*(engine: MyouEngine, width, height: int32, title: string, opengl_version = 330, opengl_es = false, + samples = 1, ) = + assert samples == 1, "Samples != 1 not supported on this platform yet" assert window != nil let major = opengl_version div 100 diff --git a/src/platform/glfw_wrap.nim b/src/platform/glfw_wrap.nim index 63be00b..36dadb6 100644 --- a/src/platform/glfw_wrap.nim +++ b/src/platform/glfw_wrap.nim @@ -177,6 +177,7 @@ var max_messages = 0 proc init_graphics*(engine: MyouEngine, width, height: int32, title: string, opengl_version = 330, opengl_es = false, + samples = 1, ) = # TODO!! Option to delay this to simulate the situation in mobile platforms @@ -196,6 +197,8 @@ proc init_graphics*(engine: MyouEngine, width, height: int32, title: string, # ignored for ES glfw.windowHint(OPENGL_PROFILE, OPENGL_CORE_PROFILE) glfw.windowHint(OPENGL_FORWARD_COMPAT, 1) + if samples > 1: + glfw.windowHint(SAMPLES.cint, samples.cint) let window = make_window(width, height, title) window.makeContextCurrent() diff --git a/src/screen.nim b/src/screen.nim index 5785e93..90cbff2 100644 --- a/src/screen.nim +++ b/src/screen.nim @@ -41,7 +41,7 @@ import ./types # Forward declarations proc newScreen*(engine: MyouEngine, width, height: int32, title: string): Screen proc newFramebufferScreen*(engine: MyouEngine, fb: Framebuffer): Screen -proc newTextureScreen*(engine: MyouEngine, texture: Texture, depth_type: FramebufferDepthType = DepthRenderBuffer, depth_format = Depth_u24): Screen +proc newTextureScreen*(engine: MyouEngine, texture: Texture, depth_type: FramebufferDepthType = DepthRenderBuffer, depth_format = Depth_u24, samples = 1): Screen proc setTexture*(self: Screen, texture: Texture) proc destroy*(self: Screen) proc resize*(self: Screen, width, height: int32, orientation = self.orientation) @@ -98,7 +98,7 @@ proc newFramebufferScreen*(engine: MyouEngine, fb: Framebuffer): Screen = result.frame_interval = 1 result.display_scale = 1.0 -proc newTextureScreen*(engine: MyouEngine, texture: Texture, depth_type: FramebufferDepthType = DepthRenderBuffer, depth_format = Depth_u24): Screen = +proc newTextureScreen*(engine: MyouEngine, texture: Texture, depth_type: FramebufferDepthType = DepthRenderBuffer, depth_format = Depth_u24, samples = 1): Screen = ## Creates a new virtual screen from a texture to render to it. # TODO: check that it's not a compressed texture @@ -109,6 +109,7 @@ proc newTextureScreen*(engine: MyouEngine, texture: Texture, depth_type: Frameb texture = texture, depth_type = depth_type, depth_format = depth_format, + samples = samples, ) proc setTexture*(self: Screen, texture: Texture) = @@ -133,6 +134,10 @@ proc resize*(self: Screen, width, height: int32, orientation = self.orientation) self.width = width self.height = height self.orientation = orientation + if self.framebuffer.nonNil: + self.framebuffer.resize(width, height) + if self.framebuffer_multisample.nonNil: + self.framebuffer_multisample.resize(width, height) for vp in self.viewports: let (x,y,w,h) = vp.rect # TODO: verify this doesn't leave gaps/overlaps @@ -292,3 +297,26 @@ proc emulateMouseWithTouch*(screen: Screen, touch: int32, ending: bool, x, y: fl proc switch*(self: Screen): bool {.discardable.} = self.platform_switch_screen() +proc set_MSAA_samples*(self: Screen, samples: int) = + # TODO: remove or add depth of main buffer + if samples == 1: + # if we're not using MSAA or scaling, we don't need it + if self.framebuffer_multisample != nil: + self.framebuffer_multisample.destroy() + self.framebuffer_multisample = nil + else: + # var max_samples: GLint + # glGetIntegerv(GL_MAX_SAMPLES, addr max_samples) + # let samples = min(samples, max_samples) + if self.framebuffer_multisample == nil: + # TODO: configurable color and depth formats + self.framebuffer_multisample = self.engine.newFramebuffer( + self.width, self.height, + format = RGBA_u8, + depth_type = DepthRenderBuffer, + depth_format = Depth_u24, + samples = samples, + ) + else: + self.framebuffer_multisample.resize(self.width, self.height, samples) + diff --git a/src/types.nim b/src/types.nim index 261e78c..b805ba0 100644 --- a/src/types.nim +++ b/src/types.nim @@ -682,7 +682,7 @@ type Framebuffer* = ref object of RootObj engine* {.cursor.}: MyouEngine width*, height*, layer_count*: int - format*: TextureFormat + format*, depth_format*: TextureFormat depth_type*: FramebufferDepthType texture*, depth_texture*: Texture is_complete*: bool @@ -690,8 +690,10 @@ type current_width*, current_height*: int current_mipmap_level*: int use_sRGB*: bool + samples*: int # API objects - framebuffer*, render_buffer*: GLuint + framebuffer*: GLuint ## private + color_render_buffer*, depth_render_buffer*: GLuint ## private TexturePixels* = object pixels*: ArrRef[float32] @@ -913,6 +915,10 @@ type frame_inset*: FrameInset display_scale*: float + # this is here temporarily, + # we want it in the post processing stack instead + framebuffer_multisample*: Framebuffer + PlatformEvent* = enum PlatformPause PlatformResume