# HG changeset patch # User Sam # Date 1705756623 -25200 # Node ID 1ee397815b0b899602a08a24e74441fe28ed3711 # Parent 1f1e959a5fa3ca3f86cef35a6179b7d06140cf9d did: image & font refactoring, add texture-atlas-packing diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/core/fonttypes.nim --- a/semicongine/core/fonttypes.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/core/fonttypes.nim Sat Jan 20 20:17:03 2024 +0700 @@ -32,5 +32,4 @@ fontAtlas*: Texture maxHeight*: int kerning*: Table[(Rune, Rune), float32] - resolution*: float32 fontscale*: float32 diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/core/imagetypes.nim --- a/semicongine/core/imagetypes.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/core/imagetypes.nim Sat Jan 20 20:17:03 2024 +0700 @@ -4,35 +4,52 @@ import ./vector type - Pixel* = array[4, uint8] - ImageObject* = object + RGBAPixel* = array[4, uint8] + GrayPixel* = uint8 + Pixel* = RGBAPixel or GrayPixel + ImageObject*[T: Pixel] = object width*: int height*: int - imagedata*: seq[Pixel] + imagedata*: seq[T] + Image*[T: Pixel] = ref ImageObject[T] + Sampler* = object magnification*: VkFilter = VK_FILTER_LINEAR minification*: VkFilter = VK_FILTER_LINEAR wrapModeS*: VkSamplerAddressMode = VK_SAMPLER_ADDRESS_MODE_REPEAT wrapModeT*: VkSamplerAddressMode = VK_SAMPLER_ADDRESS_MODE_REPEAT - - Image* = ref ImageObject Texture* = object name*: string - image*: Image + case isGrayscale*: bool = false + of false: colorImage*: Image[RGBAPixel] + of true: grayImage*: Image[GrayPixel] sampler*: Sampler -converter toRGBA*(p: Pixel): Vec4f = +proc `==`*(a, b: Texture): bool = + if a.isGrayscale != b.isGrayscale or a.name != b.name or a.sampler != b.sampler: + return false + elif a.isGrayscale: + return a.grayImage == b.grayImage + else: + return a.colorImage == b.colorImage + +converter toRGBA*(p: RGBAPixel): Vec4f = newVec4f(float32(p[0]) / 255'f32, float32(p[1]) / 255'f32, float32(p[2]) / 255'f32, float32(p[3]) / 255'f32) +converter toGrayscale*(p: GrayPixel): float32 = + float32(p) / 255'f32 proc `$`*(image: Image): string = &"{image.width}x{image.height}" proc `$`*(texture: Texture): string = - &"{texture.name} {texture.image}" + if texture.isGrayscale: + &"{texture.name} {texture.grayImage} (gray)" + else: + &"{texture.name} {texture.colorImage} (color)" proc `[]`*(image: Image, x, y: int): Pixel = - assert x < image.width - assert y < image.height + assert x < image.width, &"{x} < {image.width} is not true" + assert y < image.height, &"{y} < {image.height} is not true" image[].imagedata[y * image.width + x] @@ -42,30 +59,25 @@ image[].imagedata[y * image.width + x] = value -const EMPTYPIXEL = [0'u8, 0'u8, 0'u8, 0'u8] -proc newImage*(width, height: int, imagedata: seq[Pixel] = @[], fill=EMPTYPIXEL): Image = +proc newImage*[T: Pixel](width, height: int, imagedata: seq[T]= @[]): Image[T] = assert width > 0 and height > 0 assert imagedata.len == width * height or imagedata.len == 0 - result = new Image - result.imagedata = (if imagedata.len == 0: newSeq[Pixel](width * height) else: imagedata) + result = new Image[T] + result.imagedata = (if imagedata.len == 0: newSeq[T](width * height) else: imagedata) assert width * height == 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 -let INVALID_TEXTURE* = Texture(name: "Invalid texture", image: newImage(1, 1, @[[255'u8, 0'u8, 255'u8, 255'u8]]), sampler: Sampler( +let INVALID_TEXTURE* = Texture(name: "Invalid texture", isGrayscale: false, colorImage: newImage(1, 1, @[[255'u8, 0'u8, 255'u8, 255'u8]]), sampler: Sampler( magnification: VK_FILTER_NEAREST, minification: VK_FILTER_NEAREST, wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT, wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT, ) ) -let EMPTY_TEXTURE* = Texture(name: "Empty texture", image: newImage(1, 1, @[[255'u8, 255'u8, 255'u8, 255'u8]]), sampler: Sampler( +let EMPTY_TEXTURE* = Texture(name: "Empty texture", isGrayscale: false, colorImage: newImage(1, 1, @[[255'u8, 255'u8, 255'u8, 255'u8]]), sampler: Sampler( magnification: VK_FILTER_NEAREST, minification: VK_FILTER_NEAREST, wrapModeS: VK_SAMPLER_ADDRESS_MODE_REPEAT, diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/engine.nim --- a/semicongine/engine.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/engine.nim Sat Jan 20 20:17:03 2024 +0700 @@ -213,3 +213,6 @@ if enable != engine.fullscreen: engine.fullscreen = enable engine.window.fullscreen(engine.fullscreen) + +func limits*(engine: Engine): VkPhysicalDeviceLimits = + engine.gpuDevice().physicalDevice.properties.limits diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/resources.nim --- a/semicongine/resources.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/resources.nim Sat Jan 20 20:17:03 2024 +0700 @@ -1,5 +1,6 @@ import std/streams import std/strutils +import std/sequtils import std/strformat import std/os import std/unicode @@ -22,7 +23,10 @@ Zip # Zip files Exe # Embeded in executable -const thebundletype = parseEnum[ResourceBundlingType](BUNDLETYPE.toLowerAscii().capitalizeAscii()) +const + thebundletype = parseEnum[ResourceBundlingType](BUNDLETYPE.toLowerAscii().capitalizeAscii()) + ASCII_CHARSET = PrintableChars.toSeq.toRunes + var selectedMod* = "default" # resource loading @@ -127,12 +131,17 @@ else: raise newException(Exception, "Unsupported audio file type: " & path) -proc loadFont*(path: string, name="", color=newVec4f(1, 1, 1, 1), resolution=100'f32): Font = +proc loadFont*( + path: string, + name="", + lineHeightPixels=80'f32, + additional_codepoints: openArray[Rune]=[], + charset=ASCII_CHARSET +): Font = var thename = name if thename == "": thename = path.splitFile().name - let defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=+[{]};:,<.>/? $!@#%^&*()\"'".toRunes() - loadResource_intern(path).readTrueType(name, defaultCharset, color, resolution) + loadResource_intern(path).readTrueType(name, charset & additional_codepoints.toSeq, lineHeightPixels) proc loadMeshes*(path: string, defaultMaterial: MaterialType): seq[MeshTree] = loadResource_intern(path).readglTF(defaultMaterial) diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/resources/font.nim --- a/semicongine/resources/font.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/resources/font.nim Sat Jan 20 20:17:03 2024 +0700 @@ -1,64 +1,99 @@ +import times import std/tables -import std/math +import std/strformat import std/streams import std/os import std/unicode +import std/logging import ../core/vector import ../core/imagetypes import ../core/fonttypes +import ../algorithms +import ./image {.emit: "#define STBTT_STATIC" .} {.emit: "#define STB_TRUETYPE_IMPLEMENTATION" .} {.emit: "#include \"" & currentSourcePath.parentDir() & "/stb_truetype.h\"" .} -type - stbtt_fontinfo {.importc, incompleteStruct .} = object +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_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 free(p: pointer) {.importc.} -proc readTrueType*(stream: Stream, name: string, codePoints: seq[Rune], color: Vec4f, resolution: float32): Font = +proc readTrueType*(stream: Stream, name: string, codePoints: seq[Rune], lineHeightPixels: 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") - result.resolution = resolution - result.fontscale = float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(resolution))) + result.fontscale = float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels))) + + # 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 offsetX = 0 bitmaps: Table[Rune, (cstring, cint, cint)] topOffsets: Table[Rune, int] + images: seq[Image[GrayPixel]] for codePoint in codePoints: var width, height: cint offX, offY: cint + let data = stbtt_GetCodepointBitmap( addr fontinfo, - result.fontscale, result.fontscale, + result.fontscale, + result.fontscale, cint(codePoint), addr width, addr height, addr offX, addr offY ) + + if width > 0 and height > 0: + var bitmap = newSeq[GrayPixel](width * height) + for i in 0 ..< width * height: + bitmap[i] = GrayPixel(data[i]) + images.add newImage[GrayPixel](int(width), int(height), bitmap) + bitmaps[codePoint] = (data, width, height) result.maxHeight = max(result.maxHeight, int(height)) - offsetX += width + 1 + offsetX += width topOffsets[codePoint] = offY + assert offsetX < MAX_TEXTURE_WIDTH, &"Font size too big, choose a smaller lineHeightPixels when loading the font (required texture width is {offsetX} but max is {MAX_TEXTURE_WIDTH}), must be smaller than {lineHeightPixels * float(MAX_TEXTURE_WIDTH) / float(offsetX) } (approx.)" + + let packed = pack(images) + packed.atlas.writePNG("tmp.png") result.name = name result.fontAtlas = Texture( name: name & "_texture", - image: newImage(offsetX, result.maxHeight + 1), + isGrayscale: true, + grayImage: newImage[GrayPixel](offsetX, result.maxHeight), sampler: FONTSAMPLER_SOFT ) + for codePoint in codePoints: + let + bitmap = bitmaps[codePoint][0] + width = bitmaps[codePoint][1] + height = bitmaps[codePoint][2] + offsetX = 0 for codePoint in codePoints: let @@ -69,13 +104,7 @@ # bitmap data for y in 0 ..< height: for x in 0 ..< 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)) - ] + result.fontAtlas.grayImage[x + offsetX, y] = uint8(bitmap[y * width + x]) # horizontal spaces: var advance, leftBearing: cint @@ -84,16 +113,16 @@ 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), + newVec2f((float32(offsetX) + 0.5) / float32(result.fontAtlas.grayImage.width), (float32(height) - 1.0) / float32(result.maxHeight)), + newVec2f((float32(offsetX) + 0.5) / float32(result.fontAtlas.grayImage.width), 0.5 / float32(result.maxHeight)), + newVec2f((float32(offsetX + width) - 1.0) / float32(result.fontAtlas.grayImage.width), 0.5 / float32(result.maxHeight)), + newVec2f((float32(offsetX + width) - 1.0) / float32(result.fontAtlas.grayImage.width), (float32(height) - 1.0) / float32(result.maxHeight)), ], topOffset: float32(topOffsets[codePoint]), leftOffset: float32(leftBearing) * result.fontscale, advance: float32(advance) * result.fontscale, ) - offsetX += width + 1 + offsetX += width free(bitmap) for codePointAfter in codePoints: result.kerning[(codePoint, codePointAfter)] = float32(stbtt_GetCodepointKernAdvance( diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/resources/image.nim --- a/semicongine/resources/image.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/resources/image.nim Sat Jan 20 20:17:03 2024 +0700 @@ -1,8 +1,11 @@ -import os +import std/os +# import std/syncio import std/streams import std/bitops +import std/strformat import ../core/imagetypes +import ../core/utils const COMPRESSION_BI_RGB = 0'u32 const COMPRESSION_BI_BITFIELDS = 3'u32 @@ -36,7 +39,7 @@ gammaGreen: uint32 # not used yet gammaBlue: uint32 # not used yet -proc readBMP*(stream: Stream): Image = +proc readBMP*(stream: Stream): Image[RGBAPixel] = var bitmapFileHeader: BitmapFileHeader dibHeader: DIBHeader @@ -73,13 +76,13 @@ stream.setPosition(int(bitmapFileHeader.dataStart)) var padding = ((int32(dibHeader.bitsPerPixel div 8)) * dibHeader.width) mod 4 - data = newSeq[Pixel](dibHeader.width * abs(dibHeader.height)) + data = newSeq[RGBAPixel](dibHeader.width * abs(dibHeader.height)) if padding > 0: padding = 4 - padding for row in 0 ..< abs(dibHeader.height): for col in 0 ..< dibHeader.width: - var pixel: Pixel = [0'u8, 0'u8, 0'u8, 255'u8] + var pixel: RGBAPixel = [0'u8, 0'u8, 0'u8, 255'u8] # if we got channeld bitmasks if dibHeader.compression in [COMPRESSION_BI_BITFIELDS, COMPRESSION_BI_ALPHABITFIELDS]: var value = stream.readUint32() @@ -106,9 +109,11 @@ {.compile: currentSourcePath.parentDir() & "/lodepng.c" .} proc lodepng_decode32(out_data: ptr cstring, w: ptr cuint, h: ptr cuint, in_data: cstring, insize: csize_t): cuint {.importc.} +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 free(p: pointer) {.importc.} # for some reason the lodepng pointer can only properly be freed with the native free -proc readPNG*(stream: Stream): Image = +proc readPNG*(stream: Stream): Image[RGBAPixel] = let indata = stream.readAll() var w, h: cuint var data: cstring @@ -117,9 +122,45 @@ raise newException(Exception, "An error occured while loading PNG file") let imagesize = w * h * 4 - var imagedata = newSeq[Pixel](w * h) + var imagedata = newSeq[RGBAPixel](w * h) copyMem(addr imagedata[0], data, imagesize) free(data) result = newImage(width=int(w), height=int(h), imagedata=imagedata) + +proc toPNG*[T: Pixel](image: Image[T]): seq[uint8] = + when T is GrayPixel: + 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 + # stdout.write image[x, y] + # stdout.write ' ' + # echo "" + 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]) + free(pngData) + +proc writePNG*[T: Pixel](image: Image[T], 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() diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/resources/mesh.nim --- a/semicongine/resources/mesh.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/resources/mesh.nim Sat Jan 20 20:17:03 2024 +0700 @@ -127,7 +127,7 @@ else: copyMem(dstPointer, addr mainBuffer[bufferOffset], length) -proc loadImage(root: JsonNode, imageIndex: int, mainBuffer: seq[uint8]): Image = +proc loadImage(root: JsonNode, imageIndex: int, mainBuffer: seq[uint8]): Image[RGBAPixel] = if root["images"][imageIndex].hasKey("uri"): raise newException(Exception, "Unsupported feature: Load images from external files") @@ -145,7 +145,8 @@ proc loadTexture(root: JsonNode, textureIndex: int, mainBuffer: seq[uint8]): Texture = let textureNode = root["textures"][textureIndex] - result.image = loadImage(root, textureNode["source"].getInt(), mainBuffer) + result = Texture(isGrayscale: false) + result.colorImage = loadImage(root, textureNode["source"].getInt(), mainBuffer) result.name = root["images"][textureNode["source"].getInt()]["name"].getStr() if result.name == "": result.name = &"Texture{textureIndex}" diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/text.nim --- a/semicongine/text.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/text.nim Sat Jan 20 20:17:03 2024 +0700 @@ -15,13 +15,14 @@ Left Center Right - Textbox* = object + Text* = object maxLen*: int text: seq[Rune] dirty: bool alignment*: TextAlignment = Center font*: Font mesh*: Mesh + color*: Vec4f const TRANSFORM_ATTRIB = "transform" @@ -30,7 +31,7 @@ TEXT_MATERIAL_TYPE* = MaterialType( name: "default-text-material-type", vertexAttributes: {TRANSFORM_ATTRIB: Mat4F32, POSITION_ATTRIB: Vec3F32, UV_ATTRIB: Vec2F32}.toTable, - attributes: {"fontAtlas": TextureType}.toTable, + attributes: {"fontAtlas": TextureType, "color": Vec4F32}.toTable, ) TEXT_SHADER* = createShaderConfiguration( inputs=[ @@ -40,21 +41,29 @@ ], intermediates=[attr[Vec2f]("uvFrag")], outputs=[attr[Vec4f]("color")], + uniforms=[attr[Vec4f]("color")], samplers=[attr[Texture]("fontAtlas")], vertexCode= &"""gl_Position = vec4({POSITION_ATTRIB}, 1.0) * {TRANSFORM_ATTRIB}; uvFrag = {UV_ATTRIB};""", - fragmentCode= &"""color = texture(fontAtlas, uvFrag);""", + fragmentCode= &"""color = vec4(Uniforms.color.rgb, Uniforms.color.a * texture(fontAtlas, uvFrag).r);""" ) -proc updateMesh(textbox: var Textbox) = +proc updateMesh(textbox: var Text) = # 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] 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])] + maxWidth = max(width, maxWidth) - let centerX = width / 2 + let centerX = maxWidth / 2 let centerY = textbox.font.maxHeight / 2 var offsetX = 0'f32 @@ -88,16 +97,16 @@ textbox.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f() -func text*(textbox: Textbox): seq[Rune] = +func text*(textbox: Text): seq[Rune] = textbox.text -proc `text=`*(textbox: var Textbox, text: seq[Rune]) = +proc `text=`*(textbox: var Text, text: seq[Rune]) = textbox.text = text textbox.updateMesh() -proc `text=`*(textbox: var Textbox, text: string) = +proc `text=`*(textbox: var Text, text: string) = `text=`(textbox, text.toRunes) -proc initTextbox*(maxLen: int, font: Font, text="".toRunes): Textbox = +proc initText*(maxLen: int, font: Font, text="".toRunes, color=newVec4f(0, 0, 0, 1)): Text = var positions = newSeq[Vec3f](int(maxLen * 4)) indices: seq[array[3, uint16]] @@ -109,7 +118,7 @@ [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)], ] - result = Textbox(maxLen: maxLen, text: text, font: font, dirty: true) + result = Text(maxLen: maxLen, text: text, font: font, dirty: true) result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs, name = &"textbox-{instanceCounter}") inc instanceCounter result.mesh[].renameAttribute("position", POSITION_ATTRIB) @@ -117,10 +126,10 @@ result.mesh.material = initMaterialData( theType=TEXT_MATERIAL_TYPE, name=font.name & " text", - attributes={"fontAtlas": initDataList(@[font.fontAtlas])}, + attributes={"fontAtlas": initDataList(@[font.fontAtlas]), "color": initDataList(@[color])}, ) result.updateMesh() -proc initTextbox*(maxLen: int, font: Font, text=""): Textbox = - initTextbox(maxLen=maxLen, font=font, text=text.toRunes) +proc initText*(maxLen: int, font: Font, text="", color=newVec4f(0, 0, 0, 1)): Text = + initText(maxLen=maxLen, font=font, text=text.toRunes, color=color) diff -r 1f1e959a5fa3 -r 1ee397815b0b semicongine/vulkan/image.nim --- a/semicongine/vulkan/image.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/semicongine/vulkan/image.nim Sat Jan 20 20:17:03 2024 +0700 @@ -281,7 +281,10 @@ proc uploadTexture*(device: Device, texture: Texture): VulkanTexture = assert device.vk.valid - result.image = createImage(device=device, width=texture.image.width, height=texture.image.height, depth=4, data=addr texture.image.imagedata[0][0]) + if texture.isGrayscale: + result.image = createImage(device=device, width=texture.grayImage.width, height=texture.grayImage.height, depth=1, data=addr texture.grayImage.imagedata[0]) + else: + result.image = createImage(device=device, width=texture.colorImage.width, height=texture.colorImage.height, depth=4, data=addr texture.colorImage.imagedata[0][0]) result.imageView = result.image.createImageView() result.sampler = result.image.device.createSampler(texture.sampler) diff -r 1f1e959a5fa3 -r 1ee397815b0b tests/test_font.nim --- a/tests/test_font.nim Mon Jan 08 19:18:01 2024 +0700 +++ b/tests/test_font.nim Sat Jan 20 20:17:03 2024 +0700 @@ -9,16 +9,18 @@ # build scene var scene = Scene(name: "main") - var font = loadFont("DejaVuSans.ttf", color=newVec4f(1, 0.5, 0.5, 1), resolution=20) - var textbox = initTextbox(32, font, "") + # var font = loadFont("DejaVuSans.ttf", lineHeightPixels=90'f32, charset="abcdefghijklmnopqrstuvwxyz ".toRunes) + var font = loadFont("DejaVuSans.ttf", lineHeightPixels=90'f32) + var textbox = initText(32, font, "", color=newVec4f(1, 0, 0, 1)) + let fontscale = 0.005 scene.add textbox - textbox.mesh.transform = scale(0.01, 0.01) + textbox.mesh.transform = scale(fontscale, fontscale) engine.loadScene(scene) while engine.updateInputs() == Running and not engine.keyIsDown(Escape): if engine.windowWasResized(): var winSize = engine.getWindow().size - textbox.mesh.transform = scale(0.01 * (winSize[1] / winSize[0]), 0.01) + textbox.mesh.transform = scale(fontscale * (winSize[1] / winSize[0]), fontscale) for c in [Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I, Key.J, Key.K, Key.L, Key.M, Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S, Key.T, Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z]: if engine.keyWasPressed(c): if engine.keyIsDown(ShiftL) or engine.keyIsDown(ShiftR):