Add support for MSAA, both in the OpenGL context and in any framebuffer.

This commit is contained in:
Alberto Torres 2025-03-18 17:51:01 +01:00
parent a38138582e
commit 6f21f9fa87
9 changed files with 159 additions and 19 deletions

View file

@ -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[]

View file

@ -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)

View file

@ -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 =

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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