Уроки по OpenGL с сайта OGLDev


Урок 11 - Объединение преобразований

В последних уроках мы нашли несколько преобразований, которые дают гибкость в движении объекта в 3D пространстве. Нам еще есть что изучить (контроль над камерой и проекции перспективы), но как вы уже, наверное, догадались, необходимо комбинировать преобразования. В большинстве случаев вы захотите масштабировать объект для соответствия с реальными пропорциями, вращать для получения правильной ориентации, переместить куда-либо и т.д. С этого момента у нас будет единое преобразование. В целях выполнения вышеуказанных преобразований нам необходимо умножить первую матрицу преобразований на вектор, затем умножить вторую матрицу на результат выполнения первого действия, затем третью на результат предыдущих вычислений. Так будет продолжаться до тех пор, пока все матрицы не будет умножены на вектор. Простейший путь - это предоставить все матрицы преобразований в шейдер и пускай он их все перемножает. Но это не эффективно, поскольку все матрицы будут одинаковыми для всех вершин, будет меняться только вектор позиции. К счастью, линейная алгебра предлагает набор правил, что бы сделать нашу жизнь проще. Она говорит нам, что если дан набор матриц M0…Mn и вектор V, тогда справедливо следующее: Mn * Mn-1 * … * M0 * V = (Mn* Mn-1 * … * M0) * V Так что, если считать: N = Mn * Mn-1 * … * M0 Тогда: Mn * Mn-1 * … * M0 * V = N * V

Это значит, что мы можем подсчитать умножение N и затем отправить его в шейдер как uniform-переменную, где он будет умножаться для каждой вершины. Тем самым GPU будет считать только по одному умножению матрицы на вектор для каждой вершины.

В каком порядке должны стоять матрицы во время нахождения N? Первое что нужно запомнить, что вектор умножается на матрицу справа (в нашем случае - M0).Тогда вектор преобразуется каждой матрицей начиная с правой стороны и до левой. В 3D графике у объекта обычно сначала изменяют масштаб, затем его вращают и после перемещают, преобразуют значениями камеры и наконец находят проекцию в 2D. Давайте посмотрим, что будет, если сначала вращать объект, а только затем перемещать:

Теперь посмотрим, что будет, если сначала переместить объект, а только затем начать вращать его:

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

Теперь, когда мы начали обрабатывать более одного преобразования, необходимо избавиться от привычки обновления матрицы напрямую в функции рендера. Такой способ не слишком хорош и склонен к ошибкам. Вместо этого мы введем класс конвейера. Он будет скрывать все детали умножения матриц под простым API для изменения позиции, вращения, масштабирования и т.д. На вход идут параметры, а на выход матрица, содержащая все преобразования. Ее можно передать прямо в шейдер.

Прямиком к коду!

#define ToRadian(x) ((x) * M_PI / 180.0f)
#define ToDegree(x) ((x) * 180.0f / M_PI)

С этого урока мы начинаем использовать различные значения углов. Как и ожидалось, тригонометрические функции стандартной библиотеки C берут параметры в радианах. Макросы выше принимают параметры и в градусах и в радианах, и преобразовывают в противоположный тип.

inline Matrix4f operator*(const Matrix4f& Right) const
{
    Matrix4f Ret;
    for (unsigned int i = 0 ; i < 4 ; i++) {
	    for (unsigned int j = 0 ; j < 4 ; j++) {
			Ret.m[i][j] = m[i][0] * Right.m[0][j] +
    			      m[i][1] * Right.m[1][j] +
	    		      m[i][2] * Right.m[2][j] +
		    	      m[i][3] * Right.m[3][j];
		}
    }
	return Ret;
}

Этот оператор класса матриц умножает одну на другую. Как вы видите, мы перемножаем строки из левой матрицы с столбцами из правой. Этот оператор ключевой в нашем классе конвейера.

class Pipeline
{
    public:
        Pipeline()
        { ...  }

        void Scale(float ScaleX, float ScaleY, float ScaleZ)
        { ... }

        void WorldPos(float x, float y, float z)
        { ... }

        void Rotate(float RotateX, float RotateY, float RotateZ)
        { ... }

        const Matrix4f* GetTrans();
    private:
        Vector3f m_scale;
        Vector3f m_worldPos;
        Vector3f m_rotateInfo;
        Matrix4f m_transformation;
};

Этот класс конвейера абстрагирует детали получения всех преобразований, необходимых для одного объекта. Здесь представлены 3 private вектора, хранящих результат после каждой операции нахождения матрицы преобразованием. Кроме того, представлен API для назначения данных и функция для получения итога всех преобразований.

const Matrix4f* Pipeline::GetTrans()
{
    Matrix4f ScaleTrans, RotateTrans, TranslationTrans;
    InitScaleTransform(ScaleTrans);
    InitRotateTransform(RotateTrans);
    InitTranslationTransform(TranslationTrans);
    m_transformation = TranslationTrans * RotateTrans * ScaleTrans;
    return &m_transformation;
}

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

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

Pipeline p;
p.Scale(sinf(Scale * 0.1f), sinf(Scale * 0.1f), sinf(Scale * 0.1f));
p.WorldPos(sinf(Scale), 0.0f, 0.0f);
p.Rotate(sinf(Scale) * 90.0f, sinf(Scale) * 90.0f, sinf(Scale) * 90.0f);
glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const GLfloat*)p.GetTrans());

Эти изменения в функции рендера. Мы создаем объект конвейера, настраиваем его и отправляем результат в шейдер. Попробуйте изменить параметры и посмотрите на результат.

powered byDisqus