Скорость отрисовки графики без DirectX, OpenGL и пр. - как поднять?
Задача: нужно написать что-то вроде 3d движка, сильно упрощенного.. т.е. должен быть отрисован какой-то ландшафт, натянута текстура, учтено освещение (Гуро или Фонг), в сцене должен присутствовать какой-либо объект (типа автомобиль, самолет.... ), который там может перемещаться... Камера также должна перемещатся, как свободно, так и в привязке к объекту... Особенность: все это должно быть сделано "ручками", никаких DirectX, OpenGL и т.п.
С математической стороны особых проблем не возникает, матрицы соответствующих преобразований сделал, тут, все относительно просто. Ландшфат и объект сделал импортом из 3d макса в массивы вершин и граней...
Проблема начинается на стадии отрисовки: рисовать приходится попиксельно; на канве ОЧЕНЬ медленно, через WinAPI заметно быстрее, но все равно недостаточно... Причем, основные затраты времени не на аглогритмы преобразований, а именно на вывод графики (прорисовка у меня реализована как построчное сканирование треугольников, задающих объекты, алгоритм из Шикина... но даже если и взять Polygon() или Polylene то оно конечно, быстрее, но недостаточно), даже и при ограничении 800х600...
Вопрос: какие еще варианты отрисовки могут быть, быстрее???
Рисуй все в битмап в памяти. А как отрисуешь всю сцену, копируй битмап в окно.
Цитата: Archie
Рисуй все в битмап в памяти. А как отрисуешь всю сцену, копируй битмап в окно.
Уже так
Код:
BitBlt(DC, 0, 0, frm_main->Width, frm_main->Height, CDC, 0, 0, SRCCOPY)
Работают эти апи через дрова с железом, а gdi даст только базовую функциональность и исключительно софтовую.
Цитата: Tdr
Без "DirectX, OpenGL и т.п." сделаеть сложно и дорого.
Работают эти апи через дрова с железом, а gdi даст только базовую функциональность и исключительно софтовую.
Работают эти апи через дрова с железом, а gdi даст только базовую функциональность и исключительно софтовую.
Но можно? Как?
зы. реализация на С++, билдер 6
Этот раздел тебе в помощь
Рисование осуществялется очень просто. Например, рисование точки будет выглядеть так:
Код:
pRastr[ddLinearAddress + 0] = B;
pRastr[ddLinearAddress + 1] = G;
pRastr[ddLinearAddress + 2] = R;
pRastr[ddLinearAddress + 1] = G;
pRastr[ddLinearAddress + 2] = R;
где pRastr - адрес массива, ddLinearAddress - линейный адрес точки, RGB - цвет.
Данный метод имеет массу преимуществ, взять даже заполнение экрана(растра) заданным цветом - увеличение производительности может измерятся разами, если грамотно все сделать. Если есть интерес, то могу скинуть код инициализации, вывода и рисования.
Цитата: ACW-Coder
Если есть интерес, то могу скинуть код инициализации, вывода и рисования.
Есть интерес, скидывай ;)
Обычно создаю в памяти bimap с удобной мне глубиной цвета (например 8 бит), в общем случае несовпадающей с глубиной цвета текущего режма и перебрасываю его в окно функцией StretchDIBits. Если размеры в пикселях совпадают, то преобразование к нужной глубине цвета происходит практически мгновенно, думаю, железом видеокарты (судя по скорости).
1. Создание растрового буфера
Код:
BOOL fnCreateRastrBuffer(DWORD ddWidth, DWORD ddHeight)
{
//Создание буфера для информации о растре
pBitInfo = new BITMAPINFO [sizeof(BITMAPINFO)];
if(!pBitInfo) return FALSE;
//Колчество байтов в строке
ddBytesPerLine = 4 * ddWidth;
//Размер растра в байтах
ddBufferSize = ddBytesPerLine * ddHeight;
//Ширина и высота растра
ddResX = ddWidth;
ddResY = ddHeight;
/* Информация о растре */
//Размер структуры
pBitInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
pBitInfo->bmiHeader.biWidth = ddWidth;
pBitInfo->bmiHeader.biHeight = ddHeight;
//Кол-во плоскостей (всегда 1)
pBitInfo->bmiHeader.biPlanes = 1;
//Глубина цвета (24 или 32)
pBitInfo->bmiHeader.biBitCount = 32;
pBitInfo->bmiHeader.biClrUsed = 0;
pBitInfo->bmiHeader.biClrImportant = 0;
//Использование триплетов RGB
pBitInfo->bmiHeader.biCompression = BI_RGB;
//Создание растрового буфера
pRastrBuffer = new BYTE[ddBufferSize];
if(!pRastrBuffer) return FALSE;
return TRUE;
{
//Создание буфера для информации о растре
pBitInfo = new BITMAPINFO [sizeof(BITMAPINFO)];
if(!pBitInfo) return FALSE;
//Колчество байтов в строке
ddBytesPerLine = 4 * ddWidth;
//Размер растра в байтах
ddBufferSize = ddBytesPerLine * ddHeight;
//Ширина и высота растра
ddResX = ddWidth;
ddResY = ddHeight;
/* Информация о растре */
//Размер структуры
pBitInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
pBitInfo->bmiHeader.biWidth = ddWidth;
pBitInfo->bmiHeader.biHeight = ddHeight;
//Кол-во плоскостей (всегда 1)
pBitInfo->bmiHeader.biPlanes = 1;
//Глубина цвета (24 или 32)
pBitInfo->bmiHeader.biBitCount = 32;
pBitInfo->bmiHeader.biClrUsed = 0;
pBitInfo->bmiHeader.biClrImportant = 0;
//Использование триплетов RGB
pBitInfo->bmiHeader.biCompression = BI_RGB;
//Создание растрового буфера
pRastrBuffer = new BYTE[ddBufferSize];
if(!pRastrBuffer) return FALSE;
return TRUE;
2. Рисование точки
Код:
void fnSetPoint(DWORD X, DWORD Y, BYTE R, BYTE G, BYTE B)
{
//Тест отсечения
if(X < 0 || Y < 0 || X >= ddResX || Y >= ddResY) return;
//Вычисление линейного адреса точки
DWORD ddLinearAddress = Y * ddBytesPerLine + X * 4;
//Заносим интенсивности цветовых компонент
// по соответствующим смещениям
pRastrBuffer[ddLinearAddress + 2] = R;
pRastrBuffer[ddLinearAddress + 1] = G;
pRastrBuffer[ddLinearAddress + 0] = B;
}
{
//Тест отсечения
if(X < 0 || Y < 0 || X >= ddResX || Y >= ddResY) return;
//Вычисление линейного адреса точки
DWORD ddLinearAddress = Y * ddBytesPerLine + X * 4;
//Заносим интенсивности цветовых компонент
// по соответствующим смещениям
pRastrBuffer[ddLinearAddress + 2] = R;
pRastrBuffer[ddLinearAddress + 1] = G;
pRastrBuffer[ddLinearAddress + 0] = B;
}
3. Копирование в заданный контекст
Код:
void fnVisualBackScreen(HDC hDC)
{
//Копирование растра
SetDIBitsToDevice(hDC,
0, 0, ddResX, ddResY, //dst_x, dst_y, dst_w, dts_h
0, 0, 0, ddResY, //src_x, src_y, ..., num_of_copy_lines
pRastrBuffer, pBitInfo, DIB_RGB_COLORS);
}
{
//Копирование растра
SetDIBitsToDevice(hDC,
0, 0, ddResX, ddResY, //dst_x, dst_y, dst_w, dts_h
0, 0, 0, ddResY, //src_x, src_y, ..., num_of_copy_lines
pRastrBuffer, pBitInfo, DIB_RGB_COLORS);
}
А вот еще одна функция - очистка экрана. Это самый быстрый метод, который мне удалось написать. Суть в том, что при записи не используется кеш-память, а это значительно ускоряет процесс заполнения. Требования такие: поддержка SSE (у Intel - начиная с Pentium III) и выравнивание начала растра по границе двойного слова (в VS.NET 2002 устанавливается значение 'Project' Property Pages -> C/C++ -> Code Generation -> Struct Member Alignment равное 4 Bytes).
Код:
void fnClear(BYTE R, BYTE G, BYTE B)
{
DWORD ddColor;
ddColor = (R << 16) + (G << 8) + B;
__asm
{
movss xmm0, ddColor //XMM0[0] = Color
shufps xmm0, xmm0, 0 //XMM0 = Color Color Color Color
mov eax, pRastrBuffer //EAX = Адрес растрового буфера
mov ecx, ddBufferSize //ECX = Размер буфера в байтах
shr ecx, 4 //Запись будет производиться по 16 байтов
CLEAR: movntps [eax], xmm0 //Запись 4 точек БЕЗ ИСПОЛЬЗОВАНИЯ КЕША
add eax, 16 //Корректировка указателя
dec ecx
jnz CLEAR
}
}
{
DWORD ddColor;
ddColor = (R << 16) + (G << 8) + B;
__asm
{
movss xmm0, ddColor //XMM0[0] = Color
shufps xmm0, xmm0, 0 //XMM0 = Color Color Color Color
mov eax, pRastrBuffer //EAX = Адрес растрового буфера
mov ecx, ddBufferSize //ECX = Размер буфера в байтах
shr ecx, 4 //Запись будет производиться по 16 байтов
CLEAR: movntps [eax], xmm0 //Запись 4 точек БЕЗ ИСПОЛЬЗОВАНИЯ КЕША
add eax, 16 //Корректировка указателя
dec ecx
jnz CLEAR
}
}
Кстати, если еще будут специфические вопросы, то можешь задавать их напрямую - через e-mail (acwares@narod.ru). Просто я сам с конца 2004 года занимаюсь разработкой собственной графической библиотеки, думаю, что смогу что-нибудь подсказать.
ACW-Coder
Огромное спасибо!
2 ACW-Coder
Отдельная благодарность за примеры кода. Будем разбираться ;)
А вопросы, скорее всего, будут, напишу :)
у тебя в тексте есть "//Глубина цвета (24 или 32)", но в самом коде поддерживается только нестандартное, хотя и поддерживаемое Windows значение 32. Для 24 правку придется вносить сразу в несколько мест, включая более хитрое вычисление длины сканлинии. Желательно бы определить ОДНУ константу, через нее определить другую и уже эти две константы использовать во всех формулах для универсальности.
Не знаю, насколько интересны 16-цветные режимы, но 256=цветные я бы рекомендовал использовать: во многих случаях, когда не требуется много цветов, режим "байт на пиксель" оказывается очень удобным.
Коль скоро используешь 32-разрядный цвет, не забывай чистить 4-й байт. Стандарта на этот счет нет, но рекомендуется заносить в него либо 0, либо 255.
Операцию чистки буфера гораздо проще записать через rep stosd. И вряд ли это будет медленнее, зато не будет проблем в случае, когда длина буфера некратна 16.
2. "Цветастость" - это уже выбирается исходя из того, что надо получить. Мне вот привычнее 32 бита. Просто в других вычислениях с 32 битами работать значительно проще. Например, при записи в растровый буфер с учетом значений точек к буфере глубины. У меня используется обработка по 4 точки через XMM-регистры. При 24 битах такой алгоритм реализовать было бы просто невозможно.
3. В жизни 4-й байт не чистил и ни разу не стопорилось.
4. Запись через rep stosd. Раньше тоже так думал (что самое быстрое). И такое заполнение даже функионирует в первой версии. Но позже (при разработке 2 версии) провел около 10 тестов на скорость. Смотрел скорость заполнения экрана 1280x1024x32. Вот некоторые результаты (frames per second):
1) REP STOSD - 230-310
2) MOVAPS [Memory], XMM0 - 269-305
3) MOVNTPS [Memory], XMM0 - 509-548
Т.е. весь выигрышь получается в том, что не используется кеш-память. Выровнять длину буфера до 16 байт вообще не проблема. Можно при резервировании памяти к итоговому размеру буфера просто прибавить 16. Еще команды movnt** требуют выравнивания по границе двойного слова. Эта тоже абсолюно не проблема. На ассемблере это выравнивание делается в пару-тройку строк (не знаю как компилятор ms vs относится к таким фокусам, но это и не важно - там есть опция компилятора для выравнивания). Да и в конечном счете это даже положительно сказывается на быстродействии. А строковой команде до фени с чем работать: будет выравнивание - хорошо, нет - и так сойдет, просто работать будет медленнее.
2. Я, наоборот, пользуюсь этим только при глубине цвета, не превышающей 8 разрядов. И обхожуть ММХ. Для полноцветных режимов предпочитаю OpenGL. Разные задачи - разные инструменты.
3. И не будет, пока не возникнет идея записать это в файл. (хотя, кто знает, что придумают Микрософт в очередной версии Виндоуз)
4. Думаю, щдесь не то место, где надо ловить блох. Выавнивание, кстати, на ЯВУ делается одной строкой. Вот только не надо на него закладываться.
Цитата: andriano
Разные задачи - разные инструменты.
Абсолютно согласен. Вот у меня есть задача и я делаю все, чтобы максимально ускорить выполнение, отсюда и неуниверсальность. А OpenGL - это уже другой разговор, здесь, как я понимаю, все строится только на интересе сделать все это самому. Кстатити, про MMX я уже давно не вспоманал, т.к. кончилось ее время (! Visual Studio .NET 2005 почему-то отсутствует), в общем-то как и время жизни 3DNow! и FPU. Под 64-битной платформой (AMD64), под которую я сейчас пишу вторую версию либы, эти наборы команд уже просто не поддерживается, осталось только SSE. В принципе, меня это очень даже устраивает - нацонец-то приходим к единому стандарту.
Первый раз такое слышу.
И уверен, что это не более, чем первоапрельская шутка.
PS. И то и другое, кстати, и есть "единый стандарт".
В общем, переписал по образцу ACW-Coder
Теперь всплыла другая проблема. Сама по себе отрисовка идет с приемлемой скоростью... Теперь главный висяк - в самом алгоритме растровой развертки треугольника, причем особо нет разницы в скорости при равномерной закрасе, по Гуро, или Фонга... Тормозит сам алгоритм...
Можно это дело какнить оптимизировать?