保姆级毛发算法调研分析,十万长文带你深入TressFX(三)

  1. 毛发发丝的碰撞矫正
    1. 碰撞矫正
      1. 碰撞检测使用的有向距离场
      2. 基于有向距离场的发丝碰撞矫正
    2. 后记

毛发发丝的碰撞矫正

本文仅在个人博客及个人知乎上采用”CC BY-NC-ND 4.0”(署名-不可商用-禁止演绎)协议发布,转载请注明个人博客的原文链接及作者信息,侵权必究。

博客链接:https://tis.ac.cn/blog/kongdeyou/hair_simulation_and_rendering_3/

知乎链接:https://zhuanlan.zhihu.com/p/534732852/

本文将主要分析TressFX毛发系统中物理模拟的碰撞矫正,目录结构如下:

碰撞矫正

碰撞矫正由两个部分组成:用与头发待碰撞的物体建立有向距离场,然后再用前面的发丝物理计算结果在这个有向距离场中做碰撞检测和矫正。

碰撞检测使用的有向距离场

针对场景中每个需要与头发进行碰撞检测的物体(比如玩家角色的头颅模型),均需要2个passes,第一个pass用来处理该物体的骨骼动画,第二个pass用来建场,建场又由3个subpasses组成。

BoneSkin与SDF的Pass图

处理骨骼动画的pass和一般骨骼动画的处理逻辑的思路是一样的,输入T-Pose下的vertex attributes和vertex bone indices and bone weights,通过constant buffer送下去所有bones的inverse bind matrices,然后算出受骨骼动画驱动后的vertex attributes。只不过,这里的骨骼计算和普通的骨骼计算流程不太相同,TressFX的maya导出工具会将用于碰撞检测的物体的数据存储到tfxmesh文件中,tfxmesh存储了vertex position/normal和bone indices/weights数据,此处会使用tfxmesh文件中记录的这些数据作为碰撞检测的mesh数据,因此我们的数据来源来自于tfxmesh文件(多了这个步骤后,我们就能够建一个很复杂的有很多面片数的人头模型,再做一个这个人头模型的低模,这样在渲染的时候使用高模,导给头发系统处理时使用低模,这样可以大大减少在做碰撞检测时的计算量,还能基本保证不穿模)。

骨骼计算,略,详见:https://github.com/GPUOpen-Effects/TressFX/blob/v4.1.0/src/Shaders/TressFXBoneSkinning.hlsl#L24-L94

建场过程由InitializeSignedDistanceField、ConstructSignedDistanceField、FinalizeSignedDistanceField三个subpass组成。

SDF使用物体的扩展包围盒来建立一个正方体的晶胞盒。

正方体晶胞盒

然后将物体模型放在这个晶胞盒中,逐三角形遍历临近的晶胞,计算晶胞的中心点到三角形的最小距离,作为该晶胞的值。如果晶胞中心在物体模型内,即晶胞中心指向三角形的内侧,则距离符号为负;如果在物体模型外,则距离符号为正;如果超过一定距离(即非三角形临近的晶胞),将该晶胞的值记为同一个最大值(INITIAL_DISTANCE,值为1e10f)。

InitializeSignedDistanceField做初始化这个晶胞盒的操作,会将所有的晶胞置为一个最大值,如下代码所示。

// One thread for each cell.
[numthreads(THREAD_GROUP_SIZE, 1, 1)]
void InitializeSignedDistanceField(uint GIndex : SV_GroupIndex,
    uint3 GId : SV_GroupID, uint3 DTid : SV_DispatchThreadID)
{
    int numSdfCells = g_NumCellsX * g_NumCellsY * g_NumCellsZ; // 总晶胞数

    int sdfCellIndex = GId.x * THREAD_GROUP_SIZE + GIndex; // 当前的线程所负责的晶胞
    if(sdfCellIndex >= numSdfCells) return;

    g_SignedDistanceField[sdfCellIndex] = FloatFlip3(INITIAL_DISTANCE); // 初始化当前线程所负责的晶胞记录的值为最大值
}

此处注意到,g_SignedDistanceField这个变量是个uint数组。

[[vk::binding(1, 0)]] RWStructuredBuffer<uint> g_SignedDistanceField : register(u1, space0);

这里TressFX没有直接使用float类型,因为为了节省计算量,TressFX采用的是逐三角形遍历临近的晶胞的方法,而不是逐晶胞遍历物体模型的逐三角形,这个过程在GPU中执行,那么就有可能发生两个三角形共同影响同一个晶胞,而两个三角形分别在两个线程中执行,于是在这个晶胞上就会出现读写冲突,为了使用GPU中的原子化比较写入函数InterlockedMin(该函数只能用在uint和int类型上),TressFX得把浮点转成整型,这个过程可以通过HLSL的asuint或asint函数实现。TressFX此处用了asuint,由于asuint之后,得到的是一个无符号的整数值,因此TressFX写了两个函数FloatFlip3和IFloatFlip3,计算得到SDF的晶胞值后用FloatFlip3存下来,此时会把符号位从最高位移动到最低位,避免符号位对值的大小产生影响,这样其他三角形计算出来的结果和该晶胞上的值比较时,不会因为uint导致负数变成大正数,无论正负都是同一种比较方法,在计算完所有三角形得出所有的晶胞值后,再逐晶胞调用IFloatFlip3把符号恢复回来。

uint FloatFlip3(float fl)
{
    uint f = asuint(fl);
    return (f << 1) | (f >> 31); // Rotate sign bit to least significant
}

uint IFloatFlip3(uint f2)
{
    return (f2 >> 1) | (f2 << 31); // Restore the sign bit
}

ConstructSignedDistanceField会逐三角形遍历临近的晶胞,构造出当前物体模型的有向距离场。

collMeshVertexPositions由前面的处理骨骼的pass输出,作为当前pass的输入。g_TrimeshVertexIndices存储了当前物体模型的三角形索引数据,primitive topology是triangle list。这些在pass图中均有展示。

//Triangle input to SDF builder
[[vk::binding(0, 0)]] StructuredBuffer<uint> g_TrimeshVertexIndices : register(t0, space0);
[[vk::binding(2, 0)]] RWStructuredBuffer<StandardVertex> collMeshVertexPositions : register(u2, space0);

在shader中,每一个三角形一个线程,先计算出三角形的AABB包围盒,然后根据AABB包围盒对应算出所占据的晶胞盒子集,计算出三角形到这个子集内的晶胞们的中心的距离,作为这些晶胞们的值。

// One thread per each triangle
[numthreads(THREAD_GROUP_SIZE, 1, 1)]
void ConstructSignedDistanceField(uint GIndex : SV_GroupIndex,
    uint3 GId : SV_GroupID, uint3 DTid : SV_DispatchThreadID)
{
    int triangleIndex = GId.x * THREAD_GROUP_SIZE + GIndex;

    uint numTriangleIndices, stride;
    g_TrimeshVertexIndices.GetDimensions(numTriangleIndices, stride);
    uint numTriangles = numTriangleIndices / 3;

    if (triangleIndex >= (int)numTriangles)
        return;

    // 取得当前待处理的三角形的索引
    uint index0 = g_TrimeshVertexIndices[triangleIndex * 3 + 0];
    uint index1 = g_TrimeshVertexIndices[triangleIndex * 3 + 1];
    uint index2 = g_TrimeshVertexIndices[triangleIndex * 3 + 2];

    // 取得当前待处理的三角形的顶点位置
    float3 tri0 = collMeshVertexPositions[index0].position;
    float3 tri1 = collMeshVertexPositions[index1].position;
    float3 tri2 = collMeshVertexPositions[index2].position;

    // 算出三角形的AABB包围盒
    float3 aabbMin = min(tri0, min(tri1, tri2)) - float3(MARGIN, MARGIN, MARGIN); // #define MARGIN g_CellSize
    float3 aabbMax = max(tri0, max(tri1, tri2)) + float3(MARGIN, MARGIN, MARGIN); // 包围盒扩展一个晶胞的长度大小

    // 根据AABB包围盒得到晶胞盒子集,AABB包围盒是float位置数据,此处算出对应的对角轴晶胞的ID
    int3 gridMin = GetSdfCoordinates(aabbMin) - GRID_MARGIN; // #define GRID_MARGIN int3(1, 1, 1)
    int3 gridMax = GetSdfCoordinates(aabbMax) + GRID_MARGIN; // 晶胞盒子集向外扩展一个晶胞

    // 经过上面将三角形包围盒所占据的晶胞向外延展之后,相当于我们找出了三角形临近的晶胞
    // 然后我们将这些临近的晶胞限制在有效的范围内
    gridMin.x = max(0, min(gridMin.x, g_NumCellsX - 1));
    gridMin.y = max(0, min(gridMin.y, g_NumCellsY - 1));
    gridMin.z = max(0, min(gridMin.z, g_NumCellsZ - 1));
    gridMax.x = max(0, min(gridMax.x, g_NumCellsX - 1));
    gridMax.y = max(0, min(gridMax.y, g_NumCellsY - 1));
    gridMax.z = max(0, min(gridMax.z, g_NumCellsZ - 1));

    // 针对每个临近的晶胞,计算其中心到三角形的距离,取绝对值的最小值存为该晶胞的值,即SDF中该小立方格子的值
    for (int z = gridMin.z; z <= gridMax.z; ++z)
        for (int y = gridMin.y; y <= gridMax.y; ++y)
            for (int x = gridMin.x; x <= gridMax.x; ++x)
            {
                int3 gridCellCoordinate = int3(x, y, z);
                int gridCellIndex = GetSdfCellIndex(gridCellCoordinate);
                float3 cellPosition = GetSdfCellPosition(gridCellCoordinate);

                float distance = SignedDistancePointToTriangle(cellPosition, tri0, tri1, tri2);
                uint distanceAsUint = FloatFlip3(distance);
                InterlockedMin(g_SignedDistanceField[gridCellIndex], distanceAsUint);
            }
}

在上面的代码中:GetSdfCoordinates根据场景中的点找到该位置所在的晶胞,晶胞相对于从g_Origin位置所在的坐标开始,有g_NumCellsX/Y/Z个,单个晶胞大小为g_CellSize,覆盖了场景中的g_Origin至(g_Origin+g_CellSize*vec3(g_NumCellsX,g_NumCellsY,g_NumCellsZ))的空间范围;GetSdfCellIndex和GetSdfCellPosition分别获取当前晶胞在g_SignedDistanceField数组中的索引和获取晶胞在场景中的中心位置,然后SignedDistancePointToTriangle计算出到三角形的距离,最后用InterlockedMin原子写入晶胞数组的指定位置中。

int3 GetSdfCoordinates(float3 positionInWorld)
{
    // g_Origin是整个晶胞盒在场景的世界坐标系下的偏移,减去后是相对于以g_Origin为原点的坐标系的相对坐标
    float3 sdfPosition = (positionInWorld - g_Origin.xyz) / g_CellSize;

    int3 result;
    result.x = (int)sdfPosition.x;
    result.y = (int)sdfPosition.y;
    result.z = (int)sdfPosition.z;

    return result; // Get SDF cell index coordinates (x, y and z) from a point position in world space
}

int GetSdfCellIndex(int3 gridPosition)
{
    int cellsPerLine = g_NumCellsX;
    int cellsPerPlane = g_NumCellsX * g_NumCellsY;

    return cellsPerPlane*gridPosition.z + cellsPerLine*gridPosition.y + gridPosition.x;
}

float3 GetSdfCellPosition(int3 gridPosition)
{
    float3 cellCenter = float3(gridPosition.x, gridPosition.y, gridPosition.z) * g_CellSize; // 相对位置,晶胞盒内的局部坐标
    cellCenter += g_Origin.xyz; // g_Origin是整个晶胞盒在场景的世界坐标系下的偏移,加上后取得世界坐标,三角形的三个顶点的坐标是在世界坐标系下的

    return cellCenter;
}

float SignedDistancePointToTriangle(float3 p, float3 x0, float3 x1, float3 x2) // 晶胞中心点到三角形的距离
{
    //...
}   // 几何数学计算,略,详见:https://github.com/GPUOpen-Effects/TressFX/blob/v4.1.0/src/Shaders/TressFXSDFCollision.hlsl#L147-L215

最后FinalizeSignedDistanceField将晶胞盒的所有晶胞的正负符号恢复回来。

// One thread per each cell.
[numthreads(THREAD_GROUP_SIZE, 1, 1)]
void FinalizeSignedDistanceField(uint GIndex : SV_GroupIndex,
    uint3 GId : SV_GroupID, uint3 DTid : SV_DispatchThreadID)
{
    int numSdfCells = g_NumCellsX * g_NumCellsY * g_NumCellsZ;

    int sdfCellIndex = GId.x * THREAD_GROUP_SIZE + GIndex;
    if (sdfCellIndex >= numSdfCells) return;

    uint distance = g_SignedDistanceField[sdfCellIndex];
    g_SignedDistanceField[sdfCellIndex] = IFloatFlip3(distance); // IFloatFlip3将符号位恢复回来
}

在以上的计算中,我们使用了g_Origin、g_CellSize、g_NumCellsX/Y/Z这些值,它们来自于Constant Buffer。

[[vk::binding(3, 0)]] cbuffer ConstBuffer_SDF : register(b3, space0)
{
    float4 g_Origin;  // 晶胞盒局部坐标系的坐标轴在世界坐标系下的位置
    float g_CellSize; // 一个晶胞的单位长度
    int g_NumCellsX;  // X轴晶胞个数
    int g_NumCellsY;  // Y轴晶胞个数
    int g_NumCellsZ;  // Z轴晶胞个数
    // 以下的参数目前均没有使用
    int g_MaxMarchingCubesVertices; // 这个变量全局都没有使用的地方,是废弃的
    float g_MarchingCubesIsolevel;  // 这个变量全局都没有使用的地方,也是废弃的
    float g_CollisionMargin;
    int g_NumHairVerticesPerStrand;
    int g_NumTotalHairVertices;
    float pad1;
    float pad2;
    float pad3;
}

在C++代码中该结构体定义在TressFXConstantBuffers.h头文件中。

struct TressFXSDFCollisionParams
{
    AMD::float4  m_Origin;
    float        m_CellSize;
    int          m_NumCellsX;
    int          m_NumCellsY;
    int          m_NumCellsZ;
    int          m_MaxMarchingCubesVertices;
    float        m_MarchingCubesIsolevel;
    float        m_CollisionMargin;
    int          m_NumHairVerticesPerStrand;
    int          m_NumTotalHairVertices;
    float        pad1;
    float        pad2;
    float        pad3;
};

拿着TressFXSDFCollisionParams逐步反向查找,TressFXSDFCollision类中存在该结构体的实例变量m_ConstBuffer,该变量值在TressFXSDFCollision类函数Update或CollideWithHair中更改。

TressFX此处的实现有缺陷,已向开源仓反馈,缺陷详见:https://github.com/GPUOpen-Effects/TressFX/issues/46
在创建SDF的pass中,由于g_MaxMarchingCubesVertices、g_MarchingCubesIsolevel、g_CollisionMargin、g_NumHairVerticesPerStrand、g_NumTotalHairVertices变量没有使用,因此不会出现问题。但是在后面的发丝碰撞矫正的pass中,会使用到g_CollisionMargin、g_NumHairVerticesPerStrand、g_NumTotalHairVertices,而这些数据是头发相关的数据(非有向距离场信息数据),它们是同一块ConstBuffer,TressFXSDFCollision::CollideWithHair在下发这个ConstBuffer时一把下发下去了,导致最后一个头发模型的数据覆盖掉了前面其他头发模型的数据。

此处应该分成两个结构体来实现:TressFXSDFCollisionCollideeTressFXSDFCollisionCollider,即

struct TressFXSDFCollisionCollidee
{
    AMD::float4 m_Origin;
    float       m_CellSize;
    int         m_NumCellsX;
    int         m_NumCellsY;
    int         m_NumCellsZ;
}

struct TressFXSDFCollisionCollider
{
    float m_CollisionMargin;
    int   m_NumHairVerticesPerStrand;
    int   m_NumTotalHairVertices;
    // padding...
}

针对每一个SDF(头发模型要与之碰撞的物体模型)都下发各自的TressFXSDFCollisionCollidee,而针对每一个头发模型都下发各自的TressFXSDFCollisionCollider。这样就能解掉上面的bug,而且逻辑更清晰。

在TressFXSDFCollision构造函数中初始化影响m_ConstBuffer值的相关量:

{
    ...

    // initialize SDF grid using the associated model's bounding box
    Vector3 bmin, bmax;
    m_pInputCollisionMesh->GetInitialBoundingBox(bmin, bmax);
    m_CellSize = (bmax.x - bmin.x) / m_NumCellsInXAxis;

    int numExtraPaddingCells = (int)(0.8f * (float)m_NumCellsInXAxis);
    m_PaddingBoundary = numExtraPaddingCells * m_CellSize;

    UpdateSDFGrid(bmin, bmax); // 该函数内会设置m_Origin为扩展后的包围盒:bmin-m_PaddingBoundary

    // 扩展包围盒,(int)(float/int)的操作会去尾,扩展后变成进一
    bmin -= m_PaddingBoundary;
    bmax += m_PaddingBoundary;
    m_NumCellsX = (int)((bmax.x - bmin.x) / m_CellSize);
    m_NumCellsY = (int)((bmax.y - bmin.y) / m_CellSize);
    m_NumCellsZ = (int)((bmax.z - bmin.z) / m_CellSize);

    ...
}

其中m_NumCellsInXAxis从构造函数的入参中来,而构造TressFXSDFCollision在CollisionMesh的构造函数中,依次向上找,该值存在于TressFXCollisionMeshDescription结构体的numCellsInXAxis中。而该结构体数组又存在于TressFXSceneDescription的collisionMeshes中。

// From TressFXSample.h

struct TressFXCollisionMeshDescription
{
    std::string name;
    std::string tfxMeshFilePath;
    int         numCellsInXAxis; // 每个轴方向上的晶胞个数,该值会影响每个晶胞的大小,该值越大晶胞单位长度越小,计算量越多,碰撞矫正越精细
    float       collisionMargin; // 碰撞余量,在创建SDF时没有使用,会在发丝碰撞矫正时使用
    int         mesh;
    std::string followBone;
};

struct TressFXSceneDescription
{ // TressFX中的Demo定义的该结构体存储了整个Sample场景中的所有素材数据,集成进引擎中时
  // 需要调整成我们自己的数据结构,用来存储头发和会和头发产生碰撞的物体模型的数据!!
    std::vector<TressFXObjectDescription>        objects;
    std::vector<TressFXCollisionMeshDescription> collisionMeshes;

    std::string gltfFilePath;
    std::string gltfFileName;
    std::string gltfBonePrefix;

    float startOffset;
};

再往上追溯,就到了TressFXSample.cpp中的TressFXSample::LoadScene函数实现中了。该参数的设置来源于sample中。即,该参数需要来源于外部配置。因此,我们集成进自己的引擎中时,需要在Editor的Inspector面板上暴露UI给技美,让技美选择头发需要做碰撞检测的物体模型(.tfxmesh)和生成SDF时所使用的相关的参数。

基于有向距离场的发丝碰撞矫正

针对一个头发所关联的每个SDF,都需要一个Pass来做一次碰撞矫正,我们给这个Pass输入SDF和经过物理动力学和风场计算后的发丝位置,经过矫正后输出在碰撞体外的发丝顶点新位置,这个新位置将作为整个发丝物理计算后的最终位置,用于后续的渲染中,如下图所示。

碰撞矫正Pass图

矫正的算法并不复杂:在前一小节中,我们已经生成了待碰撞体的有向距离场,而发丝顶点会在这个距离场中,我们根据发丝顶点所处的晶胞位置,能够拿出当前发丝顶点是否在待碰撞体内还是体外的信息(由上节我们知道,内部为负,外部为正,绝对值越大,距离碰撞体边界越远),如果当前的发丝顶点在待碰撞体内,那么这个顶点就是需要纠正的顶点,然后我们根据这个顶点附近周围的晶胞值,计算出一个梯度方向,这个梯度方向就是将这个顶点移出待碰撞体内所需要移动的位移最小的方向(由于此时是反算,梯度计算出来的结果,得出的是一个局部最优解,会受“附近”这一范围选取的影响),而顶点所在位置的晶胞值的绝对值,即是移动到待碰撞体边界的位移量。

我们看个TressFX提供的简单的例子:

有向距离场

上图中的有向距离场是我们在前一个Pass中建好的,图中的2D网格是在这个有向距离场中选取的其中一面的晶胞,假设网格中的三角形就是待碰撞的物体的triangle,那么我们能看到:当一个晶胞的中心在三角形内部时,晶胞值是负数;当一个晶胞的中心在三角形外时,晶胞值是正数(empty时是默认的INITIAL_DISTANCE,是最大值1e10f)。由于当晶胞是一个较小的正数时,仅表征了晶胞的中心在三角形外,但是待碰撞体的三角形有可能仍然占据了晶胞的部分(最多可能占据了二分之一),因此TressFX提供了一个裕量(即代码中的g_CollisionMargin)给技美设置,在矫正时发丝顶点的位移量会多加上这一个裕量,使得发丝尽可能保证矫正到碰撞体之外。

碰撞矫正算法

矫正过程如上图,这是当前Pass要做的事情。逐发丝顶点比较顶点所在位置的晶胞值,如果是一个小于裕量的值(晶胞值为负数、或为比裕量小的小值正数),就会计算出当前的顶点位置的晶胞值梯度方向作为矫正方向,矫正的位移量为裕量减去晶胞值(若晶胞值为负数,得到结果即晶胞值绝对值加上裕量,即SDF中记录当前晶胞中心到待碰撞体边界的最近的距离加上裕量;若晶胞值为正数,说明当前晶胞位置处在边界附近,计算出的结果只为裕量内的调整量)。

通过研究它的矫正算法,结合我在实际落地时的场景,我们能发现这个矫正算法是有一定的固有缺陷的。

首先是,我们的碰撞矫正是基于发丝的顶点做的,那么就有一种特殊的情况,待碰撞体有一个尖锐的突角,使得待碰撞体的Mesh网格中有三角形的一个点距离另外的两个点比较远,如下图所示,这种情况下发丝上的顶点所在位置的晶胞值都为正数,不会做矫正,但是发丝线却穿过了待碰撞体,产生了穿模。我们可以通过增多一根发丝上的顶点个数来缓解这一问题,但顶点数增多会导致计算量变大。

corner case 1

其次是,类似耳朵部位处的地方,当出现凹包时,即使将顶点矫正到了待碰撞体的外部(但在凹包范围内),仍有可能由于前面的缺陷,造成穿模的现象,如下图所示。我们可以通过生成tfxmesh时使用一个凸包代理模型来规避这个问题。

corner case 2

最后是,由于在生成SDF时我们采用的是遍历逐三角形去写三角形临近晶胞的方法,这样会导致待碰撞体网格内部离边界较远一点的地方仍是初始值。如下图所示,当碰撞体为内部黑色的球,建的SDF晶胞盒子是大的立方体,那图中红色虚拟球内的区域在碰撞球内远离球的边界,这部分范围内的晶胞值仍然会是初始的1e10f值。这就会导致当有发丝顶点进入了待碰撞体的内部时,矫正算法就失效了,不会移动这些发丝顶点到体外。我们可以通过确保生成tfxmesh的模型是一个完整封闭的模型且发丝顶点的初始位置一定不在碰撞体模型内来规避这个问题。

corner case 3

碰撞矫正会对所有的发丝顶点执行计算,包括引导发丝和从属发丝,最后来看一下代码,实现其实不难理解。

//SDF-Hair collision using forward differences only
// One thread per one hair vertex
[numthreads(THREAD_GROUP_SIZE, 1, 1)]
void CollideHairVerticesWithSdf_forward(uint GIndex : SV_GroupIndex,
    uint3 GId : SV_GroupID, uint3 DTid : SV_DispatchThreadID)
{
    int hairVertexGlobalIndex = GId.x * THREAD_GROUP_SIZE + GIndex;

    if(hairVertexGlobalIndex >= g_NumTotalHairVertices)
        return;

    int hairVertexLocalIndex = hairVertexGlobalIndex % g_NumHairVerticesPerStrand;

    // We don't run collision check on the first two vertices in the strand. They are fixed on the skin mesh.
    if (hairVertexLocalIndex == 0 || hairVertexLocalIndex == 1)
        return;

    float4 hairVertex = g_HairVertices[hairVertexGlobalIndex];
    float4 vertexInSdfLocalSpace = hairVertex;

    // GetSignedDistance获取晶胞值的插值结果,数学计算过程,略,详见:
    // https://github.com/GPUOpen-Effects/TressFX/blob/v4.1.0/src/Shaders/TressFXSDFCollision.hlsl#L414-L527
    float distance = GetSignedDistance(vertexInSdfLocalSpace.xyz);

    // early exit if the distance is larger than collision margin
    if(distance > g_CollisionMargin) // 前面算法描述中提及,当distance<g_CollisionMargin时,发丝在待碰撞体边界加上一个裕量的范围之外
        return;

    // small displacement. 
    float h = 0.1f * g_CellSize; // 用来计算梯度方向时的“附近”的量化范围

    float3 sdfGradient;
    {
        //Compute gradient using forward difference
        float3 offset[3];
        offset[0] = float3(1, 0, 0);
        offset[1] = float3(0, 1, 0);
        offset[2] = float3(0, 0, 1);

        float3 neighborCellPositions[3];

        for(int i = 0; i < 3; ++i) 
            neighborCellPositions[i] = vertexInSdfLocalSpace.xyz + offset[i] * h;

        //Use trilinear interpolation to get distances
        float neighborCellDistances[3];

        for(int j = 0; j < 3; ++j) 
            neighborCellDistances[j] = GetSignedDistance(neighborCellPositions[j]);

        float3 forwardDistances;
        forwardDistances.x = neighborCellDistances[0];
        forwardDistances.y = neighborCellDistances[1];
        forwardDistances.z = neighborCellDistances[2];

        sdfGradient = ( forwardDistances - float3(distance, distance, distance) ) / h; // 梯度方向
    }

    //Project hair vertex out of SDF
    float3 normal = normalize(sdfGradient);
    
    if(distance < g_CollisionMargin)
    {
        // normal * (g_CollisionMargin - distance) 算出在梯度方向上的矫正量,
        // 当发丝顶点完全在碰撞体内时,distance为负数;当发丝顶点在碰撞体边界附近的裕量范围内时,distance为正数。
        // (g_CollisionMargin-distance)得到的是矫正到碰撞体外再加上一个裕量的位置的位移量。
        float3 projectedVertex = hairVertex.xyz + normal * (g_CollisionMargin - distance);
        g_HairVertices[hairVertexGlobalIndex].xyz = projectedVertex;
        g_PrevHairVertices[hairVertexGlobalIndex].xyz = projectedVertex;
    }
}

//Faster SDF-Hair collision mixing forward and backward differences
// One thread per one hair vertex
[numthreads(THREAD_GROUP_SIZE, 1, 1)]
void CollideHairVerticesWithSdf(uint GIndex : SV_GroupIndex,
    uint3 GId : SV_GroupID, uint3 DTid : SV_DispatchThreadID)
{
    //When using forward differences to compute the SDF gradient,
    //we need to use trilinear interpolation to look up 4 points in the SDF.
    //In the worst case, this involves reading the distances in 4 different cubes (reading 32 floats from the SDF).
    //In the ideal case, only 1 cube needs to be read (reading 8 floats from the SDF).
    //The number of distance lookups varies depending on whether the 4 points cross cell boundaries.
    //
    //If we assume that (h < g_CellSize), where h is the distance between the points used for finite difference,
    //we can mix forwards and backwards differences to ensure that the points never cross cell boundaries (always read 8 floats).
    //By default we use forward differences, and switch to backwards differences for each dimension that crosses a cell boundary.
    //
    //This is much faster than using forward differences only, but it could also be less stable.
    //In particular, it has the effect of making the gradient less accurate. The amount of inaccuracy is 
    //proportional to the size of h, so it is recommended keep h as low as possible.

    ...

    TressFX提供了另一套矫正算法,思路稍有改变,主要区别在计算梯度方向和矫正量上,略,感兴趣可以参考:
    https://github.com/GPUOpen-Effects/TressFX/blob/v4.1.0/src/Shaders/TressFXSDFCollision.hlsl#L597-L715
}

后记

毛发的物理部分到这里就结束啦,到此时,我们已经准备好了可以供后续渲染处理的发丝数据(还不是三角形网格数据,只是发丝曲线上的关键点),后续的文章将进入渲染部分。

原始链接:https://tis.ac.cn/blog/kongdeyou/hair_simulation_and_rendering_3/

版权声明: "CC BY-NC-ND 4.0" 署名-不可商用-禁止演绎 转载请注明原文链接及作者信息,侵权必究。

评论区 · 欢迎大家友好交流 · 若未正常显示请刷新网页

×

喜欢或有帮助?赞赏下作者呗!