# 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

# Forward declarations
proc initScene*(self: Scene, engine: MyouEngine, name: string = "Scene",
        add_viewport_automatically: bool = true): Scene
proc newScene*(engine: MyouEngine, name: string = "Scene",
        add_viewport_automatically: bool = true): Scene
proc set_ob_name*(self: Scene, ob: GameObject, name: string)
proc add_object*(self: Scene, ob: GameObject,
        name: string = ob.name,
        parent_name: string = "", parent_bone: string = "",
        auto_update_matrix: bool = ob.auto_update_matrix): GameObject {.discardable.}
proc remove_object*(self: Scene, ob: GameObject, recursive: bool = true)
proc make_parent*(self: Scene, parent: GameObject, child: GameObject,
        keep_transform: bool = true)
proc clear_parent*(self: Scene, child: GameObject, keep_transform = true)
proc reorder_children*(self: Scene)
proc update_all_matrices*(self: Scene)
proc set_objects_auto_update_matrix*(self: Scene, objects: seq[GameObject], auto_update: bool)
proc destroy*(self: Scene)
proc new_gameobject*(self: Scene, name: string): GameObject
proc set_active_camera*(self: Scene, camera: Camera)
proc calculate_max_lights_and_cubemaps*(self: Scene)
proc get_lighting_UBOs*(self: Scene): seq[UBO]
proc get_lighting_code_defines*(self: Scene): seq[string]
proc get_lighting_code*(self: Scene): string
proc update_lights*(self: Scene)
proc sort_cubemaps*(self: Scene)
proc ensure_cubemaps*(self: Scene)
proc render_all_cubemaps*(self: Scene, use_roughness_prefiltering: bool, mipmap_shader: Material = nil)
# End forward declarations

import vmath except Quat
import ./quat
import std/algorithm
import std/math
import std/options
import std/sequtils
import std/strformat
import std/strutils
import std/tables
import ./graphics/framebuffer
import ./graphics/material
import ./graphics/texture
import ./graphics/ubo
import ./incomplete
import ./objects/camera
import ./objects/cubemap_probe
import ./objects/gameobject
import ./objects/light
import ./objects/mesh
import ./shadows/shadow_common
import ./screen
import ./util

export tables, options

var collision_seq = 0

proc initScene*(self: Scene, engine: MyouEngine, name: string = "Scene",
        add_viewport_automatically: bool = true): Scene =
    self.engine = engine
    self.name = name
    while self.name in engine.scenes:
        collision_seq += 1
        self.name = name & "$" & $collision_seq
    engine.new_scenes[self.name] = self
    self.mesh_passes.setLen 3
    self.world = newWorld(self)
    self.background_color = vec4(0, 0, 0, 1)
    self.cubemap_resolution = 128
    self.shadow_map_resolution = 1024
    self.max_point_lights = 8
    self.max_sun_lights = 1
    self.max_spot_lights = 0
    self.max_area_lights = 0
    self.children_are_ordered = true
    self.anim_fps = 30
    # self.data_dir = engine.options.data_dir
    # self.texture_dir = $self.data_dir & "/textures/"
    self.add_viewport_automatically = add_viewport_automatically
    return self

proc newScene*(engine: MyouEngine, name: string = "Scene",
        add_viewport_automatically: bool = true): Scene =
    result = new Scene
    return initScene(result, engine, name, add_viewport_automatically)

proc set_ob_name*(self: Scene, ob: GameObject, name: string) =
    var n = name
    while n in self.engine.objects:
        collision_seq += 1
        n = name & "$" & $collision_seq
    ob.name = n
    self.objects[n] = ob
    self.engine.objects[n] = ob

proc add_object*(self: Scene, ob: GameObject,
        name: string = ob.name,
        parent_name: string = "", parent_bone: string = "",
        auto_update_matrix: bool = ob.auto_update_matrix): GameObject {.discardable.} =
    if ob.scene != nil:
        if ob.scene == self:
            return
        else:
            ob.scene.remove_object(ob)
    # TODO: preserve name
    ob.scene = self
    self.children.add(ob)
    if auto_update_matrix:
        self.auto_updated_children.add(ob)
    ob.auto_update_matrix = auto_update_matrix
    # TODO: investigate
    # in andorid hwasan is upset if I don't redeclare name and if
    # there's a collision (problem is when assigning original_name)
    var name = name
    var n = name
    while n in self.engine.objects:
        collision_seq += 1
        n = name & "$" & $collision_seq
    ob.name = n
    ob.original_name = name
    self.objects[n] = ob
    self.engine.objects[n] = ob
    self.parents[name] = ob
    # echo "Added", name
    
    # Objects are always ordered parent-first
    # TODO: this is a mess
    if parent_name != "" and parent_name in self.parents:
        var p = self.parents[parent_name]
        ob.parent = p
        p.children.add(ob)
        var armature = p.get_armature
        if armature.nonNil and parent_bone != "":
            var bone = armature.bones[parent_bone]
            if bone.nonNil:
                ob.parent_bone_index = armature.bone_list.find(bone)
                bone.object_children.add(ob)
    var mesh = ob.get_mesh
    if mesh.nonNil:
        # TODO: not having number of passes hardcoded
        for p in 0'i32 .. 2:
            if p in mesh.passes:
                if "foreground_pass" in mesh.properties:
                    self.fg_pass.add(mesh)
                elif p == 0 and "background_pass" in mesh.properties:
                    self.bg_pass.add(mesh)
                else:
                    self.mesh_passes[p].add(mesh)
    elif ob.is_light:
        self.lights.add(ob.get_light)
    elif ob.is_armature:
        self.armatures.add(ob.get_armature)
    elif ob.is_cubemap_probe:
        self.cubemap_probes.add(ob.get_cubemap_probe)
    if not auto_update_matrix:
        discard ob.get_world_matrix()
    return ob

proc remove_object*(self: Scene, ob: GameObject, recursive: bool = true) =
    self.children.remove ob
    if ob.auto_update_matrix:
        self.auto_updated_children.remove ob
    self.objects.del(ob.name)
    self.parents.del(ob.original_name)
    var mesh = ob.get_mesh
    if mesh.nonNil:
        self.mesh_passes[0].remove mesh
        self.mesh_passes[1].remove mesh
        self.fg_pass.remove mesh
        self.bg_pass.remove mesh
        if mesh.data.nonNil:
            mesh.data.remove(mesh)
    elif ob.is_camera:
        for screen in self.engine.screens:
            var i = screen.viewports.high
            for vp in reversed(screen.viewports):
                if vp.camera.scene == self:
                    screen.viewports.delete(i)
                i -= 1
    elif ob.is_light:
        var light = ob.get_light
        for shadow in light.shadows:
            shadow.destroy()
        self.lights.remove light
    elif ob.is_armature:
        self.armatures.remove ob.get_armature
    if ob.parent_bone_index != -1 and ob.parent.nonNil:
        var ar = ob.parent.get_armature 
        if ar.nonNil and ar.bone_list.len > ob.parent_bone_index:
            ar.bone_list[ob.parent_bone_index].object_children.remove ob
    ob.body.destroy()
    # TODO: Remove probes if they have no users
    # for b in ob.behaviours:
    #     b.unassign(ob)
    if recursive:
        for child in reversed(ob.children):
            var child = child
            self.remove_object(child)
    ob.scene = nil
    return

proc make_parent*(self: Scene, parent: GameObject, child: GameObject,
        keep_transform: bool = true) =
    assert parent != nil and child != nil, "Arguments 'parent' and 'child' can't be nil."
    if child.parent.nonNil:
        self.clear_parent(child, keep_transform)
    # TODO: should we store the index in the objects
    # to make this check faster?
    var parent_index, child_index: int
    if parent.auto_update_matrix and child.auto_update_matrix:
        parent_index = self.auto_updated_children.find(parent)
        child_index = self.auto_updated_children.find(child)
        assert parent_index != -1, &"Object '{parent.name}' is not part of scene '{self.name}'. Both parent and child must belong to it."
        assert child_index != -1, &"Object '{child.name}' is not part of scene '{self.name}'. Both parent and child must belong to it."
    if keep_transform:
        let wm = child.get_world_matrix
        let rotation = parent.get_world_rotation
        let rotation_order = child.rotation_order
        child.set_rotation_order(Quaternion)
        var rot = inverse(rotation) * child.rotation
        let rot_inv = inverse(rot)
        var scale = child.scale
        # get local rotation matrix and scale it with parent world scale vector
        var m3 = scale(parent.get_world_scale_vector()).to_mat3 * rot.to_mat3
        # transform matrix with inverse of (unscaled) rotation matrix
        m3 = rot_inv.to_mat3 * m3
        # use it to get signs
        let scale_x_sign = copy_sign(1.0, m3[0,0])
        let scale_y_sign = copy_sign(1.0, m3[1,1])
        let scale_z_sign = copy_sign(1.0, m3[2,2])
        # scale.x /= FAILS
        scale.x = scale.x / (parent.world_matrix[0].xyz.length * scale_x_sign)
        scale.y = scale.y / (parent.world_matrix[1].xyz.length * scale_y_sign)
        scale.z = scale.z / (parent.world_matrix[2].xyz.length * scale_z_sign)
        child.position = (parent.world_matrix.inverse * wm)[3].xyz
        rot.x *= scale_x_sign
        rot.y *= scale_y_sign
        rot.z *= scale_z_sign
        rot.w *= scale_x_sign * scale_y_sign * scale_z_sign
        child.set_rotation_order(rotation_order)
    child.matrix_parent_inverse = mat4()
    child.parent = parent
    parent.children.add(child)
    # TODO: just move all descendents to the end in the same order?
    if parent.auto_update_matrix and child.auto_update_matrix:
        if parent_index > child_index:
            self.children_are_ordered = false

proc clear_parent*(self: Scene, child: GameObject, keep_transform = true) =
    let parent = child.parent
    if parent != nil:
        if keep_transform:
            let rotation_order = child.rotation_order
            let (position, rotation) = child.get_world_position_rotation
            child.position = position
            child.rotation = rotation
            child.rotation_order = Quaternion
            var scale = child.scale
            let world_matrix = child.world_matrix
            let rot_inv = inverse(rotation)
            var wm3 = rot_inv.to_mat3 * world_matrix.to_mat3
            scale.x = world_matrix[0].xyz.length.copy_sign wm3[0,0]
            scale.y = world_matrix[1].xyz.length.copy_sign wm3[1,1]
            scale.z = world_matrix[2].xyz.length.copy_sign wm3[2,2]
            child.set_rotation_order(rotation_order)
        parent.children.remove child
        child.parent = nil
        child.parent_bone_index = -1
        child.matrix_parent_inverse = mat4()
    return

proc reorder_children*(self: Scene) =
    # TODO: Only the objects marked as unordered need to be resolved here!
    #       (make a new list and append to children)
    self.auto_updated_children.set_len 0
    proc reorder(ob: GameObject) =
        if ob.auto_update_matrix:
            self.auto_updated_children.add ob
        for c in ob.children:
            reorder(c)
    
    for ob in self.children:
        if ob.parent.nonNil:
            continue
        reorder(ob)
    self.children_are_ordered = true

proc update_all_matrices*(self: Scene) =
    if self.children_are_ordered == false:
        self.reorder_children()
    # TODO: do this only for visible and modified objects
    #       (also, this is used in LookAt and other nodes)
    for ob in self.armatures:
        var ob = ob
        # TODO: Be smarter about when this is needed
        # (and) when to draw meshes with armatures too
        ob.recalculate_bone_matrices()
        # for c in ob.children:
        #     if c.visible:
        #         ob.recalculate_bone_matrices()
        #         break
    for ob in self.auto_updated_children:
        ob.update_matrices()

proc set_objects_auto_update_matrix*(self: Scene, objects: seq[GameObject], auto_update: bool) =
    for ob in objects:
        ob.auto_update_matrix = auto_update
    self.children_are_ordered = false

proc destroy*(self: Scene) =
    for ob in reversed(self.children):
        ob.destroy(recursive=false)
    self.world.destroy()
    for mat in self.materials.values:
        mat.destroy()
    if self.shadow_maps != nil:
        self.shadow_maps.destroy()
    for ubo in self.lighting_UBOs:
        ubo.destroy()
    self.engine.scenes.del(self.name)
    # bound textures can linger
    unbindAllTextures()
    # probably none of this is actually necessary
    # (needs testing without this)
    self.children = @[]
    self.auto_updated_children = @[]
    self.objects.clear()
    self.parents.clear()
    self.materials.clear()
    self.textures.clear()
    self.mesh_passes = @[]

proc enable_render*(self: Scene) =
    self.enabled = true

proc enable_physics*(self: Scene) =
    if self.world != nil:
        self.world.enabled = true

proc enable_all*(self: Scene) =
    self.enable_render()
    self.enable_physics()

proc new_gameobject*(self: Scene, name: string): GameObject =
    return self.engine.new_gameobject(name=name, scene=self)

proc new_mesh*(self: Scene, name: string,
        draw_method: MeshDrawMethod = Triangles,
        common_attributes: CommonMeshAttributes = {vertex, color},
        layout: AttributeList = @[],
        # stride: int32 = layout.stride,
        skip_upload: bool = false,
        vertex_count: int32 = 0,
        vertex_array: seq[float32] = @[],
        index_array: seq[uint16] = @[],
        pass: int32 = 0,
    ): Mesh =
    self.engine.new_mesh(name, self, draw_method, common_attributes, layout, 
        skip_upload, vertex_count, vertex_array, index_array, pass)

proc set_active_camera*(self: Scene, camera: Camera) =
    self.active_camera = camera
    if camera.scene != self:
        let name = if camera.name != "": camera.name else: "Camera"
        self.add_object(camera, name)
    if self.add_viewport_automatically:
        if self.engine.screens[0].viewports.len == 0:
            self.engine.screens[0].add_viewport(camera)
    return

proc calculate_max_lights_and_cubemaps*(self: Scene) =
    self.max_point_lights = 0
    self.max_sun_lights = 0
    self.max_spot_lights = 0
    self.max_area_lights = 0
    self.max_shadow_maps = 0
    for ob in self.lights:
        let light = ob.get_light
        case light.light_type:
            of PointLight: self.max_point_lights += 1
            of SunLight: self.max_sun_lights += 1
            of SpotLight: self.max_spot_lights += 1
            of AreaLight: self.max_area_lights += 1
    if self.shadow_maps != nil:
        self.max_shadow_maps = self.shadow_maps.layer_count.int32
    self.max_cubemaps = self.cubemap_probes.len.int32
    if self.world_material != nil:
        self.max_cubemaps += 1
    # force resizing UBOs if needed
    discard self.get_lighting_UBOs()
    # delete all shaders, so they're recreated with correct max
    # TODO: do it only when numbers have changed!
    for _,mat in self.materials:
        if mat.ubos.anyIt it in self.lighting_UBOs:
            mat.delete_all_shaders
    # echo &"calculated max lights {self.max_point_lights} {self.max_sun_lights}"
    # echo &"calculated max shadow maps {self.max_shadow_maps}"
    # echo &"calculated max cubemaps {self.max_cubemaps}"

proc get_lighting_UBOs*(self: Scene): seq[UBO] =
    # echo &"getting UBOs {self.max_point_lights} {self.max_sun_lights}"

    if self.lighting_UBOs.len == 0:
        self.point_light_UBO = newUBO(self.engine.renderer,
            "PointLights", PointLightUniform, self.max_point_lights)
        self.sun_light_UBO = newUBO(self.engine.renderer,
            "SunLights", SunLightUniform, self.max_sun_lights)
        self.shadow_maps_UBO = newUBO(self.engine.renderer,
            "ShadowMapInfos", ShadowMapUniform, self.max_shadow_maps)
        self.cubemap_UBO = newUBO(self.engine.renderer,
            "CubemapInfos", CubemapProbeUniform, self.max_cubemaps)
        self.sh9_UBO = newUBO(self.engine.renderer,
            "SphericalHarmonics", SH9Uniform, self.max_spherical_harmonics)
        self.lighting_UBOs = @[
            self.point_light_UBO,
            self.sun_light_UBO,
            self.shadow_maps_UBO,
            self.cubemap_UBO,
            self.sh9_UBO,
        ]
    else:
        self.point_light_UBO.resize(PointLightUniform, self.max_point_lights)
        self.sun_light_UBO.resize(SunLightUniform, self.max_sun_lights)
        self.shadow_maps_UBO.resize(ShadowMapUniform, self.max_shadow_maps)
        self.cubemap_UBO.resize(CubemapProbeUniform, self.max_cubemaps)
        self.sh9_UBO.resize(SH9Uniform, self.max_spherical_harmonics)
    return self.lighting_UBOs


proc get_lighting_code_defines*(self: Scene): seq[string] =
    @[
        "#define MAX_POINT_LIGHTS " & $(if self.isNil: 0 else: self.max_point_lights),
        "#define MAX_SUN_LIGHTS " & $(if self.isNil: 0 else: self.max_sun_lights),
        "#define MAX_SHADOW_MAPS " & $(if self.isNil: 0 else: self.max_shadow_maps),
        "#define MAX_CUBE_MAPS " & $(if self.isNil: 0 else: self.max_cubemaps),
        "#define MAX_SPHERICAL_HARMONICS " & $(if self.isNil: 0 else: self.max_spherical_harmonics),
        "#define USE_SHADOW_SAMPLERS " & $(USE_SHADOW_SAMPLERS.int),
    ]

proc get_lighting_code*(self: Scene): string =
    var code: seq[string]
    code.add dedent """
        #define light_position pos.xyz
        #define light_radius_sq pos.a
        #define light_diffuse_power powers.r
        #define light_specular_power powers.g
        #define light_inv_squared_cutoff powers.a
        #define light_direction dir.xyz
        #define light_shadow_index color.w
        struct PointLight {
            vec4 pos;
            vec4 color;
            vec4 powers;
        };
        #if MAX_POINT_LIGHTS
        layout(std140) uniform PointLights {
            PointLight point_lights[MAX_POINT_LIGHTS];
        };
        #endif
        struct SunLight {
            vec4 dir;
            vec4 color;
            vec4 powers;
        };
        #if MAX_SUN_LIGHTS
        layout(std140) uniform SunLights {
            SunLight sun_lights[MAX_SUN_LIGHTS];
        };
        #endif
        struct ShadowMapInfo {
            mat4 depth_matrix;
            float size;
            float bias;
            float pad0, pad1;
        };
        #if MAX_SHADOW_MAPS
        layout(std140) uniform ShadowMapInfos {
            ShadowMapInfo shadow_map_infos[MAX_SHADOW_MAPS];
        };
        #if USE_SHADOW_SAMPLERS
        uniform sampler2DArrayShadow shadow_maps;
        #else
        uniform sampler2DArray shadow_maps;
        #endif
        #endif
        struct CubemapInfo {
            mat4 world2cube;
            float resolution;
            float falloff_inv;
            float influence_type;
            float parallax_type;
            float par_dist_rel_inv;
            float intensity;
            float roughness_lod;
            float pad0;
        };
        #if MAX_CUBE_MAPS
        layout(std140) uniform CubemapInfos {
            CubemapInfo cube_map_infos[MAX_CUBE_MAPS];
        };
        uniform samplerCube cube_maps[MAX_CUBE_MAPS];
        #endif
        struct SH9 {
            vec3 c[9];
        };

        #ifndef MAX_SPHERICAL_HARMONICS
        #define MAX_SPHERICAL_HARMONICS 0
        #endif

        #if MAX_SPHERICAL_HARMONICS
        layout(std140) uniform SphericalHarmonics {
            SH9 SH9_coefficients[MAX_SPHERICAL_HARMONICS];
            mat3 SH9_rotation;
            vec4 _sh9_pad;
        };
        #endif
    """
    code.add staticOrDebugRead "shadows/simple_shadow.glsl"
    return code.join "\n"

proc update_lights*(self: Scene) =
    if self.lighting_UBOs.len == 0:
        return
    var point, sun, spot, area = 0
    for ob in self.lights:
        case ob.light_type:
        of PointLight:
            if point < self.max_point_lights:
                let radius = max(ob.light_radius, 0.000001)
                let cutoff = if ob.cutoff_distance != 0:
                    ob.cutoff_distance
                else:
                    sqrt(ob.energy)
                self.point_light_UBO.storage(PointLightUniform)[point] = PointLightUniform(
                    position: ob.get_world_position,
                    radius_squared: radius * radius,
                    color: ob.color.xyz,
                    diffuse_power: ob.energy * ob.diffuse_factor,
                    specular_power: ob.energy * ob.specular_factor,
                    inv_squared_cutoff: 1/(cutoff*cutoff),
                )
            point += 1
        of SunLight:
            if sun < self.max_sun_lights:
                let shadow_index = if ob.shadows.len != 0:
                    # TODO: Allow more than 1 shadow per light
                    ob.shadows[0].shadow_index
                else:
                    -1
                self.sun_light_UBO.storage(SunLightUniform)[sun] = SunLightUniform(
                    direction: ob.get_world_Z_vector.normalize,
                    color: ob.color.xyz,
                    diffuse_power: ob.energy * ob.diffuse_factor,
                    specular_power: ob.energy * ob.specular_factor,
                    shadow_index: shadow_index.float32,
                )
            sun += 1
        of SpotLight:
            spot += 1
        of AreaLight:
            area += 1
    
    # TODO: only upload changed and existing lights

    # TODO: save current number of lights in a uniform

    if point != 0:
        # set energy of remaining lights to 0
        # this is a temporary measure until we implement light froxels
        for i in point ..< self.max_point_lights:
            self.point_light_UBO.storage(PointLightUniform)[i].diffuse_power = 0
            self.point_light_UBO.storage(PointLightUniform)[i].specular_power = 0
        self.point_light_UBO.update()

    if sun != 0:
        for i in sun ..< self.max_sun_lights:
            self.sun_light_UBO.storage(SunLightUniform)[i].diffuse_power = 0
            self.sun_light_UBO.storage(SunLightUniform)[i].specular_power = 0
        self.sun_light_UBO.update()

proc sort_cubemaps*(self: Scene) =
    # sort by volume
    # TODO: avoid this allocation?
    var volumes = newSeqOfCap[(float32, CubemapProbe)](self.cubemap_probes.len)
    for probe in self.cubemap_probes:
        let vol = probe.influence_distance^3 * probe.world_matrix.determinant
        volumes.add (vol, probe)
    proc cmp(x,y: (float32, CubemapProbe)): int =
        (y[0] - x[0]).int
    volumes.sort cmp
    for i, (vol, probe) in volumes:
        self.cubemap_probes[i] = probe

# TODO: have a single cubemap with depth buffer for rendering?
#       or share depth buffers?

proc ensure_cubemaps*(self: Scene) =
    # echo "ensuring cubemaps"
    let res = self.cubemap_resolution
    if res == 0:
        return
    self.cubemaps.setLen 0
    if self.world_material != nil:
        if self.background_cubemap == nil:
            self.background_cubemap = self.engine.newFramebuffer(res, res,
                RGBA_f16,
                DepthNone,
                tex_type=TexCube,
                filter=TriLinear,
            )
            # self.cubemaps.insert(self.background_cubemap, 0)
        self.cubemaps.add(self.background_cubemap)
    for probe in self.cubemap_probes:
        if probe.resolution != res:
            probe.resolution = res
            if probe.cubemap != nil:
                probe.cubemap.destroy()
                probe.cubemap = nil
        if probe.cubemap == nil:
            probe.cubemap = self.engine.newFramebuffer(res, res,
                RGBA_f16,
                DepthRenderBuffer,
                tex_type=TexCube,
                filter=TriLinear,
            )
        probe.ubo_index = self.cubemaps.len.int32
        self.cubemaps.add(probe.cubemap)
    # echo &"made {self.cubemaps.len} cubemaps"

proc render_all_cubemaps*(self: Scene, use_roughness_prefiltering: bool, mipmap_shader: Material = nil) =
    if self.cubemap_UBO == nil:
        return
    if self.world_material != nil:
        self.render_background_cubemap(use_roughness_prefiltering, mipmap_shader, upload_UBO = false)
    self.sort_cubemaps()
    for probe in self.cubemap_probes:
        probe.render_cubemap(use_roughness_prefiltering, mipmap_shader)
    self.cubemap_UBO.update()