Mercurial > games > semicongine
changeset 1475:e4eed5f9ac33 default tip
did: cleanup text/font code
author | sam <sam@basx.dev> |
---|---|
date | Mon, 07 Apr 2025 23:58:41 +0700 |
parents | bb7bbe2fee78 |
children | |
files | semicongine/core/types.nim semicongine/fonts.nim semicongine/text.nim semicongine/text/textbox.nim |
diffstat | 4 files changed, 89 insertions(+), 298 deletions(-) [+] |
line wrap: on
line diff
--- a/semicongine/core/types.nim Sun Apr 06 21:56:10 2025 +0700 +++ b/semicongine/core/types.nim Mon Apr 07 23:58:41 2025 +0700 @@ -337,73 +337,6 @@ ImageArray*[T: PixelType] = ImageObject[T, true] # === fonts === - GlyphQuad*[MaxGlyphs: static int] = object - # vertex offsets to glyph center: [left, bottom, right, top] - pos*: array[MaxGlyphs, Vec4f] - uv*: array[MaxGlyphs, Vec4f] # [left, bottom, right, top] - - TextRendering* = object - aspectRatio*: float32 - - GlyphDescriptorSet*[MaxGlyphs: static int] = object - fontAtlas*: Image[Gray] - glyphquads*: GPUValue[GlyphQuad[MaxGlyphs], StorageBuffer] - - GlyphShader*[MaxGlyphs: 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: 3.}: GlyphDescriptorSet[MaxGlyphs] - 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; - -void main() { - int vertexI = indices[gl_VertexIndex]; - vec3 vertexPos = vec3( - glyphquads.pos[glyphIndex][i_x[vertexI]] * scale / textRendering.aspectRatio, - glyphquads.pos[glyphIndex][i_y[vertexI]] * scale, - 0 - ); - // the epsilon-offset is necessary, as otherwise characters with the same Z might overlap, despite transparency - gl_Position = vec4(vertexPos + position, 1.0); - gl_Position.z -= gl_InstanceIndex * epsilon; - gl_Position.z = fract(abs(gl_Position.z)); - 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); -}""" - - FontObj*[MaxGlyphs: static int] = object - advance*: Table[Rune, float32] - kerning*: Table[(Rune, Rune), float32] - leftBearing*: Table[Rune, float32] - lineAdvance*: float32 - lineHeight*: float32 # like lineAdvance - lineGap - ascent*: float32 # from baseline to highest glyph - descent*: float32 # from baseline to lowest glyph - xHeight*: float32 # from baseline to height of lowercase x - descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]] - descriptorGlyphIndex*: Table[Rune, uint16] - descriptorGlyphIndexRev*: Table[uint16, Rune] # only used for debugging atm - fallbackCharacter*: Rune - - Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs] - TextHandle* = object index*: uint32 generation*: uint32 @@ -423,17 +356,6 @@ color*: Vec4f = vec4(1, 1, 1, 1) capacity*: int - TextBuffer*[MaxGlyphs: static int] = object - cursor*: int - generation*: uint32 - font*: Font[MaxGlyphs] - baseScale*: float32 - position*: GPUArray[Vec3f, VertexBufferMapped] - color*: GPUArray[Vec4f, VertexBufferMapped] - scale*: GPUArray[float32, VertexBufferMapped] - glyphIndex*: GPUArray[uint16, VertexBufferMapped] - texts*: seq[Text] - # === background loader thread === LoaderThreadArgs*[T] = ( ptr Channel[(string, string)], @@ -521,10 +443,3 @@ proc `=copy`[S, T](dest: var ImageObject[S, T], source: ImageObject[S, T]) {.error.} proc `=copy`(dest: var Input, source: Input) {.error.} proc `=copy`(dest: var EngineObj, source: EngineObj) {.error.} -proc `=copy`[MaxGlyphs: static int]( - dest: var FontObj[MaxGlyphs], source: FontObj[MaxGlyphs] -) {.error.} - -proc `=copy`[MaxGlyphs: static int]( - dest: var TextBuffer[MaxGlyphs], source: TextBuffer[MaxGlyphs] -) {.error.}
--- a/semicongine/fonts.nim Sun Apr 06 21:56:10 2025 +0700 +++ b/semicongine/fonts.nim Mon Apr 07 23:58:41 2025 +0700 @@ -22,6 +22,36 @@ const ASCII_CHARSET = PrintableChars.toSeq.toRunes +type + GlyphQuad*[MaxGlyphs: static int] = object + # vertex offsets to glyph center: [left, bottom, right, top] + pos*: array[MaxGlyphs, Vec4f] + uv*: array[MaxGlyphs, Vec4f] # [left, bottom, right, top] + + GlyphDescriptorSet*[MaxGlyphs: static int] = object + fontAtlas*: Image[Gray] + glyphquads*: GPUValue[GlyphQuad[MaxGlyphs], StorageBuffer] + + FontObj*[MaxGlyphs: static int] = object + advance*: Table[Rune, float32] + kerning*: Table[(Rune, Rune), float32] + leftBearing*: Table[Rune, float32] + lineAdvance*: float32 + lineHeight*: float32 # like lineAdvance - lineGap + ascent*: float32 # from baseline to highest glyph + descent*: float32 # from baseline to lowest glyph + xHeight*: float32 # from baseline to height of lowercase x + descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]] + descriptorGlyphIndex*: Table[Rune, uint16] + descriptorGlyphIndexRev*: Table[uint16, Rune] # only used for debugging atm + fallbackCharacter*: Rune + + Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs] + +proc `=copy`[MaxGlyphs: static int]( + dest: var FontObj[MaxGlyphs], source: FontObj[MaxGlyphs] +) {.error.} + type stbtt_fontinfo {.importc, incompleteStruct.} = object proc stbtt_InitFont(
--- a/semicongine/text.nim Sun Apr 06 21:56:10 2025 +0700 +++ b/semicongine/text.nim Mon Apr 07 23:58:41 2025 +0700 @@ -11,6 +11,65 @@ import ./images import ./rendering/renderer import ./rendering/memory +import ./fonts + +type + TextRendering* = object + aspectRatio*: float32 + + GlyphShader*[MaxGlyphs: 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: 3.}: GlyphDescriptorSet[MaxGlyphs] + 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; + + void main() { + int vertexI = indices[gl_VertexIndex]; + vec3 vertexPos = vec3( + glyphquads.pos[glyphIndex][i_x[vertexI]] * scale / textRendering.aspectRatio, + glyphquads.pos[glyphIndex][i_y[vertexI]] * scale, + 0 + ); + // the epsilon-offset is necessary, as otherwise characters with the same Z might overlap, despite transparency + gl_Position = vec4(vertexPos + position, 1.0); + gl_Position.z -= gl_InstanceIndex * epsilon; + gl_Position.z = fract(abs(gl_Position.z)); + 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); + }""" + + TextBuffer*[MaxGlyphs: static int] = object + cursor*: int + generation*: uint32 + font*: Font[MaxGlyphs] + baseScale*: float32 + position*: GPUArray[Vec3f, VertexBufferMapped] + color*: GPUArray[Vec4f, VertexBufferMapped] + scale*: GPUArray[float32, VertexBufferMapped] + glyphIndex*: GPUArray[uint16, VertexBufferMapped] + texts*: seq[Text] + +proc `=copy`[MaxGlyphs: static int]( + dest: var TextBuffer[MaxGlyphs], source: TextBuffer[MaxGlyphs] +) {.error.} proc initTextBuffer*[MaxGlyphs: static int]( font: Font[MaxGlyphs],
--- a/semicongine/text/textbox.nim Sun Apr 06 21:56:10 2025 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,213 +0,0 @@ -type Textbox* = object - font*: Font - maxLen*: int # maximum amount of characters that will be rendered - maxWidth: float32 = 0 # if set, will cause automatic word breaks at maxWidth - baseScale: float32 - text: seq[Rune] - horizontalAlignment: HorizontalAlignment = Center - verticalAlignment: VerticalAlignment = Center - # management/internal: - dirtyGeometry: bool # is true if any of the attributes changed - dirtyShaderdata: bool # is true if any of the attributes changed - visibleText: seq[Rune] - # used to store processed (word-wrapper) text to preserve original - lastRenderedText: seq[Rune] - # stores the last rendered text, to prevent unnecessary updates - - # rendering data - position: GPUArray[Vec3f, VertexBuffer] - uv: GPUArray[Vec2f, VertexBuffer] - indices: GPUArray[uint16, IndexBuffer] - -proc `=copy`(dest: var Textbox, source: Textbox) {.error.} - -func `$`*(textbox: Textbox): string = - "\"" & $textbox.text[0 ..< min(textbox.text.len, 16)] & "\"" - -proc refreshGeometry(textbox: var Textbox) = - # pre-calculate text-width - var width = 0'f32 - var lineWidths: seq[float32] - for i in 0 ..< textbox.visibleText.len: - if textbox.visibleText[i] == NEWLINE: - lineWidths.add width - width = 0'f32 - else: - if not (i == textbox.visibleText.len - 1 and textbox.visibleText[i].isWhiteSpace): - 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])] - lineWidths.add width - var height = - float32(lineWidths.len - 1) * textbox.font.lineAdvance + textbox.font.capHeight - if lineWidths[^1] == 0 and lineWidths.len > 1: - height -= 1 - - let anchorY = - ( - case textbox.verticalAlignment - of Top: 0'f32 - of Center: -height / 2 - of Bottom: -height - ) + textbox.font.capHeight - - var - offsetX = 0'f32 - offsetY = 0'f32 - lineIndex = 0 - anchorX = - case textbox.horizontalAlignment - of Left: - 0'f32 - of Center: - lineWidths[lineIndex] / 2 - of Right: - lineWidths[lineIndex] - for i in 0 ..< textbox.maxLen: - let vertexOffset = i * 4 - if i < textbox.visibleText.len: - if textbox.visibleText[i] == Rune('\n'): - offsetX = 0 - offsetY -= textbox.font.lineAdvance - textbox.position.data[vertexOffset + 0] = vec3(0, 0, 0) - textbox.position.data[vertexOffset + 1] = vec3(0, 0, 0) - textbox.position.data[vertexOffset + 2] = vec3(0, 0, 0) - textbox.position.data[vertexOffset + 3] = vec3(0, 0, 0) - inc lineIndex - anchorX = - case textbox.horizontalAlignment - of Left: - 0'f32 - of Center: - lineWidths[lineIndex] / 2 - of Right: - lineWidths[lineIndex] - else: - let - glyph = textbox.font.glyphdata[textbox.visibleText[i]] - left = offsetX + glyph.offsetX - right = offsetX + glyph.offsetX + glyph.dimension.x - top = offsetY - glyph.offsetY - bottom = offsetY - glyph.offsetY - glyph.dimension.y - - textbox.position.data[vertexOffset + 0] = - vec3(left - anchorX, bottom - anchorY, 0) - textbox.position.data[vertexOffset + 1] = vec3(left - anchorX, top - anchorY, 0) - textbox.position.data[vertexOffset + 2] = - vec3(right - anchorX, top - anchorY, 0) - textbox.position.data[vertexOffset + 3] = - vec3(right - anchorX, bottom - anchorY, 0) - - textbox.uv.data[vertexOffset + 0] = glyph.uvs[0] - textbox.uv.data[vertexOffset + 1] = glyph.uvs[1] - textbox.uv.data[vertexOffset + 2] = glyph.uvs[2] - textbox.uv.data[vertexOffset + 3] = glyph.uvs[3] - - offsetX += glyph.advance - if i < textbox.visibleText.len - 1: - offsetX += - textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])] - updateGPUBuffer(textbox.position, count = textbox.visibleText.len.uint64 * 4) - updateGPUBuffer(textbox.uv, count = textbox.visibleText.len.uint64 * 4) - textbox.lastRenderedText = textbox.visibleText - -func text*(textbox: Textbox): seq[Rune] = - textbox.text - -proc `text=`*(textbox: var Textbox, newText: seq[Rune]) = - if newText[0 ..< min(newText.len, textbox.maxLen)] == textbox.text: - return - - textbox.text = newText[0 ..< min(newText.len, textbox.maxLen)] - - textbox.visibleText = textbox.text - if textbox.maxWidth > 0: - textbox.visibleText = WordWrapped( - textbox.visibleText, textbox.font[], textbox.maxWidth / textbox.baseScale - ) - -proc `text=`*(textbox: var Textbox, newText: string) = - `text=`(textbox, newText.toRunes) - -proc horizontalAlignment*(textbox: Textbox): HorizontalAlignment = - textbox.horizontalAlignment - -proc `horizontalAlignment=`*(textbox: var Textbox, value: HorizontalAlignment) = - if value != textbox.horizontalAlignment: - textbox.horizontalAlignment = value - textbox.dirtyGeometry = true - -proc verticalAlignment*(textbox: Textbox): VerticalAlignment = - textbox.verticalAlignment - -proc `verticalAlignment=`*(textbox: var Textbox, value: VerticalAlignment) = - if value != textbox.verticalAlignment: - textbox.verticalAlignment = value - textbox.dirtyGeometry = true - -proc refresh*(textbox: var Textbox) = - if textbox.dirtyGeometry or textbox.visibleText != textbox.lastRenderedText: - textbox.refreshGeometry() - textbox.dirtyGeometry = false - -proc render*( - commandbuffer: VkCommandBuffer, - pipeline: Pipeline, - textbox: Textbox, - position: Vec3f, - color: Vec4f, - scale: Vec2f = vec2(1, 1), -) = - renderWithPushConstant( - commandbuffer = commandbuffer, - pipeline = pipeline, - mesh = textbox, - pushConstant = - TextboxData(position: position, scale: textbox.baseScale * scale, color: color), - fixedVertexCount = textbox.visibleText.len * 6, - ) - -proc initTextbox*[T: string | seq[Rune]]( - renderdata: var RenderData, - descriptorSetLayout: VkDescriptorSetLayout, - font: Font, - baseScale: float32, - text: T = default(T), - maxLen: int = text.len, - verticalAlignment: VerticalAlignment = Center, - horizontalAlignment: HorizontalAlignment = Center, - maxWidth = 0'f32, -): Textbox = - result = Textbox( - maxLen: maxLen, - font: font, - dirtyGeometry: true, - dirtyShaderdata: true, - horizontalAlignment: horizontalAlignment, - verticalAlignment: verticalAlignment, - maxWidth: maxWidth, - baseScale: baseScale, - position: asGPUArray(newSeq[Vec3f](int(maxLen * 4)), VertexBuffer), - uv: asGPUArray(newSeq[Vec2f](int(maxLen * 4)), VertexBuffer), - indices: asGPUArray(newSeq[uint16](int(maxLen * 6)), IndexBuffer), - ) - - for i in 0 ..< maxLen: - let vertexIndex = i.uint16 * 4'u16 - result.indices.data[i * 6 + 0] = vertexIndex + 0 - result.indices.data[i * 6 + 1] = vertexIndex + 1 - result.indices.data[i * 6 + 2] = vertexIndex + 2 - result.indices.data[i * 6 + 3] = vertexIndex + 2 - result.indices.data[i * 6 + 4] = vertexIndex + 3 - result.indices.data[i * 6 + 5] = vertexIndex + 0 - - when T is string: - `text=`(result, text.toRunes()) - else: - `text=`(result, text) - - assignBuffers(renderdata, result, uploadData = false) - - result.refresh() - updateAllGPUBuffers(result, flush = true)