Points rendering with Open Inventor

Among the primitives supported by Open Inventor, we find points through the SoPointSet(C++|Java|.Net) and SoIndexedPointSet(C++|Java|.Net) classes. By default, points are rendered as squares but Open Inventor also allows to render more aesthetically pleasing round points. However, the way to achieve this result can be confusing.

The "old" way (pre Open Inventor 10.10)

There were two ways to get this result until version 10.9. The first way, documented, was to use the setSmoothing method in the SoGLRenderAction(C++|Java|.Net) class. This method was quite invasive because it activated the alpha blending behind the user's back when rendering points or lines. The second one, which was more a side effect, was simply to activate the FSAA antialiasing (note that it didn't work with other antialiasing methods such as SMAA).

As of version 10.10 neither of these methods work anymore. In order to understand why, we need to give some technical details. Open inventor relies on the OpenGL graphics library for rendering but some options are mutually exclusive. In this case, from Open Inventor 10.10 onwards we have enabled the support of point sprites by default, this feature offers many very interesting possibilities that we will detail next but unfortunately once activated it modifies the previous behavior that we have just described concerning the rendering of points.

The modern way (Open Inventor 10.10 and later)

Rendering points is very efficient since they use four times less geometry compared to quads. However, points have traditionnaly been very limited in the way they can be rendered. As we have seen previously, points can be rendered as square or circles, of different size but with a single color. To allow more customization, point sprites were introduced.

Point sprites can be thought of as a quad, as illustrated in the following picture :

Note however that a point is not a quad, in particular their clipping behavior differs. The OpenGL library specifies that points are clipped at their center coordinate ('P' on the figure above). So, if the center of the point is outside of the screen, the rest of it that might still reach into the viewing area will not be shown. In the worst case scenario, once the point is half-way out of the screen, it will suddenly disappear. This can be particularly noticeable when the point sprites are large. In practice, some OpenGL implementation (such as Nvidia's) does not follow the specification and will give the same result than using a quad.

With point sprites, we have access to the gl_PointCoord input variable in a fragment shader that contains the two-dimensional coordinates indicationg where within a point primitive the current fragment is located. Thanks to this variable we can customize the rendering of points as we wish.

For example the following code draws round points :

bool isInsideCircle()
{
  float distance = length(2.0 * gl_PointCoord - 1.0);
  return (distance <= 1.0);
}

void main()
{
  if (!isInsideCircle())
    discard;

  gl_FragColor = vec4 (1.0,1.0, 1.0, 1.0);
}

Drawing triangles, is a little bit more complicated but not much:

bool isInsideTriangle(vec2 p0, vec2 p1, vec2 p2)
{
  float dX = gl_PointCoord.x-p2.x;
  float dY = gl_PointCoord.y-p2.y;

  float dX21 = p2.x-p1.x;
  float dY12 = p1.y-p2.y;

  float D = dY12*(p0.x-p2.x) + dX21*(p0.y-p2.y);
  float s = dY12*dX + dX21*dY;
  float t = (p2.y-p0.y)*dX + (p0.x-p2.x)*dY;

  if (D<0)
    return s<=0 && t<=0 && s+t>=D;

  return s>=0 && t>=0 && s+t<=D;
}

void main()
{
  if (!isInsideTriangle(vec2 (0, 1), vec2 (1, 1), vec2 (0.5, 0)))
    discard;

  gl_FragColor = vec4 (1.0,1.0, 1.0, 1.0);
}

And of course we can just use the gl_PointCoord variable as a texture coordinate in order to render textured points :

uniform sampler2D tex;

void main()
{
  gl_FragColor = texture(tex, vec2(gl_PointCoord.x, 1.0-gl_PointCoord.y)); 
}

We can even go further and simulate lighting on our points. In the following example, only the largest sphere in the center is made up of triangles, the four that surround it are only points, but the illusion is nevertheless perfect as shown in the animation.

//!oiv_include <Inventor/oivShaderState.h>

void main()
{
  vec3 diffuseColor = vec3(0.8);
  vec3 lightDir = normalize(OivLightSourcePosition(0).xyz);
  vec3 normal;
  normal.xy = (vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y)* 2.0 - vec2(1.0));

  float dist = dot(normal.xy, normal.xy);
  if(dist > 1.0)
    discard;

  normal.z = sqrt(1.0-dist);

  float diffuseFactor = max(0.0, dot(lightDir, normal));
  gl_FragColor = vec4(diffuseColor*diffuseFactor, 1.0);
}

Another new feature introduced with Open Inventor 10.10, is the ability to modify the size of each point individually in the vertex shader, for example based on its distance to the camera as shown in the following example :

//!oiv_include <Inventor/oivShapeAttribute.h>
//!oiv_include <Inventor/oivShaderState.h>

void main()
{
  const float constAtten  = 0.1;
  const float linearAtten = 0.1;
  const float quadAtten   = 0.05;

  vec4 eyePos = OivModelViewMatrix() * OivVertexPosition(); 
  gl_Position = OivProjectionMatrix() * eyePos;

  float dist = length(eyePos);
  float att = sqrt(1.0 / (constAtten + (linearAtten * dist) + (quadAtten * dist * dist)));

  gl_PointSize = 64 * att;
}

The points are drawn at different depths (0, 5, 10, 15) and we can notice that their size decrease as we move away from the camera.

Conclusion

As we have just seen, Open Inventor 10.10 introduces some very exciting new customization possibilities for point rendering. The unfortunate consequence of these new features is that some of the methods used so far do not work anymore, but it is very easy to reproduce these old behaviors and much more thanks to the shaders we have presented in this article.