Урок №9. Камера в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 29 Авг 2020  | 

 4614

 ǀ   6 

В предыдущем уроке мы обсуждали матрицу вида и то, как можно её использовать для перемещения по сцене. OpenGL сам по себе не знаком с концепцией камеры, но мы можем попытаться смоделировать её, перемещая все объекты в сцене в обратном направлении, создавая иллюзию нашего движения вперёд.

В этом уроке мы рассмотрим, как настроить камеру в OpenGL, обсудим эффект летающей камеры, с помощью которого мы сможем свободно перемещаться по 3D-сцене. Далее затронем тему обработки входных данных клавиатуры и мышки, и закончим на пользовательском классе камеры.

Создание камеры в OpenGL

Когда речь заходит о камере/пространстве окна просмотра, то подразумевается, что все координаты вершин мы рассматриваем с точки зрения камеры, являющейся «отправной» точкой сцены: матрица вида преобразует все мировые координаты в координаты вида, которые относятся к положению и направлению камеры. Чтобы определить камеру, необходимо знать:

   её положение в мировом пространстве;

   направление, в котором смотрит камера;

   вектор, указывающий вправо;

   вектор, указывающий вверх от камеры.

Внимательный читатель может заметить, что на самом деле мы собираемся создать систему координат с тремя перпендикулярными единичными осями, причем в качестве начала координат будет использоваться положение камеры:


Пункт №1: Местоположение камеры

Получить позицию камеры очень просто — это вектор в мировом пространстве, который указывает на положение камеры. Мы установим камеру в том же месте, что и в предыдущем уроке:

Примечание: Не забывайте, что положительная часть оси Z проходит через ваш экран к вам, поэтому, если мы хотим, чтобы камера двигалась назад, необходимо двигаться вдоль положительной оси Z.

Пункт №2: Направление камеры

Следующий необходимый вектор — это вектор направления камеры, т.е. в какую точку она сфокусирована. Предположим, что камера указывает на начало нашей сцены: точку с координатами (0,0,0). Помните, что если мы вычтем два вектора друг из друга, то получим вектор, который является разницей этих двух векторов? Таким образом, вычитание вектора положения камеры из исходного вектора сцены приводит нас к искомому вектору направления. Для системы координат, определяемой матрицей вида, мы хотим, чтобы её ось Z была положительной, и поскольку, по соглашению (в OpenGL), камера указывает вдоль отрицательной оси Z, то необходимо получить вектор, противоположный вектору направления. Для этого достаточно поменять местами порядок вычитания векторов, и тогда получится вектор, направленный вдоль положительной оси Z камеры:

Примечание: Вы могли заметить, что «вектор направления» — это не самое лучшее название для такого объекта, так как вектор фактически указывает в обратном направлении от заданной точки фокуса.

Пункт №3: Вектор вправо

Ещё один вектор, который нам нужен, — это вектор-вправо, представляющий положительную ось X пространства камеры. Чтобы получить вектор-вправо, мы прибегнем к небольшому трюку, сначала задав вектор-вправо, указывающий вверх (в мировом пространстве). Затем мы выполним векторное умножение вектора-вправо с вектором направления из пункта №2. Поскольку результатом векторного произведения является вектор, перпендикулярный заданным векторам, то мы получим вектор, который указывает в направлении положительной оси X (если бы мы поменяли порядок векторного произведения, то получили бы вектор, указывающий вдоль отрицательной оси X):

Пункт №4: Вектор вверх

Теперь, когда у нас есть как вектор оси X, так и вектор оси Z, получить вектор, указывающий вдоль положительной Y-оси камеры, относительно легко: нужно воспользоваться векторным произведением вектора-вправо и вектора направления:

С помощью векторного произведения и нескольких небольших трюков нам удалось создать все необходимые векторы, формирующие пространство вида/камеры. В линейной алгебре данный процесс носит название «процесс Грама-Шмидта». Используя полученные векторы камеры, мы теперь можем определить LookAt-матрицу, которая является очень полезной для создания камеры.

LookAt-матрица


Самое замечательное в матрицах то, что если вы определяете координатное пространство с помощью 3-х перпендикулярных осей, то появляется возможность создать матрицу с данными 3-мя осями плюс вектором трансляции, и далее вы можете преобразовать любой вектор в это координатное пространство, просто перемножив его и матрицу. Именно это и делает LookAt-матрица. Теперь, когда у нас есть 3 перпендикулярные оси и вектор положения, для определения пространства камеры мы можем создать нашу собственную LookAt-матрицу:

Где R — это вектор-вправо, U — вектор-вверх, D — вектор направления и P — вектор положения камеры. Обратите внимание, что, поскольку мы хотим вращать и перемещать мир в направлении, противоположном направлению перемещения камеры, элементы матрицы вращения (левая матрица) и матрицы трансляции (правая матрица) инвертированы (т.е. транспонированы и взяты с противоположным знаком). Использование LookAt-матрицы в качестве нашей матрицы вида преобразует все мировые координаты в координаты пространства окна просмотра, которое мы только что определили. Затем LookAt-матрица выполняет именно то, из-за чего и получила своё название: она создаёт матрицу вида, которая смотрит на заданную цель.

К счастью для нас, мы можем переложить всю эту работу на библиотеку GLM. Нам необходимо только указать положение камеры, целевую позицию и вектор, который представляет собой вектор-вверх в мировом пространстве (вектор-вверх, который мы использовали для вычисления вектора-вправо). Затем, GLM создаст LookAt-матрицу, которую мы сможем использовать в качестве матрицы вида:

Аргументами функции glm::LookAt() являются позиция камеры, позиция цели и вектор-вверх. В примере выше создаётся матрица вида, аналогичная той, которую мы создавали в предыдущем уроке.

Прежде чем углубляться в пользовательский ввод, давайте сначала немного повеселимся, вращая камеру вокруг нашей сцены. Фокус направим на точку (0,0,0). Необходимо будет задействовать некоторые наши знания тригонометрии, чтобы создать для каждого кадра координаты X и Z, представляющие точку на окружности, и далее будем использовать их для задания положения нашей камеры. Пересчитывая с течением времени координаты X и Y, мы проходим все точки на окружности, и, таким образом, получается, что камера вращается вокруг сцены. Ширину окружности зададим с помощью предопределённой переменной radius. И в каждом кадре, с помощью функции glfwGetTime() из библиотеки GLFW, будем создавать новую матрицу вида:

Если вы запустите данный код, то получите что-то вроде следующего:

Теперь, с помощью вышеописанного маленького фрагмента кода, камера вращается с течением времени вокруг сцены. Не стесняйтесь экспериментировать с параметрами радиуса и положения/направления, чтобы получить представление о том, как работает LookAt-матрица.

  Google Drive / Урок №9. Камера в OpenGL — Исходный код №1

  GitHub / Урок №9. Камера в OpenGL — Исходный код №1

Двигаемся дальше

Размахивать камерой вокруг сцены — это, конечно, весело, но куда веселее делать все движения самостоятельно! Для этого сначала нам нужно настроить камеру, определив некоторые переменные камеры в верхней части нашей программы:

LookAt-функция изменится на:

Первое, что мы делаем, это устанавливаем положение камеры с помощью заранее определённой переменной cameraPos. Направление — это текущее положение + вектор направления, который мы только что определили. В результате мы получим гарантию того, что, как бы мы ни двигались, камера продолжит смотреть в направлении цели. Давайте немного поиграем с этими переменными, обновляя вектор cameraPos при нажатии некоторых клавиш.

Мы уже определили функцию processInput() для управления вводом с клавиатуры, поэтому давайте добавим несколько дополнительных ключевых команд:

Всякий раз, когда мы нажимаем одну из WASD-клавиш, соответствующим образом обновляется положение камеры. Если мы хотим двигаться вперёд или назад, мы добавляем или вычитаем вектор направления из вектора положения, масштабируемого некоторым значением скорости. Если мы хотим двигаться боком, то выполняем векторное произведение, чтобы создать вектор-вправо, и, соответственно, движемся вдоль него. Это создаёт привычный эффект стрейфа при использовании камеры.

Примечание: Обратите внимание, что мы нормализуем результирующий вектор-вправо. Если бы мы не нормализовали этот вектор, то результирующее векторное произведение могло бы вернуть векторы разного размера, основываясь на переменной cameraFront. Если бы мы не нормализовали вектор, то двигались бы медленно или быстро, в зависимости от ориентации камеры, а не от составляющей скорости движения.

К настоящему моменту вы уже должны иметь возможность немного перемещать камеру, хотя и со скоростью, характерной для конкретной системы, поэтому, возможно, потребуется настроить переменную cameraSpeed.

Скорость перемещения камеры


В настоящее время мы используем постоянное значение скорости движения при перемещении. В теории это кажется прекрасным, но на практике компьютеры имеют разные вычислительные мощности, и результатом этого является то, что некоторые компьютеры способны рендерить гораздо больше кадров в секунду, чем другие компьютеры. Чем больше количество кадров, тем чаще вызовы processInput(). В результате на некоторых компьютерах эффект движения будет выглядеть очень быстро, а на других — очень медленно. Поэтому мы должны убедиться, что наше приложение в состоянии одинаково работать на всех типах компьютеров.

Графические приложения и игры обычно отслеживают переменную deltaTime, которая хранит время, которое было затрачено для визуализации последнего кадра. Затем мы умножаем все переменные скорости на значение deltaTime. В результате получается, что, когда в кадре мы имеем большое значение deltaTime (а это означает, что последний кадр занял больше среднего времени), скорость для этого кадра также будет немного выше, чтобы это всё дело сбалансировать. При использовании данного подхода не имеет значения, насколько мощный у вас компьютер, скорость камеры будет сбалансирована соответствующим образом так, что у каждого пользователя наша программа будет работать одинаково.

Для расчета значения deltaTime необходимо отслеживать 2 глобальные переменные:

Затем, в каждом кадре, мы вычисляем новое значение deltaTime для последующего использования:

Теперь, когда у нас есть deltaTime, мы можем использовать её значение при расчете скоростей:

Поскольку теперь мы используем переменную deltaTime, камера будет двигаться с постоянной скоростью в 2.5 единицы в секунду. Вместе с предыдущим разделом, теперь для перемещения по сцене у нас должна получиться гораздо более плавная и устойчивая камера:

В итоге, камера перемещается и работает одинаково быстро в любой системе. Если вы застряли или что-то не получается – смотрите исходный код ниже.

  Google Drive / Урок №9. Камера в OpenGL — Исходный код №2

  GitHub / Урок №9. Камера в OpenGL — Исходный код №2

Осматриваемся вокруг

Не так уж и интересно передвигаться только с помощью клавиш клавиатуры. Тем более что мы не можем повернуться, и, как следствие этого, наше перемещение в пространстве является довольно ограниченным. Вот тут-то в дело и вступает мышка!

Чтобы посмотреть вокруг сцены, мы должны изменить вектор cameraFront, основываясь на входных данных от мышки. Однако изменение вектора направления на основе вращения мыши является несколько сложным и требует некоторых познаний в тригонометрии. Если вы не понимаете тригонометрию, не волнуйтесь, можно просто перейти к разделам кода и добавить их в свой код; вы всегда сможете сюда вернуться позже, если захотите узнать больше.

Углы Эйлера


Углы Эйлера — это 3 значения, которые могут представлять любое вращение в 3D, определённые Леонардом Эйлером где-то в 1700-х гг. Есть 3 угла Эйлера:

   Тангаж (англ. «pitch») — это угловое движение объекта относительно главной поперечной оси инерции (величина того, насколько мы смотрим вверх или вниз) — изображен в первой части картинки.

   Рыскание (англ. «yaw») — это угловые движения объекта относительно вертикальной оси, а также небольшие изменения курса вправо или влево — изображено во второй части картинки.

   Крен (англ. «roll») — это поворот объекта вокруг его продольной оси (он используется в камерах космического полёта) — изображен в третьей части картинки.

Следующее изображение даст нам наглядное представление углов:

Каждый из отдельно взятых углов Эйлера представлен своим значением, и при сочетании всех 3-х углов мы можем вычислить любой вектор вращения в 3D.

Для нашей камеры потребуются только значения рыскания и тангажа, поэтому мы не будем обсуждать здесь значение крена. Заданные значение тангажа и рыскания можно преобразовать в новый 3D-вектор направления. Процесс преобразования значений рыскания и тангажа в вектор направления требует немного знаний по тригонометрии. И начнем мы с самого простого случая.

Давайте немного освежим память и рассмотрим общий случай прямоугольного треугольника (с одним из углов, равным 90 градусов):

Если мы определим длину гипотенузы равной 1, то используя знания тригонометрии (косинус угла — это отношение прилежащей к углу стороны к гипотенузе, а синус угла — отношение противолежащей к углу стороны к гипотенузе), получаем, что длина основания x треугольника равна cos(θ)/h=cos(θ)/1=cos(θ), а длина высоты y треугольника равна sin(θ)/h=sin(θ)/1=sin(θ).

В результате мы имеем формулы для получения длин сторон (x и y) в прямоугольных треугольниках, в зависимости от заданного угла. Давайте воспользуемся ими для вычисления составляющих вектора направления.

Представим себе тот же самый треугольник, но теперь посмотрим на него сверху, причем прилегающая и противолежащая стороны параллельны осям X и Z сцены (как если бы смотрели вниз вдоль оси Y):

Если мы представим себе угол рыскания как угол, отсчитываемый против часовой стрелки относительно стороны X, то можно увидеть, что длина стороны X равна cos(yaw). И точно так же, длина стороны Z равна sin(yaw).

Вооружившись данными знаниями и полученным значением рыскания, мы можем создать вектор направления камеры:

Это является решением того, как можно получить 3D-вектор направления, используя значение рыскания, но это ещё не всё: ведь нам ещё нужно использовать значение тангажа, чтобы найти Y-компоненту вектора направления. Давайте посмотрим на сторону оси Y, как будто мы сидим на плоскости XZ:

Аналогично предыдущему примеру, в данном треугольнике мы можем видеть, что Y-компонента направления равна sin(pitch):

Однако, мы можем заметить, что сторона XZ находится под влиянием cos(pitch), поэтому нам необходимо добавить cos(pitch) как одну из составных частей вектора направления. С учётом этого мы получаем конечный вектор направления:

В итоге у нас есть формула для преобразования значений рыскания и тангажа в трёхмерный вектор направления, который можно использовать для того, чтобы осматриваться вокруг.

Мы настроили мир сцены таким образом, чтобы всё было расположено в направлении отрицательной оси Z. Однако, если мы посмотрим на треугольник рыскания (со сторонами X и Z), то увидим, что при угле θ равным 0 градусов мы получим то, что вектор направления камеры будет указывать в сторону положительной оси X. Чтобы убедиться, что камера по умолчанию указывает на отрицательную ось Z, мы можем установить по умолчанию значение рыскания в виде 90 градусов (по часовой стрелке). Положительное значение угла поворота определяет вращение против часовой стрелки, поэтому мы по умолчанию устанавливаем значение рыскания равным:

Вы, вероятно, уже задавались вопросом: «А как мы будем задавать и изменять эти значения рыскания и тангажа?».

Мышка как устройство ввода

Значения рыскания и тангажа берутся из входных данных движения мыши (или контроллера/джойстика), где горизонтальное движение мыши влияет на рыскание, а вертикальное — на тангаж. Идея состоит в том, чтобы сохранить позиции мыши в последнем кадре и в текущем кадре вычислить, насколько сильно эти значения изменились. Чем выше горизонтальная или вертикальная разница, тем больше мы обновляем значение тангажа или рыскания и тем больше камера должна двигаться.

Сначала мы сообщаем GLFW, что он должен скрыть курсор и захватить его. Захват курсора означает, что после того, как приложение получило фокус, курсор мыши остаётся в центре окна (если только приложение не потеряет фокус или не завершит свою работу). Мы можем сделать это с помощью одного простого вызова функции:

После этого вызова, куда бы мы не переместили мышь, она не будет видна и не покинет окно. Это идеально подходит для камеры в стиле FPS.

Чтобы вычислить значения тангажа и рыскания, нам нужно сообщить GLFW, чтобы он отслеживал события движения мыши. Выполняется это путём создания callback-функции со следующим прототипом:

Здесь xpos и ypos представляют собой текущие позиции мыши. Как только мы зарегистрируем в GLFW нашу callback-функцию, то при каждом движении мыши будет вызываться функция mouse_callback:

Обрабатывая ввод мышки для летающей камеры, мы должны выполнить несколько шагов, прежде чем сможем полностью вычислить вектор направления камеры:

   Шаг №1: Вычислить смещение мышки с момента последнего кадра.

   Шаг №2: Добавить значения смещения к значениям рыскания и тангажа камеры.

   Шаг №3: Добавить некоторые ограничения к минимальным/максимальным значениям тангажа.

   Шаг №4: Вычислить вектор направления.

Первый шаг заключается в вычислении смещения мышки с момента последнего кадра. Сначала мы должны сохранить позицию мыши в приложении, которое мы инициализируем, чтобы изначально быть в центре экрана (размер экрана 800×600):

Затем, в callback-функции мышки, мы вычисляем смещение движения между последним и текущим кадрами:

Обратите внимание, что мы умножаем значения смещения на значение переменной sensitivity (чувствительность). Если мы опустим это действие, движение мыши будет слишком сильным; поиграйтесь со значением чувствительности самостоятельно.

Затем мы добавляем значения смещения к глобально объявленным значениям тангажа и рыскания:

На третьем этапе мы хотели бы добавить некоторые ограничения к камере, чтобы пользователи не могли совершать странные движения (включая, так называемый LookAt-переворот, когда вектор направления параллелен вектору-вверх глобального мира). Тангаж должен быть ограничен таким образом, чтобы пользователи не могли смотреть выше 89 градусов (при 90 градусах мы получаем LookAt-переворот), а также не ниже -89 градусов. Это гарантирует, что пользователь сможет смотреть вверх на небо или вниз на свои ноги, но не дальше. Ограничения работают путем замены значения угла Эйлера его значением ограничения всякий раз, когда оно выходит за описанные пределы:

Обратите внимание, что мы не устанавливаем никаких ограничений на значение рыскания, так как не хотим ограничивать пользователя в горизонтальном вращении. Однако так же легко добавить ограничение к рысканию, если вам этого хочется.

Четвёртый и последний шаг состоит в том, чтобы вычислить фактический вектор направления, используя формулу из предыдущего раздела:

Данный вычисленный вектор направления содержит все вращения, вычисленные на основе движения мыши. Поскольку вектор cameraFront уже содержится в GLM-функции lookAt(), то мы готовы к работе.

Если бы вы сейчас запустили код, то заметили бы, что камера делает большой резкий скачок всякий раз, когда окно впервые получает фокус от вашего курсора мыши. Причина этого внезапного скачка заключается в том, что, как только ваш курсор входит в окно, вызывается callback-функция мыши с позицией xpos и ypos, равной местоположению, из которой ваша мышь попала на экран. Часто данная позиция находится на значительном удалении от центра экрана, что приводит к большим смещениям и, следовательно, к большому скачку движения. Мы можем обойти эту проблему, определив глобальную переменную типа bool, чтобы проверить, является ли это первый раз, когда мы получаем ввод мыши. Если это происходит в первый раз, то мы обновляем начальные позиции мыши до новых значений xpos и ypos. Результирующие движения мыши затем будут использовать вновь введенные координаты положения мыши для расчета смещений:

В итоге код превращается в:

Ну вот и всё! Дайте запустим пример, и вы увидите, что теперь мы можем свободно перемещаться по нашей 3D-сцене!

Масштабирование


В качестве небольшого дополнения к камере мы также реализуем интерфейс масштабирования. В предыдущем уроке мы говорили, что поле зрения или fov в значительной степени определяет, насколько мы можем видеть сцену. Когда поле зрения становится меньше, то проецируемое пространство сцены также становится меньше. Это уменьшенное пространство проецируется на те же NDC, создавая иллюзию увеличения масштаба. Для увеличения масштаба мы будем использовать колесо прокрутки мыши. Подобно движению мыши и вводу с клавиатуры у нас есть callback-функция для прокрутки мыши:

При вертикальной прокрутке значение yoffset сообщает нам величину, которую мы прокрутили. Вызывая функцию scroll_callback() мы изменяем содержимое глобально объявленной переменной fov. Поскольку 45.0 является fov-значением по умолчанию, то мы хотим ограничить уровень масштабирования между 1.0 и 45.0.

Теперь мы должны в каждом кадре отправлять на GPU матрицу перспективной проекции, но на этот раз с переменной fov в качестве поля зрения:

И, наконец, не забудьте зарегистрировать callback-функцию прокрутки:

И вот оно! Мы внедрили простую камеру, которая позволяет свободно перемещаться в 3D-окружении:

Посмотреть результат

  Google Drive / Урок №9. Камера в OpenGL — Исходный код №3

  GitHub / Урок №9. Камера в OpenGL — Исходный код №3

Пользовательский класс Camera

В следующих уроках мы всегда будем использовать камеру, чтобы осматривать сцену и смотреть на результаты наших объектов со всех сторон. Однако, поскольку код камеры может занимать значительное количество места в каждом уроке, мы немного абстрагируем его детали и создадим наш собственный объект камеры, который будет выполнять большую часть работы за нас с некоторыми аккуратными небольшими дополнениями. В отличие от урока о шейдерах, мы не будем рассказывать вам о создании класса Camera, но предоставим вам исходный код (полностью прокомментированный), если вы хотите знать внутреннюю работу.

Как и в случае шейдерного объекта, мы полностью определяем класс камеры в одном заголовочном файле с названием <camera.h>. Рекомендуется хотя бы один раз проверить класс в качестве примера того, как вы могли бы создать свою собственную камеру.

Примечание: Камера, которую мы только что разработали — это летающая камера, которая подходит для большинства целей и хорошо работает с углами Эйлера, но будьте осторожны при создании различных камер, таких как FPS-камера или камера моделирования полёта. Каждая камера имеет свои собственные трюки и причуды, так что не забудьте о них сначала разузнать. Например, наша летающая камера не допускает значений тангажа выше или равных 90 градусам, а статический вектор-вверх (0,1,0) не работает, когда мы учитываем значения крена.

Обновлённую версию исходного кода с использованием нового объекта камеры можно найти здесь.

  Google Drive / Урок №9. Камера в OpenGL — Исходный код №4

  GitHub / Урок №9. Камера в OpenGL — Исходный код №4

Упражнения

Задание №1

Посмотрите, можно ли преобразовать класс камеры таким образом, чтобы он стал настоящей FPS-камерой, где вы не cможете летать; вы можете только смотреть вокруг, оставаясь на плоскости XZ.

Ответ №1

Задание №2

Попробуйте создать свою собственную LookAt-функцию, в которой вы вручную создадите матрицу вида, как описано в начале этого урока. Замените GLM-функцию LookAt() своей собственной реализацией и посмотрите, работает ли она так как нужно.

Ответ №2

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (10 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 6

  1. Аватар Георгий:

    Здравствуйте! Поймал непонятную ситуацию у себя. Чтоб резких движений не делать, решил уточнить. В mouse_callback получаю все время возрастающие значения xpos и ypos, в независимости от движения мыши. Причем если убираю glfwSetInputMode(glfw_cursor_disabled), то значения уже корректные, но мышка выходит из окна. Это с opengl проблема? Есть ли ещё способ мышь в экране оставить?
    Спасибо.

  2. Аватар Артём:

    Изучил первую главу, отличные базовые уроки.
    Но теперь у меня возник один вопрос, и я не догадываюсь каким способом это можно реализовать. Надеюсь увидеть урок по этому вопросу 🙂 или подсказки в какую сторону нужно двигаться.
    Так вот, сам вопрос : вот есть на нашей сцене несколько кубов и свободная камера, каким образом можно реализовать выделение куба при клике на него мышкой?

    Мои мысли на этот счет: у нас есть трехмерные координаты вершин фигур, которые после обработки шейдерами превращаются в двухмерные координаты нашего окна на экране. При клике мышкой мы можем получить координаты нахождения курсора.
    И собственно пути решения состоит в том чтобы:
    1) каким-то образом сохранять двухмерные координаты фигур и сравнивать их с двухмерными координатами мышки.
    2) ИЛИ превращать двухмерные координаты мышки в трехмерные координаты с помощью определенных математических операций и сравнивать их с трехмерными координатами наших фигур.

  3. Аватар Арбузик❤❤❤:

    Нашел незначительную ошибку: при перемещении мыши вверх камера поворачивается вниз, а не также вверх, и наоборот. В функции ProcessMouseMovement заменил
    pitch += yoffset;
    на
    pitch -= yoffset;
    Таким образом, камера смотрит туда же куда и мышь.

    1. Дмитрий Бушуев Дмитрий Бушуев:

      Странно, только что проверил — у меня всё нормально.
      Можете указать на каком конкретно месте в статье (или в каком архиве с исходниками) у вас появилась данная проблема?

      1. Аватар Арбузик❤❤❤:

        Код я писал по мере прохождения урока, так что он отличается от исходников. Попробовал скачать последний архив и запустить проект из него у себя — всё, как вы и сказали, нормально. Думаю, у меня это произошло из-за того, что написал не так реализацию одного из методов камеры. 😀
        Извините пожалуйста за потраченное время!

        1. Дмитрий Бушуев Дмитрий Бушуев:

          Хорошо, что всё разрешилось 🙂

Добавить комментарий для Арбузик❤❤❤ Отменить ответ

Ваш E-mail не будет опубликован. Обязательные поля помечены *