Просмотр исходного кода

Mesh-based text-from-spritesheet renderer

Bill Robinson 12 лет назад
Родитель
Сommit
4205ef20a8

+ 5 - 1
Core/Contents/CMakeLists.txt

@@ -19,6 +19,7 @@ SET(polycore_SRCS
     Source/PolyEventHandler.cpp
     Source/PolyFixedShader.cpp
     Source/PolyFont.cpp
+	Source/PolyFontGlyphSheet.cpp
     Source/PolyFontManager.cpp
     Source/PolyGLCubemap.cpp
     Source/PolyGLRenderer.cpp
@@ -60,7 +61,8 @@ SET(polycore_SRCS
     Source/PolySound.cpp
     Source/PolySoundManager.cpp
     Source/PolyString.cpp
-    Source/PolyTexture.cpp
+	Source/PolyTextMesh.cpp
+	Source/PolyTexture.cpp
     Source/PolyThreaded.cpp
     Source/PolyTimer.cpp
     Source/PolyTimerManager.cpp
@@ -101,6 +103,7 @@ SET(polycore_HDRS
     Include/PolyEventHandler.h
     Include/PolyFixedShader.h
     Include/PolyFont.h
+	Include/PolyFontGlyphSheet.h
     Include/PolyFontManager.h
     Include/PolyGLCubemap.h
     Include/PolyGLHeaders.h
@@ -145,6 +148,7 @@ SET(polycore_HDRS
     Include/PolySound.h
     Include/PolySoundManager.h
     Include/PolyString.h
+	Include/PolyTextMesh.h
     Include/PolyTexture.h
     Include/PolyThreaded.h
     Include/PolyTimer.h

+ 84 - 0
Core/Contents/Include/PolyFontGlyphSheet.h

@@ -0,0 +1,84 @@
+
+#pragma once
+#include "PolyGlobals.h"
+#include "ft2build.h"
+#include "PolyString.h"
+#include "PolyVertex.h"
+#include <vector>
+#include <map>
+#include <set>
+
+#include FT_FREETYPE_H
+
+namespace Polycode {
+	
+	class String;
+	class Texture;
+	class Image;
+	class Font;
+
+	struct FontTextureGlyph {
+		Vector2 offset[4];
+		Vector2 texCoord[4];
+		Vector2 advance;
+	};
+
+	/** Wraps a sheet of rendered font glyphs on a Texture.
+	 *  Use in combination with TextMesh to render text from minimal texture creation. */
+	class _PolyExport FontGlyphSheet : public PolyBase {
+		public:
+
+			enum FontTextureGlyphMode {
+				/** Regular anti-aliased font rendering. Colour is pure-white for clean custom tinting with an alpha channel. */
+				ANTIALIAS,
+				/** Using distance-from-edge calculation as described in the Valve paper.
+				 *
+				 *  "Improved Alpha-Tested Magnification for Vector Textures and Special Effects"
+				 *  http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf
+				 *
+				 * To make the most of this:
+				 *   set renderer->alphaTestValue = 0.5
+				 *   set sceneMesh->alphaTest = true
+				 *   set sceneMesh->blendingMode = Renderer::BLEND_MODE_NONE;
+				 *
+				 * Or use a custom shader - alpha values of 0.5 indicate the boundary.
+				 * */
+				ALPHA_TEST
+			};
+
+			FontGlyphSheet(Font* font, int size = 32, FontTextureGlyphMode mode = ANTIALIAS);
+			virtual ~FontGlyphSheet();
+			
+			void setMode(FontTextureGlyphMode mode) { this->mode = mode; }
+			/** Set height of font to be rendered in pixels. */
+			void setSize(int size);
+
+			/** Scans extraCharacters for anything that isn't currently in the rendered sheet and rebuilds the sheet if necessary. */
+			void addGlyphs(String extraCharacters);
+
+			/** Convenience method to build a sheet with all of the visible ASCII characters. */
+			void buildVisibleAscii();
+
+			/** Convenience method to build a sheet of glyphs with one of each of what is in characters. */
+			void buildGlyphs(String characters);
+
+			/** Creates the sheet given a set of characters. */
+			void buildGlyphs(std::set<wchar_t> characters);
+
+			/** Returns the currently rendered characters as a set. */
+			std::set<wchar_t> getCharacters() const;
+
+			/** Used by TextMesh to generate the vertices for the given text into the vertex array.
+				@return the next index after that which was used */
+			int renderStringVertices(String text, std::vector<Vertex*>& vertices, int index = 0);
+
+			Texture* getTexture() { return texture; }
+			
+		protected:
+			Font* font;
+			FontTextureGlyphMode mode;
+			Texture* texture;
+			std::map<wchar_t,FontTextureGlyph> locations;
+			int size;
+	};
+}

+ 25 - 0
Core/Contents/Include/PolyTextMesh.h

@@ -0,0 +1,25 @@
+
+#pragma once
+#include "PolyGlobals.h"
+#include "PolyEntity.h"
+#include "PolyMesh.h"
+#include "PolyString.h"
+
+namespace Polycode {
+
+	class FontGlyphSheet;
+
+	class _PolyExport TextMesh : public Mesh {
+	public:
+		TextMesh(FontGlyphSheet* font, const String& text);
+
+		void setFont(FontGlyphSheet* font);
+		void setText(const String& text);
+
+		void rebuild();
+
+	private:
+		String text;
+		FontGlyphSheet* font;
+	};
+}

+ 2 - 0
Core/Contents/Include/Polycode.h

@@ -50,10 +50,12 @@
 #include "PolyImage.h"
 #include "PolyLabel.h"
 #include "PolyFont.h"
+#include "PolyFontGlyphSheet.h"
 #include "PolyFontManager.h"
 #include "PolyTexture.h"
 #include "PolyMaterial.h"
 #include "PolyMesh.h"
+#include "PolyTextMesh.h"
 #include "PolyShader.h"
 #include "PolyFixedShader.h"
 #include "PolySceneManager.h"

+ 312 - 0
Core/Contents/Source/PolyFontGlyphSheet.cpp

@@ -0,0 +1,312 @@
+/*
+ Copyright (C) 2011 by Ivan Safrin
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+
+#include "PolyFont.h"
+#include "PolyFontGlyphSheet.h"
+#include "OSBasics.h"
+#include "PolyLogger.h"
+#include "PolyImage.h"
+#include "PolyTexture.h"
+#include "PolyCoreServices.h"
+#include <algorithm>
+#include <set>
+#include <vector>
+
+using namespace Polycode;
+
+FontGlyphSheet::FontGlyphSheet(Font* font, int size, FontTextureGlyphMode mode)
+:	font(font)
+,	size(size)
+,	mode(mode)
+{
+}
+
+FontGlyphSheet::~FontGlyphSheet() {
+	if (texture)
+		CoreServices::getInstance()->getMaterialManager()->deleteTexture(texture);
+}
+
+struct GlyphData {
+	wchar_t character;
+	short off_x,off_y;
+	short size_x,size_y;
+	short pitch;
+	Vector2 advance;
+	unsigned char* data;
+	int texture_u,texture_v;
+
+	GlyphData()
+	:	data(NULL)
+	{
+	}
+
+	~GlyphData() {
+		delete data;
+	}
+};
+
+bool fontGlyphSorter(GlyphData* a, GlyphData* b) {
+	return a->size_y > b->size_y;
+}
+
+inline int pot_ceil(int x) {
+	--x;
+	x |= x >> 1;
+	x |= x >> 2;
+	x |= x >> 4;
+	x |= x >> 8;
+	x |= x >> 16;
+	return x+1;
+}
+
+std::set<wchar_t> FontGlyphSheet::getCharacters() const {
+	std::set<wchar_t> set;
+	for (std::map<wchar_t,FontTextureGlyph>::const_iterator it = locations.begin(); it != locations.end(); it++) {
+		set.insert(it->first);
+	}
+	return set;
+}
+
+void FontGlyphSheet::buildVisibleAscii() {
+	std::set<wchar_t> chars;
+	for (wchar_t i = 32; i < 127; i++) {
+		chars.insert(i);
+	}
+	buildGlyphs(chars);
+}
+
+void FontGlyphSheet::addGlyphs(String extraCharacters) {
+	extraCharacters.getWDataWithEncoding(String::ENCODING_UTF8);
+	std::wstring& str = extraCharacters.w_contents;
+	for (std::wstring::iterator it = str.begin(); it != str.end(); it++ ) {
+		if (locations.find(*it) == locations.end()) {
+			std::set<wchar_t> characterSet = getCharacters();
+			characterSet.insert(str.begin(), str.end());
+			buildGlyphs(characterSet);
+			break;
+		}
+	}
+}
+
+void FontGlyphSheet::setSize(int size) {
+	if (this->size == size) return;
+	this->size = size;
+	buildGlyphs(getCharacters());
+}
+
+void FontGlyphSheet::buildGlyphs(String charactersIn) {
+	charactersIn.getWDataWithEncoding(String::ENCODING_UTF8);
+	buildGlyphs(std::set<wchar_t>(charactersIn.w_contents.begin(), charactersIn.w_contents.end()));
+}
+
+void FontGlyphSheet::buildGlyphs(std::set<wchar_t> characters) {
+
+	typedef std::set<wchar_t> character_container_t;
+	typedef std::vector<GlyphData*> glyph_list_t;
+	glyph_list_t glyphData;
+	characters.insert('?');//Good backup character - make sure it's always present
+
+	int shift = 0;
+	Number scaleDown = 1.0f;
+	FT_UInt height = size;
+	if (mode == ALPHA_TEST) {
+		shift = 2;
+		scaleDown /= (1<<shift);
+		height <<= shift;
+	}
+	FT_Face ftFace = font->getFace();
+	FT_Set_Pixel_Sizes(ftFace, 0, height);
+
+	//Get all the glyph data from freetype
+	for (character_container_t::iterator it = characters.begin(); it != characters.end(); it++) {
+		FT_Int32 load_flags = FT_LOAD_RENDER;
+//		if (glyphMode == ALPHA_TEST) {
+//			load_flags |= FT_LOAD_MONOCHROME;
+//		}
+		int error = FT_Load_Char(ftFace, *it,  load_flags);
+		if (error) {
+			Logger::log("Failed to load glyph for codepoint %d '%lc' error %#x\n",*it,*it,error);
+		}
+		else {
+			glyphData.push_back(new GlyphData());
+			GlyphData& gd = *glyphData.back();
+			FT_GlyphSlot slot = ftFace->glyph;
+			gd.character = *it;
+			gd.off_x = slot->bitmap_left;
+			gd.off_y = slot->bitmap_top;
+			gd.size_x = slot->bitmap.width;
+			gd.size_y = slot->bitmap.rows;
+			gd.advance.set(Number(slot->advance.x)/(64<<shift), Number(slot->advance.y)/(64<<shift));
+			int dataLength = slot->bitmap.pitch * gd.size_y;
+			gd.data = new unsigned char[dataLength];
+			gd.pitch = slot->bitmap.pitch;
+			memcpy(gd.data, slot->bitmap.buffer, dataLength);
+		}
+	}
+
+	std::sort(glyphData.begin(), glyphData.end(), fontGlyphSorter);
+
+	//Compute the layout for the glyphs on the texture
+	const int padding = 1;
+	int sheet_width = 512, sheet_height;
+	{
+		int sheet_y = padding, sheet_x = padding;
+		int row_size_y = 0;
+		for (glyph_list_t::iterator it = glyphData.begin(); it != glyphData.end(); it++) {
+			GlyphData& gd = **it;
+			int size_x = gd.size_x + ((1<<shift)-1) >> shift;
+			int size_y = gd.size_y + ((1<<shift)-1) >> shift;
+			if (sheet_x + size_x + padding >= sheet_width) {
+				sheet_x = padding;
+				sheet_y += row_size_y + padding;
+				row_size_y = 0;
+			}
+			if (size_y > row_size_y) {
+				row_size_y = size_y;
+			}
+			gd.texture_u = sheet_x;
+			gd.texture_v = sheet_y;
+			sheet_x += size_x + padding;
+		}
+		sheet_y += row_size_y;
+		sheet_height = pot_ceil(sheet_y);
+	}
+
+	//Paste all the glyphs onto the texture and calculate the render data
+	locations.clear();
+	Image* glyphsImage = new Image(sheet_width, sheet_height);
+	for (glyph_list_t::iterator it = glyphData.begin(); it != glyphData.end(); it++) {
+		GlyphData& gd = **it;
+		int size_x = gd.size_x + ((1<<shift)-1) >> shift;
+		int size_y = gd.size_y + ((1<<shift)-1) >> shift;
+		for (int glyph_y = 0, i = 0; glyph_y < size_y; glyph_y++) {
+			for (int glyph_x = 0; glyph_x < size_x; glyph_x++, i++) {
+				unsigned char value = gd.data[i];
+				int x = gd.texture_u + glyph_x;
+				int y = gd.texture_v + glyph_y;
+				if (mode == ALPHA_TEST) {
+					const int SEARCH_RANGE = 2;
+					//Don't quite use the full range of 128 either side
+					const Number ALPHA_SCALE = 112.0f / (SEARCH_RANGE << shift);
+					int scan_x0 = glyph_x - SEARCH_RANGE << shift;
+					int scan_x1 = glyph_x + SEARCH_RANGE << shift;
+					int scan_y0 = glyph_y - SEARCH_RANGE << shift;
+					int scan_y1 = glyph_y + SEARCH_RANGE << shift;
+					if (scan_x0 < 0) scan_x0 = 0;
+					if (scan_y0 < 0) scan_y0 = 0;
+					if (scan_x1 >= gd.size_x) scan_x1 = gd.size_x - 1;
+					if (scan_y1 >= gd.size_y) scan_y1 = gd.size_y - 1;
+
+//					value = (gd.data[glyph_y * gd.pitch + (glyph_x>>3)] >> (7-(glyph_x&7))) & 1;
+					value = (gd.data[(glyph_y<<shift) * gd.pitch + (glyph_x<<shift)]>>7) & 1;
+					Number dist = SEARCH_RANGE << shift;
+					for (int scan_y = scan_y0; scan_y <= scan_y1; scan_y++) {
+						int dy = (glyph_y<<shift) - scan_y;
+						int dy2 = dy*dy;
+						for (int scan_x = scan_x0; scan_x <= scan_x1; scan_x++) {
+							int dx = (glyph_x<<shift) - scan_x;
+//							int v = (gd.data[scan_y * gd.pitch + (scan_x>>3)] >> (7-(scan_x&7))) & 1;
+							int v = (gd.data[scan_y * gd.pitch + scan_x] >> 7) & 1;
+							if (v != value) {
+								int dx2 = dx*dx;
+								Number d(sqrt(dx2+dy2));
+								if (d < dist) {
+									dist = d;
+								}
+							}
+						}
+					}
+					value = (int)round(128 + ((value+value)-1) * (dist * ALPHA_SCALE));
+//					value *= 255;
+				}
+				glyphsImage->setPixel(x, y, Color(255, 255, 255, value));
+			}
+		}
+		Number x0 = gd.off_x * scaleDown;
+		Number y0 = gd.off_y * scaleDown;
+		Number x1 = x0 + gd.size_x * scaleDown;
+		Number y1 = y0 - gd.size_y * scaleDown;
+		Number u0 = Number(gd.texture_u) / sheet_width;
+		Number v0 = 1.0f - Number(gd.texture_v) / sheet_height;
+		Number u1 = Number(gd.texture_u + size_x) / sheet_width;
+		Number v1 = 1.0f - Number(gd.texture_v + size_y) / sheet_height;
+		FontTextureGlyph& glyph = locations[gd.character];
+		glyph.offset[0].set(x0, y0);
+		glyph.offset[1].set(x0, y1);
+		glyph.offset[2].set(x1, y1);
+		glyph.offset[3].set(x1, y0);
+		glyph.texCoord[0].set(u0, v0);
+		glyph.texCoord[1].set(u0, v1);
+		glyph.texCoord[2].set(u1, v1);
+		glyph.texCoord[3].set(u1, v0);
+		glyph.advance = gd.advance;
+	}
+
+	MaterialManager *materialManager = CoreServices::getInstance()->getMaterialManager();
+	if(texture)
+		materialManager->deleteTexture(texture);
+
+	texture = materialManager->createTextureFromImage(glyphsImage, true, materialManager->mipmapsDefault);
+	delete glyphsImage;
+	for (glyph_list_t::iterator it = glyphData.begin(); it != glyphData.end(); it++) delete *it;
+}
+
+int FontGlyphSheet::renderStringVertices(String textIn, std::vector<Vertex*>& vertices, int index) {
+	textIn.getWDataWithEncoding(String::ENCODING_UTF8);
+	std::wstring& text = textIn.w_contents;
+
+	Vector2 cursor;
+	wchar_t prevChar = -1;
+	for (std::wstring::const_iterator it = text.begin(); it != text.end(); it++) {
+
+		std::map<wchar_t,FontTextureGlyph>::iterator glyphLoc = locations.find(*it);
+		if (glyphLoc == locations.end()) {
+			Logger::log("Missing glyph for codepoint %d '%lc'\n",*it,*it);
+			glyphLoc = locations.find('?');
+		}
+
+//		if (prevChar != -1) {
+//			FT_Vector delta;
+//			FT_Get_Kerning( ftFace, FT_Get_Char_Index(ftFace, prevChar), FT_Get_Char_Index(ftFace, *it), FT_KERNING_DEFAULT, &delta);
+//			cursor.x += delta.x / Number(64);
+//		}
+
+		for (int i = 0; i < 4; i++, index++) {
+			Vertex* vertex;
+			if (index == vertices.size()) {
+				vertices.push_back(vertex = new Vertex());
+			}
+			else {
+				vertex = vertices[index];
+			}
+			vertex->set(cursor.x + glyphLoc->second.offset[i].x, cursor.y + glyphLoc->second.offset[i].y, 0);
+			vertex->texCoord = glyphLoc->second.texCoord[i];
+		}
+		cursor += glyphLoc->second.advance;
+
+		prevChar = *it;
+	}
+	return index;
+}
+
+
+

+ 45 - 0
Core/Contents/Source/PolyTextMesh.cpp

@@ -0,0 +1,45 @@
+
+#include "PolyTextMesh.h"
+#include "PolyFontGlyphSheet.h"
+
+using namespace Polycode;
+
+TextMesh::TextMesh(FontGlyphSheet *font, const String &text)
+:	Mesh(QUAD_MESH)
+,	font(font)
+,	text(text)
+{
+	rebuild();
+}
+
+void TextMesh::setFont(FontGlyphSheet *font) {
+	this->font = font;
+	rebuild();
+}
+
+void TextMesh::setText(const String &text) {
+	if (text == this->text) return;
+	this->text = text;
+	rebuild();
+}
+
+void TextMesh::rebuild() {
+	arrayDirtyMap[RenderDataArray::VERTEX_DATA_ARRAY] = true;
+	arrayDirtyMap[RenderDataArray::TEXCOORD_DATA_ARRAY] = true;
+
+	if (text == "" || font == NULL) {
+		for (std::vector<Vertex*>::iterator it = vertices.begin(); it != vertices.end(); it++) delete *it;
+		vertices.clear();
+		return;
+	}
+
+	int last = font->renderStringVertices(text, vertices);
+	if (last < vertices.size()) {
+		for (int i = last; i < vertices.size(); i++) {
+			delete vertices[i];
+		}
+		vertices.resize(last);
+	}
+}
+
+