changeset 962:7855746f2d13

add: support for color space conversion, convert color space if srgb is not available for images
author sam <sam@basx.dev>
date Tue, 02 Apr 2024 22:02:44 +0700
parents 616f97e92270
children 9d3f6d742e65
files semicongine/core/imagetypes.nim semicongine/vulkan/image.nim
diffstat 2 files changed, 91 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/imagetypes.nim	Tue Apr 02 16:48:23 2024 +0700
+++ b/semicongine/core/imagetypes.nim	Tue Apr 02 22:02:44 2024 +0700
@@ -1,3 +1,4 @@
+import std/math
 import std/strformat
 
 import ./vulkanapi
@@ -38,6 +39,41 @@
 converter toGrayscale*(p: GrayPixel): float32 =
   float32(p) / 255'f32
 
+# colorspace conversion functions
+
+func linear2srgb(value: float): float =
+  clamp(
+    if (value < 0.0031308): value * 12.92
+    else: pow(value, 1.0 / 2.4) * 1.055 - 0.055,
+    0,
+    1,
+  )
+func srgb2linear(value: float): float =
+  clamp(
+    if (value < 0.04045): value / 12.92
+    else: pow((value + 0.055) / 1.055, 2.4),
+    0,
+    1,
+  )
+func linear2srgb(value: uint8): uint8 = # also covers GrayPixel
+  uint8(round(linear2srgb(float(value) / 255.0) * 255))
+func linear2srgb(value: RGBAPixel): RGBAPixel =
+  [linear2srgb(value[0]), linear2srgb(value[1]), linear2srgb(value[2]), value[3]]
+func srgb2linear(value: uint8): uint8 = # also covers GrayPixel
+  uint8(round(srgb2linear(float(value) / 255.0) * 255))
+func srgb2linear(value: RGBAPixel): RGBAPixel =
+  [srgb2linear(value[0]), srgb2linear(value[1]), srgb2linear(value[2]), value[3]]
+
+proc asSRGB*[T](image: Image[T]): Image[T] =
+  result = Image[T](width: image.width, height: image.height, imagedata: newSeq[T](image.imagedata.len))
+  for i in 0 .. image.imagedata.len:
+    result.imagedata[i] = linear2srgb(image.imagedata[i])
+
+proc asLinear*[T](image: Image[T]): Image[T] =
+  result = Image[T](width: image.width, height: image.height, imagedata: newSeq[T](image.imagedata.len))
+  for i in 0 .. image.imagedata.len:
+    result.imagedata[i] = srgb2linear(image.imagedata[i])
+
 proc `$`*(image: Image): string =
   &"{image.width}x{image.height}"
 
--- a/semicongine/vulkan/image.nim	Tue Apr 02 16:48:23 2024 +0700
+++ b/semicongine/vulkan/image.nim	Tue Apr 02 22:02:44 2024 +0700
@@ -35,12 +35,20 @@
     sampler*: VulkanSampler
 
 const DEPTH_FORMAT_MAP = {
-  PixelDepth(1): VK_FORMAT_R8_SRGB,
-  PixelDepth(2): VK_FORMAT_R8G8_SRGB,
-  PixelDepth(3): VK_FORMAT_R8G8B8_SRGB,
-  PixelDepth(4): VK_FORMAT_R8G8B8A8_SRGB,
+  PixelDepth(1): [VK_FORMAT_R8_SRGB, VK_FORMAT_R8_UNORM],
+  PixelDepth(2): [VK_FORMAT_R8G8_SRGB, VK_FORMAT_R8G8_UNORM],
+  PixelDepth(3): [VK_FORMAT_R8G8B8_SRGB, VK_FORMAT_R8G8B8_UNORM],
+  PixelDepth(4): [VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_R8G8B8A8_UNORM],
 }.toTable
 
+const LINEAR_FORMATS = [
+  VK_FORMAT_R8_UNORM,
+  VK_FORMAT_R8G8_UNORM,
+  VK_FORMAT_R8G8B8_UNORM,
+  VK_FORMAT_R8G8B8A8_UNORM,
+]
+
+
 proc requirements(image: VulkanImage): MemoryRequirements =
   assert image.vk.valid
   assert image.device.vk.valid
@@ -143,36 +151,55 @@
     )
 
 # currently only usable for texture access from shader
-proc createImage(device: Device, queue: Queue, width, height: uint32, depth: PixelDepth, data: pointer): VulkanImage =
+proc createImage[T](device: Device, queue: Queue, width, height: uint32, depth: PixelDepth, image: Image[T]): VulkanImage =
   assert device.vk.valid
   assert width > 0
   assert height > 0
   assert depth != 2
-  assert data != nil
+
+  result.device = device
+  result.usage = @[VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_USAGE_SAMPLED_BIT]
 
   let size: uint64 = width * height * uint32(depth)
-  result.device = device
+  let usageBits = toBits result.usage
+  var formatList = DEPTH_FORMAT_MAP[depth]
+  var selectedFormat: VkFormat
+  var formatProperties = VkImageFormatProperties2(sType: VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2)
+
+  for format in formatList:
+    var formatInfo = VkPhysicalDeviceImageFormatInfo2(
+      sType: VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2,
+      format: format,
+      thetype: VK_IMAGE_TYPE_2D,
+      tiling: VK_IMAGE_TILING_OPTIMAL,
+      usage: usageBits,
+    )
+    let formatCheck = device.physicalDevice.vk.vkGetPhysicalDeviceImageFormatProperties2(
+      addr formatInfo,
+      addr formatProperties,
+    )
+    if formatCheck == VK_SUCCESS: # found suitable format
+      selectedFormat = format
+      break
+    elif formatCheck == VK_ERROR_FORMAT_NOT_SUPPORTED: # nope, try to find other format
+      continue
+    else: # raise error
+      checkVkResult formatCheck
+
+  # assumption: images comes in sRGB color space
+  var data = addr image.imagedata[0]
+  if selectedFormat in LINEAR_FORMATS:
+    let linearImage = image.asLinear()
+    data = addr image.imagedata[0]
+
+  assert size <= uint64(formatProperties.imageFormatProperties.maxResourceSize)
+  assert width <= uint64(formatProperties.imageFormatProperties.maxExtent.width)
+  assert height <= uint64(formatProperties.imageFormatProperties.maxExtent.height)
+
   result.width = width
   result.height = height
   result.depth = depth
-  result.format = DEPTH_FORMAT_MAP[depth]
-  result.usage = @[VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_USAGE_SAMPLED_BIT]
-
-  var formatInfo = VkPhysicalDeviceImageFormatInfo2(
-    sType: VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2,
-    format: result.format,
-    thetype: VK_IMAGE_TYPE_2D,
-    tiling: VK_IMAGE_TILING_OPTIMAL,
-    usage: toBits result.usage,
-  )
-  var formatProperties = VkImageFormatProperties2(sType: VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2)
-  checkVkResult device.physicalDevice.vk.vkGetPhysicalDeviceImageFormatProperties2(
-    addr formatInfo,
-    addr formatProperties,
-  )
-  assert size <= uint64(formatProperties.imageFormatProperties.maxResourceSize)
-  assert width <= uint64(formatProperties.imageFormatProperties.maxExtent.width)
-  assert height <= uint64(formatProperties.imageFormatProperties.maxExtent.height)
+  result.format = selectedFormat
 
   var imageInfo = VkImageCreateInfo(
     sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
@@ -183,7 +210,7 @@
     format: result.format,
     tiling: VK_IMAGE_TILING_OPTIMAL,
     initialLayout: VK_IMAGE_LAYOUT_UNDEFINED,
-    usage: toBits result.usage,
+    usage: usageBits,
     sharingMode: VK_SHARING_MODE_EXCLUSIVE,
     samples: VK_SAMPLE_COUNT_1_BIT,
   )
@@ -291,9 +318,9 @@
 proc uploadTexture*(device: Device, queue: Queue, texture: Texture): VulkanTexture =
   assert device.vk.valid
   if texture.isGrayscale:
-    result.image = createImage(device = device, queue = queue, width = texture.grayImage.width, height = texture.grayImage.height, depth = 1, data = addr texture.grayImage.imagedata[0])
+    result.image = createImage(device = device, queue = queue, width = texture.grayImage.width, height = texture.grayImage.height, depth = 1, image = texture.grayImage)
   else:
-    result.image = createImage(device = device, queue = queue, width = texture.colorImage.width, height = texture.colorImage.height, depth = 4, data = addr texture.colorImage.imagedata[0][0])
+    result.image = createImage(device = device, queue = queue, width = texture.colorImage.width, height = texture.colorImage.height, depth = 4, image = texture.colorImage)
   result.imageView = result.image.createImageView()
   result.sampler = result.image.device.createSampler(texture.sampler)