changeset 784:fa39e67dded7

add: first complete working version of multiple materials and shaders per scene, yipie :)
author Sam <sam@basx.dev>
date Sat, 19 Aug 2023 22:24:06 +0700
parents 893ec0fbfd44
children 1880ab140165
files src/semicongine/core/gpu_data.nim src/semicongine/core/imagetypes.nim src/semicongine/engine.nim src/semicongine/mesh.nim src/semicongine/renderer.nim src/semicongine/resources/mesh.nim src/semicongine/text.nim src/semicongine/vulkan/memory.nim src/semicongine/vulkan/pipeline.nim tests/test_vulkan_wrapper.nim
diffstat 10 files changed, 285 insertions(+), 172 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/core/gpu_data.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/core/gpu_data.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -408,6 +408,10 @@
   elif T is TMat4[float64]: value.mat4f64[] = data
   else: {. error: "Virtual datatype has no values" .}
 
+func toGPUValue*[T: GPUType](value: T): DataValue =
+  result = DataValue(thetype: getDataType[T]())
+  result.setValue(value)
+
 func newDataList*(thetype: DataType): DataList =
   result = DataList(thetype: thetype)
   case result.thetype
--- a/src/semicongine/core/imagetypes.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/core/imagetypes.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -16,15 +16,12 @@
   Texture* = object
     name*: string
     image*: Image
-    sampler*: Sampler
-
-proc DefaultSampler*(): Sampler =
-  Sampler(
-    magnification: VK_FILTER_LINEAR,
-    minification: VK_FILTER_LINEAR,
-    wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
-    wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
-  )
+    sampler*: Sampler = Sampler(
+      magnification: VK_FILTER_LINEAR,
+      minification: VK_FILTER_LINEAR,
+      wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
+      wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
+    )
 
 proc `[]`*(image: Image, x, y: uint32): Pixel =
   assert x < image.width
@@ -54,3 +51,10 @@
       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(
+    magnification: VK_FILTER_NEAREST,
+    minification: VK_FILTER_NEAREST,
+    wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
+    wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
+  )
+)
--- a/src/semicongine/engine.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/engine.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -12,7 +12,6 @@
 import ./vulkan/shader
 
 import ./scene
-import ./mesh
 import ./renderer
 import ./events
 import ./audio
@@ -81,7 +80,8 @@
   if debug:
     instanceExtensions.add "VK_EXT_debug_utils"
     enabledLayers.add "VK_LAYER_KHRONOS_validation"
-    putEnv("VK_LAYER_ENABLES", "VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT")
+    # putEnv("VK_LAYER_ENABLES", "VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT")
+    putEnv("VK_LAYER_ENABLES", "")
 
   if defined(linux) and DEBUG:
     enabledLayers.add "VK_LAYER_MESA_overlay"
@@ -102,7 +102,7 @@
   )
   startMixerThread()
 
-proc setRenderer*(engine: var Engine, shaders: Table[string, ShaderConfiguration], clearColor=Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32])) =
+proc initRenderer*(engine: var Engine, shaders: Table[string, ShaderConfiguration], clearColor=Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32])) =
 
   assert not engine.renderer.isSome
   engine.renderer = some(engine.device.initRenderer(shaders=shaders, clearColor=clearColor))
--- a/src/semicongine/mesh.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/mesh.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -18,26 +18,28 @@
     Small # up to 2^16 vertices
     Big # up to 2^32 vertices
   Mesh* = ref object of Component
+    vertexCount*: uint32
     instanceCount*: uint32
-    instanceTransforms*: seq[Mat4] # this should not reside in data["transform"], as we will use data["transform"] to store the final transformation matrix (as derived from the scene-tree)
+    instanceTransforms*: seq[Mat4] # this should not reside in instanceData["transform"], as we will use instanceData["transform"] to store the final transformation matrix (as derived from the scene-tree)
     material*: Material
-    dirtyInstanceTransforms: bool
-    data: Table[string, DataList]
-    changedAttributes: seq[string]
     case indexType*: MeshIndexType
       of None: discard
       of Tiny: tinyIndices: seq[array[3, uint8]]
       of Small: smallIndices: seq[array[3, uint16]]
       of Big: bigIndices: seq[array[3, uint32]]
+    visible: bool = true
+    dirtyInstanceTransforms: bool
+    vertexData: Table[string, DataList]
+    instanceData: Table[string, DataList]
+    dirtyAttributes: seq[string]
   Material* = ref object
     materialType*: string
     name*: string
-    index*: uint16
     constants*: Table[string, DataValue]
     textures*: Table[string, Texture]
 
 proc hash*(material: Material): Hash =
-  hash(material.name)
+  hash(cast[int64](material))
 
 converter toVulkan*(indexType: MeshIndexType): VkIndexType =
   case indexType:
@@ -46,11 +48,6 @@
     of Small: VK_INDEX_TYPE_UINT16
     of Big: VK_INDEX_TYPE_UINT32
 
-func vertexCount*(mesh: Mesh): uint32 =
-  result = 0'u32
-  for list in mesh.data.values:
-    result = max(list.len, result)
-
 func indicesCount*(mesh: Mesh): uint32 =
   (
     case mesh.indexType
@@ -61,7 +58,7 @@
   ) * 3
 
 method `$`*(mesh: Mesh): string =
-  &"Mesh, vertexCount: {mesh.vertexCount}, vertexData: {mesh.data.keys().toSeq()}, indexType: {mesh.indexType}"
+  &"Mesh, vertexCount: {mesh.vertexCount}, vertexData: {mesh.vertexData.keys().toSeq()}, indexType: {mesh.indexType}"
 
 proc `$`*(material: Material): string =
   var constants: seq[string]
@@ -70,10 +67,10 @@
   var textures: seq[string]
   for key in material.textures.keys:
     textures.add &"{key}"
-  return &"""{material.name} ({material.index}) | Values: {constants.join(", ")} | Textures: {textures.join(", ")}"""
+  return &"""{material.name} | Values: {constants.join(", ")} | Textures: {textures.join(", ")}"""
 
 func prettyData*(mesh: Mesh): string =
-  for attr, data in mesh.data.pairs:
+  for attr, data in mesh.vertexData.pairs:
     result &= &"{attr}: {data}\n"
   result &= (case mesh.indexType
     of None: ""
@@ -82,17 +79,17 @@
     of Big: &"indices: {mesh.bigIndices}")
 
 proc setMeshData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, data: seq[T]) =
-  assert not (attribute in mesh.data)
-  mesh.data[attribute] = newDataList(data)
+  assert not (attribute in mesh.vertexData)
+  mesh.vertexData[attribute] = newDataList(data)
 
 proc setMeshData*(mesh: Mesh, attribute: string, data: DataList) =
-  assert not (attribute in mesh.data)
-  mesh.data[attribute] = data
+  assert not (attribute in mesh.vertexData)
+  mesh.vertexData[attribute] = data
 
 proc setInstanceData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, data: seq[T]) =
   assert uint32(data.len) == mesh.instanceCount
-  assert not (attribute in mesh.data)
-  mesh.data[attribute] = newDataList(data)
+  assert not (attribute in mesh.instanceData)
+  mesh.instanceData[attribute] = newDataList(data)
 
 func newMesh*(
   positions: openArray[Vec3f],
@@ -119,6 +116,7 @@
     instanceCount: instanceCount,
     instanceTransforms: newSeqWith(int(instanceCount), Unit4F32),
     indexType: indexType,
+    vertexCount: uint32(positions.len)
   )
   result.material = material
 
@@ -160,16 +158,29 @@
     instanceCount=instanceCount,
   )
 
-func availableAttributes*(mesh: Mesh): seq[string] =
-  mesh.data.keys.toSeq
+func vertexAttributes*(mesh: Mesh): seq[string] =
+  mesh.vertexData.keys.toSeq
+
+func instanceAttributes*(mesh: Mesh): seq[string] =
+  mesh.instanceData.keys.toSeq
 
-func dataSize*(mesh: Mesh, attribute: string): uint32 =
-  mesh.data[attribute].size
+func attributeSize*(mesh: Mesh, attribute: string): uint32 =
+  if mesh.vertexData.contains(attribute):
+    mesh.vertexData[attribute].size
+  elif mesh.instanceData.contains(attribute):
+    mesh.instanceData[attribute].size
+  else:
+    0
 
-func dataType*(mesh: Mesh, attribute: string): DataType =
-  mesh.data[attribute].theType
+func attributeType*(mesh: Mesh, attribute: string): DataType =
+  if mesh.vertexData.contains(attribute):
+    mesh.vertexData[attribute].theType
+  elif mesh.instanceData.contains(attribute):
+    mesh.instanceData[attribute].theType
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
 
-func indexDataSize*(mesh: Mesh): uint32 =
+func indexSize*(mesh: Mesh): uint32 =
   case mesh.indexType
     of None: 0'u32
     of Tiny: uint32(mesh.tinyIndices.len * sizeof(get(genericParams(typeof(mesh.tinyIndices)), 0)))
@@ -186,57 +197,91 @@
     of Small: rawData(mesh.smallIndices)
     of Big: rawData(mesh.bigIndices)
 
-func hasDataFor*(mesh: Mesh, attribute: string): bool =
-  attribute in mesh.data
+func hasAttribute*(mesh: Mesh, attribute: string): bool =
+  mesh.vertexData.contains(attribute) or mesh.instanceData.contains(attribute)
 
 func getRawData*(mesh: Mesh, attribute: string): (pointer, uint32) =
-  mesh.data[attribute].getRawData()
+  if mesh.vertexData.contains(attribute):
+    mesh.vertexData[attribute].getRawData()
+  elif mesh.instanceData.contains(attribute):
+    mesh.instanceData[attribute].getRawData()
+  else:
+    (nil, 0)
 
 proc getMeshData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string): ref seq[T] =
-  assert attribute in mesh.data
-  getValues[T](mesh.data[attribute])
+  if mesh.vertexData.contains(attribute):
+    getValues[T](mesh.vertexData[attribute])
+  elif mesh.instanceData.contains(attribute):
+    getValues[T](mesh.instanceData[attribute])
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
 
-proc initData*(mesh: Mesh, attribute: ShaderAttribute) =
-  assert not (attribute.name in mesh.data)
-  mesh.data[attribute.name] = newDataList(thetype=attribute.thetype)
+proc initAttribute*(mesh: Mesh, attribute: ShaderAttribute) =
   if attribute.perInstance:
-    mesh.data[attribute.name].initData(mesh.instanceCount)
+    mesh.instanceData[attribute.name] = newDataList(thetype=attribute.thetype)
+    mesh.instanceData[attribute.name].initData(mesh.instanceCount)
   else:
-    mesh.data[attribute.name].initData(mesh.vertexCount)
-
-proc updateMeshData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, data: seq[T]) =
-  assert attribute in mesh.data
-  mesh.changedAttributes.add attribute
-  setValues(mesh.data[attribute], data)
+    mesh.vertexData[attribute.name] = newDataList(thetype=attribute.thetype)
+    mesh.vertexData[attribute.name].initData(mesh.vertexCount)
 
-proc updateMeshData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, i: uint32, value: T) =
-  assert attribute in mesh.data
-  mesh.changedAttributes.add attribute
-  setValue(mesh.data[attribute], i, value)
+proc initAttribute*[T](mesh: Mesh, attribute: ShaderAttribute, value: T) =
+  if attribute.perInstance:
+    mesh.instanceData[attribute.name] = newDataList(thetype=attribute.thetype)
+    mesh.instanceData[attribute.name].initData(mesh.instanceCount)
+    mesh.instanceData[attribute.name].setValues(newSeqWith(int(mesh.instanceCount), value))
+  else:
+    mesh.vertexData[attribute.name] = newDataList(thetype=attribute.thetype)
+    mesh.vertexData[attribute.name].initData(mesh.vertexCount)
+    mesh.instanceData[attribute.name].setValues(newSeqWith(int(mesh.vertexCount), value))
 
-proc appendMeshData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, data: seq[T]) =
-  assert attribute in mesh.data
-  mesh.changedAttributes.add attribute
-  appendValues(mesh.data[attribute], data)
+proc updateAttributeData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, data: seq[T]) =
+  if mesh.vertexData.contains(attribute):
+    setValues(mesh.vertexData[attribute], data)
+  elif mesh.instanceData.contains(attribute):
+    setValues(mesh.instanceData[attribute], data)
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
+  mesh.dirtyAttributes.add attribute
 
-# currently only used for loading from files, shouls
-proc appendMeshData*(mesh: 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 updateAttributeData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, i: uint32, value: T) =
+  if mesh.vertexData.contains(attribute):
+    setValue(mesh.vertexData[attribute], i, value)
+  elif mesh.instanceData.contains(attribute):
+    setValue(mesh.instanceData[attribute], i, value)
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
+  mesh.dirtyAttributes.add attribute
 
 proc updateInstanceData*[T: GPUType|int|uint|float](mesh: 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)
+  if mesh.vertexData.contains(attribute):
+    setValues(mesh.vertexData[attribute], data)
+  elif mesh.instanceData.contains(attribute):
+    setValues(mesh.instanceData[attribute], data)
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
+  mesh.dirtyAttributes.add attribute
 
-proc appendInstanceData*[T: GPUType|int|uint|float](mesh: 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 appendAttributeData*[T: GPUType|int|uint|float](mesh: Mesh, attribute: string, data: seq[T]) =
+  if mesh.vertexData.contains(attribute):
+    appendValues(mesh.vertexData[attribute], data)
+  elif mesh.instanceData.contains(attribute):
+    appendValues(mesh.instanceData[attribute], data)
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
+  mesh.dirtyAttributes.add attribute
+
+# currently only used for loading from files, shouls
+proc appendAttributeData*(mesh: Mesh, attribute: string, data: DataList) =
+  if mesh.vertexData.contains(attribute):
+    assert data.thetype == mesh.vertexData[attribute].thetype
+    appendValues(mesh.vertexData[attribute], data)
+  elif mesh.instanceData.contains(attribute):
+    assert data.thetype == mesh.instanceData[attribute].thetype
+    appendValues(mesh.instanceData[attribute], data)
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
+  mesh.dirtyAttributes.add attribute
 
 proc appendIndicesData*(mesh: Mesh, v1, v2, v3: uint32) =
   case mesh.indexType
@@ -246,18 +291,24 @@
   of Big: mesh.bigIndices.add([v1, v2, v3])
 
 func hasDataChanged*(mesh: Mesh, attribute: string): bool =
-  attribute in mesh.changedAttributes
+  attribute in mesh.dirtyAttributes
 
 proc clearDataChanged*(mesh: Mesh) =
-  mesh.changedAttributes = @[]
+  mesh.dirtyAttributes = @[]
 
 proc transform*[T: GPUType](mesh: Mesh, attribute: string, transform: Mat4) =
-  assert attribute in mesh.data
-  for v in getValues[T](mesh.data[attribute])[].mitems:
-    v = transform * v
+  if mesh.vertexData.contains(attribute):
+    for v in getValues[T](mesh.vertexData[attribute])[].mitems:
+      v = transform * v
+  elif mesh.instanceData.contains(attribute):
+    for v in getValues[T](mesh.instanceData[attribute])[].mitems:
+      v = transform * v
+  else:
+    raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
 
 func rect*(width=1'f32, height=1'f32, color="ffffffff"): Mesh =
   result = Mesh(
+    vertexCount: 4,
     instanceCount: 1,
     indexType: Small,
     smallIndices: @[[0'u16, 1'u16, 2'u16], [2'u16, 3'u16, 0'u16]],
@@ -276,7 +327,7 @@
   setInstanceData(result, "transform", @[Unit4F32])
 
 func tri*(width=1'f32, height=1'f32, color="ffffffff"): Mesh =
-  result = Mesh(instanceCount: 1, instanceTransforms: @[Unit4F32])
+  result = Mesh(vertexCount: 3, instanceCount: 1, instanceTransforms: @[Unit4F32])
   let
     half_w = width / 2
     half_h = height / 2
@@ -287,7 +338,7 @@
 
 func circle*(width=1'f32, height=1'f32, nSegments=12'u16, color="ffffffff"): Mesh =
   assert nSegments >= 3
-  result = Mesh(instanceCount: 1, indexType: Small, instanceTransforms: @[Unit4F32])
+  result = Mesh(vertexCount: 3 + nSegments, instanceCount: 1, indexType: Small, instanceTransforms: @[Unit4F32])
 
   let
     half_w = width / 2
--- a/src/semicongine/renderer.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/renderer.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -1,8 +1,8 @@
 import std/options
-import std/enumerate
 import std/tables
 import std/strformat
 import std/logging
+import std/sequtils
 
 import ./core
 import ./vulkan/buffer
@@ -32,7 +32,7 @@
     materialIndexAttribute: string # name of attribute that is used for material selection
     materials: seq[Material]
     entityTransformationCache: Table[Mesh, Mat4] # remembers last transformation, avoid to send GPU-updates if no changes
-    descriptorPool*: DescriptorPool
+    descriptorPools*: Table[VkPipeline, DescriptorPool]
     descriptorSets*: Table[VkPipeline, seq[DescriptorSet]]
   Renderer* = object
     device: Device
@@ -40,7 +40,13 @@
     renderPass: RenderPass
     swapchain: Swapchain
     scenedata: Table[Scene, SceneData]
+    emptyTexture: VulkanTexture
 
+func usesMaterialType(scenedata: SceneData, materialType: string): bool =
+  for drawable in scenedata.drawables.values:
+    if drawable.mesh.material.materialType == materialType:
+      return true
+  return false
 
 proc initRenderer*(device: Device, shaders: Table[string, ShaderConfiguration], clearColor=Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32])): Renderer =
   assert device.vk.valid
@@ -54,6 +60,7 @@
     raise newException(Exception, "Unable to create swapchain")
 
   result.swapchain = swapchain.get()
+  result.emptyTexture = device.uploadTexture(EMPTYTEXTURE)
 
 func inputs(renderer: Renderer): seq[ShaderAttribute] =
   for i in 0 ..< renderer.renderPass.subpasses.len:
@@ -69,9 +76,9 @@
   assert not (scene in renderer.scenedata)
   const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment
 
-  # TODO: find all inputs and samplers from scene materials
-  let inputs = renderer.inputs
-  var samplers = renderer.samplers
+  let
+    inputs = renderer.inputs
+    samplers = renderer.samplers
   var scenedata = SceneData()
 
   # if mesh transformation are handled through the scenegraph-transformation, set it up here
@@ -98,22 +105,26 @@
   for mesh in allComponentsOfType[Mesh](scene.root):
     if mesh.material != nil and 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)
 
   # find all meshes, populate missing attribute values for shader
   var allMeshes: seq[Mesh]
   for mesh in allComponentsOfType[Mesh](scene.root):
     allMeshes.add mesh
     for inputAttr in inputs:
-      if not mesh.hasDataFor(inputAttr.name):
-        warn(&"Mesh is missing data for shader attribute {inputAttr.name}, auto-filling with empty values")
-        mesh.initData(inputAttr)
-      assert mesh.dataType(inputAttr.name) == inputAttr.thetype, &"mesh attribute {inputAttr.name} has type {mesh.dataType(inputAttr.name)} but shader expects {inputAttr.thetype}"
       if scenedata.materialIndexAttribute != "" and inputAttr.name == scenedata.materialIndexAttribute:
         assert mesh.material != nil, "Missing material specification for mesh. Either set the 'materials' attribute or pass the argument 'materialIndexAttribute=\"\"' when calling 'addScene'"
         let matIndex = scenedata.materials.find(mesh.material)
         if matIndex < 0:
           raise newException(Exception, &"Required material '{mesh.material}' not available in scene (available are: {scenedata.materials})")
-        updateMeshData[uint16](mesh, scenedata.materialIndexAttribute, 0, uint16(matIndex))
+        mesh.initAttribute(inputAttr, uint16(matIndex))
+      elif not mesh.hasAttribute(inputAttr.name):
+        warn(&"Mesh is missing data for shader attribute {inputAttr.name}, auto-filling with empty values")
+        mesh.initAttribute(inputAttr)
+      assert mesh.attributeType(inputAttr.name) == inputAttr.thetype, &"mesh attribute {inputAttr.name} has type {mesh.attributeType(inputAttr.name)} but shader expects {inputAttr.thetype}"
   
   # create index buffer if necessary
   var indicesBufferSize = 0'u64
@@ -127,7 +138,7 @@
       # index value alignment required by Vulkan
       if indicesBufferSize mod indexAlignment != 0:
         indicesBufferSize += indexAlignment - (indicesBufferSize mod indexAlignment)
-      indicesBufferSize += mesh.indexDataSize
+      indicesBufferSize += mesh.indexSize
   if indicesBufferSize > 0:
     scenedata.indexBuffer = renderer.device.createBuffer(
       size=indicesBufferSize,
@@ -155,7 +166,7 @@
       # we need to expand the buffer size as well, therefore considering alignment already here as well
       if perLocationSizes[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT != 0:
         perLocationSizes[attribute.memoryPerformanceHint] += VERTEX_ATTRIB_ALIGNMENT - (perLocationSizes[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT)
-      perLocationSizes[attribute.memoryPerformanceHint] += mesh.dataSize(attribute.name)
+      perLocationSizes[attribute.memoryPerformanceHint] += mesh.attributeSize(attribute.name)
   for memoryPerformanceHint, bufferSize in perLocationSizes.pairs:
     if bufferSize > 0:
       scenedata.vertexBuffers[memoryPerformanceHint] = renderer.device.createBuffer(
@@ -202,13 +213,6 @@
       indexBufferOffset += size
     scenedata.drawables[mesh] = drawable
 
-  # upload textures
-  for material in scenedata.materials:
-    for textureName, texture in material.textures.pairs:
-      if not scenedata.textures.hasKey(textureName):
-        scenedata.textures[textureName] = @[]
-      scenedata.textures[textureName].add renderer.device.uploadTexture(texture)
-
   # setup uniforms and samplers
   for subpass_i in 0 ..< renderer.renderPass.subpasses.len:
     for material, pipeline in renderer.renderPass.subpasses[subpass_i].pipelines.pairs:
@@ -232,13 +236,14 @@
           samplercount += (if sampler.arrayCount == 0: 1'u32 else: sampler.arrayCount)
         poolsizes.add (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, uint32(renderer.swapchain.inFlightFrames) * samplercount * 2)
     
-      scenedata.descriptorPool = renderer.device.createDescriptorSetPool(poolsizes)
+      scenedata.descriptorPools[pipeline.vk] = renderer.device.createDescriptorSetPool(poolsizes)
   
       scenedata.descriptorSets[pipeline.vk] = pipeline.setupDescriptors(
-        scenedata.descriptorPool,
+        scenedata.descriptorPools[pipeline.vk],
         scenedata.uniformBuffers.getOrDefault(pipeline.vk, @[]),
         scenedata.textures,
-        inFlightFrames=renderer.swapchain.inFlightFrames
+        inFlightFrames=renderer.swapchain.inFlightFrames,
+        emptyTexture=renderer.emptyTexture,
       )
       for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
         scenedata.descriptorSets[pipeline.vk][frame_i].writeDescriptorSet()
@@ -253,7 +258,6 @@
   var (pdata, size) = mesh.getRawData(attribute)
   let memoryPerformanceHint = sceneData.attributeLocation[attribute]
   let bindingNumber = sceneData.attributeBindingNumber[attribute]
-
   sceneData.vertexBuffers[memoryPerformanceHint].setData(pdata, size, sceneData.drawables[mesh].bufferOffsets[bindingNumber][2])
 
 proc updateMeshData*(renderer: var Renderer, scene: Scene) =
@@ -267,26 +271,33 @@
         var updatedTransform = newSeq[Mat4](int(mesh.instanceCount))
         for i in 0 ..< mesh.instanceCount:
           updatedTransform[i] = transform * mesh.getInstanceTransform(i)
+        debug &"Update mesh transformation"
         mesh.updateInstanceData(renderer.scenedata[scene].transformAttribute, updatedTransform)
         renderer.scenedata[scene].entityTransformationCache[mesh] = transform
 
     # update any changed mesh attributes
-    for attribute in mesh.availableAttributes():
+    for attribute in mesh.vertexAttributes:
       if mesh.hasDataChanged(attribute):
         renderer.scenedata[scene].refreshMeshAttributeData(mesh, attribute)
+        debug &"Update mesh vertex attribute {attribute}"
+    for attribute in mesh.instanceAttributes:
+      if mesh.hasDataChanged(attribute):
+        renderer.scenedata[scene].refreshMeshAttributeData(mesh, attribute)
+        debug &"Update mesh instance attribute {attribute}"
     var m = mesh
     m.clearDataChanged()
 
 proc updateAnimations*(renderer: var Renderer, scene: var Scene, dt: float32) =
   for animation in allComponentsOfType[EntityAnimation](scene.root):
+    debug &"Update animation {animation}"
     animation.update(dt)
 
 proc updateUniformData*(renderer: var Renderer, scene: var Scene) =
   assert scene in renderer.scenedata
 
   for i in 0 ..< renderer.renderPass.subpasses.len:
-    for material, pipeline in renderer.renderPass.subpasses[i].pipelines.pairs:
-      if renderer.scenedata[scene].uniformBuffers.hasKey(pipeline.vk) and renderer.scenedata[scene].uniformBuffers[pipeline.vk].len != 0:
+    for materialType, pipeline in renderer.renderPass.subpasses[i].pipelines.pairs:
+      if renderer.scenedata[scene].usesMaterialType(materialType) and renderer.scenedata[scene].uniformBuffers.hasKey(pipeline.vk) and renderer.scenedata[scene].uniformBuffers[pipeline.vk].len != 0:
         assert renderer.scenedata[scene].uniformBuffers[pipeline.vk][renderer.swapchain.currentInFlight].vk.valid
         var offset = 0'u64
         for uniform in pipeline.uniforms:
@@ -294,6 +305,7 @@
             raise newException(Exception, &"Uniform '{uniform.name}' not found in scene shaderGlobals")
           if uniform.thetype != scene.shaderGlobals[uniform.name].thetype:
             raise newException(Exception, &"Uniform '{uniform.name}' has wrong type {uniform.thetype}, required is {scene.shaderGlobals[uniform.name].thetype}")
+          debug &"Update uniforms {uniform.name}"
           let (pdata, size) = scene.shaderGlobals[uniform.name].getRawData()
           renderer.scenedata[scene].uniformBuffers[pipeline.vk][renderer.swapchain.currentInFlight].setData(pdata, size, offset)
           offset += size
@@ -324,12 +336,14 @@
 
   for i in 0 ..< renderer.renderPass.subpasses.len:
     for materialType, pipeline in renderer.renderPass.subpasses[i].pipelines.pairs:
-      commandBuffer.vkCmdBindPipeline(renderer.renderPass.subpasses[i].pipelineBindPoint, pipeline.vk)
-      commandBuffer.vkCmdBindDescriptorSets(renderer.renderPass.subpasses[i].pipelineBindPoint, pipeline.layout, 0, 1, addr(renderer.scenedata[scene].descriptorSets[pipeline.vk][renderer.swapchain.currentInFlight].vk), 0, nil)
+      if renderer.scenedata[scene].usesMaterialType(materialType):
+        debug &"Start pipeline for {materialType}"
+        commandBuffer.vkCmdBindPipeline(renderer.renderPass.subpasses[i].pipelineBindPoint, pipeline.vk)
+        commandBuffer.vkCmdBindDescriptorSets(renderer.renderPass.subpasses[i].pipelineBindPoint, pipeline.layout, 0, 1, addr(renderer.scenedata[scene].descriptorSets[pipeline.vk][renderer.swapchain.currentInFlight].vk), 0, nil)
 
-      for drawable in renderer.scenedata[scene].drawables.values:
-        if drawable.mesh.material.materialType == materialType:
-          drawable.draw(commandBuffer, vertexBuffers=renderer.scenedata[scene].vertexBuffers, indexBuffer=renderer.scenedata[scene].indexBuffer)
+        for drawable in renderer.scenedata[scene].drawables.values:
+          if drawable.mesh.material != nil and drawable.mesh.material.materialType == materialType:
+            drawable.draw(commandBuffer, vertexBuffers=renderer.scenedata[scene].vertexBuffers, indexBuffer=renderer.scenedata[scene].indexBuffer)
 
     if i < renderer.renderPass.subpasses.len - 1:
       commandBuffer.vkCmdNextSubpass(VK_SUBPASS_CONTENTS_INLINE)
@@ -365,6 +379,8 @@
     for textures in scenedata.textures.mvalues:
       for texture in textures.mitems:
         texture.destroy()
-    scenedata.descriptorPool.destroy()
+    for descriptorPool in scenedata.descriptorPools.mvalues:
+      descriptorPool.destroy()
+  renderer.emptyTexture.destroy()
   renderer.renderPass.destroy()
   renderer.swapchain.destroy()
--- a/src/semicongine/resources/mesh.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/resources/mesh.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -12,14 +12,6 @@
 
 import ./image
 
-let DEFAULTSAMPLER = Sampler(
-    magnification: VK_FILTER_NEAREST,
-    minification: VK_FILTER_NEAREST,
-    wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
-    wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
-  )
-let DEFAULTEXTURE = Texture(image: newImage(1, 1, @[[255'u8, 255'u8, 255'u8, 255'u8]]), sampler: DEFAULTSAMPLER)
-
 type
   glTFHeader = object
     magic: uint32
@@ -144,7 +136,6 @@
 proc loadTexture(root: JsonNode, textureIndex: int, mainBuffer: seq[uint8]): Texture =
   let textureNode = root["textures"][textureIndex]
   result.image = loadImage(root, textureNode["source"].getInt(), mainBuffer)
-  result.sampler = DefaultSampler()
 
   if textureNode.hasKey("sampler"):
     let sampler = root["samplers"][textureNode["sampler"].getInt()]
@@ -159,7 +150,7 @@
 
 
 proc loadMaterial(root: JsonNode, materialNode: JsonNode, mainBuffer: seq[uint8], materialIndex: uint16): Material =
-  result = Material(name: materialNode["name"].getStr(), index: materialIndex)
+  result = Material(name: materialNode["name"].getStr())
 
   let pbr = materialNode["pbrMetallicRoughness"]
 
@@ -190,7 +181,7 @@
       result.constants[texture & "Index"] = DataValue(thetype: UInt8)
       setValue(result.constants[texture & "Index"], pbr[texture].getOrDefault("texCoord").getInt(0).uint8)
     else:
-      result.textures[texture] = DEFAULTEXTURE
+      result.textures[texture] = EMPTYTEXTURE
       result.constants[texture & "Index"] = DataValue(thetype: UInt8)
       setValue(result.constants[texture & "Index"], 0'u8)
 
@@ -201,7 +192,7 @@
       result.constants[texture & "Index"] = DataValue(thetype: UInt8)
       setValue(result.constants[texture & "Index"], materialNode[texture].getOrDefault("texCoord").getInt(0).uint8)
     else:
-      result.textures[texture] = DEFAULTEXTURE
+      result.textures[texture] = EMPTYTEXTURE
       result.constants[texture & "Index"] = DataValue(thetype: UInt8)
       setValue(result.constants[texture & "Index"], 0'u8)
 
@@ -224,13 +215,13 @@
   var vertexCount = 0'u32
   for attribute, accessor in primitiveNode["attributes"].pairs:
     let data = root.getAccessorData(root["accessors"][accessor.getInt()], mainBuffer)
-    mesh.appendMeshData(attribute.toLowerAscii, data)
+    mesh.appendAttributeData(attribute.toLowerAscii, data)
     vertexCount = data.len
 
   var materialId = 0'u16
   if primitiveNode.hasKey("material"):
     materialId = uint16(primitiveNode["material"].getInt())
-  mesh.appendMeshData("materialIndex", newSeqWith[uint8](int(vertexCount), materialId))
+  mesh.appendAttributeData("materialIndex", newSeqWith[uint8](int(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")
--- a/src/semicongine/text.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/text.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -44,24 +44,24 @@
         top = glyph.topOffset
         bottom = glyph.topOffset + glyph.dimension.y
 
-      textbox.mesh.updateMeshData("position", vertexOffset + 0, newVec3f(left - centerX, bottom + centerY))
-      textbox.mesh.updateMeshData("position", vertexOffset + 1, newVec3f(left - centerX, top + centerY))
-      textbox.mesh.updateMeshData("position", vertexOffset + 2, newVec3f(right - centerX, top + centerY))
-      textbox.mesh.updateMeshData("position", vertexOffset + 3, newVec3f(right - centerX, bottom + centerY))
+      textbox.mesh.updateAttributeData("position", vertexOffset + 0, newVec3f(left - centerX, bottom + centerY))
+      textbox.mesh.updateAttributeData("position", vertexOffset + 1, newVec3f(left - centerX, top + centerY))
+      textbox.mesh.updateAttributeData("position", vertexOffset + 2, newVec3f(right - centerX, top + centerY))
+      textbox.mesh.updateAttributeData("position", vertexOffset + 3, newVec3f(right - centerX, bottom + centerY))
 
-      textbox.mesh.updateMeshData("uv", vertexOffset + 0, glyph.uvs[0])
-      textbox.mesh.updateMeshData("uv", vertexOffset + 1, glyph.uvs[1])
-      textbox.mesh.updateMeshData("uv", vertexOffset + 2, glyph.uvs[2])
-      textbox.mesh.updateMeshData("uv", vertexOffset + 3, glyph.uvs[3])
+      textbox.mesh.updateAttributeData("uv", vertexOffset + 0, glyph.uvs[0])
+      textbox.mesh.updateAttributeData("uv", vertexOffset + 1, glyph.uvs[1])
+      textbox.mesh.updateAttributeData("uv", vertexOffset + 2, glyph.uvs[2])
+      textbox.mesh.updateAttributeData("uv", vertexOffset + 3, glyph.uvs[3])
 
       offsetX += glyph.advance
       if i < uint32(textbox.text.len - 1):
         offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
     else:
-      textbox.mesh.updateMeshData("position", vertexOffset + 0, newVec3f())
-      textbox.mesh.updateMeshData("position", vertexOffset + 1, newVec3f())
-      textbox.mesh.updateMeshData("position", vertexOffset + 2, newVec3f())
-      textbox.mesh.updateMeshData("position", vertexOffset + 3, newVec3f())
+      textbox.mesh.updateAttributeData("position", vertexOffset + 0, newVec3f())
+      textbox.mesh.updateAttributeData("position", vertexOffset + 1, newVec3f())
+      textbox.mesh.updateAttributeData("position", vertexOffset + 2, newVec3f())
+      textbox.mesh.updateAttributeData("position", vertexOffset + 3, newVec3f())
 
 
 func text*(textbox: Textbox): seq[Rune] =
--- a/src/semicongine/vulkan/memory.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/vulkan/memory.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -73,7 +73,7 @@
     size: size,
     memoryType: memoryType,
     canMap: VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT in memoryType.flags,
-    needsFlushing: not (VK_MEMORY_PROPERTY_HOST_COHERENT_BIT in result.memoryType.flags),
+    needsFlushing: not (VK_MEMORY_PROPERTY_HOST_COHERENT_BIT in memoryType.flags),
   )
 
   var allocationInfo = VkMemoryAllocateInfo(
--- a/src/semicongine/vulkan/pipeline.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/src/semicongine/vulkan/pipeline.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -27,7 +27,7 @@
 func samplers*(pipeline: Pipeline): seq[ShaderAttribute] =
   pipeline.shaderConfiguration.samplers
 
-proc setupDescriptors*(pipeline: Pipeline, descriptorPool: DescriptorPool, buffers: seq[Buffer], textures: Table[string, seq[VulkanTexture]], inFlightFrames: int): seq[DescriptorSet] =
+proc setupDescriptors*(pipeline: Pipeline, descriptorPool: DescriptorPool, buffers: seq[Buffer], textures: var Table[string, seq[VulkanTexture]], inFlightFrames: int, emptyTexture: VulkanTexture): seq[DescriptorSet] =
   assert pipeline.vk.valid
   assert buffers.len == 0 or buffers.len == inFlightFrames # need to guard against this in case we have no uniforms, then we also create no buffers
 
@@ -46,11 +46,14 @@
       elif descriptor.thetype == ImageSampler:
         if not (descriptor.name in textures):
           raise newException(Exception, &"Missing shader texture in scene: {descriptor.name}, available are {textures.keys.toSeq}")
-        if uint32(textures[descriptor.name].len) != descriptor.count:
-          raise newException(Exception, &"Incorrect number of textures in array for {descriptor.name}: has {textures[descriptor.name].len} but needs {descriptor.count}")
-        for t in textures[descriptor.name]:
-          descriptor.imageviews.add t.imageView
-          descriptor.samplers.add t.sampler
+
+        for textureIndex in 0 ..< int(descriptor.count):
+          if textureIndex < textures[descriptor.name].len:
+            descriptor.imageviews.add textures[descriptor.name][textureIndex].imageView
+            descriptor.samplers.add textures[descriptor.name][textureIndex].sampler
+          else:
+            descriptor.imageviews.add emptyTexture.imageView
+            descriptor.samplers.add emptyTexture.sampler
 
 proc createPipeline*(device: Device, renderPass: VkRenderPass, shaderConfiguration: ShaderConfiguration, inFlightFrames: int, subpass = 0'u32): Pipeline =
   assert renderPass.valid
--- a/tests/test_vulkan_wrapper.nim	Sat Aug 19 01:10:42 2023 +0700
+++ b/tests/test_vulkan_wrapper.nim	Sat Aug 19 22:24:06 2023 +0700
@@ -3,14 +3,16 @@
 import semicongine
 
 
-let sampler = Sampler(
+let
+  sampler = Sampler(
     magnification: VK_FILTER_NEAREST,
     minification: VK_FILTER_NEAREST,
     wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
     wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
   )
-let (R, W) = ([255'u8, 0'u8, 0'u8, 255'u8], [255'u8, 255'u8, 255'u8, 255'u8])
-let mat = Material(
+  (R, W) = ([255'u8, 0'u8, 0'u8, 255'u8], [255'u8, 255'u8, 255'u8, 255'u8])
+  mat = Material(
+    name: "mat",
     materialType: "my_material",
     textures: {
       "my_little_texture": Texture(image: Image(width: 5, height: 5, imagedata: @[
@@ -22,6 +24,26 @@
       ]), sampler: sampler)
     }.toTable
   )
+  mat2 = Material(
+    name: "mat2",
+    materialType: "my_material",
+    textures: {
+      "my_little_texture": Texture(image: Image(width: 5, height: 5, imagedata: @[
+      R, W, R, W, R,
+      W, R, W, R, W,
+      R, W, R, W, R,
+      W, R, W, R, W,
+      R, W, R, W, R,
+      ]), sampler: sampler)
+    }.toTable
+  )
+  mat3 = Material(
+    name: "mat3",
+    materialType: "my_special_material",
+    constants: {
+      "colors": toGPUValue(newVec4f(0.5, 0.5, 0))
+    }.toTable
+  )
 
 proc scene_different_mesh_types(): Entity =
   result = newEntity("root", [],
@@ -39,31 +61,35 @@
       positions=[newVec3f(0.0, 0.5), newVec3f(0.5, -0.5), newVec3f(-0.5, -0.5)],
       colors=[newVec4f(1.0, 0.0, 0.0, 1), newVec4f(0.0, 1.0, 0.0, 1), newVec4f(0.0, 0.0, 1.0, 1)],
       indices=[[0'u16, 2'u16, 1'u16]],
-      material=mat,
+      material=mat2,
     ))}),
     newEntity("triangle2b", {"mesh": Component(newMesh(
       positions=[newVec3f(0.0, 0.4), newVec3f(0.4, -0.4), newVec3f(-0.4, -0.4)],
       colors=[newVec4f(1.0, 0.0, 0.0, 1), newVec4f(0.0, 1.0, 0.0, 1), newVec4f(0.0, 0.0, 1.0, 1)],
       indices=[[0'u16, 2'u16, 1'u16]],
-      material=mat,
+      material=mat2,
     ))}),
     newEntity("triangle3a", {"mesh": Component(newMesh(
       positions=[newVec3f(0.4, 0.5), newVec3f(0.9, -0.3), newVec3f(0.0, -0.3)],
       colors=[newVec4f(1.0, 1.0, 0.0, 1), newVec4f(1.0, 1.0, 0.0, 1), newVec4f(1.0, 1.0, 0.0, 1)],
       indices=[[0'u32, 2'u32, 1'u32]],
       autoResize=false,
-      material=mat,
+      material=mat2,
     ))}),
     newEntity("triangle3b", {"mesh": Component(newMesh(
       positions=[newVec3f(0.4, 0.5), newVec3f(0.9, -0.3), newVec3f(0.0, -0.3)],
       colors=[newVec4f(1.0, 1.0, 0.0, 1), newVec4f(1.0, 1.0, 0.0, 1), newVec4f(1.0, 1.0, 0.0, 1)],
       indices=[[0'u32, 2'u32, 1'u32]],
       autoResize=false,
-      material=mat,
+      material=mat2,
     ))}),
   )
   for mesh in allComponentsOfType[Mesh](result):
     mesh.setInstanceData("translate", @[newVec3f()])
+  result[0]["mesh", Mesh()].updateInstanceData("translate", @[newVec3f(-0.6, -0.6)])
+  result[1]["mesh", Mesh()].updateInstanceData("translate", @[newVec3f(-0.6, 0.6)])
+  result[2]["mesh", Mesh()].updateInstanceData("translate", @[newVec3f(0.6, -0.6)])
+  result[3]["mesh", Mesh()].updateInstanceData("translate", @[newVec3f(0.6, 0.6)])
 
 proc scene_simple(): Entity =
   var mymesh1 = newMesh(
@@ -90,17 +116,17 @@
     instanceCount=2,
     material=mat,
   )
-  mymesh1.setInstanceData("translate", @[newVec3f(0.3, 0.0)])
-  mymesh2.setInstanceData("translate", @[newVec3f(0.0, 0.3)])
-  mymesh3.setInstanceData("translate", @[newVec3f(-0.3, 0.0)])
-  mymesh4.setInstanceData("translate", @[newVec3f(0.0, -0.3), newVec3f(0.0, 0.5)])
+  mymesh1.setInstanceData("translate", @[newVec3f( 0.4,  0.4)])
+  mymesh2.setInstanceData("translate", @[newVec3f( 0.4, -0.4)])
+  mymesh3.setInstanceData("translate", @[newVec3f(-0.4, -0.4)])
+  mymesh4.setInstanceData("translate", @[newVec3f(-0.4,  0.4), newVec3f(0.0, 0.0)])
   result = newEntity("root", [], newEntity("triangle", {"mesh1": Component(mymesh4), "mesh2": Component(mymesh3), "mesh3": Component(mymesh2), "mesh4": Component(mymesh1)}))
 
 proc scene_primitives(): Entity =
   var r = rect(color="ff0000")
   var t = tri(color="0000ff")
   var c = circle(color="00ff00")
-  t.material = mat
+  r.material = mat
   t.material = mat
   c.material = mat
 
@@ -110,11 +136,20 @@
   result = newEntity("root", {"mesh1": Component(t), "mesh2": Component(r), "mesh3": Component(c)})
 
 proc scene_flag(): Entity =
-  var r = rect(color="ff0000")
+  var r = rect(color="ffffff")
   r.material = mat
-  r.updateMeshData("color", @[newVec4f(0, 0), newVec4f(1, 0), newVec4f(1, 1), newVec4f(0, 1)])
   result = newEntity("root", {"mesh": Component(r)})
 
+proc scene_multi_material(): Entity =
+  var
+    r1 = rect(color="ffffff")
+    r2 = rect(color="000000")
+  r1.material = mat
+  r2.material = mat3
+  r1.setInstanceData("translate", @[newVec3f(-0.5)])
+  r2.setInstanceData("translate", @[newVec3f(+0.5)])
+  result = newEntity("root", {"mesh1": Component(r1), "mesh2": Component(r2)})
+
 proc main() =
   var engine = initEngine("Test")
 
@@ -124,23 +159,32 @@
       inputs=[
         attr[Vec3f]("position", memoryPerformanceHint=PreferFastRead),
         attr[Vec4f]("color", memoryPerformanceHint=PreferFastWrite),
-        attr[Vec3f]("translate", perInstance=true)
+        attr[Vec3f]("translate", perInstance=true),
+        attr[uint16]("materialIndex", perInstance=true),
       ],
-      intermediates=[attr[Vec4f]("outcolor")],
+      intermediates=[
+        attr[Vec4f]("outcolor"),
+        attr[uint16]("materialIndexOut", noInterpolation=true),
+      ],
       outputs=[attr[Vec4f]("color")],
       uniforms=[attr[float32]("time")],
-      samplers=[attr[Sampler2DType]("my_little_texture")],
-      vertexCode="""gl_Position = vec4(position + translate, 1.0); outcolor = color;""",
-      fragmentCode="color = texture(my_little_texture, outcolor.xy) * 0.5 + outcolor * 0.5;",
+      samplers=[
+        attr[Sampler2DType]("my_little_texture", arrayCount=2)
+      ],
+      vertexCode="""gl_Position = vec4(position + translate, 1.0); outcolor = color; materialIndexOut = materialIndex;""",
+      fragmentCode="color = texture(my_little_texture[materialIndexOut], outcolor.xy) * 0.5 + outcolor * 0.5;",
     )
-  engine.setRenderer({"my_material": shaderConfiguration}.toTable)
+  engine.initRenderer({
+    "my_material": shaderConfiguration,
+    "my_special_material": shaderConfiguration,
+  }.toTable)
 
   # INIT SCENES
   var scenes = [
-    newScene("simple", scene_simple(), transformAttribute="", materialIndexAttribute=""),
-    newScene("different mesh types", scene_different_mesh_types(), transformAttribute="", materialIndexAttribute=""),
-    newScene("primitives", scene_primitives(), transformAttribute="", materialIndexAttribute=""),
-    newScene("flag", scene_flag(), transformAttribute="", materialIndexAttribute=""),
+    newScene("simple", scene_simple(), transformAttribute=""),
+    newScene("different mesh types", scene_different_mesh_types(), transformAttribute=""),
+    newScene("primitives", scene_primitives(), transformAttribute=""),
+    newScene("flag", scene_multi_material(), transformAttribute=""),
   ]
 
   for scene in scenes.mitems: