点精灵确实非常适合粒子系统。但它们与 VBO 和 GLSL 没有任何关系,这意味着它们是完全正交的特性。无论您是否使用点精灵,您始终必须使用 VBO 来上传几何体,无论它们只是点、预制精灵还是其他什么,并且您始终必须将此几何体通过一组着色器(在现代 OpenGL 中)当然)。
话虽这么说,点精灵在现代 OpenGL 中得到了很好的支持,只是不像旧的固定功能方法那样自动支持。不支持的是点衰减功能,该功能允许您根据点到相机的距离来缩放点的大小,您必须在顶点着色器内手动执行此操作。以同样的方式,您必须使用特殊的输入变量在适当的片段着色器中手动对点进行纹理化gl_PointCoord
(这表示当前片段在整个点的 [0,1] 方格中的位置)。例如,基本点精灵管道可能如下所示:
...
glPointSize(whatever); //specify size of points in pixels
glDrawArrays(GL_POINTS, 0, count); //draw the points
顶点着色器:
uniform mat4 mvp;
layout(location = 0) in vec4 position;
void main()
{
gl_Position = mvp * position;
}
片段着色器:
uniform sampler2D tex;
layout(location = 0) out vec4 color;
void main()
{
color = texture(tex, gl_PointCoord);
}
就这样。当然,这些着色器只是进行最基本的纹理精灵绘制,但它们是进一步功能的起点。例如,要根据到相机的距离计算精灵的大小(也许是为了给它一个固定的世界空间大小),你必须glEnable(GL_PROGRAM_POINT_SIZE)
并写入特殊输出变量gl_PointSize
在顶点着色器中:
uniform mat4 modelview;
uniform mat4 projection;
uniform vec2 screenSize;
uniform float spriteSize;
layout(location = 0) in vec4 position;
void main()
{
vec4 eyePos = modelview * position;
vec4 projVoxel = projection * vec4(spriteSize,spriteSize,eyePos.z,eyePos.w);
vec2 projSize = screenSize * projVoxel.xy / projVoxel.w;
gl_PointSize = 0.25 * (projSize.x+projSize.y);
gl_Position = projection * eyePos;
}
这将使所有点精灵具有相同的世界空间大小(因此具有不同的屏幕空间大小(以像素为单位))。
但是点精灵虽然在现代 OpenGL 中仍然得到完美支持,但也有其缺点。最大的缺点之一是它们的剪裁行为。点在其中心坐标处被剪裁(因为剪裁是在光栅化之前完成的,因此是在点“放大”之前完成的)。因此,如果该点的中心位于屏幕之外,则可能仍会到达观看区域的其余部分不会显示,因此最坏的情况是,一旦该点离开屏幕一半,它就会突然消失。然而,只有当点精灵太大时,这一点才会引人注目(或令人烦恼)。如果它们是非常小的粒子,并且每个粒子覆盖的面积不超过几个像素,那么这不会是什么大问题,我仍然认为粒子系统是点精灵的典型用例,只是不这样做将它们用于大型广告牌。
但如果这是一个问题,那么现代 OpenGL 提供了许多其他方法来实现点精灵,除了在 CPU 上将所有精灵预构建为单独的四边形的简单方法之外。您仍然可以将它们渲染为充满点的缓冲区(因此它们可能会从基于 GPU 的粒子引擎中出来)。要实际生成四边形几何体,您可以使用几何着色器,它可以让您从单个点生成四边形。首先,您仅在顶点着色器内进行模型视图转换:
uniform mat4 modelview;
layout(location = 0) in vec4 position;
void main()
{
gl_Position = modelview * position;
}
然后几何着色器完成其余的工作。它将点位置与通用 [0,1]-四边形的 4 个角结合起来,并完成到剪辑空间的转换:
const vec2 corners[4] = {
vec2(0.0, 1.0), vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(1.0, 0.0) };
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
uniform mat4 projection;
uniform float spriteSize;
out vec2 texCoord;
void main()
{
for(int i=0; i<4; ++i)
{
vec4 eyePos = gl_in[0].gl_Position; //start with point position
eyePos.xy += spriteSize * (corners[i] - vec2(0.5)); //add corner position
gl_Position = projection * eyePos; //complete transformation
texCoord = corners[i]; //use corner as texCoord
EmitVertex();
}
}
在片段着色器中,您当然可以使用自定义texCoord
变化而不是gl_PointCoord
用于纹理,因为我们不再绘制实际的点。
或者另一种可能性(也许更快,因为我记得几何着色器以速度慢而闻名)是使用实例渲染。这样你就有了一个额外的 VBO,其中包含只是一个通用的 2D 四边形(即 [0,1] 方)和你的旧 VBO 只包含点位置。然后,您要做的就是多次绘制这个单个四边形(实例化),同时从点 VBO 获取各个实例的位置:
glVertexAttribPointer(0, ...points...);
glVertexAttribPointer(1, ...quad...);
glVertexAttribDivisor(0, 1); //advance only once per instance
...
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, count); //draw #count quads
然后在顶点着色器中,将每点位置与实际角/四边形位置(也是该顶点的纹理坐标)进行组装:
uniform mat4 modelview;
uniform mat4 projection;
uniform float spriteSize;
layout(location = 0) in vec4 position;
layout(location = 1) in vec2 corner;
out vec2 texCoord;
void main()
{
vec4 eyePos = modelview * position; //transform to eye-space
eyePos.xy += spriteSize * (corner - vec2(0.5)); //add corner position
gl_Position = projection * eyePos; //complete transformation
texCoord = corner;
}
这实现了与基于几何着色器的方法相同的效果,即具有一致的世界空间大小的正确裁剪的点精灵。如果您确实想模仿实际点精灵的屏幕空间像素大小,则需要投入更多的计算工作。但这仅作为练习,并且与点精灵着色器的世界到屏幕转换完全相反。