Scaling line width with matrix transformations

Just a quick thing I’m curious about - when you use ofScale it doesn’t seem to affect the line width. Is there a mode or something else I can do to make the line width change with the transformation matrix?

cheers!

Seb

1 Like

Hey @seb_ly !

OpenGL line width is not affected by scale.
You can use ofSetLineWidth to change the width manually but it won’t change with scale.

If you want scale based line width you would need to construct a line with a triangle strip.

There seem to be a few addons that do this though:

thanks Theo, I have actually used one of those addons before! But yeah good to know thank you!

also in case it’s useful I’ve found doing some line things in Cairo (2d graphics) really helpful, when you need very precise control over line width – the cool thing is Cairo is built into OF so you just need to create a renderer connect it to a texture to draw it. it’s a little slow for certain applications but I have found it worked well for what I needed, and Cairo has really precise control over line width and other line properties. I can share a short snippet if it’s helpful…

2 Likes

oh that sounds super interesting, thanks @zach, I’d love some code snippets if it’s not too much trouble

That addon I made is kinda old. What I have been using a lot instead is to make a mesh from a polyline passing to it the normals, which have been already calculated by the polyline. then in a shader I push out the vertices based on the width that I want.


_mesh.clear();
    _mesh.setMode(OF_PRIMITIVE_TRIANGLE_STRIP);
    
    glm::vec3 v;
    glm::vec3 n;
    //    cout << poly.size() <<endl;
    for (int i =0; i < poly.size(); i++) {
        v = poly.getVertices()[i];
        n = poly.getNormalAtIndex(i);
      
        _mesh.addVertex({v.x, v.y, 1.0});// or pass a variable width per vertex here
        _mesh.addNormal(n);
        _mesh.addVertex({v.x, v.y, -1.0}); // and here. It is the same value as the other vertex but negative
        _mesh.addNormal(n);
        
    }

then I draw

        _getShader().begin();
        _getShader().setUniform1f("width", width);
        _mesh.draw();
        _getShader().end();        

the vertex shader code is


#version 150

uniform float width;
uniform mat4 modelViewProjectionMatrix;

in vec4  position;
in vec3  normal;



// ---------------------------------------------------------------------- 

void main()
{

    vec4 vPos = position; //position.z stores teh width of the poly, use for displacing the vertex
    
    vPos.z = 0.0;    

    vPos.xy = vPos.xy + normal.xy * position.z * width;

    gl_Position = modelViewProjectionMatrix * vPos  ;
}

What is nice about this method is that I use the vertex z value to pass as a variable width per point which I then use in the shader.

code is a little messy but here’s an example of using Cairo to draw a circle with a line thickness that changes

src.zip (3.5 KB)

essentially you setup Cairo as a renderer to memory, and then a texture for the that memory to be uploaded to

opengl = ofGetGLRenderer();
    cairo = make_shared<ofCairoRenderer>();
    cairo->setupMemoryOnly(ofCairoRenderer::IMAGE);
    render.allocate(ofGetWidth(),ofGetHeight(), GL_RGBA);

the cool thing is you can access the cairo context to change properties of the line, such as the line width as well as the endcaps etc.

ofSetCurrentRenderer(cairo);
    ofBackground(ofColor::mediumOrchid);
    cairo_set_line_width(cairo->getCairoContext(), width);
    cairo_set_line_cap  (cairo->getCairoContext(), CAIRO_LINE_CAP_ROUND);
    cairo_set_line_join (cairo->getCairoContext(), CAIRO_LINE_JOIN_ROUND);

also one note is that cairo is 2d – sometimes I just project the 3d lines to 2d to draw them in cairo

one note is that it can be a bit slow, and certainly uploading cairo every frame to the graphics card as a texture can be slow also…

Nice trick!
Thanks for sharing.
BTW, those sketches are really nice! :smiley:

1 Like

thank you so much everyone for your great tips and ideas, will definitely check these out soon.

Sorry to bump this but I wanted to add another tip for anyone looking at options on this topic.

My project involves drawing wide 2D polylines created with curveTo() that generally contain a few hundred vertices. I’ve settled on the build-a-mesh approach. Unfortunately, ofPolyline getNormalAtIndex() function re-calculates all the normals in the polyline every time you call it, which gets very expensive very quickly. I ended up just digging out the relevant math from the ofPolyline source and writing this function to build and draw the mesh:

void ofApp::draw_polyline( ofPolyline* pline, float width ){
    if( pline->size() < 3 ) return;
    
    ofMesh mesh;
    mesh.clear();
    mesh.setMode(OF_PRIMITIVE_TRIANGLE_STRIP);

    glm::vec3 p1, p2, p3;
    glm::vec3 rightVector= glm::vec3( 0, 0, -1 ); // used for math
    glm::vec3 normal;

    for( int i= 0; i < pline->size(); i++ ) {
        
        // first vertex normal is perp to first segment
        if( i == 0 ){
            p2= pline->getVertices()[0];     // current point
            p3= pline->getVertices()[1];     // next point
            auto v2(p3 - p2);                // vector to next point
            
            v2= glm::normalize(v2);
            normal= glm::normalize( glm::cross( rightVector, v2 ) );
        }

        // middle vertex normals are perp to tangent at vertex
        else if( i < pline->size() - 1 ){
            p1= pline->getVertices()[i - 1]; // previous point
            p2= pline->getVertices()[i];     // current point
            p3= pline->getVertices()[i + 1]; // next point
            auto v1(p1 - p2);                // vector to previous point
            auto v2(p3 - p2);                // vector to next point
            
            v1 = glm::normalize(v1);
            v2 = glm::normalize(v2);
            auto tangent= glm::length2(v2 - v1) > 0 ? glm::normalize(v2 - v1) : -v1;
            normal= glm::normalize( glm::cross( rightVector, tangent ) );
        }

        // last vertex normal is perp to last segment
        else{
            p1= pline->getVertices()[i - 1]; // previous point
            p2= pline->getVertices()[i];     // current point
            auto v1(p1 - p2);                // vector to previous point
            
            v1 = glm::normalize(v1);
            normal= glm::normalize( glm::cross( rightVector, v1 ) );
            normal*= -1; // flip to get those points in the right order
        }
        
        // add two vertices to mesh for every polyline vertex,
        // using normal to offset vertext by width
        glm::vec3 side= p2;
        side+= normal * (width/2); // one side
        mesh.addVertex( {side.x, side.y, 0} );
        side-= normal * width; // the other side
        mesh.addVertex( {side.x, side.y, 0} );
    }
    
    //mesh.drawWireframe();
    mesh.draw();
}


1 Like

Hi @gwitt,
The updateCache() function in ofPolyline gets called every time for getNormalAtIndex function because the polyline is being flagged as dirty or changed. Some methods that might set it as dirty are getVertices() or the array operator []
One way to avoid this is to access the vertices via const methods which does not flag the polyline as changed. See some examples below.
Updating my response based on this thread: ofPolyline::updateCache() and const - #6 by TimChi

ofPolyline polyline;
for( int i = 0; i < 100; i++ ) {
	polyline.addVertex(glm::vec3(ofRandom(0,100), ofRandom(0,100), ofRandom(0, 1000)));
}
// will flag the polyline as dirty, but only once
const auto& verts = polyline.getVertices();
for( int i = 0; i < polyline.size(); i++ ) {
	// since the polyline was flagged as dirty from the initial polyline.getVertices()
	// the first getNormalAtIndex() will call updateCache(), 
	// but not subsequent calls accessing verts[i] since its a stored vector
	auto v = verts[i];

	// will call flagHasChanged();
	polyline.getVertices();
	// will call flagHasChanged();
	const auto& v = polyline.getVertices()[i];
	// will call flagHasChanged();
	const auto& v = polyline[i];
	// will call flagHasChanged();
	auto v = polyline[i];
	// if flagHasChanged() was called, will call updateCache()
	auto normal = polyline.getNormalAtIndex(i);
}

Note: If you access the vertices as const you won’t be able to update them.

or you could pass the polyline as a const argument to a function.

checkUpdate(polyline);
void checkUpdate( const ofPolyline& polyline ) {
	for( int i = 0; i < polyline.size(); i++ ) {
		 const auto& v = polyline.getVertices()[i];
		auto normal = polyline.getNormalAtIndex(i);
	}
}
1 Like

Ah! Oops. That makes sense. Thank you for explaining that so thoroughly!

Here’s what I ended up with (definitely an improvement):

void ofApp::draw_polyline( ofPolyline& pline, float width ){
    if( pline.size() < 3 ) return;
    
    ofMesh mesh;
    mesh.clear();
    mesh.setMode(OF_PRIMITIVE_TRIANGLE_STRIP);

    // use a const reference to prevent polyline from updating
    // its cache on every call to getNormalAtIndex
    const auto& pline_verts= pline.getVertices();
    
    for( int i= 0; i < pline.size(); i++ ) {
        auto vert= pline_verts[i];
        glm::vec3 normal;
        
        // copy adjacent normals at ends to get square caps on line
        int last_idx= pline_verts.size() - 1;
        if( i == 0 ) normal= pline.getNormalAtIndex(1);
        else if( i == last_idx ) normal= pline.getNormalAtIndex( last_idx - 1 );
        
        // get tangent normal for all middle points
        else normal= pline.getNormalAtIndex(i);
        
        // add two vertices to mesh for every polyline vertex,
        // using normal to offset vertext by width
        mesh.addVertex( vert + width/2 * normal );
        mesh.addVertex( vert - width/2 * normal );
    }
    
    //mesh.drawWireframe();
    mesh.draw();
}

2 Likes

Hey @NickHardeman thanks for posting above! This is so helpful! I’ve run into something similar before with ofPolyline and managed to sort-of figure out why it was happening, but not the fix of using const.

1 Like