На базе нашей программки вращения кубика (из прошлых статей) разберёмся с простейшей загрузкой текстур в аппаратуру и текстурированием четырёхугольника. Забудем пока про куб и цвета и вернёмся к квадрату со стороной 1 и 2D-координатам.

UV-координаты

В компьютерной графике для “закрепления” текстуры на объекте используют так называемые 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-конвейера у нас последовательно (для каждой вершины) поступает по два атрибута:

  1. 2-вектор позиции vPosition (в вертексном шейдере ось Z и W фиксируются)
  2. 2-вектор UV-координат vUV

Каждый атрибут мы запишем в отдельный VBO в высопроизводительной памяти, а затем установим соответствующий vertexAttribPointer (для vPosition и vUV), не забывая включить “локацию” в vertexAttribArray.

Данные текстуры мы тоже загрузим в графический процессор с помощью PBO (Pixel Buffer Object).

Общая картина

Общая идея примерно такова:

  1. Старт, загрузка RGB значений из файла текстуры (см. ниже)
  2. Биндинг VAO
  3. Для каждого входа вертексного шейдера (позиция, UV):
    1. Биндинг нового Array Buffer
    2. Копирование данных в GPU (в Array Buffer) из Haskell-списка
    3. Получение из микропрограммы номера “локации” соответствующего имени входа конвейера
    4. Указание для “локации” правила доступа (размерность, смещение в загруженном буфере, и т.д.)
    5. Включение локации
  4. Установка 2D-текстуры
    1. Биндинг нового PBO (Pixel Unpack Buffer)
    2. Копирование данных в GPU (в Pixel Unpack Buffer) из Haskell-типа с RGB данными
    3. (Текстура у нас одна: не трогаем activeTexture, а значит настраиваем нулевой Texture Unit)
    4. Указание правил доступа и интерпретации пикселей для 2D-текстуры текущего юнита: texImage2D (смещение в буфере, кол-во пикселей и т.п.)
    5. Указание фильтра 2D-текстуры теущего юнита
  5. Использование VAO
  6. В фрагментном шейдере для установки цвета пикселя использовать встроенную функцию 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);
}

Результат

Без вращения

С вращением