Animations
Activities people perform, sex-animation tags, and the rig contract for animation files.
Activities: .ulactivity
What Actor:animate("smoke") plays is an activity, defined in Animations/*.ulactivity (JSON). An activity names an animation, and optionally props held in a hand or the piece of furniture it is done on.
json{
"write": {
"anim": "/Game/Heavy/New3/Anims/Daily/write_{G}",
"props": [
{
"mesh": "/Game/Heavy/New3/Anims/Daily/Props/write_prop_1",
"socket": "rHand",
"transform": { "location": [-4, 9.4, 4.7], "rotation": [0, 0, 0], "scale": [1, 1, 1] }
}
]
},
"na_sleep": {
"anim": "/Game/Heavy/New2/Daily/fnt_bed01-{g}-0-lie",
"furniture": "/Game/Heavy/New2/Furniture/fnt_bed01",
"use": ["bed_bed", "mattress", "futon", "bunk"],
"use_exclude": ["bdsm", "gurney", "bedside"]
},
"sprint": { "alias": "jog" }
}
| Field | Meaning |
|---|---|
anim |
The animation: a game asset (/Game/...) or a file of your mod, relative to its Animations/ folder (Dance/salsa_{G}.fbx). {G} becomes F or M, {g} becomes f or m, by the person's body |
props |
Meshes attached to a bone (socket), with an offset: a game asset or a .glb/.fbx relative to Animations/ (Props/guitar.glb, or Props/kit.glb#Guitar for one object of a file), or a generated piece under a mod's Rooms/ folder (Rooms/Sets/GenCore/Pieces/GenCore/Product_Mug_Diner.glb) |
furniture |
The piece the animation was made on (a bed, a treadmill): a mesh of your mod (.glb/.fbx, as for props), or the name of an original-game piece (/Game/Heavy/New2/Furniture/fnt_bed01) whose box is read from fnt_bounds.json (below). With use it only lines the animation up on the room's own piece; without use your mod's mesh is spawned under the person, wherever they stand (the original pieces are not in the game and are never spawned) |
use |
Words looked for in the names of the room's pieces (lower case, like fnt_uses.json below). The person walks to the nearest free match and the animation is played on it. An action whose ANIMATION has use is not offered in a room with no matching piece |
use_exclude |
Words that rule a piece out ("urinal" for toilet) |
alias |
Play another activity instead |
Another mod's .ulactivity adds ids or replaces them by id. The base game defines 54: smoke, dance, groupdance, drinkbottle, drinkglass, drinktea, eat, call, browse, camera, write, makeup, sweep, weights, yoga, stretch, na_running, jog, swim, sunbathe, pray, meditate, sofawatch, theatrewatch, socialize, takeorder, sell, bbq, work, walk, and more (see ul_Base/Animations/activities.ulactivity).
Outdoors, use an activity without a piece. An action whose activity has use is refused wherever the room has no matching piece, and a park or a beach has no treadmill, yoga mat or pew. When the action can happen anywhere, point it at an activity with neither furniture nor use: the base game has jog (the treadmill clip, running in place), stretch (the yoga clip without the mat) and meditate (the prayer clip without the pew) beside na_running, yoga and pray, which keep their piece. An action offered both in a gym and in a park is two actions, one per kind of place (do_cardio and go_for_a_run).
A mod's own activity:
json{
"ul_salsa": {
"anim": "Dance/salsa_{G}.fbx",
"props": [ { "mesh": "Props/rose.glb", "socket": "rHand" } ]
}
}
with Animations/Dance/salsa_F.fbx, salsa_M.fbx and Animations/Props/rose.glb in the mod. A later mod shipping the same relative path replaces the file.
Room life: ambient.json
People in a venue are never left frozen. When a room fills, each person either takes a free seat, lies on a free bed, plays a "daily", or stands in their idle. Then every few seconds one free person makes a move: walking somewhere else, going over to talk to someone, sitting down, or changing activity.
- The dailies are the
ANIMATIONof the actions available at that venue. A mod's new action with anANIMATIONtherefore shows up as background life where the action is offered. Activities that bring their ownfurnitureare never used this way. - Seats are any piece whose mesh or node name says it is a seat (
chair,stool,sofa,bench…), plus theFntSitChairslots assemblies declare. Beds are theFntSexBedslots. - The activities used are
sit,lie,socialize,idle_mandidle_ffromactivities.ulactivity, which any mod can replace. - Someone sitting sometimes plays a "seated daily" (
call,browse) on the upper body only: the legs keep the seat pose, and the daily's clip and props drive the spine fromseated_split_boneup. - Some strangers work at the venue, as in the original game: the first one always, each later one 30% of the time. Staff never sit or chat. They write (
write), walk over to a customer and take an order (takeorder, food venues only), or serve (sell). Named characters are never made staff.
Animations/ambient.json tunes all of this. A later mod's file overrides the fields it sets:
json{
"daily_chance": 0.7,
"sit_chance": 0.5,
"lie_chance": 0.0,
"step_seconds": [3, 7],
"moves": { "walk": 3, "talk": 2, "sit": 2, "daily": 3, "idle": 1 },
"exclude": ["knockedout", "gun", "strip"],
"venues": { "hospital": { "lie_chance": 0.4 }, "nightclub": { "daily_chance": 0.9 } },
"seated_daily_chance": 0.4,
"seated_dailies": ["call", "browse"],
"seated_split_bone": "abdomenUpper",
"staff": {
"first_chance": 1.1, "chance": 0.3, "age": [18, 64], "first_age": [20, 50], "none_at": ["home", "residential", "toilets"],
"write_chance": 0.33, "order_chance": 0.5, "order_distance": 250,
"order_venues": ["restaurant", "bar", "cafe"],
"write": "write", "order": "takeorder", "sell": "sell"
}
}
Nobody in a room plays an animation someone else there is already playing, as long as another one is free: seat clips, standing idles, lean loops and dailies are all drawn from what is left. The more clips a mod adds to these folders, the more varied a crowded room looks.
| Field | Meaning |
|---|---|
daily_chance |
Share of people who play a daily rather than idle (0.7, as in the original game) |
sit_chance / lie_chance |
Share who take a free seat / bed on entry |
step_seconds |
Time between two moves, min and max |
moves |
Weights of each move when a person acts |
exclude |
Activity ids never played as background life |
venues |
Per venue tag: sit_chance, lie_chance and daily_chance for that venue only |
seated_daily_chance / seated_dailies |
Share of seated people who play one of these activities on the upper body (0.4, call/browse, as in the original game) |
sit_poses |
Which seat clips may be used: folder, match ({g} = m/f), reference (the clip seat directions are tuned for) and tolerance (cm). Each sitter gets a random clip whose pelvis height suits the seat, from every clip that faces the same way as the reference . extra: more folders of seat clips, each { folder, match, reference, seat } where seat is the seat height in cm its own reference clip was made for; the base game adds the IdleAnimPack's seated loops (sitting, on the phone, dozing) this way |
stand_idles |
folder, match and chance: standing loops played instead of the legacy idle_m/idle_f that often (base game: the IdleAnimPack's upright idles, 0.6) |
lean |
folder, match, wall_gap (cm from the wall to the person's root), yaw (the clips' facing against the actor's, 180 for the IdleAnimPack) and chance (share of standing people who lean on entry). The lean move in moves sends someone to lean too. The wall is the room's real wall, never a doorway; a spot with furniture in front of it is skipped. Base game: the IdleAnimPack's AS_StandLean_* loops |
seated_split_bone |
The bone from which the seated daily takes over (G8 abdomenUpper) |
staff.first_chance / staff.chance |
Chance that the first stranger / each later stranger works here (1.1 and 0.3, as in the original game; above 1 means always) |
staff.age |
[min, max]: only strangers of these ages work (default 18-64, so no minors and few pensioners serve) |
staff.first_age |
[min, max]: when the first stranger's age is outside staff.age and first_chance is 1 or more, they are given an age in this range, so a venue always has someone working (default 20-50) |
staff.none_at |
Venue tags with no staff |
staff.write_chance, order_chance, order_distance, order_venues |
What staff do: write a third of the time; at an order venue, half the rest walk to a customer, stop order_distance cm away and take an order; otherwise serve |
staff.write / order / sell |
The activity ids played for each |
Furniture clips: fnt_uses.json
The original game shipped animations made for one piece of furniture each: looking at a shop shelf, using a fridge, running on a treadmill, a cashier at the till. Animations/fnt_uses.json says which of our pieces each one is played on:
json{ "uses": [
{ "fnt": "fnt_new067", "kind": "use", "match": ["fridge", "refrigerator"], "exclude": ["mini_fridge"] },
{ "fnt": "fnt_new039", "kind": "work", "match": ["cash_register"], "staff": true }
] }
| Field | Meaning |
|---|---|
fnt / kind |
The clip `Content/Heavy/New2/Daily/ |
match / exclude |
Words looked for in the piece's names (actor, mesh and the pack's node name, lower case) |
staff |
Played only by the people who work at the venue |
The original furniture <fnt> is the template: it is turned so the person's side faces our piece's free side, and moved so the two front faces meet. People walk over to a matching piece now and then ("use" in moves). A later mod's entry with the same fnt and kind replaces the earlier one.
The original pieces themselves are not in the game; only their size is. Animations/fnt_bounds.json gives each one's box in its own space, in cm: { "fnt_bed01": [minX, minY, minZ, maxX, maxY, maxZ] }. A mod that adds a clip for a piece not listed there adds its box to its own fnt_bounds.json; a later mod's entry for the same name replaces the earlier one.
Sex-animation tags: .ulanimtags
Sex animations are chosen by tags. .ulanimtags files (anywhere in a mod) tag the animations by base name, one per line:
textDDF_DazCow=Anal|Cowgirl|Doggy|Penetration|Vaginal
An animation for N participants is N role clips (Name_0, Name_1…), one per person. Each role is given to a participant by its role letter: M (has a penis), F (does not), S (a woman's body with a penis), or a creature's symbol from its .ulcreature (D dog, H horse, T tentacles). The letter comes from the skeleton the role's clip was made for, else from the name's prefix (DFF_DazBJ is dog, woman, woman). A role with no letter can go to any person, never to a creature, so a scene with a creature only plays animations that name its letter.
Which skeleton stands for which letter is declared in an .ulanimtags file too:
text@skeleton Dog8_Skeleton=D
@skeleton horse2_Skeleton=H
When the skeleton cannot tell the roles apart (three clips made for the same female skeleton), spell the whole clip out, one letter per role in order:
text@roles tff_01=SFF
A position's name in the pose browser and in a partner's proposal comes from an @name line (else the id is shown):
text@name MF_ms07=Missionary Anal
ul_Base/Anims/SexAnimNames.ulanimtags holds the legacy names.
@addtags <id>=<Tag>|<Tag> adds tags to a position wherever it is defined (a .ulanimtags base name, or the motion of a motions.ulanimcat pair), so a mod can tag positions it did not make:
@addtags Missionary100_03_Inserted_loop_G8F_94ac95=CanBeUnconscious
CanBeUnconscious marks a position a body can stay in while passed out (on the back, on the front, on the side, being licked). When someone passes out elsewhere, the scene moves to the marked position sharing most tags with the current one, and while they are out only marked positions can be picked. ul_Base/Anims/SexAnimUnconscious.ulanimtags holds the base list.
Where a position goes in the room comes from an @place line: @place <id>=<Kind> [<cm>] [role<N>] [wall].
text@place MF_CowgirlFast=Ground 50
@place MF_DeskJob=Raised 70 role0
@place MF_LapKiss=Seat 55 role1
@place MF_WallLift=Stand wall
| Kind | Meaning | <cm> |
|---|---|---|
Ground |
Lying, kneeling, on all fours. Goes on the biggest bed, else on the floor. | Height of the ground the clip was made on (0 = floor; Daz couples are often made on a ~50 cm bed; negative = sunk into it). |
Stand |
Someone is on their feet. Floor only, never a bed. | Same as Ground. |
Seat |
Role N sits on something (chair, sofa, bed's side); the partner stays on the floor. |
Seat height in the clip. A seat whose top is within 15 cm is chosen. |
Raised |
Role N lies or leans on a surface (desk, table, bed's edge); the partner stands on the floor. |
Surface height in the clip, matched the same way. |
wall puts the floor position against a free wall, the leaning role's back to it. A desk or a counter counts as a seat for clips made at 65 cm or more (perched on its edge).
Every place offers every height: Rooms/SceneHeights.json lists the heights positions are made on and the pieces that give each one. When a scene starts in a room with nothing within 15 cm of one of them, the first listed piece that fits is added at a free spot, facing the room, and stays there. Only when no listed piece fits do Seat and Raised fall back to the floor.
json{ "heights": [
{ "kind": "Seat", "top": 75, "pieces": [ { "set": "GenCore", "assembly": "G_Seating_Stool_Bar_Chrome" } ] },
{ "kind": "Raised", "top": 70, "pieces": [ { "set": "GenCore", "assembly": "G_Desk_Desk_Office", "top": 74 } ] }
] }
kind is Seat (sat on) or Raised (lain on: at least 100 × 50 cm). top is the height served. A piece's own height is its assembly's surface in assemblies.ulset (the top of its seat, mattress or table top, in cm), or the top given on the piece here. set/assembly are as in the set's assemblies.ulset. Every placed piece with a surface is measured by it, so give one to your furniture: without it a seat counts as 45 cm and a bed as 55. A later mod adds pieces to a height it repeats, or new heights.
A position with no @place is placed as Ground 0. The official places are in ul_Motions/Animations/Motions/SexAnimPlaces.ulanimtags and ul_Base/Anims/SexAnimPlaces.ulanimtags; a mod's line for the same id replaces them.
@roles wins over the skeleton and the name. A person with a penis fills M as well as S, so a trans woman can also take a man's role; S is only ever filled by a woman's body.
Tags are what Game:Filter, Game:AnimatePair and the narration banks' Tag(...) test. The most used ones: Penetration, Vaginal, Oral, Anal, Foreplay, Blowjob, Toys, Finish, Cowgirl, ReverseCowgirl, Missionary, Doggy, ProneBone, Standing, Kissing, Fingering, Handjob, Cunnilingus, Rimming, Deepthroat, Solo, NonSexual, Furniture, Aggressive, AggressiveFM.
A mod's own sex animations
A line whose name matches no game animation looks for the role clips as files instead: Name_0.fbx, Name_1.fbx… under the Animations/ folder of any enabled mod. The name may carry a subfolder.
text# Animations/Sex/MF_LotusSit_0.fbx (the man), Animations/Sex/MF_LotusSit_1.fbx (the woman)
Sex/MF_LotusSit=Penetration|Vaginal|Sitting
@roles MF_LotusSit=MF
The animation's id is the file name without its role number (MF_LotusSit), what Game:playSexAnimation takes. The roles come from @roles, else from the name's prefix when it spells one letter per role (MF_). Each clip keeps its root motion: it places its role relative to the others, so export every role of a position from the same scene, with the figures where they belong.
Props held during an animation: .ultoys
A dildo in a hand, a vibrator, a camera: .ultoys files (JSON, anywhere in a mod, // comment lines allowed) attach a static mesh to a bone of one role for as long as the animation plays. Keyed by animation id:
json{
"MF_dildo01": [
{
"participant": 1,
"mesh": "/Game/Heavy/New3/Anims/Toys/dildo.dildo",
"bone": "rHand",
"location": [-4.73, 6.94, 1.55],
"rotation": [-0.31, 0.72, 0.145, 0.604],
"scale": [0.5, 0.5, 0.5]
}
]
}
participant: the role index (0plays_0).bone: the bone of that body the prop follows.location,rotation(a quaternion, x y z w) andscale: relative to the bone.
The prop keeps its own size when the body has a height: only its offset follows the hand. A later mod replaces an animation's whole list, and an empty list removes the props. ul_Base/Anims/SexToys.ultoys holds the legacy game's 15 entries, values unchanged.
Pace, pleasure, resistance and pain: EnergyOrgasm.json, LikesTags.json
Both live in a mod's Anims/ folder; a later mod's file wins.
EnergyOrgasm.json—rowsof{ "tags": [...], "energy": [p0, p1], "orgasm": [p0, p1] }, plusstep_seconds(default 0.6: how often a sex scene updates energy and pleasure; the player's Sex scenes: pace setting divides it) andlead_every_seconds(default 12: how often a partner considers leading a new pose). Every step the first row whose tags overlap the playing animation sets how fast each of the two participants tires and how much the pace and their liking turn into pleasure (0.5 for other group sizes).EnergyOrgasm.jsonresistance— how much each person fights the scene, 0 to 100, shown as a bar on their card. In a non-consensual scene (Game:Filter("Aggressive")or"AggressiveFM", thenSex(aggressor, victim)) everyone after the first actor starts at 100 and stays there; every climax takesclimax_drop(3) off, so it never falls much. A willing partner starts at 0 and gainspace_riseper unit of pace error beyondpace_tolerance(too slow counts onlyslow_factorof it) pluspose_riseper point of pose like underdislike_at, and losescalmper step when pace and pose both suit them. If it reaches 100 they no longer consent: from then on the scene is non-consensual (forced faces and talk, the other one leads, the pregnancy record and witnesses count it as rape). The player's own resistance only moves when they are forced. At 100 resistance, arousal growsarousal_damp(30 %) slower.EnergyOrgasm.jsonpain— 0 to 100, always starts at 0. It rises only while resistance is aboveresistance_at(60), byrise× how far resistance is past it × the pace error beyondpace_tolerance(too slow:slow_factor), and eases off bycalmper step otherwise. Neither value moves during the dressed wait (IdleSex). At 100 the person passes out: the scene goes on, but they stop gaining arousal, talking and leading, and wear theunconsciousface fromClimax.json. The scene moves to aCanBeUnconsciousposition (see@addtagsabove). Out cold, pain drops byunconscious_calm(1) per step while nobody goes faster thanpace_toleranceallows (slowing down works); at 0 they come to.Climax.jsonfaces.painis the face shown while pain is high.Climax.jsonoptions[].motion— what the body does at that ending, in any position."press": at the next beat (the moment the hips meet) the hips hold, deepest in, forpress_seconds(3), with a spasm every 0.75 s."pullout": the hips draw backpull_cm(14) along the line between the two hips over 0.6 s, the option'sfluidonly flies once out, and they come back after 3.5 s. Nomotion: the clip plays on and the fluid flies at once.ul_Basegives Inside and In the condompress, Pull outpullout.SceneTransitions.json— how a change of position looks. Nothing is cut: every body glides from the pose it is leaving into the new clip, for any pair of positions, with no transition clips. Hands, feet and knees resting on the bed, the floor or the partner stay put (IK) while the body moves; the ones whose support changes step over one at a time (lift, carry, plant), and the glide lasts longer for each step. Joints stay withinJointLimits.json.blend_seconds(0.9): the glide when the couple stays where it is. When the new position is placed a little further away (up toglide_max_cm, 100), the glide also takes the time to walk there atwalk_cm_per_second(120). Further than that (from the bed to a wall), from one kind of@placeto another (lying to standing), or when a body rolls over (on her back to on all fours), there is no glide: it is a cut behind a dip to black offade_seconds(0.35). A later mod replaces the fields it names.SceneSounds.json— the scene's sounds (the cues themselves are in an.ulaudiofile,ul_BaseshipsAudio/Sex.ulaudio).female_voices/male_voices: the voices a person can get, each aSex.Moan.<voice>.<level>cue family; a person keeps the same one from scene to scene.levels:{ "below": 35, "level": "mild" }entries, lowest first: the arousal (0-100) under which each level plays.moan_every_seconds:[2.5, 5], the gap between moans at low arousal, about halved near climax.thrust(Sex.Thrust) plays at each beat in a position with one ofthrust_tags(Penetration);thrust_by_tagreplaces it for a tag, first match in file order (DeepthroatbeforeBlowjob).mouth_busy_tags: the second role does not moan.min_stroke_cm(1.5): a smaller sway is not a beat. A later mod replaces the fields it names.- The scene's Softer / Harder (scene arc) scale every move of the position about its own mean pose, for any clip: no slower or rougher copy of a clip is needed. Speed is the separate Slower / Faster.
SceneFluids.json— what someone can let go of during a scene, listed in the scene arc under Fluids:actsof{ "id": "pee", "label": "Pee", "fluid": "Urine", "area": "Belly", "strength": 1.0, "npc_chance": 0.0 }.fluidis a type from a fluids mod'sFluids/fluids.json(nothing happens without one); a penis aims atareaon the partner.npc_chanceis the chance per scene step that an NPC does it unasked (0 inul_Base). A later mod's entry with the sameidreplaces it. Itsinsiderules cover letting go while in the partner:{ "tags": ["Vaginal", "Anal"], "part": "Vagina", "area": "Crotch", "leak_fluid": "UrineLeak", "delay": 2.0, "strength": 1.0, "sound": "" }. When the position has one oftags, nothing flies: afterdelayseconds it runs back out of the partner'spart(VaginaorMouth) asleak_fluid,areais marked wet, andsound(an audio cue, e.g.Sex.Gag) plays. The position does not change."steer": truelets the player aim the stream with the mouse while it flows (the point under the cursor, on her); how far it reaches depends on the position, since the body only raises and turns the penis so much."hit_sounds": { "Face": "Sex.Gag" }plays a cue when the stream lands on that area of the partner from outside, at most every 2.5 s.- Lua:
Actor:getSexResistance(),Actor:getSexPain(),Actor:isUnconscious(). Line conditions in the sex text banks can readA1:resistanceandA1:painlikeA1:paceandA1:like. LikesTags.json—likes: which animation tags eachlikes_stat covers ("likes_doggy": ["Doggy", "ProneBone"]), andbondageSlots: the clothing slots that count as bondage forlikes_bondage. A partner likes an animation by the mean of the matching stats.
Restraints that hold the body: .ulrestraint
<mod>/Bodies/*.ulrestraint (JSON) makes worn restraints pose the body over whatever animation plays, as the legacy game did with handcuffs and leg cuffs. While anything is worn in one of slots, every bone from bones down takes its rotation from pose (looped):
json{
"restraints": [
{ "name": "arms", "slots": ["Handcuffs"], "bones": ["lCollar", "rCollar"],
"pose": "/Game/Heavy/New2/Anims2/MF_layBJ10_1.MF_layBJ10_1" }
]
}
A later mod replaces an entry with the same name. Only rotations are taken, so the body keeps its own proportions and height.
In a sex scene, a gag (Gag slot) rules out Blowjob animations and a chastity belt (Chastity) rules out Vaginal and Anal ones. Items whose id starts with Bdg_ are offered as restraints in the scene's Restraints panel.
The rig contract
Every animation in Underlives plays on the Genesis 8 skeleton and is matched by bone name, not retargeted. Animation files from mods (activities and sex animations) follow this contract:
- Exported from Daz Studio as FBX, on a Genesis 8 Female or Male figure.
- Bone names unchanged (
hip,pelvis,lThighBend,rHand…). - One animation per file; the first take is used.
A clip keeps the proportions of whoever plays it: bones take their rotation from the file and their length from the body playing the clip, so an animation exported on the female figure plays cleanly on a man and the reverse. Only the hips' movement (a bob, a thrust) is carried over, from the player's own hip height. Morphs in the file are ignored.
Preparing an export
Daz cannot export an animation without its figure. Tools/AnimStrip/strip_fbx_mesh.py (in the game repository) removes the mesh, skin, morphs and materials, collapses the curves of bones that never move, and leaves the motion byte for byte; a 48 MB export typically becomes 2 MB.
textpython Tools/AnimStrip/strip_fbx_mesh.py MyIdle.fbx # in place, MyIdle.fbx.bak kept
python Tools/AnimStrip/strip_fbx_mesh.py MyPose.fbx --order MF # two people: MyPose_0.fbx (man), MyPose_1.fbx
Daz writes the bones' rest position in one of two ways depending on the export options (in each bone's translation, or apart as a rotation offset with the translation holding only the motion); both are read correctly, so either export works.
A Daz scene with several people exported as one file is split into one file per figure, in the Name_0, Name_1 order a sex animation's roles use, each keeping its place relative to the others.
Previewing
The animation test level starts on a position, both bodies undressed:
textUnrealEditor.exe "<project>.uproject" "/Game/Maps/RoomTest_Generic?game=/Script/Underlives.SexAnimTestGameMode" -game -SexAnim=MF_MyPose -windowed
-SexAnimShot takes a screenshot to Saved/SexAnimTest/ and quits; -SexAnimHeight=0.6,0 gives the participants a height; -SexAnimView=yaw,pitch,distance and -SexAnimAimHips place the camera on the couple.
After re-exporting, replace the files in the mod and click Reload files: no restart. If a position does not play, the log says why (a role file that does not load, or participants that do not fit its role letters).
Fixing a clip: .ulanim over .fbx
A .ulanim file with the same path and name as an .fbx plays instead of it: Motions/couple.sex/MyPose_2M.ulanim replaces Motions/couple.sex/MyPose_2M.fbx, wherever the FBX is used (a position's role, an activity's anim). The FBX is not touched; delete the .ulanim to bring it back. A mod can ship a .ulanim under the path of another mod's clip to correct it.
The posing editor writes these files. In its Animations tab, the IN THE GAME list holds every motion of the game's catalogs (motions.ulanimcat). A click opens every role of the motion on a character of the right sex, in their places. Fix it with keys and IK, then To Game writes one .ulanim per role beside its FBX. Limits in the viewport bar shows (and, when on, saves) the joint limits a scene puts on the clip. The Motion Browser's In the game button does the same for a clip shown from its in-game rows, after moving a body by hand. Both work in a packaged game.
A .ulanim is the posing editor's text format: keyed bone tracks at 30 frames per second. Only the hip moves; every other bone only turns, with its length taken from the body playing it. A couple's places are carried by each role's hip, relative to one shared spot.
Joint limits
In a scene, every clip is held inside the joint ranges of ul_Base/Anims/JointLimits.json (degrees from the rest pose: twist about the bone, swing_a and swing_b across it, and a hinge axis for knees, elbows and fingers). clip_margin_deg gives the clip's own pose that much extra room; what IK adds (a hand on a partner, a foot held through a change of position) never gets it. The ...Twist bones of the arms and thighs only turn about themselves. A mod's file replaces the joints it names.
Body height in sex animations
Clips are authored for two bodies of standard Genesis 8 proportions. When a participant has a height, the game keeps each pelvis where the clip puts it, so the two still meet, and pulls hands and feet by IK onto the spot of the partner's body they were touching in the clip (a hand on a hip follows that hip down a short body). A foot planted on the floor or bed keeps its place. Nothing changes when nobody in the scene has a height. A very short person in a standing position may end up on tiptoe: the pelvis wins.
Clothing and hair follow the same skeleton: see Clothing.