Skip to content

Commit

Permalink
major update to eve, with basic box-based collision detection and ren…
Browse files Browse the repository at this point in the history
…aming to support path toward physics-based system working.
  • Loading branch information
Randall C. O'Reilly committed Mar 30, 2020
1 parent 7f5afbe commit 09ec980
Show file tree
Hide file tree
Showing 18 changed files with 594 additions and 144 deletions.
38 changes: 32 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,48 @@

The *Emergent Virtual Engine* (EVE) is a scenegraph-based physics simulator for creating virtual environments for neural network models to grow up in.

Ultimately we hope to figure out how the Bullet simulator works and get that running here, in a clean and simple implementation.
Ultimately we hope to figure out how the [Bullet](https://github.com/bulletphysics/bullet3) system works and get that running here, in a clean and simple implementation.

Incrementally, we will start with a very basic explicitly driven form of physics that is sufficient to get started, and build from there.

The world is made using [GoKi](https://github.com/goki/ki) based trees (groups, bodies, joints).

Rendering can *optionally* be performed using corresponding 3D renders in the `gi3d` 3D rendering framework in the [GoGi](https://github.com/goki/gi) GUI framework, using an `epev.View` object that sync's the two.

We also use the full-featured `gi.mat32` math / matrix library (adapted from the `g3n` 3D game environment package).
We also use the full-featured `goki/mat32` math / matrix library (adapted from the `g3n` 3D game environment package).

# Usual rationalization for reinventing the wheel yet again
# Organizing the World

* Pure *Go* build environment is fast, delightful, simple, clean, easy-to-read, runs fast, etc. In contrast, building other systems typically requires something like Qt or other gui dependencies, and we know what that world is like, and why we left it for Go..
It is most efficient to create a relatively deep tree with `Group` nodes that collect nearby `Body` objects at multiple levels of spatial scale. Bounding Boxes are computed at every level of Group, and pruning is done at every level, so large chunks of the tree can be eliminated easily with this strategy.

* Control, control, control.. we can do what we want in a way that we know will work.
Also, Nodes must be specifically flagged as being `Dynamic` -- otherwise they are assumed to be static -- and each type should be organized into separate top-level Groups (there can be multiple of each, but don't mix Dynamic and Static). Static nodes are never collided against each-other. Ideally, all the Dynamic nodes are in separate top-level, or at least second-to-top level groups -- this eliminates redundant A vs. B and B vs. A collisions and focuses each collision on the most relevant information.

* Physics is easy. Seriously, the basic stuff is just a few vectors and matricies and a few simple equations. Doing the full physics version is considerably more challenging technically but we should be able to figure that out in due course, and really most environments we care about don't really require the full physics anyway.
# Updating Modes

There are two major modes of updating: Scripted, Physics -- scripted requires a program to control what happens on every time step, while physics uses computed forces from contacts, plus joint constraints, to update velocities (not yet supported). The update modes are just about which methods you call.

The `Group` has a set of `World*` methods that should be used on the top-level world Group node node to do all the init and update steps. The update loops automatically exclude non Dynamic nodes.

* `WorldInit` -- everyone calls this at the start to set the initial config

* `WorldRelToAbs` -- for scripted mode when updating relative positions, rotations.

* `WorldStepPhys` -- for either scripted or physics modes, to update from current velocities.

* `WorldCollide` -- returns list of potential collision contacts based on projected motion, focusing on dynamic vs. static and dynamic vs. dynamic bodies, with optimized tree filtering. This is the first pass for collision detection.

## Scripted Mode

For Scripted mode, each update step typically involves manually updating the `Rel.Pos` and `.Quat` fields on `Body` objects to update their relative positions. This field is a `Phys` type and has `MoveOnAxis` and `RotateOnAxis` (and a number of other rotation methods). The Move methods update the `LinVel` field to reflect any delta in movement.

It is also possible to manually set the `Abs.LinVel` and `Abs.AngVel` fields and call `StepPhys` to update.

For collision detection, it is essential to have the `Abs.LinVel` field set to anticipate the effects of motion and determine likely future impacts. The RelToAbs update call does this automatically, and if you're instead using StepPhys the LinVel is already set. Both calls will automatically compute an updated BBox and VelBBox.

It is up to the user to manage the list of potential collisions, e.g., by setting velocity to 0 or bouncing back etc.

## Physics Mode

The good news so far is that the full physics version as in Bullet is actually not too bad. The core update step is a super simple forward Euler, intuitive update (just add velocity to position, with a step size factor). The remaining work is just in computing the forces to update those velocities. Bullet uses a hybrid approach that is clearly described in the [Mirtich thesis](https://people.eecs.berkeley.edu/~jfc/mirtich/thesis/mirtichThesis.pdf), which combines *impulses* with a particular way of handling joints, due originally to Featherstone. Impulses are really simple conceptually: when two objects collide, they bounce back off of each other in proportion to their `Bounce` (coefficient of restitution) factor -- these collision impact forces dominate everything else, and aren't that hard to compute (similar conceptually to the `marbles` example in GoGi). The joint constraint stuff is a bit more complicated but not the worst. Everything can be done incrementally. And the resulting system will avoid the brittle nature of the full constraint-based approach taken in ODE, which caused a lot of crashes and instability in `cemer`.

One of the major problems with the impulse-based approach: that it causes otherwise "still" objects to jiggle around and slip down planes, seems eminently tractable with special-case code that doesn't seem too hard.
2 changes: 1 addition & 1 deletion eve/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ clean:
$(GOCLEAN)

# NOTE: MUST update version number here prior to running 'make release'
VERS=v0.50.0
VERS=v0.5.1
PACKAGE=eve
GIT_COMMIT=`git rev-parse --short HEAD`
VERS_DATE=`date -u +%Y-%m-%d\ %H:%M`
Expand Down
25 changes: 21 additions & 4 deletions eve/bbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import "github.com/goki/mat32"

// BBox contains bounding box and other gross object properties
type BBox struct {
BBox mat32.Box3 `desc:"bounding box in local coords"`
BBox mat32.Box3 `desc:"bounding box in world coords (Axis-Aligned Bounding Box = AABB)"`
VelBBox mat32.Box3 `desc:"velocity-projected bounding box in world coords: extend BBox to include future position of moving bodies -- collision must be made on this basis"`
BSphere mat32.Sphere `desc:"bounding sphere in local coords"`
Area float32 `desc:"area"`
Volume float32 `desc:"volume"`
Expand All @@ -28,9 +29,25 @@ func (bb *BBox) UpdateFmBBox() {
bb.Volume = sz.X * sz.Y * sz.Z
}

// XForm transforms bounds with given quat and position offset
// XForm transforms bounds with given quat and position offset to convert to world coords
func (bb *BBox) XForm(q mat32.Quat, pos mat32.Vec3) {
bb.BBox = bb.BBox.MulQuat(q)
bb.BBox = bb.BBox.Translate(pos)
bb.BBox = bb.BBox.MulQuat(q).Translate(pos)
bb.BSphere.Translate(pos)
}

// VelProject computes the velocity-projected bounding box for given velocity and step size
func (bb *BBox) VelProject(vel mat32.Vec3, step float32) {
eb := bb.BBox.Translate(vel.MulScalar(step))
bb.VelBBox = bb.BBox
bb.VelBBox.ExpandByBox(eb)
}

// VelNilProject is for static items -- just copy the BBox
func (bb *BBox) VelNilProject() {
bb.VelBBox = bb.BBox
}

// IntersectsVelBox returns true if two velocity-projected bounding boxes intersect
func (bb *BBox) IntersectsVelBox(oth *BBox) bool {
return bb.VelBBox.IntersectsBox(oth.VelBBox)
}
15 changes: 12 additions & 3 deletions eve/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ type Body interface {

// AsBodyBase returns the body as a BodyBase
AsBodyBase() *BodyBase

// SetDynamic sets the Dynamic flag for this body, indicating that it moves.
// It is important to collect all dynamic objects into separate top-level group(s)
// for more efficiently organizing the collision detection process.
SetDynamic()
}

// BodyBase is the base type for all specific Body types
type BodyBase struct {
NodeBase
Surf Surface `desc:"surface properties, including friction and bouncyness"`
Vis string `desc:"visualization name -- looks up an entry in the scene library that provides the visual representation of this body"`
Color string `desc:"default color of body for basic InitLibrary configuration"`
Rigid Rigid `desc:"rigid body properties, including mass, bounce, friction etc"`
Vis string `desc:"visualization name -- looks up an entry in the scene library that provides the visual representation of this body"`
Color string `desc:"default color of body for basic InitLibrary configuration"`
}

func (bb *BodyBase) NodeType() NodeTypes {
Expand All @@ -35,3 +40,7 @@ func (bb *BodyBase) AsBodyBase() *BodyBase {
func (bb *BodyBase) GroupBBox() {

}

func (bb *BodyBase) SetDynamic() {
bb.SetFlag(int(Dynamic))
}
18 changes: 13 additions & 5 deletions eve/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Box struct {
var KiT_Box = kit.Types.AddType(&Box{}, BoxProps)

var BoxProps = ki.Props{
"EnumType:Flag": ki.KiT_Flags,
"EnumType:Flag": KiT_NodeFlags,
}

// AddNewBox adds a new box of given name, initial position and size to given parent
Expand All @@ -35,12 +35,20 @@ func (bx *Box) SetBBox() {
bx.BBox.XForm(bx.Abs.Quat, bx.Abs.Pos)
}

func (bx *Box) InitPhys(par *NodeBase) {
bx.InitBase(par)
func (bx *Box) InitAbs(par *NodeBase) {
bx.InitAbsBase(par)
bx.SetBBox()
bx.BBox.VelNilProject()
}

func (bx *Box) UpdatePhys(par *NodeBase) {
bx.UpdateBase(par)
func (bx *Box) RelToAbs(par *NodeBase) {
bx.RelToAbsBase(par)
bx.SetBBox()
bx.BBox.VelProject(bx.Abs.LinVel, 1)
}

func (bx *Box) StepPhys(step float32) {
bx.StepPhysBase(step)
bx.SetBBox()
bx.BBox.VelProject(bx.Abs.LinVel, step)
}
18 changes: 13 additions & 5 deletions eve/capsule.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Capsule struct {
var KiT_Capsule = kit.Types.AddType(&Capsule{}, CapsuleProps)

var CapsuleProps = ki.Props{
"EnumType:Flag": ki.KiT_Flags,
"EnumType:Flag": KiT_NodeFlags,
}

// AddNewCapsule adds a new capsule of given name, initial position
Expand All @@ -43,12 +43,20 @@ func (cp *Capsule) SetBBox() {
cp.BBox.XForm(cp.Abs.Quat, cp.Abs.Pos)
}

func (cp *Capsule) InitPhys(par *NodeBase) {
cp.InitBase(par)
func (cp *Capsule) InitAbs(par *NodeBase) {
cp.InitAbsBase(par)
cp.SetBBox()
cp.BBox.VelNilProject()
}

func (cp *Capsule) UpdatePhys(par *NodeBase) {
cp.UpdateBase(par)
func (cp *Capsule) RelToAbs(par *NodeBase) {
cp.RelToAbsBase(par)
cp.SetBBox()
cp.BBox.VelProject(cp.Abs.LinVel, 1)
}

func (cp *Capsule) StepPhys(step float32) {
cp.StepPhysBase(step)
cp.SetBBox()
cp.BBox.VelProject(cp.Abs.LinVel, step)
}
72 changes: 72 additions & 0 deletions eve/collide.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2019, The Emergent Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package eve

import (
"github.com/goki/ki/ki"
"github.com/goki/mat32"
)

// Contact is one pairwise point of contact between two bodies.
// Contacts are represented in spherical terms relative to the
// spherical BBox of A and B.
type Contact struct {
A Body `desc:"one body"`
B Body `desc:"the other body"`
NormB mat32.Vec3 `desc:"normal pointing from center of B to center of A"`
PtB mat32.Vec3 `desc:"point on spherical shell of B where A is contacting"`
Dist float32 `desc:"distance from PtB along NormB to contact point on spherical shell of A"`
}

// UpdtDist updates the distance information for the contact
func (c *Contact) UpdtDist() {

}

// Contacts is a slice list of contacts
type Contacts []*Contact

// AddNew adds a new contact to the list
func (cs *Contacts) AddNew(a, b Body) *Contact {
c := &Contact{A: a, B: b}
*cs = append(*cs, c)
return c
}

// BodyVelBBoxIntersects returns the list of potential contact nodes between a and b
// (could be the same or different groups) that have intersecting velocity-projected
// bounding boxes. In general a should be dynamic bodies and b either dynamic or static.
// This is the broad first-pass filtering.
func BodyVelBBoxIntersects(a, b Node) Contacts {
var cts Contacts
a.FuncDownMeFirst(0, a.This(), func(k ki.Ki, level int, d interface{}) bool {
aii, ai := KiToNode(k)
if aii == nil {
return false // going into a different type of thing, bail
}
if aii.NodeType() != BODY {
return true
}
abod := aii.AsBody() // only consider bodies from a

b.FuncDownMeFirst(0, b.This(), func(k ki.Ki, level int, d interface{}) bool {
bii, bi := KiToNode(k)
if bii == nil {
return false // going into a different type of thing, bail
}
if !ai.BBox.IntersectsVelBox(&bi.BBox) {
return false // done
}
if bii.NodeType() == BODY {
cts.AddNew(abod, bii.AsBody())
return false // done
}
return true // keep going
})

return false
})
return cts
}
18 changes: 13 additions & 5 deletions eve/cylinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Cylinder struct {
var KiT_Cylinder = kit.Types.AddType(&Cylinder{}, CylinderProps)

var CylinderProps = ki.Props{
"EnumType:Flag": ki.KiT_Flags,
"EnumType:Flag": KiT_NodeFlags,
}

// AddNewCylinder adds a new cylinder of given name, initial position
Expand Down Expand Up @@ -53,12 +53,20 @@ func (cy *Cylinder) SetBBox() {
cy.BBox.XForm(cy.Abs.Quat, cy.Abs.Pos)
}

func (cy *Cylinder) InitPhys(par *NodeBase) {
cy.InitBase(par)
func (cy *Cylinder) InitAbs(par *NodeBase) {
cy.InitAbsBase(par)
cy.SetBBox()
cy.BBox.VelNilProject()
}

func (cy *Cylinder) UpdatePhys(par *NodeBase) {
cy.UpdateBase(par)
func (cy *Cylinder) RelToAbs(par *NodeBase) {
cy.RelToAbsBase(par)
cy.SetBBox()
cy.BBox.VelProject(cy.Abs.LinVel, 1)
}

func (cy *Cylinder) StepPhys(step float32) {
cy.StepPhysBase(step)
cy.SetBBox()
cy.BBox.VelProject(cy.Abs.LinVel, step)
}
Loading

0 comments on commit 09ec980

Please sign in to comment.