matrices, etc

This commit is contained in:
Josh Deprez 2021-09-07 17:10:26 +10:00
parent 88fdaf18ac
commit ebbf76d312
5 changed files with 215 additions and 12 deletions

View file

@ -36,6 +36,22 @@ func (b Box) Size() Int3 {
return b.Max.Sub(b.Min) return b.Max.Sub(b.Min)
} }
// Add offsets the box by vector p.
func (b Box) Add(p Int3) Box {
return Box{
Min: b.Min.Add(p),
Max: b.Max.Add(p),
}
}
// Sub offsets the box by (-p).
func (b Box) Sub(p Int3) Box {
return Box{
Min: b.Min.Sub(p),
Max: b.Max.Sub(p),
}
}
// Back returns an image.Rectangle representing the back of the box, using // Back returns an image.Rectangle representing the back of the box, using
// the given projection π. // the given projection π.
func (b Box) Back(π IntProjection) image.Rectangle { func (b Box) Back(π IntProjection) image.Rectangle {

View file

@ -1,6 +1,9 @@
package engine package engine
import "image" import (
"errors"
"image"
)
// IntMatrix3 implements a 3x3 integer matrix. // IntMatrix3 implements a 3x3 integer matrix.
type IntMatrix3 [3][3]int type IntMatrix3 [3][3]int
@ -48,6 +51,21 @@ func (a IntMatrix3x4) Apply(v Int3) Int3 {
} }
} }
// ToRatMatrix3 returns the 3x3 submatrix as a rational matrix equivalent.
func (a IntMatrix3x4) ToRatMatrix3() RatMatrix3 {
return RatMatrix3{
0: [3]Rat{{a[0][0], 1}, {a[0][1], 1}, {a[0][2], 1}},
1: [3]Rat{{a[1][0], 1}, {a[1][1], 1}, {a[1][2], 1}},
2: [3]Rat{{a[2][0], 1}, {a[2][1], 1}, {a[2][2], 1}},
}
}
// Translation returns the translation component of the matrix (last column)
// i.e. what you get if you Apply the matrix to the zero vector.
func (a IntMatrix3x4) Translation() Int3 {
return Int3{X: a[0][3], Y: a[1][3], Z: a[2][3]}
}
// IntMatrix2x3 implements a 2 row, 3 column matrix (as two row vectors). // IntMatrix2x3 implements a 2 row, 3 column matrix (as two row vectors).
type IntMatrix2x3 struct{ X, Y Int3 } type IntMatrix2x3 struct{ X, Y Int3 }
@ -58,3 +76,53 @@ func (a IntMatrix2x3) Apply(v Int3) image.Point {
Y: v.Dot(a.Y), Y: v.Dot(a.Y),
} }
} }
// RatMatrix3 implements a 3x3 matrix with rational number entries.
type RatMatrix3 [3][3]Rat
// IdentityRatMatrix3x4 is the identity matrix for RatMatrix3x4.
var IdentityRatMatrix3 = RatMatrix3{
0: [3]Rat{0: {1, 1}},
1: [3]Rat{1: {1, 1}},
2: [3]Rat{2: {1, 1}},
}
// IntApply applies the matrix to the integer vector v. Any remainder is lost.
func (a RatMatrix3) IntApply(v Int3) Int3 {
x, y, z := IntRat(v.X), IntRat(v.Y), IntRat(v.Z)
return Int3{
X: x.Mul(a[0][0]).Add(y.Mul(a[0][1])).Add(z.Mul(a[0][2])).Int(),
Y: x.Mul(a[1][0]).Add(y.Mul(a[1][1])).Add(z.Mul(a[1][2])).Int(),
Z: x.Mul(a[2][0]).Add(y.Mul(a[2][1])).Add(z.Mul(a[2][2])).Int(),
}
}
// Inverse returns the inverse of the matrix.
func (a RatMatrix3) Inverse() (RatMatrix3, error) {
adj := RatMatrix3{
0: [3]Rat{
0: a[1][1].Mul(a[2][2]).Sub(a[1][2].Mul(a[2][1])),
},
1: [3]Rat{
0: a[1][0].Mul(a[2][2]).Sub(a[1][2].Mul(a[2][0])).Neg(),
},
2: [3]Rat{
0: a[1][0].Mul(a[2][1]).Sub(a[1][1].Mul(a[2][0])),
},
// other columns after determinant...
}
det := a[0][0].Mul(adj[0][0]).Add(a[0][1].Mul(adj[1][0])).Add(a[0][2].Mul(adj[2][0]))
if det.N == 0 {
return RatMatrix3{}, errors.New("matrix is singular")
}
adj[0][0] = adj[0][0].Div(det)
adj[1][0] = adj[1][0].Div(det)
adj[2][0] = adj[2][0].Div(det)
adj[0][1] = a[0][1].Mul(a[2][2]).Sub(a[0][2].Mul(a[2][1])).Neg().Div(det)
adj[0][2] = a[0][1].Mul(a[1][2]).Sub(a[0][2].Mul(a[1][1])).Div(det)
adj[1][1] = a[0][0].Mul(a[2][2]).Sub(a[0][2].Mul(a[2][0])).Div(det)
adj[1][2] = a[0][0].Mul(a[1][2]).Sub(a[0][2].Mul(a[1][0])).Neg().Div(det)
adj[2][1] = a[0][0].Mul(a[2][1]).Sub(a[0][1].Mul(a[2][0])).Neg().Div(det)
adj[2][2] = a[0][0].Mul(a[1][1]).Sub(a[0][1].Mul(a[1][0])).Div(det)
return adj, nil
}

View file

@ -2,7 +2,9 @@ package engine
import ( import (
"encoding/gob" "encoding/gob"
"fmt"
"image" "image"
"log"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -28,31 +30,57 @@ func init() {
gob.Register(&Prism{}) gob.Register(&Prism{})
} }
// PrismMap // PrismMap is a generalised tilemap/wallmap/etc.
type PrismMap struct { type PrismMap struct {
ID ID
Disabled Disabled
Hidden Hidden
Ersatz bool
Map map[Int3]*Prism // pos -> prism Map map[Int3]*Prism // pos -> prism
DrawOrderBias image.Point // dot with pos.XY() = bias value DrawOrderBias image.Point // dot with pos.XY() = bias value
DrawOffset image.Point // offset applies to whole map DrawOffset image.Point // offset applies to whole map
PosToWorld IntMatrix3x4 // p.pos -> voxelspace PosToWorld IntMatrix3x4 // p.pos -> world voxelspace
PrismSize Int3 // in voxelspace PrismSize Int3 // in world voxelspace units
Sheet Sheet Sheet Sheet
game *Game game *Game
pwinverse RatMatrix3
} }
func (m *PrismMap) CollidesWith(b Box) bool { func (m *PrismMap) CollidesWith(b Box) bool {
// Back corner of a prism p is: if m.Ersatz {
// m.PrismPos.Apply(p.pos) return false
}
// To find the prisms need to test, we need to invert PosToWorld.
// Step 1: subtract whatever the translation component of PosToWorld is,
// reducing the rest of the problem to the 3x3 submatrix.
b = b.Sub(m.PosToWorld.Translation())
// Step 2: invert the rest of the fucking matrix.
// (Spoilers: I did this already in Prepare)
b.Min = m.pwinverse.IntApply(b.Min)
b.Max = m.pwinverse.IntApply(b.Max.Sub(Int3{1, 1, 1}))
for k := b.Min.Z; k < b.Max.Z; k++ {
for j := b.Min.Y; j < b.Max.Y; j++ {
for i := b.Min.X; i < b.Max.X; i++ {
// TODO: take into account the prism shape...
if _, found := m.Map[Int3{i, j, k}]; found {
return true
}
}
}
}
return false return false
} }
func (m *PrismMap) Prepare(g *Game) error { func (m *PrismMap) Prepare(g *Game) error {
m.game = g m.game = g
pwi, err := m.PosToWorld.ToRatMatrix3().Inverse()
if err != nil {
return fmt.Errorf("inverting PosToWorld: %w", err)
}
m.pwinverse = pwi
log.Printf("inverted PosToWorld: %v", pwi)
for v, p := range m.Map { for v, p := range m.Map {
p.pos = v p.pos = v
p.m = m p.m = m

92
engine/rational.go Normal file
View file

@ -0,0 +1,92 @@
package engine
// Rat is a (small) rational number implementation. Overflow can happen.
type Rat struct{ N, D int }
// IntRat returns the rational representation of n.
func IntRat(n int) Rat { return Rat{N: n, D: 1} }
// Int returns r.N / r.D.
func (r Rat) Int() int { return r.N / r.D }
// Rem returns r.N % r.D.
func (r Rat) Rem() int { return r.N % r.D }
// Canon puts the rational number into reduced form.
func (r Rat) Canon() Rat {
if r.D == 0 {
panic("division by zero")
}
if r.N == 0 {
r.D = 1
return r
}
if r.D < 0 {
r.N, r.D = -r.N, -r.D
}
if d := gcd(abs(r.N), r.D); d > 1 {
r.N /= d
r.D /= d
}
return r
}
// Neg returns -r.
func (r Rat) Neg() Rat {
r.N = -r.N
return r
}
// Add returns r + q.
func (r Rat) Add(q Rat) Rat {
return Rat{
N: r.N*q.D + q.N*r.D,
D: r.D * q.D,
}.Canon()
}
// Sub returns r - q.
func (r Rat) Sub(q Rat) Rat {
return Rat{
N: r.N*q.D - q.N*r.D,
D: r.D * q.D,
}.Canon()
}
// Mul returns r * q.
func (r Rat) Mul(q Rat) Rat {
return Rat{
N: r.N * q.N,
D: r.D * q.D,
}.Canon()
}
// Invert returns 1/r if it exists, otherwise panics.
func (r Rat) Invert() Rat {
return Rat{N: r.D, D: r.N}.Canon()
}
// Div returns r/q.
func (r Rat) Div(q Rat) Rat {
return Rat{
N: r.N * q.D,
D: r.D * q.N,
}.Canon()
}
func abs(n int) int {
if n < 0 {
return -n
}
return n
}
func gcd(a, b int) int {
if a < b {
return gcd(b, a)
}
for b != 0 {
a, b = b, a%b
}
return a
}

View file

@ -140,7 +140,6 @@ func level1() *engine.Scene {
&engine.PrismMap{ &engine.PrismMap{
ID: "hexagons", ID: "hexagons",
DrawOrderBias: image.Pt(0, -1), // draw higher Y after lower Y DrawOrderBias: image.Pt(0, -1), // draw higher Y after lower Y
DrawOffset: image.Pt(-8, 0),
PosToWorld: engine.IntMatrix3x4{ PosToWorld: engine.IntMatrix3x4{
0: [4]int{24, 0, 0, 0}, 0: [4]int{24, 0, 0, 0},
1: [4]int{0, 16, 0, 0}, 1: [4]int{0, 16, 0, 0},
@ -159,9 +158,9 @@ func level1() *engine.Scene {
engine.Pt3(4, -1, 0): {}, engine.Pt3(4, -1, 0): {},
engine.Pt3(4, 0, 0): {}, engine.Pt3(4, 0, 0): {},
engine.Pt3(6, 0, -4): {},
engine.Pt3(6, 0, -3): {},
engine.Pt3(6, 0, -2): {}, engine.Pt3(6, 0, -2): {},
engine.Pt3(6, 0, -1): {},
engine.Pt3(6, 0, 0): {},
}, },
}, },
&engine.Tilemap{ &engine.Tilemap{