From 5f6f178438e4836909b57ec8080d3a1028f83bd3 Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Thu, 28 Mar 2019 18:50:12 +0100 Subject: [PATCH] Fixed compression when convolving --- libraries/image/src/image/CubeMap.cpp | 165 +++++++++++++++++++------- libraries/image/src/image/CubeMap.h | 6 +- libraries/image/src/image/Image.cpp | 147 ++++++++++++----------- libraries/image/src/image/Image.h | 13 ++ 4 files changed, 216 insertions(+), 115 deletions(-) diff --git a/libraries/image/src/image/CubeMap.cpp b/libraries/image/src/image/CubeMap.cpp index a8aba0454c..68fc6fe848 100644 --- a/libraries/image/src/image/CubeMap.cpp +++ b/libraries/image/src/image/CubeMap.cpp @@ -16,6 +16,9 @@ #include "RandomAndNoise.h" #include "Image.h" +#include "ImageLogging.h" + +#include #ifndef M_PI #define M_PI 3.14159265359 @@ -57,11 +60,16 @@ static const glm::vec3 FACE_NORMALS[24] = { }; struct CubeFaceMip { + CubeFaceMip(gpu::uint16 level, const CubeMap* cubemap) { _dims = cubemap->getMipDimensions(level); _lineStride = _dims.x + 2; } + CubeFaceMip(const CubeFaceMip& other) : _dims(other._dims), _lineStride(other._lineStride) { + + } + gpu::Vec2i _dims; int _lineStride; }; @@ -101,10 +109,13 @@ private: class CubeMap::Mip : public CubeFaceMip { public: - Mip(gpu::uint16 level, CubeMap* cubemap) : + explicit Mip(gpu::uint16 level, CubeMap* cubemap) : CubeFaceMip(level, cubemap), _faces(cubemap->_mips[level]) { } + Mip(const Mip& other) : CubeFaceMip(other), _faces(other._faces) { + } + void applySeams() { // Copy edge rows and columns from neighbouring faces to fix seam filtering issues seamColumnAndRow(gpu::Texture::CUBE_FACE_TOP_POS_Y, _dims.x, gpu::Texture::CUBE_FACE_RIGHT_POS_X, -1, -1); @@ -139,6 +150,14 @@ private: Faces& _faces; + inline static void copy(const glm::vec4* srcFirst, const glm::vec4* srcLast, int srcStride, glm::vec4* dstBegin, int dstStride) { + while (srcFirst <= srcLast) { + *dstBegin = *srcFirst; + srcFirst += srcStride; + dstBegin += dstStride; + } + } + static std::pair getSrcAndDst(int dim, int value) { int src; int dst; @@ -177,14 +196,6 @@ private: copyRowToRow(face1, coords1.first, face0, coords0.second, inc); } - inline static void copy(const glm::vec4* srcFirst, const glm::vec4* srcLast, int srcStride, glm::vec4* dstBegin, int dstStride) { - while (srcFirst <= srcLast) { - *dstBegin = *srcFirst; - srcFirst += srcStride; - dstBegin += dstStride; - } - } - void copyColumnToColumn(int srcFace, int srcCol, int dstFace, int dstCol, const int dstInc) { const auto lastOffset = _lineStride * (_dims.y - 1); auto srcFirst = _faces[srcFace].data() + srcCol + _lineStride; @@ -254,33 +265,83 @@ CubeMap::CubeMap(int width, int height, int mipCount) { reset(width, height, mipCount); } -CubeMap::CubeMap(gpu::Texture* texture, const std::atomic& abortProcessing) { - reset(texture->getWidth(), texture->getHeight(), texture->getNumMips()); +struct CubeMap::MipMapOutputHandler : public nvtt::OutputHandler { + MipMapOutputHandler(CubeMap* cube) : _cubemap(cube) {} + + void beginImage(int size, int width, int height, int depth, int face, int miplevel) override { + _data = _cubemap->editFace(miplevel, face); + _current = _data; + } + + bool writeData(const void* data, int size) override { + assert((size % sizeof(glm::vec4)) == 0); + memcpy(_current, data, size); + _current += size / sizeof(glm::vec4); + return true; + } + + void endImage() override { + _data = nullptr; + _current = nullptr; + } + + CubeMap* _cubemap{ nullptr }; + glm::vec4* _data{ nullptr }; + glm::vec4* _current{ nullptr }; +}; + +CubeMap::CubeMap(const std::vector& faces, gpu::Element srcTextureFormat, int mipCount, const std::atomic& abortProcessing) { + reset(faces.front().width(), faces.front().height(), mipCount); - const auto srcTextureFormat = texture->getTexelFormat(); int face; - for (gpu::uint16 mipLevel = 0; mipLevel < texture->getNumMips(); ++mipLevel) { - auto mipDims = texture->evalMipDimensions(mipLevel); - auto srcLineStride = (int) (sizeof(gpu::uint32)*mipDims.x); - auto dstLineStride = getFaceLineStride(mipLevel); - - for (face = 0; face < 6; face++) { - auto sourcePixels = texture->accessStoredMipFace(mipLevel, face)->data(); - auto destPixels = editFace(mipLevel, face); - - convertToFloat(sourcePixels, mipDims.x, mipDims.y, srcLineStride, srcTextureFormat, destPixels, dstLineStride); - if (abortProcessing.load()) { - return; - } + struct MipMapErrorHandler : public nvtt::ErrorHandler { + virtual void error(nvtt::Error e) override { + qCWarning(imagelogging) << "Texture mip map creation error:" << nvtt::errorString(e); } + }; + // Compute mips + for (face = 0; face < 6; face++) { + auto sourcePixels = faces[face].bits(); + auto floatPixels = editFace(0, face); + + convertToFloat(sourcePixels, _width, _height, faces[face].bytesPerLine(), srcTextureFormat, floatPixels, _width); + + nvtt::Surface surface; + surface.setImage(nvtt::InputFormat_RGBA_32F, _width, _height, 1, floatPixels); + surface.setAlphaMode(nvtt::AlphaMode_None); + surface.setWrapMode(nvtt::WrapMode_Clamp); + + auto mipLevel = 0; + copyFace(_width, _height, reinterpret_cast(surface.data()), surface.width(), editFace(0, face), getFaceLineStride(0)); + + while (surface.canMakeNextMipmap() && !abortProcessing.load()) { + surface.buildNextMipmap(nvtt::MipmapFilter_Box); + mipLevel++; + + copyFace(surface.width(), surface.height(), reinterpret_cast(surface.data()), surface.width(), editFace(mipLevel, face), getFaceLineStride(mipLevel)); + } + } + + if (abortProcessing.load()) { + return; + } + + for (gpu::uint16 mipLevel = 0; mipLevel < mipCount; ++mipLevel) { Mip mip(mipLevel, this); - mip.applySeams(); } } +void CubeMap::copyFace(int width, int height, const glm::vec4* source, int srcLineStride, glm::vec4* dest, int dstLineStride) { + for (int y = 0; y < height; y++) { + std::copy(source, source + width, dest); + source += srcLineStride; + dest += dstLineStride; + } +} + void CubeMap::reset(int width, int height, int mipCount) { assert(mipCount >0 && _width > 0 && _height > 0); _width = width; @@ -301,29 +362,45 @@ void CubeMap::reset(int width, int height, int mipCount) { void CubeMap::copyTo(gpu::Texture* texture, const std::atomic& abortProcessing) const { assert(_width == texture->getWidth() && _height == texture->getHeight() && texture->getNumMips() == _mips.size()); - // Convert all mip data back from float - unsigned char* convertedPixels = new unsigned char[_width * _height * sizeof(gpu::uint32)]; - const auto textureFormat = texture->getTexelFormat(); + struct CompressionpErrorHandler : public nvtt::ErrorHandler { + virtual void error(nvtt::Error e) override { + qCWarning(imagelogging) << "Texture compression error:" << nvtt::errorString(e); + } + }; - for (gpu::uint16 mipLevel = 0; mipLevel < texture->getNumMips(); ++mipLevel) { - auto mipDims = texture->evalMipDimensions(mipLevel); - auto mipSize = texture->evalMipFaceSize(mipLevel); - auto srcLineStride = getFaceLineStride(mipLevel); - auto dstLineStride = (int)(sizeof(gpu::uint32)*mipDims.x); + CompressionpErrorHandler errorHandler; + nvtt::OutputOptions outputOptions; + outputOptions.setOutputHeader(false); + outputOptions.setErrorHandler(&errorHandler); - for (auto face = 0; face < 6; face++) { - auto srcPixels = getFace(mipLevel, face); + nvtt::Surface surface; + surface.setAlphaMode(nvtt::AlphaMode_None); + surface.setWrapMode(nvtt::WrapMode_Clamp); - convertFromFloat(convertedPixels, mipDims.x, mipDims.y, dstLineStride, textureFormat, srcPixels, srcLineStride); - texture->assignStoredMipFace(mipLevel, face, mipSize, convertedPixels); - if (abortProcessing.load()) { - delete[] convertedPixels; - return; - } + glm::vec4* packedPixels = new glm::vec4[_width * _height]; + for (int face = 0; face < 6; face++) { + nvtt::CompressionOptions compressionOptions; + std::unique_ptr outputHandler{ getNVTTCompressionOutputHandler(texture, face, compressionOptions) }; + + outputOptions.setOutputHandler(outputHandler.get()); + + SequentialTaskDispatcher dispatcher(abortProcessing); + nvtt::Context context; + context.setTaskDispatcher(&dispatcher); + + for (gpu::uint16 mipLevel = 0; mipLevel < _mips.size() && !abortProcessing.load(); mipLevel++) { + auto mipDims = getMipDimensions(mipLevel); + + copyFace(mipDims.x, mipDims.y, getFace(mipLevel, face), getFaceLineStride(mipLevel), packedPixels, mipDims.x); + surface.setImage(nvtt::InputFormat_RGBA_32F, mipDims.x, mipDims.y, 1, packedPixels); + context.compress(surface, face, mipLevel, compressionOptions, outputOptions); + } + + if (abortProcessing.load()) { + break; } } - - delete[] convertedPixels; + delete[] packedPixels; } void CubeMap::getFaceUV(const glm::vec3& dir, int* index, glm::vec2* uv) { diff --git a/libraries/image/src/image/CubeMap.h b/libraries/image/src/image/CubeMap.h index 0d926cdf44..17bc5642eb 100644 --- a/libraries/image/src/image/CubeMap.h +++ b/libraries/image/src/image/CubeMap.h @@ -18,13 +18,15 @@ #include #include +#include + namespace image { class CubeMap { public: CubeMap(int width, int height, int mipCount); - CubeMap(gpu::Texture* texture, const std::atomic& abortProcessing = false); + CubeMap(const std::vector& faces, gpu::Element faceFormat, int mipCount, const std::atomic& abortProcessing = false); void reset(int width, int height, int mipCount); void copyTo(gpu::Texture* texture, const std::atomic& abortProcessing = false) const; @@ -58,6 +60,7 @@ namespace image { private: struct GGXSamples; + struct MipMapOutputHandler; class Mip; class ConstMip; @@ -70,6 +73,7 @@ namespace image { static void getFaceUV(const glm::vec3& dir, int* index, glm::vec2* uv); static void generateGGXSamples(GGXSamples& data, float roughness, const int resolution); + static void copyFace(int width, int height, const glm::vec4* source, int srcLineStride, glm::vec4* dest, int dstLineStride); void convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic& abortProcessing) const; glm::vec4 computeConvolution(const glm::vec3& normal, const GGXSamples& samples) const; diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 53a76e3b0f..8877176699 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -35,7 +35,6 @@ using namespace gpu; #define CPU_MIPMAPS 1 -#include #undef _CRT_SECURE_NO_WARNINGS #include @@ -50,7 +49,7 @@ std::atomic RECTIFIED_TEXTURE_COUNT{ 0 }; // we use a ref here to work around static order initialization // possibly causing the element not to be constructed yet -static const auto& HDR_FORMAT = gpu::Element::COLOR_R11G11B10; +static const auto& GPUTEXTURE_HDRFORMAT = gpu::Element::COLOR_R11G11B10; const QImage::Format image::QIMAGE_HDRFORMAT = QImage::Format_RGB30; uint rectifyDimension(const uint& dimension) { @@ -236,7 +235,7 @@ static std::function getPackingFunction(const gpu::Ele } std::function getHDRPackingFunction() { - return getPackingFunction(HDR_FORMAT); + return getPackingFunction(GPUTEXTURE_HDRFORMAT); } std::function getUnpackingFunction(const gpu::Element& format) { @@ -254,7 +253,7 @@ std::function getUnpackingFunction(const gpu::Element& f } std::function getHDRUnpackingFunction() { - return getUnpackingFunction(HDR_FORMAT); + return getUnpackingFunction(GPUTEXTURE_HDRFORMAT); } QImage processRawImageData(QIODevice& content, const std::string& filename) { @@ -504,22 +503,18 @@ struct MyErrorHandler : public nvtt::ErrorHandler { } }; -class SequentialTaskDispatcher : public nvtt::TaskDispatcher { -public: - SequentialTaskDispatcher(const std::atomic& abortProcessing) : _abortProcessing(abortProcessing) {}; +SequentialTaskDispatcher::SequentialTaskDispatcher(const std::atomic& abortProcessing) : _abortProcessing(abortProcessing) { +} - const std::atomic& _abortProcessing; - - virtual void dispatch(nvtt::Task* task, void* context, int count) override { - for (int i = 0; i < count; i++) { - if (!_abortProcessing.load()) { - task(context, i); - } else { - break; - } +void SequentialTaskDispatcher::dispatch(nvtt::Task* task, void* context, int count) { + for (int i = 0; i < count; i++) { + if (!_abortProcessing.load()) { + task(context, i); + } else { + break; } } -}; +} void image::convertToFloat(const unsigned char* source, int width, int height, size_t srcLineByteStride, gpu::Element sourceFormat, glm::vec4* output, size_t outputLinePixelStride) { @@ -561,6 +556,40 @@ void image::convertFromFloat(unsigned char* output, int width, int height, size_ } } +nvtt::OutputHandler* getNVTTCompressionOutputHandler(gpu::Texture* outputTexture, int face, nvtt::CompressionOptions& compressionOptions) { + auto outputFormat = outputTexture->getStoredMipFormat(); + + nvtt::InputFormat inputFormat = nvtt::InputFormat_RGBA_32F; + nvtt::WrapMode wrapMode = nvtt::WrapMode_Mirror; + nvtt::AlphaMode alphaMode = nvtt::AlphaMode_None; + + compressionOptions.setQuality(nvtt::Quality_Production); + + // TODO: gles: generate ETC mips instead? + if (outputFormat == gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB) { + compressionOptions.setFormat(nvtt::Format_BC6); + } else if (outputFormat == gpu::Element::COLOR_RGB9E5) { + compressionOptions.setFormat(nvtt::Format_RGB); + compressionOptions.setPixelType(nvtt::PixelType_Float); + compressionOptions.setPixelFormat(32, 32, 32, 0); + } else if (outputFormat == gpu::Element::COLOR_R11G11B10) { + compressionOptions.setFormat(nvtt::Format_RGB); + compressionOptions.setPixelType(nvtt::PixelType_Float); + compressionOptions.setPixelFormat(32, 32, 32, 0); + } else { + qCWarning(imagelogging) << "Unknown mip format"; + Q_UNREACHABLE(); + return nullptr; + } + + if (outputFormat == gpu::Element::COLOR_RGB9E5 || outputFormat == gpu::Element::COLOR_R11G11B10) { + // Don't use NVTT (at least version 2.1) as it outputs wrong RGB9E5 and R11G11B10F values from floats + return new PackedFloatOutputHandler(outputTexture, face, outputFormat); + } else { + return new OutputHandler(outputTexture, face); + } +} + void generateHDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target, const std::atomic& abortProcessing, int face) { // Take a local copy to force move construction // https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f18-for-consume-parameters-pass-by-x-and-stdmove-the-parameter @@ -577,47 +606,23 @@ void generateHDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target nvtt::WrapMode wrapMode = nvtt::WrapMode_Mirror; nvtt::AlphaMode alphaMode = nvtt::AlphaMode_None; - nvtt::CompressionOptions compressionOptions; - compressionOptions.setQuality(nvtt::Quality_Production); - - // TODO: gles: generate ETC mips instead? - if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB) { - compressionOptions.setFormat(nvtt::Format_BC6); - } else if (mipFormat == gpu::Element::COLOR_RGB9E5) { - compressionOptions.setFormat(nvtt::Format_RGB); - compressionOptions.setPixelType(nvtt::PixelType_Float); - compressionOptions.setPixelFormat(32, 32, 32, 0); - } else if (mipFormat == gpu::Element::COLOR_R11G11B10) { - compressionOptions.setFormat(nvtt::Format_RGB); - compressionOptions.setPixelType(nvtt::PixelType_Float); - compressionOptions.setPixelFormat(32, 32, 32, 0); - } else { - qCWarning(imagelogging) << "Unknown mip format"; - Q_UNREACHABLE(); - return; - } - data.resize(width * height); - convertToFloat(localCopy.bits(), width, height, localCopy.bytesPerLine(), HDR_FORMAT, data.data(), width); + convertToFloat(localCopy.bits(), width, height, localCopy.bytesPerLine(), GPUTEXTURE_HDRFORMAT, data.data(), width); // We're done with the localCopy, free up the memory to avoid bloating the heap localCopy = QImage(); // QImage doesn't have a clear function, so override it with an empty one. nvtt::OutputOptions outputOptions; outputOptions.setOutputHeader(false); - std::unique_ptr outputHandler; + + nvtt::CompressionOptions compressionOptions; + std::unique_ptr outputHandler{ getNVTTCompressionOutputHandler(texture, face, compressionOptions) }; + MyErrorHandler errorHandler; outputOptions.setErrorHandler(&errorHandler); nvtt::Context context; int mipLevel = 0; - if (mipFormat == gpu::Element::COLOR_RGB9E5 || mipFormat == gpu::Element::COLOR_R11G11B10) { - // Don't use NVTT (at least version 2.1) as it outputs wrong RGB9E5 and R11G11B10F values from floats - outputHandler.reset(new PackedFloatOutputHandler(texture, face, mipFormat)); - } else { - outputHandler.reset(new OutputHandler(texture, face)); - } - outputOptions.setOutputHandler(outputHandler.get()); nvtt::Surface surface; @@ -836,27 +841,27 @@ void generateLDRMips(gpu::Texture* texture, QImage&& image, BackendTarget target #endif -void generateMips(gpu::Texture* texture, QImage&& image, BackendTarget target, const std::atomic& abortProcessing = false, int face = -1, bool forceCPUBuild = false) { - if (forceCPUBuild || CPU_MIPMAPS) { - PROFILE_RANGE(resource_parse, "generateMips"); +void generateMips(gpu::Texture* texture, QImage&& image, BackendTarget target, const std::atomic& abortProcessing = false, int face = -1) { +#if CPU_MIPMAPS + PROFILE_RANGE(resource_parse, "generateMips"); - if (target == BackendTarget::GLES32) { - generateLDRMips(texture, std::move(image), target, abortProcessing, face); - } else { - if (image.format() == QIMAGE_HDRFORMAT) { - generateHDRMips(texture, std::move(image), target, abortProcessing, face); - } else { - generateLDRMips(texture, std::move(image), target, abortProcessing, face); - } - } + if (target == BackendTarget::GLES32) { + generateLDRMips(texture, std::move(image), target, abortProcessing, face); } else { - texture->setAutoGenerateMips(true); + if (image.format() == QIMAGE_HDRFORMAT) { + generateHDRMips(texture, std::move(image), target, abortProcessing, face); + } else { + generateLDRMips(texture, std::move(image), target, abortProcessing, face); + } } +#else + texture->setAutoGenerateMips(true); +#endif } -void convolveForGGX(gpu::Texture* texture, BackendTarget target, const std::atomic& abortProcessing = false) { +void convolveForGGX(const std::vector& faces, gpu::Element faceFormat, gpu::Texture* texture, const std::atomic& abortProcessing = false) { PROFILE_RANGE(resource_parse, "convolveForGGX"); - CubeMap source(texture, abortProcessing); + CubeMap source(faces, faceFormat, texture->getNumMips(), abortProcessing); CubeMap output(texture->getWidth(), texture->getHeight(), texture->getNumMips()); source.convolveForGGX(output, abortProcessing); @@ -1488,7 +1493,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI if (targetCubemapFormat == QIMAGE_HDRFORMAT && image.format() != targetCubemapFormat) { // If the target format is HDR but the image isn't, we need to convert the // image to HDR. - image = convertToHDRFormat(std::move(image), HDR_FORMAT); + image = convertToHDRFormat(std::move(image), GPUTEXTURE_HDRFORMAT); } else if (image.format() == QIMAGE_HDRFORMAT && image.format() != targetCubemapFormat) { // If the target format isn't HDR (such as on GLES) but the image is, we need to // convert the image to LDR @@ -1504,7 +1509,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI formatGPU = gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB; } } else { - formatGPU = HDR_FORMAT; + formatGPU = GPUTEXTURE_HDRFORMAT; } formatMip = formatGPU; @@ -1559,7 +1564,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI if (target == BackendTarget::GLES32) { irradianceFormat = gpu::Element::COLOR_SRGBA_32; } else { - irradianceFormat = HDR_FORMAT; + irradianceFormat = GPUTEXTURE_HDRFORMAT; } auto irradianceTexture = gpu::Texture::createCube(irradianceFormat, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); @@ -1575,14 +1580,16 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI theTexture->overrideIrradiance(irradiance); } - for (uint8 face = 0; face < faces.size(); ++face) { - // Force building the mip maps right now on CPU if we are convolving for GGX later on - generateMips(theTexture.get(), std::move(faces[face]), target, abortProcessing, face, (options & CUBE_GGX_CONVOLVE) == CUBE_GGX_CONVOLVE); + if (options & CUBE_GGX_CONVOLVE) { + convolveForGGX(faces, GPUTEXTURE_HDRFORMAT, theTexture.get(), abortProcessing); + } else { + // Create mip maps and compress to final format in one go + for (uint8 face = 0; face < faces.size(); ++face) { + // Force building the mip maps right now on CPU if we are convolving for GGX later on + generateMips(theTexture.get(), std::move(faces[face]), target, abortProcessing, face); + } } - if (options & CUBE_GGX_CONVOLVE) { - convolveForGGX(theTexture.get(), target, abortProcessing); - } } return theTexture; diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index 237dfcc6e7..e925718347 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -14,6 +14,7 @@ #include #include +#include #include @@ -107,6 +108,18 @@ gpu::TexturePointer processImage(std::shared_ptr content, const std:: int maxNumPixels, TextureUsage::Type textureType, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing = false); +#if defined(NVTT_API) +class SequentialTaskDispatcher : public nvtt::TaskDispatcher { +public: + SequentialTaskDispatcher(const std::atomic& abortProcessing); + + const std::atomic& _abortProcessing; + + void dispatch(nvtt::Task* task, void* context, int count) override; +}; + +nvtt::OutputHandler* getNVTTCompressionOutputHandler(gpu::Texture* outputTexture, int face, nvtt::CompressionOptions& compressOptions); +#endif } // namespace image #endif // hifi_image_Image_h