9#include <glm/geometric.hpp>
14constexpr float kEpsilon = 1e-8f;
31 params_.
maxDt = std::max(1e-4f, params_.
maxDt);
33 restPoints_.assign(guides.
points().begin(), guides.
points().end());
45 for (
size_t ci = 0; ci < guides.
curveCount(); ++ci) {
50 "GuideSimulator::reset: guide curve needs >= 2 points",
"hair.sim.curve"));
53 span.pointOffset = uint32_t(pos_.size());
54 span.pointCount = uint32_t(pts.size());
56 for (
size_t pi = 0; pi < pts.size(); ++pi) {
57 const glm::vec3
p = pts[pi].position;
60 pinned_.push_back(pi == 0 ? uint8_t(1) : uint8_t(0));
65 s.i0 = span.pointOffset + uint32_t(pi - 1);
66 s.i1 = span.pointOffset + uint32_t(pi);
67 s.restLength = std::max(seg, kEpsilon);
68 segments_.push_back(
s);
72 curves_.push_back(span);
92 params_.
maxDt = std::max(1e-4f, params_.
maxDt);
101 if (!rootPositionsXYZ || rootCount !=
int(curves_.size())) {
104 "GuideSimulator::setPinnedRoots: rootCount must equal curveCount",
"hair.sim.roots"));
106 for (
size_t ci = 0; ci < curves_.size(); ++ci) {
107 const uint32_t
idx = curves_[ci].pointOffset;
108 const glm::vec3
p{rootPositionsXYZ[ci * 3 + 0], rootPositionsXYZ[ci * 3 + 1],
109 rootPositionsXYZ[ci * 3 + 2]};
110 if (!std::isfinite(
p.x) || !std::isfinite(
p.y) || !std::isfinite(
p.z)) {
115 const glm::vec3 delta =
p - pos_[
idx];
118 for (uint32_t pi = 1; pi < curves_[ci].pointCount; ++pi) {
119 pos_[
idx + pi] += delta;
120 prev_[
idx + pi] += delta;
126void GuideSimulator::solveDistances(
float dt) {
127 const float dt2 = dt * dt;
128 const float alpha = params_.
compliance / std::max(dt2, kEpsilon);
130 for (
int it = 0; it < params_.
iterations; ++it) {
131 for (
const Segment &seg : segments_) {
132 glm::vec3 &p0 = pos_[seg.i0];
133 glm::vec3 &p1 = pos_[seg.i1];
134 const float w0 = pinned_[seg.i0] ? 0.f : 1.f;
135 const float w1 = pinned_[seg.i1] ? 0.f : 1.f;
136 const float wSum = w0 + w1;
137 if (wSum < kEpsilon)
continue;
139 glm::vec3 delta = p1 - p0;
140 float len = glm::length(delta);
141 if (len < kEpsilon) {
142 delta = glm::vec3(0.f, kEpsilon, 0.f);
145 const glm::vec3
n = delta / len;
146 const float C = len - seg.restLength;
147 const float corr = C / (wSum + alpha);
148 p0 +=
n * (corr * w0);
149 p1 -=
n * (corr * w1);
154void GuideSimulator::resolveCollisions() {
156 if (yPlane < -1.0e5f)
return;
157 for (
size_t i = 0; i < pos_.size(); ++i) {
158 if (pinned_[i])
continue;
159 if (pos_[i].
y < yPlane) {
161 if (prev_[i].
y < yPlane) prev_[i].y = yPlane;
171 if (!std::isfinite(dt) || dt <= 0.f) {
177 const float stepDt = std::min(dt, params_.
maxDt);
178 const float damp = std::pow(std::clamp(params_.
damping, 0.f, 1.f), stepDt);
179 const glm::vec3 accel = params_.
gravity + params_.
wind;
181 for (
size_t i = 0; i < pos_.size(); ++i) {
186 const glm::vec3 cur = pos_[i];
187 const glm::vec3 vel = (cur - prev_[i]) * damp;
188 pos_[i] = cur + vel + accel * (stepDt * stepDt);
192 solveDistances(stepDt);
195 for (
size_t i = 0; i < pos_.size(); ++i) {
196 if (pinned_[i]) pos_[i] = prev_[i];
206 if (restPoints_.size() != pos_.size()) {
212 std::vector<StrandPoint>
points = restPoints_;
213 for (
size_t i = 0; i < pos_.size(); ++i) {
214 points[i].position = pos_[i];
216 std::vector<StrandCurve> curves;
217 curves.reserve(curves_.size());
218 for (
const CurveSpan &span : curves_) {
221 curve.pointCount = span.pointCount;
223 for (uint32_t pi = 1; pi < span.pointCount; ++pi) {
224 len += glm::length(pos_[span.pointOffset + pi] - pos_[span.pointOffset + pi - 1]);
227 curves.push_back(
curve);
Stable, structured diagnostics shared by engine modules.
std::array< float, 3 > position
std::shared_ptr< const std::vector< glm::vec2 > > points
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
void setParams(const GuideSimParams ¶ms)
Sets the params.
Result< void > step(float dt)
Advance simulation by dt seconds (clamped by params.maxDt).
Result< StrandsDatas > snapshot() const
Export current particle positions as StrandsDatas (same topology). @ownership Returned buffer uniquel...
const GuideSimParams & params() const
Params.
Result< void > reset(const StrandsDatas &guides, const GuideSimParams ¶ms={})
Capture rest topology and pin every curve root. @ownership Copies guides; caller retains the source.
Result< void > setPinnedRoots(const float *rootPositionsXYZ, int rootCount)
Teleport pinned roots (e.g. after skin binding deform).
bool isReady() const
True when ready.
CPU-authoritative strand buffer (UE FHairStrandsDatas analogue).
Result< void > validate() const
Verify offsets, counts, and finite values.
void setCurves(std::vector< StrandCurve > curves)
Sets the curves.
void setPoints(std::vector< StrandPoint > points)
Sets the points.
size_t curveCount() const
std::span< const StrandPoint > points() const
Borrowed points; invalidated by mutation.
std::span< const StrandPoint > curvePoints(size_t curveIndex) const
Borrowed points of one curve.
size_t pointCount() const
std::vector< ParamSpec > params
Procedural hair/fur card meshes + material/LOD helpers (no strand simulation).
Parameters for CPU guide-strand dynamics (UE groom sim lite analogue).
int iterations
XPBD solver iterations per substep (clamped to [1, 16]).
float collisionY
Infinite ground plane y = collisionY; particles below are pushed up. Set very low (default) to disabl...
float compliance
XPBD distance compliance (m/N); 0 ≈ hard constraint. Larger values allow more stretch.
float damping
Velocity retention per second (0 = freeze, 1 = no damping).
float maxDt
Clamp for a single step (seconds).
One strand as a contiguous point range. @ownership Value type owned by StrandsDatas.