diff --git a/FUTURE.md b/FUTURE.md index 5f757858f..07255f1e4 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -27,3 +27,5 @@ - Add VFONT write support - `vpkpp` - Cache file handles so they're not constantly getting closed/opened +- `vtfpp` + - Allow directly applying an HDRI to a VTF without requiring the user to write a custom wrapper diff --git a/README.md b/README.md index 55c2cc083..bea85b695 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,8 @@ found on PyPI in the [sourcepp](https://pypi.org/project/sourcepp) package. - `steampp` is based on the [SteamAppPathProvider](https://github.com/Trico-Everfire/SteamAppPathProvider) library by [@Trico Everfire](https://github.com/Trico-Everfire) and [Momentum Mod](https://momentum-mod.org) contributors. - `vpkpp`'s GCF parser was contributed by [@bt](https://github.com/caatge) and [@ymgve](https://github.com/ymgve). - `vpkpp`'s WAD3 parser was contributed by [@ozxybox](https://github.com/ozxybox). -- `vtfpp`'s write support is based on work by [@Trico Everfire](https://github.com/Trico-Everfire). +- `vtfpp`'s write support is loosely based on work by [@Trico Everfire](https://github.com/Trico-Everfire). +- `vtfpp`'s HDRI to cubemap conversion code is modified from the [HdriToCubemap](https://github.com/ivarout/HdriToCubemap) library by [@ivarout](https://github.com/ivarout). ## Gallery diff --git a/THIRDPARTY_LEGAL_NOTICES.txt b/THIRDPARTY_LEGAL_NOTICES.txt index a6d198797..8b0acb3c6 100644 --- a/THIRDPARTY_LEGAL_NOTICES.txt +++ b/THIRDPARTY_LEGAL_NOTICES.txt @@ -129,6 +129,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------- HdriToCubemap --------------- + +MIT License + +Copyright (c) 2020 Ingvar Out + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --------------- liblzma --------------- Permission to use, copy, modify, and/or distribute this diff --git a/include/sourcepp/Math.h b/include/sourcepp/Math.h index aedcf157a..f3452f836 100644 --- a/include/sourcepp/Math.h +++ b/include/sourcepp/Math.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,11 @@ using half_float::half; namespace sourcepp::math { +template +constexpr F pi = std::numbers::pi_v; +constexpr auto pi_f32 = pi; +constexpr auto pi_f64 = pi; + template concept Arithmetic = std::is_arithmetic_v || std::same_as; diff --git a/include/vtfpp/ImageConversion.h b/include/vtfpp/ImageConversion.h index 8d30f51a4..2ce55881f 100644 --- a/include/vtfpp/ImageConversion.h +++ b/include/vtfpp/ImageConversion.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -323,6 +324,12 @@ namespace ImageConversion { /// Converts several images from one format to another. [[nodiscard]] std::vector convertSeveralImageDataToFormat(std::span imageData, ImageFormat oldFormat, ImageFormat newFormat, uint8_t mipCount, uint16_t frameCount, uint16_t faceCount, uint16_t width, uint16_t height, uint16_t sliceCount); +/// Converts an HDRI into a cubemap. The output image data is in the same image format as the input. +/// The output images have the following order: front, back, left, right, down, up. +/// Resolution is the output size (width, height) of each image slice. 0 leaves it at the input size, the height of the HDRI. +/// Fails (returns empty vectors) if the input data is empty, the given width is not 2x the height, or an error was encountered. +[[nodiscard]] std::array, 6> convertHDRIToCubeMap(std::span imageData, ImageFormat format, uint16_t width, uint16_t height, uint16_t resolution = 0, bool bilinear = true); + enum class FileFormat { DEFAULT, PNG, @@ -336,7 +343,7 @@ enum class FileFormat { /// PNG for integer formats, EXR for floating point formats [[nodiscard]] FileFormat getDefaultFileFormatForImageFormat(ImageFormat format); -/// Converts image data to a PNG or EXR file. EXR format will be used for floating-point image formats. +/// Converts image data to the given file format (PNG or EXR by default). [[nodiscard]] std::vector convertImageDataToFile(std::span imageData, ImageFormat format, uint16_t width, uint16_t height, FileFormat fileFormat = FileFormat::DEFAULT); [[nodiscard]] std::vector convertFileToImageData(std::span fileData, ImageFormat& format, int& width, int& height, int& frameCount); diff --git a/lang/python/src/vtfpp.h b/lang/python/src/vtfpp.h index 92e0c6b32..3fb9838c3 100644 --- a/lang/python/src/vtfpp.h +++ b/lang/python/src/vtfpp.h @@ -118,6 +118,11 @@ void register_python(py::module_& m) { return py::bytes{d.data(), d.size()}; }, py::arg("image_data"), py::arg("old_format"), py::arg("new_format"), py::arg("mip_count"), py::arg("frame_count"), py::arg("face_count"), py::arg("width"), py::arg("height"), py::arg("slice_count")); + ImageConversion.def("convert_hdri_to_cubemap", [](const py::bytes& imageData, ImageFormat format, uint16_t width, uint16_t height, uint16_t resolution = 0, bool bilinear = true) -> std::tuple { + const auto ds = convertHDRIToCubeMap({reinterpret_cast(imageData.data()), imageData.size()}, format, width, height, resolution, bilinear); + return {py::bytes{ds[0].data(), ds[0].size()}, py::bytes{ds[1].data(), ds[1].size()}, py::bytes{ds[2].data(), ds[2].size()}, py::bytes{ds[3].data(), ds[3].size()}, py::bytes{ds[4].data(), ds[4].size()}, py::bytes{ds[5].data(), ds[5].size()}}; + }, py::arg("image_data"), py::arg("format"), py::arg("width"), py::arg("height"), py::arg("resolution") = 0, py::arg("bilinear") = true); + py::enum_(ImageConversion, "FileFormat") .value("DEFAULT", FileFormat::DEFAULT) .value("PNG", FileFormat::PNG) diff --git a/src/vtfpp/ImageConversion.cpp b/src/vtfpp/ImageConversion.cpp index 447a25e03..66fda663f 100644 --- a/src/vtfpp/ImageConversion.cpp +++ b/src/vtfpp/ImageConversion.cpp @@ -5,11 +5,16 @@ #include #include #include -#include #include #include #include +#ifdef SOURCEPP_BUILD_WITH_OPENCL +#define CL_HPP_MINIMUM_OPENCL_VERSION 120 +#define CL_HPP_TARGET_OPENCL_VERSION 120 +#include +#endif + #ifdef SOURCEPP_BUILD_WITH_TBB #include #endif @@ -434,7 +439,7 @@ namespace { case InputType: { VTFPP_CONVERT(InputType, r, g, b, a); } break #define VTFPP_CONVERT_REMAP(InputType, r, g, b, a) \ - do { \ + do { \ if constexpr (ImageFormatDetails::alpha(ImageFormat::InputType) > 1) { \ VTFPP_CONVERT(InputType, \ VTFPP_REMAP_TO_16((r), ImageFormatDetails::red(ImageFormat::InputType)), \ @@ -769,6 +774,83 @@ namespace { return newData; } +void convertHDRIToCubeMapCPUFallback(std::span imageDataRGBA32323232F, ImageFormat outputFormat, uint16_t width, uint16_t height, uint16_t resolution, bool bilinear, const std::array, 6>& startRightUp, std::array, 6>& faceData) { + for (int i = 0; i < 6; i++) { + const math::Vec3f& start = startRightUp[i][0]; + const math::Vec3f& right = startRightUp[i][1]; + const math::Vec3f& up = startRightUp[i][2]; + + faceData[i].resize(resolution * resolution * sizeof(ImagePixel::RGBA32323232F)); + std::span face{reinterpret_cast(faceData[i].data()), reinterpret_cast(faceData[i].data() + faceData[i].size())}; + + for (int row = 0; row < resolution; row++) { + for (int col = 0; col < resolution; col++) { + math::Vec3f pixelDirection3d{ + start[0] + ((float) col * 2.f + 0.5f) / (float) resolution * right[0] + ((float) row * 2.f + 0.5f) / (float) resolution * up[0], + start[1] + ((float) col * 2.f + 0.5f) / (float) resolution * right[1] + ((float) row * 2.f + 0.5f) / (float) resolution * up[1], + start[2] + ((float) col * 2.f + 0.5f) / (float) resolution * right[2] + ((float) row * 2.f + 0.5f) / (float) resolution * up[2], + }; + float azimuth = std::atan2(pixelDirection3d[0], -pixelDirection3d[2]) + math::pi_f32; // add pi to move range to 0-360 deg + float elevation = std::atan(pixelDirection3d[1] / std::sqrt(pixelDirection3d[0] * pixelDirection3d[0] + pixelDirection3d[2] * pixelDirection3d[2])) + math::pi_f32 / 2.f; + float colHdri = (azimuth / math::pi_f32 / 2.f) * (float) width; // add pi to azimuth to move range to 0-360 deg + float rowHdri = (elevation / math::pi_f32) * (float) height; + if (!bilinear) { + int colNearest = std::clamp((int) colHdri, 0, width - 1); + int rowNearest = std::clamp((int) rowHdri, 0, height - 1); + face[col * 4 + resolution * row * 4 + 0] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 0]; + face[col * 4 + resolution * row * 4 + 1] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 1]; + face[col * 4 + resolution * row * 4 + 2] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 2]; + face[col * 4 + resolution * row * 4 + 3] = imageDataRGBA32323232F[colNearest * 4 + width * rowNearest * 4 + 3]; + } else { + float intCol, intRow; + // factor gives the contribution of the next column, while the contribution of intCol is 1 - factor + float factorCol = std::modf(colHdri - 0.5f, &intCol); + float factorRow = std::modf(rowHdri - 0.5f, &intRow); + int low_idx_row = static_cast(intRow); + int low_idx_column = static_cast(intCol); + int high_idx_column; + if (factorCol < 0.0f) { + // modf can only give a negative value if the azimuth falls in the first pixel, left of the + // center, so we have to mix with the pixel on the opposite side of the panoramic image + high_idx_column = width - 1; + } else if (low_idx_column == width - 1) { + // if we are in the right-most pixel, and fall right of the center, mix with the left-most pixel + high_idx_column = 0; + } else { + high_idx_column = low_idx_column + 1; + } + int high_idx_row; + if (factorRow < 0.0f) { + high_idx_row = height - 1; + } else if (low_idx_row == height - 1) { + high_idx_row = 0; + } else { + high_idx_row = low_idx_row + 1; + } + factorCol = std::abs(factorCol); + factorRow = std::abs(factorRow); + float f1 = (1 - factorRow) * (1 - factorCol); + float f2 = factorRow * (1 - factorCol); + float f3 = (1 - factorRow) * factorCol; + float f4 = factorRow * factorCol; + for (int j = 0; j < 4; j++) { + auto interpolatedValue = static_cast( + face[low_idx_column * 4 + width * low_idx_row * 4 + j] * f1 + + face[low_idx_column * 4 + width * high_idx_row * 4 + j] * f2 + + face[high_idx_column * 4 + width * low_idx_row * 4 + j] * f3 + + face[high_idx_column * 4 + width * high_idx_row * 4 + j] * f4 + ); + face[col * 4 + resolution * row * 4 + j] = std::clamp(interpolatedValue, 0, 255); + } + } + } + } + if (outputFormat != ImageFormat::RGBA32323232F) { + faceData[i] = ImageConversion::convertImageDataToFormat(faceData[i], ImageFormat::RGBA32323232F, outputFormat, resolution, resolution); + } + } +} + } // namespace std::vector ImageConversion::convertImageDataToFormat(std::span imageData, ImageFormat oldFormat, ImageFormat newFormat, uint16_t width, uint16_t height) { @@ -874,6 +956,122 @@ std::vector ImageConversion::convertSeveralImageDataToFormat(std::spa return out; } +std::array, 6> ImageConversion::convertHDRIToCubeMap(std::span imageData, ImageFormat format, uint16_t width, uint16_t height, uint16_t resolution, bool bilinear) { + if (imageData.empty() || format == ImageFormat::EMPTY) { + return {}; + } + + if (!resolution) { + resolution = height; + } + + std::span imageDataRGBA32323232F{reinterpret_cast(imageData.data()), reinterpret_cast(imageData.data() + imageData.size())}; + + std::vector possiblyConvertedDataOrEmptyDontUseMeDirectly; + if (format != ImageFormat::RGBA32323232F) { + possiblyConvertedDataOrEmptyDontUseMeDirectly = convertImageDataToFormat(imageData, format, ImageFormat::RGBA32323232F, width, height); + imageDataRGBA32323232F = {reinterpret_cast(possiblyConvertedDataOrEmptyDontUseMeDirectly.data()), reinterpret_cast(possiblyConvertedDataOrEmptyDontUseMeDirectly.data() + possiblyConvertedDataOrEmptyDontUseMeDirectly.size())}; + } + + // For each face, contains the 3d starting point (corresponding to left bottom pixel), right direction, + // and up direction in 3d space, corresponding to pixel x,y coordinates of each face + static constexpr std::array, 6> startRightUp = {{ + {{{-1.0f, -1.0f, -1.0f}, { 1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}}}, // front + {{{ 1.0f, -1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}}}, // back + {{{-1.0f, -1.0f, 1.0f}, { 0.0f, 0.0f, -1.0f}, {0.0f, 1.0f, 0.0f}}}, // left + {{{ 1.0f, -1.0f, -1.0f}, { 0.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 0.0f}}}, // right + {{{-1.0f, 1.0f, -1.0f}, { 1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}}, // up + {{{-1.0f, -1.0f, 1.0f}, { 1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, -1.0f}}}, // down + }}; + + std::array, 6> faceData; + +#ifdef SOURCEPP_BUILD_WITH_OPENCL + std::vector platforms; + if (cl::Platform::get(&platforms) != CL_SUCCESS || platforms.empty()) { + ::convertHDRIToCubeMapCPUFallback(imageDataRGBA32323232F, format, width, height, resolution, bilinear, startRightUp, faceData); + return faceData; + } + + std::vector devices; + for (const auto& platform : platforms) { + if (platforms.front().getDevices(CL_DEVICE_TYPE_GPU, &devices) == CL_SUCCESS && !devices.empty()) { + break; + } + devices.clear(); + } + if (devices.empty()) { + ::convertHDRIToCubeMapCPUFallback(imageDataRGBA32323232F, format, width, height, resolution, bilinear, startRightUp, faceData); + return faceData; + } + const auto& device = devices.front(); + + cl::Program::Sources sources{R"( +__constant sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | VTFPP_FILTER; +__kernel void processFace(read_only image2d_t hdriImg, write_only image2d_t faceImg, __global float* startRightUp) { + int2 pixelCoordFace = (int2)(get_global_id(0), get_global_id(1)); + int resolutionFace = get_global_size(0); + float3 start = (float3)(startRightUp[0], startRightUp[1], startRightUp[2]); + float3 right = (float3)(startRightUp[3], startRightUp[4], startRightUp[5]); + float3 up = (float3)(startRightUp[6], startRightUp[7], startRightUp[8]); + float3 direction = (float3)( + start.x + (pixelCoordFace.x * 2.f + 0.5f)/(float)resolutionFace * right.x + (pixelCoordFace.y * 2.f + 0.5f)/(float)resolutionFace * up.x, + start.y + (pixelCoordFace.x * 2.f + 0.5f)/(float)resolutionFace * right.y + (pixelCoordFace.y * 2.f + 0.5f)/(float)resolutionFace * up.y, + start.z + (pixelCoordFace.x * 2.f + 0.5f)/(float)resolutionFace * right.z + (pixelCoordFace.y * 2.f + 0.5f)/(float)resolutionFace * up.z); + float azimuth = atan2(direction.x, -direction.z) + radians(180.f); + float elevation = atan(direction.y / sqrt(pow(direction.x, 2) + pow(direction.z, 2))) + radians(90.f); + float2 pixelCoordHdri = (float2)(azimuth / radians(360.f) * get_image_width(hdriImg), elevation / radians(180.f) * get_image_height(hdriImg)); + uint4 pixel = read_imageui(hdriImg, sampler, pixelCoordHdri); + write_imageui(faceImg, pixelCoordFace, pixel); +})"}; + cl::Context context{device}; + cl::Program program{context, sources}; + if (int err = program.build(bilinear ? "-cl-std=CL1.2 -DVTFPP_FILTER=CLK_FILTER_LINEAR" : "-cl-std=CL1.2 -DVTFPP_FILTER=CLK_FILTER_NEAREST"); err != CL_SUCCESS) { +#ifdef DEBUG + if (err == CL_BUILD_PROGRAM_FAILURE) { + const auto buildLog = program.getBuildInfo(); + SOURCEPP_DEBUG_BREAK; + } +#endif + ::convertHDRIToCubeMapCPUFallback(imageDataRGBA32323232F, format, width, height, resolution, bilinear, startRightUp, faceData); + return faceData; + } + + cl::ImageFormat clFormat{CL_RGBA, CL_FLOAT}; + cl::Image2D imgHdri{context, CL_MEM_READ_ONLY, clFormat, width, height}; + cl::Image2D imgFace{context, CL_MEM_WRITE_ONLY, clFormat, resolution, resolution}; + cl::CommandQueue queue{context, device}; + queue.enqueueWriteImage(imgHdri, CL_TRUE, {0, 0, 0}, {width, height, 1}, 0, 0, imageDataRGBA32323232F.data()); + cl::Buffer bufDirections(context, CL_MEM_READ_ONLY, sizeof(float) * 9); + + for (int i = 0; i < 6; i++) { + auto& face = faceData[i]; + face.resize(resolution * resolution * sizeof(ImagePixel::RGBA32323232F)); + + cl::Kernel kernel{program, "processFace"}; + kernel.setArg(0, imgHdri); + kernel.setArg(1, imgFace); + kernel.setArg(2, bufDirections); + + queue.enqueueWriteBuffer(bufDirections, CL_TRUE, 0, 9 * sizeof(float), &startRightUp[i][0]); + queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(resolution, resolution)); + cl::finish(); + + std::array origin_out{0, 0, 0}; + std::array region_out{resolution, resolution, 1}; + queue.enqueueReadImage(imgFace, CL_TRUE, origin_out, region_out, 0, 0, face.data()); + cl::finish(); + + if (format != ImageFormat::RGBA32323232F) { + faceData[i] = ImageConversion::convertImageDataToFormat(faceData[i], ImageFormat::RGBA32323232F, format, resolution, resolution); + } + } +#else + ::convertHDRIToCubeMapCPUFallback(imageDataRGBA32323232F, format, width, height, resolution, bilinear, startRightUp, faceData); +#endif + return faceData; +} + ImageConversion::FileFormat ImageConversion::getDefaultFileFormatForImageFormat(ImageFormat format) { using enum FileFormat; return ImageFormatDetails::decimal(format) ? EXR : PNG; diff --git a/src/vtfpp/_vtfpp.cmake b/src/vtfpp/_vtfpp.cmake index 8dc22f0cc..fc6fec5f3 100644 --- a/src/vtfpp/_vtfpp.cmake +++ b/src/vtfpp/_vtfpp.cmake @@ -11,6 +11,7 @@ add_pretty_parser(vtfpp "${CMAKE_CURRENT_LIST_DIR}/PPL.cpp" "${CMAKE_CURRENT_LIST_DIR}/VTF.cpp") +sourcepp_add_opencl(vtfpp) sourcepp_add_tbb(vtfpp) sourcepp_add_threads(vtfpp) target_link_compressonator(vtfpp)