Uniform grid atlas completed

This commit is contained in:
Chlumsky 2024-03-09 23:03:56 +01:00
parent 0f48aaa727
commit 37d185878d
4 changed files with 114 additions and 75 deletions

View File

@ -7,14 +7,14 @@ namespace msdf_atlas {
static int floorPOT(int x) { static int floorPOT(int x) {
int y = 1; int y = 1;
while (x >= y) while (x >= y && y<<1)
y <<= 1; y <<= 1;
return y>>1; return y>>1;
} }
static int ceilPOT(int x) { static int ceilPOT(int x) {
int y = 1; int y = 1;
while (x > y) while (x > y && y<<1)
y <<= 1; y <<= 1;
return y; return y;
} }
@ -103,7 +103,8 @@ GridAtlasPacker::GridAtlasPacker() :
miterLimit(0), miterLimit(0),
pxAlignOriginX(false), pxAlignOriginY(false), pxAlignOriginX(false), pxAlignOriginY(false),
scaleMaximizationTolerance(.001), scaleMaximizationTolerance(.001),
alignedColumnsBias(.125) alignedColumnsBias(.125),
cutoff(false)
{ } { }
msdfgen::Shape::Bounds GridAtlasPacker::getMaxBounds(double &maxWidth, double &maxHeight, GlyphGeometry *glyphs, int count, double scale, double range) const { msdfgen::Shape::Bounds GridAtlasPacker::getMaxBounds(double &maxWidth, double &maxHeight, GlyphGeometry *glyphs, int count, double scale, double range) const {
@ -176,12 +177,12 @@ double GridAtlasPacker::scaleToFit(GlyphGeometry *glyphs, int count, int cellWid
return minScale; return minScale;
} }
// TODO tame spaghetti code // Can this spaghetti code be simplified?
// Idea: Maybe it could be rewritten into a while (not all properties deduced) cycle, and compute one value in each iteration
int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) { int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
if (!count) if (!count)
return 0; return 0;
bool cellHeightFinal = cellHeight > 0; GridAtlasPacker initial(*this);
bool explicitRows = rows > 0;
int cellCount = 0; int cellCount = 0;
if (columns > 0 && rows > 0) if (columns > 0 && rows > 0)
cellCount = columns*rows; cellCount = columns*rows;
@ -201,29 +202,23 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
} }
} }
bool dimensionsChanged = false; if (width < 0 && cellWidth > 0 && columns > 0)
if (width < 0 && cellWidth > 0 && columns > 0) { width = columns*cellWidth;
width = columns*cellWidth-padding; if (height < 0 && cellHeight > 0 && rows > 0)
dimensionsChanged = true; height = rows*cellHeight;
} if (width != initial.width || height != initial.height)
if (height < 0 && cellHeight > 0 && rows > 0) {
height = rows*cellHeight-padding;
dimensionsChanged = true;
}
if (dimensionsChanged)
raiseToConstraint(width, height, dimensionsConstraint); raiseToConstraint(width, height, dimensionsConstraint);
dimensionsChanged = false; if (cellWidth < 0 && width > 0 && columns > 0)
if (cellWidth < 0 && width > 0 && columns > 0) {
cellWidth = (width+padding)/columns; cellWidth = (width+padding)/columns;
dimensionsChanged = true; if (cellHeight < 0 && height > 0 && rows > 0)
}
if (cellHeight < 0 && height > 0 && rows > 0) {
cellHeight = (height+padding)/rows; cellHeight = (height+padding)/rows;
dimensionsChanged = true; if (cellWidth != initial.cellWidth || cellHeight != initial.cellHeight) {
} bool positiveCellWidth = cellWidth > 0, positiveCellHeight = cellHeight > 0;
if (dimensionsChanged)
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
if ((cellWidth == 0 && positiveCellWidth) || (cellHeight == 0 && positiveCellHeight))
return -1;
}
if ((cellWidth > 0 && cellWidth-padding-1 <= pxRange) || (cellHeight > 0 && cellHeight-padding-1 <= pxRange)) // cells definitely too small if ((cellWidth > 0 && cellWidth-padding-1 <= pxRange) || (cellHeight > 0 && cellHeight-padding-1 <= pxRange)) // cells definitely too small
return -1; return -1;
@ -238,8 +233,11 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
if (cellWidth > 0 || cellHeight > 0) { if (cellWidth > 0 || cellHeight > 0) {
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight); scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale) if (scale < minScale) {
return -1; scale = minScale;
cutoff = true;
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, unitRange+pxRange/scale);
}
} }
else if (width > 0 && height > 0) { else if (width > 0 && height > 0) {
@ -250,34 +248,33 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
int rows = (cellCount+cols-1)/cols; int rows = (cellCount+cols-1)/cols;
int tWidth = (width+padding)/cols; int tWidth = (width+padding)/cols;
int tHeight = (height+padding)/rows; int tHeight = (height+padding)/rows;
if (!(tWidth > 0 && tHeight > 0))
continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint); lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
double curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight); if (tWidth > 0 && tHeight > 0) {
if (curScale > scale) { double curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight);
scale = curScale; if (curScale > scale) {
bestCols = cols; scale = curScale;
bestCols = cols;
}
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
} }
if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedScale = curScale;
bestAlignedCols = cols;
}
cols = (cellCount+q-1)/q; cols = (cellCount+q-1)/q;
rows = (cellCount+cols-1)/cols; rows = (cellCount+cols-1)/cols;
tWidth = (width+padding)/cols; tWidth = (width+padding)/cols;
tHeight = (height+padding)/rows; tHeight = (height+padding)/rows;
if (!(tWidth > 0 && tHeight > 0))
continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint); lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight); if (tWidth > 0 && tHeight > 0) {
if (curScale > scale) { double curScale = scaleToFit(glyphs, count, tWidth, tHeight, maxBounds, maxWidth, maxHeight);
scale = curScale; if (curScale > scale) {
bestCols = cols; scale = curScale;
} bestCols = cols;
if (cols*tWidth == width && curScale > bestAlignedScale) { }
bestAlignedScale = curScale; if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedCols = cols; bestAlignedScale = curScale;
bestAlignedCols = cols;
}
} }
} }
if (!bestCols) if (!bestCols)
@ -294,9 +291,11 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
cellHeight = (height+padding)/rows; cellHeight = (height+padding)/rows;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight); scale = scaleToFit(glyphs, count, cellWidth, cellHeight, maxBounds, maxWidth, maxHeight);
if (scale < minScale)
scale = -1;
} }
else { if (scale <= 0) {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, minScale, unitRange+pxRange/minScale); maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, minScale, unitRange+pxRange/minScale);
cellWidth = (int) ceil(maxWidth)+padding+1; cellWidth = (int) ceil(maxWidth)+padding+1;
cellHeight = (int) ceil(maxHeight)+padding+1; cellHeight = (int) ceil(maxHeight)+padding+1;
@ -306,8 +305,14 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale = minScale, unitRange+pxRange/minScale); maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale = minScale, unitRange+pxRange/minScale);
} }
if (!explicitRows && !cellHeightFinal) if (initial.rows < 0 && initial.cellHeight < 0) {
cellHeight = (int) ceil(maxHeight)+padding+1; int optimalCellWidth = cellWidth, optimalCellHeight = (int) ceil(maxHeight)+padding+1;
raiseToConstraint(optimalCellWidth, optimalCellHeight, cellDimensionsConstraint);
if (optimalCellHeight < cellHeight && optimalCellWidth <= cellWidth) {
cellWidth = optimalCellWidth;
cellHeight = optimalCellHeight;
}
}
} else { } else {
@ -332,8 +337,10 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
scale = std::min(hScale, vScale); scale = std::min(hScale, vScale);
else else
scale = hScale+vScale; scale = hScale+vScale;
if (scale < minScale) if (scale < minScale) {
return -1; scale = minScale;
cutoff = true;
}
} }
else if (width > 0 && height > 0) { else if (width > 0 && height > 0) {
@ -344,19 +351,19 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
int rows = (cellCount+cols-1)/cols; int rows = (cellCount+cols-1)/cols;
int tWidth = (width+padding)/cols; int tWidth = (width+padding)/cols;
int tHeight = (height+padding)/rows; int tHeight = (height+padding)/rows;
if (!(tWidth > 0 && tHeight > 0))
continue;
lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint); lowerToConstraint(tWidth, tHeight, cellDimensionsConstraint);
hScale = (tWidth-hSlack-padding-pxRange)/maxWidth; if (tWidth > 0 && tHeight > 0) {
vScale = (tHeight-vSlack-padding-pxRange)/maxHeight; hScale = (tWidth-hSlack-padding-1-pxRange)/maxWidth;
double curScale = std::min(hScale, vScale); vScale = (tHeight-vSlack-padding-1-pxRange)/maxHeight;
if (curScale > scale) { double curScale = std::min(hScale, vScale);
scale = curScale; if (curScale > scale) {
bestCols = cols; scale = curScale;
} bestCols = cols;
if (cols*tWidth == width && curScale > bestAlignedScale) { }
bestAlignedScale = curScale; if (cols*tWidth == width && curScale > bestAlignedScale) {
bestAlignedCols = cols; bestAlignedScale = curScale;
bestAlignedCols = cols;
}
} }
} }
if (!bestCols) if (!bestCols)
@ -372,9 +379,11 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
cellWidth = (width+padding)/columns; cellWidth = (width+padding)/columns;
cellHeight = (height+padding)/rows; cellHeight = (height+padding)/rows;
lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); lowerToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
if (scale < minScale)
scale = -1;
} }
else { if (scale <= 0) {
cellWidth = (int) ceil(minScale*maxWidth+pxRange)+hSlack+padding+1; cellWidth = (int) ceil(minScale*maxWidth+pxRange)+hSlack+padding+1;
cellHeight = (int) ceil(minScale*maxHeight+pxRange)+vSlack+padding+1; cellHeight = (int) ceil(minScale*maxHeight+pxRange)+vSlack+padding+1;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
@ -383,8 +392,14 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
scale = std::min(hScale, vScale); scale = std::min(hScale, vScale);
} }
if (!explicitRows && !cellHeightFinal) if (initial.rows < 0 && initial.cellHeight < 0) {
cellHeight = (int) ceil(scale*maxHeight+pxRange)+vSlack+padding+1; int optimalCellWidth = cellWidth, optimalCellHeight = (int) ceil(scale*maxHeight+pxRange)+vSlack+padding+1;
raiseToConstraint(optimalCellWidth, optimalCellHeight, cellDimensionsConstraint);
if (optimalCellHeight < cellHeight && optimalCellWidth <= cellWidth) {
cellWidth = optimalCellWidth;
cellHeight = optimalCellHeight;
}
}
maxBounds.l *= scale, maxBounds.b *= scale; maxBounds.l *= scale, maxBounds.b *= scale;
maxBounds.r *= scale, maxBounds.t *= scale; maxBounds.r *= scale, maxBounds.t *= scale;
maxWidth *= scale, maxHeight *= scale; maxWidth *= scale, maxHeight *= scale;
@ -393,11 +408,14 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
} else { } else {
maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, unitRange+pxRange/scale); maxBounds = getMaxBounds(maxWidth, maxHeight, glyphs, count, scale, unitRange+pxRange/scale);
int optimalCellWidth = (int) ceil(maxWidth)+padding+1;
int optimalCellHeight = (int) ceil(maxHeight)+padding+1;
if (cellWidth < 0 || cellHeight < 0) { if (cellWidth < 0 || cellHeight < 0) {
cellWidth = (int) ceil(maxWidth)+padding+1; cellWidth = optimalCellWidth;
cellHeight = (int) ceil(maxHeight)+padding+1; cellHeight = optimalCellHeight;
raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint); raiseToConstraint(cellWidth, cellHeight, cellDimensionsConstraint);
} } else if (cellWidth < optimalCellWidth || cellHeight < optimalCellHeight)
cutoff = true;
} }
// Compute fixed origin // Compute fixed origin
@ -445,17 +463,27 @@ int GridAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
} }
width = columns*cellWidth, height = rows*cellHeight; width = columns*cellWidth, height = rows*cellHeight;
raiseToConstraint(width, height, dimensionsConstraint); raiseToConstraint(width, height, dimensionsConstraint);
// raiseToConstraint may have increased dimensions significantly, rerun if cell dimensions can be optimized.
if (dimensionsConstraint != DimensionsConstraint::NONE && initial.cellWidth < 0 && initial.cellHeight < 0) {
cellWidth = initial.cellWidth;
cellHeight = initial.cellHeight;
columns = initial.columns;
rows = initial.rows;
scale = initial.scale;
return pack(glyphs, count);
}
} }
if (columns < 0) { if (columns < 0) {
columns = (width+padding)/cellWidth; columns = (width+padding)/cellWidth;
rows = (cellCount+columns-1)/columns; rows = (cellCount+columns-1)/columns;
} }
if (rows*cellHeight > height)
rows = height/cellHeight;
int col = 0, row = 0; int col = 0, row = 0;
for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) { for (GlyphGeometry *glyph = glyphs, *end = glyphs+count; glyph < end; ++glyph) {
if (!glyph->isWhitespace()) { if (!glyph->isWhitespace()) {
Rectangle rect = { };
glyph->frameBox(scale, unitRange+pxRange/scale, miterLimit, cellWidth-padding, cellHeight-padding, hFixed ? &fixedX : nullptr, vFixed ? &fixedY : nullptr, pxAlignOriginX, pxAlignOriginY); glyph->frameBox(scale, unitRange+pxRange/scale, miterLimit, cellWidth-padding, cellHeight-padding, hFixed ? &fixedX : nullptr, vFixed ? &fixedY : nullptr, pxAlignOriginX, pxAlignOriginY);
glyph->placeBox(col*cellWidth, height-(row+1)*cellHeight); glyph->placeBox(col*cellWidth, height-(row+1)*cellHeight);
if (++col >= columns) { if (++col >= columns) {
@ -567,7 +595,7 @@ double GridAtlasPacker::getScale() const {
} }
double GridAtlasPacker::getPixelRange() const { double GridAtlasPacker::getPixelRange() const {
return pxRange; return pxRange+scale*unitRange;
} }
void GridAtlasPacker::getFixedOrigin(double &x, double &y) { void GridAtlasPacker::getFixedOrigin(double &x, double &y) {
@ -575,4 +603,8 @@ void GridAtlasPacker::getFixedOrigin(double &x, double &y) {
y = fixedY-.5/scale; y = fixedY-.5/scale;
} }
bool GridAtlasPacker::hasCutoff() const {
return cutoff;
}
} }

View File

@ -51,14 +51,20 @@ public:
/// Outputs the atlas's final dimensions /// Outputs the atlas's final dimensions
void getDimensions(int &width, int &height) const; void getDimensions(int &width, int &height) const;
/// Outputs the horizontal and vertical difference between the bottom left corners of consecutive grid cells
void getCellDimensions(int &width, int &height) const; void getCellDimensions(int &width, int &height) const;
/// Returns the final number of grid columns
int getColumns() const; int getColumns() const;
/// Returns the final number of grid rows
int getRows() const; int getRows() const;
/// Returns the final glyph scale /// Returns the final glyph scale
double getScale() const; double getScale() const;
/// Returns the final combined pixel range (including converted unit range) /// Returns the final combined pixel range (including converted unit range)
double getPixelRange() const; double getPixelRange() const;
/// Outputs the position of the origin within each cell, each value is only valid if the origin is fixed in the respective dimension
void getFixedOrigin(double &x, double &y); void getFixedOrigin(double &x, double &y);
/// Returns true if the explicitly constrained cell dimensions aren't large enough to fit each glyph fully
bool hasCutoff() const;
private: private:
int columns, rows; int columns, rows;
@ -77,6 +83,7 @@ private:
bool pxAlignOriginX, pxAlignOriginY; bool pxAlignOriginX, pxAlignOriginY;
double scaleMaximizationTolerance; double scaleMaximizationTolerance;
double alignedColumnsBias; double alignedColumnsBias;
bool cutoff;
static void lowerToConstraint(int &width, int &height, DimensionsConstraint constraint); static void lowerToConstraint(int &width, int &height, DimensionsConstraint constraint);
static void raiseToConstraint(int &width, int &height, DimensionsConstraint constraint); static void raiseToConstraint(int &width, int &height, DimensionsConstraint constraint);

View File

@ -116,8 +116,6 @@ int TightAtlasPacker::pack(GlyphGeometry *glyphs, int count) {
scale = packAndScale(glyphs, count); scale = packAndScale(glyphs, count);
if (scale <= 0) if (scale <= 0)
return -1; return -1;
pxRange += scale*unitRange;
unitRange = 0;
return 0; return 0;
} }
@ -174,7 +172,7 @@ double TightAtlasPacker::getScale() const {
} }
double TightAtlasPacker::getPixelRange() const { double TightAtlasPacker::getPixelRange() const {
return pxRange; return pxRange+scale*unitRange;
} }
} }

View File

@ -1123,6 +1123,8 @@ int main(int argc, const char * const *argv) {
return 1; return 1;
} }
} }
if (atlasPacker.hasCutoff())
fputs("Warning: Grid cell too constrained to fully fit all glyphs, some may be cut off!\n", stderr);
atlasPacker.getDimensions(config.width, config.height); atlasPacker.getDimensions(config.width, config.height);
if (!(config.width > 0 && config.height > 0)) if (!(config.width > 0 && config.height > 0))
ABORT("Unable to determine atlas size."); ABORT("Unable to determine atlas size.");