OpenSubdiv/opensubdiv/sdc/OVERVIEW.txt
manuelk c399655dcc Landing 3.0.0.alpha
Sync'ing the 'dev' branch with the 'feature_3.0dev' branch at commit 68c6d11fc36761ae1a5e6cdc3457be16f2e9704a

The branch 'feature_3.0dev' is now locked and preserved for historical purposes.
2014-09-05 15:07:46 -07:00

339 lines
20 KiB
Plaintext

This text file is written to accompany the opensubdiv/sdc source during review. It is intended to
give a high level overview of the organization of the source, the rationale for various decisions,
as well as highlight open issues or situations where the choices made thus far warrant further
deliberation.
As previously noted, the primary goal of Sdc is to separate the core subdivision logic from Hbr
to facilate other classes internal (and later external) to OpenSubdiv generating consistent and
compliant results. For the purposes of this overview I've divided it into three sections, with
varying numbers of header files for each:
1) types, traits and options for the supported subdivision schemes
2) computations required to support semi-sharp creasing
3) computing mask weights for subdivided vertices for all schemes
Each section will reference a specific subset of a small number of headers. Comments within the
headers may elaborate on the details provided in the synopses, but enough context will be provided
here to raise the main issues -- the source should then serve as an illustration.
Overall the approach taken was to extract the functionality at as low a level as possible. In some
cases they are not far from being simple global functions. The intent was to start at a low level
and build any higher level functionality as needed. What exists now is functional for ongoing
development and anticipated needs within OpenSubdiv for the near future.
1) Types, traits and options for supported subdivision schemes:
Associated headers:
sdc/type.h
sdc/options.h
Synopsis:
The most basic addition here is an enumerated type that identifies the fixed set of subdivision
schemes supported by OpenSubdiv: Bilinear, Catmark and Loop. With this alone, we should be able
to avoid all of the existing dynamic casting issues related to the scheme by simply adding members
to the associated subclasses for inspection. In addition to the type enum itself, a class defining
a set of TypeTraits<TYPE> for each scheme is provided, which is to be specialized for each scheme.
The second contribution is the collection of all variations in one place that can be applied to the
subdivision schemes, i.e. the boundary interpolation rules, creasing method, edge subdivision
choices, etc. The fact that these are all declared in one place alone should help clients see the
full set of variations that are possible.
A simple Options struct (a set of bitfields) aggregates all of these variations into a single
object (an integer in this case) that are passed around to other Sdc classes and/or methods and
are expected to be used at a higher level both within OpenSubdiv and externally. By aggregating
the options and passing them around as a group, it allows us to extend the set easily in future
without the need to rewire a lot of interfaces to accomodate the new choice. Clients can enables
new choices at the highest level and be assured that they will propogate to the lowest level where
they are relevant.
This aggregating pattern is something I prefer to use and hope to see employed in other places,
e.g. a set of options for refinement choices. Some do not prefer this though, and there are some
legitimate objections that can be made. Since this is the mechanism I intend to use to pass
options from the client's mesh, through the refinement process and eventually to Sdc calls, I hope
we can sign off on this. If we choose not to adopt it, I am likely to introduce something similar
for internal use. In its absence we will need to be more explicit in passing the individual
options around appropriately.
It's worth looking ahead here and noting that a simple Scheme<TYPE> class combines both the Type
and the Options in an instance, which is then used to make queries. There are some debates as to
when the term Type vs Schemes should be used -- I'm less concerned about naming at this point than
with the factoring of the functionality. Once we agree that the factoring is appropriate, we can
change the labels later.
Questions:
- is TypeTraits<> worth it having vs static methods in the Scheme<> class?
- inline specializations for TypeTraits<> or .cpp files? (prefer to avoid header bloat
and not include all via <sdc/type.h>)
- is the aggregate Options worthwhile, or do individual enums suffice?
- do we want to nest the various enum options within the Options class or not?
2) Support for semi-sharp creasing:
Associated headers:
sdc/crease.h:
Synopsis:
Support for semi-sharp creasing is currently independent of the subdivision scheme. Most of
the computations involving sharpness values are also somewhat independent of topology -- there are
vertices and edges with sharpness values, but knowledge of faces or boundary edges is not required.
So the complexity of topological neighborhoods that is required for mask queries is arguably not
necessary here.
As a result, creasing computations are methods defined on a Crease class that is constructed with
a set of Options. Its methods typically take sharpness values as inputs and compute one or a
corresponding set of new sharpness values as a result. For the "Normal" creasing method, the
computations may be so trivial as to question whether such an interface is worth it, but for
"Chaikin" or other schemes in future that are non-trivial, the benefits should be clear.
With this division between triviality vs complexity, I wondered if it was worth given the client
hints as to when they need the full power of these methods. I initially added a method IsSimple()
to return if the trivial Normal crease method was in use -- I choose "Simple" rather than "Normal"
to emphasize that the associated computations are "simple". Unaware that Hbr/RenderMan referred
to the basic creasing method as "Normal" had suggested it be called "Uniform", which is a better
distinction as it reflects the nature of the evaluations and the dependencies on neighborhoods.
So I've included IsUniform() instead -- with the intent being that we may rename some of the
methods to include "Uniform" or "NonUniform" in their names -- typically any method requiring
the full neighborhood of sharpness values around a vertex is non-uniform.
Also included as part of the Crease class is the Rule enum -- this indicates if a vertex is Smooth,
Crease, Dart or Corner (referred to as the "mask" in Hbr) and is a function of the sharpness values
at and around a vertex. Knowing the Rule for a vertex can accelerate mask queries, and the Rule
can often be inferred based on the origin of a vertex, e.g. it originated from the middle of a
face, was the child of a Smooth vertex, etc.
Constants are also defined as static members -- requiring a .cpp for its shared instance. This is
a point that warrants discussion as there were no clear examples to follow within the OpenSubdiv
code. Constants for SMOOTH (0.0) and INFINITE (10.0) are provided and declared as the float values
they are -- in contrast to Hbr which had them as enumerations. The "sharp" value of 1.0 that was
defined and used by Hbr is not included -- specifically as any comparison to 1.0 typically assumes
the simple creasing method and is not applicable for Chaikin or potentially other schemes (e.g. a
sharpness of 0.9 may remain non-zero after one level of subdivision, just as a sharpness value of
1.1 may decay to zero between the same levels). The only significance of 1.0 is that it is what is
subtracted either the original or an intermediate sharpness value between levels.
Methods are defined for the Crease class to:
- subdivide edge and vertex sharpness values
- determine the Rule for a vertex based on incident sharpness values
- determine the transitional weight between two sets of sharpness values
Being all low-level and working directly on sharpness values, it is a client's responsibility to
coordinate the application of any hierarchical crease edits with their computations.
Similarly, in keeping with this as a low-level interface, values are passed as primitive arrays.
This follows the trend in OpenSubdiv of dealing with data of various kinds (e.g. weights, component
indices, now sharpness values, etc.) in small contiguous sets of values. In most internal cases
we can refer to a set of values or gather what will typically be a small number of values on the
stack for temporary use.
Questions:
- should we allow/promote the use of the IsUniform() test?
- is it reasonable to require that topological features, e.g. boundaries, be embedded in the
sharpness values (only applied once at level 0)? (consensus thus far is Yes)
- would methods to apply sharp edges given the topological neighborhood and associated
options be worthwhile? (consensus this far is No, not necessary)
3) Vertex masks and topological neighborhoods:
Associated headers:
sdc/scheme.h
sdc/catmarkScheme.h
sdc/bilinearScheme.h
Synopsis:
Recently referred as "stencils", we've now adopted the term "mask" to refer to the collection
of weights defining a subdivided vertex in terms of its parent vertices in the subdivision level
preceding it. Stencils define a vertex in terms of vertices in the base cage, and public classes
Unfortulately Hbr uses "mask" for another purpose -- to describe if a feature is smooth, a crease,
corner or dart. We've adopted "rule" in place of "mask" for that purpose.
The computation of mask weights for subdivided vertices is arguably the most significant
contributions of Sdc. The use of semi-sharp creasing with each non-linear subdivision scheme
complicates what are otherwise simple masks detemined solely by the topology, and packaging
that functionality for widespread usage has been the challenge here.
Mask queries are defined in the "Scheme" class template, which has specializations for each of the
supported subdivision schemes. In an earlier version for review, the mask queries were defined in
terms of two kinds of classes: "Neighborhood" classes that defined the topological neighborhoods
for each component type of a mesh (face, edge and vertex) and a "Stencil" class to contain the set
of weights associated with the topology. We've shifted to a more generic solution where both of
these are classes are now provided as template parameters, i.e.:
template <typename FACE, typename MASK>
void ComputeFaceVertexMask(FACE const& faceNeighborhood, MASK& faceVertexMask) const;
Each mask query is expected to call methods defined for the FACE, EDGE or VERTEX classes to obtain
the information they require -- typically these methods are simple queries about the topology and
associated sharpness values. Clients are free to use their own mesh representations to gather the
requested information as quickly as possible, or to cache some subset as member variables for
immediate inline retrieval.
In general, the set of weights for a subdivided vertex is dependent on the following:
- the topology around the parent component from which the vertex originates
- the type of subdivision Rule applicable to the parent component
- requiring the sharpness values at and around that component
- the type of subdivision Rule applicable to the new child vertex
- requiring the subdivided sharpness values at and around the new vertex
- sometimes trivially inferred from the parent rule
- a weight blending the effect between differing rules for parent and child
- requiring all parent and child sharpness values
Clearly the sharpness values are inspected multiple times and so it pays to have them available
for retrieval. Computing them on an as-needed basis may be simple for uniform creasing, but a
non-uniform creasing method requires traversing topological neighborhoods, and that in addition to
the computation itself can be costly.
The point here is that it is potentially unreasonable to expect to evaluate the mask weights
completely independent of any other consideration. Expecting and encouraging the client to have
subdivided sharpness values first, for use in more than one place, is therefore recommended.
The complexity of the general case above is also unnecessary for most vertices. Any client using
Sdc typically has more information about the nature of the vertex being subdivided and much of
this can be avoided -- particularly for the smooth interior case that often dominates. More on
that in the details of the Scheme classes.
The Neighbborhood template parameters:
The Neighborhood "classes" are template parameters currently labeled FACE, EDGE and VERTEX.
This naming perhaps implies more generality than intended as the classes are only expected to
provide the methods required of the mask queries to complete their task. While all methods
must be defined, some may rarely be invoked, and the client has considerable flexibility in the
implementation of these -- they can defer some evaluations lazily until required, or be pro-active
and cache other information in member variables for immediate access.
An approach discussed in the past has alluded to iterator classes that clients would write to
traverse their meshes. The mask queries would then be parameterized in terms of a more general
and generic mesh component that would make use of more general traversal iterators. The
advantage here is the iterators are written once, then traversal is left to the query and only
what is necessary is gathered. The disadvantages are that clients are forced to write these to
do anything, getting them correct and efficient may not be trivial (or possible in some cases),
and that the same data (e.g. subdivided sharpness) may be gathered or computed multiple times
for different purposes.
The other extreme was to gather everything possible required at once, but that is objectionable.
The approach taken here provides a reasonable compromise between the two. The mask queries ask
for exactly what they want, and the provided classes are expected to deliver it as efficiently
as possible. In some cases the client may already be storing it in a more accessible form and
general topological iteration can be avoided.
The information requested of these classes in the three mask queries is as follows:
For FACE:
- the number of incident vertices
For EDGE:
- the number of incident faces
- the sharpness value of the parent edge
- the sharpness values of the two child edges
- the number of vertices per incident face
For VERTEX:
- the number of incident faces
- the number of incident edges
- the sharpness value of the parent vertex
- the sharpness values for each incident parent edge
- the sharpness value of the child vertex
- the sharpness values for each incident child edge
The latter should not be surprising given the dependencies noted above. There are also a few
more to consider for future use, e.g. whether the EDGE or VERTEX is manifold or not. In most
cases additional information can be provided to the mask queries (i.e. pre-determined Rules)
and most of the child sharpness values are not necessary. The most demanding sitation is a
fractional crease that decays to zero -- in which case all parent and child sharpness values in
the neighborhood are required to determine the proper transitional weight.
Questions:
- any ideas for labeling these in a way that more reflects their purpose, i.e. other than
FACE, EDGE or VERTEX?
- how do we document the interfaces expected of <class FACE, EDGE, VERTEX>?
The MASK template parameter:
Previously we were considering a concrete class to contain the results of the mask queries,
but with the shift to class template parameters for the topology -- and the resulting need to
export the full implementation in public headers -- I decided to do the same with the mask class.
The MASK template parameter, that all mask queries are required to populate, is now completely
determined by the client. The set of mask weights is divided into vertex-weights, edge-weights and
face-weights -- consistent with existing usage in OpenSubdiv and giving some correllation between
the full set of weights and topology. The vertex-weights refer to parent vertices incident the
parent component, the edge-weights the vertices opposite incident edges, and the face-weights the
center of indicent faces (note the latter is NOT in terms of vertices in the parent, at least not
for Catmark, possibly for Loop -- this is a point needing attention).
So the mask queries require the following capabilities:
- assign the number of vertex, edge and/or face weights
- retrieve the number of vertex, edge and/or face weights
- assign individual vertex, edge and/or face weights by index
- retrieve individual vertex, edge and/or face weights by index
through a set of methods required of all MASK classes. Knowing the maximum nunber of weights based
on the topology, typical usage within Vtr, Far or Hbr is expected to simply define buffers on the
stack or in pre-allocated tables to be partitioned into the three sets of weights on construction
of a MASK and then populated by the mask queries.
A potentially useful side-effect of this is that the client can define their weights to be stored
in either single- or double-precision. With that possibility in mind, care was taken within the
mask queries to make use of a declared type in the MASK interface (MASK::Weight) for intermediate
calculations. Having support for double-precision masks in Sdc does enable it at higher levels in
OpenSubdiv if later desired, and that support is made almost trivial with MASK being generic.
It is important to remember here that these masks are being defined consistent with existing usage
within OpenSubdiv: both Hbr and the subdivision tables generated by Far. As noted above, the
"face weights" correspond to the centers of incident faces, i.e. vertices on the same level as
the vertex for which the mask is being computed, and not relative to vertices in the parent
level as with the other sets of weights. It is true that the weights can be translated into a
set in terms solely of parent vertices, but in the general case (i.e. Catmark subdivision with
non-quads in the base mesh) this requires additional topological association. In general we would
need N-3 weights for the N-3 vertices between the two incident edges, where N is the number of
vertices of each face (typically 4 even at level 0). Perhaps such a translation method could be
provided on the mask class, with an optional indication of the incident face topology for the
irregular cases. The Loop scheme does not have "face weights", for a vertex-vertex mask, but for
an edge-vertex mask it does require weights associated with the faces incident the edge -- either
the vertex opposite the edge for each triangle, or its center (which has no other use for Loop).
Questions:
- how do we document the interfaces expected of <class MASK>?
- do we need to support masks relative to the one-ring rather than using the face-centers
as is currently done everywhere for Catmark now? Could this differ per scheme (would
warrant a TypeTrait<> if so)?
The Scheme<TYPE> class template:
Given that most of the complexity has been moved into the template parameters for the mask
queries, the Scheme class remains fairly simple. Like the Crease class, it is instantiated with
a set of Options to avoid them cluttering the interface. It is currently little more than three
methods for the mask queries for each vertex type. The set of masks may need to be extended in
future to include limit masks and (potentially) masks for face-varying data sets (whose
neighborhoods may vary in their definition).
The mask queries have been written in a way that should simplify the specializations required
for each scheme. The generic implementation for both the edge-vertex and vertex-vertex masks
take care of all of the creasing logic, requiring only a small set of specific masks to be
assigned for each Scheme: smooth and crease masks for an edge-vertex, and smooth, crease and
corner masks for a vertex-vertex. Other than the Bilinear case, which will specialize the mask
queries to trivialize them for linear interpolation, the specializations for each Scheme should
only require defining this set of masks -- and with two of them common (edge-vertex crease and
vertex-vertex corner) the Catmark scheme only needs to define three.
Questions:
- should Scheme<TYPE> include more than just mask queries, e.g. should it absorb the
funtionality of TypeTraits<TYPE> in <sdc/type.h> as static methods?
- is a class template with a set interface preferable to separate and independent classes
that provides an expected interfce, e.g. Scheme<CATMARK> vs CatmarkScheme (we don't
want the overhead of a common base class here)?