changeset 329:69e18f69713b

add: scene/shader compatability check, fix collision code to work with new APIs
author Sam <sam@basx.dev>
date Tue, 29 Aug 2023 00:01:13 +0700
parents 8d0ffcacc7e3
children 04531bec3583
files src/semicongine/collision.nim src/semicongine/core/gpu_data.nim src/semicongine/mesh.nim src/semicongine/renderer.nim src/semicongine/scene.nim src/semicongine/vulkan/pipeline.nim tests/test_collision.nim tests/test_vulkan_wrapper.nim
diffstat 8 files changed, 229 insertions(+), 165 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/collision.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/src/semicongine/collision.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -1,42 +1,44 @@
-import std/sequtils
-
 import ./core
 
 const MAX_COLLISON_DETECTION_ITERATIONS = 20
 const MAX_COLLISON_POINT_CALCULATION_ITERATIONS = 20
 
 type
-  HitBox* = object
-    transform*: Mat4
-  HitSphere* = object
-    transform*: Mat4
-    radius*: float32
+  ColliderType* = enum
+    Box, Sphere
+  Collider* = object
+    transform*: Mat4 = Unit4F32
+    case theType*: ColliderType
+      of Box: discard
+      of Sphere: radius*: float32
 
 func between(value, b1, b2: float32): bool =
   min(b1, b2) <= value and value <= max(b1, b2)
 
-func contains*(hitbox: HitBox, x: Vec3f): bool =
+func contains*(collider: Collider, x: Vec3f): bool =
   # from https://math.stackexchange.com/questions/1472049/check-if-a-point-is-inside-a-rectangular-shaped-area-3d
-  let
-    t = hitbox.transform
-    P1 = t * newVec3f(0, 0, 0) # origin
-    P2 = t * Z
-    P4 = t * X
-    P5 = t * Y
-    u = (P1 - P4).cross(P1 - P5)
-    v = (P1 - P2).cross(P1 - P5)
-    w = (P1 - P2).cross(P1 - P4)
-    uP1 = u.dot(P1)
-    uP2 = u.dot(P2)
-    vP1 = v.dot(P1)
-    vP4 = v.dot(P4)
-    wP1 = w.dot(P1)
-    wP5 = w.dot(P5)
-    ux = u.dot(x)
-    vx = v.dot(x)
-    wx = w.dot(x)
-
-  result = ux.between(uP1, uP2) and vx.between(vP1, vP4) and wx.between(wP1, wP5)
+  case collider.theType:
+  of Box:
+    let
+      P1 = collider.transform * newVec3f(0, 0, 0) # origin
+      P2 = collider.transform * Z
+      P4 = collider.transform * X
+      P5 = collider.transform * Y
+      u = (P1 - P4).cross(P1 - P5)
+      v = (P1 - P2).cross(P1 - P5)
+      w = (P1 - P2).cross(P1 - P4)
+      uP1 = u.dot(P1)
+      uP2 = u.dot(P2)
+      vP1 = v.dot(P1)
+      vP4 = v.dot(P4)
+      wP1 = w.dot(P1)
+      wP5 = w.dot(P5)
+      ux = u.dot(x)
+      vx = v.dot(x)
+      wx = w.dot(x)
+    ux.between(uP1, uP2) and vx.between(vP1, vP4) and wx.between(wP1, wP5)
+  of Sphere:
+    (collider.transform * x).length < (collider.transform * newVec3f()).length
 
 # implementation of GJK, based on https://blog.winter.dev/2020/gjk-algorithm/
 
@@ -51,12 +53,7 @@
       maxDist = dist
       result = p
 
-func findFurthestPoint(hitsphere: HitSphere, direction: Vec3f): Vec3f =
-  let directionNormalizedToSphere = ((direction / direction.length) * hitsphere.radius)
-  return hitsphere.transform * directionNormalizedToSphere
-
-func findFurthestPoint(hitbox: HitBox, direction: Vec3f): Vec3f =
-  let transform = hitbox.transform
+func findFurthestPoint(transform: Mat4, direction: Vec3f): Vec3f =
   return findFurthestPoint(
     [
       transform * newVec3f(0, 0, 0),
@@ -70,8 +67,15 @@
     ],
     direction
   )
+func findFurthestPoint(collider: Collider, direction: Vec3f): Vec3f =
+  case collider.theType
+    of Sphere:
+      let directionNormalizedToSphere = ((direction / direction.length) * collider.radius)
+      collider.transform * directionNormalizedToSphere
+    of Box:
+      findFurthestPoint(collider.transform, direction)
 
-func supportPoint[A, B](a: A, b: B, direction: Vec3f): Vec3f =
+func supportPoint(a, b: Collider, direction: Vec3f): Vec3f =
   a.findFurthestPoint(direction) - b.findFurthestPoint(-direction)
 
 func sameDirection(direction: Vec3f, ao: Vec3f): bool =
@@ -191,7 +195,7 @@
   of 4: simplex.tetrahedron(direction)
   else: raise newException(Exception, "Error in simplex")
 
-func collisionPoint3D[A, B](simplex: var seq[Vec3f], a: A, b: B): tuple[normal: Vec3f, penetrationDepth: float32] =
+func collisionPoint3D(simplex: var seq[Vec3f], a, b: Collider): tuple[normal: Vec3f, penetrationDepth: float32] =
   var
     polytope = simplex
     faces = @[
@@ -263,7 +267,7 @@
   result = (normal: minNormal, penetrationDepth: minDistance + 0.001'f32)
 
 
-func collisionPoint2D*[A, B](polytopeIn: seq[Vec3f], a: A, b: B): tuple[normal: Vec2f, penetrationDepth: float32] =
+func collisionPoint2D(polytopeIn: seq[Vec3f], a, b: Collider): tuple[normal: Vec2f, penetrationDepth: float32] =
   var
     polytope = polytopeIn
     minIndex = 0
@@ -302,7 +306,7 @@
 
   result = (normal: minNormal, penetrationDepth: minDistance + 0.001'f32)
 
-func intersects*[A, B](a: A, b: B): bool =
+func intersects*(a, b: Collider): bool =
   var
     support = supportPoint(a, b, newVec3f(0.8153, -0.4239, 0.5786)) # just random initial vector
     simplex = newSeq[Vec3f]()
@@ -321,7 +325,7 @@
       direction[0] = 0.0001
     inc n
 
-func collision*[A, B](a: A, b: B): tuple[hasCollision: bool, normal: Vec3f, penetrationDepth: float32] =
+func collision*(a, b: Collider): tuple[hasCollision: bool, normal: Vec3f, penetrationDepth: float32] =
   var
     support = supportPoint(a, b, newVec3f(0.8153, -0.4239, 0.5786)) # just random initial vector
     simplex = newSeq[Vec3f]()
@@ -341,7 +345,7 @@
       direction[0] = 0.0001
     inc n
 
-func collision2D*[A, B](a: A, b: B): tuple[hasCollision: bool, normal: Vec2f, penetrationDepth: float32] =
+func collision2D*(a, b: Collider): tuple[hasCollision: bool, normal: Vec2f, penetrationDepth: float32] =
   var
     support = supportPoint(a, b, newVec3f(0.8153, -0.4239, 0)) # just random initial vector
     simplex = newSeq[Vec3f]()
@@ -361,7 +365,7 @@
       direction[0] = 0.0001
     inc n
 
-func calculateHitbox*(points: seq[Vec3f]): HitBox =
+func calculateCollider*(points: seq[Vec3f], theType: ColliderType): Collider =
   var
     minX = high(float32)
     maxX = low(float32)
@@ -369,6 +373,7 @@
     maxY = low(float32)
     minZ = high(float32)
     maxZ = low(float32)
+    center: Vec3f
 
   for p in points:
     minX = min(minX, p.x)
@@ -377,15 +382,17 @@
     maxY = max(maxY, p.y)
     minZ = min(minZ, p.z)
     maxZ = max(maxz, p.z)
+    center = center + p
+  center = center / float32(points.len)
 
   let
     scaleX = (maxX - minX)
     scaleY = (maxY - minY)
     scaleZ = (maxZ - minZ)
 
-  HitBox(transform: translate(minX, minY, minZ) * scale(scaleX, scaleY, scaleZ))
+  result = Collider(theType: theType, transform: translate(minX, minY, minZ) * scale(scaleX, scaleY, scaleZ))
 
-func calculateHitsphere*(points: seq[Vec3f]): HitSphere =
-  result = HitSphere()
-  for p in points:
-    result.radius = max(result.radius, p.length)
+  if theType == Sphere:
+    result.transform = translate(center)
+    for p in points:
+      result.radius = max(result.radius, (p - center).length)
--- a/src/semicongine/core/gpu_data.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/src/semicongine/core/gpu_data.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -54,7 +54,7 @@
     Mat4F64
     Sampler2D
   DataValue* = object
-    case thetype*: DataType
+    case theType*: DataType
     of Float32: float32: float32
     of Float64: float64: float64
     of Int8: int8: int8
@@ -100,7 +100,7 @@
     of Sampler2D: discard
   DataList* = object
     len*: int
-    case thetype*: DataType
+    case theType*: DataType
     of Float32: float32: ref seq[float32]
     of Float64: float64: ref seq[float64]
     of Int8: int8: ref seq[int8]
@@ -148,14 +148,14 @@
     PreferFastRead, PreferFastWrite
   ShaderAttribute* = object
     name*: string
-    thetype*: DataType
+    theType*: DataType
     arrayCount*: int
     perInstance*: bool
     noInterpolation: bool
     memoryPerformanceHint*: MemoryPerformanceHint
 
 func hash*(value: DataList): Hash =
-  case value.thetype
+  case value.theType
     of Float32: hash(cast[pointer](value.float32))
     of Float64: hash(cast[pointer](value.float64))
     of Int8: hash(cast[pointer](value.int8))
@@ -200,10 +200,10 @@
     of Mat4F64: hash(cast[pointer](value.mat4f64))
     of Sampler2D: raise newException(Exception, "hash not defined for Sampler2D")
 
-func `==`*(a, b: DataList): bool =
-  if a.thetype != b.thetype:
+func `==`*(a, b: DataList | DataValue): bool =
+  if a.theType != b.theType:
     return false
-  case a.thetype
+  case a.theType
     of Float32: return a.float32 == b.float32
     of Float64: return a.float64 == b.float64
     of Int8: return a.int8 == b.int8
@@ -258,15 +258,15 @@
     if attr.perInstance == false:
       result.add attr
 
-func numberOfVertexInputAttributeDescriptors*(thetype: DataType): int =
-  case thetype:
+func numberOfVertexInputAttributeDescriptors*(theType: DataType): int =
+  case theType:
     of Mat2F32, Mat2F64, Mat23F32, Mat23F64: 2
     of Mat32F32, Mat32F64, Mat3F32, Mat3F64, Mat34F32, Mat34F64: 3
     of Mat43F32, Mat43F64, Mat4F32, Mat4F64: 4
     else: 1
 
-func size*(thetype: DataType): int =
-  case thetype:
+func size*(theType: DataType): int =
+  case theType:
     of Float32: 4
     of Float64: 8
     of Int8: 1
@@ -313,22 +313,22 @@
 
 func size*(attribute: ShaderAttribute, perDescriptor=false): int =
   if perDescriptor:
-    attribute.thetype.size div attribute.thetype.numberOfVertexInputAttributeDescriptors
+    attribute.theType.size div attribute.theType.numberOfVertexInputAttributeDescriptors
   else:
     if attribute.arrayCount == 0:
-      attribute.thetype.size
+      attribute.theType.size
     else:
-      attribute.thetype.size * attribute.arrayCount
+      attribute.theType.size * attribute.arrayCount
 
-func size*(thetype: seq[ShaderAttribute]): int =
-  for attribute in thetype:
+func size*(theType: seq[ShaderAttribute]): int =
+  for attribute in theType:
     result += attribute.size
 
 func size*(value: DataValue): int =
-  value.thetype.size
+  value.theType.size
 
 func size*(value: DataList): int =
-  value.thetype.size * value.len
+  value.theType.size * value.len
 
 func getDataType*[T: GPUType|int|uint|float](): DataType =
   when T is float32: Float32
@@ -393,7 +393,7 @@
 ): auto =
   ShaderAttribute(
     name: name,
-    thetype: getDataType[T](),
+    theType: getDataType[T](),
     perInstance: perInstance,
     arrayCount: arrayCount,
     noInterpolation: noInterpolation,
@@ -504,12 +504,12 @@
   else: {. error: "Virtual datatype has no values" .}
 
 func toGPUValue*[T: GPUType](value: T): DataValue =
-  result = DataValue(thetype: getDataType[T]())
+  result = DataValue(theType: getDataType[T]())
   result.setValue(value)
 
-func newDataList*(thetype: DataType): DataList =
-  result = DataList(thetype: thetype)
-  case result.thetype
+func newDataList*(theType: DataType): DataList =
+  result = DataList(theType: theType)
+  case result.theType
     of Float32: result.float32 = new seq[float32]
     of Float64: result.float64 = new seq[float64]
     of Int8: result.int8 = new seq[int8]
@@ -615,8 +615,8 @@
   else: {. error: "Virtual datatype has no values" .}
 
 func getRawData*(value: DataValue): (pointer, int) =
-  result[1] = value.thetype.size
-  case value.thetype
+  result[1] = value.theType.size
+  case value.theType
     of Float32: result[0] = addr value.float32
     of Float64: result[0] = addr value.float64
     of Int8: result[0] = addr value.int8
@@ -664,8 +664,8 @@
 func getRawData*(value: DataList): (pointer, int) =
   if value.len == 0:
     return (nil, 0)
-  result[1] = value.thetype.size * value.len
-  case value.thetype
+  result[1] = value.theType.size * value.len
+  case value.theType
     of Float32: result[0] = addr value.float32[][0]
     of Float64: result[0] = addr value.float64[][0]
     of Int8: result[0] = addr value.int8[][0]
@@ -712,7 +712,7 @@
 
 func initData*(value: var DataList, len: int) =
   value.len = len
-  case value.thetype
+  case value.theType
     of Float32: value.float32[].setLen(len)
     of Float64: value.float64[].setLen(len)
     of Int8: value.int8[].setLen(len)
@@ -861,9 +861,9 @@
   else: {. error: "Virtual datatype has no values" .}
 
 func appendValues*(value: var DataList, data: DataList) =
-  assert value.thetype == data.thetype
+  assert value.theType == data.theType
   value.len += data.len
-  case value.thetype:
+  case value.theType:
   of Float32: value.float32[].add data.float32[]
   of Float64: value.float64[].add data.float64[]
   of Int8: value.int8[].add data.int8[]
@@ -909,9 +909,9 @@
   else: raise newException(Exception, &"Unsupported data type for GPU data:" )
 
 func appendValue*(value: var DataList, data: DataValue) =
-  assert value.thetype == data.thetype
+  assert value.theType == data.theType, &"appendValue expected {value.theType} but got {data.theType}"
   value.len += 1
-  case value.thetype:
+  case value.theType:
   of Float32: value.float32[].add data.float32
   of Float64: value.float64[].add data.float64
   of Int8: value.int8[].add data.int8
@@ -1053,11 +1053,11 @@
     Mat4F64: VK_FORMAT_R64G64B64A64_SFLOAT,
 }.toTable
 
-func getVkFormat*(thetype: DataType): VkFormat =
-  TYPEMAP[thetype]
+func getVkFormat*(theType: DataType): VkFormat =
+  TYPEMAP[theType]
 
 # from https://registry.khronos.org/vulkan/specs/1.3-extensions/html/chap15.html
-func nLocationSlots*(thetype: DataType): int =
+func nLocationSlots*(theType: DataType): int =
   #[
   single location:
     16-bit scalar and vector types, and
@@ -1066,7 +1066,7 @@
   two locations
     64-bit three- and four-component vectors
   ]#
-  case thetype:
+  case theType:
     of Float32: 1
     of Float64: 1
     of Int8: 1
@@ -1111,12 +1111,12 @@
     of Mat4F64: 2
     of Sampler2D: 1
 
-func glslType*(thetype: DataType): string =
+func glslType*(theType: DataType): string =
   # todo: likely not correct as we would need to enable some 
   # extensions somewhere (Vulkan/GLSL compiler?) to have 
   # everything work as intended. Or maybe the GPU driver does
   # some automagic conversion stuf..
-  case thetype:
+  case theType:
     of Float32: "float"
     of Float64: "double"
     of Int8, Int16, Int32, Int64: "int"
@@ -1162,9 +1162,9 @@
   for attribute in group:
     assert attribute.arrayCount == 0, "arrays not supported for shader vertex attributes"
     let flat = if attribute.noInterpolation: "flat " else: ""
-    result.add &"layout(location = {i}) {flat}in {attribute.thetype.glslType} {attribute.name};"
-    for j in 0 ..< attribute.thetype.numberOfVertexInputAttributeDescriptors:
-      i += attribute.thetype.nLocationSlots
+    result.add &"layout(location = {i}) {flat}in {attribute.theType.glslType} {attribute.name};"
+    for j in 0 ..< attribute.theType.numberOfVertexInputAttributeDescriptors:
+      i += attribute.theType.nLocationSlots
 
 func glslUniforms*(group: openArray[ShaderAttribute], blockName="Uniforms", binding: int): seq[string] =
   if group.len == 0:
@@ -1175,7 +1175,7 @@
     var arrayDecl = ""
     if attribute.arrayCount > 0:
       arrayDecl = &"[{attribute.arrayCount}]"
-    result.add(&"    {attribute.thetype.glslType} {attribute.name}{arrayDecl};")
+    result.add(&"    {attribute.theType.glslType} {attribute.name}{arrayDecl};")
   result.add(&"}} {blockName};")
 
 func glslSamplers*(group: openArray[ShaderAttribute], basebinding: int): seq[string] =
@@ -1186,7 +1186,7 @@
     var arrayDecl = ""
     if attribute.arrayCount > 0:
       arrayDecl = &"[{attribute.arrayCount}]"
-    result.add(&"layout(binding = {thebinding}) uniform {attribute.thetype.glslType} {attribute.name}{arrayDecl};")
+    result.add(&"layout(binding = {thebinding}) uniform {attribute.theType.glslType} {attribute.name}{arrayDecl};")
     inc thebinding
 
 func glslOutput*(group: openArray[ShaderAttribute]): seq[string] =
@@ -1196,5 +1196,5 @@
   for attribute in group:
     assert attribute.arrayCount == 0, "arrays not supported for outputs"
     let flat = if attribute.noInterpolation: "flat " else: ""
-    result.add &"layout(location = {i}) {flat}out {attribute.thetype.glslType} {attribute.name};"
+    result.add &"layout(location = {i}) {flat}out {attribute.theType.glslType} {attribute.name};"
     i += 1
--- a/src/semicongine/mesh.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/src/semicongine/mesh.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -30,12 +30,17 @@
     vertexData: Table[string, DataList]
     instanceData: Table[string, DataList]
     dirtyAttributes: seq[string]
-  Material* = ref object
+  Material* = object
     materialType*: string
     name*: string
     constants*: Table[string, DataValue]
     textures*: Table[string, Texture]
 
+let EMPTY_MATERIAL = Material(
+  materialType: "EMPTY MATERIAL",
+  name: "empty material"
+)
+
 func `$`*(mesh: Mesh): string =
   &"Mesh(vertexCount: {mesh.vertexCount}, vertexData: {mesh.vertexData.keys().toSeq()}, instanceData: {mesh.instanceData.keys().toSeq()}, indexType: {mesh.indexType})"
 
@@ -58,7 +63,7 @@
   mesh.vertexAttributes & mesh.instanceAttributes
 
 func hash*(material: Material): Hash =
-  hash(cast[pointer](material))
+  hash(material.name)
 
 func instanceCount*(mesh: Mesh): int =
   mesh.instanceTransforms.len
@@ -114,7 +119,7 @@
   uvs: openArray[Vec2f]=[],
   transform: Mat4=Unit4F32,
   instanceTransforms: openArray[Mat4]=[Unit4F32],
-  material: Material=nil,
+  material: Material=EMPTY_MATERIAL,
   autoResize=true,
 ): Mesh =
   assert colors.len == 0 or colors.len == positions.len
@@ -164,7 +169,7 @@
   uvs: openArray[Vec2f]=[],
   transform: Mat4=Unit4F32,
   instanceTransforms: openArray[Mat4]=[Unit4F32],
-  material: Material=nil,
+  material: Material=EMPTY_MATERIAL,
 ): Mesh =
   newMesh(
     positions=positions,
@@ -347,4 +352,4 @@
 
 func getCollisionPoints*(mesh: Mesh, positionAttribute="position"): seq[Vec3f] =
   for p in getAttribute[Vec3f](mesh, positionAttribute)[]:
-    result.add p
+    result.add mesh.transform * p
--- a/src/semicongine/renderer.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/src/semicongine/renderer.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -46,6 +46,12 @@
 func usesMaterialType(scene: Scene, materialType: string): bool =
   return scene.meshes.anyIt(it.material.materialType == materialType)
 
+func getPipelineForMaterialtype(renderer: Renderer, materialType: string): Option[Pipeline] =
+  for i in 0 ..< renderer.renderPass.subpasses.len:
+    for pipelineMaterialType, pipeline in renderer.renderPass.subpasses[i].pipelines.pairs:
+      if pipelineMaterialType == materialType:
+        return some(pipeline)
+
 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
   
@@ -78,8 +84,65 @@
       if scene.usesMaterialType(materialType):
         result.add pipeline.samplers
 
+func materialCompatibleWithPipeline(scene: Scene, material: Material, pipeline: Pipeline): (bool, string) =
+  for uniform in pipeline.uniforms:
+    if scene.shaderGlobals.contains(uniform.name):
+      if scene.shaderGlobals[uniform.name].theType != uniform.theType:
+        return (false, &"shader uniform needs type {uniform.theType} but scene global is of type {scene.shaderGlobals[uniform.name].theType}")
+    else:
+      var foundMatch = false
+      for name, constant in material.constants.pairs:
+        if name == uniform.name and constant.theType == uniform.theType:
+          foundMatch = true
+          break
+      if not foundMatch:
+        return (false, "shader uniform '{uniform.name}' was not found in scene globals or scene materials")
+  for sampler in pipeline.samplers:
+    var foundMatch = false
+    for name, value in material.textures:
+      if name == sampler.name:
+        foundMatch = true
+        break
+    if not foundMatch:
+      return (false, "Required texture for shader sampler '{sampler.name}' was not found in scene materials")
+
+  return (true, "")
+
+func meshCompatibleWithPipeline(scene: Scene, mesh: Mesh, pipeline: Pipeline): (bool, string) =
+  for input in pipeline.inputs:
+    if not (input.name in mesh.attributes):
+      return (false, &"Shader input '{input.name}' is not available for mesh '{mesh}'")
+    if input.theType != mesh.attributeType(input.name):
+      return (false, &"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 (false, &"Shader input '{input.name}' expects to be per instance, but mesh '{mesh}' has is not as instance attribute")
+
+  return materialCompatibleWithPipeline(scene, mesh.material, pipeline)
+
+func checkSceneIntegrity(renderer: Renderer, scene: Scene) =
+  var foundRenderableObject = false
+  var shaderTypes: seq[string]
+  for i in 0 ..< renderer.renderPass.subpasses.len:
+    for materialType, pipeline in renderer.renderPass.subpasses[i].pipelines.pairs:
+      shaderTypes.add materialType
+      for mesh in scene.meshes:
+        if mesh.material.materialType == materialType:
+          foundRenderableObject = true
+          let (error, message) = scene.meshCompatibleWithPipeline(mesh, pipeline)
+          if error:
+            raise newException(Exception, &"Mesh '{mesh}' not compatible with assigned pipeline ({materialType}) because: {message}")
+
+  if not foundRenderableObject:
+    var materialTypes: seq[string]
+    for mesh in scene.meshes:
+      if not materialTypes.contains(mesh.material.materialType):
+          materialTypes.add mesh.material.materialType
+    raise newException(Exception, &"Scene {scene.name} has been added but materials are not compatible with any registered shader: Materials in scene: {materialTypes}, registered shader-materialtypes: {shaderTypes}")
+
 proc setupDrawableBuffers*(renderer: var Renderer, scene: var Scene) =
   assert not (scene in renderer.scenedata)
+  renderer.checkSceneIntegrity(scene)
+
   const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment
 
   let
@@ -88,7 +151,7 @@
   var scenedata = SceneData()
 
   for mesh in scene.meshes:
-    if mesh.material != nil and not scenedata.materials.contains(mesh.material):
+    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):
@@ -101,7 +164,6 @@
       if inputAttr.name == TRANSFORMATTRIBUTE:
         mesh.initInstanceAttribute(inputAttr.name, inputAttr.thetype)
       elif inputAttr.name == MATERIALINDEXATTRIBUTE:
-        assert mesh.material != nil, "Missing material specification for mesh. Set material attribute on mesh"
         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})")
@@ -178,9 +240,10 @@
     var offsets: Table[VkPipeline, seq[(string, MemoryPerformanceHint, int)]]
     for subpass_i in 0 ..< renderer.renderPass.subpasses.len:
       for materialType, pipeline in renderer.renderPass.subpasses[subpass_i].pipelines.pairs:
-        offsets[pipeline.vk] = newSeq[(string, MemoryPerformanceHint, int)]()
-        for attribute in pipeline.inputs:
-          offsets[pipeline.vk].add (attribute.name, attribute.memoryPerformanceHint, scenedata.vertexBufferOffsets[(meshIndex, attribute.name)])
+        if scene.usesMaterialType(materialType):
+          offsets[pipeline.vk] = newSeq[(string, MemoryPerformanceHint, int)]()
+          for attribute in pipeline.inputs:
+            offsets[pipeline.vk].add (attribute.name, attribute.memoryPerformanceHint, scenedata.vertexBufferOffsets[(meshIndex, attribute.name)])
 
     # create drawables
     let indexed = mesh.indexType != MeshIndexType.None
@@ -274,12 +337,19 @@
         assert renderer.scenedata[scene].uniformBuffers[pipeline.vk][renderer.swapchain.currentInFlight].vk.valid
         var offset = 0
         for uniform in pipeline.uniforms:
-          if not scene.shaderGlobals.hasKey(uniform.name):
-            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()
+          var value = newDataList(thetype=uniform.thetype)
+          if scene.shaderGlobals.hasKey(uniform.name):
+            assert scene.shaderGlobals[uniform.name].thetype == value.thetype
+            value = scene.shaderGlobals[uniform.name]
+          else:
+            for mat in renderer.scenedata[scene].materials:
+                for name, materialConstant in mat.constants.pairs:
+                  if uniform.name == name:
+                    value.appendValue(materialConstant)
+          if value.len == 0:
+            raise newException(Exception, &"Uniform '{uniform.name}' not found in scene shaderGlobals or materials")
+          debug &"Update uniform {uniform.name}"
+          let (pdata, size) = value.getRawData()
           renderer.scenedata[scene].uniformBuffers[pipeline.vk][renderer.swapchain.currentInFlight].setData(pdata, size, offset)
           offset += size
 
@@ -315,7 +385,7 @@
         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, meshIndex) in renderer.scenedata[scene].drawables:
-          if scene.meshes[meshIndex].material != nil and scene.meshes[meshIndex].material.materialType == materialType:
+          if scene.meshes[meshIndex].material.materialType == materialType:
             drawable.draw(commandBuffer, vertexBuffers=renderer.scenedata[scene].vertexBuffers, indexBuffer=renderer.scenedata[scene].indexBuffer, pipeline.vk)
 
     if i < renderer.renderPass.subpasses.len - 1:
--- a/src/semicongine/scene.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/src/semicongine/scene.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -1,11 +1,7 @@
-import std/strformat
-import std/strutils
 import std/tables
 import std/hashes
-import std/typetraits
 
 import ./core
-import ./animation
 import ./mesh
 
 type
--- a/src/semicongine/vulkan/pipeline.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/src/semicongine/vulkan/pipeline.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -14,7 +14,7 @@
     device*: Device
     vk*: VkPipeline
     layout*: VkPipelineLayout
-    shaderConfiguration: ShaderConfiguration
+    shaderConfiguration*: ShaderConfiguration
     shaderModules*: (ShaderModule, ShaderModule)
     descriptorSetLayout*: DescriptorSetLayout
 
--- a/tests/test_collision.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/tests/test_collision.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -1,67 +1,54 @@
+import std/tables
+
 import semicongine
 
-
 proc main() =
-  var scene = newScene("main", root=newEntity("rect"))
+  var scene = Scene(name: "main")
 
-  var obj1 = newEntity("Obj1", {
-    "mesh": Component(rect(color="f00f")),
-    "hitbox": Component(HitBox(transform: translate3d(-0.5, -0.5, -0.5)))
-  })
-  var obj2 = newEntity("Obj2", {
-    "mesh": Component(rect()),
-    "hitbox": Component(HitBox(transform: translate3d(-0.5, -0.5, -0.5)))
-  })
-  var obj3 = newEntity("Obj3", {
-    "mesh": Component(circle(color="0f0f")),
-    "hitbox": Component(HitSphere(radius: 0.5))
-  })
-  
-  scene.root.add obj2
-  scene.root.add obj1
-  scene.root.add obj3
-  obj1.transform = scale3d(0.8, 0.8)
-  obj3.transform = scale3d(0.1, 0.1)
+  scene.meshes.add rect(color="f00f")
+  scene.meshes.add rect()
+  scene.meshes.add circle(color="0f0f")
+  scene.meshes[1].transform = scale(0.8, 0.8)
+  scene.meshes[2].transform = scale(0.1, 0.1)
+  scene.addShaderGlobal("perspective", Unit4F32)
 
   const
-    vertexInput = @[
-      attr[Mat4]("transform", memoryPerformanceHint=PreferFastRead, perInstance=true),
-      attr[Vec3f]("position", memoryPerformanceHint=PreferFastRead),
-      attr[Vec4f]("color", memoryPerformanceHint=PreferFastRead),
-    ]
-    intermediate = @[attr[Vec4f]("colorout"),]
-    uniforms = @[attr[Mat4]("perspective")]
-    fragOutput = @[attr[Vec4f]("fragcolor")]
-    (vertexCode, fragmentCode) = compileVertexFragmentShaderSet(
-      inputs=vertexInput,
-      intermediate=intermediate,
-      outputs=fragOutput,
-      uniforms=uniforms,
+    shaderConfiguration = createShaderConfiguration(
+      inputs=[
+        attr[Mat4]("transform", memoryPerformanceHint=PreferFastRead, perInstance=true),
+        attr[Vec3f]("position", memoryPerformanceHint=PreferFastRead),
+        attr[Vec4f]("color", memoryPerformanceHint=PreferFastRead),
+      ],
+      intermediates=[attr[Vec4f]("colorout")],
+      uniforms=[attr[Mat4]("perspective")],
+      outputs=[attr[Vec4f]("fragcolor")],
       vertexCode="""gl_Position = vec4(position, 1.0) * (transform * Uniforms.perspective); colorout = color;""",
       fragmentCode="""fragcolor = colorout;""",
     )
 
   var engine = initEngine("Test collisions")
-  engine.setRenderer(engine.gpuDevice.simpleForwardRenderPass(vertexCode, fragmentCode))
-  engine.addScene(scene, vertexInput, @[], materialIndexAttribute="")
-  scene.addShaderGlobal("perspective", Unit4F32)
+
+  engine.initRenderer({"": shaderConfiguration}.toTable)
+  engine.addScene(scene)
 
   while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
     if engine.windowWasResized():
       var winSize = engine.getWindow().size
       scene.setShaderGlobal("perspective", orthoWindowAspect(winSize[1] / winSize[0]))
-    if engine.keyIsDown(A): obj1.transform = obj1.transform * translate3d(-0.001,      0, 0)
-    if engine.keyIsDown(D): obj1.transform = obj1.transform * translate3d( 0.001,      0, 0)
-    if engine.keyIsDown(W): obj1.transform = obj1.transform * translate3d(     0, -0.001, 0)
-    if engine.keyIsDown(S): obj1.transform = obj1.transform * translate3d(     0,  0.001, 0)
-    if engine.keyIsDown(Q): obj1.transform = obj1.transform * rotate3d(-0.001, Z)
-    if engine.keyIsDown(Key.E): obj1.transform = obj1.transform * rotate3d( 0.001, Z)
+    if engine.keyIsDown(A): scene.meshes[0].transform = scene.meshes[0].transform * translate(-0.001,      0, 0)
+    if engine.keyIsDown(D): scene.meshes[0].transform = scene.meshes[0].transform * translate( 0.001,      0, 0)
+    if engine.keyIsDown(W): scene.meshes[0].transform = scene.meshes[0].transform * translate(     0, -0.001, 0)
+    if engine.keyIsDown(S): scene.meshes[0].transform = scene.meshes[0].transform * translate(     0,  0.001, 0)
+    if engine.keyIsDown(Q): scene.meshes[0].transform = scene.meshes[0].transform * rotate(-0.001, Z)
+    if engine.keyIsDown(Key.E): scene.meshes[0].transform = scene.meshes[0].transform * rotate( 0.001, Z)
 
-    if engine.keyIsDown(Key.Z): obj2.transform = obj2.transform * rotate3d(-0.001, Z)
-    if engine.keyIsDown(Key.X): obj2.transform = obj2.transform * rotate3d( 0.001, Z)
-    if engine.keyIsDown(Key.C): obj2.transform = obj2.transform * translate3d(0, -0.001, 0)
-    if engine.keyIsDown(Key.V): obj2.transform = obj2.transform * translate3d(0,  0.001, 0)
-    echo intersects(obj1["hitbox", HitBox()], obj3["hitbox", HitSphere()])
+    if engine.keyIsDown(Key.Z): scene.meshes[1].transform = scene.meshes[1].transform * rotate(-0.001, Z)
+    if engine.keyIsDown(Key.X): scene.meshes[1].transform = scene.meshes[1].transform * rotate( 0.001, Z)
+    if engine.keyIsDown(Key.C): scene.meshes[1].transform = scene.meshes[1].transform * translate(0, -0.001, 0)
+    if engine.keyIsDown(Key.V): scene.meshes[1].transform = scene.meshes[1].transform * translate(0,  0.001, 0)
+    let hitbox = Collider(theType: Box, transform: scene.meshes[0].transform * translate(-0.5, -0.5))
+    let hitsphere = Collider(theType: Sphere, transform: scene.meshes[2].transform, radius: 0.5)
+    echo intersects(hitbox, hitsphere)
     engine.renderScene(scene)
   engine.destroy()
 
--- a/tests/test_vulkan_wrapper.nim	Fri Aug 25 01:14:04 2023 +0700
+++ b/tests/test_vulkan_wrapper.nim	Tue Aug 29 00:01:13 2023 +0700
@@ -13,7 +13,7 @@
   (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",
+    materialType: "textures_material",
     textures: {
       "my_little_texture": Texture(image: Image(width: 5, height: 5, imagedata: @[
       R, R, R, R, R,
@@ -26,7 +26,7 @@
   )
   mat2 = Material(
     name: "mat2",
-    materialType: "my_material",
+    materialType: "textures_material",
     textures: {
       "my_little_texture": Texture(image: Image(width: 5, height: 5, imagedata: @[
       R, W, R, W, R,
@@ -39,7 +39,7 @@
   )
   mat3 = Material(
     name: "mat3",
-    materialType: "my_special_material",
+    materialType: "plain",
     constants: {
       "color": toGPUValue(newVec4f(0, 1, 0, 1))
     }.toTable
@@ -192,8 +192,8 @@
       fragmentCode="color = outcolor;",
     )
   engine.initRenderer({
-    "my_material": shaderConfiguration1,
-    "my_special_material": shaderConfiguration2,
+    "textures_material": shaderConfiguration1,
+    "plain": shaderConfiguration2,
   }.toTable)
 
   # INIT SCENES
@@ -205,7 +205,6 @@
     Scene(name: "multimaterial", meshes: scene_multi_material()),
   ]
 
-  scenes[4].addShaderGlobal("color", newVec4f(1, 0, 0, 1))
   for scene in scenes.mitems:
     scene.addShaderGlobal("time", 0.0'f32)
     engine.addScene(scene)