changeset 369:807f111cc4f3

fix: setup of materials, still need to check with multiple materials in scene (maybe write new test?)
author Sam <sam@basx.dev>
date Wed, 22 Nov 2023 23:22:47 +0700
parents 51ee41c1d8ed
children fad6490fb728
files src/semicongine/core/imagetypes.nim src/semicongine/renderer.nim src/semicongine/resources.nim src/semicongine/resources/mesh.nim src/semicongine/text.nim
diffstat 5 files changed, 120 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/core/imagetypes.nim	Sat Oct 21 01:06:51 2023 +0700
+++ b/src/semicongine/core/imagetypes.nim	Wed Nov 22 23:22:47 2023 +0700
@@ -1,3 +1,5 @@
+import std/strformat
+
 import ./vulkanapi
 import ./vector
 
@@ -22,6 +24,12 @@
 converter toRGBA*(p: Pixel): Vec4f =
   newVec4f(float32(p[0]) / 255'f32, float32(p[1]) / 255'f32, float32(p[2]) / 255'f32, float32(p[3]) / 255'f32)
 
+proc `$`*(image: Image): string =
+  &"{image.width}x{image.height}"
+
+proc `$`*(texture: Texture): string =
+  &"{texture.name} {texture.image}"
+
 proc `[]`*(image: Image, x, y: int): Pixel =
   assert x < image.width
   assert y < image.height
@@ -50,14 +58,14 @@
       for x in 0 ..< width:
         result[x, y] = fill
 
-let INVALID_TEXTURE* = Texture(image: newImage(1, 1, @[[255'u8, 0'u8, 255'u8, 255'u8]]), sampler: Sampler(
+let INVALID_TEXTURE* = Texture(name: "Invalid texture", image: newImage(1, 1, @[[255'u8, 0'u8, 255'u8, 255'u8]]), sampler: Sampler(
     magnification: VK_FILTER_NEAREST,
     minification: VK_FILTER_NEAREST,
     wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
     wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
   )
 )
-let EMPTY_TEXTURE* = Texture(image: newImage(1, 1, @[[255'u8, 255'u8, 255'u8, 255'u8]]), sampler: Sampler(
+let EMPTY_TEXTURE* = Texture(name: "Empty texture", image: newImage(1, 1, @[[255'u8, 255'u8, 255'u8, 255'u8]]), sampler: Sampler(
     magnification: VK_FILTER_NEAREST,
     minification: VK_FILTER_NEAREST,
     wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT,
--- a/src/semicongine/renderer.nim	Sat Oct 21 01:06:51 2023 +0700
+++ b/src/semicongine/renderer.nim	Wed Nov 22 23:22:47 2023 +0700
@@ -21,7 +21,8 @@
 import ./mesh
 import ./material
 
-const TRANSFORMATTRIBUTE = "transform"
+const TRANSFORM_ATTRIBUTE = "transform"
+const MATERIALINDEX_ATTRIBUTE = "materialIndex"
 const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment
 
 type
@@ -109,7 +110,7 @@
 
 func meshCompatibleWithPipeline(scene: Scene, mesh: Mesh, pipeline: Pipeline): (bool, string) =
   for input in pipeline.inputs:
-    if input.name == TRANSFORMATTRIBUTE: # will be populated automatically
+    if input.name == TRANSFORM_ATTRIBUTE: # will be populated automatically
       continue
     if not (input.name in mesh[].attributes):
       return (true, &"Shader input '{input.name}' is not available for mesh")
@@ -143,14 +144,31 @@
             raise newException(Exception, &"Mesh '{mesh}' not compatible with assigned pipeline ({materialType}) because: {message}")
 
   if not foundRenderableObject:
-    var matTypes: seq[string]
+    var matTypes: Table[string, MaterialType]
     for mesh in scene.meshes:
       if not matTypes.contains(mesh.material.name):
-          matTypes.add mesh.material.name
-    raise newException(Exception, &"Scene '{scene.name}' has been added but materials are not compatible with any registered shader: Materials in scene: {matTypes}, registered shader-materialtypes: {materialTypes}")
+          matTypes[mesh.material.name] = mesh.material.theType
+    assert false, &"Scene '{scene.name}' has been added but materials are not compatible with any registered shader: Materials in scene: {matTypes}, registered shader-materialtypes: {materialTypes}"
 
 proc setupDrawableBuffers*(renderer: var Renderer, scene: var Scene) =
   assert not (scene in renderer.scenedata)
+
+  # find all material data and group it by material type
+  var materials: Table[MaterialType, seq[MaterialData]]
+
+  for mesh in scene.meshes:
+    if not materials.contains(mesh.material.theType):
+      materials[mesh.material.theType] = @[]
+    if not materials[mesh.material.theType].contains(mesh.material):
+      materials[mesh.material.theType].add mesh.material
+
+  # automatically populate material and tranform attributes
+  for mesh in scene.meshes:
+    if not (TRANSFORM_ATTRIBUTE in mesh[].attributes):
+        mesh[].initInstanceAttribute(TRANSFORM_ATTRIBUTE, Unit4)
+    if not (MATERIALINDEX_ATTRIBUTE in mesh[].attributes):
+      mesh[].initInstanceAttribute(MATERIALINDEX_ATTRIBUTE, uint16(materials[mesh.material.theType].find(mesh.material)))
+
   renderer.checkSceneIntegrity(scene)
 
   let
@@ -158,6 +176,7 @@
     samplers = renderer.samplers(scene)
   var scenedata = SceneData()
 
+  # collect textures from mesh materials, add them to scenedata and upload to GPU
   for mesh in scene.meshes:
     if not scenedata.materials.contains(mesh.material):
       scenedata.materials.add mesh.material
@@ -170,6 +189,7 @@
               scenedata.textures[name] = @[]
             scenedata.textures[name].add renderer.device.uploadTexture(getValue[Texture](value, 0))
 
+  # collect textures from shader globals, add them to scenedata and upload to GPU
   for name, value in scene.shaderGlobals.pairs:
     if value.theType == TextureType:
       assert not scenedata.textures.contains(name) # should be handled by the above code
@@ -177,10 +197,11 @@
       for texture in getValues[Texture](value)[]:
         scenedata.textures[name].add renderer.device.uploadTexture(texture)
 
+ #[ this might be a bad idea, I think originaly just a work around
   # find all meshes, populate missing attribute values for shader
   for mesh in scene.meshes.mitems:
     for inputAttr in inputs:
-      if inputAttr.name == TRANSFORMATTRIBUTE:
+      if inputAttr.name == TRANSFORM_ATTRIBUTE:
         mesh[].initInstanceAttribute(inputAttr.name, inputAttr.thetype)
       elif not mesh[].attributes.contains(inputAttr.name):
         warn(&"Mesh is missing data for shader attribute {inputAttr.name}, auto-filling with empty values")
@@ -189,6 +210,7 @@
         else:
           mesh[].initVertexAttribute(inputAttr.name, inputAttr.thetype)
       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
@@ -336,8 +358,8 @@
   assert scene in renderer.scenedata
 
   for (drawable, mesh) in renderer.scenedata[scene].drawables.mitems:
-    if mesh[].attributes.contains(TRANSFORMATTRIBUTE):
-      mesh[].updateInstanceTransforms(TRANSFORMATTRIBUTE)
+    if mesh[].attributes.contains(TRANSFORM_ATTRIBUTE):
+      mesh[].updateInstanceTransforms(TRANSFORM_ATTRIBUTE)
     let attrs = (if forceAll: mesh[].attributes else: mesh[].dirtyAttributes)
     for attribute in attrs:
       renderer.refreshMeshAttributeData(scene, drawable, mesh, attribute)
--- a/src/semicongine/resources.nim	Sat Oct 21 01:06:51 2023 +0700
+++ b/src/semicongine/resources.nim	Wed Nov 22 23:22:47 2023 +0700
@@ -10,6 +10,7 @@
 import ./resources/mesh
 import ./resources/font
 import ./mesh
+import ./material
 
 export image
 export audio
@@ -133,11 +134,11 @@
   let defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=+[{]};:,<.>/? $!@#%^&*()\"'".toRunes()
   loadResource_intern(path).readTrueType(name, defaultCharset, color, resolution)
 
-proc loadMeshes*(path: string): seq[MeshTree] =
-  loadResource_intern(path).readglTF()
+proc loadMeshes*(path: string, defaultMaterial: MaterialType): seq[MeshTree] =
+  loadResource_intern(path).readglTF(defaultMaterial)
 
-proc loadFirstMesh*(path: string): Mesh =
-  loadResource_intern(path).readglTF()[0].toSeq[0]
+proc loadFirstMesh*(path: string, defaultMaterial: MaterialType): Mesh =
+  loadResource_intern(path).readglTF(defaultMaterial)[0].toSeq[0]
 
 proc modList*(): seq[string] =
   modList_intern()
--- a/src/semicongine/resources/mesh.nim	Sat Oct 21 01:06:51 2023 +0700
+++ b/src/semicongine/resources/mesh.nim	Wed Nov 22 23:22:47 2023 +0700
@@ -44,6 +44,17 @@
     33648: VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT,
     10497: VK_SAMPLER_ADDRESS_MODE_REPEAT
   }.toTable
+  GLTF_MATERIAL_MAPPING = {
+    "color": "baseColorFactor",
+    "emissiveColor": "emissiveFactor",
+    "metallic": "metallicFactor",
+    "roughness", "roughnessFactor",
+    "baseTexture": "baseColorTexture",
+    "metallicRoughnessTexture": "metallicRoughnessTexture",
+    "normalTexture": "normalTexture",
+    "occlusionTexture": "occlusionTexture",
+    "emissiveTexture": "emissiveTexture",
+  }.toTable
 
 proc getGPUType(accessor: JsonNode, attribute: string): DataType =
   # TODO: no full support for all datatypes that glTF may provide
@@ -136,6 +147,8 @@
   let textureNode = root["textures"][textureIndex]
   result.image = loadImage(root, textureNode["source"].getInt(), mainBuffer)
   result.name = root["images"][textureNode["source"].getInt()]["name"].getStr()
+  if result.name == "":
+    result.name = &"Texture{textureIndex}"
 
   if textureNode.hasKey("sampler"):
     let sampler = root["samplers"][textureNode["sampler"].getInt()]
@@ -149,64 +162,71 @@
       result.sampler.wrapModeT = SAMPLER_WRAP_MODE_MAP[sampler["wrapS"].getInt()]
 
 
-proc loadMaterial(root: JsonNode, materialNode: JsonNode, mainBuffer: seq[uint8]): MaterialData =
-  result = MaterialData(name: materialNode["name"].getStr())
+proc loadMaterial(root: JsonNode, materialNode: JsonNode, defaultMaterial: MaterialType, mainBuffer: seq[uint8]): MaterialData =
   let pbr = materialNode["pbrMetallicRoughness"]
+  var attributes: Table[string, DataList]
 
   # color
-  result.attributes["baseColorFactor"] = initDataList(thetype=Vec4F32)
-  if pbr.hasKey("baseColorFactor"):
-    setValue(result.attributes["baseColorFactor"], @[newVec4f(
-      pbr["baseColorFactor"][0].getFloat(),
-      pbr["baseColorFactor"][1].getFloat(),
-      pbr["baseColorFactor"][2].getFloat(),
-      pbr["baseColorFactor"][3].getFloat(),
-    )])
-  else:
-    setValue(result.attributes["baseColorFactor"], @[newVec4f(1, 1, 1, 1)])
+  if defaultMaterial.attributes.contains("color"):
+    attributes["color"] = initDataList(thetype=Vec4F32)
+    if pbr.hasKey(GLTF_MATERIAL_MAPPING["color"]):
+      setValue(attributes["color"], @[newVec4f(
+        pbr[GLTF_MATERIAL_MAPPING["color"]][0].getFloat(),
+        pbr[GLTF_MATERIAL_MAPPING["color"]][1].getFloat(),
+        pbr[GLTF_MATERIAL_MAPPING["color"]][2].getFloat(),
+        pbr[GLTF_MATERIAL_MAPPING["color"]][3].getFloat(),
+      )])
+    else:
+      setValue(attributes["color"], @[newVec4f(1, 1, 1, 1)])
 
-  # pbr material values
-  for factor in ["metallicFactor", "roughnessFactor"]:
-    result.attributes[factor] = initDataList(thetype=Float32)
-    if pbr.hasKey(factor):
-      setValue(result.attributes[factor], @[float32(pbr[factor].getFloat())])
-    else:
-      setValue(result.attributes[factor], @[0.5'f32])
+    # pbr material values
+    for factor in ["metallic", "roughness"]:
+      if defaultMaterial.attributes.contains(factor):
+        attributes[factor] = initDataList(thetype=Float32)
+        if pbr.hasKey(GLTF_MATERIAL_MAPPING[factor]):
+          setValue(attributes[factor], @[float32(pbr[GLTF_MATERIAL_MAPPING[factor]].getFloat())])
+        else:
+          setValue(attributes[factor], @[0.5'f32])
 
   # pbr material textures
-  for texture in ["baseColorTexture", "metallicRoughnessTexture"]:
-    result.attributes[texture] = initDataList(thetype=TextureType)
-    result.attributes[texture & "Index"] = initDataList(thetype=UInt8)
-    if pbr.hasKey(texture):
-      setValue(result.attributes[texture], @[loadTexture(root, pbr[texture]["index"].getInt(), mainBuffer)])
-      setValue(result.attributes[texture & "Index"], @[pbr[texture].getOrDefault("texCoord").getInt(0).uint8])
-    else:
-      setValue(result.attributes[texture & "Index"], @[EMPTY_TEXTURE])
-      setValue(result.attributes[texture & "Index"], @[0'u8])
+  for texture in ["baseTexture", "metallicRoughnessTexture"]:
+    if defaultMaterial.attributes.contains(texture):
+      attributes[texture] = initDataList(thetype=TextureType)
+      # attributes[texture & "Index"] = initDataList(thetype=UInt8)
+      if pbr.hasKey(GLTF_MATERIAL_MAPPING[texture]):
+        setValue(attributes[texture], @[loadTexture(root, pbr[GLTF_MATERIAL_MAPPING[texture]]["index"].getInt(), mainBuffer)])
+        # setValue(attributes[texture & "Index"], @[pbr[GLTF_MATERIAL_MAPPING[texture]].getOrDefault("texCoord").getInt(0).uint8])
+      else:
+        setValue(attributes[texture], @[EMPTY_TEXTURE])
+        # setValue(attributes[texture & "Index"], @[0'u8])
 
   # generic material textures
   for texture in ["normalTexture", "occlusionTexture", "emissiveTexture"]:
-    result.attributes[texture] = initDataList(thetype=TextureType)
-    result.attributes[texture & "Index"] = initDataList(thetype=UInt8)
-    if materialNode.hasKey(texture):
-      setValue(result.attributes[texture], @[loadTexture(root, materialNode[texture]["index"].getInt(), mainBuffer)])
-      setValue(result.attributes[texture & "Index"], @[materialNode[texture].getOrDefault("texCoord").getInt(0).uint8])
-    else:
-      setValue(result.attributes[texture], @[EMPTY_TEXTURE])
-      setValue(result.attributes[texture & "Index"], @[0'u8])
+    if defaultMaterial.attributes.contains(texture):
+      attributes[texture] = initDataList(thetype=TextureType)
+      # attributes[texture & "Index"] = initDataList(thetype=UInt8)
+      if materialNode.hasKey(GLTF_MATERIAL_MAPPING[texture]):
+        setValue(attributes[texture], @[loadTexture(root, materialNode[texture]["index"].getInt(), mainBuffer)])
+        # setValue(attributes[texture & "Index"], @[materialNode[texture].getOrDefault("texCoord").getInt(0).uint8])
+      else:
+        setValue(attributes[texture], @[EMPTY_TEXTURE])
+        # setValue(attributes[texture & "Index"], @[0'u8])
 
   # emissiv color
-  result.attributes["emissiveFactor"] = initDataList(thetype=Vec3F32)
-  if materialNode.hasKey("emissiveFactor"):
-    setValue(result.attributes["emissiveFactor"], @[newVec3f(
-      materialNode["emissiveFactor"][0].getFloat(),
-      materialNode["emissiveFactor"][1].getFloat(),
-      materialNode["emissiveFactor"][2].getFloat(),
-    )])
-  else:
-    setValue(result.attributes["emissiveFactor"], @[newVec3f(1'f32, 1'f32, 1'f32)])
+  if defaultMaterial.attributes.contains("emissiveColor"):
+    attributes["emissiveColor"] = initDataList(thetype=Vec3F32)
+    if materialNode.hasKey(GLTF_MATERIAL_MAPPING["emissiveColor"]):
+      setValue(attributes["emissiveColor"], @[newVec3f(
+        materialNode[GLTF_MATERIAL_MAPPING["emissiveColor"]][0].getFloat(),
+        materialNode[GLTF_MATERIAL_MAPPING["emissiveColor"]][1].getFloat(),
+        materialNode[GLTF_MATERIAL_MAPPING["emissiveColor"]][2].getFloat(),
+      )])
+    else:
+      setValue(attributes["emissiveColor"], @[newVec3f(1'f32, 1'f32, 1'f32)])
 
-proc loadMesh(meshname: string, root: JsonNode, primitiveNode: JsonNode, mainBuffer: seq[uint8]): Mesh =
+  result = initMaterialData(materialType=defaultMaterial, name=materialNode["name"].getStr(), attributes=attributes)
+
+proc loadMesh(meshname: string, root: JsonNode, primitiveNode: JsonNode, defaultMaterial: MaterialType, mainBuffer: seq[uint8]): Mesh =
   if primitiveNode.hasKey("mode") and primitiveNode["mode"].getInt() != 4:
     raise newException(Exception, "Currently only TRIANGLE mode is supported for geometry mode")
 
@@ -236,7 +256,7 @@
 
   if primitiveNode.hasKey("material"):
     let materialId = primitiveNode["material"].getInt()
-    result[].material = loadMaterial(root, root["materials"][materialId], mainBuffer)
+    result[].material = loadMaterial(root, root["materials"][materialId], defaultMaterial, mainBuffer)
   else:
     result[].material = DEFAULT_MATERIAL
 
@@ -263,13 +283,13 @@
         raise newException(Exception, &"Unsupported index data type: {data.thetype}")
   transform[Vec3f](result[], "position", scale(1, -1, 1))
 
-proc loadNode(root: JsonNode, node: JsonNode, mainBuffer: var seq[uint8]): MeshTree =
+proc loadNode(root: JsonNode, node: JsonNode, defaultMaterial: MaterialType, mainBuffer: var seq[uint8]): MeshTree =
   result = MeshTree()
   # mesh
   if node.hasKey("mesh"):
     let mesh = root["meshes"][node["mesh"].getInt()]
     for primitive in mesh["primitives"]:
-      result.children.add MeshTree(mesh: loadMesh(mesh["name"].getStr(), root, primitive, mainBuffer))
+      result.children.add MeshTree(mesh: loadMesh(mesh["name"].getStr(), root, primitive, defaultMaterial, mainBuffer))
 
   # transformation
   if node.hasKey("matrix"):
@@ -305,16 +325,16 @@
   # children
   if node.hasKey("children"):
     for childNode in node["children"]:
-      result.children.add loadNode(root, root["nodes"][childNode.getInt()], mainBuffer)
+      result.children.add loadNode(root, root["nodes"][childNode.getInt()], defaultMaterial, mainBuffer)
 
-proc loadMeshTree(root: JsonNode, scenenode: JsonNode, mainBuffer: var seq[uint8]): MeshTree =
+proc loadMeshTree(root: JsonNode, scenenode: JsonNode, defaultMaterial: MaterialType, mainBuffer: var seq[uint8]): MeshTree =
   result = MeshTree()
   for nodeId in scenenode["nodes"]:
-    result.children.add loadNode(root, root["nodes"][nodeId.getInt()], mainBuffer)
+    result.children.add loadNode(root, root["nodes"][nodeId.getInt()], defaultMaterial, mainBuffer)
   result.updateTransforms()
 
 
-proc readglTF*(stream: Stream): seq[MeshTree] =
+proc readglTF*(stream: Stream, defaultMaterial: MaterialType): seq[MeshTree] =
   var
     header: glTFHeader
     data: glTFData
@@ -344,4 +364,4 @@
   debug "Loading mesh: ", data.structuredContent.pretty
 
   for scenedata in data.structuredContent["scenes"]:
-    result.add data.structuredContent.loadMeshTree(scenedata, data.binaryBufferData)
+    result.add data.structuredContent.loadMeshTree(scenedata, defaultMaterial, data.binaryBufferData)
--- a/src/semicongine/text.nim	Sat Oct 21 01:06:51 2023 +0700
+++ b/src/semicongine/text.nim	Wed Nov 22 23:22:47 2023 +0700
@@ -115,6 +115,7 @@
   result.mesh[].renameAttribute("position", POSITION_ATTRIB)
   result.mesh[].renameAttribute("uv", UV_ATTRIB)
   result.mesh.material = MaterialData(
+    theType: TEXT_MATERIAL_TYPE,
     name: font.name & " text",
     attributes: {"fontAtlas": initDataList(@[font.fontAtlas])}.toTable,
   )