OpenGL в Haskell: загрузка текстуры в GPU, UV-координаты
На базе нашей программки вращения кубика (из прошлых статей) разберёмся с простейшей загрузкой текстур в аппаратуру и текстурированием четырёхугольника. Забудем пока про куб и цвета и вернёмся к квадрату со стороной 1 и 2D-координатам.
UV-координаты⌗
В компьютерной графике для “закрепления” текстуры на объекте используют так называемые UV-координаты.
Размеры текстуры нормализуются до [0..1] по оси X и Y, и каждая вершина треугольников, из которых состоит объект, мапится к пикселю картинки как показано выше. Само название осей “UV” используется просто потому, что буквы X и Y уже как бы зарезервированы для абсциссы и ординаты объекта.
Таким образом, для нанесения текстуры на объект (пока ограничимся квадратом) про каждую вершину, помимо её пространственных координат, нужно сообщить также и её UV-координаты:
rectVertices :: [Vertex2 GLfloat]
rectVertices =
[ Vertex2 (-0.5) (0.5)
, Vertex2 (0.5) (0.5)
, Vertex2 (0.5) (-0.5)
, Vertex2 (-0.5) (-0.5)
]
rectUVs :: [Vertex2 GLfloat]
rectUVs =
[ Vertex2 0 1
, Vertex2 1 1
, Vertex2 1 0
, Vertex2 0 0
]
UV-координаты будут использованы в фрагментном шейдере при вызове функции texture()
(об этом ниже).
Передача данных⌗
Фактически, на вход GL-конвейера у нас последовательно (для каждой вершины) поступает по два атрибута:
- 2-вектор позиции
vPosition
(в вертексном шейдере ось Z и W фиксируются) - 2-вектор UV-координат
vUV
Каждый атрибут мы запишем в отдельный VBO в высопроизводительной памяти, а затем установим соответствующий vertexAttribPointer
(для vPosition
и vUV
), не забывая включить “локацию” в vertexAttribArray
.
Данные текстуры мы тоже загрузим в графический процессор с помощью PBO (Pixel Buffer Object).
Общая картина⌗
Общая идея примерно такова:
- Старт, загрузка RGB значений из файла текстуры (см. ниже)
- Биндинг VAO
- Для каждого входа вертексного шейдера (позиция, UV):
- Биндинг нового Array Buffer
- Копирование данных в GPU (в Array Buffer) из Haskell-списка
- Получение из микропрограммы номера “локации” соответствующего имени входа конвейера
- Указание для “локации” правила доступа (размерность, смещение в загруженном буфере, и т.д.)
- Включение локации
- Установка 2D-текстуры
- Биндинг нового PBO (Pixel Unpack Buffer)
- Копирование данных в GPU (в Pixel Unpack Buffer) из Haskell-типа с RGB данными
- (Текстура у нас одна: не трогаем
activeTexture
, а значит настраиваем нулевой Texture Unit) - Указание правил доступа и интерпретации пикселей для 2D-текстуры текущего юнита:
texImage2D
(смещение в буфере, кол-во пикселей и т.п.) - Указание фильтра 2D-текстуры теущего юнита
- Использование VAO
- В фрагментном шейдере для установки цвета пикселя использовать встроенную функцию
texture()
вместе с дефольтным 2D-сэмплером и UV-координатами.
Загрузка текстуры из файла⌗
OpenGL ничего не знает о JPEG, PNG и т.п., а поэтому мы должны преобразовать картинку из файла изображения в линейный массив сырых байтов: значений R,G и B в памяти (оттуда потом будем копировать в GPU). Для загрузки текстуры из JPG или PNG файла воспользуемся модулем Codec.Image.STB
. В инициализации GLUTAbstraction
:
e <- loadImage "./crate.jpg"
case e of
Left s -> error s
Right bm -> do
putStrLn $ "Loaded image: " ++ show bm
descriptor <- pipelineSetup bm
depthFunc $= Just Less
displayCallback $= display descriptor
specialCallback $= Just (specKeyDown descriptor)
В pipeLineSetup
передаём чистые RGB-данные для каждого пикселя JPEG-картинки.
Изменения в pipeLineSetup⌗
Немного обобщим функцию загрузки и настройки вершинных данных:
setupArrayBuffer :: Storable a => Program -> [a] -> Int -> String -> IO ()
setupArrayBuffer program dat dim atname =
-- VBO, copy the position data
withArray dat $ \ptr -> do
-- bind new buffer
buf <- genObjectName
bindBuffer ArrayBuffer $= Just buf
-- transfer the data
let size = fromIntegral (numEntries dat * entrySize dat)
bufferData ArrayBuffer $= (size, ptr, StaticDraw)
-- point shader atname
atloc <- get $ attribLocation program atname
vertexAttribPointer atloc $=
(ToFloat,
VertexArrayDescriptor (fromIntegral dim) Float 0 (bufferOffset 0))
-- enable the location (a block of bytes in the VBO via the above pointer)
vertexAttribArray atloc $= Enabled
Функция загрузки и настройки текстуры также повторяет описанный выше алгоритм. Для буферизации задаём размер данных в байтах (из объекта Bitmap), для texImage2D
– уже в пикселях:
setupTexture :: Bitmap Word8 -> IO ()
setupTexture bm =
withBitmap bm $ \(w,h) nchn padding ptr -> do
-- bind PBO (so the bufferData PixelUnpackBuffer) has a target
pbo <- genObjectName
bindBuffer PixelUnpackBuffer $= Just pbo
-- transfer the data into pixel unpack buffer
bufferData PixelUnpackBuffer $= (fromIntegral (bitmapSizeInBytes bm), ptr, StaticDraw)
-- specify the texture for the current texture unit (specified with activeTexture)
texImage2D Texture2D NoProxy 0 RGB8
(TextureSize2D (fromIntegral w) (fromIntegral h)) 0
-- we would use last parameter as direct bitmap data (i.e. 'ptr' from above)
-- but we've loaded something in PBO (PixelUnpackBuffer), so the parameter
-- becomes an offset into the loaded pixel unpack buffer
(PixelData RGB UnsignedByte (bufferOffset 0))
textureFilter Texture2D $= ((Nearest, Nothing), Nearest)
Вот, собственно, и всё. Наша функция инициализации принимает такой вид:
pipelineSetup :: Bitmap Word8 -> IO Descriptor
pipelineSetup bm = do
-- rotation angles ioref
avref <- newIORef $ Vector3 0 0 (0 :: Float)
-- compile and link shader program
program <- loadShaders [
ShaderInfo VertexShader (FileSource "vertex.glsl"),
ShaderInfo FragmentShader (FileSource "fragment.glsl")]
currentProgram $= Just program
-- bind new VAO
vao <- genObjectName
bindVertexArrayObject $= Just vao
-- bind and setup new VBO
setupArrayBuffer program rectVertices 2 "vPosition"
-- bind and setup new VBO
setupArrayBuffer program rectUVs 2 "vUV"
-- bind and setup new PBO, specify and enable the 2d texture
setupTexture bm
Шейдеры⌗
В фрагментном шейдере используем 2D-сэмплер, в нулевом (дефолтном) юните которого применяется активная 2D-текстура:
#version 420 core
in vec2 UV;
out vec4 outColor;
uniform sampler2D samp;
void
main()
{
outColor = texture(samp, UV);
}