changeset 1399:dde74be11b49

did: continue a lot on glyph renderer
author sam <sam@basx.dev>
date Sun, 15 Dec 2024 00:21:12 +0700
parents f7bf7a5cc1e7
children 20602878744e
files semicongine/contrib/algorithms/texture_packing.nim semicongine/image.nim semicongine/rendering.nim semicongine/rendering/shaders.nim semicongine/text.nim semicongine/text/font.nim tests/test_text.nim
diffstat 7 files changed, 142 insertions(+), 128 deletions(-) [+]
line wrap: on
line diff
--- 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
 
--- 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
--- 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
 
--- 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):
--- 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,
+  )
--- 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 =
--- 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