Source code for simvx.graphics.assets.cubemap_loader

"""Load cubemap textures from 6 face images or equirectangular HDR."""


from __future__ import annotations

import logging
from typing import Any

import numpy as np
import vulkan as vk

from ..gpu.memory import create_sampler

__all__ = ["load_cubemap"]

log = logging.getLogger(__name__)


[docs] def load_cubemap( device: Any, physical_device: Any, queue: Any, cmd_pool: Any, face_paths: list[str] | None = None, colour: tuple[float, float, float] | None = None, ) -> tuple[Any, Any, Any, Any]: """Load a cubemap from 6 face images or a solid colour. Args: face_paths: List of 6 image paths [+X, -X, +Y, -Y, +Z, -Z]. colour: Solid colour (r, g, b) in 0-1 range (fallback if no paths). Returns: (image_view, sampler, image, memory) tuple for the cubemap. """ ffi = vk.ffi if face_paths and len(face_paths) == 6: faces, width, height = _load_face_images(face_paths) else: # Solid colour fallback c = colour or (0.2, 0.3, 0.5) width, height = 64, 64 pixel = np.array([c[0], c[1], c[2], 1.0], dtype=np.float32) face_data = np.tile(pixel, width * height).astype(np.float32) faces = [face_data.tobytes() for _ in range(6)] fmt = vk.VK_FORMAT_R32G32B32A32_SFLOAT pixel_size = 16 # 4 floats * 4 bytes # Create cubemap image image_ci = ffi.new("VkImageCreateInfo*") image_ci.sType = vk.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO image_ci.imageType = vk.VK_IMAGE_TYPE_2D image_ci.format = fmt image_ci.extent.width = width image_ci.extent.height = height image_ci.extent.depth = 1 image_ci.mipLevels = 1 image_ci.arrayLayers = 6 image_ci.samples = vk.VK_SAMPLE_COUNT_1_BIT image_ci.tiling = vk.VK_IMAGE_TILING_OPTIMAL image_ci.usage = vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT | vk.VK_IMAGE_USAGE_SAMPLED_BIT image_ci.sharingMode = vk.VK_SHARING_MODE_EXCLUSIVE image_ci.initialLayout = vk.VK_IMAGE_LAYOUT_UNDEFINED image_ci.flags = vk.VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT image_out = ffi.new("VkImage*") result = vk._vulkan._callApi( vk._vulkan.lib.vkCreateImage, device, image_ci, ffi.NULL, image_out, ) if result != vk.VK_SUCCESS: raise RuntimeError(f"vkCreateImage failed: {result}") image = image_out[0] # Allocate and bind memory mem_req = vk.vkGetImageMemoryRequirements(device, image) from ..gpu.memory import _find_memory_type mem_type = _find_memory_type(physical_device, mem_req.memoryTypeBits, vk.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) alloc_info = vk.VkMemoryAllocateInfo( allocationSize=mem_req.size, memoryTypeIndex=mem_type, ) memory = vk.vkAllocateMemory(device, alloc_info, None) vk.vkBindImageMemory(device, image, memory, 0) # Upload face data via staging buffer face_size = width * height * pixel_size from ..gpu.memory import create_buffer, upload_numpy staging_buf, staging_mem = create_buffer( device, physical_device, face_size, vk.VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | vk.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, ) # Transition to TRANSFER_DST, copy each face, transition to SHADER_READ cmd_ai = vk.VkCommandBufferAllocateInfo( commandPool=cmd_pool, level=vk.VK_COMMAND_BUFFER_LEVEL_PRIMARY, commandBufferCount=1, ) cmds = vk.vkAllocateCommandBuffers(device, cmd_ai) cmd = cmds[0] vk.vkBeginCommandBuffer( cmd, vk.VkCommandBufferBeginInfo( flags=vk.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ), ) # Transition entire cubemap to TRANSFER_DST barrier = vk.VkImageMemoryBarrier( srcAccessMask=0, dstAccessMask=vk.VK_ACCESS_TRANSFER_WRITE_BIT, oldLayout=vk.VK_IMAGE_LAYOUT_UNDEFINED, newLayout=vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, image=image, subresourceRange=vk.VkImageSubresourceRange( aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, baseMipLevel=0, levelCount=1, baseArrayLayer=0, layerCount=6, ), ) vk.vkCmdPipelineBarrier( cmd, vk.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, vk.VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, None, 0, None, 1, [barrier] ) for face_idx in range(6): # Upload face to staging face_bytes = faces[face_idx] if isinstance(face_bytes, np.ndarray): face_bytes = face_bytes.tobytes() upload_numpy(device, staging_mem, np.frombuffer(face_bytes, dtype=np.uint8)) # Copy staging → cubemap face region = vk.VkBufferImageCopy( bufferOffset=0, bufferRowLength=0, bufferImageHeight=0, imageSubresource=vk.VkImageSubresourceLayers( aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, mipLevel=0, baseArrayLayer=face_idx, layerCount=1, ), imageOffset=vk.VkOffset3D(x=0, y=0, z=0), imageExtent=vk.VkExtent3D(width=width, height=height, depth=1), ) vk.vkCmdCopyBufferToImage(cmd, staging_buf, image, vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, [region]) # Transition to SHADER_READ barrier2 = vk.VkImageMemoryBarrier( srcAccessMask=vk.VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask=vk.VK_ACCESS_SHADER_READ_BIT, oldLayout=vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, newLayout=vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, image=image, subresourceRange=vk.VkImageSubresourceRange( aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, baseMipLevel=0, levelCount=1, baseArrayLayer=0, layerCount=6, ), ) vk.vkCmdPipelineBarrier( cmd, vk.VK_PIPELINE_STAGE_TRANSFER_BIT, vk.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, None, 0, None, 1, [barrier2], ) vk.vkEndCommandBuffer(cmd) vk.vkQueueSubmit( queue, 1, [ vk.VkSubmitInfo( commandBufferCount=1, pCommandBuffers=[cmd], ) ], None, ) vk.vkQueueWaitIdle(queue) vk.vkFreeCommandBuffers(device, cmd_pool, 1, [cmd]) # Cleanup staging vk.vkDestroyBuffer(device, staging_buf, None) vk.vkFreeMemory(device, staging_mem, None) # Create cubemap image view view = vk.vkCreateImageView( device, vk.VkImageViewCreateInfo( image=image, viewType=vk.VK_IMAGE_VIEW_TYPE_CUBE, format=fmt, subresourceRange=vk.VkImageSubresourceRange( aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, baseMipLevel=0, levelCount=1, baseArrayLayer=0, layerCount=6, ), ), None, ) sampler = create_sampler(device) log.debug("Cubemap loaded (%dx%d)", width, height) return view, sampler, image, memory
def _load_face_images(paths: list[str]) -> tuple[list[bytes], int, int]: """Load 6 face images and return (face_data_list, width, height).""" try: from PIL import Image except ImportError: raise ImportError("Pillow is required for cubemap face loading: pip install Pillow") from None faces = [] width = height = 0 for path in paths: img = Image.open(path).convert("RGBA") if width == 0: width, height = img.size else: img = img.resize((width, height)) # Convert to float32 RGBA data = np.array(img, dtype=np.float32) / 255.0 faces.append(data.tobytes()) return faces, width, height