myou-engine/src/graphics/render.nim
Alberto Torres cea7df6947 First commit.
* Incomplete port of myou-engine-js to nimskull, after many months of work, and
  a few extra features that weren't exactly necessary for a "first commit" to
  work. Excuse the lack of commit history up to this point.
* Bare bones structure of the documentation and the process to update it.
* Restructure of the whole project to have a more sensible organization.
* Making submodules of forks of larger libraries.
* README, licenses, AUTHORS.md.
2024-08-20 13:08:19 +02:00

620 lines
25 KiB
Nim

# 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"