HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
SOP/SOP_CustomBrush.C
/*
* Copyright (c) 2024
* Side Effects Software Inc. All rights reserved.
*
* Redistribution and use of Houdini Development Kit samples in source and
* binary forms, with or without modification, are permitted provided that the
* following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. The name of Side Effects Software may not be used to endorse or
* promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE `AS IS' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
* NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*----------------------------------------------------------------------------
*/
#include <GA/GA_Handle.h>
#include <GA/GA_Types.h>
#include <UT/UT_CPIO.h>
#include <UT/UT_IStream.h>
#include <UT/UT_Map.h>
#include <UT/UT_OStream.h>
#include <UT/UT_Undo.h>
#include <UT/UT_Vector3.h>
#include <SYS/SYS_Types.h>
#include <stddef.h>
// undo for CustomBrush
namespace HDK_Sample {
class SOP_UndoCustomBrushData : public UT_Undo
{
public:
SOP_UndoCustomBrushData(SOP_CustomBrush *sop,
GA_Size oldnumpts,
GA_Size numpts,
void undo() override;
void redo() override;
private:
int mySopId;
GA_Size myOldNumPts;
GA_Size myNumPts;
};
} // End HDK_Sample namespace
using namespace HDK_Sample;
GA_Size oldnumpts,
GA_Size numpts,
mySopId(sop->getUniqueId()),
myOldNumPts(oldnumpts),
myNumPts(numpts),
myOldData(olddata),
myData(data)
{
// include the memory allocated by the UT_RefArrays to accurately compute
// the total memory used by this undo
addToMemoryUsage(olddata.getMemoryUsage() + data.getMemoryUsage());
}
void
SOP_UndoCustomBrushData::undo()
{
node->updateData(myOldNumPts, myOldData);
}
void
{
node->updateData(myNumPts, myData);
}
void
{
table->addOperator(new OP_Operator("proto_custombrush", "Custom Brush",
1, 1));
}
static PRM_Name sopOriginName("origin", "Origin");
static PRM_Name sopDirectionName("direction", "Direction");
static PRM_Name sopRadiusName("radius", "Radius");
static PRM_Name sopColorName("color", "Color");
static PRM_Name sopAlphaName("alpha", "Alpha");
static PRM_Name sopOperationName("operation", "Operation");
static PRM_Name sopEventName("event", "Event");
static PRM_Name sopClearAllName("clearall", "Clear All");
{
};
static PRM_Name sopOperationMenuNames[] =
{
PRM_Name("paint", "Paint"),
PRM_Name("erase", "Erase"),
};
static PRM_ChoiceList sopOperationMenu(PRM_CHOICELIST_SINGLE, sopOperationMenuNames);
static PRM_Default sopOperationDefault(SOP_CUSTOMBRUSHOPERATION_PAINT);
{
};
static PRM_Name sopEventMenuNames[] =
{
PRM_Name("begin", "Begin Stroke"),
PRM_Name("active", "Active Stroke"),
PRM_Name("end", "End Stroke"),
PRM_Name("nop", "No-op"),
};
static PRM_ChoiceList sopEventMenu(PRM_CHOICELIST_SINGLE, sopEventMenuNames);
static PRM_Default sopEventDefault(SOP_CUSTOMBRUSHEVENT_NOP);
PRM_Template(PRM_XYZ_J, 3, &sopOriginName),
PRM_Template(PRM_XYZ_J, 3, &sopDirectionName),
PRM_Template(PRM_FLT_J, 1, &sopRadiusName, PRMoneDefaults),
PRM_Template(PRM_RGB_J, 3, &sopColorName, PRMoneDefaults),
PRM_Template(PRM_ORD, 1, &sopOperationName, &sopOperationDefault,
&sopOperationMenu),
PRM_Template(PRM_ORD, 1, &sopEventName, &sopEventDefault, &sopEventMenu),
PRM_Template(PRM_CALLBACK, 1, &sopClearAllName, 0, 0, 0, &clearAllStatic),
};
{
return new SOP_CustomBrush(net, name, op);
}
SOP_Node(net, name, op),
myGroup(0),
myNumPts(0),
myOldNumPts(0)
{
// This indicates that this SOP manually manages its data IDs,
// so that Houdini can identify what attributes may have changed,
// e.g. to reduce work for the viewport, or other SOPs that
// check whether data IDs have changed.
// By default, (i.e. if this line weren't here), all data IDs
// would be bumped after the SOP cook, to indicate that
// everything might have changed.
// If some data IDs don't get bumped properly, the viewport
// may not update, or SOPs that check data IDs
// may not cook correctly, so be *very* careful!
mySopFlags.setManagesDataIDs(true);
}
SOP_CustomBrush::~SOP_CustomBrush()
{
}
{
// The SOP_Node::cookInputPointGroups() provides a good default
// implementation for just handling a point selection.
context, // This is needed for cooking the group parameter, and cooking the input if alone.
myGroup, // The group (or NULL) is written to myGroup if not alone.
alone, // This is true iff called outside of cookMySop to update handles.
// true means the group will be for the input geometry.
// false means the group will be for gdp (the working/output geometry).
true, // (default) true means to set the selection to the group if not alone and the highlight flag is on.
0, // (default) Parameter index of the group field
-1, // (default) Parameter index of the group type field (-1 since there isn't one)
true, // (default) true means that a pointer to an existing group is okay; false means group is always new.
false, // (default) false means new groups should be unordered; true means new groups should be ordered.
true, // (default) true means that all new groups should be detached, so not owned by the detail;
// false means that new point and primitive groups on gdp will be owned by gdp.
0 // (default) Index of the input whose geometry the group will be made for if alone.
);
}
{
// We must lock our inputs before we try to access their geometry.
// OP_AutoLockInputs will automatically unlock our inputs when we return.
// NOTE: Don't call unlockInputs yourself when using this!
OP_AutoLockInputs inputs(this);
if (inputs.lock(context) >= UT_ERROR_ABORT)
return error();
fpreal t = context.getTime();
// make a copy of input 0's geometry if it different from our last
// cook
int changed_input;
duplicateChangedSource(0, context, &changed_input);
return error();
if(myData.size() == 0)
{
// we have no paint yet, when we apply additional paint we will
// require there to be 'npts' points
myNumPts = npts;
}
else if(myNumPts != npts)
{
// we cannot apply the paint as the point numbers have changed
return error();
}
int event = getEvent(t);
{
// we are starting a new brush stroke
myOldData.setSize(0);
myOldNumPts = myNumPts;
}
{
// we are in the middle of performing a brush stroke
UT_Vector3 origin = getOrigin(t);
UT_Vector3 direction = getDirection(t);
direction.normalize();
fpreal radius = getRadius(t);
fpreal radius2 = radius * radius;
fpreal alpha = getBrushAlpha(t);
UT_Vector3 color = getBrushColor(t);
int operation = getOperation(t);
// build lookup table from a point number to the paint applied to
// that point
for(exint i = 0; i < myData.size(); ++i)
table[myData(i).myPtNum] = i;
for(exint i = 0; i < myOldData.size(); ++i)
oldtable[myOldData(i).myPtNum] = i;
GA_Offset ptoff;
GA_FOR_ALL_GROUP_PTOFF(gdp, myGroup, ptoff)
{
// determine if we should apply paint to this point
UT_Vector3 pos = gdp->getPos3(ptoff);
UT_Vector3 p = pos - origin;
p.normalize();
fpreal dot_p_dir = dot(p, direction);
if (dot_p_dir <= 0)
continue;
UT_Vector3 par = dot_p_dir * direction;
UT_Vector3 perp = p - par;
fpreal parlen2 = dot_p_dir * dot_p_dir;
if (parlen2 <= 0 || perp.length2() >= radius2 * parlen2)
continue;
// we will apply paint to this point
GA_Index ptnum = gdp->pointIndex(ptoff);
// find the current amount of applied paint
auto it = table.find(ptnum);
if (it == table.end())
{
// no paint has been applied yet
index = myData.append(SOP_CustomBrushData(ptnum, 0, 0, 0, 0));
table[ptnum] = index;
}
else
index = it->second;
SOP_CustomBrushData &d = myData(index);
auto oldit = oldtable.find(ptnum);
if (oldit == oldtable.end())
{
// remember the old paint value for undos
index = myOldData.append(d);
oldtable[ptnum] = index;
}
// update the paint value
fpreal one_minus_alpha = 1 - alpha;
switch(operation)
{
d.myRed = alpha * color.x() + one_minus_alpha * d.myRed;
d.myGreen = alpha * color.y() + one_minus_alpha * d.myGreen;
d.myBlue = alpha * color.z() + one_minus_alpha * d.myBlue;
d.myAlpha = alpha + one_minus_alpha * d.myAlpha;
break;
d.myRed *= one_minus_alpha;
d.myGreen *= one_minus_alpha;
d.myBlue *= one_minus_alpha;
d.myAlpha *= one_minus_alpha;
break;
}
}
}
{
// we have finished performing a brush stroke
{
// create an undo for the entire brush stroke
man->addToUndoBlock(new SOP_UndoCustomBrushData(this, myOldNumPts, myNumPts, myOldData, myData));
myOldData.setSize(0);
}
}
// find the color attribute in the original geometry
const GU_Detail *input0 = inputGeo(0);
// find the color attribute in the current geometry, creating one
// if necessary
if (!handle.isValid())
{
}
// update the colour for all painted points
for (exint i = 0; i < myData.size(); ++i)
{
SOP_CustomBrushData &data = myData(i);
fpreal r = data.myRed;
fpreal g = data.myGreen;
fpreal b = data.myBlue;
if (input_handle.isValid())
{
fpreal one_minus_alpha = 1 - data.myAlpha;
GA_Offset ptoff = input0->pointOffset(data.myPtNum);
UT_Vector3 f = input_handle.get(ptoff);
r += one_minus_alpha * f.x();
g += one_minus_alpha * f.y();
b += one_minus_alpha * f.z();
}
GA_Offset ptoff = gdp->pointOffset(data.myPtNum);
handle.set(ptoff, UT_Vector3(r, g, b));
}
if (myData.size() > 0)
handle.bumpDataId();
return error();
}
std::ostream &os,
const OP_SaveFlags &saveflags,
const char *path_prefix,
const UT_String &name_override)
{
if(SOP_Node::save(os, saveflags, path_prefix, name_override) >= UT_ERROR_ABORT)
return error();
// create a new packet for our paint
UT_CPIO packet;
const char *ext = saveflags.getBinary() ? "bpaint" : "paint";
path.sprintf("%s%s.%s", path_prefix, (const char *)getName(), ext);
packet.open(os, path.buffer());
{
UT_OStream out(os, saveflags.getBinary());
out.write(&myNumPts, 1, true);
exint n = myData.size();
out.write(&n, 1, true);
for (exint i = 0; i < n; ++i)
{
SOP_CustomBrushData &data = myData(i);
out.write((GA_Size *)&data.myPtNum);
out.write<fpreal32>(&data.myRed);
out.write<fpreal32>(&data.myGreen);
out.write<fpreal32>(&data.myBlue);
out.write<fpreal32>(&data.myAlpha, 1, (i == n - 1));
}
}
packet.close(os);
return error();
}
bool
SOP_CustomBrush::load(UT_IStream &is, const char *ext, const char *path)
{
// update our paint values if this is a paint packet
if(strcmp(ext, "bpaint") == 0 || strcmp(ext, "paint") == 0)
{
myNumPts = 0;
myData.setSize(0);
myOldData.setSize(0);
if(!is.read(&myNumPts))
return false;
if(!is.read(&n))
return false;
for(exint i = 0; i < n; ++i)
{
GA_Size idx;
if(!is.read(&idx))
return false;
float r;
if(!is.read<fpreal32>(&r))
return false;
float g;
if(!is.read<fpreal32>(&g))
return false;
float b;
if(!is.read<fpreal32>(&b))
return false;
float a;
if(!is.read<fpreal32>(&a))
return false;
myData.append(SOP_CustomBrushData(idx, r, g, b, a));
}
return true;
}
return SOP_Node::load(is, ext, path);
}
void
{
myNumPts = numpts;
if (myNumPts == 0)
{
myData.setSize(0);
// we make the SOP think the input geometry has changed so it will
// duplicate the input geometry to reset the attribute values. This
// is necessary as our cook method only modified the color attribute
// of points indicated by myData, which is now empty.
}
else
{
// build lookup table from a point number to the paint applied to
// that point
for (exint i = 0; i < myData.size(); ++i)
table[myData(i).myPtNum] = i;
for (exint i = 0; i < data.size(); ++i)
{
if(it != table.end())
{
// we already have paint applied to this point, just update the
// paint values
myData(it->second) = d;
}
else
{
// create a new entry for the paint
exint index = myData.append(d);
table[d.myPtNum] = index;
}
}
}
// tell our SOP to re-cook as we have changed the paint values
}
int
{
sop->clearAll();
return 1;
}
void
{
UT_AutoUndoBlock undoblock("Clear All", ANYLEVEL);
GA_Size oldnumpts = myNumPts;
myNumPts = 0;
myOldData = myData;
myData.setSize(0);
{
man->addToUndoBlock(new SOP_UndoCustomBrushData(this, oldnumpts, myNumPts, myOldData, myData));
myOldData.setSize(0);
}
// we make the SOP think the input geometry has changed so it will
// duplicate the input geometry to reset the attribute values. This is
// necessary as our cook method only modified the color attribute of
// points indicated by myData, which is now empty.
}