local function knot(u, p, q, r) local c1, s1 = math.cos(u), math.sin(u) local c2, s2 = math.cos(q / p * u), math.sin(q / p * u) return r * (2 + c2) * .5 * c1, r * (2 + c2) * .5 * s1, r * s2 * .5 end points = {} local ct = 300 local p, q, r = 2, 3, 1 for t = 0, ct do local th = t / ct * p * 2 * math.pi local x, y, z = knot(th, p, q, r) table.insert(points, x) table.insert(points, y) table.insert(points, z) end local quads = {} local indexData = { 0,1,2, 2,1,3 ; 1,5,3, 3,5,7 } for i = 1, ct do for j = 1, #indexData do table.insert(quads, (i - 1) * 8 + indexData[j]) end end local vertices = lovr.graphics.newBuffer('float', points) local indices = lovr.graphics.newBuffer('u16', quads) local shader = lovr.graphics.newShader([[ readonly buffer Points { float data[]; }; flat out vec3 head; flat out vec3 tail; flat out vec4 headColor; flat out vec4 tailColor; out float width; vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } vec4 lovrmain() { uint p = 3 * (VertexIndex >> 3); head = vec3(ViewFromLocal * vec4(data[p + 0], data[p + 1], data[p + 2], 1.)); tail = vec3(ViewFromLocal * vec4(data[p + 3], data[p + 4], data[p + 5], 1.)); vec4 headClip = ClipFromView * vec4(head, 1.); vec4 tailClip = ClipFromView * vec4(tail, 1.); vec3 mid = mix(head, tail, .5); vec3 az = normalize(tail - head); vec3 a_ = normalize(cross(az, mid)); vec3 ax = normalize(cross(az, a_)); vec3 ay = normalize(cross(az, ax)); float x = (VertexIndex & 0x1) >> 0; float y = (VertexIndex & 0x2) >> 1; float z = (VertexIndex & 0x4) >> 2; if (dot(mid, ax) > 0.) x = 1. - x; if (dot(mid, az) < 0.) z = 1. - z; width = .4; float w = mix(headClip.w, tailClip.w, z); if (w > 0.) { float minPixelWidth = 1.; width = max(width, Projection[0][0] * w * (minPixelWidth / Resolution.x)); } vec3 local = vec3(x - .5, y - .5, z); mat3 basis = mat3(ax * width, ay * width, az * (length(tail - head) + width)); vec3 start = head - az * width * .5; PositionWorld = start + basis * local; headColor = vec4(hsv2rgb(vec3(float(p/3 + 0) / 101., 1, 1)), 1.); tailColor = vec4(hsv2rgb(vec3(float(p/3 + 1) / 101., 1, 1)), 1.); return ClipFromView * vec4(PositionWorld, 1.); } ]], [[ flat in vec3 head; flat in vec3 tail; flat in vec4 headColor; flat in vec4 tailColor; in float width; layout(depth_less) out float FragDepth; vec4 lovrmain() { vec3 A = tail - head; vec3 B = head; float AoA = dot(A, A); float AoB = dot(A, B); float BoB = dot(B, B); float r = width * .5; int coverage = 0; float param; for (int i = 0; i < 4; i++) { vec3 N = normalize(interpolateAtSample(PositionWorld, i)); float NoA = dot(N, A); float NoB = dot(N, B); float a = AoA - NoA * NoA; float t1 = clamp((NoB * NoA - AoB) / a, 0., 1.); float t2 = t1 * NoA + NoB; float dist = distance(N * t2, head + A * t1); if (dist <= r) { coverage |= (1 << i); param = t1; } } gl_SampleMask[0] = coverage; if (coverage == 0) { discard; } vec3 N = normalize(PositionWorld); B = -head; AoB = dot(A, B); BoB = dot(B, B); float NoA = dot(N, A); float NoB = dot(N, B); // Quadratic formula float a = AoA - NoA * NoA; float b = AoA * NoB - AoB * NoA; float c = AoA * BoB - AoB * AoB - r * r * AoA; float d = max(b * b - a * c, 0.); float t1 = (-b - sqrt(d)) / a; float t2 = (AoB + t1 * NoA) / AoA; // If the hit is outside the cylindrical part of the capsule, see if it intersects the end caps if (t2 <= 0. || t2 >= 1.) { vec3 C = t2 <= 0. ? B : -tail; b = dot(N, C); c = dot(C, C) - r * r; d = max(b * b - c, 0.); t1 = -b - sqrt(d); } vec3 P = N * t1; vec4 clip = Projection * vec4(P, 1.); FragDepth = clip.z / clip.w; return Color * mix(headColor, tailColor, clamp(t2, 0., 1.)); } ]]) function lovr.draw(pass) pass:push() pass:translate(0, 1.7, -3) pass:rotate(lovr.timer.getTime() / 3, 0, 1, 0) pass:setShader(shader) pass:send('Points', vertices) pass:mesh(nil, indices) pass:pop() end