Mercurial > games > semicongine
changeset 731:c12b11a5f112
did: overhaul some of the mesh-data uploading and transformation handling, added: text/font rendering
author | Sam <sam@basx.dev> |
---|---|
date | Tue, 30 May 2023 16:58:14 +0700 |
parents | 88ab3e6dcaa0 |
children | dcc12ab20a91 |
files | src/semicongine/core/buildconfig.nim src/semicongine/core/fonttypes.nim src/semicongine/core/imagetypes.nim src/semicongine/core/matrix.nim src/semicongine/engine.nim src/semicongine/mesh.nim src/semicongine/renderer.nim src/semicongine/resources.nim src/semicongine/resources/font.nim src/semicongine/scene.nim src/semicongine/text.nim |
diffstat | 11 files changed, 265 insertions(+), 99 deletions(-) [+] |
line wrap: on
line diff
--- a/src/semicongine/core/buildconfig.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/core/buildconfig.nim Tue May 30 16:58:14 2023 +0700 @@ -5,7 +5,7 @@ import std/logging import std/os -const ENGINENAME = "semicongine" +const ENGINENAME* = "semicongine" const ENGINEVERSION* = static: var nimbleFile = newStringStream(staticRead("../../../semicongine.nimble")) var config = loadConfig(nimbleFile) @@ -52,5 +52,10 @@ const LOGLEVEL {.strdefine.}: string = (when DEBUG: "lvlAll" else: "lvlWarn") const ENGINE_LOGLEVEL* = parseEnum[Level](LOGLEVEL) -const RESOURCEROOT* {.strdefine.}: string = "resources" -const BUNDLETYPE* {.strdefine.}: string = "dir" # dir, zip, exe +# resource bundleing settings, need to be configured per project +const RESOURCEROOT* {.strdefine.}: string = "" # should be the "mod" directory +const BUNDLETYPE* {.strdefine.}: string = "" # dir, zip, exe + +static: + assert RESOURCEROOT != "", ENGINENAME & " requires -d:RESOURCEROOT=resources" + assert BUNDLETYPE in ["dir", "zip", "exe"], ENGINENAME & " requires one of -d:BUNDLETYPE=dir -d:BUNDLETYPE=zip -d:BUNDLETYPE=exe"
--- a/src/semicongine/core/fonttypes.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/core/fonttypes.nim Tue May 30 16:58:14 2023 +0700 @@ -1,12 +1,36 @@ import std/tables import std/unicode +import ./vulkanapi import ./imagetypes 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, + ) +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, + ) + + type + GlyphInfo* = object + uvs*: array[4, Vec2f] + dimension*: Vec2f + topOffset*: float32 + leftOffset*: float32 + advance*: float32 Font* = object name*: string # used to reference fontAtlas will be referenced in shader - characterUVs*: Table[Rune, array[4, Vec2f]] - characterDimensions*: Table[Rune, Vec2f] - fontAtlas*: Image + glyphs*: Table[Rune, GlyphInfo] + fontAtlas*: Texture + maxHeight*: int + kerning*: Table[(Rune, Rune), float32] + resolution*: float32 + fontscale*: float32
--- a/src/semicongine/core/imagetypes.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/core/imagetypes.nim Tue May 30 16:58:14 2023 +0700 @@ -11,7 +11,6 @@ minification*: VkFilter wrapModeS*: VkSamplerAddressMode wrapModeT*: VkSamplerAddressMode - filter*: VkFilter # TODO: replace with mag/minification Image* = ref ImageObject Texture* = object @@ -27,17 +26,6 @@ wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT, ) -proc newImage*(width, height: uint32, imagedata: seq[Pixel] = @[]): Image = - assert width > 0 and height > 0 - assert uint32(imagedata.len) == width * height or imagedata.len == 0 - - result = new Image - result.imagedata = (if imagedata.len == 0: newSeq[Pixel](width * height) else: imagedata) - assert width * height == uint32(result.imagedata.len) - - result.width = width - result.height = height - proc `[]`*(image: Image, x, y: uint32): Pixel = assert x < image.width assert y < image.height @@ -50,3 +38,19 @@ image[].imagedata[y * image.width + x] = value +const EMPTYPIXEL = [0'u8, 0'u8, 0'u8, 0'u8] +proc newImage*(width, height: uint32, imagedata: seq[Pixel] = @[], fill=EMPTYPIXEL): Image = + assert width > 0 and height > 0 + assert uint32(imagedata.len) == width * height or imagedata.len == 0 + + result = new Image + result.imagedata = (if imagedata.len == 0: newSeq[Pixel](width * height) else: imagedata) + assert width * height == uint32(result.imagedata.len) + + result.width = width + result.height = height + if fill != EMPTYPIXEL: + for y in 0 ..< height: + for x in 0 ..< width: + result[x, y] = fill +
--- a/src/semicongine/core/matrix.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/core/matrix.nim Tue May 30 16:58:14 2023 +0700 @@ -394,3 +394,14 @@ 0, 0, 1 / (zFar - zNear), -zNear / (zFar - zNear), 0, 0, 1, 1, ]) + +# create an orthographic perspective that will map from -1 .. 1 on all axis and keep a 1:1 aspect ratio +# the smaller dimension (width or height) will always be 1 and the larger dimension will be larger, to keep the ratio +func orthoWindowAspect*(windowAspect: float32): Mat4 = + if windowAspect > 1: + let space = 2 * (windowAspect - 1) / 2 + ortho(-1, 1, -1 - space, 1 + space, -1, 1) + else: + let space = 2 * (1 / windowAspect - 1) / 2 + ortho(-1 - space, 1 + space, -1, 1, -1, 1) +
--- a/src/semicongine/engine.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/engine.nim Tue May 30 16:58:14 2023 +0700 @@ -109,7 +109,7 @@ engine.renderer.get.destroy() engine.renderer = some(engine.device.initRenderer(renderPass)) -proc addScene*(engine: var Engine, scene: Scene, vertexInput: seq[ShaderAttribute], samplers: seq[ShaderAttribute], transformAttribute="") = +proc addScene*(engine: var Engine, scene: Scene, vertexInput: seq[ShaderAttribute], samplers: seq[ShaderAttribute], transformAttribute="transform") = assert engine.state != Destroyed assert transformAttribute == "" or transformAttribute in map(vertexInput, proc(a: ShaderAttribute): string = a.name) assert engine.renderer.isSome
--- a/src/semicongine/mesh.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/mesh.nim Tue May 30 16:58:14 2023 +0700 @@ -16,6 +16,8 @@ Big # up to 2^32 vertices Mesh* = ref object of Component instanceCount*: uint32 + instanceTransforms: seq[Mat4] # this should not reside in data["transform"], as we will use data["transform"] to store the final transformation matrix (as derived from the scene-tree) + dirtyInstanceTransforms: bool data: Table[string, DataList] changedAttributes: seq[string] case indexType*: MeshIndexType @@ -65,6 +67,11 @@ assert not (attribute in mesh.data) mesh.data[attribute] = data +proc setInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) = + assert uint32(data.len) == mesh.instanceCount + assert not (attribute in mesh.data) + mesh.data[attribute] = newDataList(data) + func newMesh*( positions: openArray[Vec3f], indices: openArray[array[3, uint32|int32|uint16|int16|int]], @@ -76,7 +83,7 @@ assert colors.len == 0 or colors.len == positions.len assert uvs.len == 0 or uvs.len == positions.len - result = Mesh(instanceCount: instanceCount) + result = Mesh(instanceCount: instanceCount, instanceTransforms: newSeqWith(int(instanceCount), Unit4F32)) setMeshData(result, "position", positions.toSeq) if colors.len > 0: setMeshData(result, "color", colors.toSeq) if uvs.len > 0: setMeshData(result, "uv", uvs.toSeq) @@ -101,6 +108,7 @@ result.indexType = Big for i, tri in enumerate(indices): result.bigIndices.add [uint32(tri[0]), uint32(tri[1]), uint32(tri[2])] + setInstanceData(result, "transform", newSeqWith(int(instanceCount), Unit4F32)) func newMesh*( positions: openArray[Vec3f], @@ -176,11 +184,6 @@ mesh.changedAttributes.add attribute appendValues(mesh.data[attribute], data) -proc setInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) = - assert uint32(data.len) == mesh.instanceCount - assert not (attribute in mesh.data) - mesh.data[attribute] = newDataList(data) - proc updateInstanceData*[T: GPUType|int|uint|float](mesh: var Mesh, attribute: string, data: seq[T]) = assert uint32(data.len) == mesh.instanceCount assert attribute in mesh.data @@ -215,7 +218,12 @@ v = transform * v func rect*(width=1'f32, height=1'f32, color="ffffffff"): Mesh = - result = Mesh(instanceCount: 1, indexType: Small, smallIndices: @[[0'u16, 1'u16, 2'u16], [2'u16, 3'u16, 0'u16]]) + result = Mesh( + instanceCount: 1, + indexType: Small, + smallIndices: @[[0'u16, 1'u16, 2'u16], [2'u16, 3'u16, 0'u16]], + instanceTransforms: @[Unit4F32] + ) let half_w = width / 2 @@ -226,19 +234,21 @@ setMeshData(result, "position", pos) setMeshData(result, "color", @[c, c, c, c]) setMeshData(result, "uv", @[newVec2f(0, 0), newVec2f(1, 0), newVec2f(1, 1), newVec2f(0, 1)]) + setInstanceData(result, "transform", @[Unit4F32]) func tri*(width=1'f32, height=1'f32, color="ffffffff"): Mesh = - result = Mesh(instanceCount: 1) + result = Mesh(instanceCount: 1, instanceTransforms: @[Unit4F32]) let half_w = width / 2 half_h = height / 2 colorVec = hexToColorAlpha(color) setMeshData(result, "position", @[newVec3f(0, -half_h), newVec3f( half_w, half_h), newVec3f(-half_w, half_h)]) setMeshData(result, "color", @[colorVec, colorVec, colorVec]) + setInstanceData(result, "transform", @[Unit4F32]) func circle*(width=1'f32, height=1'f32, nSegments=12'u16, color="ffffffff"): Mesh = assert nSegments >= 3 - result = Mesh(instanceCount: 1, indexType: Small) + result = Mesh(instanceCount: 1, indexType: Small, instanceTransforms: @[Unit4F32]) let half_w = width / 2 @@ -255,3 +265,24 @@ setMeshData(result, "position", pos) setMeshData(result, "color", col) + setInstanceData(result, "transform", @[Unit4F32]) + +proc areInstanceTransformsDirty*(mesh: var Mesh): bool = + result = mesh.dirtyInstanceTransforms + mesh.dirtyInstanceTransforms = false + +proc setInstanceTransform*(mesh: var Mesh, i: uint32, mat: Mat4) = + assert 0 <= i and i < mesh.instanceCount + mesh.instanceTransforms[i] = mat + mesh.dirtyInstanceTransforms = true + +proc setInstanceTransforms*(mesh: var Mesh, mat: seq[Mat4]) = + mesh.instanceTransforms = mat + mesh.dirtyInstanceTransforms = true + +proc getInstanceTransform*(mesh: Mesh, i: uint32): Mat4 = + assert 0 <= i and i < mesh.instanceCount + mesh.instanceTransforms[i] + +proc getInstanceTransforms*(mesh: Mesh): seq[Mat4] = + mesh.instanceTransforms
--- a/src/semicongine/renderer.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/renderer.nim Tue May 30 16:58:14 2023 +0700 @@ -52,7 +52,7 @@ raise newException(Exception, "Unable to create swapchain") result.swapchain = swapchain.get() -proc setupDrawableBuffers*(renderer: var Renderer, scene: Scene, inputs: seq[ShaderAttribute], samplers: seq[ShaderAttribute], transformAttribute="") = +proc setupDrawableBuffers*(renderer: var Renderer, scene: Scene, inputs: seq[ShaderAttribute], samplers: seq[ShaderAttribute], transformAttribute="transform") = assert not (scene in renderer.scenedata) const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment var data = SceneData() @@ -163,19 +163,6 @@ indexBufferOffset += size data.drawables[mesh] = drawable - #[ - # extract textures - var sampler = DefaultSampler() - sampler.magnification = VK_FILTER_NEAREST - sampler.minification = VK_FILTER_NEAREST - # for mesh in allComponentsOfType[Mesh](scene.root): - for textbox in allEntitiesOfType[Textbox](scene.root): - if not (textbox.font.name in data.textures): - data.textures[textbox.font.name] = @[ - renderer.device.uploadTexture(Texture(image: textbox.font.fontAtlas, sampler: sampler)) - ] - ]# - for material in scene.getMaterials(): for textureName, texture in material.textures.pairs: if not data.textures.hasKey(textureName): @@ -207,7 +194,12 @@ data.descriptorPool = renderer.device.createDescriptorSetPool(poolsizes) - data.descriptorSets[pipeline.vk] = pipeline.setupDescriptors(data.descriptorPool, data.uniformBuffers[pipeline.vk], data.textures, inFlightFrames=renderer.swapchain.inFlightFrames) + data.descriptorSets[pipeline.vk] = pipeline.setupDescriptors( + data.descriptorPool, + data.uniformBuffers.getOrDefault(pipeline.vk, @[]), + data.textures, + inFlightFrames=renderer.swapchain.inFlightFrames + ) for frame_i in 0 ..< renderer.swapchain.inFlightFrames: data.descriptorSets[pipeline.vk][frame_i].writeDescriptorSet() @@ -221,6 +213,7 @@ var (pdata, size) = mesh.getRawData(attribute) let memoryPerformanceHint = sceneData.attributeLocation[attribute] let bindingNumber = sceneData.attributeBindingNumber[attribute] + sceneData.vertexBuffers[memoryPerformanceHint].setData(pdata, size, sceneData.drawables[mesh].bufferOffsets[bindingNumber][2]) proc updateMeshData*(renderer: var Renderer, scene: Scene) = @@ -230,8 +223,11 @@ # if mesh transformation attribute is enabled, update the model matrix if renderer.scenedata[scene].transformAttribute != "": let transform = mesh.entity.getModelTransform() - if not (mesh in renderer.scenedata[scene].entityTransformationCache) or renderer.scenedata[scene].entityTransformationCache[mesh] != transform: - mesh.updateInstanceData(renderer.scenedata[scene].transformAttribute, @[transform]) + if not (mesh in renderer.scenedata[scene].entityTransformationCache) or renderer.scenedata[scene].entityTransformationCache[mesh] != transform or mesh.areInstanceTransformsDirty: + var updatedTransform = newSeq[Mat4](int(mesh.instanceCount)) + for i in 0 ..< mesh.instanceCount: + updatedTransform[i] = transform * mesh.getInstanceTransform(i) + mesh.updateInstanceData(renderer.scenedata[scene].transformAttribute, updatedTransform) renderer.scenedata[scene].entityTransformationCache[mesh] = transform # update any changed mesh attributes @@ -246,7 +242,7 @@ for i in 0 ..< renderer.renderPass.subpasses.len: for pipeline in renderer.renderPass.subpasses[i].pipelines.mitems: - if renderer.scenedata[scene].uniformBuffers[pipeline.vk].len != 0: + if renderer.scenedata[scene].uniformBuffers.hasKey(pipeline.vk) and renderer.scenedata[scene].uniformBuffers[pipeline.vk].len != 0: assert renderer.scenedata[scene].uniformBuffers[pipeline.vk][renderer.swapchain.currentInFlight].vk.valid var offset = 0'u64 for uniform in pipeline.uniforms:
--- a/src/semicongine/resources.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/resources.nim Tue May 30 16:58:14 2023 +0700 @@ -127,9 +127,12 @@ else: raise newException(Exception, "Unsupported audio file type: " & path) -proc loadFont*(path: string, name: string): Font = - let defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=+[{]};:,<.>/?".toRunes() - loadResource_intern(path).readTrueType(name, defaultCharset) +proc loadFont*(path: string, name="", color=newVec4f(1, 1, 1, 1), resolution=100'f32): Font = + var thename = name + if thename == "": + thename = path.splitFile().name + let defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=+[{]};:,<.>/? ".toRunes() + loadResource_intern(path).readTrueType(name, defaultCharset, color, resolution) proc loadMesh*(path: string): Entity = loadResource_intern(path).readglTF()[0].root
--- a/src/semicongine/resources/font.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/resources/font.nim Tue May 30 16:58:14 2023 +0700 @@ -1,5 +1,5 @@ -import std/strformat import std/tables +import std/math import std/streams import std/os import std/unicode @@ -16,57 +16,88 @@ stbtt_fontinfo {.importc, incompleteStruct .} = object proc stbtt_InitFont(info: ptr stbtt_fontinfo, data: ptr char, offset: cint): cint {.importc, nodecl.} -proc stbtt_ScaleForPixelHeight(info: ptr stbtt_fontinfo, pixels: float): cfloat {.importc, nodecl.} -proc stbtt_GetCodepointBitmap(info: ptr stbtt_fontinfo, scale_x: cfloat, scale_y: cfloat, codepoint: cint, width: ptr cint, height: ptr cint, xoff: ptr cint, yoff: ptr cint): cstring {.importc, nodecl.} -# proc free(p: pointer) {.importc.} +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_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 readTrueType*(stream: Stream, name: string, codePoints: seq[Rune]): Font = +proc free(p: pointer) {.importc.} + +proc readTrueType*(stream: Stream, name: string, codePoints: seq[Rune], color: Vec4f, resolution: float32): Font = var indata = stream.readAll() fontinfo: stbtt_fontinfo if stbtt_InitFont(addr fontinfo, addr indata[0], 0) == 0: raise newException(Exception, "An error occured while loading PNG file") - let fontheight = stbtt_ScaleForPixelHeight(addr fontinfo, 100) + result.resolution = resolution + result.fontscale = float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(resolution))) var - charOffset: Table[Rune, uint32] offsetX: uint32 - maxheight: uint32 bitmaps: Table[Rune, (cstring, cint, cint)] - baselines: Table[Rune, int] + topOffsets: Table[Rune, int] for codePoint in codePoints: var width, height: cint - leftStart, baseline: cint + offX, offY: cint data = stbtt_GetCodepointBitmap( addr fontinfo, - 0, fontheight, - cint('a'), + result.fontscale, result.fontscale, + cint(codePoint), addr width, addr height, - addr leftStart, addr baseline + addr offX, addr offY ) bitmaps[codePoint] = (data, width, height) - maxheight = max(maxheight, uint32(height)) - charOffset[codePoint] = offsetX - offsetX += uint32(width) - baselines[codePoint] = baseline + result.maxHeight = max(result.maxHeight, int(height)) + offsetX += uint32(width + 1) + topOffsets[codePoint] = offY result.name = name - result.fontAtlas = newImage(offsetX, maxheight) + result.fontAtlas = Texture( + name: name & "_texture", + image: newImage(offsetX, uint32(result.maxHeight + 1)), + sampler: FONTSAMPLER_SOFT + ) offsetX = 0 for codePoint in codePoints: - let d = bitmaps[codePoint][0] - let width = uint32(bitmaps[codePoint][1]) - let height = uint32(bitmaps[codePoint][2]) + let + bitmap = bitmaps[codePoint][0] + width = uint32(bitmaps[codePoint][1]) + height = uint32(bitmaps[codePoint][2]) + + # bitmap data for y in 0 ..< height: for x in 0 ..< width: - result.fontAtlas[x + offsetX, y] = [255'u8, 255'u8, 255'u8, uint8(d[y * width + x])] - result.characterDimensions[codePoint] = newVec2f(float32(width), float32(height)) - result.characterUVs[codePoint] = [ - newVec2f(float32(offsetX) / float32(result.fontAtlas.width), 0), - newVec2f(float32(offsetX + width) / float32(result.fontAtlas.width), 0), - newVec2f(float32(offsetX) / float32(result.fontAtlas.width), 1), - newVec2f(float32(offsetX + width) / float32(result.fontAtlas.width), 1), - ] - offsetX += width + let value = float32(bitmap[y * width + x]) + result.fontAtlas.image[x + offsetX, y] = [ + uint8(round(color.r * 255'f32)), + uint8(round(color.g * 255'f32)), + uint8(round(color.b * 255'f32)), + uint8(round(color.a * value)) + ] + + # horizontal spaces: + var advance, leftBearing: cint + stbtt_GetCodepointHMetrics(addr fontinfo, cint(codePoint), addr advance, addr leftBearing) + + result.glyphs[codePoint] = GlyphInfo( + dimension: newVec2f(float32(width), float32(height)), + uvs: [ + newVec2f(float32(offsetX) / float32(result.fontAtlas.image.width), int(height) / result.maxHeight), + newVec2f(float32(offsetX) / float32(result.fontAtlas.image.width), 0), + newVec2f(float32(offsetX + width) / float32(result.fontAtlas.image.width), 0), + newVec2f(float32(offsetX + width) / float32(result.fontAtlas.image.width), int(height) / result.maxHeight), + ], + topOffset: float32(topOffsets[codePoint]), + leftOffset: float32(leftBearing) * result.fontscale, + advance: float32(advance) * result.fontscale, + ) + offsetX += width + 1 + free(bitmap) + for codePointAfter in codePoints: + result.kerning[(codePoint, codePointAfter)] = float32(stbtt_GetCodepointKernAdvance( + addr fontinfo, + cint(codePoint), + cint(codePointAfter) + )) * result.fontscale
--- a/src/semicongine/scene.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/scene.nim Tue May 30 16:58:14 2023 +0700 @@ -31,6 +31,7 @@ components*: seq[Component] func getModelTransform*(entity: Entity): Mat4 = + assert not entity.isNil result = Unit4 var currentEntity = entity while currentEntity != nil: @@ -156,7 +157,7 @@ iterator allEntitiesOfType*[T: Entity](root: Entity): T = var queue = @[root] while queue.len > 0: - let entity = queue.pop + var entity = queue.pop if entity of T: yield T(entity) for i in countdown(entity.children.len - 1, 0):
--- a/src/semicongine/text.nim Sun May 28 18:36:11 2023 +0700 +++ b/src/semicongine/text.nim Tue May 30 16:58:14 2023 +0700 @@ -1,3 +1,7 @@ +import std/sequtils +import std/tables +import std/unicode + import ./scene import ./mesh import ./core/vector @@ -10,26 +14,82 @@ Center Right Textbox* = ref object of Entity - columns*: uint32 - rows*: uint32 - text*: string + maxLen*: uint32 + text: seq[Rune] + dirty: bool alignment*: TextAlignment font*: Font - lettermesh*: Mesh + mesh*: Mesh + +proc updateMesh(textbox: var Textbox) = + + # pre-calculate text-width + var width = 0'f32 + for i in 0 ..< min(uint32(textbox.text.len), textbox.maxLen): + width += textbox.font.glyphs[textbox.text[i]].advance + if i < uint32(textbox.text.len - 1): + width += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])] + + let centerX = width / 2 + let centerY = textbox.font.maxHeight / 2 -func len*(textbox: Textbox): uint32 = - textbox.columns * textbox.rows + var offsetX = 0'f32 + for i in 0 ..< textbox.maxLen: + let vertexOffset = i * 4 + if i < uint32(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 + + textbox.mesh.updateMeshData("position", vertexOffset + 0, newVec3f(left - centerX, bottom + centerY)) + textbox.mesh.updateMeshData("position", vertexOffset + 1, newVec3f(left - centerX, top + centerY)) + textbox.mesh.updateMeshData("position", vertexOffset + 2, newVec3f(right - centerX, top + centerY)) + textbox.mesh.updateMeshData("position", vertexOffset + 3, newVec3f(right - centerX, bottom + centerY)) + + textbox.mesh.updateMeshData("uv", vertexOffset + 0, glyph.uvs[0]) + textbox.mesh.updateMeshData("uv", vertexOffset + 1, glyph.uvs[1]) + textbox.mesh.updateMeshData("uv", vertexOffset + 2, glyph.uvs[2]) + textbox.mesh.updateMeshData("uv", vertexOffset + 3, glyph.uvs[3]) -proc newTextbox*(columns, rows: uint32, font: Font, text=""): Textbox = - result = Textbox(columns: columns, rows: rows, text: text, font: font) - result.lettermesh = newMesh( - positions = [newVec3f(0, 0), newVec3f(0, 1), newVec3f(1, 1), newVec3f(1, 0)], - indices = [[0'u16, 1'u16, 2'u16], [0'u16, 0'u16, 0'u16]], - uvs = [newVec2f(0, 0), newVec2f(0, 1), newVec2f(1, 1), newVec2f(1, 0)], - instanceCount = result.len, - ) - var transforms = newSeq[Mat4](result.len) - for i in 0 ..< result.len: - transforms[i] = Unit4f32 - setInstanceData(result.lettermesh, "transform", transforms) - result.components.add result.lettermesh + offsetX += glyph.advance + if i < uint32(textbox.text.len - 1): + offsetX += textbox.font.kerning[(textbox.text[i], textbox.text[i + 1])] + else: + textbox.mesh.updateMeshData("position", vertexOffset + 0, newVec3f()) + textbox.mesh.updateMeshData("position", vertexOffset + 1, newVec3f()) + textbox.mesh.updateMeshData("position", vertexOffset + 2, newVec3f()) + textbox.mesh.updateMeshData("position", vertexOffset + 3, newVec3f()) + + +func text*(textbox: Textbox): seq[Rune] = + textbox.text + +proc `text=`*(textbox: var Textbox, text: seq[Rune]) = + textbox.text = text + textbox.name = $text + textbox.updateMesh() + +proc newTextbox*(maxLen: uint32, font: Font, text=toRunes("")): Textbox = + var + positions = newSeq[Vec3f](int(maxLen * 4)) + indices: seq[array[3, uint32]] + uvs = newSeq[Vec2f](int(maxLen * 4)) + for i in 0 ..< maxLen: + let offset = i * 4 + indices.add [[offset + 0, offset + 1, offset + 2], [offset + 2, offset + 3, offset + 0]] + + result = Textbox(maxLen: maxLen, text: text, font: font, dirty: true) + result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs) + result.mesh.setInstanceTransforms(@[Unit4F32]) + result.name = $text + result.transform = Unit4F32 + + # wrap the text mesh in a new entity to preserve the font-scaling + var box = newEntity("box", result.mesh) + # box.transform = scale3d(font.fontscale * 0.002, font.fontscale * 0.002) + box.transform = scale3d(1 / font.resolution, 1 / font.resolution) + result.add box + result.updateMesh()