Game Development Community

Blender Exporter 0.5 Available

by James Urquhart · in Artist Corner · 01/24/2004 (11:03 am) · 117 replies

Hey again,

I have released yet another version of my blender exporter. (for Torque .DTS Models)

Fixes / Improvements since 0.4 :
- Trigger support should now work.
- Billboard detail levels now supported.
- Improved the GUI (consistent layout)
- Improved Material Handling
- Triangle strips can now be generated (VTK + python bindings required)
- Improved reading of existing shapes (in the dts code)
- Added Double Faces flag for meshes.
- Improved the output of the exporter.

"Billboard detail levels" require some modifications to torque if you want to have a go at using them; See my 3Space bug post in the SDK Bugs forum.

If you have made any shapes in the previous version of the exporter, you need to make sure you have assigned materials to your mesh. The exporter now only takes into account the material of each face on a mesh, not the image assigned in the UV editor. However, it still takes into account the UV texture coordinates in the UV editor. (change the material mapping options if you want to see the model with texture properly in blender, e.g for a render)

In additon, i have written a manual for the exporter, which, is by no means complete.

Hopefully by next release i will have figured out how i can support IFL's in blender :)

The Resource URL is : www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=4662
#21
02/20/2004 (11:27 am)
Well, I wouldn't have time to get you anything for this weekend for sure. Next weekend is possible, but don't hold up any release waiting on something from me.
#22
02/29/2004 (12:58 pm)
Weird problem with exporter. I have a simple animation of a tower. I have created an action called "Spin". I have animated a bone to spin around through 20 frames. I have a bone called "pivot" and one called "pole". The "Spin" animation is associated with the "pivot" bone. I have a cone mesh with vertices assigned to the "pivot" vertex group. I have a cylinder mesh with vertices assigned to the "pole" vertex group. In Blender when I press alt-a the cone rotates around as expected. When I export it to a dts file it rotates the entire object around the "pole" bone axis. It acts as though the exporter is assigning the animation for the "pivot" bone to the "pole". Has anyone else run into this?

Thanks
#23
02/29/2004 (1:14 pm)
Make sure you've applied any transformations on your armatures and meshes via tha CTRL-AKEY before you export. Make sure you click the update button (and later probably save) in the main export window. Its also a good idea to select all your bones in edit mode and fix their roll angles with CTRL-NKEY.

Its unclear whether or not you have multiple armatures or only one. The exporter exports evertyhing more or less and is a little lax about trying to connect bones and action channels. If you have bones with the same prefix names, like Bone and Bone.001, etc., you're likely to run into problems. If you have multiple armatures with the same bone names in them, its likely to get confused also.

I had pretty good luck playing with this some over the last week on simpler rigs/armatures, so I'd expect what you're doing should work. However, if you are working with more complicated armatures with constraints and baking them to export the baked animations, you'll get mixed results. I haven't gotten to the bottom of why the more complicated rigs aren't exporting as my time is pretty tight these days.
#24
02/29/2004 (1:15 pm)
Thor,

Send me your blend and i'll have a check.
#25
02/29/2004 (1:51 pm)
Created a new animation using Blender 2.32 instead of 2.28a. Now the animation seems to be working initially. Could be an issue with 2.28a and the exporter. I will try to make another tower animation to see if it was 2.28a of Blender.
#26
02/29/2004 (9:15 pm)
I rebuilt the animation with 2.32 and it works fine. I think this was an issue with 2.28a. I have another problem however.
How do you properly "bake" an animation with IK solver constraints? Is there another way to tell Blender to take the resulting transformations for each bone regardless of source?

thanks
#27
03/01/2004 (11:55 am)
I've had some success with baking, but only after I hacked both blender and the script. The underlying problem is that blender's python interface doesn't support action ipo curves, so you can't tell the curve type directly and have to intuit it.

This export script and others assume that you've added all your action key frames with IKEY->LocRot because that insures that the order of the action ipo curves is LocX, LocY, LocZ, QuatX, Quaty, QuatZ, QuatW. Although this script does make provisions for Loc and Rot key frames also.

When you Bake and action via the Bake button in the action window, it inserts the Ipo curves in an order not expected by this or other scipts and includes the Size ipo curves as well. So, once you've baked the action, this script and others can't determine which curves are which properly.

Also, this script assumes (because there aren't many other ways) a naming convention of ipo curves to map them back to the Action and Bones. Normally an Ipo curve will be called Action.Bone so this script splits that into Action and Bone by taking everything to the left of the first perion/dot as the action name and the rest as the bone name. Since the baked action is called Action.Baked and the ipo curves are called Action.Baked.Bone, the script tries to find a node/bone called Baked.Bone which doesn't exist.

Since blender has some limits on the size of names for the ipo curves, well named actions and bones can cause naming collisions. For example, action: Walk, bone: Shoulder.L when baked makes an ipo curve called Walk.Baked.Shoulder.L or should at least. The ipo curve name is truncated to 16 or 20 chars and the script can't find the bone/node to properly export it.

The hacks I made to blender to help get around this were to change the baked name from Action.BAKED.Bone to bAction.Bone and then I added read only support for action ipo curves so you can now do ipo.getCurveCurval ("QuatW"), etc. and the order of the curves is unimportant. A hack that should be added to this for blender would be to only add key frames every X frames when baking an action, where X is either hard coded or user modifyable. Having key frames every frame takes quite a while to export and is unneeded in this application.

The hack I made to these scripts were to make use of the ipo.getCurveCurval ("QuatX/QuatY/QuatZ/etc."). Additionally, I added a function I call in several places called shouldIgnore() that returns true if the bone should be ignored. I did this because exporting IK_*, *Null and *Lock bones for me, along with some facial animation control bones makes no sense. shouldIgnore() is definitely a personalized hack for me now and I'd like to see something more generalized added to this.

Once I did these things, a simple rig/armature exported fine after I baked it. I had some IK, Track To and Lock Track constraints on it and everything animated fine in torque. By simple, I mean your basic IK rigged armature with IK bones for hand, foot and neck, maybe 20 to 30 bones.

I have however not been successful at exporting one of my blender animation rigs. This one is much more complex with 170 some bones and many, many constraints. Its hard to say where this is failing, I actually suspect there are several bugs stopping this from working starting with blender's baking and possibly something in the script and/or torque (exceeding a limit of some sort). I'm at a bit of a loss why this won't export properly and am at the point I don't think its worth the time since I never really planned on using this complicated of an armature in torque.
#28
03/01/2004 (7:20 pm)
Hello,
Awesome work on your Blender exporter and Blender itself changes! I did notice that the actions were being named screwy and have been changing the IPO and the action names to be B. instead of .BAKED..

Do you have patches of the exporter and blender? I can compile Blender using gcc. I would just need to download the source.

I was thinking that maybe there was a way to put Blender in a frame position by the script. For each position the script would look at internal variables (if available) for each bone to get the position. (Having no idea how the Blender interface works with Python this was just a guess.) I was thinking I could somehow create a Python based "BAKE" command.

I have been wanting to look at Blender code and will take a
look to see what can be done. I also want to see if I can't interface "real world" sensors to allow animation recording of real people and faces. This would be a lot of fun and could make creating animations for Torque very exciting. Of course I would have to make this available to the users of Torque. : )

Later
#29
03/01/2004 (7:44 pm)
James Urquhart is the owner/maintainer of the exporter, I was just hacking on it some to get it working for my situation, :-).

I'm planning on submitting the ipo curve name patch to blender.org as a bug or the bf-committers mailing list, so it should be generally available at some point ... hopefully. I'll try putting the patch here for you now and if that doesn't work and you need it soon, I can email it.
Index: source/blender/python/api2_2x/Ipo.c
===================================================================
RCS file: /cvsroot/bf-blender/blender/source/blender/python/api2_2x/Ipo.c,v
retrieving revision 1.19
diff -u -r1.19 Ipo.c
--- source/blender/python/api2_2x/Ipo.c 21 Jan 2004 04:42:13 -0000      1.19
+++ source/blender/python/api2_2x/Ipo.c 2 Mar 2004 04:11:11 -0000
@@ -345,7 +345,7 @@
  
  
  
-void GetIpoCurveName(IpoCurve *icu,char*s);
+void GetIpoCurveName(IpoCurve *icu,char*s,int doAction);
 void getname_mat_ei(int nr, char *str);
 void getname_world_ei(int nr, char *str);
 void getname_cam_ei(int nr, char *str);
@@ -360,7 +360,7 @@
                return (EXPP_ReturnPyObjError (PyExc_TypeError, "expected string argument"));
  for (icu=self->ipo->curve.first; icu; icu=icu->next){
         char str1[80];
-        GetIpoCurveName(icu,str1);
+        GetIpoCurveName(icu,str1, 0);
         if (!strcmp(str1,str))return IpoCurve_CreatePyObject(icu);
         }
  
@@ -369,7 +369,7 @@
 }
  
  
-void GetIpoCurveName(IpoCurve *icu,char*s)
+void GetIpoCurveName(IpoCurve *icu,char*s, int doAction)
 {
        switch (icu->blocktype)
                                {
@@ -377,6 +377,15 @@
                                case ID_WO : {getname_world_ei(icu->adrcode,s);break;}
                                case ID_CA : {getname_cam_ei(icu->adrcode,s);break;}
                                case ID_OB : {getname_ob_ei(icu->adrcode,s);break;}
+                               case ID_AC :
+                                  if (doAction) {
+                                     getname_ac_ei(icu->adrcode,s);
+                                     break;
+                                  }
+                                  /*FALLTHROUGH*/
+                               default:
+                                  s[0]= 0;
+                                  break;
                                }
 }
  
@@ -576,7 +585,7 @@
                        while (icu)
                                {
                                        char str1[10];
-                                       GetIpoCurveName(icu,str1);
+                                       GetIpoCurveName(icu,str1, 1);
                                        if (!strcmp(str1,stringname))break;
                                        icu=icu->next;
                                }
#30
03/01/2004 (7:45 pm)
Exceeded post limit, continuing with the first patch:
Index: source/blender/python/api2_2x/Ipocurve.c
===================================================================
RCS file: /cvsroot/bf-blender/blender/source/blender/python/api2_2x/Ipocurve.c,v
retrieving revision 1.10
diff -u -r1.10 Ipocurve.c
--- source/blender/python/api2_2x/Ipocurve.c    3 Jan 2004 03:50:58 -0000       1.10
+++ source/blender/python/api2_2x/Ipocurve.c    2 Mar 2004 04:11:11 -0000
@@ -191,20 +191,20 @@
  
 static PyObject* IpoCurve_getName (C_IpoCurve *self)
 {
-       char * nametab[24] = {"LocX","LocY","LocZ","dLocX","dLocY","dLocZ","RotX","RotY","RotZ","dRotX","dRotY","dRotZ","SizeX","SizeY","SizeZ","dSizeX","dSizeY","dSizeZ","Layer","Time","ColR","ColG","ColB","ColA"};
+       char * nametab[28] = {"LocX","LocY","LocZ","dLocX","dLocY","dLocZ","RotX","RotY","RotZ","dRotX","dRotY","dRotZ","SizeX","SizeY","SizeZ","dSizeX","dSizeY","dSizeZ","Layer","Time","ColR","ColG","ColB","ColA","QuatW","QuatX","QuatY","QuatZ"};
  
-       if (self->ipocurve->blocktype != ID_OB)
+       if (self->ipocurve->blocktype != ID_OB && self->ipocurve->blocktype != ID_AC)
                return EXPP_ReturnPyObjError (PyExc_TypeError,
-                       "This function doesn't support this ipocurve type yet");
+                       "This function only supports Action and Object curves.");
  
        //      printf("IpoCurve_getName %d\n",self->ipocurve->vartype);
        if (self->ipocurve->adrcode <=0 )
-return PyString_FromString("Index too small");
-       if (self->ipocurve->adrcode >= 25 )
-return PyString_FromString("Index too big");
-
-return PyString_FromString(nametab[self->ipocurve->adrcode-1]);
+         return PyString_FromString("Index too small");
+       if ((self->ipocurve->blocktype == ID_OB && self->ipocurve->adrcode >= 25) ||
+           (self->ipocurve->blocktype == ID_AC && self->ipocurve->adrcode >= 29))
+         return PyString_FromString("Index too big");
  
+       return PyString_FromString(nametab[self->ipocurve->adrcode-1]);
 }
  
  
@@ -274,9 +274,9 @@
 /*****************************************************************************/
 static PyObject *IpoCurveRepr (C_IpoCurve *self)
 {
-       void GetIpoCurveName(IpoCurve *icu,char*s);
+       void GetIpoCurveName(IpoCurve *icu,char*s, int doAction);
        char s[100],s1[100];
-       GetIpoCurveName(self->ipocurve,s1);
+       GetIpoCurveName(self->ipocurve,s1, 1);
        sprintf(s,"IpoCurve %s \n",s1);
   return PyString_FromString(s);
 }
#31
03/01/2004 (7:46 pm)
I doubt my BAKED patch will make it into the source tree and its pretty trivial, but here it is just for completeness.
Index: source/blender/src/editaction.c
===================================================================
RCS file: /cvsroot/bf-blender/blender/source/blender/src/editaction.c,v
retrieving revision 1.22
diff -u -r1.22 editaction.c
--- source/blender/src/editaction.c     20 Feb 2004 04:57:07 -0000      1.22
+++ source/blender/src/editaction.c     2 Mar 2004 04:12:04 -0000
@@ -174,7 +174,7 @@
        result = add_empty_action();
  
        /* Assign the new action a unique name */
-       sprintf (newname, "%s.BAKED", act->id.name+2);
+       sprintf (newname, "b%s", act->id.name+2);
        rename_id(&result->id, newname);
  
        actlen = calc_action_end(act);

My Dts_Blender.py file has too many temporary hacks from trying to debug my problems to give you a proper patch, so here are the lines from it. The original lines are commented out and the new lines using my new blender functionality replaces them. It all begins near line 900 or so in Dts_Blender.py.

#						curve_id = 0
#						if has_loc:
#							loc += Vector(
#								ipo.getCurveCurval(curve_id),
#								ipo.getCurveCurval(curve_id + 1),
#								ipo.getCurveCurval(curve_id + 2))
#							curve_id += 3

#						if has_rot:
#							ipo_rot = Quaternion(
#								ipo.getCurveCurval(curve_id),
#								ipo.getCurveCurval(curve_id + 1),
#								ipo.getCurveCurval(curve_id + 2),
#								ipo.getCurveCurval(curve_id + 3))
#							rot = ipo_rot.inverse() * rot
#							curve_id += 4

						if has_loc:
							loc += Vector(
								ipo.getCurveCurval("LocX"),
								ipo.getCurveCurval("LocY"),
								ipo.getCurveCurval("LocZ"))

						if has_rot:
							ipo_rot = Quaternion(
								ipo.getCurveCurval("QuatX"),
								ipo.getCurveCurval("QuatY"),
								ipo.getCurveCurval("QuatZ"),
								ipo.getCurveCurval("QuatW"))
							rot = ipo_rot.inverse() * rot

If you don't put the BAKED -> b change in, this change to Dts_Blender_Prefs.py might be handy also:
def SplitIpoName(name):
	splitted = name.split(".")
	if len(splitted) < 2:
		print "Un-analysable IPO name :", name
		return None, None
	animation_name = splitted[0]
	if splitted [1] == 'B' or splitted [1] == "BAKED":
		bone_name = splitted[2]
		for i in range(3,len(splitted)):
			bone_name += "." + splitted[i]
	else:
		bone_name = splitted[1]
		for i in range(2,len(splitted)):
			bone_name += "." + splitted[i]
	return animation_name, bone_name

I check for both B and BAKED as my first hack on the naming from baking was to replace BAKED with B. Putting a lower case b on the beginning of the action name gives me one more character and the changes to SplitIpoName aren't needed then.
#32
03/01/2004 (7:48 pm)
Oops, one more change in Dts_Blender.py or it won't work. These changes go above the getCurveCurval() changes and could be handled better, but they work.

# Determine which bone attributes are modified by this IPO block
					nb_curve = ipo.getNcurves()
					if nb_curve == 3:
						# Could also just be size
						has_loc = 1
						has_rot = 0
					elif nb_curve == 4:
						has_loc = 0
						has_rot = 1
					else:
						has_loc = 1
						has_rot = 1
					#has_loc = nb_curve in (3, 7)
					#has_rot = nb_curve in (4, 7)
#33
03/01/2004 (8:35 pm)
Thanks,
I will get check it out in a bit.

I did some checking on the interface to Blender. It looks as though you can access and change the current animation frame. It also looks like you can access the bones themselves and get the current quaternion value for it. The trick is determining which ones are used in the animation sequence. Perhaps this could be done by looking at the sequence and seeing which bones have been inserted into the action at which frames. Then set the frame for the position you want, and instead of using the IPOs get the quaternion from the bone itself. I am guessing that blender will have the actual quaternion value rather than what gets put into the IPO. I will have to hack a small program to view the bone data and see if it changes when you move the bone by changing the frames from a script. This could be the default way of getting the quaternion values for the bones regardless of how they are animated!

Later
#34
03/01/2004 (8:50 pm)
Yeah, that's pretty easy:
Blender.Set('curframe', frame)
  Blender.Redraw ();

The redraw is important to force the bones, meshes etc. to update and it definitely slows things up if you don't have to do this.

I was trying this and depending on what you're trying to do, it may not help you. If the bones are involved in an IK chain (and probably other constraints), they never actually rotate as far as this is concerned.

I tried this with:
import Blender
from Blender import Armature
 
def findBone (bones, name):
        for b in bones:
                if b.getName () == name:
                        return b
                if len (b.getChildren ()) > 0:
                        b = findBone (b.getChildren (), name)
                        if b is not None:
                                return b
        return None
 
arm = Armature.Get ('Skeleton')
bicep = findBone (arm.getBones (), 'Bicep.R')
print bicep
 
for i in range (1,22):
        Blender.Set ('curframe', i)
        Blender.Redraw ()
        #print bicep.getQuat (), bicep.getRoll (), bicep.getLoc ()
        print bicep.getHead (), bicep.getTail ()

Unfortunately none of these values changed even though the bone was definitely animated through the IK chain. There might be another way of grabbing this information, but the above doesn't work.

I know with meshes, it matters whether you call NMesh.GetRaw() or NMesh.GetRawFromObject(). If there's something like that for armtures, it might resolve this.
#35
03/02/2004 (3:10 am)
Zaz,

Great work on improving the exporter :)
As far as i can tell, getting the values from the IPO curves is the only decent way to get the bone animation data out of blender.

On a sidenote, if anyone is intrested in the latest exporter code (which i have not yet released properly), i have put up a snapshot at :
www.kitsuneaye.co.uk/blender/exporter
#36
03/05/2004 (1:21 am)
Greetings,
I have been trying to get my Blender constraint based animations to work with the exporter. I built a simple two cube model with an armature made of 4 bones, plus one more for a constraint handle. I first animated one of the bones without constraints to watch the object animating. This worked fine. Next I created an action that caused the same bone to animate, but through a constraint on the seperate constraint bone. Then I "baked" this animation. This created an animation set of all bones with the "BAKED" keyword in it. I renamed the original animation in order to keep it seperate in the exporters eyes. Then I renamed the IPO for the bone of interest to remove the BAKED wording. I also removed the BAKED keyword from the action name. This is where is gets screwy. I then try to export and I am getting this error from the scipt:

DTS Exporter 0.5
Exporting to: /home/demolishun/torque/blender_shapes/test_bake2.dts
> Adding Armatures
Armature : Armature
^^ Bone [BRoot] (parent 0)
^^ Bone [Bone] (parent 1)
^^ Bone [Bone.001] (parent 2)
^^ Bone [Bone.002] (parent 3)
^^ Bone [BoneIK] (parent 0)
Armature : Root
> Adding Materials
> Adding Objects
Object : Cube
Type : Standard
Import : Cube
Object : Cube.001
Type : Standard
Import : Cube.001
> Adding 1 Detail Levels
> Adding Sequences...
Sequence : Action (Flags : 17)
Frames: 15
Track: BAKED.Bone (node 0)
Track: BAKED.Bone.Bone (node 0)
Track: BAKED.BRoot (node 0)
Track: BAKED.BoneIK (node 0)
Track: Bone.001 (node 3)
> Finishing... Done.
Writing out DTS file...
Traceback (most recent call last):
File "/home/demolishun/blender_dir/Python_Torque/Dts_Blender_Gui.py", line 693, in button_event
if res != 1: # If not handled
File "/home/demolishun/blender_dir/Python_Torque/Dts_Blender_Gui.py", line 173, in handleMainSheet
Dts_Blender.Prefs.generateTriangleStrips = toggleTruth(Dts_Blender.Prefs.generateTriangleStrips)
File "/home/demolishun/blender_dir/Python_Torque/Dts_Blender.py", line 921, in exportModel
Shape.write(Stream)
File "/home/demolishun/blender_dir/Python_Torque/Dts_Shape.py", line 760, in write
dstream.writeQuat16(cnt)
File "/home/demolishun/blender_dir/Python_Torque/Dts_Stream.py", line 340, in writeQuat16
self.writes16(q16.y)
File "/home/demolishun/blender_dir/Python_Torque/Dts_Stream.py", line 244, in writes16
self.write16(value)
File "/home/demolishun/blender_dir/Python_Torque/Dts_Stream.py", line 179, in write16
self.buffer16.append(value)
OverflowError: signed short integer is less than minimum

I am not sure why it is crapping out on this. As you can see that the bone of interest "bone.001" seems to be getting data for it, but the quaternion section seems to die.

Thanks for any input,
Frank
#37
03/05/2004 (1:29 am)
Read my posts above, I explain why what you're trying to do won't work, at least without the patches I posted above also.
#38
03/05/2004 (7:40 am)
Frank,

I have encountered the signed short overflow before.
Basically, torque writes the rotation for the bones as 16bit quaternions (instead of 32bit float versions) to reduce the file size.

Unfortunatly, these values are more limited, so you get the overflow error.
The only way i solved this was either redoing the animation (being a bit more careful), or redoing the whole thing.

Zaz,

I'm not quite sure how this relates to your issues. Is it because of the order of the ipo curve values in BAKED anims?
On a sidenote, have your changes been intergrated into blender at all yet?
P.s i have node and animation renaming(controlled via the .cfg), aswell as "BAKED" ipo name ignoring in the snpshot.
#39
03/05/2004 (9:45 am)
Ok, late night/early AM post there, I got a bit terse, :-).

My posts above don't directly deal with his python stack trace, at least my more recent posts. What I was trying to say was that even if he got around the issue causing the stack trace, what he was trying to do wouldn't work at least because of the ordering of the BAKED ipo curves unless he's hacked the code to handle this.

No, the patches above have not made into blender yet. I sent my IPO curve patch directly to bf-committers and asked if that was the right place. I never know, a few of my patches have been grabbed immediately when I've done that. Some of them never get a comment at all. Unfortunately this one didn't get commented on at all. Its probably time to either file a bug report or to maybe send it to the python list.

I see two ways of working around this though. The simplest way would be to just put a toggle button on the sequence to indicate if its BAKED or not and if it is, assume the other ordering of the curves in Dts_Blender.py. You need to make changes similar to what I have above in your computation about has_loc and has_rot, i.e. BAKED actions have all 10 ipo curves and the 0.5 code won't find any locs or rots in actions with 10 curves.

The second way around this is what I originally implemented. I made a map of the curves and made a few assumptions if I got exceptions. I used this code originally and only made the ipo curve patch for blender because I wasn't getting correct results. I think now though that I was getting the correct curves and something else was breaking things for me on that complex armature. I can dig this code up if you're interested as its just commented out in my .py files here.

Hmm, you could even generalize this idea more if you wanted. You could let the user set the starting curve number for loc, rot and size curves, then use those starting numbers in Dts_Blender.py when you read the values. Yeah, this would complicate the usage of the scripts for non-techies, so maybe this would go on an advanced tab or something.

BTW, do torque skeletons/node support size changes in the sequences ? Initially I assumed it didn't because you didn't export size, but looking at the torque code the other day, it looks like it does. If it does, are you considering exporting size at some point ?

BTW, if you decide to add the BAKED toggle button on the sequnce screen, consider adding a toggle button that enables/disables exporting of that sequence. Some of the constraints I use are Action constraints and shouldn't be exported. Also, when you BAKE an action you'll want to export the baked version and NOT export the unbaked version.

This was on my todo list to implement, but I spent all my available time for blender and torque last week trying to determine why my complex armature and its baked actions weren't exporting properly. Oh well, another week or so and I get some time back, :-).
#40
03/05/2004 (10:23 am)
Zaz,

Torque supports size on the bones. (See Dts_Shape and the Sequence Class for pointers).
However, i did not implement it because :
1) I did not know how to properly deal with getting the size ipo's out of blender.
2) I don't need sizing at the moment.

Banning exporting of certain sequences should be easy to do... i'll have a go at that tonight.