// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) // SPDX-FileCopyrightText: 2026 Jorrit Rouwe // SPDX-License-Identifier: MIT #pragma once #include #include #include #include #include JPH_NAMESPACE_BEGIN class PhysicsSystem; #ifdef JPH_DEBUG_RENDERER class DebugRenderer; #endif class HairShaders; /// Hair simulation instance /// /// Note that this system is currently still in development, it is missing important features like: /// /// - Level of detail /// - Wind forces /// - Advection step for the grid velocity field /// - Support for collision detection against shapes other than ConvexHullShape /// - The Gradient class is very limited and will be replaced by a texture lookup /// - Gravity preload factor is not fully functioning yet /// - It is wasteful of memory (e.g. stores everything both on CPU and GPU) /// - Only supports a single neutral pose to drive towards /// - It could use further optimizations class JPH_EXPORT Hair : public NonCopyable { public: /// Constructor / destructor Hair(const HairSettings *inSettings, RVec3Arg inPosition, QuatArg inRotation, ObjectLayer inLayer); ~Hair(); /// Initialize void Init(ComputeSystem *inComputeSystem); /// Position and rotation of the hair in world space void SetPosition(RVec3Arg inPosition) { mPosition = inPosition; } void SetRotation(QuatArg inRotation) { mRotation = inRotation; } RMat44 GetWorldTransform() const { return RMat44::sRotationTranslation(mRotation, mPosition); } /// Access to the hair settings object which contains the configuration of the hair const HairSettings * GetHairSettings() const { return mSettings; } /// The hair will be initialized in its default pose with zero velocity at the new position and rotation during the next update void OnTeleported() { mTeleported = true; } /// Ability to externally provide the scalp vertices buffer. This allows skipping skinning the scalp during the simulation update. You may need to override JPH_SHADER_BIND_SCALP_VERTICES in HairSkinRootsBindings.h to match the format of the provided buffer. void SetScalpVerticesCB(ComputeBuffer *inBuffer) { mScalpVerticesCB = inBuffer; } /// Ability to externally provide the scalp triangle indices buffer. This allows skipping skinning the scalp in during the simulation update. You may need to override JPH_SHADER_BIND_SCALP_TRIANGLES in HairSkinRootsBindings.h to match the format of the provided buffer. void SetScalpTrianglesCB(ComputeBuffer *inBuffer) { mScalpTrianglesCB = inBuffer; } /// When skipping skinning, this allow specifying a transform that transforms the scalp mesh into head space. void SetScalpToHead(Mat44Arg inMat) { mScalpToHead = inMat; } /// Function that converts the render positions buffer to Float3 vertices for debugging purposes. It maps an application defined format to Float3. Third parameter is the number of vertices. using RenderPositionsToFloat3 = std::function; /// Enable externally set render vertices buffer (with potentially different vertex layout). Note that this also requires replacing the HairCalculateRenderPositions shader. void OverrideRenderPositionsCB(const RenderPositionsToFloat3 &inRenderPositionsToFloat3) { JPH_ASSERT(mRenderPositionsCB == nullptr, "Must be called before Init"); mRenderPositionsOverridden = true; mRenderPositionsToFloat3 = inRenderPositionsToFloat3; } /// Allow setting the render vertices buffer externally in case it has special requirements for the calling application. You may need to override JPH_SHADER_BIND_RENDER_POSITIONS in HairCalculateRenderPositionsBindings.h to match the format of the provided buffer. void SetRenderPositionsCB(ComputeBuffer *inBuffer) { JPH_ASSERT(mRenderPositionsOverridden, "Must call OverrideRenderPositionsCB first"); mRenderPositionsCB = inBuffer; } /// Step the hair simulation forward in time /// @param inDeltaTime Time step /// @param inJointToHair Transform that transforms from joint space to hair local space (as defined by GetWorldTransform) /// @param inJointMatrices Array of joint matrices in world space, length needs to match HairSettings::mScalpInverseBindPose.size() /// @param inSystem Physics system used for collision detection /// @param inShaders Preloaded hair compute shaders /// @param inComputeSystem Compute system to use /// @param inComputeQueue Compute queue to use void Update(float inDeltaTime, Mat44Arg inJointToHair, const Mat44 *inJointMatrices, const PhysicsSystem &inSystem, const HairShaders &inShaders, ComputeSystem *inComputeSystem, ComputeQueue *inComputeQueue); /// Access to the resulting simulation data ComputeBuffer * GetScalpVerticesCB() const { return mScalpVerticesCB; } ///< Skinned scalp vertices ComputeBuffer * GetScalpTrianglesCB() const { return mScalpTrianglesCB; } ///< Skinned scalp triangle indices ComputeBuffer * GetPositionsCB() const { return mPositionsCB; } ///< Note transposed for better memory access ComputeBuffer * GetVelocitiesCB() const { return mVelocitiesCB; } ///< Note transposed for better memory access ComputeBuffer * GetVelocityAndDensityCB() const { return mVelocityAndDensityCB; } ///< Velocity grid ComputeBuffer * GetRenderPositionsCB() const { return mRenderPositionsCB; } ///< Render positions of the hair strands (see HairSettings::mRenderStrands to see where each strand starts and ends) /// Read back the GPU state so that the functions below can be used. For debugging purposes only, this is slow! void ReadBackGPUState(ComputeQueue *inComputeQueue); /// Lock/unlock the data buffers so that the functions below return valid values. void LockReadBackBuffers(); void UnlockReadBackBuffers(); /// Access to the resulting simulation data (only valid when ReadBackGPUState has been called and the buffers have been locked) const Float3 * GetScalpVertices() const { return mScalpVertices; } const Float3 * GetPositions() const { return mPositions; } const Quat * GetRotations() const { return mRotations; } StridedPtr GetVelocities() const { return { (const Float3 *)&mVelocities->mVelocity, sizeof(JPH_HairVelocity) }; } StridedPtr GetAngularVelocities() const { return { (const Float3 *)&mVelocities->mAngularVelocity, sizeof(JPH_HairVelocity) }; } const Float4 * GetGridVelocityAndDensity() const { return mVelocityAndDensity; } const Float3 * GetRenderPositions() const { return mRenderPositions; } #ifdef JPH_DEBUG_RENDERER enum class ERenderStrandColor { PerRenderStrand, PerSimulatedStrand, GravityFactor, WorldTransformInfluence, GridVelocityFactor, GlobalPose, SkinGlobalPose, }; struct DrawSettings { /// This specifies the range of simulation strands to draw, when drawing render strands we only draw the strands that belong to these simulation strands. uint mSimulationStrandBegin = 0; uint mSimulationStrandEnd = UINT_MAX; bool mDrawRods = true; ///< Draws the simulated rods bool mDrawUnloadedRods = false; ///< Draw rods in their unloaded pose. This pose is obtained by removing gravity influence from the modeled pose. bool mDrawVertexVelocity = false; ///< Draws the velocity at each simulated vertex as an arrow bool mDrawAngularVelocity = false; ///< Draws the angular velocity at each simulated vertex as an arrow bool mDrawOrientations = false; ///< Draws a coordinate space for each simulated vertex bool mDrawNeutralDensity = false; ///< Draws grid density of the hair in its neutral pose bool mDrawGridDensity = false; ///< Draws the current grid density of the hair bool mDrawGridVelocity = false; ///< Draws the velocity of each grid cell as an arrow bool mDrawSkinPoints = false; ///< Draws the skinning points on the scalp bool mDrawRenderStrands = false; ///< Draws the render strands (slow, for debugging purposes!) bool mDrawInitialGravity = true; ///< Draws the configured initial gravity vector used to calculate the unloaded vertex positions ERenderStrandColor mRenderStrandColor = ERenderStrandColor::PerSimulatedStrand; ///< Color for each strand }; /// Debug functionality to draw the hair and its simulation properties void Draw(const DrawSettings &inSettings, DebugRenderer *inRenderer); #endif // JPH_DEBUG_RENDERER protected: using Gradient = HairSettings::Gradient; using GradientSampler = HairSettings::GradientSampler; // Information about a colliding shape. Is always a leaf shape, compound shapes are expanded. struct LeafShape { LeafShape() = default; LeafShape(Mat44Arg inTransform, Vec3Arg inScale, Vec3Arg inLinearVelocity, Vec3Arg inAngularVelocity, const Shape *inShape) : mTransform(inTransform), mScale(inScale), mLinearVelocity(inLinearVelocity), mAngularVelocity(inAngularVelocity), mShape(inShape) { } Mat44 mTransform; Vec3 mScale; Vec3 mLinearVelocity; Vec3 mAngularVelocity; RefConst mShape; }; // Internal context used during a simulation step struct UpdateContext { Mat44 mDeltaTransform; // Transforms positions from the old hair transform to the new Quat mDeltaTransformQuat; // Rotation part of mDeltaTransform uint mNumIterations; // Number of iterations to run the solver for bool mNeedsCollision; // If collision detection should be performed bool mNeedsGrid; // If the grid should be calculated bool mGlobalPoseOnly; // If no simulation is needed and only the global pose needs to be applied bool mHasTransformChanged; // If the world transform has changed float mDeltaTime; // Delta time for a sub step float mHalfDeltaTime; // 0.5 * mDeltaTime float mInvDeltaTimeSq; // 1 / mDeltaTime^2 float mTwoDivDeltaTime; // 2 / mDeltaTime float mTimeRatio; // Ratio between sub step delta time and default sub step delta time Vec3 mSubStepGravity; // Gravity to apply in a sub step Array mShapes; // List of colliding shapes }; // Calculate the UpdateContext parameters void InitializeContext(UpdateContext &outCtx, float inDeltaTime, const PhysicsSystem &inSystem); RefConst mSettings; // Shared hair settings, must be kept alive during the lifetime of this hair instance RVec3 mPrevPosition; // Position at the start of the last time step RVec3 mPosition; // Current position in world space Quat mPrevRotation; // Rotation at the start of the last time step Quat mRotation; // Current rotation in world space bool mTeleported = true; // If the hair got teleported and should be set to the default pose ObjectLayer mLayer; // Layer for the hair to collide with Mat44 mScalpToHead = Mat44::sIdentity(); // When skipping skinning, this allow specifying a transform that transforms the scalp mesh into head space bool mRenderPositionsOverridden = false; // Indicates that the render positions buffer is provided externally RenderPositionsToFloat3 mRenderPositionsToFloat3; // Function that transforms the render positions buffer to Float3 vertices for debugging purposes Ref mScalpJointMatricesCB; Ref mScalpVerticesCB; Ref mScalpTrianglesCB; Ref mTargetPositionsCB; // Target root positions determined by skinning (where we're interpolating to, eventually written to mPositionsCB) Ref mTargetGlobalPoseTransformsCB; // Target global pose transforms determined by skinning (where we're interpolating to, eventually written to mGlobalPoseTransformsCB) Ref mGlobalPoseTransformsCB; // Current global pose transforms used for skinning the hairs Ref mShapePlanesCB; Ref mShapeVerticesCB; Ref mShapeIndicesCB; Ref mCollisionPlanesCB; Ref mCollisionShapesCB; Ref mMaterialsCB; Ref mPreviousPositionsCB; Ref mPositionsCB; Ref mVelocitiesCB; Ref mVelocityAndDensityCB; Ref mConstantsCB; Array> mIterationConstantsCB; Ref mRenderPositionsCB; // Only valid after ReadBackGPUState has been called Ref mScalpVerticesReadBackCB; Ref mPositionsReadBackCB; Ref mVelocitiesReadBackCB; Ref mVelocityAndDensityReadBackCB; Ref mRenderPositionsReadBackCB; const Float3 * mScalpVertices = nullptr; Float3 * mPositions = nullptr; Quat * mRotations = nullptr; JPH_HairVelocity * mVelocities = nullptr; const Float4 * mVelocityAndDensity = nullptr; const Float3 * mRenderPositions = nullptr; }; JPH_NAMESPACE_END