In this post I am going to examine the implementation of current Solidify modifier.

The applyModifier of Solidify is really a long function. But the essential idea is not so hard to get.

I use a parameter configuration that “Fill Rim” is turned off. In this way the actual active code path of applyModifier is very short and it is easier to understand the essence.

Implementation and source code excerpts

Firstly we get the statistics of the original input DerivedMesh:

const unsigned int numVerts = (unsigned int)dm->getNumVerts(dm);
const unsigned int numEdges = (unsigned int)dm->getNumEdges(dm);
const unsigned int numFaces = (unsigned int)dm->getNumPolys(dm);
const unsigned int numLoops = (unsigned int)dm->getNumLoops(dm);

And then several flags are determined. The do_shell is one flag that we are particularly interested in. It is a boolean flag indicating whether we are going to construct the offset surface. In our setting the do_shell is true. Thus we will double vertices/edges/faces count for the offset surface:

const unsigned int stride = do_shell ? 2 : 1;

Now we move on through the code. Since we turned off “Fill Rim”, the whole block of if (smd->flag & MOD_SOLIDIFY_RIM) { can be omitted.

Next we create the result DerivedMesh and duplicate the original input surface:

result = CDDM_from_template(dm,
                                (int)((numVerts * stride) + newVerts),
                                (int)((numEdges * stride) + newEdges + rimVerts), 0,
                                (int)((numLoops * stride) + newLoops),
                                (int)((numFaces * stride) + newFaces));

mpoly = CDDM_get_polys(result);
mloop = CDDM_get_loops(result);
medge = CDDM_get_edges(result);
mvert = CDDM_get_verts(result);

if (do_shell) {
    DM_copy_vert_data(dm, result, 0, 0, (int)numVerts);
    DM_copy_vert_data(dm, result, 0, (int)numVerts, (int)numVerts);

    DM_copy_edge_data(dm, result, 0, 0, (int)numEdges);
    DM_copy_edge_data(dm, result, 0, (int)numEdges, (int)numEdges);

    DM_copy_loop_data(dm, result, 0, 0, (int)numLoops);
    DM_copy_loop_data(dm, result, 0, (int)numLoops, (int)numLoops);

    DM_copy_poly_data(dm, result, 0, 0, (int)numFaces);
    DM_copy_poly_data(dm, result, 0, (int)numFaces, (int)numFaces);
}

Next we construct the offset surface in two phases. In the first phase we duplicate the original input surface, and reverse the winding order to flip the normals: (some irrelevant source codes are eliminated for clarity, and I also supply some comments)

if (do_shell) {
    unsigned int i;

    mp = mpoly + numFaces;

    /* Traverse all attributes of original DerivedMesh */
    for (i = 0; i < dm->numPolyData; i++, mp++) {
        MLoop *ml2;
        unsigned int e;
        int j;

        /* Get the starting loop of the offset surface */ 
        ml2 = mloop + mp->loopstart + dm->numLoopData;

        /* Copy the loop data in reversed order */
        for (j = 0; j < mp->totloop; j++) {
            CustomData_copy_data(
                &dm->loopData, &result->loopData,
                /* The source index */
                mp->loopstart + j,
                /* The destination index */
                mp->loopstart + (mp->totloop - j - 1) + dm->numLoopData, 1);
        }

        e = ml2[0].e;
        for (j = 0; j < mp->totloop - 1; j++) {
            ml2[j].e = ml2[j + 1].e;
        }
        ml2[mp->totloop - 1].e = e;

        mp->loopstart += dm->numLoopData;

        /* Offset the edge/vertex index for each loop in the offset surface */
        for (j = 0; j < mp->totloop; j++) {
            ml2[j].e += numEdges;
            ml2[j].v += numVerts;
        }
    }

    /* Offset the edge/vertex index for each edge in the offset surface */
    for (i = 0, ed = medge + numEdges; i < numEdges; i++, ed++) {
        ed->v1 += numVerts;
        ed->v2 += numVerts;
    }
}

In the second phase of the offset surface construction, the duplicated vertices/edges/faces are offset along the normal direction. This is the essential part of the Solidify modifier:

if (ofs_orig != 0.0f) {
    scalar_short = scalar_short_vgroup = ofs_orig / 32767.0f;

    for (i_orig = 0; i_orig < i_end; i_orig++, mv++) {
        const unsigned int i = do_shell_align ? i_orig : new_vert_arr[i_orig];
        if (dvert) {
            /* Omitted since this code path is not active. */ 
        }
        if (do_clamp) {
            /* Omitted since this code path is not active. */
        }
        madd_v3v3short_fl(mv->co, mv->no, scalar_short_vgroup);
    }
}

The madd_v3v3short_fl is just a multiply-and-add operation. It is defined as:

BLI_INLINE void madd_v3v3short_fl(float r[3], const short a[3], const float f)
{
    r[0] += (float)a[0] * f;
    r[1] += (float)a[1] * f;
    r[2] += (float)a[2] * f;
}

After that, the vertex normals are flipped for the offset surface:

unsigned int i;
/* flip vertex normals for copied verts */
mv = mvert + numVerts;
for (i = 0; i < numVerts; i++, mv++) {
    negate_v3_short(mv->no);
}

Then the essential work of Solidify modifier is done.

Some thoughts

The current implementation of Solidify modifier depends on the normal heavily.

Is the normal direction a geometric characteristic, which is guaranteed to be the gradient of signed distance field, or just a characteristic of visual appearance, which can be edited independent with the underlying geometry?

I thought the gradient of signed distance field is the correct offset direction.

Moreover, current Solidify modifier does not takes offset depth limitation into consideration, which is one cause of the self-intersection. And I think this why we need some preprocessing to build shape skeleton using mean curvature flow (for example, this one) or just the level set method.