Animation and Movie file format proposal

35 replies [Last post]
Joined: 11/04/2008

So as you know, I documented the animation and movie game binary file formats recently.

To summarise, an animation (.anim.binltl) is a sequence of keyframes affecting scale, rotation, position, alpha, colour and sound. But it doesn't specify what is animated, only how it is animated. It's used for animating single elements, e.g. an object in a level. A good example is rot_1fps with rotates 360 degrees every second and is used in many levels.

A movie (.movie.binltl) is basically one of the game cut-scenes (though there are a few special-case ones like end-of-level movies). It consists of a sequence of actors (images or text) each of which is animated according to an animation in the same format as above.

There's no magical encryption to these files, they're simply binary data that corresponds exactly to the structs used by the game itself. This makes it impossible for us to provide a standard "encrypt/decrypt" function for addin authors. So on the (seven hour) train ride today I spent some time perfecting my .binltl import and coming up with an XML file format describing both animations and movies.

The first thing I came up with was a direct like-for-like translation of the binary formats to XML. Check out anim_and_movie_v1.zip of both animations and movies.

Then I analysed the existing files a little more closely and came up with the following observations:

Keyframes

Even though a keyframe has fields for alpha, colour, angle, position, and sound strings, I have confirmed the following as true for all existing movies and animations:

  • x,y are always -1 except for scale and translate transform keyframes. For those they are never -1
  • angle is always -1 except for rotate transform keyframes, where it is never -1
  • alpha is always -1 except for alpha keyframes, where it is never -1
  • colour is always -1. On the few anims where colour keyframes exist (e.g. treeBlow_trunk) there is only one keyframe and its colour value is still -1. It's not used at all on movies, only animations. Not sure whether colour frames therefore have any purpose.
  • soundStr is always 0 except for sound keyframes. 0 is an offset in the string table to a null-terminated string of length 0. Sound keyframes only ever exist in movies, not raw anims.

Transforms

  • For movies, there are always exactly three transforms, and they're always SCALE followed by ROTATE followed by TRANSLATE (which is of course the order you want when doing matrix multiplication)
  • For anims, this isn't always the case. Specifically, ball_counter_ocd breaks this by having SCALE,SCALE,ROTATE,TRANSLATE (the first scale is 2.0,2.0) and rot_1rps breaks it by having no scale or translate keyframes at all.

Frame congruency

For movies, there are always a matching set of transform, alpha and colour frames, each frame having the same nextframe index and same interpolation type.

This doesn't apply to anims. e.g. some only have a single alpha=255 frame for their whole duration.

This doesn't apply to sound frames in movies either, which have their own existence. The sound frames are inserted at the point when the sound needs to play, even if no other types of frame exist at this point in time. The other frame types then skip over this frame via nextframe.

Sound frames are normally (but not always) added to the last actor in a movie, but in every case there's never more than one actor with sound frames.

Interpolation

Interpolation is from the current item to its nextframe, not from the previous frame to this frame.

Interpolation is consistent within a movie across all keyframe types.

Analysis

All these things seem to imply that anims were created by hand or script, whereas movies were probably exported from some other format.

In movies, if you take out all the sound frames and then shuffle down empty keyframes, the frames are all consecutive per actor in a movie and so nextframe is superfluous (this isn't true for anims, specifically ball_counter_ocd with its dual SCALE transforms).

All actors have a final keyframe that coincides with the movie's end time, even if nothing changes between their last "real" keyframe and the movie end. Example:

<movie length="31.571428">
 <actor type="image" depth="0.0" align="center">
   <image id="IMAGE_MOVIE_CHAPTER1END_BLACK32"/>
   <simple-animation>
     <keyframe time="0.0" x="547.5" y="300.0" angle="0.0" scale-x="34.218964" scale-y="18.750275" alpha="255"/>
     <keyframe time="31.571428" x="547.5" y="300.0" angle="0.0" scale-x="34.218964" scale-y="18.750275" alpha="255"/>
   </simple-animation>
 </actor>

The following facts are true for all movie files (I define this as "simple" animation):

    // A simple animation has:
    // 1. No colour, and no sound.
    // 2. Has transforms, exactly 3 of them: SCALE, ROTATE and TRANSLATE in that order
    // 3. No skipped keyframes. So all frames are present for all transforms. And they're all sequential, ending with -1.
    // 4. Has alpha, and no skipped frames there either. And an alpha frame exists for each transform frame, and vice versa.

So to simplify the XML for the most common cases, I created an <animation> element, which has a simple list of unnumbered keyframes, all with alpha. All movies can be generated into "simple" animations.

Some but not all anims can be generated into "simple" animations. The remainder retain the original format of individual keyframes of each type, within a <complex-animation> animation.

I therefore came up with a simplified and less verbose form for the common case.

I would very much appreciate your feedback on the anim_and_movie_v2.zip file format.

Note in particular that, since sound frames only ever get "attached" to an existing actor, I've removed them from the animation entirely and seperated them out into their own "sounds" element, and removed any remaining redundant keyframes from the original element.

I'm not too happy with having both animation and complex-animation but I think it's the best way to achieve full flexibility where required, without making "normal usage" overly verbose. And at the end of the day it's only a bit more code for GooTool.

Upon consensus this will become the standard format for ".anim.xml" and ".movie.xml" in the goomod specification version 1.1 and will be implemented in GooTool in the next release.

NB NB NB Don't bother coding anything to this format yet! It's liable to change, and if we get consensus I'll document it properly.

-davidc

Joined: 11/04/2008

Last call for comments. Work on the next version of GooTool is now underway and will include animation and movie compilation.

Joined: 11/04/2008

Beginning to take form:

Joined: 03/22/2009

Genius, pure genius.

Joined: 04/10/2009

Did you try to google it?

Crazeh man!

Joined: 04/10/2009

sorry Sad

Crazeh man!

Joined: 07/21/2009

wtf is this?

Joined: 04/10/2009

it's the code of the 2dboylogo.movie.bintl i got this code when i opened it with notepad

Crazeh man!

Joined: 11/04/2008

Here's the source of the xml->flash converter script, as far as I got with it (producing the movie above). Obviously flash->xml is much more important but I haven't done anything with that.

This runs as an addon inside Flash CS4 so you need the Flash editor installed to do anything with it. The file goes in C:\Users\[user]\AppData\Local\Adobe\Flash CS4\en\Configuration\Commands\Import WoG Movie.jsfl

fl.outputPanel.clear()
 
//var movie = selectMovie();
var movie = FLfile.read("file:///C|/Dev/gootool/movie/2dboyLogo.movie.xml");
 
var resourcesXml = FLfile.read("file:///C|/Dev/wog-extracted/res/movie/2dboyLogo/2dboyLogo.resrc.xml");
var rootDir = "file:///C|/Games/WorldOfGoo1.30/";
 
var resources = readResources(resourcesXml, rootDir);
 
if (movie) {
  fl.trace("IMPORT STARTING----->");
  processMovie(movie, resources);
}
else {
  fl.trace("Movie not found");
}
 
function selectMovie()
{
  var obsoleteDWPreviewAreaObject = {};
  var macFormatStr = "XML File|TEXT[*.xml||";
  var winFormatStr = "XML File|*.xml||";
  fileURL = fl.browseForFileURL("open", "Open WoG Movie XML", obsoleteDWPreviewAreaObject, macFormatStr, winFormatStr);
  if (!fileURL || !fileURL.length)
    return;
  //	alert("No file selected");
  //	fl.trace(fileURL);
 
  var ending = fileURL.slice(-4);
  if (FLfile.exists(fileURL) && ending == '.xml')
  {
    var contents = FLfile.read(fileURL);
    //		fl.trace(contents);
    return contents;
  }
}
 
function processMovie(movie, resources)
{
  var doc = fl.createDocument();
  if (!doc)
    return;
 
  doc.width = 800;
  doc.height = 600;
  var timeline = doc.getTimeline();
 
  var fps = 30; // TODO detect
 
  doc.frameRate = fps;
 
  var movieXml = new XML(movie);
 
  var lastFrameTime = movieXml.attribute("length")[0];
  var lastFrame = Math.round(lastFrameTime * fps);
  fl.trace("lastFrame = " + lastFrame);
 
  var actors = movieXml.descendants("actor");
  fl.trace("actors:" + actors.length());
 
  var importedAlready = Array();
 
  for (var i = 0; i < actors.length(); ++i) {
    var actor = actors[i];
 
    var type = actor.attribute("type")[0];
    var layerName;
    var libItem;
    if (type == 'image') {
      var imageId = actor.descendants("image")[0].attribute("id")[0];
      layerName = imageId.toLowerCase();
      if (layerName.substr(0, 12) == "image_movie_")
        layerName = layerName.substr(12);
      layerName = layerName.substr(layerName.indexOf("_") + 1).replace("_", " ");
      if (!resources[imageId]) {
        alert("Can't find image for resource " + imageId + ", aborting");
        return;
      }
 
      libItem = importedAlready[imageId];
      if (!libItem) {
        doc.importFile(resources[imageId], true);
        //weak....
        var libItemIndex = doc.library.findItemIndex(resources[imageId].substr(resources[imageId].lastIndexOf('/') + 1));
        libItem = doc.library.items[libItemIndex];
        importedAlready[imageId] = libItem;
      }
    }
    else {
      alert("Unknown actor type " + type + ", aborting");
      return;
    }
    //		fl.trace(actor);
    var layerNum = timeline.addNewLayer(layerName);
    //		layer
 
    var keyframes = actor.descendants("keyframe");
 
    //		timeline.insertBlankKeyframe(lastFrame);
    //		timeline.createMotionTween(0, lastFrame);
 
    var prevFrame = 0;
 
    var lastX = 0;
    var lastY = 0;
    var lastAngle;
    var lastScaleX;
    var lastScaleY;
    var element;
 
    for (var j = 0; j < keyframes.length(); ++j) {
 
      var x = parseFloat(keyframes[j].attribute("x")[0]);
      var y = parseFloat(keyframes[j].attribute("y")[0]);
      var angle = 360 - parseFloat(keyframes[j].attribute("angle")[0]);
      var scaleX = parseFloat(keyframes[j].attribute("scale-x")[0]);
      if (!scaleX) scaleX = 1;
      var scaleY = parseFloat(keyframes[j].attribute("scale-y")[0]);
      if (!scaleY) scaleY = 1;
      var alpha = (parseInt(keyframes[j].attribute("alpha")[0]) * 100) / 255;
 
      var time = keyframes[j].attribute("time")[0];
      var frame = Math.round(time * fps);
 
      fl.trace("frame = " + frame + ", x = " + x + ", y = " + y + ", alpha = " + alpha);
      fl.trace("scale-x = " + scaleX + ", scale-y = " + scaleY + ", angle = " + angle);
 
      //			var layer = timeline.layers[layerNum];
      //			var frame = layer.frames[frameIndex];
 
      timeline.setSelectedFrames(frame, frame + 1);
      timeline.setSelectedLayers(layerNum);
      timeline.currentFrame = frame;
      if (j == 0) {
        fl.trace("adding " + libItem.name);
        doc.addItem({x:0,y:0}, libItem);
        element = timeline.layers[layerNum].frames[0].elements[0];
        //			timeline.createMotionTween(0, lastFrame);
      }
      else {
        timeline.insertKeyframe(frame);
      }
 
      //			timeline.layers[layerNum].frames[frame].tweenType = 'motion';
 
      var instance = timeline.layers[layerNum].frames[frame].elements[0];
 
      fl.trace("instance of " + instance.libraryItem.name + " on layer " + layerNum);
 
      //			instance.depth = parseFloat(actor.attribute("depth")[0]);
      //			instance.left = x - (instance.width/2);
      //			instance.top = y - (instance.height/2);
      doc.selection = [instance];
      //doc.selectNone();
      //instance.selected = true;
      fl.trace("sel = " + doc.selection);
      fl.trace("move by y = " + (y - lastY));
      doc.moveSelectionBy({x:(x - lastX), y:(y - lastY)});
      doc.setInstanceAlpha(alpha);
 
      instance.scaleX = scaleX;
      instance.scaleY = scaleY;
      instance.rotation = angle;
      //	doc.scaleSelection(scaleX, scaleY);
      //	doc.rotateSelection(angle);//-lastAngle);
 
      lastX = x;
      lastY = y;
      lastAngle = angle;
      lastScaleX = scaleX;
      lastScaleY = scaleY;
 
      //					timeline.setFrameProperty("tweenType", "motion");
      prevFrame = j;
    }
 
    //		timeline.setSelectedFrames(0, lastFrame);
    //		timeline.setFrameProperty("tweenType", "motion");
    //		timeline.createMotionTween(0, lastFrame);
    //if (i == 5) break;
  }
  // delete the original layer
  timeline.deleteLayer(actors.length());
}
 
function readResources(resourcesString, rootDir)
{
  var resourcesXml = new XML(resourcesString);
  var resources = Array();
  var resourcesNodes = resourcesXml.descendants("Resources");
 
  for (var i = 0; i < resourcesNodes.length(); ++i) {
    var resourcesEl = resourcesNodes[i];
 
    var defaultPath = rootDir;
    var defaultIdPrefix = "";
 
    for (var j = 0; j < resourcesEl.children().length(); ++j) {
      var node = resourcesEl.children()[j];
      if (node.name() == "SetDefaults") {
        defaultPath = rootDir + node.attribute("path")[0];
        defaultIdPrefix = node.attribute("idprefix")[0];
      }
      else if (node.name() == "Image") {
        var id = defaultIdPrefix + node.attribute("id")[0];
        var f = defaultPath + node.attribute("path")[0] + ".png";
        resources[id] = f;
        //				fl.trace(id + "->" + f);
      }
      else if (node.name() == "Sound") {
        var id = defaultIdPrefix + node.attribute("id")[0];
        var f = defaultPath + node.attribute("path")[0] + ".ogg";
        resources[id] = f;
        //				fl.trace(id + "->" + f);
      }
    }
  }
  return resources;
}

Joined: 12/31/2009

Why does "presents" keep on stickin' to the top left?

Joined: 11/04/2008

Because I didn't implement visibility, so any item that hasn't been displayed yet is display at coordinates (0,0 ) at the top left.

Joined: 10/16/2009

2D Boy made a complex system for all the files in World of Goo. It makes me think that maybe THEY have some sort of level editor or animator that produces files in their formats. What is the probability of this being true?

I'll tell you, it's VERY HIGH.

Joined: 03/31/2009

Nooo, they made their levels in paint. And then manually wrote xmls...

My Gooish profile | Videos on YouTube | My WOG Mods

Joined: 12/31/2009

They do a really good job of it to have that kind of art.

Joined: 10/16/2009

Pavke wrote:
Nooo, they made their levels in paint. And then manually wrote xmls...

I thought that the xml encryption was just a creation of Mr. Croft...

Joined: 03/31/2009

Ron and Kyle first wrote xmls manually as ordinary text xml and then they encrypted them. After the game came out, few people including Soultaker cracked the encryption. I think davidc just implemented their formula in gootool.

My Gooish profile | Videos on YouTube | My WOG Mods

thB
thB's picture
ContributorAddin AuthorKleptomaniacToo Much Free TimeSerious OCD
Joined: 04/17/2009

The game was released with hand-written encrypted XMLs. Someone "cracked" the encryption to make editing the game possible. Remember that user-generated content wouldn't have been possible without this step. When davidc wrote GooTool, he implemented en-/decrypting as well, because otherwise GooTool wouldn't work in the first place.

my gooey profile | my video channel | author of Hazardous Environment

Joined: 10/16/2009

thB wrote:
The game was released with hand-written encrypted XMLs. Someone "cracked" the encryption to make editing the game possible. Remember that user-generated content wouldn't have been possible without this step. When davidc wrote GooTool, he implemented en-/decrypting as well, because otherwise GooTool wouldn't work in the first place.

If those functions weren't there in the first place, GooTool wouldn't even be a possiblity, well, except for the override folder in the addins. That wouldn't really need encryptions.

Joined: 03/31/2009

Enchanter49 wrote:

If those functions weren't there in the first place, GooTool wouldn't even be a possiblity, well, except for the override folder in the addins. That wouldn't really need encryptions.

But how would you make/write that addin?

My Gooish profile | Videos on YouTube | My WOG Mods

Joined: 10/16/2009

Pavke wrote:
Enchanter49 wrote:

If those functions weren't there in the first place, GooTool wouldn't even be a possiblity, well, except for the override folder in the addins. That wouldn't really need encryptions.

But how would you make/write that addin?

The override folder won't need encryptions. The addin.xml is still just an xml and can withdraw data no matter what.

Joined: 12/11/2009

davidc, why you put to Goo Tool DECRYPTING animations without posibility of ENCRYPTING?
I couldn't try to make custom animations! And I don't know if I understand it...

Joined: 11/04/2008

Encryption is missing until the file format is finalised. And more to the point it was decidedly harder to do than decrypting, particularly since I wanted to simultaneously release a flash exporter. It's not a high priority right now. The flash exporter script is in the GooTool source bundle at /flash.jsfl I think, if someone wants to work on it.

-davidc

Joined: 11/29/2009

When did this encryption/decryption thing come in? Wasn't the animations unencrypted by default? (Or are we talking about decoding/encoding? If so, please use the right terminology to reduce confusion.)

Author of World of Goo Portable. Download here!

Joined: 11/04/2008

Correct, he meant decoding/encoding. I can't remember if the files are encrypted or not (I think not), but the fundamental problem with them was their binary file format (which is also different on Linux 64-bit IIRC, since it's direct correspondence to C structs).

Possibly confusion arose because of the GooTool menus being named Encrypt/Decrypt which I will fix in the next release.

-davidc

Joined: 06/19/2009

I've been pondering the original purpose of this thread...
OK, I'm about a year late... but ....

Seems to me that with just 1 exception, and 1 "unknown", the stand-alone animations could all be decoded to a slightly modified version of the simple animation.

The exception is ball_counter_ocd with it's double scale, but I think that's just "laziness" on the part of 2DBoy. Likely sometime during development / beta someone said "Hey can you make the OCD! notification bigger"... and the easiest way to do that was just to add an extra initial Scale x 2 transform, rather than redo the animation from scratch, or modify all those scale values individually.

The modification to the spec/format is simply...
not every attribute / transform is required for every frame.

When decoding, if an attribute is missing from a frame (skipped by NextFrame, or maybe an entire transform is missing) that attribute is omitted from the output. (Like you do when interpolation=None in movie anims)
The encoding would simply set the "next frame" for that attribute to correspond to the next frame in which the given attribute did appear.

The "unknown" (at least by me) is whether interpolation is entirely "per frame" or whether it can be set "per attribute" as well. The file format would suggest that "per attribute" is possible, but as we see interpolation is uniform across the various tranforms, alpha etc in any frame.

Joined: 08/06/2010

How can you open the binltl files themselves? I tried using a hex editor, but it said something about a weird encoding.

Another Planet finally has an official release! Download chapters 1 through 3 here! Thank you for waiting so long while I kept starting over.

Joined: 06/19/2009

Regardless of whether you use a hex editor or notepad or whatever, looking at any binltl file "directly" is pretty pointless.
These files a different from .bin ... they are not encrypted xml, they are raw binary data in a 2DBoy "custom" format. The exact format and contents depend on what the file is... they are used on the MAC version to store all the game images, and on all versions to store animation and movie data.
At some point, the animation format was "cracked" / "investigated", and we found out what everything meant, but that didn't really help very much because the data is still all just numbers.. there's no text hiding in the files that you can "decode / decrypt"
So davidc came up with a conversion process to decode the information into an XML format.. and you can use GooTool's advanced Decrypt menu to do that.
However there's currently no encoding process available... so creating your own animations and movies is "near enough" impossible.

Joined: 09/01/2009

Ouch.
We're hoping to have custom animations in WOG2. I guess I didn't realize how tricky this would be... Sad

Joined: 12/11/2009

There's a possibility of making custom movies, but we need encrypting program for it...
Goo Tool can only decrypt it for now...

Joined: 08/06/2010

The data has to be some sort of text encoding, because of the txt dumps of Chapter5End and Credits. How were those made?

Another Planet finally has an official release! Download chapters 1 through 3 here! Thank you for waiting so long while I kept starting over.

Joined: 12/11/2009

I have decrypted evey movie with Goo Tool and every text/xml file is correct.
Realy usefull is Notepad++!