2022/05/29(日) [n年前の日記]
#1 [xscreensaver] xscreensaver用のスクリーンセーバを作りたい。その2
_昨日
に続いて、xscreensaver用のスクリーンセーバを自分で作れないか実験中。
_README.hacking に、新しいスクリーンセーバを作る際に意識することが書かれてあるので、眺めながら作業。
環境は Ubuntu Linxu 20.04 LTS。
とりあえず、hacks/ディレクトリ内で、deluxe.c、deluxe.man、config/deluxe.xml をコピーして、helloxsaver.c、helloxsaver.man、config/helloxsaver.xml の3ファイルを作ったけれど。まだ中身が deluxe.c のままだし、deluxe.c は色々と複雑な処理をしているように見えるので、ガシガシ削って、ダブルバッファ描画処理の骨の部分だけを残して、更に、画面の中でボール(円)が跳ね回るだけの処理を書いてみた。
実行結果は以下のような感じ。どこでもよく見かけるアレ。
_README.hacking に、新しいスクリーンセーバを作る際に意識することが書かれてあるので、眺めながら作業。
環境は Ubuntu Linxu 20.04 LTS。
とりあえず、hacks/ディレクトリ内で、deluxe.c、deluxe.man、config/deluxe.xml をコピーして、helloxsaver.c、helloxsaver.man、config/helloxsaver.xml の3ファイルを作ったけれど。まだ中身が deluxe.c のままだし、deluxe.c は色々と複雑な処理をしているように見えるので、ガシガシ削って、ダブルバッファ描画処理の骨の部分だけを残して、更に、画面の中でボール(円)が跳ね回るだけの処理を書いてみた。
実行結果は以下のような感じ。どこでもよく見かけるアレ。
◎ ソースファイル。 :
ソースは以下。長い。長いけど、「ball」で検索すれば、肝心の処理部分だけがなんとなく見えてくるかなと。それ以外はダブルバッファ描画関係の処理なので、極端な話、気にしなくいい。ダブルバッファ処理をしたいなら、まるっとコピペして、後は独自の処理だけを書き加えればいいのではなかろうかと。
_helloxsaver.c
マニュアルファイル(.man)と、設定画面定義ファイル(.xml) は以下。
_helloxsaver.man
_helloxsaver.xml
make でビルド。sudo make install でインストールして、xscreensaverの設定画面を出して動作確認。
_helloxsaver.c
/* xscreensaver, Copyright (c) 2022 YOURNAME <YOURNAME@example.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
#include <math.h>
#include "screenhack.h"
#include "alpha.h"
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
#include "xdbe.h"
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
#define PI 3.141592
/* degree to radian */
static double deg2rad(double angle)
{
return (angle * PI / 180.0);
}
/* screensaver state */
struct state
{
Display *display;
Window window;
int nplanes;
unsigned long base_pixel;
unsigned long *plane_masks;
Bool monochrome;
Bool use_dbuf;
int delay;
int speed;
double x, y; /* ball position */
double dx, dy; /* ball direction */
int w, h; /* ball width, height */
GC gc; /* graphics context */
XWindowAttributes xgwa;
XColor color;
GC erase_gc;
/* double-buffer to reduce flicker */
Pixmap backbuf;
Pixmap ba;
Pixmap bb;
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
Bool dbeclear_p;
XdbeBackBuffer backb;
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
};
/* initialize ball position */
static void
init_ball_pos(struct state *st)
{
int sw, sh;
sw = st->xgwa.width; /* window width */
sh = st->xgwa.height; /* window height */
/* set width and height */
st->w = st->h = ((sw > sh) ? sh : sw) / 16;
/* set position */
st->x = (random() % (sw / 2)) + (sw / 4);
st->y = (random() % (sh / 2)) + (sh / 4);
/* set direction */
{
double rad;
rad = deg2rad(random() % 360);
st->dx = st->speed * cos(rad);
st->dy = st->speed * sin(rad);
}
}
/*
* init screensaver.
* Return an object holding your global state.
*/
static void *
helloxsaver_init(Display *display, Window window)
{
struct state *st = (struct state *)calloc(1, sizeof(*st));
st->display = display;
st->window = window;
/* get parameter */
st->delay = get_integer_resource(st->display, "delay", "Integer");
st->speed = get_integer_resource(st->display, "speed", "Speed");
st->use_dbuf = get_boolean_resource(st->display, "doubleBuffer", "Boolean");
st->monochrome = get_boolean_resource(st->display, "mono", "Boolean");
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
st->dbeclear_p = get_boolean_resource(st->display, "useDBEClear", "Boolean");
#endif
#ifdef HAVE_JWXYZ /* Don't second-guess Quartz's double-buffering */
st->use_dbuf = False;
#endif
/* get window attributes */
XGetWindowAttributes(st->display, st->window, &st->xgwa);
/* define colors */
st->color.pixel = get_pixel_resource(st->display, st->xgwa.colormap,
"foreground", "Foreground");
if (st->use_dbuf)
{
/* init double buffer */
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
if (st->dbeclear_p)
st->backbuf = xdbe_get_backbuffer(st->display, st->window, XdbeBackground);
else
st->backbuf = xdbe_get_backbuffer(st->display, st->window, XdbeUndefined);
st->backb = st->backbuf;
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
if (!st->backbuf)
{
/* create buffer A, B */
st->ba = XCreatePixmap(st->display, st->window, st->xgwa.width, st->xgwa.height, st->xgwa.depth);
st->bb = XCreatePixmap(st->display, st->window, st->xgwa.width, st->xgwa.height, st->xgwa.depth);
st->backbuf = st->ba;
}
}
else
{
st->backbuf = st->window;
}
/* init graphics context. use erase. */
{
XGCValues gcv;
gcv.foreground = get_pixel_resource(st->display, st->xgwa.colormap,
"background", "Background");
st->erase_gc = XCreateGC(st->display, st->backbuf, GCForeground, &gcv);
}
/* clear buffer A, B */
if (st->ba)
XFillRectangle(st->display, st->ba, st->erase_gc, 0, 0, st->xgwa.width, st->xgwa.height);
if (st->bb)
XFillRectangle(st->display, st->bb, st->erase_gc, 0, 0, st->xgwa.width, st->xgwa.height);
/* initialize ball position */
init_ball_pos(st);
/* init graphics context */
{
XGCValues gcv;
unsigned long flags;
flags = GCForeground;
gcv.foreground = st->color.pixel;
gcv.line_width = 2;
gcv.cap_style = CapProjecting;
gcv.join_style = JoinMiter;
flags |= (GCLineWidth | GCCapStyle | GCJoinStyle);
st->gc = XCreateGC(st->display, window, flags, &gcv);
}
return st;
}
/* Draw a single frame */
static unsigned long
helloxsaver_draw(Display *display, Window window, void *closure)
{
struct state *st = (struct state *)closure;
/* clear double buffer */
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
if (!st->dbeclear_p || !st->backb)
XFillRectangle(st->display, st->backbuf, st->erase_gc, 0, 0, st->xgwa.width, st->xgwa.height);
#else
XFillRectangle(st->display, st->backbuf, st->erase_gc, 0, 0, st->xgwa.width, st->xgwa.height);
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
/* ball move and draw */
{
/* draw circle */
XFillArc(st->display, st->backbuf, st->gc, st->x, st->y, st->w, st->h, 0, 360 * 64);
/* move */
st->x += st->dx;
st->y += st->dy;
/* change directon */
if (st->x <= 0 || (st->x + st->w) >= st->xgwa.width)
st->dx *= -1;
if (st->y <= 0 || (st->y + st->h) >= st->xgwa.height)
st->dy *= -1;
}
/* draw double buffer */
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
if (st->backb)
{
XdbeSwapInfo info[1];
info[0].swap_window = st->window;
info[0].swap_action = (st->dbeclear_p ? XdbeBackground : XdbeUndefined);
XdbeSwapBuffers(st->display, info, 1);
}
else
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
if (st->use_dbuf)
{
XCopyArea(st->display, st->backbuf, st->window, st->erase_gc, 0, 0,
st->xgwa.width, st->xgwa.height, 0, 0);
st->backbuf = (st->backbuf == st->ba ? st->bb : st->ba);
}
return st->delay;
}
/* Called when the window is resized. */
static void
helloxsaver_reshape(Display *display, Window window, void *closure,
unsigned int w, unsigned int h)
{
struct state *st = (struct state *)closure;
if (!st->use_dbuf)
{ /* #### more complicated if we have a back buffer... */
XGetWindowAttributes(st->display, st->window, &st->xgwa);
XClearWindow(display, window);
init_ball_pos(st);
}
}
/* Called when a keyboard or mouse event happens. */
static Bool
helloxsaver_event(Display *display, Window window, void *closure, XEvent *event)
{
return False;
}
/* Free everything you've allocated. */
static void
helloxsaver_free(Display *display, Window window, void *closure)
{
struct state *st = (struct state *)closure;
XFreeGC(display, st->erase_gc);
if (st->ba)
XFreePixmap(display, st->ba);
if (st->bb)
XFreePixmap(display, st->bb);
if (st->plane_masks)
free(st->plane_masks);
XFreeGC(display, st->gc);
free(st);
}
/* Default values for the resources you use. */
static const char *helloxsaver_defaults[] = {
".background: black",
".foreground: white",
"*delay: 10000",
"*speed: 15",
"*doubleBuffer: True",
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
"*useDBE: True",
"*useDBEClear: True",
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
#ifdef HAVE_MOBILE
"*ignoreRotation: True",
#endif
0};
/* The command-line options you accept. */
static XrmOptionDescRec helloxsaver_options[] = {
{"-delay", ".delay", XrmoptionSepArg, 0},
{"-speed", ".speed", XrmoptionSepArg, 0},
{"-db", ".doubleBuffer", XrmoptionNoArg, "True"},
{"-no-db", ".doubleBuffer", XrmoptionNoArg, "False"},
{0, 0, 0, 0}};
/*
* The last line of the file should be
* XSCREENSAVER_MODULE ("YourSaverName", yoursavername)
*/
XSCREENSAVER_MODULE("Helloxsaver", helloxsaver)
マニュアルファイル(.man)と、設定画面定義ファイル(.xml) は以下。
_helloxsaver.man
_helloxsaver.xml
- *.c, *.man は、hacks/ 以下にコピー。
- *.xml は、hacks/config/ 以下にコピー。
make でビルド。sudo make install でインストールして、xscreensaverの設定画面を出して動作確認。
make sudo make install xscreensaver-settings &
◎ 少し解説。 :
最初のあたりで state という構造体を定義している。これは、このスクリーンセーバが動いてる最中、ずっと保持されるべきワークエリアの定義。動作に必要な変数は、全てこの state の中に入れておくことになる。
helloxsaver_init() は初期化処理を担当する。state 分のメモリ領域を確保・初期化して、関数を抜けるときに return st; で、呼び出し元に state のポインタを返している。xscreensaver本体は、この返されたメモリ領域を、スクリーンセーバが動いてる最中、ずっと保持しているのだろう。たぶん。
helloxsaver_draw() は、1フレーム分の描画を担当する関数。引数として state のポインタが渡されているので、そのワークエリアを使って処理をする。
helloxsaver_reshape() は、ウインドウサイズが変更された時に呼ばれるらしい。ここでは、ウインドウの情報(横幅、縦幅等々)を取得し直して、画面クリアと、ボールの位置を初期化してる。
helloxsaver_event() は、キーボード等が押された時に呼ばれるらしいが、return False; だけを書いておけばいい。
helloxsaver_free() は、ワークエリアの開放処理を書く。
グローバル変数、helloxsaver_defaults は、パラメータのデフォルト値を列挙。helloxsaver_options には、コマンドラインオプションを列挙する。
そして、一番最後の行に、以下を書く。
もし、hoge という名前のスクリーンセーバを書くなら、最後の行は以下になるはず。
ちなみに、xscreensaver用のスクリーンセーバとして公開する時は、*.c、*.man、*.xml 内の日付(年)や YOURNAME の部分を自分のソレに書き換えるようにと、 _README.hacking には書かれていた。
helloxsaver_init() は初期化処理を担当する。state 分のメモリ領域を確保・初期化して、関数を抜けるときに return st; で、呼び出し元に state のポインタを返している。xscreensaver本体は、この返されたメモリ領域を、スクリーンセーバが動いてる最中、ずっと保持しているのだろう。たぶん。
helloxsaver_draw() は、1フレーム分の描画を担当する関数。引数として state のポインタが渡されているので、そのワークエリアを使って処理をする。
helloxsaver_reshape() は、ウインドウサイズが変更された時に呼ばれるらしい。ここでは、ウインドウの情報(横幅、縦幅等々)を取得し直して、画面クリアと、ボールの位置を初期化してる。
helloxsaver_event() は、キーボード等が押された時に呼ばれるらしいが、return False; だけを書いておけばいい。
helloxsaver_free() は、ワークエリアの開放処理を書く。
グローバル変数、helloxsaver_defaults は、パラメータのデフォルト値を列挙。helloxsaver_options には、コマンドラインオプションを列挙する。
そして、一番最後の行に、以下を書く。
XSCREENSAVER_MODULE("Helloxsaver", helloxsaver)
もし、hoge という名前のスクリーンセーバを書くなら、最後の行は以下になるはず。
XSCREENSAVER_MODULE("Hoge", hoge)
ちなみに、xscreensaver用のスクリーンセーバとして公開する時は、*.c、*.man、*.xml 内の日付(年)や YOURNAME の部分を自分のソレに書き換えるようにと、 _README.hacking には書かれていた。
◎ もう一つ書いてみた。 :
helloxsaver2 という名前でもう一つ書いてみた。実行結果は以下。ボールが複数跳ね回る例のアレ。
ソースファイル等は以下。
_helloxsaver2.c
_helloxsaver2.man
_helloxsaver2.xml
ファイルが増えたので、以下の2つを修正する必要がある。
_Makefile.in.patch
_XScreenSaver.ad.in.patch
各所に、helloxsaver2.* を追加する。
修正できたら、以下を実行して、Makefile や XScreenSaver.ad を自動生成し直す。
make してビルドして、インストールして、設定画面を出して動作確認。
ソースファイル等は以下。
_helloxsaver2.c
_helloxsaver2.man
_helloxsaver2.xml
ファイルが増えたので、以下の2つを修正する必要がある。
- hacks/Makefile.in
- driver/XScreenSaver.ad.in
_Makefile.in.patch
_XScreenSaver.ad.in.patch
各所に、helloxsaver2.* を追加する。
修正できたら、以下を実行して、Makefile や XScreenSaver.ad を自動生成し直す。
./configure --prefix=/usr --libexecdir=/usr/lib
make してビルドして、インストールして、設定画面を出して動作確認。
make sudo make install xscreensaver-settings &
◎ 課題。 :
X11の描画機能を使って、線のみで円を描いたり、塗り潰しで円を描いたりすることができると分かった。円が描けるなら、おそらく四角や線や点も描けそうな気がする。
ただ、できれば画像を描画したい。最低限、画像の描画さえできれば、大昔の2Dゲームっぽい画面を作れるだろうから、かなり表現の幅が広がるはず。
しかし、画像を描画する方法が分からない…。
C言語のソースに、xpm画像をインポートしてやれば ―― #include "images/hoge.xpm" とでも書いてやれば、実行バイナリに画像データを含めることができるのではないかと思ったけれど、試してみたら xpm画像は扱えないと怒られた。xscreensaver は、xpm関連ライブラリをリンクしているわけではないのかもしれない。
画像を読み込んで描画してそうなスクリーンセーバはどれかなと探してみたけど、これがなかなか…。noseguy.c あたりがソレっぽいかなと眺めてみたけど、何が何だか。
ただ、できれば画像を描画したい。最低限、画像の描画さえできれば、大昔の2Dゲームっぽい画面を作れるだろうから、かなり表現の幅が広がるはず。
しかし、画像を描画する方法が分からない…。
C言語のソースに、xpm画像をインポートしてやれば ―― #include "images/hoge.xpm" とでも書いてやれば、実行バイナリに画像データを含めることができるのではないかと思ったけれど、試してみたら xpm画像は扱えないと怒られた。xscreensaver は、xpm関連ライブラリをリンクしているわけではないのかもしれない。
画像を読み込んで描画してそうなスクリーンセーバはどれかなと探してみたけど、これがなかなか…。noseguy.c あたりがソレっぽいかなと眺めてみたけど、何が何だか。
◎ 画像の扱い方について。 :
画像の扱い方について調べてたのだけど、少し分かってきた。
hacks/images/ の中に、大量の画像が入っている。これらが、xscreensaver が利用する画像なのだろう。
また、hacks/images/Makefile による指定で、.png を .h に変換してることも分かった。ワイルドカード(*.png)を使って対象ファイル群が指定されているので、とにかく hacks/images/ の中にpng画像を入れておけば、それだけで変換処理対象として扱ってくれるらしい。
hacks/images/gen/ の中に、大量の *.h が入ってるけれど、これが、hacks/images/*.png をC言語のソース形式に変換したもの。中では unsigned char で、png画像のバイナリが列挙されている。
さておき。これら、images/gen/*.h を、スクリーンセーバの *.c の中で include すれば、スクリーンセーバの実行バイナリの中に、png画像データもまるっと含まれて、画像データにアクセスできるようになるのだろう。例えば、下記なら、nose-f[1-4].png が実行バイナリに含まれることになる。
画像の読み込みをする場合は、ximage-loader.h を include する模様。
ximage-loader.h の中を覗いたら、一番最初の行に、「ximage-loader.h --- converts XPM data to Pixmaps.」と書いてあった。もしかして、xpm画像も使える…? また、その下で、image_data_to_pixmap()、image_data_to_ximage() 等々、それらしい名前の関数が宣言されていた。これらの関数を使えば、png画像のバイナリを Pixmap や XImage に変換できそう。たぶん。
hacks/images/ の中に、大量の画像が入っている。これらが、xscreensaver が利用する画像なのだろう。
また、hacks/images/Makefile による指定で、.png を .h に変換してることも分かった。ワイルドカード(*.png)を使って対象ファイル群が指定されているので、とにかく hacks/images/ の中にpng画像を入れておけば、それだけで変換処理対象として扱ってくれるらしい。
hacks/images/gen/ の中に、大量の *.h が入ってるけれど、これが、hacks/images/*.png をC言語のソース形式に変換したもの。中では unsigned char で、png画像のバイナリが列挙されている。
static const unsigned char nose_f1_png[] = "\211PNG\015\012\032...\000IEND\256B`\202";
- hacks/images/gen/*.h は、make 時に自動生成されるので、ソースファイル群を解凍した段階では存在していない。
- png画像から *.h への変換は、utils/bin2c というシェルスクリプトで行われている。実際には、bin2c の中で、Perl のワンライナーを指定して変換処理をしている。
さておき。これら、images/gen/*.h を、スクリーンセーバの *.c の中で include すれば、スクリーンセーバの実行バイナリの中に、png画像データもまるっと含まれて、画像データにアクセスできるようになるのだろう。例えば、下記なら、nose-f[1-4].png が実行バイナリに含まれることになる。
#include "images/gen/nose-f1_png.h" #include "images/gen/nose-f2_png.h" #include "images/gen/nose-f3_png.h" #include "images/gen/nose-f4_png.h"
画像の読み込みをする場合は、ximage-loader.h を include する模様。
#include "screenhack.h" #include "ximage-loader.h"
ximage-loader.h の中を覗いたら、一番最初の行に、「ximage-loader.h --- converts XPM data to Pixmaps.」と書いてあった。もしかして、xpm画像も使える…? また、その下で、image_data_to_pixmap()、image_data_to_ximage() 等々、それらしい名前の関数が宣言されていた。これらの関数を使えば、png画像のバイナリを Pixmap や XImage に変換できそう。たぶん。
[ ツッコむ ]
以上、1 日分です。