// Shave and a Haircut // (c) 2019 Epic Games // US Patent 6720962 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shaveGlobals.h" #include "shaveHairShape.h" #include "shaveRender.h" #include "shaveRenderCallback.h" #include "shaveSDKTYPES.h" #include "shaveTextureStore.h" #include "shaveUtil.h" extern "C" { #include "shaveSDKCALLBACKS.h" VERT zeroVert = { 0.0f, 0.0f, 0.0f }; // Callback functions (shave calls you) #ifndef _DEBUG VERT SHAVEapply_GI(VERT v,CURVEINFO *cc) { return (zeroVert); } #endif void SHAVEapply_inst_color( WFTYPE *instance_geom, int hairID, int slgID, unsigned long shaveINSTID ) {} VERT SHAVEdisplace_root(VERT *root,CURVEINFO *ci,int nodeID) { NODETEXINFO* nodeTexInfo = getTexInfoLookup((unsigned)nodeID); VERT ret = zeroVert; if ((nodeTexInfo != NULL) && (nodeTexInfo->maxPasses > 0) && (ci->hairID < nodeTexInfo->count) && (nodeTexInfo->displacement)) { // // See SHAVEapply_texture() for an explanation of why we're doing // this modulus. // int pass = ci->depthpass % nodeTexInfo->maxPasses; ret = nodeTexInfo->displacement[pass][ci->hairID]; } return ret; } static int shadowTicksLeft = 0; static bool progressBarActive = false; // // This function gets called every 100 hairs when rendering so that you can // make a progress bar. Estimated total is the number of times this // function should get called during a render. If you want to cancel the // render you should return 1 for the rest of the calls to this function // until the render releases control. // int SHAVEprogress(int actual, int estimated_total) { // // If actual == -1, or if the render has been cancelled, then kill off // the progress bar. // if (progressBarActive && ((actual == -1) || shaveRenderCancelled)) { MGlobal::executeCommand("shave_closeProgressBar"); progressBarActive = false; } if (shadowRender) { if ((actual > 0) && (--shadowTicksLeft > 0)) return 0; shadowTicksLeft = 400; } if (shaveRenderCancelled) return 1; // // We don't need a progress bar in batch mode. // if (MGlobal::mayaState() != MGlobal::kInteractive) return 0; int killit = 0; if (shaveEnableProgressBar) { if (actual > estimated_total) actual = estimated_total; if (actual == 0) { if (!progressBarActive) { MString cmd = MString("shave_progressBarInit ") + (double)estimated_total + " \"" + (shadowRender?"Shadow":"Hair") + "\""; MGlobal::executeCommand(cmd); progressBarActive = true; } } else { MGlobal::executeCommand("shave_progressBarQuery()", killit); if (!killit) { MGlobal::executeCommand( MString("shave_progressBarStep ") + (double)actual ); } else { shaveRenderCancelled = true; MGlobal::executeCommand("shave_closeProgressBar"); progressBarActive = false; } } if (shaveRender::getFrameGlobals().verbose) { float pct = ((float)actual/(float)estimated_total)*100; fprintf(stderr, "Done %f percent.\n",pct); } } return killit; } void SHAVEcoord_convertTOSHAVE(VERT *in) {} void SHAVEcoord_convertFROMSHAVE(VERT *in) {} #ifdef _DEBUG VERT SHAVEapply_GI( VERT vv, CURVEINFO * ci ) { VERT t; t.x = 0.0f; t.y = 0.0f; t.z = 0.0f; return ( t ); } extern float SHAVEapply_falloff( int lightNUM, VERT pos, float cone ) { return ( cone ); } #endif float SHAVEapply_texture( CURVEINFO* ci, VERT rest_root_worldpos, unsigned long shaveINSTID, int parm, float inbound_value ) { NODETEXINFO* nodeTexInfo = getTexInfoLookup(shaveINSTID); if ((nodeTexInfo != NULL) && (nodeTexInfo->maxPasses > 0) && (ci->hairID < nodeTexInfo->count)) { // // If there are multiple shaveHairShapes in the scene, then Shave // will use the same number of passes for all of them. For // example, if one shaveHairShape specifies 2 passes and another // specifies 5, then both shaveHairShapes will be called for 5 // passes. // // So it is possible that we are being called with a pass number // which is larger than the number of passes specified for this // particular shaveHairShape. In that case, we use a modulus to // wrap around to the values used in earlier passes. // // No, this isn't a horrible kludge, this is really what Shave is // expecting. :-) // int passToUse = ci->depthpass % nodeTexInfo->maxPasses; passToUse=0; // // Copy the UV values for this hair into the CURVEINFO so that // later callbacks have access to them. // ci->u = nodeTexInfo->u[passToUse][ci->hairID]; ci->v = nodeTexInfo->v[passToUse][ci->hairID]; if (((int)ci->hairID < nodeTexInfo->maxIndex[parm]) && nodeTexInfo->textured[parm]) { float texVal = (float)(nodeTexInfo->textureLookup[passToUse][parm][ci->hairID]); //printf("tex %i hair %i value %f\n",parm,ci->hairID,texVal );fflush(stdout); return (texVal); // * inbound_value); } } return inbound_value; } void MAYAexternal_forces(VERT *lastpos, VERT *velocity, int y) { const MDagPathArray& fieldList = shaveUtil::getFields(); unsigned int fieldCount = fieldList.length(); if (fieldCount) { MStatus stat; MDagPath dagPath; MFnField field; MPoint p(lastpos->x, lastpos->y, lastpos->z); MVector v(velocity->x, velocity->y, velocity->z); MPointArray pointArray; pointArray.append(p); MVectorArray forces; MVectorArray velocityArray; velocityArray.append(v); MDoubleArray massArray; unsigned int i; for(i = 0; i < fieldCount; i++) { // shaveUtil::getFields() only updates the field list when a // new one is added, not when one is deleted, so we have to // check the field to make sure that it still exists. if (fieldList[i].isValid()) { field.setObject(fieldList[i]); stat = field.getForceAtPoint( pointArray, velocityArray, massArray, forces ); } } velocity->x+=((float)forces[0].x)*((float)0.01); velocity->y+=((float)forces[0].y)*((float)0.01); velocity->z+=((float)forces[0].z)*((float)0.01); } } // These methods must be used when running multiple threads. // // Call MAYAcache_forces(0) from Maya's main thread (or its proxy) to create // the cache for *all* of the current node's guides. // // From within individual threads you can then call MAYAapply_cached_forces(..) // for each guide vertex you want forces applied to. // // When done, call MAYAcache_forces(1) from Maya's main thread (or its proxy) // to free up the cache. // MVectorArray forcesCache; void MAYAcache_forces(int clearCache) { forcesCache.clear(); if (clearCache == 0) { const MDagPathArray& fieldList = shaveUtil::getFields(); unsigned int fieldCount = fieldList.length(); if (fieldCount > 0) { shaveHairShape* hairShape = shaveUtil::getLoadedHairShape(); // TODO: // // We want the new guide positions here, but they're not yet // available. The call to this function is part of the process // of calculating those new positions. // // For now we'll work around the problem by calculating the // forces using the old positions. If we call getGuides() at // this point it will still return the old positions because // they haven't been updated yet, but it will also mark the // guide cache as clean, which messes things up later down the // line. So instead we call getDirtyGuides() which will return // the current guide cache without attempting to update it. // const shaveHairShape::GuidesSnapshot& curGuides = hairShape->getDirtyGuides(); const shaveHairShape::GuidesSnapshot& prevGuides = hairShape->getPrevGuides(); MVectorArray points; MVectorArray velocities; MDoubleArray masses; points.setLength( static_cast(curGuides.guides.size() * SHAVE_VERTS_PER_GUIDE) ); velocities.setLength( static_cast(curGuides.guides.size() * SHAVE_VERTS_PER_GUIDE) ); // If the previous guide snapshot has the same time as the // current one, or a different number of guides, then we won't // be able to use it to calculate velocities and will have to // assume zero velocity. // float deltaT = curGuides.frame - prevGuides.frame; bool haveVelocity = ( (fabs(deltaT) > 0.000001f) && (prevGuides.guides.size() == curGuides.guides.size()) ); unsigned int vertIdx = 0; for (size_t g = 0; g < curGuides.guides.size(); ++g) { const shaveHairShape::Guide& guide = curGuides.guides[g]; for (unsigned int v = 0; v < SHAVE_VERTS_PER_GUIDE; ++v) { points[vertIdx] = static_cast(guide.verts[v]); if (haveVelocity) { velocities[vertIdx] = static_cast( (guide.verts[v] - prevGuides.guides[g].verts[v]) / deltaT ); } else { velocities[vertIdx] = MVector::zero; } ++vertIdx; } } for (unsigned int f = 0; f < fieldCount; ++f) { // shaveUtil::getFields() only updates the field list when a // new one is added, not when one is deleted, so we have to // check the field to make sure that it still exists. // if (fieldList[f].isValid()) { MFnField fieldFn(fieldList[f]); fieldFn.getForceAtPoint( points, velocities, masses, forcesCache ); } } } } } // Forces are added to whatever value is already in velocity. // void MAYAapply_cached_forces(int guideNum, int vertNum, VERT* velocity) { if ((guideNum >= 0) && (guideNum < static_cast(forcesCache.length())) && (vertNum >= 0) && (vertNum < SHAVE_VERTS_PER_GUIDE)) { MVector& delta = forcesCache[guideNum * SHAVE_VERTS_PER_GUIDE + vertNum]; velocity->x += static_cast(delta.x * 0.01); velocity->y += static_cast(delta.y * 0.01); velocity->z += static_cast(delta.z * 0.01); } } VERT SHAVEapply_illumination(int LIGHTID, VERT wpos,VERT vector, VERT color) { return color; } #ifndef _DEBUG float SHAVEapply_falloff(int lightID, VERT p, float intensity) { MStatus st; // // When doing native illumination, the light samples taken in // SHAVEapply_illuminationWF() will already have taken decay into // account, so we don't want to further apply it here. // if (!shaveRender::getFrameGlobals().useNativeIllumination && (intensity > 0.0f)) { int lightIndex = shaveUtil::getLightIndexFromID(lightID); if (lightIndex >= 0) { MDagPath lightDag = shaveUtil::globalLightList[lightIndex].path; // // Only non-ambient lights have decay. // MFnNonAmbientLight lightFn(lightDag, &st); if (st) { short decay = lightFn.decayRate(); if (decay != 0) { // // How far is the sample point from the light? // MTransformationMatrix lightWorldMatrix; lightWorldMatrix = lightDag.inclusiveMatrix(); MVector lightPos = lightWorldMatrix.translation(MSpace::kWorld); MVector samplePos((double)p.x, (double)p.y, (double)p.z); float dist = (float)(samplePos - lightPos).length(); // // Maya's lights don't decay within the first unit of // distance. // if (dist > 1.0) { switch (decay) { case 1: // Linear intensity = intensity / dist; break; case 2: // Quadratic intensity = intensity / (dist * dist); break; case 3: // Cubic intensity = intensity / (dist * dist * dist); break; default: break; } } } } } } return intensity; } #endif void SHAVEapply_illuminationWF(int LIGHTID, WFTYPE* samples) { // ranges are 0.0 - 1.0 (return isn't clipped until after shading) // modify or replace 'samples->color' - it contains light info for // current test before shave shadows are applied // samples->v is the position // samples->totalverts is the total number of points if (shaveRender::getFrameGlobals().useNativeIllumination && (samples != NULL) && (samples->totalverts > 0)) { int lightIndex = shaveUtil::getLightIndexFromID(LIGHTID); if (lightIndex >= 0) { MStatus status; MTransformationMatrix lightWorldMatrix; MDagPath lightDag = shaveUtil::globalLightList[lightIndex].path; lightWorldMatrix = lightDag.inclusiveMatrix(); MVector lightTranslation = lightWorldMatrix.translation(MSpace::kWorld ); MFloatVectorArray normals; MFloatPointArray pointArray; unsigned int i; for (i = 0; i < (unsigned int)samples->totalverts; i++) { pointArray.append( samples->v[i].x, samples->v[i].y, samples->v[i].z ); MFloatVector sampleAsVec( samples->v[i].x, samples->v[i].y, samples->v[i].z ); MFloatVector vec(lightTranslation - sampleAsVec); vec.normalize(); normals.append(vec); } MFloatVectorArray vertColorArray; MFloatVectorArray vertTranspArray; MFloatMatrix camMatrix; MString attrName = lightDag.fullPathName() + ".lightIntensity"; status = MRenderUtil::sampleShadingNetwork( attrName, //attribute to sample (int)pointArray.length(), //samples false, //shadows false, //reuse shad maps camMatrix, //camMatrix &pointArray, //points NULL, //u coords NULL, //v coords &normals, //normals NULL, //ref points NULL, //u tan NULL, //v tan NULL, //filter size vertColorArray, //out color vertTranspArray //out transp ); if (status) { for (i = 0; i < vertColorArray.length(); i++) { samples->color[i].x = vertColorArray[i].x; samples->color[i].y = vertColorArray[i].y; samples->color[i].z = vertColorArray[i].z; } } } } } // this is a callback for applying atmospherics/depth cueing VERT SHAVEapply_atmosphere(VERT wpos,VERT inbound_color ) { // range = 0.0 - 1.0 (shave will clip out of bound returns) // do your tint here based on wpos // here's how: // fogval=your_compute_func_for_fog(wpos); // if (fog_val>1.0) fog_val=1.0; // if (fog_val<0.0) fog_val=0.0; // inbound_color.x=inbound_color.x*(1.0-fog_val)+fog_color.x*fog_val; // inbound_color.y=inbound_color.y*(1.0-fog_val)+fog_color.y*fog_val; // inbound_color.z=inbound_color.z*(1.0-fog_val)+fog_color.z*fog_val; return inbound_color; } // // This function callback gives you the opportunity to apply external // vertpaint to any/every channel in shave. Normally the inbound_value // will be 1.0 (unless you've painted a map inside shave) the return, // should contain a value who's range is (float) 0-1. // float SHAVEapply_VMAP(long SHAVEINSTID,int VERTID,int chan, float inbound_value) { return applyVertTexValue(SHAVEINSTID, VERTID, chan, inbound_value); } // This is called whenever Shave finishes rendering a tile into the pixel // buffer. void SHAVEdraw_tile_callback(VERT* min, VERT* max) { shaveRenderCallback::tileRendered( (unsigned int)min->x, (unsigned int)max->x, (unsigned int)min->y, (unsigned int)max->y ); } }