Files

391 lines
8.7 KiB
Go

package world
import (
"aego/core/mathx"
)
const (
maxPolyVerts = 8
maxChainPoints = 4096
)
type GeometryKind uint8
const (
KindCircle GeometryKind = iota
KindBox
KindCapsule
KindPolygon
KindChain
)
func (k GeometryKind) String() string {
switch k {
case KindCircle:
return "circle"
case KindBox:
return "box"
case KindCapsule:
return "capsule"
case KindPolygon:
return "polygon"
case KindChain:
return "chain"
}
return "unknown"
}
type Geometry interface {
Kind() GeometryKind
}
type Circle struct {
R float32
}
type Box struct {
Half Vec2
}
type Capsule struct {
HalfLen float32
R float32
}
type Polygon struct {
Pts [maxPolyVerts]Vec2
N int
}
type Chain struct {
Pts []Vec2
Loop bool
GhostPrev Vec2
GhostNext Vec2
}
func (Circle) Kind() GeometryKind { return KindCircle }
func (Box) Kind() GeometryKind { return KindBox }
func (Capsule) Kind() GeometryKind { return KindCapsule }
func (Polygon) Kind() GeometryKind { return KindPolygon }
func (Chain) Kind() GeometryKind { return KindChain }
type convex struct {
v [maxPolyVerts]Vec2
n [maxPolyVerts]Vec2
count int
radius float32
centroid Vec2
}
func (c *convex) computeNormals() {
if c.count == 2 {
e, _ := normalizeLen(c.v[1].Sub(c.v[0]))
c.n[0] = rightPerp(e)
c.n[1] = c.n[0].Neg()
return
}
for i := 0; i < c.count; i++ {
j := i + 1
if j == c.count {
j = 0
}
e, _ := normalizeLen(c.v[j].Sub(c.v[i]))
c.n[i] = rightPerp(e)
}
}
func (c *convex) transformed(x xform) convex {
out := convex{count: c.count, radius: c.radius, centroid: x.apply(c.centroid)}
for i := 0; i < c.count; i++ {
out.v[i] = x.apply(c.v[i])
out.n[i] = x.q.apply(c.n[i])
}
return out
}
func (c *convex) aabb(x xform) AABB {
lo := x.apply(c.v[0])
hi := lo
for i := 1; i < c.count; i++ {
p := x.apply(c.v[i])
lo, hi = min2(lo, p), max2(hi, p)
}
return boxExpand(AABB{Min: lo, Max: hi}, c.radius)
}
type chainData struct {
pts []Vec2
loop bool
ghostPrev Vec2
ghostNext Vec2
}
func (c *chainData) segments() int {
if c.loop {
return len(c.pts)
}
return len(c.pts) - 1
}
func (c *chainData) point(i int) Vec2 {
n := len(c.pts)
return c.pts[((i%n)+n)%n]
}
func (c *chainData) segment(i int) (convex, Vec2, Vec2) {
v1, v2 := c.point(i), c.point(i+1)
var g0, g3 Vec2
switch {
case c.loop:
g0, g3 = c.point(i-1), c.point(i+2)
default:
if i == 0 {
g0 = c.ghostPrev
} else {
g0 = c.pts[i-1]
}
if i+2 < len(c.pts) {
g3 = c.pts[i+2]
} else {
g3 = c.ghostNext
}
}
s := convex{count: 2, centroid: v1.Add(v2).Scale(0.5)}
s.v[0], s.v[1] = v1, v2
s.computeNormals()
return s, g0, g3
}
type geom struct {
kind GeometryKind
c convex
chain *chainData
}
func (g *geom) children() int {
if g.kind == KindChain {
return g.chain.segments()
}
return 1
}
func (g *geom) child(i int) convex {
if g.kind == KindChain {
s, _, _ := g.chain.segment(i)
return s
}
return g.c
}
func (g *geom) childAABB(i int, x xform) AABB {
if g.kind == KindChain {
s, _, _ := g.chain.segment(i)
return s.aabb(x)
}
return g.c.aabb(x)
}
func validTransform(t Transform) error {
if !finite2(t.Position) || !finite(t.Angle) {
return bad("local transform is not finite")
}
return nil
}
func buildGeom(g Geometry, local Transform, slop float32) (geom, error) {
if err := validTransform(local); err != nil {
return geom{}, err
}
x := local.xf()
switch s := g.(type) {
case Circle:
if !finite(s.R) || s.R <= 0 {
return geom{}, bad("circle radius %v must be positive", s.R)
}
out := geom{kind: KindCircle, c: convex{count: 1, radius: s.R}}
out.c.v[0] = x.p
out.c.centroid = x.p
return out, nil
case Box:
if !finite2(s.Half) || s.Half.X <= slop || s.Half.Y <= slop {
return geom{}, bad("box half extents %v must exceed linear slop %v", s.Half, slop)
}
hx, hy := s.Half.X, s.Half.Y
out := geom{kind: KindBox, c: convex{count: 4}}
out.c.v[0] = x.apply(Vec2{X: -hx, Y: -hy})
out.c.v[1] = x.apply(Vec2{X: hx, Y: -hy})
out.c.v[2] = x.apply(Vec2{X: hx, Y: hy})
out.c.v[3] = x.apply(Vec2{X: -hx, Y: hy})
out.c.computeNormals()
out.c.centroid = x.p
return out, nil
case Capsule:
if !finite(s.HalfLen) || !finite(s.R) || s.HalfLen <= slop || s.R <= 0 {
return geom{}, bad("capsule half length %v and radius %v must be positive", s.HalfLen, s.R)
}
out := geom{kind: KindCapsule, c: convex{count: 2, radius: s.R}}
out.c.v[0] = x.apply(Vec2{X: -s.HalfLen})
out.c.v[1] = x.apply(Vec2{X: s.HalfLen})
out.c.computeNormals()
out.c.centroid = x.p
return out, nil
case Polygon:
return buildPolygon(s, x, slop)
case Chain:
return buildChain(s, x, slop)
case nil:
return geom{}, bad("shape has no geometry")
}
return geom{}, bad("unknown geometry %T", g)
}
func buildPolygon(s Polygon, x xform, slop float32) (geom, error) {
n := s.N
if n < 3 || n > maxPolyVerts {
return geom{}, bad("polygon needs 3..%d points, got %d", maxPolyVerts, n)
}
var p [maxPolyVerts]Vec2
for i := 0; i < n; i++ {
if !finite2(s.Pts[i]) {
return geom{}, bad("polygon point %d is not finite", i)
}
p[i] = s.Pts[i]
}
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if p[i].Sub(p[j]).LenSq() < slop*slop {
return geom{}, bad("polygon points %d and %d are closer than linear slop", i, j)
}
}
}
var area float32
for i := 0; i < n; i++ {
area += p[i].Cross(p[(i+1)%n])
}
if mathx.Abs(area)*0.5 < slop*slop {
return geom{}, bad("polygon is degenerate")
}
if area < 0 {
for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
p[i], p[j] = p[j], p[i]
}
}
for i := 0; i < n; i++ {
a, b, c := p[i], p[(i+1)%n], p[(i+2)%n]
e1, _ := normalizeLen(b.Sub(a))
e2, _ := normalizeLen(c.Sub(b))
if e1.Cross(e2) <= 1e-4 {
return geom{}, bad("polygon is not strictly convex at point %d", (i+1)%n)
}
}
out := geom{kind: KindPolygon, c: convex{count: n}}
for i := 0; i < n; i++ {
out.c.v[i] = x.apply(p[i])
}
out.c.computeNormals()
out.c.centroid = polygonMass(&out.c, 1).center
return out, nil
}
func buildChain(s Chain, x xform, slop float32) (geom, error) {
n := len(s.Pts)
switch {
case n < 2:
return geom{}, bad("chain needs at least 2 points")
case s.Loop && n < 3:
return geom{}, bad("chain loop needs at least 3 points")
case n > maxChainPoints:
return geom{}, bad("chain has %d points, the limit is %d", n, maxChainPoints)
}
ch := &chainData{pts: make([]Vec2, n), loop: s.Loop}
for i, p := range s.Pts {
if !finite2(p) {
return geom{}, bad("chain point %d is not finite", i)
}
ch.pts[i] = x.apply(p)
}
segs := ch.segments()
for i := 0; i < segs; i++ {
if ch.point(i+1).Sub(ch.point(i)).LenSq() < slop*slop {
return geom{}, bad("chain segment %d is shorter than linear slop", i)
}
}
if !s.Loop {
if !finite2(s.GhostPrev) || !finite2(s.GhostNext) {
return geom{}, bad("chain ghost vertices are not finite")
}
gp, gn := x.apply(s.GhostPrev), x.apply(s.GhostNext)
if s.GhostPrev == (Vec2{}) || gp.Sub(ch.pts[0]).LenSq() < slop*slop {
gp = ch.pts[0].Add(ch.pts[0].Sub(ch.pts[1]))
}
if s.GhostNext == (Vec2{}) || gn.Sub(ch.pts[n-1]).LenSq() < slop*slop {
gn = ch.pts[n-1].Add(ch.pts[n-1].Sub(ch.pts[n-2]))
}
ch.ghostPrev, ch.ghostNext = gp, gn
}
out := geom{kind: KindChain, chain: ch}
var c Vec2
for _, p := range ch.pts {
c = c.Add(p)
}
out.c.centroid = c.Scale(1 / float32(n))
return out, nil
}
type massData struct {
mass float32
center Vec2
inertia float32
}
func polygonMass(c *convex, density float32) massData {
s := c.v[0]
var area, inertia float32
var center Vec2
const inv3 = float32(1.0 / 3.0)
for i := 0; i < c.count; i++ {
e1 := c.v[i].Sub(s)
e2 := c.v[(i+1)%c.count].Sub(s)
d := e1.Cross(e2)
tri := 0.5 * d
area += tri
center = center.Add(e1.Add(e2).Scale(tri * inv3))
intx2 := e1.X*e1.X + e2.X*e1.X + e2.X*e2.X
inty2 := e1.Y*e1.Y + e2.Y*e1.Y + e2.Y*e2.Y
inertia += (0.25 * inv3 * d) * (intx2 + inty2)
}
if area <= 0 {
return massData{center: s}
}
center = center.Scale(1 / area)
mass := density * area
return massData{mass: mass, center: center.Add(s), inertia: density*inertia - mass*center.Dot(center)}
}
func (g *geom) mass(density float32) massData {
switch g.kind {
case KindCircle:
r := g.c.radius
m := density * mathx.Pi * r * r
return massData{mass: m, center: g.c.v[0], inertia: 0.5 * m * r * r}
case KindCapsule:
r := g.c.radius
rr := r * r
p1, p2 := g.c.v[0], g.c.v[1]
length := p2.Sub(p1).Len()
circle := density * mathx.Pi * rr
box := density * 2 * r * length
lc := 4 * r / (3 * mathx.Pi)
h := 0.5 * length
return massData{
mass: circle + box,
center: p1.Lerp(p2, 0.5),
inertia: circle*(0.5*rr+h*h+2*h*lc) + box*(4*rr+length*length)/12,
}
case KindBox, KindPolygon:
return polygonMass(&g.c, density)
}
return massData{center: g.c.centroid}
}