# HG changeset patch # User sam # Date 1734196872 -25200 # Node ID dde74be11b49fceb28ec24d25eca0a6600af8747 # Parent f7bf7a5cc1e7b95eba021e2dcb46e33cb1cde600 did: continue a lot on glyph renderer diff -r f7bf7a5cc1e7 -r dde74be11b49 semicongine/contrib/algorithms/texture_packing.nim --- a/semicongine/contrib/algorithms/texture_packing.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/semicongine/contrib/algorithms/texture_packing.nim Sun Dec 15 00:21:12 2024 +0700 @@ -1,4 +1,5 @@ import std/algorithm +import std/strformat import ../../image diff -r f7bf7a5cc1e7 -r dde74be11b49 semicongine/image.nim --- a/semicongine/image.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/semicongine/image.nim Sun Dec 15 00:21:12 2024 +0700 @@ -2,6 +2,7 @@ import std/typetraits import std/streams import std/strutils +import std/strformat import ./core import ./resources diff -r f7bf7a5cc1e7 -r dde74be11b49 semicongine/rendering.nim --- a/semicongine/rendering.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/semicongine/rendering.nim Sun Dec 15 00:21:12 2024 +0700 @@ -186,6 +186,9 @@ proc `[]`*[T, S](a: GPUArray[T, S], i: SomeInteger): T = a.data[i] +proc len*[T, S](a: GPUArray[T, S]): int = + a.data.len + proc `[]=`*[T, S](a: var GPUArray[T, S], i: SomeInteger, value: T) = a.data[i] = value diff -r f7bf7a5cc1e7 -r dde74be11b49 semicongine/rendering/shaders.nim --- a/semicongine/rendering/shaders.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/semicongine/rendering/shaders.nim Sun Dec 15 00:21:12 2024 +0700 @@ -332,7 +332,8 @@ discard elif hasCustomPragma(value, PushConstant): assert pushConstants.len == 0, "Only one push constant value allowed" - assert value is object, "push constants need to be objects" + static: + assert value is object, "push constants need to be objects" pushConstants.add "layout( push_constant ) uniform constants" pushConstants.add "{" for constFieldName, constFieldValue in fieldPairs(value): diff -r f7bf7a5cc1e7 -r dde74be11b49 semicongine/text.nim --- a/semicongine/text.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/semicongine/text.nim Sun Dec 15 00:21:12 2024 +0700 @@ -28,7 +28,15 @@ leftBearing*: float32 advance*: float32 - FontObj* = object + GlyphData[N: static int] = object + pos: array[N, Vec4f] # vertex offsets to glyph center: [left, bottom, right, top] + uv: array[N, Vec4f] # [left, bottom, right, top] + + GlyphDescriptorSet*[N: static int] = object + fontAtlas*: Image[Gray] + glyphData*: GPUValue[GlyphData[N], StorageBuffer] + + FontObj*[N: static int] = object glyphs*: Table[Rune, GlyphInfo] fontAtlas*: Image[Gray] maxHeight*: int @@ -38,37 +46,10 @@ lineAdvance*: float32 capHeight*: float32 xHeight*: float32 - - Font = ref FontObj - - TextboxData = object - color: Vec4f - position: Vec3f - tmp: float32 - scale: Vec2f + descriptorSet*: DescriptorSetData[GlyphDescriptorSet[N]] + descriptorGlyphIndex: Table[Rune, uint16] - DefaultFontShader*[T] = object - position {.VertexAttribute.}: Vec3f - uv {.VertexAttribute.}: Vec2f - # TODO: maybe we can keep the uvs in a uniform buffer and just pass an index - fragmentUv {.Pass.}: Vec2f - color {.ShaderOutput.}: Vec4f - textbox {.PushConstant.}: TextboxData - descriptorSets {.DescriptorSet: 0.}: T - vertexCode* = - """void main() { - gl_Position = vec4(position * vec3(textbox.scale, 1) + textbox.position, 1.0); - fragmentUv = uv; -} """ - fragmentCode* = - """void main() { - float v = texture(fontAtlas, fragmentUv).r; - // CARFULL: This can lead to rough edges at times - if(v == 0) { - discard; - } - color = vec4(textbox.color.rgb, textbox.color.a * v); -}""" + Font*[N: static int] = ref FontObj[N] Glyphs* = object position*: GPUArray[Vec3f, VertexBufferMapped] @@ -76,19 +57,15 @@ scale*: GPUArray[float32, VertexBufferMapped] glyphIndex*: GPUArray[uint16, VertexBufferMapped] - GlyphData[N: static int] = object - pos: array[N, Vec4f] # [left, bottom, right, top] - uv: array[N, Vec4f] # [left, bottom, right, top] - - GlyphDescriptorSet*[N: static int] = object - fontAtlas*: Image[Gray] - glyphData*: GPUValue[GlyphData[N], StorageBuffer] + TextRendering* = object + aspectRatio*: float32 GlyphShader*[N: static int] = object position {.InstanceAttribute.}: Vec3f color {.InstanceAttribute.}: Vec4f scale {.InstanceAttribute.}: float32 glyphIndex {.InstanceAttribute.}: uint16 + textRendering {.PushConstant.}: TextRendering fragmentUv {.Pass.}: Vec2f fragmentColor {.PassFlat.}: Vec4f @@ -99,38 +76,33 @@ const int[6] indices = int[](0, 1, 2, 2, 3, 0); const int[4] i_x = int[](0, 0, 2, 2); const int[4] i_y = int[](1, 3, 3, 1); -// const float epsilon = 0.000000000000001; -const float epsilon = 0.1; +const float epsilon = 0.0000001; +// const float epsilon = 0.1; void main() { int vertexI = indices[gl_VertexIndex]; vec3 pos = vec3( glyphData.pos[glyphIndex][i_x[vertexI]] * scale, - glyphData.pos[glyphIndex][i_y[vertexI]] * scale, - 1 - (gl_InstanceIndex + 1) * epsilon + glyphData.pos[glyphIndex][i_y[vertexI]] * scale * textRendering.aspectRatio, + 1 - (gl_InstanceIndex + 1) * epsilon // allows overlapping glyphs to make proper depth test ); + gl_Position = vec4(pos + position, 1.0); vec2 uv = vec2(glyphData.uv[glyphIndex][i_x[vertexI]], glyphData.uv[glyphIndex][i_y[vertexI]]); - gl_Position = vec4(pos + position, 1.0); fragmentUv = uv; fragmentColor = color; } """ fragmentCode* = """void main() { - float v = texture(fontAtlas, fragmentUv).r; - // if(v == 0) { - // discard; - // } - outColor = vec4(fragmentColor.rgb, fragmentColor.a * v); - if (v == 0) { - outColor = vec4(1, 0, 1, 1); - } + float a = texture(fontAtlas, fragmentUv).r; + outColor = vec4(fragmentColor.rgb, fragmentColor.a * a); }""" -proc `=copy`(dest: var FontObj, source: FontObj) {.error.} +proc `=copy`[T: static int](dest: var FontObj[T], source: FontObj[T]) {.error.} +proc `=copy`(dest: var Glyphs, source: Glyphs) {.error.} include ./text/font -include ./text/textbox +#[ proc glyphDescriptorSet*( font: Font, maxGlyphs: static int ): (DescriptorSetData[GlyphDescriptorSet[maxGlyphs]], Table[Rune, uint16]) = @@ -144,11 +116,11 @@ var i = 0'u16 for rune, info in font.glyphs.pairs(): let - left = -info.offsetX - right = -info.offsetX + info.dimension.x - top = font.lineHeight + info.offsetY - bottom = font.lineHeight + info.offsetY - info.dimension.y - glyphData.pos[i] = vec4(left, bottom, right, top) * 0.005'f32 + left = info.leftBearing + info.offsetX + right = left + info.dimension.x + top = -info.offsetY + bottom = top - info.dimension.y + glyphData.pos[i] = vec4(left, bottom, right, top) * 0.001'f32 assert info.uvs[0].x == info.uvs[1].x, "Currently only axis aligned rectangles are allowed for info boxes in font texture maps" assert info.uvs[0].y == info.uvs[3].y, @@ -170,3 +142,40 @@ ), glyphTable, ) +]# + +func initGlyphs*(count: int): Glyphs = + result.position.data.setLen(count) + result.scale.data.setLen(count) + result.color.data.setLen(count) + result.glyphIndex.data.setLen(count) + +func set*( + glyphs: var Glyphs, + font: FontObj, + text: seq[Rune], + position: Vec3f, + scale = 1'f32, + color = vec4(1, 1, 1, 1), +) = + assert text.len <= glyphs.position.len, + &"Set {text.len} but Glyphs-object only supports {glyphs.position.len}" + var cursor = position + for i in 0 ..< text.len: + glyphs.position[i] = cursor + glyphs.scale[i] = scale + glyphs.color[i] = color + glyphs.glyphIndex[i] = font.descriptorGlyphIndex[text[i]] + +type EMPTY = object +const EMPTYOBJECT = EMPTY() + +proc renderGlyphs*(commandBuffer: VkCommandBuffer, pipeline: Pipeline, glyphs: Glyphs) = + renderWithPushConstant( + commandbuffer, + pipeline, + EMPTYOBJECT, + glyphs, + pushConstant = TextRendering(aspectRatio: getAspectRatio()), + fixedVertexCount = 6, + ) diff -r f7bf7a5cc1e7 -r dde74be11b49 semicongine/text/font.nim --- a/semicongine/text/font.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/semicongine/text/font.nim Sun Dec 15 00:21:12 2024 +0700 @@ -44,16 +44,19 @@ info: ptr stbtt_fontinfo, ascent, descent, lineGap: ptr cint ) {.importc, nodecl.} -proc readTrueType( +proc readTrueType[N: static int]( stream: Stream, name: string, codePoints: seq[Rune], lineHeightPixels: float32 -): Font = +): Font[N] = + assert codePoints.len <= N, + "asked for " & $codePoints.len & " glyphs but shader is only configured for " & $N + var indata = stream.readAll() fontinfo: stbtt_fontinfo if stbtt_InitFont(addr fontinfo, indata.ToCPointer, 0) == 0: raise newException(Exception, "An error occured while loading font file") - result = Font( + result = Font[N]( fontscale: float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels))) ) @@ -134,7 +137,7 @@ ], offsetX: float32(offsetX[codePoint]), offsetY: float32(offsetY[codePoint]), - leftBearing: float32(leftBearing), + leftBearing: float32(leftBearing) * result.fontscale, advance: float32(advance), ) @@ -146,15 +149,47 @@ ) ) * result.fontscale -proc loadFont*( +proc loadFont*[N: static int]( path: string, lineHeightPixels = 80'f32, additional_codepoints: openArray[Rune] = [], charset = ASCII_CHARSET, package = DEFAULT_PACKAGE, -): Font = - loadResource_intern(path, package = package).readTrueType( - path.splitFile().name, charset & additional_codepoints.toSeq, lineHeightPixels +): Font[N] = + result = readTrueType[N]( + loadResource_intern(path, package = package), + path.splitFile().name, + charset & additional_codepoints.toSeq, + lineHeightPixels, + ) + + var glyphData = GlyphData[N]() + + var i = 0'u16 + for rune, info in result.glyphs.pairs(): + let + left = info.leftBearing + info.offsetX + right = left + info.dimension.x + top = -info.offsetY + bottom = top - info.dimension.y + glyphData.pos[i] = vec4(left, bottom, right, top) * 0.001'f32 + assert info.uvs[0].x == info.uvs[1].x, + "Currently only axis aligned rectangles are allowed for info boxes in font texture maps" + assert info.uvs[0].y == info.uvs[3].y, + "Currently only axis aligned rectangles are allowed for info boxes in font texture maps" + assert info.uvs[2].x == info.uvs[3].x, + "Currently only axis aligned rectangles are allowed for info boxes in font texture maps" + assert info.uvs[1].y == info.uvs[2].y, + "Currently only axis aligned rectangles are allowed for info boxes in font texture maps" + glyphData.uv[i] = vec4(info.uvs[0].x, info.uvs[0].y, info.uvs[2].x, info.uvs[2].y) + result.descriptorGlyphIndex[rune] = i + inc i + + result.descriptorSet = asDescriptorSetData( + GlyphDescriptorSet[N]( + fontAtlas: result.fontAtlas.copy(), + glyphData: asGPUValue(glyphData, StorageBuffer), + ) ) func textWidth*(theText: seq[Rune] | string, font: FontObj): float32 = diff -r f7bf7a5cc1e7 -r dde74be11b49 tests/test_text.nim --- a/tests/test_text.nim Sat Dec 14 17:17:51 2024 +0700 +++ b/tests/test_text.nim Sun Dec 15 00:21:12 2024 +0700 @@ -16,60 +16,28 @@ type EMPTY = object -const N_GLYPHS = 200 +const MAX_GLYPHS = 200 proc test_01_static_label_new(time: float32) = # var font = loadFont("Overhaul.ttf", lineHeightPixels = 160) - var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 160) + var font = loadFont[MAX_GLYPHS]("DejaVuSans.ttf", lineHeightPixels = 160) var renderdata = initRenderData() var pipeline = - createPipeline[GlyphShader[N_GLYPHS]](renderPass = vulkan.swapchain.renderPass) - var (ds, glyphtable) = glyphDescriptorSet(font, N_GLYPHS) - var glyphs = Glyphs( - position: asGPUArray( - [ - vec3(-1, 0, 0), - vec3(-0.6, 0, 0), - vec3(-0.3, 0, 0), - vec3(0, 0, 0), - vec3(0.3, 0, 0), - vec3(0.6, 0, 0), - ], - VertexBufferMapped, - ), - scale: asGPUArray([1'f32, 1'f32, 1'f32, 1'f32, 1'f32, 1'f32], VertexBufferMapped), - color: asGPUArray( - [ - vec4(1, 1, 0, 1), - vec4(0, 0, 1, 1), - vec4(1, 1, 1, 1), - vec4(1, 1, 0, 1), - vec4(0, 0, 1, 1), - vec4(1, 1, 1, 1), - ], - VertexBufferMapped, - ), - glyphIndex: asGPUArray( - [ - glyphtable[Rune('a')], - glyphtable[Rune('l')], - glyphtable[Rune('i')], - glyphtable[Rune('g')], - glyphtable[Rune('x')], - glyphtable[Rune('x')], - ], - VertexBufferMapped, - ), - ) + createPipeline[GlyphShader[MAX_GLYPHS]](renderPass = vulkan.swapchain.renderPass) + var glyphs = initGlyphs(1000) assignBuffers(renderdata, glyphs) - assignBuffers(renderdata, ds) - uploadImages(renderdata, ds) - initDescriptorSet(renderdata, pipeline.layout(0), ds) + assignBuffers(renderdata, font.descriptorSet) + uploadImages(renderdata, font.descriptorSet) + initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet) + + glyphs.set(font[], "semicongine".toRunes(), vec3()) + + glyphs.updateAllGPUBuffers(flush = true) var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: withNextFrame(framebuffer, commandbuffer): - bindDescriptorSet(commandbuffer, ds, 0, pipeline) + bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) withRenderPass( vulkan.swapchain.renderPass, framebuffer, @@ -79,24 +47,19 @@ vec4(0, 0, 0, 0), ): withPipeline(commandbuffer, pipeline): - render( - commandbuffer, - pipeline, - EMPTY(), - glyphs, - fixedVertexCount = 6, # fixedInstanceCount = 2, - ) + renderGlyphs(commandbuffer, pipeline, glyphs) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) destroyPipeline(pipeline) destroyRenderData(renderdata) +#[ proc test_01_static_label(time: float32) = - var font = loadFont("Overhaul.ttf", lineHeightPixels = 160) + var font = loadFont[MAX_GLYPHS]("Overhaul.ttf", lineHeightPixels = 160) var renderdata = initRenderData() var pipeline = - createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) + createPipeline[GlyphShader[MAX_GLYPHS]](renderPass = vulkan.swapchain.renderPass) var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas.copy())) uploadImages(renderdata, ds) @@ -126,13 +89,13 @@ destroyRenderData(renderdata) proc test_02_multiple_animated(time: float32) = - var font1 = loadFont("Overhaul.ttf", lineHeightPixels = 40) - var font2 = loadFont("Overhaul.ttf", lineHeightPixels = 160) - var font3 = loadFont("DejaVuSans.ttf", lineHeightPixels = 160) + var font1 = loadFont[MAX_GLYPHS]("Overhaul.ttf", lineHeightPixels = 40) + var font2 = loadFont[MAX_GLYPHS]("Overhaul.ttf", lineHeightPixels = 160) + var font3 = loadFont[MAX_GLYPHS]("DejaVuSans.ttf", lineHeightPixels = 160) var renderdata = initRenderData() var pipeline = - createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) + createPipeline[GlyphShader[MAX_GLYPHS]](renderPass = vulkan.swapchain.renderPass) var ds1 = asDescriptorSetData(FontDS(fontAtlas: font1.fontAtlas.copy())) uploadImages(renderdata, ds1) @@ -201,11 +164,11 @@ destroyRenderData(renderdata) proc test_03_layouting(time: float32) = - var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 40) + var font = loadFont[MAX_GLYPHS]("DejaVuSans.ttf", lineHeightPixels = 40) var renderdata = initRenderData() var pipeline = - createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) + createPipeline[GlyphShader[MAX_GLYPHS]](renderPass = vulkan.swapchain.renderPass) var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas.copy())) uploadImages(renderdata, ds) @@ -274,11 +237,11 @@ destroyRenderData(renderdata) proc test_04_lots_of_texts(time: float32) = - var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 160) + var font = loadFont[MAX_GLYPHS]("DejaVuSans.ttf", lineHeightPixels = 160) var renderdata = initRenderData() var pipeline = - createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) + createPipeline[GlyphShader[MAX_GLYPHS]](renderPass = vulkan.swapchain.renderPass) var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas.copy())) uploadImages(renderdata, ds) @@ -319,6 +282,7 @@ checkVkResult vkDeviceWaitIdle(vulkan.device) destroyPipeline(pipeline) destroyRenderData(renderdata) +]# when isMainModule: var time = 1000'f32