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

CPU光线追踪器负纹理坐标异常的排查与解决

纹理映射负坐标处理差异问题排查

问题背景

多年前实现的CPU光线追踪软件运行正常,后续开发DirectX 12查看器时,发现二者在纹理映射的负坐标处理上存在不符合预期的差异。测试采用Sponza模型的"sponza_34"网格。

渲染结果对比

  • DirectX 12渲染结果:
    DirectX 12结果
  • CPU光线追踪结果:
    CPU光线追踪结果

问题区域放大

问题区域

负坐标区域细节对比

  • CPU结果:
    CPU对比图
  • DirectX 12结果:
    DX12对比图

已完成排查步骤

  • 通过PIX验证GPU端纹理加载正常
    PIX验证结果
  • 验证GPU与CPU端顶点、索引缓冲区首尾顶点匹配(共612个顶点未逐一验证)

推测问题出在CPU光线追踪器的纹理坐标处理逻辑中,相关代码如下:


DirectX 12代码

采用延迟渲染系统,以下是GBuffer渲染相关代码:

顶点着色器

#include "MeshGroup.hlsli"

struct VS_INPUT
{
    float3 position : POSITION;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
    float3 bitangent : BITANGENT;
    float2 texCoord: TEXCOORD;
};

struct VS_OUTPUT
{
    float4 position: SV_POSITION;
    float3 worldPosition : POSITION;
    float3 tangent : Tangent;
    float3 bitangent : Bitangent;
    float3 normal : NORMAL;
    float2 texCoord: TEXCOORD;
};

cbuffer VertexShaderSharedCB : register(b0)
{
    float4x4 vpMat;
};

VS_OUTPUT main(VS_INPUT input, uint instanceID : SV_InstanceID)
{
    VS_OUTPUT output;

    const float4x4 modelMat = meshGroupDatas[instanceID].transform;
    const float4 worldPosition = mul(float4(input.position, 1.0f), modelMat);
    output.worldPosition = worldPosition.xyz;
    output.position = mul(worldPosition, vpMat);
    output.texCoord = input.texCoord;
    output.normal = normalize(mul(float4(input.normal, 0.0f), modelMat));
    output.tangent = normalize(mul(float4(input.tangent, 0.0f), modelMat));
    output.bitangent = normalize(mul(float4(input.bitangent, 0.0f), modelMat));

    return output;
}

像素着色器

#include "SharedLightning_PS.hlsli"

struct GBufferPSOut
{
    float4 positionWsOccluded : SV_TARGET0;
    float4 normalWs : SV_TARGET1;
    float4 tangentWs : SV_TARGET2;
    float4 bitangentWs : SV_TARGET3;
    float4 albedoShininess : SV_TARGET4;
    float4 specularAnisotropy : SV_TARGET5;
    float4 emissiveMaterialType : SV_TARGET6;
};

GBufferPSOut main(VS_OUTPUT input)
{
    const float3 P = input.worldPosition;
    const float3 N = computeNormal(Material, normalTex, tSampler, input.normal, input.tangent, input.bitangent, input.texCoord);

    float4 matBaseColor = Material.baseColor;
    if (Material.hasBaseColorTex)
    {
        matBaseColor *= baseColorTex.Sample(tSampler, input.texCoord);
    }

    float matShininess = Material.shininess;
    if (Material.hasGlossTex)
    {
        float roughness = _GLOSS(matShininess);
        roughness *= glossTex.Sample(tSampler, input.texCoord).r;
        matShininess = _SHININESS(roughness);
    }

    GBufferPSOut psOut;
    psOut.positionWsOccluded = float4(P, 1.0f);
    psOut.normalWs = float4(N, 1.0f);
    psOut.tangentWs = float4(input.tangent, 1.0f);
    psOut.bitangentWs = float4(input.bitangent, 1.0f);
    psOut.albedoShininess = float4(matBaseColor.xyz, matShininess);
    psOut.specularAnisotropy = Material.specular;

    if (Material.type == EMITTER_IDX)
        psOut.emissiveMaterialType = float4(matBaseColor.xyz, Material.type);
    else
        psOut.emissiveMaterialType = float4(Material.emissive.xyz, Material.type);

    return psOut;
}

CPU端代码

纹理坐标计算函数

inline Math::Vec2 BaseMaterial::interpolateTexCoordinates(const Math::Vec2& t1, const Math::Vec2& t2, const Math::Vec2& t3, const Math::Vec3& coefs) const
{
    Math::Vec2 texCoord = (t1 * coefs.x) + (t2 * coefs.y) + (t3 * coefs.z);

    texCoord.s = std::abs(texCoord.s);
    texCoord.t = std::abs(texCoord.t);

    double dummy;
    if (texCoord.s > 1.0f)
        texCoord.s = (float)std::modf(texCoord.s, &dummy);

    if (texCoord.t > 1.0f)
        texCoord.t = (float)std::modf(texCoord.t, &dummy);

    return texCoord;
}

调用函数

IntersectionProperties buildIntersectionProperties(const Math::Ray& ray, const Intersector::IntersectionInfo& info, const Scene::BaseScene* scene)
    {
        const auto mesh = info.object;
        const auto P = ray.getPoint(info.meshIntersectData.t);

        const uint32_t triStartIdx = info.meshIntersectData.primId * _PRIMITIVE_NB_VTX;

        _ASSERT(_PRIMITIVE_NB_VTX == 3u);
        const auto v1 = mesh->buildTransformedVertexFromIndex(triStartIdx);
        const auto v2 = mesh->buildTransformedVertexFromIndex(triStartIdx + 1);
        const auto v3 = mesh->buildTransformedVertexFromIndex(triStartIdx + 2);

        float area = 0.0f;
        {
            const Math::Vec3 e2 = v2.position - v1.position;
            const Math::Vec3 e3 = v3.position - v1.position;

            area = 0.5f * glm::length(glm::cross(e2, e3));
            area = glm::max(area, 1e-10f);
        }

        const Math::Vec3 coefs = Math::interpolate(v1.position, v2.position, v3.position, P, area);

        // Read material
        const Model::ModelPtr& model = scene->getModel();
        const Material::BaseMaterial* material = model->fastGetMaterialRawPtr_FromEntityOrDefault(mesh->getMaterialId());

        // Texture coordinates
        Math::Vec2 texCoord = material->interpolateTexCoordinates(v1.texCoord, v2.texCoord, v3.texCoord,  coefs);

        // Eye vector
        const Math::Vec3 V = -ray.getDirection();

        // Compute normal
        Math::Vec3 N = (v1.normal * coefs.x) + (v2.normal * coefs.y) + (v3.normal * coefs.z);

        // Tangent and bitangent
        Math::Vec3 T = (v1.tangent * coefs.x) + (v2.tangent * coefs.y) + (v3.tangent * coefs.z);
        Math::Vec3 B = (v1.bitangent * coefs.x) + (v2.bitangent * coefs.y) + (v3.bitangent * coefs.z);

        // Apply normal mapping
        if (material->isFresnelMaterial())
        {
            const auto* fresnelMat = static_cast<const Material::FresnelMaterial*>(material);
            const EntityIdentifier normalMapId = fresnelMat->getNormalImageId();
            
            if (normalMapId)
            {
                // Read bump map
                const auto image = Texture::fastGetRGBAImageRawPtr_FromEntity(normalMapId);
                if (image)
                {
                    const RGBAFColor bumpMapNormal = image->getNormalizedPixelFromRatio(texCoord) * 2.0f - 1.0f;
                    const Math::Mat3 tbn = Math::Mat3(T, B, N);

                    // Bump mapped normal
                    N = tbn * glm::swizzle<glm::X, glm::Y, glm::Z>(bumpMapNormal);
                }
            }
        }

        // Finalize normal
        N = glm::normalize(N);

        if (glm::dot(N, V) < 0.0f)
            N *= -1;

        IntersectionProperties props;
        props.P = P;
        props.deltaP = getOffsetedPositionInDirection(P, N, scene->getCurrentRenderSettings().m_rayEpsilon);
        props.inDeltaP = getOffsetedPositionInDirection(P, -N, scene->getCurrentRenderSettings().m_rayEpsilon);
        props.V = V;
        props.texCoord = texCoord;
        props.BsdfProps.N = N;
        props.BsdfProps.T = T;
        props.BsdfProps.B = B;

        return props;
    }

错误定位与修正

核心错误点

CPU端interpolateTexCoordinates函数的纹理坐标处理逻辑与DirectX 12的默认纹理寻址模式不匹配:

  1. 错误地对负坐标取绝对值:DirectX的Wrap模式(默认采样器设置)会将负坐标映射为texCoord = texCoord - floor(texCoord),例如-0.2会映射为0.8,而取绝对值会把-0.2变成0.2,完全改变了采样位置。
  2. 仅处理大于1的正坐标:原代码只对大于1的正坐标取小数部分,未处理负坐标的模运算,导致负坐标在取绝对值后直接进入[0,1]区间,和GPU行为不一致。

修正后的函数

inline Math::Vec2 BaseMaterial::interpolateTexCoordinates(const Math::Vec2& t1, const Math::Vec2& t2, const Math::Vec2& t3, const Math::Vec3& coefs) const
{
    Math::Vec2 texCoord = (t1 * coefs.x) + (t2 * coefs.y) + (t3 * coefs.z);

    // 实现与GPU Wrap模式一致的纹理坐标寻址
    texCoord.s = texCoord.s - std::floor(texCoord.s);
    texCoord.t = texCoord.t - std::floor(texCoord.t);

    // 处理浮点数精度问题导致的微小负数,确保坐标落在[0,1)区间
    if (texCoord.s < 0.0f) texCoord.s += 1.0f;
    if (texCoord.t < 0.0f) texCoord.t += 1.0f;

    return texCoord;
}

该修正逻辑会将任意正负纹理坐标正确映射到[0,1)区间,和DirectX的Wrap采样行为完全一致,解决负坐标区域的渲染差异问题。


内容的提问来源于stack exchange,提问作者TheChamp

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 12:39:50