/* Vector font tool - Gaz Davidson December 2006-2012 I noticed bitmap fonts were taking massive amounts of video memory at reasonable sizes, so I decided to make a vector font. I always wanted to try converting pixels to triangles... And I failed! This is a collection of the ugliest, bloated, most inneficient algorithms i've ever written, but its kinda working so I'm not changing it. */ #ifndef __VECTOR_FONT_TOOL_INCLUDED__ #define __VECTOR_FONT_TOOL_INCLUDED__ #include "irrlicht.h" #include "CFontTool.h" #include using namespace irr; using namespace video; struct STriangleList { core::array positions; core::array indexes; // for adding one triangle list to another, // these triangles share positions, but dont share triangles STriangleList& operator+=(STriangleList &other) { core::matrix4 m; core::array map; map.set_used(other.positions.size()); for (u32 i=0; i positions; bool isMember(s32 x, s32 y) { for (u32 i=0; i h1((f32)(positions[i-1].X - positions[i].X),(f32)(positions[i-1].Y - positions[i].Y)), h2((f32)(positions[i].X - positions[i+1].X),(f32)(positions[i].Y - positions[i+1].Y)); h1.normalize(); h2.normalize(); if (h1==h2) // erase the current point positions.erase(i--); } // level 1- if point1 points at point3, we can skip point2 // level 2+ allow a deviation of level-1 } }; // contains an array of lines for triangulation struct SLineList { core::array lines; SLineList() : lines() { } void addEdge(const SEdge &edge) { // adds lines to the buffer for (u32 i=1; i 0 && lb.getPointOrientation(l2.start) > 0 && lc.getPointOrientation(l2.start) > 0) return true; //if (la.getPointOrientation(l2.start) < 0 && // lb.getPointOrientation(l2.start) < 0 && // lc.getPointOrientation(l2.start) < 0) // return true; core::vector2df out; //if (la.intersectWith(l2,out)) // if (out != la.start && out != la.end && // out != l2.start && out != l2.end) // return true; if (lb.intersectWith(l2,out)) if (!out.equals(lb.start) && !out.equals(lb.end) && !out.equals(l2.start) && !out.equals(l2.end)) return true; if (lc.intersectWith(l2,out)) if (!out.equals(lc.start) && !out.equals(lc.end) && !out.equals(l2.start) && !out.equals(l2.end)) return true; // my shit intersection code only works with lines in certain directions :( if (l2.intersectWith(lb,out)) if (!out.equals(lb.start) && !out.equals(lb.end) && !out.equals(l2.start) && !out.equals(l2.end)) return true; if (l2.intersectWith(lc,out)) if (!out.equals(lc.start) && !out.equals(lc.end) && !out.equals(l2.start) && !out.equals(l2.end)) return true; if (lb.isPointOnLine(l2.start) && l2.start != lb.start && l2.start != lb.end) return true; if (lc.isPointOnLine(l2.start) && l2.start != lc.start && l2.start != lc.end) return true; } return false; } }; // an area of adjacent pixels struct SPixelGroup { SPixelGroup(IrrlichtDevice *device) : triangles(), pixelWidth(0), pixelHeight(0), Device(device) {} core::array pixels; core::array edges; STriangleList triangles; core::array ll; core::array isMemberCache; s32 pixelWidth; s32 pixelHeight; IrrlichtDevice *Device; void triangulate() { // find edges in this group makeEdges(); // triangulate the group makeTriangles(); } void drawTriangle( core::line2df line, core::vector2df point) { //const u32 endt = Device->getTimer()->getTime() + t; f32 scale = 5; //while(Device->getTimer()->getTime() < endt ) //{ Device->run(); Device->getVideoDriver()->beginScene(video::ECBF_COLOR | video::ECBF_DEPTH, video::SColor(0,0,0,0)); for (u32 v=0;vgetVideoDriver()->draw2DLine(st,en, SColor(255,255,255,255)); } // draw this triangle const core::position2di st((s32)(line.start.X*scale)+50, (s32)(line.start.Y*scale)+50); const core::position2di en((s32)(line.end.X*scale)+50, (s32)(line.end.Y*scale)+50); const core::position2di p((s32)(point.X*scale)+50, (s32)(point.Y*scale)+50); Device->getVideoDriver()->draw2DLine(st,en, SColor(255,255,0,0)); Device->getVideoDriver()->draw2DLine(en,p, SColor(255,0,255,0)); Device->getVideoDriver()->draw2DLine(p,st, SColor(255,0,0,255)); Device->getVideoDriver()->endScene(); //} } void makeTriangles() { // make lines from edges, because they're easier to deal with ll.clear(); for (u32 i=0; i < edges.size(); ++i) { SLineList l; l.addEdge(edges[i]); ll.push_back(l); } // add an extra one for inside edges SLineList innerlines; ll.push_back(innerlines); // loop through each edge and make triangles for (u32 i=0; ibestScore) { bestScore = score; bestEdge = k; bestPoint = j; } } // hopefully we found one if (bestEdge >= 0 && bestPoint >= 0 && bestScore >= 0.0f) { //assert(bestEdge >= 0 && bestPoint >= 0); //assert(bestScore >= 0.0f); core::vector2df point(ll[bestEdge].lines[bestPoint].start.X, ll[bestEdge].lines[bestPoint].start.Y); // add it to the triangles list triangles.add(currentLine.start, currentLine.end, point); // add inner lines to the line buffer, but only if they arent in others core::line2df la(point,currentLine.start); core::line2df lb(currentLine.end,point); bool found = false; for (u32 lineno=0;lineno= 3); // all edges should be closed assert(edges[i].positions[0] == edges[i].positions[edges[i].positions.size()-1] ); } } // adds a line to the edges arrays void addToEdges(s32 x1, s32 y1, s32 x2, s32 y2) { bool found=false; // loop through each edge for (u32 i=0; ipixelWidth || y>pixelHeight || x<0 || y<0) return false; else return isMemberCache[pixelWidth*y + x]; } void refreshIsMemberCache() { isMemberCache.clear(); pixelWidth=0; pixelHeight=0; for (u32 i=0; ipixelWidth) pixelWidth=pixels[i].X; if (pixels[i].Y>pixelHeight) pixelHeight=pixels[i].Y; } pixelWidth+=2; pixelHeight+=2; isMemberCache.set_used(pixelWidth*pixelHeight+1); for (u32 i=0; igetTimer()->getTime(); const u32 endt = stt + t; while(device->getTimer()->getTime() < endt ) { const f32 phase = f32((device->getTimer()->getTime()-stt) % 500) / 500.0f; device->run(); device->getVideoDriver()->beginScene(video::ECBF_COLOR | video::ECBF_DEPTH, video::SColor(0,0,0,0)); for (u32 g=0;ggetVideoDriver()->draw2DLine(st,en); device->getVideoDriver()->draw2DLine(st,ep,video::SColor(255,255,0,0) ); } device->getVideoDriver()->endScene(); } } void drawTriangles(IrrlichtDevice *device, u32 t, s32 scale) { const u32 stt = device->getTimer()->getTime(); const u32 endt = stt + t; while(device->getTimer()->getTime() < endt ) { const f32 phase = f32((device->getTimer()->getTime()-stt) % 500) / 500.0f; device->run(); device->getVideoDriver()->beginScene(video::ECBF_COLOR | video::ECBF_DEPTH, video::SColor(0,0,0,0)); for (u32 g=0;ggetVideoDriver()->draw2DLine(st,en, SColor(255,255,0,0)); st = core::position2di((s32)(t.positions[t.indexes[v+1]].X*scale)+50,(s32)(t.positions[t.indexes[v+1]].Y*scale)+50); en = core::position2di((s32)(t.positions[t.indexes[v+2]].X*scale)+50,(s32)(t.positions[t.indexes[v+2]].Y*scale)+50); device->getVideoDriver()->draw2DLine(st,en, SColor(255,0,255,0)); st = core::position2di((s32)(t.positions[t.indexes[v+2]].X*scale)+50,(s32)(t.positions[t.indexes[v+2]].Y*scale)+50); en = core::position2di((s32)(t.positions[t.indexes[v+0]].X*scale)+50,(s32)(t.positions[t.indexes[v+0]].Y*scale)+50); device->getVideoDriver()->draw2DLine(st,en, SColor(255,0,0,255)); } device->getVideoDriver()->endScene(); } } void drawTriLines(IrrlichtDevice *device, u32 t, s32 scale) { const u32 endt = device->getTimer()->getTime() + t; while(device->getTimer()->getTime() < endt ) { device->run(); device->getVideoDriver()->beginScene(video::ECBF_COLOR | video::ECBF_DEPTH, video::SColor(0,0,0,0)); for (u32 g=0;ggetVideoDriver()->draw2DLine(st,en, SColor(255,255,0,0)); } device->getVideoDriver()->endScene(); } } void drawTri3D(IrrlichtDevice *device, u32 t) { for (u32 g=0;g verts; verts.clear(); for(u32 v=0; v< t.positions.size(); ++v) { verts.push_back(S3DVertex( -t.positions[v].X, -t.positions[v].Y, -100, 0,0,1,SColor(255,255,255,255),0,0)); } device->getVideoDriver()->drawIndexedTriangleList(verts.pointer(),verts.size(),t.indexes.pointer(), t.indexes.size()/3 ); } } // process all pixels void findGroups() { for (int y=0; y0) // look one behind { grp = getRef(x-1,y); if (grp) found=true; } if (y>0) // look above { if (x>0) // top left { g = getRef(x-1,y-1); if (g) { if (found) { mergeGroups(grp, g); } else { grp = g; found = true; } } } if (x groups; core::array groupRefs; core::array refbuffer; bool *mem; IrrlichtDevice *Device; }; // creates a simple vector font from a bitmap from the font tool class CVectorFontTool { public: CVectorFontTool(CFontTool *fonttool) : triangulator(0), FontTool(fonttool), letterHeight(0), letterWidth(0), triangles() { core::map::Iterator it = FontTool->CharMap.getIterator(); while(!it.atEnd()) { CFontTool::SFontArea &fa = FontTool->Areas[(*it).getValue()]; if (fa.rectangle.getWidth() > letterWidth) letterWidth = fa.rectangle.getWidth(); if (fa.rectangle.getHeight() > letterHeight) letterHeight = fa.rectangle.getHeight(); it++; } // number of verts is one more than number of pixels because it's a grid of squares letterWidth++; letterHeight++; // create image memory imagedata.set_used(letterWidth*letterHeight); // create vertex list, set position etc verts.set_used(letterWidth*letterHeight); for (s32 y=0; yCharMap.getIterator(); while(!it.atEnd()) { addChar((*it).getKey()); it++; } } ~CVectorFontTool() { if (triangulator) delete triangulator; } void addChar(wchar_t thischar) { const s32 area = FontTool->CharMap[thischar]; const CFontTool::SFontArea &fa = FontTool->Areas[area]; const s32 img = fa.sourceimage; const core::rect& r = fa.rectangle; // init image memory IImage *image = FontTool->currentImages[img]; for (u32 i=0; i < imagedata.size(); ++i) imagedata[i] = false; for (s32 y=r.UpperLeftCorner.Y; y < r.LowerRightCorner.Y; ++y) { for (s32 x=r.UpperLeftCorner.X; x < r.LowerRightCorner.X ; ++x) if (image->getPixel(x,y).getBlue() > 0) { imagedata[letterWidth*(y-r.UpperLeftCorner.Y) +(x-r.UpperLeftCorner.X)] = true; } } // get shape areas triangulator = new CGroupFinder(imagedata.pointer(), letterWidth, letterHeight, FontTool->Device ); wprintf(L"Created character '%c' in texture %d\n", thischar, img ); //floodfill->drawEdges(FontTool->Device, 500, 3); //floodfill->drawTriangles(FontTool->Device, 500,30); //floodfill->drawTriLines(FontTool->Device, 200,3); /* if (area==32 && map == 0) { scene::ISceneManager *smgr = FontTool->Device->getSceneManager(); smgr->addCameraSceneNodeFPS(); while(FontTool->Device->run()) { //floodfill->drawEdges(FontTool->Device, 100, 30); FontTool->Device->getVideoDriver()->beginScene(true, true, video::SColor(0,200,200,200)); smgr->drawAll(); floodfill->drawTri3D(FontTool->Device, 100); FontTool->Device->getVideoDriver()->endScene(); } }*/ u32 lastind = triangles.indexes.size(); // loop through each shape and add it to the current character... for (u32 i=0; i < triangulator->groups.size(); ++i) triangles += triangulator->groups[i].triangles; // add character details charstarts.push_back(lastind); charlengths.push_back(triangles.indexes.size() - lastind); chars.push_back(thischar); } bool saveVectorFont(const c8 *filename, const c8 *formatname) { IrrlichtDevice *Device = FontTool->Device; if (triangles.indexes.size() == 0) { Device->getLogger()->log("No vector data to write, aborting."); return false; } core::stringc fn = filename; if (core::stringc(formatname) == core::stringc("xml")) { fn += ".xml"; io::IXMLWriter *writer = FontTool->Device->getFileSystem()->createXMLWriter(fn.c_str()); // header and line breaks writer->writeXMLHeader(); writer->writeLineBreak(); // write info header writer->writeElement(L"font", false, L"type", L"vector"); writer->writeLineBreak(); writer->writeLineBreak(); // write each letter for (u32 n=0; nCharMap[chars[n]]; CFontTool::SFontArea &fa = FontTool->Areas[i]; wchar_t c[2]; c[0] = chars[n]; c[1] = L'\0'; core::stringw area, under, over; area = core::stringw(fa.rectangle.LowerRightCorner.X- fa.rectangle.UpperLeftCorner.X); area += L", "; area += fa.rectangle.LowerRightCorner.Y- fa.rectangle.UpperLeftCorner.Y; core::array names; core::array values; names.clear(); values.clear(); // char names.push_back(core::stringw(L"c")); values.push_back(core::stringw(c)); // width+height names.push_back(core::stringw(L"wh")); values.push_back(area); // start names.push_back(core::stringw(L"st")); values.push_back(core::stringw(charstarts[n])); // length names.push_back(core::stringw(L"len")); values.push_back(core::stringw(charlengths[n])); if (fa.underhang != 0) { under = core::stringw(fa.underhang); names.push_back(core::stringw(L"u")); values.push_back(under); } if (fa.overhang != 0) { over = core::stringw(fa.overhang); names.push_back(core::stringw(L"o")); values.push_back(over); } writer->writeElement(L"c", true, names, values); writer->writeLineBreak(); } // write vertex data core::stringw data, count; data = L""; count = core::stringw(triangles.positions.size()); for (u32 i=0; iwriteElement(L"Vertices", true, L"count", count.c_str(), L"data", data.c_str()); writer->writeLineBreak(); // write index list data = L""; count = core::stringw(triangles.indexes.size()); for (u32 i=0; iwriteElement(L"Indices", true, L"count", count.c_str(), L"data", data.c_str()); writer->writeLineBreak(); writer->writeClosingTag(L"font"); writer->drop(); Device->getLogger()->log("Font saved."); return true; } else if (core::stringc(formatname) == core::stringc("bin")) { FontTool->Device->getLogger()->log("binary fonts not supported yet, sorry"); return false; } else { FontTool->Device->getLogger()->log("unsupported file format, unable to save vector font"); return false; } } S3DVertex& getVert(s32 x, s32 y) { return verts[letterWidth*y +x]; } core::array verts; core::array inds; core::array imagedata; core::array charstarts; // start position of each char core::array charlengths; // index count core::array chars; // letters CGroupFinder* triangulator; CFontTool* FontTool; s32 letterHeight; s32 letterWidth; STriangleList triangles; }; #endif // __VECTOR_FONT_TOOL_INCLUDED__