myou-engine/src/util.nim

226 lines
8.5 KiB
Nim

# 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 ./platform/gl
import vmath except Quat
import std/math
import std/algorithm
import arr_ref
template to_tuple*[T](v: GVec2[T]): (T, T) =
(v.x, v.y)
template to_tuple*[T](v: GVec3[T]): (T, T, T) =
(v.x, v.y, v.z)
template to_tuple*[T](v: GVec4[T]): (T, T, T, T) =
(v.x, v.y, v.z, v.w)
## Converts a 4x4 matrix to a 3x3 rotation matrix
## ignoring any scale and skew.
## If it's not orthogonal, the Z axis is preserved
## and the other two are made orthogonal.
func to_mat3_rotation*[T](m: GMat4[T]): GMat3[T] =
var x = m[0].xyz.normalize
var y = m[1].xyz.normalize
var z = m[2].xyz.normalize
# This favours the Z axis to preserve
# the direction of cameras and lights
x = cross(y,z)
y = cross(z,x)
return mat3(x,y,z)
func to_normal_matrix*[T](m: GMat4[T]): GMat3[T] =
# TODO: test: is it inverse.transpose or transpose.inverse?
m.to_mat3.inverse.transpose
func to_mat3*[T](m: GMat4[T]): GMat3[T] =
return mat3(m[0].xyz, m[1].xyz, m[2].xyz)
func to_mat4*[T](m: GMat3[T]): GMat4[T] =
return mat4(m[0].vec4, m[1].vec4, m[2].vec4, vec4(0,0,0,1))
func remove_scale_skew*[T](m: GMat4[T]): GMat4[T] =
result = m.to_mat3_rotation.to_mat4
result[3,0] = m[3,0]
result[3,1] = m[3,1]
result[3,2] = m[3,2]
proc remove*[T](s: var seq[T], element: T): bool {.raises:[],discardable.} =
## Removes a value from a seq if it exists. Returns whether an item was
## removed. Preserves the order of the other elements. If the element is
## repeated, only one instance is removed.
let index = s.find element
if index != -1:
# Since we only remove an existing element, it should never raise,
# but we have to use try/except to satisfy raises:[]
try: s.delete index
except: discard
return true
proc remove_unordered*[T](s: seq[T], element: T): bool {.raises:[],discardable.} =
## Removes a value from a seq if it exists. Returns whether an item was
## removed. It's quicker than `remove` by moving the last element to the slot
## of the removed one. If the element is repeated, only one instance is
## removed.
let index = s.find element
if index != -1:
# Since we only remove an existing element, it should never raise,
# but we have to use try/except to satisfy raises:[]
try: s.del index
except: discard
return true
func align4*[T: SomeInteger](n: T): T =
# Increments the number to the nearest multiplier of 4 if it's not already
# aligned.
n + ((4-(n and 3)) and 3)
func align*[T: SomeInteger](n, align: T): T =
## Increments the number to the nearest multiplier of the `align` parameter,
## if it's not already aligned. `align` must be a power of two.
let mask = align - 1
assert((align and mask) == 0, "align must be a power of two")
return n + ((align-(n and mask)) and mask)
func bytelen*[T](s: seq[T]): int =
## Provides the size of the seq in bytes.
s.len * sizeof(T)
func getOrDefault*[T](s: seq[T], i: int, default: T = T.default): T =
## Returns an element of the seq if the index is within bounds, otherwise it
## returns a default value, optionally given as argument.
if i >= s.low and i <= s.high:
return s[i]
return default
template rotate_cw*[T](v: GVec3[T]): GVec3[T] = gvec3[T](v.y, -v.x, v.z)
template rotate_ccw*[T](v: GVec3[T]): GVec3[T] = gvec3[T](-v.y, v.x, v.z)
template PGLfloat*[T](v: T): ptr GLfloat =
cast[ptr GLfloat](unsafeAddr v)
template get_ptr_len*[T](arr: seq[T]): (ptr UncheckedArray[T], int) =
if arr.len != 0:
(cast[ptr UncheckedArray[T]](addr arr[0]), arr.len)
else:
(nil, 0)
template error_enum_to_string*(e: GLenum): string =
case e:
of GL_NO_ERROR: "GL_NO_ERROR"
of GL_INVALID_ENUM: "GL_INVALID_ENUM"
of GL_INVALID_FRAMEBUFFER_OPERATION: "GL_INVALID_FRAMEBUFFER_OPERATION"
of GL_INVALID_INDEX: "GL_INVALID_INDEX"
of GL_INVALID_OPERATION: "GL_INVALID_OPERATION"
of GL_INVALID_VALUE: "GL_INVALID_VALUE"
else: $e
proc fibonacci_sphere*(samples: int): seq[Vec3] =
const phi: float32 = PI * (sqrt(5'f32) - 1) # golden angle in radians
for i in 0 ..< samples:
let y = (1 - (i / (samples - 1)) * 2) * (1 - 0.5'f32/samples.float32)
let radius = sqrt(1 - y * y) # radius at y
let theta = phi * i.float32 # golden angle increment
let x = cos(theta) * radius
let z = sin(theta) * radius
result.add vec3(x, y, z)
proc `|=`*(a: var GLbitfield, b: GLbitfield) =
a = (a.GLuint or b.GLuint).GLbitfield
when defined(opengl_es):
proc `not`*(x: GLbitfield): GLbitfield = (not x.GLuint).GLbitfield
proc `and`*(a: GLbitfield, b: GLbitfield): GLbitfield =
(a.GLuint and b.GLuint).GLbitfield
proc `$`*(x: GLenum): string = $(x.uint32)
func get_culling_planes*(mat: Mat4): array[6, Vec4] =
# Gribb/Hartmann method
[
mat[3] + mat[0], # left
mat[3] - mat[0], # right
mat[3] + mat[1], # bottom
mat[3] - mat[1], # top
mat[3] + mat[2], # near
mat[3] - mat[2], # far
]
template staticOrDebugRead*(path: string): string =
when not defined(release) and not
(defined(ios) or defined(android) or defined(emscripten)):
const dir = currentSourcePath.rsplit('/',1)[0] & '/'
readFile dir & path
else:
staticRead path
template nonNil*(x: untyped): bool = x != nil
# TODO: move to vmath fork
template low*[T](x: typedesc[GVec2[T]]): GVec2[T] = gvec2[T](T.low,T.low)
template low*[T](x: typedesc[GVec3[T]]): GVec3[T] = gvec3[T](T.low,T.low,T.low)
template low*[T](x: typedesc[GVec4[T]]): GVec4[T] = gvec4[T](T.low,T.low,T.low,T.low)
template high*[T](x: typedesc[GVec2[T]]): GVec2[T] = gvec2[T](T.high,T.high)
template high*[T](x: typedesc[GVec3[T]]): GVec3[T] = gvec3[T](T.high,T.high,T.high)
template high*[T](x: typedesc[GVec4[T]]): GVec4[T] = gvec4[T](T.high,T.high,T.high,T.high)
proc min*[T](v: GVec2[T]): T {.inline.} = min(v.x, v.y)
proc min*[T](v: GVec3[T]): T {.inline.} = min(min(v.x, v.y), v.z)
proc min*[T](v: GVec4[T]): T {.inline.} = min(min(min(v.x, v.y), v.z), v.w)
proc max*[T](v: GVec2[T]): T {.inline.} = max(v.x, v.y)
proc max*[T](v: GVec3[T]): T {.inline.} = max(max(v.x, v.y), v.z)
proc max*[T](v: GVec4[T]): T {.inline.} = max(max(max(v.x, v.y), v.z), v.w)
# bounding box operations
template `&`*[T](a, b: (GVec3[T], GVec3[T])): (GVec3[T], GVec3[T]) =
(min(a[0], b[0]), max(a[1], b[1]))
template `|`*[T](a, b: (GVec3[T], GVec3[T])): (GVec3[T], GVec3[T]) =
(max(a[0], b[0]), min(a[1], b[1]))
iterator box_corners*[T](bb: (GVec3[T], GVec3[T])): GVec3[T] =
let bb = cast[array[2, GVec3[T]]](bb)
for z in [0,1]:
for y in [0,1]:
for x in [0,1]:
yield gvec3[T](bb[x].x, bb[y].y, bb[z].z)