суббота, 10 декабря 2011 г.

Некоторые базовые понятия OpenGL


Область (объем) отсечения определяется минимальным и максимальным значениями пространственных (логических декартовых) координат, которые видно в окне.
Поле просмотра - это область внутри клиента окна, которое используется для рисования области отсечения. Измеряется в пикселах или реальных экранных координатах. Поле просмотра обычно эквивалентно области окна, но не всегда. С помощью поля просмотра можно сжимать или растягивать изображения внутри окна, а также отоьражать только часть области отсечения (в этом случае поле просмотра задается больше чем область клиента окна).
Есть два типа отображения трехмерной области отсечения на поле просмотра: это ортографическая и перспективная проекции.
glViewport - используется для задания поля просмотра в окне.
glOrtho - устанавливает границы объема отсечения.
Между заданием вершин и появлением их на экране происходит три типа преобразований:
1)наблюдения;
2)модели;
3)проецирования;
Терминология преобразований OpenGL
1)преобразование наблюдения - задает положение камеры;
2)преобразование модели - перемещает объекты по сцене;
3)преобразование наблюдения модели - описывает "дуализм" преобразований наблюдения и ммодели;
4)преобразование проецирования - задает наблюдаемый объем;
5)преобразование поля просмотра - псевдопреобразование, масштабирующее конечный результат согласно размерам окна.
Координаты наблюдения отсчитываются от точки, в которой расположен глаз наблюдателя (эти координаты можно рассматривать как абсолютные экранные координаты). Все преобразования описываются с точки зрения их эффекта в системе координат наблюдения. При отсутствии преобразований система координат наблюдения такова, что камера находится в начале координат и смотрит по направлению отрицательных z. Верх камеры ориентирован в этом случае в сторону положительных у.
Проекция модели (modelview) означает, что это преобразование можно считать либо преобразованием модели либо преобразованием проецирования. Разницы между ними для конечного результата нет.
Тоже касается преобразования наблюдения модели.
Исходное преобразование дает точку отсчета для других преобразований.
Конвейер преобразований вершины
1)исходные данные по вершине:
2)матрица наблюдения модели;
3)преобразованные координаты наблюдения;
4)матрица проекции;
5)координаты отсечения;
6)перспективное деление и преобразование поля просмотра;
7)координаты окна;
glFrustum - трехмерный аналог gluOrtho2D. Создает матрицу перспективной проекции и умножает ее на текущую. Наблюдатель расположен в (0,0,0) и смотрит в отрицательном направлении z.
gluLookAt - создает видовую матрицу. Определяет преобразование наблюдения на основе положения наблюдателя, центра сцены и вектора, указывающего "вверх" от наблюдателя.
gluPerspective - создает матрицу наблюдения перспективной проекции, описывающую наблюдаемый объем (усеченная пирамида) в глобальных координатах. Аспект должен соответствовать характеристическому отношению поля просмотра (задается glViewPort)

пятница, 9 декабря 2011 г.

Кватернион и матрица вращения OpenGL

Кваренион - это такая четырехкомпонентная сущность, с помощью которой можно задавать вращения в трехмерном пространстве. Их преимущество перед матрицами, соответствующим этим поворотам, состоит в том, что для них, кватернионов, нет так называемой "шарнирной блокировки" и что вычисления кватернионной математики менее ресурсоемки, чем для соответствующих матричных операций. Но в OpenGL пока еще манипулируют поворотами в пространстве с помощью матриц. Посмотрим, как соотносятся кватернион и соответствующая ему матрица, задающая поворот.
Кватернион представляется в виде черырех чисел x, y, z, w:

где:
угол альфа выражается в радианах, а е - это единичный вектор, вокруг которого совершается вращение.
Вектор v переводится вращением в вектор v' с помощью кватерниона так:

Матрица поворота выражается через компоненты кватерниона следующим способом:

Не забываем, что в OpenGL матрицы разворачиваются по столбцам, а не построкам. Таким образом, если матрицу обозначаем через matrixOfRotation, то, используя синтаксис языка си мы ее элементы запишем следующим образом:


matrixOfRotation[0] = 1.0f - 2.0f*( y*y + z*z );
matrixOfRotation[1] = 2.0f * ( x*y + z*w);
matrixOfRotation[2] = 2.0f * ( x*z - y*w);
matrixOfRotation[3] = 0.0f;
matrixOfRotation[4] = 2.0f * ( x*y - z*w );
matrixOfRotation[5] = 1.0f - 2.0f*( x*x + z*z );
matrixOfRotation[6] = 2.0f * ( z*y + x*w );
matrixOfRotation[7] = 0.0f;
matrixOfRotation[8] = 2.0f * ( x*z + y*w );
matrixOfRotation[9] = 2.0f * ( y*z - x*w );
matrixOfRotation[10] = 1.0f - 2.0f*( x*x + y*y );
matrixOfRotation[11] = 0.0f;
matrixOfRotation[12] = 0;
matrixOfRotation[13] = 0;
matrixOfRotation[14] = 0;
matrixOfRotation[15] = 1.0f;


четверг, 27 октября 2011 г.

Создание видеоролика из галереи jpg-файлов и обратная операция


Создать галерею изображений из видеофайла можно утилитой mplayer:
$mplayer -vo jpeg -vf framestep=5 yourfile.mpg

вместо -vf framestep=5 можно использовать что-то вроде этого: -sstep 30 -frames 10, но эта конструкция не всегда работает. Чем меньше параметр у framestep, тем больше будет выведено картинок.
Обратная операция, а именно создание видеофайла из галереи файлов jpg, осуществима с помощью утилиты mencoder:
$mencoder "mf://*.jpg" -mf fps=0.5 -o yourfile.avi -ovc lavc -lavcopts vcodec=msmpeg4:vbitrate=1800

Чем меньше параметр fps, тем дольше в созданном видео будет показываться отдельная картинка.
Все команды запускались из командной строки в Debian Squeeze.
Добавить звуковое сопровождение в созданный ролик:
$mencoder -ovc copy -audiofile soundtrack.mp3 -oac copy videoWithoutSound.avi -o videoWithSound.avi

Удалить:
$mencoder -ovc copy -nosound videoWithSound.avi -o videoWithoutSound.avi

воскресенье, 23 октября 2011 г.

Создание простейшего приложения с графическим интерфейсом, используя Glade и gtkmm

Создание графического интерфейса с помощью Glade и gtk+ было рассмотрено в этом же блоге ранее. Вот ссылка. В данном посте заменим gtk+ на gtkmm.
Логика выполнения программы остается той же:
1)создается "xml-объект" (это чисто мой термин, возможно неудачный) из файла, созданного в интуитивно понятном графическом редакторе Glade;
2)из этого объекта "вытягиваются" имена элементов графического интерфейса, с которыми мы будем оперировать в коде;
3)из него же вытягиваем имена обработчиков сигналов для этих элементов и привязываем их каждого к своему элементу;
4)пишем код, реализующий эти обработчики;
Теперь все это в картинках:
Создаем главное окно приложения:

Придумываем ему какой-нибудь заголовок:

Создаем место для меню (в нашем случае оно и не надо для наших целей, но просто для красоты):

Добавляем это самое меню (красный кружок слева) и удаляем одно из мест для кнопок (красный кружок справа), так как у нас будет одна только кнопка, для выхода:

Добавляем в оставшееся место для кнопок кнопку из стандартного набора (это будет quit):

И добавляем обработчик сигнала (в нашем случае щелчок мыши) к созданной кнопке на вкладке Signals:

Сохраняем файл. Обратите внимание на File format в левом нижнем углу:

Теперь создаем файл hello.cpp:

Выделенные части должны иметь свои соответствия в предыдущих уже описанных нами действиях.
Компилируем:
$g++ hello.cpp `pkg-config gtkmm-2.4 libglademm-2.4 --cflags --libs`
и запускаем на выполнение:
$a.out
Должно получиться что-то вроде этого:

Все описанные действия выполнялись на Debian Squeeze.
Для облегчения изучения вот код не картинкой, а текстом:
#include <libglademm/xml.h>
#include <gtkmm.h> 
#include <iostream>
Gtk::Dialog* pDialog = 0;

void
button1_clicked_cb(){
 if(pDialog)
 pDialog->hide(); //hide() will cause main::run() to end.
}

int
main (int argc, char **argv){
 Gtk::Main kit(argc, argv);
 //Load the Glade file and instiate its widgets:
 Glib::RefPtr <Gnome::Glade::Xml>refXml;
 try{
      refXml = Gnome::Glade::Xml::create("hello.glade");
 }
 catch(const Gnome::Glade::XmlError& ex){
      std::cerr << ex.what() << std::endl;
      return 1;
 }
 //Get the Glade-instantiated Dialog:
 refXml->get_widget("dialog1", pDialog);
 if(pDialog){
      //Get the Glade-instantiated Button, and connect a signal handler:
      Gtk::Button* pButton = 0;
      refXml->get_widget("button1", pButton);
      if(pButton){
          pButton->signal_clicked().connect( sigc::ptr_fun(button1_clicked_cb) );
      }
      kit.run(*pDialog);
 }
 return 0;
}

Красивых интерфейсов вам.

четверг, 20 октября 2011 г.

Простейшие примеры использования CUDA

Что такое CUDA? Смотрите в википедии.
Рассмотрим коротко простейшие примеры использования этой интересной технологии. Этот пост является конспектом презентации Introduction to CUDA C автора Jason Sanders на конференции NVIDIA GTC 2010 - GPU Technology Conference. (Pre-Conference Tutorials, GTC 2010 - ID 2131).
Итак. Самая базовая терминология:
Host - CPU и его память (host memory);
Device - GPU и его память (device memory);
Начнем с “Hello, World!”. А куда ж без него?
Код делится на тот, что выполняется на CPU и тот что выполняется на GPU. Следующий самый просто пример, это программа, которая выводит приветствие миру и вызывает функцию из GPU, которая ничего не делает:

__global__ void kernel( void ) {
}

int main( void ) {
kernel<<< 1, 1 >>>();
printf( "Hello, World!\n" );
return 0;
}

Ключевое слово __global__ в объявлении и определении функции обозначает, что функция выполняется на графической карте. И что эта функция вызывается из CPU. Неизменным аттрибутом такой функции являются скобки <<< >>> при вызове этой функции. Аргументы внутри этих скобок обсудим ниже. Компилятор nvcc разделяет исходный код на части, которые выполняются отдельно на хосте и девайсе. Код, сгенерированный для таких функций как kernel из данного примера, выполняется на девайсе. Кстати, как я понял, device - это не обязательно GPU.
Рассмотрим более сложный пример. CPU передает некие величины в GPU для сложения их там.
__global__ void add( int *a, int *b, int *c ) {
*c = *a + *b;
}

int main( void ) {
int a, b, c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии of a, b, c
int size = sizeof( int );
//выделяем память для device копий для a, b, c
cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, size );
a = 2;
b = 7;
// копируем ввод на device
cudaMemcpy( dev_a, &a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, &b, size, cudaMemcpyHostToDevice );
// запускаем add() kernel на GPU, передавая параметры
add<<< 1, 1 >>>( dev_a, dev_b, dev_c );
// copy device result back to host copy of c
cudaMemcpy( &c, dev_c, size, cudaMemcpyDeviceToHost );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
}

Базовый CUDA API для работы с памятью GPU (и девайса вообще):
cudaMalloc();
cudaFree();
cudaMemcpy();
Их Си аналоги: malloc(), free(), memcpy().
__global__ void add( int *a, int *b, int *c ) {
c[blockIdx.x] = a[blockIdx.x] + b[blockIdx.x];
}

#define N 512
int main( void ) {
int *a, *b, *c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии a, b, c
int size = N * sizeof( int );
//выделяем память для device копий a, b, c
cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, size );
a = (int*)malloc( size );
b = (int*)malloc( size );
c = (int*)malloc( size );
random_ints( a, N );
random_ints( b, N );
// копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
// launch add() kernel with N parallel blocks
add<<< N, 1 >>>( dev_a, dev_b, dev_c );
// копируем результат работы device обратно на host - копию c
cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
}

Главное отличие этого кода от предыдущих это аргумент N в вызове функции add<<< N, 1 >>>. Он обозначает число так называемых блоков, на которые разбивается код на девайсе при распараллеливании. Обращаться к отдельному блоку можно через индекс массива блоков blockIdx.x (это встроенная переменная). Блоки, в свою очередь могут разбиваться на нити (threads)
Перепишем наш код для использования нитей, а не блоков (будем использовать не N блоков по одной нити, а один блок с N нитями):
__global__ void add( int *a, int *b, int *c ) {
c[ threadIdx.x ] = a[ threadIdx.x ] + b[ threadIdx.x ];
// blockIdx.x blockIdx.x blockIdx.x
}

#define N 512
int main( void ) {
int *a, *b, *c; //host копии a, b, c
int *dev_a, *dev_b, *dev_c; //device копии of a, b, c
int size = N * sizeof( int );
//выделяем память для копий a, b, c
cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, size );
a = (int*)malloc( size );
b = (int*)malloc( size );
c = (int*)malloc( size );
random_ints( a, N );
random_ints( b, N );
// копируем ввод device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
// запускаем на выполнение add() kernel с N нитями в блоке
add<<< 1, N >>>( dev_a, dev_b, dev_c );
// N, 1
// копируем результат работы device обратно на host (копия c)
cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
}

Теперь рассмотрим код, который использует и несколько блоков и несколько нитей в каждом блоке:
__global__ void add( int *a, int *b, int *c ) {
int index = threadIdx.x + blockIdx.x * blockDim.x;
c[index] = a[index] + b[index];
}

#define N (2048*2048)
#define THREADS_PER_BLOCK 512
int main( void ) {
int *a, *b, *c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии of a, b, c
int size = N * sizeof( int );
//выделяем память на device для of a, b, c
cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, size );
a = (int*)malloc( size );
b = (int*)malloc( size );
c = (int*)malloc( size );
random_ints( a, N );
random_ints( b, N );
//копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
//запускаем на выполнение add() kernel с блоками и нитями
add<<< N/THREADS_PER_BLOCK, THREADS_PER_BLOCK >>>( dev_a, dev_b, dev_c );
// копируем результат работы device на host ( копия c )
cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
}

Нити на первый взгляд кажутся избыточными. Параллельные нити, в отличие параллельных блоков, имеют механизмы для, так называемых, коммуникации и синхронизации. Давайте рассмотрим этот вопрос.
Возьмем в качестве примера на этот раз не сложение, а скалярное произведение.
Нити в одном блоке могут иметь общую память shared memory. Эта память декларируется в коде ключевым словом __shared__. Эта память не видна в другом блоке.
Также нити можно "синхронизировать". То есть каждая нить в некой "точке синхронизации" будет ждать пока выполнение других нитей тоже не достигнет этой точки. Синхронизация нитей истинна только для нитей одного блока. Синхронизация вводиться функцией __syncthreads().
Вот код, с синхронизацией и использованием shared memory:
//__global__ void dot( int *a, int *b, int *c )   {
// Каждая нить вычисляет одно слагаемое скалярного произведения
// int temp = a[threadIdx.x] * b[threadIdx.x];
//Не может вычислить сумму
//Каждая копия ‘temp’ приватна для своей нити
//}

__global__ void dot( int *a, int *b, int *c ) {
__shared__ int temp[N];
temp[threadIdx.x] = a[threadIdx.x] * b[threadIdx.x];
__syncthreads();
if( 0 == threadIdx.x ) {
int sum = 0;
for( int i = 0; i < N; i++ )
sum += temp[i];
*c = sum;
}
}

#define N 512
int main( void ) {
int *a, *b, *c;
int *dev_a, *dev_b, *dev_c;
int size = N * sizeof( int );

cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, sizeof( int ) );
a = (int *)malloc( size );
b = (int *)malloc( size );
c = (int *)malloc( sizeof( int ) );
random_ints( a, N );
random_ints( b, N );
// копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
//запускаем на выполнение dot() kernel с 1 блоком и N нитями
dot<<< 1, N >>>( dev_a, dev_b, dev_c );
//копируем результат работы device на host копией c
cudaMemcpy( c, dev_c, sizeof( int ) , cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
}

threadIdx.x используется для доступе к отдельной нити. threadIdx - это встроенная переменная. Для индексации вводимых/выводимых элементов используем выражение (threadIdx.x + blockIdx.x * blockDim.x).blockDim.x - это встроенная переменная, обозначающая число нитей в блоке.
В последнем примере использовался только один блок. А если нам надо использовать много блоков? Скалярное произведение же у нас тут только для примера.
__global__ void dot( int *a, int *b, int *c ) {
__shared__ int temp[THREADS_PER_BLOCK];
int index = threadIdx.x + blockIdx.x * blockDim.x;
temp[threadIdx.x] = a[index] * b[index];
__syncthreads();
if( 0 == threadIdx.x ) {
int sum = 0;
for( int i = 0; i < THREADS_PER_BLOCK; i++ )
sum += temp[i];
*c += sum;
}
}

В таком варианте функции dot у нас возникает race condition. Что это? Не тема этого поста. Смотрим википедию. С этой проблемой боремся с помощью атомарных функций. В данном случае - это atomicAdd.
__global__ void dot( int *a, int *b, int *c ) {
__shared__ int temp[THREADS_PER_BLOCK];
int index = threadIdx.x + blockIdx.x * blockDim.x;
temp[threadIdx.x] = a[index] * b[index];
__syncthreads();
if( 0 == threadIdx.x ) {
int sum = 0;
for( int i = 0; i < THREADS_PER_BLOCK; i++ )
sum += temp[i];
atomicAdd( c , sum );
}
}

Функция main будет такая:
#define N (2048*2048)
#define THREADS_PER_BLOCK 512
int main( void ) {
int *a, *b, *c; // host копии a, b, c
int *dev_a, *dev_b, *dev_c; // device копии a, b, c
int size = N * sizeof( int );
//выделяем место в памяти для device копий a, b, c
cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, sizeof( int ) );
a = (int *)malloc( size );
b = (int *)malloc( size );
c = (int *)malloc( sizeof( int ) );
random_ints( a, N );
random_ints( b, N );
//копируем ввод на device
cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );
//запускаем на выполнение dot() kernel
dot<<< N/THREADS_PER_BLOCK, THREADS_PER_BLOCK >>>( dev_a, dev_b, dev_c );
//копируем результат работы device обратно на host (копия c)
cudaMemcpy( c, dev_c, sizeof( int ) , cudaMemcpyDeviceToHost );
free( a ); free( b ); free( c );
cudaFree( dev_a );
cudaFree( dev_b );
cudaFree( dev_c );
return 0;
}

CUDA предоставляет атомарные функции, которые гарантируют "безопасность" read-modify-write операции.
Пример атомарных функций в CUDA:
atomicInc()
atomicAdd()
atomicDec()
atomicSub()
atomicExch()
atomicMin()
atomicCAS()
atomicMax()
С помощью этих примеров продемонстрированы приемы, которые можно использовать для применения в параллельных вычислениях на видеокартах NVIDIA.