Mercurial > games > semicongine
changeset 876:164b41a2d5a6
add: font/text improvments, support for newline rendering
author | Sam <sam@basx.dev> |
---|---|
date | Sat, 27 Jan 2024 00:31:11 +0700 |
parents | a9d2f56556c5 |
children | 773af36148bd |
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)