-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Dynamic Runtime Rasterized MSDF Sprite Font #3055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 26 commits
d49b9ff
3275c16
5d9185f
75f5369
1fa1cd4
2f0b165
dd4f8e0
0dc668c
69e2f15
7ad4caa
b04c959
d5d9576
18e227d
217a8fd
e2e9f09
ae2e234
a0058c4
914819d
048b521
d7d8d5b
dbba408
a22eb17
e455a49
cc65561
4d9159c
dd6b53a
738280e
92e5460
739c69e
0bea15f
6a98305
a457fd6
3463072
113d16d
f2930a2
3176a76
dcfae4b
f8aadd1
0753019
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) | ||
| // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. | ||
|
|
||
| using System.ComponentModel; | ||
| using Stride.Core; | ||
| using Stride.Core.Annotations; | ||
| using Stride.Core.Mathematics; | ||
|
|
||
| namespace Stride.Assets.SpriteFont | ||
| { | ||
| [DataContract("RuntimeSignedDistanceFieldSpriteFontType")] | ||
| [Display("Runtime SDF")] | ||
| public class RuntimeSignedDistanceFieldSpriteFontType : SpriteFontTypeBase | ||
| { | ||
| /// <inheritdoc/> | ||
| [DataMember(30)] | ||
| [DataMemberRange(MathUtil.ZeroTolerance, 2)] | ||
| [DefaultValue(20)] | ||
| [Display("Default Size")] | ||
| public override float Size { get; set; } = 64; | ||
|
|
||
| /// <summary> | ||
| /// Distance field range/spread (in pixels) used during MSDF generation. | ||
| /// </summary> | ||
| [DataMember(40)] | ||
| [DefaultValue(8)] | ||
| [DataMemberRange(1, 64, 1, 4, 0)] | ||
| [Display("Pixel Range")] | ||
| public int PixelRange { get; set; } = 8; | ||
|
|
||
| /// <summary> | ||
| /// Extra padding around each glyph inside the atlas (in pixels). | ||
| /// </summary> | ||
| [DataMember(50)] | ||
| [DefaultValue(2)] | ||
| [DataMemberRange(0, 16, 1, 2, 0)] | ||
| [Display("Padding")] | ||
| public int Padding { get; set; } = 2; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,39 +40,55 @@ protected override void Prepare(AssetCompilerContext context, AssetItem assetIte | |
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new SignedDistanceFieldFontCommand(targetUrlInStorage, assetClone, assetItem.Package)); | ||
| } | ||
| else | ||
| if (asset.FontType is RuntimeRasterizedSpriteFontType) | ||
| else if (asset.FontType is RuntimeRasterizedSpriteFontType) | ||
| { | ||
| UFile fontPathOnDisk = asset.FontSource.GetFontPath(result); | ||
| if (fontPathOnDisk == null) | ||
| { | ||
| UFile fontPathOnDisk = asset.FontSource.GetFontPath(result); | ||
| if (fontPathOnDisk == null) | ||
| { | ||
| result.Error($"Runtime rasterized font compilation failed. Font {asset.FontSource.GetFontName()} was not found on this machine."); | ||
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new FailedFontCommand()); | ||
| return; | ||
| } | ||
|
|
||
| var fontImportLocation = FontHelper.GetFontPath(asset.FontSource.GetFontName(), asset.FontSource.Style); | ||
|
|
||
| result.Error($"Runtime rasterized font compilation failed. Font {asset.FontSource.GetFontName()} was not found on this machine."); | ||
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new ImportStreamCommand { SourcePath = fontPathOnDisk, Location = fontImportLocation }); | ||
| result.BuildSteps.Add(new RuntimeRasterizedFontCommand(targetUrlInStorage, asset, assetItem.Package)); | ||
| result.BuildSteps.Add(new FailedFontCommand()); | ||
| return; | ||
| } | ||
| else | ||
| { | ||
| var fontTypeStatic = asset.FontType as OfflineRasterizedSpriteFontType; | ||
| if (fontTypeStatic == null) | ||
| throw new ArgumentException("Tried to compile a non-offline rasterized sprite font with the compiler for offline resterized fonts!"); | ||
|
|
||
| // copy the asset and transform the source and character set file path to absolute paths | ||
| var assetClone = AssetCloner.Clone(asset); | ||
| var assetDirectory = assetAbsolutePath.GetParent(); | ||
| assetClone.FontSource = asset.FontSource; | ||
| fontTypeStatic.CharacterSet = !string.IsNullOrEmpty(fontTypeStatic.CharacterSet) ? UPath.Combine(assetDirectory, fontTypeStatic.CharacterSet): null; | ||
| var fontImportLocation = FontHelper.GetFontPath(asset.FontSource.GetFontName(), asset.FontSource.Style); | ||
|
|
||
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new ImportStreamCommand { SourcePath = fontPathOnDisk, Location = fontImportLocation }); | ||
| result.BuildSteps.Add(new RuntimeRasterizedFontCommand(targetUrlInStorage, asset, assetItem.Package)); | ||
| } | ||
| else if (asset.FontType is RuntimeSignedDistanceFieldSpriteFontType) | ||
| { | ||
| UFile fontPathOnDisk = asset.FontSource.GetFontPath(result); | ||
| if (fontPathOnDisk == null) | ||
| { | ||
| result.Error($"Runtime SDF font compilation failed. Font {asset.FontSource.GetFontName()} was not found on this machine."); | ||
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new OfflineRasterizedFontCommand(targetUrlInStorage, assetClone, colorSpace, assetItem.Package)); | ||
| result.BuildSteps.Add(new FailedFontCommand()); | ||
| return; | ||
| } | ||
|
|
||
| var fontImportLocation = FontHelper.GetFontPath(asset.FontSource.GetFontName(), asset.FontSource.Style); | ||
|
|
||
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new ImportStreamCommand { SourcePath = fontPathOnDisk, Location = fontImportLocation }); | ||
| result.BuildSteps.Add(new RuntimeSignedDistanceFieldFontCommand(targetUrlInStorage, asset, assetItem.Package)); | ||
| } | ||
| else | ||
| { | ||
| var fontTypeStatic = asset.FontType as OfflineRasterizedSpriteFontType; | ||
| if (fontTypeStatic == null) | ||
| throw new ArgumentException("Tried to compile a non-offline rasterized sprite font with the compiler for offline resterized fonts!"); | ||
|
|
||
| // copy the asset and transform the source and character set file path to absolute paths | ||
| var assetClone = AssetCloner.Clone(asset); | ||
| var assetDirectory = assetAbsolutePath.GetParent(); | ||
| assetClone.FontSource = asset.FontSource; | ||
| fontTypeStatic.CharacterSet = !string.IsNullOrEmpty(fontTypeStatic.CharacterSet) ? UPath.Combine(assetDirectory, fontTypeStatic.CharacterSet) : null; | ||
|
|
||
| result.BuildSteps = new AssetBuildStep(assetItem); | ||
| result.BuildSteps.Add(new OfflineRasterizedFontCommand(targetUrlInStorage, assetClone, colorSpace, assetItem.Package)); | ||
| } | ||
| } | ||
|
|
||
| internal class OfflineRasterizedFontCommand : AssetCommand<SpriteFontAsset> | ||
|
|
@@ -118,7 +134,7 @@ protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandC | |
| { | ||
| staticFont = OfflineRasterizedFontCompiler.Compile(FontDataFactory, Parameters, colorspace == ColorSpace.Linear); | ||
| } | ||
| catch (FontNotFoundException ex) | ||
| catch (FontNotFoundException ex) | ||
| { | ||
| commandContext.Logger.Error($"Font [{ex.FontName}] was not found on this machine.", ex); | ||
| return Task.FromResult(ResultStatus.Failed); | ||
|
|
@@ -190,9 +206,9 @@ public RuntimeRasterizedFontCommand(string url, SpriteFontAsset description, IAs | |
| protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandContext) | ||
| { | ||
| var dynamicFont = FontDataFactory.NewDynamic( | ||
| Parameters.FontType.Size, Parameters.FontSource.GetFontName(), Parameters.FontSource.Style, | ||
| Parameters.FontType.AntiAlias, useKerning:false, extraSpacing:Parameters.Spacing, extraLineSpacing:Parameters.LineSpacing, | ||
| defaultCharacter:Parameters.DefaultCharacter); | ||
| Parameters.FontType.Size, Parameters.FontSource.GetFontName(), Parameters.FontSource.Style, | ||
| Parameters.FontType.AntiAlias, useKerning: false, extraSpacing: Parameters.Spacing, extraLineSpacing: Parameters.LineSpacing, | ||
| defaultCharacter: Parameters.DefaultCharacter); | ||
|
|
||
| var assetManager = new ContentManager(MicrothreadLocalDatabases.ProviderService); | ||
| assetManager.Save(Url, dynamicFont); | ||
|
|
@@ -201,6 +217,40 @@ protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandC | |
| } | ||
| } | ||
|
|
||
| internal class RuntimeSignedDistanceFieldFontCommand : AssetCommand<SpriteFontAsset> | ||
| { | ||
| public RuntimeSignedDistanceFieldFontCommand(string url, SpriteFontAsset description, IAssetFinder assetFinder) | ||
| : base(url, description, assetFinder) | ||
| { | ||
| } | ||
|
|
||
| protected override Task<ResultStatus> DoCommandOverride(ICommandContext commandContext) | ||
| { | ||
| // M1 NOTE: | ||
| // This is a scaffolding step. We serialize a functional font so the pipeline works end-to-end. | ||
| // In M3/M4, this will be replaced by a real Runtime MSDF font object. | ||
| commandContext.Logger.Warning("Runtime SDF font is currently scaffolded in M1 (temporary). It will behave like a runtime raster font until the MSDF runtime generator is implemented."); | ||
|
|
||
| var runtimeSdfType = (RuntimeSignedDistanceFieldSpriteFontType)Parameters.FontType; | ||
|
|
||
| var sdfFont = FontDataFactory.NewRuntimeSignedDistanceField( | ||
| runtimeSdfType.Size, | ||
| Parameters.FontSource.GetFontName(), | ||
| Parameters.FontSource.Style, | ||
| runtimeSdfType.PixelRange, | ||
| runtimeSdfType.Padding, | ||
| useKerning: false, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why kerning is always off, on this call site and all other usage I could find
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The initial basis of this project is an exact copy of the runtime rasterized font that was already in the system. And kerning was set as off for it, so I just copied. |
||
| extraSpacing: Parameters.Spacing, | ||
| extraLineSpacing: Parameters.LineSpacing, | ||
| defaultCharacter: Parameters.DefaultCharacter); | ||
|
|
||
| var assetManager = new ContentManager(MicrothreadLocalDatabases.ProviderService); | ||
| assetManager.Save(Url, sdfFont); | ||
|
|
||
| return Task.FromResult(ResultStatus.Successful); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Proxy command which always fails, called when font is compiled with the wrong assets | ||
| /// </summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) | ||
| // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Data; | ||
| using Stride.Core; | ||
|
|
||
| namespace Stride.Graphics.Font | ||
| { | ||
| /// <summary> | ||
| /// An RGBA bitmap representing a glyph (4 bytes per pixel). | ||
| /// Intended for runtime MSDF font (stored in RGB, alpha optional). | ||
| /// </summary> | ||
| internal sealed class CharacterBitmapRgba : IDisposable | ||
| { | ||
| private readonly int width; | ||
| private readonly int rows; | ||
| private readonly int pitch; | ||
| private readonly IntPtr buffer; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't have to be an IntPtr, or have the class as disposable.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made another branch to use Color[] without the dispose, however, it doesn't seem to display anything. This one is probably one of these changes that requires a lot of debugging and I suggest that we leave it until later. |
||
|
|
||
| private bool disposed; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a null bitmap. | ||
| /// </summary> | ||
| public CharacterBitmapRgba() | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Allocates an RGBA bitmap (uninitialized). | ||
| /// </summary> | ||
| public CharacterBitmapRgba(int width, int rows) | ||
| { | ||
| if (width < 0) throw new ArgumentOutOfRangeException(nameof(width)); | ||
| if (rows < 0) throw new ArgumentOutOfRangeException(nameof(rows)); | ||
|
|
||
| this.width = width; | ||
| this.rows = rows; | ||
| pitch = checked(width * 4); | ||
|
|
||
| if (width != 0 && rows != 0) | ||
| { | ||
| buffer = MemoryUtilities.Allocate(checked(pitch * rows), 1); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Allocates an RGBA bitmap and copies data from a source buffer with the given pitch. | ||
| /// </summary> | ||
| public unsafe CharacterBitmapRgba(IntPtr srcRgba, int width, int rows, int srcPitchBytes) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused |
||
| : this(width, rows) | ||
| { | ||
| if (srcRgba == IntPtr.Zero && (width != 0 || rows != 0)) | ||
| throw new ArgumentNullException(nameof(srcRgba)); | ||
| if (srcPitchBytes < 0) throw new ArgumentOutOfRangeException(nameof(srcPitchBytes)); | ||
|
|
||
| if (buffer == IntPtr.Zero) | ||
| return; | ||
|
|
||
| var src = (byte*)srcRgba; | ||
| var dst = (byte*)buffer; | ||
|
|
||
| // Copy row-by-row to handle pitch differences. | ||
| var copyBytesPerRow = Math.Min(srcPitchBytes, pitch); | ||
| for (int y = 0; y < rows; y++) | ||
| { | ||
| var srcRow = src + y * srcPitchBytes; | ||
| var dstRow = dst + y * pitch; | ||
|
|
||
| MemoryUtilities.CopyWithAlignmentFallback(dstRow, srcRow, (uint)copyBytesPerRow); | ||
|
|
||
| if (copyBytesPerRow < pitch) | ||
| { | ||
| MemoryUtilities.Clear(dstRow + copyBytesPerRow, (uint)(pitch - copyBytesPerRow)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public bool IsDisposed => disposed; | ||
|
|
||
| public int Width | ||
| { | ||
| get | ||
| { | ||
| ThrowIfDisposed(); | ||
| return width; | ||
| } | ||
| } | ||
|
|
||
| public int Rows | ||
| { | ||
| get | ||
| { | ||
| ThrowIfDisposed(); | ||
| return rows; | ||
| } | ||
| } | ||
|
|
||
| public int Pitch | ||
| { | ||
| get | ||
| { | ||
| ThrowIfDisposed(); | ||
| return pitch; | ||
| } | ||
| } | ||
|
|
||
| public IntPtr Buffer | ||
| { | ||
| get | ||
| { | ||
| ThrowIfDisposed(); | ||
| return buffer; | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| if (disposed) | ||
| return; | ||
|
|
||
| if (buffer != IntPtr.Zero) | ||
| MemoryUtilities.Free(buffer); | ||
|
|
||
| disposed = true; | ||
| } | ||
|
|
||
| private void ThrowIfDisposed() | ||
| { | ||
| if (disposed) | ||
| throw new ObjectDisposedException(nameof(CharacterBitmapRgba), "Cannot access a disposed object."); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems either unrelated or too loosely documented, what do you mean by M1 and co. ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are personal milestones for me to keep track of project development. Since MVP Is complete, this can and will be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just had the warning to label it as an experimental feature.