/// <summary> /// Renders a text to the specified target <see cref="Duality.Drawing.PixelData"/>. /// </summary> /// <param name="text"></param> /// <param name="target"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="clr"></param> public void RenderToBitmap(string text, PixelData target, float x, float y, ColorRgba clr) { if (this.pixelData == null) { return; } PixelData bitmap = this.pixelData.MainLayer; float curOffset = 0.0f; GlyphData glyphData; Rect uvRect; float glyphXOff; float glyphYOff; float glyphXAdv; for (int i = 0; i < text.Length; i++) { this.ProcessTextAdv(text, i, out glyphData, out uvRect, out glyphXAdv, out glyphXOff, out glyphYOff); Vector2 dataCoord = uvRect.Pos * new Vector2(this.pixelData.Width, this.pixelData.Height) / this.texture.UVRatio; bitmap.DrawOnto(target, BlendMode.Alpha, MathF.RoundToInt(x + curOffset + glyphXOff), MathF.RoundToInt(y + glyphYOff), glyphData.Width, glyphData.Height, MathF.RoundToInt(dataCoord.X), MathF.RoundToInt(dataCoord.Y), clr); curOffset += glyphXAdv; } }
/// <summary> /// Renders a text to the specified target <see cref="Duality.Drawing.PixelData"/>. /// </summary> /// <param name="text"></param> /// <param name="target"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="clr"></param> public void RenderToBitmap(string text, PixelData target, float x, float y, ColorRgba clr) { if (this.pixmap == null) { return; } PixelData bitmap = this.pixmap.MainLayer; float curOffset = 0.0f; FontGlyphData glyphData; Rect uvRect; float glyphXAdv; for (int i = 0; i < text.Length; i++) { this.ProcessTextAdv(text, i, out glyphData, out uvRect, out glyphXAdv); Vector2 dataCoord = uvRect.Pos * bitmap.Size / this.texture.UVRatio; bitmap.DrawOnto(target, BlendMode.Alpha, MathF.RoundToInt(x + curOffset - glyphData.Offset.X), MathF.RoundToInt(y - glyphData.Offset.Y), (int)glyphData.Size.X, (int)glyphData.Size.Y, MathF.RoundToInt(dataCoord.X), MathF.RoundToInt(dataCoord.Y), clr); curOffset += glyphXAdv; } }
/// <summary> /// Loads the specified <see cref="Duality.Resources.Pixmap">Pixmaps</see> pixel data. /// </summary> /// <param name="basePixmap">The <see cref="Duality.Resources.Pixmap"/> that is used as pixel data source.</param> /// <param name="sizeMode">Specifies behaviour in case the source data has non-power-of-two dimensions.</param> public void LoadData(ContentRef <Pixmap> basePixmap, TextureSizeMode sizeMode) { if (this.nativeTex == null) { this.nativeTex = DualityApp.GraphicsBackend.CreateTexture(); } this.needsReload = false; this.basePixmap = basePixmap; this.texSizeMode = sizeMode; if (!this.basePixmap.IsExplicitNull) { PixelData pixelData = null; Pixmap basePixmapRes = this.basePixmap.IsAvailable ? this.basePixmap.Res : null; if (basePixmapRes != null) { pixelData = basePixmapRes.MainLayer; bool hasAtlas = (basePixmapRes.Atlas != null && basePixmapRes.Atlas.Count > 0); this.atlas = hasAtlas ? basePixmapRes.Atlas.ToArray() : null; } if (pixelData == null) { pixelData = Pixmap.Checkerboard.Res.MainLayer; } this.AdjustSize(pixelData.Width, pixelData.Height); this.SetupNativeRes(); if (this.texSizeMode != TextureSizeMode.NonPowerOfTwo && (this.pxWidth != this.texWidth || this.pxHeight != this.texHeight)) { if (this.texSizeMode == TextureSizeMode.Enlarge) { PixelData oldData = pixelData; pixelData = oldData.CloneResize(this.texWidth, this.texHeight); // Fill border pixels manually - that's cheaper than ColorTransparentPixels here. oldData.DrawOnto(pixelData, BlendMode.Solid, this.pxWidth, 0, 1, this.pxHeight, this.pxWidth - 1, 0); oldData.DrawOnto(pixelData, BlendMode.Solid, 0, this.pxHeight, this.pxWidth, 1, 0, this.pxHeight - 1); } else { pixelData = pixelData.CloneRescale(this.texWidth, this.texHeight, ImageScaleFilter.Linear); } } // Load pixel data to video memory this.nativeTex.LoadData( this.pixelformat, pixelData.Width, pixelData.Height, pixelData.Data, ColorDataLayout.Rgba, ColorDataElementType.Byte); // Adjust atlas to represent UV coordinates if (this.atlas != null) { Vector2 scale; scale.X = this.uvRatio.X / this.pxWidth; scale.Y = this.uvRatio.Y / this.pxHeight; for (int i = 0; i < this.atlas.Length; i++) { this.atlas[i].X *= scale.X; this.atlas[i].W *= scale.X; this.atlas[i].Y *= scale.Y; this.atlas[i].H *= scale.Y; } } } else { this.atlas = null; this.AdjustSize(this.size.X, this.size.Y); this.SetupNativeRes(); } }
/// <summary> /// Renders the <see cref="Duality.Resources.Font"/> using the specified system font. /// This method assumes that the system font's size and style match the one specified in /// the specified Duality font. /// </summary> private RenderedFontData RenderGlyphs(SysDrawFont internalFont, FontCharSet charSet, bool antialiazing, bool monospace) { DualityFont.GlyphData[] glyphs = new DualityFont.GlyphData[charSet.Chars.Length]; for (int i = 0; i < glyphs.Length; i++) { glyphs[i].Glyph = charSet.Chars[i]; } int bodyAscent = 0; int baseLine = 0; int descent = 0; int ascent = 0; TextRenderingHint textRenderingHint; if (antialiazing) { textRenderingHint = TextRenderingHint.AntiAliasGridFit; } else { textRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; } int cols; int rows; cols = rows = (int)Math.Ceiling(Math.Sqrt(glyphs.Length)); PixelData pixelLayer = new PixelData( MathF.RoundToInt(cols * internalFont.Size * 1.2f), MathF.RoundToInt(rows * internalFont.Height * 1.2f), ColorRgba.TransparentBlack); PixelData glyphTemp; PixelData glyphTempTypo; Bitmap bm; Bitmap measureBm = new Bitmap(1, 1); Rect[] atlas = new Rect[glyphs.Length]; using (Graphics measureGraphics = Graphics.FromImage(measureBm)) { Brush fntBrush = new SolidBrush(Color.Black); StringFormat formatDef = StringFormat.GenericDefault; formatDef.LineAlignment = StringAlignment.Near; formatDef.FormatFlags = 0; StringFormat formatTypo = StringFormat.GenericTypographic; formatTypo.LineAlignment = StringAlignment.Near; int x = 1; int y = 1; for (int i = 0; i < glyphs.Length; ++i) { string str = glyphs[i].Glyph.ToString(CultureInfo.InvariantCulture); bool isSpace = str == " "; SizeF charSize = measureGraphics.MeasureString(str, internalFont, pixelLayer.Width, formatDef); // Rasterize a single glyph for rendering bm = new Bitmap((int)Math.Ceiling(Math.Max(1, charSize.Width)), internalFont.Height + 1); using (Graphics glyphGraphics = Graphics.FromImage(bm)) { glyphGraphics.Clear(Color.Transparent); glyphGraphics.TextRenderingHint = textRenderingHint; glyphGraphics.DrawString(str, internalFont, fntBrush, new RectangleF(0, 0, bm.Width, bm.Height), formatDef); } glyphTemp = new PixelData(); glyphTemp.FromBitmap(bm); // Rasterize a single glyph in typographic mode for metric analysis if (!isSpace) { Point2 glyphTempOpaqueTopLeft; Point2 glyphTempOpaqueSize; glyphTemp.GetOpaqueBoundaries(out glyphTempOpaqueTopLeft, out glyphTempOpaqueSize); glyphTemp.SubImage(glyphTempOpaqueTopLeft.X, 0, glyphTempOpaqueSize.X, glyphTemp.Height); if (charSet.CharBodyAscentRef.Contains(glyphs[i].Glyph)) { bodyAscent += glyphTempOpaqueSize.Y; } if (charSet.CharBaseLineRef.Contains(glyphs[i].Glyph)) { baseLine += glyphTempOpaqueTopLeft.Y + glyphTempOpaqueSize.Y; } if (charSet.CharDescentRef.Contains(glyphs[i].Glyph)) { descent += glyphTempOpaqueTopLeft.Y + glyphTempOpaqueSize.Y; } bm = new Bitmap((int)Math.Ceiling(Math.Max(1, charSize.Width)), internalFont.Height + 1); using (Graphics glyphGraphics = Graphics.FromImage(bm)) { glyphGraphics.Clear(Color.Transparent); glyphGraphics.TextRenderingHint = textRenderingHint; glyphGraphics.DrawString(str, internalFont, fntBrush, new RectangleF(0, 0, bm.Width, bm.Height), formatTypo); } glyphTempTypo = new PixelData(); glyphTempTypo.FromBitmap(bm); glyphTempTypo.Crop(true, false); } else { glyphTempTypo = glyphTemp; } // Update xy values if it doesn't fit anymore if (x + glyphTemp.Width + 2 > pixelLayer.Width) { x = 1; y += internalFont.Height + MathF.Clamp((int)MathF.Ceiling(internalFont.Height * 0.1875f), 3, 10); } // Memorize atlas coordinates & glyph data glyphs[i].Width = glyphTemp.Width; glyphs[i].Height = glyphTemp.Height; glyphs[i].OffsetX = glyphTemp.Width - glyphTempTypo.Width; glyphs[i].OffsetY = 0; // ttf fonts are rendered on blocks that are the whole size of the height - so no need for offset if (isSpace) { glyphs[i].Width /= 2; glyphs[i].OffsetX /= 2; } atlas[i].X = x; atlas[i].Y = y; atlas[i].W = glyphTemp.Width; atlas[i].H = (internalFont.Height + 1); // Draw it onto the font surface glyphTemp.DrawOnto(pixelLayer, BlendMode.Solid, x, y); x += glyphTemp.Width + MathF.Clamp((int)MathF.Ceiling(internalFont.Height * 0.125f), 2, 10); } } // White out texture except alpha channel. for (int i = 0; i < pixelLayer.Data.Length; i++) { pixelLayer.Data[i].R = 255; pixelLayer.Data[i].G = 255; pixelLayer.Data[i].B = 255; } // Monospace offset adjustments if (monospace) { int maxGlyphWidth = 0; for (int i = 0; i < glyphs.Length; i++) { maxGlyphWidth = Math.Max(maxGlyphWidth, glyphs[i].Width); } for (int i = 0; i < glyphs.Length; ++i) { glyphs[i].OffsetX -= (int)Math.Round((maxGlyphWidth - glyphs[i].Width) / 2.0f); } } // Determine Font properties { float lineSpacing = internalFont.FontFamily.GetLineSpacing(internalFont.Style); float emHeight = internalFont.FontFamily.GetEmHeight(internalFont.Style); float cellAscent = internalFont.FontFamily.GetCellAscent(internalFont.Style); float cellDescent = internalFont.FontFamily.GetCellDescent(internalFont.Style); ascent = (int)Math.Round(cellAscent * internalFont.Size / emHeight); bodyAscent /= charSet.CharBodyAscentRef.Length; baseLine /= charSet.CharBaseLineRef.Length; descent = (int)Math.Round(((float)descent / charSet.CharDescentRef.Length) - (float)baseLine); } // Aggregate rendered and generated data into our return value FontMetrics metrics = new FontMetrics( size: internalFont.SizeInPoints, height: (int)internalFont.Height, ascent: ascent, bodyAscent: bodyAscent, descent: descent, baseLine: baseLine, monospace: monospace); return(new RenderedFontData { Bitmap = pixelLayer, Atlas = atlas, GlyphData = glyphs, Metrics = metrics }); }
/// <summary> /// Composes pixel and atlas data for a single <see cref="Tileset"/> visual layer. /// </summary> private LayerPixelData ComposeLayerPixelData(TilesetRenderInput renderInput, PixelData sourceData, LayerGeometry geometry) { // Create a buffer for writing target pixel data LayerPixelData target; target.PixelData = new PixelData(geometry.TargetTextureSize.X, geometry.TargetTextureSize.Y); target.Atlas = new List <Rect>(); // Iterate over source tiles and copy each tile from source to target Point2 targetTilePos = new Point2(0, 0); for (int tileIndex = 0; tileIndex < geometry.SourceTileCount; tileIndex++) { // Determine source and target positions on pixel data / buffer Point2 sourceTilePos = geometry.GetSourceTilePos(tileIndex); Point2 targetContentPos = new Point2( targetTilePos.X + renderInput.TargetTileMargin, targetTilePos.Y + renderInput.TargetTileMargin); // Draw the source tile onto the target buffer, including its spacing / border sourceData.DrawOnto(target.PixelData, BlendMode.Solid, targetContentPos.X, targetContentPos.Y, renderInput.SourceTileSize.X, renderInput.SourceTileSize.Y, sourceTilePos.X, sourceTilePos.Y); // Fill up the target spacing area with similar pixels if (renderInput.TargetTileMargin > 0) { FillTileSpacing(target.PixelData, renderInput.TargetTileMargin, targetContentPos, renderInput.SourceTileSize); } // Update whether the tile is considered visually empty if (this.tiles.Data[tileIndex].IsVisuallyEmpty) { bool isLayerVisuallyEmpty = IsCompletelyTransparent( sourceData, sourceTilePos, renderInput.SourceTileSize); if (!isLayerVisuallyEmpty) { this.tiles.Data[tileIndex].IsVisuallyEmpty = false; } } // Add an entry to the generated atlas Rect atlasRect = new Rect( targetTilePos.X + renderInput.TargetTileMargin, targetTilePos.Y + renderInput.TargetTileMargin, geometry.TargetTileAdvance.X - renderInput.TargetTileMargin * 2, geometry.TargetTileAdvance.Y - renderInput.TargetTileMargin * 2); target.Atlas.Add(atlasRect); // Advance the target tile position targetTilePos.X += geometry.TargetTileAdvance.X; if (targetTilePos.X + geometry.TargetTileAdvance.X > target.PixelData.Width) { targetTilePos.X = 0; targetTilePos.Y += geometry.TargetTileAdvance.Y; } } // Generate additional target tiles as scheduled foreach (GeneratedQuadTile tile in this.generateTileSchedule) { // Determine source and target positions on pixel data / buffer Point2 sourceTilePosTopLeft = geometry.GetSourceTilePos(tile.SourceTopLeftIndex); Point2 sourceTilePosTopRight = geometry.GetSourceTilePos(tile.SourceTopRightIndex); Point2 sourceTilePosBottomRight = geometry.GetSourceTilePos(tile.SourceBottomRightIndex); Point2 sourceTilePosBottomLeft = geometry.GetSourceTilePos(tile.SourceBottomLeftIndex); Point2 targetContentPos = new Point2( targetTilePos.X + renderInput.TargetTileMargin, targetTilePos.Y + renderInput.TargetTileMargin); // Draw the source tile onto the target buffer, including its spacing / border Point2 quadSize = renderInput.SourceTileSize / 2; sourceData.DrawOnto(target.PixelData, BlendMode.Solid, targetContentPos.X, targetContentPos.Y, quadSize.X, quadSize.Y, sourceTilePosTopLeft.X, sourceTilePosTopLeft.Y); sourceData.DrawOnto(target.PixelData, BlendMode.Solid, targetContentPos.X + quadSize.X, targetContentPos.Y, quadSize.X, quadSize.Y, sourceTilePosTopRight.X + quadSize.X, sourceTilePosTopRight.Y); sourceData.DrawOnto(target.PixelData, BlendMode.Solid, targetContentPos.X + quadSize.X, targetContentPos.Y + quadSize.Y, quadSize.X, quadSize.Y, sourceTilePosBottomRight.X + quadSize.X, sourceTilePosBottomRight.Y + quadSize.Y); sourceData.DrawOnto(target.PixelData, BlendMode.Solid, targetContentPos.X, targetContentPos.Y + quadSize.Y, quadSize.X, quadSize.Y, sourceTilePosBottomLeft.X, sourceTilePosBottomLeft.Y + quadSize.Y); // Fill up the target spacing area with similar pixels if (renderInput.TargetTileMargin > 0) { FillTileSpacing(target.PixelData, renderInput.TargetTileMargin, targetContentPos, renderInput.SourceTileSize); } // Add an entry to the generated atlas Rect atlasRect = new Rect( targetTilePos.X + renderInput.TargetTileMargin, targetTilePos.Y + renderInput.TargetTileMargin, geometry.TargetTileAdvance.X - renderInput.TargetTileMargin * 2, geometry.TargetTileAdvance.Y - renderInput.TargetTileMargin * 2); target.Atlas.Add(atlasRect); // Advance the target tile position targetTilePos.X += geometry.TargetTileAdvance.X; if (targetTilePos.X + geometry.TargetTileAdvance.X > target.PixelData.Width) { targetTilePos.X = 0; targetTilePos.Y += geometry.TargetTileAdvance.Y; } } // Update which tiles are considered visually empty for (int tileIndex = 0; tileIndex < this.outputTileCount; tileIndex++) { // Determine target positions on pixel data / buffer Rect targetRect = target.Atlas[tileIndex]; // Update whether the tile is considered visually empty if (this.tiles.Data[tileIndex].IsVisuallyEmpty) { bool isLayerVisuallyEmpty = IsCompletelyTransparent( target.PixelData, (Point2)targetRect.Pos, (Point2)targetRect.Size); if (!isLayerVisuallyEmpty) { this.tiles.Data[tileIndex].IsVisuallyEmpty = false; } } } return(target); }
/// <summary> /// Generates pixel and atlas data for a single <see cref="Tileset"/> visual layer. /// </summary> /// <param name="renderInput"></param> /// <param name="sourceData"></param> /// <param name="geometry"></param> /// <param name="tileData"></param> /// <returns></returns> private LayerPixelData GenerateLayerPixelData(TilesetRenderInput renderInput, PixelData sourceData, LayerGeometry geometry, RawList <TileInfo> tileData) { // Create a buffer for writing target pixel data LayerPixelData target; target.PixelData = new PixelData(geometry.TargetTextureSize.X, geometry.TargetTextureSize.Y); target.Atlas = new List <Rect>(); // Iterate over tiles and move each tile from source to target Point2 targetTilePos = new Point2(0, 0); for (int tileIndex = 0; tileIndex < geometry.SourceTileCount; tileIndex++) { // Initialize a new tile info when necessary if (tileIndex >= tileData.Count) { tileData.Count++; tileData.Data[tileIndex].IsVisuallyEmpty = true; } // Determine where on the source buffer the tile is located Point2 sourceTilePos = new Point2( geometry.SourceTileAdvance.X * (tileIndex % geometry.SourceTilesPerRow), geometry.SourceTileAdvance.Y * (tileIndex / geometry.SourceTilesPerRow)); // Draw the source tile onto the target buffer, including its spacing / border Point2 targetContentPos = new Point2( targetTilePos.X + renderInput.TargetTileMargin, targetTilePos.Y + renderInput.TargetTileMargin); sourceData.DrawOnto(target.PixelData, BlendMode.Solid, targetContentPos.X, targetContentPos.Y, renderInput.SourceTileSize.X, renderInput.SourceTileSize.Y, sourceTilePos.X, sourceTilePos.Y); // Fill up the target spacing area with similar pixels if (renderInput.TargetTileMargin > 0) { FillTileSpacing(target.PixelData, renderInput.TargetTileMargin, targetContentPos, renderInput.SourceTileSize); } // Update whether the tile is considered visually empty if (tileData.Data[tileIndex].IsVisuallyEmpty) { bool isLayerVisuallyEmpty = IsCompletelyTransparent( sourceData, sourceTilePos, renderInput.SourceTileSize); if (!isLayerVisuallyEmpty) { tileData.Data[tileIndex].IsVisuallyEmpty = false; } } // Add an entry to the generated atlas Rect atlasRect = new Rect( targetTilePos.X + renderInput.TargetTileMargin, targetTilePos.Y + renderInput.TargetTileMargin, geometry.TargetTileAdvance.X - renderInput.TargetTileMargin * 2, geometry.TargetTileAdvance.Y - renderInput.TargetTileMargin * 2); target.Atlas.Add(atlasRect); // Advance the target tile position targetTilePos.X += geometry.TargetTileAdvance.X; if (targetTilePos.X + geometry.TargetTileAdvance.X > target.PixelData.Width) { targetTilePos.X = 0; targetTilePos.Y += geometry.TargetTileAdvance.Y; } } return(target); }