Revamp shadow map system into a working state. Fix related bugs.

* `light.configure_shadow` has been added. It can either use a camera frustum or
  calculate the bounds for a static shadow that doesn't need to be updated every
  frame.
* `Light` and `newLight` have `use_shadow`, however it's not used automatically
  at the moment. Instead you should call `configure_shadow`.
* Added `updateShadowStorage` which at the moment has to be called manually
  after using `configure_shadow`.
* All shadows now share a single texture array per scene, so they can be
  accessed by index in the shader and take less shader locations and draw calls.
* Shadow map resolution is also global per scene.
* Add `set_texture_shadow` and `unset_texture_shadow` to configure a texture as
  shadow sampler.
* Bias has been removed as an option, it's calculated automatically instead.
* Bias now moves along the normal of the mesh instead of towards the light,
  allowing very small biases.
This commit is contained in:
Alberto Torres 2024-09-11 20:49:00 +02:00
parent c8f9beaff9
commit c77187fa9f
16 changed files with 409 additions and 189 deletions

View file

@ -53,6 +53,7 @@ import ./objects/cubemap_probe
import ./objects/gameobject
import ./objects/light
import ./objects/mesh
import ./shadows/shadow_common
import ./shadows/simple_shadow
import ./quat
import ./scene
@ -81,6 +82,7 @@ export quat
export render
export scene
export screen
export shadow_common
export simple_shadow
export tables
export texture

View file

@ -101,6 +101,7 @@ proc newFramebuffer*(engine: MyouEngine,
self.engine = engine
self.width = width
self.height = height
self.layer_count = layer_count
self.format = format
self.depth_type = depth_type
var tex_type = tex_type

View file

@ -491,22 +491,18 @@ proc initShader*(self: Shader, engine: MyouEngine, material: Material,
ubo_names.setLen 0
self.texture_locations.setLen 0
var extra_location_count = 0
for name in material.textures.keys:
self.texture_locations.add glGetUniformLocation(prog, name.cstring)
if defined(myouEnsureTextureLocations) and not defined(release):
assert self.texture_locations[^1] != -1, "invalid texture location for " & name
self.shadowmap_locations.setLen 0
self.cubemap_locations.setLen 0
self.shadowmap_location = -1
if material.scene != nil:
for i in 0 ..< material.scene.max_shadow_maps:
# TODO: test with a single glUniform1iv instead of changing
# the individual array elements
self.shadowmap_locations.add glGetUniformLocation(prog,
(&"shadow_maps[{i}]").cstring)
# TODO: do this when we know they're used
# when not defined(release):
# assert self.shadowmap_locations[^1] != -1
self.shadowmap_location = glGetUniformLocation(prog, "shadow_maps".cstring)
if self.shadowmap_location != -1:
extra_location_count.inc
for i in 0 ..< material.scene.max_cubemaps:
# TODO: test with a single glUniform1iv instead of changing
@ -519,7 +515,7 @@ proc initShader*(self: Shader, engine: MyouEngine, material: Material,
# dump self.cubemap_locations
assert self.texture_locations.len + self.cubemap_locations.len +
self.shadowmap_locations.len <= self.engine.renderer.max_textures
extra_location_count <= self.engine.renderer.max_textures
self.program = prog

View file

@ -63,6 +63,7 @@ import ../objects/gameobject
# import ../objects/light
import ../objects/mesh
import ../platform/platform
import ../shadows/shadow_common
import ../quat
import ../scene
import ../util
@ -209,7 +210,19 @@ proc draw_all*(self: RenderManager) =
if update_probe_ubo:
scene.cubemap_UBO.update()
# TODO: render shadows required by cameras of enabled screens
# TODO: we need to refactor all this loop that was originally just
# to update matrices
var update_shadow_ubo = false
for light in scene.lights:
if light.shadows.len != 0:
for s in light.shadows:
# TODO: also use UBO's logic for UpdateOnFirstUse
if s.auto_render and s.shadow_index != -1:
let u = s.renderShadow(scene.active_camera)
update_shadow_ubo = update_shadow_ubo or u
if update_shadow_ubo:
scene.shadow_maps_ubo.update()
for screen in self.engine.screens:
if not screen.enabled:
continue
@ -354,13 +367,12 @@ proc draw_mesh*(self: RenderManager, mesh: Mesh, mesh2world: Mat4, cam_data: Ren
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]
let loc = shader.shadowmap_location
let fb = scene.shadow_maps
if loc != -1 and fb != nil:
# TODO: allow access to both texture and depth_texure
let tex = when USE_SHADOW_SAMPLERS: fb.depth_texture
else: fb.texture
if tex != nil:
textures_to_bind.add tex
texture_locations.add loc

View file

@ -398,10 +398,6 @@ proc ensure_storage*(self: Texture) =
self.storage = newTextureStorage(self.tex_type, self.width, self.height, self.depth, self.format)
self.setFilter self.filter
self.mipmap_range = (0, self.mipmapHigh)
# TODO: toggle support for classic sampler2DShadow textures
# if self.format in [Depth_u16, Depth_u24, Depth_f32]:
# glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE.GLint)
# glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL.GLint)
proc newTexture*(engine: MyouEngine, name: string,
width, height: int; depth: int = 1,
@ -570,3 +566,16 @@ func vec3size*(self: Texture, mip_level = -1): Vec3 =
vec3((max(1, self.width shr mip_level)).float32,
(max(1, self.height shr mip_level)).float32,
(max(1, depth shr mip_level)).float32)
proc set_texture_shadow*(self: Texture) =
self.bind_it(needs_active_texture=true)
if self.format in [Depth_u16, Depth_u24, Depth_f32]:
glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE.GLint)
# TODO: would GL_LESS be better? That way depth 1.0 would always be not in shadow
# so we wouldn't need to clamp in shader
glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL.GLint)
proc unset_texture_shadow*(self: Texture) =
self.bind_it(needs_active_texture=true)
glTexParameteri(self.storage.target, GL_TEXTURE_COMPARE_MODE, GL_NONE.GLint)

View file

@ -71,6 +71,9 @@ proc initUBO*[T](self: UBO, renderer: RenderManager, name: string, utype: typede
template storage*[T](self: UBO, utype: typedesc[T]): ArrRef[T] = self.byte_storage.to(T)
proc len*[T](self: UBO, utype: typedesc[T]): int =
self.byte_storage.byte_len div sizeof(T)
# NOTE: does not preserve contents
proc resize*[T](self: UBO, utype: typedesc[T], count: int) =
if count == self.storage(T).len:

View file

@ -367,7 +367,7 @@ proc loadObjectImpl(self: BlendLoader, scene: Scene, obn: FNode): (GameObject, s
PointLight
let color = vec3(data.r.f32[0], data.g.f32[0], data.b.f32[0])
let mode = data.mode.i32[0]
# let use_shadow = mode.testBit(0)
let use_shadow = mode.testBit(0)
let use_dist = mode.testBit(20)
let cutoff = if use_dist: data.att_dist.f32[0] else: 0'f32
var ob = self.engine.new_light(
@ -377,7 +377,7 @@ proc loadObjectImpl(self: BlendLoader, scene: Scene, obn: FNode): (GameObject, s
energy = data.energy.f32[0],
spot_size = data.spotsize.f32[0],
spot_blend = data.spotblend.f32[0],
# use_shadow = use_shadow,
use_shadow = use_shadow,
cutoff_distance = cutoff,
diffuse_factor = data.diff_fac.f32[0],
specular_factor = data.spec_fac.f32[0],
@ -560,6 +560,7 @@ proc loadSceneImpl(self: BlendLoader, scn: FNode, scene: Scene) =
# dump (k, nodes[i].mat[0].id.name.str, nodes[i].data.mat[0].id.name.str)
# echo nodes[i]
i += 1
scene.reorder_children()
if update_light_counts:
scene.calculate_max_lights_and_cubemaps()

View file

@ -32,6 +32,7 @@
import ../types
import vmath except Quat
import ../util
when defined(nimdoc):
type TYPES* = LightType | Light
@ -48,6 +49,7 @@ proc newLight*(engine: MyouEngine, name: string="",
light_radius: float32 = 0.01,
spot_size: float32 = 1.3,
spot_blend: float32 = 0.15,
use_shadow = false,
): Light
proc instance_physics*(this: Light)
@ -62,7 +64,11 @@ method get_light*(this: Light): Light =
# End forward declarations and ob type methods
import ./gameobject
import ./mesh
import ../scene
import ../shadows/shadow_common
import ../shadows/simple_shadow
import quickhull
proc newLight*(engine: MyouEngine, name: string="",
scene: Scene = nil,
@ -75,6 +81,7 @@ proc newLight*(engine: MyouEngine, name: string="",
light_radius: float32 = 0.01,
spot_size: float32 = 1.3,
spot_blend: float32 = 0.15,
use_shadow = false,
): Light =
var this = new Light
discard procCall(this.GameObject.initGameObject(engine, name))
@ -88,10 +95,81 @@ proc newLight*(engine: MyouEngine, name: string="",
this.cutoff_distance = cutoff_distance
this.diffuse_factor = diffuse_factor
this.specular_factor = specular_factor
this.use_shadow = use_shadow
if scene != nil:
scene.add_object(this, name=name)
return this
proc configure_shadow*(self: Light,
camera: Camera = nil,
max_distance: float32 = 0.0,
objects = self.scene.children) =
# Configure a shadow for this light object, either by following a camera
# passed as argument, or a static shadow for the specified objects.
#
# `max_distance` is only used when `camera` is supplied.
#
# `objects` is only used in static mode (when `camera` is nil). In this
# mode, the shadow map will be tailored for the size of the bounding boxes
# of the objects. It will exclude flat objects (planes) that are under every
# other object because they can't project a shadow to themselves.
for s in self.shadows:
s.destroy()
self.shadows.setLen 0
if camera == nil:
# Static shadow strategy (ideal for small scenes):
# For casting shadows, include bounds for all visible meshes
# except for ground planes.
# TODO: when a ground plane is detected, if light Z is positive
# (pointing upwards) the shadow map should just be cleared with a depth
# of -1.0 (all in shadow); or render it with some offset, or allow the
# detection of ground planes with some amount of thickness to prevent
# light bleeding.
# TODO: instead of a ground plane, we can just exclude the largest
# convex object, as long as we can clamp the polygons outside the
# frustum. Can we avoid clipping by setting W?
self.scene.update_all_matrices()
var casters: seq[Vec3]
for ob in self.scene.children:
if not (ob.is_mesh and ob.visible):
continue
echo "adding ", ob.name, " otype ", ob.otype
# discard ob.get_world_matrix
let me = ob.get_mesh
let bb = me.bound_box
let world_dim = (ob.world_matrix * vec4(bb[1] - bb[0], 0.0)).xyz
var casts = true
if world_dim.z < 0.00001:
# Possible flat floor plane,
# check if all meshes have their centers above it
when defined(myouDebugShadows):
echo "Floor plane detected: ", ob.name
var all_above = true
let z = ob.world_center.z - 0.00001
for ob2 in self.scene.children:
if ob2.is_mesh and ob2.visible and ob2.world_center.z < z:
when defined(myouDebugShadows):
echo " Object is below floor plane: ", ob2.name
all_above = false
break
casts = not all_above
if casts:
for v in box_corners(bb):
casters.add ob.world_matrix * v
casters = quickhull_points(casters)
let shadow = newSimpleShadowManager(self, use_camera = false)
shadow.caster_bounding_points = casters
else:
# TODO: Use a sphere as bounds.
# TODO: Have a mode where the sphere surrounds all possible orientations
# of the camera and some amount of translation.
newSimpleShadowManager(self, use_camera = true, far = max_distance)
proc instance_physics*(this: Light) =
discard

View file

@ -605,22 +605,16 @@ proc update_bounding_box*(self: Mesh) =
raise newException(Defect, "not implemented")
let varray = self.data.varrays[0]
let stride = self.data.stride div 4
var minx, miny, minz = Inf.float32
var maxx, maxy, maxz = -Inf.float32
var minv = Vec3.high
var maxv = Vec3.low
var i = 0
while i < varray.len:
var v = varray[i]
minx = min(minx, v)
maxx = max(maxx, v)
v = varray[i + 1]
miny = min(miny, v)
maxy = max(maxy, v)
v = varray[i + 2]
minz = min(minz, v)
maxz = max(maxz, v)
var v = cast[ptr Vec3](varray[i].addr)
minv = min(minv, v[])
maxv = max(maxv, v[])
i += stride
self.bound_box[0] = vec3(minx, miny, minz)
self.bound_box[1] = vec3(maxx, maxy, maxz)
self.bound_box = (minv, maxv)
self.center = mix(minv, maxv, 0.5)
proc debug_print_vertices*(self: Mesh, starts: int = 0, ends: int = 10) =
if self.data.varrays[0].len == 0:

View file

@ -73,6 +73,7 @@ import std/strutils
import std/tables
import ./graphics/framebuffer
import ./graphics/material
import ./graphics/texture
import ./graphics/ubo
import ./incomplete
import ./objects/camera
@ -80,7 +81,7 @@ import ./objects/cubemap_probe
import ./objects/gameobject
import ./objects/light
import ./objects/mesh
import ./shadows/simple_shadow
import ./shadows/shadow_common
import ./screen
import ./util
@ -100,6 +101,7 @@ proc initScene*(self: Scene, engine: MyouEngine, name: string = "Scene",
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
@ -331,24 +333,26 @@ proc update_all_matrices*(self: Scene) =
# if c.visible:
# ob.recalculate_bone_matrices()
# break
# TODO: why can't I mutate in for loops???
for i in 0 .. self.auto_updated_children.high:
self.auto_updated_children[i].update_matrices()
return
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
return
proc destroy*(self: Scene) =
for ob in reversed(self.children):
var ob = ob
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)
return
proc enable_render*(self: Scene) =
self.enabled = true
@ -401,8 +405,8 @@ proc calculate_max_lights_and_cubemaps*(self: Scene) =
of SunLight: self.max_sun_lights += 1
of SpotLight: self.max_spot_lights += 1
of AreaLight: self.max_area_lights += 1
if light.shadows.len != 0:
self.max_shadow_maps += 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
@ -416,8 +420,6 @@ proc calculate_max_lights_and_cubemaps*(self: Scene) =
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}"
# TODO: rethink how shadow maps are handled
self.shadow_maps.setLen self.max_shadow_maps
proc get_lighting_UBOs*(self: Scene): seq[UBO] =
# echo &"getting UBOs {self.max_point_lights} {self.max_sun_lights}"
@ -456,7 +458,7 @@ proc get_lighting_code_defines*(self: Scene): seq[string] =
"#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 0",
"#define USE_SHADOW_SAMPLERS " & $(USE_SHADOW_SAMPLERS.int),
]
proc get_lighting_code*(self: Scene): string =
@ -468,6 +470,7 @@ proc get_lighting_code*(self: Scene): string =
#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;
@ -491,16 +494,17 @@ proc get_lighting_code*(self: Scene): string =
struct ShadowMapInfo {
mat4 depth_matrix;
float size;
float pad0, pad1, pad2;
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 sampler2DShadow shadow_maps[MAX_SHADOW_MAPS];
uniform sampler2DArrayShadow shadow_maps;
#else
uniform sampler2D shadow_maps[MAX_SHADOW_MAPS];
uniform sampler2DArray shadow_maps;
#endif
#endif
struct CubemapInfo {
@ -563,11 +567,17 @@ proc update_lights*(self: Scene) =
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:

View file

@ -275,9 +275,11 @@ vec4 principled_node(vec3 base, vec3 I, vec3 normal, vec3 pos, float metallic, f
vec3 spec = D * G * spec_common / dotNL;
spec *= ld.light_specular_power;
vec3 dif = diffuse * diffuse_sun_light(ld.light_diffuse_power);
float shadow = get_shadow(pos, 0);
float shadow = 1.0;
if(ld.light_shadow_index >= 0.0){
shadow = get_shadow(pos, normal, ld.light_shadow_index);
}
color += dotNL * (dif + spec) * ld.color.rgb * shadow;
// color += debug_shadow(pos);
}
#endif
// NOTE TODO: these factors are fake, I'm still figuring out how to make

View file

@ -0,0 +1,61 @@
import ../types
import ../graphics/framebuffer
import ../graphics/texture
import ../scene
const USE_SHADOW_SAMPLERS* = false
when defined(nimdoc):
type TYPES* = ShadowManager | ShadowMapUniform
method renderShadow*(self: ShadowManager, camera: Camera): bool {.base,locks:"unknown".} =
raise Defect.newException "Not implemented"
method destroy*(self: ShadowManager) {.base,locks:"unknown".} =
raise Defect.newException "Not implemented"
# TODO! Call this automatically before render after one or more shadows have
# been configured.
proc updateShadowStorage*(scene: Scene) =
## Creates or updates the storage for all shadow maps of the scene. It must
## be called after `configure_shadow` and before render.
var count = 0'i32
var color_channels = 0
var uses_depth = false
for light in scene.lights:
for s in light.shadows:
s.shadow_index = count
color_channels = max(color_channels, s.uses_color_channels)
uses_depth = uses_depth or s.uses_depth
count += s.required_texture_count
if count == 0:
return
let fb_size = scene.shadow_map_resolution
assert color_channels <= 4
if scene.shadow_maps != nil:
scene.shadow_maps.destroy()
scene.shadow_maps = scene.engine.newFramebuffer(fb_size, fb_size,
if color_channels != 0:
(R_f16.int - 1 + color_channels).TextureFormat
else:
R_u8, # TODO: make fb without color attachment
if uses_depth: DepthTexture else: DepthRenderBuffer,
filter = Linear,
tex_type = if color_channels != 0: Tex2DArray else: Tex2D,
depth_tex_type = if uses_depth: Tex2DArray else: Tex2D,
layer_count = count)
scene.shadow_maps.texture.setExtrapolation Clamp
if uses_depth:
set_texture_shadow(scene.shadow_maps.depth_texture)
# TODO: being able to use set_texture_shadow on color when needed
# TODO: separate logic for each type of UBO?
scene.calculate_max_lights_and_cubemaps()

View file

@ -1,28 +1,32 @@
float get_shadow(vec3 position, vec3 normal, float findex) {
#if MAX_SHADOW_MAPS
float get_shadow(vec3 position, int index) {
vec3 co = vec3(shadow_map_infos[0].depth_matrix * vec4(position, 1.0));
if(co.x < 0.0 || co.x >= 1.0 || co.y < 0.0 || co.y >= 1.0) return 1.0;
ShadowMapInfo info = shadow_map_infos[int(findex)];
float px_size = 1.5/info.size;
vec3 p = position + normal * info.bias * 2.0;
vec3 co = vec3(info.depth_matrix * vec4(p, 1.0));
if(co.x <= 0.0 || co.x >= 0.999999 || co.y <= 0.0 || co.y >= 0.999999) return 1.0;
co.z = min(co.z, 0.999999);
#if USE_SHADOW_SAMPLERS
return texture(shadow_maps[0], co);
#else
// vec2 co2 = co.xy * shadow_map_infos[0].size;
// vec2 co20 = floor(co2);
// ivec2 ico = vec2(co20);
// return mix(
// texelFetch(shadow_maps[0], )
// )
float px_size = 2.0/shadow_map_infos[0].size;
float count = 0.0;
float vis = 0.0;
// TODO: better and faster filtering by using radial samples
// and testing outer ring first
// return texture(shadow_maps, vec4(co.xy, findex, co.z)).r;
// return texture(shadow_maps, vec3(co.xy,findex)).r;
for(float y=co.y-px_size;y<=co.y+px_size;y+=px_size*0.499){
for(float x=co.x-px_size;x<=co.x+px_size;x+=px_size*0.499){
vis += texture(shadow_maps[0], vec2(x,y)).r <= co.z?0.0:1.0;
#if USE_SHADOW_SAMPLERS
vis += texture(shadow_maps, vec4(x,y, findex, co.z));
#else
vis += texture(shadow_maps, vec3(x,y,findex)).r <= co.z?0.0:1.0;
#endif // USE_SHADOW_SAMPLERS
count += 1.0;
}
}
// return mix(vis/count, 1.0, 0.14);
return vis/count;
#endif
#else
return 1.0;
#endif // MAX_SHADOW_MAPS
}
#endif

View file

@ -36,83 +36,77 @@ import vmath except Quat
import ../graphics/framebuffer
import ../graphics/material
import ../graphics/render
import ../graphics/texture
import ../graphics/ubo
# import ../objects/light
import ../objects/mesh
import ../objects/gameobject
import ../quat
import ../scene
import ../util
# TODO: Move these base methods
method renderShadow*(self: ShadowManager, camera: Camera) {.base.} =
discard
method destroy*(self: ShadowManager) {.base.} =
discard
import ./shadow_common
when defined(nimdoc):
# TODO: ShadowMapUniform goes elsewhere
type TYPES* = SimpleShadowManager | ShadowMapUniform
type TYPES* = SimpleShadowManager
# Z depth is not modified, only X and Y for UVs
const LIGHT_PROJ_TO_DEPTH = mat4(0.5, 0, 0, 0, 0, 0.5, 0, 0,
0, 0, 1, 0, 0.5, 0.5, 0, 1)
when USE_SHADOW_SAMPLERS:
const LIGHT_PROJ_TO_DEPTH = mat4(0.5, 0, 0, 0, 0, 0.5, 0, 0,
0, 0, 0.5, 0, 0.5, 0.5, 0.5, 1)
else:
# Z depth is not modified, only X and Y for UVs
const LIGHT_PROJ_TO_DEPTH = mat4(0.5, 0, 0, 0, 0, 0.5, 0, 0,
0, 0, 1, 0, 0.5, 0.5, 0, 1)
when not defined(release):
when defined(myouDebugShadows):
var debug_mesh: Mesh
proc newSimpleShadowManager*(light: Light; fb_size: int;
proc newSimpleShadowManager*(light: Light;
use_camera: bool;
near, far: float32 = 0.0;
bias: float32 = 0.001;
): SimpleShadowManager =
): SimpleShadowManager {.discardable.} =
new(result)
result.engine = light.engine
result.light = light
result.depth_range = (near, far)
result.bias = bias
result.auto_render = true
result.required_texture_count = 1
result.shadow_index = -1
result.use_camera = use_camera
light.shadows.add result
let self = result
result.engine.renderer.enqueue proc()=
# TODO: allow configuring use of a depth-only pass with this code
# and with sampler2DShadow support in texture.nim
when USE_SHADOW_SAMPLERS:
# depth-only pass
self.material = light.engine.newMaterial("simple_shadow", nil,
fragment=dedent "",
)
# TODO: Disable color texture
self.uses_color_channels = 0
self.uses_depth = true
else:
self.material = light.engine.newMaterial("simple_shadow", nil,
fragment=dedent """
in vec4 ppos;
out float color;
void main(){
color = ppos.z/ppos.w;
}""",
varyings = @[Varying(vtype: ProjPosition, varname: "ppos")]
)
self.uses_color_channels = 1
self.uses_depth = false
# self.material = light.engine.newMaterial("simple_shadow", nil,
# fragment="",
# )
# self.framebuffer = light.engine.newFramebuffer(fb_size, fb_size,
# RGB_u8, DepthTexture, depth_format = Depth_u16, filter=Linear)
# self.texture = self.framebuffer.depth_texture
self.material = light.engine.newMaterial("simple_shadow", nil,
fragment=dedent """
in vec4 ppos;
out float color;
void main(){
color = ppos.z/ppos.w;
}""",
varyings = @[Varying(vtype: ProjPosition, varname: "ppos")]
)
self.framebuffer = light.engine.newFramebuffer(fb_size, fb_size,
R_f16, DepthRenderBuffer, filter=Linear)
self.texture = self.framebuffer.texture
self.texture.setExtrapolation Clamp
when not defined(release):
when defined(myouDebugShadows):
debug_mesh = self.engine.newMesh(vertex_count = 100, draw_method = Lines)
debug_mesh.materials.add self.engine.newSolidMaterial("white", vec4(0,1,0,1))
debug_mesh.materials.add self.engine.newSolidMaterial("green", vec4(0,1,0,1))
debug_mesh.visible = false
# TODO: why the FUCK do I need this here?
self.engine.renderer.enqueue proc()=
light.scene.add_object debug_mesh
light.scene.add_object debug_mesh
method destroy*(self: SimpleShadowManager) =
method destroy*(self: SimpleShadowManager) {.locks:"unknown".} =
if self.material != nil:
self.material.destroy()
if self.framebuffer != nil:
self.framebuffer.destroy()
when not defined(release):
when defined(myouDebugShadows):
proc show_mat(mat: Mat4) =
var points = newSeqOfCap[Vec3](8)
for z in [-1'f32,1'f32]:
@ -125,9 +119,64 @@ when not defined(release):
debug_mesh.data.update_varray
debug_mesh.visible = true
method renderShadow*(self: SimpleShadowManager, scene: Scene, bounding_points: seq[Vec3], min_z: float32, index: int)
proc renderShadow*(self: SimpleShadowManager, scene: Scene,
bounding_points: seq[Vec3],
sphere_center: Vec3 = vec3(0),
sphere_radius: float32 = 0,
min_z: float32 = 0) =
# Find the projection matrix that fits all bounding points and sphere
# TODO: option to rotate proj to favor vertical lines?
when not defined(release):
debug_mesh.clear_vertices()
let rotm = self.light.world_matrix.to_mat3_rotation
let rotmi = rotm.inverse
var pmin = vec3(Inf)
var pmax = vec3(-Inf)
if sphere_radius != 0:
pmin = sphere_center - vec3(sphere_radius)
pmax = sphere_center + vec3(sphere_radius)
for p in bounding_points:
let p = rotmi * p
pmin = min(pmin, p)
pmax = max(pmax, p)
pmax.z = max(pmin.z+min_z, pmax.z)
let mid = mix(pmin, pmax, 0.5)
var scale = (pmax-pmin) * vec3(0.5, 0.5, -0.5)
scale.xy = vec2(max(scale.x, scale.y)) # make it square
# TODO: grow 1 px and snap the box to increments of pixels
let proj = scale(1'f32/scale)
let light_matrix = rotm.to_mat4 * translate(mid)
# calculate the bias = the diagonal of half a pixel
# multiplied by twice the radius in the shader (1.5*2 at the moment)
# plus 1 because of texture filtering (GL_COMPARE_REF_TO_TEXTURE)
# TODO: test at multiple resolutions and different filtering radii
let resolution = scene.shadow_maps.width.float32
let bias = 4.0 * sqrt(2.0f) * scale.x/resolution
when defined(myouDebugShadows):
show_mat inverse(proj * inverse(light_matrix))
method renderShadow*(self: SimpleShadowManager, camera: Camera, index: int) =
scene.shadow_maps.enable(layer = self.shadow_index)
scene.shadow_maps.clear(color=vec4(1), layer = self.shadow_index)
let cd = newRenderCameraData(light_matrix,
proj, proj.get_culling_planes(), vec2(scene.shadow_maps.width.float32))
let rm = self.engine.renderer
if not rm.initialized: # TODO: enqueue but only once
return
let material = self.material
for ob in scene.mesh_passes[0]:
if ob.visible and ob.draw_method != Lines:
rm.draw_mesh(ob, ob.world_matrix, cd, 0, material)
scene.shadow_maps.disable()
var depth_matrix = LIGHT_PROJ_TO_DEPTH * proj * inverse(light_matrix)
scene.shadow_maps_ubo.storage(ShadowMapUniform)[self.shadow_index] =
ShadowMapUniform(depth_matrix: depth_matrix,
tex_size: resolution, bias: bias)
proc renderShadowWithCamera*(self: SimpleShadowManager, camera: Camera) =
# first we'll make a list of points that matches the corners of the camera
# frustum and given depth range
@ -150,15 +199,17 @@ method renderShadow*(self: SimpleShadowManager, camera: Camera, index: int) =
depth_to_screen(self.depth_range[1], camera.projection_matrix),
]
let mat = camera.world_matrix.remove_scale_skew * camera.projection_matrix_inverse
# show_mat mat
when defined(myouDebugShadows):
show_mat mat
var points = newSeqOfCap[Vec3](8)
for z in screen_depth:
for y in [-1'f32,1'f32]:
for x in [-1'f32,1'f32]:
let v = mat * vec4(x,y,z,1)
points.add v.xyz/v.w
# for i in [0,1,1,3,3,2,2,0,4,5,5,7,7,6,6,4,0,4,1,5,2,6,3,7]:
# debug_mesh.add_vertex points[i], vec4(1)
when defined(myouDebugShadows):
for i in [0,1,1,3,3,2,2,0,4,5,5,7,7,6,6,4,0,4,1,5,2,6,3,7]:
debug_mesh.add_vertex points[i], vec4(1)
var zrange = self.depth_range[1] - self.depth_range[0]
if zrange == 0.0:
@ -166,58 +217,23 @@ method renderShadow*(self: SimpleShadowManager, camera: Camera, index: int) =
# extend z range an arbitrary amount
# TODO: make it configurable
# or better: clamp polygons in the shader!
self.renderShadow(camera.scene, points, min_z=zrange*4, index)
self.renderShadow(camera.scene, points, min_z=zrange*4)
method renderShadow*(self: SimpleShadowManager, scene: Scene, bounding_points: seq[Vec3], min_z: float32, index: int) =
# Find the projection matrix that fits all bounding points
# TODO: find bounds of bounding sphere instead
# TODO: option to favor vertical lines?
when not defined(release):
debug_mesh.clear_vertices()
let rotm = self.light.world_matrix.to_mat3_rotation
let rotmi = rotm.inverse
var pmin = vec3(Inf)
var pmax = vec3(-Inf)
for p in bounding_points:
let p = rotmi * p
pmin = min(pmin, p)
pmax = max(pmax, p)
# TODO: extend pmin.z so it never falls inside the complete frustum
pmax.z = max(pmin.z+min_z, pmax.z)
let mid = mix(pmin, pmax, 0.5)
var scale = (pmax-pmin) * vec3(0.5, 0.5, -0.5)
scale.xy = vec2(max(scale.x, scale.y))
let proj = scale(1'f32/scale)
let light_matrix = rotm.to_mat4 * translate(mid)
# show_mat inverse(proj * inverse(light_matrix))
self.texture.unbind()
self.framebuffer.enable()
self.framebuffer.clear(color=vec4(1))
let cd = newRenderCameraData(light_matrix,
proj, proj.get_culling_planes(), vec2(self.framebuffer.width.float32))
let rm = self.engine.renderer
if not rm.initialized: # TODO: enqueue but only once
return
let material = self.material
for ob in scene.mesh_passes[0]:
if ob.visible:
rm.draw_mesh(ob, ob.world_matrix, cd, 0, material)
self.framebuffer.disable()
scene.shadow_maps[index] = self.texture
var depth_matrix = LIGHT_PROJ_TO_DEPTH * proj * inverse(light_matrix)
# vmath doesn't let me do this properly
depth_matrix[3,2] = depth_matrix[3,2] - self.bias
# cast[ptr float32](depth_matrix[3,2].addr)[] -= self.bias
scene.shadow_maps_ubo.storage(ShadowMapUniform)[index] =
ShadowMapUniform(depth_matrix: depth_matrix,
tex_size: self.framebuffer.width.float32)
# TODO: do this outside
scene.shadow_maps_ubo.update()
method renderShadow*(self: SimpleShadowManager, camera: Camera): bool {.locks:"unknown".} =
if self.use_camera:
self.renderShadowWithCamera(
camera = camera)
return true
else:
let v = get_world_Z_vector(self.light)
let angle = angle(v, self.last_light_dir)
# We use < and not <= so it always renders when min delta is 0
if angle < self.min_light_angle_delta:
return
self.last_light_dir = v
self.renderShadow(
scene = camera.scene,
bounding_points = self.caster_bounding_points,
min_z = -Inf)
return true

View file

@ -269,9 +269,7 @@ type
Light* = ref object of GameObject
light_type*: LightType
shadows*: seq[ShadowManager]
# shadow_fb*: Framebuffer
# shadow_texture*: Texture
# render_shadow*: bool
use_shadow*: bool
color*: Vec3
energy*: float32
diffuse_factor*: float32
@ -292,21 +290,27 @@ type
ShadowManager* = ref object of RootObj
engine* {.cursor.}: MyouEngine
light* {.cursor.}: Light
framebuffer*: Framebuffer
texture*: Texture
required_texture_count*: int32
uses_color_channels*: int8
uses_depth*: bool
shadow_index*: int32
sampler_type*: string
material*: Material
auto_render*: bool
min_light_angle_delta*: float32
last_light_dir*: Vec3
SimpleShadowManager* = ref object of ShadowManager
depth_range*: (float32, float32)
bias*: float32
caster_bounding_points*: seq[Vec3]
use_camera*: bool
# NOTE: layout must match struct ShadowMapInfo in scene.nim
ShadowMapUniform* = object
depth_matrix*: Mat4
tex_size*: float32
pad0, pad1, pad2: float32
bias*: float32
pad0, pad1: float32
# scene.nim
@ -326,11 +330,11 @@ type
direction*: Vec3
padding0: float32
color*: Vec3
padding1: float32
shadow_index*: float32
diffuse_power*: float32
specular_power*: float32
padding1: float32
padding2: float32
padding3: float32
Scene* = ref object
engine* {.cursor.}: MyouEngine
@ -361,6 +365,7 @@ type
# render options
background_color*: Vec4
shadow_map_resolution*: int32
cubemap_resolution*: int32
max_point_lights*: int32
max_sun_lights*: int32
@ -370,7 +375,7 @@ type
max_cubemaps*: int32
max_spherical_harmonics*: int32
shadow_maps*: seq[Texture]
shadow_maps*: Framebuffer
world_material*: Material
background_cubemap*: Framebuffer
cubemaps*: seq[Framebuffer]
@ -576,7 +581,7 @@ type
object_ubo_index*: GLuint
texture_locations*: seq[GLint]
shadowmap_locations*: seq[GLint]
shadowmap_location*: GLint
cubemap_locations*: seq[GLint]
when not defined(release):
@ -666,7 +671,7 @@ type
Framebuffer* = ref object of RootObj
engine* {.cursor.}: MyouEngine
width*, height*: int
width*, height*, layer_count*: int
format*: TextureFormat
depth_type*: FramebufferDepthType
texture*, depth_texture*: Texture

View file

@ -232,3 +232,29 @@ template staticOrDebugRead*(path: string): string =
staticRead path
template nonNil*(x: untyped): bool = x != nil
# TODO: move to vmath fork
template low*[T](x: typedesc[GVec2[T]]): GVec2[T] = gvec2[T](T.low,T.low)
template low*[T](x: typedesc[GVec3[T]]): GVec3[T] = gvec3[T](T.low,T.low,T.low)
template low*[T](x: typedesc[GVec4[T]]): GVec4[T] = gvec4[T](T.low,T.low,T.low,T.low)
template high*[T](x: typedesc[GVec2[T]]): GVec2[T] = gvec2[T](T.high,T.high)
template high*[T](x: typedesc[GVec3[T]]): GVec3[T] = gvec3[T](T.high,T.high,T.high)
template high*[T](x: typedesc[GVec4[T]]): GVec4[T] = gvec4[T](T.high,T.high,T.high,T.high)
# bounding box operations
template `&`*[T](a, b: (GVec3[T], GVec3[T])): (GVec3[T], GVec3[T]) =
(min(a[0], b[0]), max(a[1], b[1]))
template `|`*[T](a, b: (GVec3[T], GVec3[T])): (GVec3[T], GVec3[T]) =
(max(a[0], b[0]), min(a[1], b[1]))
iterator box_corners*[T](bb: (GVec3[T], GVec3[T])): GVec3[T] =
let bb = cast[array[2, GVec3[T]]](bb)
for z in [0,1]:
for y in [0,1]:
for x in [0,1]:
yield gvec3[T](bb[x].x, bb[y].y, bb[z].z)