Mercurial > games > semicongine
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)