changeset 1334:d5a6f69dc855

add: support for multi-layer images
author sam <sam@basx.dev>
date Thu, 22 Aug 2024 22:18:33 +0700
parents 966032c7a3aa
children f2ff6f624932
files semicongine/gltf.nim semicongine/image.nim semicongine/rendering.nim semicongine/rendering/renderer.nim semicongine/rendering/vulkan_wrappers.nim tests/resources/default/art1.png tests/test_rendering.nim
diffstat 7 files changed, 144 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/gltf.nim	Thu Aug 22 18:32:21 2024 +0700
+++ b/semicongine/gltf.nim	Thu Aug 22 22:18:33 2024 +0700
@@ -211,7 +211,8 @@
 
   let bufferView =
     root["bufferViews"][root["images"][imageIndex]["bufferView"].getInt()]
-  result = loadImageData[BGRA](getBufferViewData(bufferView, mainBuffer))
+  let img = loadImageData[BGRA](getBufferViewData(bufferView, mainBuffer))
+  result = Image[BGRA](width: img.width, height: img.height, data: img.data)
 
   if textureNode.hasKey("sampler"):
     let sampler = root["samplers"][textureNode["sampler"].getInt()]
--- a/semicongine/image.nim	Thu Aug 22 18:32:21 2024 +0700
+++ b/semicongine/image.nim	Thu Aug 22 22:18:33 2024 +0700
@@ -45,12 +45,14 @@
   ImageArray*[T: PixelType] = ImageObject[T, true]
 
 template nLayers*(image: Image): untyped =
-  1
+  1'u32
 
 proc `=copy`[S, T](dest: var ImageObject[S, T], source: ImageObject[S, T]) {.error.}
 
 # loads single layer image
-proc loadImageData*[T: PixelType](pngData: string | seq[uint8]): Image[T] =
+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:
@@ -69,8 +71,8 @@
   if data == nil:
     raise newException(Exception, "An error occured while loading PNG file")
 
-  let imagesize = w * h * 4
-  result = Image[T](width: w.uint32, height: h.uint32, data: newSeq[T](w * h))
+  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)
 
@@ -78,40 +80,18 @@
     for i in 0 ..< result.data.len:
       swap(result.data[i][0], result.data[i][2])
 
-proc addImageLayer*[T: PixelType](image: var Image[T], pngData: string | seq[uint8]) =
-  when T is Gray:
-    const nChannels = 1.cint
-  elif T is BGRA:
-    const 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")
+proc addImageLayer*[T: PixelType](
+    image: var ImageArray[T], pngData: string | seq[uint8]
+) =
+  let (w, h, data) = loadImageData[T](pngData)
 
   assert w == image.width,
     "New image layer has dimension {(w, y)} but image has dimension {(image.width, image.height)}"
   assert h == image.height,
     "New image layer has dimension {(w, y)} but image has dimension {(image.width, image.height)}"
 
-  let imagesize = image.width * image.height * nChannels
-  let layerOffset = image.width * image.height * image.nLayers
   inc image.nLayers
-  image.data.setLen(image.nLayers * image.width * image.height)
-  copyMem(addr(image.data[layerOffset]), data, imagesize)
-  nativeFree(data)
-
-  when T is BGRA: # convert to BGRA
-    for i in 0 ..< image.data.len:
-      swap(image.data[layerOffset + i][0], image.data[layerOffset + i][2])
+  image.data.add data
 
 proc loadImage*[T: PixelType](path: string, package = DEFAULT_PACKAGE): Image[T] =
   assert path.splitFile().ext.toLowerAscii == ".png",
@@ -121,7 +101,27 @@
   elif T is BGRA:
     let pngType = 6.cint
 
-  result = loadImageData[T](loadResource_intern(path, package = package).readAll())
+  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] =
+  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
+  when T is Gray:
+    let pngType = 0.cint
+  elif T is BGRA:
+    let pngType = 6.cint
+
+  let (width, height, data) =
+    loadImageData[T](loadResource_intern(paths[0], package = package).readAll())
+  result = ImageArray[T](width: width, height: height, data: data, nLayers: 1)
+  for path in paths[1 .. ^1]:
+    result.addImageLayer(loadResource_intern(path, package = package).readAll())
 
 proc `[]`*(image: Image, x, y: uint32): auto =
   assert x < image.width, &"{x} < {image.width} is not true"
--- a/semicongine/rendering.nim	Thu Aug 22 18:32:21 2024 +0700
+++ b/semicongine/rendering.nim	Thu Aug 22 22:18:33 2024 +0700
@@ -189,7 +189,7 @@
 ): untyped =
   var `bindingNumber` {.inject.} = 0'u32
   for theFieldname, `valuename` in fieldPairs(shader):
-    when typeof(`valuename`) is Image:
+    when typeof(`valuename`) is ImageObject:
       block:
         const `typename` {.inject.} = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
         const `countname` {.inject.} = 1'u32
@@ -202,7 +202,7 @@
         body
         `bindingNumber`.inc
     elif typeof(`valuename`) is array:
-      when elementType(`valuename`) is Image:
+      when elementType(`valuename`) is ImageObject:
         block:
           const `typename` {.inject.} = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
           const `countname` {.inject.} = uint32(typeof(`valuename`).len)
--- a/semicongine/rendering/renderer.nim	Thu Aug 22 18:32:21 2024 +0700
+++ b/semicongine/rendering/renderer.nim	Thu Aug 22 22:18:33 2024 +0700
@@ -289,7 +289,7 @@
   template selectedBlock(): untyped =
     renderData.memory[memoryType][selectedBlockI]
 
-  # let selectedBlock = 
+  # let selectedBlock =
   renderData.memory[memoryType][selectedBlockI].offsetNextFree =
     alignedTo(selectedBlock.offsetNextFree, memoryRequirements.alignment)
   checkVkResult vkBindBufferMemory(
@@ -425,7 +425,9 @@
     for memory in memoryBlocks:
       vkFreeMemory(vulkan.device, memory.vk, nil)
 
-proc transitionImageLayout(image: VkImage, oldLayout, newLayout: VkImageLayout) =
+proc transitionImageLayout(
+    image: VkImage, oldLayout, newLayout: VkImageLayout, nLayers: uint32
+) =
   var
     barrier = VkImageMemoryBarrier(
       sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
@@ -439,7 +441,7 @@
         baseMipLevel: 0,
         levelCount: 1,
         baseArrayLayer: 0,
-        layerCount: 1,
+        layerCount: nLayers,
       ),
     )
     srcStage: VkPipelineStageFlagBits
@@ -557,16 +559,19 @@
 
   # data transfer and layout transition
   transitionImageLayout(
-    image.vk, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
+    image.vk, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+    image.nLayers,
   )
   if image.data.len > 0:
     withStagingBuffer(
-      (image.vk, image.width, image.height), memoryRequirements.size, stagingPtr
+      (image.vk, image.width, image.height, image.nLayers),
+      memoryRequirements.size,
+      stagingPtr,
     ):
       copyMem(stagingPtr, image.data.ToCPointer, image.size)
   transitionImageLayout(
     image.vk, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, image.nLayers,
   )
 
 proc uploadImages*(renderdata: var RenderData, descriptorSet: var DescriptorSetData) =
--- a/semicongine/rendering/vulkan_wrappers.nim	Thu Aug 22 18:32:21 2024 +0700
+++ b/semicongine/rendering/vulkan_wrappers.nim	Thu Aug 22 22:18:33 2024 +0700
@@ -128,7 +128,7 @@
     imageType: VK_IMAGE_TYPE_2D,
     extent: VkExtent3D(width: width, height: height, depth: 1),
     mipLevels: min(1'u32, imageProps.maxMipLevels),
-    arrayLayers: min(nLayers, imageProps.maxArrayLayers),
+    arrayLayers: nLayers,
     format: format,
     tiling: VK_IMAGE_TILING_OPTIMAL,
     initialLayout: VK_IMAGE_LAYOUT_UNDEFINED,
@@ -147,7 +147,7 @@
   var createInfo = VkImageViewCreateInfo(
     sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
     image: image,
-    viewType: VK_IMAGE_VIEW_TYPE_2D,
+    viewType: if nLayers == 1: VK_IMAGE_VIEW_TYPE_2D else: VK_IMAGE_VIEW_TYPE_2D_ARRAY,
     format: format,
     components: VkComponentMapping(
       r: VK_COMPONENT_SWIZZLE_IDENTITY,
@@ -363,7 +363,7 @@
     vkDestroyFence(vulkan.device, fence, nil)
     vkDestroyCommandPool(vulkan.device, commandBufferPool, nil)
 
-template withStagingBuffer*[T: (VkBuffer, uint64) | (VkImage, uint32, uint32)](
+template withStagingBuffer*[T: (VkBuffer, uint64) | (VkImage, uint32, uint32, uint32)](
     target: T, bufferSize: uint64, dataPointer, body: untyped
 ): untyped =
   var `dataPointer` {.inject.}: pointer
@@ -419,7 +419,7 @@
         regionCount = 1,
         pRegions = addr(copyRegion),
       )
-    elif T is (VkImage, uint32, uint32):
+    elif T is (VkImage, uint32, uint32, uint32):
       let region = VkBufferImageCopy(
         bufferOffset: 0,
         bufferRowLength: 0,
@@ -428,7 +428,7 @@
           aspectMask: toBits [VK_IMAGE_ASPECT_COLOR_BIT],
           mipLevel: 0,
           baseArrayLayer: 0,
-          layerCount: 1,
+          layerCount: target[3],
         ),
         imageOffset: VkOffset3D(x: 0, y: 0, z: 0),
         imageExtent: VkExtent3D(width: target[1], height: target[2], depth: 1),
Binary file tests/resources/default/art1.png has changed
--- a/tests/test_rendering.nim	Thu Aug 22 18:32:21 2024 +0700
+++ b/tests/test_rendering.nim	Thu Aug 22 22:18:33 2024 +0700
@@ -666,6 +666,86 @@
   destroyPipeline(pipeline4)
   destroyRenderData(renderdata)
 
+proc test_08_texture_array(time: float32) =
+  var renderdata = initRenderData()
+
+  type
+    Uniforms = object
+      textures: ImageArray[BGRA]
+
+    Shader = object
+      position {.VertexAttribute.}: Vec3f
+      uv {.VertexAttribute.}: Vec2f
+      fragmentUv {.Pass.}: Vec2f
+      outColor {.ShaderOutput.}: Vec4f
+      descriptorSets {.DescriptorSet: 0.}: Uniforms
+      # code
+      vertexCode: string =
+        """
+void main() {
+    fragmentUv = uv;
+    gl_Position = vec4(position, 1);
+}"""
+      fragmentCode: string =
+        """
+void main() {
+    vec4 col1 = texture(textures, vec3(fragmentUv, 0));
+    vec4 col2 = texture(textures, vec3(fragmentUv, 1));
+    float w = length(fragmentUv * 2 - 1) / 1.41421;
+    outColor = (1 - w) * col1 + w * col2;
+}"""
+
+    Quad = object
+      position: GPUArray[Vec3f, VertexBuffer]
+      uv: GPUArray[Vec2f, VertexBuffer]
+
+  var mesh = Quad(
+    position: asGPUArray(
+      [
+        vec3(-0.8, -0.5),
+        vec3(-0.8, 0.5),
+        vec3(0.8, 0.5),
+        vec3(0.8, 0.5),
+        vec3(0.8, -0.5),
+        vec3(-0.8, -0.5),
+      ],
+      VertexBuffer,
+    ),
+    uv: asGPUArray(
+      [vec2(0, 1), vec2(0, 0), vec2(1, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1)],
+      VertexBuffer,
+    ),
+  )
+  assignBuffers(renderdata, mesh)
+  renderdata.flushAllMemory()
+
+  var pipeline = createPipeline[Shader](renderPass = vulkan.swapchain.renderPass)
+  var uniforms1 = asDescriptorSetData(
+    Uniforms(textures: loadImageArray[BGRA](["art.png", "art1.png"]))
+  )
+  uploadImages(renderdata, uniforms1)
+  initDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], uniforms1)
+
+  var start = getMonoTime()
+  while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
+    withNextFrame(framebuffer, commandbuffer):
+      withRenderPass(
+        vulkan.swapchain.renderPass,
+        framebuffer,
+        commandbuffer,
+        vulkan.swapchain.width,
+        vulkan.swapchain.height,
+        vec4(0, 0, 0, 0),
+      ):
+        withPipeline(commandbuffer, pipeline):
+          bindDescriptorSet(commandbuffer, uniforms1, 0, pipeline)
+          render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = mesh)
+
+  # cleanup
+  checkVkResult vkDeviceWaitIdle(vulkan.device)
+  destroyPipeline(pipeline)
+  destroyRenderData(renderdata)
+
 proc test_07_png_texture(time: float32) =
   var renderdata = initRenderData()
 
@@ -741,7 +821,7 @@
   destroyPipeline(pipeline)
   destroyRenderData(renderdata)
 
-proc test_08_triangle_2pass(
+proc test_09_triangle_2pass(
     time: float32, depthBuffer: bool, samples: VkSampleCountFlagBits
 ) =
   var (offscreenRP, presentRP) =
@@ -952,32 +1032,33 @@
     setupSwapchain(renderpass = renderpass)
 
     # tests a simple triangle with minimalistic shader and vertex format
-    test_01_triangle(time)
+    # test_01_triangle(time)
 
     # tests instanced triangles and quads, mixing meshes and instances
-    test_02_triangle_quad_instanced(time)
+    # test_02_triangle_quad_instanced(time)
 
     # teste descriptor sets
-    test_03_simple_descriptorset(time)
+    # test_03_simple_descriptorset(time)
 
     # tests multiple descriptor sets and arrays
-    test_04_multiple_descriptorsets(time)
+    # test_04_multiple_descriptorsets(time)
 
     # rotating cube
-    test_05_cube(time)
+    # test_05_cube(time)
 
     # different draw modes (lines, points, and topologies)
-    test_06_different_draw_modes(time)
+    # test_06_different_draw_modes(time)
 
-    # load PNG texture
-    test_07_png_texture(time)
+    # test_07_png_texture(time)
+
+    test_08_texture_array(time)
 
     checkVkResult vkDeviceWaitIdle(vulkan.device)
     destroyRenderPass(renderpass)
     clearSwapchain()
 
   # test multiple render passes
-  for i, (depthBuffer, samples) in renderPasses:
-    test_08_triangle_2pass(time, depthBuffer, samples)
+  # for i, (depthBuffer, samples) in renderPasses:
+  # test_09_triangle_2pass(time, depthBuffer, samples)
 
   destroyVulkan()