changeset 33:94c38e4b5782

did: refactoring, move more from make to nimscript
author Sam <sam@basx.dev>
date Sun, 15 Jan 2023 23:23:54 +0700
parents 9edca5dc4e93
children a3d46f434616
files Makefile config.nims examples/hello_triangle.nim examples/squares.nim notes src/zamikongine/buffer.nim src/zamikongine/descriptor.nim src/zamikongine/engine.nim src/zamikongine/mesh.nim src/zamikongine/shader.nim src/zamikongine/thing.nim src/zamikongine/vertex.nim
diffstat 12 files changed, 232 insertions(+), 116 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sat Jan 14 23:34:50 2023 +0700
+++ b/Makefile	Sun Jan 15 23:23:54 2023 +0700
@@ -1,53 +1,9 @@
-SOURCES := $(shell find src -name '*.nim')
-
 # compilation requirements
 examples/glslangValidator: thirdparty/bin/linux/glslangValidator
 	cp $< examples
 examples/glslangValidator.exe: thirdparty/bin/windows/glslangValidator.exe
 	cp $< examples
 
-# build hello_triangle
-build/debug/linux/hello_triangle: ${SOURCES} examples/hello_triangle.nim examples/glslangValidator
-	nim build_linux_debug -o:$@ examples/hello_triangle.nim
-build/release/linux/hello_triangle: ${SOURCES} examples/hello_triangle.nim examples/glslangValidator
-	nim build_linux_release -o:$@ examples/hello_triangle.nim
-build/debug/windows/hello_triangle.exe: ${SOURCES} examples/hello_triangle.nim examples/glslangValidator.exe
-	nim build_windows_debug -o:$@ examples/hello_triangle.nim
-build/release/windows/hello_triangle.exe: ${SOURCES} examples/hello_triangle.nim examples/glslangValidator.exe
-	nim build_windows_release -o:$@ examples/hello_triangle.nim
-
-build_all_linux_hello_triangle: build/debug/linux/hello_triangle build/release/linux/hello_triangle
-build_all_windows_hello_triangle: build/debug/windows/hello_triangle.exe build/release/windows/hello_triangle.exe
-build_all_hello_triangle: build_all_linux_hello_triangle build_all_windows_hello_triangle
-
-# build alotof_triangles
-build/debug/linux/alotof_triangles: ${SOURCES} examples/alotof_triangles.nim examples/glslangValidator
-	nim build_linux_debug -o:$@ examples/alotof_triangles.nim
-build/release/linux/alotof_triangles: ${SOURCES} examples/alotof_triangles.nim examples/glslangValidator
-	nim build_linux_release -o:$@ examples/alotof_triangles.nim
-build/debug/windows/alotof_triangles.exe: ${SOURCES} examples/alotof_triangles.nim examples/glslangValidator.exe
-	nim build_windows_debug -o:$@ examples/alotof_triangles.nim
-build/release/windows/alotof_triangles.exe: ${SOURCES} examples/alotof_triangles.nim examples/glslangValidator.exe
-	nim build_windows_release -o:$@ examples/alotof_triangles.nim
-
-build_all_linux_alotof_triangles: build/debug/linux/alotof_triangles build/release/linux/alotof_triangles
-build_all_windows_alotof_triangles: build/debug/windows/alotof_triangles.exe build/release/windows/alotof_triangles.exe
-build_all_alotof_triangles: build_all_linux_alotof_triangles build_all_windows_alotof_triangles
-
-# clean
-clean:
-	rm -rf build
-	rm -rf thirdparty
-
-# tests
-.PHONY: tests
-tests:
-	testament p tests/
-
-# publish
-publish:
-	rsync -rv build/ basx.dev:/var/www/public.basx.dev/zamikongine
-
 # download thirdparty-libraries
 
 thirdparty/bin/linux/glslangValidator:
--- a/config.nims	Sat Jan 14 23:34:50 2023 +0700
+++ b/config.nims	Sun Jan 15 23:23:54 2023 +0700
@@ -1,6 +1,12 @@
-import os
+import std/strformat
+import std/strutils
+import std/os
 
-const buildbase = "build"
+const BUILDBASE = "build"
+const DEBUG = "debug"
+const RELEASE = "release"
+const LINUX = "linux"
+const WINDOWS = "windows"
 
 proc compilerFlags() =
   switch("path", "src")
@@ -19,30 +25,85 @@
   switch("checks", "off")
   switch("assertions", "off")
 
-task build_linux_debug, "build linux debug":
+task single_linux_debug, "build linux debug":
   compilerFlags()
   compilerFlagsDebug()
-  buildbase.joinPath("debug/linux").mkDir()
+  switch("outdir", BUILDBASE / DEBUG / LINUX)
+  setCommand "c"
+  mkDir(BUILDBASE / DEBUG / LINUX)
+
+task single_linux_release, "build linux release":
+  compilerFlags()
+  compilerFlagsRelease()
+  switch("outdir", BUILDBASE / RELEASE / LINUX)
   setCommand "c"
+  mkDir(BUILDBASE / RELEASE / LINUX)
 
-task build_linux_release, "build linux release":
+task single_windows_debug, "build windows debug":
+  compilerFlags()
+  compilerFlagsDebug()
+  # for some the --define:mingw does not work from inside here...
+  # so we need to set it when calling the task and use "/" to prevent
+  # the use of backslash while crosscompiling
+  switch("define", "mingw")
+  switch("outdir", BUILDBASE & "/" & DEBUG & "/" & WINDOWS)
+  setCommand "c"
+  mkDir(BUILDBASE & "/" & DEBUG & "/" & WINDOWS)
+
+task single_windows_release, "build windows release":
   compilerFlags()
   compilerFlagsRelease()
-  buildbase.joinPath("release/linux").mkDir()
-  setCommand "c"
-
-task build_windows_debug, "build windows debug":
-  compilerFlags()
-  compilerFlagsDebug()
+  switch("outdir", BUILDBASE & "/" & RELEASE & "/" & WINDOWS)
   switch("define", "mingw")
-  buildbase.joinPath("debug/windows").mkDir()
   setCommand "c"
+  mkDir(BUILDBASE & "/" & RELEASE & "/" & WINDOWS)
 
-task build_windows_release, "build windows release":
+task build_all_linux_debug, "build all examples with linux/debug":
+  for file in listFiles("examples"):
+    if file.endsWith(".nim"):
+      selfExec(&"single_linux_debug {file}")
+
+task build_all_linux_release, "build all examples with linux/release":
+  for file in listFiles("examples"):
+    if file.endsWith(".nim"):
+      selfExec(&"single_linux_release {file}")
+
+task build_all_windows_debug, "build all examples with windows/debug":
+  for file in listFiles("examples"):
+    if file.endsWith(".nim"):
+      exec(&"nim single_windows_debug --define:mingw {file}")
+
+task build_all_windows_release, "build all examples with windows/release":
+  for file in listFiles("examples"):
+    if file.endsWith(".nim"):
+      exec(&"nim single_windows_release --define:mingw {file}")
+
+task build_all_debug, "build all examples with */debug":
+  build_all_linux_debugTask()
+  build_all_windows_debugTask()
+
+task build_all_release, "build all examples with */release":
+  build_all_linux_releaseTask()
+  build_all_windows_releaseTask()
+
+task build_all_linux, "build all examples with linux/*":
+  build_all_linux_debugTask()
+  build_all_linux_releaseTask()
+
+task build_all_windows, "build all examples with windows/*":
+  build_all_windows_debugTask()
+  build_all_windows_releaseTask()
+
+task build_all, "build all examples":
+  build_all_linuxTask()
+  build_all_windowsTask()
+
+task clean, "remove all build files":
+  exec(&"rm -rf {BUILDBASE}")
+
+task publish, "publish all build":
+  exec("rsync -rv build/ basx.dev:/var/www/public.basx.dev/zamikongine")
+
+
+if getCommand() == "c":
   compilerFlags()
-  compilerFlagsRelease()
-  switch("define", "mingw")
-  buildbase.joinPath("release/windows").mkDir()
-  setCommand "c"
-
-compilerFlags()
--- a/examples/hello_triangle.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/examples/hello_triangle.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -17,25 +17,11 @@
   VertexDataA = object
     position: PositionAttribute[Vec2[float32]]
     color: ColorAttribute[Vec3[float32]]
-  Uniforms = object
-    mat: Descriptor[Mat44[float32]]
-    dt: Descriptor[float32]
 
-var pipeline: RenderPipeline[VertexDataA, Uniforms]
+var pipeline: RenderPipeline[VertexDataA, void]
 
-var pos = 0'f32;
-var uniforms = Uniforms(
-  mat: Descriptor[Mat44[float32]](value: Unit44f32),
-  dt: Descriptor[float32](value: 0'f32),
-)
-var scaledir = 1'f32
 proc globalUpdate(engine: var Engine, dt: float32) =
-  uniforms.mat.value = uniforms.mat.value * scale3d(1 + scaledir * dt, 1 + scaledir * dt, 0'f32)
-  if uniforms.mat.value[0, 0] > 2'f32 or uniforms.mat.value[0, 0] < 0.5'f32:
-    scaledir = - scaledir
-  for buffer in pipeline.uniformBuffers:
-    buffer.updateData(uniforms)
-  echo uniforms.mat.value
+  discard
 
 # vertex data (types must match the above VertexAttributes)
 const
@@ -65,22 +51,11 @@
   triangle.parts.add trianglemesh
 
   # upload data, prepare shaders, etc
-  const vertexShader = generateVertexShaderCode[VertexDataA, Uniforms](
-    # have 1 at:
-    # [2][0] [0][3]
-    # "out_position = vec4(in_position[0] + uniforms.mat[0][0], in_position[1] + uniforms.mat[0][0], 0, 1);"
-    "out_position = uniforms.mat * vec4(in_position, 0, 1);"
-    # "out_position = vec4(in_position, 0, 1);"
+  const vertexShader = generateVertexShaderCode[VertexDataA, void](
+    # "out_position = uniforms.mat * vec4(in_position, 0, 1);"
   )
   const fragmentShader = generateFragmentShaderCode[VertexDataA]()
-  static:
-    echo "--------------"
-    for (i, line) in enumerate(vertexShader.splitLines()):
-      echo $(i + 1) & " " & line
-    echo "--------------"
-    echo fragmentShader
-    echo "--------------"
-  pipeline = setupPipeline[VertexDataA, Uniforms, uint16](
+  pipeline = setupPipeline[VertexDataA, void, uint16](
     myengine,
     triangle,
     vertexShader,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/squares.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -0,0 +1,109 @@
+import std/times
+import std/strutils
+import std/math
+import std/random
+import std/enumerate
+
+import zamikongine/engine
+import zamikongine/math/vector
+import zamikongine/math/matrix
+import zamikongine/vertex
+import zamikongine/descriptor
+import zamikongine/mesh
+import zamikongine/thing
+import zamikongine/shader
+import zamikongine/buffer
+
+type
+  VertexDataA = object
+    position11: PositionAttribute[Vec2[float32]]
+    color22: ColorAttribute[Vec3[float32]]
+    index: GenericAttribute[uint32]
+  Uniforms = object
+    t: Descriptor[float32]
+
+var
+  pipeline: RenderPipeline[VertexDataA, Uniforms]
+  uniformdata = Uniforms(t: Descriptor[float32](value: 0'f32))
+
+proc globalUpdate(engine: var Engine, dt: float32) =
+  uniformdata.t.value += dt
+  for buffer in pipeline.uniformBuffers:
+    buffer.updateData(uniformdata)
+
+when isMainModule:
+  randomize()
+  var myengine = igniteEngine("A lot of triangles")
+  const
+    COLUMNS = 10
+    ROWS = 10
+    WIDTH = 2'f32 / COLUMNS
+    HEIGHT = 2'f32 / ROWS
+  var
+    vertices: array[COLUMNS * ROWS * 4, Vec2[float32]]
+    colors: array[COLUMNS * ROWS * 4, Vec3[float32]]
+    iValues: array[COLUMNS * ROWS * 4, uint32]
+    indices: array[COLUMNS * ROWS * 2, array[3, uint16]]
+
+  for row in 0 ..< ROWS:
+    for col in 0 ..< COLUMNS:
+      let
+        y: float32 = (row * 2 / COLUMNS) - 1
+        x: float32 = (col * 2 / ROWS) - 1
+        color = Vec3[float32]([(x + 1) / 2, (y + 1) / 2, 0'f32])
+        squareIndex = row * COLUMNS + col
+        vertIndex = squareIndex * 4
+      vertices[vertIndex + 0] = Vec2[float32]([x, y])
+      vertices[vertIndex + 1] = Vec2[float32]([x + WIDTH, y])
+      vertices[vertIndex + 2] = Vec2[float32]([x + WIDTH, y + HEIGHT])
+      vertices[vertIndex + 3] = Vec2[float32]([x, y + HEIGHT])
+      colors[vertIndex + 0] = color
+      colors[vertIndex + 1] = color
+      colors[vertIndex + 2] = color
+      colors[vertIndex + 3] = color
+      iValues[vertIndex + 0] = uint32(squareIndex)
+      iValues[vertIndex + 1] = uint32(squareIndex)
+      iValues[vertIndex + 2] = uint32(squareIndex)
+      iValues[vertIndex + 3] = uint32(squareIndex)
+      indices[squareIndex * 2 + 0] = [uint16(vertIndex + 0), uint16(vertIndex + 1), uint16(vertIndex + 2)]
+      indices[squareIndex * 2 + 1] = [uint16(vertIndex + 2), uint16(vertIndex + 3), uint16(vertIndex + 0)]
+
+  var scene = new Thing
+
+  type PIndexedMesh = ref IndexedMesh[VertexDataA, uint16] # required so we can use ctor with ref/on heap
+  var squaremesh = PIndexedMesh(
+    vertexData: VertexDataA(
+      position11: PositionAttribute[Vec2[float32]](data: @vertices),
+      color22: ColorAttribute[Vec3[float32]](data: @colors),
+      index: GenericAttribute[uint32](data: @iValues),
+    ),
+    indices: @indices
+  )
+  var childthing = new Thing
+  childthing.parts.add squaremesh
+  scene.children.add childthing
+
+  const vertexShader = generateVertexShaderCode[VertexDataA, Uniforms](
+    """
+    float pos_weight = index / 100.0; // add some gamma correction?
+    float t = sin(uniforms.t * 0.5) * 0.5 + 0.5;
+    float v = min(1, max(0, pow(pos_weight - t, 2)));
+    v = pow(1 - v, 3000);
+    out_color = vec3(in_color.r, in_color.g, v * 0.5);
+    """
+  )
+  const fragmentShader = generateFragmentShaderCode[VertexDataA]()
+  static:
+    echo "--------------"
+    for (i, line) in enumerate(vertexShader.splitLines()):
+      echo $(i + 1) & " " & line
+    echo "--------------"
+  pipeline = setupPipeline[VertexDataA, Uniforms, uint16](
+    myengine,
+    scene,
+    vertexShader,
+    fragmentShader
+  )
+  myengine.run(pipeline, globalUpdate)
+  pipeline.trash()
+  myengine.trash()
--- a/notes	Sat Jan 14 23:34:50 2023 +0700
+++ b/notes	Sun Jan 15 23:23:54 2023 +0700
@@ -17,7 +17,8 @@
 
 Rendering:
 
-- [ ] Uniforms
+- [x] Uniforms
+- [ ] Per-instance vertex attributes
 - [ ] Textures
 - [ ] Depth buffering
 
--- a/src/zamikongine/buffer.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/buffer.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -21,9 +21,11 @@
 
 proc trash*(buffer: var Buffer) =
   assert int64(buffer.vkBuffer) != 0
-  assert int64(buffer.memory) != 0
   vkDestroyBuffer(buffer.device, buffer.vkBuffer, nil)
   buffer.vkBuffer = VkBuffer(0)
+  if buffer.size == 0: # for zero-size buffers there are no memory allocations
+    return
+  assert int64(buffer.memory) != 0
   vkFreeMemory(buffer.device, buffer.memory, nil)
   buffer.memory = VkDeviceMemory(0)
 
@@ -65,7 +67,8 @@
     allocationSize: result.memoryRequirements.size,
     memoryTypeIndex: result.findMemoryType(physicalDevice, VkMemoryPropertyFlags(memoryProperties))
   )
-  checkVkResult result.device.vkAllocateMemory(addr(allocInfo), nil, addr(result.memory))
+  if result.size > 0:
+    checkVkResult result.device.vkAllocateMemory(addr(allocInfo), nil, addr(result.memory))
   checkVkResult result.device.vkBindBufferMemory(result.vkBuffer, result.memory, VkDeviceSize(0))
   if persistentMapping:
     checkVkResult vkMapMemory(
--- a/src/zamikongine/descriptor.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/descriptor.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -33,8 +33,8 @@
     )
   checkVkResult device.vkCreateDescriptorSetLayout(addr(layoutInfo), nil, addr(result))
 
-proc createUniformBuffers*[nBuffers: static int, T](device: VkDevice, physicalDevice: VkPhysicalDevice): array[nBuffers, Buffer] =
-  let size = sizeof(T)
+proc createUniformBuffers*[nBuffers: static int, Uniforms](device: VkDevice, physicalDevice: VkPhysicalDevice): array[nBuffers, Buffer] =
+  let size = sizeof(Uniforms)
   for i in 0 ..< nBuffers:
     var buffer = InitBuffer(
       device,
@@ -51,14 +51,15 @@
 func generateGLSLUniformDeclarations*[Uniforms](binding: int = 0): string {.compileTime.} =
   var stmtList: seq[string]
 
-  let uniformTypeName = name(Uniforms).toUpper()
-  let uniformInstanceName = name(Uniforms).toLower()
-  stmtList.add(&"layout(binding = {binding}) uniform {uniformTypeName} {{")
-  for fieldname, value in Uniforms().fieldPairs:
-    when typeof(value) is Descriptor:
-      let glsltype = getGLSLType[getDescriptorType(value)]()
-      let n = fieldname
-      stmtList.add(&"    {glsltype} {n};")
-  stmtList.add(&"}} {uniformInstanceName};")
+  when not (Uniforms is void):
+    let uniformTypeName = name(Uniforms).toUpper()
+    let uniformInstanceName = name(Uniforms).toLower()
+    stmtList.add(&"layout(binding = {binding}) uniform {uniformTypeName} {{")
+    for fieldname, value in Uniforms().fieldPairs:
+      when typeof(value) is Descriptor:
+        let glsltype = getGLSLType[getDescriptorType(value)]()
+        let n = fieldname
+        stmtList.add(&"    {glsltype} {n};")
+    stmtList.add(&"}} {uniformInstanceName};")
 
   return stmtList.join("\n")
--- a/src/zamikongine/engine.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/engine.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -501,7 +501,7 @@
   ) = result.vulkan.device.device.setupSyncPrimitives()
 
 
-proc setupPipeline*[VertexType, UniformType: object, IndexType: uint16|uint32](engine: var Engine, scenedata: ref Thing, vertexShader, fragmentShader: static string): RenderPipeline[VertexType, UniformType] =
+proc setupPipeline*[VertexType; UniformType; IndexType: uint16|uint32](engine: var Engine, scenedata: ref Thing, vertexShader, fragmentShader: static string): RenderPipeline[VertexType, UniformType] =
   engine.currentscenedata = scenedata
   result = initRenderPipeline[VertexType, UniformType](
     engine.vulkan.device.device,
@@ -576,7 +576,7 @@
     vkUpdateDescriptorSets(result.device, 1, addr(descriptorWrite), 0, nil)
 
 
-proc runPipeline(commandBuffer: VkCommandBuffer, pipeline: var RenderPipeline, currentFrame: int) =
+proc runPipeline[VertexType; Uniforms](commandBuffer: VkCommandBuffer, pipeline: var RenderPipeline[VertexType, Uniforms], currentFrame: int) =
   vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.pipeline)
 
   vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.layout, 0, 1, addr(pipeline.descriptors[currentFrame]), 0, nil)
@@ -588,7 +588,7 @@
       vertexBuffers.add buffer.vkBuffer
       offsets.add VkDeviceSize(0)
 
-    vkCmdBindVertexBuffers(commandBuffer, firstBinding=0'u32, bindingCount=2'u32, pBuffers=addr(vertexBuffers[0]), pOffsets=addr(offsets[0]))
+    vkCmdBindVertexBuffers(commandBuffer, firstBinding=0'u32, bindingCount=uint32(vertexBuffers.len), pBuffers=addr(vertexBuffers[0]), pOffsets=addr(offsets[0]))
     vkCmdDraw(commandBuffer, vertexCount=vertexCount, instanceCount=1'u32, firstVertex=0'u32, firstInstance=0'u32)
 
   for (vertexBufferSet, indexBuffer, indicesCount, indexType) in pipeline.indexedVertexBuffers:
@@ -599,7 +599,7 @@
       vertexBuffers.add buffer.vkBuffer
       offsets.add VkDeviceSize(0)
 
-    vkCmdBindVertexBuffers(commandBuffer, firstBinding=0'u32, bindingCount=2'u32, pBuffers=addr(vertexBuffers[0]), pOffsets=addr(offsets[0]))
+    vkCmdBindVertexBuffers(commandBuffer, firstBinding=0'u32, bindingCount=uint32(vertexBuffers.len), pBuffers=addr(vertexBuffers[0]), pOffsets=addr(offsets[0]))
     vkCmdBindIndexBuffer(commandBuffer, indexBuffer.vkBuffer, VkDeviceSize(0), indexType)
     vkCmdDrawIndexed(commandBuffer, indicesCount, 1, 0, 0, 0)
 
--- a/src/zamikongine/mesh.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/mesh.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -4,6 +4,7 @@
 import ./thing
 import ./buffer
 import ./vertex
+import ./math/vector
 
 type
   Mesh*[T] = object of Part
@@ -101,3 +102,10 @@
   result[2] = uint32(mesh.indices.len * mesh.indices[0].len)
 
   result[3] = getVkIndexType(mesh)
+
+func squareData*[T:SomeFloat](): auto = PositionAttribute[Vec2[T]](
+  data: @[Vec2[T]([T(0), T(0)]), Vec2[T]([T(0), T(1)]), Vec2[T]([T(1), T(1)]), Vec2[T]([T(1), T(0)])]
+)
+func squareIndices*[T:uint16|uint32](): auto = seq[array[3, T]](
+  @[[T(1), T(0), T(3)], [T(2), T(1), T(3)], ]
+)
--- a/src/zamikongine/shader.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/shader.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -35,8 +35,6 @@
   let stagename = stage2string(stage)
 
   # TODO: compiles only on linux for now (because we don't have compile-time functionality in std/tempfile)
-  if not defined(linux):
-    raise newException(Exception, "Compilation is currently only supported on linux (need mktemp command), sorry!")
   let (tmpfile, exitCode) = gorgeEx(command=fmt"mktemp --tmpdir shader_XXXXXXX.{stagename}")
   if exitCode != 0:
     raise newException(Exception, tmpfile)
--- a/src/zamikongine/thing.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/thing.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -1,5 +1,4 @@
 {.experimental: "codeReordering".}
-import std/times
 
 type
   Part* = object of RootObj
--- a/src/zamikongine/vertex.nim	Sat Jan 14 23:34:50 2023 +0700
+++ b/src/zamikongine/vertex.nim	Sun Jan 15 23:23:54 2023 +0700
@@ -14,9 +14,9 @@
     Unknown, Position Color
   GenericAttribute*[T:VertexAttributeType] = object
     data*: seq[T]
-  PositionAttribute*[T:VertexAttributeType] = object
+  PositionAttribute*[T:Vec] = object
     data*: seq[T]
-  ColorAttribute*[T:VertexAttributeType] = object
+  ColorAttribute*[T:Vec] = object
     data*: seq[T]
   VertexAttribute* = GenericAttribute|PositionAttribute|ColorAttribute
 
@@ -85,6 +85,11 @@
       else:
         assert result == uint32(value.data.len)
 
+func VertexAttributesCount*[T](): uint32 =
+  for name, value in T().fieldPairs:
+    when typeof(value) is VertexAttribute:
+      result += 1
+
 func generateGLSLVertexDeclarations*[T](): string =
   var stmtList: seq[string]
   var i = 0