myou-engine/src/objects/mesh.nim
Alberto Torres cea7df6947 First commit.
* Incomplete port of myou-engine-js to nimskull, after many months of work, and
  a few extra features that weren't exactly necessary for a "first commit" to
  work. Excuse the lack of commit history up to this point.
* Bare bones structure of the documentation and the process to update it.
* Restructure of the whole project to have a more sensible organization.
* Making submodules of forks of larger libraries.
* README, licenses, AUTHORS.md.
2024-08-20 13:08:19 +02:00

1064 lines
41 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.
##
# TODO: Split mesh object and mesh data
import ../types
import vmath except Quat
import arr_ref
when defined(nimdoc):
type TYPES* = Mesh | MeshData | MeshDrawMethod | SortSign
# Forward declarations and ob type methods
proc load_from_va_ia*(self: Mesh,
varrays: seq[ArrRef[float32]],
iarrays: seq[ArrRef[uint16]] | seq[ArrRef[int32]] = newSeq[ArrRef[uint16]](),
layout: AttributeList = @[],
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
import elvis
import ../quat
import std/algorithm
# import std/sequtils
import std/math
import std/tables
import std/strformat
import std/strutils
import ./gameobject
import ../scene
import ../util
import ../attributes
export arr_ref
export tables
# TODO: move MeshData elsewhere
import ../platform/gl
method is_mesh*(self: GameObject): bool {.base.} =
return false
method get_mesh*(self: GameObject): Mesh {.base.} =
return nil
method is_mesh*(self: Mesh): bool =
return true
method get_mesh*(self: Mesh): Mesh =
return self
proc newMeshData(engine: MyouEngine): MeshData =
result = new MeshData
result.draw_method = Triangles
result.engine = engine
proc remove*(self: MeshData, ob: Mesh, delete_buffers: bool = true) =
var idx = self.users.find(ob)
while idx != -1:
self.users.delete(idx)
idx = self.users.find(ob)
if self.users.len == 0:
# if delete_buffers:
# glBindBuffer(GL_ARRAY_BUFFER, 0)
# for buf in self.vertex_buffers:
# glDeleteBuffers(1, addr buf)
# for buf in self.index_buffers:
# glDeleteBuffers(1, addr buf)
# glBindVertexArray(0)
# for vao in self.vaos:
# glDeleteVertexArrays(1, addr vao)
self.engine.mesh_datas.del(self.hash)
ob.data = nil
proc write_vaos*(self: MeshData) =
for i,vao in self.vaos:
if vao == 0:
continue
glBindVertexArray(vao)
for vb_attrs in self.vao_specs[i].vbs.mitems:
glBindBuffer(GL_ARRAY_BUFFER, vb_attrs.vb)
for a in vb_attrs.attribs:
glEnableVertexAttribArray(a.location.GLuint)
glVertexAttribPointer(a.location.GLuint, a.count.GLint, a.dtype.GLenum, false,
self.stride.GLsizei, cast[pointer](cast[int](a.offset)))
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vao_specs[i].ib)
when defined(myouUseRenderdoc):
let name = &"{self.name} {i}"
glObjectLabel(GL_VERTEX_ARRAY, vao, GLsizei(name.len), name.cstring)
glBindVertexArray(0)
proc gpu_buffers_upload*(self: MeshData) =
var num_submeshes = self.varrays.len
self.vertex_starts.setLen(0)
let had_indices = self.num_indices.len != 0
if had_indices:
self.num_indices.setLen num_submeshes
self.index_types.setLen num_submeshes
for idx,va in self.varrays:
if va.len == 0:
self.num_indices.add(0)
self.vertex_buffers.add(0)
self.index_buffers.add(0)
self.vaos.add(0)
self.vao_specs.add VaoSpec()
continue
var vb: GLuint
var tf: array[2, GLuint]
glGenBuffers(1, addr vb)
glBindBuffer(GL_ARRAY_BUFFER, vb)
let buffer_size = va.bytelen
# TODO: initialize without uploading when we know that the array is just zeroes
glBufferData(GL_ARRAY_BUFFER, buffer_size.GLsizeiptr, addr va[0], GL_DYNAMIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, 0)
self.vertex_buffers.add(vb)
var ib: GLuint = 0
var num_indices: Natural
if idx < self.iarrays.len:
let ia = self.iarrays[idx]
num_indices = ia.len
if self.index_types[idx] == Unknown:
# deduce index type from vertex buffer length
# TODO: move this logic to loader?
self.index_types[idx] = if num_indices >= 65536: UInt else: UShort
if self.index_types[idx] == UInt:
num_indices = num_indices div 2
else:
assert self.index_types[idx] == UShort, "Index type must be UInt or UShort"
glGenBuffers(1, addr ib)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib)
let buffer_size = ia.bytelen
glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer_size.GLsizeiptr, nil, GL_STATIC_DRAW)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer_size.GLsizeiptr, addr ia[0], GL_STATIC_DRAW)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
if not had_indices:
self.num_indices.add(num_indices.int32)
elif not had_indices:
self.num_indices.add((buffer_size div (self.stride ?: 0)).int32)
self.index_buffers.add(ib)
if self.use_tf:
self.tf_vbos.setLen 2
glGenBuffers(2, addr tf[0])
let tf_buffer_size = if self.draw_method == Points or num_indices == 0:
(buffer_size div self.stride) * self.tf_layout.stride
else:
self.iarrays[idx].len * self.tf_layout.stride
glBindBuffer(GL_ARRAY_BUFFER, tf[0])
glBufferData(GL_ARRAY_BUFFER, tf_buffer_size.GLsizeiptr, nil, GL_DYNAMIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, tf[1])
glBufferData(GL_ARRAY_BUFFER, tf_buffer_size.GLsizeiptr, nil, GL_DYNAMIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, 0)
self.tf_vbos[idx].add tf[0]
self.tf_vbos[idx].add tf[1]
# If transform feedback is enabled, we create two VAOs that have two
# parts:
# * The regular mesh, which has the same buffers and attributes in each
# * The TF buffer (with tf_layout), for storing data that will be
# available next time it's rendered. This one is unique in each, to be
# able to write in one while we read from the other.
# IMPORTANT! This is ONLY for *loopback* use of transform feedback.
# If we don't want to read the same data we write, we need a different
# API. We should probably rename this one.
#
# TODO: Document this well enough.
#
# TODO: A way to distinguish between execution in the same frame or a
# different one. Probably with a #define in the shader. So e.g.
# particles are not calculated twice in VR with different positions.
var vao_spec = VaoSpec(
vbs: @[VertexBufferAttribs(vb: vb, attribs: self.layout)],
ib: ib
)
var vao: GLuint
glGenVertexArrays(1, addr vao)
self.vaos.add(vao)
if self.use_tf:
var vao_tf = vao_spec
vao_spec.vbs.add VertexBufferAttribs(vb: tf[0], attribs: self.tf_layout)
vao_tf.vbs.add VertexBufferAttribs(vb: tf[0], attribs: self.tf_layout)
self.tf_vao_specs.add vao_tf
self.vao_specs.add vao_spec
self.write_vaos()
self.loaded = true
# TODO: do this but only in WebGL
# This forces the mesh to be uploaded
# draw_mesh(mesh, mat4(0), RenderCameraData(), ...)
# set loaded to true after that
proc gpu_buffers_delete*(self: MeshData) =
self.vertex_buffers = @[]
self.index_buffers = @[]
self.loaded = false
glBindVertexArray(0)
for i in 0 ..< self.vaos.len:
glDeleteVertexArrays(1, addr self.vaos[i])
# TODO! delete individual buffers?
self.vaos = @[]
if self.users.len == 0:
self.engine.mesh_datas.del(self.hash)
proc update_varray*(self: MeshData) =
if self.vertex_buffers.len == 0:
return
for i,va in self.varrays:
glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffers[i])
glBufferSubData(GL_ARRAY_BUFFER, 0.GLintptr, cast[GLsizeiptr](va.bytelen), addr va[0])
proc update_iarray*(self: MeshData) =
if self.index_buffers.len == 0:
return
for i,ia in self.iarrays:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.index_buffers[i])
glBufferData(GL_ELEMENT_ARRAY_BUFFER, cast[GLsizeiptr](ia.bytelen), addr ia[0], GL_STATIC_DRAW)
proc clone*(self: MeshData): MeshData =
var d = new MeshData
d[] = self[]
d.users = @[]
return d
proc initMesh*(self: Mesh, engine: MyouEngine, name: string, scene: Scene = nil,
draw_method: MeshDrawMethod = Triangles,
common_attributes: CommonMeshAttributes = {vertex, color},
layout: AttributeList = @[],
# stride: int32 = layout.stride,
skip_upload: bool = false,
vertex_count: int32 = 0,
vertex_array: seq[float32] = @[],
index_array: seq[uint16] = @[],
pass: int32 = 0,
): Mesh =
discard procCall(self.GameObject.initGameObject(engine, name))
self.otype = TMesh
self.passes = @[0'i32]
self.sort_sign = BackToFront
self.draw_method = draw_method
self.layout = layout
if layout.len == 0:
if vertex in common_attributes:
self.layout.add Attribute(name: "vertex", dtype: Float, count: 3)
if color in common_attributes:
self.layout.add Attribute(name: "vc_color", dtype: UByte, count: 4)
if normal in common_attributes:
self.layout.add Attribute(name: "normal", dtype: Byte, count: 4)
if uv in common_attributes:
self.layout.add Attribute(name: "uv_0", dtype: Float, count: 2)
# self.stride = if stride != 0: stride else: self.layout.stride
if vertex_count != 0:
self.ensure_capacity(vertex_count)
# var va = newArrRef(vertex_array)
# if vertex_count != 0:
# va.set_len max(va.len, vertex_count * self.stride div 4)
# if va.len != 0:
# var ia = newArrRef(index_array)
# self.skip_upload = skip_upload
# self.load_from_va_ia(@[va], @[ia])
# self.data.num_indices ?= @[0.int32]
# if ?vertex_array:
# if not ?ia:
# self.data.num_indices[0] = vertex_count
# assert((self.data.varray.bytelen div self.data.stride) >= vertex_count,
# &"Array is {self.data.varray.bytelen} bytes long but expected at least {vertex_count * self.data.stride}")
# else:
# self.data.num_indices[0] = 0
if scene != nil:
scene.add_object(self, name=name)
return self
proc newMesh*(engine: MyouEngine, name: string="mesh", scene: Scene = nil,
draw_method: MeshDrawMethod = Triangles,
common_attributes: CommonMeshAttributes = {vertex, color},
layout: AttributeList = @[],
# stride: int32 = layout.stride,
skip_upload: bool = false,
vertex_count: int32 = 0,
vertex_array: seq[float32] = @[],
index_array: seq[uint16] = @[],
pass: int32 = 0,
): Mesh =
result = new Mesh
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.} =
# TODO: Check it actually has a color attribute!!
var varray = self.data.varrays[0]
var varray_byte = varray.to(uint8)
let stride = self.data.stride
let index = self.data.num_indices[0]
var fpos = (stride div 4) * index
let bpos = stride * index
# dump (fpos, varray.len)
if fpos >= varray.len:
# not adding anything more
return -1
varray[fpos] = x
varray[fpos + 1] = y
varray[fpos + 2] = z
varray_byte[bpos + 12] = r
varray_byte[bpos + 13] = g
varray_byte[bpos + 14] = b
varray_byte[bpos + 15] = a
self.data.num_indices[0] += 1
return index+1
proc add_vertex*(self: Mesh, x, y, z, r, g, b, a: SomeFloat): int {.discardable.} =
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.} =
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) =
self.data.num_indices[0] = 0
proc remove_vertex*(self: Mesh, index: int) =
if not (index >= 0):
return
# -1 or undefined
let num_indices = self.data.num_indices
let stride = self.data.stride
# move the last vertex to the one in index
let pos0 = num_indices[0] * stride
let pos1 = index * stride
var bytes = self.data.varrays[0].to(int8)
bytes.copyWithin(pos1, pos0, pos0 + stride)
proc ensure_capacity*(self: Mesh, extra_elements: int) =
if self.data == nil:
let stride = self.layout.stride
assert stride != 0
self.load_from_va_ia @[newArrRef[float32](4*stride*extra_elements)]
self.data.num_indices[0] = 0
return
let varray = self.data.varrays[0]
let bytelen = varray.byteLen
let current_index = self.data.num_indices[0]
let stride = self.data.stride
let index = current_index + extra_elements
let fpos = (stride div 4) * index
if fpos >= bytelen:
var cap = bytelen * 2
while fpos >= cap:
cap *= 2
var va = newArrRef[float32](cap)
varray.copy_bytes_to va
self.load_from_va_ia @[va]
self.data.num_indices[0] = current_index
# proc load_from_arraybuffer*(self: Mesh, data: unknown, buffer_offset: int = 0) =
# # ASSUMING LITTLE ENDIAN
# let vlen = self.offsets[self.offsets.len - 2]
# let ilen = self.offsets[self.offsets.len - 1]
# let offset = (self.pack_offset or 0) + buffer_offset
# try:
# let va = makeSeq[float32](data, offset, vlen)
# let ia = makeSeq[uint16](data, offset + vlen * 4, ilen)
# except Exception as e:
# let e = Error(&"Mesh {self.name} is corrupt")
# raise e
# self.load_from_va_ia(va, ia)
proc load_from_va_ia*(self: Mesh,
varrays: seq[ArrRef[float32]],
iarrays: seq[ArrRef[uint16]] | seq[ArrRef[int32]] = newSeq[ArrRef[uint16]](),
layout: AttributeList = @[],
index_types: seq[DataType] = @[]) =
if self.data != nil:
self.data.remove(self)
# if @hash and (@data = @engine.mesh_datas[@hash])?
# @data.users.push @
# @engine.main_loop?.reset_timeout()
# return
var data = newMeshData(self.engine)
when defined(myouUseRenderdoc):
data.name = self.name
self.data = data
self.engine.mesh_datas[self.hash] = data
data.users.add(self)
data.draw_method = self.draw_method
data.hash = self.hash
data.varrays = varrays
data.iarrays.setLen 0
for ia in iarrays:
data.iarrays.add ia.to(uint16)
while data.iarrays.len != 0 and data.iarrays[^1].len == 0:
discard data.iarrays.pop()
data.index_types = index_types
if layout.len != 0:
self.layout = layout
data.stride = self.layout.stride
assert data.stride != 0, "Invalid stride"
# TODO: Will there be meshes with same hash but different layout?
data.layout = self.layout
if self.generate_tangents:
for a in self.layout:
if not a.name.startsWith("tg_"):
continue
self.data.generate_tangents("uv_" & a.name[3 ..< ^0], a.name)
# Apply modifiers
let skip_upload = self.skip_upload
var vertex_modifiers = self.vertex_modifiers
if vertex_modifiers.len != 0:
self.skip_upload = true
self.vertex_modifiers = @[]
# for modifier in vertex_modifiers:
# if modifier.prepare_mesh != nil:
# modifier.prepare_mesh(self)
self.skip_upload = skip_upload
self.vertex_modifiers = vertex_modifiers
data = self.data
if not self.skip_upload:
self.engine.renderer.enqueue proc()=
data.gpu_buffers_upload()
proc load_from_va_ia*(self: Mesh,
varray: seq[float32],
iarray: seq[uint16] = @[],
layout: AttributeList = @[]) =
self.load_from_va_ia(@[newArrRef varray], @[newArrRef iarray], layout)
proc add_modifier*(self: Mesh, modifier: VertexModifier) =
self.vertex_modifiers.add(modifier)
if ?modifier.prepare_mesh and ?self.data:
echo &"applying modifiers of {self.name} after it was already loaded"
modifier.prepare_mesh(self)
# self.update_signature()
proc insert_modifier*(self: Mesh, index: int, modifier: VertexModifier) =
self.vertex_modifiers.insert(modifier, index)
if ?modifier.prepare_mesh and ?self.data:
echo &"applying modifiers of {self.name} after it was already loaded"
modifier.prepare_mesh(self)
# self.update_signature()
proc remove_modifier*(self: Mesh, index: int) =
self.vertex_modifiers.delete(index)
# self.update_signature()
proc remove_modifier*(self: Mesh, modifier: VertexModifier) =
let index = self.vertex_modifiers.find(modifier)
if index != -1:
self.remove_modifier index
# proc force_lod_level*(self: Mesh, level: unknown) =
# if not level != nil:
# for cam, data in pairs(self.last_lod):
# data.render_tick = -1
# return
# let mesh = self.lod_objects[level].?object ? self
# for cam, data in pairs(self.last_lod):
# data.mesh = mesh
# data.render_tick = nil
# return
# proc get_lod_mesh*(self: Mesh, viewport: unknown, min_length_px: unknown, render_tick: int): Mesh =
# # TODO: put min_length_px and render_tick in camera?
# var amesh = self
# if self.lod_objects.len != 0:
# var camera = viewport.camera
# camera = viewport.debug_camera ? camera
# # remember previous mesh and
# # avoid doing the same calculation several times
# var last_lod_data = self.last_lod[camera.name]
# if not last_lod_data != nil:
# self.last_lod[camera.name] = last_lod_data = {mesh: nil, render_tick: -1}.toTable
# let previous_mesh = last_lod_data.mesh
# if last_lod_data.render_tick >= render_tick:
# return previous_mesh
# last_lod_data.render_tick = render_tick
# let cwm = camera.world_matrix
# let cam_pos = vec3(cwm.m12, cwm.m13, cwm.m14)
# # Approximation to nearest point to the surface:
# # We clamp the camera position to the object's bounding box
# # we transform the point with the inverse matrix
# # we clamp with dimensions
# # we clamp with radius
# # we transform back with matrix
# # that's the approximate near distance
# # TODO: Optimize
# let inv = invert(self.world_matrix)
# if not inv != nil or not self.bound_box:
# if self.data != nil:
# # Return highest loaded LoD
# return self
# for {object}.toTable in self.lod_objects:
# if not object.data != nil:
# continue
# return object
# return self
# var p = transformMat4(cam_pos, inv)
# p = max(p, self.bound_box[0])
# p = min(p, self.bound_box[1])
# p = transformMat4(p, self.world_matrix)
# let distance_to_camera = dist(p, cam_pos)
# # world scale: assuming all three axes have same scale as X
# let m00 = self.world_matrix.m00
# let m01 = self.world_matrix.m01
# let m02 = self.world_matrix.m02
# let world_scale = sqrt(m00 * m00 + m01 * m01 + m02 * m02)
# # number that converts a length to screen pixels
# let poly_length_to_visual_size = (viewport.units_to_pixels / distance_to_camera) * world_scale
# # we'll going to find the biggest length
# # that is small enough on screen
# var biggest_length = self.avg_poly_length
# amesh = self
# # from highest to lowest
# for lod in reversed(self.lod_objects):
# var h_ratio = 1
# if amesh == previous_mesh:
# # hysteresis: ratio of distance the mesh can go
# # further away before popping back to a lower LoD
# h_ratio += lod.hysteresis
# let ob = lod.object
# let ob_apl = ob.avg_poly_length
# let visual_size_px = ob_apl * poly_length_to_visual_size * h_ratio
# if not amesh.data != nil or (ob_apl > biggest_length and visual_size_px < min_length_px):
# biggest_length = ob_apl
# amesh = ob
# last_lod_data.mesh = amesh
# return amesh
proc clone_impl*(self: Mesh, clone: Mesh): Mesh =
clone.last_lod.clear()
if ?clone.data:
clone.data.users.add(clone)
return clone
proc clone*(self: Mesh,
recursive: bool=true,
with_behaviours: bool=true,
name: string=self.name,
new_parent: GameObject=self.parent,
scene: Scene=self.scene,
instance_body: bool=true
): Mesh =
var n = new Mesh
n[] = self[]
discard procCall(self.GameObject.clone_impl(n.GameObject, recursive, with_behaviours, name, new_parent, scene, instance_body))
return self.clone_impl(n)
proc sort_faces*(self: Mesh, camera_position: Vec3) =
# if self.data?.draw_method != GL_TRIANGLES:
# return
# let BIG_ENDIAN = 0
# let offsets = self.offsets
# let num_submeshes = (offsets.len / 2) - 1
# let varray = self.data.varray
# let iarray = self.data.iarray
# var stride = self.data.stride
# stride >>= 2
# let v = vec3()
# let vt = vec3()
# # we scale this vector so we avoid dividing triangle positions by 3
# var camera_position3 = camera_position
# var m4 = self.world_matrix
# m4 = invert(m4)
# camera_position3 = transformMat4(camera_position3, m4)
# camera_position3 = camera_position3 * 3
# let cp3x = camera_position3.x
# let cp3y = camera_position3.y
# let cp3z = camera_position3.z
# let sign = self.sort_sign
# # # Find out the furthest possible distance we can find, so that's 2**16
# # vec3
# for i in 0 ..< num_submeshes:
# let i2 = i << 1
# let va = varray.subarray(offsets[i2], offsets[i2 + 2])
# var ia = iarray.subarray(offsets[i2 + 1], offsets[i2 + 3])
# let num_triangles = (ia.len * 0.3333333333333333) or 0
# if face_sort_array.len < num_triangles:
# face_sort_array = makeSeq[float64](num_triangles)
# face_sort_array32 = makeSeq[uint32](face_sort_array.buffer)
# iarray_temporary = makeSeq[uint32](ia.len)
# var j2 = var j3 = 0
# for j in 0 ..< num_triangles:
# let v0 = ia[j3] * stride
# let v1 = ia[j3 + 1] * stride
# let v2 = ia[j3 + 2] * stride
# # vec3.set v, va[v0], va[v0+1], va[v0+2]
# # vec3.set vt, va[v1], va[v1+1], va[v1+2]
# # vec3.add v, v, vt
# # vec3.set vt, va[v2], va[v2+1], va[v2+2]
# # vec3.add v, v, vt
# # sqr_dist = vec3.sqrDist v, camera_position3
# var x = va[v0]
# var y = va[v0 + 1]
# var z = va[v0 + 2]
# x += va[v1]
# y += va[v1 + 1]
# z += va[v1 + 2]
# x += va[v2] - cp3x
# y += va[v2 + 1] - cp3y
# z += va[v2 + 2] - cp3z
# let sqr_dist = x * x + y * y + z * z
# face_sort_array[j] = sqr_dist * sign
# face_sort_array32[j2 + BIG_ENDIAN] = j
# j2 += 2
# j3 += 3
# face_sort_array.subarray(0, num_triangles).sort()
# iarray_temporary.set(ia)
# j3 = 0
# j2 = BIG_ENDIAN
# while j2 < num_triangles * 2:
# let t = face_sort_array32[j2]
# let t3 = t + (t << 1)
# ia[j3] = iarray_temporary[t3]
# ia[j3 + 1] = iarray_temporary[t3 + 1]
# ia[j3 + 2] = iarray_temporary[t3 + 2]
# j3 += 3
# j2 += 2
# self.update_iarray()
return
# proc generate_normals*(self: Mesh, normal_offset: float = 4 * 3) =
# # dbg?.clear_vertices()
# let offsets = self.offsets
# let num_submeshes = (offsets.len / 2) - 1
# let varray = self.data.varray
# let varray_byte = self.data.varray_byte
# let iarray = self.data.iarray
# var stride = self.data.stride
# let varray_int8 = makeSeq[int8](varray.buffer, varray.byteOffset, varray.bytelength)
# let iarray_u32 = makeSeq[uint32](iarray.buffer, iarray.byteOffset, iarray.bytelength >> 2)
# stride = stride
# let stride_f = stride >> 2
# # TODO: have the size be of the largest submesh, not the whole thing
# var normals = makeSeq[float32](varray.len * 3 / stride_f)
# var a = vec3()
# var b = vec3()
# let c = vec3()
# for i in 0 ..< num_submeshes:
# let i2 = i << 1
# let va = varray.subarray(offsets[i2], offsets[i2 + 2])
# # pre-adding normal offset to slice instead of adding it each time
# var va_n = varray_int8.subarray((offsets[i2] << 2) + normal_offset, (offsets[i2 + 2] << 2) + normal_offset)
# if va.bytelength / stride >= 65536:
# var ia = iarray_u32.subarray(offsets[i2 + 1] >> 1, offsets[i2 + 3] >> 1)
# else:
# ia = iarray.subarray(offsets[i2 + 1], offsets[i2 + 3])
# j = 0
# while j < ia.len:
# # get vertex indices, multiply by stride_f
# var v0 = var v0_3 = ia[j]
# var v1 = var v1_3 = ia[j + 1]
# var v2 = var v2_3 = ia[j + 2]
# v0 *= stride_f
# v1 *= stride_f
# v2 *= stride_f
# v0_3 *= 3
# v1_3 *= 3
# v2_3 *= 3
# # get delta position of v1 v2
# var x = va[v0]
# var y = va[v0 + 1]
# var z = va[v0 + 2]
# let v1x = va[v1] - x
# let v1y = va[v1 + 1] - y
# let v1z = va[v1 + 2] - z
# let v2x = va[v2] - x
# let v2y = va[v2 + 1] - y
# let v2z = va[v2 + 2] - z
# a = vec3(v1x, v1y, v1z)
# b = vec3(v2x, v2y, v2z)
# a = cross(a, b)
# a = normalize(a)
# # if dbg?
# # vec3.set b, x,y,z
# # vec3.add b, b, {x: v1x/3, y: v1y/3, z: v1z/3}
# # vec3.add b, b, {x: v2x/3, y: v2y/3, z: v2z/3}
# # vec3.transformMat4 c, b, @world_matrix
# # dbg.add_vertex c, {r:1,g:0,b:0,a:1}
# # vec3.scale a, a, 0.01
# # vec3.add b, b, a
# # vec3.scale a, a, 100
# # vec3.transformMat4 c, b, @world_matrix
# # dbg.add_vertex c, {r:1,g:0,b:0,a:1}
# # add to all vertices
# # TODO: add normal count
# # to divide at the ends instead of normalizing
# let (x,y,z) = a.toTuple
# normals[v0_3] += x
# normals[v0_3 + 1] += y
# normals[v0_3 + 2] += z
# normals[v1_3] += x
# normals[v1_3 + 1] += y
# normals[v1_3 + 2] += z
# normals[v2_3] += x
# normals[v2_3 + 1] += y
# normals[v2_3 + 2] += z
# j += 3
# # copy normals normalized to bytes
# var i_b = 0
# i3 = 0
# while i3 < normals.len:
# a = vec3(normals[i3], normals[i3 + 1], normals[i3 + 2])
# a = normalize(a)
# # console.log a
# va_n[i_b] = a.x * 127
# va_n[i_b + 1] = a.y * 127
# va_n[i_b + 2] = a.z * 127
# i_b += stride
# i3 += 3
# 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) =
if self.varrays.len != self.iarrays.len:
raise newException(ValueError,
"Function generate_tangents() doesn't support meshes without indices at the moment")
var uv_offset, tangent_offset: int32
for attr in self.layout:
if attr.name == uv_layer_name:
uv_offset = (attr.offset div 4).int32
elif attr.name == tangent_layer_name:
tangent_offset = attr.offset.int32
if uv_offset == 0 or tangent_offset == 0:
raise newException(KeyError, "Cound't find attributes")
let winding_offset = tangent_offset + 3
let stride = (self.stride).int32
let stride_f = (stride div 4).int32
var max_len = 0
for va in self.varrays:
max_len = max(max_len, va.len)
for ia in self.iarrays:
var maxi = 0
for idx in ia.to int32:
maxi = max(maxi, idx)
# dump (max_len, maxi)
var tangents = makeSeq[Vec3](max_len div stride_f.int)
# for va, ia in zip(self.varrays, self.iarrays):
for i in 0 ..< self.varrays.len:
let va = self.varrays[i]
var ia = self.iarrays[i].to int32
if va.len == 0 or ia.len == 0:
continue
# let va_i8 = va.to int8 # TODO: why is this not working?
let va_i8 = cast[ArrRef[int8]](va)
var j = 0
var ialen = ia.len
while j < ialen:
# get vertex indices
var v0 = ia[j]
var v1 = ia[j + 1]
var v2 = ia[j + 2]
var v0s = v0*stride_f
var v1s = v1*stride_f
var v2s = v2*stride_f
# get delta position of v1 v2
var x = va[v0s]
var y = va[v0s + 1]
var z = va[v0s + 2]
var v1x = va[v1s] - x
var v1y = va[v1s + 1] - y
var v1z = va[v1s + 2] - z
var v2x = va[v2s] - x
var v2y = va[v2s + 1] - y
var v2z = va[v2s + 2] - z
# get delta position of uv1 uv2
x = va[v0s + uv_offset]
y = va[v0s + uv_offset + 1]
let uv1x = va[v1s + uv_offset] - x
let uv1y = va[v1s + uv_offset + 1] - y
let uv2x = va[v2s + uv_offset] - x
let uv2y = va[v2s + uv_offset + 1] - y
# get UV winding direction
let w = sgn(uv1x * uv2y - uv2x * uv1y).int8
# multiply v deltas by uv.y (squish to X)
v1x *= uv2y
v1y *= uv2y
v1z *= uv2y
v2x *= uv1y
v2y *= uv1y
v2z *= uv1y
var v = vec3(v2x - v1x, v2y - v1y, v2z - v1z)
v = v * -w.float32
v = normalize(v)
# add to all vertices
# dump (v0, v1, v2, tangents.len)
tangents[v0] += v
tangents[v1] += v
tangents[v2] += v
# write winding
va_i8[(v0 * stride + winding_offset).int] = w
va_i8[(v1 * stride + winding_offset).int] = w
va_i8[(v2 * stride + winding_offset).int] = w
j += 3
# copy tangents normalized to bytes
var i_b = tangent_offset
let va_i8_len = va_i8.len
for v in tangents:
let vn = normalize(v)
va_i8[i_b] = (vn.x * 127).int8
va_i8[i_b + 1] = (vn.y * 127).int8
va_i8[i_b + 2] = (vn.z * 127).int8
i_b += stride
if i_b >= va_i8_len:
break
# proc displace_vertices*(self: Mesh, offset: Vec3) =
# if not self.data:
# return
# let (x,y,z) = offset.toTuple
# var varray = self.data.varray
# var stride = self.data.stride
# stride >>= 2
# i = 0
# while i < varray.len:
# varray[i] += x
# varray[i + 1] += y
# varray[i + 2] += z
# i += stride
# for bb in self.bound_box:
# let bb = bb + offset
# if self.data.vertex_buffers.len != 0:
# self.update_varray()
# return
# proc has_attribute*(self: Mesh, attribute_name: unknown): bool =
# for {name}.toTable in self.layout:
# if not attribute_name == name:
# continue
# return true
# return false
# proc insert_attribute*(self: Mesh, attribute: unknown) =
# # {name: 'xxxx', type: 'f', count: 3, offset: 0, location: 0}
# let varray = self.data.varray
# let iarray = self.data.iarray
# let stride = self.data.stride
# self.data?.remove(self)
# attribute.offset = stride
# attribute.location = self.layout[self.layout.len - 1].location + 1
# self.layout.add(attribute)
# let stride_f = stride >> 2 # assuming it's always aligned to 4 bytes
# var stride_f_out = stride_f
# let attr_size = attribute.dtype.size * attribute.count
# stride_f_out += ceil(attr_size / 4)
# let varray2 = makeSeq[float32]((varray.len / stride_f) * stride_f_out)
# var pos_in = var pos_out = 0
# let len = varray2.len
# let uarray = makeSeq[uint32](varray.buffer, varray.byteOffset, varray.len)
# var uarray2 = makeSeq[uint32](varray2.buffer, varray2.byteOffset, varray2.len)
# while pos_out < len:
# for i in 0 ..< stride_f:
# uarray2[pos_out + i] = uarray[pos_in + i]
# pos_in += stride_f
# pos_out += stride_f_out
# self.stride = stride_f_out << 2
# i = 2
# while i < self.offsets.len:
# self.offsets[i] = (self.offsets[i] / stride_f) * stride_f_out
# i += 2
# self.load_from_va_ia(varray2, iarray)
# proc split_triangles*(self: Mesh) =
# var varray_byte = self.data.varray_byte
# var iarray = self.data.iarray
# let stride = self.data.stride
# let vertices = @[]
# let indices = @[]
# let idx_out = 0
# for idx_in in iarray:
# let i = idx_in * stride
# vertices.add(varray_byte.subarray(i, i + stride)...)
# indices.add(++idx_out)
# varray_byte = makeSeq[uint8](vertices)
# let varray = makeSeq[float32](varray_byte.buffer)
# if idx_out > (1 << 16):
# iarray = makeSeq[uint32](indices)
# iarray = makeSeq[uint16](iarray.buffer)
# else:
# iarray = makeSeq[uint16](indices)
# self.offsets = @[0, 0, varray.len, iarray.bytelength >> 1]
# self.load_from_va_ia(varray, iarray)
# return
# proc origin_to_bounding_box_center*(self: Mesh, options: Table[string, unknown] = {:}.toTable) =
# let preserve_visual_position = options.preserve_visual_position ?: true
# let (bb0,bb1) = self.bound_box.toTuple
# var v = vec3()
# v = lerp(bb0, bb1, 0.5)
# v = -v
# self.displace_vertices(v)
# v = v * self.scale
# v = -v
# if preserve_visual_position:
# self.translate(v, self)
# return
# proc update_BB*(self: Mesh) =
# return self.update_bounding_box()
proc update_bounding_box*(self: Mesh) =
if self.data == nil or self.layout.len == 0:
return
# TODO: byte vertices
if self.layout[0].dtype != Float:
raise newException(Defect, "not implemented")
let varray = self.data.varrays[0]
let stride = self.data.stride div 4
var minx, miny, minz = Inf.float32
var maxx, maxy, maxz = -Inf.float32
var i = 0
while i < varray.len:
var v = varray[i]
minx = min(minx, v)
maxx = max(maxx, v)
v = varray[i + 1]
miny = min(miny, v)
maxy = max(maxy, v)
v = varray[i + 2]
minz = min(minz, v)
maxz = max(maxz, v)
i += stride
self.bound_box[0] = vec3(minx, miny, minz)
self.bound_box[1] = vec3(maxx, maxy, maxz)
# proc unload*(self: Mesh) =
# self.data?.remove(self)
# # @data = null
# for tex in self.related_textures:
# var idx = tex.mesh_users.find(self)
# if idx == -1:
# raise Error("this shouldn't happen")
# var popped = tex.mesh_users.pop()
# if popped != self:
# tex.mesh_users[idx] = popped
# if tex.mesh_users.len == 0:
# console.log(&"Texture {tex.name} has no more users, unloading")
# tex.unload()
# self.related_textures = @[]
# return
# proc destroy*(self: Mesh, recursive: bool = true) =
# self.data?.remove(self)
# procCall(self.GameObject.destroy(recursive))
proc debug_print_vertices*(self: Mesh, starts: int = 0, ends: int = 10) =
if self.data.varrays[0].len == 0:
return
let varray = cast[ptr UncheckedArray[int8]](self.data.varrays[0][0].addr)
let stride = self.data.stride
for i in starts ..< ends:
echo &"index {i}"
for attr in self.layout:
let offset = i * stride + attr.offset
let data = case attr.dtype:
of Float:
($cast[ptr array[16, float32]](varray[offset].addr)[]).split(',')[0 ..< attr.count].join(",") & "]"
of Byte:
($cast[ptr array[16, int8]](varray[offset].addr)[]).split(',')[0 ..< attr.count].join(",") & "]"
of UByte:
($cast[ptr array[16, uint8]](varray[offset].addr)[]).split(',')[0 ..< attr.count].join(",") & "]"
of Short:
($cast[ptr array[16, uint16]](varray[offset].addr)[]).split(',')[0 ..< attr.count].join(",") & "]"
# of HalfFloat:
# let a = makeSeq[uint16](varray.buffer, offset, attr.count)
# @[read_f16(a[0]), read_f16(a[1]), read_f16(a[2])]
else: ""
echo &"{attr.name} {data}"
proc add_polygonal_line*(self: Mesh, orig, dest: Vec3, width: float) =
let last = self.last_polyline_point
let last_left = self.last_polyline_left
let has_last = self.data.num_indices[0] != 0
var left = ((dest - orig).normalize * (width/2)).rotate_ccw
if has_last and dist(orig, dest)*2 < width:
left = last_left
var inleft = left
if has_last and (last ~= orig):
inleft = mix(left, last_left, 0.5).normalize * (width/2)
let angle = (left.xy.angle - last_left.xy.angle).fixAngle
inleft *= 1/cos(angle/2)
if angle > 2.5:
# may be too big, ignore it
inleft = left
if has_last and not (last ~= orig):
## NaN polygons
self.add_vertex(vec3(), vec4(0,0,0,1))
self.add_vertex(vec3(), vec4(0,0,0,1))
# ## degen tris
# self.add_vertex(last, vec4(0,0,0,1))
# self.add_vertex(last, vec4(0,0,0,1))
# self.add_vertex(orig, vec4(0,0,0,1))
# self.add_vertex(orig, vec4(0,0,0,1))
# self.data.num_indices[0] -= 2
self.add_vertex(orig + inleft, vec4(0,0,0,1))
self.add_vertex(orig - inleft, vec4(0,0,0,1))
self.add_vertex(dest + left, vec4(0,0,0,1))
self.add_vertex(dest - left, vec4(0,0,0,1))
self.last_polyline_point = dest
self.last_polyline_left = left
self.data.update_varray()
# var fl32 = makeSeq[float32](1)
# var ui32 = makeSeq[uint32](fl32.buffer)
# let rounder = (1 << 12)
# proc write_f16(float: unknown): int =
# fl32[0] = float
# var fltInt32 = ui32[0]
# fltInt32 += fltInt32 and rounder
# var fltInt16 = (fltInt32 >> 31) << 5
# var tmp = (fltInt32 >> 23) and 255
# tmp = (tmp - 112) and (((112 - tmp) >> 4) >> 27)
# fltInt16 = (fltInt16 or tmp) << 10
# fltInt16 |= (fltInt32 >> 13) and 1023
# return fltInt16
# ui32[0] = (254 - 15) << 23
# let magic = fl32[0]
# ui32[0] = (127 + 16) << 23
# let was_inf_nan = fl32[0]
# proc read_f16(short: unknown) =
# ui32[0] = (short and 32767) << 13
# fl32[0] *= magic
# if fl32[0] >= was_inf_nan:
# ui32[0] |= 255 << 23
# ui32[0] |= (short and 32768) << 16
# return fl32[0]