openFrameworks
openFrameworks copied to clipboard
ofTrueTypeFont snaps to pixels (no subpixel aliasing)
It looks like ofTrueTypeFont
doesn't render properly unless it's drawn at an integer pixel position due to subpixel aliasing. This makes doing effects with moving text look clunky and low-framerate.
I posted a question about this on the forum, and @ofTheo mentioned rendering it to an ofFbo
and then drawing it, which works great as a solution. As this is something I do frequently with text, I was considering adding functionality to render to an FBO first as a method to ofTrueTypeFont
, or perhaps even making it the default drawing method.
But first, I wanted to get a sense of whether there's a reason this hasn't been finished already. I'm more than happy to write the code myself and send it a PR.
Here is the thread in the forum which this came from. https://forum.openframeworks.cc/t/oftruetypefont-looks-clunky-like-slow-framerate-while-translating/23753/6
any reason we shouldn't use this type of approach to rendering the text out ( minus the translate ) vs the current one?
//do this once
ofVboMesh mesh;
mesh = font.getStringMesh("sentence",0,0);
// draw
ofTranslate(x, y);
font.getFontTexture().bind();
mesh.draw();
font.getFontTexture().unbind();
that's more or less what's happening behind the scenes in ofTruetypefont the problem is not how we do the translation but how the ttf are rendered. for this to work we would need subpixel rendering which works by loading the fonts as rgb instead of grayscale and then using a shader to do the antialiasing depending on the fraction of the position and the rgb value of the pixels.
it's like using the rgb components of each pixel as if they were small pixels themselves https://en.wikipedia.org/wiki/Subpixel_rendering
ahh - yeah - I just made this following test:
ofTrueTypeFont ttf;
ofVboMesh mesh;
ofFbo fboText;
ofRectangle textBounds;
//--------------------------------------------------------------
void ofApp::setup(){
ttf.load("arial", 20);
ofRectangle textBounds = ttf.getStringBoundingBox("hello slow moving text - as an fbo", 0, 0);
fboText.allocate(textBounds.getWidth()+2, textBounds.getHeight()+2);
}
//--------------------------------------------------------------
void ofApp::update(){
}
//--------------------------------------------------------------
void ofApp::draw(){
float x = ofGetElapsedTimef()*3.09;
ttf.drawString("hello slow moving text - drawString", x, 200);
mesh = ttf.getStringMesh("hello slow moving text - as a mesh", 0, 0);
ofPushMatrix();
ofTranslate(x, 220);
ttf.getFontTexture().bind();
mesh.draw();
ttf.getFontTexture().unbind();
ofPopMatrix();
fboText.begin();
ofClear(255, 255, 255, 0);
ttf.drawString("hello slow moving text - as an fbo", 0, ttf.getStringBoundingBox("H", 0, 0).getHeight());
fboText.end();
fboText.draw(x, 220);
}
and only the fbo approach resulted in smooth slow movement across the screen. . Would it be worth looking into non shader based ways to get the font into a more standard RGBA texture? Could you see any low cost way that we could do it without using an FBO?
huh - so I can get the text to not jump between pixels with the ofMesh / bind approach if I comment out these GL_NEAREST lines:
//if(bAntiAliased && fontSize>20){
texAtlas.setTextureMinMagFilter(GL_LINEAR,GL_LINEAR);
//}else{
// texAtlas.setTextureMinMagFilter(GL_NEAREST,GL_NEAREST);
//}
in: https://github.com/openframeworks/openFrameworks/blob/master/libs/openFrameworks/graphics/ofTrueTypeFont.cpp#L937
The first ttf.drawString call still jumps, but doing:
mesh = ttf.getStringMesh("hello slow moving text - as a mesh", 0, 0);
ofPushMatrix();
ofTranslate(x, 220);
ttf.getFontTexture().bind();
mesh.draw();
ttf.getFontTexture().unbind();
ofPopMatrix();
results in smooth movement. Doing it without the translate however ( and push an pop calls ), results in non smooth movement.
Not really sure where this leaves us :) I can see why we don't do this internally ( push / pop would add overhead ) Is it worth exploring this as an alternate rendering option where smooth motion is preferable to performance?
if it works with translate but not with the mesh coordinates then there might be a bug in the implementation. in the end doing translate is just multiplying the position of each vertex by a matrix so they'll end up in the same position as setting them directly in the vertices.
there might be some cast to int or similar in the calculations. but in any case as long as there's no subpixel antialiasing there will be a pixel jump since antialiasing in opengl doesn't work for textures and the precalculated antialiasing won't work in subpixel as it is now
yeah it does work with that last code snippet with no pixel jumping ( at least none that I can perceive ), whereas the drawString command you clearly see the pixels jump.
maybe there is a cast to int somewhere? it makes sense as aligning on the pixel boundary will probably make the font look crisper, when drawing non moving text.
here is the full code for reference the second two approaches are smooth, first one jumps:
ofTrueTypeFont ttf;
ofVboMesh mesh;
ofFbo fboText;
ofRectangle textBounds;
//--------------------------------------------------------------
void ofApp::setup(){
ttf.load("arial", 24);
ofRectangle textBounds = ttf.getStringBoundingBox("hello slow moving text - as an fbo", 0, 0);
fboText.allocate(textBounds.getWidth()+2, textBounds.getHeight()+2);
}
//--------------------------------------------------------------
void ofApp::update(){
}
//--------------------------------------------------------------
void ofApp::draw(){
float x = ofGetElapsedTimef()*3.09;
ttf.drawString("hello slow moving text - drawString", x, 200);
mesh = ttf.getStringMesh("hello slow moving text - as a mesh", 0, 0);
ofPushMatrix();
ofTranslate(x, 220);
ttf.getFontTexture().bind();
mesh.draw();
ttf.getFontTexture().unbind();
ofPopMatrix();
fboText.begin();
ofClear(255, 255, 255, 0);
ttf.drawString("hello slow moving text - as an fbo", 0, ttf.getStringBoundingBox("H", 0, 0).getHeight());
fboText.end();
fboText.draw(x, 220);
}
Hey guys -- thanks for looking into this! I'm not entirely sure where this leaves us; I'm still trying to grok the different methods (VBO vs FBO vs push/pop and translate) and their relative benefits. I'm going to continue to poke around and will add to this if I find anything.
Not sure if related but OF uses an int to get kerning pairs. I think (not sure) kerning returned is already proportional to the font size, so it makes sense to be a float
int ofTrueTypeFont::getKerning(uint32_t leftC, uint32_t rightC) const
in fact I've noticed lots of glyph properties are stored as long, but it is a mistake Freetype itself store some values as a fixed point 26.6 in a long, but in OF values were being converted (bit shifted) and assigned to a long so I'm changing that in a PR
yeah it does work with that last code snippet with no pixel jumping ( at least none that I can perceive ), whereas the drawString command you clearly see the pixels jump.
@ofTheo, I've just tested here with the code before and after the PR for fixing integer coordinates and it seems your test is running as smooth in drawString and using a mesh. they are not "bumping" anymore, but still for some reason fbo is still a little better.
to use the last commit before the PR:
git checkout 4caad2a1ce9dd64a7c53130a3b2032a88d42c12c
there is still a little "vibration" if you look at the "i" and "l" characters but I think this is something related to mesh drawing. maybe this issue can be closed as complete by https://github.com/openframeworks/openFrameworks/pull/7065
EDIT: yes the only residual effect is this rasterization "wobbling" described here https://stackoverflow.com/questions/60572538/opengl-vertices-jitter-when-moving-2d-scene so this one can be closed
Thanks @dimitre !!! 🙌
I still have this issue. Very low-quality text render. It seems to be a very old topic, but I can't find a solution. I'm using two fonts but both show similar problems.
void Button::setup(int x, int y, int size_x, int size_y, float round) { info.load("avenir.ttf", 10, true, true); info.setLineHeight(18.0f); buttonPos = glm::vec2(x, y); widthHeight = glm::vec2(size_x, size_y); rounded = round; button = ofRectangle(buttonPos.x, buttonPos.y, widthHeight.x, widthHeight.y); }
` void Button::draw(){
if(showId) info.drawString(to_string(buttonId), textPos.x, textPos.y);
if(edit) {
ofFill();
ofSetColor(255,0,0);
ofDrawCircle(buttonPos.x, buttonPos.y, 1);
}
ofColor fillColor;
if (onHoverCheck && clicked) {
fillColor = buttonClicked;
} else if (onHoverCheck) {
fillColor = hoverColor;
} else {
fillColor = notClicked;
}
// Create path for button (fill and border)
ofPath buttonPath;
buttonPath.setFilled(true);
buttonPath.setFillColor(fillColor);
buttonPath.setStrokeWidth(0.0);
buttonPath.setStrokeColor(ofColor(50)); // Border color: Black with 39% opacity
// Add rounded rectangle to the path
buttonPath.rectRounded(button.x, button.y, button.width, button.height, rounded);
// Draw the button (fill and border)
buttonPath.draw();
// Draw the rotated text
ofPushMatrix(); // Save the current coordinate system
ofTranslate(textPos.x, textPos.y); // Move to the center of the button
ofRotateDeg(-90); // Rotate the coordinate system (change angle as needed)
ofTranslate(-textCenter.x, textCenter.y); // Adjust the position to center the text
ofSetColor(infoColor);
//info.drawString(note, 0, 0); // Draw at the adjusted position
info.getFontTexture().bind();
mesh.draw();
info.getFontTexture().unbind();
// fboText.begin();
// ofClear(255, 255, 255, 0);
// info.drawString(note, 0, info.getStringBoundingBox("H", 0, 0).getHeight());
// fboText.end();
// fboText.draw(0, -8);
ofPopMatrix(); // Restore the previous coordinate system
}`