Mercurial > games > semicongine
view semicongine/text.nim @ 1404:80cfa19d1e2c
did: finally get text/glyph layouting correct again, 2/4 test adapted to new glyph API
author | sam <sam@basx.dev> |
---|---|
date | Thu, 19 Dec 2024 23:32:45 +0700 |
parents | 02d302c868d5 |
children | aeb15aa9768c |
line wrap: on
line source
import std/algorithm import std/logging import std/os import std/sequtils import std/streams import std/strformat import std/strutils import std/tables import std/unicode import ./core import ./resources import ./rendering import ./rendering/vulkan/api import ./image import ./contrib/algorithms/texture_packing const NEWLINE = Rune('\n') SPACE = Rune(' ') type TextAlignment* = enum Left Center Right 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] glyphquads*: GPUValue[GlyphQuad[N], StorageBuffer] FontObj*[N: static int] = object advance*: Table[Rune, float32] kerning*: Table[(Rune, Rune), float32] lineAdvance*: float32 lineHeight*: float32 # like lineAdvance - lineGap ascent*: float32 # from baseline to highest glyph descent*: float32 # from baseline to highest glyph descriptorSet*: DescriptorSetData[GlyphDescriptorSet[N]] descriptorGlyphIndex: Table[Rune, uint16] fallbackCharacter: Rune Font*[N: static int] = ref FontObj[N] Glyphs*[N: static int] = object cursor: int font*: Font[N] baseScale*: float32 position*: GPUArray[Vec3f, VertexBufferMapped] color*: GPUArray[Vec4f, VertexBufferMapped] scale*: GPUArray[float32, VertexBufferMapped] glyphIndex*: GPUArray[uint16, VertexBufferMapped] 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 outColor {.ShaderOutput.}: Vec4f glyphData {.DescriptorSet: 0.}: GlyphDescriptorSet[N] vertexCode* = """ 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.0000001; // const float epsilon = 0.1; void main() { int vertexI = indices[gl_VertexIndex]; vec3 pos = vec3( glyphquads.pos[glyphIndex][i_x[vertexI]] * scale / textRendering.aspectRatio, glyphquads.pos[glyphIndex][i_y[vertexI]] * scale, 1 - (gl_InstanceIndex + 1) * epsilon // allows overlapping glyphs to make proper depth test ); vec3 offset = vec3( (position.x - textRendering.aspectRatio + 1) / textRendering.aspectRatio, position.y, position.z ); gl_Position = vec4(pos + offset, 1.0); vec2 uv = vec2(glyphquads.uv[glyphIndex][i_x[vertexI]], glyphquads.uv[glyphIndex][i_y[vertexI]]); fragmentUv = uv; fragmentColor = color; } """ fragmentCode* = """void main() { float a = texture(fontAtlas, fragmentUv).r; outColor = vec4(fragmentColor.rgb, fragmentColor.a * a); }""" 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 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) iterator splitLines(text: seq[Rune]): seq[Rune] = var current = newSeq[Rune]() for c in text: if c == Rune('\n'): yield current current = newSeq[Rune]() else: current.add c yield current proc width(font: Font, text: seq[Rune], scale: float32): float32 = for i in 0 ..< text.len: if not (i == text.len - 1 and text[i].isWhiteSpace): if text[i] in font.advance: result += font.advance[text[i]] * scale else: result += font.advance[font.fallbackCharacter] * scale if i < text.len - 1: result += font.kerning.getOrDefault((text[i], text[i + 1]), 0) * scale return result * 0.5 / getAspectRatio() proc textDimension*(font: Font, text: seq[Rune], scale: float32): Vec2f = let nLines = text.countIt(it == Rune('\n')).float32 let h = (nLines * font.lineAdvance * scale + font.lineHeight * scale) * 0.5 let w = max(splitLines(text).toSeq.mapIt(width(font, it, scale))) return vec2(w, h) proc add*( glyphs: var Glyphs, text: seq[Rune], position: Vec3f, alignment: TextAlignment = Left, anchor: Vec2f = vec2(0, 1), scale: float32 = 1'f32, color: Vec4f = vec4(1, 1, 1, 1), ) = ## Add text for rendering. ## `position` is the display position, where as `(0, 0) is top-left and (1, 1) is bottom right. ## The z-compontent goes from 0 (near plane) to 1 (far plane) and is usually just used for ordering layers ## this should be called again after aspect ratio of window changes ## Anchor is the anchor to use inside the text assert text.len <= glyphs.position.len, &"Set {text.len} but Glyphs-object only supports {glyphs.position.len}" let globalScale = scale * glyphs.baseScale dim = textDimension(glyphs.font, text, globalScale) baselineStart = vec2(0, glyphs.font.ascent * globalScale * 0.5) pos = position.xy - anchor * dim + baselineStart # lineWidths need to be converted to NDC lineWidths = splitLines(text).toSeq.mapIt(width(glyphs.font, it, globalScale)) # also dimension must be in NDC maxWidth = dim.x var origin = vec3( pos.x * getAspectRatio() * 2'f32 - 1'f32, -(pos.y * 2'f32 - 1'f32), position.z ) cursorPos = origin lineI = 0 case alignment of Left: cursorPos.x = origin.x of Center: cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / 2) of Right: cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) * getAspectRatio() * 2 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]) / 2) of Right: cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) * getAspectRatio() * 2 cursorPos.y = cursorPos.y - glyphs.font.lineAdvance * globalScale else: if not text[i].isWhitespace(): glyphs.position[glyphs.cursor] = cursorPos glyphs.scale[glyphs.cursor] = globalScale 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]] * globalScale else: cursorPos.x = cursorPos.x + glyphs.font.advance[glyphs.font.fallbackCharacter] * globalScale if i < text.len - 1: cursorPos.x = cursorPos.x + glyphs.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale proc add*( glyphs: var Glyphs, text: string, position: Vec3f, alignment: TextAlignment = Left, anchor: Vec2f = vec2(0, 1), scale: float32 = 1'f32, color: Vec4f = vec4(1, 1, 1, 1), ) = add(glyphs, text.toRunes, position, alignment, anchor, scale, color) proc reset*(glyphs: var Glyphs) = glyphs.cursor = 0 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, fixedInstanceCount = glyphs.cursor, )