SDLで描く | 16.78MHz

SDLで描く

16.78MHz-sdl_sample音を出したら次は画面に映像を表示したくなるもの。Linux環境でグラフィカルなプログラムを書く方法はいくつかあるが、中でも最もシンプルなのがSDLを使う方法だ。SDLはウィジットのような高度なGUIを一切提供しておらず、開発者はSDLが用意したフレームバッファを読み書きすることで表示内容を変更する。
まずはサンプルコード
#include <stdlib.h>
#include <math.h>

#include <errno.h>

#include <SDL/SDL.h>

// 現在時刻を返す
double getTime() {
return SDL_GetTicks() * 0.001;
}

// 指定された時刻までスリープ
void waitUntil( double _until ) {
double diff = _until - SDL_GetTicks() * 0.001;
if( diff > 0.0f )
SDL_Delay( diff * 1000 );
}

// このプログラムでは16x16を1タイルとしてタイルブリットを行う
// タイルの更新フラグを作る
void initUpdateList(
SDL_Surface *_screen,
uint8_t **_udlist
) {
*_udlist = malloc( sizeof( uint8_t ) * _screen->w * _screen->h / 256 );
}

// タイルの更新フラグを捨てる
void finalUpdateList(
uint8_t *_udlist
) {
free( _udlist );
}

// タイルの更新フラグを全て下げる
void clearUpdateList(
SDL_Surface *_screen,
uint8_t *_udlist
) {
int x, y;
for( y = 0; y != _screen->h / 16; y++ )
for( x = 0; x != _screen->w / 16; x++ )
_udlist[ y * _screen->w / 16 + x ] = 0u;
}

// タイルの更新フラグを立てる
void setUpdateList(
SDL_Surface *_screen,
uint8_t *_udlist,
unsigned int _x, unsigned int _y
) {
_x /= 16;
_y /= 16;
_udlist[ _y * _screen->w / 16 + _x ] = 1u;
}

// 更新の必要な全てのタイルの更新を要求する
void update(
SDL_Surface *_screen,
uint8_t *_udlist
) {
int x, y;
for( y = 0; y != _screen->h / 16; y++ )
for( x = 0; x != _screen->w / 16; x++ )
if( _udlist[ y * _screen->w / 16 + x ] )
SDL_UpdateRect( _screen, x * 16, y * 16, 16, 16 );
}

// 点を描く
void drawPixel(
SDL_Surface *_screen,
uint8_t *_udlist,
unsigned int _x, unsigned int _y,
uint32_t _color
) {
uint32_t native_color =
SDL_MapRGB(
_screen->format,
( _color >> 16 ) & 0xFF,
( _color >> 8 ) & 0xFF,
_color & 0xFF
);
setUpdateList( _screen, _udlist, _x, _y );
// 256色だったら
if( _screen->format->BytesPerPixel == 1 )
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x ) =
(uint8_t)native_color;
// 16bitだったら
else if( _screen->format->BytesPerPixel == 2 )
*( (uint16_t*)_screen->pixels + _y * _screen->pitch / 2 + _x ) =
(uint16_t)native_color;
// 24bitだったら
else if( _screen->format->BytesPerPixel == 3 ) {
// 24bitの型というものは無いのでリトルエンディアンとビッグエンディアンで分けて処理
if( SDL_BYTEORDER == SDL_LIL_ENDIAN ) {
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x * 3 ) =
(uint8_t)native_color;
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x * 3 + 1 ) =
(uint8_t)( native_color >> 8 );
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x * 3 + 2 ) =
(uint8_t)( native_color >> 16 );
}
else {
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x * 3 + 2 ) =
(uint8_t)native_color;
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x * 3 + 1 ) =
(uint8_t)( native_color >> 8 );
*( (uint8_t*)_screen->pixels + _y * _screen->pitch + _x * 3 ) =
(uint8_t)( native_color >> 16 );
}
}
// 32bitだったら
else if( _screen->format->BytesPerPixel == 4 )
*( (uint32_t*)_screen->pixels + _y * _screen->pitch / 4 + _x ) =
(uint32_t)native_color;
else {
printf( "Unknown pixel format.\n" );
abort();
}
}

// 線を描く
void drawLine(
SDL_Surface *_screen,
uint8_t *_udlist,
unsigned int _begin_x, unsigned int _begin_y,
unsigned int _end_x, unsigned int _end_y,
uint32_t _color
) {
// 傾きを求めて
float tangent = ( (float)_end_y - (float)_begin_y ) / ( (float)_end_x - (float)_begin_x );
// 傾きが1未満だったら
if( fabsf( tangent ) < 1.0f ) {
// 切片を求めて
float intercept = (float)_end_y - tangent * (float)_end_x;
int current_x;
if( _end_x < _begin_x ) {
unsigned int temp = _end_x;
_end_x = _begin_x;
_begin_x = temp;
}
// x軸方向のピクセル数分だけ点を描く
for( current_x = _begin_x; current_x <= _end_x; current_x++ )
drawPixel( _screen, _udlist, current_x, tangent * current_x + intercept, _color );
}
// 傾きが1以上だったら
else {
// 傾きをひっくり返して x = tangent * y + intercept の形にして
tangent = 1.0f / tangent;
// 切片をもとめて
float intercept = (float)_end_x - tangent * (float)_end_y;
int current_y;
if( _end_y < _begin_y ) {
unsigned int temp = _end_y;
_end_y = _begin_y;
_begin_y = temp;
}
// y軸方向のピクセル数分だけ点を描く
for( current_y = _begin_y; current_y <= _end_y; current_y++ )
drawPixel( _screen, _udlist, current_y * tangent + intercept, current_y, _color );
}
}

// 解像度に依存せずに直線を描く
// 画面の左下が(-1.0,-1.0)、右上が(1.0,1.0)
void drawLineIndep(
SDL_Surface *_screen,
uint8_t *_udlist,
float _begin_x, float _begin_y, float _begin_z,
float _end_x, float _end_y, float _end_z,
uint32_t _color,
float *_matrix
) {
float translated_begin[ 4 ] = {
_matrix[ 0 ] * _begin_x + _matrix[ 1 ] * _begin_y +
_matrix[ 2 ] * _begin_z + _matrix[ 3 ],
_matrix[ 4 ] * _begin_x + _matrix[ 5 ] * _begin_y +
_matrix[ 6 ] * _begin_z + _matrix[ 7 ],
_matrix[ 8 ] * _begin_x + _matrix[ 9 ] * _begin_y +
_matrix[ 10 ] * _begin_z + _matrix[ 11 ],
_matrix[ 12 ] * _begin_x + _matrix[ 13 ] * _begin_y +
_matrix[ 14 ] * _begin_z + _matrix[ 15 ],
};
translated_begin[ 0 ] /= translated_begin[ 3 ];
translated_begin[ 1 ] /= translated_begin[ 3 ];
float translated_end[ 4 ] = {
_matrix[ 0 ] * _end_x + _matrix[ 1 ] * _end_y +
_matrix[ 2 ] * _end_z + _matrix[ 3 ],
_matrix[ 4 ] * _end_x + _matrix[ 5 ] * _end_y +
_matrix[ 6 ] * _end_z + _matrix[ 7 ],
_matrix[ 8 ] * _end_x + _matrix[ 9 ] * _end_y +
_matrix[ 10 ] * _end_z + _matrix[ 11 ],
_matrix[ 12 ] * _end_x + _matrix[ 13 ] * _end_y +
_matrix[ 14 ] * _end_z + _matrix[ 15 ],
};
translated_end[ 0 ] /= translated_end[ 3 ];
translated_end[ 1 ] /= translated_end[ 3 ];

if(
fabsf( translated_begin[ 0 ] ) < 1.0f && fabsf( translated_begin[ 1 ] ) < 1.0f &&
fabsf( translated_end[ 0 ] ) < 1.0f && fabsf( translated_end[ 1 ] ) < 1.0f
) {
unsigned int rast_begin[ 2 ] = {
( translated_begin[ 0 ] + 1.0f ) / 2 * _screen->w,
_screen->h - ( translated_begin[ 1 ] + 1.0f ) / 2 * _screen->h
};
unsigned int rast_end[ 2 ] = {
( translated_end[ 0 ] + 1.0f ) / 2 * _screen->w,
_screen->h - ( translated_end[ 1 ] + 1.0f ) / 2 * _screen->h
};
drawLine( _screen, _udlist, rast_begin[ 0 ], rast_begin[ 1 ], rast_end[ 0 ], rast_end[ 1 ], _color );
}
}

// 適当な3Dモデルを描く
void drawObjectIndep(
SDL_Surface *_screen,
uint8_t *_udlist,
uint32_t _color,
float *_matrix
) {
drawLineIndep( _screen, _udlist, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, _color, _matrix );
drawLineIndep( _screen, _udlist, -1.0, 0.0, 0.0, 0.0, 0.0, -1.0, _color, _matrix );
drawLineIndep( _screen, _udlist, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, -1.0, 0.0, 0.0, 0.0, -1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 0.0, 0.0, -1.0, 0.0, -1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, _color, _matrix );
drawLineIndep( _screen, _udlist, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, _color, _matrix );
}

// 行列を単位行列に初期化する
void identMatrix(
float *_matrix
) {
const static float ident[ 16 ] = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
memcpy( _matrix, ident, sizeof( float ) * 16 );
}

#define M( m, x, y ) ( m[ x + y * 4 ] )

// 2つの行列を掛けて結果を左辺に代入する
void multMatrix(
float *_left,
float *_right
) {
float temp[ 16 ];
memcpy( temp, _left, sizeof( float ) * 16 );
unsigned int x, y;
for( y = 0; y != 4; y++ )
for( x = 0; x != 4; x++ ) {
M( _left, x, y ) =
M( temp, 0, x ) * M( _right, y, 0 ) +
M( temp, 1, x ) * M( _right, y, 1 ) +
M( temp, 2, x ) * M( _right, y, 2 ) +
M( temp, 3, x ) * M( _right, y, 3 );
}
}

// OpenGL glRotate互換の回転
void rotateMatrix(
float *_matrix,
float _angle,
float _x,
float _y,
float _z
) {
float cos = cosf( _angle );
float sin = sinf( _angle );
float rotate[ 16 ] = {
_x * _x * ( 1 - cos ) + cos, _x * _y * ( 1 - cos ) - _z * sin, _x * _z * ( 1 - cos ) + _y * sin, 0.0f,
_y * _x * ( 1 - cos ) + _z * sin, _y * _y * ( 1 - cos ) + cos, _y * _z * ( 1 - cos ) - _x * sin, 0.0f,
_z * _x * ( 1 - cos ) - _y * sin, _z * _y * ( 1 - cos ) + _x * sin, _z * _z * ( 1 - cos ) + cos, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
multMatrix( _matrix, rotate );
}

// OpenGL glScale互換の拡大縮小
void scaleMatrix(
float *_matrix,
float _x,
float _y,
float _z
) {
float scale[ 16 ] = {
_x, 0.0f, 0.0f, 0.0f,
0.0f, _y, 0.0f, 0.0f,
0.0f, 0.0f, _z, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
multMatrix( _matrix, scale );
}

// フレームバッファを書き換えるためにロックする
void lockSurface( SDL_Surface *_screen ) {
if( SDL_MUSTLOCK( _screen ) ) {
if( SDL_LockSurface( _screen ) ) {
printf( "Unable to lock screen: %s\n", SDL_GetError() );
abort();
}
}
}

// フレームバッファのロックを解除する
void unlockSurface( SDL_Surface *_screen ) {
if( SDL_MUSTLOCK( _screen ) ) {
SDL_UnlockSurface( _screen );
}
}

int main() {
// SDLを初期化
if ( SDL_Init( SDL_INIT_VIDEO ) ) {
printf( "Unable to init SDL: %s\n", SDL_GetError() );
abort();
}
// フレームバッファを1つ作成
SDL_Surface *screen = SDL_SetVideoMode(1024, 768, 32, SDL_SWSURFACE );
if( !screen ) {
printf( "Unable to set screen size to 640x480: %s\n", SDL_GetError() );
abort();
}
uint8_t *udlist;
float matrix[ 16 ];
double system_time = getTime();
int cycle_count;
// タイルの更新フラグを初期化
initUpdateList( screen, &udlist );
clearUpdateList( screen, udlist );
// 60フレームで5秒間
for( cycle_count = 0; cycle_count != 60 * 5; cycle_count++ ) {
// 同次座標を初期化
identMatrix( matrix );
// ピッチ回転 20度
rotateMatrix( matrix, M_PI / 9.0f, 1.0f, 0.0f, 0.0f );
// ヨー回転 経過時間 * 180度
rotateMatrix( matrix, M_PI * cycle_count / 120.0f, 0.0f, 1.0f, 0.0f );
// 縮小 80% (ついでにアスペクト比修正)
scaleMatrix( matrix, 0.8f * 768 / 1024, 0.8f, 0.8f );
// フレームバッファをロック
lockSurface( screen );
// 3Dモデルを描画
drawObjectIndep( screen, udlist, 0xFF0000, matrix );
// フレームバッファのロックを解除
unlockSurface( screen );
// フレームバッファを更新
update( screen, udlist );
// タイルの更新フラグを消去
clearUpdateList( screen, udlist );
// 1/60秒スリープ
waitUntil( system_time + 0.016667 );
system_time = getTime();
// フレームバッファをロック
lockSurface( screen );
// 先ほどと同じモデルを黒で描く(消去する)
drawObjectIndep( screen, udlist, 0x000000, matrix );
// フレームバッファをアンロック
unlockSurface( screen );
}
// タイルの更新フラグを破棄
finalUpdateList( udlist );
}

$ gcc sdl_sample.c -lSDL -lm -o sdl_sample
$ ./sdl_sample

ワイヤーフレームの8面体が出てきて回転したら成功。重要な所をつまんでいくと、
  if ( SDL_Init( SDL_INIT_VIDEO ) ) {
SDLの他の全ての関数を呼ぶ前にまずSDLを初期化する。引数としてSDLのどの機能を使う予定かを指定する。今回は描画まわりだけを使うつもりなのでSDL_INIT_VIDEOだけを指定する。
  SDL_Surface *screen = SDL_SetVideoMode(1024, 768, 32, SDL_SWSURFACE );
1024x768ピクセルで1ピクセルあたり32bitのソフトウェア(ハードウェア上に直接確保されていなくても構わない)フレームバッファを作る
    if( SDL_LockSurface( _screen ) ) {
フレームバッファはよそのスレッドから同時に書き換えられたり、書き換え中に描画されてしまったりしないように書き換える前にロックするのがマナー。SDL_Surface::pixelsがフレームバッファへのポインタなので、ここからフレームバッファの内容を書き換え
    SDL_UnlockSurface( _screen );
ロックを解除して、
        SDL_UpdateRect( _screen, x * 16, y * 16, 16, 16 );
更新が必要な矩形範囲の更新を要求すると新しいフレームバッファの内容が画面に表示される。
ちなみにこのプログラムでは書いていないが、本当はSDL_FreeSurfaceでフレームバッファを破棄して、SDL_Quitで終了する必要がある。
SDLにはこの他にもイベントハンドラや入力デバイスの処理、サウンド、ポータブルスレッドといった機能が備わっている。詳しいことはドキュメントを参照。