B3D-style Hierarchy System

BlitzMax Forums/OpenGL Module/B3D-style Hierarchy System

simonh(Posted 2006) [#1]
I am currently writing a B3D-style hierarchy entity system as part of a small 3D engine I'm working on.

The hierarchy system work the same as B3D - you specify a parent when creating an entity, and can use local or global parameters with the position/rotate commands.

I've got it working fine up to a point - after creating child entities, you are able to position and rotate everything using local coordinates, which works as expected.

However, I can't quite figure out how to get things working when you specify global coordinates for a child.

Internally, I have a global matrix for each entity and local x,y,z,pitch,yaw,roll values. I'm pretty sure that for global values to work correctly within a hierarchy system, I need to somehow convert the specified global coordinates into local coordinates.

But I haven't figured out how to do this. Can anyone help?


N(Posted 2006) [#2]
<Disclaimer: I guarantee none of this to be true. It is based entirely on what I remember (and my 3D math book). If you computer goes boomy, you're on your own. That said, I don't think I'm wrong.>

Let's say you have the following entity class:

Type Entity
  Field transform:Matrix
  Field parent:Entity
End Type


In order to convert global coordinates to object space coordinates, you have to transform them by the entity's transform.

Assuming the transform is relative to the parent entity's transform (that is, when you render the child, you would do something like glPushMatrix( ) glMultMatrixf( mat.GetPtr( ) )).

Anyhow, now onto how to do the transformation.

i:Entity = some entity with a variable number of parents

Local mat:Matrix = New Matrix ' identity matrix
Local transforms:TList = New TList

While i.parent <> Null
  i = i.parent
  transforms.AddFirst( i.mat )
Wend

For Local m:Matrix = EachIn transforms
  mat.Transform( m ) ' Transform mat by m
Next

' Now transform the point by the transformed matrix
mat.Transform( point )

' The point is now in object space


I think that's about right. As for converting rotations, you probably want to convert from euler to a matrix, apply the transformations, and then go back to euler.


simonh(Posted 2006) [#3]
I don't think that works, unfortunately.

The global coordinates need to be maintained, but at the same time, I need to somehow calculate the local offset from the parent entities.

So next time the whole matrix strack is calculated (using a similar method to above), when the child's local coordinates are multipled with the parent matrix, the child's global coordinates will be produced.

The closest I have come is subtracting the parent's global coordinates from the child's global coordinates, and saving those as the local x,y,z values.

Doesn't quite work though when rotations are involved.


AdrianT(Posted 2006) [#4]
hwh, this is exactly the problem we have had with using B3D's in Bmax and Torque. Everything works until you get to local offsets from parents. Everything goes mental at that point.

If you find a solution please share ;P


N(Posted 2006) [#5]
Simon, you only need to store the local coordinates. The global coordinates can be reproduced by transforming them.


simonh(Posted 2006) [#6]
I do store the local coordinates. The problem is getting the local coordinates to equal global coordinates when you do something like:

PositionEntity ent,23,45,75,True

The entity has to appear at that exact position. But it also has to be attached to its parent. So to attach it to its parent, it needs to have a local offset from the parent.

But how to calculate that offset, taking into account rotations and everything else?

I've got a feeling inverse matrices or something might need to be used somewhere.

I'll crack this eventually, then once it's done I'll post it here.


simonh(Posted 2006) [#7]
Well I solved it. Some silly oversights here and there were throwing everything out slightly. The hierarchy system now works the same as B3D's.

Global position entity -

Method PositionEntity(x_#,y_#,z_#,glob=False)

	x=x_#
	y=y_#
	z=-z_#

	' conv glob to local. x/y/z always local to parent or global if no parent
	If glob=True And parent<>Null
			
		x=x-parent.EntityX(True)
		y=y-parent.EntityY(True)
		z=z-parent.EntityZ(True)
			
		xa=EntityPitch(True)
		ya=EntityYaw(True)
		za=EntityRoll(True)
			
		Local new_mat:Matrix=New Matrix
		new_mat.LoadIdentity()
		new_mat.Rotate(-xa,-ya,-za)
		new_mat.Translate(x,y,z)

		x=new_mat.grid(3,0)
		y=new_mat.grid(3,1)
		z=new_mat.grid(3,2)
				
	EndIf

(code continues)
Global rotate entity:

Method RotateEntity(pitch_#,yaw_#,roll_#,glob=False)

	pitch=-pitch_#
	yaw=yaw_#
	roll=roll_#
		
	' conv glob to local. pitch/yaw/roll always local to parent or global if no parent
	If glob=True And parent<>Null

		pitch=pitch-parent.EntityPitch(True)
		yaw=yaw-parent.EntityYaw(True)
		roll=roll-parent.EntityRoll(True)
		
	EndIf

(code continues)
The above code calculates the local offsets from the parent entity.

Evak, I'm not sure this will help you out as .b3d's use quaternions I believe which is a different problem altogether. I intend to tackle .b3d loading next though so if I get a fully working .b3d loader working I'll let you know.


Chris C(Posted 2006) [#8]
tom speed put a rather nice entity type system on this forum a while ago, you'd do well to look at his code...


simonh(Posted 2006) [#9]
I just tried Tom's, it works well but the results don't seem to be the same as B3D's.

I suppose it doesn't matter normally but for the purposes of my project (a Mac conversion), I need to mimic B3D's behaviour exactly.


AdrianT(Posted 2006) [#10]
man, this may be exactly what were looking for, thanks for posting :). Will have to check it out, some of our problems do lie with quat euler conversion too I believe, If you do figure it out that would be fantastic.

Would have to add you to the credits in my project.


AntonyWells(Posted 2006) [#11]
Simon have you reached b3d file loading yet? Please let me kniow if you manage it while retain heierachies.


JoshK(Posted 2006) [#12]
Here's code to handle transformations. Do a search for my code to handle Blitz3D-style quaternions.

Global TFORMEDVALUE_X#
Global TFORMEDVALUE_Y#
Global TFORMEDVALUE_Z#
Global TFORMEDVALUE_W#

Function TFormedX#()
Return TFORMEDVALUE_X
End Function

Function TFormedY#()
Return TFORMEDVALUE_Y
End Function

Function TFormedZ#()
Return TFORMEDVALUE_Z
End Function

Function TFormedW#()
Return TFORMEDVALUE_W
End Function

Function TFormPoint(x#,y#,z#,src:tentity,dst:tentity)

	TFORMEDVALUE_X=x
	TFORMEDVALUE_Y=y
	TFORMEDVALUE_Z=z
	If src=dst Return

	'Transform from source to common parent
	If src<>Null
		If src.matrixchanged src.UpdateMatrix()
		mat00#=src.matrix[0,0]
		mat10#=src.matrix[1,0]
		mat20#=src.matrix[2,0]
		mat01#=src.matrix[0,1]
		mat11#=src.matrix[1,1]
		mat21#=src.matrix[2,1]
		mat02#=src.matrix[0,2]
		mat12#=src.matrix[1,2]
		mat22#=src.matrix[2,2]
		x=x*src.scalex
		y=y*src.scaley
		z=z*src.scalez
		TFORMEDVALUE_X#=x#*mat00#+y*mat10#+z*mat20#
		TFORMEDVALUE_y#=x#*mat01#+y*mat11#+z*mat21#
		TFORMEDVALUE_z#=x#*mat02#+y*mat12#+z*mat22#
		TFORMEDVALUE_x#=TFORMEDVALUE_x#+src.x
		TFORMEDVALUE_y#=TFORMEDVALUE_y#+src.y
		TFORMEDVALUE_z#=TFORMEDVALUE_z#+src.z
		If src.parent<>Null
			tformpoint(TFORMEDVALUE_X,TFORMEDVALUE_Y,TFORMEDVALUE_Z,src.parent,Null)
		EndIf
	EndIf
	
	'Transform from common parent to destination
	While dst<>Null
		If dst.matrixchanged dst.UpdateMatrix()
		x=TFORMEDVALUE_x-dst.x
		y=TFORMEDVALUE_y-dst.y
		z=TFORMEDVALUE_z-dst.z
		qx#=dst.qx
		qy#=dst.qy
		qz#=dst.qz
		qw#=-dst.qw
		mat00=(1.0-2.0*qx#*qx#-2.0*qz#*qz#)
		mat10=-(2.0*qz#*qy#-2.0*qw#*qx#)
		mat20=-(2.0*qx#*qy#+2.0*qw#*qz#)
		mat01=-(2.0*qz#*qy#+2.0*qw#*qx#)
		mat11=(1.0-2.0*qy#*qy#-2.0*qx#*qx#)
		mat21=-(2.0*qw#*qy#-2.0*qx#*qz#)
		mat02=2.0*qw#*qz#-2.0*qx#*qy#
		mat12=2.0*qx#*qz#+2.0*qw#*qy#
		mat22=1.0-2.0*qz#*qz#-2.0*qy#*qy#
		TFORMEDVALUE_X#=x#*mat00#+y*mat10#+z*mat20#
		TFORMEDVALUE_y#=x#*mat01#+y*mat11#+z*mat21#
		TFORMEDVALUE_z#=x#*mat02#+y*mat12#+z*mat22#		
		TFORMEDVALUE_X#=TFORMEDVALUE_X/dst.scalex
		TFORMEDVALUE_Y#=TFORMEDVALUE_Y/dst.scaley
		TFORMEDVALUE_Z#=TFORMEDVALUE_Z/dst.scalez
		dst=dst.parent
		If dst=Null Exit
	Wend
		
End Function


Function TFormVector(x#,y#,z#,src:tentity,dst:tentity)
	TFORMEDVALUE_X=x
	TFORMEDVALUE_Y=y
	TFORMEDVALUE_Z=z
	If src=dst Return
	If src<>Null
		If src.matrixchanged src.UpdateMatrix()
		mat00#=src.matrix[0,0]
		mat10#=src.matrix[1,0]
		mat20#=src.matrix[2,0]
		mat01#=src.matrix[0,1]
		mat11#=src.matrix[1,1]
		mat21#=src.matrix[2,1]
		mat02#=src.matrix[0,2]
		mat12#=src.matrix[1,2]
		mat22#=src.matrix[2,2]
		x=x*src.scalex
		y=y*src.scaley
		z=z*src.scalez
		TFORMEDVALUE_X#=x#*mat00#+y*mat10#+z*mat20#
		TFORMEDVALUE_y#=x#*mat01#+y*mat11#+z*mat21#
		TFORMEDVALUE_z#=x#*mat02#+y*mat12#+z*mat22#
		TFORMEDVALUE_x#=TFORMEDVALUE_x#
		TFORMEDVALUE_y#=TFORMEDVALUE_y#
		TFORMEDVALUE_z#=TFORMEDVALUE_z#
		If src.parent<>Null
			tformpoint(TFORMEDVALUE_X,TFORMEDVALUE_Y,TFORMEDVALUE_Z,src.parent,Null)
		EndIf
	EndIf
	While dst<>Null
		If dst.matrixchanged dst.UpdateMatrix()
		x=TFORMEDVALUE_x
		y=TFORMEDVALUE_y
		z=TFORMEDVALUE_z
		qx#=dst.qx
		qy#=dst.qy
		qz#=dst.qz
		qw#=-dst.qw
		mat00=(1.0-2.0*qx#*qx#-2.0*qz#*qz#)
		mat10=-(2.0*qz#*qy#-2.0*qw#*qx#)
		mat20=-(2.0*qx#*qy#+2.0*qw#*qz#)
		mat01=-(2.0*qz#*qy#+2.0*qw#*qx#)
		mat11=(1.0-2.0*qy#*qy#-2.0*qx#*qx#)
		mat21=-(2.0*qw#*qy#-2.0*qx#*qz#)
		mat02=2.0*qw#*qz#-2.0*qx#*qy#
		mat12=2.0*qx#*qz#+2.0*qw#*qy#
		mat22=1.0-2.0*qz#*qz#-2.0*qy#*qy#
		TFORMEDVALUE_X#=x#*mat00#+y*mat10#+z*mat20#
		TFORMEDVALUE_y#=x#*mat01#+y*mat11#+z*mat21#
		TFORMEDVALUE_z#=x#*mat02#+y*mat12#+z*mat22#		
		TFORMEDVALUE_X#=TFORMEDVALUE_X/dst.scalex
		TFORMEDVALUE_Y#=TFORMEDVALUE_Y/dst.scaley
		TFORMEDVALUE_Z#=TFORMEDVALUE_Z/dst.scalez
		dst=dst.parent
		If dst=Null Exit
	Wend
		
End Function

Function TFormNormal(x#,y#,z#,src:tentity,dst:tentity)
	TFORMEDVALUE_X=x
	TFORMEDVALUE_Y=y
	TFORMEDVALUE_Z=z
	If src=dst Return
	If src<>Null
		If src.matrixchanged src.UpdateMatrix()
		mat00#=src.matrix[0,0]
		mat10#=src.matrix[1,0]
		mat20#=src.matrix[2,0]
		mat01#=src.matrix[0,1]
		mat11#=src.matrix[1,1]
		mat21#=src.matrix[2,1]
		mat02#=src.matrix[0,2]
		mat12#=src.matrix[1,2]
		mat22#=src.matrix[2,2]
		TFORMEDVALUE_X#=x#*mat00#+y*mat10#+z*mat20#
		TFORMEDVALUE_y#=x#*mat01#+y*mat11#+z*mat21#
		TFORMEDVALUE_z#=x#*mat02#+y*mat12#+z*mat22#
		TFORMEDVALUE_x#=TFORMEDVALUE_x#
		TFORMEDVALUE_y#=TFORMEDVALUE_y#
		TFORMEDVALUE_z#=TFORMEDVALUE_z#
		If src.parent<>Null
			tformpoint(TFORMEDVALUE_X,TFORMEDVALUE_Y,TFORMEDVALUE_Z,src.parent,Null)
		EndIf
	EndIf
	While dst<>Null
		If dst.matrixchanged dst.UpdateMatrix()
		x=TFORMEDVALUE_x
		y=TFORMEDVALUE_y
		z=TFORMEDVALUE_z
		qx#=dst.qx
		qy#=dst.qy
		qz#=dst.qz
		qw#=-dst.qw
		mat00=(1.0-2.0*qx#*qx#-2.0*qz#*qz#)
		mat10=-(2.0*qz#*qy#-2.0*qw#*qx#)
		mat20=-(2.0*qx#*qy#+2.0*qw#*qz#)
		mat01=-(2.0*qz#*qy#+2.0*qw#*qx#)
		mat11=(1.0-2.0*qy#*qy#-2.0*qx#*qx#)
		mat21=-(2.0*qw#*qy#-2.0*qx#*qz#)
		mat02=2.0*qw#*qz#-2.0*qx#*qy#
		mat12=2.0*qx#*qz#+2.0*qw#*qy#
		mat22=1.0-2.0*qz#*qz#-2.0*qy#*qy#
		TFORMEDVALUE_X#=x#*mat00#+y*mat10#+z*mat20#
		TFORMEDVALUE_y#=x#*mat01#+y*mat11#+z*mat21#
		TFORMEDVALUE_z#=x#*mat02#+y*mat12#+z*mat22#
		dst=dst.parent
		If dst=Null Exit
	Wend
End Function


Update the entity matrix:
	Method UpdateMatrix()
		matrixchanged=False
		matrix[0,0]=(1.0-2.0*qx#*qx#-2.0*qz#*qz#)
		matrix[1,0]=-(2.0*qz#*qy#-2.0*qw#*qx#)
		matrix[2,0]=-(2.0*qx#*qy#+2.0*qw#*qz#)
		matrix[0,1]=-(2.0*qz#*qy#+2.0*qw#*qx#)
		matrix[1,1]=(1.0-2.0*qy#*qy#-2.0*qx#*qx#)
		matrix[2,1]=-(2.0*qw#*qy#-2.0*qx#*qz#)
		matrix[0,2]=2.0*qw#*qz#-2.0*qx#*qy#
		matrix[1,2]=2.0*qx#*qz#+2.0*qw#*qy#
		matrix[2,2]=1.0-2.0*qz#*qz#-2.0*qy#*qy#
	EndMethod



JoshK(Posted 2006) [#13]
Oh, here is BlitzPlus code for tforming rotations:
Function TFormQuat(x#,y#,z#,w#,src,dst)
TFORMEDVALUE_X=x
TFORMEDVALUE_Y=y
TFORMEDVALUE_Z=z
TFORMEDVALUE_W=w
If src=dst Return

;Transform from source to parent
If src
	qx#=ObjectQuatX(src)
	qy#=ObjectQuatY(src)
	qz#=ObjectQuatZ(src)
	qw#=ObjectQuatW(src)
	MulQuat TFORMEDVALUE_X,TFORMEDVALUE_Y,TFORMEDVALUE_Z,TFORMEDVALUE_W,-qx,-qy,-qz,qw
	TFORMEDVALUE_X=VectorX()
	TFORMEDVALUE_Y=VectorY()
	TFORMEDVALUE_Z=VectorZ()
	TFORMEDVALUE_W=VectorW()
	EndIf

;Transform from parent to source
If dst
	EndIf
End Function

Function TFormEuler(pitch#,yaw#,roll#,src,dst)
EulerAsQuat pitch#,yaw#,roll#
TFormQuat vectorx(),vectory(),vectorz(),vectorw(),src,dst
QuatAsEuler TFormedX(),TFormedY(),TFormedZ(),TformedW()
TFORMEDVALUE_X=VectorX()
TFORMEDVALUE_Y=VectorY()
TFORMEDVALUE_Z=VectorZ()
TFORMEDVALUE_W=0.0
End Function


Quat code:
Function EulerAsQuat(pitch#,yaw#,roll#)
cr#=Cos(-roll#/2.0)
cp#=Cos(pitch#/2.0)
cy#=Cos(yaw#/2.0)
sr#=Sin(-roll#/2.0)
sp#=Sin(pitch#/2.0)
sy#=Sin(yaw#/2.0)
cpcy#=cp#*cy#
spsy#=sp#*sy#
spcy#=sp#*cy#
cpsy#=cp#*sy#
vectorw#=cr#*cpcy#+sr#*spsy#
vectorx#=sr#*cpcy#-cr#*spsy#
vectory#=cr#*spcy#+sr#*cpsy#
vectorz#=cr#*cpsy#-sr#*spcy#
End Function

Function QuatAsEuler(x#,y#,z#,w#)
sint#=(2.0*w*y)-(2.0*x*z)
cost_temp#=1.0-(sint#*sint#)
If Abs(cost_temp#)>QuatToEulerAccuracy
	cost#=Sqr(cost_temp#)
	Else
	cost#=0.0
	EndIf
If Abs(cost#)>0.001
	sinv#=((2.0*y*z)+(2.0*w*x))/cost#
	cosv#=(1.0-(2.0*x*x)-(2.0*y*y))/cost#
	sinf#=((2.0*x*y)+(2.0*w*z))/cost#
	cosf#=(1.0-(2.0*y*y)-(2.0*z*z))/cost#
	Else
	sinv#=(2.0*w*x)-(2.0*y*z)
	cosv#=1.0-(2.0*x*x)-(2.0*z*z)
	sinf#=0.0
	cosf#=1.0
	EndIf
vectorz#=-ATan2(sinv#,cosv#)
vectorx#=ATan2(sint#,cost#)
vectory#=ATan2(sinf#,cosf#)
End Function



Damien Sturdy(Posted 2006) [#14]
now, this shouldn't be lost! :D


JoshK(Posted 2006) [#15]
You might have to change it a bit to adapt it to your own engine, but all the math is correct, and has been tested meticulously against Blitz3D calculations.


AdrianT(Posted 2006) [#16]
wow! lots of good stuff here, thanks Halo :)


N(Posted 2006) [#17]
Indeed. Thank you very much.


AntonyWells(Posted 2006) [#18]
has been tested meticulously against Blitz3D calculations.


Are you sure your quat to matrix code is right? I mean did you ever support loading of b3ds with Hierarchy support?

I ask because that's how far we've got, but even your quat to matrix code results in faulty rotations.


JoshK(Posted 2006) [#19]
Rendering a left-handed coord sys in GL:

		glMatrixMode GL_MODELVIEW
		glPushMatrix()
		glLoadIdentity()

		glRotatef camera.roll,0,0,1
		glRotatef camera.pitch,1,0,0
		glRotatef -camera.yaw,0,1,0
		glTranslatef -camera.x,-camera.y,camera.z
		glScalef 1,1,-1


Now when you draw a model:
	glpushmatrix()
	gltranslatef entity.x,entity.y,entity.z
	glRotatef -entity.yaw,0,1,0
	glRotatef entity.pitch,1,0,0
	glRotatef entity.roll,0,0,1
	glScalef entity.scalex,entity.scaley,entity.scalez
	



VIP3R(Posted 2006) [#20]
Very useful stuff, nice work simonh/halo :)


Tom(Posted 2006) [#21]
Hi all, memories a bit groggy as I aint touched my code in a few weeks but...

Re: The code I posted a while back, GL does use an inverted Z axis, so some rotation will be negative. But on the whole, they should match up with B3Ds if you ignore the sign.

i.e, B3Ds TurnEntity entity,30,30,30 would be entity.Turn 30,-30,30 in my GL code, and of course (or something similar, can't recall which axis it affects :)

Give my code another look over. It does cater for hierarchial rotation & position in local, parent & world space (something I've not seen in ANY other GL demo code on the net!!!) and it's an efficient way of rendering a scene IMO.

Later!
Tom