# 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 ../platform/gl import arr_ref import vmath except Quat # Forward declarations func newRenderCameraData*(world_matrix, proj_matrix: Mat4, cull_planes: array[6, Vec4], viewport_size: Vec2): RenderCameraData func updateCullPlanes*(self: var RenderCameraData, cull_planes: array[6, Vec4]) proc newRenderManager*(engine: MyouEngine): RenderManager proc initialize*(self: RenderManager) proc uninitialize*(self: RenderManager) proc set_premultiplied_alpha*(use_premultipied: bool) proc draw_all*(self: RenderManager) proc draw_mesh*(self: RenderManager, mesh: Mesh, mesh2world: Mat4, cam_data: RenderCameraData, pass: int = -1, material_override: Material = nil) proc draw_background*(self: RenderManager, scene: Scene, cam_data: RenderCameraData) proc draw_quad*(self: RenderManager, material: Material, scene: Scene, cam_data: RenderCameraData) proc draw_viewport*(self: RenderManager, viewport: Viewport, rect: (int32,int32,int32,int32), dest_buffer: Framebuffer, passes: seq[int]) proc draw_cubemap*(self: RenderManager, scene: Scene, cubemap_fb: Framebuffer, cube2world, world2cube: Mat4, near, far: float32, background_only: bool) proc get_render_uniform_blocks*(): string # End forward declarations import elvis import std/algorithm import std/options import std/strformat import std/strutils import std/tables import ../math_utils/g3 import ../myou_engine import ../objects/cubemap_probe import ../objects/gameobject # import ../objects/light import ../objects/mesh import ../platform/platform import ../quat import ../scene import ../util import ./framebuffer import ./material import ./texture import ./ubo func newRenderCameraData*(world_matrix, proj_matrix: Mat4, cull_planes: array[6, Vec4], viewport_size: Vec2): RenderCameraData = result.cam2world = world_matrix.remove_scale_skew result.world2cam = result.cam2world.inverse result.projection_matrix = proj_matrix # TODO: should we get this as argument? Camera has it result.projection_matrix_inverse = proj_matrix.inverse result.clipping_plane = vec4(0, 0, -1, 0) result.updateCullPlanes(cull_planes) result.viewport_size = viewport_size result.viewport_size_inv = 1'f32/viewport_size func updateCullPlanes*(self: var RenderCameraData, cull_planes: array[6, Vec4]) = # TODO: check if this works just by multiplying the planes w matrix var p4 = vec4() var n4 = vec4() for i, plane in cull_planes: n4 = plane n4.w = 0 p4 = n4 * -plane.w p4.w = 1 p4 = self.cam2world * p4 n4 = self.cam2world * n4 self.cull_planes[i] = plane_from_norm_point(n4.xyz, p4.xyz) proc newRenderManager*(engine: MyouEngine): RenderManager = result = new RenderManager result.engine = engine result.use_frustum_culling = true result.use_sort_faces = true result.use_debug_draw = true result.max_buffer_size_upload = int.high result.cull_face_enabled = true # result.active_texture = -1 result.effect_ratio = 1 result.num_views = 1 proc initialize*(self: RenderManager) = glGetIntegerv(GL_MAX_TEXTURE_SIZE, addr self.max_texture_size.GLint) glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, addr self.max_textures.GLint) glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, addr self.max_uniform_block_size.GLint) glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, addr self.max_uniform_buffer_bindings.GLint) self.max_textures = min(self.max_textures, HARDCODED_MAXTEXTURES) self.max_uniform_buffer_bindings = min(self.max_uniform_buffer_bindings, HARDCODED_MAXUBOS) # TODO: is this fine? setMaxTextures self.max_textures self.bound_ubos.setLen self.max_uniform_buffer_bindings self.next_ubo = 0 # TODO: detect float texture/framebuffer supportglClearDepthf(1) glEnable(GL_DEPTH_TEST) glDepthFunc(GL_LEQUAL) glEnable(GL_CULL_FACE) glCullFace(GL_BACK) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) self.bg_mesh = newMesh(self.engine, "_bg_mesh", common_attributes={vertex}) self.bg_mesh.load_from_va_ia(@[-1'f32, -1, -1, 3, -1, -1, -1, 3, -1]) self.bg_mesh.radius = Inf self.bg_mesh.materials = @[nil.Material] self.no_material = self.engine.newSolidMaterial("_no_material", vec4(1,0,1,1)); self.camera_render_ubo = self.newUBO("CameraRenderUniform", CameraRenderUniform, 1) # since this is a ref, it won't be destroyed until it's used let zero = newArrRef[uint8](3) zero.fill 0 self.blank_texture = self.engine.newTexture("",1,1,1,RGB_u8,pixels=zero.to float32) self.initialized = true for fun in self.queue: try: fun() except Exception as e: # TODO: use logging for line in e.getStackTrace.split '\n': echo line echo getCurrentExceptionMsg() self.queue.set_len 0 proc uninitialize*(self: RenderManager) = self.initialized = false self.bg_mesh.destroy() self.camera_render_ubo.destroy() proc set_premultiplied_alpha*(use_premultipied: bool) = if use_premultipied: glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) else: glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) template set_cull_face(self: RenderManager, enable: bool) = if self.cull_face_enabled != enable: self.cull_face_enabled = enable if enable: glEnable(GL_CULL_FACE) else: glDisable(GL_CULL_FACE) template set_flip_normals(self: RenderManager, flip: bool) = if self.front_face_is_cw != flip: self.front_face_is_cw = flip if flip: glFrontFace(GL_CW) else: glFrontFace(GL_CCW) proc draw_all*(self: RenderManager) = self.render_tick += 1 self.indices_drawn = 0 self.meshes_drawn = 0 resetNextTextureSlot() self.next_ubo = 0 # calculate all matrices first for screen in self.engine.screens: if not screen.enabled: continue for vp in screen.viewports: let scene = vp.camera.scene if not scene.enabled and scene.last_update_matrices_tick < self.render_tick: continue scene.update_all_matrices() scene.update_lights() # TODO: render probes required by cameras of enabled screens # TODO: render shadows required by cameras of enabled screens for screen in self.engine.screens: if not screen.enabled: continue discard screen.platform_switch_screen() screen.pre_draw(screen) 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]) screen.post_draw(screen) glUseProgram(0) glBindVertexArray(0) proc draw_mesh*(self: RenderManager, mesh: Mesh, mesh2world: Mat4, cam_data: RenderCameraData, pass: int = -1, material_override: Material = nil) = if mesh.sqscale < 0.000001: mesh.culled_in_last_frame = true return if self.use_frustum_culling and mesh.parent.is_armature: # Cull object if it's outside camera frustum let pos4 = mesh.world_center # TODO: USE SCALE let r = -mesh.radius for plane in cam_data.cull_planes: if dot(plane, pos4) < r: mesh.culled_in_last_frame = true return mesh.culled_in_last_frame = false # TODO: Also check with cam_data.clipping_plane! # TODO: Select alternative mesh / LoD var amesh = mesh if not (amesh.data != nil and amesh.data.loaded): return self.set_flip_normals mesh.flip let data = amesh.data # for ubo in self.bound_ubos: # if ubo != nil: # ubo.unbind() # self.next_ubo = 0 # unbindAllTextures() # Main routine for each submesh # (vao may be null but that's handled later) for submesh_idx, vao in data.vaos: if vao == 0: continue if not (pass == -1 or mesh.passes[submesh_idx] == pass): continue var mat = if material_override == nil: amesh.materials.get_or_default(submesh_idx) ?: self.no_material else: material_override let shader = mat.get_shader(mesh) if shader.program == 0: continue shader.use() self.set_cull_face(not mat.double_sided) var ubos_to_bind: seq[UBO] var ubo_indices: seq[GLuint] var ubos_to_update: seq[UBO] let mvm = cam_data.world2cam * mesh2world let nm = mvm.to_normal_matrix() if shader.object_render_ubo_index.is_valid: let ubo = mesh.object_render_ubo ubos_to_bind.add ubo ubo_indices.add shader.object_render_ubo_index ubo.storage(ObjectRenderUniform)[0] = ObjectRenderUniform( model_view_matrix: mvm, model_view_matrix_inverse: mvm.inverse, normal_matrix: [ vec4(nm[0], 0.0), vec4(nm[1], 0.0), vec4(nm[2], 0.0), ], ) ubos_to_update.add ubo # TODO: move this UBO, consolidate with cameradata # TODO: update only in draw_viewport if shader.camera_render_ubo_index.is_valid: let ubo = self.camera_render_ubo ubos_to_bind.add ubo ubo_indices.add shader.camera_render_ubo_index ubo.storage(CameraRenderUniform)[0] = CameraRenderUniform( view_matrix: cam_data.world2cam, view_matrix_inverse: cam_data.cam2world, projection_matrix: cam_data.projection_matrix, projection_matrix_inverse: cam_data.projection_matrix_inverse, viewport_size: cam_data.viewport_size, viewport_size_inv: cam_data.viewport_size_inv, ) ubos_to_update.add ubo if shader.object_ubo_index.is_valid: let ubo = mesh.object_ubo ubos_to_bind.add ubo ubo_indices.add shader.object_ubo_index const diag = vec3(1).normalize ubo.storage(ObjectUniform)[0] = ObjectUniform( model_matrix: mesh2world, model_matrix_inverse: mesh2world.inverse, # TODO: condition for when these two are used? mesh_center: mix(mesh.bound_box[1], mesh.bound_box[0], 0.5), mesh_inv_dimensions: vec3(2) / min(mesh.bound_box[1] - mesh.bound_box[0], vec3(0.000001)), average_world_scale: (mesh.world_matrix * vec4(diag,0)).xyz.length, color: mesh.object_color, ) ubos_to_update.add ubo # UBOs for i,idx in shader.ubo_indices: let ubo = shader.ubos[i] ubos_to_bind.add ubo ubo_indices.add idx ubos_to_bind.bind_all() for i,ubo in ubos_to_bind: ubo.set_prog_index(shader.program, ubo_indices[i]) for ubo in ubos_to_update: ubo.update() var textures_to_bind: seq[Texture] var texture_locations = shader.texture_locations for name, texture in mat.textures: if not defined(release): assert texture != nil, &"texture {name} is nil" # TODO: move this to bind_it/bind_all if texture.loaded and not texture.is_framebuffer_active: textures_to_bind.add texture else: textures_to_bind.add self.blank_texture let scene = mesh.scene if scene != nil: let shadow_maps = scene.shadow_maps for i,loc in shader.shadowmap_locations: if i == shadow_maps.len: break if loc == -1: continue let tex = shadow_maps[i] if tex != nil: textures_to_bind.add tex texture_locations.add loc let cubemaps = scene.cubemaps for i,loc in shader.cubemap_locations: if i == cubemaps.len: break if loc == -1: continue let tex = cubemaps[i].texture textures_to_bind.add tex texture_locations.add loc bind_all(textures_to_bind, texture_locations) let index_buffer = data.index_buffers[submesh_idx] let num_indices = data.num_indices[submesh_idx] glBindVertexArray(vao) if not data.use_tf: if index_buffer != 0: glDrawElements(data.draw_method.GLenum, num_indices.GLsizei, data.index_types[submesh_idx].GLenum, cast[pointer](0)) else: glDrawArrays(data.draw_method.GLenum, 0, num_indices) else: assert index_buffer == 0, "Loopback transform feedback is not compatible with indexed meshes" glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, data.tf_vbos[1][submesh_idx]) glBeginTransformFeedback(data.draw_method.GLenum) glDrawArrays(data.draw_method.GLenum, 0, num_indices) glEndTransformFeedback() # glBindVertexArray(0) self.meshes_drawn += 1 self.indices_drawn += num_indices if data.use_tf: # Swap vaos which include both the regular mesh vao # and each loopback tf (one for reading, the other for writing) glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0) # TODO: check if this swap avoids allocations swap(data.tf_vbos[0], data.tf_vbos[1]) let vaos = data.vaos data.vaos = data.tf_vaos data.tf_vaos = vaos proc draw_background*(self: RenderManager, scene: Scene, cam_data: RenderCameraData) = self.bg_mesh.scene = scene self.draw_mesh(self.bg_mesh, cam_data.cam2world, cam_data, -1, scene.world_material) proc draw_quad*(self: RenderManager, material: Material, scene: Scene, cam_data: RenderCameraData) = self.bg_mesh.scene = scene self.draw_mesh(self.bg_mesh, cam_data.cam2world, cam_data, -1, material) proc draw_viewport*(self: RenderManager, viewport: Viewport, rect: (int32,int32,int32,int32), dest_buffer: Framebuffer, passes: seq[int]) = # Configure camera data let cam = viewport.debug_camera ?: viewport.camera let scene = cam.scene if self.was_right_eye != viewport.is_right_eye: self.was_right_eye = viewport.is_right_eye if scene.on_swap_eye != nil: scene.on_swap_eye(self.was_right_eye) var cd = newRenderCameraData(cam.world_matrix, cam.projection_matrix, cam.cull_planes, vec2(rect[2].float32, rect[3].float32)) var cam_pos = cam.world_matrix[3].xyz if self.show_debug_frustum_culling: let cd2 = newRenderCameraData(viewport.camera.world_matrix, viewport.camera.projection_matrix, cam.cull_planes, cd.viewport_size) cd.cull_planes = cd2.cull_planes cam.world_to_screen_matrix = cam.projection_matrix * cd.world2cam # Main drawing code to destination buffer (usually the screen) dest_buffer.enable(some(rect)) var clear_bits: GLbitfield when defined(myouAlwaysClearColor): clear_bits = GL_COLOR_BUFFER_BIT if viewport.clear_color: clear_bits |= GL_COLOR_BUFFER_BIT if viewport.clear_depth: clear_bits |= GL_DEPTH_BUFFER_BIT var c = scene.background_color if scene.world_material != nil and c.a >= 1: when not defined(myouAlwaysClearColor): # Don't bother clearing color since we'll be rendering over it clear_bits = clear_bits and not GL_COLOR_BUFFER_BIT elif (clear_bits and GL_COLOR_BUFFER_BIT).bool: glClearColor(c.r, c.g, c.b, c.a) if clear_bits.bool: glClear(clear_bits) glDisable(GL_BLEND) glDepthMask(true) if self.use_draw_background_first: # Scene background if scene.world_material != nil and scene.background_color.a > 0.001: if scene.background_color.a < 1: glEnable(GL_BLEND) self.draw_background(scene, cd) if scene.background_color.a < 1: glDisable(GL_BLEND) # # PASS -1 ("always in background" meshes) # if scene.bg_pass.len != 0: # for ob in scene.bg_pass: # if ob.visible == true: # self.draw_mesh(ob, ob.world_matrix, cd, 0) # glClear(GL_DEPTH_BUFFER_BIT) # PASS 0 (opaque) if passes.find(0) >= 0 and scene.mesh_passes[0].len != 0: # Sort by distence to camera # TODO: sort by material first, then reduce precision of depth let z = cd.cam2world[2].xyz for ob in scene.mesh_passes[0]: let v = ob.world_matrix[3].xyz ob.sqdist = -dot(v, z) - (ob.zindex * (ob.dimensions.x + ob.dimensions.y + ob.dimensions.z) * 0.166666) # debugger if ob._sqdist != ob._sqdist scene.mesh_passes[0].sort proc(a, b: auto): auto = return cmp(b.sqdist, a.sqdist) if self.use_sort_faces_opaque: # Sort some meshes, for now just one per frame, with more iterations # for nearby meshes (TODO: Calculate which one has more divergence) # TODO!! Mixed meshes! let num_meshes = scene.mesh_passes[0].len var idx = self.render_tick mod num_meshes if (self.render_tick and 1) == 0: idx = idx shr 1 idx = num_meshes - idx - 1 let ob = scene.mesh_passes[0][idx] ob.sort_sign = FrontToBack # TODO: sort LoD mesh ob.sort_faces(cam_pos) for i, ob in scene.mesh_passes[0]: if ob.visible: # and not ob.bg and not ob.fg self.draw_mesh(ob, ob.world_matrix, cd, 0) if not self.use_draw_background_first: # Scene background if scene.world_material != nil and scene.background_color.a > 0.001: if scene.background_color.a < 1: glEnable(GL_BLEND) self.draw_background(scene, cd) if scene.background_color.a < 1: glDisable(GL_BLEND) if passes.find(1) >= 0 and scene.mesh_passes[1].len != 0: glDepthMask(false) glEnable(GL_BLEND) # Sort by distence to camera for ob in scene.mesh_passes[1]: let v = ob.world_matrix[3].xyz ob.sqdist = dist_sq(-v, cam_pos) - ob.zindex * ob.zindex # TODO: squared dist and zindex? # ob._sqdist = -vec3.dist(v,cam_pos) - (ob.zindex * \ # (ob.dimensions.x+ob.dimensions.y+ob.dimensions.z)*0.166666) scene.mesh_passes[1].sort proc(a, b: auto): auto = return cmp(b.sqdist, a.sqdist) if self.use_sort_faces: # Sort some meshes, for now just one per frame, with more iterations # for nearby meshes (TODO: Calculate which one has more divergence) let num_meshes = scene.mesh_passes[1].len var idx = self.render_tick mod num_meshes if (self.render_tick and 1) == 0: idx = idx shr 1 idx = num_meshes - idx - 1 let ob = scene.mesh_passes[1][idx] # (ob.last_lod[cam_name].?mesh ? ob).sort_faces(cam_pos) ob.sort_faces(cam_pos) for ob in scene.mesh_passes[1]: if ob.visible: self.draw_mesh(ob, ob.world_matrix, cd, 1) glDisable(GL_BLEND) glDepthMask(true) # PASS 2 was here (translucent objects) # by making a copy of the buffer to use as input texture. # If we implement TAA, we may just read the previous opaque frame instead. # For that, the new pass system will nave to support multiple buffers # to alternate between each frame. # # foreground (always-on-top) objects # if scene.fg_pass.len != 0: # glClear(GL_DEPTH_BUFFER_BIT) # var do_blend = false # for ob in scene.fg_pass: # if ob.visible == true: # if ob.passes[0] == 1: # if not do_blend: # glDepthMask(false) # glEnable(GL_BLEND) # do_blend = true # self.draw_mesh(ob, ob.world_matrix, cd, 1) # else: # if do_blend: # glDisable(GL_BLEND) # glDepthMask(true) # do_blend = false # self.draw_mesh(ob, ob.world_matrix, cd, 0) # if do_blend: # glDisable(GL_BLEND) # glDepthMask(true) # FOREGROUND_PLANES (similar to background, but drawn after) if scene.foreground_planes.len != 0: glDepthMask(false) var blending = false for fg_plane in scene.foreground_planes: if blending != fg_plane.is_alpha: if fg_plane.is_alpha: glEnable(GL_BLEND) else: glDisable(GL_BLEND) blending = fg_plane.is_alpha self.draw_quad(fg_plane.material, scene, cd) if blending: glDisable(GL_BLEND) glDepthMask(true) # TODO: reimplement DebugDraw # if self.use_debug_draw and scene.debug_draw != nil: # scene.debug_draw.draw(cam) proc draw_cubemap*(self: RenderManager, scene: Scene, cubemap_fb: Framebuffer, cube2world, world2cube: Mat4, near, far: float32, background_only: bool) = # TODO: # * use frustum culling # * override LoD detection or set a camera for this # * maybe use multidraw assert cubemap_fb.texture.tex_type == TexCube, "Framebuffer must have a cubemap texture" let use_frustum_culling = self.use_frustum_culling self.use_frustum_culling = false self.num_views = 1 var cd: RenderCameraData cd.viewport_size = vec2(cubemap_fb.width.float32) cd.viewport_size_inv = 1'f32/cd.viewport_size let scale = vec3( cube2world[0].xyz.length, cube2world[1].xyz.length, cube2world[2].xyz.length, ) let inv_scale = 1'f32/scale let s_near = near * min(min(inv_scale.x, inv_scale.y), inv_scale.z) # let s_far = far * max(max(inv_scale.x, inv_scale.y), inv_scale.z) let near_box = s_near * scale let world2cube = cube2world.inverse for side in 0 ..< 6: cubemap_fb.enable(layer=side) cd.world2cam = getCubemapSideMatrix(side) cd.cam2world = transpose(cd.world2cam) let near_plane = abs(cd.world2cam * near_box) cd.world2cam = cd.world2cam * world2cube * scale(scale) cd.cam2world = scale(inv_scale) * cube2world * cd.cam2world cd.projection_matrix = frustum( -near_plane.x, near_plane.x, -near_plane.y, near_plane.y, near_plane.z, far) cd.projection_matrix_inverse = inverse(cd.projection_matrix) # cd.updateCullPlanes() let (r,g,b,_) = scene.background_color.toTuple glClearColor(r, g, b, 1) glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) # TODO!!! Disable specular # also increase diffuse to compensate? # TODO: shadows? if not background_only: for ob in scene.mesh_passes[0]: # if ob.probe_cube != nil: # if probe.object == ob and probe.cubemap == cubemap: # continue if ob.visible and ob.data != nil: self.draw_mesh(ob, ob.world_matrix, cd, 0) if scene.world_material != nil: self.draw_background(scene, cd) cubemap_fb.disable() self.use_frustum_culling = use_frustum_culling proc get_render_uniform_blocks*(): string = return staticRead "../shaders/render_ubos.glsl"