changeset 322:6dab370d1758

add: first, incomplete version of material use
author Sam <sam@basx.dev>
date Sat, 19 Aug 2023 01:10:42 +0700
parents 30117d8f0052
children 9defff46da48
files src/semicongine.nim src/semicongine/engine.nim src/semicongine/material.nim src/semicongine/mesh.nim src/semicongine/renderer.nim src/semicongine/resources/mesh.nim src/semicongine/scene.nim src/semicongine/vulkan/pipeline.nim src/semicongine/vulkan/renderpass.nim tests/test_vulkan_wrapper.nim
diffstat 10 files changed, 271 insertions(+), 249 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -7,7 +7,6 @@
 import semicongine/collision
 import semicongine/scene
 import semicongine/events
-import semicongine/material
 import semicongine/mesh
 import semicongine/renderer
 import semicongine/resources
@@ -22,7 +21,6 @@
 export collision
 export scene
 export events
-export material
 export mesh
 export renderer
 export resources
--- a/src/semicongine/engine.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/engine.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -1,4 +1,4 @@
-import std/sequtils
+import std/tables
 import std/options
 import std/logging
 import std/os
@@ -9,10 +9,10 @@
 import ./vulkan/instance
 import ./vulkan/device
 import ./vulkan/physicaldevice
-import ./vulkan/renderpass
 import ./vulkan/shader
 
 import ./scene
+import ./mesh
 import ./renderer
 import ./events
 import ./audio
@@ -102,21 +102,14 @@
   )
   startMixerThread()
 
-proc setRenderer*(engine: var Engine, renderPass: RenderPass) =
-  if engine.renderer.isSome:
-    engine.renderer.get.destroy()
-  engine.renderer = some(engine.device.initRenderer(renderPass))
+proc setRenderer*(engine: var Engine, shaders: Table[string, ShaderConfiguration], clearColor=Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32])) =
 
-proc addScene*(engine: var Engine, scene: Scene, vertexInput: seq[ShaderAttribute], samplers: seq[ShaderAttribute], transformAttribute="transform", materialIndexAttribute="materialIndex") =
-  assert transformAttribute == "" or transformAttribute in map(vertexInput, proc(a: ShaderAttribute): string = a.name)
-  assert materialIndexAttribute == "" or materialIndexAttribute in map(vertexInput, proc(a: ShaderAttribute): string = a.name)
+  assert not engine.renderer.isSome
+  engine.renderer = some(engine.device.initRenderer(shaders=shaders, clearColor=clearColor))
+
+proc addScene*(engine: var Engine, scene: Scene) =
   assert engine.renderer.isSome
-  # TODO:
-  # rethink when and how we set up those buffers
-  # scene should have no idea about shader inputs and samplers, but we need to use those in the setup
-  # idea: gather material-names -> get materials -> get shaders -> determine vertexInputs and samplers?
-  # also, be aware: need to support multiple pipelines/shaders
-  engine.renderer.get.setupDrawableBuffers(scene, vertexInput, samplers)
+  engine.renderer.get.setupDrawableBuffers(scene)
 
 proc renderScene*(engine: var Engine, scene: var Scene) =
   assert engine.state == Running
--- a/src/semicongine/material.nim	Tue Aug 15 23:51:37 2023 +0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-import std/tables
-import std/strformat
-import std/strutils
-
-import ./core
-
-type
-  Material* = ref object
-    name*: string
-    index*: int
-    constants*: Table[string, DataValue]
-    textures*: Table[string, Texture]
-
-proc `$`*(material: Material): string =
-  var constants: seq[string]
-  for key, value in material.constants.pairs:
-    constants.add &"{key}: {value}"
-  var textures: seq[string]
-  for key in material.textures.keys:
-    textures.add &"{key}"
-  return &"""{material.name} ({material.index}) | Values: {constants.join(", ")} | Textures: {textures.join(", ")}"""
--- a/src/semicongine/mesh.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/mesh.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -1,8 +1,10 @@
-import std/math as nimmath
+import std/hashes
+import std/options
 import std/typetraits
 import std/tables
+import std/strformat
 import std/enumerate
-import std/strformat
+import std/strutils
 import std/sequtils
 
 import ./core
@@ -18,7 +20,7 @@
   Mesh* = ref object of Component
     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)
-    materials*: seq[string]
+    material*: Material
     dirtyInstanceTransforms: bool
     data: Table[string, DataList]
     changedAttributes: seq[string]
@@ -27,6 +29,15 @@
       of Tiny: tinyIndices: seq[array[3, uint8]]
       of Small: smallIndices: seq[array[3, uint16]]
       of Big: bigIndices: seq[array[3, uint32]]
+  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)
 
 converter toVulkan*(indexType: MeshIndexType): VkIndexType =
   case indexType:
@@ -52,6 +63,15 @@
 method `$`*(mesh: Mesh): string =
   &"Mesh, vertexCount: {mesh.vertexCount}, vertexData: {mesh.data.keys().toSeq()}, indexType: {mesh.indexType}"
 
+proc `$`*(material: Material): string =
+  var constants: seq[string]
+  for key, value in material.constants.pairs:
+    constants.add &"{key}: {value}"
+  var textures: seq[string]
+  for key in material.textures.keys:
+    textures.add &"{key}"
+  return &"""{material.name} ({material.index}) | Values: {constants.join(", ")} | Textures: {textures.join(", ")}"""
+
 func prettyData*(mesh: Mesh): string =
   for attr, data in mesh.data.pairs:
     result &= &"{attr}: {data}\n"
@@ -79,6 +99,7 @@
   indices: openArray[array[3, uint32|int32|uint16|int16|int]],
   colors: openArray[Vec4f]=[],
   uvs: openArray[Vec2f]=[],
+  material: Material=nil,
   instanceCount=1'u32,
   autoResize=true
 ): auto =
@@ -94,7 +115,13 @@
     elif autoResize and uint32(positions.len) < uint32(high(uint16)):
       indexType = Small
 
-  result = Mesh(instanceCount: instanceCount, instanceTransforms: newSeqWith(int(instanceCount), Unit4F32), indexType: indexType)
+  result = Mesh(
+    instanceCount: instanceCount,
+    instanceTransforms: newSeqWith(int(instanceCount), Unit4F32),
+    indexType: indexType,
+  )
+  result.material = material
+
   setMeshData(result, "position", positions.toSeq)
   if colors.len > 0: setMeshData(result, "color", colors.toSeq)
   if uvs.len > 0: setMeshData(result, "uv", uvs.toSeq)
@@ -122,8 +149,16 @@
   colors: openArray[Vec4f]=[],
   uvs: openArray[Vec2f]=[],
   instanceCount=1'u32,
+  material: Material=nil,
 ): auto =
-  newMesh(positions, newSeq[array[3, int]](), colors, uvs, instanceCount)
+  newMesh(
+    positions=positions,
+    indices=newSeq[array[3, int]](),
+    colors=colors,
+    uvs=uvs,
+    material=material,
+    instanceCount=instanceCount,
+  )
 
 func availableAttributes*(mesh: Mesh): seq[string] =
   mesh.data.keys.toSeq
--- a/src/semicongine/renderer.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/renderer.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -1,25 +1,23 @@
 import std/options
-import std/sequtils
 import std/enumerate
 import std/tables
 import std/strformat
-import std/strutils
 import std/logging
 
 import ./core
 import ./vulkan/buffer
 import ./vulkan/device
 import ./vulkan/drawable
+import ./vulkan/physicaldevice
 import ./vulkan/pipeline
-import ./vulkan/physicaldevice
 import ./vulkan/renderpass
 import ./vulkan/swapchain
+import ./vulkan/shader
 import ./vulkan/descriptor
 import ./vulkan/image
 
 import ./scene
 import ./mesh
-import ./material
 
 type
   SceneData = object
@@ -32,6 +30,7 @@
     attributeBindingNumber*: Table[string, int]
     transformAttribute: string # name of attribute that is used for per-instance mesh transformation
     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
     descriptorSets*: Table[VkPipeline, seq[DescriptorSet]]
@@ -43,22 +42,36 @@
     scenedata: Table[Scene, SceneData]
 
 
-proc initRenderer*(device: Device, renderPass: RenderPass): Renderer =
+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
-  assert renderPass.vk.valid
-
+  
   result.device = device
-  result.renderPass = renderPass
+  result.renderPass = device.simpleForwardRenderPass(shaders, clearColor=clearColor)
   result.surfaceFormat = device.physicalDevice.getSurfaceFormats().filterSurfaceFormat()
   # use last renderpass as output for swapchain
   let swapchain = device.createSwapchain(result.renderPass.vk, result.surfaceFormat, device.firstGraphicsQueue().get().family)
   if not swapchain.isSome:
     raise newException(Exception, "Unable to create swapchain")
+
   result.swapchain = swapchain.get()
 
-proc setupDrawableBuffers*(renderer: var Renderer, scene: Scene, inputs: seq[ShaderAttribute], samplers: seq[ShaderAttribute]) =
+func inputs(renderer: Renderer): seq[ShaderAttribute] =
+  for i in 0 ..< renderer.renderPass.subpasses.len:
+    for pipeline in renderer.renderPass.subpasses[i].pipelines.values:
+      result.add pipeline.inputs
+
+func samplers(renderer: Renderer): seq[ShaderAttribute] =
+  for i in 0 ..< renderer.renderPass.subpasses.len:
+    for pipeline in renderer.renderPass.subpasses[i].pipelines.values:
+      result.add pipeline.samplers
+
+proc setupDrawableBuffers*(renderer: var Renderer, scene: Scene) =
   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
   var scenedata = SceneData()
 
   # if mesh transformation are handled through the scenegraph-transformation, set it up here
@@ -67,7 +80,7 @@
     for input in inputs:
       if input.name == scene.transformAttribute:
         assert input.perInstance == true, $input
-        assert getDataType[Mat4]() == input.thetype
+        assert getDataType[Mat4]() == input.thetype, $input
         hasTransformAttribute = true
     assert hasTransformAttribute
     scenedata.transformAttribute = scene.transformAttribute
@@ -82,6 +95,10 @@
     assert hasMaterialIndexAttribute
     scenedata.materialIndexAttribute = scene.materialIndexAttribute
 
+  for mesh in allComponentsOfType[Mesh](scene.root):
+    if mesh.material != nil and not scenedata.materials.contains(mesh.material):
+      scenedata.materials.add mesh.material
+
   # find all meshes, populate missing attribute values for shader
   var allMeshes: seq[Mesh]
   for mesh in allComponentsOfType[Mesh](scene.root):
@@ -92,13 +109,11 @@
         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.materials.len > 0, "Missing material specification for mesh. Either set the 'materials' attribute or pass the argument 'materialIndexAttribute=\"\"' when calling 'addScene'"
-        assert mesh.materials.len == getMeshData[uint16](mesh, scenedata.materialIndexAttribute)[].len
-        for i, material in enumerate(mesh.materials):
-          let matIndex = scene.materialIndex(material)
-          if matIndex < 0:
-            raise newException(Exception, &"Required material '{material}' not available in scene (available are: {scene.materials.toSeq})")
-          updateMeshData[uint16](mesh, scenedata.materialIndexAttribute, uint32(i), uint16(matIndex))
+        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))
   
   # create index buffer if necessary
   var indicesBufferSize = 0'u64
@@ -187,7 +202,8 @@
       indexBufferOffset += size
     scenedata.drawables[mesh] = drawable
 
-  for material in scene.materials:
+  # upload textures
+  for material in scenedata.materials:
     for textureName, texture in material.textures.pairs:
       if not scenedata.textures.hasKey(textureName):
         scenedata.textures[textureName] = @[]
@@ -195,7 +211,7 @@
 
   # setup uniforms and samplers
   for subpass_i in 0 ..< renderer.renderPass.subpasses.len:
-    for pipeline in renderer.renderPass.subpasses[subpass_i].pipelines.mitems:
+    for material, pipeline in renderer.renderPass.subpasses[subpass_i].pipelines.pairs:
       var uniformBufferSize = 0'u64
       for uniform in pipeline.uniforms:
         uniformBufferSize += uniform.size
@@ -269,7 +285,7 @@
   assert scene in renderer.scenedata
 
   for i in 0 ..< renderer.renderPass.subpasses.len:
-    for pipeline in renderer.renderPass.subpasses[i].pipelines.mitems:
+    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:
         assert renderer.scenedata[scene].uniformBuffers[pipeline.vk][renderer.swapchain.currentInFlight].vk.valid
         var offset = 0'u64
@@ -301,18 +317,19 @@
   commandBuffer = commandBufferResult.get()
   commandBuffer.beginRenderCommands(renderer.renderPass, renderer.swapchain.currentFramebuffer())
 
+  debug "Scene buffers:"
+  for (location, buffer) in renderer.scenedata[scene].vertexBuffers.pairs:
+    debug "  ", location, ": ", buffer
+  debug "  Index buffer: ", renderer.scenedata[scene].indexBuffer
+
   for i in 0 ..< renderer.renderPass.subpasses.len:
-    for pipeline in renderer.renderPass.subpasses[i].pipelines.mitems:
+    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)
 
-      debug "Scene buffers:"
-      for (location, buffer) in renderer.scenedata[scene].vertexBuffers.pairs:
-        debug "  ", location, ": ", buffer
-      debug "  Index buffer: ", renderer.scenedata[scene].indexBuffer
-
       for drawable in renderer.scenedata[scene].drawables.values:
-        drawable.draw(commandBuffer, vertexBuffers=renderer.scenedata[scene].vertexBuffers, indexBuffer=renderer.scenedata[scene].indexBuffer)
+        if 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)
--- a/src/semicongine/resources/mesh.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/resources/mesh.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -1,5 +1,4 @@
 import std/strutils
-import std/enumerate
 import std/json
 import std/logging
 import std/tables
@@ -9,7 +8,6 @@
 
 import ../scene
 import ../mesh
-import ../material
 import ../core
 
 import ./image
@@ -92,7 +90,7 @@
     of Float32: return Vec4F32
     else: raise newException(Exception, &"Unsupported data type: {componentType} {theType}")
 
-proc getBufferViewData(bufferView: JsonNode, mainBuffer: var seq[uint8], baseBufferOffset=0): seq[uint8] =
+proc getBufferViewData(bufferView: JsonNode, mainBuffer: seq[uint8], baseBufferOffset=0): seq[uint8] =
   assert bufferView["buffer"].getInt() == 0, "Currently no external buffers supported"
 
   result = newSeq[uint8](bufferView["byteLength"].getInt())
@@ -127,6 +125,98 @@
   else:
     copyMem(dstPointer, addr mainBuffer[bufferOffset], length)
 
+proc loadImage(root: JsonNode, imageIndex: int, mainBuffer: seq[uint8]): Image =
+  if root["images"][imageIndex].hasKey("uri"):
+    raise newException(Exception, "Unsupported feature: Load images from external files")
+
+  let bufferView = root["bufferViews"][root["images"][imageIndex]["bufferView"].getInt()]
+  let imgData = newStringStream(cast[string](getBufferViewData(bufferView, mainBuffer)))
+
+  let imageType = root["images"][imageIndex]["mimeType"].getStr()
+  case imageType
+  of "image/bmp":
+    result = readBMP(imgData)
+  of "image/png":
+    result = readPNG(imgData)
+  else:
+    raise newException(Exception, "Unsupported feature: Load image of type " & imageType)
+
+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()]
+    if sampler.hasKey("magFilter"):
+      result.sampler.magnification = SAMPLER_FILTER_MODE_MAP[sampler["magFilter"].getInt()]
+    if sampler.hasKey("minFilter"):
+      result.sampler.minification = SAMPLER_FILTER_MODE_MAP[sampler["minFilter"].getInt()]
+    if sampler.hasKey("wrapS"):
+      result.sampler.wrapModeS = SAMPLER_WRAP_MODE_MAP[sampler["wrapS"].getInt()]
+    if sampler.hasKey("wrapT"):
+      result.sampler.wrapModeT = SAMPLER_WRAP_MODE_MAP[sampler["wrapS"].getInt()]
+
+
+proc loadMaterial(root: JsonNode, materialNode: JsonNode, mainBuffer: seq[uint8], materialIndex: uint16): Material =
+  result = Material(name: materialNode["name"].getStr(), index: materialIndex)
+
+  let pbr = materialNode["pbrMetallicRoughness"]
+
+  # color
+  result.constants["baseColorFactor"] = DataValue(thetype: Vec4F32)
+  if pbr.hasKey("baseColorFactor"):
+    setValue(result.constants["baseColorFactor"], newVec4f(
+      pbr["baseColorFactor"][0].getFloat(),
+      pbr["baseColorFactor"][1].getFloat(),
+      pbr["baseColorFactor"][2].getFloat(),
+      pbr["baseColorFactor"][3].getFloat(),
+    ))
+  else:
+    setValue(result.constants["baseColorFactor"], newVec4f(1, 1, 1, 1))
+
+  # pbr material constants
+  for factor in ["metallicFactor", "roughnessFactor"]:
+    result.constants[factor] = DataValue(thetype: Float32)
+    if pbr.hasKey(factor):
+      setValue(result.constants[factor], float32(pbr[factor].getFloat()))
+    else:
+      setValue(result.constants[factor], 0.5'f32)
+
+  # pbr material textures
+  for texture in ["baseColorTexture", "metallicRoughnessTexture"]:
+    if pbr.hasKey(texture):
+      result.textures[texture] = loadTexture(root, pbr[texture]["index"].getInt(), mainBuffer)
+      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.constants[texture & "Index"] = DataValue(thetype: UInt8)
+      setValue(result.constants[texture & "Index"], 0'u8)
+
+  # generic material textures
+  for texture in ["normalTexture", "occlusionTexture", "emissiveTexture"]:
+    if materialNode.hasKey(texture):
+      result.textures[texture] = loadTexture(root, materialNode[texture]["index"].getInt(), mainBuffer)
+      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.constants[texture & "Index"] = DataValue(thetype: UInt8)
+      setValue(result.constants[texture & "Index"], 0'u8)
+
+  # emissiv color
+  result.constants["emissiveFactor"] = DataValue(thetype: Vec3F32)
+  if materialNode.hasKey("emissiveFactor"):
+    setValue(result.constants["emissiveFactor"], newVec3f(
+      materialNode["emissiveFactor"][0].getFloat(),
+      materialNode["emissiveFactor"][1].getFloat(),
+      materialNode["emissiveFactor"][2].getFloat(),
+    ))
+  else:
+    setValue(result.constants["emissiveFactor"], newVec3f(1'f32, 1'f32, 1'f32))
+
+
 proc addPrimitive(mesh: var 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")
@@ -141,7 +231,10 @@
   if primitiveNode.hasKey("material"):
     materialId = uint16(primitiveNode["material"].getInt())
   mesh.appendMeshData("materialIndex", newSeqWith[uint8](int(vertexCount), materialId))
-  mesh.materials.add newSeqWith[string](int(vertexCount), root["materials"][int(materialId)]["name"].getStr())
+  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
 
   if primitiveNode.hasKey("indices"):
     assert mesh.indexType != None
@@ -253,95 +346,6 @@
 
   newScene(scenenode["name"].getStr(), rootEntity)
 
-proc loadImage(root: JsonNode, imageIndex: int, mainBuffer: var seq[uint8]): Image =
-  if root["images"][imageIndex].hasKey("uri"):
-    raise newException(Exception, "Unsupported feature: Load images from external files")
-
-  let bufferView = root["bufferViews"][root["images"][imageIndex]["bufferView"].getInt()]
-  let imgData = newStringStream(cast[string](getBufferViewData(bufferView, mainBuffer)))
-
-  let imageType = root["images"][imageIndex]["mimeType"].getStr()
-  case imageType
-  of "image/bmp":
-    result = readBMP(imgData)
-  of "image/png":
-    result = readPNG(imgData)
-  else:
-    raise newException(Exception, "Unsupported feature: Load image of type " & imageType)
-
-proc loadTexture(root: JsonNode, textureIndex: int, mainBuffer: var 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()]
-    if sampler.hasKey("magFilter"):
-      result.sampler.magnification = SAMPLER_FILTER_MODE_MAP[sampler["magFilter"].getInt()]
-    if sampler.hasKey("minFilter"):
-      result.sampler.minification = SAMPLER_FILTER_MODE_MAP[sampler["minFilter"].getInt()]
-    if sampler.hasKey("wrapS"):
-      result.sampler.wrapModeS = SAMPLER_WRAP_MODE_MAP[sampler["wrapS"].getInt()]
-    if sampler.hasKey("wrapT"):
-      result.sampler.wrapModeT = SAMPLER_WRAP_MODE_MAP[sampler["wrapS"].getInt()]
-
-proc loadMaterial(root: JsonNode, materialNode: JsonNode, mainBuffer: var seq[uint8], materialIndex: int): Material =
-  result = Material(name: materialNode["name"].getStr(), index: materialIndex)
-
-  let pbr = materialNode["pbrMetallicRoughness"]
-
-  # color
-  result.constants["baseColorFactor"] = DataValue(thetype: Vec4F32)
-  if pbr.hasKey("baseColorFactor"):
-    setValue(result.constants["baseColorFactor"], newVec4f(
-      pbr["baseColorFactor"][0].getFloat(),
-      pbr["baseColorFactor"][1].getFloat(),
-      pbr["baseColorFactor"][2].getFloat(),
-      pbr["baseColorFactor"][3].getFloat(),
-    ))
-  else:
-    setValue(result.constants["baseColorFactor"], newVec4f(1, 1, 1, 1))
-
-  # pbr material constants
-  for factor in ["metallicFactor", "roughnessFactor"]:
-    result.constants[factor] = DataValue(thetype: Float32)
-    if pbr.hasKey(factor):
-      setValue(result.constants[factor], float32(pbr[factor].getFloat()))
-    else:
-      setValue(result.constants[factor], 0.5'f32)
-
-  # pbr material textures
-  for texture in ["baseColorTexture", "metallicRoughnessTexture"]:
-    if pbr.hasKey(texture):
-      result.textures[texture] = loadTexture(root, pbr[texture]["index"].getInt(), mainBuffer)
-      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.constants[texture & "Index"] = DataValue(thetype: UInt8)
-      setValue(result.constants[texture & "Index"], 0'u8)
-
-  # generic material textures
-  for texture in ["normalTexture", "occlusionTexture", "emissiveTexture"]:
-    if materialNode.hasKey(texture):
-      result.textures[texture] = loadTexture(root, materialNode[texture]["index"].getInt(), mainBuffer)
-      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.constants[texture & "Index"] = DataValue(thetype: UInt8)
-      setValue(result.constants[texture & "Index"], 0'u8)
-
-  # emissiv color
-  result.constants["emissiveFactor"] = DataValue(thetype: Vec3F32)
-  if materialNode.hasKey("emissiveFactor"):
-    setValue(result.constants["emissiveFactor"], newVec3f(
-      materialNode["emissiveFactor"][0].getFloat(),
-      materialNode["emissiveFactor"][1].getFloat(),
-      materialNode["emissiveFactor"][2].getFloat(),
-    ))
-  else:
-    setValue(result.constants["emissiveFactor"], newVec3f(1'f32, 1'f32, 1'f32))
 
 proc readglTF*(stream: Stream): seq[Scene] =
   var
@@ -373,9 +377,4 @@
   debug "Loading mesh: ", data.structuredContent.pretty
 
   for scenedata in data.structuredContent["scenes"]:
-    var scene = data.structuredContent.loadScene(scenedata, data.binaryBufferData)
-    for i, materialNode in enumerate(data.structuredContent["materials"]):
-      let material = loadMaterial(data.structuredContent, materialNode, data.binaryBufferData, i)
-      scene.addMaterial material
-
-    result.add scene
+    result.add data.structuredContent.loadScene(scenedata, data.binaryBufferData)
--- a/src/semicongine/scene.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/scene.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -1,12 +1,10 @@
 import std/strformat
-import std/sequtils
 import std/strutils
 import std/tables
 import std/hashes
 import std/typetraits
 
 import ./core
-import ./material
 import ./animation
 
 type
@@ -14,7 +12,6 @@
     name*: string
     root*: Entity
     shaderGlobals*: Table[string, DataList]
-    materials: OrderedTable[string, Material]
     transformAttribute*: string = "transform"
     materialIndexAttribute*: string = "materialIndex"
 
@@ -103,28 +100,8 @@
 func appendShaderGlobalArray*[T](scene: var Scene, name: string, value: seq[T]) =
   appendValues[T](scene.shaderGlobals[name], value)
 
-func newScene*(name: string, root: Entity): Scene =
-  Scene(name: name, root: root)
-
-func addMaterial*(scene: var Scene, material: Material) =
-  assert not scene.materials.contains(material.name), &"Material with name '{material.name}' already exists in scene"
-  for name, value in material.constants.pairs:
-    scene.shaderGlobals[name] = newDataList(thetype=value.thetype)
-
-  for name, value in material.constants.pairs:
-    scene.shaderGlobals[name].appendValue(value)
-
-  scene.materials[material.name] = material
-
-func materialIndex*(scene: Scene, materialName: string): int =
-  for name in scene.materials.keys:
-    if name == materialName:
-      return result
-    inc result 
-  return -1
-
-func materials*(scene: Scene): auto =
-  scene.materials.values.toSeq
+func newScene*(name: string, root: Entity, transformAttribute="transform", materialIndexAttribute="materialIndex"): Scene =
+  Scene(name: name, root: root, transformAttribute: transformAttribute, materialIndexAttribute: materialIndexAttribute)
 
 func hash*(scene: Scene): Hash =
   hash(scene.name)
--- a/src/semicongine/vulkan/pipeline.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/vulkan/pipeline.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -18,12 +18,16 @@
     shaderModules*: (ShaderModule, ShaderModule)
     descriptorSetLayout*: DescriptorSetLayout
 
-func inputs*(pipeline: Pipeline): seq[ShaderAttribute] = pipeline.shaderConfiguration.inputs
+func inputs*(pipeline: Pipeline): seq[ShaderAttribute] =
+  pipeline.shaderConfiguration.inputs
 
 func uniforms*(pipeline: Pipeline): seq[ShaderAttribute] =
   pipeline.shaderConfiguration.uniforms
 
-proc setupDescriptors*(pipeline: var Pipeline, descriptorPool: DescriptorPool, buffers: seq[Buffer], textures: Table[string, seq[VulkanTexture]], inFlightFrames: int): seq[DescriptorSet] =
+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] =
   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
 
@@ -54,17 +58,18 @@
 
   result.device = device
   result.shaderModules = device.createShaderModules(shaderConfiguration)
+  result.shaderConfiguration = shaderConfiguration
   
   var descriptors: seq[Descriptor]
-  if shaderConfiguration.uniforms.len > 0:
+  if result.shaderConfiguration.uniforms.len > 0:
     descriptors.add Descriptor(
       name: "Uniforms",
       thetype: Uniform,
       count: 1,
       stages: @[VK_SHADER_STAGE_VERTEX_BIT, VK_SHADER_STAGE_FRAGMENT_BIT],
-      size: shaderConfiguration.uniforms.size(),
+      size: result.shaderConfiguration.uniforms.size(),
     )
-  for sampler in shaderConfiguration.samplers:
+  for sampler in result.shaderConfiguration.samplers:
     descriptors.add Descriptor(
       name: sampler.name,
       thetype: ImageSampler,
@@ -93,7 +98,7 @@
   var
     bindings: seq[VkVertexInputBindingDescription]
     attributes: seq[VkVertexInputAttributeDescription]
-    vertexInputInfo = shaderConfiguration.getVertexInputInfo(bindings, attributes)
+    vertexInputInfo = result.shaderConfiguration.getVertexInputInfo(bindings, attributes)
     inputAssembly = VkPipelineInputAssemblyStateCreateInfo(
       sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
       topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
--- a/src/semicongine/vulkan/renderpass.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/src/semicongine/vulkan/renderpass.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -1,4 +1,5 @@
 import std/options
+import std/tables
 
 import ../core
 import ./device
@@ -12,12 +13,9 @@
     clearColor*: Vec4f
     pipelineBindPoint*: VkPipelineBindPoint
     flags: VkSubpassDescriptionFlags
-    inputs: seq[VkAttachmentReference]
     outputs: seq[VkAttachmentReference]
-    resolvers: seq[VkAttachmentReference]
     depthStencil: Option[VkAttachmentReference]
-    preserves: seq[uint32]
-    pipelines*: seq[Pipeline]
+    pipelines*: Table[string, Pipeline]
   RenderPass* = object
     vk*: VkRenderPass
     device*: Device
@@ -39,14 +37,14 @@
     subpassesList.add VkSubpassDescription(
       flags: subpass.flags,
       pipelineBindPoint: subpass.pipelineBindPoint,
-      inputAttachmentCount: uint32(subpass.inputs.len),
-      pInputAttachments: subpass.inputs.toCPointer,
+      inputAttachmentCount: 0,
+      pInputAttachments: nil,
       colorAttachmentCount: uint32(subpass.outputs.len),
       pColorAttachments: subpass.outputs.toCPointer,
-      pResolveAttachments: subpass.resolvers.toCPointer,
+      pResolveAttachments: nil,
       pDepthStencilAttachment: if subpass.depthStencil.isSome: addr(subpass.depthStencil.get) else: nil,
-      preserveAttachmentCount: uint32(subpass.preserves.len),
-      pPreserveAttachments: subpass.preserves.toCPointer,
+      preserveAttachmentCount: 0,
+      pPreserveAttachments: nil,
     )
 
   var createInfo = VkRenderPassCreateInfo(
@@ -64,19 +62,19 @@
 
 proc simpleForwardRenderPass*(
   device: Device,
-  shaderConfiguration: ShaderConfiguration,
+  shaders: Table[string, ShaderConfiguration],
   inFlightFrames=2,
-  format=VK_FORMAT_UNDEFINED ,
   clearColor=Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32])
 ): RenderPass =
+  # TODO: check wether materials are compatible with the assigned shaders
+  {.warning: "Need to implement material -> shader compatability" .}
+  
   assert device.vk.valid
-  assert shaderConfiguration.outputs.len == 1
-  var theformat = format
-  if theformat == VK_FORMAT_UNDEFINED:
-    theformat = device.physicalDevice.getSurfaceFormats().filterSurfaceFormat().format
+  for shaderconfig in shaders.values:
+    assert shaderconfig.outputs.len == 1
   var
     attachments = @[VkAttachmentDescription(
-        format: theformat,
+        format: device.physicalDevice.getSurfaceFormats().filterSurfaceFormat().format,
         samples: VK_SAMPLE_COUNT_1_BIT,
         loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR,
         storeOp: VK_ATTACHMENT_STORE_OP_STORE,
@@ -102,7 +100,9 @@
       dstAccessMask: toBits [VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT],
     )]
   result = device.createRenderPass(attachments=attachments, subpasses=subpasses, dependencies=dependencies)
-  result.subpasses[0].pipelines.add device.createPipeline(result.vk, shaderConfiguration, inFlightFrames, 0)
+  for material, shaderconfig in shaders.pairs:
+    result.subpasses[0].pipelines[material] = device.createPipeline(result.vk, shaderconfig, inFlightFrames, 0)
+
 
 proc beginRenderCommands*(commandBuffer: VkCommandBuffer, renderpass: RenderPass, framebuffer: Framebuffer) =
   assert commandBuffer.valid
@@ -159,7 +159,7 @@
   assert renderPass.vk.valid
   renderPass.device.vk.vkDestroyRenderPass(renderPass.vk, nil)
   renderPass.vk.reset
-  for subpass in renderPass.subpasses.mitems:
-    for pipeline in subpass.pipelines.mitems:
+  for i in 0 ..< renderPass.subpasses.len:
+    for pipeline in renderPass.subpasses[i].pipelines.mvalues:
       pipeline.destroy()
   renderPass.subpasses = @[]
--- a/tests/test_vulkan_wrapper.nim	Tue Aug 15 23:51:37 2023 +0700
+++ b/tests/test_vulkan_wrapper.nim	Sat Aug 19 01:10:42 2023 +0700
@@ -2,37 +2,64 @@
 
 import semicongine
 
+
+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(
+    materialType: "my_material",
+    textures: {
+      "my_little_texture": Texture(image: Image(width: 5, height: 5, imagedata: @[
+      R, R, R, R, R,
+      R, R, W, R, R,
+      R, W, W, W, R,
+      R, R, W, R, R,
+      R, R, R, R, R,
+      ]), sampler: sampler)
+    }.toTable
+  )
+
 proc scene_different_mesh_types(): Entity =
   result = newEntity("root", [],
     newEntity("triangle1", {"mesh": Component(newMesh(
       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)],
+      material=mat,
     ))}),
     newEntity("triangle1b", {"mesh": Component(newMesh(
       positions=[newVec3f(0.0, -0.4), newVec3f(0.4, 0.4), newVec3f(-0.4, 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)],
+      material=mat,
     ))}),
     newEntity("triangle2a", {"mesh": Component(newMesh(
       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]]
+      indices=[[0'u16, 2'u16, 1'u16]],
+      material=mat,
     ))}),
     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]]
+      indices=[[0'u16, 2'u16, 1'u16]],
+      material=mat,
     ))}),
     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
+      autoResize=false,
+      material=mat,
     ))}),
     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
+      autoResize=false,
+      material=mat,
     ))}),
   )
   for mesh in allComponentsOfType[Mesh](result):
@@ -42,22 +69,26 @@
   var mymesh1 = newMesh(
     positions=[newVec3f(0.0, -0.3), newVec3f(0.3, 0.3), newVec3f(-0.3, 0.3)],
     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)],
+    material=mat,
   )
   var mymesh2 = newMesh(
     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)],
+    material=mat,
   )
   var mymesh3 = newMesh(
     positions=[newVec3f(0.0, -0.6), newVec3f(0.6, 0.6), newVec3f(-0.6, 0.6)],
     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, 1'u32, 2'u32]],
-    autoResize=false
+    autoResize=false,
+    material=mat,
   )
   var mymesh4 = newMesh(
     positions=[newVec3f(0.0, -0.8), newVec3f(0.8, 0.8), newVec3f(-0.8, 0.8)],
     colors=[newVec4f(0.0, 0.0, 1.0, 1), newVec4f(0.0, 0.0, 1.0, 1), newVec4f(0.0, 0.0, 1.0, 1)],
     indices=[[0'u16, 1'u16, 2'u16]],
-    instanceCount=2
+    instanceCount=2,
+    material=mat,
   )
   mymesh1.setInstanceData("translate", @[newVec3f(0.3, 0.0)])
   mymesh2.setInstanceData("translate", @[newVec3f(0.0, 0.3)])
@@ -69,14 +100,18 @@
   var r = rect(color="ff0000")
   var t = tri(color="0000ff")
   var c = circle(color="00ff00")
+  t.material = mat
+  t.material = mat
+  c.material = mat
 
   r.setInstanceData("translate", @[newVec3f(0.5, -0.3)])
   t.setInstanceData("translate", @[newVec3f(0.3,  0.3)])
   c.setInstanceData("translate", @[newVec3f(-0.3,  0.1)])
-  result = newEntity("root", {"mesh1": Component(t), "mesh1": Component(r), "mesh1": Component(c)})
+  result = newEntity("root", {"mesh1": Component(t), "mesh2": Component(r), "mesh3": Component(c)})
 
 proc scene_flag(): Entity =
   var r = rect(color="ff0000")
+  r.material = mat
   r.updateMeshData("color", @[newVec4f(0, 0), newVec4f(1, 0), newVec4f(1, 1), newVec4f(0, 1)])
   result = newEntity("root", {"mesh": Component(r)})
 
@@ -98,35 +133,19 @@
       vertexCode="""gl_Position = vec4(position + translate, 1.0); outcolor = color;""",
       fragmentCode="color = texture(my_little_texture, outcolor.xy) * 0.5 + outcolor * 0.5;",
     )
-  var renderPass = engine.gpuDevice.simpleForwardRenderPass(shaderConfiguration)
-  engine.setRenderer(renderPass)
+  engine.setRenderer({"my_material": shaderConfiguration}.toTable)
 
   # INIT SCENES
   var scenes = [
-    newScene("simple", scene_simple()),
-    newScene("different mesh types", scene_different_mesh_types()),
-    newScene("primitives", scene_primitives()),
-    newScene("flag", scene_flag()),
+    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=""),
   ]
-  var sampler = DefaultSampler()
-  sampler.magnification = VK_FILTER_NEAREST
-  sampler.minification = VK_FILTER_NEAREST
+
   for scene in scenes.mitems:
     scene.addShaderGlobal("time", 0.0'f32)
-    let (R, W) = ([255'u8, 0'u8, 0'u8, 255'u8], [255'u8, 255'u8, 255'u8, 255'u8])
-    scene.addMaterial(Material(
-      name: "my_material",
-      textures: {
-        "my_little_texture": Texture(image: Image(width: 5, height: 5, imagedata: @[
-        R, R, R, R, R,
-        R, R, W, R, R,
-        R, W, W, W, R,
-        R, R, W, R, R,
-        R, R, R, R, R,
-        ]), sampler: sampler)
-      }.toTable
-    ))
-    engine.addScene(scene, vertexInput, samplers, transformAttribute="", materialIndexAttribute="")
+    engine.addScene(scene)
 
   # MAINLOOP
   echo "Setup successfull, start rendering"