Mercurial > games > semicongine
changeset 1407:56f927b89716
did: finally got typography right, still improving text-rendering API to cache text parts
author | sam <sam@basx.dev> |
---|---|
date | Sun, 22 Dec 2024 00:31:29 +0700 |
parents | aeb15aa9768c |
children | 17d960ff6a24 |
files | semicongine/text.nim semicongine/text/font.nim tests/test_text.nim |
diffstat | 3 files changed, 158 insertions(+), 120 deletions(-) [+] |
line wrap: on
line diff
--- a/semicongine/text.nim Sat Dec 21 19:32:59 2024 +0700 +++ b/semicongine/text.nim Sun Dec 22 00:31:29 2024 +0700 @@ -25,40 +25,51 @@ Center Right - GlyphQuad[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] + GlyphQuad[MaxGlyphs: static int] = object + pos: array[MaxGlyphs, Vec4f] + # vertex offsets to glyph center: [left, bottom, right, top] + uv: array[MaxGlyphs, Vec4f] # [left, bottom, right, top] - GlyphDescriptorSet*[N: static int] = object + GlyphDescriptorSet*[MaxGlyphs: static int] = object fontAtlas*: Image[Gray] - glyphquads*: GPUValue[GlyphQuad[N], StorageBuffer] + glyphquads*: GPUValue[GlyphQuad[MaxGlyphs], StorageBuffer] - FontObj*[N: static int] = object + FontObj*[MaxGlyphs: static int] = object advance*: Table[Rune, float32] kerning*: Table[(Rune, Rune), float32] lineAdvance*: float32 lineHeight*: float32 # like lineAdvance - lineGap ascent*: float32 # from baseline to highest glyph descent*: float32 # from baseline to highest glyph - descriptorSet*: DescriptorSetData[GlyphDescriptorSet[N]] + xHeight*: float32 # from baseline to height of lowercase x + descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]] descriptorGlyphIndex: Table[Rune, uint16] fallbackCharacter: Rune - Font*[N: static int] = ref FontObj[N] + Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs] + Text = object + bufferOffset: int + text: seq[Rune] + position: Vec3f = vec3() + alignment: TextAlignment = Left + anchor: Vec2f = vec2() + scale: float32 = 0 + color: Vec4f = vec4(1, 1, 1, 1) - Glyphs*[N: static int] = object + TextBuffer*[MaxGlyphs: static int] = object cursor: int - font*: Font[N] + font*: Font[MaxGlyphs] baseScale*: float32 position*: GPUArray[Vec3f, VertexBufferMapped] color*: GPUArray[Vec4f, VertexBufferMapped] scale*: GPUArray[float32, VertexBufferMapped] glyphIndex*: GPUArray[uint16, VertexBufferMapped] + texts: seq[Text] TextRendering* = object aspectRatio*: float32 - GlyphShader*[N: static int] = object + GlyphShader*[MaxGlyphs: static int] = object position {.InstanceAttribute.}: Vec3f color {.InstanceAttribute.}: Vec4f scale {.InstanceAttribute.}: float32 @@ -68,7 +79,7 @@ fragmentUv {.Pass.}: Vec2f fragmentColor {.PassFlat.}: Vec4f outColor {.ShaderOutput.}: Vec4f - glyphData {.DescriptorSet: 0.}: GlyphDescriptorSet[N] + glyphData {.DescriptorSet: 0.}: GlyphDescriptorSet[MaxGlyphs] vertexCode* = """ const int[6] indices = int[](0, 1, 2, 2, 3, 0); @@ -95,21 +106,27 @@ outColor = vec4(fragmentColor.rgb, fragmentColor.a * a); }""" -proc `=copy`[N: static int](dest: var FontObj[N], source: FontObj[N]) {.error.} -proc `=copy`[N: static int](dest: var Glyphs[N], source: Glyphs[N]) {.error.} +proc `=copy`[MaxGlyphs: static int]( + dest: var FontObj[MaxGlyphs], source: FontObj[MaxGlyphs] +) {.error.} + +proc `=copy`[MaxGlyphs: static int]( + dest: var TextBuffer[MaxGlyphs], source: TextBuffer[MaxGlyphs] +) {.error.} include ./text/font -func initGlyphs*[N: static int]( - font: Font[N], count: int, baseScale = 1'f32 -): Glyphs[N] = +func initTextBuffer*[MaxGlyphs: static int]( + font: Font[MaxGlyphs], maxCharacters: int, baseScale = 1'f32 +): TextBuffer[MaxGlyphs] = result.cursor = 0 result.font = font result.baseScale = baseScale - result.position.data.setLen(count) - result.scale.data.setLen(count) - result.color.data.setLen(count) - result.glyphIndex.data.setLen(count) + result.position.data.setLen(maxCharacters) + result.scale.data.setLen(maxCharacters) + result.color.data.setLen(maxCharacters) + result.glyphIndex.data.setLen(maxCharacters) + result.texts.setLen(maxCharacters) # waste a lot of memory? iterator splitLines(text: seq[Rune]): seq[Rune] = var current = newSeq[Rune]() @@ -140,36 +157,45 @@ return vec2(w, h) proc add*( - glyphs: var Glyphs, + textbuffer: var TextBuffer, text: seq[Rune], position: Vec3f, alignment: TextAlignment = Left, - anchor: Vec2f = vec2(-1, 1), + anchor: Vec2f = vec2(0, 0), scale: float32 = 1'f32, color: Vec4f = vec4(1, 1, 1, 1), ) = - ## Add text for rendering. - ## `position` is the display position, where as `(0, 0) is top-left and (1, 1) is bottom right. - ## The z-compontent goes from 0 (near plane) to 1 (far plane) and is usually just used for ordering layers - ## this should be called again after aspect ratio of window changes - ## Anchor is the anchor to use inside the text + ## This should be called again after aspect ratio of window changes + + assert text.len <= textbuffer.position.len, + &"Set {text.len} but TextBuffer-object only supports {textbuffer.position.len}" - assert text.len <= glyphs.position.len, - &"Set {text.len} but Glyphs-object only supports {glyphs.position.len}" + textbuffer.texts.add Text( + bufferOffset: textbuffer.cursor, + text: text, + position: position, + alignment: alignment, + anchor: anchor, + scale: scale, + color: color, + ) let - globalScale = scale * glyphs.baseScale - dim = textDimension(glyphs.font, text, globalScale) - baselineStart = vec2(0, glyphs.font.ascent * globalScale) - pos = position.xy - anchor * dim + baselineStart - # lineWidths need to be converted to NDC - lineWidths = splitLines(text).toSeq.mapIt(width(glyphs.font, it, globalScale)) - # also dimension must be in NDC - maxWidth = dim.x + globalScale = scale * textbuffer.baseScale + box = textDimension(textbuffer.font, text, globalScale) + xH = textbuffer.font.xHeight * globalScale + origin = vec3( + position.x - (anchor.x * 0.5 + 0.5) * box.x / getAspectRatio(), + position.y + (anchor.y * -0.5 + 0.5) * box.y - xH * 0.5 - + textbuffer.font.lineHeight * globalScale * 0.5, + position.z, + ) + lineWidths = splitLines(text).toSeq.mapIt(width(textbuffer.font, it, globalScale)) + maxWidth = box.x aratio = getAspectRatio() + # echo text, anchor var - origin = vec3(pos.x, pos.y, position.z) cursorPos = origin lineI = 0 @@ -177,9 +203,9 @@ of Left: cursorPos.x = origin.x of Center: - cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / 2) + cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5) of Right: - cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) * aratio * 2 + cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio for i in 0 ..< text.len: if text[i] == Rune('\n'): @@ -188,59 +214,66 @@ of Left: cursorPos.x = origin.x of Center: - cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / 2) + cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5) of Right: - cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) * aratio * 2 - cursorPos.y = cursorPos.y - glyphs.font.lineAdvance * globalScale + cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio + cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale else: if not text[i].isWhitespace(): - glyphs.position[glyphs.cursor] = cursorPos - glyphs.scale[glyphs.cursor] = globalScale - glyphs.color[glyphs.cursor] = color - if text[i] in glyphs.font.descriptorGlyphIndex: - glyphs.glyphIndex[glyphs.cursor] = glyphs.font.descriptorGlyphIndex[text[i]] + textbuffer.position[textbuffer.cursor] = cursorPos + textbuffer.scale[textbuffer.cursor] = globalScale + textbuffer.color[textbuffer.cursor] = color + if text[i] in textbuffer.font.descriptorGlyphIndex: + textbuffer.glyphIndex[textbuffer.cursor] = + textbuffer.font.descriptorGlyphIndex[text[i]] else: - glyphs.glyphIndex[glyphs.cursor] = - glyphs.font.descriptorGlyphIndex[glyphs.font.fallbackCharacter] - inc glyphs.cursor + textbuffer.glyphIndex[textbuffer.cursor] = + textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter] + inc textbuffer.cursor - if text[i] in glyphs.font.advance: - cursorPos.x = cursorPos.x + glyphs.font.advance[text[i]] * globalScale / aratio + if text[i] in textbuffer.font.advance: + cursorPos.x = + cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio else: cursorPos.x = cursorPos.x + - glyphs.font.advance[glyphs.font.fallbackCharacter] * globalScale / aratio + textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale / + aratio if i < text.len - 1: cursorPos.x = cursorPos.x + - glyphs.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale / + textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale / aratio proc add*( - glyphs: var Glyphs, + textbuffer: var TextBuffer, text: string, position: Vec3f, alignment: TextAlignment = Left, - anchor: Vec2f = vec2(0, 1), + anchor: Vec2f = vec2(0, 0), scale: float32 = 1'f32, color: Vec4f = vec4(1, 1, 1, 1), ) = - add(glyphs, text.toRunes, position, alignment, anchor, scale, color) + add(textbuffer, text.toRunes, position, alignment, anchor, scale, color) -proc reset*(glyphs: var Glyphs) = - glyphs.cursor = 0 +proc reset*(textbuffer: var TextBuffer) = + textbuffer.cursor = 0 + for i in 0 ..< textbuffer.texts.len: + textbuffer.texts[i] = default(Text) type EMPTY = object const EMPTYOBJECT = EMPTY() -proc renderGlyphs*(commandBuffer: VkCommandBuffer, pipeline: Pipeline, glyphs: Glyphs) = +proc renderTextBuffer*( + commandBuffer: VkCommandBuffer, pipeline: Pipeline, textbuffer: TextBuffer +) = renderWithPushConstant( commandbuffer, pipeline, EMPTYOBJECT, - glyphs, + textbuffer, pushConstant = TextRendering(aspectRatio: getAspectRatio()), fixedVertexCount = 6, - fixedInstanceCount = glyphs.cursor, + fixedInstanceCount = textbuffer.cursor, )
--- a/semicongine/text/font.nim Sat Dec 21 19:32:59 2024 +0700 +++ b/semicongine/text/font.nim Sun Dec 22 00:31:29 2024 +0700 @@ -26,7 +26,9 @@ width, height, xoff, yoff: ptr cint, ): cstring {.importc, nodecl.} -# proc stbtt_GetCodepointBitmapBox(info: ptr stbtt_fontinfo, codepoint: cint, scale_x, scale_y: cfloat, ix0, iy0, ix1, iy1: ptr cint) {.importc, nodecl.} +proc stbtt_GetCodepointBox( + info: ptr stbtt_fontinfo, codepoint: cint, x0, y0, x1, y1: ptr cint +): cint {.importc, nodecl.} proc stbtt_GetCodepointHMetrics( info: ptr stbtt_fontinfo, codepoint: cint, advance, leftBearing: ptr cint @@ -151,10 +153,15 @@ var ascent, descent, lineGap: cint stbtt_GetFontVMetrics(addr fi, addr ascent, addr descent, addr lineGap) result.lineAdvance = float32(ascent - descent + lineGap) * glyph2QuadScale - result.lineHeight = float32(ascent - descent) * glyph2QuadScale + result.lineHeight = float32(ascent - descent) * glyph2QuadScale # should be 1 result.ascent = float32(ascent) * glyph2QuadScale result.descent = float32(descent) * glyph2QuadScale + var x0, y0, x1, y1: cint + discard + stbtt_GetCodepointBox(addr fi, cint(Rune('x')), addr x0, addr y0, addr x1, addr y1) + result.xHeight = float32(y1 - y0) * glyph2QuadScale + proc loadFont*[N: static int]( path: string, lineHeightPixels = 80'f32,
--- a/tests/test_text.nim Sat Dec 21 19:32:59 2024 +0700 +++ b/tests/test_text.nim Sun Dec 22 00:31:29 2024 +0700 @@ -21,9 +21,9 @@ var pipeline = createPipeline[GlyphShader[MAX_CODEPOINTS]]( renderPass = vulkan.swapchain.renderPass ) - var glyphs = font.initGlyphs(1000, baseScale = 0.1) + var textbuffer = font.initTextBuffer(1000, baseScale = 0.1) - assignBuffers(renderdata, glyphs) + assignBuffers(renderdata, textbuffer) assignBuffers(renderdata, font.descriptorSet) uploadImages(renderdata, font.descriptorSet) initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet) @@ -31,9 +31,9 @@ var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let t = getMonoTime() - glyphs.reset() - glyphs.add("Hello semicongine!", vec3(0.5, 0.5), anchor = vec2(0.5, 0.5)) - glyphs.updateAllGPUBuffers(flush = true) + textbuffer.reset() + textbuffer.add("Hello semicongine!", vec3(0.5, 0.5), anchor = vec2(0.5, 0.5)) + textbuffer.updateAllGPUBuffers(flush = true) withNextFrame(framebuffer, commandbuffer): bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) @@ -46,7 +46,7 @@ vec4(0, 0, 0, 0), ): withPipeline(commandbuffer, pipeline): - renderGlyphs(commandbuffer, pipeline, glyphs) + renderTextBuffer(commandbuffer, pipeline, textbuffer) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) @@ -73,13 +73,13 @@ initDescriptorSet(renderdata, pipeline.layout(0), font2.descriptorSet) initDescriptorSet(renderdata, pipeline.layout(0), font3.descriptorSet) - var glyphs1 = font1.initGlyphs(10, baseScale = 0.1) - var glyphs2 = font2.initGlyphs(10, baseScale = 0.1) - var glyphs3 = font3.initGlyphs(10, baseScale = 0.1) + var textbuffer1 = font1.initTextBuffer(10, baseScale = 0.1) + var textbuffer2 = font2.initTextBuffer(10, baseScale = 0.1) + var textbuffer3 = font3.initTextBuffer(10, baseScale = 0.1) - assignBuffers(renderdata, glyphs1) - assignBuffers(renderdata, glyphs2) - assignBuffers(renderdata, glyphs3) + assignBuffers(renderdata, textbuffer1) + assignBuffers(renderdata, textbuffer2) + assignBuffers(renderdata, textbuffer3) var labels = [" 0", " 1", " 2"] @@ -87,17 +87,17 @@ var p = 0 while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time - glyphs1.reset() - glyphs2.reset() - glyphs3.reset() + textbuffer1.reset() + textbuffer2.reset() + textbuffer3.reset() - glyphs1.add($(p + 0), vec3(0.3, 0.5)) - glyphs2.add($(p + 1), vec3(0.5, 0.5)) - glyphs3.add($(p + 2), vec3(0.7, 0.5)) + textbuffer1.add($(p + 0), vec3(0.3, 0.5)) + textbuffer2.add($(p + 1), vec3(0.5, 0.5)) + textbuffer3.add($(p + 2), vec3(0.7, 0.5)) - glyphs1.updateAllGPUBuffers(flush = true) - glyphs2.updateAllGPUBuffers(flush = true) - glyphs3.updateAllGPUBuffers(flush = true) + textbuffer1.updateAllGPUBuffers(flush = true) + textbuffer2.updateAllGPUBuffers(flush = true) + textbuffer3.updateAllGPUBuffers(flush = true) inc p withNextFrame(framebuffer, commandbuffer): @@ -111,11 +111,11 @@ ): withPipeline(commandbuffer, pipeline): bindDescriptorSet(commandbuffer, font1.descriptorSet, 0, pipeline) - renderGlyphs(commandbuffer, pipeline, glyphs1) + renderTextBuffer(commandbuffer, pipeline, textbuffer1) bindDescriptorSet(commandbuffer, font2.descriptorSet, 0, pipeline) - renderGlyphs(commandbuffer, pipeline, glyphs2) + renderTextBuffer(commandbuffer, pipeline, textbuffer2) bindDescriptorSet(commandbuffer, font3.descriptorSet, 0, pipeline) - renderGlyphs(commandbuffer, pipeline, glyphs3) + renderTextBuffer(commandbuffer, pipeline, textbuffer3) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) @@ -134,30 +134,31 @@ uploadImages(renderdata, font.descriptorSet) initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet) - var glyphs = font.initGlyphs(1000, baseScale = 0.1) - assignBuffers(renderdata, glyphs) + var textbuffer = font.initTextBuffer(1000, baseScale = 0.1) + assignBuffers(renderdata, textbuffer) var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time - glyphs.reset() - glyphs.add("Anchor Center", vec3(0, 0), anchor = vec2(0, 0)) - glyphs.add("Anchor top left", vec3(0, 0), anchor = vec2(-1, 1)) - glyphs.add("Anchor top right", vec3(0, 0), anchor = vec2(1, 1)) - glyphs.add("Anchor bottom left", vec3(0, 0), anchor = vec2(-1, -1)) - glyphs.add("Anchor bottom right", vec3(0, 0), anchor = vec2(1, -1)) + textbuffer.reset() + textbuffer.add("Anchor at center", vec3(0, 0), anchor = vec2(0, 0)) + textbuffer.add("Anchor at top left`", vec3(-1, 1), anchor = vec2(-1, 1)) + textbuffer.add("Anchor at top right", vec3(1, 1), anchor = vec2(1, 1)) + textbuffer.add("Anchor at bottom left", vec3(-1, -1), anchor = vec2(-1, -1)) + textbuffer.add("Anchor at bottom right", vec3(1, -1), anchor = vec2(1, -1)) - glyphs.add( - """Paragraph - This is a somewhat longer paragraph with a few newlines and a maximum width of 0.2. + textbuffer.add( + "Mutiline text\nLeft aligned\nCool!", vec3(-0.5, -0.5), alignment = Left + ) + textbuffer.add( + "Mutiline text\nCenter aligned\nCool!!", vec3(0, -0.5), alignment = Center + ) + textbuffer.add( + "Mutiline text\nRight aligned\nCool!!!", vec3(0.5, -0.5), alignment = Right + ) - It should display with some space above and have a pleasing appearance overall! :)""", - vec3(0.5, 0.5), - anchor = vec2(0, 0), - alignment = Center, - ) - glyphs.updateAllGPUBuffers(flush = true) + textbuffer.updateAllGPUBuffers(flush = true) withNextFrame(framebuffer, commandbuffer): bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) @@ -170,14 +171,13 @@ vec4(0, 0, 0, 0), ): withPipeline(commandbuffer, pipeline): - renderGlyphs(commandbuffer, pipeline, glyphs) + renderTextBuffer(commandbuffer, pipeline, textbuffer) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) destroyPipeline(pipeline) destroyRenderData(renderdata) -#[ proc test_04_lots_of_texts(time: float32) = var font = loadFont[MAX_CODEPOINTS]("DejaVuSans.ttf", lineHeightPixels = 160) var renderdata = initRenderData() @@ -186,9 +186,12 @@ renderPass = vulkan.swapchain.renderPass ) - var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas.copy())) - uploadImages(renderdata, ds) - initDescriptorSet(renderdata, pipeline.layout(0), ds) + assignBuffers(renderdata, font.descriptorSet) + uploadImages(renderdata, font.descriptorSet) + initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet) + + var textbuffer = font.initTextBuffer(1000, baseScale = 0.1) + assignBuffers(renderdata, textbuffer) var labels: seq[Textbox] var positions = newSeq[Vec3f](100) @@ -203,10 +206,9 @@ var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: - for l in labels.mitems: - l.refresh() + textbuffer.reset() withNextFrame(framebuffer, commandbuffer): - bindDescriptorSet(commandbuffer, ds, 0, pipeline) + bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) withRenderPass( vulkan.swapchain.renderPass, framebuffer, @@ -216,16 +218,12 @@ vec4(0, 0, 0, 0), ): withPipeline(commandbuffer, pipeline): - for i in 0 ..< labels.len: - render( - commandbuffer, pipeline, labels[i], positions[i], colors[i], scales[i] - ) + renderTextBuffer(commandbuffer, pipeline, textbuffer) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) destroyPipeline(pipeline) destroyRenderData(renderdata) -]# when isMainModule: var time = 100'f32