changeset 1400:20602878744e

did: finished basic implementation of new glyph-rendering system
author sam <sam@basx.dev>
date Mon, 16 Dec 2024 00:27:40 +0700
parents dde74be11b49
children 4ecb004ee7f8
files semicongine/text.nim semicongine/text/font.nim semicongine/text/textbox.nim tests/test_text.nim
diffstat 4 files changed, 112 insertions(+), 165 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/text.nim	Sun Dec 15 00:21:12 2024 +0700
+++ b/semicongine/text.nim	Mon Dec 16 00:27:40 2024 +0700
@@ -20,38 +20,29 @@
   SPACE = Rune(' ')
 
 type
-  GlyphInfo* = object
-    uvs*: array[4, Vec2f]
-    dimension*: Vec2f
-    offsetX*: float32
-    offsetY*: float32
-    leftBearing*: float32
-    advance*: float32
-
-  GlyphData[N: static int] = object
+  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]
 
   GlyphDescriptorSet*[N: static int] = object
     fontAtlas*: Image[Gray]
-    glyphData*: GPUValue[GlyphData[N], StorageBuffer]
+    glyphquads*: GPUValue[GlyphQuad[N], StorageBuffer]
 
   FontObj*[N: static int] = object
-    glyphs*: Table[Rune, GlyphInfo]
-    fontAtlas*: Image[Gray]
-    maxHeight*: int
+    advance*: Table[Rune, float32]
     kerning*: Table[(Rune, Rune), float32]
-    fontscale*: float32
     lineHeight*: float32
     lineAdvance*: float32
-    capHeight*: float32
-    xHeight*: float32
     descriptorSet*: DescriptorSetData[GlyphDescriptorSet[N]]
     descriptorGlyphIndex: Table[Rune, uint16]
+    fallbackCharacter: Rune
 
   Font*[N: static int] = ref FontObj[N]
 
-  Glyphs* = object
+  Glyphs*[N: static int] = object
+    cursor: int
+    font: Font[N]
+    baseScale*: float32
     position*: GPUArray[Vec3f, VertexBufferMapped]
     color*: GPUArray[Vec4f, VertexBufferMapped]
     scale*: GPUArray[float32, VertexBufferMapped]
@@ -82,12 +73,12 @@
 void main() {
   int vertexI = indices[gl_VertexIndex];
   vec3 pos = vec3(
-    glyphData.pos[glyphIndex][i_x[vertexI]] * scale,
-    glyphData.pos[glyphIndex][i_y[vertexI]] * scale * textRendering.aspectRatio,
+    glyphquads.pos[glyphIndex][i_x[vertexI]] * scale,
+    glyphquads.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]]);
+  vec2 uv = vec2(glyphquads.uv[glyphIndex][i_x[vertexI]], glyphquads.uv[glyphIndex][i_y[vertexI]]);
   fragmentUv = uv;
   fragmentColor = color;
 }  """
@@ -97,62 +88,24 @@
     outColor = vec4(fragmentColor.rgb, fragmentColor.a * a);
 }"""
 
-proc `=copy`[T: static int](dest: var FontObj[T], source: FontObj[T]) {.error.}
-proc `=copy`(dest: var Glyphs, source: Glyphs) {.error.}
+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.}
 
 include ./text/font
 
-#[
-proc glyphDescriptorSet*(
-    font: Font, maxGlyphs: static int
-): (DescriptorSetData[GlyphDescriptorSet[maxGlyphs]], Table[Rune, uint16]) =
-  assert font.glyphs.len <= maxGlyphs,
-    "font has " & $font.glyphs.len & " glyphs but shader is only configured for " &
-      $maxGlyphs
-
-  var glyphData = GlyphData[maxGlyphs]()
-  var glyphTable: Table[Rune, uint16]
-
-  var i = 0'u16
-  for rune, info in font.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)
-    glyphTable[rune] = i
-    inc i
-
-  (
-    asDescriptorSetData(
-      GlyphDescriptorSet[maxGlyphs](
-        fontAtlas: font.fontAtlas.copy(),
-        glyphData: asGPUValue(glyphData, StorageBuffer),
-      )
-    ),
-    glyphTable,
-  )
-]#
-
-func initGlyphs*(count: int): Glyphs =
+func initGlyphs*[N: static int](
+    font: Font[N], count: int, baseScale = 1'f32
+): Glyphs[N] =
+  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)
 
-func set*(
+proc add*(
     glyphs: var Glyphs,
-    font: FontObj,
     text: seq[Rune],
     position: Vec3f,
     scale = 1'f32,
@@ -160,12 +113,33 @@
 ) =
   assert text.len <= glyphs.position.len,
     &"Set {text.len} but Glyphs-object only supports {glyphs.position.len}"
-  var cursor = position
+  var cursorPos = 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]]
+    if not text[i].isWhitespace():
+      glyphs.position[glyphs.cursor] = cursorPos
+      glyphs.scale[glyphs.cursor] = scale * glyphs.baseScale
+      glyphs.color[glyphs.cursor] = color
+      if text[i] in glyphs.font.descriptorGlyphIndex:
+        glyphs.glyphIndex[glyphs.cursor] = glyphs.font.descriptorGlyphIndex[text[i]]
+      else:
+        glyphs.glyphIndex[glyphs.cursor] =
+          glyphs.font.descriptorGlyphIndex[glyphs.font.fallbackCharacter]
+      inc glyphs.cursor
+
+    if text[i] in glyphs.font.advance:
+      cursorPos.x =
+        cursorPos.x + glyphs.font.advance[text[i]] * scale * glyphs.baseScale
+    else:
+      cursorPos.x =
+        cursorPos.x +
+        glyphs.font.advance[glyphs.font.fallbackCharacter] * scale * glyphs.baseScale
+
+    if i < text.len - 1:
+      cursorPos.x =
+        cursorPos.x + glyphs.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * scale
+
+proc reset*(glyphs: var Glyphs) =
+  glyphs.cursor = 0
 
 type EMPTY = object
 const EMPTYOBJECT = EMPTY()
@@ -178,4 +152,5 @@
     glyphs,
     pushConstant = TextRendering(aspectRatio: getAspectRatio()),
     fixedVertexCount = 6,
+    fixedInstanceCount = glyphs.cursor,
   )
--- a/semicongine/text/font.nim	Sun Dec 15 00:21:12 2024 +0700
+++ b/semicongine/text/font.nim	Mon Dec 16 00:27:40 2024 +0700
@@ -50,104 +50,105 @@
   assert codePoints.len <= N,
     "asked for " & $codePoints.len & " glyphs but shader is only configured for " & $N
 
+  result = Font[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[N](
-    fontscale:
-      float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels)))
-  )
-
   var ascent, descent, lineGap: cint
   stbtt_GetFontVMetrics(addr fontinfo, addr ascent, addr descent, addr lineGap)
 
-  result.lineHeight = float32(ascent - descent) * result.fontscale
-  result.lineAdvance = float32(ascent - descent + lineGap) * result.fontscale
-
+  let fscale =
+    float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels)))
   # ensure all codepoints are available in the font
   for codePoint in codePoints:
     if stbtt_FindGlyphIndex(addr fontinfo, cint(codePoint)) == 0:
       warn &"Loading font {name}: Codepoint '{codePoint}' ({cint(codePoint)}) has no glyph"
 
   var
-    offsetY: Table[Rune, int]
-    offsetX: Table[Rune, int]
-    images: seq[Image[Gray]]
+    offsetY: Table[Rune, cint]
+    offsetX: Table[Rune, cint]
+    bitmaps: seq[Image[Gray]]
 
+  # render all glyphs to bitmaps and store quad geometry info
   for codePoint in codePoints:
-    var
-      width, height: cint
-      offX, offY: cint
+    offsetX[codePoint] = 0
+    offsetY[codePoint] = 0
+    var width, height: cint
     let data = stbtt_GetCodepointBitmap(
       addr fontinfo,
-      result.fontscale,
-      result.fontscale,
+      fscale,
+      fscale,
       cint(codePoint),
       addr width,
       addr height,
-      addr offX,
-      addr offY,
+      addr (offsetX[codePoint]),
+      addr (offsetY[codePoint]),
     )
-    offsetX[codePoint] = offX
-    offsetY[codePoint] = offY
-
-    if char(codePoint) in UppercaseLetters:
-      result.capHeight = float32(height)
-    if codePoint == Rune('x'):
-      result.xHeight = float32(height)
 
     if width > 0 and height > 0:
       var bitmap = newSeq[Gray](width * height)
       for i in 0 ..< width * height:
         bitmap[i] = vec1u8(data[i].uint8)
-      images.add Image[Gray](width: width.uint32, height: height.uint32, data: bitmap)
+      bitmaps.add Image[Gray](width: width.uint32, height: height.uint32, data: bitmap)
     else:
-      images.add Image[Gray](width: 1, height: 1, data: @[vec1u8()])
+      bitmaps.add Image[Gray](width: 1, height: 1, data: @[vec1u8(0)])
 
     nativeFree(data)
 
-  let packed = pack(images)
+  # generate glyph atlas from bitmaps
+  let packed = pack(bitmaps)
+  result.descriptorSet.data.fontAtlas = packed.atlas
 
-  result.fontAtlas = packed.atlas
-
-  let w = float32(result.fontAtlas.width)
-  let h = float32(result.fontAtlas.height)
+  # generate quad-information for use in shader
   for i in 0 ..< codePoints.len:
-    let
-      codePoint = codePoints[i]
-      coord = (x: float32(packed.coords[i].x), y: float32(packed.coords[i].y))
-      iw = float32(images[i].width)
-      ih = float32(images[i].height)
-    # horizontal spaces:
-    var advance, leftBearing: cint
+    let codePoint = codePoints[i]
+    var advance, leftBearing: cint # is in glyph-space, needs to be scaled to pixel-space
     stbtt_GetCodepointHMetrics(
       addr fontinfo, cint(codePoint), addr advance, addr leftBearing
     )
+    result.advance[codePoint] = float32(advance) * fscale * (1 / lineHeightPixels)
 
-    result.glyphs[codePoint] = GlyphInfo(
-      dimension: vec2(float32(images[i].width), float32(images[i].height)),
-      uvs: [
-        vec2((coord.x + 0.5) / w, (coord.y + ih - 0.5) / h),
-        vec2((coord.x + 0.5) / w, (coord.y + 0.5) / h),
-        vec2((coord.x + iw - 0.5) / w, (coord.y + 0.5) / h),
-        vec2((coord.x + iw - 0.5) / w, (coord.y + ih - 0.5) / h),
-      ],
-      offsetX: float32(offsetX[codePoint]),
-      offsetY: float32(offsetY[codePoint]),
-      leftBearing: float32(leftBearing) * result.fontscale,
-      advance: float32(advance),
+    let
+      atlasW = float32(result.descriptorSet.data.fontAtlas.width)
+      atlasH = float32(result.descriptorSet.data.fontAtlas.height)
+      uv = vec2(packed.coords[i].x, packed.coords[i].y)
+      bitmapW = float32(bitmaps[i].width)
+      bitmapH = float32(bitmaps[i].height)
+      left = float32(leftBearing) * fscale + float32(offsetX[codePoint])
+      right = left + bitmapW
+      top = -float32(offsetY[codePoint])
+      bottom = top - bitmapH
+
+    template glyphquads(): untyped =
+      result.descriptorSet.data.glyphquads.data
+
+    glyphquads.pos[i] = vec4(left, bottom, right, top) * (1 / lineHeightPixels)
+    glyphquads.uv[i] = vec4(
+      (uv.x + 0.5) / atlasW, # left
+      (uv.y + bitmapH - 0.5) / atlasH, # bottom
+      (uv.x + bitmapW - 0.5) / atlasW, # right
+      (uv.y + 0.5) / atlasH, # top
     )
+    if i == 0:
+      result.fallbackCharacter = codePoint
+    result.descriptorGlyphIndex[codePoint] = i.uint16
 
+    # kerning
     for codePointAfter in codePoints:
       result.kerning[(codePoint, codePointAfter)] =
         float32(
           stbtt_GetCodepointKernAdvance(
             addr fontinfo, cint(codePoint), cint(codePointAfter)
           )
-        ) * result.fontscale
+        ) * fscale
+
+  # line spacing
+  result.lineHeight = float32(ascent - descent) * fscale
+  result.lineAdvance = float32(ascent - descent + lineGap) * fscale
 
 proc loadFont*[N: static int](
     path: string,
@@ -156,42 +157,13 @@
     charset = ASCII_CHARSET,
     package = DEFAULT_PACKAGE,
 ): Font[N] =
-  result = readTrueType[N](
+  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 =
   var text = when theText is string: theText.toRunes else: theText
   var currentWidth = 0'f32
@@ -202,7 +174,7 @@
       currentWidth = 0'f32
     else:
       if not (i == text.len - 1 and text[i].isWhiteSpace):
-        currentWidth += font.glyphs[text[i]].advance
+        currentWidth += font.advance[text[i]]
       if i < text.len - 1:
         currentWidth += font.kerning[(text[i], text[i + 1])]
   lineWidths.add currentWidth
--- a/semicongine/text/textbox.nim	Sun Dec 15 00:21:12 2024 +0700
+++ b/semicongine/text/textbox.nim	Mon Dec 16 00:27:40 2024 +0700
@@ -34,7 +34,7 @@
       width = 0'f32
     else:
       if not (i == textbox.visibleText.len - 1 and textbox.visibleText[i].isWhiteSpace):
-        width += textbox.font.glyphs[textbox.visibleText[i]].advance
+        width += textbox.font.glyphdata[textbox.visibleText[i]].advance
       if i < textbox.visibleText.len - 1:
         width +=
           textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])]
@@ -85,7 +85,7 @@
             lineWidths[lineIndex]
       else:
         let
-          glyph = textbox.font.glyphs[textbox.visibleText[i]]
+          glyph = textbox.font.glyphdata[textbox.visibleText[i]]
           left = offsetX + glyph.offsetX
           right = offsetX + glyph.offsetX + glyph.dimension.x
           top = offsetY - glyph.offsetY
--- a/tests/test_text.nim	Sun Dec 15 00:21:12 2024 +0700
+++ b/tests/test_text.nim	Mon Dec 16 00:27:40 2024 +0700
@@ -18,24 +18,24 @@
 
 const MAX_GLYPHS = 200
 proc test_01_static_label_new(time: float32) =
-  # var font = loadFont("Overhaul.ttf", lineHeightPixels = 160)
-  var font = loadFont[MAX_GLYPHS]("DejaVuSans.ttf", lineHeightPixels = 160)
+  var font = loadFont[MAX_GLYPHS]("Overhaul.ttf", lineHeightPixels = 200)
   var renderdata = initRenderData()
   var pipeline =
     createPipeline[GlyphShader[MAX_GLYPHS]](renderPass = vulkan.swapchain.renderPass)
-  var glyphs = initGlyphs(1000)
+  var glyphs = font.initGlyphs(1000, baseScale = 0.3)
 
   assignBuffers(renderdata, glyphs)
   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:
+    let t = getMonoTime()
+    glyphs.reset()
+    glyphs.add("semicongine".toRunes())
+    glyphs.updateAllGPUBuffers(flush = true)
+
     withNextFrame(framebuffer, commandbuffer):
       bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
       withRenderPass(
@@ -49,7 +49,7 @@
         withPipeline(commandbuffer, pipeline):
           renderGlyphs(commandbuffer, pipeline, glyphs)
 
-        # cleanup
+  # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
   destroyPipeline(pipeline)
   destroyRenderData(renderdata)