# The contents of this file are subject to the Common Public Attribution License # Version 1.0 (the “License”); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla # Public License Version 1.1 but Sections 14 and 15 have been added to cover use # of software over a computer network and provide for limited attribution for # the Original Developer. In addition, Exhibit A has been modified to be # consistent with Exhibit B. # # Software distributed under the License is distributed on an “AS IS” basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is Myou Engine. # # the Original Developer is the Initial Developer. # # The Initial Developer of the Original Code is the Myou Engine developers. # All portions of the code written by the Myou Engine developers are Copyright # (c) 2024. All Rights Reserved. # # Alternatively, the contents of this file may be used under the terms of the # GNU Affero General Public License version 3 (the [AGPL-3] License), in which # case the provisions of [AGPL-3] License are applicable instead of those above. # # If you wish to allow use of your version of this file only under the terms of # the [AGPL-3] License and not to allow others to use your version of this file # under the CPAL, indicate your decision by deleting the provisions above and # replace them with the notice and other provisions required by the [AGPL-3] # License. If you do not delete the provisions above, a recipient may use your # version of this file under either the CPAL or the [AGPL-3] License. import ../types import std/options import std/tables import vmath except Quat # Forward declarations proc newFramebuffer*(engine: MyouEngine, width, height: int, format: TextureFormat, depth_type: FramebufferDepthType = DepthNone, depth_format = Depth_u16, depth_only = false, filter: TextureFilter = Linear, tex_type: TextureType = Tex2D, depth_filter: TextureFilter = Linear, depth_tex_type: TextureType = Tex2D, layer_count = 1, texture: Texture = nil, ): Framebuffer proc set_attachments(self: Framebuffer; layer, mipmap_level: int) proc enable*(self: Framebuffer, rect = none((int32,int32,int32,int32)), layer = -1, mipmap_level = 0, mark_textures = true): Framebuffer {.discardable.} proc clear*(self: Framebuffer, color = vec4(0,0,0,0), clear_color = true, clear_depth = true, layer = -1) {.inline.} 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 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 # End forward declarations import std/strformat import ../platform/gl import ../objects/cubemap_probe import ../postprocessing/effect_shaders import ./render import ./texture import ./ubo var active_buffer: Framebuffer var active_rect: (int32,int32,int32,int32) var active_layer: int proc newFramebuffer*(engine: MyouEngine, width, height: int, format: TextureFormat, depth_type: FramebufferDepthType = DepthNone, depth_format = Depth_u16, depth_only = false, filter: TextureFilter = Linear, tex_type: TextureType = Tex2D, depth_filter: TextureFilter = Linear, depth_tex_type: TextureType = Tex2D, layer_count = 1, texture: Texture = nil, ): Framebuffer = assert Tex3D notin [tex_type, depth_tex_type], "Tex3D framebuffer not supported" assert TexExternal notin [tex_type, depth_tex_type], "TexExternal framebuffer not supported" let self = new Framebuffer # TODO: detect support for float textures and framebuffers in renderer if width == 0 or height == 0: raise newException(ValueError, "Invalid framebuffer size") self.engine = engine self.width = width self.height = height self.layer_count = layer_count self.format = format self.depth_type = depth_type var tex_type = tex_type # self.filters_should_blend = false if not depth_only: if texture == nil: if self.texture != nil: # TODO: rely on destructor instead self.texture.destroy() self.texture = engine.newTexture("fb_tex", width, height, layer_count, format, tex_type=tex_type, filter=filter) # TODO: set as loaded only after having rendered something? self.texture.loaded = true self.texture.setExtrapolation Clamp if self.texture.needsMipmap: self.texture.generateMipmap() else: # Use an existing texture # (to E.G. render to a mipmap of itself without depth buffer) self.texture = texture tex_type = texture.tex_type # TODO: fall back to other texture types when not supported if self.depth_texture != nil: self.depth_texture.destroy() self.depth_texture = nil if depth_type == DepthTexture: self.depth_texture = engine.newTexture("fb_depth", width, height, layer_count, depth_format, tex_type=depth_tex_type, filter=depth_filter) self.depth_texture.loaded = true var fb, rb: GLuint glGenFramebuffers(1, fb.addr) self.framebuffer = fb glBindFramebuffer(GL_FRAMEBUFFER, fb) self.set_attachments(0, 0) if depth_type == DepthRenderBuffer: glGenRenderbuffers(1, rb.addr) self.render_buffer = rb glBindRenderbuffer(GL_RENDERBUFFER, rb) glRenderbufferStorage(GL_RENDERBUFFER, depth_format.toInternalFormat, width.GLsizei, height.GLsizei) glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb) self.is_complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE self.has_mipmap = false # self.last_viewport = nil if self.texture != nil: self.texture.unbind() if self.depth_texture != nil: self.depth_texture.unbind() if rb != 0: glBindRenderbuffer(GL_RENDERBUFFER, 0) glBindFramebuffer(GL_FRAMEBUFFER, 0) active_buffer = nil engine.all_framebuffers.add(self) return self proc set_attachments(self: Framebuffer; layer, mipmap_level: int) = let attachments = [GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT] for i,tex in [self.texture, self.depth_texture]: if tex != nil: if tex.tex_type == Tex2DArray: 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" 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) proc enable*(self: Framebuffer, rect = none((int32,int32,int32,int32)), layer = -1, mipmap_level = 0, mark_textures = true): Framebuffer {.discardable.} = self.has_mipmap = false var left, bottom, width, height = 0'i32 if not rect.isSome: width = self.width.int32 height = self.height.int32 else: (left, bottom, width, height) = rect.get width = width shr mipmap_level height = height shr mipmap_level if active_buffer == self and active_rect == (left,bottom,width,height) and active_layer == layer and self.current_mipmap_level == mipmap_level: return self.current_width = width self.current_height = height if self.texture != nil: self.texture.unbind() if self.depth_texture != nil: self.depth_texture.unbind() glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self.framebuffer) when not defined(opengl_es): if self.use_sRGB: glEnable(GL_FRAMEBUFFER_SRGB) self.set_attachments(layer, mipmap_level) self.current_mipmap_level = mipmap_level active_layer = layer glViewport(left, bottom, width, height) active_rect = (left,bottom,width,height) if active_buffer != nil and active_buffer != self: if active_buffer.texture != nil: active_buffer.texture.is_framebuffer_active = false if active_buffer.depth_texture != nil: active_buffer.depth_texture.is_framebuffer_active = false active_buffer = self if mark_textures and self.texture != nil: self.texture.is_framebuffer_active = true if mark_textures and self.depth_texture != nil: self.depth_texture.is_framebuffer_active = true # filters_should_blend = self.filters_should_blend return self proc clear*(self: Framebuffer, color = vec4(0,0,0,0), clear_color = true, clear_depth = true, layer = -1) {.inline.} = self.enable(layer=layer) var bits: uint32 if clear_color: glClearColor(color.r, color.g, color.b, color.a) bits = GL_COLOR_BUFFER_BIT.uint32 if clear_depth: bits = bits or GL_DEPTH_BUFFER_BIT.uint32 glClear(bits.GLbitfield) proc disable*(self: Framebuffer) = glBindFramebuffer(GL_FRAMEBUFFER, 0) when not defined(opengl_es): if self.use_sRGB: glDisable(GL_FRAMEBUFFER_SRGB) active_buffer = nil active_layer = -1 if self.texture != nil: self.texture.is_framebuffer_active = false if self.depth_texture != nil: self.depth_texture.is_framebuffer_active = false proc draw*(dest: Framebuffer, shader_mat: Material, inputs: seq[Texture], rect = none((int32,int32,int32,int32)), layer = -1, mipmap_level = 0) = glUseProgram(0) dest.enable(rect, layer, mipmap_level, mark_textures=false) var i = 0 for tex in shader_mat.textures.mvalues: if i >= inputs.len: break tex = inputs[i] inc(i) if shader_mat.ubos.len != 0: let ubo = shader_mat.ubos[0] if ubo.name == "EffectShaderInputs": var sto = ubo.storage(EffectShaderInput) var i = 0 for tname,tex in shader_mat.textures: sto[i] = EffectShaderInput( size: tex.vec3_size, px_size: vec3(1)/sto[i].size, px_lod: tex.mipmapHigh.float32, lod: mipmap_level.float32, ) inc(i) ubo.update() # TODO: use framebuffers as inputs and pass the last camera data used? # and the last depth buffer let renderer = dest.engine.renderer glDepthMask(false) glDepthFunc(GL_ALWAYS) let use_frustum_culling = renderer.use_frustum_culling renderer.use_frustum_culling = false var cd = RenderCameraData(world2cam: mat4(), cam2world: mat4(), projection_matrix: mat4()) if layer != -1 and inputs.len != 0 and inputs[0].tex_type == TexCube: cd.world2cam = getCubemapSideMatrix(layer) cd.cam2world = transpose(cd.world2cam) renderer.draw_mesh(renderer.bg_mesh, mat4(), cd, -1, shader_mat) renderer.use_frustum_culling = use_frustum_culling glDepthFunc(GL_LEQUAL) glDepthMask(true) if shader_mat.ubos.len != 0: let ubo = shader_mat.ubos[0] if ubo.name == "EffectShaderInputs": ubo.unbind() 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" else: &"Unknown status {status.GLint}" self.disable() # TODO: move to renderer or to engine var cube_mipmap_shader: Material proc generate_mipmap*(self: Framebuffer, custom_shader: Material = nil) = if self.texture.needsMipmap and not self.has_mipmap: if active_buffer == self: self.disable() var custom_shader = custom_shader if custom_shader == nil and self.texture.tex_type == TexCube: # cube mipmaps seem to be broken, use a custom shader for it if cube_mipmap_shader == nil: cube_mipmap_shader = self.engine.newEffectShader(@["cube"], texture_types = @[TexCube], code = "outColor = vec4(textureLod(cube, coord3D, 0.0).rgb, 1.0);", add_input_size_ubo = false) custom_shader = cube_mipmap_shader if custom_shader == nil: # use OpenGL built-in method self.texture.bind_it() glGenerateMipmap(self.texture.storage.target) self.has_mipmap = true return let filter = self.texture.filter let mipmap_range = self.texture.mipmap_range let layer_range = if self.texture.tex_type == TexCube: 0 ..< 6 else: -1 .. -1 self.texture.setFilter Linear for m in mipmap_range[0] .. mipmap_range[1] - 1: self.texture.setMipmapRange(m, m) for i in layer_range: self.draw(custom_shader, @[self.texture], layer = i, mipmap_level = m+1) self.texture.setFilter filter self.texture.setMipmapRange(mipmap_range[0], mipmap_range[1]) self.has_mipmap = true proc destroy*(self: Framebuffer, remove_from_context: bool = true) = if active_buffer == self: self.disable() if self.texture != nil: self.texture.destroy() if self.depth_texture != nil: self.depth_texture.destroy() if self.render_buffer != 0: glDeleteRenderbuffers(1, self.render_buffer.addr) glDeleteFramebuffers(1, self.framebuffer.addr) if remove_from_context: var index = self.engine.all_framebuffers.find(self) if index != -1: self.engine.all_framebuffers.del(index) proc newMainFramebuffer*(engine: MyouEngine): Framebuffer = result = new Framebuffer result.engine = engine result.framebuffer = 0 result.is_complete = true result.use_sRGB = not engine.use_glsl_tone_mapping