mirror of
https://github.com/PixarAnimationStudios/OpenSubdiv
synced 2024-09-19 22:30:05 +00:00
c399655dcc
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.
339 lines
20 KiB
Plaintext
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)?
|