CPU光线追踪器负纹理坐标异常的排查与解决
纹理映射负坐标处理差异问题排查
问题背景
多年前实现的CPU光线追踪软件运行正常,后续开发DirectX 12查看器时,发现二者在纹理映射的负坐标处理上存在不符合预期的差异。测试采用Sponza模型的"sponza_34"网格。
渲染结果对比
- DirectX 12渲染结果:

- CPU光线追踪结果:

问题区域放大

负坐标区域细节对比
- CPU结果:

- DirectX 12结果:

已完成排查步骤
- 通过PIX验证GPU端纹理加载正常

- 验证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的默认纹理寻址模式不匹配:
- 错误地对负坐标取绝对值:DirectX的Wrap模式(默认采样器设置)会将负坐标映射为
texCoord = texCoord - floor(texCoord),例如-0.2会映射为0.8,而取绝对值会把-0.2变成0.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
相关产品推荐
相关产品推荐

