763 lines
30 KiB
Nim
763 lines
30 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
|
|
|
|
# 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, 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_del_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) =
|
|
## Rename the object and update the tables that reference it.
|
|
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.} =
|
|
## Add an object to the scene
|
|
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) =
|
|
## Remove an object from the scene
|
|
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) =
|
|
## Set the parent of an object. It calculates the transformation so it
|
|
## remains the same in world space unless `keep_transform = false` is passed.
|
|
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.quat
|
|
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) =
|
|
## Disconnects an object from its parent. It calculates the transformation so
|
|
## it remains the same in world space unless `keep_transform = false` is
|
|
## passed.
|
|
if child.parent != nil:
|
|
if keep_transform:
|
|
let rotation_order = child.rotation_order
|
|
let (position, rotation) = child.get_world_position_rotation
|
|
child.position = position
|
|
child.rotation.quat = 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)
|
|
child.parent.children.remove child
|
|
child.parent = nil
|
|
child.parent_bone_index = -1
|
|
child.matrix_parent_inverse = mat4()
|
|
return
|
|
|
|
proc reorder_children*(self: Scene) =
|
|
## Ensures that the order of children objects have parents before children.
|
|
## To be used by scene loaders that may add objects in any order.
|
|
|
|
# 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) =
|
|
## Calculates the matrices of all non-static objects and bones. Called
|
|
## automatically before rendering.
|
|
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) =
|
|
## Adds or removes the specified objects from the auto update matrix list.
|
|
## I.e. those not in the list are static.
|
|
for ob in objects:
|
|
ob.auto_update_matrix = auto_update
|
|
self.children_are_ordered = false
|
|
|
|
proc destroy*(self: Scene) =
|
|
## Destroy the scene and all its resources.
|
|
|
|
# This may not be necessary. TODO: test
|
|
for ob in self.children:
|
|
if ob.is_mesh:
|
|
for m in ob.get_mesh.materials:
|
|
if m != nil:
|
|
for tex in m.textures.mvalues:
|
|
tex.destroy()
|
|
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()
|
|
if self.background_cubemap != nil:
|
|
self.background_cubemap.destroy()
|
|
for cube in self.cubemaps:
|
|
cube.destroy()
|
|
self.cubemaps = @[]
|
|
self.engine.new_del_scenes[self.name] = nil
|
|
# 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) =
|
|
## Enable rendering of the scene
|
|
self.enabled = true
|
|
|
|
proc enable_physics*(self: Scene) =
|
|
## Enable the phyisics in the scene
|
|
if self.world != nil:
|
|
self.world.enabled = true
|
|
|
|
proc enable_all*(self: Scene) =
|
|
## Enable rendering and physics of the scene.
|
|
self.enable_render()
|
|
self.enable_physics()
|
|
|
|
proc new_gameobject*(self: Scene, name: string): GameObject =
|
|
## Create a GameObject and add it to the scene.
|
|
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 =
|
|
## Create a new Mesh object and add it to this scene.
|
|
##
|
|
## The most common draw methods are `Triangles` (the default), `Points`,
|
|
## `Lines`, and `TriangleStrip` (for `add_polygonal_line()`).
|
|
##
|
|
## If you supply a layout, it will be used. Otherwise a layout will be
|
|
## created from `common_attributes`, which by default is `{vertex, color}`.
|
|
## The available common attributes are `{vertex, color, normal, uv}` and they
|
|
## will be added in that order.
|
|
##
|
|
## You can give a `vertex_count` to allocate a mesh with capacity for that
|
|
## amount of vertices, but you can always resize it later. Meant to be used
|
|
## with `add_vertex()` and `add_polygonal_line`
|
|
##
|
|
## If you give a vertex array and an index array, they will be used directly
|
|
## as GPU buffers. If you only supply the vertex array, indices will
|
|
## implicitely be sequential.
|
|
self.engine.new_mesh(name, self, draw_method, common_attributes, layout,
|
|
skip_upload, vertex_count, vertex_array, index_array, pass)
|
|
|
|
proc newCamera*(self: Scene, name: string="camera",
|
|
near_plane: float32 = 0.1,
|
|
far_plane: float32 = 10000,
|
|
field_of_view: float32 = toRadians(30),
|
|
ortho_scale: float32 = 8,
|
|
aspect_ratio: float32 = 1,
|
|
cam_type: CameraType = Perspective,
|
|
sensor_fit: SensorFit = Auto,
|
|
shift: Vec2 = vec2(),
|
|
): Camera =
|
|
## Create a new Camera object and add it to the scene.
|
|
## If the scene doesn't have an active camera, it will be set as active.
|
|
result = self.engine.newCamera(name, self, near_plane, far_plane, field_of_view,
|
|
ortho_scale, aspectRatio, cam_type, sensor_fit, shift)
|
|
if self.active_camera.isNil:
|
|
self.set_active_camera result
|
|
|
|
proc set_active_camera*(self: Scene, camera: Camera) =
|
|
## Change the active camera of the scene, and if there are no viewports in
|
|
## the main screen, create one.
|
|
|
|
# TODO: change the camera in the viewports that use it as well.
|
|
# TODO: should only one viewport allowed per 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) =
|
|
## Change the limits for shader lighting data (number of lights of each type,
|
|
## cubemaps, etc.) depending on the existing objects and settings. It also
|
|
## resets the shaders.
|
|
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] =
|
|
## Get all the Uniform Buffer Objects (UBOs) with lighting information used
|
|
## in shaders. You need to pass it when creating a custom material that uses
|
|
## them. You also need to pass the GLSL code returned by `get_lighting_code`_.
|
|
|
|
# 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] =
|
|
## Returns the macro defines used in materials with the current limits. Don't
|
|
## use it. It is added automatically.
|
|
@[
|
|
"#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 =
|
|
## Returns the GLSL code to be able to use the UBOs given by
|
|
## `get_lighting_UBOs`_. It includes the definition of the UBOs, texture
|
|
## uniforms, and shadow map functions.
|
|
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 {
|
|
vec4 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) =
|
|
## Update the lighting UBOs. This is called automatically during rendering.
|
|
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 cubemaps by volume, from largest to smallest. The shaders expect this
|
|
## order. You don't need to call this, it is called automatically.
|
|
|
|
# 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) =
|
|
## Creates any missing cube maps. Called automatically.
|
|
|
|
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) =
|
|
## Render all cubemaps of the scene regardless of whether they need updating
|
|
## or not.
|
|
if self.cubemap_UBO == nil:
|
|
return
|
|
if self.world_material != nil:
|
|
self.render_background_cubemap(use_roughness_prefiltering, mipmap_shader, upload_UBO = false)
|
|
# TODO: Don't do this if objects haven't changed!
|
|
self.sort_cubemaps()
|
|
for probe in self.cubemap_probes:
|
|
probe.render_cubemap(use_roughness_prefiltering, mipmap_shader)
|
|
self.cubemap_UBO.update()
|