Mercurial > games > semicongine
changeset 1408:17d960ff6a24
did: implement decent text rendering (I hope, we'll see)
author | sam <sam@basx.dev> |
---|---|
date | Sun, 22 Dec 2024 22:32:12 +0700 |
parents | 56f927b89716 |
children | 5a56f8ac328b |
files | semicongine/text.nim tests/test_text.nim |
diffstat | 2 files changed, 178 insertions(+), 114 deletions(-) [+] |
line wrap: on
line diff
--- a/semicongine/text.nim Sun Dec 22 00:31:29 2024 +0700 +++ b/semicongine/text.nim Sun Dec 22 22:32:12 2024 +0700 @@ -47,6 +47,8 @@ fallbackCharacter: Rune Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs] + + TextHandle* = distinct int Text = object bufferOffset: int text: seq[Rune] @@ -55,6 +57,7 @@ anchor: Vec2f = vec2() scale: float32 = 0 color: Vec4f = vec4(1, 1, 1, 1) + capacity: int TextBuffer*[MaxGlyphs: static int] = object cursor: int @@ -156,44 +159,30 @@ return vec2(w, h) -proc add*( - textbuffer: var TextBuffer, - text: seq[Rune], - position: Vec3f, - alignment: TextAlignment = Left, - anchor: Vec2f = vec2(0, 0), - scale: float32 = 1'f32, - color: Vec4f = vec4(1, 1, 1, 1), -) = - ## This should be called again after aspect ratio of window changes +proc updateGlyphData*(textbuffer: var TextBuffer, textHandle: TextHandle) = + let + i = int(textHandle) + text = textbuffer.texts[i].text + position = textbuffer.texts[i].position + alignment = textbuffer.texts[i].alignment + anchor = textbuffer.texts[i].anchor + scale = textbuffer.texts[i].scale + color = textbuffer.texts[i].color + offset = textbuffer.texts[i].bufferOffset + capacity = textbuffer.texts[i].capacity - assert text.len <= textbuffer.position.len, - &"Set {text.len} but TextBuffer-object only supports {textbuffer.position.len}" - - textbuffer.texts.add Text( - bufferOffset: textbuffer.cursor, - text: text, - position: position, - alignment: alignment, - anchor: anchor, - scale: scale, - color: color, - ) - - let globalScale = scale * textbuffer.baseScale box = textDimension(textbuffer.font, text, globalScale) xH = textbuffer.font.xHeight * globalScale + aratio = getAspectRatio() origin = vec3( - position.x - (anchor.x * 0.5 + 0.5) * box.x / getAspectRatio(), + position.x - (anchor.x * 0.5 + 0.5) * box.x / aratio, 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 cursorPos = origin @@ -207,44 +196,88 @@ of Right: cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio - for i in 0 ..< text.len: - if text[i] == Rune('\n'): - inc lineI - case alignment - of Left: - cursorPos.x = origin.x - of Center: - cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5) - of Right: - cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio - cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale + for i in 0 ..< capacity: + if i < text.len: + if text[i] == Rune('\n'): + inc lineI + case alignment + of Left: + cursorPos.x = origin.x + of Center: + cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5) + of Right: + cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio + cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale + else: + if not text[i].isWhitespace(): + textbuffer.position[offset + i] = cursorPos + textbuffer.scale[offset + i] = globalScale + textbuffer.color[offset + i] = color + if text[i] in textbuffer.font.descriptorGlyphIndex: + textbuffer.glyphIndex[offset + i] = + textbuffer.font.descriptorGlyphIndex[text[i]] + else: + textbuffer.glyphIndex[offset + i] = + textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter] + + if text[i] in textbuffer.font.advance: + cursorPos.x = + cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio + else: + cursorPos.x = + cursorPos.x + + textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale / + aratio + + if i < text.len - 1: + cursorPos.x = + cursorPos.x + + textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale / + aratio else: - if not text[i].isWhitespace(): - 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: - textbuffer.glyphIndex[textbuffer.cursor] = - textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter] - inc textbuffer.cursor + textbuffer.position[offset + i] = vec3() + textbuffer.scale[offset + i] = 0 + textbuffer.color[offset + i] = vec4() + textbuffer.glyphIndex[offset + i] = 0 + +proc updateGlyphData*(textbuffer: var TextBuffer) = + for i in 0 ..< textbuffer.texts.len: + textbuffer.updateGlyphData(TextHandle(i)) + +proc refresh*(textbuffer: var TextBuffer) = + textbuffer.updateGlyphData() + textbuffer.updateAllGPUBuffers(flush = true) - if text[i] in textbuffer.font.advance: - cursorPos.x = - cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio - else: - cursorPos.x = - cursorPos.x + - textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale / - aratio +proc add*( + textbuffer: var TextBuffer, + text: seq[Rune], + position: Vec3f, + alignment: TextAlignment = Left, + anchor: Vec2f = vec2(0, 0), + scale: float32 = 1'f32, + color: Vec4f = vec4(1, 1, 1, 1), + capacity: int = 0, +): TextHandle = + ## This should be called again after aspect ratio of window changes - if i < text.len - 1: - cursorPos.x = - cursorPos.x + - textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale / - aratio + let cap = if capacity == 0: text.len else: capacity + assert textbuffer.cursor + cap <= textbuffer.position.len, + &"Text is too big for TextBuffer ({textbuffer.position.len - textbuffer.cursor} left, but need {cap})" + + result = TextHandle(textbuffer.texts.len) + + textbuffer.texts.add Text( + bufferOffset: textbuffer.cursor, + text: text, + position: position, + alignment: alignment, + anchor: anchor, + scale: scale, + color: color, + capacity: cap, + ) + textbuffer.cursor += cap + textbuffer.updateGlyphData(result) proc add*( textbuffer: var TextBuffer, @@ -254,8 +287,36 @@ anchor: Vec2f = vec2(0, 0), scale: float32 = 1'f32, color: Vec4f = vec4(1, 1, 1, 1), + capacity: int = 0, +): TextHandle = + add(textbuffer, text.toRunes, position, alignment, anchor, scale, color, capacity) + +proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: seq[Rune]) = + if text.len <= textbuffer.texts[int(textHandle)].capacity: + textbuffer.texts[int(textHandle)].text = text + else: + textbuffer.texts[int(textHandle)].text = + text[0 ..< textbuffer.texts[int(textHandle)].capacity] + +proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: string) = + text(textbuffer, textHandle, text.toRunes) + +proc position*(textbuffer: var TextBuffer, textHandle: TextHandle, position: Vec3f) = + textbuffer.texts[int(textHandle)].position = position + +proc alignment*( + textbuffer: var TextBuffer, textHandle: TextHandle, alignment: TextAlignment ) = - add(textbuffer, text.toRunes, position, alignment, anchor, scale, color) + textbuffer.texts[int(textHandle)].alignment = alignment + +proc anchor*(textbuffer: var TextBuffer, textHandle: TextHandle, anchor: Vec2f) = + textbuffer.texts[int(textHandle)].anchor = anchor + +proc scale*(textbuffer: var TextBuffer, textHandle: TextHandle, scale: float32) = + textbuffer.texts[int(textHandle)].scale = scale + +proc color*(textbuffer: var TextBuffer, textHandle: TextHandle, color: Vec4f) = + textbuffer.texts[int(textHandle)].color = color proc reset*(textbuffer: var TextBuffer) = textbuffer.cursor = 0
--- a/tests/test_text.nim Sun Dec 22 00:31:29 2024 +0700 +++ b/tests/test_text.nim Sun Dec 22 22:32:12 2024 +0700 @@ -28,12 +28,13 @@ uploadImages(renderdata, font.descriptorSet) initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet) + discard textbuffer.add("Hello semicongine!", vec3()) + var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let t = getMonoTime() - textbuffer.reset() - textbuffer.add("Hello semicongine!", vec3(0.5, 0.5), anchor = vec2(0.5, 0.5)) - textbuffer.updateAllGPUBuffers(flush = true) + if windowWasResized(): + textbuffer.refresh() withNextFrame(framebuffer, commandbuffer): bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) @@ -81,25 +82,25 @@ assignBuffers(renderdata, textbuffer2) assignBuffers(renderdata, textbuffer3) - var labels = [" 0", " 1", " 2"] + var p = 0 + let l1 = textbuffer1.add($(p + 0), vec3(0.3, 0.5), capacity = 5) + let l2 = textbuffer2.add($(p + 1), vec3(0.5, 0.5), capacity = 5) + let l3 = textbuffer3.add($(p + 2), vec3(0.7, 0.5), capacity = 5) var start = getMonoTime() - var p = 0 while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time - textbuffer1.reset() - textbuffer2.reset() - textbuffer3.reset() - - 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)) - - textbuffer1.updateAllGPUBuffers(flush = true) - textbuffer2.updateAllGPUBuffers(flush = true) - textbuffer3.updateAllGPUBuffers(flush = true) inc p + + textbuffer1.text(l1, $(p + 0)) + textbuffer2.text(l2, $(p + 1)) + textbuffer3.text(l3, $(p + 2)) + + textbuffer1.refresh() + textbuffer2.refresh() + textbuffer3.refresh() + withNextFrame(framebuffer, commandbuffer): withRenderPass( vulkan.swapchain.renderPass, @@ -137,28 +138,27 @@ var textbuffer = font.initTextBuffer(1000, baseScale = 0.1) assignBuffers(renderdata, textbuffer) + discard textbuffer.add("Anchor at center", vec3(0, 0), anchor = vec2(0, 0)) + discard textbuffer.add("Anchor at top left`", vec3(-1, 1), anchor = vec2(-1, 1)) + discard textbuffer.add("Anchor at top right", vec3(1, 1), anchor = vec2(1, 1)) + discard textbuffer.add("Anchor at bottom left", vec3(-1, -1), anchor = vec2(-1, -1)) + discard textbuffer.add("Anchor at bottom right", vec3(1, -1), anchor = vec2(1, -1)) + + discard textbuffer.add( + "Mutiline text\nLeft aligned\nCool!", vec3(-0.5, -0.5), alignment = Left + ) + discard textbuffer.add( + "Mutiline text\nCenter aligned\nCool!!", vec3(0, -0.5), alignment = Center + ) + discard textbuffer.add( + "Mutiline text\nRight aligned\nCool!!!", vec3(0.5, -0.5), alignment = Right + ) + var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time - - 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)) - - 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 - ) - - textbuffer.updateAllGPUBuffers(flush = true) + if windowWasResized(): + textbuffer.refresh() withNextFrame(framebuffer, commandbuffer): bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) @@ -190,24 +190,27 @@ uploadImages(renderdata, font.descriptorSet) initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet) - var textbuffer = font.initTextBuffer(1000, baseScale = 0.1) + var textbuffer = font.initTextBuffer(3000, baseScale = 0.1) assignBuffers(renderdata, textbuffer) - var labels: seq[Textbox] - var positions = newSeq[Vec3f](100) - var colors = newSeq[Vec4f](100) - var scales = newSeq[Vec2f](100) - for i in 0 ..< 100: - positions[i] = vec3(rand(-0.5 .. 0.5), rand(-0.5 .. 0.5), rand(-0.1 .. 0.1)) - colors[i] = - vec4(rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0)) - scales[i] = vec2(rand(0.5'f32 .. 1.5'f32), rand(0.5'f32 .. 1.5'f32)) - labels.add initTextbox(renderdata, pipeline.layout(0), font, 0.001, $i) + for i in 0 ..< 1000: + discard textbuffer.add( + $i, + vec3(rand(-0.8 .. 0.8), rand(-0.8 .. 0.8), rand(-0.1 .. 0.1)), + color = + vec4(rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0)), + scale = rand(0.5'f32 .. 1.5'f32), + ) var start = getMonoTime() + var last = start while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: - textbuffer.reset() + let n = getMonoTime() + echo (n - last).inMicroseconds() / 1000 + last = n withNextFrame(framebuffer, commandbuffer): + if windowWasResized(): + textbuffer.refresh() bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline) withRenderPass( vulkan.swapchain.renderPass, @@ -226,7 +229,7 @@ destroyRenderData(renderdata) when isMainModule: - var time = 100'f32 + var time = 1'f32 initVulkan() for depthBuffer in [true, false]: @@ -234,10 +237,10 @@ setupSwapchain(renderpass = renderpass) # tests a simple triangle with minimalistic shader and vertex format - # test_01_static_label(time) - # test_02_multi_counter(time) + test_01_static_label(time) + test_02_multi_counter(time) test_03_layouting(time) - # test_04_lots_of_texts(time) + test_04_lots_of_texts(time) checkVkResult vkDeviceWaitIdle(vulkan.device) vkDestroyRenderPass(vulkan.device, renderpass.vk, nil)