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. Это потому, что это не очень распространено. Давайте посмотрим на два распространенных случая рендеринга:
3D: Когда у вас есть трехмерный мир, на самом деле не принято определять его координаты в единицах, основанных на экране, потому что не существует отображения с 3d-сцены на 2d-экран и используется система координат, где единицы равны пикселям. не имеет смысла. В этой настройке вы, скорее всего, классифицируете ее как часть матрицы вида модели для преобразования мира в другую систему единиц. Но в этом случае вам потребуются реальные трехмерные преобразования, а не только такое полуобожженное 2-мерное решение.
2D: при рендеринге 2d-сцены (например, GUI, HUD или просто текста) иногда вам действительно нужна система координат на основе экрана. Но в этом случае вы, скорее всего, будете использовать ортографическую проекцию (без какой-либо перспективы). Такая орфографическая матрица на самом деле не более чем эта
ts
матрица (с некоторым дополнительным масштабным переводом для z, основанным на ближней дальности). Таким образом, в этом случае матрица принадлежит или фактически является матрицей проекции. Просто посмотрите, как старая добрая функция glOrtho строит свою матрицу, и вы увидите, что она не более чемts
,