Explorar o código

Merge pull request #17578 from endragor/ft-outlines

Perfect FreeType-based outlines for DynamicFonts
Rémi Verschelde %!s(int64=7) %!d(string=hai) anos
pai
achega
9dbfe5dc61

+ 1 - 0
editor/output_strings.cpp

@@ -87,6 +87,7 @@ void OutputStrings::_notification(int p_what) {
 			float h_ofs = (int)h_scroll->get_value();
 			Point2 icon_ofs = Point2(0, (font_height - (int)icon_error->get_height()) / 2);
 
+			FontDrawer drawer(font, Color(1, 1, 1));
 			while (E && ofs.y < (size_height - (int)margin.y)) {
 
 				String str = E->get().text;

+ 2 - 1
scene/gui/item_list.cpp

@@ -1121,6 +1121,7 @@ void ItemList::_notification(int p_what) {
 					text_ofs += base_ofs;
 					text_ofs += items[i].rect_cache.position;
 
+					FontDrawer drawer(font, Color(1, 1, 1));
 					for (int j = 0; j < ss; j++) {
 
 						if (j == line_limit_cache[line]) {
@@ -1129,7 +1130,7 @@ void ItemList::_notification(int p_what) {
 							if (line >= max_text_lines)
 								break;
 						}
-						ofs += font->draw_char(get_canvas_item(), text_ofs + Vector2(ofs + (max_len - line_size_cache[line]) / 2, line * (font_height + line_separation)).floor(), items[i].text[j], items[i].text[j + 1], modulate);
+						ofs += drawer.draw_char(get_canvas_item(), text_ofs + Vector2(ofs + (max_len - line_size_cache[line]) / 2, line * (font_height + line_separation)).floor(), items[i].text[j], items[i].text[j + 1], modulate);
 					}
 
 					//special multiline mode

+ 7 - 5
scene/gui/label.cpp

@@ -93,6 +93,7 @@ void Label::_notification(int p_what) {
 		bool use_outline = get_constant("shadow_as_outline");
 		Point2 shadow_ofs(get_constant("shadow_offset_x"), get_constant("shadow_offset_y"));
 		int line_spacing = get_constant("line_spacing");
+		Color font_outline_modulate = get_color("font_outline_modulate");
 
 		style->draw(ci, Rect2(Point2(0, 0), get_size()));
 
@@ -150,6 +151,7 @@ void Label::_notification(int p_what) {
 
 		int line = 0;
 		int line_to = lines_skipped + (lines_visible > 0 ? lines_visible : 1);
+		FontDrawer drawer(font, font_outline_modulate);
 		while (wc) {
 			/* handle lines not meant to be drawn quickly */
 			if (line >= line_to)
@@ -244,11 +246,11 @@ void Label::_notification(int p_what) {
 								n = String::char_uppercase(c);
 							}
 
-							float move = font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + shadow_ofs, c, n, font_color_shadow);
+							float move = font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + shadow_ofs, c, n, font_color_shadow, false);
 							if (use_outline) {
-								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, shadow_ofs.y), c, n, font_color_shadow);
-								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow);
-								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow);
+								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, shadow_ofs.y), c, n, font_color_shadow, false);
+								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow, false);
+								font->draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow, false);
 							}
 							x_ofs_shadow += move;
 							chars_total_shadow++;
@@ -265,7 +267,7 @@ void Label::_notification(int p_what) {
 							n = String::char_uppercase(c);
 						}
 
-						x_ofs += font->draw_char(ci, Point2(x_ofs, y_ofs), c, n, font_color);
+						x_ofs += drawer.draw_char(ci, Point2(x_ofs, y_ofs), c, n, font_color);
 						chars_total++;
 					}
 				}

+ 4 - 3
scene/gui/line_edit.cpp

@@ -656,6 +656,7 @@ void LineEdit::_notification(int p_what) {
 			}
 
 			int caret_height = font->get_height() > y_area ? y_area : font->get_height();
+			FontDrawer drawer(font, Color(1, 1, 1));
 			while (true) {
 
 				//end of string, break!
@@ -683,7 +684,7 @@ void LineEdit::_notification(int p_what) {
 								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color);
 							}
 
-							font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color);
+							drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color);
 
 							x_ofs += im_char_width;
 							ofs++;
@@ -704,7 +705,7 @@ void LineEdit::_notification(int p_what) {
 				if (selected)
 					VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(char_width, caret_height)), selection_color);
 
-				font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, selected ? font_color_selected : font_color);
+				drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, selected ? font_color_selected : font_color);
 
 				if (char_ofs == cursor_pos && draw_caret) {
 					if (ime_text.length() == 0) {
@@ -737,7 +738,7 @@ void LineEdit::_notification(int p_what) {
 							VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color);
 						}
 
-						font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color);
+						drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color);
 
 						x_ofs += im_char_width;
 						ofs++;

+ 3 - 2
scene/gui/rich_text_label.cpp

@@ -286,6 +286,7 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 				}
 
 				rchar = 0;
+				FontDrawer drawer(font, Color(1, 1, 1));
 				while (*c) {
 
 					int end = 0;
@@ -395,9 +396,9 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 									}
 
 									if (selected) {
-										font->draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), c[i], c[i + 1], override_selected_font_color ? selection_fg : color);
+										drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), c[i], c[i + 1], override_selected_font_color ? selection_fg : color);
 									} else {
-										cw = font->draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), c[i], c[i + 1], color);
+										cw = drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), c[i], c[i + 1], color);
 									}
 								}
 

+ 6 - 5
scene/gui/text_edit.cpp

@@ -782,6 +782,7 @@ void TextEdit::_notification(int p_what) {
 			int line = cursor.line_ofs - 1;
 			// another row may be visible during smooth scrolling
 			int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0);
+			FontDrawer drawer(cache.font, Color(1, 1, 1));
 			for (int i = 0; i < draw_amount; i++) {
 
 				line++;
@@ -1040,7 +1041,7 @@ void TextEdit::_notification(int p_what) {
 
 							if (brace_open_mismatch)
 								color = cache.brace_mismatch_color;
-							cache.font->draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, ofs_y + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_selected_color : color);
+							drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, ofs_y + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_selected_color : color);
 						}
 
 						if (
@@ -1049,7 +1050,7 @@ void TextEdit::_notification(int p_what) {
 
 							if (brace_close_mismatch)
 								color = cache.brace_mismatch_color;
-							cache.font->draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, ofs_y + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_selected_color : color);
+							drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, ofs_y + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_selected_color : color);
 						}
 					}
 
@@ -1082,7 +1083,7 @@ void TextEdit::_notification(int p_what) {
 									VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color);
 								}
 
-								cache.font->draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color);
+								drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color);
 
 								char_ofs += im_char_width;
 								ofs++;
@@ -1109,7 +1110,7 @@ void TextEdit::_notification(int p_what) {
 					}
 
 					if (str[j] >= 32) {
-						int w = cache.font->draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, ofs_y + ascent), str[j], str[j + 1], in_selection && override_selected_font_color ? cache.font_selected_color : color);
+						int w = drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, ofs_y + ascent), str[j], str[j + 1], in_selection && override_selected_font_color ? cache.font_selected_color : color);
 						if (underlined) {
 							draw_rect(Rect2(char_ofs + char_margin + ofs_x, ofs_y + ascent + 2, w, 1), in_selection && override_selected_font_color ? cache.font_selected_color : color);
 						}
@@ -1158,7 +1159,7 @@ void TextEdit::_notification(int p_what) {
 								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color);
 							}
 
-							cache.font->draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color);
+							drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color);
 
 							char_ofs += im_char_width;
 							ofs++;

+ 1 - 0
scene/resources/default_theme/default_theme.cpp

@@ -415,6 +415,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 	theme->set_color("font_color", "Label", Color(1, 1, 1));
 	theme->set_color("font_color_shadow", "Label", Color(0, 0, 0, 0));
+	theme->set_color("font_outline_modulate", "Label", Color(1, 1, 1));
 
 	theme->set_constant("shadow_offset_x", "Label", 1 * scale);
 	theme->set_constant("shadow_offset_y", "Label", 1 * scale);

+ 284 - 248
scene/resources/dynamic_font.cpp

@@ -33,8 +33,12 @@
 #include "os/file_access.h"
 #include "os/os.h"
 
-bool DynamicFontData::CacheID::operator<(CacheID right) const {
+#include FT_STROKER_H
+
+#define __STDC_LIMIT_MACROS
+#include <stdint.h>
 
+bool DynamicFontData::CacheID::operator<(CacheID right) const {
 	return key < right.key;
 }
 
@@ -212,8 +216,8 @@ Error DynamicFontAtSize::_load() {
 		error = FT_Set_Pixel_Sizes(face, 0, id.size * oversampling);
 	}
 
-	ascent = (face->size->metrics.ascender >> 6) / oversampling * scale_color_font;
-	descent = (-face->size->metrics.descender >> 6) / oversampling * scale_color_font;
+	ascent = (face->size->metrics.ascender / 64.0) / oversampling * scale_color_font;
+	descent = (-face->size->metrics.descender / 64.0) / oversampling * scale_color_font;
 	linegap = 0;
 	texture_flags = 0;
 	if (id.mipmaps)
@@ -241,18 +245,11 @@ float DynamicFontAtSize::get_descent() const {
 	return descent;
 }
 
-Size2 DynamicFontAtSize::get_char_size(CharType p_char, CharType p_next, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const {
+const Pair<const DynamicFontAtSize::Character *, DynamicFontAtSize *> DynamicFontAtSize::_find_char_with_font(CharType p_char, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const {
+	const Character *chr = char_map.getptr(p_char);
+	ERR_FAIL_COND_V(!chr, (Pair<const Character *, DynamicFontAtSize *>(NULL, NULL)));
 
-	if (!valid)
-		return Size2(1, 1);
-	const_cast<DynamicFontAtSize *>(this)->_update_char(p_char);
-
-	const Character *c = char_map.getptr(p_char);
-	ERR_FAIL_COND_V(!c, Size2());
-
-	Size2 ret(0, get_height());
-
-	if (!c->found) {
+	if (!chr->found) {
 
 		//not found, try in fallbacks
 		for (int i = 0; i < p_fallbacks.size(); i++) {
@@ -262,52 +259,53 @@ Size2 DynamicFontAtSize::get_char_size(CharType p_char, CharType p_next, const V
 				continue;
 
 			fb->_update_char(p_char);
-			const Character *ch = fb->char_map.getptr(p_char);
-			ERR_CONTINUE(!ch);
+			const Character *fallback_chr = fb->char_map.getptr(p_char);
+			ERR_CONTINUE(!fallback_chr);
 
-			if (!ch->found)
+			if (!fallback_chr->found)
 				continue;
 
-			c = ch;
-			break;
+			return Pair<const Character *, DynamicFontAtSize *>(fallback_chr, fb);
 		}
-		//not found, try 0xFFFD to display 'not found'.
 
-		if (!c->found) {
-
-			const_cast<DynamicFontAtSize *>(this)->_update_char(0xFFFD);
-			c = char_map.getptr(0xFFFD);
-			ERR_FAIL_COND_V(!c, Size2());
-		}
+		//not found, try 0xFFFD to display 'not found'.
+		const_cast<DynamicFontAtSize *>(this)->_update_char(0xFFFD);
+		chr = char_map.getptr(0xFFFD);
+		ERR_FAIL_COND_V(!chr, (Pair<const Character *, DynamicFontAtSize *>(NULL, NULL)));
 	}
 
-	if (c->found) {
-		ret.x = c->advance;
-	}
+	return Pair<const Character *, DynamicFontAtSize *>(chr, const_cast<DynamicFontAtSize *>(this));
+}
+
+float DynamicFontAtSize::_get_kerning_advance(const DynamicFontAtSize *font, CharType p_char, CharType p_next) const {
+	float advance = 0.0;
 
 	if (p_next) {
 		FT_Vector delta;
-		FT_Get_Kerning(face, p_char, p_next, FT_KERNING_DEFAULT, &delta);
+		FT_Get_Kerning(font->face, p_char, p_next, FT_KERNING_DEFAULT, &delta);
+		advance = (delta.x / 64.0) / oversampling;
+	}
 
-		if (delta.x == 0) {
-			for (int i = 0; i < p_fallbacks.size(); i++) {
+	return advance;
+}
 
-				DynamicFontAtSize *fb = const_cast<DynamicFontAtSize *>(p_fallbacks[i].ptr());
-				if (!fb->valid)
-					continue;
+Size2 DynamicFontAtSize::get_char_size(CharType p_char, CharType p_next, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const {
 
-				FT_Get_Kerning(fb->face, p_char, p_next, FT_KERNING_DEFAULT, &delta);
+	if (!valid)
+		return Size2(1, 1);
+	const_cast<DynamicFontAtSize *>(this)->_update_char(p_char);
 
-				if (delta.x == 0)
-					continue;
+	Pair<const Character *, DynamicFontAtSize *> char_pair_with_font = _find_char_with_font(p_char, p_fallbacks);
+	const Character *ch = char_pair_with_font.first;
+	DynamicFontAtSize *font = char_pair_with_font.second;
+	ERR_FAIL_COND_V(!ch, Size2());
 
-				ret.x += (delta.x >> 6) / oversampling;
-				break;
-			}
-		} else {
-			ret.x += (delta.x >> 6) / oversampling;
-		}
+	Size2 ret(0, get_height());
+
+	if (ch->found) {
+		ret.x = ch->advance;
 	}
+	ret.x += _get_kerning_advance(font, p_char, p_next);
 
 	// ensures oversampled glyphs will have enough space when this value is used by clipping/wrapping algorithms
 	ret.x = Math::ceil(ret.x);
@@ -324,102 +322,42 @@ void DynamicFontAtSize::set_texture_flags(uint32_t p_flags) {
 	}
 }
 
-float DynamicFontAtSize::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const {
+float DynamicFontAtSize::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks, bool p_advance_only) const {
 
 	if (!valid)
 		return 0;
 
 	const_cast<DynamicFontAtSize *>(this)->_update_char(p_char);
 
-	const Character *c = char_map.getptr(p_char);
+	Pair<const Character *, DynamicFontAtSize *> char_pair_with_font = _find_char_with_font(p_char, p_fallbacks);
+	const Character *ch = char_pair_with_font.first;
+	DynamicFontAtSize *font = char_pair_with_font.second;
 
-	float advance = 0;
+	ERR_FAIL_COND_V(!ch, 0.0);
 
-	if (!c->found) {
-
-		//not found, try in fallbacks
-		bool used_fallback = false;
-
-		for (int i = 0; i < p_fallbacks.size(); i++) {
-
-			DynamicFontAtSize *fb = const_cast<DynamicFontAtSize *>(p_fallbacks[i].ptr());
-			if (!fb->valid)
-				continue;
+	float advance = 0.0;
 
-			fb->_update_char(p_char);
-			const Character *ch = fb->char_map.getptr(p_char);
-			ERR_CONTINUE(!ch);
+	if (ch->found) {
+		ERR_FAIL_COND_V(ch->texture_idx < -1 || ch->texture_idx >= font->textures.size(), 0);
 
-			if (!ch->found)
-				continue;
+		if (!p_advance_only && ch->texture_idx != -1) {
 			Point2 cpos = p_pos;
 			cpos.x += ch->h_align;
-			cpos.y -= fb->get_ascent();
+			cpos.y -= font->get_ascent();
 			cpos.y += ch->v_align;
-			ERR_FAIL_COND_V(ch->texture_idx < -1 || ch->texture_idx >= fb->textures.size(), 0);
-			if (ch->texture_idx != -1) {
-				Color modulate = p_modulate;
-				if (FT_HAS_COLOR(fb->face)) {
-					modulate.r = modulate.g = modulate.b = 1;
-				}
-				VisualServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, Rect2(cpos, ch->rect.size * Vector2(fb->scale_color_font, fb->scale_color_font)), fb->textures[ch->texture_idx].texture->get_rid(), ch->rect_uv, modulate, false, RID(), false);
-			}
-			advance = ch->advance;
-			used_fallback = true;
-			break;
-		}
-		//not found, try 0xFFFD to display 'not found'.
-
-		if (!used_fallback) {
-
-			const_cast<DynamicFontAtSize *>(this)->_update_char(0xFFFD);
-			c = char_map.getptr(0xFFFD);
-		}
-	}
-
-	if (c->found) {
-
-		Point2 cpos = p_pos;
-		cpos.x += c->h_align;
-		cpos.y -= get_ascent();
-		cpos.y += c->v_align;
-		ERR_FAIL_COND_V(c->texture_idx < -1 || c->texture_idx >= textures.size(), 0);
-		if (c->texture_idx != -1) {
 			Color modulate = p_modulate;
 			if (FT_HAS_COLOR(face)) {
-				modulate.r = modulate.g = modulate.b = 1;
+				modulate.r = modulate.g = modulate.b = 1.0;
 			}
-			VisualServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, Rect2(cpos, c->rect.size * Vector2(scale_color_font, scale_color_font)), textures[c->texture_idx].texture->get_rid(), c->rect_uv, modulate, false, RID(), false);
+			RID texture = font->textures[ch->texture_idx].texture->get_rid();
+			VisualServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, Rect2(cpos, ch->rect.size * Vector2(font->scale_color_font, font->scale_color_font)), texture, ch->rect_uv, modulate, false, RID(), false);
 		}
-		advance = c->advance;
-		//textures[c->texture_idx].texture->draw(p_canvas_item,Vector2());
-	}
 
-	if (p_next) {
-
-		FT_Vector delta;
-		FT_Get_Kerning(face, p_char, p_next, FT_KERNING_DEFAULT, &delta);
-
-		if (delta.x == 0) {
-			for (int i = 0; i < p_fallbacks.size(); i++) {
-
-				DynamicFontAtSize *fb = const_cast<DynamicFontAtSize *>(p_fallbacks[i].ptr());
-				if (!fb->valid)
-					continue;
-
-				FT_Get_Kerning(fb->face, p_char, p_next, FT_KERNING_DEFAULT, &delta);
-
-				if (delta.x == 0)
-					continue;
-
-				advance += (delta.x >> 6) / oversampling;
-				break;
-			}
-		} else {
-			advance += (delta.x >> 6) / oversampling;
-		}
+		advance = ch->advance;
 	}
 
+	advance += _get_kerning_advance(font, p_char, p_next);
+
 	return advance;
 }
 
@@ -443,96 +381,37 @@ void DynamicFontAtSize::_ft_stream_close(FT_Stream stream) {
 	memdelete(f);
 }
 
-void DynamicFontAtSize::_update_char(CharType p_char) {
-
-	if (char_map.has(p_char))
-		return;
-
-	_THREAD_SAFE_METHOD_
-
-	FT_GlyphSlot slot = face->glyph;
-
-	if (FT_Get_Char_Index(face, p_char) == 0) {
-		//not found
-		Character ch;
-		ch.texture_idx = -1;
-		ch.advance = 0;
-		ch.h_align = 0;
-		ch.v_align = 0;
-		ch.found = false;
-
-		char_map[p_char] = ch;
-		return;
-	}
-
-	int ft_hinting;
-
-	switch (font->hinting) {
-		case DynamicFontData::HINTING_NONE:
-			ft_hinting = FT_LOAD_NO_HINTING;
-			break;
-		case DynamicFontData::HINTING_LIGHT:
-			ft_hinting = FT_LOAD_TARGET_LIGHT;
-			break;
-		default:
-			ft_hinting = FT_LOAD_TARGET_NORMAL;
-			break;
-	}
-
-	int error = FT_Load_Char(face, p_char, FT_HAS_COLOR(face) ? FT_LOAD_COLOR : FT_LOAD_DEFAULT | (font->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0) | ft_hinting);
-	if (!error) {
-		error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
-	}
-	if (error) {
-
-		int advance = 0;
-		Character ch;
-		ch.texture_idx = -1;
-		ch.advance = advance;
-		ch.h_align = 0;
-		ch.v_align = 0;
-		ch.found = false;
-
-		char_map[p_char] = ch;
-
-		return;
-	}
-
-	int w = slot->bitmap.width;
-	int h = slot->bitmap.rows;
-	int yofs = slot->bitmap_top;
-	int xofs = slot->bitmap_left;
-	int advance = slot->advance.x >> 6;
-
-	int mw = w + rect_margin * 2;
-	int mh = h + rect_margin * 2;
-
-	if (mw > 4096 || mh > 4096) {
-
-		ERR_FAIL_COND(mw > 4096);
-		ERR_FAIL_COND(mh > 4096);
-	}
+DynamicFontAtSize::Character DynamicFontAtSize::Character::not_found() {
+	Character ch;
+	ch.texture_idx = -1;
+	ch.advance = 0;
+	ch.h_align = 0;
+	ch.v_align = 0;
+	ch.found = false;
+	return ch;
+}
 
-	//find a texture to fit this...
+DynamicFontAtSize::TexturePosition DynamicFontAtSize::_find_texture_pos_for_glyph(int p_color_size, Image::Format p_image_format, int p_width, int p_height) {
+	TexturePosition ret;
+	ret.index = -1;
+	ret.x = 0;
+	ret.y = 0;
 
-	int color_size = slot->bitmap.pixel_mode == FT_PIXEL_MODE_BGRA ? 4 : 2;
-	Image::Format require_format = color_size == 4 ? Image::FORMAT_RGBA8 : Image::FORMAT_LA8;
-	int tex_index = -1;
-	int tex_x = 0;
-	int tex_y = 0;
+	int mw = p_width;
+	int mh = p_height;
 
 	for (int i = 0; i < textures.size(); i++) {
 
 		CharTexture &ct = textures[i];
 
-		if (ct.texture->get_format() != require_format)
+		if (ct.texture->get_format() != p_image_format)
 			continue;
 
 		if (mw > ct.texture_size || mh > ct.texture_size) //too big for this texture
 			continue;
 
-		tex_y = 0x7FFFFFFF;
-		tex_x = 0;
+		ret.y = 0x7FFFFFFF;
+		ret.x = 0;
 
 		for (int j = 0; j < ct.texture_size - mw; j++) {
 
@@ -545,25 +424,27 @@ void DynamicFontAtSize::_update_char(CharType p_char) {
 					max_y = y;
 			}
 
-			if (max_y < tex_y) {
-				tex_y = max_y;
-				tex_x = j;
+			if (max_y < ret.y) {
+				ret.y = max_y;
+				ret.x = j;
 			}
 		}
 
-		if (tex_y == 0x7FFFFFFF || tex_y + mh > ct.texture_size)
+		if (ret.y == 0x7FFFFFFF || ret.y + mh > ct.texture_size)
 			continue; //fail, could not fit it here
 
-		tex_index = i;
+		ret.index = i;
 		break;
 	}
 
-	if (tex_index == -1) {
+	//print_line("CHAR: "+String::chr(p_char)+" TEX INDEX: "+itos(tex_index)+" X: "+itos(tex_x)+" Y: "+itos(tex_y));
+
+	if (ret.index == -1) {
 		//could not find texture to fit, create one
-		tex_x = 0;
-		tex_y = 0;
+		ret.x = 0;
+		ret.y = 0;
 
-		int texsize = MAX(id.size * 8, 256);
+		int texsize = MAX(id.size * oversampling * 8, 256);
 		if (mw > texsize)
 			texsize = mw; //special case, adapt to it?
 		if (mh > texsize)
@@ -575,13 +456,13 @@ void DynamicFontAtSize::_update_char(CharType p_char) {
 
 		CharTexture tex;
 		tex.texture_size = texsize;
-		tex.imgdata.resize(texsize * texsize * color_size);
+		tex.imgdata.resize(texsize * texsize * p_color_size); //grayscale alpha
 
 		{
 			//zero texture
 			PoolVector<uint8_t>::Write w = tex.imgdata.write();
-			ERR_FAIL_COND(texsize * texsize * color_size > tex.imgdata.size());
-			for (int i = 0; i < texsize * texsize * color_size; i++) {
+			ERR_FAIL_COND_V(texsize * texsize * p_color_size > tex.imgdata.size(), ret);
+			for (int i = 0; i < texsize * texsize * p_color_size; i++) {
 				w[i] = 0;
 			}
 		}
@@ -590,12 +471,31 @@ void DynamicFontAtSize::_update_char(CharType p_char) {
 			tex.offsets[i] = 0;
 
 		textures.push_back(tex);
-		tex_index = textures.size() - 1;
+		ret.index = textures.size() - 1;
 	}
 
+	return ret;
+}
+
+DynamicFontAtSize::Character DynamicFontAtSize::_bitmap_to_character(FT_Bitmap bitmap, int yofs, int xofs, float advance) {
+	int w = bitmap.width;
+	int h = bitmap.rows;
+
+	int mw = w + rect_margin * 2;
+	int mh = h + rect_margin * 2;
+
+	ERR_FAIL_COND_V(mw > 4096, Character::not_found());
+	ERR_FAIL_COND_V(mh > 4096, Character::not_found());
+
+	int color_size = bitmap.pixel_mode == FT_PIXEL_MODE_BGRA ? 4 : 2;
+	Image::Format require_format = color_size == 4 ? Image::FORMAT_RGBA8 : Image::FORMAT_LA8;
+
+	TexturePosition tex_pos = _find_texture_pos_for_glyph(color_size, require_format, mw, mh);
+	ERR_FAIL_COND_V(tex_pos.index < 0, Character::not_found());
+
 	//fit character in char texture
 
-	CharTexture &tex = textures[tex_index];
+	CharTexture &tex = textures[tex_pos.index];
 
 	{
 		PoolVector<uint8_t>::Write wr = tex.imgdata.write();
@@ -603,30 +503,30 @@ void DynamicFontAtSize::_update_char(CharType p_char) {
 		for (int i = 0; i < h; i++) {
 			for (int j = 0; j < w; j++) {
 
-				int ofs = ((i + tex_y + rect_margin) * tex.texture_size + j + tex_x + rect_margin) * color_size;
-				ERR_FAIL_COND(ofs >= tex.imgdata.size());
-				switch (slot->bitmap.pixel_mode) {
+				int ofs = ((i + tex_pos.y + rect_margin) * tex.texture_size + j + tex_pos.x + rect_margin) * color_size;
+				ERR_FAIL_COND_V(ofs >= tex.imgdata.size(), Character::not_found());
+				switch (bitmap.pixel_mode) {
 					case FT_PIXEL_MODE_MONO: {
-						int byte = i * slot->bitmap.pitch + (j >> 3);
+						int byte = i * bitmap.pitch + (j >> 3);
 						int bit = 1 << (7 - (j % 8));
 						wr[ofs + 0] = 255; //grayscale as 1
-						wr[ofs + 1] = slot->bitmap.buffer[byte] & bit ? 255 : 0;
+						wr[ofs + 1] = bitmap.buffer[byte] & bit ? 255 : 0;
 					} break;
 					case FT_PIXEL_MODE_GRAY:
 						wr[ofs + 0] = 255; //grayscale as 1
-						wr[ofs + 1] = slot->bitmap.buffer[i * slot->bitmap.pitch + j];
+						wr[ofs + 1] = bitmap.buffer[i * bitmap.pitch + j];
 						break;
 					case FT_PIXEL_MODE_BGRA: {
-						int ofs_color = i * slot->bitmap.pitch + (j << 2);
-						wr[ofs + 2] = slot->bitmap.buffer[ofs_color + 0];
-						wr[ofs + 1] = slot->bitmap.buffer[ofs_color + 1];
-						wr[ofs + 0] = slot->bitmap.buffer[ofs_color + 2];
-						wr[ofs + 3] = slot->bitmap.buffer[ofs_color + 3];
+						int ofs_color = i * bitmap.pitch + (j << 2);
+						wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
+						wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
+						wr[ofs + 0] = bitmap.buffer[ofs_color + 2];
+						wr[ofs + 3] = bitmap.buffer[ofs_color + 3];
 					} break;
 					// TODO: FT_PIXEL_MODE_LCD
 					default:
-						ERR_EXPLAIN("Font uses unsupported pixel format: " + itos(slot->bitmap.pixel_mode));
-						ERR_FAIL();
+						ERR_EXPLAIN("Font uses unsupported pixel format: " + itos(bitmap.pixel_mode));
+						ERR_FAIL_V(Character::not_found());
 						break;
 				}
 			}
@@ -648,31 +548,105 @@ void DynamicFontAtSize::_update_char(CharType p_char) {
 
 	// update height array
 
-	for (int k = tex_x; k < tex_x + mw; k++) {
-
-		tex.offsets[k] = tex_y + mh;
+	for (int k = tex_pos.x; k < tex_pos.x + mw; k++) {
+		tex.offsets[k] = tex_pos.y + mh;
 	}
 
 	Character chr;
 	chr.h_align = xofs * scale_color_font / oversampling;
 	chr.v_align = ascent - (yofs * scale_color_font / oversampling); // + ascent - descent;
 	chr.advance = advance * scale_color_font / oversampling;
-	chr.texture_idx = tex_index;
+	chr.texture_idx = tex_pos.index;
 	chr.found = true;
 
-	chr.rect_uv = Rect2(tex_x + rect_margin, tex_y + rect_margin, w, h);
+	chr.rect_uv = Rect2(tex_pos.x + rect_margin, tex_pos.y + rect_margin, w, h);
 	chr.rect = chr.rect_uv;
 	chr.rect.position /= oversampling;
-	chr.rect.size /= oversampling;
+	chr.rect.size = chr.rect.size * scale_color_font / oversampling;
+	return chr;
+}
 
-	char_map[p_char] = chr;
+DynamicFontAtSize::Character DynamicFontAtSize::_make_outline_char(CharType p_char) {
+	Character ret = Character::not_found();
+
+	if (FT_Load_Char(face, p_char, FT_LOAD_NO_BITMAP | (font->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0)) != 0)
+		return ret;
+
+	FT_Stroker stroker;
+	if (FT_Stroker_New(library, &stroker) != 0)
+		return ret;
+
+	FT_Stroker_Set(stroker, (int)(id.outline_size * oversampling * 64.0), FT_STROKER_LINECAP_BUTT, FT_STROKER_LINEJOIN_ROUND, 0);
+	FT_Glyph glyph;
+	FT_BitmapGlyph glyph_bitmap;
+
+	if (FT_Get_Glyph(face->glyph, &glyph) != 0)
+		goto cleanup_stroker;
+	if (FT_Glyph_Stroke(&glyph, stroker, 1) != 0)
+		goto cleanup_glyph;
+	if (FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, 0, 1) != 0)
+		goto cleanup_glyph;
+
+	glyph_bitmap = (FT_BitmapGlyph)glyph;
+	ret = _bitmap_to_character(glyph_bitmap->bitmap, glyph_bitmap->top, glyph_bitmap->left, glyph->advance.x / 65536.0);
+
+cleanup_glyph:
+	FT_Done_Glyph(glyph);
+cleanup_stroker:
+	FT_Stroker_Done(stroker);
+	return ret;
 }
 
-bool DynamicFontAtSize::update_oversampling() {
-	if (oversampling == font_oversampling)
-		return false;
-	if (!valid)
-		return false;
+void DynamicFontAtSize::_update_char(CharType p_char) {
+
+	if (char_map.has(p_char))
+		return;
+
+	_THREAD_SAFE_METHOD_
+
+	Character character = Character::not_found();
+
+	FT_GlyphSlot slot = face->glyph;
+
+	if (FT_Get_Char_Index(face, p_char) == 0) {
+		char_map[p_char] = character;
+		return;
+	}
+
+	int ft_hinting;
+
+	switch (font->hinting) {
+		case DynamicFontData::HINTING_NONE:
+			ft_hinting = FT_LOAD_NO_HINTING;
+			break;
+		case DynamicFontData::HINTING_LIGHT:
+			ft_hinting = FT_LOAD_TARGET_LIGHT;
+			break;
+		default:
+			ft_hinting = FT_LOAD_TARGET_NORMAL;
+			break;
+	}
+
+	int error = FT_Load_Char(face, p_char, FT_HAS_COLOR(face) ? FT_LOAD_COLOR : FT_LOAD_DEFAULT | (font->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0));
+	if (error) {
+		char_map[p_char] = character;
+		return;
+	}
+
+	if (id.outline_size > 0) {
+		character = _make_outline_char(p_char);
+	} else {
+		error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
+		if (!error)
+			character = _bitmap_to_character(slot->bitmap, slot->bitmap_top, slot->bitmap_left, slot->advance.x / 64.0);
+	}
+
+	char_map[p_char] = character;
+}
+
+void DynamicFontAtSize::update_oversampling() {
+	if (oversampling == font_oversampling || !valid)
+		return;
 
 	FT_Done_FreeType(library);
 	textures.clear();
@@ -680,8 +654,6 @@ bool DynamicFontAtSize::update_oversampling() {
 	oversampling = font_oversampling;
 	valid = false;
 	_load();
-
-	return true;
 }
 
 DynamicFontAtSize::DynamicFontAtSize() {
@@ -710,11 +682,27 @@ DynamicFontAtSize::~DynamicFontAtSize() {
 void DynamicFont::_reload_cache() {
 
 	ERR_FAIL_COND(cache_id.size < 1);
-	if (!data.is_valid())
+	if (!data.is_valid()) {
+		data_at_size.unref();
+		outline_data_at_size.unref();
+		fallback_data_at_size.resize(0);
+		fallback_outline_data_at_size.resize(0);
 		return;
+	}
+
 	data_at_size = data->_get_dynamic_font_at_size(cache_id);
+	if (outline_cache_id.outline_size > 0) {
+		outline_data_at_size = data->_get_dynamic_font_at_size(outline_cache_id);
+		fallback_outline_data_at_size.resize(fallback_data_at_size.size());
+	} else {
+		outline_data_at_size.unref();
+		fallback_outline_data_at_size.resize(0);
+	}
+
 	for (int i = 0; i < fallbacks.size(); i++) {
 		fallback_data_at_size[i] = fallbacks[i]->_get_dynamic_font_at_size(cache_id);
+		if (outline_cache_id.outline_size > 0)
+			fallback_outline_data_at_size[i] = fallbacks[i]->_get_dynamic_font_at_size(outline_cache_id);
 	}
 
 	emit_changed();
@@ -724,12 +712,10 @@ void DynamicFont::_reload_cache() {
 void DynamicFont::set_font_data(const Ref<DynamicFontData> &p_data) {
 
 	data = p_data;
-	if (data.is_valid())
-		data_at_size = data->_get_dynamic_font_at_size(cache_id);
-	else
-		data_at_size = Ref<DynamicFontAtSize>();
+	_reload_cache();
 
 	emit_changed();
+	_change_notify();
 }
 
 Ref<DynamicFontData> DynamicFont::get_font_data() const {
@@ -742,6 +728,7 @@ void DynamicFont::set_size(int p_size) {
 	if (cache_id.size == p_size)
 		return;
 	cache_id.size = p_size;
+	outline_cache_id.size = p_size;
 	_reload_cache();
 }
 
@@ -750,6 +737,30 @@ int DynamicFont::get_size() const {
 	return cache_id.size;
 }
 
+void DynamicFont::set_outline_size(int p_size) {
+	if (outline_cache_id.outline_size == p_size)
+		return;
+	ERR_FAIL_COND(p_size < 0 || p_size > UINT8_MAX);
+	outline_cache_id.outline_size = p_size;
+	_reload_cache();
+}
+
+int DynamicFont::get_outline_size() const {
+	return outline_cache_id.outline_size;
+}
+
+void DynamicFont::set_outline_color(Color p_color) {
+	if (p_color != outline_color) {
+		outline_color = p_color;
+		emit_changed();
+		_change_notify();
+	}
+}
+
+Color DynamicFont::get_outline_color() const {
+	return outline_color;
+}
+
 bool DynamicFont::get_use_mipmaps() const {
 
 	return cache_id.mipmaps;
@@ -760,6 +771,7 @@ void DynamicFont::set_use_mipmaps(bool p_enable) {
 	if (cache_id.mipmaps == p_enable)
 		return;
 	cache_id.mipmaps = p_enable;
+	outline_cache_id.mipmaps = p_enable;
 	_reload_cache();
 }
 
@@ -773,6 +785,7 @@ void DynamicFont::set_use_filter(bool p_enable) {
 	if (cache_id.filter == p_enable)
 		return;
 	cache_id.filter = p_enable;
+	outline_cache_id.filter = p_enable;
 	_reload_cache();
 }
 
@@ -862,13 +875,24 @@ bool DynamicFont::is_distance_field_hint() const {
 	return false;
 }
 
-float DynamicFont::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate) const {
+bool DynamicFont::has_outline() const {
+	return outline_cache_id.outline_size > 0;
+}
+
+float DynamicFont::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, bool p_outline) const {
+	const Ref<DynamicFontAtSize> &font_at_size = p_outline && outline_cache_id.outline_size > 0 ? outline_data_at_size : data_at_size;
 
-	if (!data_at_size.is_valid())
+	if (!font_at_size.is_valid())
 		return 0;
 
-	return data_at_size->draw_char(p_canvas_item, p_pos, p_char, p_next, p_modulate, fallback_data_at_size) + spacing_char;
+	const Vector<Ref<DynamicFontAtSize> > &fallbacks = p_outline && outline_cache_id.outline_size > 0 ? fallback_outline_data_at_size : fallback_data_at_size;
+	Color color = p_outline && outline_cache_id.outline_size > 0 ? p_modulate * outline_color : p_modulate;
+
+	// If requested outline draw, but no outline is present, simply return advance without drawing anything
+	bool advance_only = p_outline && outline_cache_id.outline_size == 0;
+	return font_at_size->draw_char(p_canvas_item, p_pos, p_char, p_next, color, fallbacks, advance_only) + spacing_char;
 }
+
 void DynamicFont::set_fallback(int p_idx, const Ref<DynamicFontData> &p_data) {
 
 	ERR_FAIL_COND(p_data.is_null());
@@ -882,6 +906,8 @@ void DynamicFont::add_fallback(const Ref<DynamicFontData> &p_data) {
 	ERR_FAIL_COND(p_data.is_null());
 	fallbacks.push_back(p_data);
 	fallback_data_at_size.push_back(fallbacks[fallbacks.size() - 1]->_get_dynamic_font_at_size(cache_id)); //const..
+	if (outline_cache_id.outline_size > 0)
+		fallback_outline_data_at_size.push_back(fallbacks[fallbacks.size() - 1]->_get_dynamic_font_at_size(outline_cache_id));
 
 	_change_notify();
 	emit_changed();
@@ -966,6 +992,12 @@ void DynamicFont::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_size", "data"), &DynamicFont::set_size);
 	ClassDB::bind_method(D_METHOD("get_size"), &DynamicFont::get_size);
 
+	ClassDB::bind_method(D_METHOD("set_outline_size", "size"), &DynamicFont::set_outline_size);
+	ClassDB::bind_method(D_METHOD("get_outline_size"), &DynamicFont::get_outline_size);
+
+	ClassDB::bind_method(D_METHOD("set_outline_color", "color"), &DynamicFont::set_outline_color);
+	ClassDB::bind_method(D_METHOD("get_outline_color"), &DynamicFont::get_outline_color);
+
 	ClassDB::bind_method(D_METHOD("set_use_mipmaps", "enable"), &DynamicFont::set_use_mipmaps);
 	ClassDB::bind_method(D_METHOD("get_use_mipmaps"), &DynamicFont::get_use_mipmaps);
 	ClassDB::bind_method(D_METHOD("set_use_filter", "enable"), &DynamicFont::set_use_filter);
@@ -981,6 +1013,8 @@ void DynamicFont::_bind_methods() {
 
 	ADD_GROUP("Settings", "");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "size"), "set_size", "get_size");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "outline_size"), "set_outline_size", "get_outline_size");
+	ADD_PROPERTY(PropertyInfo(Variant::COLOR, "outline_color"), "set_outline_color", "get_outline_color");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_mipmaps"), "set_use_mipmaps", "get_use_mipmaps");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_filter"), "set_use_filter", "get_use_filter");
 	ADD_GROUP("Extra Spacing", "extra_spacing");
@@ -1008,6 +1042,7 @@ DynamicFont::DynamicFont() :
 	spacing_bottom = 0;
 	spacing_char = 0;
 	spacing_space = 0;
+	outline_color = Color(1, 1, 1);
 	if (dynamic_font_mutex)
 		dynamic_font_mutex->lock();
 	dynamic_fonts.add(&font_list);
@@ -1043,7 +1078,8 @@ void DynamicFont::update_oversampling() {
 	SelfList<DynamicFont> *E = dynamic_fonts.first();
 	while (E) {
 
-		if (E->self()->data_at_size.is_valid() && E->self()->data_at_size->update_oversampling()) {
+		if (E->self()->data_at_size.is_valid()) {
+			E->self()->data_at_size->update_oversampling();
 			changed.push_back(Ref<DynamicFont>(E->self()));
 		}
 		E = E->next();

+ 33 - 4
scene/resources/dynamic_font.h

@@ -35,6 +35,7 @@
 #include "io/resource_loader.h"
 #include "os/mutex.h"
 #include "os/thread_safe.h"
+#include "pair.h"
 #include "scene/resources/font.h"
 
 #include <ft2build.h>
@@ -49,10 +50,10 @@ class DynamicFontData : public Resource {
 
 public:
 	struct CacheID {
-
 		union {
 			struct {
 				uint32_t size : 16;
+				uint32_t outline_size : 8;
 				bool mipmaps : 1;
 				bool filter : 1;
 			};
@@ -148,8 +149,22 @@ class DynamicFontAtSize : public Reference {
 			texture_idx = 0;
 			v_align = 0;
 		}
+
+		static Character not_found();
 	};
 
+	struct TexturePosition {
+		int index;
+		int x;
+		int y;
+	};
+
+	const Pair<const Character *, DynamicFontAtSize *> _find_char_with_font(CharType p_char, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const;
+	Character _make_outline_char(CharType p_char);
+	float _get_kerning_advance(const DynamicFontAtSize *font, CharType p_char, CharType p_next) const;
+	TexturePosition _find_texture_pos_for_glyph(int p_color_size, Image::Format p_image_format, int p_width, int p_height);
+	Character _bitmap_to_character(FT_Bitmap bitmap, int yofs, int xofs, float advance);
+
 	static unsigned long _ft_stream_io(FT_Stream stream, unsigned long offset, unsigned char *buffer, unsigned long count);
 	static void _ft_stream_close(FT_Stream stream);
 
@@ -174,10 +189,10 @@ public:
 
 	Size2 get_char_size(CharType p_char, CharType p_next, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const;
 
-	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks) const;
+	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, const Vector<Ref<DynamicFontAtSize> > &p_fallbacks, bool p_advance_only = false) const;
 
 	void set_texture_flags(uint32_t p_flags);
-	bool update_oversampling();
+	void update_oversampling();
 
 	DynamicFontAtSize();
 	~DynamicFontAtSize();
@@ -200,17 +215,23 @@ public:
 private:
 	Ref<DynamicFontData> data;
 	Ref<DynamicFontAtSize> data_at_size;
+	Ref<DynamicFontAtSize> outline_data_at_size;
 
 	Vector<Ref<DynamicFontData> > fallbacks;
 	Vector<Ref<DynamicFontAtSize> > fallback_data_at_size;
+	Vector<Ref<DynamicFontAtSize> > fallback_outline_data_at_size;
 
 	DynamicFontData::CacheID cache_id;
+	DynamicFontData::CacheID outline_cache_id;
+
 	bool valid;
 	int spacing_top;
 	int spacing_bottom;
 	int spacing_char;
 	int spacing_space;
 
+	Color outline_color;
+
 protected:
 	void _reload_cache();
 
@@ -227,6 +248,12 @@ public:
 	void set_size(int p_size);
 	int get_size() const;
 
+	void set_outline_size(int p_size);
+	int get_outline_size() const;
+
+	void set_outline_color(Color p_color);
+	Color get_outline_color() const;
+
 	bool get_use_mipmaps() const;
 	void set_use_mipmaps(bool p_enable);
 
@@ -251,7 +278,9 @@ public:
 
 	virtual bool is_distance_field_hint() const;
 
-	virtual float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1)) const;
+	virtual bool has_outline() const;
+
+	virtual float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1), bool p_outline = false) const;
 
 	SelfList<DynamicFont> font_list;
 

+ 12 - 10
scene/resources/dynamic_font_stb.cpp

@@ -162,7 +162,7 @@ Size2 DynamicFontAtSize::get_char_size(CharType p_char, CharType p_next) const {
 	return ret;
 }
 
-float DynamicFontAtSize::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate) const {
+float DynamicFontAtSize::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, bool p_outline) const {
 
 	const_cast<DynamicFontAtSize *>(this)->_update_char(p_char);
 
@@ -172,13 +172,15 @@ float DynamicFontAtSize::draw_char(RID p_canvas_item, const Point2 &p_pos, CharT
 		return 0;
 	}
 
-	Point2 cpos = p_pos;
-	cpos.x += c->h_align;
-	cpos.y -= get_ascent();
-	cpos.y += c->v_align;
-	ERR_FAIL_COND_V(c->texture_idx < -1 || c->texture_idx >= textures.size(), 0);
-	if (c->texture_idx != -1)
-		VisualServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, Rect2(cpos, c->rect.size), textures[c->texture_idx].texture->get_rid(), c->rect, p_modulate);
+	if (!p_outline) {
+		Point2 cpos = p_pos;
+		cpos.x += c->h_align;
+		cpos.y -= get_ascent();
+		cpos.y += c->v_align;
+		ERR_FAIL_COND_V(c->texture_idx < -1 || c->texture_idx >= textures.size(), 0);
+		if (c->texture_idx != -1)
+			VisualServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, Rect2(cpos, c->rect.size), textures[c->texture_idx].texture->get_rid(), c->rect, p_modulate);
+	}
 
 	//textures[c->texture_idx].texture->draw(p_canvas_item,Vector2());
 
@@ -459,12 +461,12 @@ bool DynamicFont::is_distance_field_hint() const {
 	return false;
 }
 
-float DynamicFont::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate) const {
+float DynamicFont::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, bool p_outline) const {
 
 	if (!data_at_size.is_valid())
 		return 0;
 
-	return data_at_size->draw_char(p_canvas_item, p_pos, p_char, p_next, p_modulate);
+	return data_at_size->draw_char(p_canvas_item, p_pos, p_char, p_next, p_modulate, p_outline);
 }
 
 DynamicFont::DynamicFont() {

+ 2 - 2
scene/resources/dynamic_font_stb.h

@@ -136,7 +136,7 @@ public:
 
 	Size2 get_char_size(CharType p_char, CharType p_next = 0) const;
 
-	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1)) const;
+	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1), bool p_outline = false) const;
 
 	DynamicFontAtSize();
 	~DynamicFontAtSize();
@@ -171,7 +171,7 @@ public:
 
 	virtual bool is_distance_field_hint() const;
 
-	virtual float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1)) const;
+	virtual float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1), bool p_outline = false) const;
 
 	DynamicFont();
 	~DynamicFont();

+ 27 - 16
scene/resources/font.cpp

@@ -32,12 +32,12 @@
 
 #include "core/io/resource_loader.h"
 #include "core/os/file_access.h"
+#include "method_bind_ext.gen.inc"
 
-void Font::draw_halign(RID p_canvas_item, const Point2 &p_pos, HAlign p_align, float p_width, const String &p_text, const Color &p_modulate) const {
-
+void Font::draw_halign(RID p_canvas_item, const Point2 &p_pos, HAlign p_align, float p_width, const String &p_text, const Color &p_modulate, const Color &p_outline_modulate) const {
 	float length = get_string_size(p_text).width;
 	if (length >= p_width) {
-		draw(p_canvas_item, p_pos, p_text, p_modulate, p_width);
+		draw(p_canvas_item, p_pos, p_text, p_modulate, p_width, p_outline_modulate);
 		return;
 	}
 
@@ -56,13 +56,14 @@ void Font::draw_halign(RID p_canvas_item, const Point2 &p_pos, HAlign p_align, f
 			ERR_PRINT("Unknown halignment type");
 		} break;
 	}
-	draw(p_canvas_item, p_pos + Point2(ofs, 0), p_text, p_modulate, p_width);
+	draw(p_canvas_item, p_pos + Point2(ofs, 0), p_text, p_modulate, p_width, p_outline_modulate);
 }
 
-void Font::draw(RID p_canvas_item, const Point2 &p_pos, const String &p_text, const Color &p_modulate, int p_clip_w) const {
-
+void Font::draw(RID p_canvas_item, const Point2 &p_pos, const String &p_text, const Color &p_modulate, int p_clip_w, const Color &p_outline_modulate) const {
 	Vector2 ofs;
 
+	int chars_drawn = 0;
+	bool with_outline = has_outline();
 	for (int i = 0; i < p_text.length(); i++) {
 
 		int width = get_char_size(p_text[i]).width;
@@ -70,7 +71,15 @@ void Font::draw(RID p_canvas_item, const Point2 &p_pos, const String &p_text, co
 		if (p_clip_w >= 0 && (ofs.x + width) > p_clip_w)
 			break; //clip
 
-		ofs.x += draw_char(p_canvas_item, p_pos + ofs, p_text[i], p_text[i + 1], p_modulate);
+		ofs.x += draw_char(p_canvas_item, p_pos + ofs, p_text[i], p_text[i + 1], with_outline ? p_outline_modulate : p_modulate, with_outline);
+		++chars_drawn;
+	}
+
+	if (has_outline()) {
+		ofs = Vector2(0, 0);
+		for (int i = 0; i < chars_drawn; i++) {
+			ofs.x += draw_char(p_canvas_item, p_pos + ofs, p_text[i], p_text[i + 1], p_modulate, false);
+		}
 	}
 }
 
@@ -81,13 +90,14 @@ void Font::update_changes() {
 
 void Font::_bind_methods() {
 
-	ClassDB::bind_method(D_METHOD("draw", "canvas_item", "position", "string", "modulate", "clip_w"), &Font::draw, DEFVAL(Color(1, 1, 1)), DEFVAL(-1));
+	ClassDB::bind_method(D_METHOD("draw", "canvas_item", "position", "string", "modulate", "clip_w", "outline_modulate"), &Font::draw, DEFVAL(Color(1, 1, 1)), DEFVAL(-1), DEFVAL(Color(1, 1, 1)));
 	ClassDB::bind_method(D_METHOD("get_ascent"), &Font::get_ascent);
 	ClassDB::bind_method(D_METHOD("get_descent"), &Font::get_descent);
 	ClassDB::bind_method(D_METHOD("get_height"), &Font::get_height);
 	ClassDB::bind_method(D_METHOD("is_distance_field_hint"), &Font::is_distance_field_hint);
 	ClassDB::bind_method(D_METHOD("get_string_size", "string"), &Font::get_string_size);
-	ClassDB::bind_method(D_METHOD("draw_char", "canvas_item", "position", "char", "next", "modulate"), &Font::draw_char, DEFVAL(-1), DEFVAL(Color(1, 1, 1)));
+	ClassDB::bind_method(D_METHOD("has_outline"), &Font::has_outline);
+	ClassDB::bind_method(D_METHOD("draw_char", "canvas_item", "position", "char", "next", "modulate", "outline"), &Font::draw_char, DEFVAL(-1), DEFVAL(Color(1, 1, 1)), DEFVAL(false));
 	ClassDB::bind_method(D_METHOD("update_changes"), &Font::update_changes);
 }
 
@@ -494,23 +504,24 @@ Ref<BitmapFont> BitmapFont::get_fallback() const {
 	return fallback;
 }
 
-float BitmapFont::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate) const {
+float BitmapFont::draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next, const Color &p_modulate, bool p_outline) const {
 
 	const Character *c = char_map.getptr(p_char);
 
 	if (!c) {
 		if (fallback.is_valid())
-			return fallback->draw_char(p_canvas_item, p_pos, p_char, p_next, p_modulate);
+			return fallback->draw_char(p_canvas_item, p_pos, p_char, p_next, p_modulate, p_outline);
 		return 0;
 	}
 
-	Point2 cpos = p_pos;
-	cpos.x += c->h_align;
-	cpos.y -= ascent;
-	cpos.y += c->v_align;
 	ERR_FAIL_COND_V(c->texture_idx < -1 || c->texture_idx >= textures.size(), 0);
-	if (c->texture_idx != -1)
+	if (!p_outline && c->texture_idx != -1) {
+		Point2 cpos = p_pos;
+		cpos.x += c->h_align;
+		cpos.y -= ascent;
+		cpos.y += c->v_align;
 		VisualServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas_item, Rect2(cpos, c->rect.size), textures[c->texture_idx]->get_rid(), c->rect, p_modulate, false, RID(), false);
+	}
 
 	return get_char_size(p_char, p_next).width;
 }

+ 45 - 4
scene/resources/font.h

@@ -56,14 +56,55 @@ public:
 
 	virtual bool is_distance_field_hint() const = 0;
 
-	void draw(RID p_canvas_item, const Point2 &p_pos, const String &p_text, const Color &p_modulate = Color(1, 1, 1), int p_clip_w = -1) const;
-	void draw_halign(RID p_canvas_item, const Point2 &p_pos, HAlign p_align, float p_width, const String &p_text, const Color &p_modulate = Color(1, 1, 1)) const;
-	virtual float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1)) const = 0;
+	void draw(RID p_canvas_item, const Point2 &p_pos, const String &p_text, const Color &p_modulate = Color(1, 1, 1), int p_clip_w = -1, const Color &p_outline_modulate = Color(1, 1, 1)) const;
+	void draw_halign(RID p_canvas_item, const Point2 &p_pos, HAlign p_align, float p_width, const String &p_text, const Color &p_modulate = Color(1, 1, 1), const Color &p_outline_modulate = Color(1, 1, 1)) const;
+
+	virtual bool has_outline() const { return false; }
+	virtual float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1), bool p_outline = false) const = 0;
 
 	void update_changes();
 	Font();
 };
 
+// Helper class to that draws outlines immediately and draws characters in its destructor.
+class FontDrawer {
+	const Ref<Font> &font;
+	Color outline_color;
+	bool has_outline;
+
+	struct PendingDraw {
+		RID canvas_item;
+		Point2 pos;
+		CharType chr;
+		CharType next;
+		Color modulate;
+	};
+
+	Vector<PendingDraw> pending_draws;
+
+public:
+	FontDrawer(const Ref<Font> &p_font, const Color &p_outline_color) :
+			font(p_font),
+			outline_color(p_outline_color) {
+		has_outline = p_font->has_outline();
+	}
+
+	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1)) {
+		if (has_outline) {
+			PendingDraw draw = { p_canvas_item, p_pos, p_char, p_next, p_modulate };
+			pending_draws.push_back(draw);
+		}
+		return font->draw_char(p_canvas_item, p_pos, p_char, p_next, has_outline ? outline_color : p_modulate, has_outline);
+	}
+
+	~FontDrawer() {
+		for (int i = 0; i < pending_draws.size(); ++i) {
+			const PendingDraw &draw = pending_draws[i];
+			font->draw_char(draw.canvas_item, draw.pos, draw.chr, draw.next, draw.modulate, false);
+		}
+	}
+};
+
 class BitmapFont : public Font {
 
 	GDCLASS(BitmapFont, Font);
@@ -153,7 +194,7 @@ public:
 	void set_distance_field_hint(bool p_distance_field);
 	bool is_distance_field_hint() const;
 
-	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1)) const;
+	float draw_char(RID p_canvas_item, const Point2 &p_pos, CharType p_char, CharType p_next = 0, const Color &p_modulate = Color(1, 1, 1), bool p_outline = false) const;
 
 	BitmapFont();
 	~BitmapFont();