2015-10-18 20:58:56

I decided I should try harder to get good at using ODE for 3D physics, and log how it goes, in case anyone can find anything useful in it.

In practice, I just went for 2 hours and tapped out, and most of my comments weren't helpful so much as whiny and snarky. Mostly in the form of "But that takes too much typing!"

I thought I'd go ahead and post what I have anyway. I'm hoping the snippets I quote from the pyODE tutorial aren't violating a license or something.

It's about 23KB in length, so I'll break it up into a post for each timestamped section.
Hopefully, someone gets something out of this. Who knows; maybe I'll pick it up again, or someone will make something both accessible and physicsy, or I dunno.

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-10-18 20:59:12

3:20 PM 10/5/2015

I've looked into ODE before, but could never really get my attention span to stay still long enough to make anything with it.
I'll try again, and hopefully someone can learn something from this.

First of all, I'm using Pyode. I have no idea which version I have installed; I got it last year, and also installed VPython, but also-also installed Visual Pyode, which tries to combine the two (losing some unique elements of both in the process). The best I can suggest is to check out http://pyode.sourceforge.net , since that seems to be where all the most important bits are.

So, here's the first pyode tutorial-example. Let's take a look, shall we?

http://pyode.sourceforge.net/tutorials/tutorial1.html

First thing's first: vectors are given as tooples with length 3. The example opens like so:

# pyODE example 1: Getting started
    
import ode

# Create a world object
world = ode.World()
world.setGravity( (0,-9.81,0) )

So far so simple. What's next?

# Create a body inside the world
body = ode.Body(world)
M = ode.Mass()
M.setSphere(2500.0, 0.05)
M.mass = 1.0
body.setMass(M)

body.setPosition( (0,2,0) )
body.addForce( (0,200,0) )

When the best I can say about this bit is that it's not as ridiculously convoluted as Java3D, and it at least makes intuitive sense, that isn't really a good sign.
To make matters more frustrating, that first line:
body = ode.Body(world)
Takes the world as a parameter! Why? Why does it take the world as a parameter?
Can't we create bodies outside of the world? If we're creating a body in the world, wouldn't it be better if the world had a method for creating bodies?
I suppose it is a constructor. It's easy to miss those when you've spent the past 10 years avoiding object definitions with dots in them.
OK, OK, this isn't too bad.
But, still, we need to simplify it. Seven lines to create a sphere is six lines too many.

The thing is, geometry and physics are separate. That's great and all, but I like thinking in terms of real-world objects, where they are not, in fact, separate.
So, let's try to wrap these in a simple factory function.
Oh, and I can never keep track of how enums work, since no two languages seem to do them the same, so for this utility module, I'll just use constants defined in an iterable.

So I guess I'm calling this ou.py. ou for "ode utilities". If there is already a popular ou.py out there, I guess I could prepend a jp or something.

import ode, math

"""
Utility functions for common ode operations.
"""



# Shape constants. We'll add to these later.
SPHERE=(1, )

# We use shape constants to avoid writing functions for every single object type.


def new_body(world, type, position, size, density=-1, mass=-1,) :
    if(density<0 and mass<0) : density=1.0
    elif(density<0) :
        if(type==SPHERE) : density=mass/((4/3)*(size**3)*math.pi)
    # adjust the density to fit the mass and volume.
    
    body = ode.Body(world)
    M = ode.Mass()
    if(type==SPHERE) : M.setSphere(density, size)
    M.mass = mass # Wait, isn't this kinda redundant? Why did the original example have both of these? I'm confused!
    body.setMass(M)

    body.setPosition( position )
    
    return body
# Shape generator.

There, now we can make spheres using that function.

body=ou.new_body(world, ou.SPHERE, position=(0,2,0), size=0.05, density=2500.0, mass=1.0)

You don't have to specify the parameter names like that. I just do it so I don't have to remember the order.

Anyway, guess I should compile and debug that little thing I just wrote.
[...]
Yay, no obvious typos or problems. Though putting "ou." in front of everything is kinda annoying, so I'll probably just go with "from ou import *" from now on.

Continuing with the tutorial!


# Do the simulation...
total_time = 0.0
dt = 0.04
while total_time<2.0:
    x,y,z = body.getPosition()
    u,v,w = body.getLinearVel()
    print "%1.2fsec: pos=(%6.3f, %6.3f, %6.3f)  vel=(%6.3f, %6.3f, %6.3f)" % \
          (total_time, x, y, z, u,v,w)
    world.step(dt)
    total_time+=dt

OK, other than that print statement, this is exactly what I hoped it would be. And I have problems with most print-like calls that are weirder than print (string), even though everyone else ever seems to require the fancy versions in the vein of printf.

Anyway, I've got that tutorial somewhere, so let's run it and see what happens.


Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win
32
Type "help", "copyright", "credits" or "license" for more information.
>>> import tutorial1
1.04sec: pos=( 0.000,  4.811,  0.000)  vel=( 0.000, -2.202,  0.000)
1.08sec: pos=( 0.000,  4.707,  0.000)  vel=( 0.000, -2.595,  0.000)
1.12sec: pos=( 0.000,  4.587,  0.000)  vel=( 0.000, -2.987,  0.000)
1.16sec: pos=( 0.000,  4.452,  0.000)  vel=( 0.000, -3.380,  0.000)
1.20sec: pos=( 0.000,  4.301,  0.000)  vel=( 0.000, -3.772,  0.000)
1.24sec: pos=( 0.000,  4.135,  0.000)  vel=( 0.000, -4.164,  0.000)
1.28sec: pos=( 0.000,  3.953,  0.000)  vel=( 0.000, -4.557,  0.000)
1.32sec: pos=( 0.000,  3.755,  0.000)  vel=( 0.000, -4.949,  0.000)
1.36sec: pos=( 0.000,  3.541,  0.000)  vel=( 0.000, -5.342,  0.000)
1.40sec: pos=( 0.000,  3.312,  0.000)  vel=( 0.000, -5.734,  0.000)
1.44sec: pos=( 0.000,  3.066,  0.000)  vel=( 0.000, -6.126,  0.000)
1.48sec: pos=( 0.000,  2.806,  0.000)  vel=( 0.000, -6.519,  0.000)
1.52sec: pos=( 0.000,  2.529,  0.000)  vel=( 0.000, -6.911,  0.000)
1.56sec: pos=( 0.000,  2.237,  0.000)  vel=( 0.000, -7.304,  0.000)
1.60sec: pos=( 0.000,  1.929,  0.000)  vel=( 0.000, -7.696,  0.000)
1.64sec: pos=( 0.000,  1.606,  0.000)  vel=( 0.000, -8.088,  0.000)
1.68sec: pos=( 0.000,  1.267,  0.000)  vel=( 0.000, -8.481,  0.000)
1.72sec: pos=( 0.000,  0.912,  0.000)  vel=( 0.000, -8.873,  0.000)
1.76sec: pos=( 0.000,  0.541,  0.000)  vel=( 0.000, -9.266,  0.000)
1.80sec: pos=( 0.000,  0.155,  0.000)  vel=( 0.000, -9.658,  0.000)
1.84sec: pos=( 0.000, -0.247,  0.000)  vel=( 0.000, -10.050,  0.000)
1.88sec: pos=( 0.000, -0.665,  0.000)  vel=( 0.000, -10.443,  0.000)
1.92sec: pos=( 0.000, -1.098,  0.000)  vel=( 0.000, -10.835,  0.000)
1.96sec: pos=( 0.000, -1.548,  0.000)  vel=( 0.000, -11.228,  0.000)
>>>

Well, the first second went off screen faster than I could copy it, but you get the idea.

Also, there is clearly no bottom to this world. See how pose goes below 0 in the last 0.2 seconds?

Well, generator function aside, that doesn't seem too bad.
...
If all we're doing is throwing balls around.
Just as a hint as to the sort of torment a decent-looking game would involve, you are welcome to google "pyode ragdoll tutorial". Keep in mind, that ragdoll is still just made of capsules, and at best would look like a mannequin. And it doesn't have any muscle strength or friction keeping it from immediately collapsing once the simulation starts.

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-10-18 21:00:25

4:01 PM 10/5/2015:

OK, so, we can throw balls around, and figure out their positions and velocities. This is sufficient to make audio examples, but it doesn't give us collision detection or boundaries or anything more interesting than balls in space.
And I'm guessing ode is not designed for simulating planetary motion, so really we're just tossing balls into oblivion and watching them fall. Forever.



There are some useful bits from the first tutorial I didn't mention. Specifically, there are some methods of the mass object worth examining:

|      setBox(density, lx, ly, lz)
|
|      Set the mass parameters to represent a box of the given
|      dimensions and density, with the center of mass at (0,0,0)
|      relative to the body. The side lengths of the box along the x,
|      y and z axes are lx, ly and lz.

I can only hope that the position variable is the center for every type of object. Not because I can guess where the center of mass would be on, say, a cone, but because then I don't have to ask.


|  setBoxTotal(...)
|      setBoxTotal(total_mass, lx, ly, lz)
|
|      Set the mass parameters to represent a box of the given
|      dimensions and mass, with the center of mass at (0,0,0)
|      relative to the body. The side lengths of the box along the x,
|      y and z axes are lx, ly and lz.

^ same thing, but you're using mass instead of density.

|  setCapsule(...)
|      setCapsule(density, direction, radius, length)
|
|      Set the mass parameters to represent a capsule of the given parameters
|      and density, with the center of mass at (0,0,0) relative to the body.
|      The radius of the cylinder (and the spherical cap) is radius. The length
|      of the cylinder (not counting the spherical cap) is length. The
|      cylinder's long axis is oriented along the body's x, y or z axis
|      according to the value of direction (1=x, 2=y, 3=z). The first function
|      accepts the density of the object, the second accepts its total mass.

For reference, that means, from tip to tip, a capsule is length+2*radius.
That these are axis-aligned is a little perplexing. DO they rotate? Will python complain if I try to store (radius, length, axis) in a toople for purposes of the generator function?


|      setCylinder(density, direction, r, h)
|
|      Set the mass parameters to represent a flat-ended cylinder of
|      the given parameters and density, with the center of mass at
|      (0,0,0) relative to the body. The radius of the cylinder is r.
|      The length of the cylinder is h. The cylinder's long axis is
|      oriented along the body's x, y or z axis according to the value
|      of direction (1=x, 2=y, 3=z).

Well, this is exactly the same as setCapsule, right?

Let's go ahead and update the new_body function, shall we?
This requires 3 separate sections: adding the type constants, adjusting the density catching (although maybe using setXTotal would have been a better workaround?), and adding the M.setX() bits.

Oh, dear; it seems I neglected to do something about the case where mass is an illegal value, too.

So, ou.py currently looks like this:

import ode, math

"""
Utility functions for common ode operations.
"""



# Shape constants. We'll add to these later.
SPHERE, BOX, CYLINDER, CAPSULE=(1, 2, 3, 4)

# We use shape constants to avoid writing functions for every single object type.


def new_body(world, type, position, size, density=-1, mass=-1,) :
    if(density<0 and mass<0) : density=1.0
    elif(density<0) :
        if(type==SPHERE) : density=mass/((4/3)*(size**3)*math.pi)
        elif type==BOX : density=mass/(size[0]*size[1]*size[2])
        elif type==CYLINDER : density=mass/(size[1]*size[1]*math.pi*size[2])
        elif type==CAPSULE : density=mass/( (size[1]*size[1]*math.pi*size[2]) + ((4.0/3.0)*(size[1]**3)*math.pi))
    # adjust the density to fit the mass and volume.
    
    body = ode.Body(world)
    M = ode.Mass()
    
    if(type==SPHERE) : M.setSphere(density, size)
    elif type==BOX : M.setBox(density, size[0], size[1], size[2])
    elif type==CAPSULE : M.setCapsule(density, size[0], size[1], size[2])
    elif type==CYLINDER : M.setCylinder(density, size[0], size[1], size[2])
    
    if(mass>0) : M.mass = mass # Wait, isn't this kinda redundant? Why did the original example have both of these? I'm confused!
    body.setMass(M)

    body.setPosition( position )
    
    return body
# Shape generator.

Although, I think remembering the numbers for axes might be a bit annoying, so let's add this after the shape constants:

XAXIS, YAXIS, ZAXIS=(1, 2, 3)


Back to the tutorial...

We get this nice table of materials and their densities, which will probably be great for referencing later:

Material
Density (kg/m^3)
Balsa wood
120
Brick
2000
Copper
8900
Cork
250
Diamond
3300
Glass
2500
Gold
19300
Iron
7900
Lead
11300
Styrofoam
100

I think water is ~100 on this scale, although that seems kinda odd to me as I seem to remember it being 1.0, but if it was 1.0 styrofoam wouldn't float, so, meh.
I don't have any idea if ode has boyancy or other fluid dynamics simulations, but at this point, I don't think figuring out how to deal with swimming and floating are quite so urgent.



Before we move on to tutorial 2, tutorial 1 has this useful bit of advice:

If you have to simulate more complex objects you basically have two options:

  • Approximate the complex object by composing several simple ones. In this case you can add the individual masses (you can actually use the + operator which will call the ODE dMassAdd() function internally).

  • Calculate the properties yourself and set them with Mass.setParameters(). If you're representing your objects as a polyhedron, then you might find the following paper helpful: Brian Mirtich Fast and Accurate Computation of Polyhedral Mass Properties,journal of graphics tools, volume 1, number 2, 1996

I especially point you to the ability to add masses with the plus operator! I think that's awesome; don't you think that's awesome?
Of course, I have no idea how that handles things like distribution and density in a complex object. I mean, if we try to make a capsule by adding two spheres to a cylinder, what is the resulting mass?
... Wait, we can test that!


>>> import ode
>>> can=ode.Mass()
>>> can.setCylinder(2500.0, 1, 0.05, 1.0)
>>> ball=ode.Mass()
>>> ball.setSphere(2500.0, 0.05)
>>> ball.translate((0.5,0,0))
>>> m=can+ball
>>> m.mass
22.252946853637695
>>> can.mass
22.252946853637695
>>> ball.mass
1.3089970350265503

(Hint: I made liberal use of help(ode.Mass), or rather, help(m) when writing this. I just didn't copy the parts where I screwed up and had to check.)

Somehow, the mass of the ball is not being considered at all in m. Let's see what happens if I try it the other way around?

>>> m=ball+can
>>> m.mass
23.56194305419922

Oh, my. That is troubling.
Wait; we didn't see what happens in the space where they overlap. Eh, that's not too hard:

>>> m.mass-ball.mass
0.0

(If I were doing this in Microsoft Word, I'd probably set a macro for those BB tags.)

So, I can only hope that the distribution of the mass is not quite perfect. But, eh, what does it matter, anyway?

I mean, I checked, and those 4 are the only basic shapes in the mass class. You can design more complex shapes, if you understand the 3x3 matrix that it uses internally. But I don't, so there.


Ah, wait! Disaster!
I decided to check the weird communitive property violation up there, and this happened:

>>> m=can+ball
>>> m.mass
22.252946853637695
>>> can.mass
22.252946853637695
>>> ball.mass
1.3089970350265503
>>> m=ball+can
>>> m.mass
23.56194305419922
>>> m.mass-ball.mass
0.0
>>> (can+ball).mass-(ball+can).mass
-23.56194305419922
>>> (can+ball).mass-(ball+can).mass
-69.37682342529297
>>> (can+ball).mass-(ball+can).mass
-184.56854248046875
>>> (can+ball).mass-(ball+can).mass
-484.32879638671875
>>> (can+ball).mass-(ball+can).mass
-1268.41796875

It seems that those additions are permanently altering the original masses. Oops.
OK, be careful when adding masses. Got it.

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-10-18 21:01:23

4:55 PM 10/5/2015:

Tutorial 2!
I really want to get past this tutorial crap and into actually making a game I can testplay--and add to--as quickly as possible. If the transition from engine to testing, or from engine testing to game content , involves too big a change in mental gears, projects tend to die in their tracks. And since I can't just go grab ritalin and pretend it changes either of those things, I'ma try and breeze through this as quickly as possible.

Except this is where things get both necessary and complicated.


This tutorial uses pygame for graphics and junk. But since that is pretty useless, maybe we should modify it to attach a sound to the moving parts?
http://pyode.sourceforge.net/tutorials/tutorial2.html

# pyODE example 2: Connecting bodies with joints

import pygame
from pygame.locals import *
import ode


def coord(x,y):
    "Convert world coordinates to pixel coordinates."
    return 320+170*x, 400-170*y


# Initialize pygame
pygame.init()

# Open a display
srf = pygame.display.set_mode((640,480))

Oh, this is going to be fun, I can already tell.


# Create a world object
world = ode.World()
world.setGravity((0,-9.81,0))

# Create two bodies
body1 = ode.Body(world)
M = ode.Mass()
M.setSphere(2500, 0.05)
body1.setMass(M)
body1.setPosition((1,2,0))

body2 = ode.Body(world)
M = ode.Mass()
M.setSphere(2500, 0.05)
body2.setMass(M)
body2.setPosition((2,2,0))

Hey, check it out; 5 lines per ball. Five lines to create one tiny ball. A game will need a few more masses than that. Generally speaking, no one is going to bother building on such an involved, tedious, repetitive level such as this. Is this some kinda test? "Write your own generator functions, or waste your own time. Only the worthy may use this library."?
Well, luckily, we wrote our own generator function the first time we ran into this overly elaborate deathtrap, so let's see if we can translate those last 10 lines into 2 lines.

body1=ou.new_body(world, ou.SPHERE, position=(1,2,0), size=(0.05), density=2500.0)
body2=ou.new_body(world, ou.SPHERE, (2,2,0), 0.05, 2500.0)

Hey, I wonder if we can change this up so we can drop the world parameter? It's not like we're going to need multiple worlds, most of the time.


Well, here's some more stuff that is a little harder to wrap in convenience functions:

# Connect body1 with the static environment
j1 = ode.BallJoint(world)
j1.attach(body1, ode.environment)
j1.setAnchor( (0,2,0) )

# Connect body2 with body1
j2 = ode.BallJoint(world)
j2.attach(body1, body2)
j2.setAnchor( (1,2,0) )

I am having a hard time guessing what the anchor bit is supposed to mean. Does that prevent the joints from being affected by gravity? Hmm.
Anyway, these aren't that bad.
The simulation loop runs exactly as you would expect, expect for the part where the shapes are drawn using pygame.

Oh, and the background is white for some reason. That's not important; I just noticed it and kinda went "Ow! At least I don't have to see that!"


The author of the tutorial emphasizes that it is important to position the bodies before connecting them with joints. This makes sense to me, though I am wondering what would happen if you forceably changed their positions afterward.

So, it seems like the anchor actually... umm... I'm confused.
The first joint by itself forms a pendulum. Notice that it is anchored to (0, 2, 0), which is 2m above the x axis. However, body1 is at (1, 2, 0).
So, apparently, the anchor is a point with no body or mass. I wonder what happens if we don't set an anchor? The joint takes two parameters when adding bodies. In this case, we used the environment (not attached to the world, even) as the second body.

Now things get even more confusing: the second joint connects two bodies, which is exactly how I would expect a joint to work... but it also sets an anchor, at the center of the first body.
Se, I would have expected the anchor to substitute for a body in the joint, and only be used if we need to fix the joint to the environment.
With two bodies and an anchor, I no longer have any idea what's going on. Does it mean that the joint is fixed to (1, 2, 0), and will swing around that point forever? But if that's the case, won't it prevent the first pendulum from swinging, since the body in that joint is attached to the other joint, which is anchored at its center? Gah!

If you run the program you see a swinging double pendulum.

Umm.

...

I could change this to print the positions of the two bodies, but I'm not sure that would help it make any more sense to me. Does the anchor move because it is inside the first body? Why call it an anchor, then? Is a double pendulum a special sort of thing that no one ever told me about? But wouldn't an anchor imply that the bodies can't move more than their original distance away from the anchor? So how can body1 move at all?

I HAVE NO IDEA WHAT'S GOING ON!

Oh, and apparently ball joints don't support motors, which is weird and annoying since motors are the only obvious way to simulate muscular forces and humanoids have a few important ball joints.

And then things got even weirder, since motors dispense with having intuitively named methods or properties, and uses this instead:

j1.setParam(ode.ParamVel, 3)
j1.setParam(ode.ParamFMax, 22)

I mean, I know what those mean, because vel is short for velocity, and param is short for parameter, and that last one has something to do with maximum force. I'd still have to see it in action to understand it for sure, but that's not really the point.

The point is that my takeaway from this tutorial is a big mess:

  • Ball joints and hinge joints have a nice attach method that takes two bodies, or the ode.ENVIRONMENT constant as parameters. These look like they'd attach the two bodies via the joint, so that's nice.

  • Joints have a setAnchor method, and I have no idea what it does or how it interacts with bodies, or how bodies interact with multiple joints.

  • Ball Joints don't support motors, so if you're building a shoulder, you'll need a separate joint for the pecs and for the deltoid.

  • I'm expected to either use 5 lines to generate a single body, and use the setParam method to build motors, or build my own workarounds. In either case, I'm expected to be either needlessly conscientious, or needlessly familiar with enough minute details of how ode works that I shouldn't need a tutorial in the first place.

But wait, there's more!

Tutorial 3 uses openGL, which is kinda like the C of graphics programming, but everyone uses it anyway, because everyone hates me.
Skipping past the dozens of lines of initialization and conversion functions just to get OpenGL up and running, we finally come to the meat of the tutorial, which is about collision detection.
Collision detection uses joints, for some reason.

They finally decided to make a function for generating shapes, and it adds even more confusion.

def create_box(world, space, density, lx, ly, lz):
    """Create a box body and its corresponding geom."""

    # Create body
    body = ode.Body(world)
    M = ode.Mass()
    M.setBox(density, lx, ly, lz)
    body.setMass(M)

    # Set parameters for drawing the body
    body.shape = "box"
    body.boxsize = (lx, ly, lz)

    # Create a box geom for collision detection
    geom = ode.GeomBox(space, lengths=body.boxsize)
    geom.setBody(body)

    return body, geom

Because requiring 3 separate objects to create a single box wasn't bad enough, we now care about geometry. Before, it was just mass distribution.
Well, ok, then. Guess we need to add this to ou.new_body, somehow.

Except the anchors and openGL are exhausting. I'ma go do something else first.

5:36 PM 10/5/2015.

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-10-18 21:10:38

So far so good.  You have more patience than I do, my friend!  big_smile  This is a lot to go over.

- Aprone
Please try out my games and programs:
Aprone's software

2015-10-18 21:29:15

I feel like I'm kinda stuck at the end, there. I really need to understand how joints, collisions, and motors work to do anything more interesting than 3D Pong. I feel like Box2D has a much more intuitive way of handling these, even if it can occasionally get cumbersome. And, as we know, the only thing I ever got Box2D to do was drop a ball on a rectangle, then slap the ball into oblivion.
I could try and wrap things to be easier to work with, but I can't wrap what I don't understand.

(Also, a crude-but-obvious work-around for collision callbacks, if someone wanted to torment themselves with a BGT-compatible DLL: have the wrapper log everything in its callbacks, then have functions that BGT could use to retrieve the logged data. This doesn't help with the fact that BGT can only get and send primitives and sometimes arrays via DLLs, but eh.)

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-12-09 17:52:34

(Oh, hey, there was more that I never posted. Probably because I wanted a small test program to go with it, and never followed up on that.)

2:34 PM 10/19/2015:

I typed "I do not understand ODE joints" into Google, and lo and behold: the official ODE user guide: http://www.ode.org/ode-latest-userguide.html

Let's see if this helps in the slightest, shall we?

OK, first, let's decide to make something to use as a test application. Preferably something short. Preferably-preferably something I can complete in the next 10 or so days. So a Halloween theme would be neat.
So, OK, let's take the easy way out and model pumpkins as spheres, even though they are not spheres and are structured such that they don't actually roll all that easily.
And we can build people like Snowmen or something: upper body, lower body, joint that connects them.
I'd still like cones or conics, because how else would you model a stereotypical witch, other than with a cone on top of a sphere on top of a capsule merged with a cone? (Apparently, this game will not include people with arms.)
But that's irrelevant. Let's make sure we understand the engine well enough to actually use it.


An excerpt from the features list:

• Joint types: ball-and-socket, hinge, slider (prismatic), hinge-2, fixed, angular motor, universal.

I don't really know what the last four are, but I can sorta try to guess (is a Fixed joint just sticking two objects together to make a new object? Say cone + sphere = icecream cone? Also, why doesn't ODE have cones? Cones aren't that hard, are they?)

This is less restrictive than it sounds:

• Collision primitives: sphere, box, capped cylinder, plane, ray, and triangular mesh.

The ray is the important part. Raycasting is one of those things that comes up a lot in mainstream game development discussions, but for audio games, that's what we'd want to use to create a Swamp-style radar.
Meshes are complicated, if I remember correctly. Let's ignore them until I figure out how to learn math from the internet.


Friction:

• Contact and friction model: This is based on the Dantzig LCP solver described by Baraff, although ODE implements a faster approximation to the Coloumb friction model.

That would sound nice, if I hadn't left off on "contact joints? Anchors? I'm confused!".

• Has a native C interface (even though ODE is mostly written in C++).

This is more helpful than it sounds. I think. I'm not really sure.


Random side note: Springs and sponges. Would be great if I had any idea how to influence ERP and CFM. They're just kinda mentioned without any explanation as to ... ah, anyway:

By adjusting the values of ERP and CFM, you can achieve various effects. For example you can simulate springy constraints, where the two bodies oscillate as though connected by springs. Or you can simulate more spongy constraints, without the oscillation. In fact, ERP and CFM can be selected to have the same effect as any desired spring and damper constants. If you have a spring constant kp and damping constant kd, then the corresponding ODE constants are:
ERP = h kp / (h kp + kd)
CFM = 1 / (h kp + kd)
where h is the stepsize. These values will give the same effect as a spring-and-damper system simulated with implicit first order integration.

I have no idea what stepsize is--is that dt in our simulation stepping?
Anyway, oscilators is about where gen physics lost me, which is weird because they always look really simple and are quite useful. So, let's put this on the "for later" burner.


The important part - collision detection:

Collisions between bodies or between bodies and the static environment are handled as follows:
1. Before each simulation step, the user calls collision detection functions to determine what is touching what. These functions return a list of contact points. Each contact point specifies a position in space, a surface normal vector, and a penetration depth.
2. A special contact joint is created for each contact point. The contact joint is given extra information about the contact, for example the friction present at the contact surface, how bouncy or soft it is, and various other properties.
3. The contact joints are put in a joint "group", which allows them to be added to and removed from the system very quickly. The simulation speed goes down as the number of contacts goes up, so various strategies can be used to limit the number of contact points.
4. A simulation step is taken.
5. All contact joints are removed from the system.
Note that the built-in collision functions do not have to be used - other collision detection libraries can be used as long as they provide the right kinds of contact point information.


Ok. I think.

This looks about like I'd expect, too:

A typical simulation will proceed like this:
1. Create a dynamics world.
2. Create bodies in the dynamics world.
3. Set the state (position etc) of all bodies.
4. Create joints in the dynamics world.
5. Attach the joints to the bodies.
6. Set the parameters of all joints.
7. Create a collision world and collision geometry objects, as necessary.
8. Create a joint group to hold the contact joints.
9. Loop:
1. Apply forces to the bodies as necessary.
2. Adjust the joint parameters as necessary.
3. Call collision detection.
4. Create a contact joint for every collision point, and put it in the contact joint group.
5. Take a simulation step.
6. Remove all joints in the contact joint group.
10. Destroy the dynamics and collision worlds.

Paradigm issues: if we associate a body with a non-ODE object, such as a character or item class, are we going to run into confusion when trying to handle collisions and movement? I'm really not sure. Presumably, there's a way to attach information to bodies, so we can keep track of what non-ODE objects they're associated with? I think Box2D had something like this.

(Reading through the section on joints, they, at least, have user data pointers. At least, in the C/C++ version.)


Now, the part about anchors and parameters and junk. Will it clear up anything? Stay tuned to find out!

void dJointSetBallAnchor (dJointID, dReal x, dReal y, dReal z);
Set the joint anchor point. The joint will try to keep this point on each body together. The input is specified in world coordinates.

This makes sense in a ball joint, I guess.
Waaait... what sort of joints were we using before?
It was ball joints! Aaargh!
Ok, ok, let's try and think...
A ball joint... takes...
A ball, and a socket.
So... we're... umm...
... Oh, look, more information:

void dJointGetBallAnchor (dJointID, dVector3 result);
Get the joint anchor point, in world coordinates. This returns the point on body 1. If the joint is perfectly satisfied, this will be the same as the point on body 2.
void dJointGetBallAnchor2 (dJointID, dVector3 result);
Get the joint anchor point, in world coordinates. This returns the point on body 2. You can think of a ball and socket joint as trying to keep the result of dJointGetBallAnchor() and dJointGetBallAnchor2() the same. If the joint is perfectly satisfied, this function will return the same value as dJointGetBallAnchor to within roundoff errors. dJointGetBallAnchor2 can be used, along with dJointGetBallAnchor, to see how far the joint has come apart.

Yay! I mean, sure, the fact that joints can come apart if you don't correct them is problematic, but finally this is trying to make sense!

So, let's revisit tutorial 2.

# Connect body1 with the static environment
j1 = ode.BallJoint(world)
j1.attach(body1, ode.environment)
j1.setAnchor( (0,2,0) )

# Connect body2 with body1
j2 = ode.BallJoint(world)
j2.attach(body1, body2)
j2.setAnchor( (1,2,0) )

If you'll recall, body 1 was a ball at (1, 2, 0) and body2 was a ball at (2, 2, 0). They both have radii of 0.05, which means they do not overlap.

So, j1 is a ball joint attached to the static environment, anchored to (0, 2, 0).
j2 is a ball joint connecting body1 and body2, anchored to (1, 2, 0).
I can sort of understand the idea when we're just working with j1. If it means that body1 can't move any further than its initial distance from the anchor, or something like that, then we have a pendulum.
But when we have body1 in two joints, with different anchors... it still seems like body1 shouldn't move at all.
Now, I haven't actually run this example with an accessible output method to figure out what actually happens. But, from the descriptions, I rather doubt it's just body1 swinging from body2. Also, the drawing section (which I omitted somewhy) will make it pretty clear what to expect: if it draws two circles and maybe one line, then we expect only body2 to move. If it draws three circles and maybe two lines, both are moving, making the anchors weird and possibly very loose.

Checking, and, weirdly enough, it only draws two circles, but also draws two lines. Guh.

    # Clear the screen
    srf.fill((255,255,255))

    # Draw the two bodies
    x1,y1,z1 = body1.getPosition()
    x2,y2,z2 = body2.getPosition()
    pygame.draw.circle(srf, (55,0,200), coord(x1,y1), 20, 0)
    pygame.draw.line(srf, (55,0,200), coord(0,2), coord(x1,y1), 2)
    pygame.draw.circle(srf, (55,0,200), coord(x2,y2), 20, 0)
    pygame.draw.line(srf, (55,0,200), coord(x1,y1), coord(x2,y2), 2)

    pygame.display.flip()

Well, I give up. I'd have to actually run the simulation in a way I can get feedback from to figure out what's going on. ... What, you think I should actually do that? Bah!

I'm just going to assume good things about the fact that there are two anchors, and bad things about the fact that the double pendulum exploits their softness. And if that makes an ass out of u and me, well, it wouldn't be the first ass-making I've bumbled into.

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-12-09 17:52:51

Hinges have an axis (makes sense, I guess?) and you can get the angle and angle rate for the hinge's current configuration.
Question... if we try and combine joints, such that we have multiple hinges linked... will the axis remain stable
... Wait, for that matter, I don't actually know how to define axes as vectors. Is the line that passes through both the vector and the origin the axis? That could be kinda weird. Is it the angle of the line that passes through the point and the origin? ... My education regarding vectors was abismal. I still have no idea what dot and cross product are or why or how they work. I just copied some javascript functions on the off chance that I'll need them for something.

*ahem*

Sliders have an axis, and they have getPosition instead of getAngle, because they slide instead of rotate.

Universal joints look interesting. It'd help if the examples came before the technical definition, because by the time I get to the examples, my attention is slipping.

A universal joint is like a ball and socket joint that constrains an extra degree of rotational freedom. Given axis 1 on body 1, and axis 2 on body 2 that is perpendicular to axis 1, it keeps them perpendicular. In other words, rotation of the two bodies about the direction perpendicular to the two axes will be equal.
In the picture, the two bodies are joined together by a cross. Axis 1 is attached to body 1, and axis 2 is attached to body 2. The cross keeps these axes at 90 degrees, so if you grab body 1 and twist it, body 2 will twist as well.
A Universal joint is equivalent to a hinge-2 joint where the hinge-2's axes are perpendicular to each other, and with a perfectly rigid connection in place of the suspension.
Universal joints show up in cars, where the engine causes a shaft, the drive shaft, to rotate along its own axis. At some point you'd like to change the direction of the shaft. The problem is, if you just bend the shaft, then the part after the bend won't rotate about its own axis. So if you cut it at the bend location and insert a universal joint, you can use the constraint to force the second shaft to rotate about the same angle as the first shaft.
Another use of this joint is to attach the arms of a simple virtual creature to its body. Imagine a person holding their arms straight out. You may want the arm to be able to move up and down, and forward and back, but not to rotate about its own axis.

Something something cars, got it. ... Not really. I don't got it. But we'll move on, anyway.



Hinge-2? OK! Hey, look, the example comes first this time! W00t!

The hinge-2 joint is the same as two hinges connected in series, with different hinge axes. An example, shown in the above picture is the steering wheel of a car, where one axis allows the wheel to be steered and the other axis allows the wheel to rotate.
The hinge-2 joint has an anchor point and two hinge axes. Axis 1 is specified relative to body 1 (this would be the steering axis if body 1 is the chassis). Axis 2 is specified relative to body 2 (this would be the wheel axis if body 2 is the wheel).
Axis 1 can have joint limits and a motor, axis 2 can only have a motor.
Axis 1 can function as a suspension axis, i.e. the constraint can be compressible along that axis.
The hinge-2 joint where axis1 is perpendicular to axis 2 is equivalent to a universal joint with added suspension.

I think we just confirmed that axes are described relative to the bodies, which I hope means that (1, 0, 0) is pointing to the right, no matter what the center of the body is. Hey, since we're describing an axis, would (1, 0, 0), (-1, 0, 0), and (9001, 0, 0) all be the same axis?


Fixed joints do exactly what I guessed, except:

The fixed joint maintains a fixed relative position and orientation between two bodies, or between a body and the static environment. Using this joint is almost never a good idea in practice, except when debugging. If you need two bodies to be glued together it is better to represent that as a single body.

Oh, bah. You talk like I have the faintest idea how body concatination works.

Also, it seems like setFixed applies to ... all joints? Hmm. Ominous.

The contact joint section is big and complicated and has lots of parameters. Dragons live there.

Well, that was tiring. Let's go restore some AP by fighting monsters.

3:54 PM 10/19/2015.

看過來!
"If you want utopia but reality gives you Lovecraft, you don't give up, you carve your utopia out of the corpses of dead gods."
MaxAngor wrote:
    George... Don't do that.

2015-12-09 20:05:25 (edited by camlorn 2015-12-09 22:15:49)

Note that ODE likes relative coordinate systems, and that your joint axes are relative to bodies in the local coordinate systems of the bodies.  Furthermore, an axis is almost certainly a unit vector, and using anything else will probably silently cause bad things.  You have been warned.  ODE is not helpful for audiogames.  It is simply a way to waste a bunch of time going nowhere.  Use Box2D, or only use ODE's geometry collision functions.  A 3D game at the level of the sighted is not possible; any effort in that direction will almost certainly need to write a custom physics engine that's friendly to audiogames.
Anyhow, that's not why I'm bothering to post.  Here is everything I know about the dot and cross product that I can share without you knowing Calculus or at least how to parameterize lines and stuff.  If this looks complicated, it is until you get it; like much of math, it's the most obvious thing in the world once you get it.  It's harder without pictures, though, yes.
Let's do cross first, because it's more useful when trying to build 3D engines.  There are formulas for it in 2 and 3 dimensions on Wikipedia.  It is not defined in anything greater than 3 dimensions (except maybe also 7, but you'll never use that one if it is and I think maybe that's complex number analogs and not this).  It is not defined in 1 dimension because 1-dimensional quantities are scalars.
Consider the 2d plane.  If I draw two vectors, I have two sides of a parallelogram.  When I take the cross product, I'm going to get the area of the imaginary parallelogram.  The sign depends on which vector is first.  To determine it, use the right-hand rule: place the four fingers of your right hand in the direction of the first vector, curl them towards the second vector, and consider the direction your thumb has to point if you stick it out.  If it's upwards, it's positive; otherwise it's negative.
One other useful property is that if you halve the area you get out, you get the area of the imaginary triangle that you'd get if you pretend that the vectors you drew were two sides of one.  On account that every triangle can be defined only with two of its sides as vectors, this works as an area formula for any triangle ever.
Okay.  SO if you don't get that, practice it.  Or ask for clarification,.  Or something.  I might be able to go in depth on the visualizations a bit more, but it's hard.
SO...3D.
In 3 dimensions, every pair of two vectors lie in a plane.  It doesn't matter which two vectors you give me, as long as they're not pointing in the same or opposite directions.  If the angle is not 0 or 180, I can take an imaginary sheet of paper and position it so that both vectors are lying on it.  This is one of two ways to define a plane.
When I take the cross product in 3 dimensions, I get a vector instead of a scalar.  The length of this vector (Pythagorean theorem) is the parallelogram's area.  The direction is still done with the right-hand rule in the same way as you did with 2D, but your thumb is now free to point in any direction.  If you take this vector and you normalize it (divide all components by magnitude), you have a normal (in the game sense, not the math sense; the math sense doesn't care about length) to the plane in which the other two vectors lie.
In either 2 or 3 dimensions, the cross product of vectors pointing in the same or opposite directions is 0.  In 3D space, the vector (0, 0, 0) is ambiguous, and there are an infinite number of planes you could pick.
If you get all of this, you're now about 6 inches from knowing how to write down any 3D rotation matrix from a pair of vectors representing the orientation of an object.  You're also within about 6 inches of being able to write down equations for planes.
Dot is easier.  Dot is computed in one of two ways.
You can take the magnitude of both vectors and the angle between them (say a for angle, p and q for magnitudes), and then do cos(a)*p*q.
Alternatively, given vectors p and q, it's p.x*q.x+p.y*q.y+p.z*q.z.
Which means that you have a way to find the angle between any two vectors whatsoever, regardless of orientation.
So, why?  What's the use?
For the cross product, given a front and up vector (that is, one pointing out your belly button and one pointing out the top of your head), you can get the right vector.  To see this, hold your arm and hand out in front of you, curl your fingers toward your head, notice that your thumb goes rightward.  From these three vectors and the position of the object in 3D space, you can translate to and from global coordinates quickly and write down a 3D transformation matrix that does this for you on demand.  This is how the Libaudioverse listener works, as well as OpenAL, OpenGL, DirectX, sound cones, all sorts of stuff.
From three points on a triangle listed in clockwise or counterclockwise order, the cross product gives you normals to the triangle.  The normal is used for lighting, but also to define the plane in which the triangle lies.  It is very inexpensive to tell if a line intersects a plane or if a point is on a plane.  Since most things in games are not colliding and the point needs to be in the same plane as the triangle, it's a useful first step to determining if two triangles intersect.  Every library will mandate a different orientation for the vertices, so check docs.
The dot product has a visual interpretation.  If one of the vectors is a pillar and the other is a vector of length 1 (a unit vector) pointing along the surface of a hill, the dot product gives you a vector representing the shadow of the pillar.  Sort of, real shadows don't work that way, but it's along the right lines.  We call this a projection, and there's a lot of useful things you can do with it.
But one of the most useful is is this.  Given two spaceships s1 and s2 with velocities v1 and v2 and positions p1 and p2, we can determine how fast they're moving towards each other. This is done as follows.
p=p2-p1 is the vector pointing from p1 to p2.
p=p/magnitude(p) makes p a unit vector.
v=(v2 dot p)-(v1 dot p)
And v is the velocity of s2 relative to s1, as a scalar.
The second most useful?  Given a vector from the player to an object and a vector representing the front of the player, the dot product is positive if and only if the object is in front of the player.  In 2D, if the vector for the front of the player is a unit vector and you also store one for the right of the player, then you can do the entire panning function without once using a trig function, and in about half the lines.
There's a lot more.  You can use the dot and cross product to determine how far the top of the pillar is from the ground of the hill.  You can use the dot and cross products to determine the distance of any point from any line.  The dot product applied 4 times is equivalent to a matrix-vector multiplication, which is why 3D transformation matrices work at all.
If you insist on a 3D game you need this knowledge.  In 2D, understanding it can lead to smaller and faster code, especially in C++ where using the sine and cosine is slower than a couple additions and multiplications (really, really long story, suffice it to say that the C standard libraries don't use the assembly instruction half the time).  But just getting functions for it in case they're useful will go nowhere.  In 3D, asking even basic questions about layout and whatnot needs a full understanding of the dot and cross products, and you quite probably also need 3D transformation matrices.  Most of the stuff from 2D fails to generalize in the ways you'd expect.
Edit: removed some unneeded magnitudes because dot is a scalar.
Edit again: Some stupidity. I haven't touched relative velocities seriously since I3d.  If anyone spots mistakes, speak up, but I think it's all right now.

My Blog
Twitter: @ajhicks1992