Files
AV-Live/avlivebody-mac/Sources/AVLiveBody/Resources/scene.metal
T
L'électron rare 131f6ed5a8 feat(avlivebody): port metal scene renderer
Copy scene.metal from archive, add SceneRenderer.swift (Bundle.main),
stride-guard test (144 B). Xcode compiles .metal in Resources phase
even with buildPhase:resources, so use a postBuildScript with
UNLOCALIZED_RESOURCES_FOLDER_PATH to copy the source as plain text.
2026-06-26 13:57:55 +02:00

614 lines
22 KiB
Metal

// scene.metal — fond reactif aux flux data_feeds + skeleton overlay.
//
// 9 modes visuels style demoscene 2023+ (raymarching SDF, fractales,
// parallax, palette IQ). Reactivite open-data via SceneUniforms.
// 0 storm fbm tissu palette Kp/Bz + lightning flash
// 1 tunnel raymarched tube avec anneaux translucents (wind, RMS)
// 2 plasma volumetric noise palette IQ (Kp, social_rate)
// 3 kaleido fractal KIFS 6-fold rotation 3D (flare, time)
// 4 voronoi cellular 3D crystal sphere (lightning, RMS)
// 5 metaballs raymarched SDF metaballs colored shading (RMS, beat)
// 6 starfield galaxy spiral parallax + god rays (wind, kp)
// 7 bars 3D pillars en perspective avec depth fog (RMS+social)
// 8 hands3d raymarching mandelbox-like + hands camera control
#include <metal_stdlib>
using namespace metal;
struct SceneUniforms {
float time;
float rms;
float kp_norm;
float netz_dev;
float lightning_flash;
float flare;
float wind_norm;
float bz_norm;
float social_rate;
float pose_alive;
float pose_count;
float width;
float height;
float viz_mode;
float hand_l_x;
float hand_l_y;
float hand_r_x;
float hand_r_y;
// ---- 2026-05-14 face / hand / body3d derivatives ----
// mouth_open : |lip51.y - lip57.y| normalized 0..~0.15
// eye_open_l/r : H/W ratio of eye bbox, ~0.15 (closed) .. 0.45 (open)
// head_tilt : atan2(eye_R.y-eye_L.y, eye_R.x-eye_L.x) rad
// head_yaw : nose.y - eyes_mid.y (proxy pitch, normalized)
// finger_pinch_l/r: simd_distance(thumb_tip[4], index_tip[8]) px-norm
// body_x/y/z : pelvis world meters (MediaPipe hip-relative)
// body_height : |hip.y - head.y| world meters
// arm_spread : |wristL.x - wristR.x| world meters
// pose_velocity : EMA(|pelvis(t) - pelvis(t-1)|), alpha=0.3
float mouth_open;
float eye_open_l;
float eye_open_r;
float head_tilt;
float head_yaw;
float finger_pinch_l;
float finger_pinch_r;
float body_x;
float body_y;
float body_z;
float body_height;
float arm_spread;
float pose_velocity;
float _pad0;
float _pad1;
float _pad2;
float _pad3;
float _pad4;
};
struct VsOut {
float4 position [[position]];
float2 uv;
};
vertex VsOut bg_vertex(uint vid [[vertex_id]]) {
float2 p = float2((vid << 1) & 2, vid & 2);
VsOut o;
o.position = float4(p * 2.0 - 1.0, 0.0, 1.0);
o.uv = p;
return o;
}
// ===== Helpers ====================================================
float hash21(float2 p) {
p = fract(p * float2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float hash31(float3 p) {
p = fract(p * 0.1031);
p += dot(p, p.yzx + 33.33);
return fract((p.x + p.y) * p.z);
}
float noise2(float2 p) {
float2 i = floor(p);
float2 f = fract(p);
float a = hash21(i);
float b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1));
float d = hash21(i + float2(1, 1));
float2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(float2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 5; ++i) { v += a * noise2(p); p *= 2.13; a *= 0.5; }
return v;
}
// Palette cosinusoidale IQ : 3 tons doux
float3 palIQ(float t, float3 a, float3 b, float3 c, float3 d) {
return a + b * cos(6.28318 * (c * t + d));
}
// Rotations
float3 rotY(float3 p, float a) {
float c = cos(a), s = sin(a);
return float3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}
float3 rotX(float3 p, float a) {
float c = cos(a), s = sin(a);
return float3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
}
float3 rotZ(float3 p, float a) {
float c = cos(a), s = sin(a);
return float3(c * p.x - s * p.y, s * p.x + c * p.y, p.z);
}
// SDF primitives
float sdSphere(float3 p, float r) { return length(p) - r; }
float sdBox(float3 p, float3 b) {
float3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float sdTorus(float3 p, float2 t) {
float2 q = float2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
float vignette(float2 p) {
return 1.0 - smoothstep(0.6, 1.5, length(p));
}
// ===== Modes =======================================================
// ---- 0 storm : tissu fbm reactif + bloom-fake ----
// Pose mods : pose_velocity boost intensity, head_tilt shifts hue.
float3 mode_storm(float2 p, constant SceneUniforms& U) {
float storm = saturate(U.kp_norm * 1.0 + max(-U.bz_norm, 0.0) * 0.5
+ U.pose_velocity * 2.0);
float speed = 0.08 + U.wind_norm * 1.5 + U.pose_velocity * 3.0;
float zoom = 1.8 - U.rms * 1.2;
float n = fbm(p * zoom + float2(U.time * speed, U.time * speed * 0.7));
n = pow(n, 1.2 - U.rms * 0.5);
float netz = sin(U.time * 50.0 + U.netz_dev * 800.0) * 0.06;
float hue_shift = U.head_tilt * 0.25;
float3 base = palIQ(n + storm * 0.5 + hue_shift,
float3(0.10, 0.05, 0.20),
float3(0.40, 0.30, 0.55),
float3(1.0, 1.0, 1.0),
float3(0.0, 0.33, 0.67));
float bloom = smoothstep(0.7, 1.0, n);
float velocity_boost = 1.0 + U.pose_velocity * 1.5;
return (base * (n * 1.4 + 0.3) + netz + U.rms * 1.2
+ bloom * 0.7) * velocity_boost
+ float3(1.0, 0.55, 0.1) * U.flare * 1.4
+ float3(U.lightning_flash * 0.7);
}
// ---- 1 tunnel : raymarched cylindrical tube avec anneaux ----
float3 mode_tunnel(float2 p, constant SceneUniforms& U) {
// Pseudo-3D tunnel: r/theta + scrolling z
float r = length(p);
float a = atan2(p.y, p.x);
float z = U.time * (1.5 + U.wind_norm * 8.0 + U.rms * 4.0);
// Repeat depth
float d = 1.0 / max(r, 0.04) + z;
// anneaux + spirale
float ring = sin(d * 4.0) * 0.5 + 0.5;
float spiral = sin(a * (8.0 + U.kp_norm * 6.0) + d * 0.6);
float v = ring * (0.4 + 0.6 * spiral);
// Iris central
v *= smoothstep(0.05, 0.20, r);
float3 base = palIQ(d * 0.06 + U.time * 0.05,
float3(0.15, 0.05, 0.35),
float3(0.55, 0.25, 0.35),
float3(1.0, 1.0, 0.8),
float3(0.0, 0.10, 0.20));
float3 col = base * v;
// Chromatic aberration fake : sample displaced
float chrom = U.lightning_flash * 0.15;
col.r *= 1.0 + chrom; col.b *= 1.0 - chrom;
return col + float3(1.0, 0.7, 0.3) * U.flare * 1.5
+ float3(U.lightning_flash * 0.6);
}
// ---- 2 plasma : volumetric noise palette IQ ----
// Pose mods : mouth_open modulates spatial frequency; head_yaw shifts hue.
float3 mode_plasma(float2 p, constant SceneUniforms& U) {
float t = U.time * (0.5 + U.rms * 1.5);
float freq_boost = 1.0 + U.mouth_open * 12.0;
// 3 octaves de sin/cos en composition
float v = sin(p.x * 4.0 * freq_boost + t)
+ sin(p.y * 5.0 * freq_boost - t * 1.2)
+ sin((p.x + p.y) * 3.5 + t * 0.7)
+ sin(length(p) * (8.0 + U.kp_norm * 4.0) - t * 1.8);
v = v * 0.25 + 0.5;
// Fake volumetric "depth" : repeat layers
float layer2 = sin(p.x * 2.0 - t * 0.5) * sin(p.y * 2.5 + t * 0.7);
v = mix(v, v * 0.5 + 0.5 * (layer2 + 1.0) * 0.5, 0.35);
float hue_offset = U.head_yaw * 0.5;
float3 col = palIQ(v + hue_offset,
float3(0.5),
float3(0.5),
float3(1.0, 1.0, 1.0),
float3(0.0, 0.33, 0.67));
col *= 0.8 + U.kp_norm * 0.7 + U.social_rate * 0.5
+ U.mouth_open * 1.0;
return col + float3(0.6, 0.3, 1.0) * U.lightning_flash * 0.5;
}
// ---- 3 kaleido : KIFS fractal 6-fold avec rot 3D fake ----
// Pose mods : arm_spread drives segment count (4..16).
float3 mode_kaleido(float2 p, constant SceneUniforms& U) {
float ang = U.time * 0.15 + U.flare * 2.0;
float c = cos(ang), s = sin(ang);
p = float2(c * p.x - s * p.y, s * p.x + c * p.y);
float r = length(p);
float a = atan2(p.y, p.x);
float seg_count = clamp(ceil(4.0 + U.arm_spread * 8.0), 3.0, 16.0);
float seg = 6.28318 / seg_count;
a = abs(fmod(a + seg * 0.5, seg) - seg * 0.5);
float2 q = float2(cos(a), sin(a)) * r;
// Iteration KIFS-like
float scale = 1.0;
for (int i = 0; i < 4; ++i) {
q = abs(q) - 0.35;
if (q.y > q.x) q = q.yx;
q *= 1.5; scale *= 1.5;
}
float v = length(q) / scale;
float n = fbm(q * 3.0 + U.time * 0.2);
float3 col = palIQ(v + n * 0.3,
float3(0.20, 0.10, 0.30),
float3(0.55, 0.40, 0.50),
float3(1.0, 1.0, 0.5),
float3(0.0, 0.25, 0.50));
col = col * (1.0 - exp(-v * 6.0));
return col * (0.8 + U.rms * 1.0)
+ float3(1.0, 0.6, 0.2) * U.flare * 1.2;
}
// ---- 4 voronoi : 3D crystalline cellular ----
float3 mode_voronoi(float2 p, constant SceneUniforms& U) {
// 3D voronoi : on echantillonne dans une grille 3D animee
float t = U.time * (0.4 + U.rms * 1.0);
float3 P = float3(p * 3.5, t);
float3 ip = floor(P);
float3 fp = fract(P);
float d1 = 10.0, d2 = 10.0;
for (int z = -1; z <= 1; ++z)
for (int y = -1; y <= 1; ++y)
for (int x = -1; x <= 1; ++x) {
float3 g = float3(float(x), float(y), float(z));
float3 o = float3(hash31(ip + g + 13.0),
hash31(ip + g + 71.0),
hash31(ip + g + 47.0));
o = 0.5 + 0.5 * sin(t + 6.28 * o);
float3 dv = g + o - fp;
float d = dot(dv, dv);
if (d < d1) { d2 = d1; d1 = d; }
else if (d < d2) { d2 = d; }
}
d1 = sqrt(d1); d2 = sqrt(d2);
float edge = smoothstep(0.0, 0.04, d2 - d1); // walls between cells
float face = smoothstep(0.0, 0.6, d1);
float3 base = palIQ(d1,
float3(0.05, 0.08, 0.20),
float3(0.45, 0.35, 0.55),
float3(1.0, 1.0, 0.6),
float3(0.2, 0.3, 0.0));
return base * (1.0 - face) + float3(1.0) * (1.0 - edge) * 0.5
+ U.lightning_flash * 0.8;
}
// ---- 5 metaballs : raymarched SDF ----
float metaballs_dist(float3 p, constant SceneUniforms& U) {
float t = U.time * 0.7;
float d = 100.0;
for (int k = 0; k < 5; ++k) {
float fk = float(k);
float3 c = float3(
sin(t * (0.6 + 0.13 * fk) + fk * 1.7) * 1.2,
cos(t * (0.5 + 0.11 * fk) + fk * 2.1) * 1.0,
sin(t * (0.4 + 0.09 * fk) + fk * 3.0) * 0.8
);
float radius = 0.45 + 0.15 * U.rms + 0.05 * sin(t + fk);
d = smin(d, sdSphere(p - c, radius), 0.45);
}
return d;
}
float3 mode_metaballs(float2 p, constant SceneUniforms& U) {
float3 ro = float3(0, 0, -3.5);
float3 rd = normalize(float3(p, 1.5));
float t = 0.0;
float glow = 0.0;
int i;
for (i = 0; i < 64; ++i) {
float3 pos = ro + rd * t;
float d = metaballs_dist(pos, U);
if (d < 0.01) break;
glow += 0.02 / (1.0 + d * d * 4.0);
t += d * 0.9;
if (t > 8.0) break;
}
float3 col = float3(0);
if (t < 8.0) {
float3 pos = ro + rd * t;
// normal via gradient
float2 e = float2(0.001, 0);
float3 n = normalize(float3(
metaballs_dist(pos + e.xyy, U) - metaballs_dist(pos - e.xyy, U),
metaballs_dist(pos + e.yxy, U) - metaballs_dist(pos - e.yxy, U),
metaballs_dist(pos + e.yyx, U) - metaballs_dist(pos - e.yyx, U)));
float3 lightDir = normalize(float3(0.6, 0.8, -0.5));
float lambert = max(0.0, dot(n, lightDir));
float fres = pow(1.0 - max(0.0, dot(n, -rd)), 2.0);
col = palIQ(pos.x * 0.3 + pos.y * 0.2 + U.time * 0.1,
float3(0.2, 0.0, 0.3),
float3(0.5, 0.5, 0.4),
float3(1.0),
float3(0.0, 0.33, 0.67)) * lambert;
col += float3(0.3, 0.7, 1.0) * fres * (0.7 + U.kp_norm);
}
col += float3(0.2, 0.6, 1.0) * glow * 1.5;
return col + U.lightning_flash * 0.6;
}
// ---- 6 starfield : galaxy spiral + parallax ----
// Pose mods : finger_pinch L+R drives star density per layer.
float3 mode_starfield(float2 p, constant SceneUniforms& U) {
float warp = U.time * (1.5 + U.wind_norm * 6.0);
float pinch = saturate((U.finger_pinch_l + U.finger_pinch_r) * 2.0);
int stars_per_layer = 30 + int(pinch * 70.0); // 30..100
// 3 layers of stars at different speeds
float3 col = float3(0);
for (int L = 0; L < 3; ++L) {
float speed = (1.0 + float(L) * 0.5);
float scale = 6.0 + float(L) * 4.0;
for (int k = 0; k < 100; ++k) {
if (k >= stars_per_layer) break;
float fk = float(k + L * 50);
float r0 = hash21(float2(fk, 7.0 + float(L)));
float a0 = hash21(float2(fk, 17.0 + float(L))) * 6.28;
// Spirale galactique
float angle = a0 + r0 * 4.0;
float dist = fract(r0 + warp * 0.04 * speed) * 1.6;
float2 q = float2(cos(angle + dist * 1.5),
sin(angle + dist * 1.5)) * dist;
float d = length(p - q);
float bright = smoothstep(0.012 / speed, 0.0, d);
col += float3(0.5 + r0 * 0.5, 0.7 - r0 * 0.3, 1.0) * bright
* (1.4 - dist) * (1.0 / speed);
}
}
// God rays subtils depuis le centre
float ang = atan2(p.y, p.x);
float rays = 0.5 + 0.5 * sin(ang * 8.0 + U.time);
col += float3(0.3, 0.4, 0.7) * rays * (1.0 - length(p)) * 0.15
* (0.5 + U.kp_norm);
return col + U.flare * float3(1.0, 0.5, 0.2) * 0.4;
}
// ---- 7 bars : 3D pillars en perspective ----
float3 mode_bars(float2 p, constant SceneUniforms& U) {
// Pseudo-3D : barres "horizontales" qui s'eloignent
int nbars = 24;
float t = U.time * 0.4;
float3 col = float3(0);
// Sky gradient
float3 sky = mix(float3(0.05, 0.0, 0.15), float3(0.25, 0.1, 0.35),
p.y * 0.5 + 0.5);
col = sky;
for (int i = 0; i < nbars; ++i) {
float fi = float(i) / float(nbars);
// Position en profondeur (z = 0 proche, 1 loin)
float z = fract(fi + t * (0.15 + U.rms * 0.3));
float perspective = 1.0 / (z + 0.1);
float y_base = -0.6 + z * 1.2; // ligne d'horizon
// Hauteur barre depend du bin "i" via hash + RMS
float h0 = hash21(float2(float(i), 0.0));
float h = sin(t * (0.5 + h0 * 4.0) + float(i)) * 0.5 + 0.5;
// body_height (0..~1.8 m) + eye_open avg modulate bar height
float eyes = (U.eye_open_l + U.eye_open_r) * 0.5;
h = h * (0.3 + U.rms * 1.5 + U.social_rate * 0.4
+ U.body_height * 0.6 + eyes * 0.8);
h = clamp(h, 0.02, 0.95);
float bar_top = y_base + h * perspective * 0.3;
// Largeur = 1 / nbars perspective
float bx = (fi - 0.5) * perspective * 1.5;
float bw = 0.5 / float(nbars) * perspective;
if (abs(p.x - bx) < bw &&
p.y > y_base && p.y < bar_top) {
float3 c = palIQ(fi,
float3(0.5), float3(0.5),
float3(1.0, 1.0, 0.5),
float3(0.0, 0.33, 0.67));
// Fog selon z
c *= 1.0 - z * 0.6;
col = mix(col, c, 1.0 - z * 0.3);
}
}
// Grille du sol scanline
float floor_y = -0.6;
if (p.y < floor_y) {
float depth = (floor_y - p.y) * 4.0;
float grid = step(0.95, fract(p.x * 8.0 / max(depth, 0.1)));
grid += step(0.95, fract(depth * 4.0 + t));
col += float3(0.2, 0.3, 0.6) * grid * 0.4;
}
return col + U.flare * float3(1.0, 0.5, 0.2) * 0.3;
}
// ---- 8 hands3d : voyage 3D pilote par les mains ----
float map_hands(float3 p, constant SceneUniforms& U) {
float3 q = fmod(p + 2.0, 4.0) - 2.0;
float d = length(q) - 0.6;
float pulse = 0.8 + U.rms * 0.6;
d = min(d, length(p) - pulse);
d += sin(p.x * 2.0 + U.time) * 0.15 * U.kp_norm;
return d;
}
float3 mode_hands3d(float2 p, constant SceneUniforms& U) {
float hl_active = (abs(U.hand_l_x) + abs(U.hand_l_y)) > 0.01 ? 1.0 : 0.0;
float hr_active = (abs(U.hand_r_x) + abs(U.hand_r_y)) > 0.01 ? 1.0 : 0.0;
float3 cam_pos = float3(
U.hand_l_x * 5.0,
U.hand_l_y * 3.0,
-U.time * (1.5 + U.hand_l_y * 4.0 * hl_active)
);
float yaw = U.hand_r_x * 1.2 * hr_active;
float pitch = -U.hand_r_y * 0.8 * hr_active;
float3 rd = normalize(float3(p.x, p.y, 1.5));
rd = rotX(rd, pitch);
rd = rotY(rd, yaw);
float t = 0.0, glow = 0.0;
for (int i = 0; i < 64; ++i) {
float3 pos = cam_pos + rd * t;
float d = map_hands(pos, U);
if (d < 0.005) break;
glow += 0.02 / (1.0 + d * d * 8.0);
t += d * 0.85;
if (t > 30.0) break;
}
float3 col = float3(0);
if (t < 30.0) {
float3 pos = cam_pos + rd * t;
float fog = 1.0 - saturate(t / 30.0);
col = float3(
0.5 + 0.5 * sin(pos.x * 0.4 + U.time),
0.5 + 0.5 * sin(pos.y * 0.5 + U.time * 1.3),
0.5 + 0.5 * sin(pos.z * 0.3 + U.time * 0.7)
) * fog;
}
col += float3(0.2, 0.6, 1.0) * glow * 1.5;
col += float3(1.0, 0.5, 0.0) * U.flare * 0.8;
return col;
}
// ---- 9 openpos : fond minimal radial pour faire ressortir le squelette ----
// Le rendu des joints + bones se fait par le skel_pipeline rendu PAR-DESSUS
// (cf renderer.py). On laisse juste un degrade radial sombre pour le contraste.
float3 mode_openpos(float2 p, constant SceneUniforms& U) {
float r = length(p);
// Centre legerement plus clair, bords sombres. Touche de couleur
// chaude au centre selon rms pour reagir a la musique.
float3 inner = float3(0.05, 0.05, 0.10) + float3(0.30, 0.12, 0.18) * U.rms;
float3 outer = float3(0.01, 0.01, 0.02);
float3 col = mix(inner, outer, smoothstep(0.0, 1.4, r));
// Grille de points discrete pour donner une ref de profondeur
float2 g = fmod(p * 12.0, 2.0) - 1.0;
float dot_grid = exp(-dot(g, g) * 6.0) * 0.04;
col += float3(dot_grid);
// Pulsation legere sur le kick / drop
col *= 1.0 + U.rms * 0.4;
return col;
}
// ===== Fragment dispatcher =========================================
fragment float4 bg_fragment(VsOut in [[stage_in]],
constant SceneUniforms& U [[buffer(0)]]) {
float2 uv = in.uv;
float2 p = uv * 2.0 - 1.0;
p.x *= U.width / U.height;
int mode = int(U.viz_mode + 0.5);
float3 color;
if (mode == 1) color = mode_tunnel(p, U);
else if (mode == 2) color = mode_plasma(p, U);
else if (mode == 3) color = mode_kaleido(p, U);
else if (mode == 4) color = mode_voronoi(p, U);
else if (mode == 5) color = mode_metaballs(p, U);
else if (mode == 6) color = mode_starfield(p, U);
else if (mode == 7) color = mode_bars(p, U);
else if (mode == 8) color = mode_hands3d(p, U);
else if (mode == 9) color = mode_openpos(p, U);
else color = mode_storm(p, U);
// Flash global + vignette
color += float3(U.lightning_flash * 1.2);
color *= vignette(p);
// Tone mapping doux (Reinhard)
color = color / (1.0 + color);
// Gamma
color = pow(color, float3(0.85));
// Alpha pour transparence quand pose active (webcam visible dessous)
// Overlay vidéo : translucide même sans pose (la webcam doit rester
// visible en fond). Pose active = encore plus translucide.
float alpha = mix(0.55, 0.25, U.pose_alive);
alpha = max(alpha, U.lightning_flash * 0.8);
alpha = max(alpha, U.flare * 0.6);
return float4(color, alpha);
}
// ===== Skeleton overlay ============================================
struct SkelIn {
float3 pos [[attribute(0)]]; // x,y dans NDC, z profondeur (~ -0.5..+0.5)
float conf [[attribute(1)]];
float pid [[attribute(2)]]; // person_id (0..9)
};
struct SkelOut {
float4 position [[position]];
float conf;
float pid;
float depth;
};
// Projection perspective douce : eloigne avec z, garde NDC en x,y
vertex SkelOut skel_vertex(SkelIn in [[stage_in]],
constant SceneUniforms& U [[buffer(1)]]) {
SkelOut o;
float z = clamp(in.pos.z, -1.0, 1.0);
// Perspective : plus z augmente, plus le point est loin → scale < 1
// RMS pulse fait respirer la profondeur
float pulse = 1.0 + U.rms * 0.25;
float persp = 1.0 / (1.0 + z * 0.8);
float2 xy = in.pos.xy * persp * pulse;
o.position = float4(xy, 0.0, 1.0);
o.conf = in.conf;
o.pid = in.pid;
o.depth = z;
return o;
}
// Palette 6 couleurs par personne (turquoise, magenta, jaune, ambre, lilas, vert)
constant float3 PERSON_COLORS[6] = {
float3(0.0, 1.0, 0.85), // 0 turquoise
float3(1.0, 0.3, 0.7), // 1 magenta
float3(1.0, 0.9, 0.2), // 2 jaune
float3(1.0, 0.55, 0.1), // 3 ambre
float3(0.7, 0.5, 1.0), // 4 lilas
float3(0.4, 1.0, 0.3), // 5+ vert (mains)
};
// ===== Mesh overlay (triangles face/hand/body) =====================
// Reuse meme layout que skel : pos.xyz + conf + pid.
vertex SkelOut mesh_vertex(SkelIn in [[stage_in]],
constant SceneUniforms& U [[buffer(1)]]) {
SkelOut o;
float z = clamp(in.pos.z, -1.0, 1.0);
float pulse = 1.0 + U.rms * 0.25;
float persp = 1.0 / (1.0 + z * 0.8);
float2 xy = in.pos.xy * persp * pulse;
o.position = float4(xy, 0.0, 1.0);
o.conf = in.conf;
o.pid = in.pid;
o.depth = z;
return o;
}
fragment float4 mesh_fragment(SkelOut in [[stage_in]]) {
int pid = int(in.pid + 0.5);
pid = ((pid % 6) + 6) % 6;
float3 col = PERSON_COLORS[pid];
float c = saturate(in.conf);
// Saturation boost : couleurs vives quand pose detectee
col = mix(col, col * 1.6, c);
// Fog par profondeur (proche = plus lumineux)
float depth_fog = 1.0 - clamp(in.depth + 0.5, 0.0, 1.0) * 0.5;
col *= depth_fog;
// Alpha TRES VISIBLE quand confiance haute : 0.85 sur skin, 0.3 fade
return float4(col, mix(0.3, 0.85, c));
}
fragment float4 skel_fragment(SkelOut in [[stage_in]]) {
// Skeleton ULTRA visible quand pose detectee : couleur vive + opaque
int pid = int(in.pid + 0.5);
pid = ((pid % 6) + 6) % 6; // modulo positif
float3 col = PERSON_COLORS[pid] * 1.4; // saturation boost
float c = saturate(in.conf);
// Depth fog : eclaircit ce qui est proche, eteint ce qui est loin
float depth_fog = 1.0 - clamp(in.depth + 0.5, 0.0, 1.0) * 0.6;
col *= depth_fog * (0.5 + 0.5 * c);
// Alpha plein-opaque quand confiance haute (= squelette ultra net)
return float4(col, mix(0.5, 1.0, c));
}