# 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, 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) # End forward declarations and ob type methods 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 import ../quat export arr_ref export tables # TODO: move MeshData elsewhere 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) idx = self.users.find(ob) if self.users.len == 0: self.engine.mesh_datas.del(self.hash) ob.data = nil proc write_vaos(self: MeshData) = for i,vao in self.vaos: if vao.int == 0: continue glBindVertexArray(vao.GLuint) 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.GLuint, 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.GPUBuffer) self.index_buffers.add(0.GPUBuffer) self.vaos.add(0.GPUVao) 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.GPUBuffer) 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).int32) self.index_buffers.add(ib.GPUBuffer) 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].GPUBuffer self.tf_vbos[idx].add tf[1].GPUBuffer # 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.GPUVao) 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) = ## Removes GPU buffers. For internal use. Use GameObject.destroy() instead. glBindVertexArray(0) self.vaos = @[] self.tf_vaos = @[] self.vao_specs = @[] self.tf_vao_specs = @[] glBindBuffer(GL_ARRAY_BUFFER, 0) self.vertex_buffers = @[] self.index_buffers = @[] self.loaded = false if self.users.len == 0: 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: glBindBuffer(GL_ARRAY_BUFFER, self.vertex_buffers[i].GLuint) 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: glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.index_buffers[i].GLuint) 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) let stride = self.layout.stride if vertex_array.len != 0: var va = newArrRef(vertex_array) var ia = newArrRef(index_array) self.skip_upload = skip_upload self.load_from_va_ia(@[va], @[ia]) elif vertex_count != 0: self.ensure_capacity(vertex_count) 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 = ## 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 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.} = ## 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 = 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) = ## 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" let stride = self.data.stride # move the last vertex to the one in index let pos0 = self.data.num_indices[0] * stride let pos1 = index * stride var bytes = self.data.varrays[0].to(int8) for i in 0 ..< stride: bytes[pos1+i] = bytes[pos0+i] 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 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) copyMem(va.toPointer, varray.toPointer, bytelen) 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 = newSeq[float32](data, offset, vlen) # let ia = newSeq[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] = @[]) = ## 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) # TODO: hash meshes to reuse existing copies automatically 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 = @[]) = ## 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" modifier.prepare_mesh(self) # 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" modifier.prepare_mesh(self) # 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 proc clone_impl*(self: Mesh, clone: Mesh): Mesh = clone.last_lod.clear() if clone.data.nonNil: 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) = ## 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") 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 = newSeq[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 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 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 minv = Vec3.high var maxv = Vec3.low var i = 0 while i < varray.len: var v = cast[ptr Vec3](varray[i].addr) minv = min(minv, v[]) maxv = max(maxv, v[]) i += stride self.bound_box = (minv, maxv) 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) 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 = newSeq[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, color = vec4(1), update = true) = ## 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°). ## ## Optionally you can supply a color. ## ## If you're going to add many lines, set `update` to false, then call ## `update_varray` at the end. ## ## It requires the mesh to have draw method TriangleStrip. when not defined(release): assert self.draw_method == TriangleStrip, "Error: mesh draw_method should be TriangleStrip" 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): ## degen tris self.add_vertex(last - last_left, color) self.add_vertex(orig + inleft, color) # self.data.num_indices[0] -= 2 self.add_vertex(orig + inleft, color) self.add_vertex(orig - inleft, color) self.add_vertex(dest + left, color) self.add_vertex(dest - left, color) self.last_polyline_point = dest self.last_polyline_left = left if update: self.data.update_varray()