diff --git a/src/attributes.nim b/src/attributes.nim index 5522437..604a0e9 100644 --- a/src/attributes.nim +++ b/src/attributes.nim @@ -39,6 +39,8 @@ import std/strformat import ./util func size*(dtype: DataType): int16 = + ## Size of a DataType in bytes + assert dtype != Unknown, "Can't use unknown type" case dtype: of Float, Int, UInt: 4 @@ -47,14 +49,14 @@ func size*(dtype: DataType): int16 = of Unknown: 0 func stride*(layout: AttributeList, align: int32 = 4): int32 = - # Stride of an attribute list, aligned to a byte boundary (default 4) + ## Stride of an attribute list, aligned to a byte boundary (default 4) for a in layout: result = max(result, a.offset + a.dtype.size * a.count) result = align(result, align) proc add*(layout: var AttributeList, attr: Attribute) = - # Adds an attribute to a seq setting the approriate location and offset - # (aligned to a 4 byte boundary) + ## Adds an attribute to a seq setting the approriate location and offset + ## (aligned to a 4 byte boundary) var attr = attr if layout.len != 0: let last = layout[layout.high] @@ -67,11 +69,13 @@ proc add*(layout: var AttributeList, attr: Attribute) = layout[^1] = attr proc contains*(layout: AttributeList, name: string): bool = + ## Returns whether an attribute with name `name` exists. for attr in layout: if attr.name == name: return true proc `[]`*(layout: AttributeList, name: string): Attribute = + ## Gets an attribute from a seq of attributes from its name. for attr in layout: if attr.name == name: return attr diff --git a/src/objects/camera.nim b/src/objects/camera.nim index 89943a9..b1a5270 100644 --- a/src/objects/camera.nim +++ b/src/objects/camera.nim @@ -51,12 +51,16 @@ import ./gameobject # import ../graphics/ubo method is_camera*(self: GameObject): bool {.base.} = + ## Return whether a GameObject is a Camera return false method get_camera*(self: GameObject): Camera {.base.} = + ## Get the Camera object of a GameObject, or nil if it's not a camera return nil method is_camera*(self: Camera): bool = + ## Return whether a GameObject is a Camera return true method get_camera*(self: Camera): Camera = + ## Get the Camera object of a GameObject, or nil if it's not a camera return self proc newCamera*(engine: MyouEngine, name: string="camera", scene: Scene = nil, @@ -69,6 +73,8 @@ proc newCamera*(engine: MyouEngine, name: string="camera", scene: Scene = nil, sensor_fit: SensorFit = Auto, shift: Vec2 = vec2(), ): Camera = + ## Create a new Camera object. If you supply `scene` it will be added to that + ## scene. var self = new Camera discard procCall(self.GameObject.initGameObject(engine, name)) self.otype = TCamera @@ -105,10 +111,16 @@ proc instance_physics*(self: Camera) = discard proc get_ray_direction*(self: Camera, x, y: float32): Vec3 = + ## Converts a viewport coordinate to a vector direction in world space + ## relative to the camera. The upper left corner of the viewport has + ## coordinates (0,0), and the lower right corner (1,1). return self.get_world_rotation * (self.projection_matrix_inverse * vec3(x * 2 - 1, 1 - y * 2, 1)) proc get_ray_direction_local*(self: Camera, x, y: float32): Vec3 = + ## Converts a viewport coordinate to a vector direction in local space + ## relative to the camera. The upper left corner of the viewport has + ## coordinates (0,0), and the lower right corner (1,1). assert self.rotation_order == Quaternion return self.rotation * (self.projection_matrix_inverse * vec3(x * 2 - 1, 1 - y * 2, 1)) @@ -122,6 +134,11 @@ proc get_ray_direction_local*(self: Camera, x, y: float32): Vec3 = # procCall(self.GameObject.look_at(target, options)) proc is_vertical_fit*(self: Camera): bool = + ## Returns whether `field_of_view` represents the vertical field of view + ## according to `sensor_fit`. If it's false, `field_of_view` represents the + ## horizontal FoV. + ## + ## Only applicable when both `field_of_view` and `sensor_fit` are used. return case self.sensor_fit: of Auto: self.aspect_ratio <= 1 of Horizontal: false @@ -130,6 +147,12 @@ proc is_vertical_fit*(self: Camera): bool = of Contain: self.aspect_ratio > self.target_aspect_ratio proc set_projection_matrix*(self: Camera, matrix: Mat4, adjust_aspect_ratio: bool = false) = + ## Sets a custom projection matrix to the camera, and optionally adjust the + ## aspect ratio if `adjust_aspect_ratio` is set to `true`. If you use this, + ## `field_of_view` won't be used. + ## + ## Note that the projection matrix will be reset if the viewport is resized, + ## so you may want to call this inside a resize event. self.projection_matrix = matrix if adjust_aspect_ratio: let m_ratio = matrix[0, 0] / matrix[1, 1] @@ -144,10 +167,19 @@ proc set_projection_matrix*(self: Camera, matrix: Mat4, adjust_aspect_ratio: boo self.calculate_culling_planes() proc update_projection*(self: Camera) = + ## Calculate projection matrices and culling planes from camera parameters. + ## + ## You may want to call this if you change any camera parameter. + ## + ## Called automatically on viewport creation and on resize events. self.calculate_projection() self.calculate_culling_planes() proc calculate_projection*(self: Camera) = + ## Calculate projection matrices from camera parameters. Called automatically + ## on viewport creation and on resize events. + ## + ## If you change any camera parameters, use `update_projection`_ instead. let near_plane = self.near_plane let far_plane = self.far_plane var top, right, bottom, left: float32 @@ -208,6 +240,10 @@ proc calculate_projection*(self: Camera) = self.projection_matrix_inverse = inverse(self.projection_matrix) proc calculate_culling_planes*(self: Camera) = + ## Calculate culling planes from the projection matrix. Called automatically + ## on viewport creation, on resize events, and with `set_projection_matrix`_. + ## + ## If you change any camera parameters, use `update_projection`_ instead. self.cull_planes = self.projection_matrix.get_culling_planes() proc get_oblique_projection_matrix_into*(self: Camera, clip_plane: Vec4): Mat4 = diff --git a/src/objects/cubemap_probe.nim b/src/objects/cubemap_probe.nim index 1f0196f..b48d59c 100644 --- a/src/objects/cubemap_probe.nim +++ b/src/objects/cubemap_probe.nim @@ -53,12 +53,16 @@ proc render_cubemap*(self: CubemapProbe, use_roughness_prefiltering = false, mip proc render_background_cubemap*(scene: Scene, use_roughness_prefiltering = false, mipmap_shader: Material = nil, world_to_cube_matrix: Mat4 = mat4(), upload_UBO = true) method is_cubemap_probe*(self: GameObject): bool {.base.} = + ## Return whether a GameObject is a CubemapProbe return false method get_cubemap_probe*(self: GameObject): CubemapProbe {.base.} = + ## Get the CubemapProbe object of a GameObject, or nil if it's not a cubemap probe return nil method is_cubemap_probe*(self: CubemapProbe): bool = + ## Return whether a GameObject is a CubemapProbe return true method get_cubemap_probe*(self: CubemapProbe): CubemapProbe = + ## Get the CubemapProbe object of a GameObject, or nil if it's not a cubemap probe return self # End forward declarations and ob type methods @@ -82,6 +86,8 @@ proc newCubemapProbe*(engine: MyouEngine, name: string="camera", scene: Scene = parallax_type: ProbeParallaxType = NoParallax, parallax_distance: float32 = 0.0, # 0 means auto ): CubemapProbe = + ## Create a new Cubemap Probe object. If you supply `scene` it will be added + ## to that scene. var self = new CubemapProbe discard procCall(self.GameObject.initGameObject(engine, name)) self.ubo_index = -1 @@ -148,6 +154,7 @@ template get_roughness_prefilter(engine: MyouEngine): Material = roughness_prefilter proc render_cubemap*(self: CubemapProbe, use_roughness_prefiltering = false, mipmap_shader: Material = nil) = + ## Updates the contents of the cubemap by rendering the scene on each side of the cubemap. if self.ubo_index == -1: self.scene.ensure_cubemaps() let cube2world = self.world_matrix * scale(vec3(self.influence_distance)) @@ -181,6 +188,7 @@ proc render_cubemap*(self: CubemapProbe, use_roughness_prefiltering = false, mip self.cubemap.disable() proc render_background_cubemap*(scene: Scene, use_roughness_prefiltering = false, mipmap_shader: Material = nil, world_to_cube_matrix: Mat4 = mat4(), upload_UBO = true) = + ## Updates the contents of the background cubemap by rendering the world material on each side of the cubemap. if scene.background_cubemap == nil: scene.ensure_cubemaps() if not defined(release): @@ -204,9 +212,9 @@ proc render_background_cubemap*(scene: Scene, use_roughness_prefiltering = false if upload_UBO: scene.cubemap_UBO.update() -proc generate_cubemap_mipmaps*(scene: Scene, cubemap: Framebuffer, use_roughness_prefiltering = false, mipmap_shader: Material = nil) = - let mipmap_shader = if use_roughness_prefiltering and mipmap_shader == nil: - scene.engine.get_roughness_prefilter() - else: - mipmap_shader - cubemap.generate_mipmap(mipmap_shader) +proc generate_cubemap_mipmaps_with_roughness_prefiltering*(scene: Scene, cubemap: Framebuffer) = + ## Generates cubemap.generate_mipmap but using the roughness prefilter. This + ## is called automatically when rendering cubemaps with + ## `use_roughness_prefiltering = true`. Use it only when you write data to + ## the cubemap manually. + cubemap.generate_mipmap(scene.engine.get_roughness_prefilter()) diff --git a/src/objects/gameobject.nim b/src/objects/gameobject.nim index 20788e9..024414c 100644 --- a/src/objects/gameobject.nim +++ b/src/objects/gameobject.nim @@ -144,26 +144,40 @@ proc initGameObject*(self: GameObject, engine: MyouEngine, name: string="", scen return self proc newGameObject*(engine: MyouEngine, name: string="", scene: Scene=nil): GameObject = - result = new GameObject + ## Create a new GameObject. If you supply `scene` it will be added to that + ## scene. + new(result) return initGameObject(result, engine, name, scene) proc show*(self: GameObject, recursive: static[bool] = true) = + ## Enable visibility of the object, its children, their children and so on. + ## If you set `recursive = false`, only the visibility of the object will be + ## changed. self.visible = true when recursive: for c in self.children: c.show(recursive) proc hide*(self: GameObject, recursive: static[bool] = true) = + ## Disable visibility of the object, its children, their children and so on. + ## If you set `recursive = false`, only the visibility of the object will be + ## changed. self.visible = false when recursive: for c in self.children: c.hide(recursive) proc set_visibility*(self: GameObject, visible: bool, recursive: static[bool] = true) = + ## Set the visibility of the object and its descendants to the value of the + ## argument `visible`. If you set `recursive = false`, only the visibility of + ## the object will be changed. if visible: self.show(recursive) else: self.hide(recursive) proc update_matrices*(self: GameObject) = + ## Calculate the matrices of the object from its position, rotation, scale, + ## and parent matrix. It assumes the parent object and/or bone has its world + ## matrix already up to date. var q = if self.rotation_order == Quaternion: self.rotation else: @@ -214,11 +228,14 @@ proc update_matrices*(self: GameObject) = return proc update_matrices_recursive(self: GameObject) = + ## Calculate the matrices of the object from its position, rotation, scale, + ## and parent. It will update the parent matrices first if it has a parent. if self != nil: self.parent.update_matrices_recursive() self.update_matrices() proc set_rotation_order*(self: GameObject, order: RotationOrder) = + ## Change the rotation mode and order of the object. if order == self.rotation_order: return var q = self.rotation @@ -237,11 +254,14 @@ proc set_rotation_order*(self: GameObject, order: RotationOrder) = self.rotation_order = order proc get_world_matrix*(self: GameObject): Mat4 = + ## Calculates and returns the world matrix. # TODO: use dirty flag self.update_matrices_recursive() return self.world_matrix proc get_local_matrix*(self: GameObject): Mat4 = + ## Calculates and returns the transformation matrix in local space. If the + ## object has no parent, this is equivalent to the world matrix. var q = if self.rotation_order == Quaternion: self.rotation else: @@ -271,16 +291,19 @@ proc get_local_matrix*(self: GameObject): Mat4 = ) proc get_world_position*(self: GameObject): Vec3 = + ## Calculates and returns the position in world space. if self.parent == nil: return self.position return self.get_world_matrix[3].xyz proc get_world_rotation*(self: GameObject): Quat = + ## Calculates and returns the rotation in world space as a quaternion. let wm = self.get_world_matrix # TODO: Calculate rotation matrix more efficiently (dirty flag?) return wm.to_mat3_rotation.to_quat proc get_world_position_rotation*(self: GameObject): (Vec3, Quat) = + ## Calculates and returns the position and rotation in world space. let wm = self.get_world_matrix let position: Vec3 = wm[3].xyz # TODO: Calculate rotation matrix more efficiently (dirty flag?) @@ -289,6 +312,9 @@ proc get_world_position_rotation*(self: GameObject): (Vec3, Quat) = return (position, rotation) proc translate*(self: GameObject, vector: Vec3, relative_object: GameObject=nil): GameObject {.discardable.} = + ## Translate (move) the object by adding the vector `vector` to its position, + ## either relative to the scene, or relative to the specified object in + ## `relative_object` var vector = vector if relative_object != nil: vector = relative_object.get_world_rotation() * vector @@ -299,12 +325,18 @@ proc translate*(self: GameObject, vector: Vec3, relative_object: GameObject=nil) return self proc translate_x*(self: GameObject, x: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.} = + ## Translate (move) the object along the X axis of the scene, or the X axis + ## of the specified object in `relative_object` return self.translate(vec3(x, 0, 0), relative_object) proc translate_y*(self: GameObject, y: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.} = + ## Translate (move) the object along the Y axis of the scene, or the Y axis + ## of the specified object in `relative_object` return self.translate(vec3(0, y, 0), relative_object) proc translate_z*(self: GameObject, z: SomeFloat, relative_object: GameObject=nil): GameObject {.discardable.} = + ## Translate (move) the object along the Z axis of the scene, or the Z axis + ## of the specified object in `relative_object` return self.translate(vec3(0, 0, z), relative_object) # proc rotate_euler*(self: GameObject, vector: Vec3, relative_object: GameObject=nil): GameObject = @@ -317,6 +349,10 @@ proc translate_z*(self: GameObject, z: SomeFloat, relative_object: GameObject=ni # self.rotate_quat(q, relative_object) proc rotate_quat*(self: GameObject, q: Quat, relative_object: GameObject=nil): GameObject {.discardable.} = + # Rotate the object by applying (multiplying) the quaternion `q`, either + # relative to the scene, or relative to the specified object in + # `relative_object` + # TODO: optimize (dirty flag?) var rel = quat() var inv_rel = quat() @@ -338,28 +374,42 @@ proc rotate_quat*(self: GameObject, q: Quat, relative_object: GameObject=nil): G return self proc rotate_x*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) = + ## Rotate the object around the X axis of the scene, or the X axis of the + ## specified object in `relative_object`. The angle is in radians. self.rotate_quat(rotateX(angle.float32).to_mat3.to_quat, relative_object) proc rotate_y*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) = + ## Rotate the object around the Y axis of the scene, or the Y axis of the + ## specified object in `relative_object`. The angle is in radians. self.rotate_quat(rotateY(angle.float32).to_mat3.to_quat, relative_object) proc rotate_z*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) = + ## Rotate the object around the Z axis of the scene, or the Z axis of the + ## specified object in `relative_object`. The angle is in radians. self.rotate_quat(rotateZ(angle.float32).to_mat3.to_quat, relative_object) proc rotate_x_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) = + ## Rotate the object around the X axis of the scene, or the X axis of the + ## specified object in `relative_object`. The angle is in degrees. self.rotate_quat(rotateX(angle.float32.to_radians).to_mat3.to_quat, relative_object) proc rotate_y_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) = + ## Rotate the object around the Y axis of the scene, or the Y axis of the + ## specified object in `relative_object`. The angle is in degrees. self.rotate_quat(rotateY(angle.float32.to_radians).to_mat3.to_quat, relative_object) proc rotate_z_deg*(self: GameObject, angle: SomeFloat, relative_object: GameObject=nil) = + ## Rotate the object around the Z axis of the scene, or the Z axis of the + ## specified object in `relative_object`. The angle is in degrees. self.rotate_quat(rotateZ(angle.float32.to_radians).to_mat3.to_quat, relative_object) proc set_world_position_rotation*( self: GameObject, position: Vec3, rotation: Quat) = - + ## Changes the position and the rotation of the object to match the supplied + ## position and rotation in world space. + if self.parent != nil: # TODO: optimize and use less memory? # TODO: does it work well with parents? @@ -372,9 +422,13 @@ proc set_world_position_rotation*( self.rotation_order = Quaternion proc set_world_position*(self: GameObject, position: Vec3) = + ## Changes the position of the object to match the supplied position in world + ## space. self.set_world_position_rotation(position, self.get_world_rotation()) proc set_world_rotation*(self: GameObject, rotation: Quat) = + ## Changes the rotation of the object to match the supplied rotation in world + ## space. self.set_world_position_rotation(self.get_world_position(), rotation) type LookAtAxis* = enum @@ -530,6 +584,9 @@ proc destroy*(self: GameObject, recursive: bool = true) = self.object_ubo.destroy() proc convert_bone_child_to_bone_parent*(self: GameObject) = + ## Turns an object that is parented to a bone, into the parent of the same + ## bone. It disconnects the bone from its former parent, if any. Used + ## internally to create ragdolls. assert self.parent == nil or self.parent.is_armature, "An armature parent is required" var parent = self.parent.get_armature if not (parent != nil and (self.parent_bone_index) >= 0): @@ -635,6 +692,7 @@ proc set_world_size*(self: GameObject, size: SomeFloat) = return proc set_name*(self: GameObject, name: string) = + ## Rename the object and update the tables that use it. assert self.scene != nil, "Object has no scene" self.engine.objects.del(self.name) self.scene.objects.del(self.name) diff --git a/src/objects/light.nim b/src/objects/light.nim index 091e3c7..b8e66d1 100644 --- a/src/objects/light.nim +++ b/src/objects/light.nim @@ -55,12 +55,16 @@ proc newLight*(engine: MyouEngine, name: string="", 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 @@ -84,6 +88,8 @@ proc newLight*(engine: MyouEngine, name: string="", 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 @@ -108,15 +114,15 @@ 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. + ## 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 @@ -162,7 +168,6 @@ proc configure_shadow*(self: Light, break casts = not all_above if casts: - echo "adding casting ", ob.name for v in box_corners(bb): casters.add ob.world_matrix * v diff --git a/src/objects/mesh.nim b/src/objects/mesh.nim index bcd3b7f..c9e22c2 100644 --- a/src/objects/mesh.nim +++ b/src/objects/mesh.nim @@ -49,7 +49,6 @@ proc load_from_va_ia*(self: Mesh, index_types: seq[DataType] = @[]) proc generate_tangents*(self: MeshData, uv_layer_name, tangent_layer_name: string) proc ensure_capacity*(self: Mesh, extra_elements: int) -proc write_vaos*(self: MeshData) # End forward declarations and ob type methods @@ -72,20 +71,27 @@ export tables import ../platform/gl method is_mesh*(self: GameObject): bool {.base.} = + ## Return whether a GameObject is a Mesh return false method get_mesh*(self: GameObject): Mesh {.base.} = + ## Get the Mesh object of a GameObject, or nil if it's not a mesh return nil method is_mesh*(self: Mesh): bool = + ## Return whether a GameObject is a Mesh return true method get_mesh*(self: Mesh): Mesh = + ## Get the Mesh object of a GameObject, or nil if it's not a mesh return self proc newMeshData(engine: MyouEngine): MeshData = + ## Creates a new MeshData. It's recommended to use newMesh arguments instead. result = new MeshData result.draw_method = Triangles result.engine = engine proc remove*(self: MeshData, ob: Mesh, delete_buffers: bool = true) = + ## Removes this MeshData from a mesh object, and deletes itself if it doesn't + ## have any more users. var idx = self.users.find(ob) while idx != -1: self.users.delete(idx) @@ -94,7 +100,7 @@ proc remove*(self: MeshData, ob: Mesh, delete_buffers: bool = true) = self.engine.mesh_datas.del(self.hash) ob.data = nil -proc write_vaos*(self: MeshData) = +proc write_vaos(self: MeshData) = for i,vao in self.vaos: if vao.int == 0: continue @@ -209,6 +215,7 @@ proc gpu_buffers_upload*(self: MeshData) = # set loaded to true after that proc gpu_buffers_delete*(self: MeshData) = + ## Removes GPU buffers. For internal use. Use GameObject.destroy() instead. glBindVertexArray(0) self.vaos = @[] self.tf_vaos = @[] @@ -222,6 +229,8 @@ proc gpu_buffers_delete*(self: MeshData) = self.engine.mesh_datas.del(self.hash) proc update_varray*(self: MeshData) = + ## Update vertex GPU buffers with the contents of vertex arrays. Call this + ## after updating vertex arrays. if self.vertex_buffers.len == 0: return for i,va in self.varrays: @@ -229,6 +238,8 @@ proc update_varray*(self: MeshData) = glBufferSubData(GL_ARRAY_BUFFER, 0.GLintptr, cast[GLsizeiptr](va.bytelen), addr va[0]) proc update_iarray*(self: MeshData) = + ## Update index GPU buffers with the contents of vertex arrays. Call this + ## after updating index arrays. if self.index_buffers.len == 0: return for i,ia in self.iarrays: @@ -241,7 +252,7 @@ proc clone*(self: MeshData): MeshData = d.users = @[] return d -proc initMesh*(self: Mesh, engine: MyouEngine, name: string, scene: Scene = nil, +proc initMesh(self: Mesh, engine: MyouEngine, name: string, scene: Scene = nil, draw_method: MeshDrawMethod = Triangles, common_attributes: CommonMeshAttributes = {vertex, color}, layout: AttributeList = @[], @@ -302,14 +313,42 @@ proc newMesh*(engine: MyouEngine, name: string="mesh", scene: Scene = nil, index_array: seq[uint16] = @[], pass: int32 = 0, ): Mesh = - result = new Mesh + ## Create a new Mesh object. If you supply `scene` it will be added to that + ## 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. + + # TODO: document pass and skip_upload + new(result) result.otype = TMesh return initMesh(result, engine, name, scene, draw_method, common_attributes, layout, skip_upload, vertex_count, vertex_array, index_array, pass) proc add_vertex*(self: Mesh, x, y, z: float32, r, g, b, a: uint8): int {.discardable.} = + ## Tries to add a vertex to the mesh with the specified position and color. + ## It will fill an unused vertex if any. Returns the vertex index, or -1 if + ## the vertex could not be added. + ## + ## At the moment you need to call mesh.data.update_varray() after adding + ## vertices. It's most efficient when only doing it once. + # TODO: Check it actually has a color attribute!! + # TODO: ensure it doesn't have a, index array? var varray = self.data.varrays[0] var varray_byte = varray.to(uint8) let stride = self.data.stride @@ -331,18 +370,38 @@ proc add_vertex*(self: Mesh, x, y, z: float32, r, g, b, a: uint8): int {.discard return index+1 proc add_vertex*(self: Mesh, x, y, z, r, g, b, a: SomeFloat): int {.discardable.} = + ## Tries to add a vertex to the mesh with the specified position and color. + ## It will fill an unused vertex if any. Returns the vertex index, or -1 if + ## the vertex could not be added. + ## + ## At the moment you need to call mesh.data.update_varray() after adding + ## vertices. It's most efficient when only doing it once. return self.add_vertex(x, y, z, (r*255).uint8, (g*255).uint8, (b*255).uint8, (a*255).uint8) -proc add_vertex*(self: Mesh, position: Vec3, color: Vec4): int {.discardable.} = +proc add_vertex*(self: Mesh, position: Vec3, color: Vec4 = vec4(1)): int {.discardable.} = + ## Tries to add a vertex to the mesh with the specified position and color. + ## It will fill an unused vertex if any. Returns the vertex index, or -1 if + ## the vertex could not be added. + ## + ## At the moment you need to call mesh.data.update_varray() after adding + ## vertices. It's most efficient when only doing it once. let (x,y,z) = position.to_tuple let (r,g,b,a) = color.to_tuple return self.add_vertex(x, y, z, r, g, b, a) proc clear_vertices*(self: Mesh) = + ## Removes all vertices of the mesh by marking them as unused. You don't need + ## to call update_varray() self.data.num_indices[0] = 0 proc remove_vertex*(self: Mesh, index: int) = - # TODO: This is only valid for points mode + ## Removes a vertices of the mesh by marking it as unused. At the moment it + ## only works in points mode. + ## + ## At the moment you need to call mesh.data.update_varray() after adding or + ## removing vertices. It's most efficient when only doing it once. + + # TODO: Remove a whole line/triangle in line/triangle mode. if not (index >= 0): return # ignore -1 assert self.data.num_indices[0] > index, "Invalid index" @@ -356,6 +415,8 @@ proc remove_vertex*(self: Mesh, index: int) = self.data.num_indices[0] -= 1 proc ensure_capacity*(self: Mesh, extra_elements: int) = + ## Checks if there's enough space for the desired number of elements to be + ## added, and resizes the mesh if it's needed (by a factor of two). if self.data == nil: let stride = self.layout.stride assert stride != 0 @@ -395,6 +456,11 @@ proc load_from_va_ia*(self: Mesh, iarrays: seq[ArrRef[uint16]] | seq[ArrRef[int32]] = newSeq[ArrRef[uint16]](), layout: AttributeList = @[], index_types: seq[DataType] = @[]) = + ## Loads the mesh to the GPU from the given vertex and index arrays, layout + ## and index type. It's recommended to use newMesh arguments instead. + ## + ## This version takes multiple vertex and index arrays, which can have 16 or + ## 32 bit indices. if self.data != nil: self.data.remove(self) @@ -446,9 +512,16 @@ proc load_from_va_ia*(self: Mesh, varray: seq[float32], iarray: seq[uint16] = @[], layout: AttributeList = @[]) = + ## Loads the mesh to the GPU from the given vertex and index arrays and + ## layout. It's recommended to use newMesh arguments instead. + ## + ## This version takes a single vertex array and an optional index array, with + ## 16 bit indices. self.load_from_va_ia(@[newArrRef varray], @[newArrRef iarray], layout) proc add_modifier*(self: Mesh, modifier: VertexModifier) = + ## Add a vertex modifier to the mesh, and changes the mesh data if necessary. + ## It's added to the end of the stack. self.vertex_modifiers.add(modifier) if modifier.prepare_mesh.nonNil and self.data.nonNil: echo &"applying modifiers of {self.name} after it was already loaded" @@ -456,6 +529,8 @@ proc add_modifier*(self: Mesh, modifier: VertexModifier) = # self.update_signature() proc insert_modifier*(self: Mesh, index: int, modifier: VertexModifier) = + ## Insert a vertex modifier to the mesh (at position `index`), and changes + ## the mesh data if necessary. self.vertex_modifiers.insert(modifier, index) if modifier.prepare_mesh.nonNil and self.data.nonNil: echo &"applying modifiers of {self.name} after it was already loaded" @@ -463,10 +538,18 @@ proc insert_modifier*(self: Mesh, index: int, modifier: VertexModifier) = # self.update_signature() proc remove_modifier*(self: Mesh, index: int) = + ## Removes a vertex modifier from the mesh at the given index. + ## + ## Note that currently if the modifier changed the mesh, it will remain + ## changed when removing the modifier. self.vertex_modifiers.delete(index) # self.update_signature() proc remove_modifier*(self: Mesh, modifier: VertexModifier) = + ## Removes the given vertex modifier from the mesh. + ## + ## Note that currently if the modifier changed the mesh, it will remain + ## changed when removing the modifier. let index = self.vertex_modifiers.find(modifier) if index != -1: self.remove_modifier index @@ -491,12 +574,25 @@ proc clone*(self: Mesh, return self.clone_impl(n) proc sort_faces*(self: Mesh, camera_position: Vec3) = + ## TODO: unimplemented return # TODO: support multiple index types with a generic? # NOTE: doing it using the triangles and not the original polygons might be # slightly inaccurate, unless mesh is trianglulated when baking proc generate_tangents*(self: MeshData, uv_layer_name, tangent_layer_name: string) = + ## Fills the given tangent attribute with tangent vectors based on the given + ## UV layer. + ## + ## Intended to be indirectly used by a mesh loader: + ## + ## * Reserving an attribute with a name starting by "tg_". + ## * With the same name as a UV layer minus the initial "uv_". For example + ## "tg_UVMap" will contain the tangents for "uv_UVMap". + ## * Then setting `mesh.generate_tangents` to `true` + ## + ## If the flag is set, this will be called automatically for each attribute + ## pair. if self.varrays.len != self.iarrays.len: raise newException(ValueError, "Function generate_tangents() doesn't support meshes without indices at the moment") @@ -592,6 +688,8 @@ proc generate_tangents*(self: MeshData, uv_layer_name, tangent_layer_name: strin break proc update_bounding_box*(self: Mesh) = + ## Calculates the local bounding box of the mesh and stores it in + ## `mesh.bound_box`. if self.data == nil or self.layout.len == 0: return # TODO: byte vertices @@ -611,6 +709,10 @@ proc update_bounding_box*(self: Mesh) = self.center = mix(minv, maxv, 0.5) proc debug_print_vertices*(self: Mesh, starts: int = 0, ends: int = 10) = + ## Debugging helper for mesh format loaders. It prints the values of each + ## attribute of vertices starting by index `starts` and ending before index + ## `ends` (not included). If you don't specify range, it will print the first + ## 10 vertices. if self.data.varrays[0].len == 0: return let varray = cast[ptr UncheckedArray[int8]](self.data.varrays[0][0].addr) @@ -635,6 +737,11 @@ proc debug_print_vertices*(self: Mesh, starts: int = 0, ends: int = 10) = echo &"{attr.name} {data}" proc add_polygonal_line*(self: Mesh, orig, dest: Vec3, width: float) = + ## Adds a line made of a quad, from `orig` to `dest` with a given width. The + ## quad will face up the Z axis. If the line starts where the previous line + ## ended, it will change both ends to match (unless the angle is under ~37°). + ## + ## It requires the mesh to have draw method TriangleStrip. let last = self.last_polyline_point let last_left = self.last_polyline_left let has_last = self.data.num_indices[0] != 0 diff --git a/src/scene.nim b/src/scene.nim index 4a4a32b..b53229e 100644 --- a/src/scene.nim +++ b/src/scene.nim @@ -119,6 +119,7 @@ proc newScene*(engine: MyouEngine, name: string = "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 @@ -131,6 +132,7 @@ 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 @@ -191,6 +193,7 @@ proc add_object*(self: Scene, ob: GameObject, 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 @@ -235,6 +238,8 @@ proc remove_object*(self: Scene, ob: GameObject, recursive: bool = true) = 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) @@ -281,8 +286,10 @@ proc make_parent*(self: Scene, parent: GameObject, child: GameObject, self.children_are_ordered = false proc clear_parent*(self: Scene, child: GameObject, keep_transform = true) = - let parent = child.parent - if parent != nil: + ## 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 @@ -297,13 +304,16 @@ proc clear_parent*(self: Scene, child: GameObject, keep_transform = true) = scale.y = world_matrix[1].xyz.length.copy_sign wm3[1,1] scale.z = world_matrix[2].xyz.length.copy_sign wm3[2,2] child.set_rotation_order(rotation_order) - parent.children.remove child + child.parent.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 @@ -320,6 +330,8 @@ proc reorder_children*(self: Scene) = 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 @@ -337,11 +349,15 @@ proc update_all_matrices*(self: Scene) = 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: @@ -378,17 +394,21 @@ proc destroy*(self: Scene) = 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, @@ -402,10 +422,32 @@ proc new_mesh*(self: Scene, name: string, 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 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" @@ -416,6 +458,9 @@ proc set_active_camera*(self: Scene, camera: 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 @@ -445,6 +490,10 @@ proc calculate_max_lights_and_cubemaps*(self: Scene) = # 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: @@ -475,6 +524,8 @@ proc get_lighting_UBOs*(self: Scene): seq[UBO] = 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), @@ -485,6 +536,9 @@ proc get_lighting_code_defines*(self: Scene): seq[string] = ] 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 @@ -567,6 +621,7 @@ proc get_lighting_code*(self: Scene): string = 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 @@ -627,7 +682,9 @@ proc update_lights*(self: Scene) = self.sun_light_UBO.update() proc sort_cubemaps*(self: Scene) = - # sort by volume + ## 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: @@ -643,7 +700,8 @@ proc sort_cubemaps*(self: Scene) = # or share depth buffers? proc ensure_cubemaps*(self: Scene) = - # echo "ensuring cubemaps" + ## Creates any missing cube maps. Called automatically. + let res = self.cubemap_resolution if res == 0: return @@ -676,10 +734,13 @@ proc ensure_cubemaps*(self: Scene) = # 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) diff --git a/src/screen.nim b/src/screen.nim index ca41916..b9fd4d1 100644 --- a/src/screen.nim +++ b/src/screen.nim @@ -52,6 +52,8 @@ import ./input import ./util proc newScreen*(engine: MyouEngine, width, height: int32, title: string): Screen = + ## Creates a new screen or window. The first one is created by the engine for you. + result = new Screen result.engine = engine result.width = width @@ -71,11 +73,17 @@ proc newScreen*(engine: MyouEngine, width, height: int32, title: string): Screen result.frame_interval = 1 proc destroy*(self: Screen) = + ## Destroys the screen/window and all its resources. If you destroy the main + ## screen, the first screen created after it will become the main screen. If + ## there are no screens left, the engine will exit. + self.engine.screens.remove self if self.engine.screen == self and self.engine.screens.len != 0: self.engine.screen = self.engine.screens[0] proc resize*(self: Screen, width, height: int32, orientation = self.orientation) = + ## Resize the screen/window and all its viewports. + self.width = width self.height = height self.orientation = orientation @@ -95,6 +103,8 @@ proc resize*(self: Screen, width, height: int32, orientation = self.orientation) f(self) proc add_viewport*(self: Screen, camera: Camera) = + ## Add a viewport to the screen with a given camera. + let vp = new Viewport vp.camera = camera vp.clear_color = true @@ -104,14 +114,19 @@ proc add_viewport*(self: Screen, camera: Camera) = self.resize(self.width, self.height) proc `vsync=`*(self: Screen, vsync: bool) = + ## Change the vsync setting of the screen/window. cast[Window](self.window).set_vsync vsync proc get_ray_direction*(viewport: Viewport, position: Vec2): Vec3 = + ## Calculates a vector that points from the camera towards the given screen + ## coordinates, in world space. let x = (position.x - viewport.rect_pix[0].float32) / viewport.rect_pix[2].float32 let y = (position.y - viewport.rect_pix[1].float32) / viewport.rect_pix[3].float32 return viewport.camera.get_ray_direction(x,y) proc get_ray_direction_local*(viewport: Viewport, position: Vec2): Vec3 = + ## Calculates a vector that points from the camera towards the given screen + ## coordinates, in camera space. let x = (position.x - viewport.rect_pix[0].float32) / viewport.rect_pix[2].float32 let y = (position.y - viewport.rect_pix[1].float32) / viewport.rect_pix[3].float32 return viewport.camera.get_ray_direction_local(x,y)