OpenGL ES 2.0: почему эта матрица перспективной проекции не дает правильного результата? - PullRequest
2 голосов
/ 16 октября 2011

Около 2 дней назад я решил написать код для явного вычисления матрицы Model-View-Projection ("MVP"), чтобы понять, как она работает.С тех пор у меня не было ничего, кроме проблем, по-видимому из-за матрицы проекции, которую я использую.

Работая с дисплеем iPhone, я создаю квадрат по центру экрана, описываемый этими 4 угловыми вершинами:

        const CGFloat cy = screenHeight/2.0f;
        const CGFloat z = -1.0f;
        const CGFloat dim = 50.0f;

        vxData[0] = cx-dim;
        vxData[1] = cy-dim;
        vxData[2] = z;
        vxData[3] = cx-dim;
        vxData[4] = cy+dim;
        vxData[5] = z;
        vxData[6] = cx+dim;
        vxData[7] = cy+dim;
        vxData[8] = z;
        vxData[9] = cx+dim;
        vxData[10] = cy-dim;
        vxData[11] = z;

Поскольку я использую OGLES 2.0, я передаю MVP как форму для своего вершинного шейдера, а затем просто применяю преобразование к текущей позиции вершины:

uniform mat4 mvp;
attribute vec3 vpos;
void main()
{
  gl_Position = mvp * vec4(vpos, 1.0);
}

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

CGRect screenBounds = [[UIScreen mainScreen] bounds];
const CGFloat screenWidth = screenBounds.size.width;
const CGFloat screenHeight = screenBounds.size.height;

const GLfloat n = 0.01f;
const GLfloat f = 100.0f;
const GLfloat fov = 60.0f * 2.0f * M_PI / 360.0f;
const GLfloat a = screenWidth/screenHeight;
const GLfloat d = 1.0f / tanf(fov/2.0f);


// Standard perspective projection.
GLKMatrix4 projectionMx = GLKMatrix4Make(d/a, 0.0f, 0.0f, 0.0f,
                                         0.0f, d, 0.0f, 0.0f,
                                         0.0f, 0.0f, (n+f)/(n-f), -1.0f,
                                         0.0f, 0.0f, (2*n*f)/(n-f), 0.0f);
// The one I found online.
GLKMatrix4 projectionMx = GLKMatrix4Make(2.0f/screenWidth,0.0f,0.0f,0.0f,
                                         0.0f,2.0f/-screenHeight,0.0f,0.0f,
                                         0.0f,0.0f,1.0f,0.0f,
                                         -1.0f,1.0f,0.0f,1.0f);

При использовании матрицы явных значений квадрат отображается точно так, как нужно, в центре экрана с правильным размером.При использовании матрицы перспективной проекции на экране ничего не отображается.Я сделал распечатки значений положения, сгенерированных для центра экрана (screenWidth/2, screenHeight/2, 0) матрицей перспективной проекции, и они огромны.Матрица явных значений правильно выдает ноль.

Я думаю, что матрица явных значений - это матрица ортографической проекции - это правильно?Я разочарован тем, что не могу понять, почему моя матрица перспективной проекции не работает.

Я был бы чрезвычайно признателен, если бы кто-нибудь смог мне помочь с этой проблемой.Большое спасибо.

ОБНОВЛЕНИЕ Для Кристиана Рау:

 #define Zn 0.0f
 #define Zf 100.0f
 #define PRIMITIVE_Z 1.0f

 //...

 CGRect screenBounds = [[UIScreen mainScreen] bounds];
 const CGFloat screenWidth = screenBounds.size.width;
 const CGFloat screenHeight = screenBounds.size.height;

 //...

 glUseProgram(program);

 //...

 glViewport(0.0f, 0.0f, screenBounds.size.width, screenBounds.size.height);

 //...

 const CGFloat cx = screenWidth/2.0f;
 const CGFloat cy = screenHeight/2.0f;
 const CGFloat z = PRIMITIVE_Z;
 const CGFloat dim = 50.0f;

 vxData[0] = cx-dim;
 vxData[1] = cy-dim;
 vxData[2] = z;
 vxData[3] = cx-dim;
 vxData[4] = cy+dim;
 vxData[5] = z;
 vxData[6] = cx+dim;
 vxData[7] = cy+dim;
 vxData[8] = z;
 vxData[9] = cx+dim;
 vxData[10] = cy-dim;
 vxData[11] = z;

 //...

 const GLfloat n = Zn;
 const GLfloat f = Zf;
 const GLfloat fov = 60.0f * 2.0f * M_PI / 360.0f;
 const GLfloat a = screenWidth/screenHeight;
 const GLfloat d = 1.0f / tanf(fov/2.0f);
 GLKMatrix4 projectionMx = GLKMatrix4Make(d/a, 0.0f, 0.0f, 0.0f,
                                          0.0f, d, 0.0f, 0.0f,
                                          0.0f, 0.0f, (n+f)/(n-f), -1.0f,
                                          0.0f, 0.0f, (2*n*f)/(n-f), 0.0f);

 //...

 // ** Here is the matrix you recommended, Christian:
 GLKMatrix4 ts = GLKMatrix4Make(2.0f/screenWidth, 0.0f, 0.0f, -1.0f,
                                0.0f, 2.0f/screenHeight, 0.0f, -1.0f,
                                0.0f, 0.0f, 1.0f, 0.0f,
                                0.0f, 0.0f, 0.0f, 1.0f);

 GLKMatrix4 mvp = GLKMatrix4Multiply(projectionMx, ts);

ОБНОВЛЕНИЕ 2

Новый код MVP:

GLKMatrix4 ts = GLKMatrix4Make(2.0f/screenWidth, 0.0f, 0.0f, -1.0f,
                               0.0f, 2.0f/-screenHeight, 0.0f, 1.0f,
                               0.0f, 0.0f, 1.0f, 0.0f,
                               0.0f, 0.0f, 0.0f, 1.0f);

// Using Apple perspective, view matrix generators
// (I can solve bugs in my own implementation later..!)
GLKMatrix4 _p = GLKMatrix4MakePerspective(60.0f * 2.0f * M_PI / 360.0f,
                                          screenWidth / screenHeight,
                                          Zn, Zf);
GLKMatrix4 _mv = GLKMatrix4MakeLookAt(0.0f, 0.0f, 1.0f,
                                      0.0f, 0.0f, -1.0f,
                                      0.0f, 1.0f, 0.0f);
GLKMatrix4 _mvp = GLKMatrix4Multiply(_p, _mv);
GLKMatrix4 mvp = GLKMatrix4Multiply(_mvp, ts);

В центре экрана все еще ничего не видно, и преобразованные координаты x, y центра экрана не равны нулю.

ОБНОВЛЕНИЕ 3

Использование транспонирования ts вместо приведенного выше кода работает!Но квадрат больше не кажется квадратным;теперь он имеет соотношение сторон screenHeight/screenWidth, то есть имеет более длинный размер, параллельный (короткой) ширине экрана, и более короткий размер, параллельный (длинной) высоте экрана.

Мне бы очень хотелосьзнать (а), почему требуется транспонирование и является ли это корректным исправлением, (б) как правильно исправить неквадратное измерение, и (в) как эта дополнительная матрица transpose(ts), которую мы используем, вписывается в цепочку преобразованияиз Окно просмотра * Проекция * Вид * Модель * Точка .

Для (c): Я понимаю, что матрица делает , то есть объяснение Кристиана Рау относительно того, какмы преобразуем в диапазон [-1, 1].Но правильно ли включать эту дополнительную работу в качестве отдельной матрицы преобразований, или какую-то часть нашей цепочки MVP будет выполнять эту работу вместо этого?

Искренняя благодарность Кристиану Рау за его ценный вклад до сих пор.

ОБНОВЛЕНИЕ 4

Мой вопрос о том, "как вписывается ts", не так ли? Весь смысл в том, что матрица нужна только потому, что я выбираю использоватьэкранные координаты для моих вершин;если бы я с самого начала использовал координаты в мировом пространстве, то эта работа не была бы нужна!

Спасибо Кристиан за вашу помощь, это было неоценимо :) Проблема решена.

1 Ответ

2 голосов
/ 16 октября 2011

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

Итак, поскольку ваша матрица вида модели идентична, первая матрица проекции предполагает, что координаты моделей находятся где-то в [-1,1], тогда как вторая матрица уже содержит часть масштабирования и перевода (посмотрите на значения screenWidth/Height там ) и поэтому принимает координаты ly в [0,screenWidth] x [0,screenHeight].

Таким образом, вы должны умножить матрицу проекции вправо на матрицу, которая сначала масштабируется [0,screenWidth] до [0,2] и [0,screenHeight] до [0,2], а затем переводит [0,2] в [-1,1] (используя w для screenWidth и h для screenHeight):

[ 2/w   0     0   -1 ]
[ 0     2/h   0   -1 ]
[ 0     0     1    0 ]
[ 0     0     0    1 ]

, что приведет к матрице

[ 2*d/h   0       0             -d/a        ]
[ 0       2*d/h   0             -d          ]
[ 0       0       (n+f)/(n-f)   2*n*f/(n-f) ]
[ 0       0       -1            0           ]

Итак, вы видите, что ваша вторая матрица соответствует fov 90 градусов, соотношению сторон 1: 1 и ближнему дальнему диапазону [-1,1]. Кроме того, он также инвертирует ось Y, так что начало координат находится в верхнем левом углу, что приводит к отрицанию второй строки:

[ 0   -2*d/h   0   d ]

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

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

РЕДАКТИРОВАТЬ: К вашему третьему обновлению:

(a) Транспонирование необходимо, потому что я полагаю, что ваша GLKMatrix4Make функция принимает ее параметры в формате столбца, а матрица располагается в строке.

(б) Я допустил небольшую ошибку. Вы должны изменить screenWidth в матрице ts на screenHeight (или, может быть, наоборот). На самом деле нам нужна единая шкала, потому что соотношение сторон уже учитывается проекционной матрицей.

(c) Нелегко классифицировать эту матрицу в обычный конвейер MVP. Это потому, что это не очень распространено. Давайте рассмотрим два распространенных случая рендеринга:

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

  2. 2D: при рендеринге 2d-сцены (например, GUI или HUD или просто некоторого текста) иногда требуется действительно система координат на основе экрана. Но в этом случае вы, скорее всего, будете использовать ортографическую проекцию (без какой-либо перспективы). Такая орфографическая матрица на самом деле является не чем иным, как матрицей ts (с некоторым дополнительным масштабным преобразованием для z, основанным на ближней дальности). Таким образом, в этом случае матрица принадлежит или фактически является матрицей проекции. Достаточно взглянуть на то, как старая функция glOrtho создает свою матрицу, и вы увидите ее не более чем ts.

...