Files
Frametop--frametop/screens/handmesh.cpp
T
DeeJanuzandClaude Opus 5.5 3c4964bc56 Hand cutouts: a round hole per hand by default (HANDS_MATTE=circle)
Until the tracking is good enough for a hand-shaped hole, each hand cuts a
disk facing the eye, wide enough for the whole hand plus the margin, drawn
as the stadium around the ellipse each eye sees on the screen with a soft
edge. mesh and capsules stay selectable (HANDS_MATTE, cutouts matte).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:17:04 -06:00

214 lines
9.1 KiB
C++

// Rendered hands, the CPU side (see handmesh.h).
#include "handmesh.h"
#include <algorithm>
#include <cmath>
namespace handmesh {
namespace {
struct V {
double x, y, z;
V(double a = 0, double b = 0, double c = 0) : x(a), y(b), z(c) {}
explicit V(const float p[3]) : x(p[0]), y(p[1]), z(p[2]) {}
V operator+(const V &o) const { return {x + o.x, y + o.y, z + o.z}; }
V operator-(const V &o) const { return {x - o.x, y - o.y, z - o.z}; }
V operator*(double k) const { return {x * k, y * k, z * k}; }
double dot(const V &o) const { return x * o.x + y * o.y + z * o.z; }
V cross(const V &o) const { return {y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x}; }
double norm() const { return std::sqrt(dot(*this)); }
V unit() const {
const double n = norm();
return n > 1e-12 ? *this * (1 / n) : V{};
}
};
// Any unit vector square to d.
V Square(const V &d) {
const V a = std::fabs(d.x) < 0.9 ? V(1, 0, 0) : V(0, 1, 0);
return d.cross(a).unit();
}
void Push(Mesh &m, const V &p, const V &n) {
m.v.push_back({{float(p.x), float(p.y), float(p.z)}, {float(n.x), float(n.y), float(n.z)}, 0});
}
constexpr int kAround = 10; // a capsule's segments around its axis
constexpr int kCap = 3; // ...and along each round end
constexpr float kFaded = 0.11f; // metres behind the wrist: the forearm has faded out by then (handview.cpp)
} // namespace
void AddCapsule(const handcut::Capsule &c, Mesh &out) {
const V a(c.a), b(c.b);
V d = b - a;
if (d.norm() < 1e-6) d = V(0, 1, 0);
d = d.unit();
const V u = Square(d), w = d.cross(u);
const size_t base = out.v.size();
if (base + (2 * kCap + 2) * kAround > 65535) return;
static double around[kAround][2], along[2 * kCap + 2][2]; // cos, sin
static const bool once = [] {
for (int i = 0; i < kAround; ++i) around[i][0] = std::cos(i * 2 * M_PI / kAround), around[i][1] = std::sin(i * 2 * M_PI / kAround);
for (int r = 0; r < 2 * kCap + 2; ++r) {
const double th = (r <= kCap ? r - kCap : r - kCap - 1) * (M_PI / 2) / kCap;
along[r][0] = std::cos(th), along[r][1] = std::sin(th);
}
return true;
}();
(void)once;
// rings from a's pole to b's: a's round end (-90..0 degrees), then b's (0..90)
const int rings = 2 * kCap + 2;
for (int k = 0; k < rings; ++k) {
const bool end = k > kCap;
const V centre = end ? b : a;
const double r = end ? c.rb : c.ra;
for (int i = 0; i < kAround; ++i) {
const V n = d * along[k][1] + (u * around[i][0] + w * around[i][1]) * along[k][0];
Push(out, centre + n * r, n);
}
}
for (int r = 0; r + 1 < rings; ++r)
for (int i = 0; i < kAround; ++i) {
const int j = (i + 1) % kAround;
const uint16_t p0 = uint16_t(base + r * kAround + i), p1 = uint16_t(base + r * kAround + j);
const uint16_t q0 = uint16_t(p0 + kAround), q1 = uint16_t(p1 + kAround);
out.idx.insert(out.idx.end(), {p0, p1, q1, p0, q1, q0});
}
}
void AddHull(const std::vector<std::array<float, 3>> &pts, Mesh &out) {
const size_t n = pts.size();
if (n < 4) return;
V mid;
for (const auto &p : pts) mid = mid + V(p.data()) * (1.0 / n);
// every triangle with all the points on one side of it is on the hull (n is at most 16:
// a few thousand dot products); a face of more than 3 points comes out as overlapping
// triangles in its plane, which draw the same
for (size_t i = 0; i < n; ++i)
for (size_t j = i + 1; j < n; ++j)
for (size_t k = j + 1; k < n; ++k) {
const V a(pts[i].data()), b(pts[j].data()), c(pts[k].data());
V nrm = (b - a).cross(c - a);
const double len = nrm.norm();
if (len < 1e-9) continue;
nrm = nrm * (1 / len);
bool above = false, below = false;
for (size_t q = 0; q < n && !(above && below); ++q) {
const double s = (V(pts[q].data()) - a).dot(nrm);
above |= s > 1e-5, below |= s < -1e-5;
}
if (above && below) continue;
if ((mid - a).dot(nrm) > 0) nrm = nrm * -1; // outward
if (out.v.size() + 3 > 65535) return;
const uint16_t at = uint16_t(out.v.size());
Push(out, a, nrm), Push(out, b, nrm), Push(out, c, nrm);
out.idx.insert(out.idx.end(), {at, uint16_t(at + 1), uint16_t(at + 2)});
}
}
void Build(const handcut::Hand &h, const double headPos[3], Mesh &out) {
out.clear();
handcut::MatteParams p;
p.shape = handcut::MatteShape::Mesh;
p.marginDeg = p.motionMaxDeg = p.minMarginM = 0;
handcut::Matte matte;
handcut::BuildMatte({h}, {}, headPos, p, matte);
// the palm's pieces fill the gaps between the fingers' first bones and the knuckles: a
// capsule from the wrist to each knuckle, under the palm's slab
const float *w = h.p[0];
for (int j : {5, 9, 13, 17}) {
handcut::Capsule c{{w[0], w[1], w[2]}, {h.p[j][0], h.p[j][1], h.p[j][2]}, 0.012f, 0.0095f};
AddCapsule(c, out);
}
for (const handcut::Capsule &c : matte.caps) AddCapsule(c, out);
for (const handcut::Hull &hull : matte.hulls) AddHull(hull.v, out);
// how far behind the wrist each point is, along the hand; the forearm's stub ends just past
// where it has faded out (the matte's goes on to 18 cm), so its square isn't bigger than that
const V wrist(h.p[0]), fwd = (V(h.p[9]) - wrist).unit();
const double f[3] = {fwd.x, fwd.y, fwd.z};
for (Vertex &v : out.v) {
v.back = float(-(V(v.p) - wrist).dot(fwd));
if (v.back <= kFaded) continue;
for (int i = 0; i < 3; ++i) v.p[i] += float(f[i] * (v.back - kFaded));
v.back = kFaded;
}
}
bool Place(const Mesh &m, const double eyes[2][3], double margin, Quad &out) {
if (m.v.empty()) return false;
const V e0(eyes[0][0], eyes[0][1], eyes[0][2]), e1(eyes[1][0], eyes[1][1], eyes[1][2]);
const V head = (e0 + e1) * 0.5;
V lo(1e9, 1e9, 1e9), hi(-1e9, -1e9, -1e9);
for (const Vertex &v : m.v) {
lo = V(std::min(lo.x, double(v.p[0])), std::min(lo.y, double(v.p[1])), std::min(lo.z, double(v.p[2])));
hi = V(std::max(hi.x, double(v.p[0])), std::max(hi.y, double(v.p[1])), std::max(hi.z, double(v.p[2])));
}
const V c = (lo + hi) * 0.5;
const double dist = (head - c).norm();
if (dist < 0.08) return false;
const V z = (head - c) * (1 / dist);
V y = V(0, 1, 0) - z * z.y;
if (y.norm() < 1e-3) y = Square(z);
y = y.unit();
const V x = y.cross(z);
// what each eye sees of the mesh, on the square's plane
double half = 0;
for (const V &e : {e0, e1}) {
const double ez = (e - c).dot(z);
if (ez < 0.08) return false;
for (const Vertex &v : m.v) {
const V d = V(v.p) - e;
const double dz = d.dot(z); // negative: away from the eye, toward the square
if (dz > -0.04) continue; // at or behind the eye: clipped when drawn
const V q = e + d * (-ez / dz) - c;
half = std::max({half, std::fabs(q.dot(x)), std::fabs(q.dot(y))});
}
}
if (half <= 0) return false;
half = std::min(half * (1 + 2 * margin), 0.6);
handcut::Mat &P = out.pose;
const V cols[3] = {x, y, z};
for (int col = 0; col < 3; ++col) {
P.m[0][col] = float(cols[col].x), P.m[1][col] = float(cols[col].y), P.m[2][col] = float(cols[col].z);
}
P.m[0][3] = float(c.x), P.m[1][3] = float(c.y), P.m[2][3] = float(c.z);
out.size = 2 * half;
out.distance = dist;
return true;
}
void EyeMatrix(const Quad &q, const double eye[3], double nearM, double farM, float out[16]) {
const handcut::Mat &P = q.pose;
// room -> the square's frame: R^T (p - t)
double R[3][4];
for (int r = 0; r < 3; ++r) {
double t = 0;
for (int i = 0; i < 3; ++i) R[r][i] = P.m[i][r], t += P.m[i][r] * P.m[i][3];
R[r][3] = -t;
}
double E[3]; // the eye in the square's frame
for (int r = 0; r < 3; ++r) E[r] = R[r][0] * eye[0] + R[r][1] * eye[1] + R[r][2] * eye[2] + R[r][3];
// the view from the eye, V = local - E; what it meets on the square, as clip coordinates:
// x = (ez Vx - ex Vz) / h, y = -(ez Vy - ey Vz) / h (the top first), w = -Vz
// z = A w + B (depth from nearM to farM along the view)
const double h = q.size / 2, ex = E[0], ey = E[1], ez = E[2];
const double A = (farM + nearM) / (farM - nearM), B = -2 * farM * nearM / (farM - nearM);
const double K[4][4] = {{ez / h, 0, -ex / h, 0}, {0, -ez / h, ey / h, 0}, {0, 0, -A, B}, {0, 0, -1, 0}};
// K * (local - E): fold -E into the translation, then K * [R | t - E]
double L[4][4] = {};
for (int r = 0; r < 3; ++r) {
for (int i = 0; i < 3; ++i) L[r][i] = R[r][i];
L[r][3] = R[r][3] - E[r];
}
L[3][3] = 1;
for (int r = 0; r < 4; ++r)
for (int col = 0; col < 4; ++col) {
double s = 0;
for (int k = 0; k < 4; ++k) s += K[r][k] * L[k][col];
out[col * 4 + r] = float(s);
}
}
} // namespace handmesh