changeset 1331:1abdd42f5cfe

add: image layers
author sam <sam@basx.dev>
date Thu, 22 Aug 2024 18:31:03 +0700
parents b165359f45d7
children df3c075e5dea
files semicongine/image.nim semicongine/rendering.nim semicongine/rendering/renderer.nim semicongine/rendering/shaders.nim semicongine/rendering/vulkan_wrappers.nim
diffstat 5 files changed, 92 insertions(+), 34 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/image.nim	Mon Aug 19 19:48:40 2024 +0700
+++ b/semicongine/image.nim	Thu Aug 22 18:31:03 2024 +0700
@@ -22,7 +22,8 @@
   Gray* = TVec1[uint8]
   BGRA* = TVec4[uint8]
   PixelType* = Gray | BGRA
-  Image*[T: PixelType] = object
+
+  ImageObject*[T: PixelType, IsArray: static bool] = object
     width*: uint32
     height*: uint32
     minInterpolation*: VkFilter = VK_FILTER_LINEAR
@@ -35,7 +36,17 @@
     sampler*: VkSampler
     isRenderTarget*: bool = false
     samples*: VkSampleCountFlagBits = VK_SAMPLE_COUNT_1_BIT
+    when IsArray:
+      nLayers*: uint32
+  Image*[T: PixelType] = ImageObject[T, false]
+  ImageArray*[T: PixelType] = ImageObject[T, true]
 
+template nLayers*(image: Image): untyped =
+  1
+
+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] =
   when T is Gray:
     let nChannels = 1.cint
@@ -60,10 +71,43 @@
   copyMem(result.data.ToCPointer, data, imagesize)
   nativeFree(data)
 
-  when T is BGRA: # converkt to BGRA
+  when T is BGRA: # convert to BGRA
     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")
+
+  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])
+
 proc loadImage*[T: PixelType](path: string, package = DEFAULT_PACKAGE): Image[T] =
   assert path.splitFile().ext.toLowerAscii == ".png", "Unsupported image type: " & path.splitFile().ext.toLowerAscii
   when T is Gray:
@@ -73,6 +117,32 @@
 
   result = loadImageData[T](loadResource_intern(path, package = package).readAll())
 
+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.}
--- a/semicongine/rendering.nim	Mon Aug 19 19:48:40 2024 +0700
+++ b/semicongine/rendering.nim	Thu Aug 22 18:31:03 2024 +0700
@@ -374,16 +374,3 @@
     limits.framebufferColorSampleCounts.uint32 and limits.framebufferDepthSampleCounts.uint32
   ).toEnums
   return min(max(available), maxSamples)
-
-
-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
--- a/semicongine/rendering/renderer.nim	Mon Aug 19 19:48:40 2024 +0700
+++ b/semicongine/rendering/renderer.nim	Thu Aug 22 18:31:03 2024 +0700
@@ -56,7 +56,7 @@
   (if count == 0: gpuArray.data.len.uint64 else: count).uint64 * sizeof(elementType(gpuArray.data)).uint64
 template size(gpuValue: GPUValue): uint64 =
   sizeof(gpuValue.data).uint64
-func size(image: Image): uint64 =
+func size(image: ImageObject): uint64 =
   image.data.len.uint64 * sizeof(elementType(image.data)).uint64
 
 template rawPointer(gpuArray: GPUArray): pointer =
@@ -80,12 +80,12 @@
   for theName, value in descriptorSet.data.fieldPairs:
     when typeof(value) is GPUValue:
       assert value.buffer.vk.Valid
-    elif typeof(value) is Image:
+    elif typeof(value) is ImageObject:
       assert value.vk.Valid
       assert value.imageview.Valid
       assert value.sampler.Valid
     elif typeof(value) is array:
-      when elementType(value) is Image:
+      when elementType(value) is ImageObject:
         for t in value.litems:
           assert t.vk.Valid
           assert t.imageview.Valid
@@ -132,7 +132,7 @@
           pImageInfo: nil,
           pBufferInfo: addr(bufferWrites[^1]),
         )
-      elif typeof(fieldValue) is Image:
+      elif typeof(fieldValue) is ImageObject:
         imageWrites.add VkDescriptorImageInfo(
           sampler: fieldValue.sampler,
           imageView: fieldValue.imageView,
@@ -149,7 +149,7 @@
           pBufferInfo: nil,
         )
       elif typeof(fieldValue) is array:
-        when elementType(fieldValue) is Image:
+        when elementType(fieldValue) is ImageObject:
           for image in fieldValue.litems:
             imageWrites.add VkDescriptorImageInfo(
               sampler: image.sampler,
@@ -465,14 +465,14 @@
   )
   checkVkResult vkCreateSampler(vulkan.device, addr(samplerInfo), nil, addr(result))
 
-proc createVulkanImage(renderData: var RenderData, image: var Image) =
+proc createVulkanImage(renderData: var RenderData, image: var ImageObject) =
   assert image.vk == VkImage(0), "Image has already been created"
   var usage = @[VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_USAGE_SAMPLED_BIT]
   if image.isRenderTarget:
     usage.add VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
   let format = getVkFormat(grayscale = elementType(image.data) is Gray, usage = usage)
 
-  image.vk = svkCreate2DImage(image.width, image.height, format, usage, image.samples)
+  image.vk = svkCreate2DImage(image.width, image.height, format, usage, image.samples, image.nLayers)
   renderData.images.add image.vk
   image.sampler = createSampler(
     magFilter = image.magInterpolation,
@@ -512,7 +512,7 @@
   renderData.memory[memoryType][selectedBlockI].offsetNextFree += memoryRequirements.size
 
   # imageview can only be created after memory is bound
-  image.imageview = svkCreate2DImageView(image.vk, format)
+  image.imageview = svkCreate2DImageView(image.vk, format, nLayers=image.nLayers)
   renderData.imageViews.add image.imageview
 
   # data transfer and layout transition
@@ -529,10 +529,10 @@
 
 proc uploadImages*(renderdata: var RenderData, descriptorSet: var DescriptorSetData) =
   for name, value in fieldPairs(descriptorSet.data):
-    when typeof(value) is Image:
+    when typeof(value) is ImageObject:
       renderdata.createVulkanImage(value)
     elif typeof(value) is array:
-      when elementType(value) is Image:
+      when elementType(value) is ImageObject:
         for image in value.mitems:
           renderdata.createVulkanImage(image)
 
--- a/semicongine/rendering/shaders.nim	Mon Aug 19 19:48:40 2024 +0700
+++ b/semicongine/rendering/shaders.nim	Thu Aug 22 18:31:03 2024 +0700
@@ -1,4 +1,4 @@
-func glslType[T: SupportedGPUType|Image](value: T): string =
+func glslType[T: SupportedGPUType|ImageObject](value: T): string =
   when T is float32: "float"
   elif T is float64: "double"
   elif T is int8 or T is int16 or T is int32 or T is int64: "int"
@@ -36,6 +36,7 @@
   elif T is TMat4[float32]: "mat4"
   elif T is TMat4[float64]: "dmat4"
   elif T is Image: "sampler2D"
+  elif T is ImageArray: "sampler2DArray"
   else:
     const n = typetraits.name(T)
     {.error: "Unsupported data type on GPU: " & n.}
@@ -86,7 +87,7 @@
   else: {.error: "Unsupported data type on GPU".}
 
 
-func numberOfVertexInputAttributeDescriptors[T: SupportedGPUType|Image](value: T): uint32 =
+func numberOfVertexInputAttributeDescriptors[T: SupportedGPUType|ImageObject](value: T): uint32 =
   when T is TMat2[float32] or T is TMat2[float64] or T is TMat23[float32] or T is TMat23[float64]:
     2
   elif T is TMat32[float32] or T is TMat32[float64] or T is TMat3[float32] or T is TMat3[float64] or T is TMat34[float32] or T is TMat34[float64]:
@@ -96,7 +97,7 @@
   else:
     1
 
-func nLocationSlots[T: SupportedGPUType|Image](value: T): uint32 =
+func nLocationSlots[T: SupportedGPUType|ImageObject](value: T): uint32 =
   #[
   single location:
     - any scalar
@@ -169,7 +170,7 @@
 
       var descriptorBinding = 0
       for descriptorName, descriptorValue in fieldPairs(value):
-        when typeof(descriptorValue) is Image:
+        when typeof(descriptorValue) is ImageObject:
           samplers.add "layout(set=" & $setIndex & ", binding = " & $descriptorBinding & ") uniform " & glslType(descriptorValue) & " " & descriptorName & ";"
           descriptorBinding.inc
 
@@ -189,7 +190,7 @@
 
         elif typeof(descriptorValue) is array:
 
-          when elementType(descriptorValue) is Image:
+          when elementType(descriptorValue) is ImageObject:
 
             let arrayDecl = "[" & $typeof(descriptorValue).len & "]"
             samplers.add "layout(set=" & $setIndex & ", binding = " & $descriptorBinding & ") uniform " & glslType(default(elementType(descriptorValue))) & " " & descriptorName & "" & arrayDecl & ";"
--- a/semicongine/rendering/vulkan_wrappers.nim	Mon Aug 19 19:48:40 2024 +0700
+++ b/semicongine/rendering/vulkan_wrappers.nim	Thu Aug 22 18:31:03 2024 +0700
@@ -103,7 +103,7 @@
     addr(result),
   )
 
-proc svkCreate2DImage*(width, height: uint32, format: VkFormat, usage: openArray[VkImageUsageFlagBits], samples = VK_SAMPLE_COUNT_1_BIT): VkImage =
+proc svkCreate2DImage*(width, height: uint32, format: VkFormat, usage: openArray[VkImageUsageFlagBits], samples = VK_SAMPLE_COUNT_1_BIT, nLayers = 1'u32): VkImage =
   var imageProps: VkImageFormatProperties
   checkVkResult vkGetPhysicalDeviceImageFormatProperties(
     vulkan.physicalDevice,
@@ -120,7 +120,7 @@
     imageType: VK_IMAGE_TYPE_2D,
     extent: VkExtent3D(width: width, height: height, depth: 1),
     mipLevels: min(1'u32, imageProps.maxMipLevels),
-    arrayLayers: min(1'u32, imageProps.maxArrayLayers),
+    arrayLayers: min(nLayers, imageProps.maxArrayLayers),
     format: format,
     tiling: VK_IMAGE_TILING_OPTIMAL,
     initialLayout: VK_IMAGE_LAYOUT_UNDEFINED,
@@ -130,7 +130,7 @@
   )
   checkVkResult vkCreateImage(vulkan.device, addr imageInfo, nil, addr(result))
 
-proc svkCreate2DImageView*(image: VkImage, format: VkFormat, aspect = VK_IMAGE_ASPECT_COLOR_BIT): VkImageView =
+proc svkCreate2DImageView*(image: VkImage, format: VkFormat, aspect = VK_IMAGE_ASPECT_COLOR_BIT, nLayers=1'u32): VkImageView =
   var createInfo = VkImageViewCreateInfo(
     sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
     image: image,
@@ -147,7 +147,7 @@
       baseMipLevel: 0,
       levelCount: 1,
       baseArrayLayer: 0,
-      layerCount: 1,
+      layerCount: nLayers,
     ),
   )
   checkVkResult vkCreateImageView(vulkan.device, addr(createInfo), nil, addr(result))