0%

Render line

渲染API对于线渲染的支持

wireframe line mode line width
OpenGL glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) glDrawArrays: GL_LINES/GL_LINE_STRIP/GL_LINE_LOOP glLineWidth
Vulkan VkPipelineRasterizationStateCreateInfo/VK_POLYGON_MODE_LINE VkPipelineInputAssemblyStateCreateInfo: VK_PRIMITIVE_TOPOLOGY_LINE_LIST/VK_PRIMITIVE_TOPOLOGY_LINE_STRIP vkCmdSetLineWidth, VkPipelineRasterizationStateCreateInfo::lineWidth
DirectX 11 D3D11_RASTERIZER_DESC/D3D11_FILL_WIREFRAME IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST)
DirectX 12 D3D12_GRAPHICS_PIPELINE_STATE_DESC/D3D12_FILL_WIREFRAME D3D12_GRAPHICS_PIPELINE_STATE_DESC/D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE, IASetPrimitiveTopology(D3D12_PRIMITIVE_TOPOLOGY_LINELIST)

glLineWidth的文档中说只保证宽度1的设置,其他宽度依赖硬件实现。

Only width 1 is guaranteed to be supported; others depend on the implementation.

glspec46.core#678 E.2.1 Deprecated But Still Supported Features把glLineWidth设为Deprecated。 They may be removed from a future version of OpenGL, and are removed in a forward compatible context implementing the core profile

Widelines- LineWidthvalues greater than 1.0 will generate an INVALID_VALUE error.

Vulkan设置线宽度,如果VkPipelineDynamicStateCreateInfo::pDynamicStates设置了VK_DYNAMIC_STATE_LINE_WIDTH,则使用vkCmdSetLineWidth设置后续的线渲染宽度;否则通过VkPipelineRasterizationStateCreateInfo::lineWidth创建pipeline时设置。如果设置非1.0的宽度,依赖硬件的支持,需要查询硬件是否支持VkphysicalDeviceFeatures::wideLines特性,可以通过GPUinfo查询。

If the wideLines feature is not enabled, lineWidth must be 1.0

DX11/12都没有明确的API和数据结构支持wide line的渲染。只发现Line Drawing Support in D3DX (Direct3D 9)中描述了基于上古D3D9封装的ID3DXLine通过textured polygons来模拟线宽模式设置。

基于我在本地平台测试,在Nvidia GeForce RTX 4080 Super显卡上,glLineWidth无效,后来发现其实在我的平台上glLineWidth是有效的,主要是因为设置了glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE),GLFW_OPENGL_FORWARD_COMPAT导致OpenGL上下文是向前兼容模式,效果就是移除Deprecated的API,glLineWidth就是其中一个(4.6 Deprecated); Vulkan通过VkPipelineRasterizationStateCreateInfo::lineWidth设置宽度有效,且有相应的上限值。在网络上线宽设置无效的技术贴早已有之,比如glLineWidth and glLineStipple sustainability, glLineWidth - doesn’t work !, Line_width not working in Vulkan都有涉及。

On my hardware, lines are always 1 pixel large, whatever the parameters are, for many years now. Create polygons for larger lines is the solution that will always work.

可见,虽然各家API各异,但是在线渲染宽度上基本一致,完全取决于驱动的实现和硬件的能力。在一般情况下,即便API给出了设置的接口,却并不能十足相信其能够work,最稳妥的方法,还是通过对线段顶点进行四边形扩展来实现宽线段的渲染。

如何渲染线

关于线的渲染,如果显卡具备相应的功能,并且API也支持,那当然万事大吉、探囊取物了;但是,问题就出在这个但是,如果不凑巧,您当前的显卡和开发环境不允许,或者您的代码需要跨平台,可能就需要更稳妥的方案了。其实也没有多少方案,唯四边形扩展一种尔,既稳妥又好用。真是越朴实简易的东西,越能穿越时间,历久弥坚,真正是重剑无锋大巧不工,大道至简。

线的宽度

关于线的宽度,要以什么单位去计量?很直观的结论就是,在屏幕空间,以像素计。我觉得有两点理由:1)线理论上是没有宽度的,如果在模型空间给线一个宽度,经过透视投影之后,有近大远小,这就很奇怪;2)线的光栅化,首先把两个点变换到屏幕空间,然后再对两个点之间线条进行光栅化,这个光栅化的结果很自然就是一个像素宽度的片元序列(如果线平行于坐标轴),所以某些API和硬件默认只能光栅化1个像素宽度的线也就不足为奇了。

线宽算法

知道了线宽的单位是屏幕空间像素计量之后,使用quad polygon把线扩展成两个三角形的方法就比较明显了,以任何方法在屏幕空间把线段的两个顶点扩展成4个顶点,这个过程可以在CPU里计算,也可以在GPU的VSGS等顶点处理阶段计算。如果在顶点处理阶段完成,输出的坐标是clip space裁剪空间坐标,而我们扩展四边形的操作是在屏幕空间完成的,就涉及到坐标变换的具体实现了。

quad的生成算法如下:

  1. Transform line segment end points p and q to normalized device space. At this point we work in 2D space, regardless of whether original line was specified as 2D or 3D line.
  2. Calculate direction d, corrected for viewport aspect ratio.
  3. Calculate unit normal vector n = {-d.y, d.x}
  4. Modulate n by approperiate line widths to get vectors n_a and n_b
  5. Create quad points a = p + n_a, b = p - n_a, c = q + n_b, d = q - n_b
  6. Move points a, b, c, d back to clip space and pass them down the rasterization pipeline

具体实现

根据OpenGL Line Widthrabbid76的回答,整理了一套可以运行的代码示例,我是直接拖进LearnOpenGL运行的。运行结果如下:
line_width

line_width.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void main()
{
int line_i = gl_VertexID / 6;
int tri_i = gl_VertexID % 6;

vec4 va[4];
for (int i=0; i<4; ++i)
{
va[i] = u_mvp * vertex[line_i+i];
va[i].xyz /= va[i].w;
va[i].xy = (va[i].xy + 1.0) * 0.5 * u_resolution;
}

vec2 v_line = normalize(va[2].xy - va[1].xy);
vec2 nv_line = vec2(-v_line.y, v_line.x);

vec4 pos;
if (tri_i == 0 || tri_i == 1 || tri_i == 3)
{
vec2 v_pred = normalize(va[1].xy - va[0].xy);
vec2 v_miter = normalize(nv_line + vec2(-v_pred.y, v_pred.x));

pos = va[1];
pos.xy += v_miter * u_thickness * (tri_i == 1 ? -0.5 : 0.5) / dot(v_miter, nv_line);
}
else
{
vec2 v_succ = normalize(va[3].xy - va[2].xy);
vec2 v_miter = normalize(nv_line + vec2(-v_succ.y, v_succ.x));

pos = va[2];
pos.xy += v_miter * u_thickness * (tri_i == 5 ? 0.5 : -0.5) / dot(v_miter, nv_line);
}

pos.xy = pos.xy / u_resolution * 2.0 - 1.0;
pos.xyz *= pos.w;
gl_Position = pos;
}

点评一下:

  • 考虑了line_strip连接处的法线,利用连接处前后两个线段法线的平分线作为法线法向,并通过cos计算应该沿法线延申的长度;给顶点buffer多添加首点和末点,减少判断
  • 接近平行的两端线段,cos值接近于0,除法操作导致顶点沿着法线延伸的长度过大,需要处理一下,比如max(dot(v_miter, nv_line), 0.8),可以缓解不能消除
    line_width_defact

抗锯齿

硬件抗锯齿

参考