changeset 794:59c54c4486c4

fix: material handling, gltf loading, loader example
author Sam <sam@basx.dev>
date Mon, 04 Sep 2023 00:31:17 +0700
parents c31e42d72253
children 4a15d94d9418
files src/semicongine/core/gpu_data.nim src/semicongine/core/imagetypes.nim src/semicongine/mesh.nim src/semicongine/renderer.nim src/semicongine/resources/mesh.nim src/semicongine/text.nim tests/test_mesh.nim
diffstat 7 files changed, 158 insertions(+), 94 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/core/gpu_data.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/src/semicongine/core/gpu_data.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -1,15 +1,15 @@
 import std/strformat
 import std/tables
-import std/hashes
 
 import ./vulkanapi
 import ./vector
 import ./matrix
 import ./utils
+import ./imagetypes
 
 type
   Sampler2DType* = object
-  GPUType* = float32 | float64 | int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | TVec2[int32] | TVec2[int64] | TVec3[int32] | TVec3[int64] | TVec4[int32] | TVec4[int64] | TVec2[uint32] | TVec2[uint64] | TVec3[uint32] | TVec3[uint64] | TVec4[uint32] | TVec4[uint64] | TVec2[float32] | TVec2[float64] | TVec3[float32] | TVec3[float64] | TVec4[float32] | TVec4[float64] | TMat2[float32] | TMat2[float64] | TMat23[float32] | TMat23[float64] | TMat32[float32] | TMat32[float64] | TMat3[float32] | TMat3[float64] | TMat34[float32] | TMat34[float64] | TMat43[float32] | TMat43[float64] | TMat4[float32] | TMat4[float64] | Sampler2DType
+  GPUType* = float32 | float64 | int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | TVec2[int32] | TVec2[int64] | TVec3[int32] | TVec3[int64] | TVec4[int32] | TVec4[int64] | TVec2[uint32] | TVec2[uint64] | TVec3[uint32] | TVec3[uint64] | TVec4[uint32] | TVec4[uint64] | TVec2[float32] | TVec2[float64] | TVec3[float32] | TVec3[float64] | TVec4[float32] | TVec4[float64] | TMat2[float32] | TMat2[float64] | TMat23[float32] | TMat23[float64] | TMat32[float32] | TMat32[float64] | TMat3[float32] | TMat3[float64] | TMat34[float32] | TMat34[float64] | TMat43[float32] | TMat43[float64] | TMat4[float32] | TMat4[float64] | Texture
   DataType* = enum
     Float32
     Float64
@@ -98,7 +98,7 @@
     of Mat43F64: mat43f64: TMat43[float64]
     of Mat4F32: mat4f32: TMat4[float32]
     of Mat4F64: mat4f64: TMat4[float64]
-    of Sampler2D: discard
+    of Sampler2D: texture: Texture
   DataList* = object
     len*: int
     case theType*: DataType
@@ -142,9 +142,9 @@
     of Mat34F64: mat34f64: seq[TMat34[float64]]
     of Mat43F32: mat43f32: seq[TMat43[float32]]
     of Mat43F64: mat43f64: seq[TMat43[float64]]
-    of Mat4F32: mat4f32*: seq[TMat4[float32]]
+    of Mat4F32: mat4f32: seq[TMat4[float32]]
     of Mat4F64: mat4f64: seq[TMat4[float64]]
-    of Sampler2D: discard
+    of Sampler2D: texture: seq[Texture]
   MemoryPerformanceHint* = enum
     PreferFastRead, PreferFastWrite
   ShaderAttribute* = object
@@ -201,7 +201,7 @@
     of Mat43F64: return a.mat43f64 == b.mat43f64
     of Mat4F32: return a.mat4f32 == b.mat4f32
     of Mat4F64: return a.mat4f64 == b.mat4f64
-    of Sampler2D: raise newException(Exception, "'==' not defined for Sampler2D")
+    of Sampler2D: a.texture == b.texture
 
 func vertexInputs*(attributes: seq[ShaderAttribute]): seq[ShaderAttribute] =
   for attr in attributes:
@@ -334,7 +334,7 @@
   elif T is TMat43[float64]: Mat43F64
   elif T is TMat4[float32]: Mat4F32
   elif T is TMat4[float64]: Mat4F64
-  elif T is Sampler2DType: Sampler2D
+  elif T is Texture: Sampler2D
   else:
     static:
       raise newException(Exception, &"Unsupported data type for GPU data: {name(T)}" )
@@ -404,6 +404,7 @@
   elif T is TMat43[float64]: value.mat43f64
   elif T is TMat4[float32]: value.mat4f32
   elif T is TMat4[float64]: value.mat4f64
+  elif T is Texture: value.texture
   else: {.error: "Virtual datatype has no value" .}
 
 func setValues*[T: GPUType|int|uint|float](value: var DataList, data: seq[T]) =
@@ -456,6 +457,7 @@
   elif T is TMat43[float64]: value.mat43f64 = data
   elif T is TMat4[float32]: value.mat4f32 = data
   elif T is TMat4[float64]: value.mat4f64 = data
+  elif T is Texture: value.texture = data
   else: {. error: "Virtual datatype has no values" .}
 
 func newDataList*(theType: DataType): DataList =
@@ -571,6 +573,7 @@
   elif T is TMat43[float64]: value.mat43f64
   elif T is TMat4[float32]: value.mat4f32
   elif T is TMat4[float64]: value.mat4f64
+  elif T is Texture: value.texture
   else: {. error: "Virtual datatype has no values" .}
 
 func getValue*[T: GPUType|int|uint|float](value: DataList, i: int): T =
@@ -622,6 +625,7 @@
   elif T is TMat43[float64]: value.mat43f64[i]
   elif T is TMat4[float32]: value.mat4f32[i]
   elif T is TMat4[float64]: value.mat4f64[i]
+  elif T is Texture: value.texture[i]
   else: {. error: "Virtual datatype has no values" .}
 
 func getRawData*(value: var DataValue): (pointer, int) =
@@ -816,6 +820,7 @@
   elif T is TMat43[float64]: value.mat43f64 = data
   elif T is TMat4[float32]: value.mat4f32 = data
   elif T is TMat4[float64]: value.mat4f64 = data
+  elif T is Texture: value.texture = data
   else: {.error: "Virtual datatype has no value" .}
 
 func appendValues*[T: GPUType|int|uint|float](value: var DataList, data: seq[T]) =
@@ -868,6 +873,7 @@
   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
+  elif T is Texture: value.texture.add data
   else: {. error: "Virtual datatype has no values" .}
 
 func appendValues*(value: var DataList, data: DataList) =
@@ -1015,6 +1021,7 @@
   elif T is TMat43[float64]: value.mat43f64 = data
   elif T is TMat4[float32]: value.mat4f32 = data
   elif T is TMat4[float64]: value.mat4f64 = data
+  elif T is Texture: value.texture = data
   else: {. error: "Virtual datatype has no values" .}
 
 func setValue*[T: GPUType|int|uint|float](value: var DataList, i: int, data: T) =
@@ -1067,6 +1074,7 @@
   elif T is TMat43[float64]: value.mat43f64[i] = data
   elif T is TMat4[float32]: value.mat4f32[i] = data
   elif T is TMat4[float64]: value.mat4f64[i] = data
+  elif T is Texture: value.texture[i] = data
   else: {. error: "Virtual datatype has no values" .}
 
 const TYPEMAP = {
--- a/src/semicongine/core/imagetypes.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/src/semicongine/core/imagetypes.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -46,10 +46,17 @@
       for x in 0 ..< width:
         result[x, y] = fill
 
-let EMPTYTEXTURE* = Texture(image: newImage(1, 1, @[[255'u8, 0'u8, 255'u8, 255'u8]]), sampler: Sampler(
+let INVALID_TEXTURE* = Texture(image: newImage(1, 1, @[[255'u8, 0'u8, 255'u8, 255'u8]]), sampler: Sampler(
     magnification: VK_FILTER_NEAREST,
     minification: VK_FILTER_NEAREST,
     wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
     wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
   )
 )
+let EMPTY_TEXTURE* = Texture(image: newImage(1, 1, @[[255'u8, 255'u8, 255'u8, 255'u8]]), sampler: Sampler(
+    magnification: VK_FILTER_NEAREST,
+    minification: VK_FILTER_NEAREST,
+    wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
+    wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
+  )
+)
--- a/src/semicongine/mesh.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/src/semicongine/mesh.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -35,6 +35,7 @@
     name*: string
     constants*: Table[string, DataList]
     textures*: Table[string, Texture]
+    index*: uint16 # optional, may be used to index into uniform arrays in shader
 
 let EMPTY_MATERIAL = Material(
   name: "empty material"
@@ -300,7 +301,7 @@
 proc clearDirtyAttributes*(mesh: var MeshObject) =
   mesh.dirtyAttributes.reset
 
-proc transform*[T: GPUType](mesh: MeshObject, attribute: string, transform: Mat4) =
+proc transform*[T: GPUType](mesh: var MeshObject, attribute: string, transform: Mat4) =
   if mesh.vertexData.contains(attribute):
     for i in 0 ..< mesh.vertexData[attribute].len:
       setValue(mesh.vertexData[attribute], i, transform * getValue[T](mesh.vertexData[attribute], i))
@@ -310,6 +311,12 @@
   else:
     raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
 
+func getCollisionPoints*(mesh: MeshObject, positionAttribute="position"): seq[Vec3f] =
+  for p in getAttribute[Vec3f](mesh, positionAttribute):
+    result.add mesh.transform * p
+
+# GENERATORS ============================================================================
+
 proc rect*(width=1'f32, height=1'f32, color="ffffffff"): Mesh =
   result = Mesh(
     vertexCount: 4,
@@ -358,6 +365,25 @@
   result[].initVertexAttribute("position", pos)
   result[].initVertexAttribute("color", col)
 
-func getCollisionPoints*(mesh: MeshObject, positionAttribute="position"): seq[Vec3f] =
-  for p in getAttribute[Vec3f](mesh, positionAttribute):
-    result.add mesh.transform * p
+# MESH TREES =============================================================================
+
+type
+  MeshTree* = ref object
+    mesh*: Mesh
+    transform*: Mat4 = Unit4F32
+    children*: seq[MeshTree]
+
+proc toSeq*(tree: MeshTree): seq[Mesh] =
+  var queue = @[tree]
+  while queue.len > 0:
+    var current = queue.pop
+    if not current.mesh.isNil:
+      result.add current.mesh
+    queue.add current.children
+
+proc updateTransforms*(tree: MeshTree, parentTransform=Unit4F32) =
+  let currentTransform = parentTransform * tree.transform
+  if not tree.mesh.isNil:
+    tree.mesh.transform = currentTransform
+  for child in tree.children:
+    child.updateTransforms(currentTransform)
--- a/src/semicongine/renderer.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/src/semicongine/renderer.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -91,13 +91,17 @@
       if not foundMatch:
         return (true, &"shader uniform '{uniform.name}' was not found in scene globals or scene materials")
   for sampler in pipeline.samplers:
-    var foundMatch = true
-    for name, value in material.textures:
-      if name == sampler.name:
-        foundMatch = true
-        break
-    if not foundMatch:
-      return (true, &"Required texture for shader sampler '{sampler.name}' was not found in scene materials")
+    if scene.shaderGlobals.contains(sampler.name):
+      if scene.shaderGlobals[sampler.name].theType != sampler.theType:
+        return (true, &"shader sampler '{sampler.name}' needs type {sampler.theType} but scene global is of type {scene.shaderGlobals[sampler.name].theType}")
+    else:
+      var foundMatch = true
+      for name, value in material.textures:
+        if name == sampler.name:
+          foundMatch = true
+          break
+      if not foundMatch:
+        return (true, &"Required texture for shader sampler '{sampler.name}' was not found in scene materials")
 
   return (false, "")
 
@@ -109,8 +113,10 @@
       return (true, &"Shader input '{input.name}' is not available for mesh '{mesh}'")
     if input.theType != mesh[].attributeType(input.name):
       return (true, &"Shader input '{input.name}' expects type {input.theType}, but mesh '{mesh}' has {mesh[].attributeType(input.name)}")
-    if input.perInstance != mesh[].instanceAttributes.contains(input.name):
-      return (true, &"Shader input '{input.name}' expects to be per instance, but mesh '{mesh}' has is not as instance attribute")
+    if not input.perInstance and not mesh[].vertexAttributes.contains(input.name):
+      return (true, &"Shader input '{input.name}' expected to be vertex attribute, but mesh has no such vertex attribute (available are: {mesh[].vertexAttributes})")
+    if input.perInstance and not mesh[].instanceAttributes.contains(input.name):
+      return (true, &"Shader input '{input.name}' expected to be per instance attribute, but mesh has no such instance attribute (available are: {mesh[].instanceAttributes})")
 
   return materialCompatibleWithPipeline(scene, mesh.material, pipeline)
 
@@ -150,9 +156,20 @@
     if not scenedata.materials.contains(mesh.material):
       scenedata.materials.add mesh.material
       for textureName, texture in mesh.material.textures.pairs:
-        if not scenedata.textures.hasKey(textureName):
-          scenedata.textures[textureName] = @[]
-        scenedata.textures[textureName].add renderer.device.uploadTexture(texture)
+        if scene.shaderGlobals.contains(textureName) and scene.shaderGlobals[textureName].theType == Sampler2D:
+          warn &"Ignoring material texture '{textureName}' as scene-global textures with the same name have been defined"
+        else:
+          if not scenedata.textures.hasKey(textureName):
+            scenedata.textures[textureName] = @[]
+          scenedata.textures[textureName].add renderer.device.uploadTexture(texture)
+
+  for name, value in scene.shaderGlobals.pairs:
+    if value.theType == Sampler2D:
+      assert not scenedata.textures.contains(name) # should be handled by the above code
+      scenedata.textures[name] = @[]
+      for texture in getValues[Texture](value):
+        scenedata.textures[name].add renderer.device.uploadTexture(texture)
+
 
   # find all meshes, populate missing attribute values for shader
   for mesh in scene.meshes.mitems:
--- a/src/semicongine/resources/mesh.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/src/semicongine/resources/mesh.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -19,9 +19,6 @@
   glTFData = object
     structuredContent: JsonNode
     binaryBufferData: seq[uint8]
-  MeshTree* = ref object
-    mesh*: MeshObject
-    children*: seq[MeshTree]
 
 const
   JSON_CHUNK = 0x4E4F534A
@@ -152,8 +149,7 @@
 
 
 proc loadMaterial(root: JsonNode, materialNode: JsonNode, mainBuffer: seq[uint8], materialIndex: uint16): Material =
-  result = Material(name: materialNode["name"].getStr())
-
+  result = Material(name: materialNode["name"].getStr(), index: materialIndex)
   let pbr = materialNode["pbrMetallicRoughness"]
 
   # color
@@ -183,7 +179,7 @@
       result.constants[texture & "Index"] = newDataList(thetype=UInt8)
       setValue(result.constants[texture & "Index"], @[pbr[texture].getOrDefault("texCoord").getInt(0).uint8])
     else:
-      result.textures[texture] = EMPTYTEXTURE
+      result.textures[texture] = EMPTY_TEXTURE
       result.constants[texture & "Index"] = newDataList(thetype=UInt8)
       setValue(result.constants[texture & "Index"], @[0'u8])
 
@@ -194,7 +190,7 @@
       result.constants[texture & "Index"] = newDataList(thetype=UInt8)
       setValue(result.constants[texture & "Index"], @[materialNode[texture].getOrDefault("texCoord").getInt(0).uint8])
     else:
-      result.textures[texture] = EMPTYTEXTURE
+      result.textures[texture] = EMPTY_TEXTURE
       result.constants[texture & "Index"] = newDataList(thetype=UInt8)
       setValue(result.constants[texture & "Index"], @[0'u8])
 
@@ -209,30 +205,31 @@
   else:
     setValue(result.constants["emissiveFactor"], @[newVec3f(1'f32, 1'f32, 1'f32)])
 
-
-proc addPrimitive(mesh: var MeshObject, root: JsonNode, primitiveNode: JsonNode, mainBuffer: seq[uint8]) =
+proc addPrimitive(mesh: Mesh, root: JsonNode, primitiveNode: JsonNode, mainBuffer: seq[uint8]) =
   if primitiveNode.hasKey("mode") and primitiveNode["mode"].getInt() != 4:
     raise newException(Exception, "Currently only TRIANGLE mode is supported for geometry mode")
 
   var vertexCount = 0
   for attribute, accessor in primitiveNode["attributes"].pairs:
     let data = root.getAccessorData(root["accessors"][accessor.getInt()], mainBuffer)
-    mesh.appendAttributeData(attribute.toLowerAscii, data)
+    mesh[].appendAttributeData(attribute.toLowerAscii, data)
+    assert data.len == vertexCount or vertexCount == 0
     vertexCount = data.len
 
   var materialId = 0'u16
   if primitiveNode.hasKey("material"):
     materialId = uint16(primitiveNode["material"].getInt())
-  mesh.appendAttributeData("materialIndex", newSeqWith[uint8](vertexCount, materialId))
+  mesh[].appendAttributeData("materialIndex", newSeqWith(vertexCount, materialId))
   let material = loadMaterial(root, root["materials"][int(materialId)], mainBuffer, materialId)
-  # if mesh.material != nil and mesh.material[] != material[]:
-    # raise newException(Exception, &"Only one material per mesh supported at the moment")
-  mesh.material = material
+  # FIX: materialIndex is designed to support multiple different materials per mesh (as it is per vertex),
+  # but or current mesh/rendering implementation is only designed for a single material
+  # currently this is usually handled by adding the values as shader globals
+  mesh[].material = material
 
   if primitiveNode.hasKey("indices"):
-    assert mesh.indexType != None
+    assert mesh[].indexType != None
     let data = root.getAccessorData(root["accessors"][primitiveNode["indices"].getInt()], mainBuffer)
-    let baseIndex = mesh.indicesCount
+    let baseIndex = mesh[].indicesCount
     var tri: seq[int]
     case data.thetype
       of UInt16:
@@ -240,20 +237,20 @@
           tri.add int(entry) + baseIndex
           if tri.len == 3:
             # FYI gltf uses counter-clockwise indexing
-            mesh.appendIndicesData(tri[0], tri[2], tri[1])
+            mesh[].appendIndicesData(tri[0], tri[1], tri[2])
             tri.setLen(0)
       of UInt32:
         for entry in getValues[uint32](data):
           tri.add int(entry)
           if tri.len == 3:
             # FYI gltf uses counter-clockwise indexing
-            mesh.appendIndicesData(tri[0], tri[2], tri[1])
+            mesh[].appendIndicesData(tri[0], tri[1], tri[2])
             tri.setLen(0)
       else:
         raise newException(Exception, &"Unsupported index data type: {data.thetype}")
 
 # TODO: use one mesh per primitive?? right now we are merging primitives... check addPrimitive below
-proc loadMesh(root: JsonNode, meshNode: JsonNode, mainBuffer: seq[uint8]): MeshObject =
+proc loadMesh(root: JsonNode, meshNode: JsonNode, mainBuffer: seq[uint8]): Mesh =
 
   # check if and how we use indexes
   var indexCount = 0
@@ -267,7 +264,7 @@
     else:
       indexType = Big
 
-  result = MeshObject(instanceTransforms: @[Unit4F32], indexType: indexType)
+  result = Mesh(instanceTransforms: @[Unit4F32], indexType: indexType)
 
   # check we have the same attributes for all primitives
   let attributes = meshNode["primitives"][0]["attributes"].keys.toSeq
@@ -276,14 +273,15 @@
 
   # prepare mesh attributes
   for attribute, accessor in meshNode["primitives"][0]["attributes"].pairs:
-    result.initVertexAttribute(attribute.toLowerAscii, root["accessors"][accessor.getInt()].getGPUType())
-  result.initInstanceAttribute("materialIndex", 0'u16)
+    result[].initVertexAttribute(attribute.toLowerAscii, root["accessors"][accessor.getInt()].getGPUType())
+  result[].initVertexAttribute("materialIndex", UInt16)
 
   # add all mesh data
   for primitive in meshNode["primitives"]:
     result.addPrimitive(root, primitive, mainBuffer)
 
 proc loadNode(root: JsonNode, node: JsonNode, mainBuffer: var seq[uint8]): MeshTree =
+  result = MeshTree()
   # mesh
   if node.hasKey("mesh"):
     result.mesh = loadMesh(root, root["meshes"][node["mesh"].getInt()], mainBuffer)
@@ -293,7 +291,7 @@
     var mat: Mat4
     for i in 0 ..< node["matrix"].len:
       mat[i] = node["matrix"][i].getFloat()
-    result.mesh.transform = mat
+    result.transform = mat
   else:
     var (t, r, s) = (Unit4F32, Unit4F32, Unit4F32)
     if node.hasKey("translation"):
@@ -317,7 +315,7 @@
         float32(node["scale"][1].getFloat()),
         float32(node["scale"][2].getFloat())
       )
-    result.mesh.transform = t * r * s
+    result.transform = t * r * s
 
   # children
   if node.hasKey("children"):
@@ -328,6 +326,8 @@
   result = MeshTree()
   for nodeId in scenenode["nodes"]:
     result.children.add loadNode(root, root["nodes"][nodeId.getInt()], mainBuffer)
+  result.transform = scale(1, -1, 1)
+  result.updateTransforms()
 
 
 proc readglTF*(stream: Stream): seq[MeshTree] =
--- a/src/semicongine/text.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/src/semicongine/text.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -28,7 +28,7 @@
     ],
     intermediates=[attr[Vec2f]("uvFrag")],
     outputs=[attr[Vec4f]("color")],
-    samplers=[attr[Sampler2DType]("fontAtlas")],
+    samplers=[attr[Texture]("fontAtlas")],
     vertexCode="""gl_Position = vec4(position, 1.0) * transform; uvFrag = uv;""",
     fragmentCode="""color = texture(fontAtlas, uvFrag);""",
   )
--- a/tests/test_mesh.nim	Sun Sep 03 17:46:40 2023 +0700
+++ b/tests/test_mesh.nim	Mon Sep 04 00:31:17 2023 +0700
@@ -1,67 +1,73 @@
+import std/algorithm
 import std/sequtils
 import std/tables
 import semicongine
 
 proc main() =
-  var ent1 = newEntity("hoho", {"mesh": Component(rect())})
-  var ent2 = newEntity("hehe", [], ent1)
-  var myScene = newScene("hi", ent2)
-  myScene.root.transform = translate3d(0.2'f32, 0'f32, 0'f32)
-  myScene.root[0].transform = translate3d(0'f32, 0.2'f32, 0'f32)
+  # var myScene = Scene(name: "hi", meshes: @[rect()])
+  # myScene.meshes[0].transform = translate3d(0.2'f32, 0'f32, 0'f32)
+  # myScene.root[0].transform = translate3d(0'f32, 0.2'f32, 0'f32)
   var scenes = [
     # loadScene("default_cube.glb", "1"),
     # loadScene("default_cube1.glb", "3"),
     # loadScene("default_cube2.glb", "4"),
     # loadScene("flat.glb", "5"),
-    loadScene("tutorialk-donat.glb", "6"),
+    Scene(name: "Donut", meshes: loadMeshes("tutorialk-donat.glb")[0].toSeq),
     # myScene,
     # loadScene("personv3.glb", "2"),
   ]
 
   var engine = initEngine("Test meshes")
   const
-    vertexInput = @[
-      attr[Vec3f]("position", memoryPerformanceHint=PreferFastRead),
-      attr[uint16]("materialIndex", memoryPerformanceHint=PreferFastRead),
-      attr[Vec2f]("texcoord_0", memoryPerformanceHint=PreferFastRead),
-      attr[Mat4]("transform", memoryPerformanceHint=PreferFastWrite, perInstance=true),
-    ]
-    intermediate = @[
-      attr[Vec4f]("vertexColor"),
-      attr[Vec2f]("colorTexCoord"),
-      attr[uint16]("materialIndexOut", noInterpolation=true)
-    ]
-    fragOutput = @[attr[Vec4f]("color")]
-    uniforms = @[
-      attr[Mat4]("projection"),
-      attr[Mat4]("view"),
-      attr[Vec4f]("baseColorFactor", arrayCount=4),
-    ]
-    samplers = @[attr[Sampler2DType]("baseColorTexture", arrayCount=4)]
-    (vertexCode, fragmentCode) = compileVertexFragmentShaderSet(
-      inputs=vertexInput,
-      intermediate=intermediate,
-      outputs=fragOutput,
-      uniforms=uniforms,
-      samplers=samplers,
+    shaderConfiguration = createShaderConfiguration(
+      inputs=[
+        attr[Vec3f]("position", memoryPerformanceHint=PreferFastRead),
+        attr[uint16]("materialIndex", memoryPerformanceHint=PreferFastRead),
+        attr[Vec2f]("texcoord_0", memoryPerformanceHint=PreferFastRead),
+        attr[Mat4]("transform", memoryPerformanceHint=PreferFastWrite, perInstance=true),
+      ],
+      intermediates=[
+        attr[Vec4f]("vertexColor"),
+        attr[Vec2f]("colorTexCoord"),
+        attr[uint16]("materialIndexOut", noInterpolation=true)
+      ],
+      outputs=[attr[Vec4f]("color")],
+      uniforms=[
+        attr[Mat4]("projection"),
+        attr[Mat4]("view"),
+        attr[Vec4f]("baseColorFactor", arrayCount=4),
+      ],
+      samplers=[attr[Texture]("baseColorTexture", arrayCount=4)],
       vertexCode="""
-gl_Position =  vec4(position, 1.0) * (transform * Uniforms.view * Uniforms.projection);
-vertexColor = Uniforms.baseColorFactor[materialIndex];
-colorTexCoord = texcoord_0;
-materialIndexOut = materialIndex;
-""",
-      fragmentCode="""
-// vec4 col[4] = vec4[4](vec4(1, 0, 0, 1), vec4(0, 1, 0, 1), vec4(0, 0, 1, 1), vec4(1, 1, 1, 1));
-color = texture(baseColorTexture[materialIndexOut], colorTexCoord) * vertexColor;
-"""
+  gl_Position =  vec4(position, 1.0) * (transform * Uniforms.view * Uniforms.projection);
+  vertexColor = Uniforms.baseColorFactor[materialIndex];
+  colorTexCoord = texcoord_0;
+  materialIndexOut = materialIndex;
+  """,
+      fragmentCode="color = texture(baseColorTexture[materialIndexOut], colorTexCoord) * vertexColor;"
     )
-  engine.setRenderer(engine.gpuDevice.simpleForwardRenderPass(vertexCode, fragmentCode, clearColor=newVec4f(0, 0, 0, 1)))
+  engine.initRenderer({
+    "Material": shaderConfiguration,
+    "Material.001": shaderConfiguration,
+    "Material.002": shaderConfiguration,
+    "Material.004": shaderConfiguration,
+  }.toTable)
+
   for scene in scenes.mitems:
-    engine.addScene(scene, vertexInput, samplers)
-    scene.addShaderGlobal("projection", Unit4)
-    scene.addShaderGlobal("view", Unit4)
-    let baseColors = scene.materials.map(proc(i: Material): Vec4f = getValue[Vec4f](i[].constants["baseColorFactor"]))
+    scene.addShaderGlobal("projection", Unit4F32)
+    scene.addShaderGlobal("view", Unit4F32)
+    var materials: Table[uint16, Material]
+    for mesh in scene.meshes:
+      if not materials.contains(mesh.material.index):
+        materials[mesh.material.index] = mesh.material
+    let baseColors = sortedByIt(values(materials).toSeq, it.index).mapIt(getValue[Vec4f](it.constants["baseColorFactor"], 0))
+    let baseTextures = sortedByIt(values(materials).toSeq, it.index).mapIt(it.textures["baseColorTexture"])
+    for t in baseTextures:
+      echo "- ", t
     scene.addShaderGlobalArray("baseColorFactor", baseColors)
+    scene.addShaderGlobalArray("baseColorTexture", baseTextures)
+    engine.addScene(scene)
+
   var
     size = 1'f32
     elevation = 0'f32
@@ -94,7 +100,7 @@
     scenes[currentScene].setShaderGlobal("projection", ortho(-ratio, ratio, -1, 1, -1, 1))
     scenes[currentScene].setShaderGlobal(
       "view",
-       scale3d(size, size, size) * rotate3d(elevation, newVec3f(1, 0, 0)) * rotate3d(azimut, Yf32)
+       scale(size, size, size) * rotate(elevation, newVec3f(1, 0, 0)) * rotate(azimut, Yf32)
     )
     engine.renderScene(scenes[currentScene])
   engine.destroy()