Mercurial > games > semicongine
changeset 882:5392cbd9db41
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 | 11a55a45aba2 |
children | 98b66663e07c |
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 =