1 module sbylib.collision.narrow.capsulepolygon; 2 3 import sbylib.collision.shape.polygon : CollisionPolygon; 4 import sbylib.collision.shape.capsule : CollisionCapsule; 5 import sbylib.math; 6 import std.typecons : Nullable, nullable; 7 8 enum CapsulePolygonCollisionType { 9 Edge, 10 Face, 11 Penetrating, 12 Parallel 13 } 14 15 struct CapsulePolygonResult { 16 vec3 _pushVector; 17 CapsulePolygonCollisionType type; 18 19 vec3 pushVector(CollisionPolygon) { 20 return _pushVector; 21 } 22 23 vec3 pushVector(CollisionCapsule) { 24 return -_pushVector; 25 } 26 } 27 28 auto detect(Capsule : CollisionCapsule, Polygon : CollisionPolygon) 29 (Capsule capsule, Polygon plane) { 30 return detect(plane, capsule); 31 } 32 33 Nullable!(CapsulePolygonResult) detect(Polygon : CollisionPolygon, Capsule : CollisionCapsule) 34 (Polygon plane, Capsule capsule) 35 in (plane.vertices.length >= 3) 36 { 37 import std.algorithm : map, maxElement; 38 import std.math : abs; 39 import sbylib.collision.bounds.aabb : intersect; 40 41 const p0 = plane.vertices[0]; 42 const n = normalize(cross(plane.vertices[0] - plane.vertices[1], plane.vertices[1] - plane.vertices[2])); 43 44 // exclude the obvious pattern 45 const d1 = dot(capsule.ends[0] - p0, n); 46 const d2 = dot(capsule.ends[1] - p0, n); 47 if (d1 > +capsule.radius && d2 > +capsule.radius) return typeof(return).init; 48 if (d1 < -capsule.radius && d2 < -capsule.radius) return typeof(return).init; 49 if (intersect(plane.getAABB(), capsule.getAABB()) is false) return typeof(return).init; 50 51 const r = segPoly(capsule.ends, n, plane.vertices); 52 if (r.dist > capsule.radius) return typeof(return).init; 53 54 const depth = capsule.ends[].map!(end => dot(end - p0, n)).maxElement + capsule.radius; 55 return nullable(CapsulePolygonResult(r.pushVector * depth, r.type)); 56 } 57 58 private auto segPoly(const vec3[2] ends, const vec3 n, const vec3[] ps) { 59 // 平行でないとき 60 // 線分が完全にポリゴン(平面)の片側に寄っている場合 61 // 線分の端点が面領域に入っているとき 62 // 1. 線分の端点とポリゴンの垂線ベクトル 63 // 線分の端点が面領域に入っていないとき 64 // 2. 辺と端点との最接近ベクトル 65 // 線分が平面の両側に存在している場合 66 // 線分がポリゴンを貫いているとき 67 // 3. 0ベクトル(線分がポリゴンを貫いている) 68 // 線分がポリゴンの横を通っているとき 69 // 4. 辺と線分との最接近ベクトル 70 // 平行なとき, 線分が点になっているとき 71 // 線分が完全にポリゴンの面領域に収まっているとき 72 // 5. 線分のどこかの点とポリゴンの垂線ベクトル 73 // 線分がポリゴンの面領域からはみ出ているとき 74 // 6. 辺と線分との最接近ベクトル 75 76 // 距離が正のとき、つまり線分がポリゴンを貫いていないときはめり込み解消ベクトル=最小距離ベクトルになるが、 77 // 貫いているときはめりこみ解消ベクトルは異なる 78 // このとき、ベクトルの候補は 79 // 1. ポリゴンの法線 80 // 2. 辺と線分の外積 81 // なお、返り値の法線は必ずポリゴンの表方向にしか押し出さないとする 82 83 import std.algorithm : map, all, clamp, reduce, minElement; 84 import std.range : iota; 85 import std.math : abs; 86 87 struct PolySegResult { 88 float dist; 89 vec3 pushVector; 90 CapsulePolygonCollisionType type; 91 } 92 93 const v = ends[1] - ends[0]; 94 const denom = dot(v, n); 95 96 alias min = (a, b) => a.lengthSq < b.lengthSq ? a : b; 97 98 if (denom != 0) { 99 const t1 = clamp(dot(ps[0] - ends[0], n) / denom, 0, 1); 100 const p = ends[0] + t1 * v; 101 auto ss = ps.length.iota.map!(i => dot(n, cross(ps[(i+1)%$] - ps[i], ps[i] - p))); 102 if (ss.all!(s => s > 0) || ss.all!(s => s < 0)) { 103 if (t1 == 0 || t1 == 1) { 104 // 線分が片側に寄っているとき 105 // 1 106 const dist = abs(dot(ps[0] - p, n)); 107 return PolySegResult(dist, n, CapsulePolygonCollisionType.Face); 108 } else { 109 // 3 110 return PolySegResult(0, n, CapsulePolygonCollisionType.Penetrating); 111 } 112 } else { 113 // 2, 4 114 const r = ps.length.iota.map!(i => segseg(ends, [ps[i], ps[(i+1)%$]])).minElement!(v => v.length); 115 return PolySegResult(r.length, n, CapsulePolygonCollisionType.Edge); 116 } 117 } else { 118 auto ss0 = ps.length.iota.map!(i => dot(n, cross(ps[(i+1)%$] - ps[i], ps[i] - ends[0]))); 119 auto ss1 = ps.length.iota.map!(i => dot(n, cross(ps[(i+1)%$] - ps[i], ps[i] - ends[1]))); 120 const sInFaceRegion = ss0.all!(s => s > 0) || ss0.all!(s => s < 0); 121 const eInFaceRegion = ss1.all!(s => s > 0) || ss1.all!(s => s < 0); 122 if (sInFaceRegion && eInFaceRegion) { 123 //ポリゴンは凸形状なので、端点が両方とも面領域に入っていれば全体が面領域に入っている 124 // 5 125 const dist = abs(dot(ps[0] - ends[0], n)); 126 return PolySegResult(dist, n, CapsulePolygonCollisionType.Parallel); 127 } else { 128 // 6 129 const r = ps.length.iota.map!(i => segseg(ends, [ps[i], ps[(i+1)%$]])).reduce!min; 130 return PolySegResult(r.length, n, CapsulePolygonCollisionType.Parallel); 131 } 132 } 133 } 134 135 private vec3 segseg(const vec3[2] ends0, const vec3[2] ends1) { 136 import std.algorithm : clamp, minElement; 137 138 if (ends0[0] == ends0[1]) { 139 if (ends1[0] == ends1[1]) { 140 // point & point 141 return ends0[0] - ends1[0]; 142 } else { 143 // point & line 144 return -segPoint(ends1, ends0[0]); 145 } 146 } else { 147 if (ends1[0] == ends1[1]) { 148 // point & line 149 return +segPoint(ends0, ends1[0]); 150 } 151 } 152 const v1 = normalize(ends0[1] - ends0[0]); 153 const v2 = normalize(ends1[1] - ends1[0]); 154 const d1 = dot(ends1[0] - ends0[0], v1); 155 const d2 = dot(ends1[0] - ends0[0], v2); 156 const dv = dot(v1, v2); 157 float denom = 1 - dv * dv; 158 if (denom > 0) { 159 denom = 1 / denom; 160 const t1 = (d1 - dv * d2) * denom; 161 const t2 = (d1 * dv - d2) * denom; 162 if (0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1) { 163 // line & line 164 const p1 = ends0[0] + t1 * v1; 165 const p2 = ends1[0] + t2 * v2; 166 return p1 - p2; 167 } 168 return [segPoint(ends0, ends1[0]), segPoint(ends0, ends1[1]), segPoint(ends1, ends0[0]), segPoint(ends1, ends0[1])] 169 .minElement!(v => length(v)); 170 } 171 // parallel 172 vec3 v = ends0[0] - ends1[0]; 173 v -= dot(v, v1) * v1; 174 return v; 175 } 176 177 private vec3 segPoint(const vec3[2] ends, const vec3 p) { 178 return segPoint(ends[0], ends[1] - ends[0],p); 179 } 180 181 private vec3 segPoint(const vec3 s, const vec3 v, const vec3 p) { 182 import std.algorithm : clamp; 183 184 const l = v.length; 185 const vn = v / l; 186 const ps = p - s; 187 const t = dot(ps, vn); 188 const tc = clamp(t, 0, l); 189 return s + tc * vn - p; 190 }