diff --git a/62_CAD/DrawResourcesFiller.cpp b/62_CAD/DrawResourcesFiller.cpp index ec5058232..5d2fea009 100644 --- a/62_CAD/DrawResourcesFiller.cpp +++ b/62_CAD/DrawResourcesFiller.cpp @@ -661,10 +661,9 @@ bool DrawResourcesFiller::ensureGeoreferencedImageAvailability_AllocateIfNeeded( auto evictCallback = [&](image_id imageID, const CachedImageRecord& evicted) { evictImage_SubmitIfNeeded(imageID, evicted, intendedNextSubmit); }; CachedImageRecord* cachedImageRecord = imagesCache->insert(imageID, intendedNextSubmit.getFutureScratchSemaphore().value, evictCallback); - // TODO: Function call that gets you image creaation params based on georeferencedImageParams (extents and mips and whatever), it will also get you the GEOREFERENED TYPE + // TODO: Function call that gets you image creaation params based on georeferencedImageParams (extents and mips and whatever), it will also get you the GEOREFERENCED TYPE IGPUImage::SCreationParams imageCreationParams = {}; - ImageType georeferenceImageType; - determineGeoreferencedImageCreationParams(imageCreationParams, georeferenceImageType, params); + ImageType imageType = determineGeoreferencedImageCreationParams(imageCreationParams, params); // imageParams = cpuImage->getCreationParameters(); imageCreationParams.usage |= IGPUImage::EUF_TRANSFER_DST_BIT|IGPUImage::EUF_SAMPLED_BIT; @@ -691,7 +690,7 @@ bool DrawResourcesFiller::ensureGeoreferencedImageAvailability_AllocateIfNeeded( const auto cachedImageType = cachedImageRecord->type; // image type and creation params (most importantly extent and format) should match, otherwise we evict, recreate and re-pus const auto currentParams = static_cast(imageCreationParams); - const bool needsRecreation = cachedImageType != georeferenceImageType || cachedParams != currentParams; + const bool needsRecreation = cachedImageType != imageType || cachedParams != currentParams; if (needsRecreation) { // call the eviction callback so the currently cached imageID gets eventually deallocated from memory arena. @@ -729,7 +728,7 @@ bool DrawResourcesFiller::ensureGeoreferencedImageAvailability_AllocateIfNeeded( if (allocResults.isValid()) { - cachedImageRecord->type = georeferenceImageType; + cachedImageRecord->type = imageType; cachedImageRecord->state = ImageState::CREATED_AND_MEMORY_BOUND; cachedImageRecord->lastUsedFrameIndex = currentFrameIndex; // there was an eviction + auto-submit, we need to update AGAIN cachedImageRecord->allocationOffset = allocResults.allocationOffset; @@ -885,7 +884,7 @@ void DrawResourcesFiller::addImageObject(image_id imageID, const OrientedBoundin endMainObject(); } -void DrawResourcesFiller::addGeoreferencedImage(image_id imageID, const GeoreferencedImageParams& params, SIntendedSubmitInfo& intendedNextSubmit) +void DrawResourcesFiller::addGeoreferencedImage(StreamedImageManager& manager, const float64_t3x3& NDCToWorld, SIntendedSubmitInfo& intendedNextSubmit) { beginMainObject(MainObjectType::STREAMED_IMAGE); @@ -897,11 +896,30 @@ void DrawResourcesFiller::addGeoreferencedImage(image_id imageID, const Georefer return; } + // Query imageType + auto cachedImageRecord = imagesCache->peek(manager.imageID); + + manager.maxResidentTiles.x = cachedImageRecord->gpuImageView->getCreationParameters().image->getCreationParameters().extent.width / manager.TileSize; + manager.maxResidentTiles.y = manager.maxResidentTiles.x; + // Create a "sliding window OBB" that we use to offset tiles + manager.fromTopLeftOBB.topLeft = manager.georeferencedImageParams.worldspaceOBB.topLeft; + manager.fromTopLeftOBB.dirU = manager.georeferencedImageParams.worldspaceOBB.dirU * float32_t(manager.TileSize * manager.maxResidentTiles.x) / float32_t(manager.georeferencedImageParams.imageExtents.x); + manager.fromTopLeftOBB.aspectRatio = float32_t(manager.maxResidentTiles.y) / float32_t(manager.maxResidentTiles.x); + + // Generate upload data + auto uploadData = manager.generateTileUploadData(cachedImageRecord->type, NDCToWorld); + + // Queue image uploads + for (const auto& imageCopy : uploadData.tiles) + queueGeoreferencedImageCopy_Internal(manager.imageID, imageCopy); + GeoreferencedImageInfo info = {}; - info.topLeft = params.worldspaceOBB.topLeft; - info.dirU = params.worldspaceOBB.dirU; - info.aspectRatio = params.worldspaceOBB.aspectRatio; - info.textureID = getImageIndexFromID(imageID, intendedNextSubmit); // for this to be valid and safe, this function needs to be called immediately after `addStaticImage` function to make sure image is in memory + info.topLeft = uploadData.worldspaceOBB.topLeft; + info.dirU = uploadData.worldspaceOBB.dirU; + info.aspectRatio = uploadData.worldspaceOBB.aspectRatio; + info.textureID = getImageIndexFromID(manager.imageID, intendedNextSubmit); // for this to be valid and safe, this function needs to be called immediately after `addStaticImage` function to make sure image is in memory + info.minUV = uploadData.minUV; + info.maxUV = uploadData.maxUV; if (!addGeoreferencedImageInfo_Internal(info, mainObjIdx)) { // single image object couldn't fit into memory to push to gpu, so we submit rendering current objects and reset geometry buffer and draw objects @@ -1385,7 +1403,7 @@ bool DrawResourcesFiller::pushStaticImagesUploads(SIntendedSubmitInfo& intendedN std::vector nonResidentImageRecords; for (auto& [id, record] : imagesCache) { - if (record.staticCPUImage && record.type == ImageType::STATIC && record.state < ImageState::GPU_RESIDENT_WITH_VALID_STATIC_DATA) + if (record.staticCPUImage && (record.type == ImageType::STATIC || record.type == ImageType::GEOREFERENCED_FULL_RESOLUTION) && record.state < ImageState::GPU_RESIDENT_WITH_VALID_STATIC_DATA) nonResidentImageRecords.push_back(&record); } @@ -1572,7 +1590,7 @@ bool DrawResourcesFiller::pushStreamedImagesUploads(SIntendedSubmitInfo& intende std::vector afterCopyImageBarriers; afterCopyImageBarriers.reserve(streamedImageCopies.size()); - // Pipeline Barriers before imageCopy + // Pipeline Barriers after imageCopy for (auto& [imageID, imageCopies] : streamedImageCopies) { auto* imageRecord = imagesCache->peek(imageID); @@ -2486,35 +2504,45 @@ DrawResourcesFiller::ImageAllocateResults DrawResourcesFiller::tryCreateAndAlloc return ret; } -void DrawResourcesFiller::determineGeoreferencedImageCreationParams(nbl::asset::IImage::SCreationParams& outImageParams, ImageType& outImageType, const GeoreferencedImageParams& georeferencedImageParams) +ImageType DrawResourcesFiller::determineGeoreferencedImageCreationParams(nbl::asset::IImage::SCreationParams& outImageParams, const GeoreferencedImageParams& params) { // Decide whether the image can reside fully into memory rather than get streamed. // TODO: Improve logic, currently just a simple check to see if the full-screen image has more pixels that viewport or not // TODO: add criterial that the size of the full-res image shouldn't consume more than 30% of the total memory arena for images (if we allowed larger than viewport extents) - const bool betterToResideFullyInMem = georeferencedImageParams.imageExtents.x * georeferencedImageParams.imageExtents.y <= georeferencedImageParams.viewportExtents.x * georeferencedImageParams.viewportExtents.y; + const bool betterToResideFullyInMem = params.imageExtents.x * params.imageExtents.y <= params.viewportExtents.x * params.viewportExtents.y; + + ImageType imageType; if (betterToResideFullyInMem) - outImageType = ImageType::GEOREFERENCED_FULL_RESOLUTION; + imageType = ImageType::GEOREFERENCED_FULL_RESOLUTION; else - outImageType = ImageType::GEOREFERENCED_STREAMED; + imageType = ImageType::GEOREFERENCED_STREAMED; outImageParams.type = asset::IImage::ET_2D; outImageParams.samples = asset::IImage::ESCF_1_BIT; - outImageParams.format = georeferencedImageParams.format; + outImageParams.format = params.format; - if (outImageType == ImageType::GEOREFERENCED_FULL_RESOLUTION) + if (imageType == ImageType::GEOREFERENCED_FULL_RESOLUTION) { - outImageParams.extent = { georeferencedImageParams.imageExtents.x, georeferencedImageParams.imageExtents.y, 1u }; + outImageParams.extent = { params.imageExtents.x, params.imageExtents.y, 1u }; } else { - // TODO: Better Logic, area around the view, etc... - outImageParams.extent = { georeferencedImageParams.viewportExtents.x, georeferencedImageParams.viewportExtents.y, 1u }; + // Enough to cover twice the viewport at mip 0 (so that when zooming out to mip 1 the whole viewport still gets covered with mip 0 tiles) + // and in any rotation (taking the longest side suffices). Can be increased to avoid frequent tile eviction when moving the camera at mip close to 1 + const uint32_t diagonal = static_cast(nbl::hlsl::ceil( + nbl::hlsl::sqrt(static_cast(params.viewportExtents.x * params.viewportExtents.x + + params.viewportExtents.y * params.viewportExtents.y)) + ) + ); + const uint32_t gpuImageSidelength = 2 * core::roundUp(diagonal, StreamedImageManager::TileSize) + StreamedImageManager::PaddingTiles * StreamedImageManager::TileSize; + outImageParams.extent = { gpuImageSidelength, gpuImageSidelength, 1u }; } - outImageParams.mipLevels = 1u; // TODO: Later do mipmapping outImageParams.arrayLayers = 1u; + + return imageType; } void DrawResourcesFiller::setGlyphMSDFTextureFunction(const GetGlyphMSDFTextureFunc& func) @@ -2649,4 +2677,180 @@ void DrawResourcesFiller::flushDrawObjects() drawCalls.push_back(drawCall); drawObjectsFlushedToDrawCalls = resourcesCollection.drawObjects.getCount(); } -} \ No newline at end of file +} + +DrawResourcesFiller::StreamedImageManager::StreamedImageManager(image_id _imageID, GeoreferencedImageParams&& _georeferencedImageParams, ImageLoader&& _loader) + : imageID(_imageID), georeferencedImageParams(std::move(_georeferencedImageParams)), loader(std::move(_loader)) +{ + maxImageTileIndices = georeferencedImageParams.imageExtents / uint32_t2(TileSize, TileSize); + // If it fits perfectly along any dimension, we need one less tile with this scheme + maxImageTileIndices -= uint32_t2(maxImageTileIndices.x * TileSize == georeferencedImageParams.imageExtents.x, maxImageTileIndices.y * TileSize == georeferencedImageParams.imageExtents.y); + + // R^2 can be covered with a lattice of image tiles. Real tiles (those actually covered by the image) are indexed in the range [0, maxImageTileIndices.x] x [0, maxImageTileIndices.y], + // but part of the algorithm to figure out which tiles need to be resident for a draw involves figuring out the coordinates in this lattice of each of the viewport corners. + // To that end, we devise an algorithm that maps a point in worldspace to its coordinates in this tile lattice: + // 1. Get the displacement (will be an offset vector in world coords and world units) from the `topLeft` corner of the image to the point + // 2. Transform this displacement vector into a displacement into the coordinates spanned by the basis {dirU, dirV}. Notice that these vectors are still in world units + // 3. Map world units to tile units. This scaling is generally nonuniform, since it depends on the ratio of pixels to world units per coordinate. + // The product of the above matrices is `world2Tile` after the fact that it maps a world coordinate to a coordinate in the tile lattice + + // 1. Displacement. The following matrix computes the offset for an input point `p` with homogenous worldspace coordinates. + // By foregoing the homogenous coordinate we can keep only the vector part, that's why it's `2x3` and not `3x3` + float64_t2 topLeftWorld = georeferencedImageParams.worldspaceOBB.topLeft; + float64_t2x3 displacementMatrix(1., 0., - topLeftWorld.x, 0., 1., - topLeftWorld.y); + + // 2. Change of Basis. Since {dirU, dirV} are orthogonal, the matrix to change from world coords to `span{dirU, dirV}` coords has a quite nice expression + // Non-uniform scaling doesn't affect this, but this has to change if we allow for shearing (basis vectors stop being orthogonal) + float64_t2 dirU = georeferencedImageParams.worldspaceOBB.dirU; + float64_t2 dirV = float32_t2(dirU.y, -dirU.x) * georeferencedImageParams.worldspaceOBB.aspectRatio; + float64_t dirULengthSquared = nbl::hlsl::dot(dirU, dirU); + float64_t dirVLengthSquared = nbl::hlsl::dot(dirV, dirV); + float64_t2 firstRow = dirU / dirULengthSquared; + float64_t2 secondRow = dirV / dirVLengthSquared; + float64_t2x2 changeOfBasisMatrix(firstRow, secondRow); + + // Put them all together + world2UV = nbl::hlsl::mul(changeOfBasisMatrix, displacementMatrix); +} + +DrawResourcesFiller::StreamedImageManager::TileUploadData DrawResourcesFiller::StreamedImageManager::generateTileUploadData(const ImageType imageType, const float64_t3x3& NDCToWorld) +{ + // I think eventually it's better to just transform georeferenced images that aren't big enough into static images and forget about them + if (imageType == ImageType::GEOREFERENCED_FULL_RESOLUTION) //Pass imageID as parameter, down from the addGeoRef call + return TileUploadData{ {}, georeferencedImageParams.worldspaceOBB }; + + currentMappedRegion = computeViewportTileAlignedObb(NDCToWorld); + + // Now we have the indices of the tiles we want to upload, so create the vector of `StreamedImageCopies` - 1 per tile. + core::vector tiles; + uint32_t nTiles = (currentMappedRegion.bottomRight.x - currentMappedRegion.topLeft.x + 1) * (currentMappedRegion.topLeft.y - currentMappedRegion.topLeft.y + 1); + tiles.reserve(nTiles); + + // Assuming a 1 pixel per block format - otherwise math here gets a bit trickier + auto bytesPerPixel = getTexelOrBlockBytesize(georeferencedImageParams.format); + const size_t bytesPerSide = bytesPerPixel * TileSize; + + // Dangerous code - assumes image can be perfectly covered with tiles. Otherwise will need to handle edge cases + for (uint32_t tileX = currentMappedRegion.topLeft.x; tileX <= currentMappedRegion.bottomRight.x; tileX++) + { + for (uint32_t tileY = currentMappedRegion.topLeft.y; tileY <= currentMappedRegion.bottomRight.y; tileY++) + { + // Flush the loaded sections into the buffer - they should be done uploading by now + loader.clear(); + // Reserve enough sections + loader.reserve(nTiles); + auto tile = loader.load(uint32_t2(tileX * TileSize, tileY * TileSize), uint32_t2(TileSize, TileSize)); + // Alias the buffer + asset::IBuffer::SCreationParams bufParams = { .size = tile->getBuffer()->getSize(), .usage = tile->getBuffer()->getUsageFlags() }; + ICPUBuffer::SCreationParams cpuBufParams(std::move(bufParams)); + cpuBufParams.data = tile->getBuffer()->getPointer(); + cpuBufParams.memoryResource = core::getNullMemoryResource(); + auto aliasedBuffer = ICPUBuffer::create(std::move(cpuBufParams), nbl::core::adopt_memory_t{}); + + // The math here is like this because of the buffer we're getting (full image in the emulated case) + // When moving to actual ECW loading, bufferOffset will be 0, bufferRowLength will be the extent.width of the loaded section, + // imageExtent will be the extent of the loaded section, and imageOffset will be the appropriate offset (we will be loading whole sections of tiles + // that can be made into a rectangle instead of tile by tile) + asset::IImage::SBufferCopy bufCopy; + bufCopy.bufferOffset = (tileY * (maxImageTileIndices.x + 1) * TileSize + tileX) * bytesPerSide; + bufCopy.bufferRowLength = georeferencedImageParams.imageExtents.x; + bufCopy.bufferImageHeight = 0; + bufCopy.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; + bufCopy.imageSubresource.mipLevel = 0u; + bufCopy.imageSubresource.baseArrayLayer = 0u; + bufCopy.imageSubresource.layerCount = 1u; + bufCopy.imageOffset = { (tileX - currentMappedRegion.topLeft.x) * TileSize, (tileY - currentMappedRegion.topLeft.y) * TileSize, 0u }; + bufCopy.imageExtent.width = TileSize; + bufCopy.imageExtent.height = TileSize; + bufCopy.imageExtent.depth = 1; + + tiles.emplace_back(georeferencedImageParams.format, aliasedBuffer, std::move(bufCopy)); + } + } + + // Last, we need to figure out an obb that covers only the currently loaded tiles + // By shifting the `fromTopLeftOBB` an appropriate number of tiles in each direction, we get an obb that covers at least the uploaded tiles + + OrientedBoundingBox2D worldspaceOBB = fromTopLeftOBB; + const float32_t2 dirV = float32_t2(worldspaceOBB.dirU.y, -worldspaceOBB.dirU.x) * worldspaceOBB.aspectRatio; + worldspaceOBB.topLeft += worldspaceOBB.dirU * float32_t(currentMappedRegion.topLeft.x) / float32_t(maxResidentTiles.x); + worldspaceOBB.topLeft += dirV * float32_t(currentMappedRegion.topLeft.y) / float32_t(maxResidentTiles.y); + + // Compute minUV, maxUV + // Since right now we don't shift the obb around, minUV will always be (0,0), but this is bound to change later on (shifting obb will happen when we want to reuse tiles and not + // reupload them on every frame in the next phase) + float32_t2 minUV(0.f, 0.f); + // By default we have a large-as-possible obb so maxUV is (1,1). This is bound to change in the general case when we shift obb. However, since right now we need to shrink the obb + // to ensure we don't draw outside the bounds of the real image, we also need to change the maxUV; + float32_t2 maxUV(1.f, 1.f); + int32_t excessTiles = currentMappedRegion.topLeft.x + maxResidentTiles.x - 1 - maxImageTileIndices.x; + if (excessTiles > 0) + { + // Shrink obb to only fit necessary tiles, compute maxUV.x which turns out to be exactly the shrink factor for dirU. + maxUV.x = float32_t(maxResidentTiles.x - excessTiles) / maxResidentTiles.x; + } + // De the same along the other axis + excessTiles = currentMappedRegion.topLeft.y + maxResidentTiles.y - 1 - maxImageTileIndices.y; + if (excessTiles > 0) + { + // Analogously, maxUV.y is the shrink factor for dirV. + maxUV.y = float32_t(maxResidentTiles.y - excessTiles) / maxResidentTiles.y; + } + // Recompute dirU and aspect ratio + // Multiply dirU by the shrink factor + worldspaceOBB.dirU *= maxUV.x; + // Scale the aspect ratio by the relative shrinkage of U,V. Remember our aspect ratio is V / U. + worldspaceOBB.aspectRatio *= maxUV.y / maxUV.x; + + return TileUploadData{ std::move(tiles), worldspaceOBB, minUV, maxUV }; + +} + +float64_t DrawResourcesFiller::StreamedImageManager::computeViewportMipLevel(const float64_t3x3& NDCToWorld, float64_t2 viewportExtents) +{ + const auto viewportWidthVectorWorld = nbl::hlsl::mul(NDCToWorld, topRightViewportNDCH - topLeftViewportNDCH); + const auto viewportHeightVectorWorld = nbl::hlsl::mul(NDCToWorld, bottomLeftViewportNDCH - topLeftViewportNDCH); + // Abuse of notation, we're passing a vector not a point, won't be affected by offset so that's ok + const auto viewportWidthImagePixelLengthVector = transformWorldCoordsToPixelCoords(viewportWidthVectorWorld); + const auto viewportHeightImagePixelLengthVector = transformWorldCoordsToPixelCoords(viewportHeightVectorWorld); + + const auto viewportWidthImagePixelLength = nbl::hlsl::length(viewportWidthImagePixelLengthVector); + const auto viewportHeightImagePixelLength = nbl::hlsl::length(viewportHeightImagePixelLengthVector); + + // Mip is decided based on max of these + const auto maxPixelLength = nbl::hlsl::max(viewportWidthImagePixelLength, viewportHeightImagePixelLength); + return maxPixelLength / nbl::hlsl::max(viewportExtents.x, viewportExtents.y); +} + +DrawResourcesFiller::StreamedImageManager::TileLatticeAlignedObb DrawResourcesFiller::StreamedImageManager::computeViewportTileAlignedObb(const float64_t3x3& NDCToWorld) +{ + const float64_t3 topLeftViewportWorldH = nbl::hlsl::mul(NDCToWorld, topLeftViewportNDCH); + const float64_t3 topRightViewportWorldH = nbl::hlsl::mul(NDCToWorld, topRightViewportNDCH); + const float64_t3 bottomLeftViewportWorldH = nbl::hlsl::mul(NDCToWorld, bottomLeftViewportNDCH); + const float64_t3 bottomRightViewportWorldH = nbl::hlsl::mul(NDCToWorld, bottomRightViewportNDCH); + + // We can use `world2Tile` to get tile lattice coordinates for each of these points + const float64_t2 topLeftTileLattice = transformWorldCoordsToTileCoords(topLeftViewportWorldH); + const float64_t2 topRightTileLattice = transformWorldCoordsToTileCoords(topRightViewportWorldH); + const float64_t2 bottomLeftTileLattice = transformWorldCoordsToTileCoords(bottomLeftViewportWorldH); + const float64_t2 bottomRightTileLattice = transformWorldCoordsToTileCoords(bottomRightViewportWorldH); + + // Get the min and max of each lattice coordinate + const float64_t2 minTop = nbl::hlsl::min(topLeftTileLattice, topRightTileLattice); + const float64_t2 minBottom = nbl::hlsl::min(bottomLeftTileLattice, bottomRightTileLattice); + const float64_t2 minAll = nbl::hlsl::min(minTop, minBottom); + + const float64_t2 maxTop = nbl::hlsl::max(topLeftTileLattice, topRightTileLattice); + const float64_t2 maxBottom = nbl::hlsl::max(bottomLeftTileLattice, bottomRightTileLattice); + const float64_t2 maxAll = nbl::hlsl::max(maxTop, maxBottom); + + // Floor them to get an integer for the tiles they're in + const int32_t2 minAllFloored = nbl::hlsl::floor(minAll); + const int32_t2 maxAllFloored = nbl::hlsl::floor(maxAll); + + // Clamp them to reasonable tile indices + TileLatticeAlignedObb retVal = {}; + retVal.topLeft = nbl::hlsl::clamp(minAllFloored, int32_t2(0, 0), int32_t2(maxImageTileIndices)); + retVal.bottomRight = nbl::hlsl::clamp(maxAllFloored, int32_t2(0, 0), nbl::hlsl::min(int32_t2(maxImageTileIndices), int32_t2(currentMappedRegion.topLeft + maxResidentTiles - uint32_t2(1, 1)))); + return retVal; +} diff --git a/62_CAD/DrawResourcesFiller.h b/62_CAD/DrawResourcesFiller.h index 547926767..3855afba6 100644 --- a/62_CAD/DrawResourcesFiller.h +++ b/62_CAD/DrawResourcesFiller.h @@ -18,6 +18,8 @@ static_assert(sizeof(DrawObject) == 16u); static_assert(sizeof(MainObject) == 20u); static_assert(sizeof(LineStyle) == 88u); +//TODO[Francisco]: Update briefs for geotex related functions + // ! DrawResourcesFiller // ! This class provides important functionality to manage resources needed for a draw. // ! Drawing new objects (polylines, hatches, etc.) should go through this function. @@ -120,6 +122,122 @@ struct DrawResourcesFiller geometryInfo.getAlignedStorageSize(); } }; + + // Used to load pieces of an ECW from disk - currently just emulated + struct ImageLoader + { + ImageLoader(core::smart_refctd_ptr&& _geoReferencedImage) : geoReferencedImage(std::move(_geoReferencedImage)) {} + + // Emulates the loading of a rectangle from the original image + core::smart_refctd_ptr load(uint32_t2 offset, uint32_t2 extent) + { + // Create a new buffer pointing to same data - this is not what the class will be doing when streaming ECW + auto imageBuffer = geoReferencedImage->getBuffer(); + asset::IBuffer::SCreationParams bufCreationParams = { .size = imageBuffer->getSize(), .usage = imageBuffer->getUsageFlags() }; + ICPUBuffer::SCreationParams cpuBufCreationParams(std::move(bufCreationParams)); + cpuBufCreationParams.data = imageBuffer->getPointer(); + cpuBufCreationParams.memoryResource = core::getNullMemoryResource(); + auto imageBufferAlias = ICPUBuffer::create(std::move(cpuBufCreationParams), core::adopt_memory_t{}); + // Now set up the image region + auto bytesPerPixel = getTexelOrBlockBytesize(geoReferencedImage->getCreationParameters().format); + + auto regions = core::make_refctd_dynamic_array>(1u); + auto& region = regions->front(); + region.bufferOffset = (offset.y * geoReferencedImage->getCreationParameters().extent.width + offset.x) * bytesPerPixel; + region.bufferRowLength = geoReferencedImage->getCreationParameters().extent.width; + region.bufferImageHeight = 0; + region.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; + region.imageSubresource.mipLevel = 0u; + region.imageSubresource.baseArrayLayer = 0u; + region.imageSubresource.layerCount = 1u; + region.imageOffset = { 0u, 0u, 0u }; + region.imageExtent.width = extent.x; + region.imageExtent.height = extent.y; + region.imageExtent.depth = 1; + + ICPUImage::SCreationParams loadedImageParams = geoReferencedImage->getCreationParameters(); + loadedImageParams.extent = { extent.x, extent.y, 1u }; + auto loadedImage = ICPUImage::create(std::move(loadedImageParams)); + loadedImage->setBufferAndRegions(std::move(imageBufferAlias), regions); + + loadedSections.push_back(loadedImage); + + return loadedImage; + } + + void clear() + { + loadedSections.clear(); + } + + void reserve(uint32_t sections) + { + loadedSections.reserve(sections); + } + + // This will be the path to the image + core::smart_refctd_ptr geoReferencedImage; + private: + // This will be actually loaded sections (each section is a rectangle of one or more tiles) of the above image. Since we alias buffers for uploads, + // we want this class to track their lifetime so they don't get deallocated before they get uploaded. + core::vector> loadedSections; + }; + + // @brief Used to load tiles into VRAM, keep track of loaded tiles, determine how they get sampled etc. + struct StreamedImageManager + { + friend class DrawResourcesFiller; + constexpr static uint32_t TileSize = 128u; + constexpr static uint32_t PaddingTiles = 2; + + constexpr static float64_t3 topLeftViewportNDCH = float64_t3(-1.0, -1.0, 1.0); + constexpr static float64_t3 topRightViewportNDCH = float64_t3(1.0, -1.0, 1.0); + constexpr static float64_t3 bottomLeftViewportNDCH = float64_t3(-1.0, 1.0, 1.0); + constexpr static float64_t3 bottomRightViewportNDCH = float64_t3(1.0, 1.0, 1.0); + + StreamedImageManager(image_id _imageID, GeoreferencedImageParams&& _georeferencedImageParams, ImageLoader&& _loader); + + struct TileLatticeAlignedObb + { + uint32_t2 topLeft; + uint32_t2 bottomRight; + }; + + struct TileUploadData + { + core::vector tiles; + OrientedBoundingBox2D worldspaceOBB; + float32_t2 minUV; + float32_t2 maxUV; + }; + + // Right now it's generating tile-by-tile. Can be improved to produce at worst 4 different rectangles to load (depending on how we need to load tiles) + TileUploadData generateTileUploadData(const ImageType imageType, const float64_t3x3& NDCToWorld); + + // These are NOT UV, pixel or tile coords into the mapped image region, rather into the real, huge image + // Tile coords are always in mip 0 tile size. Translating to other mips levels is trivial + float64_t2 transformWorldCoordsToUV(const float64_t3 worldCoordsH) {return nbl::hlsl::mul(world2UV, worldCoordsH);} + float64_t2 transformWorldCoordsToPixelCoords(const float64_t3 worldCoordsH) { return float64_t2(georeferencedImageParams.imageExtents) * transformWorldCoordsToUV(worldCoordsH); } + float64_t2 transformWorldCoordsToTileCoords(const float64_t3 worldCoordsH) { return (1.0 / TileSize) * transformWorldCoordsToPixelCoords(worldCoordsH); } + + // Compute repeated here, can both be done together if necessary + float64_t computeViewportMipLevel(const float64_t3x3& NDCToWorld, float64_t2 viewportExtents); + TileLatticeAlignedObb computeViewportTileAlignedObb(const float64_t3x3& NDCToWorld); + + image_id imageID; + GeoreferencedImageParams georeferencedImageParams; + // This and the logic they're in will likely change later with Toroidal updating + private: + uint32_t currentResidentMipLevel = {}; + uint32_t2 maxResidentTiles = {}; + uint32_t2 maxImageTileIndices = {}; + TileLatticeAlignedObb currentMappedRegion = {}; + float64_t2x3 world2UV = {}; + // Worldspace OBB that covers the top left `maxResidentTiles.x x maxResidentTiles.y` tiles of the image. + // We shift this OBB by appropriate tile offsets when loading tiles + OrientedBoundingBox2D fromTopLeftOBB = {}; + ImageLoader loader; + }; DrawResourcesFiller(); @@ -351,7 +469,7 @@ struct DrawResourcesFiller void addImageObject(image_id imageID, const OrientedBoundingBox2D& obb, SIntendedSubmitInfo& intendedNextSubmit); // This function must be called immediately after `addStaticImage` for the same imageID. - void addGeoreferencedImage(image_id imageID, const GeoreferencedImageParams& params, SIntendedSubmitInfo& intendedNextSubmit); + void addGeoreferencedImage(StreamedImageManager& manager, const float64_t3x3& NDCToWorld, SIntendedSubmitInfo& intendedNextSubmit); /// @brief call this function before submitting to ensure all buffer and textures resourcesCollection requested via drawing calls are copied to GPU /// records copy command into intendedNextSubmit's active command buffer and might possibly submits if fails allocation on staging upload memory. @@ -596,7 +714,7 @@ struct DrawResourcesFiller bool addImageObject_Internal(const ImageObjectInfo& imageObjectInfo, uint32_t mainObjIdx);; /// Attempts to upload a georeferenced image info considering resource limitations (not accounting for the resource image added using ensureStaticImageAvailability function) - bool addGeoreferencedImageInfo_Internal(const GeoreferencedImageInfo& georeferencedImageInfo, uint32_t mainObjIdx);; + bool addGeoreferencedImageInfo_Internal(const GeoreferencedImageInfo& georeferencedImageInfo, uint32_t mainObjIdx); uint32_t getImageIndexFromID(image_id imageID, const SIntendedSubmitInfo& intendedNextSubmit); @@ -660,9 +778,9 @@ struct DrawResourcesFiller * * @param[out] outImageParams Structure to be filled with image creation parameters (format, size, etc.). * @param[out] outImageType Indicates whether the image should be fully resident or streamed. - * @param[in] georeferencedImageParams Parameters describing the full image extents, viewport extents, and format. + * @param[in] manager Manager for the georeferenced image */ - void determineGeoreferencedImageCreationParams(nbl::asset::IImage::SCreationParams& outImageParams, ImageType& outImageType, const GeoreferencedImageParams& georeferencedImageParams); + ImageType determineGeoreferencedImageCreationParams(nbl::asset::IImage::SCreationParams& outImageParams, const GeoreferencedImageParams& params); /** * @brief Used to implement both `drawHatch` and `drawFixedGeometryHatch` without exposing the transformation type parameter diff --git a/62_CAD/Images.h b/62_CAD/Images.h index a341eadd6..2e66f3c44 100644 --- a/62_CAD/Images.h +++ b/62_CAD/Images.h @@ -28,7 +28,6 @@ struct GeoreferencedImageParams uint32_t2 imageExtents = {}; uint32_t2 viewportExtents = {}; asset::E_FORMAT format = {}; - // TODO: Need to add other stuff later. }; /** @@ -205,7 +204,7 @@ class ImagesCache : public core::ResizableLRUCache struct StreamedImageCopy { asset::E_FORMAT srcFormat; - core::smart_refctd_ptr srcBuffer; // Make it 'std::future' later? + smart_refctd_ptr srcBuffer; // Make it 'std::future' later? asset::IImage::SBufferCopy region; }; diff --git a/62_CAD/main.cpp b/62_CAD/main.cpp index f4a886791..62496ef64 100644 --- a/62_CAD/main.cpp +++ b/62_CAD/main.cpp @@ -49,6 +49,7 @@ static constexpr bool DebugRotatingViewProj = false; static constexpr bool FragmentShaderPixelInterlock = true; static constexpr bool LargeGeoTextureStreaming = true; static constexpr bool CacheAndReplay = false; // caches first frame resources (buffers and images) from DrawResourcesFiller and replays in future frames, skiping CPU Logic +static constexpr bool textCameraRotation = true; enum class ExampleMode { @@ -64,6 +65,7 @@ enum class ExampleMode CASE_9, // DTM CASE_10, // testing fixed geometry and emulated fp64 corner cases CASE_11, // grid DTM + CASE_12, // Georeferenced streamed images CASE_COUNT }; @@ -80,10 +82,11 @@ constexpr std::array cameraExtents = 600.0, // CASE_8 600.0, // CASE_9 10.0, // CASE_10 - 1000.0 // CASE_11 + 1000.0, // CASE_11 + 10.0 // CASE_12 }; -constexpr ExampleMode mode = ExampleMode::CASE_8; +constexpr ExampleMode mode = ExampleMode::CASE_12; class Camera2D { @@ -133,7 +136,7 @@ class Camera2D if (ev.type == nbl::ui::SMouseEvent::EET_SCROLL) { - m_bounds = m_bounds + float64_t2{ (double)ev.scrollEvent.verticalScroll * -0.1 * m_aspectRatio, (double)ev.scrollEvent.verticalScroll * -0.1}; + m_bounds = m_bounds + float64_t2{ (double)ev.scrollEvent.verticalScroll * -0.0025 * m_aspectRatio, (double)ev.scrollEvent.verticalScroll * -0.0025}; m_bounds = float64_t2{ core::max(m_aspectRatio, m_bounds.x), core::max(1.0, m_bounds.y) }; } } @@ -1263,6 +1266,8 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio gridDTMHeightMap = loadImage("../../media/gridDTMHeightMap.exr"); + bigTiledGrid = loadImage("../../media/tiled_grid.exr"); + // set diagonals of cells to TOP_LEFT_TO_BOTTOM_RIGHT or BOTTOM_LEFT_TO_TOP_RIGHT randomly { // assumption is that format of the grid DTM height map is *_SRGB, I don't think we need any code to ensure that @@ -1311,7 +1316,8 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { - m_Camera.mouseProcess(events); + if (m_window->hasMouseFocus()) + m_Camera.mouseProcess(events); } , m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void @@ -1492,17 +1498,9 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio projectionToNDC = m_Camera.constructViewProjection(); // TEST CAMERA ROTATION -#if 1 - // double rotation = 0.25 * PI(); - double rotation = abs(cos(m_timeElapsed * 0.0004)) * 0.25 * PI() ; - float64_t2 rotationVec = float64_t2(cos(rotation), sin(rotation)); - float64_t3x3 rotationParameter = float64_t3x3 { - rotationVec.x, rotationVec.y, 0.0, - -rotationVec.y, rotationVec.x, 0.0, - 0.0, 0.0, 1.0 - }; - projectionToNDC = nbl::hlsl::mul(projectionToNDC, rotationParameter); -#endif + if constexpr (textCameraRotation) + projectionToNDC = rotateBasedOnTime(projectionToNDC); + Globals globalData = {}; uint64_t baseAddress = resourcesGPUBuffer->getDeviceAddress(); globalData.pointers = { @@ -3127,12 +3125,6 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio //printf("\n"); } - GeoreferencedImageParams geoRefParams = {}; - geoRefParams.format = asset::EF_R8G8B8A8_SRGB; - geoRefParams.imageExtents = uint32_t2 (2048, 2048); - geoRefParams.viewportExtents = (m_realFrameIx <= 5u) ? uint32_t2(1280, 720) : uint32_t2(3840, 2160); // to test trigerring resize/recreation - // drawResourcesFiller.ensureGeoreferencedImageAvailability_AllocateIfNeeded(6996, geoRefParams, intendedNextSubmit); - LineStyleInfo lineStyle = { .color = float32_t4(1.0f, 0.1f, 0.1f, 0.9f), @@ -3686,6 +3678,48 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio } #endif } + else if (mode == ExampleMode::CASE_12) + { + const static float64_t3 topLeftViewportH = float64_t3(-1.0, -1.0, 1.0); + const static float64_t3 topRightViewportH = float64_t3(1.0, -1.0, 1.0); + const static float64_t3 bottomLeftViewportH = float64_t3(-1.0, 1.0, 1.0); + const static float64_t3 bottomRightViewportH = float64_t3(1.0, 1.0, 1.0); + + image_id tiledGridID = 6996; + static GeoreferencedImageParams tiledGridParams; + auto& tiledGridCreationParams = bigTiledGrid->getCreationParameters(); + // Position at topLeft viewport + auto projectionToNDC = m_Camera.constructViewProjection(); + // TEST CAMERA ROTATION + if constexpr (textCameraRotation) + projectionToNDC = rotateBasedOnTime(projectionToNDC); + auto inverseViewProj = nbl::hlsl::inverse(projectionToNDC); + + const static auto startingTopLeft = nbl::hlsl::mul(inverseViewProj, topLeftViewportH); + tiledGridParams.worldspaceOBB.topLeft = startingTopLeft; + + // Get 1 viewport pixel to match `startingImagePixelsPerViewportPixel` pixels of the image by choosing appropriate dirU + const static float64_t startingImagePixelsPerViewportPixels = 2.0; + const static auto startingViewportWidthVector = nbl::hlsl::mul(inverseViewProj, topRightViewportH - topLeftViewportH); + const static auto dirU = startingViewportWidthVector * float64_t(bigTiledGrid->getCreationParameters().extent.width) / float64_t(startingImagePixelsPerViewportPixels * m_window->getWidth()); + tiledGridParams.worldspaceOBB.dirU = dirU; + tiledGridParams.worldspaceOBB.aspectRatio = 1.0; + tiledGridParams.imageExtents = { tiledGridCreationParams.extent.width, tiledGridCreationParams.extent.height}; + tiledGridParams.viewportExtents = uint32_t2{ m_window->getWidth(), m_window->getHeight() }; + tiledGridParams.format = tiledGridCreationParams.format; + + static auto bigTileGridPtr = bigTiledGrid; + static DrawResourcesFiller::ImageLoader loader(std::move(bigTileGridPtr)); + static DrawResourcesFiller::StreamedImageManager tiledGridManager(tiledGridID, std::move(tiledGridParams), std::move(loader)); + + drawResourcesFiller.ensureGeoreferencedImageAvailability_AllocateIfNeeded(tiledGridID, tiledGridManager.georeferencedImageParams, intendedNextSubmit); + + drawResourcesFiller.addGeoreferencedImage(tiledGridManager, inverseViewProj, intendedNextSubmit); + + // Mip level calculation + // Uncomment to print mip + //std::cout << "mip level: " << tiledGridManager.computeViewportMipLevel(inverseViewProj, float64_t2(m_window->getWidth(), m_window->getHeight())) << std::endl; + } } double getScreenToWorldRatio(const float64_t3x3& viewProjectionMatrix, uint32_t2 windowSize) @@ -3695,6 +3729,18 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio return hlsl::length(float64_t2(idx_0_0, idx_1_0)); } + float64_t3x3 rotateBasedOnTime(const float64_t3x3& projectionMatrix) + { + double rotation = abs(cos(m_timeElapsed * 0.0004)) * 0.25 * PI(); + float64_t2 rotationVec = float64_t2(cos(rotation), sin(rotation)); + float64_t3x3 rotationParameter = float64_t3x3{ + rotationVec.x, rotationVec.y, 0.0, + -rotationVec.y, rotationVec.x, 0.0, + 0.0, 0.0, 1.0 + }; + return nbl::hlsl::mul(projectionMatrix, rotationParameter); + } + protected: std::chrono::seconds timeout = std::chrono::seconds(0x7fffFFFFu); clock_t::time_point start; @@ -3758,6 +3804,7 @@ class ComputerAidedDesign final : public nbl::examples::SimpleWindowedApplicatio std::vector> sampleImages; smart_refctd_ptr gridDTMHeightMap; + smart_refctd_ptr bigTiledGrid; static constexpr char FirstGeneratedCharacter = ' '; static constexpr char LastGeneratedCharacter = '~'; diff --git a/62_CAD/scripts/tiled_grid.py b/62_CAD/scripts/tiled_grid.py new file mode 100644 index 000000000..737c3463e --- /dev/null +++ b/62_CAD/scripts/tiled_grid.py @@ -0,0 +1,266 @@ +from PIL import Image, ImageDraw, ImageFont +import numpy as np +import os +import OpenImageIO as oiio + + + +def create_single_tile(tile_size, color, x_coord, y_coord, font_path=None): + """ + Creates a single square tile image with a given color and two lines of centered text. + + Args: + tile_size (int): The sidelength of the square tile in pixels. + color (tuple): A tuple of three floats (R, G, B) representing the color (0.0-1.0). + x_coord (int): The X coordinate to display on the tile. + y_coord (int): The Y coordinate to display on the tile. + font_path (str, optional): The path to a TrueType font file (.ttf). + If None, a default PIL font will be used. + Returns: + PIL.Image.Image: The created tile image with text. + """ + # Convert float color (0.0-1.0) to 8-bit integer color (0-255) + int_color = tuple(int(max(0, min(1, c)) * 255) for c in color) # Ensure color components are clamped + + img = Image.new('RGB', (tile_size, tile_size), int_color) + draw = ImageDraw.Draw(img) + + text_line1 = f"x = {x_coord}" + text_line2 = f"y = {y_coord}" + + text_fill_color = (255, 255, 255) + + # --- Dynamic Font Size Adjustment --- + # Start with a relatively large font size and shrink if needed + font_size = int(tile_size * 0.25) # Initial guess for font size + max_font_size = int(tile_size * 0.25) # Don't exceed this + + font = None + max_iterations = 100 # Prevent infinite loops in font size reduction + + for _ in range(max_iterations): + current_font_path = font_path + current_font_size = max(1, font_size) # Ensure font size is at least 1 + + try: + if current_font_path and os.path.exists(current_font_path): + font = ImageFont.truetype(current_font_path, current_font_size) + else: + # Fallback to default font (size argument might not always work perfectly) + font = ImageFont.load_default() + # For default font, try to scale if load_default(size=...) is supported and works + try: + scaled_font = ImageFont.load_default(size=current_font_size) + if draw.textbbox((0, 0), text_line1, font=scaled_font)[2] > 0: # Check if usable + font = scaled_font + except Exception: + pass # Stick with original default font + + if font is None: # Last resort if no font could be loaded + font = ImageFont.load_default() + + # Measure text dimensions + bbox1 = draw.textbbox((0, 0), text_line1, font=font) + text_width1 = bbox1[2] - bbox1[0] + text_height1 = bbox1[3] - bbox1[1] + + bbox2 = draw.textbbox((0, 0), text_line2, font=font) + text_width2 = bbox2[2] - bbox2[0] + text_height2 = bbox2[3] - bbox2[1] + + # Calculate total height needed for both lines plus some padding + # Let's assume a small gap between lines (e.g., 0.1 * text_height) + line_gap = int(text_height1 * 0.2) # 20% of line height + total_text_height = text_height1 + text_height2 + line_gap + + # Check if text fits vertically and horizontally + if (total_text_height < tile_size * 0.9) and \ + (text_width1 < tile_size * 0.9) and \ + (text_width2 < tile_size * 0.9): + break # Font size is good, break out of loop + else: + font_size -= 1 # Reduce font size + if font_size <= 0: # Prevent infinite loop if text can never fit + font_size = 1 # Smallest possible font size + break + + except Exception as e: + # Handle cases where font loading or textbbox fails + print(f"Error during font sizing: {e}. Reducing font size and retrying.") + font_size -= 1 + if font_size <= 0: + font_size = 1 + break # Cannot make font smaller, stop + + # Final check: if font_size became 0 or less, ensure it's at least 1 + if font_size <= 0: + font_size = 1 + # Reload font with minimum size if needed + if font_path and os.path.exists(font_path): + font = ImageFont.truetype(font_path, font_size) + else: + font = ImageFont.load_default() + try: + scaled_font = ImageFont.load_default(size=font_size) + if draw.textbbox((0, 0), text_line1, font=scaled_font)[2] > 0: + font = scaled_font + except Exception: + pass + + + # Re-measure with final font size to ensure accurate positioning + bbox1 = draw.textbbox((0, 0), text_line1, font=font) + text_width1 = bbox1[2] - bbox1[0] + text_height1 = bbox1[3] - bbox1[1] + + bbox2 = draw.textbbox((0, 0), text_line2, font=font) + text_width2 = bbox2[2] - bbox2[0] + text_height2 = bbox2[3] - bbox2[1] + + # Calculate positions for centering + # Line 1: centered horizontally, midpoint at 1/3 tile height + x1 = (tile_size - text_width1) / 2 + y1 = (tile_size / 3) - (text_height1 / 2) + + # Line 2: centered horizontally, midpoint at 2/3 tile height + x2 = (tile_size - text_width2) / 2 + y2 = (tile_size * 2 / 3) - (text_height2 / 2) + + # Draw the text + draw.text((x1, y1), text_line1, fill=text_fill_color, font=font) + draw.text((x2, y2), text_line2, fill=text_fill_color, font=font) + + return img + +def generate_interpolated_grid_image(tile_size, count, font_path=None): + """ + Generates a large image composed of 'count' x 'count' tiles, + with colors bilinearly interpolated from corners and text indicating tile index. + + Args: + tile_size (int): The sidelength of each individual square tile in pixels. + count (int): The number of tiles per side of the large grid (e.g., if count=3, + it's a 3x3 grid of tiles). + font_path (str, optional): Path to a TrueType font file for the tile text. + If None, a default PIL font will be used. + + Returns: + PIL.Image.Image: The generated large grid image. + """ + if count <= 0: + raise ValueError("Count must be a positive integer.") + + total_image_size = count * tile_size + main_img = Image.new('RGB', (total_image_size, total_image_size)) + + # Corner colors (R, G, B) as floats (0.0-1.0) + corner_colors = { + "top_left": (1.0, 0.0, 0.0), # Red + "top_right": (1.0, 0.0, 1.0), # Purple + "bottom_left": (0.0, 1.0, 0.0), # Green + "bottom_right": (0.0, 0.0, 1.0) # Blue + } + + # Handle the edge case where count is 1 + if count == 1: + # If count is 1, there's only one tile, which is the top-left corner + tile_color = corner_colors["top_left"] + tile_image = create_single_tile(tile_size, tile_color, 0, 0, font_path=font_path) + main_img.paste(tile_image, (0, 0)) + return main_img + + for y_tile in range(count): + for x_tile in range(count): + # Calculate normalized coordinates (u, v) for interpolation + # We divide by (count - 1) to ensure 0 and 1 values at the edges + u = x_tile / (count - 1) + v = y_tile / (count - 1) + + # Apply the simplified bilinear interpolation formulas + r_component = 1 - v + g_component = v * (1 - u) + b_component = u + + # Clamp components to be within 0.0 and 1.0 (due to potential floating point inaccuracies) + current_color = ( + max(0.0, min(1.0, r_component)), + max(0.0, min(1.0, g_component)), + max(0.0, min(1.0, b_component)) + ) + + # Create the individual tile + tile_image = create_single_tile(tile_size, current_color, x_tile, y_tile, font_path=font_path) + + # Paste the tile onto the main image + paste_x = x_tile * tile_size + paste_y = y_tile * tile_size + main_img.paste(tile_image, (paste_x, paste_y)) + + return main_img + + + + +import argparse +parser = argparse.ArgumentParser(description="Process two optional named parameters.") +parser.add_argument('--ts', type=int, default=128, help='Tile Size') +parser.add_argument('--gs', type=int, default=128, help='Grid Size') + +# Parse the arguments +args = parser.parse_args() + + +# --- Configuration --- +tile_sidelength = args.ts # Size of each individual tile in pixels +grid_count = args.gs # Number of tiles per side (e.g., 15 means 15x15 grid) + +# Path to a font file (adjust this for your system) +# On Windows, you can typically use 'C:/Windows/Fonts/arial.ttf' or similar +# You might need to find a suitable font on your system. +# For testing, you can use None to let PIL use its default font. +# If a specific font path is provided and doesn't exist, it will fall back to default. +windows_font_path = "C:/Windows/Fonts/arial.ttf" # Example path for Windows +# If Arial is not found, try Times New Roman: +# windows_font_path = "C:/Windows/Fonts/times.ttf" + +font_to_use = None +if os.name == 'nt': # Check if OS is Windows + if os.path.exists(windows_font_path): + font_to_use = windows_font_path + print(f"Using font: {windows_font_path}") + else: + print(f"Warning: Windows font not found at '{windows_font_path}'. Using default PIL font.") +else: # Assume Linux/macOS for other OS types + # Common Linux/macOS font paths (adjust as needed) + linux_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" + mac_font_path = "/Library/Fonts/Arial.ttf" + if os.path.exists(linux_font_path): + font_to_use = linux_font_path + print(f"Using font: {linux_font_path}") + elif os.path.exists(mac_font_path): + font_to_use = mac_font_path + print(f"Using font: {mac_font_path}") + else: + print("Warning: No common Linux/macOS font found. Using default PIL font.") + + +# --- Generate and save the image --- +print(f"Generating a {grid_count}x{grid_count} grid of tiles, each {tile_sidelength}x{tile_sidelength} pixels.") +print(f"Total image size will be {grid_count * tile_sidelength}x{grid_count * tile_sidelength} pixels.") + +try: + final_image = generate_interpolated_grid_image(tile_sidelength, grid_count, font_path=font_to_use) + output_filename = "../../media/tiled_grid.exr" + np_img = np.array(final_image).astype(np.float32) / 255.0 # Normalize for EXR + spec = oiio.ImageSpec(final_image.width, final_image.height, 3, oiio.TypeDesc("float")) + out = oiio.ImageOutput.create(output_filename) + out.open(output_filename, spec) + out.write_image(np_img.reshape(-1)) # Flatten for OIIO’s expected input + out.close() + + print(f"Successfully created '{output_filename}'") + +except ValueError as e: + print(f"Error: {e}") +except Exception as e: + print(f"An unexpected error occurred: {e}") \ No newline at end of file diff --git a/62_CAD/shaders/globals.hlsl b/62_CAD/shaders/globals.hlsl index 5c3681910..5a1bb0d2f 100644 --- a/62_CAD/shaders/globals.hlsl +++ b/62_CAD/shaders/globals.hlsl @@ -244,10 +244,12 @@ struct ImageObjectInfo // Currently a simple OBB like ImageObject, but later will be fullscreen with additional info about UV offset for toroidal(mirror) addressing struct GeoreferencedImageInfo { - pfloat64_t2 topLeft; // 2 * 8 = 16 bytes (16) - float32_t2 dirU; // 2 * 4 = 8 bytes (24) + pfloat64_t2 topLeft; // 2 * 8 = 16 bytes (16) + float32_t2 dirU; // 2 * 4 = 8 bytes (24) float32_t aspectRatio; // 4 bytes (28) - uint32_t textureID; // 4 bytes (32) + uint32_t textureID; // 4 bytes (32) + float32_t2 minUV; // 2 * 4 = 8 bytes (40) + float32_t2 maxUV; // 2 * 4 = 8 bytes (48) }; // Goes into geometry buffer, needs to be aligned by 8 diff --git a/62_CAD/shaders/main_pipeline/vertex_shader.hlsl b/62_CAD/shaders/main_pipeline/vertex_shader.hlsl index 90394e935..9842e67b0 100644 --- a/62_CAD/shaders/main_pipeline/vertex_shader.hlsl +++ b/62_CAD/shaders/main_pipeline/vertex_shader.hlsl @@ -739,20 +739,26 @@ PSInput vtxMain(uint vertexID : SV_VertexID) } else if (objType == ObjectType::STREAMED_IMAGE) { - pfloat64_t2 topLeft = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); - float32_t2 dirU = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 4u); - float32_t aspectRatio = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2), 4u); - uint32_t textureID = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float2) + sizeof(float), 4u); + const pfloat64_t2 topLeft = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress, 8u); + const float32_t2 dirU = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2), 4u); + const float32_t aspectRatio = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float32_t2), 4u); + const uint32_t textureID = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float32_t2) + sizeof(float32_t), 4u); + const float32_t2 minUV = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + sizeof(float32_t2) + sizeof(float32_t) + sizeof(uint32_t), 4u); + const float32_t2 maxUV = vk::RawBufferLoad(globals.pointers.geometryBuffer + drawObj.geometryAddress + sizeof(pfloat64_t2) + 2 * sizeof(float32_t2) + sizeof(float32_t) + sizeof(uint32_t), 4u); + + //printf("%f %f", minUV.x, minUV.y); + //printf("%f %f", maxUV.x, maxUV.y); const float32_t2 dirV = float32_t2(dirU.y, -dirU.x) * aspectRatio; - const float2 ndcTopLeft = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, topLeft)); - const float2 ndcDirU = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirU))); - const float2 ndcDirV = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirV))); + const float32_t2 ndcTopLeft = _static_cast(transformPointNdc(clipProjectionData.projectionToNDC, topLeft)); + const float32_t2 ndcDirU = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirU))); + const float32_t2 ndcDirV = _static_cast(transformVectorNdc(clipProjectionData.projectionToNDC, _static_cast(dirV))); - float2 corner = float2(bool2(vertexIdx & 0x1u, vertexIdx >> 1)); - float2 uv = corner; // non-dilated + const bool2 corner = bool2(vertexIdx & 0x1u, vertexIdx & 0x2u); - float2 ndcCorner = ndcTopLeft + corner.x * ndcDirU + corner.y * ndcDirV; + const float32_t2 ndcCorner = ndcTopLeft + corner.x * ndcDirU + corner.y * ndcDirV; + const float32_t2 uv = float32_t2(corner.x ? maxUV.x : minUV.x, corner.y ? maxUV.y : minUV.y); + printf("%f %f", ndcCorner.x, ndcCorner.y); outV.position = float4(ndcCorner, 0.f, 1.f); outV.setImageUV(uv);