changeset 222:ddfc54036e00

add: basic loading of glTF files (*.glb), no materials yet
author Sam <sam@basx.dev>
date Mon, 15 May 2023 00:34:00 +0700
parents 2a2367d289dd
children 4c0a4b6902ff
files src/semicongine/core/gpu_data.nim src/semicongine/core/vulkanapi.nim src/semicongine/materials.nim src/semicongine/mesh.nim src/semicongine/renderer.nim src/semicongine/resources.nim src/semicongine/resources/audio.nim src/semicongine/resources/mesh.nim src/semicongine/vulkan/physicaldevice.nim src/vulkan_api/vulkan_api_generator.nim tests/resources/default/test1.glb tests/test_mesh.nim
diffstat 12 files changed, 485 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/core/gpu_data.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/core/gpu_data.nim	Mon May 15 00:34:00 2023 +0700
@@ -448,6 +448,8 @@
     of Sampler2D: result[0] = nil
 
 func getRawData*(value: var DataList): (pointer, uint32) =
+  if value.len == 0:
+    return (nil, 0'u32)
   result[1] = value.thetype.size * value.len
   case value.thetype
     of Float32: result[0] = addr value.float32[0]
@@ -644,6 +646,106 @@
   elif T is TMat4[float64]: value.mat4f64 = data
   else: {. error: "Virtual datatype has no values" .}
 
+func appendValues*[T: GPUType|int|uint|float](value: var DataList, data: seq[T]) =
+  value.len += data.len
+  when T is float32: value.float32.add data
+  elif T is float64: value.float64.add data
+  elif T is int8: value.int8.add data
+  elif T is int16: value.int16.add data
+  elif T is int32: value.int32.add data
+  elif T is int64: value.int64.add data
+  elif T is uint8: value.uint8.add data
+  elif T is uint16: value.uint16.add data
+  elif T is uint32: value.uint32.add data
+  elif T is uint64: value.uint64.add data
+  elif T is int and sizeof(int) == sizeof(int32): value.int32.add data
+  elif T is int and sizeof(int) == sizeof(int64): value.int64.add data
+  elif T is uint and sizeof(uint) == sizeof(uint32): value.uint32.add data
+  elif T is uint and sizeof(uint) == sizeof(uint64): value.uint64.add data
+  elif T is float and sizeof(float) == sizeof(float32): value.float32.add data
+  elif T is float and sizeof(float) == sizeof(float64): value.float64.add data
+  elif T is TVec2[int32]: value.vec2i32.add data
+  elif T is TVec2[int64]: value.vec2i64.add data
+  elif T is TVec3[int32]: value.vec3i32.add data
+  elif T is TVec3[int64]: value.vec3i64.add data
+  elif T is TVec4[int32]: value.vec4i32.add data
+  elif T is TVec4[int64]: value.vec4i64.add data
+  elif T is TVec2[uint32]: value.vec2u32.add data
+  elif T is TVec2[uint64]: value.vec2u64.add data
+  elif T is TVec3[uint32]: value.vec3u32.add data
+  elif T is TVec3[uint64]: value.vec3u64.add data
+  elif T is TVec4[uint32]: value.vec4u32.add data
+  elif T is TVec4[uint64]: value.vec4u64.add data
+  elif T is TVec2[float32]: value.vec2f32.add data
+  elif T is TVec2[float64]: value.vec2f64.add data
+  elif T is TVec3[float32]: value.vec3f32.add data
+  elif T is TVec3[float64]: value.vec3f64.add data
+  elif T is TVec4[float32]: value.vec4f32.add data
+  elif T is TVec4[float64]: value.vec4f64.add data
+  elif T is TMat2[float32]: value.mat2f32.add data
+  elif T is TMat2[float64]: value.mat2f64.add data
+  elif T is TMat23[float32]: value.mat23f32.add data
+  elif T is TMat23[float64]: value.mat23f64.add data
+  elif T is TMat32[float32]: value.mat32f32.add data
+  elif T is TMat32[float64]: value.mat32f64.add data
+  elif T is TMat3[float32]: value.mat3f32.add data
+  elif T is TMat3[float64]: value.mat3f64.add data
+  elif T is TMat34[float32]: value.mat34f32.add data
+  elif T is TMat34[float64]: value.mat34f64.add data
+  elif T is TMat43[float32]: value.mat43f32.add data
+  elif T is TMat43[float64]: value.mat43f64.add data
+  elif T is TMat4[float32]: value.mat4f32.add data
+  elif T is TMat4[float64]: value.mat4f64.add data
+  else: {. error: "Virtual datatype has no values" .}
+
+func appendValues*(value: var DataList, data: DataList) =
+  assert value.thetype == data.thetype
+  value.len += data.len
+  case value.thetype:
+  of Float32: value.float32.add data.float32
+  of Float64: value.float64.add data.float64
+  of Int8: value.int8.add data.int8
+  of Int16: value.int16.add data.int16
+  of Int32: value.int32.add data.int32
+  of Int64: value.int64.add data.int64
+  of UInt8: value.uint8.add data.uint8
+  of UInt16: value.uint16.add data.uint16
+  of UInt32: value.uint32.add data.uint32
+  of UInt64: value.uint64.add data.uint64
+  of Vec2I32: value.vec2i32.add data.vec2i32
+  of Vec2I64: value.vec2i64.add data.vec2i64
+  of Vec3I32: value.vec3i32.add data.vec3i32
+  of Vec3I64: value.vec3i64.add data.vec3i64
+  of Vec4I32: value.vec4i32.add data.vec4i32
+  of Vec4I64: value.vec4i64.add data.vec4i64
+  of Vec2U32: value.vec2u32.add data.vec2u32
+  of Vec2U64: value.vec2u64.add data.vec2u64
+  of Vec3U32: value.vec3u32.add data.vec3u32
+  of Vec3U64: value.vec3u64.add data.vec3u64
+  of Vec4U32: value.vec4u32.add data.vec4u32
+  of Vec4U64: value.vec4u64.add data.vec4u64
+  of Vec2F32: value.vec2f32.add data.vec2f32
+  of Vec2F64: value.vec2f64.add data.vec2f64
+  of Vec3F32: value.vec3f32.add data.vec3f32
+  of Vec3F64: value.vec3f64.add data.vec3f64
+  of Vec4F32: value.vec4f32.add data.vec4f32
+  of Vec4F64: value.vec4f64.add data.vec4f64
+  of Mat2F32: value.mat2f32.add data.mat2f32
+  of Mat2F64: value.mat2f64.add data.mat2f64
+  of Mat23F32: value.mat23f32.add data.mat23f32
+  of Mat23F64: value.mat23f64.add data.mat23f64
+  of Mat32F32: value.mat32f32.add data.mat32f32
+  of Mat32F64: value.mat32f64.add data.mat32f64
+  of Mat3F32: value.mat3f32.add data.mat3f32
+  of Mat3F64: value.mat3f64.add data.mat3f64
+  of Mat34F32: value.mat34f32.add data.mat34f32
+  of Mat34F64: value.mat34f64.add data.mat34f64
+  of Mat43F32: value.mat43f32.add data.mat43f32
+  of Mat43F64: value.mat43f64.add data.mat43f64
+  of Mat4F32: value.mat4f32.add data.mat4f32
+  of Mat4F64: value.mat4f64.add data.mat4f64
+  else: raise newException(Exception, &"Unsupported data type for GPU data:" )
+
 func setValue*[T: GPUType|int|uint|float](value: var DataList, i: uint32, data: T) =
   assert i < value.len
   when T is float32: value.float32[i] = data
--- a/src/semicongine/core/vulkanapi.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/core/vulkanapi.nim	Mon May 15 00:34:00 2023 +0700
@@ -4,7 +4,6 @@
 import std/logging
 import std/typetraits
 import std/macros
-import std/private/digitsutils
 type
   VkHandle* = distinct uint
   VkNonDispatchableHandle* = distinct uint
@@ -12063,6 +12062,4 @@
   vkEnumerateInstanceLayerProperties = cast[proc(pPropertyCount: ptr uint32, pProperties: ptr VkLayerProperties): VkResult {.stdcall.}](vkGetInstanceProcAddr(instance, "vkEnumerateInstanceLayerProperties"))
   vkCreateInstance = cast[proc(pCreateInfo: ptr VkInstanceCreateInfo, pAllocator: ptr VkAllocationCallbacks, pInstance: ptr VkInstance): VkResult {.stdcall.}](vkGetInstanceProcAddr(instance, "vkCreateInstance"))
 
-converter VkBool2NimBool*(a: VkBool32): bool = a > 0
 converter NimBool2VkBool*(a: bool): VkBool32 = VkBool32(a)
-proc `$`*(x: uint32): string {.raises: [].} = addInt(result, x)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/semicongine/materials.nim	Mon May 15 00:34:00 2023 +0700
@@ -0,0 +1,23 @@
+import ./core
+
+type
+  # based on the default material
+  # from glTF
+  PBRMetalicRoughness* = object
+    name: string
+
+    baseColor: Vec4f
+    baseColorTexture: Image
+
+    metalic: float32
+    roughness: float32
+    metalicRoughnessTexture: Image
+
+    normalScale: float32
+    normalTexture: Image
+
+    occlusionStrength: float32
+    occlusionTexture: Image
+
+    emissiveFactor: float32
+    emissiveTexture: Image
--- a/src/semicongine/mesh.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/mesh.nim	Mon May 15 00:34:00 2023 +0700
@@ -15,8 +15,6 @@
     Small # up to 2^16 vertices
     Big # up to 2^32 vertices
   Mesh* = ref object of Component
-    vertexCount*: uint32
-    indicesCount*: uint32
     instanceCount*: uint32
     data: Table[string, DataList]
     changedAttributes: seq[string]
@@ -33,9 +31,30 @@
     of Small: VK_INDEX_TYPE_UINT16
     of Big: VK_INDEX_TYPE_UINT32
 
+func vertexCount*(mesh: Mesh): uint32 =
+  if mesh.data.len == 0:
+    0'u32
+  else:
+    uint32(mesh.data[mesh.data.keys().toSeq[0]].len)
+
+func indicesCount*(mesh: Mesh): uint32 =
+  case mesh.indexType
+  of None: 0'u32
+  of Tiny: uint32(mesh.tinyIndices.len)
+  of Small: uint32(mesh.smallIndices.len)
+  of Big: uint32(mesh.bigIndices.len)
+
 method `$`*(mesh: Mesh): string =
-  &"Mesh ({mesh.vertexCount})"
+  &"Mesh, vertexCount: {mesh.vertexCount}, vertexData: {mesh.data.keys().toSeq()}, indexType: {mesh.indexType}"
 
+func prettyData*(mesh: Mesh): string =
+  for attr, data in mesh.data.pairs:
+    result &= &"{attr}: {data}\n"
+  result &= (case mesh.indexType
+    of None: ""
+    of Tiny: &"indices: {mesh.tinyIndices}"
+    of Small: &"indices: {mesh.smallIndices}"
+    of Big: &"indices: {mesh.bigIndices}")
 
 func newMesh*(
   positions: openArray[Vec3f],
@@ -49,8 +68,6 @@
   assert uvs.len == 0 or uvs.len == positions.len
 
   result = new Mesh
-  result.vertexCount = uint32(positions.len)
-  result.indicesCount = uint32(indices.len * 3)
   result.instanceCount = instanceCount
   result.data["position"] = DataList(thetype: Vec3F32)
   setValues(result.data["position"], positions.toSeq)
@@ -129,7 +146,6 @@
 proc initData*(mesh: var Mesh, attribute: ShaderAttribute) =
   assert not (attribute.name in mesh.data)
   mesh.data[attribute.name] = DataList(thetype: attribute.thetype)
-  echo "Init ", attribute, " of ", mesh
   if attribute.perInstance:
     mesh.data[attribute.name].initData(mesh.instanceCount)
   else:
@@ -140,11 +156,9 @@
   mesh.data[attribute] = DataList(thetype: getDataType[T]())
   setValues(mesh.data[attribute], data)
 
-proc setInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) =
-  assert uint32(data.len) == mesh.instanceCount
+proc setMeshData*(mesh: var Mesh, attribute: string, data: DataList) =
   assert not (attribute in mesh.data)
-  mesh.data[attribute] = DataList(thetype: getDataType[T]())
-  setValues(mesh.data[attribute], data)
+  mesh.data[attribute] = data
 
 proc updateMeshData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) =
   assert attribute in mesh.data
@@ -156,12 +170,43 @@
   mesh.changedAttributes.add attribute
   setValue(mesh.data[attribute], i, value)
 
+proc appendMeshData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) =
+  assert attribute in mesh.data
+  mesh.changedAttributes.add attribute
+  appendValues(mesh.data[attribute], data)
+
+# currently only used for loading from files, shouls
+proc appendMeshData*(mesh: var Mesh, attribute: string, data: DataList) =
+  assert attribute in mesh.data
+  assert data.thetype == mesh.data[attribute].thetype
+  mesh.changedAttributes.add attribute
+  appendValues(mesh.data[attribute], data)
+
+proc setInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) =
+  assert uint32(data.len) == mesh.instanceCount
+  assert not (attribute in mesh.data)
+  mesh.data[attribute] = DataList(thetype: getDataType[T]())
+  setValues(mesh.data[attribute], data)
+
 proc updateInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) =
   assert uint32(data.len) == mesh.instanceCount
   assert attribute in mesh.data
   mesh.changedAttributes.add attribute
   setValues(mesh.data[attribute], data)
 
+proc appendInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) =
+  assert uint32(data.len) == mesh.instanceCount
+  assert attribute in mesh.data
+  mesh.changedAttributes.add attribute
+  appendValues(mesh.data[attribute], data)
+
+proc appendIndicesData*(mesh: var Mesh, v1, v2, v3: uint32) =
+  case mesh.indexType
+  of None: raise newException(Exception, "Mesh does not support indexed data")
+  of Tiny: mesh.tinyIndices.add([uint8(v1), uint8(v2), uint8(v3)])
+  of Small: mesh.smallIndices.add([uint16(v1), uint16(v2), uint16(v3)])
+  of Big: mesh.bigIndices.add([v1, v2, v3])
+
 func hasDataChanged*(mesh: Mesh, attribute: string): bool =
   attribute in mesh.changedAttributes
 
@@ -170,8 +215,6 @@
 
 func rect*(width=1'f32, height=1'f32, color="ffffffff"): Mesh =
   result = new Mesh
-  result.vertexCount = 4
-  result.indicesCount = 6
   result.instanceCount = 1
   result.data["position"] = DataList(thetype: Vec3F32)
   result.data["color"] = DataList(thetype: Vec4F32)
@@ -191,7 +234,7 @@
 
 func tri*(width=1'f32, height=1'f32, color="ffffffff"): Mesh =
   result = new Mesh
-  result.vertexCount = 3
+  # result.vertexCount = 3
   result.instanceCount = 1
   result.data["position"] = DataList(thetype: Vec3F32)
   result.data["color"] = DataList(thetype: Vec4F32)
@@ -207,7 +250,7 @@
 func circle*(width=1'f32, height=1'f32, nSegments=12'u16, color="ffffffff"): Mesh =
   assert nSegments >= 3
   result = new Mesh
-  result.vertexCount = nSegments + 2
+  # result.vertexCount = nSegments + 2
   result.instanceCount = 1
   result.indexType = Small
   result.data["position"] = DataList(thetype: Vec3F32)
@@ -225,6 +268,5 @@
     col.add c
     result.smallIndices.add [0'u16, i + 1, i + 2]
 
-  result.indicesCount = uint32(result.smallIndices.len * 3)
   setValues(result.data["position"], pos)
   setValues(result.data["color"], col)
--- a/src/semicongine/renderer.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/renderer.nim	Mon May 15 00:34:00 2023 +0700
@@ -101,13 +101,14 @@
     perLocationOffsets: Table[MemoryPerformanceHint, uint64]
     perLocationSizes: Table[MemoryPerformanceHint, uint64]
     bindingNumber = 0
+  for hint in MemoryPerformanceHint:
+    perLocationOffsets[hint] = 0
+    perLocationSizes[hint] = 0
   for attribute in inputs:
     data.attributeLocation[attribute.name] = attribute.memoryPerformanceHint
     data.attributeBindingNumber[attribute.name] = bindingNumber
     inc bindingNumber
     # setup one buffer per attribute-location-type
-    if not (attribute.memoryPerformanceHint in perLocationSizes):
-      perLocationSizes[attribute.memoryPerformanceHint] = 0'u64
     for mesh in allMeshes:
       perLocationSizes[attribute.memoryPerformanceHint] += mesh.dataSize(attribute.name)
   for memoryPerformanceHint, bufferSize in perLocationSizes.pairs:
@@ -118,7 +119,6 @@
         requireMappable=memoryPerformanceHint==PreferFastWrite,
         preferVRAM=true,
       )
-      perLocationOffsets[memoryPerformanceHint] = 0
 
   # fill vertex buffers
   var indexBufferOffset = 0'u64
@@ -127,8 +127,9 @@
     for attribute in inputs:
       offsets.add (attribute.name, attribute.memoryPerformanceHint, perLocationOffsets[attribute.memoryPerformanceHint])
       var (pdata, size) = mesh.getRawData(attribute.name)
-      data.vertexBuffers[attribute.memoryPerformanceHint].setData(pdata, size, perLocationOffsets[attribute.memoryPerformanceHint])
-      perLocationOffsets[attribute.memoryPerformanceHint] += size
+      if pdata != nil: # no data
+        data.vertexBuffers[attribute.memoryPerformanceHint].setData(pdata, size, perLocationOffsets[attribute.memoryPerformanceHint])
+        perLocationOffsets[attribute.memoryPerformanceHint] += size
 
     let indexed = mesh.indexType != None
     var drawable = Drawable(
@@ -182,6 +183,9 @@
 
 proc refreshMeshAttributeData(sceneData: var SceneData, mesh: Mesh, attribute: string) =
   debug &"Refreshing data on mesh {mesh} for {attribute}"
+  # ignore attributes that are not used in this shader
+  if not (attribute in sceneData.attributeLocation):
+    return
   var (pdata, size) = mesh.getRawData(attribute)
   let memoryPerformanceHint = sceneData.attributeLocation[attribute]
   let bindingNumber = sceneData.attributeBindingNumber[attribute]
--- a/src/semicongine/resources.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/resources.nim	Mon May 15 00:34:00 2023 +0700
@@ -5,9 +5,13 @@
 import ./core
 import ./resources/image
 import ./resources/audio
+import ./resources/mesh
+
+from ./entity import Entity, Scene
 
 export image
 export audio
+export mesh
 
 type
   ResourceBundlingType = enum
@@ -102,6 +106,15 @@
 proc loadAudio*(path: string): Sound =
     loadResource_intern(path).readAU()
 
+proc loadMesh*(path: string): Entity =
+    loadResource_intern(path).readglTF()[0].root
+
+proc loadScene*(path: string): Scene =
+    loadResource_intern(path).readglTF()[0]
+
+proc loadScenes*(path: string): seq[Scene] =
+    loadResource_intern(path).readglTF()
+
 proc modList*(): seq[string] =
   modList_intern()
 
--- a/src/semicongine/resources/audio.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/resources/audio.nim	Mon May 15 00:34:00 2023 +0700
@@ -42,7 +42,6 @@
 
 
 # https://en.wikipedia.org/wiki/Au_file_format
-# Returns sound data and samplerate
 proc readAU*(stream: Stream): Sound =
   var header: AuHeader
 
@@ -66,3 +65,6 @@
   stream.setPosition(int(header.dataOffset))
   while not stream.atEnd():
     result[].add stream.readSample(header.encoding, int(header.channels))
+
+var a = 2'u32
+echo a
--- a/src/semicongine/resources/mesh.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/resources/mesh.nim	Mon May 15 00:34:00 2023 +0700
@@ -1,2 +1,237 @@
+import std/json
+import std/tables
+import std/sequtils
+import std/strutils
+import std/strformat
+import std/streams
+
+# from ../entity import Entity, Scene, Component
+import ../entity
+import ../mesh
+import ../core
+
+
+type
+  glTFHeader = object
+    magic: uint32
+    version: uint32
+    length: uint32
+  glTFData = object
+    structuredContent: JsonNode
+    binaryBufferData: seq[uint8]
+
+const
+  JSON_CHUNK = 0x4E4F534A
+  BINARY_CHUNK = 0x004E4942
+  ACCESSOR_TYPE_MAP = {
+    5120: Int8,
+    5121: UInt8,
+    5122: Int16,
+    5123: UInt16,
+    5125: UInt32,
+    5126: Float32,
+  }.toTable
+  VERTEX_ATTRIBUTE_DATA = 34962
+  INSTANCE_ATTRIBUTE_DATA = 34963
+
+func getGPUType(accessor: JsonNode): DataType =
+  # TODO: no full support for all datatypes that glTF may provide
+  # semicongine/core/gpu_data should maybe generated with macros to allow for all combinations
+  debugecho accessor.pretty()
+  let componentType = ACCESSOR_TYPE_MAP[accessor["componentType"].getInt()]
+  let theType = accessor["type"].getStr()
+  case theType
+  of "SCALAR":
+    return componentType
+  of "VEC2":
+    case componentType
+    of UInt32: return Vec2U32
+    of Float32: return Vec2F32
+    else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
+  of "VEC3":
+    case componentType
+    of UInt32: return Vec3U32
+    of Float32: return Vec3F32
+    else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
+  of "VEC4":
+    case componentType
+    of UInt32: return Vec4U32
+    of Float32: return Vec4F32
+    else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
+  of "MAT2":
+    case componentType
+    of Float32: return Vec4F32
+    else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
+  of "MAT3":
+    case componentType
+    of Float32: return Vec4F32
+    else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
+  of "MAT4":
+    case componentType
+    of Float32: return Vec4F32
+    else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
 
 
+proc getAccessorData(root: JsonNode, accessor: JsonNode, mainBuffer: var seq[uint8]): DataList =
+  result.thetype = accessor.getGPUType()
+  result.initData(uint32(accessor["count"].getInt()))
+
+  let bufferView = root["bufferViews"][accessor["bufferView"].getInt()]
+  assert bufferView["buffer"].getInt() == 0, "Currently no external buffers supported"
+
+  if accessor.hasKey("sparse"):
+    raise newException(Exception, "Sparce accessors are currently not implemented")
+
+  let accessorOffset = if accessor.hasKey("byteOffset"): accessor["byteOffset"].getInt() else: 0
+  let length = bufferView["byteLength"].getInt()
+  let bufferOffset = bufferView["byteOffset"].getInt() + accessorOffset
+  var dstPointer = result.getRawData()[0]
+
+  if bufferView.hasKey("byteStride"):
+    raise newException(Exception, "Congratulations, you try to test a feature (loading buffer data with stride attributes) that we have no idea where it is used and how it can be tested (need a coresponding *.glb file). Please open an issue so we can finish the implementation.")
+    # we don't support stride, have to convert stuff here... does this even work?
+    for i in 0 ..< int(result.len):
+      copyMem(dstPointer, addr mainBuffer[bufferOffset + i * bufferView["byteStride"].getInt()], int(result.thetype.size))
+      dstPointer = cast[pointer](cast[uint64](dstPointer) + result.thetype.size)
+  else:
+    copyMem(dstPointer, addr mainBuffer[bufferOffset], length)
+
+proc addPrimitive(mesh: var Mesh, root: JsonNode, primitiveNode: JsonNode, mainBuffer: var seq[uint8]) =
+  # TODO: material
+  if primitiveNode.hasKey("mode") and primitiveNode["mode"].getInt() != 4:
+    raise newException(Exception, "Currently only TRIANGLE mode is supported for geometry mode")
+
+  for attribute, accessor in primitiveNode["attributes"].pairs:
+    let data = root.getAccessorData(root["accessors"][accessor.getInt()], mainBuffer)
+    mesh.appendMeshData(attribute, data)
+
+  if primitiveNode.hasKey("indices"):
+    assert mesh.indexType != None
+    let data = root.getAccessorData(root["accessors"][primitiveNode["indices"].getInt()], mainBuffer)
+    var tri: seq[uint32]
+    case data.thetype
+      of UInt16:
+        for entry in getValues[uint16](data):
+          tri.add uint32(entry)
+          if tri.len == 3:
+            mesh.appendIndicesData(tri[0], tri[1], tri[2])
+            tri.setLen(0)
+      of UInt32:
+        for entry in getValues[uint32](data):
+          tri.add uint32(entry)
+          if tri.len == 3:
+            mesh.appendIndicesData(tri[0], tri[1], tri[2])
+            tri.setLen(0)
+      else:
+        raise newException(Exception, &"Unsupported index data type: {data.thetype}")
+
+proc loadMesh(root: JsonNode, meshNode: JsonNode, mainBuffer: var seq[uint8]): Mesh =
+  result = new Mesh
+  result.instanceCount = 1
+
+  # check if and how we use indexes
+  var indexCount = 0
+  let indexed = meshNode["primitives"][0].hasKey("indices")
+  if indexed:
+    for primitive in meshNode["primitives"]:
+      indexCount += root["accessors"][primitive["indices"].getInt()]["count"].getInt()
+    if indexCount < int(high(uint16)):
+      result.indexType = Small
+    else:
+      result.indexType = Big
+  else:
+    result.indexType = None
+
+  # check we have the same attributes for all primitives
+  let attributes = meshNode["primitives"][0]["attributes"].keys.toSeq
+  for primitive in meshNode["primitives"]:
+    assert primitive["attributes"].keys.toSeq == attributes
+
+  # prepare mesh attributes
+  for attribute, accessor in meshNode["primitives"][0]["attributes"].pairs:
+    result.setMeshData(attribute, DataList(thetype: root["accessors"][accessor.getInt()].getGPUType()))
+
+  # add all mesh data
+  for primitive in meshNode["primitives"]:
+    result.addPrimitive(root, primitive, mainBuffer)
+
+proc loadNode(root: JsonNode, node: JsonNode, mainBuffer: var seq[uint8]): Entity =
+  var name = "<Unknown>"
+  if node.hasKey("name"):
+    name = node["name"].getStr()
+  result = newEntity(name)
+
+  # transformation
+  if node.hasKey("matrix"):
+    for i in 0 .. node["matrix"].len:
+      result.transform.data[i] = node["matrix"][i].getFloat()
+  else:
+    var (t, r, s) = (Unit4F32, Unit4F32, Unit4F32)
+    if node.hasKey("translation"):
+      t = translate3d(
+        float32(node["translation"][0].getFloat()),
+        float32(node["translation"][1].getFloat()),
+        float32(node["translation"][2].getFloat())
+      )
+    if node.hasKey("rotation"):
+      t = rotate3d(
+        float32(node["rotation"][3].getFloat()),
+        newVec3f(
+          float32(node["rotation"][0].getFloat()),
+          float32(node["rotation"][1].getFloat()),
+          float32(node["rotation"][2].getFloat())
+        )
+      )
+    if node.hasKey("scale"):
+      t = scale3d(
+        float32(node["scale"][0].getFloat()),
+        float32(node["scale"][1].getFloat()),
+        float32(node["scale"][2].getFloat())
+      )
+    result.transform = t * r * s
+
+  # children
+  if node.hasKey("children"):
+    for childNode in node["children"]:
+      result.add loadNode(root, root["nodes"][childNode.getInt()], mainBuffer)
+
+  # mesh
+  if node.hasKey("mesh"):
+    result.add loadMesh(root, root["meshes"][node["mesh"].getInt()], mainBuffer)
+
+proc loadScene(root: JsonNode, scenenode: JsonNode, mainBuffer: var seq[uint8]): Scene =
+  var rootEntity = newEntity("<root>")
+  for nodeId in scenenode["nodes"]:
+    rootEntity.add loadNode(root, root["nodes"][nodeId.getInt()], mainBuffer)
+
+  newScene(scenenode["name"].getStr(), rootEntity)
+
+proc readglTF*(stream: Stream): seq[Scene] =
+  var
+    header: glTFHeader
+    data: glTFData
+
+  for name, value in fieldPairs(header):
+    stream.read(value)
+
+  assert header.magic == 0x46546C67
+  assert header.version == 2
+
+  var chunkLength = stream.readUint32()
+  assert stream.readUint32() == JSON_CHUNK
+  data.structuredContent = parseJson(stream.readStr(int(chunkLength)))
+
+  chunkLength = stream.readUint32()
+  assert stream.readUint32() == BINARY_CHUNK
+  data.binaryBufferData.setLen(chunkLength)
+  assert stream.readData(addr data.binaryBufferData[0], int(chunkLength)) == int(chunkLength)
+
+  # check that the refered buffer is the same as the binary chunk
+  # external binary buffers are not supported
+  assert data.structuredContent["buffers"].len == 1
+  assert not data.structuredContent["buffers"][0].hasKey("uri")
+  let bufferLenDiff = int(chunkLength) - data.structuredContent["buffers"][0]["byteLength"].getInt()
+  assert 0 <= bufferLenDiff <= 3 # binary buffer may be aligned to 4 bytes
+
+  for scene in data.structuredContent["scenes"]:
+    result.add data.structuredContent.loadScene(scene, data.binaryBufferData)
--- a/src/semicongine/vulkan/physicaldevice.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/semicongine/vulkan/physicaldevice.nim	Mon May 15 00:34:00 2023 +0700
@@ -103,7 +103,7 @@
   assert surface.valid
   var presentation = VkBool32(false)
   checkVkResult vkGetPhysicalDeviceSurfaceSupportKHR(family.device.vk, family.index, surface, addr presentation)
-  return presentation
+  return bool(presentation)
 
 proc canDoTransfer*(family: QueueFamily): bool =
   VK_QUEUE_TRANSFER_BIT in family.flags
--- a/src/vulkan_api/vulkan_api_generator.nim	Sat May 13 19:32:28 2023 +0700
+++ b/src/vulkan_api/vulkan_api_generator.nim	Mon May 15 00:34:00 2023 +0700
@@ -422,7 +422,6 @@
       "import std/logging",
       "import std/typetraits",
       "import std/macros",
-      "import std/private/digitsutils",
       "type",
       "  VkHandle* = distinct uint",
       "  VkNonDispatchableHandle* = distinct uint",
@@ -661,9 +660,9 @@
   for l in GLOBAL_COMMANDS:
     mainout.add procLoads[l]
   mainout.add ""
-  mainout.add "converter VkBool2NimBool*(a: VkBool32): bool = a > 0"
+  # produces error if enable both implicit converters
+  # mainout.add "converter VkBool2NimBool*(a: VkBool32): bool = a > 0"
   mainout.add "converter NimBool2VkBool*(a: bool): VkBool32 = VkBool32(a)"
-  mainout.add "proc `$`*(x: uint32): string {.raises: [].} = addInt(result, x)"
 
   writeFile outdir / &"api.nim", mainout.join("\n")
 
Binary file tests/resources/default/test1.glb has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_mesh.nim	Mon May 15 00:34:00 2023 +0700
@@ -0,0 +1,40 @@
+import std/times
+
+import semicongine
+
+proc main() =
+  var scene = loadScene("test1.glb")
+  # var scene = loadScene("tutorialk-donat.glb")
+
+  var engine = initEngine("Test meshes")
+  const
+    vertexInput = @[attr[Vec3f]("POSITION", memoryPerformanceHint=PreferFastRead)]
+    fragOutput = @[attr[Vec4f]("color")]
+    uniforms = @[attr[Mat4]("transform")]
+    vertexCode = compileGlslShader(
+      stage=VK_SHADER_STAGE_VERTEX_BIT,
+      inputs=vertexInput,
+      uniforms=uniforms,
+      main="""gl_Position = vec4(POSITION * 0.2, 1.0) * Uniforms.transform;"""
+    )
+    fragmentCode = compileGlslShader(
+      stage=VK_SHADER_STAGE_FRAGMENT_BIT,
+      outputs=fragOutput,
+      uniforms=uniforms,
+      main="""
+color = vec4(61/255, 43/255, 31/255, 1);
+"""
+    )
+  engine.setRenderer(engine.gpuDevice.simpleForwardRenderPass(vertexCode, fragmentCode))
+  engine.addScene(scene, vertexInput)
+  let rotateAxis = newVec3f(1, 1, 1)
+  scene.addShaderGlobal("transform", rotate3d(0'f32, rotateAxis))
+  var t = cpuTime()
+  while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
+    scene.setShaderGlobal("transform", rotate3d(float32(cpuTime() - t), rotateAxis))
+    engine.renderScene(scene)
+  engine.destroy()
+
+
+when isMainModule:
+  main()