当前位置: 首页 > news >正文

着色器编程:我试图用GPU画一个完美圆形,结果得到了像素化的“洋葱圈”

着色器编程:我试图用GPU画一个完美圆形,结果得到了像素化的“洋葱圈”

引言:从完美的几何梦想到像素的残酷现实

在计算机图形学中,圆形是最基础也是最优雅的图形之一。无论是游戏中的光点、用户界面元素,还是粒子特效,圆形无处不在。对于开发者而言,利用GPU绘制一个圆形似乎再简单不过:只需在片段着色器中计算每个像素到圆心的距离,然后与半径比较,就能得到清晰的圆形。然而,当真正运行代码时,你可能会惊讶地发现,屏幕上呈现的不是一个光滑的圆形,而是一个带有明显阶梯状锯齿和奇怪环形纹理的“洋葱圈”。这些环状伪像就像树木的年轮一样,一圈一圈地向外扩散,破坏了图形的视觉质量。这种现象背后隐藏着着色器编程中一个核心问题:离散采样与连续几何之间的矛盾。本文将从基础圆形绘制入手,深入剖析“洋葱圈”效应的成因,并探讨多种抗锯齿技术,帮助你真正掌握在GPU上绘制完美圆形的艺术。

第一章:基础圆形绘制 —— 最简单的实现与最明显的缺陷

1.1 屏幕空间中的圆形定义

在片段着色器中绘制圆形,通常采用“隐式表面”方法。我们假设屏幕空间被归一化到 [0,1] 区间,或者使用像素坐标。对于每个片段(像素),我们计算其位置到指定圆心的距离,如果距离小于半径,则片段属于圆形内部,否则为外部。这种基于距离场的表示方法天然适合着色器,因为每个片段可以独立计算。

最简单的实现代码如下(GLSL):

glsl

// 片段着色器 uniform vec2 u_resolution; // 屏幕分辨率 uniform vec2 u_center; // 圆心坐标(归一化或像素坐标) uniform float u_radius; // 半径 uniform vec3 u_color; // 圆形颜色 void main() { // 将片段坐标转换到 [0,1] 范围 vec2 st = gl_FragCoord.xy / u_resolution.xy; // 计算到圆心的距离(这里使用归一化坐标,圆心也在同一空间) float d = distance(st, u_center); // 判断是否在圆内:d < radius 则绘制颜色,否则背景 if (d < u_radius) { gl_FragColor = vec4(u_color, 1.0); } else { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // 背景黑色 } }

这个代码逻辑清晰,但实际效果往往令人失望。当你在屏幕上观察时,会看到圆形的边缘呈现明显的锯齿(阶梯状),并且当半径较大时,边缘附近会出现一圈一圈的同心环状条纹——这就是“洋葱圈”的雏形。

1.2 为什么会出现锯齿和洋葱圈?

锯齿(Aliasing)是数字采样系统的固有问题。当我们试图用离散的像素网格表示连续的圆形边缘时,每个像素只能选择一个颜色(要么是圆内,要么是圆外),导致边缘呈现锯齿状。这种现象在图形学中被称为“走样”。

而“洋葱圈”效应则更为微妙。当我们将圆形绘制在低分辨率或对比例敏感的场景中时,由于距离计算和比较的精度问题,某些靠近边缘的像素会被错误地分类。例如,一个像素中心恰好落在距离略小于半径的位置,它被判定为圆内;而相邻像素中心距离略大于半径,则被判定为圆外。这种二值化的判决导致圆形边缘突然变化,但在距离函数梯度变化平缓的区域(即接近圆心的地方),判决是稳定的,不会产生环状图案。环状图案通常出现在半径较大时,因为距离函数的变化率较小,像素中心落在边界附近的概率分布形成了周期性的误判,表现为一系列同心的圆环。

更深层的原因是:距离函数是连续的,但判决函数是阶跃函数。阶跃函数在频域中包含无穷高频分量,而像素网格只能采样有限频率,因此产生混叠。当圆形边缘的曲率半径很大时,边缘接近直线,锯齿主要呈现为直线阶梯;当曲率半径适中时,边缘弯曲,混叠分量相互干涉,形成环状图案。

1.3 量化误差与数值精度

除了采样问题,浮点数精度也可能导致微小的误差。在着色器中,距离计算通常使用float(32位浮点),精度足够高,一般不会直接引起可见的环状图案。但是,如果半径和中心坐标以像素为单位,并且半径接近整数,那么距离比较可能在某些像素上产生歧义。例如,半径为10.0像素的圆,像素中心正好距离10.0时,由于浮点误差可能被判为内部或外部,从而产生不稳定的边缘。不过,这通常表现为边缘闪烁,而不是规则的洋葱圈。

真正的“洋葱圈”是由采样不足导致的,我们需要从信号处理的角度理解它。

第二章:采样理论视角下的圆形绘制

2.1 连续圆形的频谱

一个理想的圆形,其指示函数是二值的:内部为1,外部为0。这个函数的傅里叶变换是一个贝塞尔函数(Jinc函数),其频谱无限延伸,包含非常高的频率分量。为了在离散像素上正确再现这个函数,我们需要满足奈奎斯特采样定理:采样频率必须至少是信号最高频率的两倍。然而,像素网格的采样频率由像素间距决定,是固定的,而圆形边缘包含无限高频,因此混叠不可避免。

当我们使用阶跃函数判决时,相当于对连续距离场进行点采样(point sampling),每个像素只根据其中心点的距离值决定颜色。这种点采样是造成走样的根本原因。

2.2 像素积分与覆盖率

理想情况下,每个像素的颜色应该等于圆形在该像素区域内的覆盖率。如果圆形边缘穿过一个像素,该像素的颜色应该是圆形颜色和背景颜色的混合,混合比例等于圆形覆盖该像素的面积比例。这种基于覆盖率的着色方式能产生平滑的边缘,消除锯齿。然而,直接计算像素与圆形的相交面积是复杂的解析几何问题,在实时着色器中通常不可行。因此,我们需要近似方法。

第三章:抗锯齿技术 —— 从简单到高级

3.1 使用 smoothstep 进行边缘模糊

最常用的抗锯齿技巧是在边缘附近引入渐变,而不是硬性截止。通过将距离场与一个平滑的过渡函数结合,可以使边缘看起来更柔和,从而减轻锯齿感。GLSL提供了smoothstep(edge0, edge1, x)函数,它在x小于edge0时返回0,大于edge1时返回1,中间进行Hermite插值。我们可以利用这个函数,在距离场边界附近创造一个小的过渡带。

glsl

float d = distance(st, u_center); float smoothed = smoothstep(u_radius - 0.01, u_radius + 0.01, d); gl_FragColor = mix(vec4(u_color,1.0), vec4(0.0,0.0,0.0,1.0), smoothed);

这里,我们定义了一个宽度为0.02(归一化坐标)的过渡带。当d小于半径-0.01时,smoothed为0,输出颜色;当d大于半径+0.01时,smoothed为1,输出背景;中间线性混合。这种方法能显著减少锯齿,但“洋葱圈”现象仍然可能存在,因为过渡带的宽度是固定的,没有考虑像素的实际大小。

更好的做法是根据像素的导数(屏幕空间变化率)动态调整过渡带宽度,从而保证无论缩放或视角如何,边缘始终具有合适的模糊程度。

3.2 基于导数的自适应抗锯齿

GLSL提供了导数函数dFdxdFdy,可以计算任意变量在屏幕空间x和y方向的偏导数。对于距离场d,我们可以计算其梯度长度,从而估计在屏幕空间上一个像素对应的距离变化量。这个变化量就是fwidth(d),它近似等于abs(dFdx(d)) + abs(dFdy(d))。我们可以利用fwidth来设置smoothstep的过渡带宽度,使得过渡带的宽度恰好覆盖一个像素。

glsl

float d = distance(st, u_center); float pixelDist = fwidth(d); float smoothed = smoothstep(u_radius - pixelDist, u_radius + pixelDist, d); gl_FragColor = mix(vec4(u_color,1.0), vec4(0.0,0.0,0.0,1.0), smoothed);

这种方法非常有效,因为它保证了边缘的过渡恰好在一个像素范围内,从而实现了解析抗锯齿的效果。但要注意,当圆形非常小(直径只有几个像素)时,fwidth(d)可能过大,导致圆形过度模糊甚至消失。此时可能需要额外的处理,比如限制最小过渡宽度。

3.3 超采样(Supersampling)

如果我们需要极高质量的抗锯齿,或者场景中圆形与其他复杂图案结合,我们可以考虑在着色器内部实现超采样。超采样的思想是每个像素计算多个子样本的颜色,然后平均。这相当于提高了采样频率,从而减少混叠。在片段着色器中,我们可以手动循环多个子像素位置,计算距离并平均。

glsl

vec2 st = gl_FragCoord.xy / u_resolution.xy; vec2 subpixelOffset[4] = vec2[](vec2(0.25,0.25), vec2(0.75,0.25), vec2(0.25,0.75), vec2(0.75,0.75)); float sum = 0.0; for (int i = 0; i < 4; i++) { vec2 sampleSt = st + subpixelOffset[i] / u_resolution.xy; float d = distance(sampleSt, u_center); sum += d < u_radius ? 1.0 : 0.0; } float alpha = sum / 4.0; gl_FragColor = mix(vec4(0.0,0.0,0.0,1.0), vec4(u_color,1.0), alpha);

这种方法可以显著改善质量,消除大部分锯齿和洋葱圈,但代价是计算量倍增。对于移动设备或性能敏感的应用,4倍甚至8倍采样可能无法承受。此外,由于我们只对二值判决进行平均,本质上是在计算覆盖率,但子样本数量有限,仍然存在残余的走样,只是频率被推高到子像素级别。

3.4 解析覆盖率计算

理论上,我们可以精确计算圆形与像素正方形的相交面积,从而得到准确的覆盖率。这需要解析求解圆形与矩形的交面积。虽然对于一般位置和半径,公式复杂,但我们可以利用符号距离场的性质进行近似。一种常见方法是利用距离场的值以及其导数,构造一个线性近似,然后计算覆盖面积。例如,假设在像素内部,距离场是线性的,那么可以通过距离值和梯度计算出像素内圆形边界的位置,进而得到面积。这种方法被称为“解析抗锯齿”或“预过滤”。

在着色器中,我们可以使用fwidthddxddy来估计梯度,然后利用一个简单的sigmoid函数(如smoothstep)模拟面积。实际上,上一节的自适应smoothstep就是一种近似解析覆盖率的方法,因为它假设距离场在像素内线性变化,并且smoothstep的过渡恰好对应像素内的线性插值。

对于圆形这种具有旋转对称性的图形,还可以通过预计算查找表或使用特殊函数来加速,但在实时着色器中并不常见。

第四章:深入“洋葱圈”的成因与解决方案

4.1 为什么自适应smoothstep能消除洋葱圈?

让我们仔细分析自适应smoothstep的工作原理。对于一个理想圆形,距离场d在屏幕空间是一个连续函数,其梯度大小在圆周边界附近接近1(如果坐标是像素单位,则梯度为1)。这意味着在边缘处,每移动一个像素,距离值大约变化1。因此,fwidth(d)≈ 1。smoothstep的过渡带从radius - 1radius + 1,覆盖了大约2个像素的范围。在这2个像素内,smoothstep输出从0平滑过渡到1。对于像素中心距离恰好在半径附近的像素,它们会得到一个介于0和1之间的alpha值,实现边缘混合。

这种混合本质上是低通滤波,滤除了圆形函数的高频分量,从而满足采样定理。由于每个像素的过渡带是根据实际梯度调整的,无论圆形如何缩放,都能保证合适的平滑度。因此,原本由高频混叠引起的洋葱圈效应被抑制了。

4.2 特殊情况下的残留洋葱圈

尽管自适应smoothstep非常强大,但在某些特殊情况下,仍然可能观察到轻微的环状图案。例如,当圆形非常小(半径只有几个像素)时,梯度可能不是1,因为距离函数在小尺度上变化剧烈。fwidth(d)可能小于1,导致过渡带太窄,无法完全消除混叠。或者,如果圆形边缘接近水平或垂直方向,梯度分量不均匀,也可能导致某些方向过渡不足。此外,如果圆形颜色和背景颜色对比度极高,人眼对边缘的微小变化依然敏感。

另一个潜在问题是:fwidth(d)是基于当前像素的相邻像素计算的,它假设梯度在像素尺度上恒定。但在曲率很大的地方(如小圆形的边界),梯度变化很快,这种线性近似可能失效,导致覆盖率估计误差,表现为微小的环状瑕疵。

4.3 结合超采样与自适应平滑

对于极端高质量要求,可以将超采样与自适应平滑结合。例如,每个像素进行4个子样本采样,每个子样本使用自适应smoothstep计算alpha值,然后平均。这样既利用了子像素采样提高频率,又通过平滑保证了每个子样本的覆盖率合理。这相当于一种混合抗锯齿技术,能够达到接近SSAA的质量,而性能消耗比纯SSAA略低(因为每个子样本只需计算一次距离场)。

glsl

float totalAlpha = 0.0; for (int i = 0; i < 4; i++) { vec2 offset = subpixelOffset[i] / u_resolution.xy; vec2 sampleSt = st + offset; float d = distance(sampleSt, u_center); float pixelDist = fwidth(d); // 注意:fwidth基于当前像素的导数,对于子样本可能不准确 float alpha = 1.0 - smoothstep(u_radius - pixelDist, u_radius + pixelDist, d); totalAlpha += alpha; } float finalAlpha = totalAlpha / 4.0; gl_FragColor = mix(vec4(0.0,0.0,0.0,1.0), vec4(u_color,1.0), finalAlpha);

这里需要小心,fwidth是基于当前像素的,对子样本使用同一像素的fwidth可能不够精确。理论上,子样本的导数应基于子样本位置,但计算复杂。实践中,对于小偏移,这种近似通常足够。

第五章:进阶技巧与性能优化

5.1 使用距离场纹理

如果我们需要绘制大量圆形,或者圆形图案非常复杂(如环形、带孔),可以考虑预先计算距离场纹理。距离场纹理存储的是到最近边缘的距离,然后在着色器中采样该纹理,并使用自适应smoothstep进行渲染。这种方法将复杂形状的绘制转化为简单的纹理采样,性能高且易于抗锯齿。但需要注意纹理的精度和过滤方式(使用线性过滤,不要使用点过滤)。

5.2 圆形边缘的高质量渲染

对于纯圆形,我们可以进一步优化距离计算。使用length()函数计算距离时,会涉及开方运算,开销稍大。如果不需要精确距离,可以使用平方距离比较来避免开方。但是,为了fwidth,我们需要实际的距离值(或其导数),开方通常是必要的。不过,我们可以利用fwidth的特性:fwidth(d)可以通过fwidth(d*d)间接计算,但公式复杂。通常,现代GPU的length()已经很快,不必过度优化。

5.3 针对移动平台的优化

移动GPU的片段着色器性能至关重要。在移动设备上绘制抗锯齿圆形,可以考虑使用gl_FragCoord和内置的导数,但要注意避免复杂循环。如果不需要动态缩放,可以预计算一个圆形纹理,使用纹理采样并开启双线性过滤,这样能得到平滑边缘且性能极佳。但纹理会占用内存且缺乏灵活性。另一种技巧是使用fwidth结合smoothstep,这是性能和质量的良好平衡。

5.4 处理圆形与背景的混合

如果背景不是纯色,而是复杂的场景,上述方法仍然适用。只需将计算出的alpha值用于混合即可。但要确保背景采样也是抗锯齿的,否则边缘仍会显得生硬。

第六章:完整示例与代码解析

6.1 一个可交互的着色器示例

下面提供一个完整的GLSL片段着色器,支持uniform控制,包含自适应抗锯齿和可选的超采样模式。读者可以在Shadertoy等平台上测试。

glsl

// 圆形绘制着色器 - 自适应抗锯齿 + 可选超采样 #ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; uniform vec2 u_mouse; // 鼠标位置作为圆心 uniform float u_time; // 时间,用于动态效果 uniform int u_mode; // 0: 硬边缘, 1: 固定平滑, 2: 自适应, 3: 4x超采样 const vec3 CIRCLE_COLOR = vec3(1.0, 0.5, 0.2); // 橙色 const vec3 BG_COLOR = vec3(0.1, 0.1, 0.2); // 深蓝色 void main() { vec2 st = gl_FragCoord.xy / u_resolution.xy; vec2 center = u_mouse / u_resolution.xy; // 圆心跟随鼠标 float radius = 0.1 + 0.05 * sin(u_time); // 半径随时间变化 float alpha; if (u_mode == 0) { // 硬边缘 float d = distance(st, center); alpha = d < radius ? 1.0 : 0.0; } else if (u_mode == 1) { // 固定宽度平滑 float d = distance(st, center); float smoothWidth = 0.01; alpha = 1.0 - smoothstep(radius - smoothWidth, radius + smoothWidth, d); } else if (u_mode == 2) { // 自适应平滑 float d = distance(st, center); float w = fwidth(d); alpha = 1.0 - smoothstep(radius - w, radius + w, d); } else if (u_mode == 3) { // 4x超采样 + 自适应平滑(每个子样本自适应) // 定义子像素偏移(在归一化坐标中) vec2 subOffsets[4] = vec2[]( vec2(0.25, 0.25), vec2(0.75, 0.25), vec2(0.25, 0.75), vec2(0.75, 0.75) ); float total = 0.0; for (int i = 0; i < 4; i++) { vec2 sampleSt = st + subOffsets[i] / u_resolution.xy; float d = distance(sampleSt, center); float w = fwidth(d); // 注意:这里fwidth基于当前像素,但对小偏移近似足够 total += 1.0 - smoothstep(radius - w, radius + w, d); } alpha = total / 4.0; } vec3 color = mix(BG_COLOR, CIRCLE_COLOR, alpha); gl_FragColor = vec4(color, 1.0); }
6.2 代码解析
  • 模式0(硬边缘):直接比较,产生锯齿和可能的洋葱圈。

  • 模式1(固定平滑):使用固定宽度过渡,能平滑边缘,但固定宽度可能不匹配屏幕空间尺度,当圆形缩放或靠近时效果不一致。

  • 模式2(自适应):使用fwidth动态调整过渡宽度,这是推荐的标准方法,能有效消除洋葱圈。

  • 模式3(超采样+自适应):在每个像素内进行4次采样,每次使用自适应平滑,然后平均。这提供了极高的质量,但性能消耗最大。

注意,模式3中fwidth的使用是一个近似。理论上,每个子样本应该有独立的导数,但实际中由于偏移量很小,误差可忽略。

6.3 测试与观察

在不同模式下运行着色器,可以直观对比效果:

  • 模式0:明显锯齿,当半径较大时,能看到一圈圈环状图案,尤其是靠近边缘的地方。

  • 模式1:边缘平滑,但可能在某些尺度下过渡带太宽(导致模糊)或太窄(残留锯齿)。

  • 模式2:边缘平滑且无残留环状,无论圆形大小如何变化,视觉效果始终一致。

  • 模式3:边缘极其平滑,几乎看不到任何瑕疵,但GPU负载较高。

第七章:数学推导与深入理解

7.1 距离场梯度与屏幕空间导数

对于圆形距离场 d=(x−xc)2+(y−yc)2d=(x−xc​)2+(y−yc​)2​,其梯度为 (x−xcd,y−ycd)(dx−xc​​,dy−yc​​),梯度长度为1。这意味着距离场在空间中是“等距”变化的,即每移动单位长度,距离值变化1。在屏幕空间中,如果坐标以像素为单位,那么梯度大小就是1。因此,fwidth(d)近似等于屏幕空间上一个像素的步长对应的距离变化,正好为1。这正是自适应过渡带的理论基础。

7.2 smoothstep函数的性质

smoothstep(edge0, edge1, x) 实现了从0到1的平滑过渡,其导数在两端为0,中间为连续。当edge0和edge1非常接近时,它近似于阶跃函数;当间隔较大时,它表现为线性插值(实际上是三次Hermite插值)。使用smoothstep进行alpha混合,相当于对边缘施加了一个低通滤波器,其频率响应取决于过渡带的宽度。通过将宽度设为fwidth(d),我们保证了滤波器的截止频率与像素网格的采样频率匹配,从而最大限度地减少混叠。

7.3 混叠的量化分析

考虑一个一维情况:信号 f(x)f(x) 为阶跃函数,在 x=0x=0 处从0跳变到1。采样频率为1(像素间距为1),采样点位于整数位置。如果阶跃位置恰好位于整数点,则采样结果完美;否则,会产生锯齿。当阶跃位置在0.5时,采样结果为0,1,0,1...产生棋盘格图案。在二维中,类似的情况发生在圆形边缘上。通过引入预滤波器(即smoothstep),我们实际上在采样前对信号进行了低通滤波,滤除了高于奈奎斯特频率的分量,从而避免混叠。

第八章:现实世界中的应用案例

8.1 UI 中的圆形图标

在用户界面中,圆形图标(如头像、按钮)需要清晰锐利,同时边缘不能有锯齿。自适应抗锯齿技术广泛用于UI渲染。许多现代UI框架(如Qt、Flutter)在底层使用类似的着色器技术绘制圆角矩形和圆形。

8.2 粒子系统中的光点

粒子系统中的粒子通常渲染为圆形光点,且经常有发光效果。如果简单地使用点精灵(point sprites)并开启纹理,可能会导致边缘锯齿。通过片段着色器绘制圆形并应用抗锯齿,可以使粒子边缘柔和,与背景融合自然,提升整体视觉质量。

8.3 游戏中的技能指示器

许多游戏使用圆形范围指示器,如英雄联盟中的技能范围。这些指示器需要精确且平滑,否则会影响玩家判断。自适应抗锯齿可以确保指示器边缘在任何缩放级别下都清晰可见,不会出现闪烁的环纹。

8.4 科学可视化

在科学可视化中,常常需要绘制精确的圆形标记。如果图像质量不佳,可能导致数据误读。高质量的圆形渲染至关重要。

第九章:常见陷阱与调试技巧

9.1 导数计算不准确

fwidth函数依赖于相邻像素的计算结果。在片段着色器中,如果代码中存在条件分支(如if语句),可能导致导数计算不正确,因为相邻像素可能走不同的分支。因此,在使用fwidth时应避免在包含导数的表达式之后出现分支,或者确保所有像素都执行相同的代码路径。通常,将导数计算放在所有分支之前,并将结果存储起来。

9.2 过渡带宽度过大导致模糊

如果fwidth(d)的值过大(例如,由于距离场梯度很大),可能导致过渡带太宽,圆形边缘变得模糊。这种情况通常发生在圆形非常小或距离场变化剧烈的地方。可以通过限制最大宽度来缓解,例如float w = min(fwidth(d), 0.1);

9.3 处理圆形的内部空洞

如果需要绘制环形(圆环),距离场可能是到两个边界的距离。此时需要计算两个smoothstep并组合。也要注意fwidth的计算可能需要针对每个距离独立进行。

9.4 调试技巧

要调试抗锯齿效果,可以尝试放大渲染到纹理,观察边缘的alpha值分布。也可以使用颜色编码显示导数大小,例如gl_FragColor = vec4(vec3(fwidth(d)), 1.0),这样能直观看到哪些区域过渡带宽度异常。

第十章:展望未来 —— 硬件与算法的演进

随着GPU架构的发展,硬件支持了更多的抗锯齿技术,如MSAA(多重采样抗锯齿)和CSAA(覆盖采样抗锯齿)。这些硬件特性可以在不增加着色器复杂度的情况下实现高质量抗锯齿。然而,对于过程式绘制的图形,如着色器生成的圆形,MSAA可能无法完全消除锯齿,因为MSAA只在几何边界处起作用,而我们的圆形是在片段着色器中生成的,对GPU而言它是一个全屏四边形,内部颜色变化通过着色器控制,MSAA无法自动识别这个内部边缘。因此,手动在着色器中实现抗锯齿仍然不可或缺。

未来,随着可变速率着色(VRS)和机器学习辅助抗锯齿技术的发展,可能会有更高效的解决方案,但基本原理不会改变。掌握基于距离场的自适应抗锯齿技术,是每个着色器程序员必备的技能。

第十一章:总结与练习

11.1 核心要点回顾
  • 简单的二值距离比较会产生锯齿和“洋葱圈”效应,原因是点采样导致的高频混叠。

  • 通过引入基于导数宽度的smoothstep过渡,可以实现有效的抗锯齿,消除洋葱圈。

  • 超采样可以进一步提高质量,但性能成本高。

  • 距离场导数(fwidth)是实现自适应抗锯齿的关键。

  • 理解采样定理和信号处理有助于深入解决问题。

11.2 实践练习
  1. 在Shadertoy上实现一个圆形绘制程序,分别尝试硬边缘、固定平滑、自适应平滑,观察不同参数下的效果。

  2. 修改代码,让圆形跟随鼠标移动,并实时改变半径,体会自适应平滑的稳定性。

  3. 尝试绘制多个圆形,并处理它们重叠时的混合。

  4. 实现一个圆环绘制,应用自适应抗锯齿,观察环的内外边缘是否平滑。

  5. 思考如何绘制椭圆,并应用相同的抗锯齿原理。

第十二章:代码附录

12.1 抗锯齿圆形绘制函数(可复用)

以下是一个封装好的函数,可以直接在片段着色器中调用:

glsl

// 计算圆形的抗锯齿alpha值 // st: 当前像素的归一化坐标 // center: 圆心归一化坐标 // radius: 半径(归一化) float circleAA(vec2 st, vec2 center, float radius) { float d = distance(st, center); float w = fwidth(d); return 1.0 - smoothstep(radius - w, radius + w, d); }
12.2 包含环形绘制的扩展

绘制圆环:

glsl

float ringAA(vec2 st, vec2 center, float radius, float thickness) { float d = abs(distance(st, center) - radius); float w = fwidth(d); return 1.0 - smoothstep(thickness - w, thickness + w, d); }
12.3 完整的GLSL示例(Shadertoy风格)

glsl

// Shadertoy 入口 void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 st = fragCoord / iResolution.xy; vec2 center = iMouse.xy / iResolution.xy; if (iMouse.z < 0.5) center = vec2(0.5); // 默认中心 float radius = 0.2 + 0.1 * sin(iTime); float alpha = circleAA(st, center, radius); vec3 col = mix(vec3(0.1,0.1,0.2), vec3(1.0,0.5,0.0), alpha); fragColor = vec4(col, 1.0); }

结语

绘制一个完美的圆形,看似简单,实则蕴含着图形学的核心思想。从最初的像素化“洋葱圈”到光滑无瑕的圆形,我们一步步探索了采样、混叠、滤波和自适应技术的奥秘。希望本文能帮助你深入理解着色器编程中的抗锯齿原理,并在实际项目中灵活运用,创作出更高质量的视觉效果。

记住,在计算机图形学中,没有绝对的完美,只有不断的逼近。但正是这种对完美的追求,推动着技术不断进步。现在,轮到你去绘制属于自己的完美圆形了。

http://www.jsqmd.com/news/477301/

相关文章:

  • LangChain:定义智能体时代新框架
  • 2026年正规线上幼犬训犬排行榜单出炉,谁能跻身TOP行列?
  • 学习笔记:Linux 创建新用户并安装Miniconda
  • 电力电子,buck型DC-DC变换器电路, 双闭环控制,开环闭环对比仿真 输入24v输出12v...
  • ​影视飓风Tim探展德施曼AWE展台,联合B站现场解锁AI智能锁新玩法
  • ESP32光传感器监测项目
  • 别再被参数带偏了!哪个品牌的护眼灯最好?从真实使用体验出发,深度对比书客、明基、柏曼、霍尼韦尔等12款热门护眼台灯,一次讲清选灯的核心逻辑
  • 2026年主流降论文AI率工具实测:哪款效果最好?
  • 别被割韭菜!过来人揭秘:赛一证书对零基础AI求职的真实价值
  • 类加载子系统
  • ISTA 3E 和 3B 的区别
  • OpenClaw 3.12 重磅更新:全新 Dashboard、移动端适配与安全增强
  • 教资笔记资源合集
  • 快速搭建Django投票应用
  • 封神!技术面试答“线上紧急故障”,用《孙子兵法》拿捏面试官[特殊字符]
  • 程序员为什么不自己开发应用赚钱
  • 新手教师管班:别让你的善良没有锋芒
  • Laravel 9.x 核心特性全解析
  • 二叉树的中序遍历
  • 什么是 Java 内存模型(JMM)?
  • OpenClaw 换机迁移指南
  • LLM几种主要的开源方式及优劣
  • EIG旗下MidOcean Energy将从JERA手中收购Gorgon LNG项目额外权益;双方探讨建立战略联盟
  • 2026 实测8款降AI率工具!知网/维普/Turnitin降AI率效果大比拼!
  • 执行引擎子系统
  • 软件测试进阶 | HTML常用标签详解:Web UI测试的“定位神器”
  • 用 AI 助手自动完成浏览器操作:OpenClaw 实战分享
  • Flutter 三方库 belatuk_combinator 鸿蒙适配指南 - 工业级组合数学运算与大规模排列枚举实战
  • 从园区到云核:传统网络与数据中心网络的分野与交汇
  • 第九章 微积分与数据分析:趋势预测和最优决策的工具