【发布时间】:2018-12-22 14:26:58
【问题描述】:
对于可缩放矢量图形的 GPU 渲染器的预处理步骤之一,我正在处理 SVG 曲线(四种类型:直线、二次和三次贝塞尔曲线以及椭圆弧)。其中一个步骤是在交叉点上进行曲线细分,作为填充双连接边列表的前置算法。
以前,我将每种类型的曲线存储在一个单独的结构中(Line、QuadraticBezier、CubicBezier 和 EllipticArc),这意味着,要对它们进行操作,我需要编写相同的(类似)这些组合中的每一个的代码(导致代码的高达 10 次)。现在,我想尝试一些不同的东西。我有两个选择:使用接口ICurve,或使用具有Type 的struct Curve 并重新组合所有操作。
在曲线上运行的代码如下所示(禁止重复):
// Reunite all the shapes
var curves = new List<ICurve>(path.PathCommands.Length);
foreach (var cmd in path.PathCommands)
/* generate the curves by evaluating the path commands */;
// Reunite all intersections to subdivide the curves
var curveRootSets = new SortedSet<float>[curves.Count];
for (int i = 0; i < curveRootSets.Length; i++)
curveRootSets[i] = new SortedSet<float>(new[] { 1f }, Half.Comparer);
// Get all intersections
for (int i = 0; i < curves.Count; i++)
for (int j = i+1; i < curves.Count; j++)
foreach (var pair in CurveIntersectionPairs(curves[i], curves[j]))
{
curveRootSets[i].Add(pair.A);
curveRootSets[j].Add(pair.B);
}
// Account for possibly-duplicate curves
foreach (var set in curveRootSets) set.Remove(0f);
// Subdivide the curves
var curvesSubdiv = curves.Zip(curveRootSets, delegate (ICurve curve, SortedSet<float> set)
{
float v = 0f;
var list = new List<ICurve>();
foreach (var l in set)
{
list.Add(curve.Subcurve(v, l));
v = l;
}
return list;
}).Aggregate(Enumerable.Empty<ICurve>(), Enumerable.Concat);
我采用的第一种方法是定义一个interface ICurve 和必要的合同,以允许它在细分阶段和 DCEL 阶段都工作。界面如下所示:
public interface ICurve
{
// Evaluates the curve at parameter "t"
Vector2 At(float t);
// The derivative of the curve (aka the "velocity curve")
ICurve Derivative { get; }
// Gets the curve that maps [0,1] to [l,r] on this curve
ICurve Subcurve(float l, float r);
// The bounding box of the curve
FRectangle BoundingBox { get; }
// The measure of how much counterclockwise the curve is
// (aka double the area swept by it and the segments that
// connect the endpoints to the origin)
float Winding { get; }
// Does the curve degenerate to a single point?
bool IsDegenerate { get; }
}
然后我会让每个结构实现接口:
public struct Line : ICurve { /* ... */ }
public struct QuadraticBezier : ICurve { /* ... */ }
public struct CubicBezier : ICurve { /* ... */ }
public struct EllipticArc : ICurve { /* ... */ }
这很神奇,每个类都正确实现了契约方法,因此我可以抽象地使用ICurves,甚至可以根据 SVG 规范的要求实现更多曲线。但是我读过一些关于接口装箱/拆箱的恐怖故事(因为它们是引用类型)以及在堆上分配的结构,这些结构将分散在内存中。我想我可以通过设置struct Curve 和public readonly CurveType Type 以及所有必要的字段来变得更好(关于缓存一致性、间接性和其他方式):
public enum CurveType { Line, QuadraticBezier, CubicBezier, EllipticArc }
public partial struct Curve
{
// four Vector2's are sufficient for now
readonly Vector2 A, B, C, D;
public readonly CurveType Type;
// Evaluates the curve at parameter "t"
public Vector2 At(float t)
{
// "Dispatch" the function based on the type
switch (Type)
{
case CurveType.Line: return LineAt(t);
case CurveType.QuadraticBezier: return QuadraticBezierAt(t);
case CurveType.CubicBezier: return CubicBezierAt(t);
case CurveType.EllipticArc: return EllipticArcAt(t);
default: return new Vector2(float.NaN, float.NaN);
}
}
// Other contract methods implemented similarly ...
}
由于structs 是值类型,我可以使用List<Curve>(在内部使用数组)在本地分配它们,并从缓存局部性中受益(至少一点)。但是,我会失去“开放继承”的能力(我认为这不是问题,因为在今天的 SVG 上使用的曲线很少)。
但也许我过度设计了它?我是 C# 的新手(使用 C++ 已经有几年了),我不知道接口会给值类型带来的复杂性,也许接口提供的动态调度会比“手动调度”更快。因此,我想询问您对这些具体方法的了解。也许还有另一个我不知道的更好的选择?
请记住,我正在准备将其用于处理真实世界的 SVG 文件,这意味着每条路径可能有数十或数百条曲线,并且之后必须对其进行细分。当我开始生成包含每条曲线的基元、分配填充和 SVG 的其他细微差别时,我可以重用这些结构。
【问题讨论】:
-
如果你寻求表演,使用固定大小的数组而不是List,没有foreach而是常规for。避免使用linq,而是制作数组副本。您的结构大小是否超过 16 个字节?如果是,您将失去堆叠优势。 (stackoverflow.com/questions/1082311/…)
标签: c#