dalboris / vpaint

Experimental vector graphics and 2D animation editor
http://www.vpaint.org
Apache License 2.0
725 stars 54 forks source link

Documentation of vec file format #122

Open skvamme opened 3 years ago

skvamme commented 3 years ago

Is there a specification of the vec file format available?

dalboris commented 3 years ago

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:

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.

skvamme commented 3 years ago

Thank you, exactly what i was looking for.

skvamme commented 3 years ago

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?

dalboris commented 3 years ago

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:

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.

skvamme commented 3 years ago

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.

dalboris commented 3 years ago

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.

skvamme commented 3 years ago

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.

dalboris commented 3 years ago

@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.

skvamme commented 3 years ago

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 :)

dalboris commented 3 years ago

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

skvamme commented 3 years ago

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.

dalboris commented 3 years ago

Nice! Interesting choice of using Erlang ;-)

skvamme commented 3 years ago

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 :)

dalboris commented 3 years ago

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?

skvamme commented 3 years ago

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).

dalboris commented 3 years ago

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 ;)

skvamme commented 3 years ago

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

skvamme commented 3 years ago

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

skvamme commented 3 years ago

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

dalboris commented 3 years ago

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 ;-)

skvamme commented 3 years ago

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!

dalboris commented 3 years ago

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.

skvamme commented 3 years ago

I'll keep you posted :)

skvamme commented 3 years ago

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

skvamme commented 3 years ago

Sorry for closing :)

dalboris commented 3 years ago

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

image

skvamme commented 3 years ago

Ah, my bad :) Thanks!

dalboris commented 3 years ago

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

dalboris commented 3 years ago

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.

skvamme commented 3 years ago

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

skvamme commented 3 years ago

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 :)