changeset 421:cc6a67473a52

fix: material handlinge, did: formatting, add: support for multi-material texts
author Sam <sam@basx.dev>
date Mon, 29 Jan 2024 00:21:16 +0700
parents 91e018270832
children a03b6db25282
files semicongine/core/constants.nim semicongine/material.nim semicongine/renderer.nim semicongine/resources.nim semicongine/resources/mesh.nim semicongine/scene.nim semicongine/text.nim
diffstat 7 files changed, 86 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/constants.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/core/constants.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -3,3 +3,4 @@
   ENGINENAME* = "semicongine"
   ENGINEVERSION* = "0.0.1"
   TRANSFORM_ATTRIB* = "transform"
+  MATERIALINDEX_ATTRIBUTE* = "materialIndex"
--- a/semicongine/material.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/material.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -1,7 +1,6 @@
 import std/tables
 import std/strformat
 import std/strutils
-import std/hashes
 
 
 import ./core
@@ -12,7 +11,7 @@
     vertexAttributes*: Table[string, DataType]
     instanceAttributes*: Table[string, DataType]
     attributes*: Table[string, DataType]
-  MaterialData* = ref object
+  MaterialData* = object
     theType*: MaterialType
     name*: string
     attributes: Table[string, DataList]
@@ -24,18 +23,6 @@
 proc hasMatchingAttribute*(material: MaterialData, attr: ShaderAttribute): bool =
   return material.attributes.contains(attr.name) and material.attributes[attr.name].theType == attr.theType
 
-proc hash*(materialType: MaterialType): Hash =
-  return hash(materialType.name)
-
-proc hash*(materialData: MaterialData): Hash =
-  return hash(materialData.name)
-
-proc `==`*(a, b: MaterialType): bool =
-  return a.name == b.name
-
-proc `==`*(a, b: MaterialData): bool =
-  return a.name == b.name
-
 template `[]`*(material: MaterialData, attributeName: string): DataList =
   material.attributes[attributeName]
 template `[]`*(material: MaterialData, attributeName: string, t: typedesc): ref seq[t] =
@@ -139,5 +126,5 @@
   var theName = name
   if theName == "":
     theName = &"material instance of '{theType}'"
-  initMaterialData(theType=theType, name=theName, attributes=attributes.toTable)
+  initMaterialData(theType = theType, name = theName, attributes = attributes.toTable)
 
--- a/semicongine/renderer.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/renderer.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -21,7 +21,6 @@
 import ./mesh
 import ./material
 
-const MATERIALINDEX_ATTRIBUTE = "materialIndex"
 const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment
 
 type
@@ -29,7 +28,7 @@
     drawables*: seq[tuple[drawable: Drawable, mesh: Mesh]]
     vertexBuffers*: Table[MemoryPerformanceHint, Buffer]
     indexBuffer*: Buffer
-    uniformBuffers*: Table[VkPipeline, seq[Buffer]] # one per frame-in-flight
+    uniformBuffers*: Table[VkPipeline, seq[Buffer]]                 # one per frame-in-flight
     textures*: Table[VkPipeline, Table[string, seq[VulkanTexture]]] # per frame-in-flight
     attributeLocation*: Table[string, MemoryPerformanceHint]
     vertexBufferOffsets*: Table[(Mesh, string), int]
@@ -44,11 +43,11 @@
     scenedata: Table[Scene, SceneData]
     emptyTexture: VulkanTexture
 
-proc initRenderer*(device: Device, shaders: openArray[(MaterialType, ShaderConfiguration)], clearColor=Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32]), backFaceCulling=true): Renderer =
+proc initRenderer*(device: Device, shaders: openArray[(MaterialType, ShaderConfiguration)], clearColor = Vec4f([0.8'f32, 0.8'f32, 0.8'f32, 1'f32]), backFaceCulling = true): Renderer =
   assert device.vk.valid
-  
+
   result.device = device
-  result.renderPass = device.simpleForwardRenderPass(shaders, clearColor=clearColor, backFaceCulling=backFaceCulling)
+  result.renderPass = device.simpleForwardRenderPass(shaders, clearColor = clearColor, backFaceCulling = backFaceCulling)
   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)
@@ -130,7 +129,7 @@
     var matTypes: Table[string, MaterialType]
     for mesh in scene.meshes:
       if not matTypes.contains(mesh.material.name):
-          matTypes[mesh.material.name] = mesh.material.theType
+        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) =
@@ -171,10 +170,10 @@
       indicesBufferSize += mesh[].indexSize
   if indicesBufferSize > 0:
     scenedata.indexBuffer = renderer.device.createBuffer(
-      size=indicesBufferSize,
-      usage=[VK_BUFFER_USAGE_INDEX_BUFFER_BIT],
-      requireMappable=false,
-      preferVRAM=true,
+      size = indicesBufferSize,
+      usage = [VK_BUFFER_USAGE_INDEX_BUFFER_BIT],
+      requireMappable = false,
+      preferVRAM = true,
     )
 
   # calculcate offsets for attributes in vertex buffers
@@ -196,10 +195,10 @@
   for memoryPerformanceHint, bufferSize in perLocationSizes.pairs:
     if bufferSize > 0:
       scenedata.vertexBuffers[memoryPerformanceHint] = renderer.device.createBuffer(
-        size=bufferSize,
-        usage=[VK_BUFFER_USAGE_VERTEX_BUFFER_BIT],
-        requireMappable=memoryPerformanceHint==PreferFastWrite,
-        preferVRAM=true,
+        size = bufferSize,
+        usage = [VK_BUFFER_USAGE_VERTEX_BUFFER_BIT],
+        requireMappable = memoryPerformanceHint == PreferFastWrite,
+        preferVRAM = true,
       )
 
   # calculate offset of each attribute for all meshes
@@ -276,7 +275,9 @@
                 scenedata.textures[shaderPipeline.vk][texture.name].add uploadedTextures[value[0]]
             assert foundTexture, &"No texture found in shaderGlobals or materials for '{texture.name}'"
           let nTextures = scenedata.textures[shaderPipeline.vk][texture.name].len
-          assert (texture.arrayCount == 0 and nTextures == 1) or texture.arrayCount == nTextures, &"Shader assigned to render '{materialType}' expected {texture.arrayCount} textures for '{texture.name}' but got {nTextures}"
+          assert (texture.arrayCount == 0 and nTextures == 1) or texture.arrayCount >= nTextures, &"Shader assigned to render '{materialType}' expected {texture.arrayCount} textures for '{texture.name}' but got {nTextures}"
+          if texture.arrayCount < nTextures:
+            warn &"Shader assigned to render '{materialType}' expected {texture.arrayCount} textures for '{texture.name}' but got {nTextures}"
 
         # gather uniform sizes
         var uniformBufferSize = 0
@@ -286,12 +287,12 @@
           scenedata.uniformBuffers[shaderPipeline.vk] = newSeq[Buffer]()
           for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
             scenedata.uniformBuffers[shaderPipeline.vk].add renderer.device.createBuffer(
-              size=uniformBufferSize,
-              usage=[VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT],
-              requireMappable=true,
-              preferVRAM=true,
+              size = uniformBufferSize,
+              usage = [VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT],
+              requireMappable = true,
+              preferVRAM = true,
             )
-            
+
         # setup descriptors
         var poolsizes = @[(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, renderer.swapchain.inFlightFrames)]
         if scenedata.textures[shaderPipeline.vk].len > 0:
@@ -299,15 +300,15 @@
           for textures in scenedata.textures[shaderPipeline.vk].values:
             textureCount += textures.len
           poolsizes.add (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, renderer.swapchain.inFlightFrames * textureCount * 2)
-      
+
         scenedata.descriptorPools[shaderPipeline.vk] = renderer.device.createDescriptorSetPool(poolsizes)
-    
+
         scenedata.descriptorSets[shaderPipeline.vk] = shaderPipeline.setupDescriptors(
           scenedata.descriptorPools[shaderPipeline.vk],
           scenedata.uniformBuffers.getOrDefault(shaderPipeline.vk, @[]),
           scenedata.textures[shaderPipeline.vk],
-          inFlightFrames=renderer.swapchain.inFlightFrames,
-          emptyTexture=renderer.emptyTexture,
+          inFlightFrames = renderer.swapchain.inFlightFrames,
+          emptyTexture = renderer.emptyTexture,
         )
         for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
           scenedata.descriptorSets[shaderPipeline.vk][frame_i].writeDescriptorSet()
@@ -327,7 +328,7 @@
     renderer.scenedata[scene].vertexBufferOffsets[(mesh, attribute)]
   )
 
-proc updateMeshData*(renderer: var Renderer, scene: var Scene, forceAll=false) =
+proc updateMeshData*(renderer: var Renderer, scene: var Scene, forceAll = false) =
   assert scene in renderer.scenedata
 
   for (drawable, mesh) in renderer.scenedata[scene].drawables.mitems:
@@ -339,7 +340,7 @@
       debug &"Update mesh attribute {attribute}"
     mesh[].clearDirtyAttributes()
 
-proc updateUniformData*(renderer: var Renderer, scene: var Scene, forceAll=false) =
+proc updateUniformData*(renderer: var Renderer, scene: var Scene, forceAll = false) =
   assert scene in renderer.scenedata
 
   let dirty = scene.dirtyShaderGlobals
@@ -383,8 +384,12 @@
                   value.appendValues(material[uniform.name])
                   foundValue = true
               assert foundValue, &"Uniform '{uniform.name}' not found in scene shaderGlobals or materials"
-            assert (uniform.arrayCount == 0 and value.len == 1) or value.len == uniform.arrayCount, &"Uniform '{uniform.name}' found has wrong length (shader declares {uniform.arrayCount} but shaderGlobals and materials provide {value.len})"
-            assert value.size == uniform.size, "During uniform update: gathered value has size {value.size} but uniform expects size {uniform.size}"
+            assert (uniform.arrayCount == 0 and value.len == 1) or value.len <= uniform.arrayCount, &"Uniform '{uniform.name}' found has wrong length (shader declares {uniform.arrayCount} but shaderGlobals and materials provide {value.len})"
+            if value.len <= uniform.arrayCount:
+              warn &"Uniform '{uniform.name}' found has short length (shader declares {uniform.arrayCount} but shaderGlobals and materials provide {value.len})"
+            assert value.size <= uniform.size, &"During uniform update: gathered value has size {value.size} but uniform expects size {uniform.size}"
+            if value.size <= uniform.size:
+              warn &"During uniform update: gathered value has size {value.size} but uniform expects size {uniform.size}"
             debug &"  update uniform {uniform.name} with value: {value}"
             # TODO: technically we would only need to update the uniform buffer of the current
             # frameInFlight (I think), but we don't track for which frame the shaderglobals are no longer dirty
@@ -425,7 +430,7 @@
         commandBuffer.vkCmdBindPipeline(renderer.renderPass.subpasses[i].pipelineBindPoint, shaderPipeline.vk)
         commandBuffer.vkCmdBindDescriptorSets(renderer.renderPass.subpasses[i].pipelineBindPoint, shaderPipeline.layout, 0, 1, addr(renderer.scenedata[scene].descriptorSets[shaderPipeline.vk][renderer.swapchain.currentInFlight].vk), 0, nil)
         for (drawable, mesh) in renderer.scenedata[scene].drawables.filterIt(it[1].visible and it[1].material.theType == materialType):
-          drawable.draw(commandBuffer, vertexBuffers=renderer.scenedata[scene].vertexBuffers, indexBuffer=renderer.scenedata[scene].indexBuffer, shaderPipeline.vk)
+          drawable.draw(commandBuffer, vertexBuffers = renderer.scenedata[scene].vertexBuffers, indexBuffer = renderer.scenedata[scene].indexBuffer, shaderPipeline.vk)
 
     if i < renderer.renderPass.subpasses.len - 1:
       commandBuffer.vkCmdNextSubpass(VK_SUBPASS_CONTENTS_INLINE)
--- a/semicongine/resources.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/resources.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -45,12 +45,12 @@
     newFileStream(realpath, fmRead)
 
   proc modList_intern(): seq[string] =
-    for kind, file in walkDir(resourceRoot(), relative=true):
+    for kind, file in walkDir(resourceRoot(), relative = true):
       if kind == pcDir:
         result.add file
 
   iterator walkResources_intern(): string =
-    for file in walkDirRec(modRoot(), relative=true):
+    for file in walkDirRec(modRoot(), relative = true):
       yield file
 
 elif thebundletype == Zip:
@@ -71,7 +71,7 @@
     archive.close()
 
   proc modList_intern(): seq[string] =
-    for kind, file in walkDir(resourceRoot(), relative=true):
+    for kind, file in walkDir(resourceRoot(), relative = true):
       if kind == pcFile and file.endsWith(".zip"):
         result.add file[0 ..< ^4]
 
@@ -95,7 +95,7 @@
       if kind == pcDir:
         let modname = moddir.splitPath.tail
         result[modname] = Table[string, string]()
-        for resourcefile in walkDirRec(moddir, relative=true):
+        for resourcefile in walkDirRec(moddir, relative = true):
         # TODO: add Lempel–Ziv–Welch compression or something similar simple
           result[modname][resourcefile] = staticRead(joinPath(moddir, resourcefile))
   const bundledResources = loadResources()
@@ -115,7 +115,7 @@
 proc loadResource*(path: string): Stream =
   loadResource_intern(path)
 
-proc loadImage*(path: string): Image =
+proc loadImage*[T](path: string): Image[RGBAPixel] =
   if path.splitFile().ext.toLowerAscii == ".bmp":
     loadResource_intern(path).readBMP()
   elif path.splitFile().ext.toLowerAscii == ".png":
@@ -133,10 +133,10 @@
 
 proc loadFont*(
   path: string,
-  name="",
-  lineHeightPixels=80'f32,
-  additional_codepoints: openArray[Rune]=[],
-  charset=ASCII_CHARSET
+  name = "",
+  lineHeightPixels = 80'f32,
+  additional_codepoints: openArray[Rune] = [],
+  charset = ASCII_CHARSET
 ): Font =
   var thename = name
   if thename == "":
@@ -152,7 +152,7 @@
 proc modList*(): seq[string] =
   modList_intern()
 
-iterator walkResources*(dir=""): string =
+iterator walkResources*(dir = ""): string =
   for i in walkResources_intern():
     if i.startsWith(dir):
       yield i
--- a/semicongine/resources/mesh.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/resources/mesh.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -92,7 +92,7 @@
     of Float32: return Vec4F32
     else: raise newException(Exception, &"Unsupported data type for attribute '{attribute}': {componentType} {theType}")
 
-proc getBufferViewData(bufferView: JsonNode, mainBuffer: 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())
@@ -104,7 +104,7 @@
   copyMem(dstPointer, addr mainBuffer[bufferOffset], result.len)
 
 proc getAccessorData(root: JsonNode, accessor: JsonNode, mainBuffer: seq[uint8]): DataList =
-  result = initDataList(thetype=accessor.getGPUType("??"))
+  result = initDataList(thetype = accessor.getGPUType("??"))
   result.setLen(accessor["count"].getInt())
 
   let bufferView = root["bufferViews"][accessor["bufferView"].getInt()]
@@ -169,7 +169,7 @@
 
   # color
   if defaultMaterial.attributes.contains("color"):
-    attributes["color"] = initDataList(thetype=Vec4F32)
+    attributes["color"] = initDataList(thetype = Vec4F32)
     if pbr.hasKey(GLTF_MATERIAL_MAPPING["color"]):
       attributes["color"] = @[newVec4f(
         pbr[GLTF_MATERIAL_MAPPING["color"]][0].getFloat(),
@@ -183,7 +183,7 @@
     # pbr material values
     for factor in ["metallic", "roughness"]:
       if defaultMaterial.attributes.contains(factor):
-        attributes[factor] = initDataList(thetype=Float32)
+        attributes[factor] = initDataList(thetype = Float32)
         if pbr.hasKey(GLTF_MATERIAL_MAPPING[factor]):
           attributes[factor] = @[float32(pbr[GLTF_MATERIAL_MAPPING[factor]].getFloat())]
         else:
@@ -192,7 +192,7 @@
   # pbr material textures
   for texture in ["baseTexture", "metallicRoughnessTexture"]:
     if defaultMaterial.attributes.contains(texture):
-      attributes[texture] = initDataList(thetype=TextureType)
+      attributes[texture] = initDataList(thetype = TextureType)
       # attributes[texture & "Index"] = initDataList(thetype=UInt8)
       if pbr.hasKey(GLTF_MATERIAL_MAPPING[texture]):
         attributes[texture] = @[loadTexture(root, pbr[GLTF_MATERIAL_MAPPING[texture]]["index"].getInt(), mainBuffer)]
@@ -202,7 +202,7 @@
   # generic material textures
   for texture in ["normalTexture", "occlusionTexture", "emissiveTexture"]:
     if defaultMaterial.attributes.contains(texture):
-      attributes[texture] = initDataList(thetype=TextureType)
+      attributes[texture] = initDataList(thetype = TextureType)
       # attributes[texture & "Index"] = initDataList(thetype=UInt8)
       if materialNode.hasKey(GLTF_MATERIAL_MAPPING[texture]):
         attributes[texture] = @[loadTexture(root, materialNode[texture]["index"].getInt(), mainBuffer)]
@@ -211,7 +211,7 @@
 
   # emissiv color
   if defaultMaterial.attributes.contains("emissiveColor"):
-    attributes["emissiveColor"] = initDataList(thetype=Vec3F32)
+    attributes["emissiveColor"] = initDataList(thetype = Vec3F32)
     if materialNode.hasKey(GLTF_MATERIAL_MAPPING["emissiveColor"]):
       attributes["emissiveColor"] = @[newVec3f(
         materialNode[GLTF_MATERIAL_MAPPING["emissiveColor"]][0].getFloat(),
@@ -221,9 +221,9 @@
     else:
       attributes["emissiveColor"] = @[newVec3f(1'f32, 1'f32, 1'f32)]
 
-  result = initMaterialData(theType=defaultMaterial, name=materialNode["name"].getStr(), attributes=attributes)
+  result = initMaterialData(theType = defaultMaterial, name = materialNode["name"].getStr(), attributes = attributes)
 
-proc loadMesh(meshname: string, root: JsonNode, primitiveNode: JsonNode, defaultMaterial: MaterialType, mainBuffer: seq[uint8]): Mesh =
+proc loadMesh(meshname: string, root: JsonNode, primitiveNode: JsonNode, materials: seq[MaterialData], 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")
 
@@ -253,7 +253,7 @@
 
   if primitiveNode.hasKey("material"):
     let materialId = primitiveNode["material"].getInt()
-    result[].material = loadMaterial(root, root["materials"][materialId], defaultMaterial, mainBuffer)
+    result[].material = materials[materialId]
   else:
     result[].material = EMPTY_MATERIAL.initMaterialData()
 
@@ -281,13 +281,13 @@
   # TODO: getting from gltf to vulkan system is still messed up somehow, see other TODO
   transform[Vec3f](result[], "position", scale(1, -1, 1))
 
-proc loadNode(root: JsonNode, node: JsonNode, defaultMaterial: MaterialType, mainBuffer: var seq[uint8]): MeshTree =
+proc loadNode(root: JsonNode, node: JsonNode, materials: seq[MaterialData], 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, defaultMaterial, mainBuffer))
+      result.children.add MeshTree(mesh: loadMesh(mesh["name"].getStr(), root, primitive, materials, mainBuffer))
 
   # transformation
   if node.hasKey("matrix"):
@@ -318,18 +318,18 @@
         float32(node["scale"][1].getFloat()),
         float32(node["scale"][2].getFloat())
       )
-    result.transform =  t * r * s
-  result.transform =  scale(1, -1, 1) * result.transform
+    result.transform = t * r * s
+  result.transform = scale(1, -1, 1) * result.transform
 
   # children
   if node.hasKey("children"):
     for childNode in node["children"]:
-      result.children.add loadNode(root, root["nodes"][childNode.getInt()], defaultMaterial, mainBuffer)
+      result.children.add loadNode(root, root["nodes"][childNode.getInt()], materials, mainBuffer)
 
-proc loadMeshTree(root: JsonNode, scenenode: JsonNode, defaultMaterial: MaterialType, mainBuffer: var seq[uint8]): MeshTree =
+proc loadMeshTree(root: JsonNode, scenenode: JsonNode, materials: seq[MaterialData], mainBuffer: var seq[uint8]): MeshTree =
   result = MeshTree()
   for nodeId in scenenode["nodes"]:
-    result.children.add loadNode(root, root["nodes"][nodeId.getInt()], defaultMaterial, mainBuffer)
+    result.children.add loadNode(root, root["nodes"][nodeId.getInt()], materials, mainBuffer)
   # TODO: getting from gltf to vulkan system is still messed up somehow (i.e. not consistent for different files), see other TODO
   # result.transform = scale(1, -1, 1)
   result.updateTransforms()
@@ -364,5 +364,9 @@
 
   debug "Loading mesh: ", data.structuredContent.pretty
 
+  var materials: seq[MaterialData]
+  for materialnode in data.structuredContent["materials"]:
+    materials.add data.structuredContent.loadMaterial(materialnode, defaultMaterial, data.binaryBufferData)
+
   for scenedata in data.structuredContent["scenes"]:
-    result.add data.structuredContent.loadMeshTree(scenedata, defaultMaterial, data.binaryBufferData)
+    result.add data.structuredContent.loadMeshTree(scenedata, materials, data.binaryBufferData)
--- a/semicongine/scene.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/scene.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -45,7 +45,7 @@
 
 proc addShaderGlobal*[T](scene: var Scene, name: string, data: T) =
   assert not scene.loaded, &"Scene {scene.name} has already been loaded, cannot add shader values"
-  scene.shaderGlobals[name] = initDataList(thetype=getDataType[T]())
+  scene.shaderGlobals[name] = initDataList(thetype = getDataType[T]())
   scene.shaderGlobals[name] = @[data]
   scene.dirtyShaderGlobals.add name
 
--- a/semicongine/text.nim	Mon Jan 29 00:19:35 2024 +0700
+++ b/semicongine/text.nim	Mon Jan 29 00:21:16 2024 +0700
@@ -8,7 +8,9 @@
 import ./material
 import ./vulkan/shader
 
-const SHADER_ATTRIB_PREFIX = "semicon_text_"
+const
+  SHADER_ATTRIB_PREFIX = "semicon_text_"
+  MAX_TEXT_MATERIALS = 10
 var instanceCounter = 0
 
 type
@@ -50,13 +52,21 @@
       attr[Mat4](TRANSFORM_ATTRIB, memoryPerformanceHint = PreferFastWrite, perInstance = true),
       attr[Vec3f](POSITION_ATTRIB, memoryPerformanceHint = PreferFastWrite),
       attr[Vec2f](UV_ATTRIB, memoryPerformanceHint = PreferFastWrite),
+      attr[uint16](MATERIALINDEX_ATTRIBUTE, memoryPerformanceHint = PreferFastRead, perInstance = true),
     ],
-    intermediates = [attr[Vec2f]("uvFrag")],
+    intermediates = [
+      attr[Vec2f]("uvFrag"),
+      attr[uint16]("materialIndexOut", noInterpolation = true)
+    ],
     outputs = [attr[Vec4f]("color")],
-    uniforms = [attr[Vec4f]("color")],
-    samplers = [attr[Texture]("fontAtlas")],
-    vertexCode = &"""gl_Position = vec4({POSITION_ATTRIB}, 1.0) * {TRANSFORM_ATTRIB}; uvFrag = {UV_ATTRIB};""",
-    fragmentCode = &"""color = vec4(Uniforms.color.rgb, Uniforms.color.a * texture(fontAtlas, uvFrag).r);"""
+    uniforms = [attr[Vec4f]("color", arrayCount = MAX_TEXT_MATERIALS)],
+    samplers = [attr[Texture]("fontAtlas", arrayCount = MAX_TEXT_MATERIALS)],
+    vertexCode = &"""
+  gl_Position = vec4({POSITION_ATTRIB}, 1.0) * {TRANSFORM_ATTRIB};
+  uvFrag = {UV_ATTRIB};
+  materialIndexOut = {MATERIALINDEX_ATTRIBUTE};
+  """,
+    fragmentCode = &"""color = vec4(Uniforms.color[materialIndexOut].rgb, Uniforms.color[materialIndexOut].a * texture(fontAtlas[materialIndexOut], uvFrag).r);"""
   )
 
 func `$`*(text: Text): string =