diff --git a/src/bundle.nim b/src/bundle.nim index 4e7e36c..b50e2a9 100644 --- a/src/bundle.nim +++ b/src/bundle.nim @@ -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 diff --git a/src/graphics/framebuffer.nim b/src/graphics/framebuffer.nim index f6541be..b3caebc 100644 --- a/src/graphics/framebuffer.nim +++ b/src/graphics/framebuffer.nim @@ -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 diff --git a/src/graphics/material.nim b/src/graphics/material.nim index ac3d57f..af05cdc 100644 --- a/src/graphics/material.nim +++ b/src/graphics/material.nim @@ -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 diff --git a/src/graphics/render.nim b/src/graphics/render.nim index 3198f86..60fa5d2 100644 --- a/src/graphics/render.nim +++ b/src/graphics/render.nim @@ -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 diff --git a/src/graphics/texture.nim b/src/graphics/texture.nim index c1638d4..11a470e 100644 --- a/src/graphics/texture.nim +++ b/src/graphics/texture.nim @@ -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) + diff --git a/src/graphics/ubo.nim b/src/graphics/ubo.nim index 5a5b9f6..818015f 100644 --- a/src/graphics/ubo.nim +++ b/src/graphics/ubo.nim @@ -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: diff --git a/src/loaders/blend.nim b/src/loaders/blend.nim index f98bc0f..78b0ea3 100644 --- a/src/loaders/blend.nim +++ b/src/loaders/blend.nim @@ -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() diff --git a/src/objects/light.nim b/src/objects/light.nim index 2702817..6d89c32 100644 --- a/src/objects/light.nim +++ b/src/objects/light.nim @@ -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 diff --git a/src/objects/mesh.nim b/src/objects/mesh.nim index 46b1071..6d0ab7d 100644 --- a/src/objects/mesh.nim +++ b/src/objects/mesh.nim @@ -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: diff --git a/src/scene.nim b/src/scene.nim index fc5fe6b..ef66bb4 100644 --- a/src/scene.nim +++ b/src/scene.nim @@ -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: diff --git a/src/shaders/shader_library.glsl b/src/shaders/shader_library.glsl index a89fae1..64a4b3c 100644 --- a/src/shaders/shader_library.glsl +++ b/src/shaders/shader_library.glsl @@ -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 diff --git a/src/shadows/shadow_common.nim b/src/shadows/shadow_common.nim new file mode 100644 index 0000000..2ff36cf --- /dev/null +++ b/src/shadows/shadow_common.nim @@ -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() + diff --git a/src/shadows/simple_shadow.glsl b/src/shadows/simple_shadow.glsl index b809b1f..415197e 100644 --- a/src/shadows/simple_shadow.glsl +++ b/src/shadows/simple_shadow.glsl @@ -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 + diff --git a/src/shadows/simple_shadow.nim b/src/shadows/simple_shadow.nim index c037e23..8dd4b8f 100644 --- a/src/shadows/simple_shadow.nim +++ b/src/shadows/simple_shadow.nim @@ -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 diff --git a/src/types.nim b/src/types.nim index 566811d..53436fb 100644 --- a/src/types.nim +++ b/src/types.nim @@ -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 diff --git a/src/util.nim b/src/util.nim index 7cf3f3e..d15fe59 100644 --- a/src/util.nim +++ b/src/util.nim @@ -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) +