dalboris / vpaint

Experimental vector graphics and 2D animation editor

Home Page:http://www.vpaint.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Documentation of vec file format

skvamme opened this issue · comments

commented

Is there a specification of the vec file format available?

Hi @skvamme , no there's currently no documentation of the vec file format yet. Here is a quick overview:

It's an XML file with the following structure:

<vec ... >
  <playback ... />
  <canvas ... />
  <layer ... >
    <background ... />
    <objects>
      ...
    </objects>
  </layer>
  ... more layers ...
</vec>

Most attributes of these elements are quite self-explanatory by opening a *.vec file with a text editor. The most complicated part of the file, but also the most important, is the content of the <objects> element. Besides the background, each layer is composed of a list of fundamental "objects", also called "cells" in this paper:

https://www.youtube.com/watch?v=Xk1_CugdytI&feature=emb_title&ab_channel=BorisDalstein
https://www.borisdalstein.com/research/vac/vac.pdf

There are 6 fundamental types of cells:

  • key vertex (<vertex>): a given position at a given frame

  • key edge (<edge>): a given curve at a given frame. It can be open ( = starts and ends at key vertices, possibly equal), or closed (a pure loop with no start or end vertices at all)

  • key face (<face>): a given area at a given frame. It is defined by one or more "cycles" of key edges. Often, there is only one cycle: the outer boundary of the face. More cycles can be used to define interior holes. It is also possible to define a face with zero cycle, although it's quite useless.

  • inbetween vertex (<inbetweenvertex>): an interpolation in time between two key vertices at two different frames.

  • inbetween edge (<inbetweenedge>): an interpolation in time between two paths of key edges at two different frames. The simplest case is an interpolation from one key edge to another key edge, but it's possible for example the interpolate from one key edge to two key edges, which allows to define an edge which is split in the middle by a vertex during the animation

  • inbetween face (<inbetweenface>): an interpolation in time between two sets of faces at two different frames. Again, the simplest case is interpolating from one key face to another key face with the same number of key edges in their boundary, but the general case is much more complicated, see the paper.

The <objects> element contains a list of these, in back to front order (elements are rendered in order of appearance). Typically, faces should appear first, then edges, then vertices, so that the boundary edges of a face are drawn after the face.

I don't have the time this week to detail the syntax of all attributes of all these objects, so just let me know if you have specific questions and I'll be happy to answer.

commented

Thank you, exactly what i was looking for.

commented

Inbetween faces are not stored explicitly in the vec-file. Is there a way to include these in the vec-file? Are they stored internally in the same way as the key faces or is it hard to get the coordinates?

It is indeed not easy, given the ID of an inbetween face and given a time t, to obtain a list of 2D coordinates representing the geometric curve of the boundary of the face at time t. When I say "not easy", I mean "not easy for humans to understand". The algorithm itself is reasonably short and fast to execute. VPaint internally caches this computation, but it doesn't make sense to include these coordinates in the vec files because it is redundant, it would make the file sizes explode, and more importantly you may want to play the animation at a different fps as the "authored fps".

Basically, an inbetween face is defined by its "boundary". This is similar to key faces, although the "boundary" of an inbetween face can be much more complex than the boundary of a key face. In the case of a key face, the boundary is basically defined by an ordered list of key edges. In the case of an inbetween face, the boundary is basically defined by an ordered list of inbetween edges, although the concept of "order" is much more complicated because they are ordered both in space and in time.

This complicated 2-dimensional order is stored via a data structure called an "animated cycle", which is some sort of graph where each node of the graph is basically an inbetween edge, and each of these nodes point to the "previous" and "next" node (spatial order), and the "before" and "after" node (temporal order). See Figure 5 of the SIGGRAPH paper:

image

In practice, for reasons explained in the paper, you can't just use the inbetween edges as "nodes" of these animated cycles. You also need to add the inbetween vertices, key edges, and key vertices in the structure, so at the end the data structure that encodes the boundary of a given inbetween face looks like this (still in Figure 5):

image

This data structure is encoded in the cycles attribute of an inbetweenface. The syntax is a bit weird, but basically it's a list of "nodes", each looking like this:

node_id:(cell_id[+|-], previous_node_id, next_node_id, before_node_id, after_node_id)

Example:

image

In this example, there is a triangle (3 key vertices + 3 key edges + 1 key face) which has been motion pasted. So in total, the animation has 6 key vertices, 6 key edges, 2 key faces, 3 inbetween vertices, 3 inbetween edges, and 1 inbetween face.

The cell ID of the inbetween face is 20, and its boundary is made of 3 inbetween vertices and 3 inbetween edges. You can see them in View > Advanced > Inspector, then selecting the inbetween face, and click "Show". In this visualization, time is represented from top to bottom, and space is represented from left to right. The cell IDs of the inbetween vertices are 14, 15, 16, and the cell IDs of the inbetween edges are 17, 18, 19.

Therefore, the "animated cycle" of the inbetween face is a graph with 6 nodes. Each nodes of this graph are attributed an ID from 1 to 6. Each node stores:

  • which cell this node refers to (note that different nodes may refer to the same cell)
  • in case the cell is a key edge of inbetween edge: which is the chosen direction for this edge ("+" and "-" signs)
  • a pointer to the "previous" node, given as a node ID
  • a pointer to the "next" node, given as a node ID
  • a pointer to the "before" node, given as a node ID
  • a pointer to the "after" node, given as a node ID

If a given node has no "before" of "after" node, it is stored as a NULL pointer internally, and represented as "_" in the file. In this simple example, all the "before" and "after" pointers are NULL.

So to get a list of 2D coordinates, you need to do the following:

  1. pick any node. For example node 1
  2. extract the geometry for this node. For example, node 1 refers to the inbetween edge 18.
  3. go to the "next" node. For example, the node next to 1 is node 3, which refers to the inbetween vertex 14.
  4. Repeat until you've completed the cycle (back at node 1).

In practice, this means you also need to have some algorithm to extract the geometry 2D coordinates of an inbetween edge or inbetween vertex at a given time t. This means doing some interpolation.

Finally, note that in the general case, even if a given node n exists at a given time t, then its "next node" pointer (or "previous node") may not exist at time t. This is because nodes may span different time-ranges. Therefore you may need to also traverse the "before node" and "after node" pointer to find the actual "next" node you are looking for.

In practice, if you know that a node n exists at time t, then to find its "next" or "previous" node that also exists at time t, you need to use these algorithms from page 6:

image

In Python-like pseudo code, it looks like this (this is what you need to do at step 3 above):

def find_previous_node(n, t):
    res = n.previous_node
    while not res.cell.exists_at_time(t):
        res = res.before_node
    return res

def find_next_node(n, t):
    res = n.next_node
    while not res.cell.exists_at_time(t):
        res = res.after_node
    return res

I hope it clarifies a little, but yeah, it's not super easy.

commented

Thank you for the explanation.

I am thinking of an application that can take a vec file as input and do something interesting.

I think it was Edsker Dijkstra who sad that if something is calculated, don't calculate it again. Adding a possibility in your code to export a vec file with all inbetweens is probably best for me as the calculation is already done. I understand that it is redundant information in VPaint but as input to a 3:rd party app it could be valuable to have the inbetweens in the file.

Can you point me to where in the source I should look for this cached inbetweens, and I'll see what I can come up with. I'm confident with C and Objecive C but not C++ but I'll give it a try.

If you wish to get the geometric contours of faces, you can call QList< QList<Eigen::Vector2d> > FaceCell::getSampling(Time time). This is a virtual method which can work both for key faces and inbetween faces. So you can iterate over all faces, iterate over all relevant times, and export those contours.

Feel free to try it, but be aware that I don't think I would merge such a change, even if it's an optional attribute. Encoding redundant information is quite bad engineering practice and usually lead to problems down the road. Computing these is really the job of the renderer, it has no place in the model specification. Otherwise, where should we draw the line? Should we also write in the file the cached computation of the thicken-outline of edges, instead of just specifying the centerline+width ? Should we write in the file the computation of the Miter/round/bevel joins between edges? Should we write the geometric outlines of text objects? Should we write the triangulations of all these things? Of course not, and SVG doesn't do it either.

Note that if I recall correctly, the geometric outline is actually not cached in VPaint. What is cached is actually the triangulation of the face. This means that if you call FaceCell::getSampling() it will actually recompute it, but it's reasonably fast so it shouldn't be a problem.

commented

I am sorry if I have expressed my question incorrectly. I am not asking you to merge anything, I just wanted to get the inbetweens in a format that I can read for my personal project.

I will try to use the virtual method, thank you. Again, I am not suggesting that you bring in redundant information in the vec file.

@skvamme Perfect, we're on the same page then :-) I was just warning you in case you had this in mind, to prevent future frustration.

The way to implement this is probably to call getSampling(t) in InbetweenFace::write_(), and write them as attribute using the XmlStreamWriter. To know which values of t you need, you can use Time t1 = beforeTime() and Time t2 = afterTime(), which you can convert from Time to int using t1.frame() and t2.frame(), then iterate over all the integers t strictly between those two.

commented

Thank you so much for your advice and for taking your time to do it!

Your VPaint project is for animation in the virtual space, my little project is for animation in the physical space, I'll let you know how it goes :)

Awesome, I can't wait to see the result then :-)

commented

Added a small utility for converting Autocad dxf to vec, in examples in my fork https://github.com/skvamme/vpaint. Example dxf and the resulting vec is a drawing of a sailboat mast foot.

Nice! Interesting choice of using Erlang ;-)

commented

Thanks :) Now when I have the profile of the mast foot in VPaint, I can insert key frames when the structure changes in the model and let VPaint create the inbetweens. Each frame will then be feed to a 3-d printer to produce the item.

3-d printers cannot print a 3-d model, they print slices of a 3-d model, one at a time. The VPaint mast foot animation will actually be slices of a 3-d model of the mast foot. That's the plan :)

Cool that's a super interesting use case. So if I understand the plan is:

  1. You import the "key frames" (or let's call them "key slices" in this case) in VPaint thanks to your dxf2vec script
  2. Then in VPaint, you specify the correspondences between key slices
  3. You save the file with some changes to VPaint's source code to export the geometry of inbetween slices
  4. You convert all these interpolated slices to gcode

Are you planning to directly generate some gcode with your own scripts, or planning to use intermediate representation?

commented

That's correct. Maybe implement export to gcode in VPaint, the same way as you currently export a sequence of PNG.

Yes, it's a cool use case, I don't know of any 3-D editor that allows free hand editing between key frames (slices).

Sounds like a great plan. Good luck and let me know how it goes! I'd love to see the first 3d-printed model (partially) designed in VPaint ;)

commented

Thanks :) I need this mast foot so there is really not much of a choice here.

commented

The View/Advanced/3D View is awesome, have a look at the proctor_heel_plug1.vec

commented

After a little Googling I think I will write vec2dxf and use https://github.com/SebKuzminsky/pycam or similar to get gcode.

Thanks for the comment on the 3D View! Does pycam does 3D printing too? I don't know the library, I just clicked out of curiosity but they only mention CNC machining.

Something you might also want to experiment with is to export the inbetween edges as a 3D mesh in the OBJ format: this option is in the 3D View Settings dialog of VPaint (currently in the master branch, it's not in VPaint 1.7 but will be in VPaint 1.8). But beware that it's not a closed mesh: it's only a "cylinder" without the top and bottom faces, and in your example there will be missing sections due to consecutive key frames without inbetween edges/faces between the two. It might be possible to cleanup the mesh in a tool like MeshLab to make it one closed mesh, then import the closed mesh in Ultimaker Cura to generate the gcode.

This seems a lot of work though, using a proper 3D modeling tool rather than VPaint is probably more effective despite the lack of per-section sculpting tool like in VPaint ;-)

commented

Thanks for your comments!
I have done a lot of CAD the last 30 years and 3D is a pain in the ass. The nice thing with VPaint is that it only takes a few key frames in 2D to get a 3D model. Editing a keyframe in VPaint is easy compared to editing a 3D-model in a 3D-modelling app.

So, pycam does only have to deal with 2D's from VPaint. 1000 2D's (it's about 40 sek of animation film).

So, interesting times ahead, dxf2vec is just about to be ok. vec2dxf will soon emerge :)

Thanks for your good work!

Great, then I'm looking forward to it! I'm just surprised that the advantages of VPaint are worth all the hassle to write custom code to make it work for a task it really wasn't designed for. But whatever works! This definitely means there's a gap in the market, someone should write a plugin in CAD software to allow sculpting cross-sections of a model à-la VPaint, there's nothing technically difficult, it would mostly take some time to design and implement an intuitive UI for the tool.

commented

I'll keep you posted :)

commented

I have a square from CAD and there is no gap. Still if I try to paint it, the ink leaks out. What am I doing wrong?
test.vec.zip

commented

Sorry for closing :)

There is a syntax error in the file, that's why:

image

commented

Ah, my bad :) Thanks!

There seems to be other problems, I deleted the superfluous /> in all edges, but now VPaint is showing nothing.

Nevermind, I "saved as" the wrong file. After removing the superfluous />, all edges show up correctly. However, the four edges are not connected properly for the paint bucket to work. You currently have 4 edges and 8 vertices. You need 4 edges and 4 vertices: the start/end vertex of each edge must be the same as the start/end vertex of another edge.

In the future, I am planning to have tools to be able to automatically "glue" the edges whose end vertices are within a certain threshold, but this isn't implemented yet.

commented

Ok, thanks, I'll fix that in dxf2vec.

commented

Deleted the duplicate vertices and paint bucket works, thanks!

Maybe I am fine with the VPaint PNG sequence export and do not have to fiddle with the VPaint source. 3-D printers works with PNG files and my 300 mm mastfoot takes 600 PNG files, 0.5 mm thick. In 24 fps that is 25 seconds.

I believe I've only scratched the surface of the vec file format. If you have any idea for dxf2vec please give me a hint :)