public ClimbInfo CheckClimbability(Vector3 offset, HeightInfo nfc, float testHeight)
        {
            Vector3 from, to;
            float d;
            var pos = Transform.Origin;
            var t1 = new Transform();
            var t2 = new Transform();
            byte upFounded;
            var castRay = new float[6];

            // init callbacks functions
            nfc.Cb = RayCb;
            nfc.Ccb = ConvexCb;
            var tmp = pos + offset;

            var ret = new ClimbInfo();
            ret.HeightInfo = CheckNextStep(offset + new Vector3(0, 0, 128), nfc);
            ret.CanHang = false;
            ret.EdgeHit = false;
            ret.EdgeObject = null;
            ret.FloorLimit = HeightInfo.FloorHit ? HeightInfo.FloorPoint.Z : -9e10f;
            ret.CeilingLimit = HeightInfo.CeilingHit ? HeightInfo.CeilingPoint.Z : 9e10f;
            if (nfc.CeilingHit && nfc.CeilingPoint.Z < ret.CeilingLimit)
            {
                ret.CeilingLimit = nfc.CeilingPoint.Z;
            }
            ret.Point = Climb.Point;

            // check max height
            if (HeightInfo.CeilingHit && tmp.Z > HeightInfo.CeilingPoint.Z - ClimbR - 1.0f)
            {
                tmp.Z = HeightInfo.CeilingPoint.Z - ClimbR - 1.0f;
            }

            // let's calculate edge
            from.X = pos.X - Transform.Basis.Column1.X * ClimbR * 2.0f;
            from.Y = pos.Y - Transform.Basis.Column1.Y * ClimbR * 2.0f;
            from.Z = pos.Z;
            to = tmp;

            t1.SetIdentity();
            t2.SetIdentity();
            upFounded = 0;
            testHeight = Math.Max(testHeight, MaxStepUpHeight);
            d = pos.Z + Bf.BBMax.Z - testHeight;
            to.CopyToArray(castRay, 0);
            to.CopyToArray(castRay, 3);
            castRay[5] -= d;
            var n0 = Vector3.Zero;
            var n1 = Vector3.Zero;
            var n0d = 0.0f;
            var n1d = 0.0f;
            do
            {
                t1.Origin = from;
                t2.Origin = to;
                nfc.Ccb.ClosestHitFraction = 1.0f;
                nfc.Ccb.HitCollisionObject = null;
                BtEngineDynamicsWorld.ConvexSweepTest(ClimbSensor, ((Matrix4) t1).ToBullet(), ((Matrix4) t2).ToBullet(), nfc.Ccb);
                if (nfc.Ccb.HasHit)
                {
                    if (nfc.Ccb.HitNormalWorld.Z >= 0.1f)
                    {
                        upFounded = 1;
                        n0 = nfc.Ccb.HitNormalWorld.ToOpenTK();
                        n0d = -n0.Dot(nfc.Ccb.HitPointWorld.ToOpenTK());
                    }
                    if (upFounded != 0 && nfc.Ccb.HitNormalWorld.Z < 0.001f)
                    {
                        n1 = nfc.Ccb.HitNormalWorld.ToOpenTK();
                        n1d = -n1.Dot(nfc.Ccb.HitPointWorld.ToOpenTK());
                        Climb.EdgeObject = nfc.Ccb.HitCollisionObject;
                        upFounded = 2;
                        break;
                    }
                }
                else
                {
                    tmp.X = to.X;
                    tmp.Y = to.Y;
                    tmp.Z = d;
                    t1.Origin = to;
                    t2.Origin = tmp;
                    t1.Origin = from;
                    t2.Origin = to;
                    nfc.Ccb.ClosestHitFraction = 1.0f;
                    nfc.Ccb.HitCollisionObject = null;
                    BtEngineDynamicsWorld.ConvexSweepTest(ClimbSensor, ((Matrix4) t1).ToBullet(), ((Matrix4) t2).ToBullet(), nfc.Ccb);
                    if (nfc.Ccb.HasHit)
                    {
                        upFounded = 1;
                        n0 = nfc.Ccb.HitNormalWorld.ToOpenTK();
                        n0d = -n0.Dot(nfc.Ccb.HitPointWorld.ToOpenTK());
                    }
                    else
                    {
                        return ret;
                    }
                }

                // mult 0.66 is magic, but it must be less than 1.0 and greater than 0.0;
                // close to 1.0 - bad precision, good speed;
                // close to 0.0 - bad speed, bad precision;
                // close to 0.5 - middle speed, good precision
                from.Z -= 0.66f * ClimbR;
                to.Z -= 0.66f * ClimbR;
            } while (to.Z >= d); // we can't climb under floor!

            if (upFounded != 2)
            {
                return ret;
            }

            // get the character plane equation
            var n2 = Transform.Basis.Column0;
            var n2d = -n2.Dot(pos);

            Assert(!n0.FuzzyZero());
            Assert(!n1.FuzzyZero());
            Assert(!n2.FuzzyZero());

            /*
             * Solve system of the linear equations by Kramer method!
             * I know - It may be slow, but it has a good precision!
             * The root is point of 3 planes intersection.
             */
            d = -n0[0] * (n1[1] * n2[2] - n1[2] * n2[1]) +
                n1[0] * (n0[1] * n2[2] - n0[2] * n2[1]) -
                n2[0] * (n0[1] * n1[2] - n0[2] * n1[1]);

            if (Math.Abs(d) < 0.005f)
            {
                return ret;
            }

            ret.EdgePoint[0] = n0d * (n1[1] * n2[2] - n1[2] * n2[1]) -
                               n1d * (n0[1] * n2[2] - n0[2] * n2[1]) +
                               n2d * (n0[1] * n1[2] - n0[2] * n1[1]);
            ret.EdgePoint[0] /= d;

            ret.EdgePoint[1] = n0[0] * (n1d * n2[2] - n1[2] * n2d) -
                               n1[0] * (n0d * n2[2] - n0[2] * n2d) +
                               n2[0] * (n0d * n1[2] - n0[2] * n1d);
            ret.EdgePoint[1] /= d;

            ret.EdgePoint[2] = n0[0] * (n1[1] * n2d - n1d * n2[1]) -
                               n1[0] * (n0[1] * n2d - n0d * n2[1]) +
                               n2[0] * (n0[1] * n1d - n0d * n1[1]);
            ret.EdgePoint[2] /= d;
            ret.Point = ret.EdgePoint;
            ret.Point.CopyToArray(castRay, 3);
            /*
             * unclimbable edge slant %)
             */
            n2 = n0.Cross(n1);
            d = CriticalSlantZComponent;
            d *= d * (n2[0] * n2[0] + n2[1] * n2[1] + n2[2] * n2[2]);
            if (n2[2] * n2[2] > d)
            {
                return ret;
            }

            /*
             * Now, let us calculate z_angle
             */
            ret.EdgeHit = true;

            n2.Z = n2.X;
            n2.X = n2.Y;
            n2.Y = -n2.Z;
            n2.Z = 0.0f;
            if (n2.X * Transform.Basis.Column1.X + n2.Y * Transform.Basis.Column1.Y > 0) // direction fixing
            {
                n2.X = -n2.X;
                n2.Y = -n2.Y;
            }

            ret.N = n2;
            ret.Up.X = 0.0f;
            ret.Up.Y = 0.0f;
            ret.Up.Z = 1.0f;
            ret.EdgeZAngle = Helper.Atan2(n2.X, -n2.Y) * DegPerRad;
            ret.EdgeTanXY.X = -n2.Y;
            ret.EdgeTanXY.Y = n2.X;
            ret.EdgeTanXY.Z = 0.0f;
            ret.EdgeTanXY /= (float) Math.Sqrt(n2.X * n2.X + n2.Y * n2.Y);
            ret.Right = ret.EdgeTanXY;

            if (!HeightInfo.FloorHit || ret.EdgePoint.Z - HeightInfo.FloorPoint.Z >= Height)
            {
                ret.CanHang = true;
            }

            ret.NextZSpace = 2.0f * Height;
            if (nfc.FloorHit && nfc.CeilingHit)
            {
                ret.NextZSpace = nfc.CeilingPoint.Z - nfc.FloorPoint.Z;
            }

            return ret;
        }
        /// <summary>
        /// Calculates next floor info + phantom filter + returns step info.
        /// Current height info must be calculated!
        /// </summary>
        public StepType CheckNextStep(Vector3 offset, HeightInfo nfc)
        {
            float delta;
            Vector3 from, to;
            StepType ret;

            var pos = Transform.Origin + offset;
            GetHeightInfo(pos, nfc);

            if (HeightInfo.FloorHit && nfc.FloorHit)
            {
                delta = nfc.FloorPoint.Z - HeightInfo.FloorPoint.Z;
                if (Math.Abs(delta) < SPLIT_EPSILON)
                {
                    from.Z = HeightInfo.FloorPoint.Z;
                    ret = StepType.Horizontal;
                }
                else if (delta < 0.0f)
                {
                    delta = -delta;
                    from.Z = HeightInfo.FloorPoint.Z;
                    if (delta <= MinStepUpHeight)
                    {
                        ret = StepType.DownLittle;
                    }
                    else if (delta <= MaxStepUpHeight)
                    {
                        ret = StepType.DownBig;
                    }
                    else if (delta <= Height)
                    {
                        ret = StepType.DownDrop;
                    }
                    else
                    {
                        ret = StepType.DownCanHang;
                    }
                }
                else
                {
                    from.Z = nfc.FloorPoint.Z;
                    if (delta <= MinStepUpHeight)
                    {
                        ret = StepType.UpLittle;
                    }
                    else if (delta <= MaxStepUpHeight)
                    {
                        ret = StepType.UpBig;
                    }
                    else if (delta <= Height)
                    {
                        ret = StepType.UpClimb;
                    }
                    else
                    {
                        ret = StepType.UpImpossible;
                    }
                }
            }
            else if (!HeightInfo.FloorHit && !nfc.FloorHit)
            {
                from.Z = pos.Z;
                ret = StepType.Horizontal;
            }
            else if (!HeightInfo.FloorHit && nfc.FloorHit)
            {
                from.Z = nfc.FloorPoint.Z;
                ret = StepType.Horizontal;
            }
            else
            {
                from.Z = HeightInfo.FloorPoint.Z;
                ret = StepType.DownCanHang;
            }

            from.Z += ClimbR;
            to.Z = from.Z;
            from.X = Transform.Origin.X;
            from.Y = Transform.Origin.Y;
            to.X = from.X;
            to.Y = from.Y;
            HeightInfo.Cb.ClosestHitFraction = 1.0f;
            HeightInfo.Cb.CollisionObject = null;
            BtEngineDynamicsWorld.RayTest(from.ToBullet(), to.ToBullet(), HeightInfo.Cb);
            if (HeightInfo.Cb.HasHit)
            {
                ret = StepType.UpImpossible;
            }

            return ret;
        }
        public bool HasStopSlant(HeightInfo nextFc)
        {
            var pos = Transform.Origin;
            var forward = Transform.Basis.Column1;
            var floor = nextFc.FloorNormale;

            return nextFc.FloorPoint.Z > pos.Z
                   && nextFc.FloorNormale.Z < CriticalSlantZComponent
                   && forward.X * floor.X + forward.Y * floor.Y < 0.0f;
        }
        /// <summary>
        /// Start position are taken from transform
        /// </summary>
        public static void GetHeightInfo(Vector3 pos, HeightInfo fc, float vOffset = 0.0f)
        {
            var cb = fc.Cb;
            var r = cb.Container?.Room;
            RoomSector rs;

            fc.FloorHit = false;
            fc.CeilingHit = false;
            fc.Water = false;
            fc.Quicksand = QuicksandPosition.None;
            fc.TransitionLevel = 32512.0f;

            r = Room.FindPosCogerrence(pos, r)?.CheckFlip();
            if (r != null)
            {
                rs = r.GetSectorXYZ(pos);
                if (r.Flags.HasFlagUns(RoomFlag.Water))
                {
                    while (rs.SectorAbove != null)
                    {
                        Assert(rs.SectorAbove != null);
                        rs = rs.SectorAbove.CheckFlip();
                        Assert(rs?.OwnerRoom != null);
                        if ((rs.OwnerRoom.Flags & (uint) RoomFlag.Water) == 0x00)
                        {
                            fc.TransitionLevel = rs.Floor;
                            fc.Water = true;
                            break;
                        }
                    }
                }
                else if (r.Flags.HasFlagUns(RoomFlag.Quicksand))
                {
                    while (rs.SectorAbove != null)
                    {
                        Assert(rs.SectorAbove != null);
                        rs = rs.SectorAbove.CheckFlip();
                        Assert(rs?.OwnerRoom != null);
                        if ((rs.OwnerRoom.Flags & (uint) RoomFlag.Quicksand) == 0x00)
                        {
                            fc.TransitionLevel = rs.Floor;
                            fc.Quicksand = fc.TransitionLevel - fc.FloorPoint.Z > vOffset
                                ? QuicksandPosition.Drowning
                                : QuicksandPosition.Sinking;
                            break;
                        }
                    }
                }
                else
                {
                    while (rs.SectorBelow != null)
                    {
                        Assert(rs.SectorBelow != null);
                        rs = rs.SectorBelow.CheckFlip();
                        Assert(rs?.OwnerRoom != null);
                        if ((rs.OwnerRoom.Flags & (uint) RoomFlag.Water) != 0x00)
                        {
                            fc.TransitionLevel = rs.Ceiling;
                            fc.Water = true;
                            break;
                        }
                        else if ((rs.OwnerRoom.Flags & (uint) RoomFlag.Quicksand) != 0x00)
                        {
                            fc.TransitionLevel = rs.Ceiling;
                            fc.Quicksand = fc.TransitionLevel - fc.FloorPoint.Z > vOffset
                                ? QuicksandPosition.Drowning
                                : QuicksandPosition.Sinking;
                            break;
                        }
                    }
                }
            }

            // get heights
            var from = pos;
            var to = from;
            to.Z -= 4096.0f;
            cb.ClosestHitFraction = 1.0f;
            cb.CollisionObject = null;
            BtEngineDynamicsWorld.RayTest(from.ToBullet(), to.ToBullet(), cb);
            fc.FloorHit = cb.HasHit;
            if (fc.FloorHit)
            {
                fc.FloorNormale = cb.HitNormalWorld.ToOpenTK();
                Helper.SetInterpolate3(out fc.FloorPoint, from, to, cb.ClosestHitFraction);
                fc.FloorObject = cb.CollisionObject;
            }

            to = from;
            to.Z += 4096.0f;
            cb.ClosestHitFraction = 1.0f;
            cb.CollisionObject = null;
            BtEngineDynamicsWorld.RayTest(from.ToBullet(), to.ToBullet(), cb);
            fc.CeilingHit = cb.HasHit;
            if (fc.CeilingHit)
            {
                fc.CeilingNormale = cb.HitNormalWorld.ToOpenTK();
                Helper.SetInterpolate3(out fc.CeilingPoint, from, to, cb.ClosestHitFraction);
                fc.CeilingObject = cb.CollisionObject;
            }
        }