Add buildable C engine, shaders and music source

This commit is contained in:
CRTkafa 2026-09-05 11:16:54 +07:00
parent 1d805b8896
commit 18721711c4
44 changed files with 23427 additions and 3 deletions

294
fx/mesh.hlsl Normal file
View file

@ -0,0 +1,294 @@
// Meshes, pulled rather than fed.
//
// Nothing is bound to the input assembler - no vertex buffer, no index
// buffer, no input layout. The vertex shader reads straight out of two typed
// buffers laid over the `const short` arrays in cats_mesh.h, and the draw is
// Draw(NT*3, 0) with the index fetched here.
//
// That is not stylistic. Three things fall out of it:
//
// There is no DXGI_FORMAT_R16G16B16_SINT, so an input layout would have to
// pad the 6-byte vertices to 8 - about 19 KB on the run cat alone, in an
// executable where the shader blobs are already the biggest thing.
//
// A typed R16_SINT buffer has a 2-byte stride, so the baked arrays upload
// verbatim. pSysMem points at the header array: no copy, no allocation,
// nothing a C runtime would have to be present for.
//
// Drawing non-indexed means the triangle id is just vid/3, so a per-triangle
// flat colour needs no vertex duplication, and the barycentric is
// float3(vid%3 == 0, 1, 2) - which is a wireframe-to-solid morph for free.
//
// The cost is one dependent load per vertex on a 790-triangle mesh.
//
// ---------------------------------------------------------------------------
// W H Y T H E R E I S A R I G D O W N T H E R E
//
// The 24 baked frames are the whole of the cat's motion, and the rate they
// play at is not free: actRate in the shot table is tuned so a stride covers
// about two body lengths of the from -> to travel, because anything else makes
// the feet skate. Run the numbers on bar 35 - 10.33 units of travel over 32
// rows at 0.63 frames a row - and the cat gets through FOUR FIFTHS of one
// stride in the entire shot. Bar 59 gets through half of one. That is the
// honest reason the cat reads as furniture: not that the animation is bad, but
// that at a skate-free rate there is barely any of it inside a two-bar shot.
//
// The rate cannot go up without the feet sliding. So everything below is
// SECONDARY motion - the parts of an animal that are not carrying its weight
// and are therefore free to move as fast as they like. A tail that lashes, a
// head that turns, ears that flick, a spine that flexes, a ribcage that
// breathes, and a body that sinks into its knees when the kick lands. The legs
// are deliberately left alone: they are doing the one job the baked frames
// already do well, and anything added to a planted paw is a skate.
//
// The rig weights are plain thresholds in the mesh's own baked integer space,
// which is why fetchVert no longer applies gAnim.z - the quantiser scale is
// applied once, at the end, and until then a number in this file can be read
// straight against cats_mesh.h. Measured off the array: the cat faces +X, the
// animation is entirely in XY (z is constant per vertex to within a unit or
// two, which is what a sagittal run cycle looks like), the tail runs from
// x=-9800 out to x=-15200, the paws bottom out around y=-6400, and the head is
// everything past x=+6600.
//
// THE THRESHOLDS CANNOT BE TAKEN OFF THE ANIMATED POSE. A paw swings 8500
// units of x during a stride - a front paw reaches x=+8800, which is deeper
// into head territory than the head is, and a rear paw reaches x=-11600, which
// is halfway down the tail. Segment on that and the rig grabs a foot. So the
// weights are read off `rf`: the midpoint of the current frame and the one
// half a cycle away. A pair of legs is in antiphase, so the midpoint very
// nearly cancels the swing - it drops the worst per-vertex x wander over the
// cycle from 15387 units to 3169, and the median from 2643 to 599 - and what
// is left is a bind pose in all but name. Three extra loads, and after it the
// paws never once cross a threshold: they stay inside x = [-9740, +6515] while
// the tail sits below -9770 and the head above +6680.
//
// Four appearances, all ACT_CAT_RUN in the corridor, and gTune.z happens to
// track the energy of each one exactly: 0.20 at bar 59 (the sparse solo
// entry, the slowest gait in the demo), 0.30 at bar 35, 0.50 at bar 43, 0.60
// at bar 77 (the tension phase, the fastest). So gTune.z is read here as
// AGITATION and it drives every amplitude in the rig. The bar 59 cat prowls;
// the bar 77 cat is coming apart. That is one animal with four moods rather
// than the same silhouette pasted in four times.
// ---------------------------------------------------------------------------
cbuffer Scene : register(b0)
{
float4 gTime; // x time, y beat, z shot fade, w aspect
float4 gCam; // xyz eye, w tan(vfov/2)
float4 gDir; // xyz look direction, w roll
float4 gTune;
float4 gSync; // x low, y mid, z high, w master peak
float4 gVoice; // x guitar, y solo, z organ, w hat
};
cbuffer Mesh : register(b1)
{
float4 gModel; // xyz world position, w uniform scale
float4 gAnim; // x fractional frame, y frame count (1 = rigid), z quant scale, w yaw
float4 gMisc; // x vertices per frame, y wireframe 0..1, z shatter, w tint
};
Buffer<int> gPos : register(t0);
Buffer<uint> gIdx : register(t1);
struct VSOut
{
float4 pos : SV_Position;
float3 wp : TEXCOORD0; // world position, for the face normal
float3 bary : TEXCOORD1;
float vz : TEXCOORD2; // view depth, for the fog
};
// Raw baked units. gAnim.z is deliberately NOT applied here - see above.
float3 fetchVert(uint vtx, uint frame)
{
uint o = (frame * (uint)gMisc.x + vtx) * 3u;
return float3(gPos.Load((int)o), gPos.Load((int)(o + 1u)),
gPos.Load((int)(o + 2u)));
}
VSOut main(uint vid : SV_VertexID)
{
VSOut o;
uint vtx = gIdx.Load((int)vid);
// The baked run cycle is 24 frames of plain vertex positions, and the
// frame index is FRACTIONAL because it comes from the song's row clock
// rather than a wall clock - the old demo stepped them against wall-clock
// time and the cat never once landed on the beat.
//
// It used to lerp two frames. At the skate-free rate the cat is only
// getting about 5.6 frames a second, so a straight line between them holds
// each limb at a constant velocity for 180 ms and then turns a corner, and
// the corner is visible - the legs tick. Catmull-Rom over four frames
// costs six more loads on a 396-vertex mesh and makes the velocity
// continuous, which is the difference between a flipbook and an arc. The
// cycle loops, so the neighbours wrap; at frac == 0 it returns frame f1
// exactly, which is what ACT_CAT_STILL depends on.
uint nf = max((uint)gAnim.y, 1u);
float3 p;
if (nf == 1u)
{
// Static props have their own proportions: no cat rig or interpolation.
p = fetchVert(vtx, 0u) * gAnim.z;
}
else
{
uint f1 = (uint)gAnim.x % nf;
uint f0 = (f1 + nf - 1u) % nf;
uint f2 = (f1 + 1u) % nf;
uint f3 = (f1 + 2u) % nf;
float t = frac(gAnim.x);
float3 P0 = fetchVert(vtx, f0), P1 = fetchVert(vtx, f1);
float3 P2 = fetchVert(vtx, f2), P3 = fetchVert(vtx, f3);
float3 g = 0.5 * (2.0 * P1
+ (P2 - P0) * t
+ (2.0 * P0 - 5.0 * P1 + 4.0 * P2 - P3) * t * t
+ (3.0 * (P1 - P2) + P3 - P0) * t * t * t);
// The stand-in bind pose: this frame averaged with its opposite. Nothing
// below reads a threshold off `g`, only off `rf`. nf>>1 is 0 for a
// single-frame mesh, which degenerates to the frame itself, correctly.
float3 rf = 0.5 * (P1 + fetchVert(vtx, (f1 + (nf >> 1)) % nf));
// ---- the rig -------------------------------------------------------
// ag agitation, the one number that separates the four appearances
// sd a phase seed, so two cats never start a sway on the same beat
// st seconds into THIS shot, for the drifts that must not be periodic
// gp gait phase, 2*pi per stride, off the row clock and so beat-locked
// dr whichever of the two loud voices is actually playing
float ag = saturate(gTune.z * 1.6);
float sd = gTune.x * 31.0 + gTune.y * 17.0 + gTune.z * 57.0;
float st = gTime.x;
float gp = 6.2831853 * gAnim.x / (float)nf;
float dr = gVoice.x * 0.45 + gVoice.y * 0.55;
// The three weights the whole rig is built out of, all in bind space.
// tail past the root of it, and thin with it - a tail stays inside
// |z| = 788 where a hind leg reaches 1578, so the second gate
// holds even where the first one is tight
// head starts at 6550, which is 35 units clear of the furthest a paw
// ever reaches, so the head rotation can never catch a foot
// torso how far above the feet: 0 at every paw (they top out at -1510),
// 1 through the spine. This is what keeps the legs out of the
// weave and turns the body's sink into knee compression.
float tail = (1.0 - smoothstep(-14800.0, -9755.0, rf.x))
* (1.0 - smoothstep(900.0, 1400.0, abs(rf.z)));
float head = smoothstep(6550.0, 9200.0, rf.x);
float torso = smoothstep(-1400.0, 900.0, rf.y);
float3 d = float3(0.0, 0.0, 0.0);
// TAIL. A travelling wave with the phase lagging further the further out
// along it you go, which is what makes a tail read as a whip rather than a
// rod, and the lag itself opens up with agitation. Amplitude is
// base + k*voice, never base*voice: a silent bar still has a moving tail.
float lag = tail * (2.2 + 1.7 * ag);
float tp = gp * 2.0 + sd - lag;
float ta = 320.0 + 820.0 * ag + 420.0 * dr;
d.z += tail * ta * sin(tp);
d.y += tail * ta * 0.40 * sin(tp * 0.5 + 1.1);
d.y += tail * (200.0 + 450.0 * ag) * sin(st * 0.55 + sd);
// SPINE. A lateral S about one wavelength along the body, plus an arch
// that peaks between the two leg pairs at x = -1750 - the mid-back is the
// part of a running cat that actually bends.
d.z += torso * (110.0 + 320.0 * ag) * sin(rf.x * 0.00022 + gp * 2.0 + sd);
float mid = 1.0 - saturate(abs(rf.x + 1750.0) / 9000.0);
d.y += torso * mid * (150.0 + 470.0 * ag) * sin(gp * 2.0 + 0.7);
// The kick lands in the knees. The body sinks and the paws do not, because
// `torso` is zero down there, so the legs compress instead of the cat
// teleporting. base + k*s, so muting the drums leaves a deliberate stance.
d.y -= torso * (0.35 + 0.65 * ag) * (90.0 + 520.0 * gSync.x);
// BREATHING. Ribcage only - not the head, not the tail, not the legs. It
// is small on purpose: this is the thing that stops a held frame from
// being a still, and it should reward a second look rather than announce
// itself. Proportional to the vertex's own offset, so it is a scale.
float rib = torso * (1.0 - head) * (1.0 - tail)
* saturate(1.0 - abs(rf.x + 1000.0) / 8000.0);
float br = sin(st * 1.7 + sd * 0.3);
d.z += rib * g.z * (0.030 + 0.045 * gVoice.z) * br;
d.y += rib * g.y * (0.020 + 0.030 * gVoice.z) * br;
// HEAD. Yawed and pitched about the base of the neck, on two slow
// incommensurable periods so it never repeats inside a shot, and lifted by
// the guitar. The rotation is applied to the live pose but blended in by a
// bind-space weight, so the shoulders carry the shear and nothing else in
// the mesh can wander into it.
float hyaw = (0.05 + 0.15 * ag) * sin(st * 0.83 + sd)
+ 0.04 * sin(st * 2.70 + sd * 1.7);
float hpit = (0.04 + 0.10 * ag) * sin(st * 0.61 + sd * 0.5)
+ 0.07 * gVoice.x;
float3 hv = g - float3(6550.0, 500.0, 0.0);
float cy = cos(hyaw), sy = sin(hyaw);
float3 hr = float3(hv.x * cy - hv.z * sy, hv.y, hv.x * sy + hv.z * cy);
float cp = cos(hpit), sp = sin(hpit);
hr = float3(hr.x * cp - hr.y * sp, hr.x * sp + hr.y * cp, hr.z);
d += (hr - hv) * head;
// EARS. The top of the head only, each one on its own phase, flicking on
// the hat. Tiny in world units - the ears are tiny - but they are the
// outermost points of the silhouette and the rim light finds them.
float ear = head * smoothstep(1900.0, 3100.0, rf.y);
float side = rf.z >= 0.0 ? 1.0 : -1.0;
float fk = sin(gp * 6.0 + sd + side * 1.9);
d.z += ear * side * (60.0 + 400.0 * gVoice.w) * fk;
d.y -= ear * (30.0 + 180.0 * gVoice.w) * (0.5 + 0.5 * fk);
// WEAVE. The whole body yaws a few degrees as it runs, pinned at the
// paws again, so head and tail swing opposite ways. This is the one that
// makes it look like it has mass.
float wv = (0.015 + 0.045 * ag) * sin(gp + sd * 0.7)
+ 0.020 * sin(st * 0.47 + sd);
float cw = cos(wv), sw = sin(wv);
d += (float3(g.x * cw - g.z * sw, g.y, g.x * sw + g.z * cw) - g) * torso;
// Out of the baked integer space, once.
p = (g + d) * gAnim.z;
}
// Shatter: push each triangle out along its own pseudo-random axis. The
// seed is the triangle id, so a face stays whole while it flies.
if (nf > 1u && gMisc.z > 0.001)
{
uint tri = vid / 3u;
float s = frac(sin(float(tri) * 12.9898) * 43758.5453);
float s2 = frac(sin(float(tri) * 78.2330) * 24634.6345);
float s3 = frac(sin(float(tri) * 39.4260) * 13875.9284);
p += (float3(s, s2, s3) - 0.5) * gMisc.z * gMisc.z * 14.0;
}
float ca = cos(gAnim.w), sa = sin(gAnim.w);
p = float3(p.x * ca - p.z * sa, p.y, p.x * sa + p.z * ca);
float3 wp = p * gModel.w + gModel.xyz;
// The camera basis, built the same way every scene shader builds it, so
// a mesh and the distance field behind it agree about where things are.
float3 fw = normalize(gDir.xyz);
float3 rt = normalize(cross(float3(0.0, 1.0, 0.0), fw));
float3 up = cross(fw, rt);
float cr = cos(gDir.w), sr = sin(gDir.w);
float3 r2 = rt * cr + up * sr;
float3 u2 = up * cr - rt * sr;
float3 dv = wp - gCam.xyz;
float z = dot(dv, fw);
// Clip space by hand. The scene shaders cast normalize(fw + (uv.x*r2 +
// uv.y*u2) * gCam.w), so a point lands at uv = (x, y) / (z * gCam.w) -
// and w = z with 0.05 in the z slot reproduces the SDF passes' own
// saturate(0.05 / vz), which is what puts them on the same depth scale.
// Keep signed w: hardware clips at z = 0.05 and rejects geometry behind
// the eye instead of projecting it onto a tiny positive denominator.
o.pos = float4(dot(dv, r2) / (gCam.w * gTime.w),
dot(dv, u2) / gCam.w,
0.05, z);
o.wp = wp;
o.vz = z;
uint c = vid % 3u;
o.bary = float3(c == 0u ? 1.0 : 0.0, c == 1u ? 1.0 : 0.0, c == 2u ? 1.0 : 0.0);
return o;
}