AMD FidelityFX™ Variable Shading
AMD FidelityFX Variable Shading 将可变速率着色引入您的游戏。
本页内容

在之前的博文中,我们向您介绍了从顶点着色器到网格着色器的转变。此外,我们还展示了如何测量和优化性能以及最佳实践。
在这篇博文中,我们演示了每图元属性以及如何使用它们来简化字体渲染。每图元属性是通过网格着色器引入的。使用我们在此处使用的字体渲染技术,我们仅通过每个字符串一次绘制调用即可获得无限级别的细节,但这仅在使用网格着色器时可行。我们发现这是一个不错、足够简单、实用且有用的示例,用于探索网格着色器带来的新功能。
| 缩放 | 调试视图 |
|---|---|
![]() | ![]() |
字体渲染可能是最普遍的计算机图形学问题之一。事实上,您现在正在阅读的这篇博文,如果没有字体渲染,您将无法阅读。由于其无处不在,我们认为字体渲染是理所当然的,但很少承认其挑战,甚至不欣赏其算法。
字体由字形组成。字形是字符的图形表示,例如字母、数字、符号等。字体渲染是将字形字符串绘制到光栅显示器上的过程。
在这篇博文中,我们展示了如何使用网格着色器来实现 Loop 和 Blinn 在 2005 年 SIGGRAPH 会议论文“使用可编程图形硬件进行分辨率无关的曲线渲染”中的方法。
然而,他们最初的顶点着色器管线实现存在三个缺点:
作为改进,我们做出了以下贡献:
我们此处提出的方法也适用于矢量图。然而,关于性能和性能优化的比较案例研究超出了这篇博文的范围。这篇博文应主要关注网格着色器提供的每图元属性功能,并通过实际示例进行演示,也许还能为您提供一些灵感。
我们从 TrueType 字体格式 (TTF) 获取字体。在那里,字形由两种类型的曲线组成:线性曲线,它们实际上是线段,以及二次曲线。我们使用线性曲线来绘制“L”之类的字母中的直线边缘,二次曲线用于模拟“.”(句号/点/全句号)之类的字符中的圆形形状,而“S”之类的字母则包含线性曲线和二次曲线的混合。在图中,我们用紫罗兰色笔触突出显示线段,用橙色笔触突出显示二次曲线。线段的端点用黑点标记。
| 线条 | 曲线 | 线条与曲线 |
|---|---|---|
多条曲线被组织起来形成一个闭合连续样条,即曲线的有序序列。相邻的曲线在一个共同点相遇。这就是为什么样条称为连续。此外,第一条和最后一条曲线也需要相邻。然后,样条形成一个循环。这就是为什么我们将样条称为闭合。上面的字形“L”、“.”和“S”就是其中一个字形只包含一个闭合连续样条的例子。
一个字形可能包含多个样条,例如分号“;”由两个样条组成。样条甚至可以嵌套形成像“O”这样的字符。
| 两个独立的样条 | 两个嵌套的样条 |
|---|---|
为了确定一个点是否需要填充,我们从该点发射一条射线到一个任意方向。如果该射线与样条相交偶数次,那么我们就位于填充部分的外部。
TTF 使用贝塞尔曲线表示线性和二次曲线段:对于线性曲线段,公式为
f(t)=(1−t)⋅a+t⋅b,t∈[0…1]
以及二次贝塞尔曲线段
f(t)=(1−t)2⋅a+2(1−t)t⋅c+t2⋅b,t∈[0…1],
其中 a, c,以及 b 是控制点。非字母顺序并非笔误:这样做是为了在两种情况下,a 和 b 是曲线段的端点。
Loop and Blinn 渲染两种类型的曲线,但二次曲线更有趣。考虑右括号字形 “)”
它由四个二次曲线段组成,其控制点为 (a0,c0,b0), (a1,c1,b1), (a3,c3,b3),以及 (a4,c4,b4),以及 (a2,b2) 和 (a5,b5)。它还有两个线性段:(a2,b2) 和 (a5,b5)。
为了得到字形的一个“有棱角的”近似,我们计算点集 {ai,bi,ci} 在以下约束条件下
在我们的括号示例中,(a0,c0,b0),(a1,c1,b1),(a3,c3,b3),(a4,c4,b4) 都是凸的,而 (a1,c1,b1) 都是凹的。这里展示了一个示例细分
我们称“有棱角的近似”的三角形为实心三角形。然而,字形仍然显得有棱角。为了得到平滑的轮廓,我们需要添加下方所示的二次贝塞尔曲线,蓝色表示凸形,红色表示凹形。
在详细介绍如何渲染贝塞尔曲线之前,让我们快速概述一下如何找到实心三角形。在那里,我们使用许多开源包中的一个,它们会生成所谓的约束 Delaunay 三角剖分。它们通常会进行标准的凸 Delaunay 三角剖分(左侧),可以移除外部三角形(中间),甚至可以检测孔洞(右侧)。我们使用后者来处理字体。
| 带有外部三角形 | 不带外部三角形,但填充了孔洞 | 带有外部三角形并移除孔洞 |
|---|---|---|
![]() | ![]() | ![]() |
我们可以使用传统的顶点着色器管线,将结果渲染为具有索引和顶点缓冲区的常规三角形网格。
现在,我们已经完成了实心三角形部分。接下来,我们必须处理构成二次贝塞尔曲线的三角形(即前面蓝色和红色的三角形)。
要渲染二次贝塞尔曲线,Loop and Blinn 建议渲染一个三角形,如下所示
这通过丢弃贝塞尔曲线三角形一侧的像素来渲染二次贝塞尔曲线。由此产生的填充区域形成凸形或凹形区域。
| 凸形 | 凹形 |
|---|---|
以下是此方法有效的原因:查看标准二次贝塞尔曲线的坐标,我们得到
[uv]=[00](1−t2)+2(1−t)t[210]+t2[11]=[tt2],这直接给出了一个点是否在该曲线上的条件 u2−v=0。我们丢弃那些与 < 和 > 比较 0 (分别用于凹形和凸形填充)不符的点。
Loop 和 Blinn 的观察是,光栅化阶段会插值 [u;v]⊤。这实现了从二次贝塞尔曲线的控制点 a, c, and b 的二次贝塞尔曲线的坐标空间。
为了渲染一个字形,Loop 和 Blinn 区分了三种三角形类型
因此,我们在预处理中创建了三个索引缓冲区。对于我们示例中的“)”字符,我们将得到
顶点缓冲区
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| x | +0.000 | +0.150 | +0.150 | +0.150 | +0.150 | +1.000 | -0.300 | -0.165 | -0.165 | -0.165 | -0.165 | -0.300 |
| y | -1.000 | -0.500 | +0.000 | +0.000 | +0.500 | +1.000 | +1.000 | +0.500 | +0.000 | +0.000 | -0.500 | -1.000 |
| u | +0.000 | +0.500 | +1.000 | +0.000 | +0.500 | +1.000 | +0.000 | +0.500 | +1.000 | +0.000 | +0.500 | +1.000 |
| v | +0.000 | +0.000 | +1.000 | +0.000 | +0.000 | +1.000 | +0.000 | +0.000 | +1.000 | +0.000 | +0.000 | +1.000 |
实心三角形的索引缓冲区
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| i0 | 11 | 10 | 2 | 8 | 7 | 7 |
| i1 | 0 | 0 | 8 | 2 | 2 | 5 |
| i2 | 10 | 2 | 10 | 7 | 5 | 6 |
带凸曲线三角形的索引缓冲区
| 索引 | 0 | 1 |
|---|---|---|
| i0 | 0 | 3 |
| i1 | 1 | 4 |
| i2 | 2 | 5 |
带凹曲线三角形的索引缓冲区
| 索引 | 0 | 1 |
|---|---|---|
| i0 | 6 | 9 |
| i1 | 7 | 10 |
| i2 | 8 | 11 |
请注意,虽然具有索引 2 和 3 的顶点具有相同的坐标,但它们的标准坐标不同。对于顶点索引 8 和 9 也是如此。
对于传统的顶点流水线渲染,合理的操作是发出三个绘图调用,每个索引缓冲区一个。然而,三个绘图调用产生的 API 开销比一个绘图调用更大。因此,我们将展示如何将字形渲染简化为具有每个图元属性的单个绘图调用。
相同类型曲线的相邻三角形可能共享一个公共点,即 a_i=b_i+1 . 然而,标准二次贝塞尔曲线所需的底层 [u;v]⊤ 坐标在 b_i 和 [0;0]⊤ 在 a_i+1 。这使得顶点重用成为不可能。绕过顶点重复的一种方法是利用贝塞尔曲线的对称性,但无法完全解决不必要的顶点重复问题。另一个缺点是,沿线携带标准贝塞尔曲线需要存储额外的顶点属性。
作为一种解决方案,我们建议不存储标准二次贝塞尔曲线的控制点,因此 [u;v]⊤ 的值,所有这些。相反,我们在像素着色器中从重心坐标计算插值 [u;v]⊤。
float2 computeUV(const float3 bary){ // The three control points a, c, b of the canonical Bézier curve: float2 a = float2(0.0f, 0.0f); float2 c = float2(0.5f, 0.0f); float2 b = float2(1.0f, 1.0f); // Explicitly carry out the interpolation using barycentrics. return bary.x * a + bary.y * c + bary.z * b;}我们可以从像素着色器输入结构中的 SV_BARYCENTRICS 语义中获得重心坐标。
struct PixelIn{ float4 position : SV_POSITION; float3 bary : SV_BARYCENTRICS;}因此,我们只需要顶点缓冲区中的二次贝塞尔曲线的位置。我们甚至可以与实心三角形共享顶点。这消除了对两个每顶点属性的需求,并解决了顶点重复问题:
在那里,顶点缓冲区具有更少的属性和更少的顶点。
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| x | +0.000 | +0.150 | +0.150 | +0.150 | +1.000 | -0.300 | -0.165 | -0.165 | -0.165 | -0.300 |
| y | -1.000 | -0.500 | +0.000 | +0.500 | +1.000 | +1.000 | +0.500 | +0.000 | -0.500 | -1.000 |
但是我们仍然卡在三个索引缓冲区上,即一个用于实心三角形的索引缓冲区,
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| i0 | 9 | 8 | 2 | 7 | 6 | 6 |
| i1 | 0 | 0 | 8 | 2 | 2 | 4 |
| i2 | 8 | 2 | 7 | 6 | 4 | 5 |
一个用于凸三角形的索引缓冲区,
| 索引 | 0 | 1 |
|---|---|---|
| i0 | 0 | 2 |
| i1 | 1 | 3 |
| i2 | 2 | 4 |
以及一个用于凹三角形的索引缓冲区。
| 索引 | 0 | 1 |
|---|---|---|
| i0 | 5 | 7 |
| i1 | 6 | 8 |
| i2 | 7 | 9 |
因此,我们仍然需要三个绘图调用。我们实际想要的是每个图元的属性,我们在其中编码三角形类型并将其传递给光栅化阶段。然而,这不受传统顶点着色器流水线的支持,而这正是现代网格着色器流水线发挥作用的地方。
由于网格着色器不依赖于由顶点缓冲区和索引缓冲区组成的固定输入格式,因此我们可以自由地从 GPU 内存中读取任何我们想要的数据。我们所要做的就是将三角形传递给光栅化阶段。因此,对于我们的小括号示例,我们创建一个顶点位置缓冲区和一个将所有三角形组合在一起的索引缓冲区。但是现在,我们添加了第三个缓冲区,我们称之为图元属性缓冲区。它存储三角形是否具有凸曲线、凹曲线或实心三角形。
| 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| i0 | 9 | 8 | 2 | 7 | 6 | 6 | 0 | 2 | 5 | 7 |
| i1 | 0 | 0 | 8 | 2 | 2 | 4 | 1 | 3 | 6 | 8 |
| i2 | 8 | 2 | 7 | 6 | 4 | 5 | 2 | 4 | 7 | 9 |
| 图元属性缓冲区 | 实心 | 实心 | 实心 | 实心 | 实心 | 实心 | 凸形 | 凸形 | 凹形 | 凹形 |
出于解释目的,我们直接在 HLSL 代码中定义了几何形状。
static const float2 pos[] ={ { +0.000, -1.00 }, // 0 { +0.150, -0.50 }, // 1 { +0.150, +0.00 }, // 2 { +0.150, +0.50 }, // 3 { +0.000, +1.00 }, // 4 { -0.300, +1.00 }, // 5 { -0.165, +0.50 }, // 6 { -0.165, +0.00 }, // 7 { -0.165, -0.50 }, // 8 { -0.300, -1.00 }, // 9};
static const uint3 tri[] ={ // Filled Triangles { 4, 5, 6 }, // 0 { 4, 6, 2 }, // 1 { 6, 7, 2 }, // 2 { 7, 8, 2 }, // 3 { 2, 8, 0 }, // 4 { 9, 0, 8 }, // 5 // Convex curve triangles { 0, 1, 2 }, // 6 { 2, 3, 4 }, // 7 // Concave curve triangles { 5, 6, 7 }, // 8 { 7, 8, 9 }, // 9};
static const uint SOLID = 0;static const uint CONVEX = 1;static const uint CONCAVE = 2;
static uint primAttr[] ={ SOLID, // 0 SOLID, // 1 SOLID, // 2 SOLID, // 3 SOLID, // 4 SOLID, // 5 CONVEX, // 6 CONVEX, // 7 CONCAVE, // 8 CONCAVE, // 9};因此,我们将网格着色器输出的最大三角形数设置为 128(out indices uint3 outputTriangles[128]),最大顶点数设置为 64(out vertices VertOut outputVertices[64])。除此之外,我们还必须为每个网格着色器工作组发出 128 个属性(out primitives PrimOut outputPrimAttr[128])。我们在这里调整了参数以最好地适应我们的测试字体,该字体从不需要超过 128 个三角形或 64 个顶点。我们可以将字形批量组合以增加顶点和三角形的数量,但这将在未来的工作中进行优化。这导致了我们的网格着色器的以下输入签名:
[NumThreads(128, 1, 1)][OutputTopology("triangle")]void MSMain( uint tid : SV_GroupThreadID, // thread id within the thread group out indices uint3 outputTriangles[128], out vertices VertOut outputVertices[64], out primitives PrimOut outputPrimAttr[128] // NEW when using primitive attributes! )outputTriangles 的每个元素都是一个 uint3,即三个指向每个网格着色器线程组的整数。 outputVertices 中的每个输出顶点都是一个 struct,其中包含位置。
struct VertOut{ float4 position : SV_POSITION;};现在,有趣的事情来了。我们有一个 struct 用于我们发出的图元 outputPrimAttr。
struct PrimOut{ uint triangleType : BLENDINDICES0;};接下来,我们设置输出图元和顶点的数量。
const uint nVerts = sizeof(pos) / sizeof(float2);const uint nPrims = sizeof(tri) / sizeof(uint3);SetMeshOutputCounts(nVerts, nPrims);输出顶点。
if (tid < nVerts){ outputVertices[tid].position = float4(pos[tid].xy, 0.0f, 1.0f);}最后,我们写入图元,**包括新颖的每个图元属性**。
if (tid < nPrims){ outputTriangles[tid] = tri[tid]; // regular mesh shader code to output triangles outputPrimAttr[tid].triangleType = primAttr[tid]; // NEW when using primitive attributes!}像素着色器随后像这样消耗这些属性:
struct PixelIn{ float4 position : SV_POSITION; float3 bary : SV_BARYCENTRICS; uint triangleType : BLENDINDICES0;};请注意,网格着色器将每个顶点的属性 position 和每个图元的属性 triangleType 输出到两个不同的 struct 中。然而,像素着色器在一个 struct 中消耗它们。网格着色器输出结构和像素着色器输入结构之间的连接是通过语义 BLENDINDICES0 和 SV_POSITION 实现的。
基于每个图元的属性,我们现在可以决定像素着色器正在处理的片段是来自凸曲线、凹曲线还是实心三角形,并相应地丢弃片段。
float4 PSMain(PixelIn p) : SV_TARGET{ const uint t = p.triangleType; // the per-primitive attribute. It is constant for all fragments! const float2 uv = computeUV(p.bary); // map back to the canonic Bézier curve const float y = uv.x * uv.x - uv.y; // evaluate the canonic Bézier curve.
// for triangle containing convex and concave curve, decide whether we are inside or outside of Bézier curve. if (((t == CONVEX) && (y > 0.0f)) || ((t == CONCAVE) && (y < 0.0f))) { discard; } // In case of a solid triangle or a non-discarded fragment of a left- or right-triangle. return float4(1, 0, 0, 1);}您可以在附录中找到完整的着色器代码。
我们现在勾勒出一个简单的字体渲染系统的工作原理。在预处理阶段,我们计算并上传每个字形的信息,以及用于在网格着色器中快速访问它们的结构。然后在运行时,我们上传一个字符串并使用网格着色器进行渲染。
对于每个 ASCII 字符,我们创建一个字形网格,我们称之为**字形片段**(glyphlet)。我们将所有字形片段的几何信息放入一个大的 GPU 顶点、索引和每个图元缓冲区中。
为了在渲染时获取每个字形片段的几何信息,我们需要能够正确地索引这些缓冲区。因此,我们存储每个字形片段的 GlyphletInfo。
struct GlyphletInfo { unsigned int vertexBaseIndex; // Index to the first vertex in the large vertex buffer. unsigned int triangleBaseIndex; // Index to the first triangle in the index-buffer and // per-primitive attribute buffer. unsigned int vertexCount; // Number of vertices for that glyph. unsigned int primitiveCount; // Number of primitives for that glyph.};并维护一个 GPU 数组,其中包含每个字符的 GlyphletInfo。我们可以直接使用字符的 ASCII 码来索引该数组。
在 HLSL 代码中,我们使用 StructuredBuffer 来访问数组。
// Large vertex buffer containing the vertex positions of all glyphlets.StructuredBuffer<float2> vertexBuffer : register(t0);// Large index buffer containing the index buffer of all glyphlets.StructuredBuffer<uint3> indexBuffer : register(t1);// Large index buffer containing the per-primitive information for all glyphlets.StructuredBuffer<uint> perPrimitiveBuffer : register(t2);
// Buffer to find starting position of for each glyphlet in the vertexBuffer,// indexBuffer, and per-primitive buffer.StructuredBuffer<GlyphletInfo> glyphletBuffer : register(t3);要在运行时渲染字符串,我们复制其所有字符并将它们的位置存储在一个数组中。
/// This is CPU Codestruct CharacterRenderInfo { float2 pos; // position of the char. uint character; // which char (8 bits would be enough, but D3D12 wants 32 Bit at least)};std::vector<CharacterRenderInfo> textToRender;每次字符串更改时,我们将 textToRender 复制到 GPU 缓冲区。
// Buffer that needs to be renderedStructuredBuffer<CharacterRenderInfo> textToRender : register(t4);然后,我们调度与字符串中的字符数相同的线程组。每个线程组渲染一个字形:我们使用组线程 ID SV_GroupID 来查找 textToRender 的 GPU 副本中的字符。
[NumThreads(128, 1, 1)][OutputTopology("triangle")]void MSMain( uint gtid : SV_GroupThreadID, uint gid : SV_GroupID, out indices uint3 outputTriangles[128], out vertices VertOutput outputVertices[128], out primitives PrimOutput outputPrimAttr[128]){
const uint glyphIndex = textToRender[gid].character;有了字符,我们就可以索引 GlyphletInfo 数组。
const GlyphletInfo glyphletInfo = glyphletBuffer[glyphIndex];并告诉光栅化阶段我们希望输出多少图元和顶点。
SetMeshOutputCounts(glyphletInfo.vertexCount, glyphletInfo.primitiveCount);剩下的就是将字形片段复制到网格着色器输出缓冲区。
if (gtid < glyphletInfo.primitiveCount) { outputTriangles[gtid] = indexBuffer[glyphletInfo.triangleBaseIndex + gtid]; outputPrimAttr[gtid].triangleType = perPrimitiveBuffer[glyphletInfo.triangleBaseIndex + gtid]; } if (gtid < glyphletInfo.vertexCount) { float2 position = vertexBuffer[glyphletInfo.vertexBaseIndex + gtid] + text[gid].pos.xy; outputVertices[gtid].position = mul(DynamicConst.transformationMatrix, float4(position, 0.0f, 1.0f)); }}这使得字符串能够进行实时、与分辨率无关的渲染。

它甚至适用于多重采样抗锯齿 (MSAA):只需启用 MSAA 并激活采样着色。这可以通过在 float3 bary 前面添加 sample 关键字来完成。
struct PixelIn{ float4 position : SV_POSITION; // Add sample & enable MSAA for Anti-aliasing sample float3 bary : SV_BARYCENTRICS; uint triangleType : BLENDINDICES0;};我们在 AMD Radeon™ RX 7600 显卡上测试了我们的系统。它运行得很好并且速度很快,但我们尚未进行性能比较研究。
我们证明了网格着色器可以改进 Loop 和 Blinn 字体渲染算法的 GPU 实现。我们的网格着色器实现比传统的顶点着色器流水线实现的内存空间需求、可用性和简洁性方面都有所改进。
请注意,我们的网格着色器实现不需要将几何图形作为主要输入。相反,它读取一个字符串,并从**字形片段**(即小的索引、顶点和每图元缓冲区)创建该字符串的网格。因此,字符串的几何图形直接在芯片上创建,就在图形流水线的中间。
请注意,这绝不是一个完全成熟/详尽的字形渲染器。存在比我们在单篇博文中能容纳的更多的边缘情况、陷阱和性能考虑因素。
static const uint SOLID = 0;static const uint CONVEX = 1;static const uint CONCAVE = 2;
static const float2 pos[] ={ { +0.000, -1.00 }, // 0 { +0.150, -0.50 }, // 1 { +0.150, +0.00 }, // 2 { +0.150, +0.50 }, // 3 { +0.000, +1.00 }, // 4 { -0.300, +1.00 }, // 5 { -0.165, +0.50 }, // 6 { -0.165, +0.00 }, // 7 { -0.165, -0.50 }, // 8 { -0.300, -1.00 }, // 9
};
static const uint3 tri[] ={ // CONVEX curve triangles { 0, 1, 2 }, // 0 { 2, 3, 4 }, // 1 // CONCAVE curve triangles { 5, 6, 7 }, // 2 { 7, 8, 9 }, // 3 // Filled Triangles { 4, 5, 6 }, // 4 { 4, 6, 2 }, // 5 { 6, 7, 2 }, // 6 { 7, 8, 2 }, // 7 { 2, 8, 0 }, // 8 { 9, 0, 8 }, // 9};
static uint primAttr[] ={ CONVEX, //0 CONVEX, // 1 CONCAVE, // 2 CONCAVE, // 3 SOLID, // 4 SOLID, // 5 SOLID, // 6 SOLID, // 7 SOLID, // 8 SOLID, // 9};
struct VertOut{ float4 position : SV_POSITION;};
struct PrimOut{ uint triangleType : BLENDINDICES0;};
struct PixelIn{ float4 position : SV_POSITION; sample float3 bary : SV_BARYCENTRICS; uint triangleType : BLENDINDICES0;};
[NumThreads(128, 1, 1)][OutputTopology("triangle")]void MSMain( uint tid : SV_GroupThreadID, out indices uint3 outputTriangles[128], out vertices VertOut outputVertices[64], out primitives PrimOut outputPrimAttr[128]){ const uint nVerts = sizeof(pos) / sizeof(float2); const uint nPrims = sizeof(tri) / sizeof(uint3);
SetMeshOutputCounts(nVerts, nPrims);
if (tid < nPrims) { outputTriangles[tid] = tri[tid]; outputPrimAttr[tid].triangleType = primAttr[tid]; } if (tid < nVerts) { outputVertices[tid].position = float4(0.9 * pos[tid].xy, 0.0f, 1.0f); }}
float2 computeUV(const float3 bary){ const float u = bary.x * 0 + bary.y * 0.5f + bary.z * 1; const float v = bary.x * 0 + bary.y * 0.0f + bary.z * 1; return float2(u, v);}
float computeQuadraticBezierFunction(const float2 uv){ return uv.x * uv.x - uv.y;}
float4 PSMain(PixelIn p) : SV_TARGET{ const uint t = p.triangleType; const float2 uv = computeUV(p.bary); const float y = computeQuadraticBezierFunction(uv);
if (((t == CONVEX) && (y > 0.0f)) || ((t == CONCAVE) && (y < 0.0f))) { discard; }
return float3(1, 0, 0, 1);}第三方网站链接仅为方便用户提供,除非另有明确说明,AMD不对任何此类链接网站的内容负责,且不暗示任何认可。GD-98