Files
sanspie 2e10968d48 Stage 11f3: landing, Getting started, guides; demo fixes (D347, D289)
Landing page and Install Aego / Your first project in English and Russian; the 2D template matches them. Platformer from scratch and Recipes with every step scene run by docs check. Builtin data and handle methods in the reference, Inspector names in field tables, Markdown copies and llms-full.txt, strict docs check in CI, engine/docs/README.md.
Fixes: a body cast from a touching contact uses the face normal, so characters no longer stick after a respawn or against walls; Respawn.resetVelocity works; a touch gamepad on exported web pages.
2026-10-25 15:31:22 +03:00

398 lines
9.6 KiB
Go

package world
const (
castIters = 30
gjkIters = 20
)
type CastHit struct {
Point Vec2
Normal Vec2
Fraction float32
Depth float32
Shape ShapeHandle
Body BodyHandle
Initial bool
}
type proxy struct {
v [maxPolyVerts]Vec2
n int
r float32
}
func proxyOf(c *convex) proxy {
p := proxy{n: c.count, r: c.radius}
copy(p.v[:c.count], c.v[:c.count])
return p
}
func (p *proxy) support(d Vec2) int {
best, bv := 0, p.v[0].Dot(d)
for i := 1; i < p.n; i++ {
if v := p.v[i].Dot(d); v > bv {
best, bv = i, v
}
}
return best
}
type simplexVertex struct {
wA, wB, w Vec2
a float32
iA, iB int
}
type simplex struct {
v [3]simplexVertex
count int
}
func (s *simplex) searchDir() Vec2 {
if s.count == 1 {
return s.v[0].w.Neg()
}
e := s.v[1].w.Sub(s.v[0].w)
if e.Cross(s.v[0].w.Neg()) > 0 {
return crossSV(1, e)
}
return crossVS(e, 1)
}
func (s *simplex) witness() (Vec2, Vec2) {
switch s.count {
case 1:
return s.v[0].wA, s.v[0].wB
case 2:
a := s.v[0].wA.Scale(s.v[0].a).Add(s.v[1].wA.Scale(s.v[1].a))
b := s.v[0].wB.Scale(s.v[0].a).Add(s.v[1].wB.Scale(s.v[1].a))
return a, b
}
p := s.v[0].wA.Scale(s.v[0].a).Add(s.v[1].wA.Scale(s.v[1].a)).Add(s.v[2].wA.Scale(s.v[2].a))
return p, p
}
func (s *simplex) solve2() {
w1, w2 := s.v[0].w, s.v[1].w
e12 := w2.Sub(w1)
d2 := -w1.Dot(e12)
if d2 <= 0 {
s.v[0].a, s.count = 1, 1
return
}
d1 := w2.Dot(e12)
if d1 <= 0 {
s.v[1].a = 1
s.v[0], s.count = s.v[1], 1
return
}
inv := 1 / (d1 + d2)
s.v[0].a, s.v[1].a = d1*inv, d2*inv
}
func (s *simplex) solve3() {
w1, w2, w3 := s.v[0].w, s.v[1].w, s.v[2].w
e12, e13, e23 := w2.Sub(w1), w3.Sub(w1), w3.Sub(w2)
d12a, d12b := w2.Dot(e12), -w1.Dot(e12)
d13a, d13b := w3.Dot(e13), -w1.Dot(e13)
d23a, d23b := w3.Dot(e23), -w2.Dot(e23)
n := e12.Cross(e13)
d123a, d123b, d123c := n*w2.Cross(w3), n*w3.Cross(w1), n*w1.Cross(w2)
switch {
case d12b <= 0 && d13b <= 0:
s.v[0].a, s.count = 1, 1
case d12a > 0 && d12b > 0 && d123c <= 0:
inv := 1 / (d12a + d12b)
s.v[0].a, s.v[1].a, s.count = d12a*inv, d12b*inv, 2
case d13a > 0 && d13b > 0 && d123b <= 0:
inv := 1 / (d13a + d13b)
s.v[0].a, s.v[2].a = d13a*inv, d13b*inv
s.v[1], s.count = s.v[2], 2
case d12a <= 0 && d23b <= 0:
s.v[1].a = 1
s.v[0], s.count = s.v[1], 1
case d13a <= 0 && d23a <= 0:
s.v[2].a = 1
s.v[0], s.count = s.v[2], 1
case d23a > 0 && d23b > 0 && d123a <= 0:
inv := 1 / (d23a + d23b)
s.v[1].a, s.v[2].a = d23a*inv, d23b*inv
s.v[0], s.count = s.v[2], 2
default:
inv := 1 / (d123a + d123b + d123c)
s.v[0].a, s.v[1].a, s.v[2].a = d123a*inv, d123b*inv, d123c*inv
s.count = 3
}
}
func gjk(a, b *proxy) (Vec2, Vec2, float32) {
var s simplex
s.v[0] = simplexVertex{wA: a.v[0], wB: b.v[0], w: b.v[0].Sub(a.v[0]), a: 1}
s.count = 1
var saveA, saveB [3]int
for range gjkIters {
n := s.count
for i := 0; i < n; i++ {
saveA[i], saveB[i] = s.v[i].iA, s.v[i].iB
}
switch s.count {
case 2:
s.solve2()
case 3:
s.solve3()
}
if s.count == 3 {
break
}
d := s.searchDir()
if d.LenSq() < fltEpsilon*fltEpsilon {
break
}
nv := &s.v[s.count]
nv.iA, nv.iB = a.support(d.Neg()), b.support(d)
nv.wA, nv.wB = a.v[nv.iA], b.v[nv.iB]
nv.w = nv.wB.Sub(nv.wA)
dup := false
for i := 0; i < n; i++ {
if nv.iA == saveA[i] && nv.iB == saveB[i] {
dup = true
break
}
}
if dup {
break
}
s.count++
}
pA, pB := s.witness()
return pA, pB, pB.Sub(pA).Len()
}
func separation(pA, pB Vec2, d float32, ca, cb, delta Vec2) Vec2 {
if d > fltEpsilon {
return pA.Sub(pB).Scale(1 / d)
}
if n, l := normalizeLen(ca.Sub(cb)); l > 0 {
return n
}
if n, l := normalizeLen(delta.Neg()); l > 0 {
return n
}
return Vec2{Y: -1}
}
func castPair(a *proxy, delta Vec2, b *proxy, slop float32) (CastHit, bool) {
target, tol := 0.25*slop, 0.125*slop
r := a.r + b.r
moved := *a
var t float32
for it := 0; it < castIters; it++ {
if t > 0 {
off := delta.Scale(t)
for i := 0; i < a.n; i++ {
moved.v[i] = a.v[i].Add(off)
}
}
pA, pB, d := gjk(&moved, b)
gap := d - r
if d < fltEpsilon {
return CastHit{}, false
}
n := pB.Sub(pA).Scale(1 / d)
if gap < target+tol {
return CastHit{Point: pB.Sub(n.Scale(b.r)), Normal: n.Neg(), Fraction: t}, true
}
speed := delta.Dot(n)
if speed <= 0 {
return CastHit{}, false
}
t += (gap - target) / speed
if t > 1 {
return CastHit{}, false
}
}
return CastHit{}, false
}
func (q *queryState) target(id int32) (*shape, convex, bool) {
pi := &q.w.prox[id]
s := &q.w.shapes.items[pi.shape.Index]
if !q.accept(s) {
return nil, convex{}, false
}
g := s.g.child(int(pi.child))
c := g.transformed(q.w.bodies.items[s.body.Index].xf)
if s.g.kind == KindChain && s.oneSided && c.n[0].Dot(q.castCentroid.Sub(c.v[0])) < 0 {
return nil, convex{}, false
}
return s, c, true
}
// overlap reports how deep the mover sits in obstacle c; cores that intersect fall back to the SAT manifold.
func (q *queryState) overlap(c *convex, delta Vec2) (CastHit, bool) {
pb := proxyOf(c)
pA, pB, d := gjk(&q.castA, &pb)
gap := d - q.castA.r - pb.r
if gap >= 0 && d > fltEpsilon {
return CastHit{}, false
}
if d > fltEpsilon {
n := pA.Sub(pB).Scale(1 / d)
return CastHit{Point: pB.Add(n.Scale(pb.r)), Normal: n, Depth: -gap, Initial: true}, true
}
var m Manifold
collideConvex(c, identityXf(), &q.castC, identityXf(), 0, &m)
if m.Count == 0 {
n := separation(pA, pB, d, q.castCentroid, c.centroid, delta)
return CastHit{Point: pB.Add(n.Scale(pb.r)), Normal: n, Initial: true}, true
}
deep := 0
for i := 1; i < m.Count; i++ {
if m.Points[i].Separation < m.Points[deep].Separation {
deep = i
}
}
sep := m.Points[deep].Separation
if sep >= 0 && delta.Dot(m.Normal) >= 0 {
return CastHit{}, false
}
return CastHit{Point: m.Points[deep].Point.Sub(m.Normal.Scale(0.5 * sep)), Normal: m.Normal, Depth: max(-sep, 0), Initial: true}, true
}
func (q *queryState) castHit(id int32) bool {
s, c, ok := q.target(id)
if !ok {
return true
}
hit, ok := q.overlap(&c, q.castDelta)
if !ok {
pb := proxyOf(&c)
hit, ok = castPair(&q.castA, q.castDelta, &pb, q.w.cfg.LinearSlop)
if ok && hit.Fraction == 0 {
// touching at the start: the GJK witness direction is unreliable there, take the face normal
var m Manifold
collideConvex(&c, identityXf(), &q.castC, identityXf(), q.w.cfg.LinearSlop, &m)
if m.Count > 0 {
hit.Normal = m.Normal
}
ok = q.castDelta.Dot(hit.Normal) < 0
}
}
if !ok {
return true
}
hit.Shape, hit.Body = q.w.prox[id].shape, s.body
if !q.castFound || hit.Fraction < q.castBest.Fraction ||
(hit.Fraction == q.castBest.Fraction && hit.Shape.Index < q.castBest.Shape.Index) {
q.castBest, q.castFound = hit, true
}
return true
}
func (q *queryState) penHit(id int32) bool {
s, c, ok := q.target(id)
if !ok {
return true
}
hit, ok := q.overlap(&c, Vec2{})
if !ok {
return true
}
hit.Shape, hit.Body = q.w.prox[id].shape, s.body
q.penOut[q.penN] = hit
q.penN++
return q.penN < len(q.penOut)
}
func (w *World) castProbe(c *convex, delta Vec2, f QueryFilter) (CastHit, bool) {
q := &w.q
q.filter = f
q.castC, q.castA, q.castCentroid, q.castDelta = *c, proxyOf(c), c.centroid, delta
q.castBest, q.castFound = CastHit{}, false
w.tree.query(boxSweep(c.aabb(identityXf()), delta), q.castFn)
return q.castBest, q.castFound
}
func (w *World) CastCircle(c Vec2, r float32, delta Vec2, f QueryFilter) (CastHit, bool) {
if !finite2(c) || !finite(r) || r <= 0 || !finite2(delta) {
return CastHit{}, false
}
p := convex{count: 1, radius: r, centroid: c}
p.v[0] = c
return w.castProbe(&p, delta, f)
}
func (w *World) CastBox(c, half Vec2, angle float32, delta Vec2, f QueryFilter) (CastHit, bool) {
if !finite2(c) || !finite2(half) || !finite(angle) || half.X <= 0 || half.Y <= 0 || !finite2(delta) {
return CastHit{}, false
}
p := convex{count: 4}
p.v[0] = Vec2{X: -half.X, Y: -half.Y}
p.v[1] = Vec2{X: half.X, Y: -half.Y}
p.v[2] = Vec2{X: half.X, Y: half.Y}
p.v[3] = Vec2{X: -half.X, Y: half.Y}
p.computeNormals()
t := p.transformed(xform{p: c, q: makeRot(angle)})
return w.castProbe(&t, delta, f)
}
func (w *World) CastCapsule(a, b Vec2, r float32, delta Vec2, f QueryFilter) (CastHit, bool) {
if !finite2(a) || !finite2(b) || !finite(r) || r <= 0 || !finite2(delta) {
return CastHit{}, false
}
if b.Sub(a).LenSq() < w.cfg.LinearSlop*w.cfg.LinearSlop {
return w.CastCircle(a.Lerp(b, 0.5), r, delta, f)
}
p := convex{count: 2, radius: r, centroid: a.Lerp(b, 0.5)}
p.v[0], p.v[1] = a, b
p.computeNormals()
return w.castProbe(&p, delta, f)
}
func (w *World) CastBody(h BodyHandle, offset, delta Vec2, f QueryFilter) (CastHit, bool) {
b, err := w.getBody(h)
if err != nil || !finite2(offset) || !finite2(delta) {
return CastHit{}, false
}
f.ExcludeBody = h
xf := xform{p: b.xf.p.Add(offset), q: b.xf.q}
var best CastHit
found := false
for _, sh := range b.shapes {
s := &w.shapes.items[sh.Index]
if s.dead || s.trigger || s.g.kind == KindChain {
continue
}
c := s.g.c.transformed(xf)
if hit, ok := w.castProbe(&c, delta, f); ok && (!found || hit.Fraction < best.Fraction) {
best, found = hit, true
}
}
return best, found
}
func (w *World) Penetration(h BodyHandle, f QueryFilter, out []CastHit) int {
b, err := w.getBody(h)
if err != nil || len(out) == 0 {
return 0
}
f.ExcludeBody = h
q := &w.q
q.filter, q.penOut, q.penN = f, out, 0
for _, sh := range b.shapes {
s := &w.shapes.items[sh.Index]
if s.dead || s.trigger || s.g.kind == KindChain {
continue
}
c := s.g.c.transformed(b.xf)
q.castC, q.castA, q.castCentroid = c, proxyOf(&c), c.centroid
w.tree.query(c.aabb(identityXf()), q.penFn)
if q.penN == len(out) {
break
}
}
n := q.penN
q.penOut = nil
return n
}