void TraverseRayAndMarkVisible(Vec3i fromPos, Vec3i toPosRel, double yoffset = 0.5)
        {
            ray.origin.Set(fromPos.X + 0.5, fromPos.Y + yoffset, fromPos.Z + 0.5);
            ray.dir.Set(toPosRel);

            toPos.Set(fromPos.X + toPosRel.X, fromPos.Y + toPosRel.Y, fromPos.Z + toPosRel.Z);
            curpos.Set(fromPos);

            BlockFacing fromFace = null, toFace;

            int manhattenLength = fromPos.ManhattenDistanceTo(toPos);
            int curMhDist;

            while ((curMhDist = curpos.ManhattenDistanceTo(fromPos)) <= manhattenLength + 2)
            {
                // Since chunks are arranged in a uniform grid, all we have to do is to find out on
                // what facing (N/E/S/W/U/D) the ray leaves the chunk and move into that direction.
                // This may seem inaccurate, but works surpisingly well
                toFace = GetExitingFace(curpos);
                if (toFace == null)
                {
                    return;
                }

                long index3d = ((long)curpos.Y * game.WorldMap.chunkMapSizeZFast + curpos.Z) * game.WorldMap.chunkMapSizeXFast + curpos.X;

                ClientChunk chunk = null;
                game.WorldMap.chunks.TryGetValue(index3d, out chunk);

                if (chunk != null)
                {
                    chunk.SetVisible(true);

                    if (curMhDist > 1 && !chunk.IsTraversable(fromFace, toFace))
                    {
                        break;
                    }
                }

                curpos.Offset(toFace);
                fromFace = toFace.GetOpposite();

                if (!game.WorldMap.IsValidChunkPosFast(curpos.X, curpos.Y, curpos.Z) && (!isAboveHeightLimit || curpos.Y <= 0))
                {
                    break;
                }
            }
        }
        public void CullInvisibleChunks()
        {
            if (!ClientSettings.Occlusionculling || game.WorldMap.chunks.Count < 100)
            {
                return;
            }

            Vec3d camPos = game.player.Entity.CameraPos;

            centerpos.Set((int)(camPos.X / chunksize), (int)(camPos.Y / chunksize), (int)(camPos.Z / chunksize));

            isAboveHeightLimit = centerpos.Y >= game.WorldMap.ChunkMapSizeY;

            playerViewVec = EntityPos.GetViewVector(game.mousePitch, game.mouseYaw).Normalize();

            lock (game.WorldMap.chunksLock)
            {
                foreach (var val in game.WorldMap.chunks)
                {
                    val.Value.SetVisible(false);
                }

                // We sometimes have issues with chunks adjacent to the player getting culled, so lets make these always visible
                for (int dx = -1; dx <= 1; dx++)
                {
                    for (int dy = -1; dy <= 1; dy++)
                    {
                        for (int dz = -1; dz <= 1; dz++)
                        {
                            long        index3d = game.WorldMap.ChunkIndex3D(dx + centerpos.X, dy + centerpos.Y, dz + centerpos.Z);
                            ClientChunk chunk   = null;

                            if (game.WorldMap.chunks.TryGetValue(index3d, out chunk))
                            {
                                chunk.SetVisible(true);
                            }
                        }
                    }
                }
            }


            // Add some 15 extra degrees to the field of view angle for safety
            float fov = GameMath.Cos(game.MainCamera.Fov + 15 * GameMath.DEG2RAD);

            for (int i = 0; i < cubicShellPositions.Length; i++)
            {
                Vec3i vec = cubicShellPositions[i];

                float dotProd = playerViewVec.Dot(cubicShellPositionsNormalized[i]);
                if (dotProd <= fov / 2)
                {
                    // Outside view frustum
                    continue;
                }

                // It seems that one trace can cause issues where chunks are culled when they shouldn't
                // 2 traces with a y-offset seems to mitigate most of that issue
                TraverseRayAndMarkVisible(centerpos, vec, 0.25);
                TraverseRayAndMarkVisible(centerpos, vec, 0.75);
            }
        }