Rendering bezier strokes with SDFs
Why SDFs
The idea is to avoid triangulating the curve. There are already methods like Loop-Blinn. picture reference
But unfortunately, these methods can't be easily extended for strokes in general case since the offset curve of for example quadratic bezier curve is not quadratic Bezier curve (it is polynomial of 6 degree). It could be easily seen for Bezier curve with extreme sharp angle (with extreme inflection). There are methods for approximating these curves near inflection points with smaller bezier curves but it requires some effort to implement such algorithms. If we just want to render bezier strokes -- SDF seems to be the most simple way to do this. Also, it gives local antialiasing for free.
Plan
Consider quadratic Bezier curves. The idea is the same as for rendering lines. There are some differences, however:
- We use bezier SDF and hence we should pass all parameters for quadratic bezier curve in our shader -- start/end point and control point.
- We should find a bounding box for each Bezier curve we render.
The result should look like this:
In this article, we'll not bother about what happens when curves overlap even at end points, The last issue was discussed in the previous article about rendering lines.
The fragment shader is quite straightforward:
# version 310 es precision highp float; // start point in vec2 af; // control point in vec2 controlf; // end point in vec2 cf; // current position in fragment shader in vec2 posf; // thickness of the curve in float thicknessf; out vec4 FragColor; float dot2( in vec2 v ) { return dot(v,v); } // borrowed from here https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm float sdBezier( in vec2 pos, in vec2 A, in vec2 B, in vec2 C ) { vec2 a = B - A; vec2 b = A - 2.0*B + C; vec2 c = a * 2.0; vec2 d = A - pos; float kk = 1.0/dot(b,b); float kx = kk * dot(a,b); float ky = kk * (2.0*dot(a,a)+dot(d,b)) / 3.0; float kz = kk * dot(d,a); float res = 0.0; float p = ky - kx*kx; float p3 = p*p*p; float q = kx*(2.0*kx*kx-3.0*ky) + kz; float h = q*q + 4.0*p3; if( h >= 0.0) { h = sqrt(h); vec2 x = (vec2(h,-h)-q)/2.0; vec2 uv = sign(x)*pow(abs(x), vec2(1.0/3.0)); float t = clamp( uv.x+uv.y-kx, 0.0, 1.0 ); res = dot2(d + (c + b*t)*t); } else { float z = sqrt(-p); float v = acos( q/(p*z*2.0) ) / 3.0; float m = cos(v); float n = sin(v)*1.732050808; vec3 t = clamp(vec3(m+m,-n-m,n-m)*z-kx,0.0,1.0); res = min( dot2(d+(c+b*t.x)*t.x), dot2(d+(c+b*t.y)*t.y) ); // the third root cannot be the closest // res = min(res,dot2(d+(c+b*t.z)*t.z)); } return sqrt( res ); } void main() { vec4 color = vec4(1., 1., 1., 1.); // we can also make it as input attribute float d = sdBezier(posf, af, controlf, cf) - thicknessf; // bigger value -- more blury float smooth_amount = 1.; float s = smoothstep(0., smooth_amount, -d); if (d < 0.) { color.a = s; } else { discard; } FragColor = color; }
I didn't do anything to support the camera as it was with lines. It could be easily added if you want.
Let's address the second problem we have -- finding proper bounding boxes. Here is the Rust code for doing this:
/// basically rotation matrix (f.e. see here https://matthew-brett.github.io/teaching/rotation_2d.html) /// pub fn rot(point: Vec2, cosb: f32, sinb: f32) -> Vec2 { vec2( cosb * point.x - sinb * point.y, sinb * point.x + cosb * point.y, ) } /// oriented are betwee two vectors (by definition det(|v1^T \n v2^T|)) /// or more importantly for us: |v1| |v2| sin (alpha) pub fn wedge(v1: Vec2, v2: Vec2) -> f32 { v1.x * v2.y - v1.y * v2.x } #[derive(Copy, Clone, Debug, PartialEq)] #[repr(C)] pub struct QuadCurve { pub a: Vec2, pub control: Vec2, pub c: Vec2, } /// basically https://www.iquilezles.org/www/articles/bezierbbox/bezierbbox.htm /// with extra rotation fn optimal_bb(&self, width: f32) -> (Vec2, Vec2, Vec2, Vec2) { let dir = self.c - self.a; let ndir = dir.normalize(); let ox = vec2(1., 0.); let sinb = wedge(ndir, ox); let cosb = (self.c - self.a).normalize().dot(ox); // align with Ox axis let p0 = self.a; // start point stays the same let p0p1 = self.control - self.a; let p1 = p0 + rot(p0p1, cosb, sinb); // control points rotates let p2 = p0 + vec2(dir.length(), 0.); // end point is parralel to X axis // calculation inflection point in this coordinates // (see original article for explanation) let mut mi = p0.min(p2); let mut ma = p0.max(p2); if p1.x < mi.x || p1.x > ma.x || p1.y < mi.y || p1.y > ma.y { let t = (p0 - p1) / (p0 - 2. * p1 + p2); let t = vec2(clamp(t.x), clamp(t.y)); let s = vec2(1., 1.) - t; let q = s * s * p0 + 2.0 * s * t * p1 + t * t * p2; mi = mi.min(q); ma = ma.max(q); } let (mi, ma) = bounding_box_frame(mi, ma, width); // now align back with bezier // We simply rotate negative to previous angle: cos(-a) = cos(a), sin(-a) = -sin(a) let ma_rotated = p0 + rot(ma - p0, cosb, -sinb); let mi_rotated = p0 + rot(mi - p0, cosb, -sinb); let offset = ndir.dot(mi_rotated - ma_rotated) * ndir; let b = ma_rotated + offset; let d = mi_rotated - offset; (mi_rotated, b, ma_rotated, d) }
It first rotates Bezier curve so that the line which connects the start and end points of the curve is parallel to X-axis. Then it calculates the extreme point(inflection point) of that curve and hence bounding box in a current rotated coordinate system. After that, we simply rotate the bounding box back.
After that, we can simply pass it into the GPU pipeline (this is for simplicity and demo -- could be done in place or via iterator)
#[derive(Copy, Clone, Debug, PartialEq)] #[repr(C)] pub struct QuadCurve { pub a: Vec2, pub control: Vec2, pub c: Vec2, } ... impl QuadCurve { ... pub fn vertices(&self, width: f32) -> (Vec<Vertex>, Vec<u16>) { let (a, b, c, d) = self.optimal_bb(width); let indices = vec![0, 1, 2, 0, 2, 3]; ( vec![ Vertex { position: a, curve: self.clone(), thickness: width, }, Vertex { position: b, curve: self.clone(), thickness: width, }, Vertex { position: c, curve: self.clone(), thickness: width, }, Vertex { position: d, curve: self.clone(), thickness: width, }, ], indices, ) } }
Cubics
For cubics, one can use approximation algorithms and render quadratic Beziers instead. Se for example Quadratic approximation