From BlenderWiki

Jump to: navigation, search

PBVH Painting Developer Documentation

Things to Know

  1. I highly recommend reading my GSOC proposal if you haven't done so already. My proposal can be found here.
  2. Try to get familiar with what a PBVH is. PBVH documentation can be found here
  3. As of 08/13/2016, much of this project reuses sculpting structs and methods.
    • If you ever see the word 'sculpt' show up in this project's code, just know that method or struct has been generalized to work with both sculpt mode and vertex/weight painting.
    • Being familiar with sculpt code isn't necessary, but could be extremely helpful in modifying vertex painting since both modes use PBVHs.
  4. This code takes a spherical based vertex selection approach as opposed to the previous 3D frustum based vertex selection.
    • Spherical selection gives us spacial locality of the vertices being selected without relying on OpenGL framebuffer tests, which improves framework performance and flexibility.

Constructing the PBVH

The actual PBVH construction is initiated in DerivedMesh.c in mesh_build_data().

Two things are checked before constructing the PBVH.

  1. The object mode.
  2. Whether or not ob->sculpt has been initialized.

After BKE_sculpt_update_mesh_elements is finished, ob->sculpt will contain a pointer to the PBVH.

//Previous mesh_build_data code...
if (ob->mode & (OB_MODE_SCULPT | OB_MODE_WEIGHT_PAINT | OB_MODE_VERTEX_PAINT) && ob->sculpt) {
	/* create PBVH immediately (would be created on the fly too,
	 * but this avoids waiting on first stroke) */
 
	BKE_sculpt_update_mesh_elements(scene, scene->toolsettings->sculpt, ob, false, false);
}

Selecting PBVH Nodes

After constructing the PBVH tree, we select the nodes containing the vertices we'd like to influence.

We do this in paint_vertex.c in both vpaint_do_paint and wpaint_do_paint by using a pbvh api which takes a boolean callback.

  • In our case, this callback is a sphere-AABB intersection test:
//Previous vwpaint_do_paint code
BKE_pbvh_search_gather(ss->pbvh, sculpt_search_sphere_cb, &data, &nodes, &totnode);

This method gives us the total nodes in our spherical brush region as well as the nodes themselves.

Selecting Vertices

After gathering the PBVH nodes to be modified, we paint vertices within them by calling the following method:

//Previous wpaint_do_paint code...
wpaint_paint_leaves(C, ob, sd, wp, wpd, wpi, me, nodes, totnode);
//Previous vpaint_do_paint code...
vpaint_paint_leaves(C, sd, vd, vpd, ob, me, nodes, totnode);

These functions determine which code is associated with the selected tool, and then queues the appropriate tool callback for each PBVH node in parallel:

//See also wpaint_paint_leaves
static void vpaint_paint_leaves(bContext *C, Sculpt *sd, VPaint *vp, VPaintData *vpd, Object *ob, Mesh *me, 
  PBVHNode **nodes, int totnode)
{
	Brush *brush = ob->sculpt->cache->brush;
 
	SculptThreadedTaskData data = {
		.sd = sd, .ob = ob, .brush = brush, .nodes = nodes, .vp = vp, .vpd = vpd, 
		.lcol = (unsigned int*)me->mloopcol, .me = me, .C = C,
	};
 
	switch (brush->vertexpaint_tool) {
		case PAINT_BLEND_AVERAGE:
			calculate_average_color(&data, nodes, totnode);
			BLI_task_parallel_range_ex( 0, totnode, &data, NULL, 0, do_vpaint_brush_draw_task_cb_ex, true, false);
			break;
		case PAINT_BLEND_BLUR:
			BLI_task_parallel_range_ex( 0, totnode, &data, NULL, 0, do_vpaint_brush_blur_task_cb_ex, true, false);
			break;
		case PAINT_BLEND_SMUDGE:
			BLI_task_parallel_range_ex( 0, totnode, &data, NULL, 0, do_vpaint_brush_smudge_task_cb_ex, true, false);
			break;
		default:
			BLI_task_parallel_range_ex( 0, totnode, &data, NULL, 0, do_vpaint_brush_draw_task_cb_ex, true, false);
			break;
	}
}

Each callback is unique. However some tools are grouped together in one callback.

  • Both "mix" and "subtract" blend modes are handled under do_vpaint_brush_draw_task_cb_ex.
  • "blur", "average", and "smudge" modes have different control flow and separated into their own callbacks.
  • If a tool doesn't have it's own callback, we default to the do_vpaint_draw_task_cb_ex callback.

Most vertex and weight paint callbacks look like the following:

static void do_vpaint_brush_draw_task_cb_ex(
  void *userdata, void *UNUSED(userdata_chunk), const int n, const int UNUSED(thread_id))
{
  /*
    Fetch required data from "userdata" here...
  */
 
  //for each vertex
  PBVHVertexIter vd;
  BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE)
  {
    SculptBrushTest test;
    sculpt_brush_test_init(ss, &test);
 
    //Test to see if the vertex coordinates are within the spherical brush region.
    if (sculpt_brush_test(&test, vd.co)) {
      /*
        Influence vertex here...
      */
    }
    BKE_pbvh_vertex_iter_end;
  }
}

Painting Vertices

Now that we've covered the PBVH node callbacks, here's an example of one in use with vertex paint.

  • After testing if the vertex is in our brush, we calculate the brush fade using the distance from the brush center to the vertex.
  • If we're using the texture option, we substitute the current color with a texture sampled color.
  • Lastly, we paint every loop that vertex owns.
  //for each vertex
  PBVHVertexIter vd;
  BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE)
  {
    SculptBrushTest test;
    sculpt_brush_test_init(ss, &test);
 
    //Test to see if the vertex coordinates are within the spherical brush region.
    if (sculpt_brush_test(&test, vd.co)) {
      int vertexIndex = vd.vert_indices[vd.i];
      const float fade = BKE_brush_curve_strength(brush, test.dist, cache->radius);
      unsigned int actualColor = data->vpd->paintcol;
 
      float alpha = 1.0;
      if (data->vpd->is_texbrush) {
        handle_texture_brush(brush, data, vd, &alpha, &actualColor);
      }
      //if a vertex is within the brush region, then paint each loop that vertex owns.
      for (int j = 0; j < ss->vert_to_loop[vertexIndex].count; ++j) {
        int loopIndex = ss->vert_to_loop[vertexIndex].indices[j];
        //Mix the new color with the original based on the brush strength and the curve.
        lcol[loopIndex] = vpaint_blend(data->vp, lcol[loopIndex], lcolorig[loopIndex], actualColor,         
                            255 * fade * bstrength * dot * alpha, brush_alpha_pressure * 255);
      }
    }
    BKE_pbvh_vertex_iter_end;
  }

The Painting Process Visualized

1. Construct the PBVH.
2. Select the nodes touching the brush.
3. Select vertices within the brush in a callback
4. Use vertex to brush distance for falloff curve

 


Splash Prevention

A "splash" occurs when a user accidentally paints a vertex. This happens very often with 3D Frustum vertex selection since it's very easy to accidentally catch a vertex in the background when painting vertices in the foreground.

Using 3D frustum selection
Splashing due to perspective constraints

 


Although sphere intersection removes the majority of this problem, we still have some splash situations.

  • A sphere might catch hidden vertices that still lie within the brush region.
  • A sphere might catch visible vertices across the 3D cursor location, like on the edge of a saddle.

To mitigate this problem, we compare vertex normals with the surface normal at the 3D cursor location by using a dot product.

const float dot = dot_vf3vs3(cache->sculpt_normal_symm, vd.no);
//If both surface normals are in the same direction
if (dot > 0.0)
        for (int j = 0; j < ss->vert_to_loop[vertexIndex].count; ++j) {
          int loopIndex = ss->vert_to_loop[vertexIndex].indices[j];
 
          //Mix the new color with the original based on the brush strength, 
           //the curve, the alpha, and the dot product of the surface normals.
          lcol[loopIndex] = vpaint_blend(data->vp, lcol[loopIndex], lcolorig[loopIndex], 
                                         actualColor, 255 * fade * bstrength * dot * alpha, 
                                         brush_alpha_pressure * 255);
        }
Before
After

 

Note
Splash prevention may become more sophisticated in the future.


Mirroring and Radial Symmetry

In addition to the initial stroke, our wpaint_do_paint and vpaint_do_paint methods are both called for each radial symmetry pass.

  • This is otherwise known as 'axial symmetry'
  • This code is very similar to sculpt mode!
static void vpaint_do_radial_symmetry(bContext *C, Sculpt *sd, VPaint *vd, VPaintData *vpd, Object *ob, 
  Mesh *me, Brush *brush, const char symm, const int axis)
{
	for (int i = 1; i < vd->radial_symm[axis - 'X']; ++i) {
		const float angle = (2.0 * M_PI) * i / vd->radial_symm[axis - 'X'];
		vpaint_do_paint(C, sd, vd, vpd, ob, me, brush, symm, axis, i, angle);
	}
}
static void wpaint_do_radial_symmetry(bContext *C, Object *ob, VPaint *wp, Sculpt *sd, WPaintData *wpd, WeightPaintInfo *wpi, 
  Mesh *me, Brush *brush, const char symm, const int axis)
{
	for (int i = 1; i < wp->radial_symm[axis - 'X']; ++i) {
		const float angle = (2.0 * M_PI) * i / wp->radial_symm[axis - 'X'];
		wpaint_do_paint(C, ob, wp, sd, wpd, wpi, me, brush, symm, axis, i, angle);
	}
}

Lastly, we add the ability to mirror the initial and radial painting across the X, Y, and/or Z axis.

  • This is otherwise known as "bilateral symmetry'
//See also wpaint_do_symmetrical_brush_actions
static void vpaint_do_symmetrical_brush_actions(bContext *C, Sculpt *sd, VPaint *vd, VPaintData *vpd, Object *ob)
{
	Brush *brush = BKE_paint_brush(&vd->paint);
	Mesh *me = ob->data;
	SculptSession *ss = ob->sculpt;
	StrokeCache *cache = ss->cache;
	const char symm = vd->paint.symmetry_flags & PAINT_SYMM_AXIS_ALL;
	int i = 0;
 
	/* initial stroke */
	vpaint_do_paint(C, sd, vd, vpd, ob, me, brush, i, 'X', 0, 0);
	vpaint_do_radial_symmetry(C, sd, vd, vpd, ob, me, brush, i, 'X');
	vpaint_do_radial_symmetry(C, sd, vd, vpd, ob, me, brush, i, 'Y');
	vpaint_do_radial_symmetry(C, sd, vd, vpd, ob, me, brush, i, 'Z');
 
	cache->symmetry = symm;
 
	/* symm is a bit combination of XYZ - 1 is mirror X; 2 is Y; 3 is XY; 4 is Z; 5 is XZ; 6 is YZ; 7 is XYZ */
	for (i = 1; i <= symm; ++i) {
		if (symm & i && (symm != 5 || i != 3) && (symm != 6 || (i != 3 && i != 5))) {
			cache->mirror_symmetry_pass = i;
			cache->radial_symmetry_pass = 0;
			calc_brushdata_symm(vd, cache, i, 0, 0);
 
			if (i & 1 << 0) {
				vpaint_do_paint(C, sd, vd, vpd, ob, me, brush, i, 'X', 0, 0);
				vpaint_do_radial_symmetry(C, sd, vd, vpd, ob, me, brush, i, 'X');
			}
			if (i & 1 << 1) {
				vpaint_do_paint(C, sd, vd, vpd, ob, me, brush, i, 'Y', 0, 0);
				vpaint_do_radial_symmetry(C, sd, vd, vpd, ob, me, brush, i, 'Y');
			}
			if (i & 1 << 2) {
				vpaint_do_paint(C, sd, vd, vpd, ob, me, brush, i, 'Z', 0, 0);
				vpaint_do_radial_symmetry(C, sd, vd, vpd, ob, me, brush, i, 'Z');
			}
		}
	}
 
	copy_v3_v3(cache->true_last_location, cache->true_location);
	cache->is_last_valid = true;
}

These functions are called in the update_step portion of the vertex painting operator.

//Previous vpaint_stroke_update_step code...
vpaint_do_symmetrical_brush_actions(C, sd, vp, vpd, ob);
//Previous wpaint_stroke_update_step code...
wpaint_do_symmetrical_brush_actions(C, ob, wp, sd, wpd, &wpi);

These are the results.

Radial Settings
Radial Symmetry
Bilateral Settings
Bilateral Symmetry

 

Adding Brushes and Blend Modes

There are a couple steps that need to be done before a new brush will appear in blender's weight painting and vertex painting modes.

You'll need to add an enumerator in DNA_brush_types.h. This is what the current enumeration looks like:

enum {
	PAINT_BLEND_MIX = 0,
	PAINT_BLEND_ADD = 1,
	PAINT_BLEND_SUB = 2,
	PAINT_BLEND_MUL = 3,
	PAINT_BLEND_BLUR = 4,
	PAINT_BLEND_LIGHTEN = 5,
	PAINT_BLEND_DARKEN = 6,
	PAINT_BLEND_AVERAGE = 7,
	PAINT_BLEND_SMUDGE = 8
};

Then, you'll need to account for the new blend mode in vpaint_blend_tool and/or wpaint_blend_tool in paint_vertex.c.

  • If your new brush only changes how each vertex reacts to the brush color, you'll only need to write an mcol_blend routine.
/* wpaint has 'wpaint_blend_tool' */
static unsigned int vpaint_blend_tool(const int tool, const unsigned int col,
                                      const unsigned int paintcol, const int alpha_i)
{
	switch (tool) {
		case PAINT_BLEND_MIX:
		case PAINT_BLEND_BLUR:     return mcol_blend(col, paintcol, alpha_i);
		case PAINT_BLEND_AVERAGE:  return mcol_blend(col, paintcol, alpha_i);
		case PAINT_BLEND_SMUDGE:   return mcol_blend(col, paintcol, alpha_i);
		case PAINT_BLEND_ADD:      return mcol_add(col, paintcol, alpha_i);
		case PAINT_BLEND_SUB:      return mcol_sub(col, paintcol, alpha_i);
		case PAINT_BLEND_MUL:      return mcol_mul(col, paintcol, alpha_i);
		case PAINT_BLEND_LIGHTEN:  return mcol_lighten(col, paintcol, alpha_i);
		case PAINT_BLEND_DARKEN:   return mcol_darken(col, paintcol, alpha_i);
		default:
			BLI_assert(0);
			return 0;
	}
}

If you need more flexibility with your brush, you'll need to create and add your own callback to be used in vpaint_paint_leaves and/or wpaint_paint_leaves.

  • I do this for the smudge brush and average brush discussed below.

You'll need to add an entry to rna_enum_brush_vertex_tool_items in rna_brush.c.

EnumPropertyItem rna_enum_brush_vertex_tool_items[] = {
	{PAINT_BLEND_MIX, "MIX", ICON_BRUSH_MIX, "Mix", "Use mix blending mode while painting"},
	{PAINT_BLEND_ADD, "ADD", ICON_BRUSH_ADD, "Add", "Use add blending mode while painting"},
	{PAINT_BLEND_SUB, "SUB", ICON_BRUSH_SUBTRACT, "Subtract", "Use subtract blending mode while painting"},
	{PAINT_BLEND_MUL, "MUL", ICON_BRUSH_MULTIPLY, "Multiply", "Use multiply blending mode while painting"},
	{PAINT_BLEND_BLUR, "BLUR", ICON_BRUSH_BLUR, "Blur", "Blur the color with surrounding values"},
	{PAINT_BLEND_LIGHTEN, "LIGHTEN", ICON_BRUSH_LIGHTEN, "Lighten", "Use lighten blending mode while painting"},
	{PAINT_BLEND_DARKEN, "DARKEN", ICON_BRUSH_DARKEN, "Darken", "Use darken blending mode while painting"},
	{PAINT_BLEND_AVERAGE, "AVERAGE", ICON_BRUSH_BLUR, "Average", "Use average blending mode while painting" },
	{PAINT_BLEND_SMUDGE, "SMUDGE", ICON_BRUSH_BLUR, "Smudge", "Use smudge blending mode while painting" },
	{0, NULL, 0, NULL, NULL}
};

Lastly, you'll need to add your brush to the default brushes by modifying versioning_defaults.c

void BLO_update_defaults_startup_blend(Main *bmain) 
{
  /*
    Previous startup_blend code...
  */
 
  //Add the new brush as a default option if not previously added...
  br = (Brush *)BKE_libblock_find_name_ex(bmain, ID_BR, "Smudge");
  if (!br) {
    br = BKE_brush_add(bmain, "Smudge", OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT);
    br->vertexpaint_tool = PAINT_BLEND_SMUDGE;
    br->ob_mode = OB_MODE_VERTEX_PAINT | OB_MODE_WEIGHT_PAINT;
  }
}

More complicated brushes

I mentioned earlier that occasionally we'll need a more flexible way to add brushes than simply creating an mcol_blend routine.

  • We can get that additional flexibility by adding callbacks.
  • Smudge mode is a good example, since vertices require information about their neighbors before being painted.

All the code for smudge is in the same 'smudge' callback below. After writing this callback, I use this callback in vpaint_paint_leaves

static void do_vpaint_brush_smudge_task_cb_ex(
  void *userdata, void *UNUSED(userdata_chunk), const int n, const int UNUSED(thread_id))
{
	SculptThreadedTaskData *data = userdata;
	SculptSession *ss = data->ob->sculpt;
	Brush *brush = data->brush;
	StrokeCache *cache = ss->cache;
	const float bstrength = cache->bstrength;
	bool shouldColor = false;
	unsigned int *lcol = data->lcol;
	unsigned int *lcolorig = data->vp->vpaint_prev;
	unsigned int finalColor;
	float brushDirection[3];
	sub_v3_v3v3(brushDirection, cache->location, cache->last_location);
	normalize_v3(brushDirection);
 
	//If the position from the last update is initialized...
          //This is needed to calculate a brush direction
	if (cache->is_last_valid) {
		//for each vertex
		PBVHVertexIter vd;
		BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE)
		{
			SculptBrushTest test;
			sculpt_brush_test_init(ss, &test);
 
			//Test to see if the vertex coordinates are within the spherical brush region.
			if (sculpt_brush_test(&test, vd.co)) {
                                //Verify we aren't splashing...
				float dot = dot_vf3vs3(cache->sculpt_normal_symm, vd.no);
				if (dot > 0.0) {
					const float fade = BKE_brush_curve_strength(brush, test.dist, cache->radius);
					int vertexIndex = vd.vert_indices[vd.i];
					MVert *currentVert = &data->me->mvert[vertexIndex];
 
					//Minimum dot product between brush direction and current to neighbor direction is 0.0,  
                                         //meaning orthogonal.
					float maxDotProduct = 0.0f;
 
					//Get the color of the loop in the opposite direction of the brush movement
					finalColor = 0;
					for (int j = 0; j < ss->vert_to_poly[vertexIndex].count; j++) {
						int polyIndex = ss->vert_to_poly[vertexIndex].indices[j];
						MPoly *poly = &data->me->mpoly[polyIndex];
						for (int k = 0; k < poly->totloop; k++) {
							unsigned int loopIndex = poly->loopstart + k;
							MLoop *loop = &data->me->mloop[loopIndex];
							unsigned int neighborIndex = loop->v;
							MVert *neighbor = &data->me->mvert[neighborIndex];
 
							//Get the direction from the selected vert to the neighbor.
							float toNeighbor[3];
							sub_v3_v3v3(toNeighbor, currentVert->co, neighbor->co);
							normalize_v3(toNeighbor);
 
							float dotProduct = dot_v3v3(toNeighbor, brushDirection);
 
							if (dotProduct > maxDotProduct) {
								maxDotProduct = dotProduct;
								finalColor = lcol[loopIndex];
								shouldColor = true;
							}
						}
					}
                                        //If we found a neighbor behind the brush direction...
					if (shouldColor) {
						//then paint each loop that vertex owns.
						for (int j = 0; j < ss->vert_to_loop[vertexIndex].count; ++j) {
							int loopIndex = ss->vert_to_loop[vertexIndex].indices[j];
							//Mix the new color with the original based on some parameters.
							lcol[loopIndex] = vpaint_blend(data->vp, lcol[loopIndex], lcolorig[loopIndex],
                                                                                   finalColor, 255.0 * fade * bstrength * dot, 255.0);
						}
					}
				}
			}
			BKE_pbvh_vertex_iter_end;
		}
	}
}

Email / IRC / Social / Web

If I missed out on an important detail, or if this documentation doesn't answer your question, feel free to shoot me a message over email, IRC, or on twitter.

bitinat2@isu.edu or nathanvollmer@outlook.com

N8V or nathanvollmer on #blendercoders

Twitter: https://twitter.com/NathanVollmer

Web: http://www2.cose.isu.edu/~bitinat2