3D pixel-sized point rendering ideas

Hey Embers,

I’ve got a bit of an odd question. I’ve been playing around trying to render something that’s very reminiscent of a classic starfield from a 90s amiga/pc crack intro. Albeit with some modern adjustments into the new era, ala. adding tiny galaxies filled with stars etc.

One thing I’ve been having trouble with though, is rendering 3d points on-screen as pixels, no matter the distance to the camera. I’m wondering if anyone has any experience or suggestions in leveraging the built in functions in Cinder to achieve this?

So far I’ve been using drawSolidCircle with a very small radius which occasionally looks fine, but at other times looks like a tiny triangle as opposed to a pixel.

If possible it’d be cool if the approach also allowed for some custom change to the pixel size, a 2x2 pixel perhaps.

It’d be ideal if the approach didn’t have to escalate to using shaders in terms of complexity, but I’ve not really found anything simple that allows these pixel-sized points to exist in 3d space and not occasionally disappear or fail to occlude behind other geometry.

Thanks in advance,
Gazoo

The default behaviour of GL_POINTS will do this. There’s also room to get fancy with GL_VERTEX_PROGRAM_POINT_SIZE and textured points using gl_PointCoord, should you need it. This will be considerably faster than drawSolidCircle too.

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/CameraUi.h"
#include "cinder/Rand.h"
#include "cinder/gl/gl.h"

using namespace ci;
using namespace ci::app;
using namespace std;

class PointsApp : public ci::app::App
{
public:
    
    void setup ( ) override
    {
        _cam = CameraPersp ( getWindowWidth(), getWindowHeight(), 60.0f, 0.1f, 1000.0f );
        _cam.lookAt( vec3 ( 0, 0, 0.5 ), vec3 ( 0 ) );
        
        _camUi = CameraUi ( &_cam, getWindow() );
        _camUi.setMinimumPivotDistance( 0.0f );
        
        gl::VertBatch batch { GL_POINTS };
        for ( int i = 0; i < 1000; i++ )
        {
            float x = randFloat(-1, 1);
            float y = randFloat(-1, 1);
            float z = randFloat(-1, 1);
            
            batch.vertex( vec3 ( x, y, z ) );
        }
        
        auto shader = gl::getStockShader( gl::ShaderDef().color() );
        _batch = gl::Batch::create ( batch, shader );
        
        gl::pointSize(4);
    }
    
    void draw ( ) override
    {
        gl::clear( Colorf::gray(0.1f));
        gl::setMatrices(_cam);
        
        gl::ScopedColor color { Colorf::white() };
        _batch->draw();

    }
    
    
    gl::BatchRef _batch;
    CameraPersp _cam;
    CameraUi _camUi;
};

CINDER_APP ( PointsApp, RendererGl );
2 Likes

Much obliged @lithium . Did the ol’ copy paste and it looks solid! Really appreciate the idea and code.

As a side note, I wish there was a simpler way of coordinating objects in the CPU with visual graphics on the GPU other than just continuously sync the data, e.g. move independent star objects on the CPU based on some model, update GPU values, render, rinse, repeat, etc.

I also had an interest in possibly rendering the points in a roundish manner and found this post, talking about GL_POINT_SMOOTH. But it appears this enum isn’t naturally part of glfw in Cinder 0.9.2?

This other post, has a potential work around for anyone wondering, but I assume just copying the enum value from the upcoming glad.h header should work too…?

Update: Hmmm no - doesn’t appear like just using that define will yield rounded points using the fixed pipeline.

Gazoo

This is where the aforementioned point sprites come in.

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/CameraUi.h"
#include "cinder/Rand.h"
#include "cinder/gl/gl.h"

using namespace ci;
using namespace ci::app;
using namespace std;

class PointsApp : public ci::app::App
{
public:
    
    void setup ( ) override
    {
        auto vert = gl::env()->generateVertexShader( gl::ShaderDef().color() );
        //auto frag = gl::env()->generateFragmentShader( gl::ShaderDef().color().texture() );

        auto frag = CI_GLSL(150,
                            out vec4 oColor;
                            uniform sampler2D uTex0;
                            in vec4 Color;
                            void main( void )
                            {
                                oColor = vec4( 1 ) * texture( uTex0, gl_PointCoord.st ) * Color;
                            });
        
        auto shader = gl::GlslProg::create ( vert, frag );
        
        
        auto fmt = gl::Texture::Format().mipmap().minFilter(GL_LINEAR_MIPMAP_LINEAR).magFilter(GL_LINEAR);
        _texture = gl::Texture::create ( loadImage( loadUrl ( "https://d1yjxggot69855.cloudfront.net/images/b/ba/Cloud-particle.png" ) ), fmt );
        
        _cam = CameraPersp ( getWindowWidth(), getWindowHeight(), 60.0f, 0.1f, 1000.0f );
        _cam.lookAt( vec3 ( 0, 0, 0.5 ), vec3 ( 0 ) );
        
        _camUi = CameraUi ( &_cam, getWindow() );
        _camUi.setMinimumPivotDistance( 0.0f );
        
        gl::VertBatch batch { GL_POINTS };
        for ( int i = 0; i < 1000; i++ )
        {
            float x = randFloat(-1, 1);
            float y = randFloat(-1, 1);
            float z = randFloat(-1, 1);
            
            batch.vertex( vec3 ( x, y, z ) );
        }
        
        
        _batch = gl::Batch::create ( batch, shader );
        
        gl::pointSize(32);
    }
    
    void draw ( ) override
    {
        gl::clear( Colorf::gray(0.1f));
        gl::setMatrices(_cam);
        
        gl::ScopedBlendAlpha blend;
        gl::ScopedColor color { Colorf::white() };
        gl::ScopedTextureBind tex0 { _texture, 0 };
        
        _batch->draw();

    }
        
    gl::TextureRef _texture;
    gl::BatchRef _batch;
    CameraPersp _cam;
    CameraUi _camUi;
};

CINDER_APP ( PointsApp, RendererGl );

You can do a lot more GPU side these days with compute shaders and SSBOs etc, but really you’re just trading one series of problems for another. It’s just the nature of the beast really, and cinder is already shielding you from the worst of it.

1 Like

Again - so awesome @lithium with a working example. Already ran it to have a look. I want to look into how to make the circles non-soft, and you’ve provided a great starting point.

You’re also right that ultimately, it’s a lot of replacing one issue with another. It’s also just a design preference as to whether one prefers arrays of objects or objects of arrays.

I find the former mentally simpler to manage so unless performance requires it, I lean towards that.

And I also 100% agree - Cinder is a great ‘annoyance’ shield. Makes so much, so much easier!

Gazoo

image

One thing I’ve yet been able to find a simple solution to is achieving uniform pixel sizes with point size. As evident by that image, all my point sprites are of varying size. I believe it’s the result of the rendering pipeline having to render the point sprite at an uneven screen coordinate or perhaps it’s uneven lookups into the texture? It’s unclear to me what I need to do to ensure a uniform square pixel size.

Open to ideas!

I acknowledge doing so will make the rendering slightly more inaccurate position wise.

Gazoo

These look ok to me, perhaps it’s more pronounced when in motion?

Anyway, since you’re only doing circles, perhaps a procedural approach is better than a textured one in your case. If you replace the fragment shader with this, it will draw circles for you. You can tweak the uRadius and uBlur uniforms to adjust the softness of the circles.

auto frag = CI_GLSL(150,

            float circle ( in vec2 st, in float radius, float blur)
            {
                vec2 dist = st - vec2 ( 0.5f );
                return 1.0f - smoothstep ( radius - ( radius * blur ),
                                           radius + ( radius * blur ),
                                           dot ( dist, dist ) * 4.0 );
            }
            
            out vec4 oColor;
            in vec4 Color;

            uniform float uBlur = 0.01;
            uniform float uRadius = 0.9;

            void main( void )
            {
                float c = circle ( gl_PointCoord.st, uRadius, uBlur );
                oColor = Color * vec4(1, 1, 1, c);
            });
1 Like

More amazingness from you @lithium. Your provided fragment shader effectively minimizes the issue to the point of it being invisible - so I’m leaning towards adopting it.

I’m keen to still investigate the issue I’m having as I could see it being an issue I might run into again in the future. I’m pretty sure we’re on the same page, but given the sizable help you’ve provided @lithium , I thought I’d still detail the issue I had.

CinderChat
Above are three concatenated images (from left to right):

  • A slice of the previous screenshot
  • A zoomed version of that slice, where I’ve highlighted nice ‘uniform square’ using stars with green rings, and red arrows to point out some of the misshapen stars
  • The used texture

The issue is definitely more pronounced while in movement as you note lithium, but even when static, the non-uniform sizing of the texture is pretty visible. Given that lots of two dimensional games don’t have this issue, I’m sure there’s a way to side-step in the pipeline. I’ll keep investigating.

One last question to you @lithium - Would you recommend rendering these point sprites from back to front? I think it’s the only way to properly handle overlapping transparency. Otherwise you can get this delicious outcome:

Animation4

A semi-helpful intermediate solution is to append the following:

if (oColor.a <= 0.0) {
	discard;
}

to the fragment shader - but that doesn’t resolve the transparent pixels.

  1. What filtering are you using on your particle texture, and does the texture vary in size from what glPointSize you’re using? Ideally they’d be 1 to 1, but failing that, you’ll probably get best results with gl::Texture::Format().mipmap().minFilter(GL_LINEAR_MIPMAP_LINEAR).magFilter(GL_LINEAR);
  1. If you’re deliberately going for a pixelated look with GL_NEAREST, there’s some filtering techniques you can implement in your shader to mitigate the shimmering. See here or here for sample implementations.

  2. What is your gl state when you’re drawing? With depth read/write on and alpha blending I can’t reproduce what you’re seeing.
    ezgif-7-818be3b3f4

I know that point sprites differ in the implementations across vendors, amd and nvidia handle them way differently so this may be an nvidia specific problem. In which case you can either sort, discard in the shader, or if your background allows it, draw with additive blending (gl::ScopedBlendAdditive blend; above your draw)

A

1 Like
  1. That’s a great idea - I’ll keep that one in mind for sure.

  2. Those are some amazing shadertoys. Even just learning of the term ‘shimmering’, is a tremendous help - very much appreciated @lithium . I would say they’ve certainly properly addressed the issue I had.

  3. My draw() call is the following:

gl::clear( Colorf::gray( 0.1f ) );
gl::setMatrices( _cam );

gl::ScopedBlendAlpha blend;
gl::ScopedColor color{ Colorf::white() };
gl::ScopedTextureBind tex0{ _texture, 0 };

gl::ScopedDepth depth( true );

_batch->draw();

Replacing the existing blend with gl::ScopedBlendAdditive blend2; has no impact on the issue. Only disabling the depth buffer addresses the issue which, in my case, happens to be an acceptable approach to allow for some nicer blurred edges.

It’d be lovely if there was a drawing call that actually allowed for the depth buffer to be active without the issue presenting itself.

I’m assuming you’re on ATi/AMD hardware then?

I was on my macbook so that has an AMD radeon pro in it, but I just tried it on my windows desktop which has an RTX 3070 and it looked the same with no transparency issues, so something else may be afoot. It’s dubious that additive blending had no effect.

Sorry i missed that you had depth enabled, ignore this.

1 Like

Just to ensure I had not naughtily injected something I wasn’t aware of, I recopied your latest ‘full program’ code, injected the newest fragment shader, enabled depth, disabled the texture bind call and get this:

image

I’m on a 2080 Super.

Should be able to get away with something like this to finagle the right draw state. First block is your existing fully depth read/write scene, the second block ( the point sprites ) respects the existing state of the depth buffer but doesn’t contribute to it, leaving you with (hopefully) the correct output.

void draw ( ) override
{
    gl::clear( Colorf::gray(0.2f));
    gl::setMatrices(_cam);
    
    {
        // Normal 3D Scene
        gl::ScopedDepth depth { true };
        
        gl::ScopedGlslProg shader { gl::getStockShader ( gl::ShaderDef().lambert() ) };
        gl::drawCube( vec3(0), vec3(0.25));
    }
    
    {
        // Point Sprites
        gl::ScopedDepthTest depthTest { true };
        gl::ScopedDepthWrite depthWrite { false };
        
        _batch->getGlslProg()->uniform( "uRadius", _radius );
        _batch->getGlslProg()->uniform( "uBlur", _blur );
        
        gl::ScopedBlendAlpha blend;
        gl::ScopedColor color { Colorf::white() };
            
        _batch->draw();
    }
}