You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

笛卡尔转球面坐标时出现纹理伪影问题排查

等距柱状图辐照度计算中笛卡尔-球面坐标转换导致的接缝伪影

问题现象

在实现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

问题根源分析

  1. 单/双精度混合导致的精度损失:
    cartesianToSpherical中使用atan2f(单精度)计算φ,但其他坐标转换逻辑用的是双精度(glm::dvec2),单精度转双精度会引入精度误差,在φ接近0或π时被放大。

  2. φ值范围不匹配:
    atan2f返回的φ范围是[-π, π],而我们需要的φ范围是[0, 2π]。直接将负φ值传入sphericalToUv会得到负u值,后续wrapAroundTexCoords的处理逻辑在处理接近0的负u时,会因std::floor(u)得到-1,导致u值校正出现精度偏差,最终采样到错误的纹理位置。

  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 08:42:02