Commit 6eeb6b80 by Simon Boyé

### Vitelotte doc: Fixes and enhancements. Plus small fixes in code.

```Vitelotte doc: Better tutorial images.

Vitelotte: Fixed finalize on edges and updated finalize doc.

parent 316ac2d1
.gitignore 0 → 100644
 __pycache__
 ... ... @@ -754,10 +754,8 @@ VGMesh<_Scalar, _Dim, _Chan>::finalizeEdge(Edge e) bool n0v = n0.isValid(); bool n1v = n1.isValid(); // if both are invalid, create a single node. Else, create // nodes independently, thus producing a discontinuity. if(!n0v && !isBoundary(h0)) n0 = addNode(); if(!n1v && !isBoundary(h1)) n1 = n0v? addNode(): n0; if(!n0v && !isBoundary(h0)) n0 = n1v? n1: addNode(); if(!n1v && !isBoundary(h1)) n1 = n0.isValid()? n0: addNode(); } if(hasEdgeGradient()) { ... ... @@ -766,10 +764,8 @@ VGMesh<_Scalar, _Dim, _Chan>::finalizeEdge(Edge e) bool n0v = n0.isValid(); bool n1v = n1.isValid(); // if both are invalid, create a single node. Else, create // nodes independently, thus producing a discontinuity. if(!n0v && !isBoundary(h0)) n0 = addNode(); if(!n1v && !isBoundary(h1)) n1 = n0v? addNode(): n0; if(!n0v && !isBoundary(h0)) n0 = n1v? n1: addNode(); if(!n1v && !isBoundary(h1)) n1 = n0.isValid()? n0: addNode(); } } ... ...
 ... ... @@ -18,8 +18,12 @@ namespace Vitelotte { /** * \brief A 1D piecewise linear function. */ template class PicewiseLinearFunction class PiecewiseLinearFunction { public: typedef _Value Value; ... ... @@ -32,7 +36,7 @@ public: typedef typename Samples::const_iterator ConstIterator; public: PicewiseLinearFunction() {} PiecewiseLinearFunction() {} bool empty() const { return m_samples.empty(); } unsigned size() const { return m_samples.size(); } ... ... @@ -57,6 +61,9 @@ private: }; /** * \brief A VGMesh with diffusion curves. */ template < typename _Scalar, int _Dims=2, int _Coeffs=4 > class DCMesh : public Vitelotte::VGMesh<_Scalar, _Dims, _Coeffs> { ... ... @@ -107,7 +114,7 @@ public: using Base::setGradientConstraint; using Base::removeGradientConstraint; typedef PicewiseLinearFunction ValueFunction; typedef PiecewiseLinearFunction ValueFunction; typedef Vitelotte::BezierPath BezierPath; ... ...
 ... ... @@ -12,8 +12,8 @@ namespace Vitelotte template typename PicewiseLinearFunction<_Value>::Value PicewiseLinearFunction<_Value>::operator()(float x) const typename PiecewiseLinearFunction<_Value>::Value PiecewiseLinearFunction<_Value>::operator()(float x) const { assert(!empty()); ... ...
 /* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ namespace Vitelotte { /*! \page vitelotte_user_manual_dc_mesh_page DCMesh The class DCMesh is an extension of VGMesh. It permits to manipulate nodes using diffusion curves defined on the mesh instead of directly manipulating nodes. It is the class used behind the scene in the [mvg_editor](@ref vitelotte_example_mvg_editor_page) example. DCMesh curves are simply paths of edges on the mesh. You can create a curve with DCMesh::addCurve and add halfedge to it with DCMesh::addHalfedgeToCurve. Halfedges must be added in order. Obviously, each curve has a set of extra information to define how it behave. First, you can use DCMesh::setFlags to set flags of a curve to a combination of DCMesh::VALUE_TEAR and DCMesh::GRADIENT_TEAR to mark the curve as respectively value discontinuous or/and gradient discontinuous (if EDGE_GRADIENT_NODE attribute is enabled). Then you can edit the [picewise linear functions](@ref PiecewiseLinearFunction) that constrain the value and the gradient on each side of a curve. If a PiecewiseLinearFunction object contains no sample, it means the values or gradients along the associated curve is unconstrained, otherwise it is constrained. Once you set the curves as you wish, you should call DCMesh::setNodesFromCurves. It will clear all nodes on the mesh and set nodes on halfedges belonging to a curve to reflect the curve constraints. You must call VGMesh::finalize after to get a valid input for the solver. You can also add point constraints. They can have a value constraint but also a gradient constraint if you use FVElements. */ }
 ... ... @@ -13,7 +13,7 @@ namespace Vitelotte { This tutorial will guide you through a simple program while introducing the main concepts of Vitelotte. We will load a `.obj` mesh in a VGMesh, then we will set a few punctual constraints and solve the diffusion before saving the result in a `.mvg`. While we will not manipulate diffusion curves for the sake of simplicity, we will show enough to get you started. The result can be visualized using the [mvg_viewer example](@ref vitelotte_example_mvg_viewer_page) program. \image html vitelotte/tutorial_solved.png "The final result." \image html vitelotte/tutorial_overview.svg "The final program takes a mesh as input, add some random color constraints and diffuse them using the solver." The complete source is available in `examples/Vitelotte/tutorial`. ... ... @@ -47,6 +47,8 @@ namespace Vitelotte { We will now pick a random set of vertices and assign a random color constraint to each one. The rest of the mesh must be as smooth as possible. So, all attachment points around a constrained vertex must point to the same constrained nodes, and all attachment points around the other vertices must point to their respective unknown node. FV elements also have some attachment points on the edges of the mesh that must be set correctly. \image html vitelotte/tutorial_constraints.svg "The first step is to add color constraints to some vectices." It may seems complicated, but in practice we will use the VGMesh::finalize method to do most of the job. All we have to do is to set _one_ attachment point somewhere around each constrained vertex before calling this method and we are done. This give us: \snippet Vitelotte/tutorial/tutorial.cpp Color dots and finalize ... ... @@ -68,6 +70,8 @@ namespace Vitelotte { The FemSolver class is parametrized with the mesh and an ElementBuilder. The element builder describes how to build the stiffness matrix and so varies in function of the element type we want to use. FVElementBuilder uses FV elements that allow to compute biharmonic diffusion and obtain quadratic color interpolation in the end. Element builders do not support singular elements directly, so we need to decorate them with the SingularElementDecorator. This may seem complicated, but in practice we always do this, so there is no need to worry about the details (unless you want to implement your own element type). \image html vitelotte/tutorial_solver.svg "The sover interpolates color constraints. Here, we do biharmonic interpolation." Solving is now straightforward: \snippet Vitelotte/tutorial/tutorial.cpp Solve the diffusion ... ... @@ -76,9 +80,6 @@ namespace Vitelotte { FemSolver::solve does what its name states. This will set a value to all unknown nodes, thus producing the final image. Note that the solver may fail, either because the input mesh is invalid or because the problem is numerically unstable, so check for errors. \image html vitelotte/tutorial_solved.png "The final result." \section vitelotte_user_manual_tutorial_output_sec Writing to mvg All is left to do is to save our VGMesh in the `.mvg` file format to load it with mvg_viewer. We will not describe the format here, it has [its own page](@ref vitelotte_user_manual_mvg_file_format_page). Just know that it is a simple text-based format inspired from the obj mesh format. ... ...
 ... ... @@ -127,26 +127,25 @@ namespace Vitelotte { When preparing the input of the solver, setting all nodes for each attachment point by hand can be tedious. We provide the method VGMesh::finalize() that takes a mesh with a few constraints set and "guesses" all the missing ones using heuristics. For edge nodes (EDGE_VALUE and EDGE_GRADIENT), the algorithm is straighforward. For each edge: Finalize process each vertex and each edge in turn. If attachment points around a vertex all reference an invalid node, a new unknown node is created an assigned to them, thus constraining the result to be continuous. Same thing goes for edges, if both edge-value nodes are invalid, they are set to a new unknown node, and similarly for edge-gradient nodes. - If both opposite nodes are invalid, a new unknown node is added and set on both side. - If one side is linked to the node `n` and the other is invalid, it is set to `n` too. - If both nodes are valid, nothing is done. \image html vitelotte/finalize_invalid.svg "Vertex and edges without valid nodes are bound to a new unknown node. Small black dots represent invalid nodes and numbers represent unknown nodes." In other words, to create a discontinuity both sides of the edge must be constrained. Else the edge is set to be smooth. Obviously, if two opposite nodes on an edge are set, they are left untouched. Vertices nodes (TO_VERTEX_VALUE and FROM_VERTEX_VALUE) are a bit more complicated. It first cycle through all edges to create a list of constrained edges. For each edges around each vertex, it applies the following rules: \image html vitelotte/finalize_edge_constraint.svg "If two opposite nodes on an edge are set, they are left untouched. Colored dot represent constraints. Different colors mean different constraints." - If both sides have invalid nodes, or one is invalid and the other is an unknown, or both are the _same_ unknown node, then both sides are set to invalid. - If one side is constrained and the other invalid, the constrained node is set on both side. - Else there are two different nodes on each sides and nothing is done. Vertices are a bit more complicated to process. Let's assume that edges around a vertex have either both of their nodes set or none. For each vertex, the algorithm starts from a valid node and turn around the vertex in search for the next valid node. Now we can set the invalid nodes in-between from the two boundary nodes using the following rules: In the end, both sides of the edge are either valid or none. Edges are added to the constraint list if they are linked to valid nodes. If there is no constraint around a vertex, a new unknown node is added to both sides of an arbitrary edge. Now every arcs (halfedges between two constrained edges) are handled separately. Note that the first and last halfedges have a node set and not the others. This leads to: \image html vitelotte/finalize_vertex_simple.svg "When both extremities are the same node, intermediate nodes are set to it." - If both extremities are the same, all halfedges of the arc are set to it. - If both nodes are different unknowns, one is discarded and all halfedges of the arc are set to the other. This mean that we disallow singularities here. It makes sense because the solver would try very hard to set the same value on all nodes here. Avoiding a singularity reduces the number of nodes and makes the solver faster and more robust. - If one node is unknown and the other is constrained, all halfedges of the arc are set to the constraint node and the unknown is discarded for the same reason as above. - If both nodes are constraints with different values, we call VGMesh::setSingularity. \image html vitelotte/finalize_vertex_singular.svg "When both extremities are two different constraint nodes, intermediate edges get their own constraint node." \image html vitelotte/finalize_vertex_cross.svg "When both extremities are two different unknown nodes, intermediate and the last nodes are set to the first one. This might lead to unexpected results in complex cases like non-local constraints." \image html vitelotte/finalize_vertex_continuous_edge.svg "Finally, when two side of an edge point to the same unknown node, they are ignored." What happen when, given a pair of opposite nodes, one is valid an the other is not ? The known node is simply assigned on both sides. However, it is recommended to avoid it when possible or to use it only on simplest cases, like in the tutorial. \warning This algorithm is not perfect. As it does simplifications (mainly replacing unknown nodes by others) it may not produce the intended result in case of non-local constraints (when a same unknown node is used on different places on the mesh). ... ...
bin/mvg.py 0 → 100755
 #!/usr/bin/python3 from numpy import ( array, identity, dot, concatenate, ) from numpy.linalg import ( norm, ) MvgLoaderError = RuntimeError TO_VALUE_FLAG = 1 FROM_VALUE_FLAG = 2 EDGE_VALUE_FLAG = 4 EDGE_GRADIENT_FLAG = 8 def always_true(*args, **kwargs): return True class Face: def __init__(self): self.verts = [] self.v_from = [] self.v_to = [] self.v_edge = [] self.g_edge = [] @staticmethod def parse_tokens(tokens, attrs): def parse_node(tok): if tok == "x": return None return int(tok) face = Face() tok_it = iter(tokens) for tok in tok_it: if tok == "-": break sub = tok.split('/') face.verts.append(int(sub[0])) if len(sub) > 1: face.v_to.append(parse_node(sub[1])) face.v_from.append(parse_node(sub[-1])) else: face.v_to.append(None) face.v_from.append(None) for tok in tok_it: sub = tok.split('/') i = 0 if attrs & EDGE_VALUE_FLAG: face.v_edge.append(parse_node(sub[i])) i += 1 if attrs & EDGE_GRADIENT_FLAG: face.g_edge.append(parse_node(sub[i])) i += 1 return face class Edge: def __init__(self, points, edge=None): self.points = points self.edge = edge class PlacedNode: def __init__(self, p, type_, v=None, attr=None, vx=None, edge=None): self.p = p self.type = type_ self.v = v self.attr = attr self.vx = vx self.edge = edge class MVG: def __init__(self): """Creates an empty mesh.""" self.attrs = 0 self.ndims = 0 self.ncoeffs = 0 self.verts = [] self.nodes = [] self.faces = [] @staticmethod def load_mvg(filename): """Loads an mvg file.""" in_file = open(filename) line_it = iter(in_file) line = next(line_it).strip() line_no = 1 def error(msg): raise MvgLoaderError("{}:{}: {}\n>> \"{}\"".format(filename, line_no, msg, line)) def error_if(cond, msg): if cond: error(msg) error_if(line != "mvg 1.0", "Bad header") mvg = MVG() for line in line_it: line_no += 1 line = line.strip() if len(line) == 0 or line[0] == "#": continue tokens = line.split() if tokens[0] == "attributes": error_if(len(tokens) != 2, "Wrong number of arguments") if tokens[1] == "none": mvg.attrs = 0 elif tokens[1] == "linear": mvg.attrs = TO_VALUE_FLAG | FROM_VALUE_FLAG elif tokens[1] == "quadratic": mvg.attrs = TO_VALUE_FLAG | FROM_VALUE_FLAG | EDGE_VALUE_FLAG elif tokens[1] == "morley": mvg.attrs = TO_VALUE_FLAG | FROM_VALUE_FLAG | EDGE_GRADIENT_FLAG elif tokens[1] == "fv": mvg.attrs = TO_VALUE_FLAG | FROM_VALUE_FLAG | EDGE_VALUE_FLAG | EDGE_GRADIENT_FLAG else: error("Unsupported attributes") elif tokens[0] == "dimensions": error_if(len(tokens) != 2, "Wrong number of arguments") mvg.ndims = int(tokens[1]) elif tokens[0] == "coefficients": error_if(len(tokens) != 2, "Wrong number of arguments") mvg.ncoeffs = int(tokens[1]) elif tokens[0] == "v": error_if(len(tokens) != mvg.ndims+1, "Wrong number of arguments") mvg.verts.append(array(tokens[1:], float)) elif tokens[0] == "n": if len(tokens) == 2 and tokens[1] == "void": mvg.nodes.append(None) else: error_if(len(tokens) != mvg.ncoeffs+1, "Wrong number of arguments") mvg.nodes.append(array(tokens[1:], float)) elif tokens[0] == "f": mvg.faces.append(Face.parse_tokens(tokens[1:], mvg.attrs)) return mvg def edges(self): edges = set() for f in self.faces: for i0 in range(len(f.verts)): i1 = (i0+1) % len(f.verts) if f.verts[i1] < f.verts[i0]: i0, i1 = i1, i0 e = (f.verts[i0], f.verts[i1]) if e not in edges: edges.add(e) yield Edge((self.verts[e[0]], self.verts[e[1]]), edge=e) def placed_nodes(self, radius, offset): def node(p, n, **kwargs): if n is None: return PlacedNode(p, "invalid", **kwargs) v = self.nodes[n] if v is None: return PlacedNode(p, "unknown", n, **kwargs) return PlacedNode(p, "constraint", v, **kwargs) for f in self.faces: for i0 in range(len(f.verts)): i1 = (i0+1) % len(f.verts) p0 = self.verts[f.verts[i0]] p1 = self.verts[f.verts[i1]] v = p1 - p0 v /= norm(v) n = array([v[1], -v[0]], float) if TO_VALUE_FLAG & self.attrs: yield node(p1 - v*radius + n*offset, f.v_to[i1], attr=TO_VALUE_FLAG, vx=i1) if FROM_VALUE_FLAG & self.attrs: yield node(p0 + v*radius + n*offset, f.v_from[i0], attr=FROM_VALUE_FLAG, vx=i0) if EDGE_VALUE_FLAG & self.attrs: yield node((p0 + p1) / 2 + n*offset, f.v_edge[i0], attr=EDGE_VALUE_FLAG, edge=(i0, i1))

This diff is collapsed.
This diff is collapsed.