|
|
|
/*==LICENSE==*
|
|
|
|
|
|
|
|
CyanWorlds.com Engine - MMOG client, server and tools
|
|
|
|
Copyright (C) 2011 Cyan Worlds, Inc.
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
Additional permissions under GNU GPL version 3 section 7
|
|
|
|
|
|
|
|
If you modify this Program, or any covered work, by linking or
|
|
|
|
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
|
|
|
|
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
|
|
|
|
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
|
|
|
|
(or a modified version of those libraries),
|
|
|
|
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
|
|
|
|
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
|
|
|
|
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
|
|
|
|
licensors of this Program grant you additional
|
|
|
|
permission to convey the resulting work. Corresponding Source for a
|
|
|
|
non-source form of such a combination shall include the source code for
|
|
|
|
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
|
|
|
|
work.
|
|
|
|
|
|
|
|
You can contact Cyan Worlds, Inc. by email legal@cyan.com
|
|
|
|
or by snail mail at:
|
|
|
|
Cyan Worlds, Inc.
|
|
|
|
14617 N Newport Hwy
|
|
|
|
Mead, WA 99021
|
|
|
|
|
|
|
|
*==LICENSE==*/
|
|
|
|
|
|
|
|
#include "HeadSpin.h"
|
|
|
|
#include <commdlg.h>
|
|
|
|
|
|
|
|
#include "Max.h"
|
|
|
|
#include "stdmat.h"
|
|
|
|
#include "bmmlib.h"
|
|
|
|
#include "iparamb2.h"
|
|
|
|
#include "meshdlib.h"
|
|
|
|
|
|
|
|
#include "MaxExport/plExportProgressBar.h"
|
|
|
|
#include "MaxPlasmaMtls/Layers/plLayerTexBitmapPB.h"
|
|
|
|
#include "MaxPlasmaMtls/Layers/plLayerTex.h"
|
|
|
|
|
|
|
|
#include "hsMaterialConverter.h"
|
|
|
|
|
|
|
|
#include "HeadSpin.h"
|
|
|
|
#include "hsGeometry3.h"
|
|
|
|
|
|
|
|
#include "plMath/plTriUtils.h"
|
|
|
|
|
|
|
|
#include "plDistributor.h"
|
|
|
|
#include "MaxMain/plMaxNode.h"
|
|
|
|
|
|
|
|
#include "plDistTree.h"
|
|
|
|
|
|
|
|
static const float kMaxHeight = 10.f;
|
|
|
|
|
|
|
|
// Inputs:
|
|
|
|
// seed mesh - geometric entity to replicate over the surface
|
|
|
|
// surf mesh - surface to cover with seed data
|
|
|
|
// spacing - nominal distance between replicants (before randomizing).
|
|
|
|
// rnd pos radius - replicants spread uniformly about nominal spacing by +- radius
|
|
|
|
// alignment vector- preferred up direction for replicants
|
|
|
|
// alignment weight- weight between alignment vector and interpolated face normal
|
|
|
|
// normal range - amount to randomize the weighted normal
|
|
|
|
// normal bunch - amount randomized normals bunch around input normal
|
|
|
|
// rotation range - amount to randomize the azimuthal rotation about the randomized normal
|
|
|
|
// prob bitmap - describes probability that a seed planted here on the surface will sprout
|
|
|
|
// prob uvws - mapping from verts to prob bitmap, one per src span vertex
|
|
|
|
// overall rnd - overall probability that a candidate seed point (passing all other criteria) will be used
|
|
|
|
//
|
|
|
|
|
|
|
|
static inline hsPoint3& hsP3(const Point3& p) { return *(hsPoint3*)&p; }
|
|
|
|
static inline hsVector3& hsV3(const Point3& p) { return *(hsVector3*)&p; }
|
|
|
|
static inline Matrix3 Transpose(const Matrix3& m)
|
|
|
|
{
|
|
|
|
return Matrix3(m.GetColumn3(0), m.GetColumn3(1), m.GetColumn3(2), Point3(0,0,0));
|
|
|
|
}
|
|
|
|
|
|
|
|
plDistributor::plDistributor()
|
|
|
|
{
|
|
|
|
IClear();
|
|
|
|
fRand.SetSeed(uint32_t(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
plDistributor::~plDistributor()
|
|
|
|
{
|
|
|
|
Reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::Reset()
|
|
|
|
{
|
|
|
|
// Let stuff go...
|
|
|
|
if( fSurfObjToDelete )
|
|
|
|
fSurfObjToDelete->DeleteThis();
|
|
|
|
|
|
|
|
// Then clear
|
|
|
|
IClear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::IClear()
|
|
|
|
{
|
|
|
|
fInterface = nil;
|
|
|
|
|
|
|
|
fDistTree = nil;
|
|
|
|
|
|
|
|
fMeshTree.Reset();
|
|
|
|
|
|
|
|
fSurfNode = nil;
|
|
|
|
fSurfMesh = nil;
|
|
|
|
fSurfObjToDelete = nil;
|
|
|
|
|
|
|
|
fRepNodes.ZeroCount();
|
|
|
|
|
|
|
|
fIsolation = kIsoLow;
|
|
|
|
fConformity = kConformNone;
|
|
|
|
fFaceNormals = false;
|
|
|
|
|
|
|
|
fMaxConform = 1.f;
|
|
|
|
|
|
|
|
fSpacing = 5.f;
|
|
|
|
fRndPosRadius = 2.5f;
|
|
|
|
|
|
|
|
fAlignVec.Set(0,0,1.f);
|
|
|
|
fAlignWgt = 0;
|
|
|
|
|
|
|
|
fOffsetMin = fOffsetMax = 0;
|
|
|
|
|
|
|
|
fPolarRange = 0;
|
|
|
|
fTanPolarRange = 0;
|
|
|
|
fAzimuthRange = M_PI;
|
|
|
|
|
|
|
|
fOverallProb = 1.f;
|
|
|
|
|
|
|
|
fPolarBunch = 0;
|
|
|
|
|
|
|
|
fScaleLock = kLockNone;
|
|
|
|
fScaleLo.Set(1.f,1.f,1.f);
|
|
|
|
fScaleHi.Set(1.f,1.f,1.f);
|
|
|
|
|
|
|
|
fProbBitmapTex = nil;
|
|
|
|
fProbLayerTex = nil;
|
|
|
|
fProbColorChan = kRed;
|
|
|
|
|
|
|
|
fProbRemapFromLo = 0;
|
|
|
|
fProbRemapFromHi = 1.f;
|
|
|
|
fProbRemapToLo = 0;
|
|
|
|
fProbRemapToHi = 1.f;
|
|
|
|
|
|
|
|
fAngProbVec.Set(0.f, 0.f, 1.f);
|
|
|
|
fAngProbHi = fAngProbLo = 0;
|
|
|
|
fAngProbTrans = 0;
|
|
|
|
|
|
|
|
fAltProbHi = fAltProbLo = 0;
|
|
|
|
fAltProbTrans = 0;
|
|
|
|
|
|
|
|
fFade.pmin = fFade.pmax = Point3(0,0,0);
|
|
|
|
|
|
|
|
fBone = nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IGetMesh(INode* node, TriObject*& objToDelete, Mesh*& retMesh) const
|
|
|
|
{
|
|
|
|
if( objToDelete )
|
|
|
|
objToDelete->DeleteThis();
|
|
|
|
|
|
|
|
retMesh = nil;
|
|
|
|
objToDelete = nil;
|
|
|
|
|
|
|
|
// Get da object
|
|
|
|
Object *obj = node->EvalWorldState(TimeValue(0)).obj;
|
|
|
|
if( !obj )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( !obj->CanConvertToType( triObjectClassID ) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Convert to triMesh object
|
|
|
|
TriObject *meshObj = (TriObject*)obj->ConvertToType(TimeValue(0), triObjectClassID);
|
|
|
|
if( !meshObj )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( meshObj != obj )
|
|
|
|
objToDelete = meshObj;
|
|
|
|
|
|
|
|
// Get the mesh
|
|
|
|
Mesh* mesh = &(meshObj->mesh);
|
|
|
|
if( !mesh->getNumFaces() )
|
|
|
|
{
|
|
|
|
if( objToDelete )
|
|
|
|
objToDelete->DeleteThis();
|
|
|
|
objToDelete = nil;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
retMesh = mesh;
|
|
|
|
retMesh->checkNormals(true);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::ISetAngProbCosines() const
|
|
|
|
{
|
|
|
|
if( fAngProbHi == fAngProbLo )
|
|
|
|
return;
|
|
|
|
|
|
|
|
float maxAng, minAng;
|
|
|
|
if( fAngProbHi > fAngProbLo )
|
|
|
|
{
|
|
|
|
maxAng = hsDegreesToRadians(fAngProbHi);
|
|
|
|
minAng = hsDegreesToRadians(fAngProbLo);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
maxAng = hsDegreesToRadians(fAngProbLo);
|
|
|
|
minAng = hsDegreesToRadians(fAngProbHi);
|
|
|
|
}
|
|
|
|
|
|
|
|
float transAng = hsDegreesToRadians(fAngProbTrans);
|
|
|
|
if( transAng > (maxAng - minAng) * 0.5f )
|
|
|
|
transAng = (maxAng - minAng) * 0.5f;
|
|
|
|
|
|
|
|
float transAngMax = maxAng < M_PI ? transAng : 0;
|
|
|
|
float transAngMin = minAng > 0 ? transAng : 0;
|
|
|
|
|
|
|
|
fCosAngProbHi = cos(minAng);
|
|
|
|
fCosAngProbLo = cos(maxAng);
|
|
|
|
fCosAngProbHiTrans = cos(minAng + transAngMin);
|
|
|
|
fCosAngProbLoTrans = cos(maxAng - transAngMax);
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::ISetSurfaceNode(INode* surfNode) const
|
|
|
|
{
|
|
|
|
if( !IGetMesh(surfNode, fSurfObjToDelete, fSurfMesh) )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
fSurfNode = surfNode;
|
|
|
|
|
|
|
|
fSurfToWorld = surfNode->GetObjectTM(TimeValue(0));
|
|
|
|
fWorldToSurf = Inverse(fSurfToWorld);
|
|
|
|
|
|
|
|
fSurfToWorldVec = Transpose(fWorldToSurf);
|
|
|
|
fWorldToSurfVec = Transpose(fSurfToWorld);
|
|
|
|
|
|
|
|
fSurfAngProbVec = FNormalize(fWorldToSurfVec * fAngProbVec);
|
|
|
|
// This doesn't have anything to do with the surface node, but it
|
|
|
|
// does have to do with the SurfAngProbVec, and this is as good a
|
|
|
|
// place as any to do it.
|
|
|
|
ISetAngProbCosines();
|
|
|
|
|
|
|
|
fSurfAlignVec = FNormalize(fWorldToSurfVec * fAlignVec);
|
|
|
|
|
|
|
|
if( INeedMeshTree() )
|
|
|
|
IMakeMeshTree();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::INeedMeshTree() const
|
|
|
|
{
|
|
|
|
switch( fConformity )
|
|
|
|
{
|
|
|
|
case kConformAll:
|
|
|
|
case kConformHeight:
|
|
|
|
case kConformCheck:
|
|
|
|
case kConformBase:
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::IMakeMeshTree() const
|
|
|
|
{
|
|
|
|
fMeshTree.Reset();
|
|
|
|
|
|
|
|
const Box3 nonFade = fMeshTree.NonFade();
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < fSurfMesh->getNumFaces(); i++ )
|
|
|
|
{
|
|
|
|
Point3 p0 = fSurfMesh->getVert(fSurfMesh->faces[i].getVert(0)) * fSurfToWorld;
|
|
|
|
Point3 p1 = fSurfMesh->getVert(fSurfMesh->faces[i].getVert(1)) * fSurfToWorld;
|
|
|
|
Point3 p2 = fSurfMesh->getVert(fSurfMesh->faces[i].getVert(2)) * fSurfToWorld;
|
|
|
|
|
|
|
|
Box3 box(p0, p0);
|
|
|
|
box += p1;
|
|
|
|
box += p2;
|
|
|
|
|
|
|
|
fMeshTree.AddBoxIData(box, nonFade, i);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::IFindFaceSet(const Box3& box, Tab<int32_t>& faces) const
|
|
|
|
{
|
|
|
|
Tab<int32_t> distNodes;
|
|
|
|
fMeshTree.HarvestBox(box, distNodes);
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < distNodes.Count(); i++ )
|
|
|
|
{
|
|
|
|
int32_t iFace = int32_t(fMeshTree.GetBox(distNodes[i]).fIData);
|
|
|
|
faces.Append(1, &iFace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IValidateSettings(INode* surfNode, plMeshCacheTab& cache) const
|
|
|
|
{
|
|
|
|
if( !fInterface )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( !ISetSurfaceNode(surfNode) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( !IReadyRepNodes(cache) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::Distribute(INode* surfNode, plDistribInstTab& reps, plMeshCacheTab& cache, plExportProgressBar& bar) const
|
|
|
|
{
|
|
|
|
// Validate current settings.
|
|
|
|
if( !IValidateSettings(surfNode, cache) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
return IDistributeOverMesh(reps, cache, bar);
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IDistributeOverMesh(plDistribInstTab& reps, plMeshCacheTab& cache, plExportProgressBar& bar) const
|
|
|
|
{
|
|
|
|
int iUpdate = (fSurfMesh->getNumFaces() >> 4) + 1;
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < fSurfMesh->getNumFaces(); i++ )
|
|
|
|
{
|
|
|
|
IDistributeOverFace(i, reps, cache);
|
|
|
|
|
|
|
|
if( ((i / iUpdate) * iUpdate) == i )
|
|
|
|
{
|
|
|
|
if( bar.Update(nil) )
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3& plDistributor::IPerturbPoint(Point3& pt) const
|
|
|
|
{
|
|
|
|
pt.x += fRand.RandMinusOneToOne() * fRndPosRadius;
|
|
|
|
pt.y += fRand.RandMinusOneToOne() * fRndPosRadius;
|
|
|
|
pt.z += fRand.RandMinusOneToOne() * fRndPosRadius;
|
|
|
|
return pt;
|
|
|
|
}
|
|
|
|
|
|
|
|
Box3 plDistributor::ISetupGrid(const Point3& p0, const Point3& p1, const Point3& p2) const
|
|
|
|
{
|
|
|
|
// Add half spacing to max's to protect against precision errors.
|
|
|
|
Box3 box(p0, p0);
|
|
|
|
box += p1;
|
|
|
|
box += p2;
|
|
|
|
|
|
|
|
Point3 mins, maxs;
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < 3; i++ )
|
|
|
|
{
|
|
|
|
float t = box.Min()[i];
|
|
|
|
t /= fSpacing;
|
|
|
|
t = floor(t);
|
|
|
|
t *= fSpacing;
|
|
|
|
mins[i] = t;
|
|
|
|
|
|
|
|
t = box.Max()[i];
|
|
|
|
t /= fSpacing;
|
|
|
|
t = ceil(t);
|
|
|
|
t *= fSpacing;
|
|
|
|
maxs[i] = t + fSpacing*0.5f;
|
|
|
|
}
|
|
|
|
box = Box3(mins, maxs);
|
|
|
|
|
|
|
|
return box;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool plDistributor::IFailsProbBitmap(int iFace, const Point3& bary) const
|
|
|
|
{
|
|
|
|
// If we don't have a probability map, or we don't have
|
|
|
|
// valid coordinates into it, just return false. That is,
|
|
|
|
// with no valid probability map, everything goes.
|
|
|
|
int uvwChan = 1;
|
|
|
|
Matrix3 uvtrans(true);
|
|
|
|
Bitmap* bm = nil;
|
|
|
|
UINT filtType = BMM_FILTER_PYRAMID;
|
|
|
|
if( fProbBitmapTex )
|
|
|
|
{
|
|
|
|
uvwChan = fProbBitmapTex->GetMapChannel();
|
|
|
|
fProbBitmapTex->GetUVTransform(uvtrans);
|
|
|
|
|
|
|
|
bm = fProbBitmapTex->GetBitmap(TimeValue(0));
|
|
|
|
|
|
|
|
if( bm && !bm->HasFilter() )
|
|
|
|
{
|
|
|
|
switch( fProbBitmapTex->GetFilterType() )
|
|
|
|
{
|
|
|
|
default:
|
|
|
|
case FILTER_PYR:
|
|
|
|
filtType = BMM_FILTER_PYRAMID;
|
|
|
|
break;
|
|
|
|
case FILTER_SAT:
|
|
|
|
filtType = BMM_FILTER_SUM;
|
|
|
|
break;
|
|
|
|
case FILTER_NADA:
|
|
|
|
filtType = BMM_FILTER_NONE;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if( fProbLayerTex )
|
|
|
|
{
|
|
|
|
uvwChan = fProbLayerTex->GetMapChannel();
|
|
|
|
fProbLayerTex->GetUVTransform(uvtrans);
|
|
|
|
|
|
|
|
bm = fProbLayerTex->GetBitmap(TimeValue(0));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !bm )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( !bm->HasFilter() )
|
|
|
|
bm->SetFilter(filtType);
|
|
|
|
|
|
|
|
bm->PrepareGChannels(&bm->Storage()->bi);
|
|
|
|
|
|
|
|
if( !fSurfMesh->mapSupport(uvwChan) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( !fSurfMesh->mapFaces(uvwChan) || !fSurfMesh->mapVerts(uvwChan) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Lookup the appropriate texel value
|
|
|
|
Point3 uvw;
|
|
|
|
uvw = fSurfMesh->mapVerts(uvwChan)[fSurfMesh->mapFaces(uvwChan)[iFace].getTVert(0)] * bary[0];
|
|
|
|
uvw += fSurfMesh->mapVerts(uvwChan)[fSurfMesh->mapFaces(uvwChan)[iFace].getTVert(1)] * bary[1];
|
|
|
|
uvw += fSurfMesh->mapVerts(uvwChan)[fSurfMesh->mapFaces(uvwChan)[iFace].getTVert(2)] * bary[2];
|
|
|
|
|
|
|
|
uvw = uvw * uvtrans;
|
|
|
|
|
|
|
|
float fu = uvw.x - int(uvw.x);
|
|
|
|
if( fu < 0 )
|
|
|
|
fu += 1.f;
|
|
|
|
float fv = 1.0f - (uvw.y - int(uvw.y));
|
|
|
|
if( fv < 0 )
|
|
|
|
fv += 1.f;
|
|
|
|
float du = 1.f / bm->Width();
|
|
|
|
float dv = 1.f / bm->Height();
|
|
|
|
|
|
|
|
BMM_Color_fl evCol;
|
|
|
|
bm->GetFiltered(fu, fv, du, dv, &evCol);
|
|
|
|
|
|
|
|
float frac;
|
|
|
|
switch( fProbColorChan )
|
|
|
|
{
|
|
|
|
case kRed:
|
|
|
|
frac = evCol.r;
|
|
|
|
break;
|
|
|
|
case kGreen:
|
|
|
|
frac = evCol.g;
|
|
|
|
break;
|
|
|
|
case kBlue:
|
|
|
|
frac = evCol.b;
|
|
|
|
break;
|
|
|
|
case kAlpha:
|
|
|
|
frac = evCol.a;
|
|
|
|
break;
|
|
|
|
case kAverageRedGreen:
|
|
|
|
frac = (evCol.r + evCol.g) / 2.f;
|
|
|
|
break;
|
|
|
|
case kAverageRedGreenTimesAlpha:
|
|
|
|
frac = (evCol.r + evCol.g) / 2.f * evCol.a;
|
|
|
|
break;
|
|
|
|
case kAverage:
|
|
|
|
frac = (evCol.r + evCol.g + evCol.b ) / 3.f;
|
|
|
|
break;
|
|
|
|
case kAverageTimesAlpha:
|
|
|
|
frac = (evCol.r + evCol.g + evCol.b ) / 3.f * evCol.a;
|
|
|
|
break;
|
|
|
|
case kMax:
|
|
|
|
case kMaxColor:
|
|
|
|
frac = hsMaximum(evCol.r, hsMaximum(evCol.g, evCol.b));
|
|
|
|
break;
|
|
|
|
case kMaxColorTimesAlpha:
|
|
|
|
frac = hsMaximum(evCol.r, hsMaximum(evCol.g, evCol.b)) * evCol.a;
|
|
|
|
break;
|
|
|
|
case kMaxRedGreen:
|
|
|
|
frac = hsMaximum(evCol.r, evCol.g);
|
|
|
|
break;
|
|
|
|
case kMaxRedGreenTimesAlpha:
|
|
|
|
frac = hsMaximum(evCol.r, evCol.g) * evCol.a;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( fProbRemapFromHi != fProbRemapFromLo )
|
|
|
|
frac = fProbRemapToLo + (frac - fProbRemapFromLo) / (fProbRemapFromHi - fProbRemapFromLo) * (fProbRemapToHi - fProbRemapToLo);
|
|
|
|
else
|
|
|
|
frac = frac > fProbRemapFromHi ? fProbRemapToHi : fProbRemapToLo;
|
|
|
|
|
|
|
|
return frac < fRand.RandZeroToOne();
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 plDistributor::IGetSurfaceNormal(int iFace, const Point3& bary) const
|
|
|
|
{
|
|
|
|
fSurfMesh->checkNormals(true);
|
|
|
|
|
|
|
|
if( !fFaceNormals )
|
|
|
|
{
|
|
|
|
Face& face = fSurfMesh->faces[iFace];
|
|
|
|
Point3 norm = FNormalize(fSurfMesh->getNormal(face.getVert(0))) * bary[0];
|
|
|
|
norm += FNormalize(fSurfMesh->getNormal(face.getVert(1))) * bary[1];
|
|
|
|
norm += FNormalize(fSurfMesh->getNormal(face.getVert(2))) * bary[2];
|
|
|
|
|
|
|
|
return norm;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 faceNorm = fSurfMesh->getFaceNormal(iFace);
|
|
|
|
return FNormalize(faceNorm);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool plDistributor::IFailsAngProb(int iFace, const Point3& bary) const
|
|
|
|
{
|
|
|
|
if( fAngProbLo == fAngProbHi )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
Point3 norm = IGetSurfaceNormal(iFace, bary);
|
|
|
|
|
|
|
|
float dot = DotProd(norm, fSurfAngProbVec);
|
|
|
|
|
|
|
|
if( dot > fCosAngProbHi )
|
|
|
|
return true;
|
|
|
|
if( dot < fCosAngProbLo )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
if( dot > fCosAngProbHiTrans )
|
|
|
|
{
|
|
|
|
float prob = fCosAngProbHi - dot;
|
|
|
|
prob /= fCosAngProbHi - fCosAngProbHiTrans;
|
|
|
|
return fRand.RandZeroToOne() >= prob;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( dot < fCosAngProbLoTrans )
|
|
|
|
{
|
|
|
|
float prob = dot - fCosAngProbLo;
|
|
|
|
prob /= fCosAngProbLoTrans - fCosAngProbLo;
|
|
|
|
return fRand.RandZeroToOne() >= prob;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
bool plDistributor::IFailsAltProb(int iFace, const Point3& bary) const
|
|
|
|
{
|
|
|
|
if( fAltProbLo == fAltProbHi )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
Face& face = fSurfMesh->faces[iFace];
|
|
|
|
Point3 pos = fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(0)) * bary[0];
|
|
|
|
pos += fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(1)) * bary[1];
|
|
|
|
pos += fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(2)) * bary[2];
|
|
|
|
|
|
|
|
pos = pos * fSurfToWorld;
|
|
|
|
|
|
|
|
if( pos.z > fAltProbHi )
|
|
|
|
return true;
|
|
|
|
if( pos.z < fAltProbLo )
|
|
|
|
return true;
|
|
|
|
if( pos.z < fAltProbLo + fAltProbTrans )
|
|
|
|
{
|
|
|
|
float prob = (pos.z - fAltProbLo) / fAltProbTrans;
|
|
|
|
return fRand.RandZeroToOne() >= prob;
|
|
|
|
}
|
|
|
|
if( pos.z > fAltProbHi - fAltProbTrans )
|
|
|
|
{
|
|
|
|
float prob = (fAltProbHi - pos.z) / fAltProbTrans;
|
|
|
|
return fRand.RandZeroToOne() >= prob;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// If |projGridPt - GridPt| < gridCubeRadius
|
|
|
|
// and probBitmap->GetPixel(src->UVW(bary)) < RandomZeroToOne()
|
|
|
|
// Also a generic random factor.
|
|
|
|
bool plDistributor::IProbablyDoIt(int iFace, Point3& del, const Point3& bary) const
|
|
|
|
{
|
|
|
|
if( fRand.RandZeroToOne() >= fOverallProb )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( (kIsoNone == fIsolation) || (kIsoLow == fIsolation) )
|
|
|
|
{
|
|
|
|
if( LengthSquared(del) >= fSpacing*fSpacing )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 faceNorm = fSurfMesh->FaceNormal(iFace, FALSE);
|
|
|
|
if( DotProd(del, faceNorm) < 0 )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( IFailsAngProb(iFace, bary) )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( IFailsAltProb(iFace, bary) )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( IFailsProbBitmap(iFace, bary) )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 plDistributor::IPerpAxis(const Point3& p) const
|
|
|
|
{
|
|
|
|
const float kMinLengthSquared = 1.e-1f;
|
|
|
|
|
|
|
|
int minAx = p.MinComponent();
|
|
|
|
Point3 ax(0,0,0);
|
|
|
|
ax[minAx] = 1.f;
|
|
|
|
|
|
|
|
Point3 perp = p ^ ax;
|
|
|
|
if( perp.LengthSquared() < kMinLengthSquared )
|
|
|
|
{
|
|
|
|
// hmm, think we might be screwed, but this shouldn't happen.
|
|
|
|
}
|
|
|
|
|
|
|
|
return perp = perp.FNormalize();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate local to world from face info (pos, normal, etc)
|
|
|
|
Matrix3 plDistributor::IGenerateTransform(int iRepNode, int iFace, const Point3& pt, const Point3& bary) const
|
|
|
|
{
|
|
|
|
const float kMinVecLengthSq = 1.e-6f;
|
|
|
|
Matrix3 l2w(true);
|
|
|
|
|
|
|
|
// First, set the scale
|
|
|
|
Point3 scale;
|
|
|
|
switch( fScaleLock )
|
|
|
|
{
|
|
|
|
case kLockX | kLockY:
|
|
|
|
scale.x = fRand.RandRangeF(fScaleLo.x, fScaleHi.x);
|
|
|
|
scale.y = scale.x;
|
|
|
|
scale.z = fRand.RandRangeF(fScaleLo.z, fScaleHi.z);
|
|
|
|
break;
|
|
|
|
case kLockX | kLockY | kLockZ:
|
|
|
|
scale.x = fRand.RandRangeF(fScaleLo.x, fScaleHi.x);
|
|
|
|
scale.y = scale.z = scale.x;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
scale.x = fRand.RandRangeF(fScaleLo.x, fScaleHi.x);
|
|
|
|
scale.y = fRand.RandRangeF(fScaleLo.y, fScaleHi.y);
|
|
|
|
scale.z = fRand.RandRangeF(fScaleLo.z, fScaleHi.z);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
l2w.Scale(scale);
|
|
|
|
|
|
|
|
// Next up, get the rotation.
|
|
|
|
// First we'll randomly rotate about local Z
|
|
|
|
float azimRot = fRand.RandMinusOneToOne() * fAzimuthRange;
|
|
|
|
Matrix3 azimMat;
|
|
|
|
azimMat.SetRotateZ(azimRot);
|
|
|
|
|
|
|
|
l2w = l2w * azimMat;
|
|
|
|
|
|
|
|
// Now align with the surface.
|
|
|
|
// Get the interpolated surface normal.
|
|
|
|
Point3 surfNorm = IGetSurfaceNormal(iFace, bary);
|
|
|
|
|
|
|
|
Matrix3 repNodeTM = fRepNodes[iRepNode]->GetNodeTM(TimeValue(0));
|
|
|
|
|
|
|
|
Point3 alignVec = repNodeTM.GetRow(2);
|
|
|
|
alignVec = alignVec * fWorldToSurfVec;
|
|
|
|
alignVec = FNormalize(alignVec);
|
|
|
|
|
|
|
|
Point3 norm = surfNorm + (alignVec - surfNorm) * fAlignWgt;
|
|
|
|
// The norm can come out of this zero length, if the surace normal
|
|
|
|
// is directly opposite the "natural" up direction and the weight
|
|
|
|
// is 50% (for example). In that case, this is just a bad place
|
|
|
|
// to drop this replicant.
|
|
|
|
if( norm.LengthSquared() < kMinVecLengthSq )
|
|
|
|
{
|
|
|
|
l2w.IdentityMatrix();
|
|
|
|
return l2w;
|
|
|
|
}
|
|
|
|
norm = norm.Normalize();
|
|
|
|
|
|
|
|
// Randomize through the cone around that.
|
|
|
|
Point3 rndNorm = norm;
|
|
|
|
Point3 rndDir = IPerpAxis(norm);
|
|
|
|
Point3 rndOut = rndDir ^ norm;
|
|
|
|
rndDir *= fRand.RandMinusOneToOne();
|
|
|
|
float len = sqrt(1.f - rndDir.LengthSquared());
|
|
|
|
rndOut *= len;
|
|
|
|
if( fRand.RandMinusOneToOne() < 0 )
|
|
|
|
rndOut *= -1.f;
|
|
|
|
Point3 rndPol = rndDir + rndOut;
|
|
|
|
|
|
|
|
float polScale = fRand.RandZeroToOne() * fTanPolarRange;
|
|
|
|
|
|
|
|
// Combine using the bunching factor
|
|
|
|
polScale = polScale * (1.f - fPolarBunch) + polScale * polScale * fPolarBunch;
|
|
|
|
|
|
|
|
rndPol *= polScale;
|
|
|
|
rndNorm += rndPol;
|
|
|
|
norm = rndNorm.Normalize();
|
|
|
|
|
|
|
|
// Have "up" alignment, now just generate random dir vector perpindicular to up
|
|
|
|
Point3 dir = repNodeTM.GetRow(1);
|
|
|
|
dir = dir * fWorldToSurfVec;
|
|
|
|
Point3 out = dir ^ norm;
|
|
|
|
if( out.LengthSquared() < kMinVecLengthSq )
|
|
|
|
{
|
|
|
|
if( fAzimuthRange < M_PI * 0.5f )
|
|
|
|
{
|
|
|
|
l2w.IdentityMatrix();
|
|
|
|
return l2w;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
dir = IPerpAxis(norm);
|
|
|
|
out = dir ^ norm;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
out = FNormalize(out);
|
|
|
|
dir = norm ^ out;
|
|
|
|
|
|
|
|
// If our "up" direction points into the surface, return an "up" direction
|
|
|
|
// tangent to the surface. Also, make the "dir" direction point out from
|
|
|
|
// the surface. So if the alignVec/fAlignWgt turns the replicant around
|
|
|
|
// to penetrate the surface, it just lies down instead.
|
|
|
|
//
|
|
|
|
// There's an early out here, for the case where the surface normal is
|
|
|
|
// exactly opposed to the destination normal. This usually means the
|
|
|
|
// surface normal is directly opposite the alignVec. In that
|
|
|
|
// case, we just want to bag it.
|
|
|
|
if( DotProd(norm, surfNorm) < 0 )
|
|
|
|
{
|
|
|
|
dir = surfNorm;
|
|
|
|
dir = dir.Normalize();
|
|
|
|
out = dir ^ norm;
|
|
|
|
if( out.LengthSquared() < kMinVecLengthSq )
|
|
|
|
{
|
|
|
|
l2w.IdentityMatrix();
|
|
|
|
return l2w;
|
|
|
|
}
|
|
|
|
out = out.Normalize();
|
|
|
|
norm = out ^ dir;
|
|
|
|
}
|
|
|
|
|
|
|
|
Matrix3 align;
|
|
|
|
align.Set(out, dir, norm, Point3(0,0,0));
|
|
|
|
|
|
|
|
l2w = l2w * align;
|
|
|
|
|
|
|
|
|
|
|
|
// Lastly, set the position.
|
|
|
|
Point3 pos = pt;
|
|
|
|
const float offset = fRand.RandRangeF(fOffsetMin, fOffsetMax);
|
|
|
|
pos += norm * offset;
|
|
|
|
l2w.Translate(pos);
|
|
|
|
|
|
|
|
l2w = l2w * fSurfNode->GetObjectTM(TimeValue(0));
|
|
|
|
|
|
|
|
return l2w;
|
|
|
|
}
|
|
|
|
|
|
|
|
int plDistributor::ISelectRepNode() const
|
|
|
|
{
|
|
|
|
if( fRepNodes.Count() < 2 )
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
int i = fRand.RandRangeI(0, fRepNodes.Count()-1);
|
|
|
|
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::ISetupNormals(plMaxNode* node, Mesh* mesh, BOOL radiateNorm) const
|
|
|
|
{
|
|
|
|
const char* dbgNodeName = node->GetName();
|
|
|
|
|
|
|
|
UVVert *normMap = mesh->mapVerts(kNormMapChan);
|
|
|
|
int numNormVerts = mesh->getNumMapVerts(kNormMapChan);
|
|
|
|
if( !mesh->mapSupport(kNormMapChan) || !mesh->mapVerts(kNormMapChan) || !mesh->mapFaces(kNormMapChan) )
|
|
|
|
{
|
|
|
|
mesh->setMapSupport(kNormMapChan);
|
|
|
|
|
|
|
|
mesh->setNumMapVerts(kNormMapChan, mesh->getNumVerts());
|
|
|
|
mesh->setNumMapFaces(kNormMapChan, mesh->getNumFaces());
|
|
|
|
}
|
|
|
|
|
|
|
|
int i;
|
|
|
|
if( radiateNorm )
|
|
|
|
{
|
|
|
|
Matrix3 otm = node->GetOTM();
|
|
|
|
Matrix3 invOtm = Inverse(otm);
|
|
|
|
invOtm.SetTrans(Point3(0,0,0));
|
|
|
|
invOtm.ValidateFlags();
|
|
|
|
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 pos = mesh->getVert(i) * otm;
|
|
|
|
pos = pos * invOtm;
|
|
|
|
|
|
|
|
mesh->setMapVert(kNormMapChan, i, pos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
mesh->checkNormals(true);
|
|
|
|
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 norm = mesh->getNormal(i);
|
|
|
|
|
|
|
|
mesh->setMapVert(kNormMapChan, i, norm);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TVFace* mapFaces = mesh->mapFaces(kNormMapChan);
|
|
|
|
Face* faces = mesh->faces;
|
|
|
|
for( i = 0; i < mesh->getNumFaces(); i++ )
|
|
|
|
{
|
|
|
|
mapFaces[i].setTVerts(faces[i].getVert(0), faces[i].getVert(1), faces[i].getVert(2));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
BOOL plDistributor::ISetupSkinWeights(plMaxNode* node, Mesh* mesh, const Point3& flex) const
|
|
|
|
{
|
|
|
|
const char* dbgNodeName = node->GetName();
|
|
|
|
|
|
|
|
Matrix3 otm = node->GetOTM();
|
|
|
|
|
|
|
|
Box3 bnd = mesh->getBoundingBox() * otm;
|
|
|
|
|
|
|
|
float meshHeight = bnd.Max().z;
|
|
|
|
float maxHeight = kMaxHeight;
|
|
|
|
if( meshHeight > maxHeight )
|
|
|
|
maxHeight = meshHeight;
|
|
|
|
float maxNorm = meshHeight / maxHeight;
|
|
|
|
|
|
|
|
float flexibility = flex[0];
|
|
|
|
|
|
|
|
UVVert *wgtMap = mesh->mapVerts(kWgtMapChan);
|
|
|
|
int numWgtVerts = mesh->getNumMapVerts(kWgtMapChan);
|
|
|
|
if( !mesh->mapSupport(kWgtMapChan) || !mesh->mapVerts(kWgtMapChan) || !mesh->mapFaces(kWgtMapChan) )
|
|
|
|
{
|
|
|
|
mesh->setMapSupport(kWgtMapChan);
|
|
|
|
|
|
|
|
mesh->setNumMapVerts(kWgtMapChan, mesh->getNumVerts());
|
|
|
|
mesh->setNumMapFaces(kWgtMapChan, mesh->getNumFaces());
|
|
|
|
}
|
|
|
|
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 pos = mesh->getVert(i) * otm;
|
|
|
|
float wgt = pos.z / meshHeight;
|
|
|
|
wgt *= wgt > 0 ? wgt : 0;
|
|
|
|
wgt *= maxNorm;
|
|
|
|
wgt *= flexibility;
|
|
|
|
|
|
|
|
pos.x = wgt;
|
|
|
|
pos.y = wgt;
|
|
|
|
pos.z = wgt;
|
|
|
|
|
|
|
|
mesh->setMapVert(kWgtMapChan, i, pos);
|
|
|
|
}
|
|
|
|
|
|
|
|
TVFace* mapFaces = mesh->mapFaces(kWgtMapChan);
|
|
|
|
Face* faces = mesh->faces;
|
|
|
|
for( i = 0; i < mesh->getNumFaces(); i++ )
|
|
|
|
{
|
|
|
|
mapFaces[i].setTVerts(faces[i].getVert(0), faces[i].getVert(1), faces[i].getVert(2));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IDuplicate2Sided(plMaxNode* node, Mesh* mesh) const
|
|
|
|
{
|
|
|
|
Mtl* mtl = node->GetMtl();
|
|
|
|
|
|
|
|
BitArray faces(mesh->getNumFaces());
|
|
|
|
|
|
|
|
int num2Sided = 0;
|
|
|
|
|
|
|
|
int origNumFaces = mesh->getNumFaces();
|
|
|
|
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < mesh->getNumFaces(); i++ )
|
|
|
|
{
|
|
|
|
if( hsMaterialConverter::IsTwoSided(mtl, mesh->faces[i].getMatID()) )
|
|
|
|
{
|
|
|
|
num2Sided++;
|
|
|
|
faces.Set(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !num2Sided )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
MeshDelta meshDelta(*mesh);
|
|
|
|
meshDelta.CloneFaces(*mesh, faces);
|
|
|
|
meshDelta.Apply(*mesh);
|
|
|
|
|
|
|
|
BitArray verts(mesh->getNumVerts());
|
|
|
|
verts.SetAll();
|
|
|
|
const float kWeldThresh = 0.1f;
|
|
|
|
meshDelta.WeldByThreshold(*mesh, verts, kWeldThresh);
|
|
|
|
meshDelta.Apply(*mesh);
|
|
|
|
|
|
|
|
hsAssert(origNumFaces + num2Sided == mesh->getNumFaces(), "Whoa, lost or gained, unexpected");
|
|
|
|
|
|
|
|
for( i = origNumFaces; i < mesh->getNumFaces(); i++ )
|
|
|
|
{
|
|
|
|
meshDelta.FlipNormal(*mesh, i);
|
|
|
|
}
|
|
|
|
meshDelta.Apply(*mesh);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IReadyRepNodes(plMeshCacheTab& cache) const
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < fRepNodes.Count(); i++ )
|
|
|
|
{
|
|
|
|
Mesh* mesh = nil;
|
|
|
|
TriObject* obj = nil;
|
|
|
|
if( IGetMesh(fRepNodes[i], obj, mesh) )
|
|
|
|
{
|
|
|
|
plMaxNode* repNode = (plMaxNode*)fRepNodes[i];
|
|
|
|
|
|
|
|
int iCache = cache.Count();
|
|
|
|
cache.SetCount(iCache + 1);
|
|
|
|
|
|
|
|
cache[iCache].fMesh = new Mesh(*mesh);
|
|
|
|
cache[iCache].fFlex = repNode->GetFlexibility();
|
|
|
|
|
|
|
|
if( obj )
|
|
|
|
obj->DeleteThis();
|
|
|
|
|
|
|
|
BOOL hasXImp = nil != repNode->GetXImposterComp();
|
|
|
|
|
|
|
|
ISetupNormals(repNode, cache[iCache].fMesh, hasXImp);
|
|
|
|
|
|
|
|
ISetupSkinWeights(repNode, cache[iCache].fMesh, cache[iCache].fFlex);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
fRepNodes.Delete(i, 1);
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return fRepNodes.Count() > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::ClearReplicateNodes()
|
|
|
|
{
|
|
|
|
fRepNodes.ZeroCount();
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::AddReplicateNode(INode* node)
|
|
|
|
{
|
|
|
|
fRepNodes.Append(1, &node);
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IProjectVertex(const Point3& pt, const Point3& dir, float maxDist, Tab<int32_t>&faces, Point3& projPt) const
|
|
|
|
{
|
|
|
|
BOOL retVal = false;
|
|
|
|
plTriUtils triUtil;
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < faces.Count(); i++ )
|
|
|
|
{
|
|
|
|
int iFace = faces[i];
|
|
|
|
const hsPoint3& p0 = hsP3(fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(0)) * fSurfToWorld);
|
|
|
|
const hsPoint3& p1 = hsP3(fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(1)) * fSurfToWorld);
|
|
|
|
const hsPoint3& p2 = hsP3(fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(2)) * fSurfToWorld);
|
|
|
|
|
|
|
|
Point3 plnPt = pt;
|
|
|
|
if( triUtil.ProjectOntoPlaneAlongVector(p0, p1, p2, hsV3(dir), hsP3(plnPt)) )
|
|
|
|
{
|
|
|
|
Point3 bary = plnPt;
|
|
|
|
plTriUtils::Bary baryVal = triUtil.ComputeBarycentric(p0, p1, p2, hsP3(plnPt), hsP3(bary));
|
|
|
|
if( (plTriUtils::kOutsideTri != baryVal) && (plTriUtils::kDegenerateTri != baryVal) )
|
|
|
|
{
|
|
|
|
float dist = DotProd((pt - plnPt), dir);
|
|
|
|
if( (dist <= maxDist) && (dist >= -maxDist) )
|
|
|
|
{
|
|
|
|
projPt = plnPt;
|
|
|
|
maxDist = dist >= 0 ? dist : -dist;
|
|
|
|
retVal = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return retVal;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IConformCheck(Matrix3& l2w, int iRepNode, plMeshCacheTab& cache, int& iCache) const
|
|
|
|
{
|
|
|
|
Matrix3 OTM = IOTM(iRepNode);
|
|
|
|
Mesh* mesh = cache[iRepNode].fMesh;
|
|
|
|
|
|
|
|
Point3 dir = l2w.VectorTransform(Point3(0.f, 0.f, 1.f));
|
|
|
|
dir = FNormalize(dir);
|
|
|
|
|
|
|
|
const float kOneOverSqrt2 = 0.707107f;
|
|
|
|
Point3 scalePt(kOneOverSqrt2, kOneOverSqrt2, 0.f);
|
|
|
|
scalePt = l2w.VectorTransform(scalePt);
|
|
|
|
float maxScaledDist = fMaxConform * scalePt.Length();
|
|
|
|
|
|
|
|
Box3 bnd = mesh->getBoundingBox() * OTM;
|
|
|
|
bnd = Box3(Point3(bnd.Min().x, bnd.Min().y, -bnd.Max().z), bnd.Max());
|
|
|
|
bnd = bnd * l2w;
|
|
|
|
Tab<int32_t> faces;
|
|
|
|
IFindFaceSet(bnd, faces);
|
|
|
|
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 pt = mesh->getVert(i) * OTM;
|
|
|
|
pt.z = 0;
|
|
|
|
|
|
|
|
pt = pt * l2w;
|
|
|
|
|
|
|
|
Point3 projPt;
|
|
|
|
if( !IProjectVertex(pt, dir, maxScaledDist, faces, projPt) )
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IConformAll(Matrix3& l2w, int iRepNode, plMeshCacheTab& cache, int& iCache) const
|
|
|
|
{
|
|
|
|
Matrix3 OTM = IOTM(iRepNode);
|
|
|
|
Mesh* mesh = cache[iRepNode].fMesh;
|
|
|
|
|
|
|
|
Point3 dir = l2w.VectorTransform(Point3(0.f, 0.f, 1.f));
|
|
|
|
dir = FNormalize(dir);
|
|
|
|
|
|
|
|
const float kOneOverSqrt2 = 0.707107f;
|
|
|
|
Point3 scalePt(kOneOverSqrt2, kOneOverSqrt2, 0.f);
|
|
|
|
scalePt = l2w.VectorTransform(scalePt);
|
|
|
|
float maxScaledDist = fMaxConform * scalePt.Length();
|
|
|
|
|
|
|
|
Box3 bnd = mesh->getBoundingBox() * OTM;
|
|
|
|
bnd = Box3(Point3(bnd.Min().x, bnd.Min().y, -bnd.Max().z), bnd.Max());
|
|
|
|
bnd = bnd * l2w;
|
|
|
|
Tab<int32_t> faces;
|
|
|
|
IFindFaceSet(bnd, faces);
|
|
|
|
|
|
|
|
// l2w, iRepNode, cache, &iCache, maxScaledDist, dir
|
|
|
|
iCache = cache.Count();
|
|
|
|
cache.SetCount(iCache + 1);
|
|
|
|
cache[iCache] = cache[iRepNode];
|
|
|
|
cache[iCache].fMesh = new Mesh(*mesh);
|
|
|
|
|
|
|
|
mesh = cache[iCache].fMesh;
|
|
|
|
|
|
|
|
Matrix3 v2w = OTM * l2w;
|
|
|
|
Matrix3 w2v = Inverse(v2w);
|
|
|
|
|
|
|
|
BOOL retVal = true;
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 pt = mesh->getVert(i) * OTM;
|
|
|
|
pt.z = 0;
|
|
|
|
|
|
|
|
pt = pt * l2w;
|
|
|
|
|
|
|
|
Point3 projPt;
|
|
|
|
if( !IProjectVertex(pt, dir, maxScaledDist, faces, projPt) )
|
|
|
|
{
|
|
|
|
retVal = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 del = w2v.VectorTransform(projPt - pt);
|
|
|
|
mesh->getVert(i) += del;
|
|
|
|
}
|
|
|
|
if( !retVal )
|
|
|
|
{
|
|
|
|
// delete cache[iCache].fMesh;
|
|
|
|
delete mesh;
|
|
|
|
cache.SetCount(iCache);
|
|
|
|
iCache = iRepNode;
|
|
|
|
}
|
|
|
|
return retVal;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IConformHeight(Matrix3& l2w, int iRepNode, plMeshCacheTab& cache, int& iCache) const
|
|
|
|
{
|
|
|
|
Matrix3 OTM = IOTM(iRepNode);
|
|
|
|
Mesh* mesh = cache[iRepNode].fMesh;
|
|
|
|
|
|
|
|
Point3 dir = l2w.VectorTransform(Point3(0.f, 0.f, 1.f));
|
|
|
|
dir = FNormalize(dir);
|
|
|
|
|
|
|
|
const float kOneOverSqrt2 = 0.707107f;
|
|
|
|
Point3 scalePt(kOneOverSqrt2, kOneOverSqrt2, 0.f);
|
|
|
|
scalePt = l2w.VectorTransform(scalePt);
|
|
|
|
float maxScaledDist = fMaxConform * scalePt.Length();
|
|
|
|
|
|
|
|
Box3 bnd = mesh->getBoundingBox() * OTM;
|
|
|
|
bnd = Box3(Point3(bnd.Min().x, bnd.Min().y, -bnd.Max().z), bnd.Max());
|
|
|
|
bnd = bnd * l2w;
|
|
|
|
Tab<int32_t> faces;
|
|
|
|
IFindFaceSet(bnd, faces);
|
|
|
|
|
|
|
|
// l2w, iRepNode, cache, &iCache, maxScaledDist, dir
|
|
|
|
iCache = cache.Count();
|
|
|
|
cache.SetCount(iCache + 1);
|
|
|
|
cache[iCache] = cache[iRepNode];
|
|
|
|
cache[iCache].fMesh = new Mesh(*mesh);
|
|
|
|
|
|
|
|
mesh = cache[iCache].fMesh;
|
|
|
|
|
|
|
|
Matrix3 v2w = OTM * l2w;
|
|
|
|
Matrix3 w2v = Inverse(v2w);
|
|
|
|
|
|
|
|
|
|
|
|
float maxZ = (mesh->getBoundingBox() * OTM).Max().z;
|
|
|
|
|
|
|
|
BOOL retVal = true;
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 pt = mesh->getVert(i) * OTM;
|
|
|
|
|
|
|
|
float conScale = 1.f - pt.z / maxZ;
|
|
|
|
if( conScale > 1.f )
|
|
|
|
conScale = 1.f;
|
|
|
|
else if( conScale < 0 )
|
|
|
|
conScale = 0;
|
|
|
|
|
|
|
|
pt.z = 0;
|
|
|
|
|
|
|
|
pt = pt * l2w;
|
|
|
|
|
|
|
|
Point3 projPt;
|
|
|
|
if( !IProjectVertex(pt, dir, maxScaledDist, faces, projPt) )
|
|
|
|
{
|
|
|
|
retVal = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 del = w2v.VectorTransform(projPt - pt);
|
|
|
|
del *= conScale;
|
|
|
|
mesh->getVert(i) += del;
|
|
|
|
}
|
|
|
|
if( !retVal )
|
|
|
|
{
|
|
|
|
delete cache[iCache].fMesh;
|
|
|
|
cache.SetCount(iCache);
|
|
|
|
iCache = iRepNode;
|
|
|
|
}
|
|
|
|
return retVal;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IConformBase(Matrix3& l2w, int iRepNode, plMeshCacheTab& cache, int& iCache) const
|
|
|
|
{
|
|
|
|
Matrix3 OTM = IOTM(iRepNode);
|
|
|
|
Mesh* mesh = cache[iRepNode].fMesh;
|
|
|
|
|
|
|
|
Point3 dir = l2w.VectorTransform(Point3(0.f, 0.f, 1.f));
|
|
|
|
dir = FNormalize(dir);
|
|
|
|
|
|
|
|
const float kOneOverSqrt2 = 0.707107f;
|
|
|
|
Point3 scalePt(kOneOverSqrt2, kOneOverSqrt2, 0.f);
|
|
|
|
scalePt = l2w.VectorTransform(scalePt);
|
|
|
|
float maxScaledDist = fMaxConform * scalePt.Length();
|
|
|
|
|
|
|
|
Box3 bnd = mesh->getBoundingBox() * OTM;
|
|
|
|
bnd = Box3(Point3(bnd.Min().x, bnd.Min().y, -bnd.Max().z), bnd.Max());
|
|
|
|
bnd = bnd * l2w;
|
|
|
|
Tab<int32_t> faces;
|
|
|
|
IFindFaceSet(bnd, faces);
|
|
|
|
|
|
|
|
// l2w, iRepNode, cache, &iCache, maxScaledDist, dir
|
|
|
|
iCache = cache.Count();
|
|
|
|
cache.SetCount(iCache + 1);
|
|
|
|
cache[iCache] = cache[iRepNode];
|
|
|
|
cache[iCache].fMesh = new Mesh(*mesh);
|
|
|
|
|
|
|
|
mesh = cache[iCache].fMesh;
|
|
|
|
|
|
|
|
Matrix3 v2w = OTM * l2w;
|
|
|
|
Matrix3 w2v = Inverse(v2w);
|
|
|
|
|
|
|
|
|
|
|
|
float maxZ = (mesh->getBoundingBox() * OTM).Max().z;
|
|
|
|
|
|
|
|
BOOL retVal = true;
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < mesh->getNumVerts(); i++ )
|
|
|
|
{
|
|
|
|
Point3 pt = mesh->getVert(i) * OTM;
|
|
|
|
|
|
|
|
const float kMaxConformZ = 0.5f;
|
|
|
|
if( pt.z < kMaxConformZ )
|
|
|
|
{
|
|
|
|
pt.z = 0;
|
|
|
|
|
|
|
|
pt = pt * l2w;
|
|
|
|
|
|
|
|
Point3 projPt;
|
|
|
|
if( !IProjectVertex(pt, dir, maxScaledDist, faces, projPt) )
|
|
|
|
{
|
|
|
|
retVal = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
Point3 del = w2v.VectorTransform(projPt - pt);
|
|
|
|
mesh->getVert(i) += del;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if( !retVal )
|
|
|
|
{
|
|
|
|
delete cache[iCache].fMesh;
|
|
|
|
cache.SetCount(iCache);
|
|
|
|
iCache = iRepNode;
|
|
|
|
}
|
|
|
|
return retVal;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::IConform(Matrix3& l2w, int iRepNode, plMeshCacheTab& cache, int& iCache) const
|
|
|
|
{
|
|
|
|
iCache = iRepNode;
|
|
|
|
switch( fConformity )
|
|
|
|
{
|
|
|
|
case kConformAll:
|
|
|
|
return IConformAll(l2w, iRepNode, cache, iCache);
|
|
|
|
case kConformHeight:
|
|
|
|
return IConformHeight(l2w, iRepNode, cache, iCache);
|
|
|
|
case kConformCheck:
|
|
|
|
return IConformCheck(l2w, iRepNode, cache, iCache);
|
|
|
|
case kConformBase:
|
|
|
|
return IConformBase(l2w, iRepNode, cache, iCache);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clone the replicant and set its transform appropriately. Should set the plMaxNode property
|
|
|
|
// that the node's spans are collapseable? No, we'll make a separate component for that.
|
|
|
|
void plDistributor::IReplicate(Matrix3& l2w, int iRepNode, plDistribInstTab& reps, plMeshCache& mCache) const
|
|
|
|
{
|
|
|
|
INode* repNode = fRepNodes[iRepNode];
|
|
|
|
plDistribInstance inst;
|
|
|
|
inst.fNode = repNode;
|
|
|
|
inst.fNodeTM = l2w;
|
|
|
|
inst.fObjectTM = repNode->GetObjectTM(TimeValue(0)) * Inverse(repNode->GetNodeTM(TimeValue(0))) * l2w;
|
|
|
|
|
|
|
|
inst.fMesh = mCache.fMesh;
|
|
|
|
|
|
|
|
inst.fFlex = mCache.fFlex;
|
|
|
|
|
|
|
|
inst.fFade = fFade;
|
|
|
|
|
|
|
|
inst.fBone = fBone;
|
|
|
|
|
|
|
|
inst.fRigid = fRigid;
|
|
|
|
|
|
|
|
reps.Append(1, &inst);
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::IDistributeOverFace(int iFace, plDistribInstTab& reps, plMeshCacheTab& cache) const
|
|
|
|
{
|
|
|
|
Point3 p0 = fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(0));
|
|
|
|
Point3 p1 = fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(1));
|
|
|
|
Point3 p2 = fSurfMesh->getVert(fSurfMesh->faces[iFace].getVert(2));
|
|
|
|
|
|
|
|
Box3 grid = ISetupGrid(p0, p1, p2);
|
|
|
|
|
|
|
|
float delta = fSpacing;
|
|
|
|
|
|
|
|
float x, y, z;
|
|
|
|
for( x = grid.Min().x; x < grid.Max().x; x += delta )
|
|
|
|
{
|
|
|
|
for( y = grid.Min().y; y < grid.Max().y; y += delta )
|
|
|
|
{
|
|
|
|
for( z = grid.Min().z; z < grid.Max().z; z += delta )
|
|
|
|
{
|
|
|
|
Point3 pt(x, y, z);
|
|
|
|
|
|
|
|
pt = IPerturbPoint(pt);
|
|
|
|
|
|
|
|
Point3 plnPt = pt;
|
|
|
|
Point3 bary;
|
|
|
|
|
|
|
|
// Get Barycentric coord of projection of grid pt onto face
|
|
|
|
plTriUtils triUtil;
|
|
|
|
plTriUtils::Bary baryVal = triUtil.ComputeBarycentricProjection(hsP3(p0), hsP3(p1), hsP3(p2), hsP3(plnPt), hsP3(bary));
|
|
|
|
if( !(baryVal & (plTriUtils::kOutsideTri | plTriUtils::kDegenerateTri)) )
|
|
|
|
{
|
|
|
|
int iRepNode = ISelectRepNode();
|
|
|
|
|
|
|
|
Matrix3 l2w = IGenerateTransform(iRepNode, iFace, plnPt, bary);
|
|
|
|
|
|
|
|
// l2w as ident means this position turned out to be not so good afterall.
|
|
|
|
if( l2w.IsIdentity() )
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
Box3 clearBox;
|
|
|
|
if( !ISpaceClear(iRepNode, l2w, clearBox, cache) )
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
int iCacheNode = iRepNode;
|
|
|
|
if( !IConform(l2w, iRepNode, cache, iCacheNode) )
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If |projGridPt - GridPt| < gridCubeRadius
|
|
|
|
// and probBitmap->GetPixel(src->UVW(bary)) < RandomZeroToOne()
|
|
|
|
// Also a generic random factor.
|
|
|
|
if( IProbablyDoIt(iFace, pt - plnPt, bary) )
|
|
|
|
{
|
|
|
|
IReplicate(l2w, iRepNode, reps, cache[iCacheNode]);
|
|
|
|
|
|
|
|
IReserveSpace(clearBox);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Matrix3 plDistributor::IInvOTM(int iRepNode) const
|
|
|
|
{
|
|
|
|
// objectTM = otm * nodeTM
|
|
|
|
// invOTM * objectTM = nodeTM
|
|
|
|
// invOTM = nodeTM * invObjectTM
|
|
|
|
INode* repNode = fRepNodes[iRepNode];
|
|
|
|
Matrix3 objectTM = repNode->GetObjectTM(TimeValue(0));
|
|
|
|
Matrix3 nodeTM = repNode->GetNodeTM(TimeValue(0));
|
|
|
|
Matrix3 invOTM = nodeTM * Inverse(objectTM);
|
|
|
|
return invOTM;
|
|
|
|
}
|
|
|
|
|
|
|
|
Matrix3 plDistributor::IOTM(int iRepNode) const
|
|
|
|
{
|
|
|
|
// objectTM = otm * nodeTM
|
|
|
|
// objectTM * Inverse(nodeTM) = otm
|
|
|
|
INode* repNode = fRepNodes[iRepNode];
|
|
|
|
Matrix3 objectTM = repNode->GetObjectTM(TimeValue(0));
|
|
|
|
Matrix3 nodeTM = repNode->GetNodeTM(TimeValue(0));
|
|
|
|
Matrix3 OTM = objectTM * Inverse(nodeTM);
|
|
|
|
return OTM;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL plDistributor::ISpaceClear(int iRepNode, const Matrix3& l2w, Box3& clearBox, plMeshCacheTab& cache) const
|
|
|
|
{
|
|
|
|
if( !fDistTree )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
// If we have high isolation,
|
|
|
|
// clearBox = Box3(Point3(-fSpacing*0.5f, -fSpacing*0.5f, 0), Point3(fSpacing*0.5f, fSpacing*0.5f, fSpacing));
|
|
|
|
// Else if we have medium isolation
|
|
|
|
// clearBox = cache[iRepNode]->getBoundingBox(); // The mesh's bounds
|
|
|
|
// Else if we have low isolation or None isolation
|
|
|
|
// clearBox = Box3(Point3(-kSmallSpace, -kSmallSpace, 0), Point3(kSmallSpace, kSmallSpace, kSmallSpace)); // kSmallSpace ~= 0.5f or one or something
|
|
|
|
|
|
|
|
// We want to set up the box (for high, low and none) in Post OTM space. So instead of multiplying
|
|
|
|
// by l2w, we want to multiply box = box * invOTM * l2w (because l2w already has OTM folded in). When using
|
|
|
|
// the mesh bounds (Medium), l2w is the right transform.
|
|
|
|
// objectTM = otm * nodeTM
|
|
|
|
// invOTM * objectTM = nodeTM
|
|
|
|
// invOTM = nodeTM * invObjectTM
|
|
|
|
const float kSmallSpace = 0.5f;
|
|
|
|
switch( fIsolation )
|
|
|
|
{
|
|
|
|
case kIsoHigh:
|
|
|
|
{
|
|
|
|
INode* repNode = fRepNodes[iRepNode];
|
|
|
|
Matrix3 objectTM = repNode->GetObjectTM(TimeValue(0));
|
|
|
|
Matrix3 nodeTM = repNode->GetNodeTM(TimeValue(0));
|
|
|
|
Matrix3 invOTM = nodeTM * Inverse(objectTM);
|
|
|
|
clearBox = Box3(Point3(-fSpacing*0.5f, -fSpacing*0.5f, 0.f), Point3(fSpacing*0.5f, fSpacing*0.5f, fSpacing));
|
|
|
|
clearBox = clearBox * invOTM;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case kIsoMedium:
|
|
|
|
clearBox = cache[iRepNode].fMesh->getBoundingBox(); // The mesh's bounds
|
|
|
|
break;
|
|
|
|
case kIsoLow:
|
|
|
|
case kIsoNone:
|
|
|
|
default:
|
|
|
|
{
|
|
|
|
INode* repNode = fRepNodes[iRepNode];
|
|
|
|
Matrix3 objectTM = repNode->GetObjectTM(TimeValue(0));
|
|
|
|
Matrix3 nodeTM = repNode->GetNodeTM(TimeValue(0));
|
|
|
|
Matrix3 invOTM = nodeTM * Inverse(objectTM);
|
|
|
|
clearBox = Box3(Point3(-kSmallSpace, -kSmallSpace, 0.f), Point3(kSmallSpace, kSmallSpace, kSmallSpace));
|
|
|
|
clearBox = clearBox * invOTM;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
clearBox = clearBox * l2w;
|
|
|
|
|
|
|
|
return fDistTree->BoxClear(clearBox, fFade);
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::IReserveSpace(const Box3& clearBox) const
|
|
|
|
{
|
|
|
|
// if isolation isn't None, add the box.
|
|
|
|
if( fDistTree && (fIsolation != kIsoNone) )
|
|
|
|
fDistTree->AddBox(clearBox, fFade);
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t plDistributor::GetRandSeed() const
|
|
|
|
{
|
|
|
|
return fRand.GetSeed();
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::SetRandSeed(int seed)
|
|
|
|
{
|
|
|
|
fRand.SetSeed(seed);
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::SetPolarRange(float deg)
|
|
|
|
{
|
|
|
|
fPolarRange = hsDegreesToRadians(deg);
|
|
|
|
fTanPolarRange = tan(fPolarRange);
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::SetProbabilityBitmapTex(BitmapTex* t)
|
|
|
|
{
|
|
|
|
fProbLayerTex = nil;
|
|
|
|
fProbBitmapTex = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
void plDistributor::SetProbabilityLayerTex(plLayerTex* t)
|
|
|
|
{
|
|
|
|
fProbBitmapTex = nil;
|
|
|
|
fProbLayerTex = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
|