A signed distant font generator and a runtime helper for MacOSX and iOS that utilize Metal and CoreText. This is a retake to my original SDFont, which depends on OpenGL & FreeType.
SDFontApple comes with the following components:
- SDFont.xcframework: The main framework for macos and iOS.
- sdfontgen: The command-line font generator for macos.
- SWOpeningRoll: A sample program for macos and iOS that shows the 3D opening roll of Star Wars film in the signed distance fonts.
- SWOpeningRollAR: A sample program for iOS that shows the opening roll in the AR environment anchored to the tapped position in the space.
Demo Video 1 : Video capture from the demo App SWOpeningRoll on Mac mini M1 2020.
Demo Video 2 : Video capture from the demo App SWOpeningRollAR on iPhone 13 mini.
Demo Video 3 : Demo Video of Sticky Notes implemented in AR together with the built-in speech recognizer.
- About Signed Distance Fonts
- Advantages of Signed Distance Fonts
- Build & Instsall
- Usage
- License
- Contact
- Background
- Acknowledgment
This is an awesome technique to render typefaces by GPU. It was originally proposed by Chris Green of Valve at SIGGRAPH 2007 [PDF]. In the normal font rendering by CPU, say, with CoreText or FreeType, the shapes of the glyphs are stored in the vector graphics format (Bezier curves), and at runtime, they are converted to a bitmap for a specific font size.
On the contrary, the signed distant fonts techinque generates a texture for the glyphs in the signed distance representation in which the value of each pixel represents the distance from the closest glyph boundary. The glyphs of the same typeface are usually packed into a single texture, and each glyph gets the corresponding rectangular bounding box in the texture coordinates. This texture generation is usually done as an off-line processing. At runtime, the glyphs are rendered simply by quads specifying the bounding boxes in the texture uv-coordinates to the GPU.
The following image shows a sample signed distance texture. It shows the first 256 glyphs in Helvetica font with the inner and outer bounding boxes for each glyph. (Click to magnify.)
- âś… Good visual quality for the wide range of font sizes with a single texture file. No noticeable jagged edges when magnified.
- âś… Little overhead for rendering at runtime. No need to convert the vector graphics to a bitmap, as the glyphs have been in a way already rendered to the texture.
- âś… Dynamic transformation of the typefaces at runtime. You can move, scale, rotate, sheer the typefaces just like the geometric transfomation for the other rendered objects.
- âś… Typographic effects by the fragment shader. They can be achieved by applying a function to the sampled value from the signed distance texture. In the following sub-section you can see some sample effects with their corresponding fragment functions.
You can change the appearance of the rendered typefaces by changing the output alpha value in the fragment shader. The following shows some notable effects with their corresponding functions to alter the alpha value. Please see the fragment shader code for details.
A drop shadow can be implemented by two render passes. In the example above, the drop shadow is rendered first by the smooth step shader in black, and then the typeface is rendered by another smooth step shader with a narrower band in brown.
- Open
SDFontApple/SDFont/SDFont.xcodeproj
, go to the projectSDFont
, and select the targetSDFont
. - Set Signing & Capabilities to your environment for your developer ID.
- Select the scheme
SDFont
, and the taret to your device (ex. My Mac or iPhone 13 mini), and build.
Alternatively, after the signing & capabilities have been setup for you, you can also run the build process with the following batch script.
- Open a terminal
$ xcode-select -s <path/to/Xcode.app>
first if necessary.$ cd SDFontApple/SDFont
$ ./build_all.sh
- During the execution of the shell script you will be asked if you want to install the macos version to /Library/Frameworks.
Do you wish to install SDFont.framework--macosx to this Mac under /Library/Frameworks?
Answer Y/n.
This will create build/SDFont.scframework, build/sdfontgen, and ./SDFont.doccarchive.
- Open
SDFontApple/SDFont/SDFont.xcodeproj
, go to the projectSDFont
, and select the targetsdfontgen
. - Set Signing & Capabilities to your environment for your developer ID.
- Select the scheme
sdfontgen
, and the target to your device (ex. My Mac), and build.
- Open
SDFontApple/SWOpeningRoll/SWOpeningRoll.xcodeproj
, go to the projectSWOpeningRoll
, and select the targetSWOpeningRollmacos
for macOS or the target 'SWOpeningRollios` for iOS. - Set Signing & Capabilities to your environment for your developer ID.
- Adjust the path to the framework SDFont if necessary.
- Select the scheme
SWOpeningRollmacos
orSWOpeningRollios
, and the target to your device (ex. My Mac or iPhone 13 mini), and build & run.
- Open
SDFontApple/SWOpeningRollAR/SWOpeningRollAR.xcodeproj
, go to the projectSWOpeningRollAR
, and select the targetSWOpeningRollAR
. - Set Signing & Capabilities to your environment for your developer ID.
- Adjust the path to the framework SDFont if necessary.
- Select the scheme
SWOpeningRollAR
, and the target to your device (ex. iPhone 13 mini), and build & run.
An API reference is available in docc archive, which can be found in SDFontApple/SDFont/SDFont.doccarchive
if you have run SDFontApple/SDFont/build_all.sh
.
On macos, run sdfontgen -showfontlist
.
mrbean@Beans-Mini % pwd
/Users/mrbean/repos/SDFontApple/SDFont/build
mrbean@Beans-Mini % ./sdfontgen -showfontlist
[..SFCompactRounded-Regular]
[.AlBayanPUA]
[.AlBayanPUA-Bold]
[.AlNilePUA]
...
[YuppySC-Regular]
[YuppyTC-Regular]
[ZapfDingbatsITC]
[Zapfino]
For iOS and macos, you can get the list programatically by calling the following API function.
SDFontRuntimeHelper {
static func listAvailableFonts() -> [String]
}
Please consult https://developer.apple.com/fonts/ for the official information from Apple.
You can use the command-line tool sdfontgen
.
mrbean@Beans-Mini % ./sdfontgen -h
sdfontgen : command-line signed distance font generator for macos.
options
-h/--help: show this message
-verbose: show INFO and WARNING messages
-showfontlist: show the list of the fonts in the system
-fontname <fontname>: name of the font, preferrably in Postscript name,
e.g. -fontname Helvetica
-glyphnumcutoff <integer num>: maximum index of the glyphs to process.
-spread <real num>: specifies the margin around each glyph in the fraction
of the average glyph width and height. Usually within the range of [0.1,0.2].
-upsample <integer num>: specifies the fontsize in the integer multiple to the original
font size, in order to sample the glyph bitmaps. The original fontsize
is determined as the best size to pack the glyphs to the output texture of
the specified size. For example if the side length of the output texture
is 2048, and the font is Helvecita with about 2000 glyphs, then the best
font size will be 43.0. If the upsample factor is 4, then the font size 172.0
is used to sample the glyph bitmap and to generate signed distance.
Usually within the range of [2,4].
-texturesize <integer num>: the length of the sides in pixels of the output texture.
Usually within the range of [512,4096].
-outputpath <path>: the path in which the output PNG and JSON files are stored.
If the outputpath is "/path/to/output", and fontname is "Helvetica",
then the output files will be /path/to/output/Helvetica.png and /path/to/output/Helvetica.json.
NOTES on -glyphnumcutoff:
This is useful for example, if you want a small texture size for some games,
and you know you use only the first 256 glyphs. However, you should be careful
as the CoreText's typesetter may select a ligature glyph such as 'fi' and 'ff'
whose indices are above 255, even if you use only English alphabets.
For a Signed Distance font for Helvetica-Bold, the output texture size of 2048x2048:
mrbean@Beans-Mini % ./sdfontgen -verbose -fontname Helvetica-Bold -spread 0.2 -upsample 4 -texturesize 2048 -outputpath .
sdfontgen : generating signed distance fonts with the following parameters.
font name: [Helvetica-Bold]
glyph number cut-off: [0]
spread: [0.2]
up-sample factor: [4]
texture size: [2048]
output path: [.]
INFO: Number of glyphs in font Helvetica-Bold: [2252].
INFO: Number of glyphs after cut-off: [2252].
INFO: Reference Font size: [42.0].
INFO: Spread in pixels: [5.122411719096914]
INFO: Occupancy Rate: [0.7282965183258057]
INFO: Up-sampled per-glyph texture side length: [288]
INFO: Up-sampled font size: [168.0]
INFO: Bounding box for glyph 1 is degenerate. Skipping.
...
INFO: Bounding box for glyph 1327 is degenerate. Skipping.
INFO: Total processing time: 4.357048988342285 seconds.
INFO: PNG file path to write [./Helvetica-Bold.png -- file:///Users/mrbean/repo/SDFontApple/SDFont/build/]:
INFO: JSON file path to write [./Helvetica-Bold.json -- file:///Users/mrbean/repo/SDFontApple/SDFont/build/]:
sdfontgen: finished processing.
mrbean@Beans-Mini % ls -l Helvetica-Bold.*
-rw-r--r--@ 1 mrbean museumstaff 243331 Oct 25 02:15 Helvetica-Bold.json
-rw-r--r-- 1 mrbean museumstaff 903737 Oct 25 02:15 Helvetica-Bold.png
This will generate two files: Helvetica-Bold.png and Helvetica-Bold.json.
You can use the class SDFontGenerator
as follows.
Following is a gist of what you should do.
import SDFont
let sdGenerator = SDFontGenerator(
device : MTLCreateSystemDefaultDevice()!,
fontName : "Helvetica-Bold",
outputTextureSideLen : 2048
spreadFactor : 0.2,
upSamplingFactor : 4,
glyphNumCutoff : 0, // 0 means no limit
verbose : true
usePosixPath : false // for iOS and macOS GUI Apps, set it false. For macos commandline Apps, which has no access-limits/sand-boxing to the file system, set true.
)
This will generate types of data: a generated signed distance texture, and a list of bounding boxes.
To retrieve them, you can either save them to the specified files in PNG and JSON,
let rtn1 = sdGenerator.writeToPNGFile(fileName: fontName, path: outputPath ) // This cannot be called, if generateMTLTexture() has been already called.
if !rtn1 {
print ("ERROR: cannot write PNG file [\(outputPath)/\(fontName).png]")
}
let rtn2 = sdGenerator.writeMetricsToJSONFile(fileName: fontName, path: outputPath )
if !rtn2 {
print ("ERROR: cannot write JSON file [\(outputPath)/\(fontName).json]")
}
or you can retrieve them as MTLTexture and [CGRect].
let texture : MTLTexture? = sdGenerator.generateMTLTexture() // Generated signed distance texture.
let bounds : [CGRect] = sdGenerator.generateTextureBounds() // List of bounding boxes for the glyphs.
You can use the class SDFontRuntimeHelper
.
Following is a gist of what you should do.
// from the PNG/JSON file pair:
let helper = SDFontRuntimeHelper(
device : MTLCreateSystemDefaultDevice()!,
fontName : "Helvecita-Bold",
fontSize : 12,
fileName : "Helvetica-Bold",
path : nil,
usePosixPath : false,
verbose : Bool
)
// from the instance of SDFontGenerator
let helper = SDFontRuntimeHelper( generator: sdGenerator, fontSize: 12, verbose: true )
After instantiating the SDFontRuntimeHelper you can perform the following.
// get the signed distance texture in MTLTexture
let texture = helper.texture()
// perform typesetting.
let bounds : [GlyphBound] = helper.typeset(
frameRect : CGRectMake( -150, -150, 300, 300),
textPlain : "Gesha coffee, sometimes referred to as Geisha coffee, is a variety of coffee tree that originated in the Gori Gesha forest, Ethiopia."
lineAlignment : .left // CTTextAlignment
)
The typeset data are provied in the array of GlyphBound
.
struct GlyphBound {
let frameBound : CGRect // The rectangular bounding box in the specified drawing area.
let textureBound : CGRect // The rectangular bounding box of the glyph in the texture coodinate space.
}
Each glyph is represented by GlyphBound. Please note that there is no one-by-one mapping between the character sequence of the string you specified to textPlain
and the glyph sequence in the list in bounds'. The member
frameBound` is used to generate the coordinates for the quad (or two triangles), and 'textureBound' for their texture uv-coordinates.
A sample for mesh generation and text rendering with Metal can be found in SDTextPlane.generateVerticesAndIndices()
in SWOpeningRoll/shared/SDTextPlane.swift.
GPL v3.
Commercial use with proper attribution can be considered. Especially for indy developers and small businesses owners a very reasonable licensing can be arranged.
For technical and commercial inquiries, please contact: Shoichiro Yamanishi
Especially if you are an indy develper, please feel free to contact me.
This is a retake to my SDFont, which depends on OpenGL & libfreetype. SDFont used to work for macOS but not anymore since Apple stopped supporting OpenGL. Also, I wanted to port SDFont to iOS but I quickly found it was difficult to use FreeType on iOS as the font files are inaccessible to the user Apps. On the other hand, Apple eco-system provides an excellent typesetting framework called CoreText, and a GPU computing facility called Metal Compute shaders. I wanted to utilize those Apple-specific technologies for signed distance fonts.
This lack of support for OpenGL and libfreetype and availability of CoreText and Metal have motivated me to develop another SDFont framework for the Apple eco-system.
Special thanks to Warren Moore for the accompanying sample code to his excellent book Metal by Example I purchased. I learned a lot about CoreText from his obj-c code, especially how the various coordinate systems in CoreText work together with CoreGraphics. His signed font generator uses a dynamic-programming algorithm (essentially a variant of Dijkstra), which is difficult to parallelize. My implementation uses Metal Compute and a per-pixel brute-force vicinity search, which can be parallelized in GPU.