OpenGL ES 2.0: почему эта матрица перспективной проекции не дает правильного результата?

Около 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) что мы используем вписывается в цепочку преобразования Viewport * Projection * View * Model * Point.

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

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

ОБНОВЛЕНИЕ 4

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

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

1 ответ

Решение

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

Итак, поскольку ваша матрица вида модели является тождественной, первая матрица проекции предполагает, что координаты моделей находятся где-то в [-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-мерную графику (например, элементы GUI или HUD), и в этом случае вы все равно используете более простую ортогональную матрицу проекции, которая является не чем иным, как упомянутой выше матрицей масштабного перевода без перспективная сложность.

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

(а) Транспонирование требуется, потому что я думаю, что ваш GLKMatrix4Make Функция принимает свои параметры в формате столбца-мажора, а матрицу вы размещаете построчно.

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

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

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

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

Другие вопросы по тегам