程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 基礎: WPF中的三維文字

基礎: WPF中的三維文字

編輯:關於.NET

諸如 TrueType 之類的矢量字技術主要供我們靈活准確排版之用,但它們也可以充當圖形處理的對象。程序員可以訪問定義每個文本字符的實際輪廓,並將它們視為矢量圖形對象。這些輪廓可以進行筆劃書寫、填充、用於剪輯或進行轉換。Microsoft® Word 中的常見“藝術字”功能便是以此概念為基礎。

認識到這些字符輪廓的特性和局限性非常重要:它們是完全幾何性的,缺少操作系統通常在屏幕上呈現字體時所用的“提示”。通過這些提示可以依據可用的像素網格智能地對字符進行光柵化處理。因此,這些無提示的字符輪廓在大字號狀態或高分辨率設備上看起來效果最佳。它們通常無法滿足在屏幕上呈現普通字號文字的要求。(不過,隨著打印機分辨率的提高以及對屏幕圖形反失真使用的增多,提示的價值已不像原來那麼重要。)

每當遇到新的 Windows® API 時,我都會特意去尋找可訪問這些字符輪廓的來源。在 Windows 窗體中,它是 GraphicsPath 類的一部分。AddString 方法的四種重載可讓您將字符輪廓添加到一個路徑。我所著書籍“Programming Microsoft Windows with C#”(Microsoft Windows 編程——C# 篇)(Microsoft Press, 2002) 和“Programming Microsoft Windows with Microsoft Visual Basic® .NET”(Microsoft Windows 編程——Microsoft Visual Basic® .NET 篇)(Microsoft Press, 2003) 的第 19 章對此過程進行過說明。

在 Windows Presentation Foundation (WPF) 中,提供字符輪廓訪問權的類和方法隱藏得更好,但它們確實存在。來自 System.Windows.Media 命名空間的 FormattedText 和 GlyphRun 類均具有名為 BuildGeometry 的方法,這些方法可為特定字體和文本字符串返回 Geometry 對象。在本文中,我將完全使用 FormattedText,因為它是兩個類中較容易的一種。我所著書籍“Applications = Code + Markup”(應用程序 = 代碼 + 標記)(Microsoft Press, 2006) 的第 28 章和第 30 章中提供了一些 FormattedText 和 BuildGeometry 與二維圖形結合使用的示例。

剛開始研究 WPF 中的三維文字時,我很自然地考慮過將這些字符輪廓轉為三維文本塊的可能性,就像在印刷媒體上或者電視上的飛行徽標效果中看到的那樣(請參見圖 1 中提供的示例)。我知道這個工作會涉及二維輪廓到三維三角形網格的轉換,而除此之外,我只確定一件事:其中涉及的一些編程不會那麼簡單。

圖 1 實心三維文字

FormattedText 和 BuildGeometry

我想大多數 WPF 程序員對 FormattedText 類都沒有太多接觸。正如文檔中所述,此類用於“對繪制文本進行低級別控制”。

FormattedText 構造函數需要一個 TypeFace 對象,該對象定義字體系列、樣式(如斜體)、可能的加粗,以及與字體相關的任何拉伸或壓縮。此外,FormattedText 構造函數還需要 em 尺寸(字體高度)、用於為字體字符著色的刷子,以及文本字符串本身。

創建 FormattedText 對象後,您可以調用各種方法對文本字符串的子集設置不同的字體、樣式或格式。通過屬性可以設置行距和文本的其他特征。

FormattedText 對象最常見的用法是與 DrawingContext 類的 DrawText 方法一起使用。通常,您會在覆蓋由 UIElement 定義的 OnRender 方法時遇到 DrawingContext 類。這是應用程序能做的最低級別的圖形輸出,仍視為純 WPF 應用程序。DrawText 方法只需要一個 FormattedText 對象和文本開始的坐標點。

相對於 FormattedText 中的任何其他內容,BuildGeometry 方法似乎有些格格不入。該方法具有一個參數(類型為 Point 的原點),並會返回一個 Geometry 對象。

Geometry 是 WPF 中的重要類,它顯然與傳統圖形路徑有關。Geometry 對象是指定為坐標點的直線與曲線的集合。其中的某些直線和曲線可能會有連接;某些連接的直線和曲線可能會閉合,用以描述封閉的區域。Geometry 對象中未包含呈現概念。在二維圖形編程中,要呈現一個 Geometry 對象,您只需將它傳遞給便於使用的 Path 類即可,該類是高級 Shapes 庫的一部分。另一種方法是 GeometryDrawing,它以 Geometry 對象、Brush 和 Pen 為基礎。

進行二維圖形編程時,通常幾乎不需要深入特定的 Geometry 對象。但是將 Geometry 對象轉換為三維文字的工作則大不相同。那麼,FormattedText.BuildGeometry 返回的 Geometry 對象究竟是什麼呢?

Geometry 是派生了其他七個類的抽象類。我的經驗如下:從 FormattedText.BuildGeometry 返回的 Geometry 對象實際上是一個或多個嵌套 GeometryGroup 對象,其中包含多個 PathGeometry 對象,每個對象對應文本字符串中的一個字符。每個 PathGeometry 對象均包含一個或多個可定義封閉路徑的 PathFigure 對象。有些字符(如 l、t 或 x)只需要一個 PathFigure。其他字符則需要兩個。如小寫的 i,其中的點便需要第二個 PathFigure。O 的外圈輪廓和內圈輪廓各自都需要一個 PathFigure。大寫的 B 需要三個 PathFigure 對象。百分比符號則需要五個。

每個 PathFigure 都是 PathSegment(另一個抽象類)類型的對象的集合。根據我的經驗,與文本輪廓相關的 PathFigure 對象包含多個 LineSegment、PolyLineSegment、BezierSegment 和 PolyBezierSegment 類型的對象,這些對象構成了單一封閉路徑的定義。

WPF 三維沒有折線或 Bezier 曲線的概念,但三角形網格以點的集合為基礎,因此第一步需要將 FormattedText.BuildGeometry 的 Geometry 對象轉換為一系列的封閉折線。原以為這會很簡單。但我很快發現,將這些折線轉換為三角形網格的算法顯然是一項復雜的數學工作,非常可能超出我的能力范圍。

輪廓和網格

在本雜志 2007 年 4 月期中,我討論了生成與 WPF 三維圖形配合使用的 MeshGeometry3D 對象的機制(請參閱 msdn.microsoft.com/msdnmag/issues/07/04/Foundations)。最起碼,您需要設置 MeshGeometry3D 對象的 Positions 和 TriangleIndices 屬性。Positions 屬性是三維空間中各點的集合。TriangleIndices 集合說明了如何根據這些點構造三角形。TriangleIndices 中的每三個整數都會引用 Positions 集合中的三個點來形成一個三角形。

我發現,從文本輪廓生成的折線會形成 MeshGeometry3D 的 Positions 集合的一部分。圖 2 顯示了 sans-serif 字體的大寫字母 A,由兩條封閉的折線及十一個點組成。第二個圖顯示了該字母如何劃分為七個三角形。對於類似的簡單字符,手工書寫輕而易舉,但是描述代碼中的過程對我來說完全沒有那麼清晰。

圖 2 由點和三角形定義的字母

顯而易見,您需要確保每個三角形的邊都不會偏離出字符的邊界,並且需要確保字符的整個外觀由三角形集合所決定。同時請記住,我此處所用的示例是簡單的 sans-serif 字體字符。考慮一下 serifed 字體的大寫字母 S 的話,就會發現這個工作變得異常困難。現在有一種稱為 Delaunay Triangulation 的技術可能會有用,但其中的數學相當復雜。

慶幸的是,希望還是有的。在關注此問題時,我看到一個廣告中的某些文字采用了不同的三維文字方式,只包含輪廓,而將每個字符的主體留空。我意識到,這就是我可以做的事。首先我需要一個類名,於是我想到了 RibbonText。

Text3D 層次結構

本文的可下載代碼中包含一個名為 Text3D 的 Visual Studio® 解決方案。該解決方案由以下部分組成:一個名為 Petzold.Text3D 的 DLL 項目和使用該 DLL 的若干個基於 XAML 的小型演示程序。請注意,DLL 中的文件具有命名空間 Petzold.Text3D。

Petzold.Text3D.dll 庫中的類層次結構以 ModelVisualBase 開頭,這是從 WPF 三維類 ModelVisual3D 派生的抽象類,即我在本雜志 2007 年 4 月期中討論過的技術。但是,此 ModelVisualBase 類與前一專欄中的 ModelVisualBase 不盡相同,因為它需要多一些靈活性。但請注意,其中的概念是相同的:ModelVisualBase 會在內部存儲 GeometryModel3D 對象和 MeshGeometry3D 對象,並定義要傳輸到 GeometryModel3D 對象的公共 Material 和 BackMaterial 屬性。

Petzold.Text3D 類中的大多數屬性受依賴關系屬性支持。ModelVisualBase 類定義了兩個 PropertyChanged 事件處理程序(一個靜態,一個實例),後代類在定義依賴關系屬性時便可使用這兩個處理程序。實例版本會適當准備內部 MeshGeometry3D 對象的各種屬性以應對各種變化(我在 4 月份專欄中討論過此過程),然後調用抽象方法 Triangulate。後代類可以覆蓋 Triangulate 以定義 MeshGeometry3D 的各種集合。

在 Petzold.Text3D 庫中,抽象類 Text3DBase 從 ModelVisualBase 派生而來。此類定義了大量與文本相關的屬性,包括 Text、FontFamily、FontStyle、FontWeight、FontStretch 和 FontSize,全部都是 FormattedText 構造函數需要的屬性。該類還定義了 BuildGeometry 方法所需的 Origin 屬性。上述任一屬性發生變化時,該類都會創建一個新的 FormattedText 對象,並根據 BuildGeometry 返回的 Geometry 對象設置 Text3DBase 的第八個屬性 TextGeometry:

FormattedText formtxt =
  new FormattedText(Text, CultureInfo.CurrentCulture,
    FlowDirection.LeftToRight,
    new Typeface(FontFamily, FontStyle,
      FontWeight, FontStretch),
      FontSize, Brushes.Transparent);
TextGeometry = formtxt.BuildGeometry(Origin);

此 TextGeometry 對象可用於所有的後代類。

進行堆陣分配之後,Text3DBase 類才能創建新的 TextGeometry 對象。該類需要分配新的 FormattedText 對象,而 BuildGeometry 無疑進行了很多自身內存分配。這些堆陣分配意味著,由 Text3DBase 類定義的屬性可能不應為動態。我已經使用由 UIPropertyMetadata 定義的 IsAnimationProhibited 屬性來標記一些將會是動畫首選的屬性。

我所編寫的創建三維文字的所有類中,Text3DBase 定義的 Origin 屬性是唯一表明文字在三維空間中所處位置的屬性,該屬性的類型為 Point,表明了二維空間中的位置。我考慮過定義更大的一組屬性,將文字精確地放置在三維空間中。這些屬性不僅必須包含三維文字的原點,還必須包含表明基線方向的三維矢量,以及表明垂直方向的另一三維矢量。我決定選用 Origin 屬性來將文字放置在三維空間中的 XY 平面上,然後使用轉換來執行其他所有定位操作。此方法最大的好處就是簡化了數學計算。

由 Text3DBase 定義的 FontSize 屬性指出了字體的 em 尺寸,這與字體字符的總高度大致相關。在三維中,字體還有深度。DeepTextBase 類從 Text3DBase 派生而來,純粹用於定義 Depth 屬性。(您最後會看到此擴展類層次結構的原因;在這些類的編程過程中發生了相當多的重構,從而形成了現在的結構。) 由於文字位於 XY 平面上,因此我考慮用 Depth 屬性來說明文字在負 Z 軸上擴展的深度。

到目前為止,這些類還未進行任何實質性工作。圖 3 所示的抽象類 GeometryTextBase 從 DeepTextBase 派生而來,並且覆蓋由 ModelVisualBase 定義的 Triangulate 方法。借助 Geometry 類中的 GetFlattenedPathGeometry 方法,GeometryTextBase 類可將 Geometry 對象(作為 TextGeometry 屬性出現)轉換為多條相連的折線,這些折線與組成文本輪廓的封閉圖形相對應。針對每個封閉圖形,GeometryTextBase 類會調用一個抽象方法,如下所示:

Figure 3 GeometryTExtBase 類

public abstract class GeometryTextBase : DeepTextBase
{
  // Field prevent re-allocations during mesh generation.
  CircularList<Point> list = new CircularList<Point>();
  protected override void Triangulate(
    DependencyPropertyChangedEventArgs args,
    Point3DCollection vertices, Vector3DCollection normals,
    Int32Collection indices, PointCollection textures)
  {
    // Clear all four collections.
    vertices.Clear();
    normals.Clear();
    indices.Clear();
    textures.Clear();
    // Convert TextGeometry to series of closed polylines.
    PathGeometry path =
      TextGeometry.GetFlattenedPathGeometry(0.001,
      ToleranceType.Relative);
    foreach (PathFigure fig in path.Figures)
    {
      list.Clear();
      list.Add(fig.StartPoint);
      foreach (PathSegment seg in fig.Segments)
      {
        if (seg is LineSegment)
        {
          LineSegment lineseg = seg as LineSegment;
          list.Add(lineseg.Point);
        }
        else if (seg is PolyLineSegment)
        {
          PolyLineSegment polyline = seg as PolyLineSegment;
          for (int i = 0; i < polyline.Points.Count; i++)
            list.Add(polyline.Points[i]);
        }
      }
      // Figure is complete. Post-processing follows.
      if (list.Count > 0)
      {
        // Remove last point if it's the same as the first.
        if (list[0] == list[list.Count - 1])
          list.RemoveAt(list.Count - 1);
        // Convert points to Y increasing up.
        for (int i = 0; i < list.Count; i++)
        {
          Point pt = list[i];
          pt.Y = 2 * Origin.Y - pt.Y;

          list[i] = pt;
        }
        // For each figure, process the points.
        ProcessFigure(list, vertices, normals,
          indices, textures);
      }
    }
  }
  // Abstract method to convert figure to mesh geometry.
  protected abstract void ProcessFigure(CircularList<Point> list,
    Point3DCollection vertices, Vector3DCollection normals,
    Int32Collection indices, PointCollection textures);
}
protected abstract void ProcessFigure(
  CircularList<Point> list, Point3DCollection vertices,
  Vector3DCollection normals, Int32Collection indices,
  PointCollection textures);

第一個參數是圖形中二維點的集合。CircularList 是我定義的集合類,功能類似於一個循環緩沖區。每當按索引訪問成員時,該對象會規范化索引,將其調整到適當范圍。換句話說,索引 -1 會訪問集合的最後一個成員,索引 list.Count 則會訪問集合的第一個成員。該集合包含封閉折線中的所有點。

ProcessFigure 的其他參數分別對應 MeshGeometry3D 對象的 Positions、Normals、TriangleIndices 和 TextureCoordinates 集合。實現 ProcessFigure 方法的類至少需要依據 CircularList 集合中的點來填充頂點和索引集合。

RibbonText 和 SliverText

我編寫的真正生成三維文字的第一個類是 RibbonText,如圖 4 所示。正如您看到的,該類從 GeometryTextBase 派生而來,完全由 ProcessFigure 方法的實現構成。它生成的 Point3D 對象完全基於 CircularList 集合中的二維點和 Depth 屬性。這些點在 XY 平面(其中 Z 等於零)上的點以及 XY 平面後的 Depth 單元之間交替。索引集合將這兩組點連接起來,以定義一系列的三角形。

Figure 4 RibbonText 類

public class RibbonText : GeometryTextBase
{
  protected override void ProcessFigure(CircularList<Point> list,
    Point3DCollection vertices, Vector3DCollection normals,
    Int32Collection indices, PointCollection textures)
  {
    int offset = vertices.Count;
    for (int i = 0; i <= list.Count; i++)
    {
      Point pt = list[i];
      // Set vertices.
      vertices.Add(new Point3D(pt.X, pt.Y, 0));
      vertices.Add(new Point3D(pt.X, pt.Y, -Depth));
      // Set texture coordinates.
      textures.Add(new Point((double)i / list.Count, 0));
      textures.Add(new Point((double)i / list.Count, 1));
      // Set triangle indices.
      if (i < list.Count)
      {
        indices.Add(offset + i * 2 + 0);
        indices.Add(offset + i * 2 + 2);
        indices.Add(offset + i * 2 + 1);
          
        indices.Add(offset + i * 2 + 1);
        indices.Add(offset + i * 2 + 2);
        indices.Add(offset + i * 2 + 3);
      }
    }
  }
}

請記住,會為特定的文本字符串多次調用 ProcessFigure 方法。GeometryTextBase 中的 Triangulate 方法負責最初清除 MeshGeometry3D 集合;ProcessFigure 類使用名為偏移的整數來確定添加到頂點集合的新點的索引。

圖 5 顯示了使用 RibbonText 類的一個小 XAML 文件,圖 6 則顯示了其外觀。從某種意義上說,它看起來要比普通的三維文本塊“更美觀”一些,可能因為它確實不尋常。但是從算法上講,這是能夠想到的三維文字的最簡單形式。

Figure 5 RibbonTextDemo.xaml

<!-- RibbonTextDemo.xaml by Charles Petzold, June 2007 -->
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:src="clr-namespace:Petzold.Text3D;assembly=Petzold.Text3D"
   WindowTitle="RibbonText Demo"
   Title="RibbonText Demo">
  <Viewport3D>
    <src:RibbonText Text="Ribbon"
        FontFamily="Times New Roman" Depth="2">
      <src:RibbonText.Material>
        <DiffuseMaterial Brush="Cyan" />
      </src:RibbonText.Material>
      <src:RibbonText.BackMaterial>
        <DiffuseMaterial Brush="Pink" />
      </src:RibbonText.BackMaterial>
    </src:RibbonText>
    <!-- Lights. -->
    <ModelVisual3D>
      <ModelVisual3D.Content>
        <Model3DGroup>
          <AmbientLight Color="#404040" />
          <DirectionalLight Color="#C0C0C0"
            Direction="2 -3 -1" />
        </Model3DGroup>
      </ModelVisual3D.Content>
    </ModelVisual3D>
    <!-- Camera. -->
    <Viewport3D.Camera>
      <PerspectiveCamera Position="-3 0 8" UpDirection="0 1 0"
                LookDirection="1 0 -2" FieldOfView="45" />
    </Viewport3D.Camera>
  </Viewport3D>
</Page>

圖 6 RibbonTextDemo 外觀

這些字符實際上是空心的。如果從正面看文字,那麼您可以直接看透它。因為只有從某一角度看文字,您才會看到外部和內部的顏色是不同的。字符的外部使用 Material 畫筆(在 RibbonTextDemo.xaml 中設置為 Cyan)著色,內部則使用 BackMaterial 畫筆(設置為 Pink)著色。但是對於某些字體,這些顏色可以反轉。這取決於定義字符輪廓的各個點的方向。可以想象,對於相同字體中的不同字符,此方向甚至會有所不同。

RibbonText 類定義了 TextureCoordinates 集合的點,因此您並不局限於實心顏色畫筆。TextureCoordinates 集合中的點以 Y 坐標 0 表示功能區的前景部分,1 表示背景部分,X 坐標則以 CircularList 集合中各點的索引為基礎。對此文本所用的任何非實心畫筆都會分別應用於每個功能區。當您以起始坐標 (0, 0) 和結尾坐標 (0, 1) 使用 LinearGradientBrush 時,便會產生最可預測的結果。其他畫筆可能會在文本字符中無法預測的位置暴露出可見接縫。

比 RibbonText 更難一些的是被我稱為 SliverText 的類。此類也僅僅側重於文本字符的輪廓,但它賦予這些輪廓一個非零寬度,我將其定義為 SliverWidth 屬性。圖 7 顯示了使用默認 FontSize 1、默認 Depth 1 和默認 SliverWidth 0.05 運行的 SliverTextDemo 程序。我根據各點的 X 和 Y 坐標定義了 SliverText 的 TextureCoordinates,允許畫筆因文本正面不同而有所不同。該示例顯示了一個漸變畫筆。字符的頂部看上去有點圓,因為頂部三角形和側面三角形在邊緣共享坐標。WPF 三維會根據平均值計算法線(它控制光線反射)。

圖 7 SliverTextDemo 外觀

如果您將 SliverWidth 設置為比 FontSize 高很多的值,那麼字符會相互合並。如果之後文字通過轉換成為動態,那麼可能會有一些難看的顫動,因為表面會爭奪對前景的控制。(此效果的技術術語稱為“Z 值爭奪”。)

SliverText 類某些二維分析幾何中納入一些偏移。每個字符輪廓都是一條二維折線,但 SliverText 需要兩組平行折線。我曾試著使用由 Geometry 定義的 GetWidenedPathGeometry 方法,但我在加寬路徑方面的經驗是,它們經常會帶來我極力想避免的產物。

因此,我改為自己加寬路徑。此工作需要計算與折線中每條單獨的線平行的線。然而,這些平行線通常需要縮短或加長,以便達到與基礎路徑相同的連續性和形狀。助我一臂之力的是我編寫的一個小結構,稱為 Line2D。此結構將 Line2D 對象和矢量的新增項定義為線條位移。兩個 Line2D 對象相乘會返回一個表明交集的 Point 對象。

SolidText 突破

在我開始編寫 RibbonText 和 SliverText 的多個月後,我很擔心在三維文字道路上不會有更大進展。我偶爾會回到將文本字符外觀分成三角形的問題上,但是我一直找不到一種方法,能夠不牽涉相當可怕的數學。

當然,我非常了解如何將文本字符串放在三維的平面中。您唯一需要的是基於 TextBlock 組件的 VisualBrush。我當然想到過可以將該平面覆蓋在 RibbonText 對象頂部,並創建實心文字效果。但據我的經驗,這些問題涉及混合光柵化文字(TextBlock 所顯示的內容)和根據幾何輪廓構造的文字。這兩種不同的算法在視覺上並不匹配。

當我意識到可以依據 RibbonText 中使用的相同幾何學,改用 DrawingBrush 覆蓋三維空間的平面時,我終於找到了突破口。概念很清晰;但實現過程並非如此。

我將生成此 DrawingBrush 的類稱為 PlanarText,它從 Text3DBase 派生而來。PlanarText 需要 Text3DBase 生成的 TextGeometry 屬性,但不需要由 DeepTextBase 定義的 Depth 屬性,因為(正如類名所示)PlanarText 是在平面上顯示文字。但是,PlanarText 類確實定義了名為 Z 的屬性,表明文本平面所在位置與 XY 平面平行。

PlanarText 根據兩個三角形簡單定義一個平面矩形,從而實現了 Triangulate 方法。此矩形四個角的坐標來自與 Geometry 對象(從 TextGeometry 屬性獲得)相關的 Bounds 屬性。

PlanarText 與其他從 ModelVisualBase 派生的類的明顯區別在於對 Material 和 BackMaterial 屬性的處理方式不同。

我發現有一點非常有趣,最初設想 ModelVisualBase 時,這些 Material 和 BackMaterial 屬性都被認為是個麻煩。ModelVisualBase 從 ModelVisual3D 派生而來,但 ModelVisual3D 並不定義 Material 和 BackMaterial 屬性。定義 Material 和 BackMaterial 屬性的 WPF 三維類實際上是 GeometryModel3D,ModelVisualBase 將該類的一個實例內部存儲為其 Content 屬性。

ModelVisualBase 需要定義 Material 和 BackMaterial 屬性,這樣您便可以在標記中的派生類中進行設置,如下所示:

<src:RibbonText Text="Ribbon">
<src:RibbonText.Material>
<DiffuseMaterial Brush="Cyan" />
</src:RibbonText.Material>

<src:RibbonText.BackMaterial>
<DiffuseMaterial Brush="Pink" />
</src:RibbonText.BackMaterial>
</src:RibbonText>

與 ModelVisualBase 所定義的 Material 和 BackMaterial 屬性相關的是屬性更改處理程序 MaterialPropertyChanged,它只是將分配到這些屬性的所有對象傳輸給內部 GeometryModel3D 對象的相同屬性。

但是 PlanarText 需要做一些不同的工作。例如,當 PlanarText 的 Material 屬性設置為 DiffuseMaterial 對象時,PlanarText 需要提取與該 DiffuseMaterial 相關的 Brush 對象,然後根據此現有的畫筆和 TextGeometry 屬性創建新的 DrawingBrush 對象:

new DrawingBrush(new GeometryDrawing(brush, null, TextGeometry))

此 DrawingBrush 會成為新 DiffuseMaterial 對象的基礎,PlanarText 然後會將該對象設置為內部 GeometryModel3D 的 Material 屬性。

如果分配到 PlanarText 的 Material 屬性是 DiffuseMaterial 對象,那麼這一過程聽上去相當簡單。但是,分配到 Material 屬性可能是 MaterialGroup 對象,並且此 MaterialGroup 對象可能具有 DiffuseMaterial、SpecularMaterial、EmissiveMaterial 甚至是其他嵌套 MaterialGroup 類型的子項。通常情況下,PlanarText 類需要構造一個要分配給其內部 GeometryModel3D 對象的 Material 對象平行結構,並且與這些 Material 對象相關的每個 DrawingBrush 都需要通過 Brush 對象(來自與 PlanarText 相關的 Material 對象)計算出來。

僅僅因為此 Material 傳輸邏輯的原因,PlanarText.cs 成了 Text3D 庫中最長的文件,即使結果看起來並不多,如圖 8 所示。

圖 8 PlanarTextDemo 外觀

我仍對 PlanarText 類不太滿意。例如,如果文字大小發生變化,PlanarText 就需要重新創建畫筆。如果該類的公共 Material 和 BackMaterial 屬性的結構與內部 GeometryModel3D 對象的屬性結構相同,則可避免重新創建大量的 Material 對象。但是如果將與這些 Material 對象相關的其中一個畫筆動態化(可能通過 ColorAnimation),則 PlanarText 必須在每次傳遞時都重新創建 GeometryDrawing 和 DrawingBrush 對象,那樣成本會很高。

PlanarText 本身不是很重要,但在實現 SolidText 類過程中起了相當作用。圖 9 所示的 SolidText 從 DeepTextBase 派生而來,這意味著它具備 Text3DBase 定義的所有文本相關屬性及 Depth 屬性。但 SolidText 本身無意於生成任何三角形網格。它會覆蓋 Triangulate 方法,但只從該方法返回。

Figure 9 SolidText 類

public class SolidText : DeepTextBase
{
public SolidText()
{
// Create RibbonText and two PlanarText children.
RibbonText ribbon = new RibbonText();
ribbon.Depth = Depth;
Children.Add(ribbon);
PlanarText planar = new PlanarText();
Children.Add(planar);
planar = new PlanarText();
planar.Z = -Depth;
Children.Add(planar);
}
// SideMaterial dependency property and property.
public static readonly DependencyProperty SideMaterialProperty =
DependencyProperty.Register("SideMaterial",
typeof(Material), typeof(SolidText),
new PropertyMetadata(SideMaterialChanged));
public Material SideMaterial
{
set { SetValue(SideMaterialProperty, value); }
get { return (Material)GetValue(SideMaterialProperty); }
}
// SideMaterialChanged handlers.
static void SideMaterialChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
((SolidText)obj).SideMaterialChanged(args);
}
void SideMaterialChanged(DependencyPropertyChangedEventArgs args)
{
// Transfer SideMaterial to RibbonText.
Text3DBase txtbase = Children[0] as Text3DBase;
txtbase.Material = args.NewValue as Material;
txtbase.BackMaterial = args.NewValue as Material;
}
// MaterialChanged override.
protected override void MaterialPropertyChanged(
DependencyPropertyChangedEventArgs args)
{
// Transfer Material and BackMaterial properties to PlanarText.
if (args.Property == MaterialProperty)
((PlanarText )Children[1] ).Material =
args.NewValue as Material;
else if (args.Property == BackMaterialProperty)
((PlanarText)Children[2]).BackMaterial =
args.NewValue as Material;
}
// TextPropertyChanged override.
protected override void TextPropertyChanged(
DependencyPropertyChangedEventArgs args)
{
base.TextPropertyChanged(args);
// Transfer text-related property to all three children.
for (int i = 0; i < 3; i++)
{
Text3DBase txtbase = Children[i] as Text3DBase;
txtbase.SetValue(args.Property, args.NewValue);
}
}
// PropertyChanged override.
protected override void PropertyChanged(
DependencyPropertyChangedEventArgs args)
{
if (args.Property == DepthProperty)
{
// Set Depth property to RibbonText and PlanarText children.
double depth = (double)args.NewValue;
((DeepTextBase )Children[0]).Depth = depth;
((PlanarText)Children[2]).Z = -depth;
}
base.PropertyChanged(args);
}
// Move on, move on. Nothing to see here.
protected override void Triangulate(
DependencyPropertyChangedEventArgs args,
Point3DCollection vertices, Vector3DCollection normals,
Int32Collection indices, PointCollection textures)
{}
}

實際上,SolidText 會充分利用它從 ModelVisual3D 繼承的屬性,即 Children 屬性。SolidText 可具有類型 ModelVisual3D 的子項,因此也可具有 RibbonText 和 PlanarText 類型的子項。應用到 SolidText 的任何轉換都會應用到它所有的子項。此外,SolidText 定義了要應用到 RibbonText 子項的 SideMaterial 屬性。SolidText 的主要工作是將對其自身設置的屬性分發到其所有子項。

SolidTextDemo 程序會旋轉 SolidText 對象,這可在上文的圖 1 中看到。為測試 PlanarText 中的 Material 傳輸邏輯,我為該圖提供了一個漸變畫筆和旋轉時可捕獲光線的反射材料。

改進工作

解決了將 RibbonText 和 PlanarText 圖形組合到統一的文本塊這一問題後,我決定嘗試將表面變得圓滑些。EllipticalText 類與 SliverText 相似,只是文本字符輪廓上的每個點變成了橢圓而不是矩形。EllipticalText 類實際上要比 SliverText 短一點。

RoundedText 從 SolidText 派生而來,並使用其構造函數來替換 SolidText 通過 EllipticalText 對象創建的 RibbonText 子項。此組合使得文字在 PlanarText 對象的邊緣具備了有些圓形的外觀,並讓文字塊的中間部分飽滿一點,在圖 10 中可以清楚地看到。

圖 10 實心三維文字 圓形程度由 EllipticalText 和 RoundedText 的 EllipseWidth 屬性控制,默認值為 0.05。如果作為 TextSize 屬性一部分的該屬性設置得過大,將會導致字符相互合並,並在動畫期間出現 Z 值爭奪。

我希望有一天能夠實現一種算法,讓我們更靈活地將矢量字分成三角形。就現在而言,這些類已經足夠。我知道目前世界上正嚴重缺乏融入了三維文字效果的飛行徽標,我謹希望此處的微薄貢獻能一解燃眉之急。

請將您想要向 Charles 詢問的問題和提出的意見發送至 [email protected].

代碼:http://download.microsoft.com/download/f/2/7/f279e71e-efb0-4155-873d-5554a0608523/Foundations2007_10.exe

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved