Add docstrings to gameobject, mesh, light, camera, scene, screen, attributes.

This commit is contained in:
Alberto Torres 2024-11-30 00:17:06 +01:00
parent 080d9bcc67
commit 893208f4c2
8 changed files with 326 additions and 32 deletions

View file

@ -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

View file

@ -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 =

View file

@ -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())

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)