Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Suggestion / Idea  ECS Physics/Graphics Support
#11
(07-05-2026, 05:42 PM)Jawsarn Wrote: I have tested this, and it does not fix the issue just out of the box. The positions will still be interplated from "Interpolate Position From Physics" system, and then there will be one or multiple physics steps next frame that cause inaccurate impulses by translation deltas.

If both simulations (ecs rigidbodies and obi) have the same timestep, they should tick in sync and interpolation *should* not have any effect.

If having the exact same timestep for both simulations doesn’t change anything, my guess is interpolation/timesteps have nothing to do with the issue.

(07-05-2026, 05:42 PM)Jawsarn Wrote: Yes, but this out of the box for me uses my interpolated positions. I could try to set the positions to fixed ones temporarily before LateUpdate to update them right before rendering, but it's a bit of a hack and not really fool proof. Using this mode without my setup, does it not cause inaccuracies in impulses from world inertia as well?

I’m quite unsure about interpolation being the culprit at this point. I’d need to see/debug your setup to accurately help with it.

(07-05-2026, 05:42 PM)Jawsarn Wrote: Additional question to that; does rigidbody interpolation happen before Obi Updates, i.e. are positions based on rigidbody interpolation or the fixed position if one would use those?

Obi always reads rigidbody.position for rigidbodies involved in the simulation. However, parenting a solver to a rigidbody generally only makes sense if the rigidbody is kinematic, and in that case there’s no interpolation. Otherwise momentum conservation goes out of the window (the rigidbody affects the solver and in turn the solver affects the rigibody, a “pull yourself up by the hair” kind of situation).

(07-05-2026, 05:42 PM)Jawsarn Wrote: Somewhat, yes. I can of course modify it all (probably how I will proceed to keep it as package and add a patch on top), but would be nice if the asset provided more customization so that could be avoided.

There should be no need to modify anything, disabling the solver and then manually calling both methods should do the trick. Haven’t tested this though. Will do asap and get back to you.
Reply
#12
(07-05-2026, 06:36 PM)josemendez Wrote: If both simulations (ecs rigidbodies and obi) have the same timestep, they should tick in sync and interpolation *should* not have any effect.

If having the exact same timestep for both simulations doesn’t change anything, my guess is interpolation/timesteps have nothing to do with the issue.
It is my understanding that it should, see image of profiler for player steps.
I think it slightly improves it, but it does not remove the jittery impulses. (Enabling interpolation on solver does not improve it). See video.
https://drive.google.com/file/d/1UtywXEa...sp=sharing

   

   

Best regards
Reply
#13
(07-05-2026, 07:29 PM)Jawsarn Wrote: It is my understanding that it should, see image of profiler for player steps.

Interpolation will of course still work, and physics state (positions/rotations) will still be interpolated from the last two physics updates when rendering a frame during which physics haven't been updated at all.

However since the timestep value is the same for both engines, Obi will read rigidbody positions/orientations during frames where a rigidbody physics update is guaranteed to have taken place. As a result, interpolated and fixed values will be the same.

(07-05-2026, 07:29 PM)Jawsarn Wrote: I think it slightly improves it, but it does not remove the jittery impulses. (Enabling interpolation on solver does not improve it). See video.
https://drive.google.com/file/d/1UtywXEa...sp=sharing

I've asked you for permission to see the video, as soon as you accept my request I'll take a look.

Regarding manually updating the solver, all you need to do is disable the solver and then manually call StartSimulation(stepDelta, numSteps) and Render(accumulatedTime). Here's an example that advances the simulation 0.03 seconds every frame you press "space":

Code:
using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiSolver))]
public class ManualSolverUpdate : MonoBehaviour
{
    ObiSolver solver;

    void Start()
    {
        solver = GetComponent<ObiSolver>();
        solver.enabled = false;
    }

    void Update()
    {

        solver.Render(0);

        if (Input.GetKey(KeyCode.Space))
        {
            solver.StartSimulation(0.01f, 1);
            solver.StartSimulation(0.01f, 1);
            solver.StartSimulation(0.01f, 1);
        }

    }
}

Keep in mind that simulation is multithreaded: StartSimulation() merely kicks off the simulation and the main thread continues execution. Calling StartSimulation() again or calling Render() will force the main thread to wait for the current simulation in flight to finish before continuing.

Because of this, it's best to call Render() as late as possible in the frame: in fact, when using asynchronous mode it's called at the *start of the next frame* to allow as much time as possible to pass between StartSimulation() and Render() and minimize the chances of stalling the main thread. I'm doing the same in the example above, but calling Render() immediately after StartSimulation would be equally valid.

Also notice the second parameter of StartSimulation(), "numSteps". This basically enqueues multiple simulation steps without forcing the main thread to wait between them, so calling StartSimulation(0.01,3) is the same as calling StartSimulation(0.01,1) 3 times -except that the main thread isn't stalled in between steps. You can't always do this, but it's a handy thing to have if you want to coalesce multiple steps.

For reference, here's how you would implement a fixed time stepping scheme using the above methods:

Code:
using UnityEngine;
using Obi;

[RequireComponent(typeof(ObiSolver))]
public class ManualSolverUpdate : MonoBehaviour
{
    ObiSolver solver;
    public float fixedTimestep = 0.02f;

    void Start()
    {
        solver = GetComponent<ObiSolver>();
        solver.enabled = false;
    }

    float accumulator;

    void Update()
    {
        accumulator += Time.deltaTime;

        while (accumulator > 0)
        {
            accumulator -= fixedTimestep;
            solver.StartSimulation(fixedTimestep, 1);
        }

        solver.Render(accumulator);
    }
}

This way of using the engine is undocumented, so let me know if you encounter any issues or have any questions.
Reply
#14
My bad. You should have access now.

And thanks for the confirmation of running it manually should work like that.

Best regards
Reply
#15
Just saw the video, it's hard to tell why the slight jittering. I can't really think of a cause off the top of my head: if both engines are simulated in sync, and Obi is picking up the solver position during LateUpdate() (after ecs physics has updated the position of the rigidbody to which the solver is parented) there should be no jitter and no delay.

I'm willing to take a closer look and debug this for you, but I'd need access to a project that reproduces the issue - even if it's a stripped down version of yours. It's ok if you can't share anything, but in that case I'm not sure where to look next.

best regards,
Reply
#16
(08-05-2026, 09:02 AM)josemendez Wrote: I can't really think of a cause off the top of my head: if both engines are simulated in sync, and Obi is picking up the solver position during LateUpdate() (after ecs physics has updated the position of the rigidbody to which the solver is parented) there should be no jitter and no delay.

Maybe there's a bit of miscommunication here. The setup is Entity { ECS CharacterController/kinematic rigidbody } -> GameObject {ObiSolver}. I'm not using GO Rigidbody. In "sync" is also vaguely true, only that they have same deltatime, ECS Client does some clamps so we're within 5% of tick rate etc, apart from other possible rounding errors.

I'll give the manual update a go in upcomming week.

Best regards,
Reply
#17
(08-05-2026, 09:17 AM)Jawsarn Wrote: Maybe there's a bit of miscommunication here. The setup is Entity { ECS CharacterController/kinematic rigidbody } -> GameObject {ObiSolver}. I'm not using GO Rigidbody.

I was using "rigidbody" to refer to the gameObject the solver is attached/parented to, that is in turn moved by an interpolated ECS rigidbody. I'm not an ECS expert, so it may be different from what I'm used to - but I'm assuming interpolation in ECS works the same way it does for GO rigidbodies. The important thing is that the solver's position is interpolated from the results of a simulation run by a different engine.

(08-05-2026, 09:17 AM)Jawsarn Wrote: In "sync" is also vaguely true, only that they have same deltatime, ECS Client does some clamps so we're within 5% of tick rate etc, apart from other possible rounding errors.

Yes, we're discussing high-level ideas. The devil is usually in the details, that's why I asked for a project that I can get my hands on to debug.

(08-05-2026, 09:17 AM)Jawsarn Wrote: I'll give the manual update a go in upcomming week.

Sure, let me know how it goes.

kind regards
Reply
#18
I only got back to this the last hour; but it seems to work doing it manual mode with inspiration of what you said. (Also posting it for any future ECSer)

Code:
public struct ObiRuntimeData : IComponentData
{
    public NetworkTick lastFixedTick;
    public double elapsedTime;
    public float deltaTime;
}

[BurstCompile]
[UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup), OrderFirst = true)]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial struct RecordLastPredictedFixedTickForObiSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        SystemAPI.TryGetSingleton<NetworkTime>(out var networkTime);
        if (!networkTime.IsFinalFullPredictionTick)
        {
            return;
        }
       
        ref var obiRuntimeData = ref SystemAPI.GetSingletonRW<ObiRuntimeData>().ValueRW;
        obiRuntimeData.lastFixedTick = networkTime.ServerTick;
        obiRuntimeData.elapsedTime = SystemAPI.Time.ElapsedTime;
        obiRuntimeData.deltaTime = SystemAPI.Time.DeltaTime;
    }
}

[UpdateInGroup(typeof(SimulationSystemGroup), OrderLast = true)] // Seems ObiBones write back in LateUpdate, so can't have in PresentationGroup
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial struct ObiManualUpdateSystem : ISystem
{
    private NetworkTick _lastFixedTick;
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<ClientServerTickRate>();
        state.EntityManager.CreateSingleton<ObiRuntimeData>();
    }

    public void OnUpdate(ref SystemState state)
    {
        var system = state.World.GetExistingSystemManaged<GhostPresentationGameObjectSystem>();
       
        SystemAPI.TryGetSingleton<ObiRuntimeData>(out var obiRuntimeData);
        var timeAhead = (float)(SystemAPI.Time.ElapsedTime - obiRuntimeData.elapsedTime);
        var timeStep = (float)obiRuntimeData.deltaTime;
        if (timeAhead < 0f || timeStep == 0f)
        {
            return;
        }
       
        // Assumption we do 1:1 physics ticks with tick rate
        var steps = _lastFixedTick.IsValid ? obiRuntimeData.lastFixedTick.TicksSince(_lastFixedTick) : 1;
        _lastFixedTick = obiRuntimeData.lastFixedTick;
       
        foreach (var (_, localTransformRef, entity) in SystemAPI.Query<RefRW<ObiSolverData>, RefRO<LocalTransform>>().WithEntityAccess())
        {
            var gameObject = system.GetGameObjectForEntity(state.EntityManager, entity);
            var obiSolver = gameObject?.GetComponent<ObiSolver>();
            if (obiSolver == null)
            {
                continue;
            }
#if UNITY_EDITOR
            if (obiSolver.isActiveAndEnabled)
            {
                Debug.LogError($"ObiSolver {gameObject.name} is enabled but running in manual mode. Please disable the component to avoid unexpected behavior.", gameObject);
            }
#endif
           
            // Use localTransform for fixed updated position compared to transform (which is updated from localToWorld)
            var interpolatedLocalPosition = gameObject.transform.localPosition;
            var interpolatedLocalRotation = gameObject.transform.localRotation;
           
            gameObject.transform.localPosition = localTransformRef.ValueRO.Position;
            gameObject.transform.localRotation = localTransformRef.ValueRO.Rotation;
           
            if (steps > 0)
            {
                obiSolver.StartSimulation(timeStep, steps);
            }
            obiSolver.Render(timeAhead);
           
            gameObject.transform.localPosition = interpolatedLocalPosition;
            gameObject.transform.localRotation = interpolatedLocalRotation;
        }
    }
}

I think the only thing now that I need to modify the asset now for is to add a setting to be able to scale the inertia wind when set in World mode, which might be a nice setting to have in general. For reference; the code below `ApplyInertialForcesJob`.

Code:
            if (inertialWind)
            {
                float4 wsPos = inertialFrame.frame.TransformPoint(positions[i]);
                wind[i] -= inertialFrame.frame.InverseTransformVector(inertialFrame.VelocityAtPoint(wsPos));
            }
Reply
#19
Alright. So I realized that my previous statement on only needing "to scale the inertia wind when set in World mode" was wrong, as I need more control on the wind inertia forces. So this is my current hacked togeather solution;

Updated the manual update system to separate velocities 


Code:
public void OnUpdate(ref SystemState state)
{
    var system = state.World.GetExistingSystemManaged<GhostPresentationGameObjectSystem>();
   
    SystemAPI.TryGetSingleton<ObiRuntimeData>(out var obiRuntimeData);
    var timeAhead = (float)(SystemAPI.Time.ElapsedTime - obiRuntimeData.elapsedTime);
    var timeStep = (float)obiRuntimeData.deltaTime;
    if (timeAhead < 0f || timeStep == 0f)
    {
        return;
    }
   
    // Assumption we do 1:1 physics ticks with tick rate
    var steps = _lastFixedTick.IsValid ? obiRuntimeData.lastFixedTick.TicksSince(_lastFixedTick) : 1;
    _lastFixedTick = obiRuntimeData.lastFixedTick;
   
    foreach (var (obiSolverSettingsDataRef, obiSolverSimulateHistoryDataRef, localTransformRef,
                 parentRelativeTransformDataRef, parentRelativeTransformCacheDataRef, entity)
             in SystemAPI.Query<RefRO<ObiSolverSettingsData>, RefRW<ObiSolverSimulateHistoryData>,
                 RefRO<LocalTransform>, RefRO<ParentRelativeTransformData>,
                 RefRO<ParentRelativeTransformCacheData>>().WithEntityAccess())
    {
        var gameObject = system.GetGameObjectForEntity(state.EntityManager, entity);
        var obiSolver = gameObject?.GetComponent<ObiSolver>();
        if (obiSolver == null)
        {
            continue;
        }
#if UNITY_EDITOR
        if (obiSolver.isActiveAndEnabled)
        {
            Debug.LogError($"ObiSolver {gameObject.name} is enabled but running in manual mode. Please disable the component to avoid unexpected behavior.", gameObject);
        }
#endif
       
        // Use localTransform for fixed updated position compared to transform (which is updated from localToWorld)
        var interpolatedLocalPosition = gameObject.transform.localPosition;
        var interpolatedLocalRotation = gameObject.transform.localRotation;

        var worldPosition = new float4(localTransformRef.ValueRO.Position, 0);
        var worldRotation = localTransformRef.ValueRO.Rotation;
        var localPosition =  new float4(parentRelativeTransformDataRef.ValueRO.position, 0);
        var localRotation = parentRelativeTransformDataRef.ValueRO.rotation;
        var parentEntity = parentRelativeTransformDataRef.ValueRO.parentEntity;
        var parentTransform = parentRelativeTransformCacheDataRef.ValueRO.parentLocalTransform;
       
        gameObject.transform.localPosition = worldPosition.xyz;
        gameObject.transform.localRotation = worldRotation;

        ref var obiSolverData = ref obiSolverSimulateHistoryDataRef.ValueRW;
        if (!obiSolverData.initialized || obiSolverData.previousParent != parentEntity)
        {
            obiSolverData.initialized = true;
            obiSolverData.previousPositionLocal = localPosition;
            obiSolverData.previousRotationLocal = localRotation;
            obiSolverData.previousPosition = worldPosition;
            obiSolverData.previousRotation = worldRotation;
            obiSolverData.previousParent = parentEntity;
        }
       
        if (steps > 0)
        {
            obiSolver.Initialize();
           
            var dt = timeStep * steps;
           
            var velocityFromLocal = BurstIntegration.DifferentiateLinear(localPosition, obiSolverData.previousPositionLocal, dt);
            velocityFromLocal = new float4(parentTransform.TransformDirection(velocityFromLocal.xyz), 0);
            var totalVelocity = BurstIntegration.DifferentiateLinear(worldPosition, obiSolverData.previousPosition, dt);
            var velocityFromParent = totalVelocity - velocityFromLocal;
   
            var angularVelocityFromLocal = BurstIntegration.DifferentiateAngular(parentTransform.TransformRotation(localRotation), parentTransform.TransformRotation(obiSolverData.previousRotationLocal), dt);
            var angularVelocityTotal = BurstIntegration.DifferentiateAngular(worldRotation, obiSolverData.previousRotation, dt);
            var angularVelocityFromParent = angularVelocityTotal - angularVelocityFromLocal;
           
            obiSolverData.previousPositionLocal = localPosition;
            obiSolverData.previousRotationLocal = localRotation;
            obiSolverData.previousPosition = worldPosition;
            obiSolverData.previousRotation = worldRotation;
               
            var inputData = new BurstInertialFrameAdditionalInputData()
            {
                velocityFromLocal = velocityFromLocal,
                velocityFromParent = velocityFromParent,
                angularVelocityFromLocal = angularVelocityFromLocal,
                angularVelocityFromParent = angularVelocityFromParent,
                inertiaScaleFromLocal = obiSolverSettingsDataRef.ValueRO.inertiaScaleFromLocal,
                inertiaScaleFromParent = obiSolverSettingsDataRef.ValueRO.inertiaScaleFromParent,
            };
     
            obiSolver.implementation.SetAdditionalData(inputData,dt);
            obiSolver.StartSimulation(timeStep, steps);
        }
        obiSolver.Render(timeAhead);
       
        gameObject.transform.localPosition = interpolatedLocalPosition;
        gameObject.transform.localRotation = interpolatedLocalRotation;
    }
}

Added data and utility methods separate from Obi code to more easily upgrade.

Code:
namespace Obi
{
    public struct BurstInertialFrameAdditionalInputData
    {
        public float4 velocityFromLocal;
        public float4 velocityFromParent;
       
        public float4 angularVelocityFromLocal;
        public float4 angularVelocityFromParent;
       
        public float inertiaScaleFromLocal;
        public float inertiaScaleFromParent;
    }
   
    public struct BurstInertialFrameAdditionalData
    {
        public BurstInertialFrameAdditionalInputData inputData;
       
        public float4 accelerationFromLocal;
        public float4 accelerationFromParent;
       
        public float4 angularAccelerationFromLocal;
        public float4 angularAccelerationFromParent;
    }

    public struct ApplyInertialForcesJobAdditionalData
    {
        public float4 localAngularVel;
        public float4 localInertialAccel;
        public float4 localEulerAccel;
       
        public float4 parentAngularVel;
        public float4 parentInertialAccel;
        public float4 parentEulerAccel;
    }
   
    public class InertialForcesUtility
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void UpdateAdditionalData(ref BurstInertialFrameAdditionalData additionalData, in BurstInertialFrameAdditionalInputData inputData, float dt)
        {
            var prevLocalVelocity = additionalData.inputData.velocityFromLocal;
            var prevParentVelocity = additionalData.inputData.velocityFromParent;
            var prevLocalAngularVelocity = additionalData.inputData.angularVelocityFromLocal;
            var prevParentAngularVelocity = additionalData.inputData.angularVelocityFromParent;

            additionalData.accelerationFromLocal = BurstIntegration.DifferentiateLinear(inputData.velocityFromLocal, prevLocalVelocity, dt);
            additionalData.accelerationFromParent = BurstIntegration.DifferentiateLinear(inputData.velocityFromParent, prevParentVelocity, dt);

            additionalData.angularAccelerationFromLocal = BurstIntegration.DifferentiateLinear(inputData.angularVelocityFromLocal, prevLocalAngularVelocity, dt);
            additionalData.angularAccelerationFromParent = BurstIntegration.DifferentiateLinear(inputData.angularVelocityFromParent, prevParentAngularVelocity, dt);
            additionalData.inputData = inputData;
        }
       
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static ApplyInertialForcesJobAdditionalData CreateApplyInertiaForcesAdditionalData(in BurstInertialFrame inertialFrame,
            in float4x4 linear, in float4x4 linearInv)
        {
            return new ApplyInertialForcesJobAdditionalData()
            {
                localAngularVel = math.mul(linearInv, math.mul(float4x4.Scale(inertialFrame.additionalData.inputData.angularVelocityFromLocal.xyz), linear)).diagonal(),
                localEulerAccel = math.mul(linearInv, math.mul(float4x4.Scale(inertialFrame.additionalData.angularAccelerationFromLocal.xyz), linear)).diagonal(),
                localInertialAccel = math.mul(linearInv, inertialFrame.additionalData.accelerationFromLocal),
                parentAngularVel = math.mul(linearInv, math.mul(float4x4.Scale(inertialFrame.additionalData.inputData.angularVelocityFromParent.xyz), linear)).diagonal(),
                parentEulerAccel = math.mul(linearInv, math.mul(float4x4.Scale(inertialFrame.additionalData.angularAccelerationFromParent.xyz), linear)).diagonal(),
                parentInertialAccel = math.mul(linearInv, inertialFrame.additionalData.accelerationFromParent),
            };
        }
       
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static float4 CalculateWorldInertialForces(in ApplyInertialForcesJobAdditionalData additionalData,
            in BurstInertialFrame inertialFrame, float4 position, float4 velocity, float worldLinearInertiaScale,
            float worldAngularInertiaScale, float deltaTime)
        {
            var fromLocalForce = CalculateInertialForce(additionalData.localEulerAccel, additionalData.localAngularVel,
                velocity, additionalData.localInertialAccel, position, worldLinearInertiaScale, worldAngularInertiaScale,
                inertialFrame.additionalData.inputData.inertiaScaleFromLocal, deltaTime);
           
            var fromParentForce = CalculateInertialForce(additionalData.parentEulerAccel, additionalData.parentAngularVel,
                velocity, additionalData.parentInertialAccel, position, worldLinearInertiaScale, worldAngularInertiaScale,
                inertialFrame.additionalData.inputData.inertiaScaleFromParent, deltaTime);
                       
            return fromLocalForce + fromParentForce;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static float4 CalculateInertialForce(float4 eulerAccel, float4 angularVel, float4 velocity,
            float4 inertialAccel, float4 position, float worldLinearInertiaScale, float worldAngularInertiaScale,
            float inertiaScale, float deltaTime)
        {
            float4 euler = new float4(math.cross(eulerAccel.xyz, position.xyz), 0);
            float4 centrifugal = new float4(math.cross(angularVel.xyz, math.cross(angularVel.xyz, position.xyz)), 0);
            float4 coriolis = 2 * new float4(math.cross(angularVel.xyz, velocity.xyz), 0);
            float4 angularAccel = euler + coriolis + centrifugal;
           
            return (inertialAccel * worldLinearInertiaScale + angularAccel * worldAngularInertiaScale) * inertiaScale * deltaTime;
        }
       
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static float4 CalculateInertialWindForces(in BurstInertialFrame inertialFrame, float4 position)
        {
            float4 wsPos = inertialFrame.frame.TransformPoint(position);
           
            var worldInertialWindForceFromLocal = VelocityAtPoint(wsPos, inertialFrame.frame.translation,
                inertialFrame.additionalData.inputData.velocityFromLocal, inertialFrame.additionalData.inputData.angularVelocityFromLocal);
            var worldInertialWindForceFromParent = VelocityAtPoint(wsPos, inertialFrame.frame.translation,
                inertialFrame.additionalData.inputData.velocityFromParent, inertialFrame.additionalData.inputData.angularVelocityFromParent);
           
            var totalWorldInertialWindForce = worldInertialWindForceFromLocal * inertialFrame.additionalData.inputData.inertiaScaleFromLocal +
                                              worldInertialWindForceFromParent * inertialFrame.additionalData.inputData.inertiaScaleFromParent;
            return inertialFrame.frame.InverseTransformVector(totalWorldInertialWindForce);
        }
       
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static float4 VelocityAtPoint(float4 worldSpacePoint, float4 prevFrameOrigin, float4 velocity, float4 angularVelocity)
        {
            return velocity + new float4(math.cross(angularVelocity.xyz, (worldSpacePoint - prevFrameOrigin).xyz), 0);
        }
    }
}

Then some smaller things;
Add `BurstInertialFrameAdditionalData` as member to `BurstInertialFrame` and `ApplyInertialForcesJobAdditionalData` to `ApplyInertialForcesJob`.
Replaced `ApplyInertialForcesJob`s velocity and wind calculate with utilities from above.
Added `SetAdditionalData` to the ISolverImpl etc (only supporting bursted for now).
In `ApplyFrame` create and add `ApplyInertialForcesJobAdditionalData`.

With this I can handle the forces separatley and use t
wo different factors `inertiaScaleFromLocal` and `inertiaScaleFromLocal` to control how much it should affect. (I need this because the parent can move very fast, and might want to modify it based on if you're in shade of the air in the velocity direction etc).

Now this all works fine.

One thing maybe you can explain to me is why you are using prevFrame.translation for VelocityAtPoint and not the current one (I changed it to that)?
Consider a case where the point is in front of the solver in direction of movement, you move with a velocity which in turn moves the solver twice the distance of the point to the solver + the solver rotates. Now the point is in the new frame world space - but you take the vector from previous solver world pos, the vector will be pointing in the opposite direction than its relative point to the solver. After crossing this would now result in a velocity in the opposite the direction compared if the solver was standing still?

Also if you have any thoughts of a neater way to integrate custom logic like this into obi.

Cheers
Reply
#20
Hi,

If I understood your code correctly, you're calculating velocity from the solver's world space positions in the previous/current frame (totalVelocity), then calculating its velocity in local space (velocityFromLocal) and subtracting them to get velocity due to the parent's movement (velocityFromParent).

Unless I'm missing something, this means that in addition to moving due to the parent-child relationship between the solver and an object driven by an ECS rigidbody, the solver is also moving in that object's local space and you need to scale inertial forces due to that as well? Not sure anymore what you are trying to do, but if it works for you it works!

(16-05-2026, 07:46 PM)Jawsarn Wrote: One thing maybe you can explain to me is why you are using prevFrame.translation for VelocityAtPoint and not the current one (I changed it to that)?

The solver's linear velocity is transform.position - prevTransform.position. To be consistent we have to calculate angular velocity using backward finite differences as well, and that means using an r vector that's also expressed in the previous frame's space.
Reply