From 97e8d1db91b4f7cc558e27a7030c473d19f44072 Mon Sep 17 00:00:00 2001
From: XProger <xproger@list.ru>
Date: Tue, 24 Mar 2020 06:26:12 +0300
Subject: [PATCH] add simple IK solver for legs and arms

---
 src/camera.h                           |   6 +
 src/controller.h                       |   3 +
 src/lara.h                             | 179 +++++++++++++++++++++++--
 src/level.h                            |  26 ++++
 src/platform/win/OpenLara.vcxproj.user |  11 +-
 src/platform/win/main.cpp              |  56 +++++---
 src/utils.h                            | 103 ++++++++++++++
 7 files changed, 348 insertions(+), 36 deletions(-)

diff --git a/src/camera.h b/src/camera.h
index db7700d..10742f9 100644
--- a/src/camera.h
+++ b/src/camera.h
@@ -57,6 +57,7 @@ struct Camera : ICamera {
     int         speed;
     bool        smooth;
 
+    bool        spectatorVR;
     bool        spectator;
     vec3        specPos, specPosSmooth;
     vec3        specRot, specRotSmooth;
@@ -67,6 +68,7 @@ struct Camera : ICamera {
         this->owner = owner;
         reset();
 
+        spectatorVR = false;
         spectator = false;
         specTimer = 0.0f;
     }
@@ -559,6 +561,10 @@ struct Camera : ICamera {
             specTimer = 0.0f;
         }
 
+        if (!spectator && spectatorVR) {
+            mViewInv = mat4(owner->mainLightPos, owner->pos, vec3(0, -1, 0));
+        }
+
         if (spectator) {
             vec2  L = specJoy.L;
             vec2  R = specJoy.R;
diff --git a/src/controller.h b/src/controller.h
index 7b134de..bff84d2 100644
--- a/src/controller.h
+++ b/src/controller.h
@@ -1432,8 +1432,11 @@ struct Controller {
             return;
         animation.getJoints(getMatrix(), -1, true, joints);
         jointsFrame = Core::stats.frame;
+        updateIK();
     }
 
+    virtual void updateIK() {}
+
     Basis& getJoint(int index) {
         updateJoints();
 
diff --git a/src/lara.h b/src/lara.h
index 4df8196..849bf82 100644
--- a/src/lara.h
+++ b/src/lara.h
@@ -44,6 +44,8 @@
 #define LARA_WADE_MAX_DEPTH 730.0f
 #define LARA_SWIM_MIN_DEPTH 512.0f
 
+#define LARA_HEEL_HEIGHT    48.0f
+
 #define LARA_MIN_SPECULAR   0.03f
 #define LARA_WET_SPECULAR   0.5f
 #define LARA_WET_TIMER      (LARA_WET_SPECULAR / 16.0f)   // 4 sec
@@ -79,7 +81,7 @@ struct Lara : Character {
 
         ANIM_STAND              = 11,
 
-        ANIM_LANDING            = 24,
+        ANIM_LANDING_HIGH       = 24,
 
         ANIM_CLIMB_JUMP         = 26,
 
@@ -107,6 +109,8 @@ struct Lara : Character {
 
         ANIM_SLIDE_FORTH        = 70,
 
+        ANIM_LANDING_LOW        = 82,
+
         ANIM_FALL_BACK          = 93,
 
         ANIM_HANG               = 96,
@@ -326,6 +330,8 @@ struct Lara : Character {
 
     bool        dozy;
     bool        canJump;
+    bool        useIK;
+    bool        useIKAim;
 
     int32       networkInput;
 
@@ -642,6 +648,9 @@ struct Lara : Character {
             } else
                 animation.setAnim(ANIM_STAND);
         }
+
+        useIK = true;
+        useIKAim = false;
     }
 
     virtual ~Lara() {
@@ -947,6 +956,27 @@ struct Lara : Character {
                || state == STATE_STEP_LEFT;
     }
 
+    bool canIKLegs() {
+        return (animation.index != ANIM_RUN_ASCEND_LEFT
+             && animation.index != ANIM_RUN_ASCEND_RIGHT
+             && animation.index != ANIM_WALK_ASCEND_LEFT
+             && animation.index != ANIM_WALK_ASCEND_RIGHT
+             && animation.index != ANIM_WALK_DESCEND_RIGHT
+             && animation.index != ANIM_WALK_DESCEND_LEFT
+             && animation.index != ANIM_BACK_DESCEND_LEFT
+             && animation.index != ANIM_BACK_DESCEND_RIGHT)
+             && (state == STATE_WALK
+              || state == STATE_RUN
+              || state == STATE_STOP
+              || state == STATE_FAST_BACK
+              || state == STATE_TURN_RIGHT
+              || state == STATE_TURN_LEFT
+              || state == STATE_BACK
+              || state == STATE_FAST_TURN
+              || state == STATE_STEP_RIGHT
+              || state == STATE_STEP_LEFT);
+    }
+
     bool wpnReady() {
         return arms[0].anim != Weapon::Anim::PREPARE && arms[0].anim != Weapon::Anim::UNHOLSTER && arms[0].anim != Weapon::Anim::HOLSTER;
     }
@@ -1018,6 +1048,11 @@ struct Lara : Character {
     }
 
     void wpnFire() {
+        if (useIKAim) {
+            doShot(Input::joy[0].down[jkA], Input::joy[1].down[jkA]);
+            return;
+        }
+
         bool armShot[2] = { false, false };
         for (int i = 0; i < 2; i++) {
             Arm &arm = arms[i];
@@ -1084,10 +1119,19 @@ struct Lara : Character {
                 game->addMuzzleFlash(this, i ? LARA_LGUN_JOINT : LARA_RGUN_JOINT, i ? LARA_LGUN_OFFSET : LARA_RGUN_OFFSET, 1 + camera->cameraIndex);
 
         // TODO: use new trace code
-            int joint = wpnCurrent == TR::Entity::SHOTGUN ? 8 : (i ? 11 : 8);
-            vec3 p = getJoint(joint).pos;
-            vec3 d = arm->rotAbs * vec3(0, 0, 1);
-            vec3 t = p + d * (24.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - vec3(1.0f)) * 1024.0f;
+            vec3 p, d, t;
+
+            if (useIKAim) {
+                int joint = wpnCurrent == TR::Entity::SHOTGUN ? JOINT_ARM_R3 : (i ? JOINT_ARM_L3 : JOINT_ARM_R3);
+                p = getJoint(joint).pos;
+                d = getJoint(joint).rot * vec3(0, 1, 0);
+                t = p + d * 15.0f * 1024.0f;
+            } else {
+                int joint = wpnCurrent == TR::Entity::SHOTGUN ? JOINT_ARM_R1 : (i ? JOINT_ARM_L1 : JOINT_ARM_R1);
+                p = getJoint(joint).pos;
+                d = arm->rotAbs * vec3(0, 0, 1);
+                t = p + d * (15.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - vec3(1.0f)) * 1024.0f;
+            }
 
             int room;
             vec3 hit = trace(getRoomIndex(), p, t, room, false);
@@ -1140,6 +1184,12 @@ struct Lara : Character {
         }
 
         if (!emptyHands()) {
+
+            if (useIK) {
+                //wpnFire(); 
+                //return;
+            }
+
             bool isRifle = wpnCurrent == TR::Entity::SHOTGUN;
 
             for (int i = 0; i < 2; i++) {
@@ -2397,7 +2447,7 @@ struct Lara : Character {
                     }
 
                     if (state == STATE_FALL && health > 0.0f)
-                        animation.setAnim(ANIM_LANDING);
+                        animation.setAnim(ANIM_LANDING_HIGH);
                 }
             }
             return STAND_GROUND;
@@ -3401,7 +3451,7 @@ struct Lara : Character {
             w *= TURN_FAST;
         else if (state == STATE_FAST_BACK)
             w *= TURN_FAST_BACK;
-        else if (state == STATE_TURN_LEFT || state == STATE_TURN_RIGHT || state == STATE_WALK || (state == STATE_STOP && animation.index == ANIM_LANDING))
+        else if (state == STATE_TURN_LEFT || state == STATE_TURN_RIGHT || state == STATE_WALK || (state == STATE_STOP && animation.index == ANIM_LANDING_HIGH))
             w *= TURN_NORMAL;
         else if (state == STATE_FORWARD_JUMP || state == STATE_BACK || state == STATE_WADE)
             w *= TURN_SLOW;
@@ -3514,7 +3564,7 @@ struct Lara : Character {
             vTilt *= 2.0f;
         vTilt *= rotFactor.y;
         bool VR = (Core::settings.detail.stereo == Core::Settings::STEREO_VR) && camera->firstPerson;
-        updateTilt((input & WALK) == 0 && (state == STATE_RUN || (state == STATE_STOP && animation.index == ANIM_LANDING) || stand == STAND_UNDERWATER) && !VR, vTilt.x, vTilt.y);
+        updateTilt((input & WALK) == 0 && (state == STATE_RUN || (state == STATE_STOP && animation.index == ANIM_LANDING_HIGH) || stand == STAND_UNDERWATER) && !VR, vTilt.x, vTilt.y);
 
         collisionOffset = vec3(0.0f);
 
@@ -3871,6 +3921,119 @@ struct Lara : Character {
             visibleMask ^= 0xFFFFFFFF;
         }
     }
+
+    void solveJointsArm(int j0, int j1, int j2) {
+        int index = j0 == JOINT_ARM_R1 ? 0 : 1;
+
+        Basis &hJoint = getJoint(jointHead);
+        vec3 hPos = hJoint.pos - hJoint.rot * vec3(0, 48, -24);
+
+        vec3 target = Input::hmd.controllers[index].getPos();
+        target += hPos;
+
+        vec3 start  = joints[j0].pos;
+        vec3 middle = joints[j1].pos;
+        vec3 end    = target;
+
+        float length1 = (middle - start).length();
+        float length2 = (joints[j2].pos - middle).length();
+
+        vec3 dir = end - start;
+        float length = dir.length();
+        if (length > length1 + length2) {
+            end = start + dir * ((length1 + length2 - 0.1f) / length);
+        }
+
+        vec3 down = vec3(0.0f, 1.0f, 0.0f);
+        vec3 pole = start + (getDir().cross(down) * (index == 0 ? -1.0f : 1.0f) + down) * 1000.0f;
+
+        if (ikSolve3D(start, end, pole, length1, length2, middle))
+        {
+            joints[j0] = Basis(start, middle, joints[j0].rot * vec3(1.0f, 0.0f, 0.0f));
+            joints[j1] = Basis(middle, end,   joints[j1].rot * vec3(1.0f, 0.0f, 0.0f));
+            joints[j2].pos = end;
+        }
+
+        joints[j2].rot = Input::hmd.controllers[index].getRot();
+    }
+
+    void solveJointsLeg(int j0, int j1, int j2, float footHeight) {
+        vec3 start  = joints[j0].pos;
+        vec3 middle = joints[j1].pos;
+        vec3 end    = joints[j2].pos;
+
+        float length1 = (middle - start).length();
+        float length2 = (end - middle).length();
+
+        if (end.y > footHeight) {
+            end.y = footHeight;
+        }
+
+        vec3 pole = middle + (middle - start + middle - end) * 1024.0f;
+
+        if (ikSolve3D(start, end, pole, length1, length2, middle)) {
+            joints[j0] = Basis(start, middle, joints[j0].rot * vec3(1.0f, 0.0f, 0.0f));
+            joints[j1] = Basis(middle, end,   joints[j1].rot * vec3(1.0f, 0.0f, 0.0f));
+            joints[j2].pos = end;
+        }
+        /*
+        
+        vec3 pole = start + getDir() * 10000.0f;
+
+        if (ikSolve3D(start, end, pole, length1, length2, middle)) {
+            float angle = middle.xy().angle();
+            quat q(vec3(1, 0, 0), PI * 0.5f - angle);
+
+            joints[j0].rot = joints[j0].rot * q;
+
+            TR::Node *node = (TR::Node*)&level->nodesData[getModel()->node];
+            TR::Node *t = node + j0;
+
+            joints[j1].rotate(q.conjugate());
+            joints[j1].pos = joints[j0].pos + joints[j0].rot * vec3(float(t->x), float(t->y), float(t->z));
+
+            t = node + j1;
+            joints[j2].pos = joints[j1].pos + joints[j1].rot * vec3(float(t->x), float(t->y), float(t->z));
+        }
+        
+        */
+    }
+
+    float getFloorHeight(const vec3 &pos) {
+        int16 roomIndex = getRoomIndex();
+        TR::Room::Sector *sector = level->getSector(roomIndex, pos);
+
+        if (!sector) {
+            return this->pos.y;
+        }
+
+        return level->getFloor(sector, pos);// - LARA_HEEL_HEIGHT;
+    }
+
+    virtual void updateIK() override {
+        if (useIK && canIKLegs()) {
+            float ikPivotOffset = -Input::hmd.head.getPos().y * ONE_METER;
+
+            float footHeightL = getFloorHeight(joints[JOINT_LEG_L3].pos);
+            float footHeightR = getFloorHeight(joints[JOINT_LEG_R3].pos);
+
+            if (fabsf(footHeightL - footHeightR) < 256.0f) {
+                ikPivotOffset += max(footHeightL, footHeightR) - pos.y;
+            }
+
+            for (int i = 0; i < getModel()->mCount; i++) {
+                joints[i].pos.y += ikPivotOffset;
+            }
+
+            solveJointsLeg(JOINT_LEG_L1, JOINT_LEG_L2, JOINT_LEG_L3, footHeightL - LARA_HEEL_HEIGHT);
+            solveJointsLeg(JOINT_LEG_R1, JOINT_LEG_R2, JOINT_LEG_R3, footHeightR - LARA_HEEL_HEIGHT);
+        }
+
+        if (useIKAim) {
+            solveJointsArm(JOINT_ARM_L1, JOINT_ARM_L2, JOINT_ARM_L3);
+            solveJointsArm(JOINT_ARM_R1, JOINT_ARM_R2, JOINT_ARM_R3);
+        }
+    }
 };
 
 #endif
diff --git a/src/level.h b/src/level.h
index 91964de..0cc0215 100644
--- a/src/level.h
+++ b/src/level.h
@@ -3252,8 +3252,34 @@ struct Level : IGame {
         }
 
         if (Core::eye == 0.0f && Core::settings.detail.isStereo()) {
+            Lara *lara = (Lara*)getLara(0);
+
+            if (Core::settings.detail.stereo == Core::Settings::STEREO_VR) {
+                if (lara && lara->camera && !lara->camera->firstPerson) {
+                    lara->camera->changeView(true);
+                }
+            }
+
             renderEye(-1, showUI, invBG);
             renderEye(+1, showUI, invBG);
+
+        #ifdef _OS_WIN
+            uint8 stereo = Core::settings.detail.stereo;
+            Core::settings.detail.stereo = Core::Settings::STEREO_OFF;
+
+            if (lara) {
+                float dt = Core::deltaTime;
+                Core::deltaTime = 1.0f;
+//                lara->camera->spectatorVR = true;
+                lara->camera->update();
+                Core::deltaTime = dt;
+            }
+            renderEye(0, showUI, invBG);
+            Core::settings.detail.stereo = stereo;
+            if (lara) {
+                lara->camera->spectatorVR = false;
+            }
+        #endif
         }  else {
             renderEye(int(Core::eye), showUI, invBG);
         }
diff --git a/src/platform/win/OpenLara.vcxproj.user b/src/platform/win/OpenLara.vcxproj.user
index 96911a8..644eef1 100644
--- a/src/platform/win/OpenLara.vcxproj.user
+++ b/src/platform/win/OpenLara.vcxproj.user
@@ -1,18 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
-    <LocalDebuggerWorkingDirectory>..\..\..\bin\TR1_PSX</LocalDebuggerWorkingDirectory>
+    <LocalDebuggerWorkingDirectory>C:\Projects\TR\TR1_PSX</LocalDebuggerWorkingDirectory>
     <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
-    <LocalDebuggerCommandArguments>
-    </LocalDebuggerCommandArguments>
+    <LocalDebuggerCommandArguments>PSXDATA/LEVEL2.PSX</LocalDebuggerCommandArguments>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Profile|Win32'">
-    <LocalDebuggerWorkingDirectory>..\..\..\bin\TR1_PSX</LocalDebuggerWorkingDirectory>
+    <LocalDebuggerWorkingDirectory>C:\Projects\TR\TR1_PSX</LocalDebuggerWorkingDirectory>
     <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
-    <LocalDebuggerWorkingDirectory>..\..\..\bin\TR1_PSX</LocalDebuggerWorkingDirectory>
+    <LocalDebuggerWorkingDirectory>C:\Projects\TR\TR1_PSX</LocalDebuggerWorkingDirectory>
     <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
-    <LocalDebuggerCommandArguments>PSXDATA/GYM.PSX</LocalDebuggerCommandArguments>
+    <LocalDebuggerCommandArguments>PSXDATA/LEVEL2.PSX</LocalDebuggerCommandArguments>
   </PropertyGroup>
 </Project>
\ No newline at end of file
diff --git a/src/platform/win/main.cpp b/src/platform/win/main.cpp
index 0d314e2..13909c1 100644
--- a/src/platform/win/main.cpp
+++ b/src/platform/win/main.cpp
@@ -584,7 +584,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lPara
         case WM_ACTIVATE :
             if (XInputEnable)
                 XInputEnable(wParam != WA_INACTIVE);
-            Input::reset();
+            //Input::reset();
             break;
         case WM_SIZE:
             Core::width  = LOWORD(lParam);
@@ -812,6 +812,8 @@ void vrUpdate() {
 
     vr::VRCompositor()->WaitGetPoses(tPose, vr::k_unMaxTrackedDeviceCount, NULL, 0);
 
+    static bool forceUpdatePose = false;
+
     for (int id = 0; id < vr::k_unMaxTrackedDeviceCount; id++) {
         vr::TrackedDevicePose_t &pose = tPose[id];
 
@@ -825,8 +827,9 @@ void vrUpdate() {
                 mat4 pR = convToMat4(hmd->GetProjectionMatrix(vr::Eye_Right, 8.0f, 45.0f * 1024.0f));
 
                 mat4 head = convToMat4(pose.mDeviceToAbsoluteTracking);
-                if (Input::hmd.zero.x == INF) {
+                if (Input::hmd.zero.x == INF || forceUpdatePose) {
                     Input::hmd.zero = head.getPos();
+                    forceUpdatePose = false;
                 }
                 head.setPos(head.getPos() - Input::hmd.zero);
 
@@ -850,32 +853,41 @@ void vrUpdate() {
                  //   continue;
                 }
 
-                Input::setJoyDown(0, jkLeft,  IS_DOWN(vr::k_EButton_DPad_Left));
-                Input::setJoyDown(0, jkUp,    IS_DOWN(vr::k_EButton_DPad_Up));
-                Input::setJoyDown(0, jkRight, IS_DOWN(vr::k_EButton_DPad_Right));
-                Input::setJoyDown(0, jkDown,  IS_DOWN(vr::k_EButton_DPad_Down));
-
-                if (IS_DOWN(vr::k_EButton_Axis0)) {
-                     Input::setJoyPos(0, jkL, vec2(state.rAxis[0].x, -state.rAxis[0].y));
+                if (IS_DOWN(vr::k_EButton_SteamVR_Touchpad)) {
+                    forceUpdatePose = true;
                 }
 
-                Input::setJoyDown(0, jkA, IS_DOWN(vr::k_EButton_Axis1) ? (state.rAxis[1].x > 0.5) : false);
-                Input::setJoyDown(0, jkY, IS_DOWN(vr::k_EButton_Grip));
-                Input::setJoyDown(0, jkX, IS_DOWN(vr::k_EButton_ApplicationMenu));
-
-                // TODO
+                int joyIndex;
                 switch (hmd->GetControllerRoleForTrackedDeviceIndex(id)) {
-                    case vr::TrackedControllerRole_LeftHand :
-                        // TODO
-                        break;
-                    case vr::TrackedControllerRole_RightHand :
-                        // TODO
-                        break;
-                    default : ;
+                    case vr::TrackedControllerRole_RightHand : joyIndex =  0; break;
+                    case vr::TrackedControllerRole_LeftHand  : joyIndex =  1; break;
+                    default                                  : joyIndex = -1; break;
                 }
-                break;
+
+                if (joyIndex == -1) {
+                    break;
+                }
+
+                Input::setJoyPos(joyIndex, jkL, vec2(state.rAxis[0].x, -state.rAxis[0].y));
+                Input::setJoyDown(joyIndex, jkA, IS_DOWN(vr::k_EButton_Axis1) ? (state.rAxis[1].x > 0.25) : false);
+                Input::setJoyDown(joyIndex, jkY, IS_DOWN(vr::k_EButton_Grip));
+                Input::setJoyDown(joyIndex, jkX, IS_DOWN(vr::k_EButton_ApplicationMenu));
+
+                mat4 m = convToMat4(pose.mDeviceToAbsoluteTracking);
+                m.setPos((m.getPos() - Input::hmd.zero) * ONE_METER);
+                
+                mat4 scaleBasis(
+                    1,  0,  0, 0,
+                    0, -1,  0, 0,
+                    0,  0, -1, 0,
+                    0,  0,  0, 1);
+
+                m = scaleBasis * m * scaleBasis.inverse();
+                Input::hmd.controllers[joyIndex] = m;
 
                 #undef IS_DOWN
+
+                break;
             }
         }
     }
diff --git a/src/utils.h b/src/utils.h
index d5f1178..251da25 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -1057,6 +1057,22 @@ struct Basis {
     Basis(const quat &rot, const vec3 &pos) : rot(rot), pos(pos), w(1.0f) {}
     Basis(const mat4 &matrix) : rot(matrix.getRot()), pos(matrix.getPos()), w(1.0f) {}
 
+    Basis(const vec3 &start, const vec3 &end, const vec3 &right) {
+        vec3 u = (end - start).normal();
+        vec3 d = right.cross(u).normal();
+        vec3 r = u.cross(d);
+
+        mat4 m;
+        m.up()     = vec4(u, 0.0f);
+        m.dir()    = vec4(d, 0.0f);
+        m.right()  = vec4(r, 0.0f);
+        m.offset() = vec4(0.0f, 0.0f, 0.0f, 1.0f);
+
+        identity();
+        translate(start);
+        rotate(m.getRot().normal());
+    }
+
     void identity() {
         rot = quat(0, 0, 0, 1);
         pos = vec3(0, 0, 0);
@@ -1094,6 +1110,93 @@ struct Basis {
     }
 };
 
+int qSolve(float a, float b, float c, float &x1, float &x2) {
+    if (fabsf(a) < EPS) {
+        if (fabsf(b) < EPS) {
+            return 0;
+        }
+        x1 = x2 = -c / b;
+        return 1;
+    }
+
+    float d = b * b - 4.0f * a * c;
+    if (d < 0.0f) {
+        return 0;
+    }
+
+    float inv2a = 1.0f / (2.0f * a);
+
+    if (d < EPS) {
+        x1 = x2 = -b * inv2a;
+        return 1;
+    }
+
+    d = sqrtf(d);
+    x1 = (-b - d) * inv2a;
+    x2 = (-b + d) * inv2a;
+    return 2;
+} 
+
+bool ikSolve2D(const vec2 &end, float length1, float length2, vec2 &middle) {
+    float length = end.length();
+
+    if (length > length1 + length2) {
+        return false;
+    }
+
+    bool flipXY = end.x < end.y;
+
+    float a, b;
+
+    if (flipXY) {
+        a = end.y;
+        b = end.x;
+    } else {
+        a = end.x;
+        b = end.y;
+    }
+    ASSERT(fabsf(a) > EPS);
+
+    float m = (SQR(length1) - SQR(length2) + SQR(a) + SQR(b)) / (2.0f * a);
+    float n = b / a;
+
+    float y1, y2;
+
+    if (qSolve(1.0f + SQR(n), -2.0f * m * n, SQR(m) - SQR(length1), y1, y2)) {
+
+        if (flipXY) {
+            middle.x = y2;
+            middle.y = m - n * y2;
+        } else {
+            middle.y = y2;
+            middle.x = m - n * y2;
+        }
+
+        return true;
+    }
+
+    middle = end * (length1 / length);
+    return false;
+}
+
+bool ikSolve3D(const vec3 &start, const vec3 &end, const vec3 &pole, float length1, float length2, vec3 &middle) {
+   vec3 d = (end - start).normal();
+   vec3 n = d.cross(pole - start).normal();
+
+   mat4 m;
+   m.right()  = vec4(n.cross(d), 0.0f);
+   m.up()     = vec4(d, 0.0f);
+   m.dir()    = vec4(n, 0.0f);
+   m.offset() = vec4(start, 1.0f);
+
+   bool res = ikSolve2D((m.inverse() * end).xy(), length1, length2, middle.xy());
+
+   middle.z = 0.0f;
+   middle = m * middle;
+
+   return res;
+}
+
 struct ubyte2 {
     uint8 x, y;
 };