# The contents of this file are subject to the Common Public Attribution License # Version 1.0 (the “License”); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla # Public License Version 1.1 but Sections 14 and 15 have been added to cover use # of software over a computer network and provide for limited attribution for # the Original Developer. In addition, Exhibit A has been modified to be # consistent with Exhibit B. # # Software distributed under the License is distributed on an “AS IS” basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is Myou Engine. # # the Original Developer is the Initial Developer. # # The Initial Developer of the Original Code is the Myou Engine developers. # All portions of the code written by the Myou Engine developers are Copyright # (c) 2024. All Rights Reserved. # # Alternatively, the contents of this file may be used under the terms of the # GNU Affero General Public License version 3 (the [AGPL-3] License), in which # case the provisions of [AGPL-3] License are applicable instead of those above. # # If you wish to allow use of your version of this file only under the terms of # the [AGPL-3] License and not to allow others to use your version of this file # under the CPAL, indicate your decision by deleting the provisions above and # replace them with the notice and other provisions required by the [AGPL-3] # License. If you do not delete the provisions above, a recipient may use your # version of this file under either the CPAL or the [AGPL-3] License. import ../types import vmath except Quat, quat import ../util when defined(nimdoc): type TYPES* = LightType | Light # Forward declarations and ob type methods proc newLight*(engine: MyouEngine, name: string="", scene: Scene = nil, light_type: LightType = PointLight, color: Vec3 = vec3(1, 1, 1), energy: float32 = 1, cutoff_distance: float32 = 0, diffuse_factor: float32 = 1, specular_factor: float32 = 1, light_radius: float32 = 0.01, spot_size: float32 = 1.3, spot_blend: float32 = 0.15, use_shadow = false, ): Light proc instance_physics*(this: Light) method is_light*(self: GameObject): bool {.base.} = ## Return whether a GameObject is a Light return false method get_light*(self: GameObject): Light {.base.} = ## Get the Light object of a GameObject, or nil if it's not a light return nil method is_light*(this: Light): bool = ## Return whether a GameObject is a Light return true method get_light*(this: Light): Light = ## Get the Light object of a GameObject, or nil if it's not a light return this # 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, light_type: LightType = PointLight, color: Vec3 = vec3(1, 1, 1), energy: float32 = 1, cutoff_distance: float32 = 0, diffuse_factor: float32 = 1, specular_factor: float32 = 1, light_radius: float32 = 0.01, spot_size: float32 = 1.3, spot_blend: float32 = 0.15, use_shadow = false, ): Light = ## Create a new Light object. If you supply `scene` it will be added to that ## scene. var this = new Light discard procCall(this.GameObject.initGameObject(engine, name)) this.otype = TLight this.light_type = light_type this.color = color this.energy = energy this.light_radius = light_radius this.spot_size = spot_size this.spot_blend = spot_blend 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 when defined(myouDebugShadows): var debug_mesh: Mesh 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 objects: if not (ob.is_mesh and ob.visible): continue let me = ob.get_mesh let bb = me.bound_box if bb[0] == bb[1]: continue 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 if casters.len == 0: return when defined(myouDebugShadows): if debug_mesh.nonNil: debug_mesh.destroy() debug_mesh = self.engine.newMesh(vertex_count = 100, draw_method = Lines) debug_mesh.materials.add self.engine.newSolidMaterial("green", vec4(0,1,0,1)) debug_mesh.visible = true self.scene.add_object debug_mesh let hull = quickhull(casters) for f in hull: debug_mesh.add_vertex f.points[0], vec4(1) for v in f.points[1..^1]: debug_mesh.add_vertex v, vec4(1) debug_mesh.add_vertex v, vec4(1) debug_mesh.add_vertex f.points[0], vec4(1) debug_mesh.data.update_varray() 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