diff --git a/languages/dust3d_zh_CN.ts b/languages/dust3d_zh_CN.ts
index 3bbe2c52..f9dca8b7 100644
--- a/languages/dust3d_zh_CN.ts
+++ b/languages/dust3d_zh_CN.ts
@@ -747,6 +747,10 @@ Tips:
映像缩放比
+
+
+ 空心
+
PoseEditWidget
diff --git a/src/document.cpp b/src/document.cpp
index 7d458d88..ee0dbed6 100644
--- a/src/document.cpp
+++ b/src/document.cpp
@@ -1069,6 +1069,8 @@ void Document::toSnapshot(Snapshot *snapshot, const std::set &limitNodeId
part["deformMapImageId"] = partIt.second.deformMapImageId.toString();
if (partIt.second.deformMapScaleAdjusted())
part["deformMapScale"] = QString::number(partIt.second.deformMapScale);
+ if (partIt.second.hollowThicknessAdjusted())
+ part["hollowThickness"] = QString::number(partIt.second.hollowThickness);
if (!partIt.second.name.isEmpty())
part["name"] = partIt.second.name;
if (partIt.second.materialAdjusted())
@@ -1367,6 +1369,9 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste)
const auto &deformMapScaleIt = partKv.second.find("deformMapScale");
if (deformMapScaleIt != partKv.second.end())
part.deformMapScale = deformMapScaleIt->second.toFloat();
+ const auto &hollowThicknessIt = partKv.second.find("hollowThickness");
+ if (hollowThicknessIt != partKv.second.end())
+ part.hollowThickness = hollowThicknessIt->second.toFloat();
const auto &materialIdIt = partKv.second.find("materialId");
if (materialIdIt != partKv.second.end())
part.materialId = oldNewIdMap[QUuid(materialIdIt->second)];
@@ -2713,6 +2718,21 @@ void Document::setPartColorSolubility(QUuid partId, float solubility)
emit skeletonChanged();
}
+void Document::setPartHollowThickness(QUuid partId, float hollowThickness)
+{
+ auto part = partMap.find(partId);
+ if (part == partMap.end()) {
+ qDebug() << "Part not found:" << partId;
+ return;
+ }
+ if (qFuzzyCompare(part->second.hollowThickness, hollowThickness))
+ return;
+ part->second.hollowThickness = hollowThickness;
+ part->second.dirty = true;
+ emit partHollowThicknessChanged(partId);
+ emit skeletonChanged();
+}
+
void Document::setPartCutRotation(QUuid partId, float cutRotation)
{
auto part = partMap.find(partId);
diff --git a/src/document.h b/src/document.h
index 0b1eabc8..dc05289c 100644
--- a/src/document.h
+++ b/src/document.h
@@ -422,6 +422,7 @@ signals:
void partChamferStateChanged(QUuid partId);
void partTargetChanged(QUuid partId);
void partColorSolubilityChanged(QUuid partId);
+ void partHollowThicknessChanged(QUuid partId);
void componentCombineModeChanged(QUuid componentId);
void cleanup();
void cleanupScript();
@@ -609,6 +610,7 @@ public slots:
void setPartChamferState(QUuid partId, bool chamfered);
void setPartTarget(QUuid partId, PartTarget target);
void setPartColorSolubility(QUuid partId, float solubility);
+ void setPartHollowThickness(QUuid partId, float hollowThickness);
void setComponentCombineMode(QUuid componentId, CombineMode combineMode);
void moveComponentUp(QUuid componentId);
void moveComponentDown(QUuid componentId);
diff --git a/src/meshgenerator.cpp b/src/meshgenerator.cpp
index 9ced6701..228f9a86 100644
--- a/src/meshgenerator.cpp
+++ b/src/meshgenerator.cpp
@@ -338,6 +338,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt
float deformThickness = 1.0;
float deformWidth = 1.0;
float cutRotation = 0.0;
+ float hollowThickness = 0.0;
auto target = PartTargetFromString(valueOfKeyInMapOrEmpty(part, "target").toUtf8().constData());
auto base = PartBaseFromString(valueOfKeyInMapOrEmpty(part, "base").toUtf8().constData());
@@ -352,6 +353,11 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt
cutRotation = cutRotationString.toFloat();
}
+ QString hollowThicknessString = valueOfKeyInMapOrEmpty(part, "hollowThickness");
+ if (!hollowThicknessString.isEmpty()) {
+ hollowThickness = hollowThicknessString.toFloat();
+ }
+
QString thicknessString = valueOfKeyInMapOrEmpty(part, "deformThickness");
if (!thicknessString.isEmpty()) {
deformThickness = thicknessString.toFloat();
@@ -559,6 +565,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt
builder->setDeformThickness(deformThickness);
builder->setDeformWidth(deformWidth);
builder->setDeformMapScale(deformMapScale);
+ builder->setHollowThickness(hollowThickness);
if (nullptr != deformImage)
builder->setDeformMapImage(deformImage);
if (PartBase::YZ == base) {
diff --git a/src/partwidget.cpp b/src/partwidget.cpp
index fddf1a28..cd8a9e2c 100644
--- a/src/partwidget.cpp
+++ b/src/partwidget.cpp
@@ -171,6 +171,7 @@ PartWidget::PartWidget(const Document *document, QUuid partId) :
connect(this, &PartWidget::setPartColorState, m_document, &Document::setPartColorState);
connect(this, &PartWidget::setPartMaterialId, m_document, &Document::setPartMaterialId);
connect(this, &PartWidget::setPartColorSolubility, m_document, &Document::setPartColorSolubility);
+ connect(this, &PartWidget::setPartHollowThickness, m_document, &Document::setPartHollowThickness);
connect(this, &PartWidget::checkPart, m_document, &Document::checkPart);
connect(this, &PartWidget::enableBackgroundBlur, m_document, &Document::enableBackgroundBlur);
connect(this, &PartWidget::disableBackgroundBlur, m_document, &Document::disableBackgroundBlur);
@@ -465,6 +466,28 @@ void PartWidget::showCutRotationSettingPopup(const QPoint &pos)
rotationLayout->addWidget(rotationEraser);
rotationLayout->addWidget(rotationWidget);
+ FloatNumberWidget *hollowThicknessWidget = new FloatNumberWidget;
+ hollowThicknessWidget->setItemName(tr("Hollow"));
+ hollowThicknessWidget->setRange(0.0, 1.0);
+ hollowThicknessWidget->setValue(part->hollowThickness);
+
+ connect(hollowThicknessWidget, &FloatNumberWidget::valueChanged, [=](float value) {
+ emit setPartHollowThickness(m_partId, value);
+ emit groupOperationAdded();
+ });
+
+ QPushButton *hollowThicknessEraser = new QPushButton(QChar(fa::eraser));
+ initToolButton(hollowThicknessEraser);
+
+ connect(hollowThicknessEraser, &QPushButton::clicked, [=]() {
+ hollowThicknessWidget->setValue(0.0);
+ emit groupOperationAdded();
+ });
+
+ QHBoxLayout *hollowThicknessLayout = new QHBoxLayout;
+ hollowThicknessLayout->addWidget(hollowThicknessEraser);
+ hollowThicknessLayout->addWidget(hollowThicknessWidget);
+
QHBoxLayout *standardFacesLayout = new QHBoxLayout;
QPushButton *buttons[(int)CutFace::Count] = {0};
@@ -525,6 +548,7 @@ void PartWidget::showCutRotationSettingPopup(const QPoint &pos)
QVBoxLayout *popupLayout = new QVBoxLayout;
popupLayout->addLayout(rotationLayout);
+ popupLayout->addLayout(hollowThicknessLayout);
popupLayout->addSpacing(10);
popupLayout->addLayout(standardFacesLayout);
popupLayout->addWidget(cutFaceListWidget);
diff --git a/src/partwidget.h b/src/partwidget.h
index 0ca439d4..6244ebc8 100644
--- a/src/partwidget.h
+++ b/src/partwidget.h
@@ -28,6 +28,7 @@ signals:
void setPartCutFaceLinkedId(QUuid partId, QUuid linkedId);
void setPartMaterialId(QUuid partId, QUuid materialId);
void setPartColorSolubility(QUuid partId, float colorSolubility);
+ void setPartHollowThickness(QUuid partId, float hollowThickness);
void movePartUp(QUuid partId);
void movePartDown(QUuid partId);
void movePartToTop(QUuid partId);
diff --git a/src/skeletondocument.h b/src/skeletondocument.h
index 61cb91f5..a845c13e 100644
--- a/src/skeletondocument.h
+++ b/src/skeletondocument.h
@@ -136,6 +136,7 @@ public:
float colorSolubility;
float deformMapScale;
QUuid deformMapImageId;
+ float hollowThickness;
SkeletonPart(const QUuid &withId=QUuid()) :
visible(true),
locked(false),
@@ -155,7 +156,8 @@ public:
cutFace(CutFace::Quad),
target(PartTarget::Model),
colorSolubility(0.0),
- deformMapScale(1.0)
+ deformMapScale(1.0),
+ hollowThickness(0.0)
{
id = withId.isNull() ? QUuid::createUuid() : withId;
}
@@ -225,6 +227,10 @@ public:
{
return fabs(cutRotation - 0.0) >= 0.01;
}
+ bool hollowThicknessAdjusted() const
+ {
+ return fabs(hollowThickness - 0.0) >= 0.01;
+ }
bool cutFaceAdjusted() const
{
return cutFace != CutFace::Quad;
diff --git a/thirdparty/nodemesh/nodemesh/builder.cpp b/thirdparty/nodemesh/nodemesh/builder.cpp
index c54a8958..a48effeb 100644
--- a/thirdparty/nodemesh/nodemesh/builder.cpp
+++ b/thirdparty/nodemesh/nodemesh/builder.cpp
@@ -422,6 +422,7 @@ bool Builder::build()
applyWeld();
applyDeform();
+ finalizeHollow();
return succeed;
}
@@ -609,7 +610,11 @@ bool Builder::generateCutsForNode(size_t nodeIndex)
node.hasAdjustableCutFace = true;
std::vector vertices;
insertCutVertices(cut, vertices, nodeIndex, cutNormal, cutFlipped);
- m_generatedFaces.push_back(vertices);
+ if (qFuzzyIsNull(m_hollowThickness)) {
+ m_generatedFaces.push_back(vertices);
+ } else {
+ m_endCuts.push_back(vertices);
+ }
m_edges[node.edges[0]].cuts.push_back({vertices, -cutNormal});
} else if (2 == neighborsCount) {
QVector3D cutNormal = node.cutNormal;
@@ -1014,6 +1019,11 @@ void Builder::setDeformMapImage(const QImage *image)
m_deformMapImage = image;
}
+void Builder::setHollowThickness(float hollowThickness)
+{
+ m_hollowThickness = hollowThickness;
+}
+
void Builder::setDeformMapScale(float scale)
{
m_deformMapScale = scale;
@@ -1027,6 +1037,46 @@ QVector3D Builder::calculateDeformPosition(const QVector3D &vertexPosition, cons
return vertexPosition + (scaledProjct - projectRayOnRevisedNormal);
}
+void Builder::finalizeHollow()
+{
+ if (qFuzzyIsNull(m_hollowThickness))
+ return;
+
+ size_t startVertexIndex = m_generatedVertices.size();
+ for (size_t i = 0; i < startVertexIndex; ++i) {
+ const auto &position = m_generatedVertices[i];
+ const auto &node = m_nodes[m_generatedVerticesSourceNodeIndices[i]];
+ auto ray = position - node.position;
+
+ auto newPosition = position - ray * m_hollowThickness;
+ m_generatedVertices.push_back(newPosition);
+ m_generatedVerticesCutDirects.push_back(m_generatedVerticesCutDirects[i]);
+ m_generatedVerticesSourceNodeIndices.push_back(m_generatedVerticesSourceNodeIndices[i]);
+ m_generatedVerticesInfos.push_back(m_generatedVerticesInfos[i]);
+ }
+
+ size_t oldFaceNum = m_generatedFaces.size();
+ for (size_t i = 0; i < oldFaceNum; ++i) {
+ auto newFace = m_generatedFaces[i];
+ std::reverse(newFace.begin(), newFace.end());
+ for (auto &it: newFace)
+ it += startVertexIndex;
+ m_generatedFaces.push_back(newFace);
+ }
+
+ for (const auto &cut: m_endCuts) {
+ for (size_t i = 0; i < cut.size(); ++i) {
+ size_t j = (i + 1) % cut.size();
+ std::vector quad;
+ quad.push_back(cut[i]);
+ quad.push_back(cut[j]);
+ quad.push_back(startVertexIndex + cut[j]);
+ quad.push_back(startVertexIndex + cut[i]);
+ m_generatedFaces.push_back(quad);
+ }
+ }
+}
+
void Builder::applyDeform()
{
for (size_t i = 0; i < m_generatedVertices.size(); ++i) {
diff --git a/thirdparty/nodemesh/nodemesh/builder.h b/thirdparty/nodemesh/nodemesh/builder.h
index c2f5dbc4..1f63c258 100644
--- a/thirdparty/nodemesh/nodemesh/builder.h
+++ b/thirdparty/nodemesh/nodemesh/builder.h
@@ -31,6 +31,7 @@ public:
void setDeformWidth(float width);
void setDeformMapImage(const QImage *image);
void setDeformMapScale(float scale);
+ void setHollowThickness(float hollowThickness);
void enableBaseNormalOnX(bool enabled);
void enableBaseNormalOnY(bool enabled);
void enableBaseNormalOnZ(bool enabled);
@@ -133,6 +134,8 @@ private:
bool m_baseNormalAverageEnabled = false;
const QImage *m_deformMapImage = nullptr;
float m_deformMapScale = 0.0;
+ float m_hollowThickness = 0.2;
+ std::vector> m_endCuts;
void sortNodeIndices();
void prepareNode(size_t nodeIndex);
@@ -168,6 +171,7 @@ private:
void stitchEdgeCuts();
void applyWeld();
void applyDeform();
+ void finalizeHollow();
QVector3D calculateDeformPosition(const QVector3D &vertexPosition, const QVector3D &ray, const QVector3D &deformNormal, float deformFactor);
bool swallowEdgeForNode(size_t nodeIndex, size_t edgeOrder);
static QVector3D calculateBaseNormalFromTraverseDirection(const QVector3D &traverseDirection);