笛卡尔转球面坐标时出现纹理伪影问题排查
等距柱状图辐照度计算中笛卡尔-球面坐标转换导致的接缝伪影
问题现象
在实现HDR图像转辐照度图的流程中:
- UV→球面坐标→UV的直接转换无异常
- 经UV→球面→笛卡尔→球面→UV中转转换后,在X轴0(对应φ=0)和0.5(对应φ=π)位置出现两条均匀RGB值的接缝伪影,且伪影不受双线性插值影响
相关代码
插值计算类
#ifndef OFFLINECUBEMAPPROCESSING_H #define OFFLINECUBEMAPPROCESSING_H #include "Texture.h" #include "../kernels/CubemapProcessing.cuh" #include <sstream> constexpr long double epsilon = 1e-10; constexpr glm::dvec3 RED = glm::dvec3(1. , 0 , 0); constexpr glm::dvec3 YELLOW = glm::dvec3(0 , 1 , 1); constexpr glm::dvec3 GREEN = glm::dvec3(0 , 1 ,0); constexpr glm::dvec3 BLUE = glm::dvec3(0 , 0 , 1); constexpr glm::dvec3 BLACK = glm::dvec3(0); template <class T> class EnvmapProcessing{ public: /** * @brief Construct a texture from an envmap double HDR with 3 channels * * @param _data * @param _width * @param _height */ EnvmapProcessing(const T *_data , const unsigned _width , const unsigned _height , const unsigned int num_channels = 3){ width = _width ; height = _height ; channels = num_channels ; for(unsigned i = 0 ; i < width * height * num_channels ; i+=num_channels){ data.push_back(glm::vec3(_data[i] , _data[i+1] , _data[i+2])); } } virtual ~EnvmapProcessing(){} const std::vector<glm::vec3>& getData(){return data;} /** * @brief Computes the radiance map according to the roughness values * * @param roughness * @return TextureData */ TextureData computeSpecularIrradiance(double roughness); /* Implementation of templated methods*/ public: /** * @brief Get the normalized UV in [0 , 1] range , from [0 ,width/height] coordinates. * @tparam D Type of coordinates . * @param x Width coordinates in the range [0 , width[. * @param y Height coordinates in the range [0 , height[. * @return const glm::dvec2 UVs in the range [0 , 1]. */ template<class D> inline const glm::dvec2 getUvFromPixelCoords(const D x , const D y) const { return glm::dvec2(static_cast<long double>(x) / static_cast<long double>(width - 1) , static_cast<long double>(y) / static_cast<long double>(height - 1)) ; } /** * @brief Get the Pixel Coords From Uv object * * @tparam D * @param u * @param v * @return const glm::dvec2 */ template<class D> inline const glm::dvec2 getPixelCoordsFromUv(const D u , const D v) const { return glm::dvec2(u * (static_cast<double>(width) - 1) , v * (static_cast<double>(height) - 1)); } /** * @brief This method wrap around if the texture coordinates provided land beyond the texture dimensions, repeating the texture values on both axes. * * @tparam D Data type of the coordinates. * @param u Horizontal UV coordinates. * @param v Vertical UV coordinates. * @return const glm::dvec2 Normalized coordinates in the [0 , 1] range. */ template<class D> inline const glm::dvec2 wrapAroundTexCoords(const D u , const D v) const { D u_integer = 0 , v_integer = 0 ; D u_double_p = 0. , v_double_p = 0. ; u_integer = std::floor(u); v_integer = std::floor(v); if(u > 1. || u < 0.) u_double_p = u - u_integer ; else u_double_p = u ; if (v > 1. || v < 0.) v_double_p = v - v_integer ; else v_double_p = v ; return glm::dvec2(u_double_p , v_double_p); } /** * @brief Normalizes a set of pixel coordinates into texture bounds. * * @param x Horizontal coordinates. * @param y Vertical coordinates. * @return const glm::dvec2 Normalized coordinates */ inline const glm::dvec2 wrapAroundPixelCoords(const int x , const int y) const { unsigned int x_coord = 0 , y_coord = 0 ; int _width = static_cast<int>(width); int _height = static_cast<int>(height); if(x >= _width) x_coord = x % _width ; else if(x < 0) x_coord = _width + (x % _width) ; else x_coord = x ; if(y >= _height) y_coord = y % _height ; else if(y < 0) y_coord = _height + (y % _height); else y_coord = y ; return glm::dvec2(x_coord , y_coord) ; } /** * @brief Computes an interpolation between 4 pixels. * * @param top_left * @param top_right * @param bottom_left * @param bottom_right * @param point * @return * const T */ const glm::dvec3 bilinearInterpolate(const glm::dvec2 top_left , const glm::dvec2 top_right , const glm::dvec2 bottom_left , const glm::dvec2 bottom_right , const glm::dvec2 point) const { const double u = (point.x - top_left.x) / (top_right.x - top_left.x); const double v = (point.y - top_left.y) / (bottom_left.y - top_left.y); const glm::dvec3 top_interp = (1 - u) * discreteSample(top_left.x , top_left.y) + u * discreteSample(top_right.x , top_right.y); const glm::dvec3 bot_interp = (1 - u) * discreteSample(bottom_left.x , bottom_left.y) + u * discreteSample(bottom_right.x , bottom_right.y) ; return (1 - v) * top_interp + v * bot_interp ; } /** * @brief Sample the textures using Integer coordinates * * @param x Horizontal coordinates * @param y Vertical coordinates * @return const T RGB value of the sampled texel */ inline const glm::dvec3 discreteSample(int x , int y) const{ const glm::dvec2 normalized = wrapAroundPixelCoords(static_cast<int>(x) , static_cast<int>(y)); const glm::dvec3 texel_value = data[normalized.x * height + normalized.y] ; return texel_value ; } /** * @brief This method samples a value from the equirectangular envmap *! Note : In case the coordinates go beyond the bounds of the texture , we wrap around . *! In addition , sampling texels may return a bilinear interpolated value when u,v are converted to a (x ,y) non integer texture coordinate. * @tparam D Type of the coordinates * @param u Horizontal uv coordinates * @param v Vertical uv coordinates * @return T Returns a texel value of type T */ template<class D> inline const glm::dvec3 uvSample(const D u , const D v) const { const glm::dvec2 wrap_uv = wrapAroundTexCoords(u , v); const glm::dvec2 pixel_coords = getPixelCoordsFromUv(wrap_uv.x , wrap_uv.y); const glm::dvec2 top_left(std::floor(pixel_coords.x) , std::floor(pixel_coords.y)); const glm::dvec2 top_right(std::floor(pixel_coords.x) + 1 , std::floor(pixel_coords.y)); const glm::dvec2 bottom_left(std::floor(pixel_coords.x) , std::floor(pixel_coords.y) + 1); const glm::dvec2 bottom_right(std::floor(pixel_coords.x) + 1 , std::floor(pixel_coords.y) + 1); const glm::dvec3 texel_value = bilinearInterpolate(top_left , top_right , bottom_left , bottom_right , pixel_coords); return texel_value ; } /** * @brief Bake an equirect envmap to an irradiance map * @param delta Size of the step * @return std::unique_ptr<TextureData> Texture data containing width , height , and double f_data about the newly created map. */ std::unique_ptr<TextureData> computeDiffuseIrradiance(const T delta) const { TextureData envmap_tex_data ; envmap_tex_data.data_format = Texture::RGB ; envmap_tex_data.internal_format = Texture::RGB32F ; envmap_tex_data.data_type = Texture::FLOAT; envmap_tex_data.width = width ; envmap_tex_data.height = height ; envmap_tex_data.mipmaps = 0; envmap_tex_data.f_data = new float[width * height * channels]; unsigned index = 0 ; for(unsigned i = 0 ; i < width ; i++){ for(unsigned j = 0 ; j < height ; j++){ glm::dvec2 uv = getUvFromPixelCoords(i , j); const glm::dvec2 sph = gpgpu_math::uvToSpherical(uv.x , uv.y); const glm::dvec2 sph_to_uv = gpgpu_math::sphericalToUv(sph); const glm::dvec3 cart = gpgpu_math::sphericalToCartesian(sph.x , sph.y); glm::dvec2 cart_to_sph = gpgpu_math::cartesianToSpherical(cart); glm::dvec2 uvt = gpgpu_math::sphericalToUv(cart_to_sph); //The problem is here , when the UVs have been obtained from Cartesians -> spherical coordinates. const glm::dvec3 irrad = uvSample(uvt.x , uvt.y); float x = static_cast<float>(irrad.x) ; float y = static_cast<float>(irrad.y) ; float z = static_cast<float>(irrad.z) ; envmap_tex_data.f_data[index++] = x ; envmap_tex_data.f_data[index++] = y ; envmap_tex_data.f_data[index++] = z ; } } return std::make_unique<TextureData>(envmap_tex_data); } protected: std::vector<glm::vec3> data ; unsigned width ; unsigned height ; unsigned channels ; }; #endif
坐标转换类
#ifndef CUBEMAPPROCESSING_CUH #define CUBEMAPPROCESSING_CUH #include "Includes.cuh" #include <cmath> namespace gpgpu_math{ template<class T> __host__ __device__ inline const glm::dvec2 uvToSpherical(const T u , const T v){ const T phi = 2 * PI * u; const T theta = PI * v ; return glm::dvec2(phi , theta); } __host__ __device__ inline const glm::dvec2 uvToSpherical(glm::dvec2 uv){ return uvToSpherical(uv.x , uv.y); } template<class T> __host__ __device__ inline const glm::dvec2 sphericalToUv(const T phi , const T theta){ const T u = phi / (2 * PI) ; const T v = theta / PI ; return glm::dvec2(u , v); } __host__ __device__ inline const glm::dvec2 sphericalToUv(glm::dvec2 sph){ return sphericalToUv(sph.x , sph.y); } template<class T> __host__ __device__ inline const glm::dvec3 sphericalToCartesian(const T phi , const T theta){ const T z = cos(theta); const T x = sin(theta) * cos(phi); const T y = sin(theta) * sin(phi); return glm::normalize(glm::dvec3(x , y , z)); } __host__ __device__ inline const glm::dvec3 sphericalToCartesian(glm::dvec2 sph){ return sphericalToCartesian(sph.x , sph.y); } template<class T> __host__ __device__ inline const glm::dvec2 cartesianToSpherical(const T x , const T y , const T z){ const T theta = acos(z); const T phi = atan2f(y , x); return glm::dvec2(phi , theta); } __host__ __device__ inline const glm::dvec2 cartesianToSpherical(glm::dvec3 xyz){ return cartesianToSpherical(xyz.x , xyz.y , xyz.z); } } #endif
问题根源分析
单/双精度混合导致的精度损失:
cartesianToSpherical中使用atan2f(单精度)计算φ,但其他坐标转换逻辑用的是双精度(glm::dvec2),单精度转双精度会引入精度误差,在φ接近0或π时被放大。φ值范围不匹配:
atan2f返回的φ范围是[-π, π],而我们需要的φ范围是[0, 2π]。直接将负φ值传入sphericalToUv会得到负u值,后续wrapAroundTexCoords的处理逻辑在处理接近0的负u时,会因std::floor(u)得到-1,导致u值校正出现精度偏差,最终采样到错误的纹理位置。Wrap逻辑的不稳定性:
原wrapAroundTexCoords通过std::floor拆分整数和小数部分的方式,在处理接近边界的浮点数时容易出现精度累积问题,导致纹理采样的不连续。
修复方案
1. 统一双精度计算
将cartesianToSpherical中的atan2f替换为双精度版本atan2,避免单精度带来的精度损失:
template<class T> __host__ __device__ inline const glm::dvec2 cartesianToSpherical(const T x , const T y , const T z){ const T theta = acos(z); const T phi = atan2(y , x); // 替换atan2f为atan2 return glm::dvec2(phi , theta); }
2. 校正φ值到[0, 2π]范围
在cartesianToSpherical中直接将φ转换为[0, 2π]范围内的值,避免后续wrap处理的精度问题:
template<class T> __host__ __device__ inline const glm::dvec2 cartesianToSpherical(const T x , const T y , const T z){ const T theta = acos(z); T phi = atan2(y , x); if (phi < 0) { phi += 2 * PI; // 将负φ转换为[0, 2π]区间 } return glm::dvec2(phi , theta); }
3. 优化Wrap逻辑
使用fmod实现更稳定的纹理坐标wrap,避免std::floor带来的边界精度问题:
template<class D> inline const glm::dvec2 wrapAroundTexCoords(const D u , const D v) const { D u_wrap = fmod(u, 1.0); if (u_wrap < 0) u_wrap += 1.0; D v_wrap = fmod(v, 1.0); if (v_wrap < 0) v_wrap += 1.0; return glm::dvec2(u_wrap, v_wrap); }
验证
修复后重新测试UV→球面→笛卡尔→球面→UV的转换流程,φ值全程保持在[0, 2π]区间,转换后的u值不会出现负方向的精度偏差,接缝伪影应完全消失。
内容的提问来源于stack exchange,提问作者Amine Bensalem
相关产品推荐
相关产品推荐

