Add color solubility setting

If one part configured color solubility, the generated color texture seam between the neighbor parts will be gradient filled using this part's color with the specified solubility.

Demo: https://twitter.com/jeremyhu2016/status/1132159910815227904
master
Jeremy Hu 2019-05-26 08:54:24 +09:30
parent bb19489ee1
commit 938b6d5d6f
14 changed files with 201 additions and 9 deletions

View File

@ -915,6 +915,8 @@ void Document::toSnapshot(Snapshot *snapshot, const std::set<QUuid> &limitNodeId
part["dirty"] = partIt.second.dirty ? "true" : "false";
if (partIt.second.hasColor)
part["color"] = partIt.second.color.name();
if (partIt.second.colorSolubilityAdjusted())
part["colorSolubility"] = QString::number(partIt.second.colorSolubility);
if (partIt.second.deformThicknessAdjusted())
part["deformThickness"] = QString::number(partIt.second.deformThickness);
if (partIt.second.deformWidthAdjusted())
@ -1190,6 +1192,9 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste)
part.color = QColor(colorIt->second);
part.hasColor = true;
}
const auto &colorSolubilityIt = partKv.second.find("colorSolubility");
if (colorSolubilityIt != partKv.second.end())
part.colorSolubility = colorSolubilityIt->second.toFloat();
const auto &deformThicknessIt = partKv.second.find("deformThickness");
if (deformThicknessIt != partKv.second.end())
part.setDeformThickness(deformThicknessIt->second.toFloat());
@ -2311,6 +2316,21 @@ void Document::setPartTarget(QUuid partId, PartTarget target)
emit skeletonChanged();
}
void Document::setPartColorSolubility(QUuid partId, float solubility)
{
auto part = partMap.find(partId);
if (part == partMap.end()) {
qDebug() << "Part not found:" << partId;
return;
}
if (qFuzzyCompare(part->second.colorSolubility, solubility))
return;
part->second.colorSolubility = solubility;
part->second.dirty = true;
emit partColorSolubilityChanged(partId);
emit skeletonChanged();
}
void Document::setPartCutRotation(QUuid partId, float cutRotation)
{
auto part = partMap.find(partId);

View File

@ -406,6 +406,7 @@ signals:
void partMaterialIdChanged(QUuid partId);
void partChamferStateChanged(QUuid partId);
void partTargetChanged(QUuid partId);
void partColorSolubilityChanged(QUuid partId);
void componentCombineModeChanged(QUuid componentId);
void cleanup();
void originChanged();
@ -565,6 +566,7 @@ public slots:
void setPartMaterialId(QUuid partId, QUuid materialId);
void setPartChamferState(QUuid partId, bool chamfered);
void setPartTarget(QUuid partId, PartTarget target);
void setPartColorSolubility(QUuid partId, float solubility);
void setComponentCombineMode(QUuid componentId, CombineMode combineMode);
void moveComponentUp(QUuid componentId);
void moveComponentDown(QUuid componentId);

View File

@ -860,6 +860,7 @@ DocumentWindow::DocumentWindow() :
connect(m_document, &Document::partCutRotationChanged, partTreeWidget, &PartTreeWidget::partCutRotationChanged);
connect(m_document, &Document::partCutFaceChanged, partTreeWidget, &PartTreeWidget::partCutFaceChanged);
connect(m_document, &Document::partMaterialIdChanged, partTreeWidget, &PartTreeWidget::partMaterialIdChanged);
connect(m_document, &Document::partColorSolubilityChanged, partTreeWidget, &PartTreeWidget::partColorSolubilityChanged);
connect(m_document, &Document::partRemoved, partTreeWidget, &PartTreeWidget::partRemoved);
connect(m_document, &Document::cleanup, partTreeWidget, &PartTreeWidget::removeAllContent);
connect(m_document, &Document::partChecked, partTreeWidget, &PartTreeWidget::partChecked);

View File

@ -308,6 +308,11 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt
if (!materialIdString.isEmpty())
materialId = QUuid(materialIdString);
float colorSolubility = 0;
QString colorSolubilityString = valueOfKeyInMapOrEmpty(part, "colorSolubility");
if (!colorSolubilityString.isEmpty())
colorSolubility = colorSolubilityString.toFloat();
auto &partCache = m_cacheContext->parts[partIdString];
partCache.outcomeNodes.clear();
partCache.outcomeNodeVertices.clear();
@ -400,6 +405,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt
outcomeNode.radius = nodeInfo.radius;
outcomeNode.color = partColor;
outcomeNode.materialId = materialId;
outcomeNode.colorSolubility = colorSolubility;
outcomeNode.boneMark = nodeInfo.boneMark;
outcomeNode.mirroredByPartId = mirroredPartIdString;
partCache.outcomeNodes.push_back(outcomeNode);
@ -622,6 +628,10 @@ QString MeshGenerator::componentColorName(const std::map<QString, QString> *comp
return QString();
}
auto &part = findPart->second;
QString colorSolubility = valueOfKeyInMapOrEmpty(part, "colorSolubility");
if (!colorSolubility.isEmpty()) {
return QString("+");
}
QString colorName = valueOfKeyInMapOrEmpty(part, "color");
if (colorName.isEmpty())
return QString("-");
@ -682,11 +692,15 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineComponentMesh(const QString &com
// Firstly, group by combine mode
int currentGroupIndex = -1;
auto lastCombineMode = CombineMode::Count;
bool foundColorSolubilitySetting = false;
for (const auto &childIdString: valueOfKeyInMapOrEmpty(*component, "children").split(",")) {
if (childIdString.isEmpty())
continue;
const auto &child = findComponent(childIdString);
QString colorName = componentColorName(child);
if (colorName == "+") {
foundColorSolubilitySetting = true;
}
auto combineMode = componentCombineMode(child);
if (lastCombineMode != combineMode || lastCombineMode == CombineMode::Inversion) {
qDebug() << "New group[" << currentGroupIndex << "] for combine mode[" << CombineModeToString(combineMode) << "]";
@ -748,7 +762,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineComponentMesh(const QString &com
}
multipleMeshes.push_back({childMesh, CombineMode::Normal});
}
nodemesh::Combiner::Mesh *subGroupMesh = combineMultipleMeshes(multipleMeshes, false);
nodemesh::Combiner::Mesh *subGroupMesh = combineMultipleMeshes(multipleMeshes, foundColorSolubilitySetting);
if (nullptr == subGroupMesh)
continue;
groupMeshes.push_back({subGroupMesh, group.first});
@ -806,6 +820,10 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineMultipleMeshes(const std::vector
}
}
}
if (nullptr != mesh && mesh->isNull()) {
delete mesh;
mesh = nullptr;
}
return mesh;
}
@ -830,6 +848,11 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineComponentChildGroupMesh(const st
componentCache.outcomeNodes.push_back(it);
for (const auto &it: childComponentCache.outcomeNodeVertices)
componentCache.outcomeNodeVertices.push_back(it);
if (nullptr == subMesh || subMesh->isNull()) {
delete subMesh;
continue;
}
multipleMeshes.push_back({subMesh, childCombineMode});
}

View File

@ -18,6 +18,7 @@ struct OutcomeNode
QVector3D origin;
float radius = 0;
QColor color;
float colorSolubility = 0;
QUuid materialId;
QUuid mirrorFromPartId;
QUuid mirroredByPartId;

View File

@ -1003,6 +1003,17 @@ void PartTreeWidget::partMaterialIdChanged(QUuid partId)
widget->updateColorButton();
}
void PartTreeWidget::partColorSolubilityChanged(QUuid partId)
{
auto item = m_partItemMap.find(partId);
if (item == m_partItemMap.end()) {
qDebug() << "Part item not found:" << partId;
return;
}
PartWidget *widget = (PartWidget *)itemWidget(item->second, 0);
widget->updateColorButton();
}
void PartTreeWidget::partChecked(QUuid partId)
{
auto item = m_partItemMap.find(partId);

View File

@ -67,6 +67,7 @@ public slots:
void partCutRotationChanged(QUuid partId);
void partCutFaceChanged(QUuid partId);
void partMaterialIdChanged(QUuid partId);
void partColorSolubilityChanged(QUuid partId);
void partChecked(QUuid partId);
void partUnchecked(QUuid partId);
void groupChanged(QTreeWidgetItem *item, int column);

View File

@ -166,6 +166,7 @@ PartWidget::PartWidget(const Document *document, QUuid partId) :
connect(this, &PartWidget::setPartCutFaceLinkedId, m_document, &Document::setPartCutFaceLinkedId);
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::checkPart, m_document, &Document::checkPart);
connect(this, &PartWidget::enableBackgroundBlur, m_document, &Document::enableBackgroundBlur);
connect(this, &PartWidget::disableBackgroundBlur, m_document, &Document::disableBackgroundBlur);
@ -375,8 +376,31 @@ void PartWidget::showColorSettingPopup(const QPoint &pos)
}
});
FloatNumberWidget *colorSolubilityWidget = new FloatNumberWidget;
colorSolubilityWidget->setItemName(tr("Solubility"));
colorSolubilityWidget->setRange(0.0, 1.0);
colorSolubilityWidget->setValue(part->colorSolubility);
connect(colorSolubilityWidget, &FloatNumberWidget::valueChanged, [=](float value) {
emit setPartColorSolubility(m_partId, value);
emit groupOperationAdded();
});
QPushButton *colorSolubilityEraser = new QPushButton(QChar(fa::eraser));
initToolButton(colorSolubilityEraser);
connect(colorSolubilityEraser, &QPushButton::clicked, [=]() {
colorSolubilityWidget->setValue(0.0);
emit groupOperationAdded();
});
QHBoxLayout *colorSolubilityLayout = new QHBoxLayout;
colorSolubilityLayout->addWidget(colorSolubilityEraser);
colorSolubilityLayout->addWidget(colorSolubilityWidget);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(colorLayout);
mainLayout->addLayout(colorSolubilityLayout);
if (m_document->materialIdList.empty()) {
InfoLabel *infoLabel = new InfoLabel;
@ -712,7 +736,7 @@ void PartWidget::updateColorButton()
qDebug() << "Part not found:" << m_partId;
return;
}
if (part->hasColor || part->materialAdjusted())
if (part->hasColor || part->materialAdjusted() || part->colorSolubilityAdjusted())
updateButton(m_colorButton, QChar(fa::eyedropper), true);
else
updateButton(m_colorButton, QChar(fa::eyedropper), false);

View File

@ -25,6 +25,7 @@ signals:
void setPartCutFace(QUuid partId, CutFace cutFace);
void setPartCutFaceLinkedId(QUuid partId, QUuid linkedId);
void setPartMaterialId(QUuid partId, QUuid materialId);
void setPartColorSolubility(QUuid partId, float colorSolubility);
void movePartUp(QUuid partId);
void movePartDown(QUuid partId);
void movePartToTop(QUuid partId);

View File

@ -93,6 +93,7 @@ public:
QUuid cutFaceLinkedId;
QUuid materialId;
PartTarget target;
float colorSolubility;
SkeletonPart(const QUuid &withId=QUuid()) :
visible(true),
locked(false),
@ -110,7 +111,8 @@ public:
dirty(true),
cutRotation(0.0),
cutFace(CutFace::Quad),
target(PartTarget::Model)
target(PartTarget::Model),
colorSolubility(0.0)
{
id = withId.isNull() ? QUuid::createUuid() : withId;
}
@ -164,6 +166,10 @@ public:
{
return deformThicknessAdjusted() || deformWidthAdjusted();
}
bool colorSolubilityAdjusted() const
{
return fabs(colorSolubility - 0.0) >= 0.01;
}
bool cutRotationAdjusted() const
{
return fabs(cutRotation - 0.0) >= 0.01;
@ -206,6 +212,7 @@ public:
dirty = other.dirty;
materialId = other.materialId;
target = other.target;
colorSolubility = other.colorSolubility;
}
void updatePreviewMesh(MeshLoader *previewMesh)
{

View File

@ -3,6 +3,7 @@
#include <QRegion>
#include <QPolygon>
#include <QElapsedTimer>
#include <QRadialGradient>
#include "texturegenerator.h"
#include "theme.h"
#include "util.h"
@ -231,9 +232,11 @@ void TextureGenerator::generate()
std::map<QUuid, QColor> partColorMap;
std::map<std::pair<QUuid, QUuid>, const OutcomeNode *> nodeMap;
std::map<QUuid, float> partColorSolubilityMap;
for (const auto &item: m_outcome->nodes) {
nodeMap.insert({{item.partId, item.nodeId}, &item});
partColorMap.insert({item.partId, item.color});
partColorSolubilityMap.insert({item.partId, item.colorSolubility});
}
auto createImageBeginTime = countTimeConsumed.elapsed();
@ -316,6 +319,81 @@ void TextureGenerator::generate()
}
}
auto drawGradient = [&](const QUuid &partId, size_t triangleIndex, size_t firstVertexIndex, size_t secondVertexIndex,
const QUuid &neighborPartId) {
const std::vector<QVector2D> &uv = triangleVertexUvs[triangleIndex];
const auto &allRects = partUvRects.find(partId);
if (allRects == partUvRects.end()) {
qDebug() << "Found part uv rects failed";
return;
}
const auto &firstPoint = uv[firstVertexIndex];
const auto &secondPoint = uv[secondVertexIndex];
auto edgeLength = firstPoint.distanceToPoint(secondPoint);
auto middlePoint = (firstPoint + secondPoint) / 2.0;
const auto &findColor = partColorMap.find(partId);
if (findColor == partColorMap.end())
return;
const auto &findNeighborColorSolubility = partColorSolubilityMap.find(neighborPartId);
if (findNeighborColorSolubility == partColorSolubilityMap.end())
return;
if (qFuzzyIsNull(findNeighborColorSolubility->second))
return;
const auto &findNeighborColor = partColorMap.find(neighborPartId);
if (findNeighborColor == partColorMap.end())
return;
for (const auto &it: allRects->second) {
if (it.contains(firstPoint.x(), firstPoint.y()) ||
it.contains(secondPoint.x(), secondPoint.y())) {
float finalRadius = (it.width() + it.height()) * 0.5 * findNeighborColorSolubility->second;
if (finalRadius < edgeLength)
finalRadius = edgeLength;
QRadialGradient gradient(QPointF(middlePoint.x() * TextureGenerator::m_textureSize,
middlePoint.y() * TextureGenerator::m_textureSize),
finalRadius * TextureGenerator::m_textureSize);
gradient.setColorAt(0.0, findNeighborColor->second);
gradient.setColorAt(1.0, Qt::transparent);
QRectF fillTarget((middlePoint.x() - finalRadius),
(middlePoint.y() - finalRadius),
(finalRadius + finalRadius),
(finalRadius + finalRadius));
auto clippedRect = it.intersected(fillTarget);
texturePainter.fillRect(clippedRect.left() * TextureGenerator::m_textureSize,
clippedRect.top() * TextureGenerator::m_textureSize,
clippedRect.width() * TextureGenerator::m_textureSize,
clippedRect.height() * TextureGenerator::m_textureSize,
gradient);
break;
}
}
};
std::map<std::pair<size_t, size_t>, std::tuple<size_t, size_t, size_t>> halfEdgeToTriangleMap;
for (size_t i = 0; i < m_outcome->triangles.size(); ++i) {
const auto &triangleIndices = m_outcome->triangles[i];
if (triangleIndices.size() != 3) {
qDebug() << "Found invalid triangle indices";
continue;
}
for (size_t j = 0; j < triangleIndices.size(); ++j) {
size_t k = (j + 1) % triangleIndices.size();
halfEdgeToTriangleMap.insert(std::make_pair(std::make_pair(triangleIndices[j], triangleIndices[k]),
std::make_tuple(i, j, k)));
}
}
for (const auto &it: halfEdgeToTriangleMap) {
auto oppositeHalfEdge = std::make_pair(it.first.second, it.first.first);
const auto &opposite = halfEdgeToTriangleMap.find(oppositeHalfEdge);
if (opposite == halfEdgeToTriangleMap.end())
continue;
const std::pair<QUuid, QUuid> &source = triangleSourceNodes[std::get<0>(it.second)];
const std::pair<QUuid, QUuid> &oppositeSource = triangleSourceNodes[std::get<0>(opposite->second)];
if (source.first == oppositeSource.first)
continue;
drawGradient(source.first, std::get<0>(it.second), std::get<1>(it.second), std::get<2>(it.second), oppositeSource.first);
drawGradient(oppositeSource.first, std::get<0>(opposite->second), std::get<1>(opposite->second), std::get<2>(opposite->second), source.first);
}
for (auto i = 0u; i < triangleVertexUvs.size(); i++) {
QPainterPath path;
const std::vector<QVector2D> &uv = triangleVertexUvs[i];
@ -337,12 +415,12 @@ void TextureGenerator::generate()
texturePainter.drawImage(0, 0, findColorTextureResult->second);
texturePainter.setClipping(false);
} else {
auto findSourceNodeResult = nodeMap.find(source);
if (findSourceNodeResult != nodeMap.end() && nullptr != findSourceNodeResult->second) {
texturePainter.fillPath(path, QBrush(findSourceNodeResult->second->color));
} else {
texturePainter.fillPath(path, QBrush(m_defaultTextureColor));
}
//auto findSourceNodeResult = nodeMap.find(source);
//if (findSourceNodeResult != nodeMap.end() && nullptr != findSourceNodeResult->second) {
// texturePainter.fillPath(path, QBrush(findSourceNodeResult->second->color));
//} else {
// texturePainter.fillPath(path, QBrush(m_defaultTextureColor));
//}
}
// Copy normal texture if there is one
auto findNormalTextureResult = m_partNormalTextureMap.find(source.first);

View File

@ -84,5 +84,12 @@ void fetchFromCgalMesh(typename CGAL::Surface_mesh<typename Kernel::Point_3> *me
}
}
template <class Kernel>
bool isNullCgalMesh(typename CGAL::Surface_mesh<typename Kernel::Point_3> *mesh)
{
typename CGAL::Surface_mesh<typename Kernel::Point_3>::Face_range faceRage = mesh->faces();
return faceRage.begin() == faceRage.end();
}
#endif

View File

@ -47,12 +47,14 @@ Combiner::Mesh::Mesh(const std::vector<QVector3D> &vertices, const std::vector<s
}
}
m_privateData = cgalMesh;
validate();
}
Combiner::Mesh::Mesh(const Mesh &other)
{
if (other.m_privateData) {
m_privateData = new CgalMesh(*(CgalMesh *)other.m_privateData);
validate();
}
}
@ -158,4 +160,16 @@ Combiner::Mesh *Combiner::combine(const Mesh &firstMesh, const Mesh &secondMesh,
return mesh;
}
void Combiner::Mesh::validate()
{
if (nullptr == m_privateData)
return;
CgalMesh *exactMesh = (CgalMesh *)m_privateData;
if (isNullCgalMesh<CgalKernel>(exactMesh)) {
delete exactMesh;
m_privateData = nullptr;
}
}
}

View File

@ -38,6 +38,8 @@ public:
private:
void *m_privateData = nullptr;
bool m_isSelfIntersected = false;
void validate();
};
static Mesh *combine(const Mesh &firstMesh, const Mesh &secondMesh, Method method,