合并小三角形
This commit is contained in:
1 parent
9d29df54a9
commit
e208883638
1 file changed
+317
-493
+317
-493
@@ -539,6 +539,14 @@ struct MeshTexture {
|
||||
}
|
||||
};
|
||||
|
||||
struct VirtualFaceGeometry {
|
||||
Point3f center; // 面片中心(世界坐标)
|
||||
Point3f normal; // 面片法线
|
||||
AABB2f uvBounds; // UV 包围盒
|
||||
cv::Mat1f homography; // 3x3 单应矩阵(从 UV 到视图)
|
||||
bool isValid = false;
|
||||
};
|
||||
|
||||
// used to interpolate adjustments color over the whole texture patch
|
||||
typedef TImage<Color> ColorMap;
|
||||
|
||||
@@ -550,6 +558,7 @@ struct MeshTexture {
|
||||
: r(_r), g(_g), b(_b), a(_a) {}
|
||||
};
|
||||
*/
|
||||
std::vector<VirtualFaceGeometry> m_virtualFaceGeometries;
|
||||
|
||||
public:
|
||||
MeshTexture(Scene& _scene, unsigned _nResolutionLevel=0, unsigned _nMinResolution=640);
|
||||
@@ -621,12 +630,20 @@ public:
|
||||
unsigned minCommonCameras, float fOutlierThreshold,
|
||||
float fRatioDataSmoothness, int nIgnoreMaskLabel,
|
||||
const IIndexArr& views);
|
||||
|
||||
bool ComputeVirtualFaceGeometry(const VirtualFaceMap& virtualFaceMap);
|
||||
|
||||
bool ComputeHomographyForVirtualFace(
|
||||
const VirtualFace& vf,
|
||||
IIndex viewID,
|
||||
VirtualFaceGeometry& geom);
|
||||
|
||||
std::vector<std::vector<IIndex>> faceViews;
|
||||
std::vector<std::vector<float>> faceViewWeights;
|
||||
|
||||
std::vector<VirtualFaceData> virtualFaceDatas;
|
||||
std::vector<std::vector<IIndex>> faceNeighbors;
|
||||
VirtualFaceGeometryArr m_virtualFaceGeometries;
|
||||
// VirtualFaceGeometryArr m_virtualFaceGeometries;
|
||||
|
||||
inline Point3f NormalizePoint3(Point3f& p)
|
||||
{
|
||||
@@ -14161,6 +14178,9 @@ void MeshTexture::FillTextureHoles(std::vector<Image8U3>& textures, Pixel8U colE
|
||||
DEBUG_EXTRA("Hole filling completed");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. RC 风格光栅化主函数
|
||||
// ============================================================
|
||||
bool MeshTexture::RasterizeVirtualFaces(
|
||||
const VirtualFaceMap& virtualFaceMap,
|
||||
const std::vector<std::vector<IIndex>>& virtualFaceViews,
|
||||
@@ -14169,7 +14189,7 @@ bool MeshTexture::RasterizeVirtualFaces(
|
||||
Pixel8U colEmpty,
|
||||
Mesh::Image8U3Arr& outTextures)
|
||||
{
|
||||
DEBUG_EXTRA("Forward Rasterization Engine: Starting...");
|
||||
DEBUG_EXTRA("RC-style Rasterization Engine: Starting...");
|
||||
TD_TIMER_START();
|
||||
|
||||
if (virtualFaceMap.empty() || virtualFaceViews.size() != virtualFaceMap.size())
|
||||
@@ -14190,162 +14210,109 @@ bool MeshTexture::RasterizeVirtualFaces(
|
||||
int textureSize = ComputeOptimalTextureSize(uvWidth, uvHeight, nTextureSizeMultiple);
|
||||
|
||||
// --------------------------------------------------
|
||||
// 2. 创建纹理与缓冲器
|
||||
// 2. 创建纹理
|
||||
// --------------------------------------------------
|
||||
outTextures.emplace_back(textureSize, textureSize);
|
||||
Image8U3& atlas = outTextures.back();
|
||||
atlas.setTo(cv::Scalar(colEmpty.b, colEmpty.g, colEmpty.r));
|
||||
|
||||
// ✅ 深度缓冲器初始化为正无穷(越小越近)
|
||||
cv::Mat1f depthBuffer(textureSize, textureSize, FLT_MAX);
|
||||
cv::Mat3f colorBuffer(textureSize, textureSize, cv::Vec3f(0.f, 0.f, 0.f));
|
||||
cv::Mat1b validBuffer(textureSize, textureSize, (uchar)0);
|
||||
// ✅ 计算所有虚拟面的几何和映射矩阵
|
||||
if (!ComputeVirtualFaceGeometry(virtualFaceMap)) {
|
||||
DEBUG_EXTRA("Failed to compute virtual face geometries");
|
||||
return false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 3. 正向光栅化主循环
|
||||
// 3. RC 风格光栅化:按虚拟面批量处理
|
||||
// --------------------------------------------------
|
||||
#ifdef _USE_OPENMP
|
||||
#pragma omp parallel for schedule(dynamic)
|
||||
#endif
|
||||
for (int_t idxVF = 0; idxVF < (int_t)virtualFaceMap.size(); ++idxVF) {
|
||||
if (virtualFaceViews[idxVF].empty())
|
||||
for (int i = 0; i < (int)virtualFaceMap.size(); ++i) {
|
||||
const VirtualFace& vf = virtualFaceMap[i];
|
||||
const VirtualFaceGeometry& geom = m_virtualFaceGeometries[i];
|
||||
|
||||
if (!geom.isValid || vf.faces.empty() || virtualFaceViews[i].empty())
|
||||
continue;
|
||||
|
||||
const IIndex viewID = virtualFaceViews[idxVF][0];
|
||||
IIndex viewID = virtualFaceViews[i][0];
|
||||
if (viewID >= (IIndex)images.size()) continue;
|
||||
|
||||
|
||||
const Image& srcImg = images[viewID];
|
||||
if (srcImg.image.empty() || srcImg.image.cols < 2 || srcImg.image.rows < 2)
|
||||
continue;
|
||||
|
||||
const Camera& cam = srcImg.camera;
|
||||
const int srcW = srcImg.image.cols;
|
||||
const int srcH = srcImg.image.rows;
|
||||
|
||||
for (FIndex faceID : virtualFaceMap[idxVF].faces) {
|
||||
if (faceID >= (FIndex)scene.mesh.faces.size()) continue;
|
||||
|
||||
const Face& face = scene.mesh.faces[faceID];
|
||||
const TexCoord* uv = &scene.mesh.faceTexcoords[faceID * 3];
|
||||
|
||||
// 验证顶点索引
|
||||
bool validIndices = true;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (face[i] >= scene.mesh.vertices.size()) {
|
||||
validIndices = false;
|
||||
break;
|
||||
// ✅ UV 包围盒 → 纹理像素范围
|
||||
int minX = std::max(0, (int)floor(geom.uvBounds.ptMin.x() * textureSize));
|
||||
int maxX = std::min(textureSize - 1, (int)ceil(geom.uvBounds.ptMax.x() * textureSize));
|
||||
int minY = std::max(0, (int)floor(geom.uvBounds.ptMin.y() * textureSize));
|
||||
int maxY = std::min(textureSize - 1, (int)ceil(geom.uvBounds.ptMax.y() * textureSize));
|
||||
|
||||
if (minX > maxX || minY > maxY) continue;
|
||||
|
||||
int patchW = maxX - minX + 1;
|
||||
int patchH = maxY - minY + 1;
|
||||
|
||||
// ✅ 映射矩阵
|
||||
cv::Mat mapX(patchH, patchW, CV_32FC1);
|
||||
cv::Mat mapY(patchH, patchW, CV_32FC1);
|
||||
|
||||
// ✅ 直接展开 H 系数(无临时 Mat,RC 标准写法)
|
||||
const float* H = geom.homography.ptr<float>();
|
||||
|
||||
for (int y = minY; y <= maxY; ++y) {
|
||||
for (int x = minX; x <= maxX; ++x) {
|
||||
float u = (float)x / (float)textureSize;
|
||||
float v = (float)y / (float)textureSize;
|
||||
|
||||
float w = H[6] * u + H[7] * v + H[8];
|
||||
|
||||
// ✅ 数值保护(防止除零)
|
||||
if (std::abs(w) < 1e-12f) {
|
||||
mapX.at<float>(y - minY, x - minX) = -1.0f;
|
||||
mapY.at<float>(y - minY, x - minX) = -1.0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
float imgX = (H[0] * u + H[1] * v + H[2]) / w;
|
||||
float imgY = (H[3] * u + H[4] * v + H[5]) / w;
|
||||
|
||||
mapX.at<float>(y - minY, x - minX) = imgX;
|
||||
mapY.at<float>(y - minY, x - minX) = imgY;
|
||||
}
|
||||
if (!validIndices) continue;
|
||||
|
||||
const Point3f* verts[3] = {
|
||||
&scene.mesh.vertices[face[0]],
|
||||
&scene.mesh.vertices[face[1]],
|
||||
&scene.mesh.vertices[face[2]]
|
||||
};
|
||||
}
|
||||
|
||||
// 计算包围盒
|
||||
float minU = std::min({uv[0].x, uv[1].x, uv[2].x});
|
||||
float maxU = std::max({uv[0].x, uv[1].x, uv[2].x});
|
||||
float minV = std::min({uv[0].y, uv[1].y, uv[2].y});
|
||||
float maxV = std::max({uv[0].y, uv[1].y, uv[2].y});
|
||||
|
||||
int minX = std::max(0, (int)floor(minU * textureSize));
|
||||
int maxX = std::min(textureSize - 1, (int)ceil(maxU * textureSize));
|
||||
int minY = std::max(0, (int)floor(minV * textureSize));
|
||||
int maxY = std::min(textureSize - 1, (int)ceil(maxV * textureSize));
|
||||
|
||||
if (minX > maxX || minY > maxY) continue;
|
||||
// ✅ 一次性 remap 整个 patch
|
||||
cv::Mat patch;
|
||||
cv::remap(srcImg.image, patch, mapX, mapY,
|
||||
cv::INTER_LINEAR, cv::BORDER_CONSTANT,
|
||||
cv::Scalar(0, 0, 0));
|
||||
|
||||
// ✅ 预计算顶点深度(用于透视校正)
|
||||
float vertexDepths[3];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
vertexDepths[i] = cam.PointDepth(*verts[i]);
|
||||
if (vertexDepths[i] <= 0.0f) {
|
||||
validIndices = false;
|
||||
break;
|
||||
// ✅ 拷贝到 atlas(OpenMP critical 区)
|
||||
#pragma omp critical
|
||||
{
|
||||
for (int y = 0; y < patchH; ++y) {
|
||||
for (int x = 0; x < patchW; ++x) {
|
||||
cv::Vec3b color = patch.at<cv::Vec3b>(y, x);
|
||||
// 跳过无效像素(BORDER_CONSTANT 产生的黑色)
|
||||
if (color[0] == 0 && color[1] == 0 && color[2] == 0)
|
||||
continue;
|
||||
|
||||
int atlasX = x + minX;
|
||||
int atlasY = y + minY;
|
||||
|
||||
// ✅ 边界保护
|
||||
if (atlasX < 0 || atlasX >= textureSize ||
|
||||
atlasY < 0 || atlasY >= textureSize)
|
||||
continue;
|
||||
|
||||
atlas(atlasY, atlasX) = Pixel8U{color[2], color[1], color[0]};
|
||||
}
|
||||
}
|
||||
if (!validIndices) continue;
|
||||
|
||||
for (int y = minY; y <= maxY; ++y) {
|
||||
for (int x = minX; x <= maxX; ++x) {
|
||||
Point2f texCoord(
|
||||
(x + 0.5f) / textureSize,
|
||||
(y + 0.5f) / textureSize
|
||||
);
|
||||
|
||||
Point3f bary;
|
||||
if (!PointInTriangle(texCoord, uv[0], uv[1], uv[2], bary))
|
||||
continue;
|
||||
|
||||
// ✅ 透视校正插值(double 精度)
|
||||
double invZ = bary.x / vertexDepths[0] +
|
||||
bary.y / vertexDepths[1] +
|
||||
bary.z / vertexDepths[2];
|
||||
if (invZ <= 0.0) continue;
|
||||
|
||||
// ✅ 校正后的重心坐标
|
||||
double u0 = (bary.x / vertexDepths[0]) / invZ;
|
||||
double u1 = (bary.y / vertexDepths[1]) / invZ;
|
||||
double u2 = (bary.z / vertexDepths[2]) / invZ;
|
||||
|
||||
// ✅ 透视校正的世界坐标(double)
|
||||
Point3d P_double(
|
||||
verts[0]->x * u0 + verts[1]->x * u1 + verts[2]->x * u2,
|
||||
verts[0]->y * u0 + verts[1]->y * u1 + verts[2]->y * u2,
|
||||
verts[0]->z * u0 + verts[1]->z * u1 + verts[2]->z * u2
|
||||
);
|
||||
|
||||
// ✅ 正确的投影(double → float)
|
||||
Point2d imgPtDouble = cam.ProjectPoint(P_double);
|
||||
Point2f imgPt(static_cast<float>(imgPtDouble.x),
|
||||
static_cast<float>(imgPtDouble.y));
|
||||
|
||||
// ✅ 边界检查
|
||||
if (imgPt.x < 0.5f || imgPt.y < 0.5f ||
|
||||
imgPt.x >= srcW - 0.5f || imgPt.y >= srcH - 0.5f)
|
||||
continue;
|
||||
|
||||
// ✅ 双线性采样
|
||||
Color color = BilinearSample(srcImg.image, imgPt);
|
||||
if (color[0] < 0 || color[1] < 0 || color[2] < 0) continue;
|
||||
|
||||
// ✅ 深度测试(float 缓冲,double 比较)
|
||||
float depthFloat = static_cast<float>(1.0 / invZ);
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
if (depthFloat < depthBuffer(y, x)) {
|
||||
depthBuffer(y, x) = depthFloat;
|
||||
colorBuffer(y, x) = cv::Vec3f(
|
||||
color[0], color[1], color[2]
|
||||
);
|
||||
validBuffer(y, x) = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// 4. 最终写入纹理
|
||||
// --------------------------------------------------
|
||||
for (int y = 0; y < textureSize; ++y) {
|
||||
for (int x = 0; x < textureSize; ++x) {
|
||||
if (validBuffer(y, x)) {
|
||||
cv::Vec3f c = colorBuffer(y, x);
|
||||
atlas(y, x) = Pixel8U{
|
||||
(unsigned char)cv::saturate_cast<uchar>(c[0]),
|
||||
(unsigned char)cv::saturate_cast<uchar>(c[1]),
|
||||
(unsigned char)cv::saturate_cast<uchar>(c[2])
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_EXTRA("Forward Rasterization completed: %s", TD_TIMER_GET_FMT().c_str());
|
||||
DEBUG_EXTRA("RC-style Rasterization completed: %s", TD_TIMER_GET_FMT().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14639,92 +14606,76 @@ bool MeshTexture::GenerateTextureWithVirtualFaces(bool bGlobalSeamLeveling, bool
|
||||
}
|
||||
}
|
||||
|
||||
bool MeshTexture::SelectBestViewsForVirtualFaces(VirtualFaceMap& virtualFaceMap,
|
||||
unsigned minCommonCameras, float fOutlierThreshold,
|
||||
float fRatioDataSmoothness, int nIgnoreMaskLabel,
|
||||
const IIndexArr& views)
|
||||
// ============================================================
|
||||
// 5. SelectBestViewsForVirtualFaces(带 patch 一致性传播)
|
||||
// ============================================================
|
||||
bool MeshTexture::SelectBestViewsForVirtualFaces(
|
||||
VirtualFaceMap& virtualFaceMap,
|
||||
unsigned minCommonCameras, float fOutlierThreshold,
|
||||
float fRatioDataSmoothness, int nIgnoreMaskLabel,
|
||||
const IIndexArr& views)
|
||||
{
|
||||
DEBUG_EXTRA("Selecting best views for %zu virtual faces", virtualFaceMap.size());
|
||||
|
||||
faceViews.resize(virtualFaceMap.size());
|
||||
faceViewWeights.resize(virtualFaceMap.size());
|
||||
|
||||
if (m_virtualFaceGeometries.empty() ||
|
||||
m_virtualFaceGeometries.size() < virtualFaceMap.size()) {
|
||||
m_virtualFaceGeometries.Resize(virtualFaceMap.size());
|
||||
for (auto& v : faceViews) v.clear();
|
||||
for (auto& w : faceViewWeights) w.clear();
|
||||
|
||||
// 确保 faceNeighbors 已填充
|
||||
if (faceNeighbors.empty()) {
|
||||
DEBUG_EXTRA("faceNeighbors empty, running ComputePureFaceVisibility first");
|
||||
if (!ComputePureFaceVisibility(fOutlierThreshold, nIgnoreMaskLabel, views)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& viewList : faceViews) viewList.clear();
|
||||
for (auto& weightList : faceViewWeights) weightList.clear();
|
||||
// faceToView 映射
|
||||
std::vector<IIndex> faceToView(scene.mesh.faces.size(), NO_ID);
|
||||
|
||||
// ===== Debug 统计 =====
|
||||
// ---------- 1. 初始视图分配 ----------
|
||||
size_t emptyCandidateViews = 0;
|
||||
size_t emptyCommonViews = 0;
|
||||
size_t fallbackByCenterFace = 0;
|
||||
size_t successVF = 0;
|
||||
|
||||
// ✅ 面片 → 视图 映射表
|
||||
std::vector<IIndex> faceToView(scene.mesh.faces.size(), NO_ID);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 1. 初始视图分配
|
||||
// ----------------------------------------------------------------
|
||||
for (size_t i = 0; i < virtualFaceMap.size(); ++i) {
|
||||
const VirtualFace& vf = virtualFaceMap[i];
|
||||
if (vf.faces.empty()) continue;
|
||||
|
||||
FIndex faceID2 = vf.faces[0];
|
||||
if (faceID2 >= faceNeighbors.size()) {
|
||||
DEBUG_EXTRA("FATAL: faceID %u out of range!", faceID2);
|
||||
continue;
|
||||
}
|
||||
FIndex faceID = vf.faces[0];
|
||||
if (faceID >= faceNeighbors.size()) continue;
|
||||
|
||||
// 收集候选视图
|
||||
std::unordered_set<IIndex> candidateViews;
|
||||
for (FIndex faceID : vf.faces) {
|
||||
if (faceID >= faceNeighbors.size()) continue;
|
||||
for (IIndex viewID : faceNeighbors[faceID]) {
|
||||
if (views.empty() || views.FindFirst(viewID) != NO_ID) {
|
||||
candidateViews.insert(viewID);
|
||||
for (FIndex fid : vf.faces) {
|
||||
if (fid >= faceNeighbors.size()) continue;
|
||||
for (IIndex vid : faceNeighbors[fid]) {
|
||||
if (views.empty() || views.FindFirst(vid) != NO_ID) {
|
||||
candidateViews.insert(vid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 2. 兜底:候选视图为空
|
||||
// ------------------------------------------------------------
|
||||
if (candidateViews.empty()) {
|
||||
++emptyCandidateViews;
|
||||
|
||||
IIndex forcedView = NO_ID;
|
||||
if (!views.empty()) {
|
||||
forcedView = views[0];
|
||||
} else if (!images.empty()) {
|
||||
forcedView = 0;
|
||||
}
|
||||
|
||||
// 兜底:用第一个可用视图
|
||||
IIndex forcedView = (!views.empty()) ? views[0] :
|
||||
(!images.empty()) ? 0 : NO_ID;
|
||||
if (forcedView != NO_ID) {
|
||||
faceViews[i].push_back(forcedView);
|
||||
faceViewWeights[i].push_back(1.0f);
|
||||
|
||||
for (FIndex fid : vf.faces) {
|
||||
if (fid < faceToView.size())
|
||||
faceToView[fid] = forcedView;
|
||||
}
|
||||
|
||||
++fallbackByCenterFace;
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_EXTRA("VF[%zu] truly hopeless: no images available", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3. 选择最佳视图(简单策略:第一个候选)
|
||||
// ------------------------------------------------------------
|
||||
// 选第一个候选(简单策略,后续可优化为角度最优)
|
||||
IIndex bestView = *candidateViews.begin();
|
||||
|
||||
faceViews[i].push_back(bestView);
|
||||
faceViewWeights[i].push_back(1.0f);
|
||||
|
||||
@@ -14732,13 +14683,10 @@ bool MeshTexture::SelectBestViewsForVirtualFaces(VirtualFaceMap& virtualFaceMap,
|
||||
if (fid < faceToView.size())
|
||||
faceToView[fid] = bestView;
|
||||
}
|
||||
|
||||
++successVF;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 4. 【轻量 Patch 一致性传播】
|
||||
// ----------------------------------------------------------------
|
||||
// ---------- 2. Patch 一致性传播 ----------
|
||||
if (scene.mesh.faceFaces.empty()) {
|
||||
scene.mesh.ListIncidenteFaceFaces();
|
||||
}
|
||||
@@ -14777,13 +14725,10 @@ bool MeshTexture::SelectBestViewsForVirtualFaces(VirtualFaceMap& virtualFaceMap,
|
||||
newFaceToView[fid] = majorityView;
|
||||
}
|
||||
}
|
||||
|
||||
faceToView.swap(newFaceToView);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 5. 根据传播后的结果,更新 virtual face 的视图
|
||||
// ----------------------------------------------------------------
|
||||
// ---------- 3. 写回 ----------
|
||||
for (size_t i = 0; i < virtualFaceMap.size(); ++i) {
|
||||
const VirtualFace& vf = virtualFaceMap[i];
|
||||
if (vf.faces.empty() || faceViews[i].empty()) continue;
|
||||
@@ -14796,22 +14741,150 @@ bool MeshTexture::SelectBestViewsForVirtualFaces(VirtualFaceMap& virtualFaceMap,
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 6. Debug 汇总
|
||||
// ----------------------------------------------------------------
|
||||
DEBUG_EXTRA("====== Virtual Face View Selection Summary ======");
|
||||
DEBUG_EXTRA("Total virtual faces : %zu", virtualFaceMap.size());
|
||||
DEBUG_EXTRA("Successfully assigned : %zu", successVF);
|
||||
DEBUG_EXTRA("Empty candidates : %zu", emptyCandidateViews);
|
||||
DEBUG_EXTRA("Fallback by center face : %zu", fallbackByCenterFace);
|
||||
DEBUG_EXTRA("Empty after relaxation : %zu", emptyCommonViews);
|
||||
DEBUG_EXTRA("Expected empty ratio : %.2f%%",
|
||||
100.0 * (emptyCandidateViews - fallbackByCenterFace) / virtualFaceMap.size());
|
||||
DEBUG_EXTRA("=================================================");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1. 计算虚拟面几何(Affine for triangle, Homography for patch)
|
||||
// ============================================================
|
||||
bool MeshTexture::ComputeVirtualFaceGeometry(const VirtualFaceMap& virtualFaceMap) {
|
||||
// ✅ std::vector 用 resize
|
||||
m_virtualFaceGeometries.resize(virtualFaceMap.size());
|
||||
|
||||
#ifdef _USE_OPENMP
|
||||
#pragma omp parallel for schedule(dynamic)
|
||||
#endif
|
||||
for (int i = 0; i < (int)virtualFaceMap.size(); ++i) {
|
||||
const VirtualFace& vf = virtualFaceMap[i];
|
||||
VirtualFaceGeometry& geom = m_virtualFaceGeometries[i];
|
||||
|
||||
if (vf.faces.empty() || faceViews[i].empty()) {
|
||||
geom.isValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
IIndex viewID = faceViews[i][0];
|
||||
geom.isValid = ComputeHomographyForVirtualFace(vf, viewID, geom);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. 核心:计算单应矩阵 / 仿射矩阵
|
||||
// - 3 个点 → Affine(getAffineTransform)
|
||||
// - ≥ 4 个点 → Homography(findHomography)
|
||||
// ============================================================
|
||||
bool MeshTexture::ComputeHomographyForVirtualFace(
|
||||
const VirtualFace& vf,
|
||||
IIndex viewID,
|
||||
VirtualFaceGeometry& geom)
|
||||
{
|
||||
if (viewID >= (IIndex)images.size()) return false;
|
||||
const Camera& cam = images[viewID].camera;
|
||||
|
||||
// ---------- 1. 收集顶点和 UV ----------
|
||||
std::vector<Point3f> points3D;
|
||||
std::vector<Point2f> pointsUV;
|
||||
points3D.reserve(vf.faces.size() * 3);
|
||||
pointsUV.reserve(vf.faces.size() * 3);
|
||||
|
||||
for (FIndex faceID : vf.faces) {
|
||||
if (faceID >= (FIndex)scene.mesh.faces.size()) continue;
|
||||
|
||||
const Face& face = scene.mesh.faces[faceID];
|
||||
const TexCoord* uv = &scene.mesh.faceTexcoords[faceID * 3];
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (face[i] >= scene.mesh.vertices.size()) continue;
|
||||
|
||||
points3D.push_back(scene.mesh.vertices[face[i]]);
|
||||
pointsUV.push_back(Point2f(uv[i].x, uv[i].y));
|
||||
}
|
||||
}
|
||||
|
||||
if (points3D.size() < 3) return false;
|
||||
|
||||
// ---------- 2. 计算中心(OpenCV 风格)----------
|
||||
geom.center = Point3f(0, 0, 0);
|
||||
for (const auto& p : points3D) geom.center += p;
|
||||
geom.center *= 1.0f / (float)points3D.size();
|
||||
|
||||
// ---------- 3. 计算法线(OpenCV 风格)----------
|
||||
if (!vf.faces.empty()) {
|
||||
FIndex faceID = vf.faces[0];
|
||||
const Face& face = scene.mesh.faces[faceID];
|
||||
const Point3f& v0 = scene.mesh.vertices[face[0]];
|
||||
const Point3f& v1 = scene.mesh.vertices[face[1]];
|
||||
const Point3f& v2 = scene.mesh.vertices[face[2]];
|
||||
|
||||
Point3f edge1 = v1 - v0;
|
||||
Point3f edge2 = v2 - v0;
|
||||
Point3f normal = edge1.cross(edge2);
|
||||
|
||||
float length = cv::norm(normal);
|
||||
if (length > 1e-8f) {
|
||||
normal *= 1.0f / length;
|
||||
} else {
|
||||
normal = Point3f(0, 0, 1);
|
||||
}
|
||||
geom.normal = normal;
|
||||
}
|
||||
|
||||
// ---------- 4. UV 包围盒 ----------
|
||||
geom.uvBounds.Reset();
|
||||
for (const auto& uv : pointsUV) {
|
||||
geom.uvBounds.InsertFull(uv);
|
||||
}
|
||||
|
||||
// ---------- 5. 投影到图像空间(double → float)----------
|
||||
std::vector<Point2f> pointsImage;
|
||||
pointsImage.reserve(points3D.size());
|
||||
|
||||
for (const auto& p3D : points3D) {
|
||||
Point3d p64(p3D.x, p3D.y, p3D.z);
|
||||
Point2d proj = cam.ProjectPoint(p64);
|
||||
pointsImage.emplace_back((float)proj.x, (float)proj.y);
|
||||
}
|
||||
|
||||
// ---------- 6. 核心分支:Affine vs Homography ----------
|
||||
if (pointsUV.size() == 3) {
|
||||
// ✅ 三角形:用仿射变换(Affine = 精确映射)
|
||||
cv::Point2f src[3] = {pointsUV[0], pointsUV[1], pointsUV[2]};
|
||||
cv::Point2f dst[3] = {pointsImage[0], pointsImage[1], pointsImage[2]};
|
||||
|
||||
cv::Mat affine = cv::getAffineTransform(src, dst);
|
||||
if (affine.empty()) return false;
|
||||
|
||||
// 转成 3x3 齐次矩阵(与 Homography 统一格式)
|
||||
geom.homography = cv::Mat1f(3, 3, 0.0f);
|
||||
for (int r = 0; r < 2; ++r) {
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
geom.homography(r, c) = affine.at<double>(r, c);
|
||||
}
|
||||
}
|
||||
geom.homography(2, 2) = 1.0f;
|
||||
}
|
||||
else if (pointsUV.size() >= 4) {
|
||||
// ✅ Patch:用 RANSAC 单应矩阵
|
||||
geom.homography = cv::findHomography(
|
||||
pointsUV, pointsImage, cv::RANSAC, 2.0);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
geom.isValid = !geom.homography.empty();
|
||||
return geom.isValid;
|
||||
}
|
||||
|
||||
bool MeshTexture::GenerateTextureWithVirtualFacesInternal(bool bGlobalSeamLeveling, bool bLocalSeamLeveling,
|
||||
unsigned nTextureSizeMultiple, unsigned nRectPackingHeuristic,
|
||||
Pixel8U colEmpty, float fSharpnessWeight, int maxTextureSize,
|
||||
@@ -14918,282 +14991,34 @@ bool MeshTexture::ConvertVectorToVirtualFaceDataArr(const std::vector<VirtualFac
|
||||
|
||||
return true;
|
||||
}
|
||||
bool MeshTexture::CreateVirtualFacesForExistingUV(VirtualFaceMap& virtualFaceMap) {
|
||||
DEBUG_EXTRA("Creating virtual faces for existing UV texture mapping");
|
||||
|
||||
// 0. 详细的输入验证
|
||||
DEBUG_EXTRA("=== Detailed Input Validation ===");
|
||||
DEBUG_EXTRA("Scene pointer: %p", &scene);
|
||||
DEBUG_EXTRA("Mesh pointer: %p", &scene.mesh);
|
||||
|
||||
// 检查基本数据结构
|
||||
// ============================================================
|
||||
// 4. CreateVirtualFacesForExistingUV(简化版)
|
||||
// ============================================================
|
||||
bool MeshTexture::CreateVirtualFacesForExistingUV(VirtualFaceMap& virtualFaceMap) {
|
||||
DEBUG_EXTRA("Creating virtual faces for existing UV (RC style)");
|
||||
|
||||
// 输入验证
|
||||
if (scene.mesh.faces.empty()) {
|
||||
DEBUG_EXTRA("ERROR: Mesh has no faces!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scene.mesh.vertices.empty()) {
|
||||
DEBUG_EXTRA("ERROR: Mesh has no vertices!");
|
||||
return false;
|
||||
}
|
||||
|
||||
virtualFaceMap.resize(scene.mesh.faces.size());
|
||||
if (scene.mesh.faceTexcoords.empty()) {
|
||||
DEBUG_EXTRA("ERROR: Mesh has no texture coordinates!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ RC 风格:每个三角形一个虚拟面(1:1)
|
||||
virtualFaceMap.resize(scene.mesh.faces.size());
|
||||
for (FIndex i = 0; i < (FIndex)scene.mesh.faces.size(); ++i) {
|
||||
virtualFaceMap[i].faces = {i};
|
||||
}
|
||||
return true;
|
||||
|
||||
// 验证faces数组的大小
|
||||
size_t numFaces = scene.mesh.faces.size();
|
||||
size_t numVertices = scene.mesh.vertices.size();
|
||||
size_t numFaceTexcoords = scene.mesh.faceTexcoords.size();
|
||||
|
||||
DEBUG_EXTRA("Number of faces: %zu", numFaces);
|
||||
DEBUG_EXTRA("Number of vertices: %zu", numVertices);
|
||||
DEBUG_EXTRA("Number of texture coordinates: %zu", numFaceTexcoords);
|
||||
|
||||
// 关键检查:必须有纹理坐标才能使用此方法
|
||||
if (numFaceTexcoords == 0) {
|
||||
DEBUG_EXTRA("ERROR: Mesh has no texture coordinates. Cannot create virtual faces for existing UV.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (numFaceTexcoords < numFaces * 3) {
|
||||
DEBUG_EXTRA("ERROR: Not enough UV coordinates! Expected %zu (faces * 3), have %zu",
|
||||
numFaces * 3, numFaceTexcoords);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证面片索引
|
||||
DEBUG_EXTRA("Validating face indices...");
|
||||
for (size_t i = 0; i < std::min(numFaces, static_cast<size_t>(10)); ++i) { // 使用static_cast
|
||||
const Mesh::Face& face = scene.mesh.faces[i];
|
||||
bool valid = true;
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
if (face[j] >= numVertices) {
|
||||
DEBUG_EXTRA("ERROR: Face %zu has invalid vertex index %u (max: %zu)",
|
||||
i, face[j], numVertices - 1);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
DEBUG_EXTRA("Face %zu: vertices [%u, %u, %u] - OK",
|
||||
i, face[0], face[1], face[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证UV坐标索引
|
||||
if (numFaceTexcoords > 0) {
|
||||
DEBUG_EXTRA("Validating UV coordinate indices...");
|
||||
if (numFaceTexcoords < numFaces * 3) {
|
||||
DEBUG_EXTRA("ERROR: Not enough UV coordinates! Expected %zu, have %zu",
|
||||
numFaces * 3, numFaceTexcoords);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 确保网格拓扑已计算
|
||||
DEBUG_EXTRA("Computing mesh topology...");
|
||||
if (scene.mesh.faceFaces.empty()) {
|
||||
DEBUG_EXTRA(" Computing incident faces...");
|
||||
try {
|
||||
scene.mesh.ListIncidenteFaces();
|
||||
scene.mesh.ListIncidenteFaceFaces();
|
||||
DEBUG_EXTRA(" Done computing incident faces");
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_EXTRA(" ERROR computing incident faces: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (scene.mesh.faceNormals.empty()) {
|
||||
DEBUG_EXTRA(" Computing face normals...");
|
||||
try {
|
||||
scene.mesh.ComputeNormalFaces();
|
||||
DEBUG_EXTRA(" Done computing face normals");
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_EXTRA(" ERROR computing face normals: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<bool> processedFaces(numFaces, false);
|
||||
|
||||
// 3. 基于曲率分割网格
|
||||
DEBUG_EXTRA("Segmenting mesh based on curvature...");
|
||||
Mesh::FaceIdxArr regionMap;
|
||||
|
||||
try {
|
||||
scene.SegmentMeshBasedOnCurvature(regionMap, 0.2f);
|
||||
DEBUG_EXTRA("Mesh segmentation completed, regionMap size: %zu", regionMap.size());
|
||||
|
||||
if (regionMap.size() != numFaces) {
|
||||
DEBUG_EXTRA("ERROR: regionMap size (%zu) doesn't match number of faces (%u)",
|
||||
regionMap.size(), numFaces);
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_EXTRA("ERROR during mesh segmentation: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 统计每个区域的面积
|
||||
DEBUG_EXTRA("Calculating region areas...");
|
||||
std::unordered_map<int, float> regionAreas;
|
||||
|
||||
for (FIndex fid = 0; fid < numFaces; ++fid) {
|
||||
if (fid % 10000 == 0 && fid > 0) {
|
||||
DEBUG_EXTRA(" Processed %u/%u faces", fid, numFaces);
|
||||
}
|
||||
|
||||
int region = regionMap[fid];
|
||||
|
||||
// 验证面片索引
|
||||
if (fid >= scene.mesh.faces.size()) {
|
||||
DEBUG_EXTRA("ERROR: Face index %u out of bounds (mesh has %zu faces)",
|
||||
fid, scene.mesh.faces.size());
|
||||
continue;
|
||||
}
|
||||
|
||||
const Mesh::Face& face = scene.mesh.faces[fid];
|
||||
|
||||
// 验证顶点索引
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (face[i] >= scene.mesh.vertices.size()) {
|
||||
DEBUG_EXTRA("ERROR: Vertex index %u out of bounds in face %u (mesh has %zu vertices)",
|
||||
face[i], fid, scene.mesh.vertices.size());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const Point3f& v0 = scene.mesh.vertices[face[0]];
|
||||
const Point3f& v1 = scene.mesh.vertices[face[1]];
|
||||
const Point3f& v2 = scene.mesh.vertices[face[2]];
|
||||
|
||||
// 计算三角形面积
|
||||
Point3f edge1 = v1 - v0;
|
||||
Point3f edge2 = v2 - v0;
|
||||
|
||||
Point3f crossProd(
|
||||
edge1.y * edge2.z - edge1.z * edge2.y,
|
||||
edge1.z * edge2.x - edge1.x * edge2.z,
|
||||
edge1.x * edge2.y - edge1.y * edge2.x
|
||||
);
|
||||
|
||||
float area = 0.5f * std::sqrt(
|
||||
crossProd.x * crossProd.x +
|
||||
crossProd.y * crossProd.y +
|
||||
crossProd.z * crossProd.z
|
||||
);
|
||||
|
||||
if (area < 0) {
|
||||
DEBUG_EXTRA("WARNING: Negative area calculated for face %u: %f", fid, area);
|
||||
area = 0.0f;
|
||||
}
|
||||
|
||||
regionAreas[region] += area;
|
||||
}
|
||||
|
||||
DEBUG_EXTRA("Region area calculation completed, found %zu regions", regionAreas.size());
|
||||
|
||||
// 5. 为每个区域收集面片
|
||||
DEBUG_EXTRA("Grouping faces by region...");
|
||||
std::unordered_map<int, std::vector<FIndex>> regionFaces;
|
||||
for (FIndex fid = 0; fid < numFaces; ++fid) {
|
||||
int region = regionMap[fid];
|
||||
regionFaces[region].push_back(fid);
|
||||
}
|
||||
|
||||
DEBUG_EXTRA("Found %zu regions with faces", regionFaces.size());
|
||||
|
||||
// 6. 创建虚拟面
|
||||
DEBUG_EXTRA("Creating virtual faces...");
|
||||
virtualFaceMap.clear();
|
||||
virtualFaceMap.reserve(regionFaces.size());
|
||||
|
||||
int regionCount = 0;
|
||||
int smallRegionCount = 0;
|
||||
int uvDiscontinuousCount = 0;
|
||||
int createdVirtualFaces = 0;
|
||||
|
||||
for (const auto& region : regionFaces) {
|
||||
regionCount++;
|
||||
int regionID = region.first;
|
||||
const std::vector<FIndex>& faceList = region.second;
|
||||
|
||||
if (regionCount % 100 == 0) {
|
||||
DEBUG_EXTRA(" Processing region %d/%d (%zu faces)", regionCount, regionFaces.size(), faceList.size());
|
||||
}
|
||||
|
||||
// 检查区域面积是否足够大
|
||||
float regionArea = regionAreas[regionID];
|
||||
if (regionArea < 0.001f) { // 面积阈值
|
||||
DEBUG_EXTRA(" Region %d is too small (area: %f), using individual faces", regionID, regionArea);
|
||||
smallRegionCount++;
|
||||
|
||||
// 区域太小,不创建虚拟面
|
||||
for (FIndex fid : faceList) {
|
||||
virtualFaceMap.push_back(VirtualFace());
|
||||
virtualFaceMap.back().faces.push_back(fid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查UV连续性
|
||||
if (!CheckUVContinuity(faceList)) {
|
||||
DEBUG_EXTRA(" Region %d has discontinuous UV, using individual faces", regionID);
|
||||
uvDiscontinuousCount++;
|
||||
|
||||
// UV不连续,保持原始面片
|
||||
for (FIndex fid : faceList) {
|
||||
virtualFaceMap.push_back(VirtualFace());
|
||||
virtualFaceMap.back().faces.push_back(fid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建虚拟面
|
||||
VirtualFace vf;
|
||||
vf.faces = faceList;
|
||||
|
||||
// 计算虚拟面的中心、法线和面积
|
||||
if (!CalculateVirtualFaceProperties(vf)) {
|
||||
DEBUG_EXTRA(" Failed to calculate properties for virtual face in region %d", regionID);
|
||||
// 计算失败,回退到原始面片
|
||||
for (FIndex fid : faceList) {
|
||||
virtualFaceMap.push_back(VirtualFace());
|
||||
virtualFaceMap.back().faces.push_back(fid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算虚拟面的UV边界
|
||||
if (!CalculateVirtualFaceUVBounds(vf)) {
|
||||
DEBUG_EXTRA(" Failed to calculate UV bounds for virtual face in region %d", regionID);
|
||||
// 计算失败,回退到原始面片
|
||||
for (FIndex fid : faceList) {
|
||||
virtualFaceMap.push_back(VirtualFace());
|
||||
virtualFaceMap.back().faces.push_back(fid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
virtualFaceMap.push_back(vf);
|
||||
createdVirtualFaces++;
|
||||
}
|
||||
|
||||
DEBUG_EXTRA("Virtual face creation completed:");
|
||||
DEBUG_EXTRA(" Total regions: %zu", regionFaces.size());
|
||||
DEBUG_EXTRA(" Small regions (area < 0.001): %d", smallRegionCount);
|
||||
DEBUG_EXTRA(" UV discontinuous regions: %d", uvDiscontinuousCount);
|
||||
DEBUG_EXTRA(" Created virtual faces: %d", createdVirtualFaces);
|
||||
DEBUG_EXTRA(" Total virtual faces (including individual faces): %zu", virtualFaceMap.size());
|
||||
|
||||
if (virtualFaceMap.empty()) {
|
||||
DEBUG_EXTRA("ERROR: No virtual faces created!");
|
||||
return false;
|
||||
}
|
||||
|
||||
DEBUG_EXTRA("Created %zu virtual faces (one per triangle)", virtualFaceMap.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -18813,49 +18638,48 @@ bool Scene::TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsi
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅✅✅ 关键修复:重新计算拓扑(否则 faceFaces 是坏的)
|
||||
mesh.ListIncidenteFaces();
|
||||
mesh.ListIncidenteFaceFaces();
|
||||
mesh.ComputeNormalFaces();
|
||||
mesh.ListBoundaryVertices();
|
||||
// ✅ 确保拓扑信息存在
|
||||
if (mesh.faceFaces.empty()) {
|
||||
mesh.ListIncidenteFaces();
|
||||
mesh.ListIncidenteFaceFaces();
|
||||
}
|
||||
if (mesh.faceNormals.empty()) {
|
||||
mesh.ComputeNormalFaces();
|
||||
}
|
||||
if (mesh.vertexBoundary.empty()) {
|
||||
mesh.ListBoundaryVertices();
|
||||
}
|
||||
|
||||
// 纯可见性计算(初始化faceNeighbors)
|
||||
if (!texture.ComputePureFaceVisibility(fOutlierThreshold, nIgnoreMaskLabel, views)) {
|
||||
// ❗ 兜底:如果ComputePureFaceVisibility没初始化faceNeighbors,手动填充
|
||||
if (texture.faceNeighbors.empty()) {
|
||||
texture.faceNeighbors.resize(mesh.faces.size());
|
||||
for (FIndex fid = 0; fid < (FIndex)mesh.faces.size(); ++fid) {
|
||||
if (!views.empty()) {
|
||||
texture.faceNeighbors[fid].insert(texture.faceNeighbors[fid].end(), views.begin(), views.end());
|
||||
} else {
|
||||
for (IIndex vid = 0; vid < (IIndex)images.size(); ++vid)
|
||||
texture.faceNeighbors[fid].push_back(vid);
|
||||
}
|
||||
}
|
||||
DEBUG_EXTRA("Forced fill faceNeighbors for %zu faces", mesh.faces.size());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// ✅ 1. 创建虚拟面(每三角形一个)
|
||||
MeshTexture::VirtualFaceMap virtualFaceMap;
|
||||
if (!texture.CreateVirtualFacesForExistingUV(virtualFaceMap))
|
||||
return false;
|
||||
|
||||
// 创建虚拟面(1:1映射)
|
||||
MeshTexture::VirtualFaceMap virtualFaceMap;
|
||||
if (!texture.CreateVirtualFacesForExistingUV(virtualFaceMap)) return false;
|
||||
// ✅ 2. 计算可见性(填充 faceNeighbors)
|
||||
if (!texture.ComputePureFaceVisibility(
|
||||
fOutlierThreshold, nIgnoreMaskLabel, views)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 选最佳视图(带patch一致性)
|
||||
if (!texture.SelectBestViewsForVirtualFaces(
|
||||
virtualFaceMap, 1, fOutlierThreshold, fRatioDataSmoothness, nIgnoreMaskLabel, views))
|
||||
return false;
|
||||
// ✅ 3. 选择最佳视图(带 patch 一致性)
|
||||
if (!texture.SelectBestViewsForVirtualFaces(
|
||||
virtualFaceMap, 1, fOutlierThreshold,
|
||||
fRatioDataSmoothness, nIgnoreMaskLabel, views))
|
||||
return false;
|
||||
|
||||
// 直接光栅化(不需要生成图集)
|
||||
Mesh::Image8U3Arr textures;
|
||||
if (!texture.RasterizeVirtualFaces(
|
||||
virtualFaceMap, texture.faceViews, texture.faceViewWeights,
|
||||
nTextureSizeMultiple, colEmpty, textures))
|
||||
return false;
|
||||
// ✅ 4. RC 风格光栅化(Affine + Homography 混合)
|
||||
Mesh::Image8U3Arr textures;
|
||||
if (!texture.RasterizeVirtualFaces(
|
||||
virtualFaceMap, texture.faceViews,
|
||||
texture.faceViewWeights,
|
||||
nTextureSizeMultiple, colEmpty, textures))
|
||||
return false;
|
||||
|
||||
mesh.texturesDiffuse = std::move(textures);
|
||||
DEBUG_EXTRA("Existing UV texturing completed: %u faces (%s)",
|
||||
mesh.faces.size(), TD_TIMER_GET_FMT().c_str());
|
||||
return true;
|
||||
|
||||
mesh.texturesDiffuse = std::move(textures);
|
||||
DEBUG_EXTRA("Existing UV texturing completed: %u faces (%s)", mesh.faces.size(), TD_TIMER_GET_FMT().c_str());
|
||||
return true; // 直接返回,不走后面的图集逻辑
|
||||
} else {
|
||||
// 3. 通用虚拟面模式(无预计算UV)
|
||||
if (!texture.FaceViewSelectionWithVirtualFaces(
|
||||
|
||||
Reference in new issue
Block a user