changeset 415:25db1fa56cb7

add: font/text improvments, support for newline rendering
author Sam <sam@basx.dev>
date Sat, 27 Jan 2024 00:31:11 +0700
parents 0deefe1c8af6
children 73cca428e27a
files semicongine/core/constants.nim semicongine/core/fonttypes.nim semicongine/renderer.nim semicongine/resources/font.nim semicongine/text.nim tests/test_font.nim
diffstat 6 files changed, 58 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/constants.nim	Thu Jan 25 20:23:22 2024 +0700
+++ b/semicongine/core/constants.nim	Sat Jan 27 00:31:11 2024 +0700
@@ -2,3 +2,4 @@
   RESOURCEROOT*: string = "resources"
   ENGINENAME* = "semicongine"
   ENGINEVERSION* = "0.0.1"
+  TRANSFORM_ATTRIB* = "transform"
--- a/semicongine/core/fonttypes.nim	Thu Jan 25 20:23:22 2024 +0700
+++ b/semicongine/core/fonttypes.nim	Sat Jan 27 00:31:11 2024 +0700
@@ -6,17 +6,17 @@
 import ./vector
 
 var FONTSAMPLER_SOFT* = Sampler(
-    magnification: VK_FILTER_LINEAR,
-    minification: VK_FILTER_LINEAR,
-    wrapModeS: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
-    wrapModeT: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
-  )
+  magnification: VK_FILTER_LINEAR,
+  minification: VK_FILTER_LINEAR,
+  wrapModeS: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+  wrapModeT: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+)
 var FONTSAMPLER_HARD* = Sampler(
-    magnification: VK_FILTER_NEAREST,
-    minification: VK_FILTER_NEAREST,
-    wrapModeS: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
-    wrapModeT: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
-  )
+  magnification: VK_FILTER_NEAREST,
+  minification: VK_FILTER_NEAREST,
+  wrapModeS: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+  wrapModeT: VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+)
 
 
 type
@@ -33,3 +33,4 @@
     maxHeight*: int
     kerning*: Table[(Rune, Rune), float32]
     fontscale*: float32
+    lineAdvance*: float32
--- a/semicongine/renderer.nim	Thu Jan 25 20:23:22 2024 +0700
+++ b/semicongine/renderer.nim	Sat Jan 27 00:31:11 2024 +0700
@@ -21,7 +21,6 @@
 import ./mesh
 import ./material
 
-const TRANSFORM_ATTRIBUTE = "transform"
 const MATERIALINDEX_ATTRIBUTE = "materialIndex"
 const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment
 
@@ -94,7 +93,7 @@
 
 func meshCompatibleWithPipeline(scene: Scene, mesh: Mesh, shaderPipeline: ShaderPipeline): (bool, string) =
   for input in shaderPipeline.inputs:
-    if input.name in [TRANSFORM_ATTRIBUTE, MATERIALINDEX_ATTRIBUTE]: # will be populated automatically
+    if input.name in [TRANSFORM_ATTRIB, MATERIALINDEX_ATTRIBUTE]: # will be populated automatically
       assert input.perInstance == true, &"Currently the {input.name} attribute must be a per instance attribute"
       continue
     if not (input.name in mesh[].attributes):
@@ -148,8 +147,8 @@
 
   # automatically populate material and tranform attributes
   for mesh in scene.meshes:
-    if not (TRANSFORM_ATTRIBUTE in mesh[].attributes):
-      mesh[].initInstanceAttribute(TRANSFORM_ATTRIBUTE, Unit4)
+    if not (TRANSFORM_ATTRIB in mesh[].attributes):
+      mesh[].initInstanceAttribute(TRANSFORM_ATTRIB, Unit4)
     if not (MATERIALINDEX_ATTRIBUTE in mesh[].attributes):
       mesh[].initInstanceAttribute(MATERIALINDEX_ATTRIBUTE, uint16(scenedata.materials[mesh.material.theType].find(mesh.material)))
 
@@ -332,8 +331,8 @@
   assert scene in renderer.scenedata
 
   for (drawable, mesh) in renderer.scenedata[scene].drawables.mitems:
-    if mesh[].attributes.contains(TRANSFORM_ATTRIBUTE):
-      mesh[].updateInstanceTransforms(TRANSFORM_ATTRIBUTE)
+    if mesh[].attributes.contains(TRANSFORM_ATTRIB):
+      mesh[].updateInstanceTransforms(TRANSFORM_ATTRIB)
     let attrs = (if forceAll: mesh[].attributes else: mesh[].dirtyAttributes)
     for attribute in attrs:
       renderer.refreshMeshAttributeData(scene, drawable, mesh, attribute)
--- a/semicongine/resources/font.nim	Thu Jan 25 20:23:22 2024 +0700
+++ b/semicongine/resources/font.nim	Sat Jan 27 00:31:11 2024 +0700
@@ -1,7 +1,5 @@
-import times
 import std/tables
 import std/strformat
-import std/sequtils
 import std/streams
 import std/os
 import std/unicode
@@ -11,7 +9,6 @@
 import ../core/imagetypes
 import ../core/fonttypes
 import ../algorithms
-import ./image
 
 {.emit: "#define STBTT_STATIC" .}
 {.emit: "#define STB_TRUETYPE_IMPLEMENTATION" .}
@@ -19,18 +16,18 @@
 
 type stbtt_fontinfo {.importc, incompleteStruct .} = object
 
-const MAX_TEXTURE_WIDTH = 4096
-
 proc stbtt_InitFont(info: ptr stbtt_fontinfo, data: ptr char, offset: cint): cint {.importc, nodecl.}
 proc stbtt_ScaleForPixelHeight(info: ptr stbtt_fontinfo, pixels: cfloat): cfloat {.importc, nodecl.}
 
 proc stbtt_GetCodepointBitmap(info: ptr stbtt_fontinfo, scale_x: cfloat, scale_y: cfloat, codepoint: cint, 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_GetCodepointBitmapBox(info: ptr stbtt_fontinfo, codepoint: cint, scale_x, scale_y: cfloat, ix0, iy0, ix1, iy1: ptr cint) {.importc, nodecl.}
 
 proc stbtt_GetCodepointHMetrics(info: ptr stbtt_fontinfo, codepoint: cint, advance, leftBearing: ptr cint) {.importc, nodecl.}
 proc stbtt_GetCodepointKernAdvance(info: ptr stbtt_fontinfo, ch1, ch2: cint): cint {.importc, nodecl.}
 proc stbtt_FindGlyphIndex(info: ptr stbtt_fontinfo, codepoint: cint): cint {.importc, nodecl.}
 
+proc stbtt_GetFontVMetrics(info: ptr stbtt_fontinfo, ascent, descent, lineGap: ptr cint) {.importc, nodecl.}
+
 proc free(p: pointer) {.importc.}
 
 proc readTrueType*(stream: Stream, name: string, codePoints: seq[Rune], lineHeightPixels: float32): Font =
@@ -43,13 +40,16 @@
   result.name = name
   result.fontscale = float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels)))
 
+  var ascent, descent, lineGap: cint
+  stbtt_GetFontVMetrics(addr fontinfo, addr ascent, addr descent, addr lineGap)
+  result.lineAdvance = float32(ascent - descent + lineGap) * result.fontscale
+
   # 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
-    bitmaps: Table[Rune, (cstring, cint, cint)]
     topOffsets: Table[Rune, int]
     images: seq[Image[GrayPixel]]
   let empty_image = newImage[GrayPixel](1, 1, [0'u8])
--- a/semicongine/text.nim	Thu Jan 25 20:23:22 2024 +0700
+++ b/semicongine/text.nim	Sat Jan 27 00:31:11 2024 +0700
@@ -25,7 +25,6 @@
     color*: Vec4f
 
 const
-  TRANSFORM_ATTRIB = "transform"
   POSITION_ATTRIB = SHADER_ATTRIB_PREFIX & "position"
   UV_ATTRIB = SHADER_ATTRIB_PREFIX & "uv"
   TEXT_MATERIAL_TYPE* = MaterialType(
@@ -52,44 +51,52 @@
   # pre-calculate text-width
   var width = 0'f32
   var maxWidth = 0'f32
-  var height = 0 # todo: finish implementation to handle newline, start here
-  const newline = ['\n'].toRunes()[0]
+  var height = 0'f32 # todo: finish implementation to handle newline, start here
+  const newline = Rune('\n')
   for i in 0 ..< min(textbox.text.len, textbox.maxLen):
     if textbox.text[i] == newline:
       maxWidth = max(width, maxWidth)
       width = 0'f32
-    width += textbox.font.glyphs[textbox.text[i]].advance
-    if i < textbox.text.len - 1:
-      width += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
+      height += textbox.font.lineAdvance
+    else:
+      width += textbox.font.glyphs[textbox.text[i]].advance
+      if i < textbox.text.len - 1:
+        width += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
   maxWidth = max(width, maxWidth)
 
   let centerX = maxWidth / 2
-  let centerY = textbox.font.maxHeight / 2
+  let centerY = height / 2
 
   var offsetX = 0'f32
+  var offsetY = 0'f32
+
   for i in 0 ..< textbox.maxLen:
     let vertexOffset = i * 4
     if i < textbox.text.len:
-      let
-        glyph = textbox.font.glyphs[textbox.text[i]]
-        left = offsetX + glyph.leftOffset
-        right = offsetX + glyph.leftOffset + glyph.dimension.x
-        top = glyph.topOffset
-        bottom = glyph.topOffset + glyph.dimension.y
+      if textbox.text[i] == Rune('\n'):
+        offsetX = 0
+        offsetY += textbox.font.lineAdvance
+      else:
+        let
+          glyph = textbox.font.glyphs[textbox.text[i]]
+          left = offsetX + glyph.leftOffset
+          right = offsetX + glyph.leftOffset + glyph.dimension.x
+          top = offsetY + glyph.topOffset
+          bottom = offsetY + glyph.topOffset + glyph.dimension.y
 
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f(left - centerX, bottom + centerY)
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f(left - centerX, top + centerY)
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f(right - centerX, top + centerY)
-      textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f(right - centerX, bottom + centerY)
+        textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f(left - centerX, bottom - centerY)
+        textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f(left - centerX, top - centerY)
+        textbox.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f(right - centerX, top - centerY)
+        textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f(right - centerX, bottom - centerY)
 
-      textbox.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0]
-      textbox.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1]
-      textbox.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2]
-      textbox.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3]
+        textbox.mesh[UV_ATTRIB, vertexOffset + 0] = glyph.uvs[0]
+        textbox.mesh[UV_ATTRIB, vertexOffset + 1] = glyph.uvs[1]
+        textbox.mesh[UV_ATTRIB, vertexOffset + 2] = glyph.uvs[2]
+        textbox.mesh[UV_ATTRIB, vertexOffset + 3] = glyph.uvs[3]
 
-      offsetX += glyph.advance
-      if i < textbox.text.len - 1:
-        offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
+        offsetX += glyph.advance
+        if i < textbox.text.len - 1:
+          offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])]
     else:
       textbox.mesh[POSITION_ATTRIB, vertexOffset + 0] = newVec3f()
       textbox.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
--- a/tests/test_font.nim	Thu Jan 25 20:23:22 2024 +0700
+++ b/tests/test_font.nim	Sat Jan 27 00:31:11 2024 +0700
@@ -27,8 +27,10 @@
           textbox.text = textbox.text & ($c).toRunes
         else:
           textbox.text = textbox.text & ($c).toRunes[0].toLower()
+    if engine.keyWasPressed(Enter):
+        textbox.text = textbox.text & Rune('\n')
     if engine.keyWasPressed(Space):
-        textbox.text = textbox.text & " ".toRunes[0]
+        textbox.text = textbox.text & Rune(' ')
     if engine.keyWasPressed(Backspace) and textbox.text.len > 0:
           textbox.text = textbox.text[0 ..< ^1]
     engine.renderScene(scene)