Shader to create an offset, inward-growing stroke?

shaders
data-visualization

#1

I’ve just started using three.js, and I’m having trouble finding resources that can lead me to solving this problem. I’ve been digging through the book of shaders, and while immensely educational, I haven’t been able to wrap my mind around it enough to solve this on my own yet.

For some context, I’m building an application that visualizes data with a 3d scatterplot. Here’s a quick clip of an early iteration of the scatterplot portion of it:

I’m currently using InstancedBufferGeometry to keep the draw calls down to a minimum as there could potentially be 10’s of thousands of nodes on display at any given time, and I’d like these nodes to be able to animate between states when the user filters / changes settings. I’m currently using a RawShaderMaterial with basic vertex and fragment shaders that take into account some fog for color adjustment.

Currently, each node is just its own colored geometry (RingBufferGeometry for the rings, and a custom ShapeBufferGeometry path to create the X’s). There’s no background to these, and in the case of the rings, no filling. It’s very difficult to discern depth, especially when zoomed in to the plot. My thought is for every node to be solid (black) and then have the node color “painted” onto it via the shader. Here’s a video example of what I’m looking to do:

As shown in the above video, I’d like to be able to pass something into the shader to determine the thickness of the inner-stroke; and because I’m going to be using multiple shapes, having it work regardless of shape would be fantastic; essentially, it’s a stroke that’s “inside” the geometry, offset a bit to allow for a black outline at all times, and grows inward.

Here’s a link to a single circle codepen that is pretty much using the same structure as my full scatterplot, just cut down to only deal with one node for the ease of trying to solve this problem:

Really appreciate any assistance!


#2

Hi!
You can pass UV coordinates from the vertex shader to the fragment one and draw a circle, using those coordinates:

I used uniforms for values of inner and outer radii, but you can use attributes for each instance to have different thickness :slight_smile:

Just out of curiousity, why do you use instanced buffer geometry instead of points with custom textures for shapes?


#3

Thank you so much for helping, @prisoner849! :grin:

Is it possible to make the shader work regardless of shape, though? Right now it will only work for a circle, but because I’m going to be using several shapes to designate different statuses, having a “one shader that rules them all” would be amazing. Is that not possible? Would it require multiple shaders (one per unique shape)?

I was reading up on normals last night, and while I can’t get it to do what I want, could that be part of the answer? (following a contour, rather than always being a circle)

Here’s a video clip of how your example looks when I change the amount of segments for the CircleGeometry to 3/4 (triangle and square, respectively):

And here’s what I’d like a single shader to do (without branching / conditional logic):

To your question about instanced buffer geometry vs points… I think I settled on Instanced Geometry because points weren’t as flexible with what you could do with them visually maybe? I don’t remember my thought process entirely at the time.

Essentially, these nodes will have several properties that can be changed by the user (color, thickness, opacity, x, y and z positions, etc.) and as the user adjusts which variable is used for each property, I want the nodes to quickly animate to their new states. You mentioned that I could be using points with custom textures, but wouldn’t the textures get blurry when zoomed in close? Not only that, but if the line thicknesses can be changed, how would that be achieved with a static texture?

One of the big selling points for me on switching from pixi.js to three.js–aside from being able to visualize data in 3d, which is awesome–was that everything is so much easier to scale and stay crisp in three.js.

Really appreciate all the help! :smile:


#4

It was an interesting task :slight_smile:
Had that idea for long time, but tried it just now :smile:

Here is THREE.Points() with changed shaders of THREE.PointsMaterial(). Each point has its own shape index and in dependence of its value we choose a shape for the point. There are 10 000 points now :slight_smile:

The triangle shape was the toughest one to create, so I just stole the code from shadertoy :sweat_smile:


#5

You are awesome! Thank you so much!!! :smile:


#6

@lunch
You’re welcome :beers:
I hope it will be useful :slight_smile:

@Mugen87 @looeee
Can you move this thread from “Questions” to “Resources”, if it worth to be there? :slight_smile:


#7

Well, it is a question that got answered so I think it’s in the right place.

Nice answer btw :smile_cat:


#8

Sorry to bump this one again… I’m trying to figure out another part to this! :joy:

The results from @prisoner849 are awesome, but currently I’m having an issue with aliasing, especially on smaller nodes, or nodes further away from the camera. It seems like when zoomed up close to a point, the smoothstep() function is doing fine to make sure things are not jaggy, but it gets pretty rough the farther away the particles are… Like this:

Any thoughts? Antialiasing is enabled in the renderer, but I’m assuming this is a result of the pixel results from the fragment shader not receiving antialiasing–only geometry edges would benefit from that?

Here’s the link to the pen that @prisoner849 created:


#9

Ah, okay :slight_smile::beers:


#10

Maybe this thread will be helpful:

I’ve tried it with FXAA:

Can’t say there is much impovement of visual. :thinking:

Hmm… Just made this :slight_smile:


#11

Hmmm, interesting… I saw a few links which tackle antialiasing through the shader, but they all use standard derivatives:

https://www.desultoryquest.com/blog/drawing-anti-aliased-circular-points-using-opengl-slash-webgl/

http://madebyevan.com/shaders/grid/

I had trouble trying to enable the standard derivatives extension in three.js, which led me to this:

Still no luck :thinking: