Jump to content
The Dark Mod Forums

Recommended Posts

Posted

Darkmod underutilizes, in my opinion, capabilities for procedural animation. We already do some, like the random head and torso turns that are added on top of existing animations. There's also possibility for separate animation for top and bottom of the character as well as head. There's a missing opportunity for varied facial expressions. I talked about animation blending in this thread.

Here's something that for example Thief: Deadly Shadows did. It's swaying side to side while turning. I used Gemini to add code to the tdm_ai_base.script. I don't know if this is the most efficient way of doing it, probably not. Here are the parts added:

Spoiler

// Procedural Banking Variables (it's a term from car racing).
float    m_bankingLastYaw;        // Stores the entity's yaw from the previous frame to calculate turn rate
float    m_bankingCurrentValue;    // The smoothed banking offset currently being applied to joints
float    m_bankingJointHandle;    // Primary joint handle
float    m_bankingJointHandle1;    // Secondary joint handle
float    m_bankingJointHandle2;    // Tertiary joint handle
boolean    m_bankingActive;        // Control flag to manage the lifecycle of the banking thread

void    initBanking();            // Setup function to identify skeletal joints and initialize tracking variables
void    updateBankingLoop();    // Background thread that calculates turn deltas and applies banking offsets with LOD skipping

Spoiler

//Initializes the procedural banking system (it's a term from car racing).
//Acquires skeletal joint handles and launches the background update thread.

void ai_darkmod_base::initBanking() {
    // Acquire handles for the skeletal hierarchy [1]
    // m_bankingJointHandle is mapped to 'Origin' for full-body tilt
    m_bankingJointHandle  = getJointHandle("Origin");
    m_bankingJointHandle1 = getJointHandle("Spine_Dummy");
    m_bankingJointHandle2 = getJointHandle("Neck");

    // Fallback: If 'Spine_Dummy' is not found, attempt to use 'Spine'
    if (m_bankingJointHandle1 == -1) {
        m_bankingJointHandle1 = getJointHandle("Spine");
    }

    // Only launch the thread if at least the primary joint is valid
    if (m_bankingJointHandle!= -1) {
        vector initialAngles = getAngles(); 
        m_bankingLastYaw = initialAngles_y; // Initialize with current Yaw to prevent a delta-spike

        m_bankingCurrentValue = 0.0;
        m_bankingActive = true;

        // Launch banking logic as an independent thread
        thread updateBankingLoop();
    }
}

Spoiler

//Arcturus / Gemini - Main worker thread for procedural swaying (while turning).
//Calculates angular delta, applies smoothing, and distributes rotation across the spine.

void ai_darkmod_base::updateBankingLoop() {
    float  currentYaw;
    float  deltaYaw;
    float  targetBanking;
    vector jointRotation;
    vector currentAngles;
    vector distVec;
    float  dist;

    sys.waitFrame(); // Synchronize with the animation system's first frame [4]

    while (m_bankingActive && getHealth() > 0) {
        // --- LOD (Level of Detail) Check ---
        // Optimization: Disable processing if the AI is far from the player
        distVec = getOrigin() - $player1.getOrigin();
        dist = sys.vecLength(distVec); // Fixed: Use sys.vecLength instead of.getLength() [1, 2]

        if (dist > 1500) {
            sys.wait(1.0); // Sleep for 1 second if distant to save CPU cycles

            // Re-sync Yaw after waking up to avoid a massive jump in deltaYaw
            currentAngles = getAngles();
            m_bankingLastYaw = currentAngles_y;
            continue;
        }

        // --- Turning Logic ---
        currentAngles = getAngles(); 
        currentYaw = currentAngles_y; // Access Yaw component via suffix [1, 5]

        // Shortest path normalization for the 360-degree heading wrap
        deltaYaw = m_bankingLastYaw - currentYaw;
        if (deltaYaw > 180) { deltaYaw -= 360; }
        else if (deltaYaw < -180) { deltaYaw += 360; }

        // targetBanking: Turn delta multiplied by intensity
        targetBanking = deltaYaw * 1.4;

        // Safety Clamp: Prevents excessive skeletal deformation [6]
        if (targetBanking > 20) targetBanking = 20;
        if (targetBanking < -20) targetBanking = -20;

        // Smoothing (EMA): Blend current value with target
        // Note: Sum of weights (0.96 + 0.04) should ideally be 1.0 for full reach
        m_bankingCurrentValue = (m_bankingCurrentValue * 0.95) + (targetBanking * 0.05);

        // --- Joint Transformation ---
        // Offset applied to X axis (Roll) for banking effect
        jointRotation_x = m_bankingCurrentValue;
        jointRotation_y = 0;
        jointRotation_z = 0; 

        // Apply distributed rotation across the spine and neck in Mode 1 (JOINTMOD_LOCAL) [7]
        setJointAngle(m_bankingJointHandle,  1, jointRotation * 1);
        setJointAngle(m_bankingJointHandle1, 1, jointRotation * 0.6);
        setJointAngle(m_bankingJointHandle2, 1, jointRotation * -1.5);

        m_bankingLastYaw = currentYaw;
        sys.waitFrame(); // Synchronize with game frame rate [4, 8]
    }
}

Gemini came up with the term "banking" which is used in car racing. I don't know if that's what it's called in animation or games. There's a lot of variables that can be tweaked. You can rotate this way any bone you wish. I use 'Origin', 'Spine_Dummy' and 'Neck' bones:

You can tweak amount of rotation for each bone.

setJointAngle(m_bankingJointHandle,  1, jointRotation * 1);
setJointAngle(m_bankingJointHandle1, 1, jointRotation * 0.6);
setJointAngle(m_bankingJointHandle2, 1, jointRotation * -1.5);

There's also global intensity:

(targetBanking = deltaYaw * 1.4;)

and attenuation:

m_bankingCurrentValue = (m_bankingCurrentValue * 0.95) + (targetBanking * 0.05);

modifiers.

Gemini also proposed to add an LOD system, which I thought was a great idea. So currently at a distance of 1500 from player the swaying will be disabled.

Here are examples, each with slightly different values:

And here's comically exaggerated one:

 

 

  • Like 4

It's only a model...

Posted

Here's more subtle version. Added slight arm movement outwards. Also added speed modifier, so the faster the NPC moves forward the stronger the tilt:

Example of the speed modifier's effect:

They will never be fully realistic, but at least we can make them less stiff.

 

It's only a model...

Posted (edited)
3 hours ago, Arcturus said:

Here's more subtle version. Added slight arm movement outwards. Also added speed modifier, so the faster the NPC moves forward the stronger the tilt:

They will never be fully realistic, but at least we can make them less stiff.

That test looks great! Not sure what "realistic" animations would add here (are we supposed to imitate mocapped animations by hand?); as long as they add convincing liveliness to these tree trunks acting as humanoids, that will do to spice up the stealth simulation. :)
I hope something can be done to AI visual detection and barks when alerted, too.

Edited by Taffingtaffer
Posted

I don't think we need to use spine. Rotation on the origin bone plus slight arms and head rotation is enough. Added a clamp on arms rotation. Attenuation is slightly faster. They return to straight pose a bit quicker.

Should I make it stronger during walking?

I guess because of the speed multiplier, they no longer sway while rotating in place, which I guess is ok.

 

tdm_ai_base.script

It's only a model...

Posted

Well Met!

Unintentional comedy of the test videos aside, I support this idea in general - I think it does accomplish its stated goal of making the characters look a little less stiff.

I think I prefer the earlier videos that apply the rotation at the spine? Applying the rotation at the origin seems a little bit off if you focus on their feet while they turn+move. Hard to articulate what I'm seeing, but it seems less natural or floatier somehow (to my admittedly untrained eye). Also, I think keeping the effect subtle is important here - having exxagerated movements fits better on cartoon-ier characters, whereas TDM's general house style leans more towards gritty realism.

Posted

Those AIs indeed love to generate lots of code...

I believe this smoothing might be FPS-dependent:

m_bankingCurrentValue = (m_bankingCurrentValue * 0.94) + (targetBanking * 0.06);

If you set com_maxfps 10 or com_maxfps 300 without vsync, does it work the same?

Interestingly, I looked in the source code, and I see that sys.waitFrame still waits 16 ms instead of one frame.
And given that com_maxTicTimestep = 17, I guess you can´t get very different results here.

Also, did you test it on non-human AIs?
I guess this code applies to various spiders and elementals as well...

 

Posted
On 4/11/2026 at 4:35 PM, stgatilov said:

If you set com_maxfps 10 or com_maxfps 300 without vsync, does it work the same?

Gemini managed to come up with a solution that's frame rate independent. However, I discovered a bug in Darkmod (unless it's a known issue).

With vertical sync off, uncap FPS on and high max FPS values (around 100 and above), the character animations get restarted (walking, running) when AI makes a turn. It looks bad and causes NPCs to stop a lot, too:

Beyond that, some additional checks were added, whether AI is dormant and for speed.

It's possible to limit which AIs use this script by adding "can_bank" "1" argument in the def files. It would be better to make separate script files for different types of creatures. A horse especially could use a custom one.

On 4/11/2026 at 7:40 PM, datiswous said:

So you're using "ai" to improve ai.

Those are just visual changes. They don't affect the behavior of AI.

Right now NPCs don't slow down at all when they make a turn, which makes them look like trains on tracks. Click on images to watch the videos:

Zrzutekranu2026-04-12171453.jpg.c308b3eb0d3e2b25901606c0dcea8194.jpg

Zrzutekranu2026-04-12164533.jpg.5104d1d2fd2aeb365b67482b87c75aaf.jpg

Zrzutekranu2026-04-12162346.jpg.697446d629c5ddb12cf4cadfde0644be.jpg

 

It's only a model...

Posted
3 hours ago, Arcturus said:

With vertical sync off, uncap FPS on and high max FPS values (around 100 and above), the character animations get restarted (walking, running) when AI makes a turn.

The bug is more than cosmetic. It affects pathfinding. I noticed in test/horse.map that a horse can't find path nodes with those settings. Turning v-sync on, or lowering FPS fixes it:

 

It's only a model...

Posted

I animated spine in the other direction.

7 hours ago, datiswous said:

I don't think you turn your head while turning

What I mean is that the head bone needs to be animated too, in order to compensate for the body tilt.

tdm_ai_base.script

It's only a model...

Posted

Anything subtle that improves the stiff animations is welcome IMHO. I think this is worth exploring and maybe implementing into the game. It doesn't look truely realistic but it's definitely better than what we have now for human NPCs. 

Also please make sure to report this bug in the bug tracker or it will get lost and forgotten in this thread. 

"Einen giftigen Trank aus Kräutern und Wurzeln für die närrischen Städter wollen wir brauen." - Text aus einem verlassenen Heidenlager

Posted
12 hours ago, SeriousToni said:

Also please make sure to report this bug in the bug tracker or it will get lost and forgotten in this thread. 

Reported.

  • Thanks 1

It's only a model...

Posted

I wonder if it happens on all fps settings above 60fps. Uncapped now defaults to the highest setting of 300 (which I think is stupid), but what if it's set to 100fps for example?

Posted (edited)
On 4/12/2026 at 11:17 PM, Arcturus said:

 

This looks pretty good actually!

I think maybe there should be set a lower maximum bend per step if possible. If you look close you see the guard on the right walk normaly but do a 180 degree turn in one step.

Edited by datiswous
Posted
1 hour ago, datiswous said:

Uncapped now defaults to the highest setting of 300

Seems to work:

I have only 60hz monitor, so I can't really see those FPS above 60.

1 hour ago, datiswous said:

I think maybe there should be set a lower maximum bend per step if possible. If you look close you see the guard on the right walk normaly but do a 180 degree turn in one step.

Animations play in a loop and turning can happen at any point during that cycle, but the speed never changes. Origin bone moves at a constant rate forward. Tilting is modified by speed already, so walking animation has lower tilt and running has bigger. It's also proportional to the turning arc.

What I've been thinking is to try to lower walking / running speed on turns. "anim_rate_run" is set to "0.8" in tdm_ai_humanoid.def which means that running animations are already slowed down. That would however further complicate the code, if it's even possible to do.

It's only a model...

Posted (edited)
57 minutes ago, Arcturus said:

Seems to work:

I don't understand what you are trying to say here. Yes you can set the Max fps setting. I meant that when you install tdm, or a new version, it defaults to the 300fps setting, which is the highest nr for that setting. If you play the game on 300 fps the gpu will do that, the monitor just doesn't follow.

I just wonder if this bug still happens if max fps is set to 100 instead of 300.

Edited by datiswous
Posted

On an unrelated note... Judging from tdm_ai_showfov, a horse sees to its right:

HorseSeesOnRight.thumb.jpg.19c94aeead183eebae8ecb177324efc7.jpg

I think it was not very important, because horses are rarely used and usually don´t react to player's presence in their FOV.
But still it's strange 😃

  • Like 1
Posted (edited)
2 hours ago, stgatilov said:

On an unrelated note... Judging from tdm_ai_showfov, a horse sees to its right:

Funny :)! Can you fix that or is it something connected to the model itself? I believe there are several missions in which horses are set to make loud noises to attract guards, if they do see you which it seems is a 50% chance ;).

Edited by wesp5

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...