本文主要是介绍17.3 OpenGL将片段和样本写入帧缓冲区:每个片段的操作,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
每个片段的操作 Per-Fragment Operations
片段(fragment)从光栅化阶段生成时带有窗口坐标(xw, yw)和深度值z。根据章节15的内容,片段会经历可编程片段处理阶段以添加附加数据。接下来,在章节17描述的一系列逐片段操作中,片段会被进一步修改,并可能被丢弃,这些操作包括但不限于模板测试、深度测试和遮挡查询等。
在某些情况下,按照第14.9节所述,如果片段着色器程序有此要求,模板测试、深度测试以及遮挡查询可以在片段着色之前进行,以便提前筛选掉不满足条件的片段。最终,通过所有测试且未被丢弃的片段将用于更新帧缓冲区相应位置的像素颜色与属性。
流程:(标有 * 的框可以在早期的每片段操作期间执行,如第 14.9 节所述。)
- Fragment(or sample) from Rasterization
- Fragment Shader (Fragment (or sample) and Associated Data)
- Alpha To Coverage Operations
- Stencil Test (*) ←→
Framebuffer
- Depth Buffer Test (*) ←→
Framebuffer
- Occlusion Query (*)
- Blending ←
Framebuffer
- SRGB Conversion
- Dithering
- Logicop ←→
Framebuffer
- Additional Multisample Fragment Operations
- Write To
Framebuffer
Alpha转覆盖 Alpha To Coverage
Alpha To Coverage 是图形渲染流水线中的一个可选步骤,主要用于在多重采样抗锯齿(Multisample Anti-Aliasing, MSAA)环境中改进边缘平滑度。当这个功能启用时,它会影响片段的透明度(alpha)值,并将其转化为覆盖样本(coverage samples)信息。
具体流程如下:
-
启用与禁用:Alpha To Coverage 功能通过调用
Enable
和Disable
函数并指定目标SAMPLE_ALPHA_TO_COVERAGE
来开启或关闭。另一个相关选项是SAMPLE_ALPHA_TO_ONE
,它将片段的 alpha 值直接替换为最大可能的 alpha 值。 -
适用条件:该步骤仅在以下条件下执行:
- MULTISAMPLE 已启用。
- SAMPLE_BUFFERS 的值为 1,表示存在多重采样缓冲区。
- 如果 draw buffer 0 不是 NONE 并且其引用的缓冲区不是整数格式。
-
Alpha 值来源:所有提及的 alpha 值均指片段着色器输出链接至颜色编号零、索引零的 alpha 组件(参见第 15.2.3 节)。
-
Alpha To Coverage 过程:若
SAMPLE_ALPHA_TO_COVERAGE
启用,则会根据片段着色器输出的 alpha 值生成一个临时的覆盖值,每个采样点的 bit 状态由对应位置的 alpha 值决定(参见第 14.3.1 节)。随后,这个临时覆盖值会与原始片段覆盖值进行 AND 操作,生成一个新的片段覆盖值。这个临时覆盖值的生成方式类似于基于片段 alpha 值的比例性和不变性原则计算的采样覆盖率(参考第 14.9.3 节),只是它是基于 alpha 值而非自定义函数,并且 alpha 值会被限制在 [0, 1] 区间内。 -
Alpha To One:如果同时启用了
SAMPLE_ALPHA_TO_ONE
,则每个片段的 alpha 值都会被替换为其所在固定点颜色缓冲区的最大可表示 alpha 值,或者是浮点缓冲区中的 1.0。反之,若未启用此功能,则 alpha 值保持不变。
模板测试 Stencil Test
根据模板缓冲区中位置 (xw, yw) 处的值与参考值之间的比较结果有条件地丢弃片段。可以使用 Enable 和 Disable 命令,使用 STENCIL_TEST 作为目标来启用或禁用测试。当禁用测试时,不进行模板测试和相关修改,片段始终会通过。该功能可用于各种高级渲染技术,如切割孔洞、应用自定义蒙版或执行多重渲染。
有两组与模板相关的状态,分别是前置模板状态集和后置模板状态集。当处理由非多边形原始(点和线)和正面多边形原始产生的片段时,模板测试和写入使用前置模板状态集;而当处理由背面多边形原始产生的片段时,则使用后置模板状态集。
当不存在模板缓冲时,尽管可能仍会调用StencilFunc,但模板测试表现得就像总是通过一样。在初始状态下,模板测试是禁用的,并且设置了默认参数用于参考值、比较函数和掩码。
void glStencilFunc( enum func, int ref, uint mask );
void glStencilFuncSeparate( enum face, enum func, int ref, uint mask );
void glStencilOp( enum sfail, enum dpfail, enum dppass );
void glStencilOpSeparate( enum face, enum sfail, enum dpfail, enum dppass );
设置模板测试函数以及参考值和掩码
void glStencilFunc( enum func, int ref, uint mask );
func
:表示模板测试函数,是一个枚举值,可以是以下之一:GL_NEVER
:永不通过。GL_ALWAYS
:总是通过。GL_LESS
:如果模板值小于参考值,则通过。GL_LEQUAL
:如果模板值小于或等于参考值,则通过。GL_EQUAL
:如果模板值等于参考值,则通过。GL_GEQUAL
:如果模板值大于或等于参考值,则通过。GL_GREATER
:如果模板值大于参考值,则通过。GL_NOTEQUAL
:如果模板值不等于参考值,则通过。
ref
:表示模板测试的参考值,是一个整数。mask
:表示模板值与掩码进行按位与运算的掩码值,是一个无符号整数。
该函数设置模板测试函数后,后续的模板测试将使用指定的函数、参考值和掩码来进行比较。
设置独立的面的模板测试函数、参考值和掩码
void glStencilFuncSeparate( enum face, enum func, int ref, uint mask );
face
:表示要设置的面,是一个枚举值,可以是以下之一:GL_FRONT
:只设置前向面(即正面)的模板测试函数。GL_BACK
:只设置背向面(即背面)的模板测试函数。GL_FRONT_AND_BACK
:同时设置前向面和背向面的模板测试函数。
设置模板缓冲区操作
void glStencilOp( enum sfail, enum dpfail, enum dppass );
sfail
:表示模板测试失败时执行的操作。它是一个枚举值,可以是以下之一:GL_KEEP
:保持当前模板值不变。GL_ZERO
:将模板值设置为零。GL_REPLACE
:用参考值替换当前模板值。GL_INCR
:递增模板值。如果已经达到最大值,则不进行更改。GL_DECR
:递减模板值。如果已经达到最小值,则不进行更改。GL_INVERT
:按位反转当前模板值。GL_INCR_WRAP
:递增模板值,并在达到最大值后循环到最小值。GL_DECR_WRAP
:递减模板值,并在达到最小值后循环到最大值。
dpfail
:表示模板测试通过但深度测试失败时执行的操作。它也是一个枚举值,可选项与sfail
参数相同。dppass
:表示模板测试和深度测试都通过时执行的操作。同样也是一个枚举值,可选项与sfail
参数相同。
这些参数用于指定在模板测试的不同阶段(失败、通过但深度测试失败、通过且深度测试通过)发生时,模板缓冲区中的模板值应该如何进行操作。
设置单独的面的模板操作
void glStencilOpSeparate( enum face, enum sfail, enum dpfail, enum dppass );
face
:指定要设置的面。可以是以下值之一:GL_FRONT
:仅影响正面(前向面)。GL_BACK
:仅影响背面(后向面)。GL_FRONT_AND_BACK
:同时影响正面和背面。
深度缓冲区测试 Depth Buffer Test
深度缓冲区测试根据深度比较结果来丢弃传入的片段。比较是通过通用的 Enable 和 Disable 命令来启用或禁用的,使用的目标是 DEPTH_TEST。当禁用时,深度比较以及随后可能对深度缓冲区值进行的更新都被跳过,片段将传递到下一个操作。但是,如果启用了深度测试,比较将会发生,并且深度缓冲区和模板值随后可能会被修改。
如果深度测试失败,传入的片段将被丢弃。在片段的 (xw, yw) 坐标处的模板值将根据当前生效的深度缓冲区测试失败函数进行更新。否则,片段将继续到下一个操作,并且深度缓冲区在片段的 (xw, yw) 位置处的值将设置为片段的 zw 值。在这种情况下,模板值将根据当前生效的深度缓冲区测试成功函数进行更新。
设置深度测试的比较函数
void glDepthFunc( enum func );
- 参数
func
: 表示深度测试的比较函数,可以是以下常量之一:GL_NEVER
:永不通过深度测试。GL_ALWAYS
:始终通过深度测试。GL_LESS
:如果传入片段的深度值小于当前深度缓冲区中的深度值,则通过深度测试。GL_LEQUAL
:如果传入片段的深度值小于或等于当前深度缓冲区中的深度值,则通过深度测试。GL_EQUAL
:如果传入片段的深度值等于当前深度缓冲区中的深度值,则通过深度测试。GL_GEQUAL
:如果传入片段的深度值大于或等于当前深度缓冲区中的深度值,则通过深度测试。GL_GREATER
:如果传入片段的深度值大于当前深度缓冲区中的深度值,则通过深度测试。GL_NOTEQUAL
:如果传入片段的深度值不等于当前深度缓冲区中的深度值,则通过深度测试。
深度测试功能允许您控制在渲染过程中如何进行深度测试,以确定片段是否应该覆盖当前深度缓冲区中的内容。
遮挡查询 Occlusion Queries
利用查询对象来统计在渲染管线的早期阶段(如深度测试后)未被丢弃的片段数量。启动和结束查询分别通过调用 BeginQuery
和 EndQuery
函数,并指定目标参数(如 SAMPLES_PASSED
、ANY_SAMPLES_PASSED
或 ANY_SAMPLES_PASSED_CONSERVATIVE
)。
-
对于
SAMPLES_PASSED
目标,会在查询开始时重置计数器,之后每个通过深度测试的样本都会使计数器递增。当有多重采样缓冲时,计数器增加的值取决于覆盖位设置的样本数;如果没有多重采样,则每片断增加一。 -
当查询结束后,累积的样本通过计数会被写入相应的查询对象作为查询结果,并标记为可获取状态。
-
若使用
ANY_SAMPLES_PASSED
作为目标,维护的是一个布尔状态,初始设为 FALSE。在查询期间,只要有任何片段通过深度测试,该布尔状态就会变为 TRUE。而ANY_SAMPLES_PASSED_CONSERVATIVE
目标允许实现采用更宽松的标准来确定是否通过,可能会导致非精确的结果(即误报),但可能提供更好的性能。
混合 Blending
混合发生在片段着色器输出颜色与帧缓冲区(framebuffer)中已存在的目标颜色之间。当启用混合时,新产生的片段的RGBA
分量会与帧缓冲区在该片段坐标(xw, yw)位置上的目标颜色的RGBA分量进行组合。组合过程遵循一个称为混合方程(blend equation)的规则。
对于固定点格式的颜色缓冲区,在执行混合方程之前,源和目标颜色以及混合因子的各分量会被分别约束到[0, 1]范围内(对于无符号归一化颜色缓冲区)或[-1, 1]范围内(对于有符号归一化颜色缓冲区)。而如果是浮点格式的颜色缓冲区,则不进行任何钳制操作。最终得到的四个结果颜色分量将被传递至后续的操作阶段。
如果颜色缓冲区采用整数格式,则直接跳过混合步骤进入下一个操作。
void glEnablei( enum target, uint index );
void glDisablei( enum target, uint index );
target
为BLEND
;index
是指定与符号常量DRAW_BUFFERi
相关联的绘制缓冲区的整数i
;若指定的颜色缓冲区属于 FRONT、BACK、LEFT、RIGHT 或 FRONT_AND_BACK(这代表了多个颜色缓冲区的情况),那么启用或禁用状态对所有这些相关联的缓冲区都有效。
也可以通过调用 Enable 或 Disable 函数并传入 GL_BLEND 目标来一次性控制所有绘制缓冲区的混合功能。
混合方程 Blend Equation
设置混合方程
// 同时设置所有绘制缓冲区
void glBlendEquation( enum mode ); // RGBA
void glBlendEquationSeparate( enum modeRGB, enum modeAlpha ); // RGB 与 A 分开设置// 设置单个绘制缓冲区
void glBlendEquationi( uint buf, enum mode ); // RGBA
void glBlendEquationSeparatei( uint buf, enum modeRGB, enum modeAlpha ); // RGB 与 A 分开设置
GL_FUNC_ADD
:将源颜色和目标颜色相加。GL_FUNC_SUBTRACT
:将源颜色减去目标颜色。GL_FUNC_REVERSE_SUBTRACT
:将目标颜色减去源颜色。GL_MIN
:选择源颜色和目标颜色中的较小值。GL_MAX
:选择源颜色和目标颜色中的较大值。
Mode | RGB Components | Alpha Component |
---|---|---|
FUNC_ADD | RGB = RGBs * Sr + RGBd * Dr | A = As ∗ Sa + Ad ∗ Da |
FUNC_SUBTRACT | RGB = RGBs * Sr - RGBd * Dr | A = As ∗ Sa − Ad ∗ Da |
FUNC_REVERSE_SUBTRACT | RGB = RGBd * Dr - RGBs * Sr | A = Ad ∗ Da − As ∗ Sa |
MIN | RGB = min( RGBs, RGBd ) | A = min(As, Ad) |
MAX | RGB = max( RGBs, RGBd ) | A = max(As, Ad) |
混合函数 Blend Functions
设置混合因子
void glBlendFunc( enum src, enum dst );
void glBlendFuncSeparate( enum srcRGB, enum dstRGB, enum srcAlpha, enum dstAlpha );void glBlendFunci( uint buf, enum src, enum dst );
void glBlendFuncSeparatei( uint buf, enum srcRGB, enum dstRGB, enum srcAlpha, enum dstAlpha );
- GL_ZERO:零
- GL_ONE:一
- GL_SRC_COLOR:源颜色
- GL_ONE_MINUS_SRC_COLOR:反源颜色
- GL_DST_COLOR:目标颜色
- GL_ONE_MINUS_DST_COLOR:反目标颜色
- GL_SRC_ALPHA:源 alpha 值
- GL_ONE_MINUS_SRC_ALPHA:反源 alpha 值
- GL_DST_ALPHA:目标 alpha 值
- GL_ONE_MINUS_DST_ALPHA:反目标 alpha 值
- GL_CONSTANT_COLOR:常量颜色
- GL_ONE_MINUS_CONSTANT_COLOR:反常量颜色
- GL_CONSTANT_ALPHA:常量 alpha 值
- GL_ONE_MINUS_CONSTANT_ALPHA:反常量 alpha 值
- GL_SRC_ALPHA_SATURATE
- GL_SRC1_COLOR
- GL_ONE_MINUS_SRC1_COLOR
- GL_SRC1_ALPHA
- GL_ONE_MINUS_SRC1_ALPHA
设置混合常量颜色(Cc)
void glBlendColor( float red, float green, float blue, float alpha );
双源混合功能,需要用到第二个颜色输入的混合函数,在启用时可能会占用额外的硬件资源,从而限制了帧缓冲区可同时附加的最大绘制缓冲区数量。具体能支持多少个双源绘制缓冲区是与图形API实现相关的,并可以通过调用 GetIntegerv
函数查询 MAX_DUAL_SOURCE_DRAW_BUFFERS
参数来获取。
在需要使用双源混合功能的情况下,应当依据 MAX_DUAL_SOURCE_DRAW_BUFFERS
而非 MAX_DRAW_BUFFERS
来确定帧缓冲区最多可以连接多少个绘制缓冲区。并且规定要求 MAX_DUAL_SOURCE_DRAW_BUFFERS
的最小值为1,这意味着如果该参数值仅为1,则无法同时进行双源混合和利用多个绘制缓冲区渲染,开发者必须在这两种特性之间做出选择。
Function | RGB Blend Factors | Alpha Blend Factor |
---|---|---|
ZERO | (0, 0, 0) | 0 |
ONE | (1, 1, 1) | 1 |
SRC_COLOR | (Rs0, Gs0, Bs0) | As0 |
ONE_MINUS_SRC_COLOR | (1, 1, 1) − (Rs0, Gs0, Bs0) | 1 − As0 |
DST_COLOR | (Rd, Gd, Bd) | Ad |
ONE_MINUS_DST_COLOR | (1, 1, 1) − (Rd, Gd, Bd) | 1 − Ad |
SRC_ALPHA | (As0, As0, As0) | As0 |
ONE_MINUS_SRC_ALPHA | (1, 1, 1) − (As0, As0, As0) | 1 − As0 |
DST_ALPHA | (Ad, Ad, Ad) | Ad |
ONE_MINUS_DST_ALPHA | (1, 1, 1) − (Ad, Ad, Ad) | 1 − Ad |
CONSTANT_COLOR | (Rc, Gc, Bc) | Ac |
ONE_MINUS_CONSTANT_COLOR | (1, 1, 1) − (Rc, Gc, Bc) | 1 − Ac |
CONSTANT_ALPHA | (Ac, Ac, Ac) | Ac |
ONE_MINUS_CONSTANT_ALPHA | (1, 1, 1) − (Ac, Ac, Ac) | 1 − Ac |
SRC_ALPHA_SATURATE | (min(As0, 1 − Ad), min, min) | 1 |
SRC1_COLOR | (Rs1, Gs1, Bs1) | As1 |
ONE_MINUS_SRC1_COLOR | (1, 1, 1) − (Rs1, Gs1, Bs1) | 1 − As1 |
SRC1_ALPHA | (As1, As1, As1) | As1 |
ONE_MINUS_SRC1_ALPHA | (1, 1, 1) − (As1, As1, As1) | 1 − As1 |
混合操作的状态
- 混合方程:每个绘制缓冲区都需要两个整数来分别指定RGB和Alpha通道的混合方程。初始状态下,RGB和Alpha通道的混合方程都是
FUNC_ADD
,意味着颜色是通过源颜色和目标颜色相加得到的。 - 混合函数:四个整数分别表示源和目标的RGB及Alpha通道的混合函数。初始化时,源RGB和Alpha的混合函数为
ONE
(即完全使用源颜色),目标RGB和Alpha的混合函数为ZERO
(即不使用目标颜色)。 - 混合启用状态:一个标志位用于表明是否启用了混合功能。默认情况下,所有绘制缓冲区的混合功能均被禁用。
- 常量混合颜色:还需要四个浮点值来存储RGBA常量混合色。初始化时,这个常量颜色为(0, 0, 0, 0)。
查询相关混合状态的方法如下:
- 查询绘制缓冲区i的混合启用状态,可以通过调用
IsEnabledi
函数,参数target设为BLEND
,索引index设为i。 - 查询混合方程和函数的值,可以调用
GetIntegeri_v
函数,并根据表23.21所示设置相应的目标和索引i。 - 对于绘制缓冲区零,也可以通过不带索引参数的方式调用
IsEnabled
或GetIntegerv
函数查询混合启用状态或混合方程与函数的值。
混合操作会在当前启用混合并允许写入的每个颜色缓冲区上执行一次,使用各自缓冲区的颜色 Cd 进行计算。如果某个颜色缓冲区没有Alpha值,则假设 Ad 等于 1。
sRGB 转换 sRGB Conversion
如果启用了 FRAMEBUFFER_SRGB
,并且目标缓冲区对应的帧缓冲区附件的 FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING
的值为 SRGB
(请参见第 9.2.3 节),则混合后的 R、G、B 值将通过计算转换为非线性 sRGB
颜色空间
if ( cl ≤ 0 ) cs = 0.0
if ( 0 < cl < 0.0031308 ) cs = 12.92 * cl
if ( 0.0031308 ≤ cl < 1 ) cs = 1.055 * cl ^ 0.41666 − 0.055
if ( cl ≥ 1 ) cs = 1.0
其中 cl
是 R、G 或 B 元素,cs
是结果(有效转换为 sRGB 颜色空间)。
如果 FRAMEBUFFER_SRGB
被禁用或者 FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING
的值不是 SRGB
,则
cs = cl
生成的 R、G 和 B 的 cs
值以及未修改的 A 形成新的 RGBA
颜色值。 如果颜色缓冲区是定点的,则每个分量都会被限制在 [0, 1] 范围内,然后使用公式 2.3 转换为定点值。 所得的四个值被发送到后续的抖动操作。
抖动 Dithering
Dithering 是一种图像处理技术,用于在有限的颜色缓冲区中模拟更多颜色细节。当某个像素的颜色值无法精确匹配到可表示的颜色时,dithering 会根据每个颜色分量的输入值 c 和像素在窗口坐标系中的位置 (xw, yw),从两个最接近的可表示颜色值中选择一个。这两个值分别是小于等于输入颜色值的最大可表示颜色值和大于等于输入颜色值的最小可表示颜色值。
启停 dithering 功能可以通过调用 OpenGL 的 Enable
或 Disable
函数,并设置目标参数为 DITHER
来实现。该功能的状态仅需要一个布尔标志位来存储,初始化时,默认是启用 dithering 的。
在禁用 dithering 的情况下,系统仍会选择一个靠近输入颜色值的可表示颜色,但这个选择不依赖于像素的 xw 和 yw 坐标。然而,当启用 dithering 时,选择算法可以利用这些坐标信息以更智能的方式进行抖动,从而产生更好的视觉效果和平滑过渡。
逻辑操作 Logical Operation
逻辑操作是对片段颜色值与帧缓冲区中相应位置存储的颜色值之间执行的一种特定运算。当启用逻辑操作时(通过调用Enable函数,并指定目标为COLOR_LOGIC_OP),片段着色器生成的最终颜色会与帧缓冲区中对应像素点(xw, yw)处的颜色值进行逻辑运算,而非进行常规的混合操作。即使BLEND功能是启用状态,一旦颜色逻辑操作启用,也会暂时忽略混合效果。
如果多个颜色被写入到多个缓冲区中,针对每个片段颜色及其对应的缓冲区都会独立地计算和应用逻辑操作。
需要注意的是,逻辑操作不会对浮点型的目标颜色缓冲区产生影响,同样,在FRAMEBUFFER_SRGB被启用且帧缓冲区附件的颜色编码格式为SRGB的情况下,逻辑操作也将无效。然而,即使在这种情况下,如果逻辑操作已启用,混合仍然会被禁用。
设置逻辑操作模式
void glLogicOp( enum op );
Logicop Mode | Operation |
---|---|
CLEAR | 0 |
AND | s ∧ d |
AND_REVERSE | s ∧ ¬d |
COPY | s |
AND_INVERTED | ¬s ∧ d |
NOOP | d |
XOR | s xor d |
OR | s ∨ d |
NOR | ¬(s ∨ d) |
EQUIV | ¬(s xor d) |
INVERT | ¬d |
OR_REVERSE | s ∨ ¬d |
COPY_INVERTED | ¬s |
OR_INVERTED | ¬s ∨ d |
NAND | ¬(s ∧ d) |
SET | all 1’s |
s
是传入片段的值,d
是存储在帧缓冲区中的值
额外的多样本片段操作 Additional Multisample Fragment Operations
- DrawBuffer模式与多重采样缓冲区:如果DrawBuffer模式设为
NONE
,则不会对任何多重采样或颜色缓冲区进行更改。然而,若非NONE
,以下规则适用。 - 启用多重采样:当多重采样(
MULTISAMPLE
)开启,并且至少有一个样本缓冲(SAMPLE_BUFFERS
= 1)时,每个像素样本分别进行模板测试、深度测试、混合、抖动和逻辑操作,而不是仅对整个片段执行一次。若某个特定样本的模板或深度测试失败,则只终止该样本的处理,而不会丢弃整个片段。这些操作发生在绑定的多重采样渲染缓冲附件(若有绘制帧缓冲对象)或默认帧缓冲区的多重采样缓冲中。 - 片段覆盖度:对于每个具有覆盖度位值为1的样本,会执行模板、深度、混合等操作;如果覆盖度位是0,则不为该样本执行任何操作。
- 禁用多重采样:当多重采样被禁用但仍有单个样本缓冲时,实现可以选择优化策略。可能仅针对中心样本进行模板和深度测试。若深度测试通过,则所有深度和颜色样本都会根据中心样本的深度值和颜色值更新;否则,不对任何多重采样缓冲的颜色或深度值进行更改。
- 未绑定帧缓冲对象:如果没有绑定绘制帧缓冲对象,在完成对多重采样缓冲的所有操作后,将多个颜色样本合并成单一颜色值,并将其写入由
DrawBuffer
或DrawBuffers
指定的相应颜色缓冲中。合并方法依赖于具体实现,但对于sRGB色彩空间,建议在写入前先进行线性化并计算样本平均值,类似于混合操作的方式。
这篇关于17.3 OpenGL将片段和样本写入帧缓冲区:每个片段的操作的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!