1 /* 2 Copyright © 2022, Inochi2D Project 3 Distributed under the 2-Clause BSD License, see LICENSE file. 4 5 Author: Asahi Lina 6 */ 7 module creator.viewport.common.spline; 8 import creator.viewport.common.mesh; 9 import creator.viewport; 10 import inochi2d; 11 import inochi2d.core.dbg; 12 import bindbc.opengl; 13 import std.algorithm.mutation; 14 import std.array; 15 import std.math : isFinite; 16 import std.stdio; 17 18 private { 19 float nearestLine(vec2 a, vec2 b, vec2 c) 20 { 21 return ((c.x - a.x) * (b.x - a.x) + (c.y - a.y) * (b.y - a.y)) / ((b.x - a.x) ^^ 2 + (b.y - a.y) ^^ 2); 22 } 23 24 BezierSegment catmullToBezier(SplinePoint *p0, SplinePoint *p1, SplinePoint *p2, SplinePoint *p3, float tension = 1.0) { 25 BezierSegment res; 26 27 res.p[0] = p1.position; 28 res.p[1] = p1.position + (p2.position - p0.position) / (6 * tension); 29 res.p[2] = p2.position - (p3.position - p1.position) / (6 * tension); 30 res.p[3] = p2.position; 31 32 return res; 33 } 34 } 35 36 struct SplinePoint { 37 vec2 position; 38 float weightL; 39 float weightR; 40 } 41 42 struct BezierSegment { 43 vec2[4] p; 44 45 void split(float t, out BezierSegment left, out BezierSegment right) { 46 float s = 1 - t; 47 48 vec2 mc = p[1] * s + p[2] * t; 49 vec2 a1 = p[0] * s + p[1] * t; 50 vec2 a2 = a1 * s + mc * t; 51 vec2 b2 = p[2] * s + p[3] * t; 52 vec2 b1 = b2 * t + mc * s; 53 vec2 m = a2 * s + b1 * t; 54 55 left = BezierSegment([p[0], a1, a2, m]); 56 right = BezierSegment([m, b1, b2, p[3]]); 57 } 58 59 vec2 eval(float t) { 60 BezierSegment left, right; 61 split(t, left, right); 62 return left.p[3]; 63 } 64 } 65 66 class CatmullSpline { 67 private: 68 vec2[] interpolated; 69 vec3[] drawLines; 70 vec3[] drawPoints; 71 vec2[] refMesh; 72 vec3[] refOffsets; 73 74 public: 75 uint resolution = 40; 76 float selectRadius = 16f; 77 SplinePoint[] points; 78 CatmullSpline target; 79 80 void createTarget(IncMesh reference) { 81 target = new CatmullSpline; 82 target.resolution = resolution; 83 target.selectRadius = selectRadius; 84 target.points = points.dup; 85 target.interpolate(); 86 87 refMesh.length = 0; 88 foreach(ref MeshVertex* vtx; reference.vertices) { 89 refMesh ~= vtx.position; 90 } 91 mapReference(); 92 } 93 94 void mapReference() { 95 if (points.length < 2) { 96 refOffsets.length = 0; 97 return; 98 } 99 // writeln("mapReference"); 100 refOffsets.length = 0; 101 102 float epsilon = 0.0001; 103 foreach(vtx; refMesh) { 104 float off = findClosestPointOffset(vtx); 105 // FIXME: calculate tangent properly 106 vec2 pt = target.eval(off); 107 vec2 pt2 = target.eval(off + epsilon); 108 vec2 tangent = pt2 - pt; 109 tangent.normalize(); 110 111 // FIXME: extrapolation... 112 if (off <= 0 || off >= (points.length - 1)) { 113 refOffsets ~= vec3(0, 0, float()); 114 continue; 115 } 116 vtx = vtx - pt; 117 vec3 rel = vec3( 118 vtx.x * tangent.x + vtx.y * tangent.y, 119 vtx.y * tangent.x - vtx.x * tangent.y, 120 off, 121 ); 122 // writefln("%s %s %s", vtx, rel, tangent); 123 refOffsets ~= rel; 124 } 125 } 126 127 void resetTarget(IncMesh mesh) { 128 foreach(i, vtx; refMesh) { 129 mesh.vertices[i].position = vtx; 130 } 131 } 132 133 void updateTarget(IncMesh mesh) { 134 if (points.length < 2) { 135 resetTarget(mesh); 136 return; 137 } 138 139 float epsilon = 0.0001; 140 foreach(i, rel; refOffsets) { 141 if (!isFinite(rel.z)) continue; 142 143 // FIXME: calculate tangent properly 144 vec2 pt = target.eval(rel.z); 145 vec2 pt2 = target.eval(rel.z + epsilon); 146 vec2 tangent = pt2 - pt; 147 tangent = tangent / abs(tangent.distance(vec2(0, 0))); 148 149 vec2 vtx = vec2( 150 pt.x + rel.x * tangent.x - rel.y * tangent.y, 151 pt.y + rel.y * tangent.x + rel.x * tangent.y 152 ); 153 // writefln("%s %s %s", vtx, rel, tangent); 154 mesh.vertices[i].position = vtx; 155 } 156 } 157 158 void update() { 159 interpolate(); 160 } 161 162 void interpolate() { 163 interpolated.length = 0; 164 drawLines.length = 0; 165 166 if (points.length == 0) return; 167 168 vec2 last; 169 foreach(pos; 0..(resolution * (points.length - 1) + 1)) { 170 vec2 p = eval(pos / cast(float)(resolution)); 171 interpolated ~= p; 172 if (pos > 0) drawLines ~= [vec3(last.x, last.y, 0), vec3(p.x, p.y, 0)]; 173 last = p; 174 } 175 176 drawPoints.length = 0; 177 foreach(p; points) { 178 drawPoints ~= vec3(p.position.x, p.position.y, 0); 179 } 180 } 181 182 vec2 eval(float off) { 183 if (off <= 0) return points[0].position; 184 if (off >= (points.length - 1)) return points[$ - 1].position; 185 186 uint ioff = cast(uint)off; 187 float t = off - ioff; 188 float s = 1 - t; 189 190 SplinePoint *p1 = &points[ioff]; 191 SplinePoint *p2 = &points[ioff + 1]; 192 193 // Two points, linear 194 if (points.length == 2) return p1.position * s + p2.position * t; 195 196 vec2 evalEdge(vec2 p0, vec2 p1, vec2 p2, float t) { 197 float t2 = t ^^ 2; 198 float t3 = t ^^ 3; 199 200 float h00 = 2 * t3 - 3 * t2 + 1; 201 float h10 = t3 - 2 * t2 + t; 202 float h01 = -2 * t3 + 3 * t2; 203 float h11 = t3 - t2; 204 205 vec2 slope = (p2 - p0) / 2; 206 return h11 * slope + h01 * p1 + h00 * p0; 207 } 208 209 // Edge segment, do the hermite spline thing 210 if (ioff == 0) 211 return evalEdge(p1.position, p2.position, points[ioff + 2].position, t); 212 else if (ioff == points.length - 2) 213 return evalEdge(p2.position, p1.position, points[ioff - 1].position, s); 214 215 // Middle segment, do Catmull-Rom 216 SplinePoint *p0 = &points[ioff - 1]; 217 SplinePoint *p3 = &points[ioff + 2]; 218 219 // By first converting it to Bezier 220 float tension = 1; 221 BezierSegment bezier = catmullToBezier(p0, p1, p2, p3); 222 return bezier.eval(t); 223 } 224 225 float findClosestPointOffset(vec2 point) { 226 vec2 tangent; 227 return findClosestPointOffset(point, tangent); 228 } 229 230 float findClosestPointOffset(vec2 point, out vec2 tangent) { 231 tangent = vec2(0, 0); 232 233 if (points.length == 0) return 0; 234 if (points.length == 1) { 235 return 0; 236 } 237 238 uint bestIdx = 0; 239 float bestDist = float.infinity; 240 // Find closest interpolated point 241 foreach(pos; 0..(resolution * (points.length - 1) + 1)) { 242 float dist = interpolated[pos].distance(point); 243 if (dist < bestDist) { 244 bestDist = dist; 245 bestIdx = cast(uint)pos; 246 } 247 } 248 // writefln("best %s %s", bestIdx, bestDist); 249 250 float left = max(0, (bestIdx - 1) / cast(float)resolution); 251 float right = min(points.length - 1, (bestIdx + 1) / cast(float)resolution); 252 vec2 leftPoint = eval(left); 253 vec2 rightPoint = eval(right); 254 float leftDist = abs(point.distance(leftPoint)); 255 float rightDist = abs(point.distance(rightPoint)); 256 257 float epsilon = 0.0001; 258 259 while ((right - left) > epsilon * 2) { 260 // writefln("left %s right %s", left, right); 261 float mid = (left + right) / 2; 262 vec2 midLPoint = eval(mid - epsilon); 263 vec2 midRPoint = eval(mid + epsilon); 264 if (abs(point.distance(midLPoint)) < abs(point.distance(midRPoint))) { 265 right = mid; 266 rightPoint = eval(right); 267 rightDist = abs(point.distance(rightPoint)); 268 } else { 269 left = mid; 270 leftPoint = eval(left); 271 leftDist = abs(point.distance(leftPoint)); 272 } 273 } 274 tangent = rightPoint - leftPoint; 275 tangent = tangent / abs(tangent.distance(vec2(0, 0))); 276 277 if (left == 0) return 0; 278 if (right == points.length - 1) return points.length - 1; 279 280 // writefln("left %s right %s", left, right); 281 282 // Linearly interpolate within the range 283 float t = nearestLine(leftPoint, rightPoint, point); 284 return left * (1 - t) + right * t; 285 } 286 287 void splitAt(float off) { 288 vec2 pos = eval(off); 289 points.insertInPlace(1 + cast(uint)off, SplinePoint(pos, 1, 1)); 290 interpolate(); 291 if (target) target.splitAt(off); 292 } 293 294 void prependPoint(vec2 point) { 295 points.insertInPlace(0, SplinePoint(point, 1, 1)); 296 interpolate(); 297 if (target) target.prependPoint(point); 298 } 299 300 void appendPoint(vec2 point) { 301 points ~= SplinePoint(point, 1, 1); 302 interpolate(); 303 if (target) target.appendPoint(point); 304 } 305 306 int findPoint(vec2 point) { 307 uint bestIdx = 0; 308 float bestDist = float.infinity; 309 foreach(idx, pt; points) { 310 float dist = pt.position.distance(point); 311 if (dist < bestDist) { 312 bestDist = dist; 313 bestIdx = cast(uint)idx; 314 } 315 } 316 317 if (bestDist > selectRadius/incViewportZoom) return -1; 318 return bestIdx; 319 } 320 321 int addPoint(vec2 point) { 322 if (points.length < 2) { 323 appendPoint(point); 324 return cast(int)points.length - 1; 325 } 326 327 float off = findClosestPointOffset(point); 328 // writefln("Found off %s", off); 329 if (off <= 0) { 330 prependPoint(point); 331 return 0; 332 } else if (off >= (points.length - 1)) { 333 appendPoint(point); 334 return cast(int)points.length - 1; 335 } 336 vec2 onCurve = eval(off); 337 if (abs(point.distance(onCurve)) < selectRadius/incViewportZoom) { 338 splitAt(off); 339 return 1 + cast(int)off; 340 } 341 return -1; 342 } 343 344 void removePoint(uint idx) { 345 points = points.remove(idx); 346 if (target) target.removePoint(idx); 347 interpolate(); 348 } 349 350 void draw(mat4 trans, vec4 color) { 351 if (drawLines.length > 0) { 352 inDbgSetBuffer(drawLines); 353 inDbgDrawLines(color, trans); 354 } 355 if (drawPoints.length > 0) { 356 inDbgSetBuffer(drawPoints); 357 inDbgPointsSize(10); 358 inDbgDrawPoints(vec4(0, 0, 0, 1), trans); 359 inDbgPointsSize(6); 360 inDbgDrawPoints(color, trans); 361 } 362 } 363 364 }