Mercurial > games > semicongine
changeset 1429:ab01c577d91c
fix: some more import-stuff
author | sam <sam@basx.dev> |
---|---|
date | Sat, 11 Jan 2025 15:34:21 +0700 |
parents | d16964858790 |
children | db3af8a0b86b |
files | semicongine.nim semicongine/audio.nim semicongine/contrib/algorithms/texture_packing.nim semicongine/core.nim semicongine/font.nim semicongine/fonts.nim semicongine/gltf.nim semicongine/image.nim semicongine/images.nim semicongine/loaders.nim semicongine/rendering.nim semicongine/text.nim tests/test_text.nim |
diffstat | 13 files changed, 497 insertions(+), 485 deletions(-) [+] |
line wrap: on
line diff
--- a/semicongine.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine.nim Sat Jan 11 15:34:21 2025 +0700 @@ -8,6 +8,8 @@ from ./semicongine/audio import audioWorker from ./semicongine/background_loaders import initBackgroundLoader import ./semicongine/loaders +import ./semicongine/images +import ./semicongine/audio proc initEngine*(appName: string) = ## Required to be called before most features of the engine can be used
--- a/semicongine/audio.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/audio.nim Sat Jan 11 15:34:21 2025 +0700 @@ -5,11 +5,13 @@ import std/monotimes import std/os import std/strformat +import std/streams +import std/strutils import std/tables import std/times -import std/streams import ./core +import ./resources const NBUFFERS = 32 # it seems that some alsa hardware has a problem with smaller buffers than 512 @@ -422,3 +424,11 @@ "Only support mono and stereo audio at the moment (1 or 2 channels), but found " & $channels, ) + +proc loadAudio*(path: string, package = DEFAULT_PACKAGE): SoundData {.gcsafe.} = + if path.splitFile().ext.toLowerAscii == ".au": + loadResource_intern(path, package = package).readAU() + elif path.splitFile().ext.toLowerAscii == ".ogg": + loadResource_intern(path, package = package).readVorbis() + else: + raise newException(Exception, "Unsupported audio file type: " & path)
--- a/semicongine/contrib/algorithms/texture_packing.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/contrib/algorithms/texture_packing.nim Sat Jan 11 15:34:21 2025 +0700 @@ -2,7 +2,7 @@ import std/strformat import ../../core -import ../../image +import ../../images type Rect = tuple[i: int, x, y, w, h: uint32]
--- a/semicongine/core.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/core.nim Sat Jan 11 15:34:21 2025 +0700 @@ -30,3 +30,7 @@ assert engine_obj_internal != nil, "Engine has not been initialized yet" assert engine_obj_internal.initialized, "Engine has not been initialized yet" return engine_obj_internal + +proc getAspectRatio*(): float32 = + assert engine().vulkan.swapchain != nil, "Swapchain has not been initialized yet" + engine().vulkan.swapchain.width.float32 / engine().vulkan.swapchain.height.float32
--- a/semicongine/font.nim Sat Jan 11 14:15:29 2025 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,252 +0,0 @@ -import std/os -import std/strutils -import std/strformat -import std/sequtils -import std/unicode -import std/streams -import std/logging -import std/tables - -import ./core -import ./resources -import ./rendering/renderer -import ./rendering/memory -import ./contrib/algorithms/texture_packing - -{.emit: "#define STBTT_STATIC".} -{.emit: "#define STB_TRUETYPE_IMPLEMENTATION".} -{. - emit: - "#include \"" & currentSourcePath.parentDir() & "/thirdparty/stb/stb_truetype.h\"" -.} - -const ASCII_CHARSET = PrintableChars.toSeq.toRunes - -type 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: 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_GetCodepointBox( - info: ptr stbtt_fontinfo, codepoint: cint, x0, y0, x1, y1: ptr cint -): 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 readTrueType[N: static int]( - stream: Stream, name: string, codePoints: seq[Rune], lineHeightPixels: float32 -): Font[N] = - assert codePoints.len <= N, - "asked for " & $codePoints.len & " glyphs but shader is only configured for " & $N - - result = Font[N]() - - var - indata = stream.readAll() - fi: stbtt_fontinfo - if stbtt_InitFont(addr fi, indata.ToCPointer, 0) == 0: - raise newException(Exception, "An error occured while loading font file") - - let - glyph2bitmapScale = - float32(stbtt_ScaleForPixelHeight(addr fi, cfloat(lineHeightPixels))) - glyph2QuadScale = glyph2bitmapScale / lineHeightPixels - - # ensure all codepoints are available in the font - for codePoint in codePoints: - if stbtt_FindGlyphIndex(addr fi, cint(codePoint)) == 0: - warn &"Loading font {name}: Codepoint '{codePoint}' ({cint(codePoint)}) has no glyph" - - var - 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: - offsetX[codePoint] = 0 - offsetY[codePoint] = 0 - var width, height: cint - let data = stbtt_GetCodepointBitmap( - addr fi, - glyph2bitmapScale, - glyph2bitmapScale, - cint(codePoint), - addr width, - addr height, - addr (offsetX[codePoint]), - addr (offsetY[codePoint]), - ) - - if width > 0 and height > 0: - var bitmap = newSeq[Gray](width * height) - for i in 0 ..< width * height: - bitmap[i] = vec1u8(data[i].uint8) - bitmaps.add Image[Gray](width: width.uint32, height: height.uint32, data: bitmap) - else: - bitmaps.add Image[Gray](width: 1, height: 1, data: @[vec1u8(0)]) - - nativeFree(data) - - # generate glyph atlas from bitmaps - let packed = pack(bitmaps) - result.descriptorSet.data.fontAtlas = packed.atlas - - # generate quad-information for use in shader - for i in 0 ..< codePoints.len: - let codePoint = codePoints[i] - var advanceUnscaled, leftBearingUnscaled: cint - # is in glyph-space, needs to be scaled to pixel-space - stbtt_GetCodepointHMetrics( - addr fi, cint(codePoint), addr advanceUnscaled, addr leftBearingUnscaled - ) - result.leftBearing[codePoint] = leftBearingUnscaled.float32 * glyph2QuadScale - result.advance[codePoint] = advanceUnscaled.float32 * glyph2QuadScale - - 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) - # divide by lineHeightPixels to get from pixel-space to quad-geometry-space - left = - result.leftBearing[codePoint] + offsetX[codePoint].float32 / lineHeightPixels - right = left + bitmapW / lineHeightPixels - top = -offsetY[codePoint].float32 / lineHeightPixels - bottom = top - bitmapH / lineHeightPixels - - template glyphquads(): untyped = - result.descriptorSet.data.glyphquads.data - - glyphquads.pos[i] = vec4(left, bottom, right, top) - 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 - result.descriptorGlyphIndexRev[i.uint16] = codePoint # only used for debugging atm - - # kerning - for codePointAfter in codePoints: - result.kerning[(codePoint, codePointAfter)] = - float32( - stbtt_GetCodepointKernAdvance(addr fi, cint(codePoint), cint(codePointAfter)) - ) * glyph2QuadScale - - # line spacing - var ascent, descent, lineGap: cint - stbtt_GetFontVMetrics(addr fi, addr ascent, addr descent, addr lineGap) - result.lineAdvance = float32(ascent - descent + lineGap) * glyph2QuadScale - result.lineHeight = float32(ascent - descent) * glyph2QuadScale # should be 1 - result.ascent = float32(ascent) * glyph2QuadScale - result.descent = float32(descent) * glyph2QuadScale - - var x0, y0, x1, y1: cint - discard - stbtt_GetCodepointBox(addr fi, cint(Rune('x')), addr x0, addr y0, addr x1, addr y1) - result.xHeight = float32(y1 - y0) * glyph2QuadScale - -proc loadFont*[N: static int]( - path: string, - lineHeightPixels = 80'f32, - additional_codepoints: openArray[Rune] = [], - charset = ASCII_CHARSET, - package = DEFAULT_PACKAGE, -): Font[N] = - readTrueType[N]( - loadResource_intern(path, package = package), - path.splitFile().name, - charset & additional_codepoints.toSeq, - lineHeightPixels, - ) - -proc upload*(font: Font, renderdata: var RenderData) = - assert font.descriptorSet.vk.allIt(not it.Valid), "Font was alread uploaded" - assignBuffers(renderdata, font.descriptorSet) - uploadImages(renderdata, font.descriptorSet) - -proc addToPipeline*(font: Font, renderdata: RenderData, pipeline: Pipeline) = - initDescriptorSet(renderdata, pipeline.layout(3), font.descriptorSet) - -proc bindTo*(font: Font, pipeline: Pipeline, commandbuffer: VkCommandBuffer) = - bindDescriptorSet(commandbuffer, font.descriptorSet, 3, pipeline) - -#[ -# needs to be adjusted to work correctly with new metrics code in text.nim -func wordWrapped*(text: seq[Rune], font: FontObj, maxWidth: float32): seq[Rune] = - var remaining: seq[seq[Rune]] = @[@[]] - for c in text: - if c == SPACE: - remaining.add newSeq[Rune]() - else: - remaining[^1].add c - remaining.reverse() - - var currentLine: seq[Rune] - - while remaining.len > 0: - var currentWord = remaining.pop() - assert not (SPACE in currentWord) - - if currentWord.len == 0: - currentLine.add SPACE - else: - assert currentWord[^1] != SPACE - # if this is the first word of the line and it is too long we need to - # split by character - if currentLine.len == 0 and (SPACE & currentWord).textWidth(font) > maxWidth: - var subWord = @[currentWord[0]] - for c in currentWord[1 .. ^1]: - if (subWord & c).textWidth(font) > maxWidth: - break - subWord.add c - result.add subWord & NEWLINE - remaining.add currentWord[subWord.len .. ^1] - # process rest of the word in next iteration - else: - if (currentLine & SPACE & currentWord).textWidth(font) <= maxWidth: - if currentLine.len == 0: - currentLine = currentWord - else: - currentLine = currentLine & SPACE & currentWord - else: - result.add currentLine & NEWLINE - remaining.add currentWord - currentLine = @[] - if currentLine.len > 0 and currentLine != @[SPACE]: - result.add currentLine - - return result - ]#
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/semicongine/fonts.nim Sat Jan 11 15:34:21 2025 +0700 @@ -0,0 +1,252 @@ +import std/os +import std/strutils +import std/strformat +import std/sequtils +import std/unicode +import std/streams +import std/logging +import std/tables + +import ./core +import ./resources +import ./rendering/renderer +import ./rendering/memory +import ./contrib/algorithms/texture_packing + +{.emit: "#define STBTT_STATIC".} +{.emit: "#define STB_TRUETYPE_IMPLEMENTATION".} +{. + emit: + "#include \"" & currentSourcePath.parentDir() & "/thirdparty/stb/stb_truetype.h\"" +.} + +const ASCII_CHARSET = PrintableChars.toSeq.toRunes + +type 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: 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_GetCodepointBox( + info: ptr stbtt_fontinfo, codepoint: cint, x0, y0, x1, y1: ptr cint +): 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 readTrueType[N: static int]( + stream: Stream, name: string, codePoints: seq[Rune], lineHeightPixels: float32 +): Font[N] = + assert codePoints.len <= N, + "asked for " & $codePoints.len & " glyphs but shader is only configured for " & $N + + result = Font[N]() + + var + indata = stream.readAll() + fi: stbtt_fontinfo + if stbtt_InitFont(addr fi, indata.ToCPointer, 0) == 0: + raise newException(Exception, "An error occured while loading font file") + + let + glyph2bitmapScale = + float32(stbtt_ScaleForPixelHeight(addr fi, cfloat(lineHeightPixels))) + glyph2QuadScale = glyph2bitmapScale / lineHeightPixels + + # ensure all codepoints are available in the font + for codePoint in codePoints: + if stbtt_FindGlyphIndex(addr fi, cint(codePoint)) == 0: + warn &"Loading font {name}: Codepoint '{codePoint}' ({cint(codePoint)}) has no glyph" + + var + 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: + offsetX[codePoint] = 0 + offsetY[codePoint] = 0 + var width, height: cint + let data = stbtt_GetCodepointBitmap( + addr fi, + glyph2bitmapScale, + glyph2bitmapScale, + cint(codePoint), + addr width, + addr height, + addr (offsetX[codePoint]), + addr (offsetY[codePoint]), + ) + + if width > 0 and height > 0: + var bitmap = newSeq[Gray](width * height) + for i in 0 ..< width * height: + bitmap[i] = vec1u8(data[i].uint8) + bitmaps.add Image[Gray](width: width.uint32, height: height.uint32, data: bitmap) + else: + bitmaps.add Image[Gray](width: 1, height: 1, data: @[vec1u8(0)]) + + nativeFree(data) + + # generate glyph atlas from bitmaps + let packed = pack(bitmaps) + result.descriptorSet.data.fontAtlas = packed.atlas + + # generate quad-information for use in shader + for i in 0 ..< codePoints.len: + let codePoint = codePoints[i] + var advanceUnscaled, leftBearingUnscaled: cint + # is in glyph-space, needs to be scaled to pixel-space + stbtt_GetCodepointHMetrics( + addr fi, cint(codePoint), addr advanceUnscaled, addr leftBearingUnscaled + ) + result.leftBearing[codePoint] = leftBearingUnscaled.float32 * glyph2QuadScale + result.advance[codePoint] = advanceUnscaled.float32 * glyph2QuadScale + + 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) + # divide by lineHeightPixels to get from pixel-space to quad-geometry-space + left = + result.leftBearing[codePoint] + offsetX[codePoint].float32 / lineHeightPixels + right = left + bitmapW / lineHeightPixels + top = -offsetY[codePoint].float32 / lineHeightPixels + bottom = top - bitmapH / lineHeightPixels + + template glyphquads(): untyped = + result.descriptorSet.data.glyphquads.data + + glyphquads.pos[i] = vec4(left, bottom, right, top) + 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 + result.descriptorGlyphIndexRev[i.uint16] = codePoint # only used for debugging atm + + # kerning + for codePointAfter in codePoints: + result.kerning[(codePoint, codePointAfter)] = + float32( + stbtt_GetCodepointKernAdvance(addr fi, cint(codePoint), cint(codePointAfter)) + ) * glyph2QuadScale + + # line spacing + var ascent, descent, lineGap: cint + stbtt_GetFontVMetrics(addr fi, addr ascent, addr descent, addr lineGap) + result.lineAdvance = float32(ascent - descent + lineGap) * glyph2QuadScale + result.lineHeight = float32(ascent - descent) * glyph2QuadScale # should be 1 + result.ascent = float32(ascent) * glyph2QuadScale + result.descent = float32(descent) * glyph2QuadScale + + var x0, y0, x1, y1: cint + discard + stbtt_GetCodepointBox(addr fi, cint(Rune('x')), addr x0, addr y0, addr x1, addr y1) + result.xHeight = float32(y1 - y0) * glyph2QuadScale + +proc loadFont*[N: static int]( + path: string, + lineHeightPixels = 80'f32, + additional_codepoints: openArray[Rune] = [], + charset = ASCII_CHARSET, + package = DEFAULT_PACKAGE, +): Font[N] = + readTrueType[N]( + loadResource_intern(path, package = package), + path.splitFile().name, + charset & additional_codepoints.toSeq, + lineHeightPixels, + ) + +proc upload*(font: Font, renderdata: var RenderData) = + assert font.descriptorSet.vk.allIt(not it.Valid), "Font was alread uploaded" + assignBuffers(renderdata, font.descriptorSet) + uploadImages(renderdata, font.descriptorSet) + +proc addToPipeline*(font: Font, renderdata: RenderData, pipeline: Pipeline) = + initDescriptorSet(renderdata, pipeline.layout(3), font.descriptorSet) + +proc bindTo*(font: Font, pipeline: Pipeline, commandbuffer: VkCommandBuffer) = + bindDescriptorSet(commandbuffer, font.descriptorSet, 3, pipeline) + +#[ +# needs to be adjusted to work correctly with new metrics code in text.nim +func wordWrapped*(text: seq[Rune], font: FontObj, maxWidth: float32): seq[Rune] = + var remaining: seq[seq[Rune]] = @[@[]] + for c in text: + if c == SPACE: + remaining.add newSeq[Rune]() + else: + remaining[^1].add c + remaining.reverse() + + var currentLine: seq[Rune] + + while remaining.len > 0: + var currentWord = remaining.pop() + assert not (SPACE in currentWord) + + if currentWord.len == 0: + currentLine.add SPACE + else: + assert currentWord[^1] != SPACE + # if this is the first word of the line and it is too long we need to + # split by character + if currentLine.len == 0 and (SPACE & currentWord).textWidth(font) > maxWidth: + var subWord = @[currentWord[0]] + for c in currentWord[1 .. ^1]: + if (subWord & c).textWidth(font) > maxWidth: + break + subWord.add c + result.add subWord & NEWLINE + remaining.add currentWord[subWord.len .. ^1] + # process rest of the word in next iteration + else: + if (currentLine & SPACE & currentWord).textWidth(font) <= maxWidth: + if currentLine.len == 0: + currentLine = currentWord + else: + currentLine = currentLine & SPACE & currentWord + else: + result.add currentLine & NEWLINE + remaining.add currentWord + currentLine = @[] + if currentLine.len > 0 and currentLine != @[SPACE]: + result.add currentLine + + return result + ]#
--- a/semicongine/gltf.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/gltf.nim Sat Jan 11 15:34:21 2025 +0700 @@ -6,7 +6,8 @@ import std/typetraits import ./core -import ./image +import ./images +import ./resources type glTFHeader = object @@ -190,7 +191,7 @@ ): TMaterial = result = TMaterial() - let pbr {.warning[InheritFromException]: off.} = materialNode["pbrMetallicRoughness"] + let pbr {.hint[XDeclaredButNotUsed]: off.} = materialNode["pbrMetallicRoughness"] for name, value in fieldPairs(result): for gltfAttribute, mappedName in fieldPairs(mapping): when gltfAttribute != "" and name == mappedName: @@ -364,3 +365,15 @@ for nodeId in items(scene["nodes"]): nodes.add nodeId.getInt() result.scenes.add nodes + +proc loadMeshes*[TMesh, TMaterial]( + path: string, + meshAttributesMapping: static MeshAttributeNames, + materialAttributesMapping: static MaterialAttributeNames, + package = DEFAULT_PACKAGE, +): GltfData[TMesh, TMaterial] {.gcsafe.} = + ReadglTF[TMesh, TMaterial]( + stream = loadResource_intern(path, package = package), + meshAttributesMapping = meshAttributesMapping, + materialAttributesMapping = materialAttributesMapping, + )
--- a/semicongine/image.nim Sat Jan 11 14:15:29 2025 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,142 +0,0 @@ -import std/os -import std/typetraits -import std/strformat - -import ./core - -{.emit: "#define STB_IMAGE_STATIC".} -{.emit: "#define STB_IMAGE_IMPLEMENTATION".} -{. - emit: "#include \"" & currentSourcePath.parentDir() & "/thirdparty/stb/stb_image.h\"" -.} - -proc stbi_load_from_memory( - buffer: ptr uint8, - len: cint, - x, y: ptr cint, - channels_in_file: ptr cint, - desired_channels: cint, -): ptr uint8 {.importc, nodecl.} - -func `$`*[S, IsArray](img: ImageObject[S, IsArray]): string = - let pixelTypeName = S.name - if IsArray == false: - $img.width & "x" & $img.height & " " & pixelTypeName - else: - $img.width & "x" & $img.height & "[" & $img.nLayers & "] " & pixelTypeName - -func copy*[S, T](img: ImageObject[S, T]): ImageObject[S, T] = - for bf, rf in fields(img, result): - rf = bf - -# loads single layer image -proc loadImageData*[T: PixelType]( - pngData: string | seq[uint8] -): tuple[width: uint32, height: uint32, data: seq[T]] = - when T is Gray: - let nChannels = 1.cint - elif T is BGRA: - let nChannels = 4.cint - - var w, h, c: cint - - let data = stbi_load_from_memory( - buffer = cast[ptr uint8](pngData.ToCPointer), - len = pngData.len.cint, - x = addr(w), - y = addr(h), - channels_in_file = addr(c), - desired_channels = nChannels, - ) - if data == nil: - raise newException(Exception, "An error occured while loading PNG file") - - let imagesize = w * h * nChannels - result = (width: w.uint32, height: h.uint32, data: newSeq[T](w * h)) - copyMem(result.data.ToCPointer, data, imagesize) - nativeFree(data) - - when T is BGRA: # convert to BGRA - for i in 0 ..< result.data.len: - swap(result.data[i][0], result.data[i][2]) - -# TODO: static versions to check for existing of files during compilation -proc addImage*[T: PixelType](imageArray: var ImageArray[T], image: sink Image[T]) = - assert image.width == imageArray.width, - &"Image needs to have same dimension as ImageArray to be added (array has {imageArray.width}x{imageArray.height} but image has {image.width}x{image.height})" - assert image.height == imageArray.height, - &"Image needs to have same dimension as ImageArray to be added (array has {imageArray.width}x{imageArray.height} but image has {image.width}x{image.height})" - - inc imageArray.nLayers - imageArray.data.add image.data - -proc `[]`*(image: Image, x, y: uint32): auto = - assert x < image.width, &"{x} < {image.width} is not true" - assert y < image.height, &"{y} < {image.height} is not true" - - image.data[y * image.width + x] - -proc `[]=`*[T](image: var Image[T], x, y: uint32, value: T) = - assert x < image.width - assert y < image.height - - image.data[y * image.width + x] = value - -proc `[]`*(image: ImageArray, layer, x, y: uint32): auto = - assert layer < image.nLayers, - &"Tried to access image layer {layer}, but image has only {image.nLayers} layers" - assert x < image.width, - &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" - assert y < image.height, - &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" - - image.data[layer * (image.width * image.height) + y * image.width + x] - -proc `[]=`*[T](image: var ImageArray[T], layer, x, y: uint32, value: T) = - assert layer < image.nLayers, - &"Tried to access image layer {layer}, but image has only {image.nLayers} layers" - assert x < image.width, - &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" - assert y < image.height, - &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" - - image.data[layer * (image.width * image.height) + y * image.width + x] = value - -# stb_image.h has no encoding support, maybe check stb_image_write or similar -# -# proc lodepng_encode_memory(out_data: ptr cstring, outsize: ptr csize_t, image: cstring, w: cuint, h: cuint, colorType: cint, bitdepth: cuint): cuint {.importc.} -# -#[ -proc toPNG[T: PixelType](image: Image[T]): seq[uint8] = - when T is Gray: - let pngType = 0 # hardcoded in lodepng.h - else: - let pngType = 6 # hardcoded in lodepng.h - var - pngData: cstring - pngSize: csize_t - for y in 0 ..< image.height: - for x in 0 ..< image.width: - discard - let ret = lodepng_encode_memory( - addr pngData, - addr pngSize, - cast[cstring](image.imagedata.ToCPointer), - cuint(image.width), - cuint(image.height), - cint(pngType), - 8, - ) - assert ret == 0, &"There was an error with generating the PNG data for image {image}, result was: {ret}" - result = newSeq[uint8](pngSize) - for i in 0 ..< pngSize: - result[i] = uint8(pngData[i]) - nativeFree(pngData) - -proc WritePNG*(image: Image, filename: string) = - let f = filename.open(mode = fmWrite) - let data = image.toPNG() - let written = f.writeBytes(data, 0, data.len) - assert written == data.len, &"There was an error while saving '{filename}': only {written} of {data.len} bytes were written" - f.close() -]#
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/semicongine/images.nim Sat Jan 11 15:34:21 2025 +0700 @@ -0,0 +1,202 @@ +import std/os +import std/typetraits +import std/strformat +import std/strutils +import std/streams + +import ./core +import ./resources + +{.emit: "#define STB_IMAGE_STATIC".} +{.emit: "#define STB_IMAGE_IMPLEMENTATION".} +{. + emit: "#include \"" & currentSourcePath.parentDir() & "/thirdparty/stb/stb_image.h\"" +.} + +proc stbi_load_from_memory( + buffer: ptr uint8, + len: cint, + x, y: ptr cint, + channels_in_file: ptr cint, + desired_channels: cint, +): ptr uint8 {.importc, nodecl.} + +func `$`*[S, IsArray](img: ImageObject[S, IsArray]): string = + let pixelTypeName = S.name + if IsArray == false: + $img.width & "x" & $img.height & " " & pixelTypeName + else: + $img.width & "x" & $img.height & "[" & $img.nLayers & "] " & pixelTypeName + +func copy*[S, T](img: ImageObject[S, T]): ImageObject[S, T] = + for bf, rf in fields(img, result): + rf = bf + +# loads single layer image +proc loadImageData*[T: PixelType]( + pngData: string | seq[uint8] +): tuple[width: uint32, height: uint32, data: seq[T]] = + when T is Gray: + let nChannels = 1.cint + elif T is BGRA: + let nChannels = 4.cint + + var w, h, c: cint + + let data = stbi_load_from_memory( + buffer = cast[ptr uint8](pngData.ToCPointer), + len = pngData.len.cint, + x = addr(w), + y = addr(h), + channels_in_file = addr(c), + desired_channels = nChannels, + ) + if data == nil: + raise newException(Exception, "An error occured while loading PNG file") + + let imagesize = w * h * nChannels + result = (width: w.uint32, height: h.uint32, data: newSeq[T](w * h)) + copyMem(result.data.ToCPointer, data, imagesize) + nativeFree(data) + + when T is BGRA: # convert to BGRA + for i in 0 ..< result.data.len: + swap(result.data[i][0], result.data[i][2]) + +# TODO: static versions to check for existing of files during compilation +proc addImage*[T: PixelType](imageArray: var ImageArray[T], image: sink Image[T]) = + assert image.width == imageArray.width, + &"Image needs to have same dimension as ImageArray to be added (array has {imageArray.width}x{imageArray.height} but image has {image.width}x{image.height})" + assert image.height == imageArray.height, + &"Image needs to have same dimension as ImageArray to be added (array has {imageArray.width}x{imageArray.height} but image has {image.width}x{image.height})" + + inc imageArray.nLayers + imageArray.data.add image.data + +proc `[]`*(image: Image, x, y: uint32): auto = + assert x < image.width, &"{x} < {image.width} is not true" + assert y < image.height, &"{y} < {image.height} is not true" + + image.data[y * image.width + x] + +proc `[]=`*[T](image: var Image[T], x, y: uint32, value: T) = + assert x < image.width + assert y < image.height + + image.data[y * image.width + x] = value + +proc `[]`*(image: ImageArray, layer, x, y: uint32): auto = + assert layer < image.nLayers, + &"Tried to access image layer {layer}, but image has only {image.nLayers} layers" + assert x < image.width, + &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" + assert y < image.height, + &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" + + image.data[layer * (image.width * image.height) + y * image.width + x] + +proc `[]=`*[T](image: var ImageArray[T], layer, x, y: uint32, value: T) = + assert layer < image.nLayers, + &"Tried to access image layer {layer}, but image has only {image.nLayers} layers" + assert x < image.width, + &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" + assert y < image.height, + &"Tried to access pixel coordinate {(x, y)} but image has size {(image.width, image.height)}" + + image.data[layer * (image.width * image.height) + y * image.width + x] = value + +# stb_image.h has no encoding support, maybe check stb_image_write or similar +# +# proc lodepng_encode_memory(out_data: ptr cstring, outsize: ptr csize_t, image: cstring, w: cuint, h: cuint, colorType: cint, bitdepth: cuint): cuint {.importc.} +# +#[ +proc toPNG[T: PixelType](image: Image[T]): seq[uint8] = + when T is Gray: + let pngType = 0 # hardcoded in lodepng.h + else: + let pngType = 6 # hardcoded in lodepng.h + var + pngData: cstring + pngSize: csize_t + for y in 0 ..< image.height: + for x in 0 ..< image.width: + discard + let ret = lodepng_encode_memory( + addr pngData, + addr pngSize, + cast[cstring](image.imagedata.ToCPointer), + cuint(image.width), + cuint(image.height), + cint(pngType), + 8, + ) + assert ret == 0, &"There was an error with generating the PNG data for image {image}, result was: {ret}" + result = newSeq[uint8](pngSize) + for i in 0 ..< pngSize: + result[i] = uint8(pngData[i]) + nativeFree(pngData) + +proc WritePNG*(image: Image, filename: string) = + let f = filename.open(mode = fmWrite) + let data = image.toPNG() + let written = f.writeBytes(data, 0, data.len) + assert written == data.len, &"There was an error while saving '{filename}': only {written} of {data.len} bytes were written" + f.close() +]# + +proc loadImage*[T: PixelType]( + path: string, package = DEFAULT_PACKAGE +): Image[T] {.gcsafe.} = + assert path.splitFile().ext.toLowerAscii == ".png", + "Unsupported image type: " & path.splitFile().ext.toLowerAscii + + let (width, height, data) = + loadImageData[T](loadResource_intern(path, package = package).readAll()) + result = Image[T](width: width, height: height, data: data) + +proc loadImageArray*[T: PixelType]( + paths: openArray[string], package = DEFAULT_PACKAGE +): ImageArray[T] {.gcsafe.} = + assert paths.len > 0, "Image array cannot contain 0 images" + for path in paths: + assert path.splitFile().ext.toLowerAscii == ".png", + "Unsupported image type: " & path.splitFile().ext.toLowerAscii + + let (width, height, data) = + loadImageData[T](loadResource_intern(paths[0], package = package).readAll()) + result = + ImageArray[T](width: width, height: height, data: data, nLayers: paths.len.uint32) + for path in paths[1 .. ^1]: + let (w, h, data) = + loadImageData[T](loadResource_intern(path, package = package).readAll()) + assert w == result.width, + "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" + assert h == result.height, + "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" + result.data.add data + +proc loadImageArray*[T: PixelType]( + path: string, tilesize: uint32, package = DEFAULT_PACKAGE +): ImageArray[T] {.gcsafe.} = + assert path.splitFile().ext.toLowerAscii == ".png", + "Unsupported image type: " & path.splitFile().ext.toLowerAscii + + let (width, height, data) = + loadImageData[T](loadResource_intern(path, package = package).readAll()) + let tilesY = height div tilesize + + result = ImageArray[T](width: tilesize, height: tilesize) + var tile = newSeq[T](tilesize * tilesize) + + for ty in 0 ..< tilesY: + for tx in 0 ..< tilesY: + var hasNonTransparent = when T is BGRA: false else: true + let baseI = ty * tilesize * width + tx * tilesize + for y in 0 ..< tilesize: + for x in 0 ..< tilesize: + tile[y * tilesize + x] = data[baseI + y * width + x] + when T is BGRA: + hasNonTransparent = hasNonTransparent or tile[y * tilesize + x].a > 0 + if hasNonTransparent: + result.data.add tile + result.nLayers.inc
--- a/semicongine/loaders.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/loaders.nim Sat Jan 11 15:34:21 2025 +0700 @@ -8,10 +8,13 @@ import ./background_loaders import ./core import ./gltf -import ./image +import ./images import ./resources import ./thirdparty/parsetoml +# necessary, so we don't need to import parsetoml extra when using this module +export parsetoml + proc loadBytes*(path, package: string): seq[byte] {.gcsafe.} = cast[seq[byte]](toSeq(path.loadResource_intern(package = package).readAll())) @@ -21,83 +24,6 @@ proc loadConfig*(path: string, package = DEFAULT_PACKAGE): TomlValueRef {.gcsafe.} = path.loadResource_intern(package = package).parseStream(filename = path) -proc loadImage*[T: PixelType]( - path: string, package = DEFAULT_PACKAGE -): Image[T] {.gcsafe.} = - assert path.splitFile().ext.toLowerAscii == ".png", - "Unsupported image type: " & path.splitFile().ext.toLowerAscii - - let (width, height, data) = - loadImageData[T](loadResource_intern(path, package = package).readAll()) - result = Image[T](width: width, height: height, data: data) - -proc loadImageArray*[T: PixelType]( - paths: openArray[string], package = DEFAULT_PACKAGE -): ImageArray[T] {.gcsafe.} = - assert paths.len > 0, "Image array cannot contain 0 images" - for path in paths: - assert path.splitFile().ext.toLowerAscii == ".png", - "Unsupported image type: " & path.splitFile().ext.toLowerAscii - - let (width, height, data) = - loadImageData[T](loadResource_intern(paths[0], package = package).readAll()) - result = - ImageArray[T](width: width, height: height, data: data, nLayers: paths.len.uint32) - for path in paths[1 .. ^1]: - let (w, h, data) = - loadImageData[T](loadResource_intern(path, package = package).readAll()) - assert w == result.width, - "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" - assert h == result.height, - "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" - result.data.add data - -proc loadImageArray*[T: PixelType]( - path: string, tilesize: uint32, package = DEFAULT_PACKAGE -): ImageArray[T] {.gcsafe.} = - assert path.splitFile().ext.toLowerAscii == ".png", - "Unsupported image type: " & path.splitFile().ext.toLowerAscii - - let (width, height, data) = - loadImageData[T](loadResource_intern(path, package = package).readAll()) - let tilesY = height div tilesize - - result = ImageArray[T](width: tilesize, height: tilesize) - var tile = newSeq[T](tilesize * tilesize) - - for ty in 0 ..< tilesY: - for tx in 0 ..< tilesY: - var hasNonTransparent = when T is BGRA: false else: true - let baseI = ty * tilesize * width + tx * tilesize - for y in 0 ..< tilesize: - for x in 0 ..< tilesize: - tile[y * tilesize + x] = data[baseI + y * width + x] - when T is BGRA: - hasNonTransparent = hasNonTransparent or tile[y * tilesize + x].a > 0 - if hasNonTransparent: - result.data.add tile - result.nLayers.inc - -proc loadAudio*(path: string, package = DEFAULT_PACKAGE): SoundData {.gcsafe.} = - if path.splitFile().ext.toLowerAscii == ".au": - loadResource_intern(path, package = package).readAU() - elif path.splitFile().ext.toLowerAscii == ".ogg": - loadResource_intern(path, package = package).readVorbis() - else: - raise newException(Exception, "Unsupported audio file type: " & path) - -proc loadMeshes*[TMesh, TMaterial]( - path: string, - meshAttributesMapping: static MeshAttributeNames, - materialAttributesMapping: static MaterialAttributeNames, - package = DEFAULT_PACKAGE, -): GltfData[TMesh, TMaterial] {.gcsafe.} = - ReadglTF[TMesh, TMaterial]( - stream = loadResource_intern(path, package = package), - meshAttributesMapping = meshAttributesMapping, - materialAttributesMapping = materialAttributesMapping, - ) - # background loaders type ResourceType =
--- a/semicongine/rendering.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/rendering.nim Sat Jan 11 15:34:21 2025 +0700 @@ -9,7 +9,7 @@ import ./core -import ./image +import ./images # in this file: # - const defintions for rendering @@ -200,10 +200,6 @@ engine().vulkan.fullscreen_internal = enable engine().vulkan.window.setFullscreen(engine().vulkan.fullscreen_internal) -proc getAspectRatio*(): float32 = - assert engine().vulkan.swapchain != nil, "Swapchain has not been initialized yet" - engine().vulkan.swapchain.width.float32 / engine().vulkan.swapchain.height.float32 - proc maxFramebufferSampleCount*( maxSamples = VK_SAMPLE_COUNT_8_BIT ): VkSampleCountFlagBits =
--- a/semicongine/text.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/semicongine/text.nim Sat Jan 11 15:34:21 2025 +0700 @@ -8,7 +8,7 @@ import ./core import ./rendering -import ./image +import ./images import ./rendering/renderer import ./rendering/memory @@ -190,8 +190,9 @@ ## This should be called again after aspect ratio of window changes let cap = if capacity == 0: text.len else: capacity - assert textbuffer.cursor + cap <= textbuffer.position.len, - &"Text is too big for TextBuffer ({textbuffer.position.len - textbuffer.cursor} left, but need {cap})" + let l = textbuffer.position.len + assert textbuffer.cursor + cap <= l, + &"Text is too big for TextBuffer ({l - textbuffer.cursor} left, but need {cap})" result = TextHandle(generation: textbuffer.generation, index: textbuffer.texts.len.uint32)
--- a/tests/test_text.nim Sat Jan 11 14:15:29 2025 +0700 +++ b/tests/test_text.nim Sat Jan 11 15:34:21 2025 +0700 @@ -13,7 +13,7 @@ import ../semicongine/rendering import ../semicongine/text import ../semicongine/input -import ../semicongine/font +import ../semicongine/fonts const MAX_CODEPOINTS = 200 const FONTNAME = "Overhaul.ttf"