mieki256's diary



2022/09/01(木) [n年前の日記]

#1 [windows][pc] メインPCが不調

夜中、メインPC(Windows10 x64 21H2)が、何かの拍子にブルースクリーン(BSOD)になる現象に悩まされた。何度も何度もブルースクリーンになる…。

ブルースクリーンは、どんな問題が起きたのかを表示しているので、スマホのカメラで撮影しようとしたものの。すぐに自動で再起動がかかってしまうから撮影もできず、何が表示されているのか結局分からないまま…。

せめて表示する秒数を長目に設定できないものだろうか。30〜60秒ぐらいに設定できれば助かるのだけど。

自動再起動を無効化してみた。 :

ググってみたら、そもそもBSOD直後の自動再起動を無効化する方法があると知った。

_Windows 10ミニTips(310) ブルースクリーン発生時の自動再起動を有効/無効にする | マイナビニュース
_Windows 10でブルースクリーン後に自動再起動を行わせない設定方法 | Ask for Windows
_NEC LAVIE公式サイト > サービス&サポート > Q&A > Q&A番号 018441

消えたら困るのでメモ。
  • sysdm.cpl を実行して「システムのプロパティ」を開いて、詳細設定 → 起動と回復 → 自動的に再起動する、のチェックを外す。
  • あるいは、管理者権限でコマンドプロンプト(cmd.exe)を開いて、「wmic recoveros set AutoReboot = false」と打つ。

一応設定しておいた。これで次回、ブルースクリーンになっても、スマホで画面を撮影できるだろう…。

ただ、この設定にした場合、再起動の手順はどうすればいいのか。

自分のPCにはリセットスイッチがついてるからソレを押せば問題無いけれど。今時の市販のPCってリセットスイッチがついてない印象があるので…。その場合、どうしたらいいのだろうなと。

電源ボタンを4秒以上長押しして強制的に電源オフにするしかないのだろうか。でも、そんな知識を持ってない人はどうすればいいのだろう。コンセントを引っこ抜くしかないのだろうか。

まあ、そのあたりが分からない環境/利用者は自動再起動を有効にしたままで使え、ということなんだろうけど。表示時間をもっと長目にしてあれば助かるのだけどな。いや、その場合、よく分からない人がコンセントを抜いてしまう事態もあり得るかな…。

chkdsk はかけておいた。 :

chkdsk c: /offlinescanandfix を実行して、スキャンディスクはかけておいた。一応、修復完了とは表示されていたけど…。

Windowsメモリ診断も実行してみたけれど、問題は表示されなかった。

#2 [pc][windows] 温度監視ツールをインストールしてみた

Windows10 x64 21H2がブルースクリーンになる件について。ひょっとして、SSDかHDDの温度が高くなってしまった時にアクセスできなくなってブルースクリーンになっているのではないかという気がしてきた。以前SSDが吹っ飛んだ時も、ケース内の温度が高くなって、SSDの基板が熱で曲がって壊れたソレだった気がするわけで。

SSD や HDD の温度を監視できるツールはないものか。ググってみたら以下の紹介記事が参考になった。

_Windows PC内部の温度を確認・監視できる常駐型フリーソフト3選 | 俺の開発研究所

CrystalDiskInfoを試用。 :

CrystalDiskInfo 8.12.8 を導入済みだったので、タスクトレイに温度を表示する機能を試してみた。機能 → 常駐、で利用できる。たしかに、常時、タスクトレイ上に温度を表示してくれた。

_CrystalDiskInfo - Crystal Dew World [ja]

ただ、タスクトレイ上のアイコンが全部同じスタイルなので、どのアイコンがどのSSD/HDDの温度なのか、分かりづらいなと…。

設定した温度を超えると警告音を鳴らしてくれるあたりはありがたい。ただ、自分の環境の場合、音声出力はヘッドフォンを繋いでいるので、普段は音が聞こえないのがアレだけど…。

Open Hardware Monitorを試用。 :

Open Hardware Monitor も導入してみた。現行版は 0.9.6。openhardwaremonitor-v0.9.6.zip をDLして解凍。OpenHardwareMonitor.exe を実行。

_Open Hardware Monitor - Core temp, fan speed and voltages in a free software gadget
_Open Hardware Monitor のダウンロードと使い方 - k本的に無料ソフト・フリーソフト

表示したい温度センサ種類を右クリックすればメニューが出てくるので、デスクトップガジェット、もしくはタスクトレイ上のアイコンで表示するように指定すれば、温度を表示してくれる。

導入直後はタスクトレイ上の温度表示/文字色が黒色で視認できなかったのだけど、タスクトレイの各アイコンを右クリックすればそれぞれの文字色を変更することができると分かった。SSD/HDD毎に色を変えておけば、どれがどの温度を示しているのかが分かりやすくなる。

ひとまず、これで温度をチェックしておこう…。ブルースクリーンになるのはSSDの温度がxx℃を超えた時、といった知見が得られればいいのだけど。

もっとも、そもそも冷却をどうするかを考えておいたほうが良さそう…。今使ってるPCケースの設計では、今時のPCパーツの発熱に対応できないのかも…。

#3 [pc][windows] AMDがWindows10上でmetrics.csvという妙なファイルを作っている

メインPC(Windows10 x64 21H2)の、Cドライブ(SSD)の中を調べていたら、C:\ProgramData\AMD\PPC\ 以下に、metrics.csv という巨大ファイルがあることに気が付いた。ファイルサイズは 8.7GB。なんじゃこりゃ…。しかもひっきりなしに何かを追加しているようで…。

環境は、AMD Ryzen 5 5600X + Windows10 x64 21H2。

AMD製のソフトウェアのせいだった。 :

ググったら以下の記事に遭遇。

_keitaikid : 【PC】SSDの容量が足りない? 原因は「AMDユーザーエクスペリエンス」
_metrics.csv this file takes too much space can i delete it or make it take less space? : AMDHelp
_FILE "METRICS" HELP ME - AMD Community

「AMDユーザーエクスペリエンス」に同意していると、この metrics.csv が作成されてしまう模様。

自分の環境の場合、AMD製GPUは使ってないけれど、AMD Ryzen CPU を簡単オーバークロックできる AMD Ryzen Master というツールをインストールしてしまっていたので、デフォルトで「AMDユーザーエクスペリエンス」に同意している状態になっていたらしい。

それにしたって、GB単位でファイルを作ることはないだろう…。

上記のやり取りを眺めると、512GB のSSD内で290GBもの metrics.csv が作られた事例もあるようで。また、「これはAMD製アプリのバグだよ」という発言も…。

とりあえず、無効にしておいた。AMD Ryzen Master のバージョンは、2.9.0.2093。
  1. AdvancedView に切替。
  2. 左下の、Settings をクリック。
  3. 「User Experience Program」が「Opt In」になっているので、クリックして「Opt Out」に変更。
「無効にしないでくれ!」的メッセージが出てくるけど無視して設定した。こんな巨大なファイルを作られてしまっては…。

設定してから、metrics.csv を手動で削除しておく必要があるらしい。

もしかして AMD Ryzen Master のバグだろうか。 :

ググっていたら、気になる話を見かけた。

_Ryzen CPUがGPUドライバーのバグによって勝手にオーバークロック設定になっていたとAMDが認める - GIGAZINE

自分の環境も、常時4GHz以上のクロックになっていて、クロックが全然落ちないな、おかしいなと思っていたけれど、もしかしてこのせいだろうか。AMD Ryzen Master をインストールしてしまったのがマズかったのだろうか…。

余談。 :

AMD Ryzen Master ウインドウ上のフォントサイズが小さ過ぎて、老眼では読めないのですが…。わざわざ豆粒フォントを並べるのも中二病の一種なんだろうか。まあ、表示できる情報量が増えるというメリットはあるけど、読めないのでは…。

2022/09/02(金) [n年前の日記]

#1 [pc][windows] SSDの温度をチェック中

メインPC(Windows10 x64 21H2)がブルースクリーンになる症状が気になっているので、SSDやHDDの温度をチェックできるツール、Open Hardware Monitor 0.9.6 を導入して様子を見ている。ちなみに、今日はブルースクリーンにならなかった。

見た感じ、室温が28度ぐらいの時、メインPCのSSD/HDDの温度は以下のような状態だった。 他にもう1台、SSD(PLEXTOR PX-256M2P)を積んでいるけれど、これは温度が取得できない製品の模様。

昨日、ブルースクリーンになった際に、CrystalDiskInfo で調べた時は、SSD の温度が 56度前後になってたような記憶があるので…。そのぐらい熱くなったら危ないのかもしれないなと…。ただ、どういう状況ならそんな温度になるのかが分からんけれど…。

これは想像だけど、ひょっとすると、AMD Ryzen Master が作成していた巨大ファイル、metrics.csv の存在が影響している可能性もあるのかもしれない。生成しないように設定したことで改善されれば助かるのだけど、さてはて…。

CPUとGPUの温度が少し不安。 :

3Dゲームを試しに動かしてみたら、CPU AMD Ryzen 5 5600X、GPU NVIDIA GeForce GTX 1060 6GB の温度が80度を超えた。怖いな…。料理が作れそうな温度だ…。

SSDのファームウェアバージョンを調べた。 :

SSD crucial MX500 CT500MX500SSD1 のファームウェアのバージョンを、HWINFO64 7.20.4700 を使って調べてみた。M3CR023 と表示されている。

crucial のサイトで調べてみたら…。

_MX500 SSDファームウェアとサポート | Crucial JP

公開されてるファームウェアでは、M3CR023 は最新のように見える。

尚、M3CR033 というバージョンもあるけれど、それは M3CR032 というバージョンが入ってる個体に対してアップデートできる版で、それ以外のバージョンが入ってる個体に対しては M3CR023 が一番新しい、ということらしい。

2022/09/03() [n年前の日記]

#1 [pc][windows] AMD Ryzen Masterをアンインストールしておいた

Windows10 x64 21H2 がブルースクリーン(BSOD)になる症状がどうにも気になるので、AMD Ryzen Master をアンインストールして様子を見てみることにした。まあ、関係あるのかないのか分からんけど…。

コントールパネル経由でアンインストールしてみたけれど、レジストリに情報が残ってしまっているように見える。これは削除しておいたほうがいいのだろうか。どうなんだろう…。ちなみに、キーは以下。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AMDRyzenMasterDriverV13
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\AMDRyzenMasterDriverV13
一応、エクスポートしてから削除しておくか…。

#2 [prog][windows] MinGWとWindows APIで画像を描画

MinGW と WIndows API (GDI) を使って、bitmap画像を描画できそうかどうか実験中。環境は Windows10 x64 21H2 + gcc 9.2.0。MSYS2 + MinGW-w64 ではなくて、MinGW + MSYS を使ってる。

数日前の作業で、bitmapをベタ描画するだけなら問題無くできることは分かったけれど、透明色というか抜け色を反映させて描画する方法がよく分からなくて調べていた。どうやら、ソース画像とは別にマスク画像を用意して、マスク画像を描画した直後に本来のソース画像を上書き描画するのが一般的っぽいなと。そのあたりの仕組みは、以下のページが参考になった。

_マスク画像で透明色
_透明色とマスク画像
_ビットマップの表示

ソース。 :

ソースは以下のような感じになった。

_01_draw_bitmap.c
#include <windows.h>

#define SCRW 512
#define SCRH 512

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
  HDC hdc, hBmpDC;
  PAINTSTRUCT ps;
  
  static HBITMAP hBitmap, hBitmap_mask, hBitmap_bg;
  static BITMAP bp;
  static int imgw, imgh;
  static int bgw, bgh;
  static int x, y;

  switch (msg)
    {
    case WM_CREATE:
      // load bitmaps
      hBitmap = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BALL_IMG"));
      hBitmap_mask = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BALL_MSK"));
      hBitmap_bg = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BG_IMG"));

      // get source image information
      GetObject(hBitmap, sizeof(BITMAP), &bp);
      imgw = bp.bmWidth;
      imgh = bp.bmHeight;

      // get bg image information
      GetObject(hBitmap_bg, sizeof(BITMAP), &bp);
      bgw = bp.bmWidth;
      bgh = bp.bmHeight;

      return 0;

    case WM_PAINT:
      x = 32;
      y = 32;

      hdc = BeginPaint(hwnd, &ps);

      // draw bg image
      hBmpDC = CreateCompatibleDC(hdc);
      SelectObject(hBmpDC, hBitmap_bg);
      BitBlt(hdc, 0, 0, bgw, bgh, hBmpDC, 0, 0, SRCCOPY);
      DeleteDC(hBmpDC);

      // draw mask
      hBmpDC = CreateCompatibleDC(hdc);
      SelectObject(hBmpDC, hBitmap_mask);
      BitBlt(hdc, x, y, imgw, imgh, hBmpDC, 0, 0, SRCAND);
      DeleteDC(hBmpDC);

      // draw source image
      hBmpDC = CreateCompatibleDC(hdc);
      SelectObject(hBmpDC, hBitmap);
      BitBlt(hdc, x, y, imgw, imgh, hBmpDC, 0, 0, SRCPAINT);
      DeleteDC(hBmpDC);

      EndPaint(hwnd, &ps);
      return 0;

    case WM_DESTROY:
      DeleteObject(hBitmap);
      DeleteObject(hBitmap_mask);
      DeleteObject(hBitmap_bg);
      PostQuitMessage(0);
      return 0;
    }

  return DefWindowProc(hwnd, msg, wp, lp);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR lpCmdLine, int nCmdShow)
{
  HWND hwnd;
  MSG msg;
  WNDCLASS winc;

  winc.style            = CS_HREDRAW | CS_VREDRAW;
  winc.lpfnWndProc      = WndProc;
  winc.cbClsExtra       = winc.cbWndExtra   = 0;
  winc.hInstance        = hInstance;
  winc.hIcon            = LoadIcon(NULL, IDI_APPLICATION);
  winc.hCursor          = LoadCursor(NULL, IDC_ARROW);
  winc.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);
  winc.lpszMenuName     = NULL;
  winc.lpszClassName    = TEXT("DrawBitmapSample");

  if (!RegisterClass(&winc)) return -1;

  hwnd = CreateWindow(
    TEXT("DrawBitmapSample"), TEXT("Draw Bitmap Sample"),
    WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    CW_USEDEFAULT, CW_USEDEFAULT,
    SCRW, SCRH,
    NULL, NULL,
    hInstance, NULL
    );

  if (hwnd == NULL) return -1;

  while (GetMessage(&msg, NULL, 0, 0))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

  return msg.wParam;
}


リソースファイルは以下。bitmap画像を3ファイルほど指定している。

_bmp_res.rc
BALL_IMG BITMAP "test.bmp"
BALL_MSK BITMAP "test_mask.bmp"
BG_IMG BITMAP "bg.bmp"


使用したbitmap画像は以下。

_images.zip
  • bg.bmp。背景にする画像。
  • test.bmp。元ソース画像。抜け部分にしたいところは黒(#000000)にしておく。
  • test_mask.bmp。マスク画像。不透明部分は黒(#000000)、透明部分は白(#ffffff)にしておく。アルファチャンネルを白黒反転させた状態とも言える。

一応、png画像に変換したものも載せておく。

bg.png

test.png

test_mask.png


Makefile は以下。

_Makefile
01_draw_bitmap.exe: 01_draw_bitmap.o bmp_res.o
        gcc 01_draw_bitmap.o bmp_res.o -o 01_draw_bitmap.exe -mwindows

01_draw_bitmap.o: 01_draw_bitmap.c
        gcc -c 01_draw_bitmap.c

bmp_res.o: bmp_res.rc test.bmp test_mask.bmp bg.bmp
        windres bmp_res.rc bmp_res.o

.PHONY: clean
clean:
        rm -f *.exe
        rm -f *.o

make と打てばコンパイル/ビルドできる。01_draw_bitmap.exe が得られた。

01_draw_bitmap.exe を実行した結果は以下。

01_draw_bitmap_ss.png

ということで、赤いボールの周辺がちゃんと抜けた状態で描画できた。

仕組み。 :

どうして抜け部分を作れるのか、一応図にしてみた。

about_gdi_draw_bmp.png
  • マスク画像を AND (SRCAND) で重ねることで、マスク画像の黒い部分は黒く、白い部分は描画先の元の色が残る状態になる。
  • 本来描きたかったソース画像を OR (SRCPAINT) で重ねることで、ぼっかり黒く抜けた部分にソース画像がハメ込まれるような状態になる。

2022/09/04() [n年前の日記]

#1 [anime] 『「宇宙戦艦ヤマト」という時代 西暦2202年の選択』を視聴

BS12で放送されていたので視聴してみた。初見。

リメイク版宇宙戦艦ヤマト、「2199」「2202」の総集編だろうか。ガンガンナレーションを入れて設定や状況を説明しつつ、真田さんがインタビューに答えていく形式で、なかなか面白いスタイルでまとめたものだなと感心してしまった。この手があったか…みたいな。

自分、そもそも「2202」自体を未見なのだけど、総集編を眺めた限りでは結構面白そうな印象を受けた。オリジナル版の雑なところもどうにかこうにか理屈をつけてしっかり埋めてそうな雰囲気を感じたし、「ええ…そんな設定盛り込んじゃいますか」と驚いた瞬間が何度もあったし。脚本の方が努力したのか、設定考証の方が努力したのか分からんけど、よくまあここまで考えたもんだなと…。個人的には、 時間断層(?)の設定が気に入りました。たった数年であそこまで復興するのって絶対おかしいもんな…。あの設定が出てきて納得したし、あんなもんがあったらこんな無茶苦茶な光景にも繋がるわなと納得。また、メカデザインその他は世界観がちょっと違う感じに思えたけれど、逆に言えばこちらが想像してなかったデザインがポンポン出てくるビックリドッキリ感は味わえるなと。

もっとも、自分、当時「さらば」の続きが作られた時点でヤマトシリーズの展開に呆れてしまって、そこですっかり思い入れが無くなってしまったので、最初から何も期待してないから面白そうに見えてしまったところはありそうだなと…。

それに自分は、オリジナルから乖離したアレンジが出てくるとそれだけで面白そうと感じてしまう、ちょろい(?)人種なので…。例えば「イデオン」小説版のメカデザインにもグッと来ちゃうので…。おそらくフツーのファン層とは感覚がずれているだろうから、評価だってあてにならない…。

「2202」は巷でとにかく不評らしいけど。おそらく、オリジナル版至上主義者にとっては腸が煮えくり返るような内容・アレンジだったのだろうなと…。お爺ちゃん達の財布をあてにしているのであろう企画で、お爺ちゃん達に忖度しない作りになってたら、そりゃ大ブーイングだわな…。もっとも、オリジナル版が良いならオリジナル版をずっと見てればいいじゃんよ、という気もするし。何のためにリメイクするのか、どういう方針で作り直すのか、どこをアレンジしてどこを変えないのか、という問題も…。そもそもお爺ちゃん達は、一体どんな「2202」を目にしたかったのだろう? まあ、その答えはお爺ちゃん達の中にしか無いわけだけど…。

さておき。「2202」本編は、BS等で全国放送されてないのだよな…。もったいない…。これはこれで結構面白そうなのに…。まあ、自分が面白そうと思ったソレは大体アレだったりするから、つまりはそういうことなんだろう…。

2022/09/05(月) [n年前の日記]

#1 [prog][windows] GDI+でpng画像を読み込んで描画

Windows10 x64 21H2上で、C/C++ と Windows APIを使って画像を描画する実験をしているところ。Windows API (GDI?)ではbitmap画像しか扱えないけれど、Windows XP から使えるようになった GDI+ なるものを使えばpng画像も扱えるという話を見かけたので試してみた。

環境は、Windows10 x64 21H2 + MinGW (g++ 9.2.0)。MSYS2 + MinGW-w64 ではなく、MinGW (+ MSYS) を使ってコンパイルしている。

ちなみに、GDI+ を使う場合は C++ を使うことになるらしい。

pngファイルを読み込む版。 :

まずは、実行ファイルと同じディレクトリに置いてある png ファイルを読み込んで、描画する処理を試してみた。以下のサンプルや解説を参考にさせてもらった。

_GDI+入門
_GDI+でBMP/PNG/JPEGの読み込みを簡単に | 株式会社ヘキサドライブ


_01_gdiplus01.cpp
#define WINVER          0x0501      // 0x0501 = WindowsXP
#define _WIN32_WINNT    0x0501

#include <windows.h>
#include <gdiplus.h>
using namespace Gdiplus;

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdiplus.lib")

#define SCRW 512
#define SCRH 512

#define TM_COUNT1 1
#define FPS    60

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static Image *img;
  static Image *bg;
  static Graphics *g;
  RECT rect;

  static float x, y, dx, dy;
  static int wdw_w, wdw_h;

  switch (uMsg)
    {
      case WM_CREATE:
        // get window size
        GetClientRect(hWnd, &rect);
        wdw_w = rect.right - rect.left;
        wdw_h = rect.bottom - rect.top;

        img = new Image(L"test.png");
        bg = new Image(L"bg.png");

        // int work
        x = (float)(wdw_w / 2);
        y = (float)(wdw_h / 2);
        dx = (float) wdw_w / (float)FPS;
        dy = dx * 0.7;

        SetTimer(hWnd, TM_COUNT1, (int)(1000 / FPS), NULL);
        break;

      case WM_TIMER:
        // main loop
        x += dx;
        y += dy;

        if (x <= 0 || x + img->GetWidth() >= wdw_w) dx *= -1;

        if (y <= 0 || y + img->GetHeight() >= wdw_h) dy *= -1;

        InvalidateRect(hWnd, NULL, TRUE);
        // InvalidateRect(hWnd, NULL, FALSE);
        break;

      case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        g = new Graphics(hdc);

        g->DrawImage(bg, 0, 0, bg->GetWidth(), bg->GetHeight());
        g->DrawImage(img, (int)x, (int)y, img->GetWidth(), img->GetHeight());

        delete (g);
        EndPaint(hWnd, &ps);
        break;

      case WM_DESTROY:
        delete (img);
        delete (bg);

        PostQuitMessage(0);
        break;

      default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }

  return 0;
}

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  GdiplusStartupInput gpSI;
  ULONG_PTR lpToken;

  /* GDI+初期化 */
  GdiplusStartup(&lpToken, &gpSI, NULL);

  HCURSOR hCursor = LoadCursor(NULL, IDC_ARROW);
  HBRUSH hBrush = (HBRUSH)(COLOR_WINDOW + 1);
  WNDCLASS wcl = { 0, WndProc, 0, 0, hInst, NULL, hCursor, hBrush, NULL, "mh" };
  DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

  if (!RegisterClass(&wcl) ||
      !CreateWindowEx(0, "mh", "gdiplus test",
                      style,
                      CW_USEDEFAULT, CW_USEDEFAULT,
                      SCRW, SCRH,
                      NULL, NULL, hInst, NULL))
    return FALSE;

  while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

  /* GDI+終了 */
  GdiplusShutdown(lpToken);

  return msg.wParam;
}

せっかくだから、SetTimer() を使って、一定時間毎にタイマーで処理が走るようにして、簡単なアニメーションをするようにしてみた。SetTimer() を使ってタイマーをセットすれば、そこで指定したミリ秒(1/1000秒)が経過すると、WM_TIMER というメッセージがWindowsから送られてくるようになるので、WM_TIMER が送られてきた時の処理を書けばアニメーションができる。また、InvalidateRect() を呼べば、再描画を要求できる(WM_PAINT メッセージが送られてくる)。

ただ、SetTimer() の精度(?)は、Windows NT系OSの場合、10ms らしい。60 FPS を期待して16ms前後を指定しても、おそらく 20ms ぐらいの間隔で処理することになるのだろう…。


使用画像は以下。

_test.png
_bg.png


コンパイルは以下。01_gdiplus01.exe が生成される。
g++ 01_gdiplus01.cpp -o 01_gdiplus01.exe -mwindows -static -lstdc++ -lgcc -lgdiplus -lgdi32
  • GDI+ を使うので、-lgdiplus -lgdi32 をつけないといかんらしい。


実行結果は以下。




たしかに、GDI+ を使うことで、png画像を読み込んで描画することができた。赤いボールのpng画像はアルファチャンネルを持っているので、周辺がちゃんと抜けた状態で描画されている。

ただ、少々問題が…。ウインドウ内がとにかくちらつく。描画処理として、ウインドウ内消去、背景画像相当の描画、ボールの描画を行っているので、その描画過程が見えちゃってちらつくのだな…。まあ、解決策については後で考えることにする。

リソースファイル内のpng画像を利用する。 :

上記のサンプルはpngファイルを読み込んで利用しているけれど、せっかくだからリソースファイル(.exeファイルに含めるデータ群)にpng画像を含めておいて、それを利用するようにしたい。

以下を参考にして作業。

_プログラミングTips : Vistaの透け透けウィンドウにアルファ付き画像を描画する
_winapi - C++ GDI+ how to get and load image from resource? - Stack Overflow
_gdi+ - "undefined reference to `CreateStreamOnHGlobal@12'" error faced on executing gdiplus c++ code - Stack Overflow

リソース内からpng画像を一発で読み込む関数は用意されてないらしくて、いくつかの手順を踏んで処理しないといけないらしい…。


さておき。リソースファイルは以下。

_res.rc
#include "resource.h"

IDI_BALL    RCDATA     "test.png"
IDI_BG      RCDATA     "bg.png"

「RCDATA」を記述することで、何らかのバイナリファイルであることを指定している。「PNG」や「IMAGE」を記述している事例も見かけたのだけど、自分の環境では「RCDATA」にしないと上手く行かなかった。MinGW を使って作業している関係だろうか…?


リソースファイル関連ヘッダファイルは以下。ボール画像とBG画像のID番号を設定している。

_resource.h
#ifndef RESOURCE_H_
#define RESOURCE_H_

#define IDI_BALL    101
#define IDI_BG      102

#endif


C++のソースは以下。

_02_gdiplus_res.cpp
#define WINVER          0x0501      // 0x0501 = WindowsXP
#define _WIN32_WINNT    0x0501

#include <windows.h>
#include <gdiplus.h>
#include "resource.h"

using namespace Gdiplus;

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdiplus.lib")

#define SCRW 512
#define SCRH 512

#define TM_COUNT1 1
#define FPS  60

HINSTANCE hInst;

// Load image from resource
Bitmap *LoadImageFromResource(
  HINSTANCE hinst,  // handle instance
  LPCTSTR pName,  // resource ID
  LPCTSTR pType   // resource type
  )
{
  // Search resource
  HRSRC hRes = FindResource(hinst, pName, pType);

  if (hRes == NULL)
    {
      MessageBox(NULL, "Not found resource", "Error", MB_OK);
      return NULL;
    }

  // get resource size
  DWORD Size = SizeofResource(hinst, hRes);

  if (Size == 0)
    {
      MessageBox(NULL, "Resource size = 0", "Error", MB_OK);
      return NULL;
    }


  HGLOBAL hData = LoadResource(hinst, hRes);

  if (hData == NULL)
    {
      MessageBox(NULL, "Failure load resource", "Error", MB_OK);
      return NULL;
    }

  const void *pData = LockResource(hData);

  if (pData == NULL)
    {
      MessageBox(NULL, "Failure lock resource", "Error", MB_OK);
      return NULL;
    }

  // Copy resource data
  HGLOBAL hBuffer = GlobalAlloc(GMEM_MOVEABLE, Size);

  if (hBuffer == NULL)
    {
      MessageBox(NULL, "Failure alloc", "Error", MB_OK);
      return NULL;
    }

  void *pBuffer = GlobalLock(hBuffer);

  if (pBuffer == NULL)
    {
      MessageBox(NULL, "Failure lock", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  CopyMemory(pBuffer, pData, Size);
  GlobalUnlock(hBuffer);

  // read image
  IStream *pStream;

  if (CreateStreamOnHGlobal(hBuffer, TRUE, &pStream) != S_OK)
    {
      MessageBox(NULL, "Failure create stream", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  Bitmap *pBitmap = Bitmap::FromStream(pStream);
  pStream->Release();

  return pBitmap;
}


LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static Gdiplus::Bitmap* img;
  static Gdiplus::Bitmap* bg;

  static Graphics *g;
  RECT rect;

  static float x, y, dx, dy;
  static int wdw_w, wdw_h;

  switch (uMsg)
    {
    case WM_CREATE:
      // get window size
      GetClientRect(hWnd, &rect);
      wdw_w = rect.right - rect.left;
      wdw_h = rect.bottom - rect.top;

      img = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BALL), RT_RCDATA);
      bg = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BG), RT_RCDATA);

      // not work
      // img = LoadImageFromResource(hInst, TEXT("IDI_BALL"), TEXT("RCDATA"));
      // bg = LoadImageFromResource(hInst, TEXT("IDI_BG"), TEXT("RCDATA"));

      if (img != NULL && bg != NULL)
        {
          // init work
          x = (float)(wdw_w / 2);
          y = (float)(wdw_h / 2);
          dx = (float) wdw_w / (float)FPS;
          dy = dx * 0.5;

          // Set timer
          SetTimer(hWnd, TM_COUNT1, (int)(1000 / FPS), NULL);
        }

      break;

    case WM_TIMER:
      // main loop
      x += dx;
      y += dy;

      if (x <= 0 || x + img->GetWidth() >= wdw_w) dx *= -1;

      if (y <= 0 || y + img->GetHeight() >= wdw_h) dy *= -1;

      InvalidateRect(hWnd, NULL, TRUE);
      // InvalidateRect(hWnd, NULL, FALSE);
      break;

    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      g = new Graphics(hdc);

      if (bg != NULL)
        g->DrawImage(bg, 0, 0, bg->GetWidth(), bg->GetHeight());

      if (img != NULL)
        g->DrawImage(img, (int)x, (int)y, img->GetWidth(), img->GetHeight());

      delete (g);
      EndPaint(hWnd, &ps);
      break;

    case WM_DESTROY:
      delete (img);
      delete (bg);

      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }

  return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  GdiplusStartupInput gpSI;
  ULONG_PTR lpToken;

  hInst = hInstance;

  /* GDI+初期化 */
  GdiplusStartup(&lpToken, &gpSI, NULL);

  HCURSOR hCursor = LoadCursor(NULL, IDC_ARROW);
  HBRUSH hBrush = (HBRUSH)(COLOR_WINDOW + 1);
  WNDCLASS wcl = { 0, WndProc, 0, 0, hInst, NULL, hCursor, hBrush, NULL, "mh" };
  DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

  if (!RegisterClass(&wcl) ||
      !CreateWindowEx(0, "mh", "gdiplus test",
                      style,
                      CW_USEDEFAULT, CW_USEDEFAULT,
                      SCRW, SCRH,
                      NULL, NULL, hInst, NULL))
    return FALSE;

  while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

  /* GDI+終了 */
  GdiplusShutdown(lpToken);

  return msg.wParam;
}


使用画像は以下。

_test.png
_bg.png


コンパイル/ビルドは以下。
windres res.rc res.o
g++ -c 02_gdiplus_res.cpp
g++ 02_gdiplus_res.o res.o -o 02_gdiplus_res.exe -mwindows -static -lstdc++ -lgcc -lgdiplus -lgdi32 -lole32
  • 上から順に、「リソースをコンパイルして .o を生成」「C++ソースをコンパイルして .o を生成」「.o群をリンクして.exe を生成」を行っている。
  • CreateStreamOnHGlobal() がリンク時に見つからなかったけど、ole32 が必要になるらしい。リンクの指定として、-lole32 を追加した。


実行結果は以下。




png画像を描画することができている。リソースからpng画像を読み込むこともできると分かった。

まあ、これもウインドウ内のちらつきが酷いけど…。

ダブルバッファを利用。 :

前述のサンプルは、ウインドウ内がとにかくちらついてしまうので、そのあたりを改善したい。ダブルバッファを導入すれば改善されるらしいので試してみる。

以下のページが参考になった。

_VC++でGDI+ そにょ8 ?ダブルバッファリング? - yuyarinの日記
_ビットマップのマルチバッファリング【Windowsプログラミング研究所】

仕組みとしては…。
  • ウインドウと同サイズの、空のbitmapを用意しておく。
  • 一旦、そのbitmapにアレコレを描画。
  • そのbitmapをウインドウ内に描画する。
これで、細かいアレコレを描画している過程を見せないようにする。また、毎回ウインドウ内をクリアしてから描画せずに、前回の画面を残したまま上書き描画することでもちらつきが改善されるらしい。

resource.h、res.rc、使用するpng画像は、前述のものをそのまま利用する。

_res.rc
_resource.h
_test.png
_bg.png

C++のソースは以下。

_03_gdiplus_dbuf.cpp
#define WINVER          0x0501      // 0x0501 = WindowsXP
#define _WIN32_WINNT    0x0501

#include <windows.h>
#include <gdiplus.h>
#include "resource.h"

using namespace Gdiplus;

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdiplus.lib")

#define SCRW 512
#define SCRH 512

#define TM_COUNT1 1
#define FPS  60

HINSTANCE hInst;

// Load image from resource
Bitmap *LoadImageFromResource(
  HINSTANCE hinst,  // handle instance
  LPCTSTR pName,  // resource ID
  LPCTSTR pType   // resource type
  )
{
  // Search resource
  HRSRC hRes = FindResource(hinst, pName, pType);

  if (hRes == NULL)
    {
      MessageBox(NULL, "Not found resource", "Error", MB_OK);
      return NULL;
    }

  // get resource size
  DWORD Size = SizeofResource(hinst, hRes);

  if (Size == 0)
    {
      MessageBox(NULL, "Resource size = 0", "Error", MB_OK);
      return NULL;
    }


  HGLOBAL hData = LoadResource(hinst, hRes);

  if (hData == NULL)
    {
      MessageBox(NULL, "Failure load resource", "Error", MB_OK);
      return NULL;
    }

  const void *pData = LockResource(hData);

  if (pData == NULL)
    {
      MessageBox(NULL, "Failure lock resource", "Error", MB_OK);
      return NULL;
    }

  // Copy resource data
  HGLOBAL hBuffer = GlobalAlloc(GMEM_MOVEABLE, Size);

  if (hBuffer == NULL)
    {
      MessageBox(NULL, "Failure alloc", "Error", MB_OK);
      return NULL;
    }

  void *pBuffer = GlobalLock(hBuffer);

  if (pBuffer == NULL)
    {
      MessageBox(NULL, "Failure lock", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  CopyMemory(pBuffer, pData, Size);
  GlobalUnlock(hBuffer);

  // read image
  IStream *pStream;

  if (CreateStreamOnHGlobal(hBuffer, TRUE, &pStream) != S_OK)
    {
      MessageBox(NULL, "Failure create stream", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  Bitmap *pBitmap = Bitmap::FromStream(pStream);
  pStream->Release();

  return pBitmap;
}


LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  
  static Bitmap* img;
  static Bitmap* bg;
  static Bitmap* offScrnBmp;

  static Graphics* onScrn;
  static Graphics* offScrn;

  RECT rect;

  static float x, y, dx, dy;
  static int wdw_w, wdw_h;

  switch (uMsg)
    {
    case WM_CREATE:
      // get window size
      GetClientRect(hWnd, &rect);
      wdw_w = rect.right - rect.left;
      wdw_h = rect.bottom - rect.top;

      // create off screen bitmap
      offScrnBmp = new Bitmap(wdw_w, wdw_h);

      // Load image from resource
      img = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BALL), RT_RCDATA);
      bg = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BG), RT_RCDATA);

      // not work
      // img = LoadImageFromResource(hInst, TEXT("IDI_BALL"), TEXT("RCDATA"));
      // bg = LoadImageFromResource(hInst, TEXT("IDI_BG"), TEXT("RCDATA"));

      if (img != NULL && bg != NULL)
        {
          x = (float)(wdw_w / 2);
          y = (float)(wdw_h / 2);
          dx = (float) wdw_w / (float)FPS;
          dy = dx * 0.5;

          SetTimer(hWnd, TM_COUNT1, (int)(1000 / FPS), NULL);
        }

      break;

    case WM_TIMER:
      // main loop
      x += dx;
      y += dy;

      if (x <= 0 || x + img->GetWidth() >= wdw_w) dx *= -1;

      if (y <= 0 || y + img->GetHeight() >= wdw_h) dy *= -1;

      // InvalidateRect(hWnd, NULL, TRUE);
      InvalidateRect(hWnd, NULL, FALSE);  // not clear background
      break;

    case WM_PAINT:
      // draw image to off screen
      offScrn = new Graphics(offScrnBmp);
      
      if (bg != NULL)
        offScrn->DrawImage(bg, 0, 0, bg->GetWidth(), bg->GetHeight());

      if (img != NULL)
        offScrn->DrawImage(img, (int)x, (int)y, img->GetWidth(), img->GetHeight());

      delete (offScrn);

      // draw off screen to window
      hdc = BeginPaint(hWnd, &ps);
      
      onScrn = new Graphics(hdc);
      onScrn->DrawImage(offScrnBmp, 0, 0, offScrnBmp->GetWidth(), offScrnBmp->GetHeight());
      delete(onScrn);
      
      EndPaint(hWnd, &ps);
      
      break;

    case WM_DESTROY:
      delete (img);
      delete (bg);
      delete (offScrnBmp);

      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }

  return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  GdiplusStartupInput gpSI;
  ULONG_PTR lpToken;

  hInst = hInstance;

  /* GDI+初期化 */
  GdiplusStartup(&lpToken, &gpSI, NULL);

  HCURSOR hCursor = LoadCursor(NULL, IDC_ARROW);
  HBRUSH hBrush = (HBRUSH)(COLOR_WINDOW + 1);
  WNDCLASS wcl = { 0, WndProc, 0, 0, hInst, NULL, hCursor, hBrush, NULL, "mh" };
  DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

  if (!RegisterClass(&wcl) ||
      !CreateWindowEx(0, "mh", "gdiplus test",
                      style,
                      CW_USEDEFAULT, CW_USEDEFAULT,
                      SCRW, SCRH,
                      NULL, NULL, hInst, NULL))
    return FALSE;

  while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

  /* GDI+終了 */
  GdiplusShutdown(lpToken);

  return msg.wParam;
}


コンパイル/ビルドは以下。03_gdiplus_dbuf.exe が生成される。
windres res.rc res.o
g++ -c 03_gdiplus_dbuf.cpp
g++ 03_gdiplus_dbuf.o res.o -o 03_gdiplus_dbuf.exe -mwindows -static -lstdc++ -lgcc -lgdiplus -lgdi32 -lole32

一応、前述の2つのファイルについても記述した Makefile も置いておく。Makefile があれば、make と打つだけでコンパイル/ビルドができる。

_Makefile


実行結果は以下。




ちらつきがかなり改善された。

まあ、bitmapをウインドウ内に転送している過程が見えちゃってるというか、いわゆるティアリングのような見た目が発生している気もするけれど…。

_ティアリング、スタッタリングについて | ドスパラ サポートFAQ よくあるご質問|お客様の「困った」や「知りたい」にお応えします。

それでも、ダブルバッファを導入してないサンプルと比べたら雲泥の差かなと…。

GDI+は遅いらしい。 :

関連情報をググっていたら、GDI+ は描画が遅いという話を見かけた。GDI+ を使わずに、GDIで済ませられるなら、GDIだけを使ったほうが速いのだとか。

_Windows 10 で描画処理が遅くなる問題について(WebArchive)
_kenjinote/DrawSpeedTest: DWM が有効か無効かで GDI+ の描画速度が異なるのでその検証を行うためのプロジェクトです。
_Pasture | GDI+のビットマップ転送速度

png画像を使えるようになったのはありがたいけど、bitmapだけを使うようにして、GDIで処理したほうがいいのかもしれない…。

#2 [zatta] ハサミを購入

ホームセンターホーマックで、家庭用ハサミ、KOKUYO SAXA スタンダード刃 (ハサ-280B)を購入。税込382円。

今まで、PLUS フィットカットカーブ SC-175SF を使っていたけど、使う時に「ギギギ」「グググ」といった感じの妙な抵抗感があって、ずっともやもやしていたわけで。ググってみたら、フィットカットカーブよりSAXAのほうが軽く切れるという話を見かけたので、この際新調してしまおうかと。

フィットカットカーブを使う前は、SAXA を購入して使ってた時期があるのだけど。その当時の SAXA はハンドル内部にゴムっぽいものがついていて、そのゴムが経年劣化で千切れてアレな状態になってフィットカットカーブに買い換えた、という経緯があり。しかし、今現在販売されている SAXA は、そのゴムっぽいものをつけない形状になっているので、前回よりは長持ちしてくれるかなと期待。

スタンダード刃以外にも、3Dグルーレス刃、フッ素コート等々、シールの類を切った時にべたつきにくいと謳う製品もあったけど。その手のアレコレが付加されると、切る時の抵抗が微妙に増えるという話も見かけたし、実際、今まで使ってたフィットカットカーブがフッ素コート版だったせいか抵抗を感じていたので、今回はスタンダード刃を選んでみた。

2022/09/06(火) [n年前の日記]

#1 [prog][windows] GDIでダブルバッファ描画

Windows10 x64 21H2上で、C/C++ (MinGW) と Windows API を使ってウインドウ描画ができるか実験中。

_昨日、GDI+を使ってpng画像を描画できるのか、 実験していたわけだけど。GDI+ は描画が遅いという話を見かけたので、GDI+ ではなく GDI だけを使って描画ができるならそのほうがいいのかなと…。そんなわけで、GDI でダブルバッファ描画をする方法を試してみた。まあ、png画像の使用を諦めて、bmp画像(bitmap画像)だけを利用することになってしまうけど…。

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。MSYS2 + MinGW-w64 ではなくて、MinGW (+ MSYS) で実験してる。

以下を参考にさせてもらいました。

_ビットマップのマルチバッファリング【Windowsプログラミング研究所】

今回書いたサンプルの実行結果は以下。ちょっと解像度が荒いけど、雰囲気は伝わるかと…。ダブルバッファで描画しているので、ちらつきは感じられない。

リソースファイル。 :

リソースファイルは以下。このファイルは、windres を使って .o に変換する。

_res.rc
BALL_IMG        BITMAP  "ball.bmp"
BALL_MSK        BITMAP  "ball_mask.bmp"
BG_IMG          BITMAP  "bg2.bmp"
  • 3つのbmp画像ファイルを含めるように指定している。
  • 先頭に書かれている「BALL_IMG」「BALL_MSK」「BG_IMG」が、C言語のソース内で指定する識別名。

使用画像。 :

使用画像は以下。

_images.zip
  • ball.bmp : 赤いボール画像。透明にしたい部分は黒(#000000)にしておく。
  • ball_mask.bmp : ボール画像のマスク画像。不透明部分は黒(#000000)、透明部分は白(#ffffff)。アルファチャンネルを反転した感じの画像。
  • bg2.bmp : 背景用画像。タイル状に描画することを前提にしている。

一応、png画像も載せておきます。

ball.png

ball_mask.png

bg2.png

C言語のソースファイル。 :

C言語のソースファイルは以下。今回は GDI+ を使ってないので、C++ ではなく C言語で書くことができる。

_01_dblbuf.c
/* GDI Double buffer sample */

#include <windows.h>

#define SCRW        1280
#define SCRH        720

#define FPS         60

#define TM_COUNT1   101

#define LPCLASSNAME     TEXT("DoubleBufferSample")
#define WINDOW_TITLE    TEXT("Double Buffer Sample")


LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
  static HBITMAP hBitmap;
  static HBITMAP hBitmap_mask;
  static HBITMAP hBitmap_bg;
  static HBITMAP offScrnBitmap;
  static int wdw_w, wdw_h;
  static int imgw, imgh;
  static int bgw, bgh;
  static int bgx, bgy;
  static float x, y;
  static float dx, dy;

  switch (msg)
    {
    case WM_CREATE:
      // get window size
      {
        RECT rc;
        GetClientRect(hwnd, &rc);
        wdw_w = rc.right - rc.left;
        wdw_h = rc.bottom - rc.top;
      }

      // create off screen bitmap
      {
        HDC hdc;
        hdc = GetDC(hwnd);
        offScrnBitmap = CreateCompatibleBitmap(hdc, wdw_w, wdw_h);
        ReleaseDC(hwnd, hdc);    // GetDC() -> ReleaseDC()
      }

      // load bitmaps from resource
      hBitmap = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BALL_IMG"));
      hBitmap_mask = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BALL_MSK"));
      hBitmap_bg = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BG_IMG"));

      // get images size
      {
        BITMAP bp;

        // get ball image size
        GetObject(hBitmap, sizeof(BITMAP), &bp);
        imgw = bp.bmWidth;
        imgh = bp.bmHeight;

        // get bg image size
        GetObject(hBitmap_bg, sizeof(BITMAP), &bp);
        bgw = bp.bmWidth;
        bgh = bp.bmHeight;
      }

      // init work
      x = wdw_w / 2;
      y = wdw_h / 2;
      dx = (float)wdw_w / (float)FPS;
      dy = dx * 0.2;
      bgx = 0;
      bgy = 0;

      SetTimer(hwnd, TM_COUNT1, (int)(1000 / FPS), NULL);
      return 0;

    case WM_TIMER:
      // main loop
      x += dx;
      y += dy;

      if (x <= 0 || (x + imgw) >= wdw_w) dx *= -1;

      if (y <= 0 || (y + imgh) >= wdw_h) dy *= -1;

      bgx -= 1;
      bgy -= 1;

      if (bgx <= -(bgw)) bgx += bgw;

      if (bgy <= -(bgh)) bgy += bgh;

      if (0)
        {
          // clear background
          InvalidateRect(hwnd, NULL, TRUE);
        }
      else
        {
          // not clear background
          InvalidateRect(hwnd, NULL, FALSE);
        }

      break;

    case WM_PAINT:
      // draw window
      {
        HDC hdc, hBmpDC, hMemDC;
        PAINTSTRUCT ps;

        hdc = BeginPaint(hwnd, &ps);
        // hdc = GetDC(hwnd);

        // set off-screen bitmap
        hMemDC = CreateCompatibleDC(hdc);
        SelectObject(hMemDC, offScrnBitmap);

        // draw bg to off-screen
        hBmpDC = CreateCompatibleDC(hdc);
        SelectObject(hBmpDC, hBitmap_bg);
        {
          int by = bgy;

          while (by <= wdw_h)
            {
              int bx = bgx;

              while (bx <= wdw_w)
                {
                  BitBlt(hMemDC, bx, by, bgw, bgh, hBmpDC, 0, 0, SRCCOPY);
                  bx += bgw;
                }

              by += bgh;
            }
        }
        DeleteDC(hBmpDC);

        // draw mask to off-screen
        hBmpDC = CreateCompatibleDC(hdc);
        SelectObject(hBmpDC, hBitmap_mask);
        BitBlt(hMemDC, (int)x, (int)y, imgw, imgh, hBmpDC, 0, 0, SRCAND);
        DeleteDC(hBmpDC);

        // draw source image to off-screen
        hBmpDC = CreateCompatibleDC(hdc);
        SelectObject(hBmpDC, hBitmap);
        BitBlt(hMemDC, (int)x, (int)y, imgw, imgh, hBmpDC, 0, 0, SRCPAINT);
        DeleteDC(hBmpDC);

        // drawing to off-screen is finished

        // draw off-screen bitmap to window
        BitBlt(hdc, 0, 0, wdw_w, wdw_h, hMemDC, 0, 0, SRCCOPY);

        DeleteDC(hMemDC);     // Create*() -> DeleteDC()

        EndPaint(hwnd, &ps);
      }

      return 0;

    case WM_DESTROY:
      KillTimer(hwnd, TM_COUNT1);  // kill timer

      DeleteObject(offScrnBitmap);
      DeleteObject(hBitmap);
      DeleteObject(hBitmap_mask);
      DeleteObject(hBitmap_bg);
      PostQuitMessage(0);
      return 0;
    }

  return DefWindowProc(hwnd, msg, wp, lp);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR lpCmdLine, int nCmdShow)
{
  HWND hwnd;
  MSG msg;
  WNDCLASS winc;

  winc.style            = CS_HREDRAW | CS_VREDRAW;
  winc.lpfnWndProc      = WndProc;
  winc.cbClsExtra       = winc.cbWndExtra   = 0;
  winc.hInstance        = hInstance;
  winc.hIcon            = LoadIcon(NULL, IDI_APPLICATION);
  winc.hCursor          = LoadCursor(NULL, IDC_ARROW);
  winc.hbrBackground    = (HBRUSH)GetStockObject(WHITE_BRUSH);
  winc.lpszMenuName     = NULL;
  winc.lpszClassName    = LPCLASSNAME;

  if (!RegisterClass(&winc)) return -1;

  hwnd = CreateWindow(
    LPCLASSNAME, WINDOW_TITLE,
    WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    CW_USEDEFAULT, CW_USEDEFAULT,
    SCRW, SCRH,
    NULL, NULL,
    hInstance, NULL
    );

  if (hwnd == NULL) return -1;

  while (GetMessage(&msg, NULL, 0, 0))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

  return msg.wParam;
}

  • SetTimer() で指定したミリ秒が経過すると、その都度、WM_TIMER というメッセージが Windows から送られてくる。
  • WM_TIMER が来た時に、ボールやBG画像の表示位置を更新することで、アニメーションをさせている。
  • InvalidateRect() を呼べば、ウインドウの再描画を要求できる。(WM_PAINTメッセージが送られてくる。)
  • InvalidateRect(hwnd, NULL, TRUE); なら、背景を消去しつつ、ウインドウを再描画(するように要求)。
  • InvalidateRect(hwnd, NULL, FALSE); なら、背景を消去せずに、ウインドウを再描画(するように要求)。ダブルバッファ描画をする時はこちらを指定する。
  • WM_PAINT が来た時に、描画処理を行う。
  • WM_DESTROY が来たら、終了処理を行う。タイマーを殺して、bitmap を全部削除しておく。

Makefile。 :

Makefile は以下。.c や .rc から .o を生成後、.o群をリンクして .exe を生成する。

_Makefile
01_dblbuf.exe: 01_dblbuf.o res.o
        gcc 01_dblbuf.o res.o -o 01_dblbuf.exe -mwindows

01_dblbuf.o: 01_dblbuf.c
        gcc -c 01_dblbuf.c

res.o: res.rc ball.bmp ball_mask.bmp bg2.bmp
        windres res.rc res.o

.PHONY: clean
clean:
        rm -f *.exe
        rm -f *.o

コンパイル/ビルド。 :

全部で以下のファイルになる。
  • 01_dblbuf.c
  • ball.bmp
  • ball_mask.bmp
  • bg2.bmp
  • Makefile
  • res.rc
これを任意のディレクトリに置いて make と打てば、コンパイル/ビルドがされて、01_dblbuf.exe が生成される。

ちなみに、make clean と打てば、*.o や *.exe を削除してくれる。

余談。 :

DeleteDC() と ReleaseDC() の使い分けが分からなくて悩んだけれど、以下のページを眺めたら分かってきた。

_API 関数解説
  • GetDC() を使って取得したデバイスコンテキストは、ReleaseDC() を使って解放する。
  • Create*() を使って取得したデバイスコンテキストは、DeleteDC() を使って解放する。

余談その2。 :

今回のソースは、中括弧(?)を利用して、変数のスコープを極力狭めつつ書いてみた。

_変数のスコープ(C言語) - 超初心者向けプログラミング入門
_C/C++ 中括弧でローカルスコープを作る - 【はてな】ガットポンポコ

#2 [prog][windows] ビットマップ画像を描画するスクリーンセーバを書いてみた

せっかく C/C++ と GDI を使って、bitmap画像を描画、ダブルバッファ描画、アニメーションができたので…。以前、MinGW を使ってビルドが通ることを確認できた、Windows用スクリーンセーバのサンプルソースにも処理を反映させてみたい。

_スクリーンセーバのビルドの実験中
_Windows10にmingwをインストールし直した

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。

ちなみに、MSYS2 + MinGW-w64 (gcc 12.2.0) では、Windows用スクリーンセーバはビルドできない。
_msys2 + mingw-w64でスクリーンセーバをビルドしようとしてハマった

さておき。今回書いたサンプルの実行結果は以下。解像度が荒くてアレだけど、雰囲気は伝わるかと…。

リソースファイル。 :

リソースファイルは以下。以前のサンプルの最初のほうに、bitmap画像群を追記した。このファイルは、windres を使って、.rc から .o に変換することになる。

_resource.rc
/* resource.rc */

#include <windows.h>
#include <scrnsave.h>

#define IDC_STATIC_ICON     2300
#define IDC_STATIC_TEXT     2301

/* bitmap */
IDI_BALL        BITMAP  "ball.bmp"
IDI_BALL_MSK    BITMAP  "ball_mask.bmp"
IDI_BG          BITMAP  "bg2.bmp"
IDI_PREVIEW     BITMAP  "scrsav_preview.bmp"

/* Config dialog */
DLG_SCRNSAVECONFIGURE DIALOG DISCARDABLE  0, 0, 220, 50
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Setting Sample Screensaver"
 FONT 9, "Segoe UI"
BEGIN
  DEFPUSHBUTTON "OK", IDOK, 85, 30, 50, 14
  ICON          IDI_WARNING, IDC_STATIC_ICON, 6, 5, 20, 20
  LTEXT         "This is the template for the configuration dialog.", 
                IDC_STATIC_TEXT, 35, 12, 230, 12
END
※ このWeb日記システムの関係で、FONT指定行がおかしな状態で表示されてます。先頭の空白は削除してもらえればと…。


ヘッダーファイルは以下。でも、今回コレは使ってない気がする…。

_resource.h
#ifndef RESOURCE_H_
#define RESOURCE_H_

#define IDC_STATIC_ICON     2300
#define IDC_STATIC_TEXT     2301

#define IDI_BALL            2401
#define IDI_BALL_MSK        2402
#define IDI_BG              2403

#endif

使用画像。 :

使用画像は以下。4つのbmp画像を使っている。

_images.zip
  • ball.bmp : 赤いボール画像。透明部分は黒(#000000)にしておく。
  • ball_mask.bmp : ボールのマスク画像。不透明部分は黒(#000000)、透明部分は白(#ffffff)。
  • bg2.bmp : bg画像(背景画像)。タイル状に描画することを前提にしている。
  • scrsav_preview.bmp : プレビュー画像。スクリーンセーバの設定画面で表示されてる小さい窓の中に表示するための画像。サイズは 152 x 112 ドット。

一応、png画像も載せておきます。

ball.png

ball_mask.png

bg2.png

scrsav_preview.png

C言語のソースファイル。 :

_mgscrsv2.c
/* mingwscr.c */

#include <windows.h>
#include <tchar.h>
#include <scrnsave.h>
#include <time.h>
#include <stdlib.h>

#define FPS        60

#define ID_TIMER   101


/* screensaver main processing */
LRESULT WINAPI ScreenSaverProc(HWND hWnd, UINT msg,
                               WPARAM wParam, LPARAM lParam)
{
  static HBITMAP hBitmap;
  static HBITMAP hBitmap_mask;
  static HBITMAP hBitmap_bg;
  static HBITMAP hBitmap_preview;
  static HBITMAP offScrnBitmap;

  static int wdw_w, wdw_h;
  static int imgw, imgh;
  static int bgw, bgh;
  static int pvw, pvh;
  static float x, y, dx, dy, bgx, bgy;
  int bx, by;

  switch (msg)
    {
    case WM_CREATE:
      /* initialize */

      // get window size
      {
        RECT rc;

        GetClientRect(hWnd, &rc);
        wdw_w = rc.right - rc.left;
        wdw_h = rc.bottom - rc.top;
      }

      // create off-screen bitmap
      {
        HDC hdc;

        hdc = GetDC(hWnd);
        offScrnBitmap = CreateCompatibleBitmap(hdc, wdw_w, wdw_h);
        ReleaseDC(hWnd, hdc);
      }

      // load bitmaps
      hBitmap = LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, TEXT("IDI_BALL"));
      hBitmap_mask = LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, TEXT("IDI_BALL_MSK"));
      hBitmap_bg = LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, TEXT("IDI_BG"));
      hBitmap_preview = LoadBitmap(((LPCREATESTRUCT)lParam)->hInstance, TEXT("IDI_PREVIEW"));

      // get images size
      {
        BITMAP bp;

        // get ball image size
        GetObject(hBitmap, sizeof(BITMAP), &bp);
        imgw = bp.bmWidth;
        imgh = bp.bmHeight;

        // get bg image size
        GetObject(hBitmap_bg, sizeof(BITMAP), &bp);
        bgw = bp.bmWidth;
        bgh = bp.bmHeight;

        // get preview image size
        GetObject(hBitmap_preview, sizeof(BITMAP), &bp);
        pvw = bp.bmWidth;
        pvh = bp.bmHeight;
      }

      // init work
      x = wdw_w / 2;
      y = wdw_h / 2;
      dx = (float)wdw_w / (float)FPS;
      dy = dx * 0.5;
      bgx = 0;
      bgy = 0;

      // create timer
      SetTimer(hWnd, ID_TIMER, (int)(1000 / FPS), NULL);
      break;

    case WM_TIMER:
      /* main loop */

      // update ball position
      x += dx;
      y += dy;

      if (x <= 0 || x + imgw >= wdw_w) dx *= -1;

      if (y <= 0 || y + imgh >= wdw_h) dy *= -1;

      // update bg position
      bgx -= 2;
      bgy -= 2;

      if (bgx <= -bgw) bgx += bgw;

      if (bgy <= -bgh) bgy += bgh;

      // draw bitmap

      if (wdw_w >= 160)
        {
          // draw fullscreen

          HDC hdc, hBmpDC, hMemDC;

          hdc = GetDC(hWnd);

          // set off-screen bitmap
          hMemDC = CreateCompatibleDC(hdc);
          SelectObject(hMemDC, offScrnBitmap);

          // draw bg to off-screen
          hBmpDC = CreateCompatibleDC(hdc);
          SelectObject(hBmpDC, hBitmap_bg);
          {
            int by = bgy;

            while (by <= wdw_h)
              {
                int bx = bgx;

                while (bx <= wdw_w)
                  {
                    BitBlt(hMemDC, bx, by, bgw, bgh, hBmpDC, 0, 0, SRCCOPY);
                    bx += bgw;
                  }

                by += bgh;
              }
          }
          DeleteDC(hBmpDC);

          // draw ball mask to off-screen
          hBmpDC = CreateCompatibleDC(hdc);
          SelectObject(hBmpDC, hBitmap_mask);
          BitBlt(hMemDC, (int)x, (int)y, imgw, imgh, hBmpDC, 0, 0, SRCAND);
          DeleteDC(hBmpDC);

          // draw ball source to off-screen
          hBmpDC = CreateCompatibleDC(hdc);
          SelectObject(hBmpDC, hBitmap);
          BitBlt(hMemDC, (int)x, (int)y, imgw, imgh, hBmpDC, 0, 0, SRCPAINT);
          DeleteDC(hBmpDC);

          // drawing to off-screen is finished

          // draw off-screen bitmap to window
          BitBlt(hdc, 0, 0, wdw_w, wdw_h, hMemDC, 0, 0, SRCCOPY);

          DeleteDC(hMemDC);      // Create*() -> DeleteDC()
          ReleaseDC(hWnd, hdc);  // GetDC() -> ReleaseDC()
        }
      else
        {
          // draw preview
          HDC hdc, hBmpDC;
          hdc = GetDC(hWnd);
          hBmpDC = CreateCompatibleDC(hdc);
          SelectObject(hBmpDC, hBitmap_preview);
          BitBlt(hdc, 0, 0, pvw, pvh, hBmpDC, 0, 0, SRCCOPY);
          DeleteDC(hBmpDC);      // Create*() -> DeleteDC()
          ReleaseDC(hWnd, hdc);  // GetDC() -> ReleaseDC()
        }

      break;

    case WM_DESTROY:
      KillTimer(hWnd, ID_TIMER);  // kill timer

      // delete bitmap
      DeleteObject(offScrnBitmap);
      DeleteObject(hBitmap);
      DeleteObject(hBitmap_mask);
      DeleteObject(hBitmap_bg);
      DeleteObject(hBitmap_preview);

      PostQuitMessage(0);
      break;

    default:
      break;
    }

  return DefScreenSaverProc(hWnd, msg, wParam, lParam);
}

/* Processing of dialog boxes for configuration */
BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg, UINT msg,
                                       WPARAM wParam, LPARAM lParam)
{
  switch (msg)
    {
    case WM_INITDIALOG:
      MessageBox(
        hDlg,
        _T("This screen saver has no configuration options."),
        _T("Sample Screensaver by using MinGW"),
        MB_OK | MB_ICONWARNING);
      EndDialog(hDlg, IDOK);
      return TRUE;

    case WM_COMMAND:
      switch (LOWORD(wParam))
        {
        case IDOK:
          EndDialog(hDlg, IDOK);
          return TRUE;

        case IDCANCEL:
          EndDialog(hDlg, IDCANCEL);
          return TRUE;
        }

      return FALSE;
    }

  return FALSE;
}

/* Register non-standard window classes required by dialog boxes for configuration */
BOOL WINAPI RegisterDialogClasses(HANDLE hInst)
{
  return TRUE;
}

Makefile。 :

Makefile は以下。

_Makefile
mgscrsv2.scr: mgscrsv2.o resource.o
        gcc mgscrsv2.o resource.o -o mgscrsv2.scr -mwindows -lscrnsave

mgscrsv2.o: mgscrsv2.c
        gcc -c mgscrsv2.c

resource.o: resource.rc resource.h ball.bmp ball_mask.bmp bg2.bmp scrsav_preview.bmp
        windres resource.rc -o resource.o

.PHONY: clean
clean:
        rm -f *.scr
        rm -f *.o
  • Windows用プログラムなので、-mwindows を渡している。
  • スクリーンセーバを作るので、-lscrnsave を渡して「libscrnsave.a (or libscrnsavw.a) をリンクせよ」と指定している。

コンパイル/ビルド。 :

全部で以下のファイルになる。
  • ball.bmp
  • ball_mask.bmp
  • bg2.bmp
  • Makefile
  • mgscrsv2.c
  • resource.h
  • resource.rc
  • scrsav_preview.bmp
これらを、同じディレクトリに置いて make と打てば、コンパイル/ビルドされて mgscrsv2.scr が生成される。

ちなみに、make clean と打てば、*.o や *.scr を削除できる。

動作確認。 :

エクスプローラ等で mgscrsv2.scr を右クリック。
  • 「Test」を選べば、フルスクリーン表示時の動作確認ができる。
  • 「構成」を選べば、スクリーンセーバの設定画面で「設定」をクリックした時の動作を確認できる。

正しく動作しているようなら、スクリーンセーバを本来置くべき場所に .scr をコピーして、スクリーンセーバとして動作するのか確認する。

  • Windows が 64bit版、スクリーンセーバが32bit版なら、C:\Windows\SysWOW64\ に .scr をコピー。
  • Windows が 64bit版、スクリーンセーバが64bit版なら、C:\Windows\System32\ に .scr をコピー。
  • Windows が 32bit版なら、C:\Windows\System32\ 以下に .scr をコピー。

今回は MinGW を使ってビルドしたので、生成された .scr は32bit版プログラム。

Windows10 64bit + 32bit版プログラムなので ―― C:\Windows\SysWOW64\ に .scr をコピーして動作確認することになる。

試してみたところ、スクリーンセーバとして、それらしく動いてくれた。

ちなみに、作業に使ったメインPCとは別のPC(Windows10 x64 21H2)にコピーして動作確認したけれど、そちらでもちゃんと動いてくれた。

覚書。 :

Windows用のプログラムをC/C++で書く場合、まずはメイン関数が必要になる。

  • コンソールプログラム(DOS窓上で使う感じのプログラム)なら、main() という名前の関数が必要。
  • GUIアプリなら、WinMain() という名前の関数が必要。

GUIアプリの場合、えてして他にも、WndProc() とか WindowProc() という名前の関数も書くことになるらしい。これはウィンドウプロシージャと呼ばれる関数で、Windowsから送られてきたメッセージを処理する関数なのだとか。

そして、スクリーンセーバはGUIアプリに分類される。だから本来は、WinMain() という関数を書かなきゃいけない。

ところが、前述のスクリーンセーバのサンプルソースを眺めても、WinMain関数なんて書いてない。…どうなってるの?

なんでも、「スクリーンセーバの処理って大体似た感じになるよね」「WinMain() や WndProc() ってフツーは同じ処理を書くよね」ということで、そのあたりは scrnsave.lib (または scrnsavw.lib。MinGW の場合は libscrnsave.a or libscrnsavw.a) の中に既に入っているらしい。そして、「そこから呼び出される3つの関数だけ書けばスクリーンセーバを作れますよ」ということになっている模様。

そんなわけで、scrnsave.h と scrnsav*.lib (libscrnsav*.a) を使う場合は、以下の3つの関数の中身だけ書けばスクリーンセーバが作れてしまう。
  • ScreenSaverProc()
  • ScreenSaverConfigureDialog()
  • RegisterDialogClasses()

とは言え、GDI / GDI+ / DirectX / OpenGL等々を使わないと画面の描画はできないので、そのあたりの知識が必要になってしまうあたり、まだハードルが高い感じはする…。もっと簡単に作れるようにできないものか…。

余談。プレビューモードのウインドウサイズ。 :

今回書いたサンプルでは、プレビューモード(スクリーンセーバ設定画面の小窓に描画する処理)をどのように処理するかで悩んでしまった。

これがもし、座標を指定して線を描画する系のスクリーンセーバなら、フルスクリーン表示だろうと、プレビューモードだろうと、ウインドウサイズに合わせて座標値を計算して描画すればいいだけだから対応するのは難しくなさそうだけど…。

しかし、bitmap画像を描画する場合、bitmapは等倍で描画されて拡大縮小してくれないわけで…。フルスクリーン表示に合わせて座標値等を決めてしまうと、プレビューモード時は変な表示になってしまう。

一応、GDIにも、処理速度はさておいて拡大縮小描画する関数があるらしいので…。あらかじめ描画サイズを決めておいて、空のbitmapにアレコレ描いてから、それをディスプレイ一杯に拡大描画、もしくはプレビューモードの小さいウインドウに縮小描画すれば対応できるだろうけど…。

そのあたり面倒臭くなってしまったので、今回は、プレビューモード用のbitmap画像を一枚用意して、おそらくプレビューモードだろうと思われる時は、そのbitmap画像だけ描画してお茶濁しすることにした。

ちなみに、Windows10上で、スクリーンセーバ設定画面の小窓のサイズを測ってみたら、152 x 112 だった。巷のスクリーンショット等を調べた感じでは、Windows95 の頃からそのサイズだった模様。

そんなわけで、ウインドウサイズを取得して、もし横幅が160ドット以下なら「これはプレビューモードで呼び出されているのだろう」と判断して処理をするようにしてみた。こういうやり方が妥当なのかどうかは分からんけど…。

2022/09/07(水) [n年前の日記]

#1 [prog][windows] Windows用スクリーンセーバの作り方をまだもうちょっと調べてる

Windows用スクリーンセーバをC/C++で作る方法を、まだもうちょっと調べてるところ。

スクリーンセーバ(.scr)のリソースファイル(.rc)に、スクリーンセーバのタイトル名や、アイコンを含めることができるらしい。以下がとても参考になった。ありがたや…。

_How to Scr: Writing an OpenGL Screensaver for Windows
_スクリーンセーバーの実体 (mixi 日記アーカイブ)
_スクリーンセーバーのコマンドライン (mixi 日記アーカイブ)
_スクリーンセーバーの実装手順 (mixi 日記アーカイブ)
_scrnsave.lib の使い方 (mixi 日記アーカイブ)
_リソースの作成 (mixi 日記アーカイブ)
_関数の実装 (mixi 日記アーカイブ)
_実行してみる (mixi 日記アーカイブ)

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。

しつこく何度も書くけれど、MinGW-w64 (+ MSYS2) じゃなくて MinGW (+ MSYS) で作業してます。MinGW-w64 は libscrnsave.a の中身が空なのでスクリーンセーバのビルドはできません。

リソースファイルに含められる情報。 :

前述の参考ページのおかげで、スクリーンセーバのリソースファイルに含める情報として、以下のシンボルが利用できると知った。
  • IDS_DESCRIPTION (値は 1) : スクリーンセーバの名前/タイトルを示す。
  • ID_APP (値は 100) : スクリーンセーバのアイコンを示す。
  • DLG_SCRNSAVECONFIGURE (値は 2003) : スクリーンセーバの設定ダイアログを示す。

ちなみに、これらは scrnsave.h の中で値が定義されている。

_winsdk-10/ScrnSave.h at master - tpn/winsdk-10

そんなわけで、昨日作ったサンプルに対して、それらのシンボルを使ってリソースを含めてみた。

_ビットマップ画像を描画するスクリーンセーバを書いてみた

  • IDS_DESCRIPTION は、"MinGW Scrnsave Sample" にした。これは、スクリーンセーバ設定画面のリスト一覧上の名前になる。
  • ID_APP は、ICON "appli.ico" に。
  • DLG_SCRNSAVECONFIGURE には、OKボタンとキャンセルボタンを並べた。

_resource.rc
/* resource.rc */

#include <windows.h>
#include <scrnsave.h>

#define IDC_STATIC_ICON     2300
#define IDC_STATIC_TEXT     2301

/* bitmap */
IDI_BALL        BITMAP  "ball.bmp"
IDI_BALL_MSK    BITMAP  "ball_mask.bmp"
IDI_BG          BITMAP  "bg2.bmp"
IDI_PREVIEW     BITMAP  "scrsav_preview.bmp"

/* screensaver title */
STRINGTABLE
BEGIN
  IDS_DESCRIPTION       "MinGW Scrnsave Sample"
END

/* Icon */
ID_APP  ICON    "appli.ico"

/* Config dialog */
DLG_SCRNSAVECONFIGURE DIALOG DISCARDABLE  0, 0, 260, 80
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Setting Sample Screensaver"
 FONT 11, "Segoe UI"
BEGIN
  DEFPUSHBUTTON "OK", IDOK, 140, 58, 50, 16
  PUSHBUTTON "Cancel", IDCANCEL, 200, 58, 50, 16
  ICON IDI_WARNING, IDC_STATIC_ICON, 8, 8, 20, 20
  LTEXT "This is the template for the configuration dialog.", IDC_STATIC_TEXT, 32, 8, 230, 25
END
※ このWeb日記システムの関係で、FONT指定行がおかしな状態で表示されてます。先頭の空白は削除してもらえればと…。


アイコンファイル(.ico) は以下。

_appli_ico.zip

リソースファイル内の設定ダイアログを呼び出し。 :

昨日のサンプルでは、スクリーンセーバ設定画面の「設定」をクリックした際、プログラム内でメッセージダイアログを表示するようにしてあったのだけれど。

そもそも、スクリーンセーバの設定ダイアログについては、リソースファイルの中で記述しておけば、そちらが利用されて表示されるらしい…。

そんなわけで、設定ダイアログ関連の記述は以下のようになった。一部だけ抜粋。

_mgscrsv2.c
#include <windows.h>
#include <tchar.h>
#include <scrnsave.h>
#include <time.h>
#include <stdlib.h>
#include "resource.h"

/* ... */

/* Processing of dialog boxes for configuration */
BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg, UINT msg,
                                       WPARAM wParam, LPARAM lParam)
{
  switch (msg)
    {
    case WM_COMMAND:
      switch (LOWORD(wParam))
        {
        case IDOK:
          EndDialog(hDlg, 0);
          return TRUE;

        case IDCANCEL:
          EndDialog(hDlg, 1);
          return TRUE;
        }

      return FALSE;
    }

  return FALSE;
}

/* Register non-standard window classes required by dialog boxes for configuration */
BOOL WINAPI RegisterDialogClasses(HANDLE hInst)
{
  return TRUE;
}
OKボタンやキャンセルボタンがクリックされた時の処理を書いておくだけでいいらしい。もちろん、色々なウィジェットを配置した場合は、それらの動作を反映させる必要があるのだろうけど…。

一応、ビルドに必要なファイル群をzipにして置いときます。

_mgscrsv2.zip

2022/09/08(木) [n年前の日記]

#1 [prog][windows] リソースエディタを試用

Windows10 x64 21H2上で動作するリソースエディタを少し試用してみた。

C/C++ でWindows用プログラムを作る際、リソースファイル(.rc)というファイルを用意して、 そういったアレコレを含めることができるのだけど。ダイアログ上のボタンその他の表示位置を修正する作業が ―― リソースファイルをテキストエディタで開いて手打ちで調整していく作業が面倒だなと…。当てずっぽうで座標値を書いてみて、ビルドして、確認して、また値を変更するのは…シンドイ…。

そのあたり、GUIで編集できるツールが無いものかと。

リソーエディタを試用。 :

ググってみたら、リソーエディタなるツールに遭遇したので試用してみた。

_リソーエディタ(RisohEditor)
_リソーエディタの詳細情報 : Vector ソフトを探す!

ss01_risoheditor.png

RisohEditor-5.7.2-portable.zip をDL・解凍。RisohEditorPortable.exe を実行。

手元の .rc を読み込ませようとするとエラーが出てしまう…。付属のサンプルは開けるから、こちらの .rc が何かおかしいのだろうか…。MinGW の windres に渡してもエラーが出ない .rc なのだけどな…。

何も開いてない状態からダイアログを追加して、といった感じで操作したら動いてくれた。一旦そんな感じで各ウィジェットの位置のアタリをつけて、といった使い方なら問題は無さそう。少なくとも、テキストエディタで.rcを書き換えていくより全然マシな気がする。

Resource Hackerを試用。 :

Resource Hacker 5.1.8 を試用してみた。

_Resource Hacker

ss02_resourcehacker.png

このツールを使っても、ある程度調整作業ができるようだなと…。
  • 何のファイルも開かない状態で、Action → Add using Script Template、を選ぶと、DIALOG の雛形がエディタ画面に挿入される。
  • ツールバー上の Compile Script (緑色の再生ボタン)をクリックするか、F5キーを叩けば、どんな見た目になるか表示される。
  • 表示されたダイアログ上で右クリックして Insert Control を選べば、ウィジェットを追加していける。

そのままリソースファイルとして利用できないかもしれないけれど、各ウィジェットの表示位置やサイズを確認する作業程度なら使えそう。

XN Resource Editorを試用。 :

XN Resource Editor というリソースエディタもあるらしいので試用してみた。日本語版もあるらしい。ありがたや。

_このサイトにある Delphi 支援アプリケーションは? [Delphi Programming]
_XN Resource Editor :職人的フリーソフトの使い方

ss03_xnresourceeditor.png

リソース → リソースを追加、で Dialog を追加後、右側のウィジェット一覧から Static Text や Button などを追加していけばいい。

プロパティ入力欄で Enterキーを押しても反映されないけれど、TABキーでフォーカスを変えていけば反映される模様。

ただ、配置したウィジェットの表示位置をマウスドラッグで変更したりはできないっぽい。ウェジットのサイズなら、マウスドラッグで変更できるようだけど…。

少し触ってるうちに、サイズの計算がなんだかおかしいことに気づいた。マウスドラッグ時に設定される値と、プロパティ一覧上の入力値が大きくずれている。どちらが正しいのか…。

オリジナルの英語版をDL・インストールしてみたけれど、そちらは何か操作する度にアプリが落ちてしまった。

_Releases - stefansundin/xn_resource_editor

BCCFormを試用。 :

Borland C++向けのツールもあるらしい。

_BCCForm and BCCSkeltonの詳細情報 : Vector ソフトを探す!

ss04_bccform.png

BCCFORM.EXE (2.41R1.5) をDLしてインストールしてみた。

セットアップファイルを実行したら、何故か D:\Program Files\Borland\BCCForm\ に決め打ちでファイルが展開されてしまった…。昔のアプリだから仕方ないのかな…。

BCCForm.exe というのがリソースエディタっぽい。実行して、ファイル → 新規作成、をしてから、編集 → リソースの追加 → ダイアログ、でダイアログが表示される。ダイアログForm上で右クリックすると、追加するウィジェット種類を選べる。初期状態ではButtonが選ばれてるので、マウスでドラッグするとそのサイズでボタンが配置される。

Pencil Projectを試用。 :

リソースファイルにはならないけれど、見た目のレイアウトを検討できる Pencil というツールを使用してみた。3.1.0 を試用。

_Home - Pencil Project

ss05_pencil.png

各オブジェクトの座標値が、リソースファイルのソレとは全然違う値になってしまった。まあ、そもそも用途/目的が違うツールだろうから、仕方ないか…。

参考ページ。 :

余談。 :

Python等で利用できる各種GUIライブラリ等と比較すると、座標を指定して位置を決める仕組み自体がどうにも…。「右に向かって並べるよ」「下に向かって並べるよ」「グリッド状に配置するよ」等々、自動である程度レイアウトをしてくれる Sizer の類は無いのかな…。

#2 [prog][windows] LCC-win32をインストールしてみた

Windows10 x64 21H2上で、LCC-win32 というCコンパイラをインストールしてみた。LCC-win32 に、weditres.exe というリソースエディタが付属しているという話を見かけたので…。

_LCC-Win: A free compiler system for Windows Operating Systems by Jacob Navia

lccwin32.exe をDLして実行。今回は D:\Dev\lcc\ にインストールしてみた。ファイルのコピー後、ライブラリのビルドが始まる。少々待たされてインストール終了。

(LCC-win32インストールフォルダ)\bin\ にPATHを通してから、lc hoge.c でコンパイル。hoge.exe が生成された。

_フリーコンパイラたちで“へろ〜わーるど”

ちなみに、lc -v と打ったら、lcc-win32 3.8. と表示された。
> lc -v
"D:\Dev\lcc\bin\lcc.exe" -v
Logiciels/Informatique lcc-win32 version 3.8. Compilation date: Mar 29 2013 13:11:27
"D:\Dev\lcc\bin\lcclnk.exe" -v
lcclnk version 1.3

weditres.exeを実行してみた。 :

リソースエディタ、weditres.exe を実行してみたのだけど、何か操作する度にアプリが落ちる。とにかく落ちる。プロジェクトの新規作成をするだけで落ちる。これでは何も作業ができない…。

weditres.exe を触ってみたくて LCC-win32 を導入したので、その weditres.exe が使えないなら意味が無い。LCC-win32 は、アンインストールすることにした。

#3 [prog][windows] MinGWでfreeglutを使ってみた

Windows10 x64 21H2 + MinGW (gcc 9.2.0) で freeglut を使ってみたい。

freeglut というのは、OpenGL を多少は簡単に使えるようにしてくれるツールキット。元々は GLUT というツールキットがあったのだけど、バグがまだあるのに開発が止まって放置状態になったそうで、その代替として登場したのが freeglut らしい。

さておき。あくまで MinGW (gcc 9.2.0) を使って作業してみる。MinGW-w64 (+ MSYS2) ではなくて、MinGW (+ MSYS)。

MinGW についてはインストール済みで、既に利用できる状態になっているものとする。

freeglutのバイナリの入手。 :

以下のページが参考になった。

_MinGW + OpenGL - 日々量産
_freeglut Windows Development Libraries

上記の英語ページの、freeglut 3.0.0 MinGW Package と書いてあるところの少し下、Download freeglut 3.0.0 for MinGW をクリックして、freeglut-MinGW-3.0.0-1.mp.zip を入手する。

MinGWの中にコピー。 :

freeglut-MinGW-3.0.0-1.mp.zip を解凍すると、bin/, include/, lib/ というフォルダがあるので、中に入ってるファイル群を、MinGWインストールフォルダ内の同名フォルダの中にコピーする。ちなみに、x64 というフォルダも入ってるけれど、それは64bit用。MinGW は 32bit なので、x64フォルダの中身は無視していい。

今回は以下のファイルをコピーした。
bin/freeglut.dll

include/GL/freeglut.h
include/GL/freeglut_ext.h
include/GL/freeglut_std.h
include/GL/glut.h

lib/libfreeglut.a
lib/libfreeglut_static.a

ちなみに、MSYS2 + MinGW-w64 を使っている場合は、パッケージとして freeglut が用意されているので、pacman を使ってインストールができるらしい。

_Base Package: mingw-w64-freeglut - MSYS2 Packages
_Package: mingw-w64-x86_64-freeglut - MSYS2 Packages
_Package: mingw-w64-i686-freeglut - MSYS2 Packages

使ってみる。 :

使い方は以下で説明されてる。

_Using freeglut or GLUT with MinGW

以下で配布されてるサンプルを利用して、コンパイルできるか試してみる。

_TransmissionZero/Hello-GLUT: A very simple "Hello World!" GLUT application demonstrating how to write OpenGL applications in C with MinGW and MSVC.

上記のサンプルは3ファイルに分かれているけど、別に1ファイルでもいいよなと思えてきた。修正して、1ファイルにしてみた。

_hello_glut.c
#include <stdlib.h>
#include <GL/glut.h>

/* Keyboard callback function */
void keyboard(unsigned char key, int x, int y)
{
  switch (key)
    {
      /* Exit on escape key press */
      case '\x1B':
      {
        exit(EXIT_SUCCESS);
        break;
      }
    }
}

/* Display callback function */
void display()
{
  glClear(GL_COLOR_BUFFER_BIT);

  /* Display a red square */
  glColor3f(1.0f, 0.0f, 0.0f);

  glBegin(GL_POLYGON);
  glVertex2f(-0.5f, -0.5f);
  glVertex2f(0.5f, -0.5f);
  glVertex2f(0.5f,  0.5f);
  glVertex2f(-0.5f,  0.5f);
  glEnd();

  glFlush();
}

/* Main method */
int main(int argc, char** argv)
{
  glutInit(&argc, argv);

  /* Create a single window with a keyboard and display callback */
  glutCreateWindow("GLUT Test");
  glutKeyboardFunc(&keyboard);
  glutDisplayFunc(&display);

  /* Run the GLUT event loop */
  glutMainLoop();

  return EXIT_SUCCESS;
}


コンパイルは以下。hello_glut.exe が生成される。
# スタティックリンクしない場合。

gcc hello_glut.c -o hello_glut.exe -lfreeglut -lopengl32 -lwinmm -lgdi32 -mwindows

# スタティックリンクする場合。

gcc hello_glut.c -o hello_glut.exe -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32 -mwindows
  • スタティックリンクしない場合は、実行ファイルのサイズは小さくなるけれど、動作には freeglut.dll が必要になる。
  • スタティックリンクする場合は、実行ファイルが freeglut.dll の中身を含んでいるので、freeglut.dll が無くても動く。ファイルサイズは少し大きくなる。

hello_glut.exe を実行して、赤い四角が表示されたらOK。ちなみに、ESCキーを押すとウインドウが閉じる。

ss_hello_glut.png

これで、freeglutを使っているCソースを、MinGWでコンパイル/ビルドできる状態になった。

2022/09/09(金) [n年前の日記]

#1 [prog][windows] OpenGLを使ったWindows用スクリーンセーバをMinGWでコンパイル

Windows10 x64 21H2上で、OpenGLを使ったWindows用スクリーンセーバのサンプルを、MinGW を使ってコンパイル/ビルドしたい。

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0) + freeglut 3.0.0-1。

MinGW-w64 (+ MSYS2) ではなくて、MinGW (+ MSYS) で作業。何度も何度も書くけれど、MinGW-w64 は libscrnsave.a (libscrnsavw.a) の中身が空っぽなのでスクリーンセーバをビルドできない。

参考にしたページは以下。OpenGL を使って、緑色の四角が円を描いて回るスクリーンセーバのソースが紹介されてる。リソースファイルやコンパイル手順までは紹介されてない。

_How to Scr: Writing an OpenGL Screensaver for Windows
_How to Scr: Sample Code for an OpenGL Screensaver

動作結果は以下。解像度が荒いけど雰囲気ぐらいは伝わるかと…。




ちなみに、このスクリーンセーバは OpenGL を使っているせいか、デスクトップキャプチャツール (OBS Studio 28.0.1) で数秒ほどキャプチャできなかった。デスクトップ画面が通常状態から OpenGL に切り替わる際にキャプチャできなくなるっぽい…。

MinGWにfreeglutを導入。 :

MinGW で freeglut を使えるようにする手順は、昨日メモしたのでそちらを参考に。

_MinGWでfreeglutを使ってみた

ソース。 :

ソースファイル群とアイコンをzipにして置いておく。

_greensquare_src.zip

中身は以下。
  • greensquare.cpp
  • icon.ico
  • Makefile
  • resource.h
  • resource.rc


一応、各ファイルも載せておきます。

リソースファイルは以下。ちなみに、元の参考ページではリソースファイルが掲載されてなかったので、Resource Hacker 5.1.8 を使って .scr からリソースを逆算(?)して書いてみた。

_resource.rc
/* resource.rc */

#include <windows.h>
#include <scrnsave.h>
#include "resource.h"

#define IDC_STATIC_ICON     2300
#define IDC_STATIC_TEXT     2301

/* LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US */

/* Screensaver title */
STRINGTABLE
BEGIN
  IDS_DESCRIPTION       "Green Square Example Scr"
END

/* Icon */
ID_APP  ICON    "icon.ico"

/* Dialog */
DLG_SCRNSAVECONFIGURE DIALOG DISCARDABLE  0, 0, 186, 82
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "GreenSquare Setup"
 FONT 11, "Segoe UI"
BEGIN
  DEFPUSHBUTTON "OK", IDOK, 39, 61, 50, 14 
  PUSHBUTTON    "Cancel", IDCANCEL, 107, 61, 50, 14
  AUTOCHECKBOX  "Square tumbles as it revolves", IDC_TUMBLE, 25, 35, 137, 12
  LTEXT "One option... that's all you get.", IDC_STATIC_TEXT, 17, 15, 150, 13
END
※ この日記システムの問題で、FONT指定行の見た目がおかしくなってます。行頭の空白を削除してもらえればと。


リソースファイルのヘッダファイルは以下。

_resource.h
#ifndef _RESOURCE_H_
#define _RESOURCE_H_

#define IDC_TUMBLE 1000

#define IDC_STATIC_ICON     2300
#define IDC_STATIC_TEXT     2301

#endif


C++ソースファイルは以下。ちなみにオリジナル版はレジストリの読み書きにも対応してるのだけど、いきなりレジストリに書き込むのはちょっと怖いので、以下のソースではレジストリ関連処理( GetConfig()、WriteConfig() ) は if (0) { 〜 } でコメントアウト/無効化してある。

_greensquare.cpp
// greensquare.cpp is an open-source example screensaver by Rachel Grey, lemming@alum.mit.edu.
// Paste into an IDE to compile if desired.
// I haven't chosen to include the resource file, so you'd need to provide a description string /and so forth.

#include <windows.h>
#include <scrnsave.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include "resource.h"

// get rid of these warnings: truncation from const double to float conversion from double to float
// #pragma warning(disable: 4305 4244)

//Define a Windows timer

#define TIMER 1

#define FPS 60

// globals for size of screen
static int Width, Height;

static bool bTumble = true;

// a global to keep track of the square's spinning
static GLfloat spin = 0;

/////////////////////////////////////////////////
////   INFRASTRUCTURE ENDS, SPECIFICS BEGIN   ///
////                                          ///
////    In a more complex scr, I'd put all    ///
////     the following into other files.      ///
/////////////////////////////////////////////////

// Initialize OpenGL
static void InitGL(HWND hWnd, HDC &hDC, HGLRC &hRC)
{
  PIXELFORMATDESCRIPTOR pfd;
  ZeroMemory(&pfd, sizeof pfd);
  pfd.nSize = sizeof pfd;
  pfd.nVersion = 1;
  //pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL; //blaine's
  pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
  pfd.iPixelType = PFD_TYPE_RGBA;
  pfd.cColorBits = 24;

  hDC = GetDC(hWnd);

  int i = ChoosePixelFormat(hDC, &pfd);
  SetPixelFormat(hDC, i, &pfd);

  hRC = wglCreateContext(hDC);
  wglMakeCurrent(hDC, hRC);
}

// Shut down OpenGL
static void CloseGL(HWND hWnd, HDC hDC, HGLRC hRC)
{
  wglMakeCurrent(NULL, NULL);
  wglDeleteContext(hRC);
  ReleaseDC(hWnd, hDC);
}


void SetupAnimation(int Width, int Height)
{
  //window resizing stuff
  glViewport(0, 0, (GLsizei) Width, (GLsizei) Height);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  if (0)
    {
      glOrtho(-300, 300, -240, 240, 25, 75);
    }
  else
    {
      float w = 300;
      float h = ((w * 2) * Height / Width) / 2;
      glOrtho(-w, w, -h, h, 25, 75);
    }

  glMatrixMode(GL_MODELVIEW);

  glLoadIdentity();
  gluLookAt(0.0, 0.0, 50.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
  //camera xyz, the xyz to look at, and the up vector (+y is up)

  //background
  glClearColor(0.0, 0.0, 0.0, 0.0); //0.0s is black

  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

  // glShadeModel(GL_FLAT);
  glShadeModel(GL_SMOOTH);
}

void OnTimer(HDC hDC) //increment and display
{
  float xvals[] = {-30.0, -30.0, 30.0,  30.0};
  float yvals[] = {-30.0,  30.0, 30.0, -30.0};

  spin = spin + 1;

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix();

  glRotatef(spin, 0.0, 0.0, 1.0);
  glTranslatef(120, 0, 0);
  glRotatef(spin * ((bTumble) ? -4.0 : -1.0), 0.0, 0.0, 1.0);

  //draw the square (rotated to be a diamond)

  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  glColor3f(0.1, 1.0, 0.3); // set green color

  glBegin(GL_POLYGON);

  for (int i = 0; i < 4; i++)
    glVertex2f(xvals[i], yvals[i]);

  glEnd();

  glPopMatrix();

  glFlush();
  SwapBuffers(hDC);
}

void CleanupAnimation()
{
  // didn't create any objects, so no need to clean them up
}


/////////   REGISTRY ACCESS FUNCTIONS     ///////////

void GetConfig()
{
  if (0)
    {
      // get configuration from registry

      HKEY key;
      //DWORD lpdw;

      if (RegOpenKeyEx(HKEY_CURRENT_USER,
                       "Software\\GreenSquare", //lpctstr
                       0,                       //reserved
                       KEY_QUERY_VALUE,
                       &key) == ERROR_SUCCESS)
        {
          DWORD dsize = sizeof(bTumble);
          DWORD dwtype =  0;

          RegQueryValueEx(key, "Tumble", NULL, &dwtype,
                          (BYTE*)&bTumble, &dsize);

          //Finished with key
          RegCloseKey(key);
        }
      else //key isn't there yet--set defaults
        {
          bTumble = true;
        }
    }
  else
    {
      bTumble = true;
    }
}

void WriteConfig(HWND hDlg)
{
  if (0)
    {
      HKEY key;
      DWORD lpdw;

      const char* null_str = "";
      LPSTR str = const_cast<LPSTR>(null_str);

      if (RegCreateKeyEx(HKEY_CURRENT_USER,
                         "Software\\GreenSquare", // lpctstr
                         0,                       // reserved
                         str,                     // ptr to null-term string specifying the object type of this key
                         REG_OPTION_NON_VOLATILE,
                         KEY_WRITE,
                         NULL,
                         &key,
                         &lpdw) == ERROR_SUCCESS)

        {
          RegSetValueEx(key, "Tumble", 0, REG_DWORD,
                        (BYTE*)&bTumble, sizeof(bTumble));

          //Finished with keys
          RegCloseKey(key);
        }
    }
}

//////////////////////////////////////////////////
////   INFRASTRUCTURE -- THE THREE FUNCTIONS   ///
//////////////////////////////////////////////////

// Screen Saver Procedure
LRESULT WINAPI ScreenSaverProc(HWND hWnd, UINT message,
                               WPARAM wParam, LPARAM lParam)
{
  static HDC hDC;
  static HGLRC hRC;
  static RECT rect;

  switch (message)
    {
      case WM_CREATE:
        // get window dimensions
        GetClientRect(hWnd, &rect);
        Width = rect.right;
        Height = rect.bottom;

        // get configuration from registry
        GetConfig();

        // setup OpenGL, then animation
        InitGL(hWnd, hDC, hRC);
        SetupAnimation(Width, Height);

        // set timer to tick every 10 ms
        SetTimer(hWnd, TIMER, (int)(1000 / FPS), NULL);
        return 0;

      case WM_TIMER:
        OnTimer(hDC);       // animate!
        return 0;

      case WM_DESTROY:
        KillTimer(hWnd, TIMER);
        CleanupAnimation();
        CloseGL(hWnd, hDC, hRC);
        return 0;
    }

  return DefScreenSaverProc(hWnd, message, wParam, lParam);
}

BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg, UINT message,
                                       WPARAM wParam, LPARAM lParam)
{
  //InitCommonControls();
  //would need this for slider bars or other common controls

  HWND aCheck;

  switch (message)
    {
      case WM_INITDIALOG:
        LoadString(hMainInstance, IDS_DESCRIPTION, szAppName, 40);

        GetConfig();

        aCheck = GetDlgItem(hDlg, IDC_TUMBLE);
        SendMessage(aCheck, BM_SETCHECK, (bTumble) ? BST_CHECKED : BST_UNCHECKED, 0);
        return TRUE;

      case WM_COMMAND:
        switch (LOWORD(wParam))
          {
            case IDC_TUMBLE:
              bTumble = (IsDlgButtonChecked(hDlg, IDC_TUMBLE) == BST_CHECKED);
              return TRUE;

            //cases for other controls would go here

            case IDOK:
              WriteConfig(hDlg);  // get info from controls
              EndDialog(hDlg, LOWORD(wParam) == IDOK);
              return TRUE;

            case IDCANCEL:
              EndDialog(hDlg, LOWORD(wParam) == IDOK);
              return TRUE;
          }

        return FALSE;
    }  //end command switch

  return FALSE;
}

// needed for SCRNSAVE.LIB
BOOL WINAPI RegisterDialogClasses(HANDLE hInst)
{
  return TRUE;
}


アイコンファイルは以下。中に icon.ico が入ってる。

_icon.cio.zip

コンパイルの仕方。 :

Makefile は以下。

_Makefile
greensquare.scr: greensquare.o resource.o
        g++ greensquare.o resource.o -o greensquare.scr -static -lstdc++ -lgcc -lscrnsave -lopengl32 -lglu32 -lgdi32 -lcomctl32 -lwinmm -mwindows

greensquare.o: greensquare.cpp resource.h
        g++ -c greensquare.cpp

resource.o: resource.rc resource.h icon.ico
        windres resource.rc resource.o

.PHONY: clean
clean:
        rm -f *.scr
        rm -f *.o

Makefile があれば、make と打つだけでコンパイル/ビルドできる。ちなみに、make clean で *.o と *.scr を削除できる。

一応説明しておくと…。
  • windres resource.rc resource.o で、リソースファイル resource.rc を .o に変換する。
  • g++ -c greensquare.cpp で、.cpp をコンパイルして .o を生成。
  • g++ greensquare.o resource.o -o greensquare.scr ... で、.oファイル群をリンクして .scr を生成。

リンク時の指定については以下。
  • -static : スタティックリンクを指定。
  • -lstdc++ -lgcc : C言語の標準的なライブラリをリンク。
  • -lscrnsave : スクリーンセーバ用ライブラリをリンク。libscrnsave.a (libscrnsavw.a) をリンクする。
  • -lopengl32 -lglu32 : OpenGLのライブラリをリンク。
  • -lgdi32 : GDIライブラリをリンク。
  • -lcomctl32 : よく分かってないけれど、これをつけないと「*.dll が見つからない」と言われて .scr が動作しなかった。
  • -lwinmm : Windowsのタイマー関係ライブラリ、らしい。
  • -mwindows : Windows用アプリケーション(GUIアプリ)であることを指定。

動作確認。 :

greensquare.scr を右クリックして動作確認。
  • 「Test」を選ぶと、フルスクリーン表示の動作確認ができる。
  • 「構成」を選ぶと、スクリーンセーバ設定画面で「設定」をクリックした時の設定ダイアログが表示される。

MinGW で生成したので、この .scr は32bit版。それをWindows10 64bit上で動かしているので、C:\Windows\SysWOW64\ 以下に .scr をコピーして動作確認。スクリーンセーバとしても動いてくれた。

とりあえず、これで OpenGL を使ったスクリーンセーバを MinGW でコンパイル/ビルドできることは分かった。後は、OpenGL を使った描画処理部分を ―― OnTimer() のあたりを改造していけば、それらしいスクリーンセーバを作れそうな予感。

#2 [python][windows] PyOpenGLが使うfreeglut.dllの場所を調べた

Windows10 x64 21H2 + Python 3.9.13 x64 + PyOpenGL 3.1.6 で実験をしていたのだけど、freeglut.dll がどこに置いてあるのかが気になったので少し調べてみた。

参考ページは以下。

_【Python】PyOpenGLのインストール方法を解説 | ジコログ

前提条件。 :

まず、自分の環境では、以下から .whl を入手してインストールしている。

_Archived: Python Extension Packages for Windows - Christoph Gohlke
  • PyOpenGL-3.1.6-cp39-cp39-win_amd64.whl
  • PyOpenGL_accelerate-3.1.6-cp39-cp39-win_amd64.whl

インストールは以下。
pip install PyOpenGL-3.1.6-cp39-cp39-win_amd64.whl
pip install PyOpenGL_accelerate-3.1.6-cp39-cp39-win_amd64.whl

この .whl には freeglut*.dll が同梱されているので、巷の解説ページで書かれているように、freeglut.dll を別途入手して C:\Windows\System32\ にコピーしなくても PyOpenGL が動いてくれる。

freeglut.dllの在り処。 :

さておき。その freeglut.dll は、実際はどこにあるのか…。調べた感じでは、どうも以下のフォルダに .dll が入ってるように見えた。

(Python3インストールフォルダ)\Lib\site-packages\OpenGL\DLLS\

以下の2つが入っていた。
  • freeglut64.vc16.dll
  • gle64.vc16.dll

ちなみに、Python 2.7.18 + PyOpenGL 3.1.0 上で確認してみたら、以下のファイルが入っていた。freeglut だけではなく GLUT も入っている。
  • freeglut32.vc9.dll
  • gle32.vc9.dll
  • glut32.vc9.dll


これらの .dll は、以下のスクリプトから読み込まれている。

(Python3インストールフォルダ)\Lib\site-packages\OpenGL\platform\win32.py

処理をしてる部分を抜き出してみる。
    def GLUT( self ):
        for possible in ('freeglut%s.%s'%(size,vc,), 'glut%s.%s'%(size,vc,)):
            # Prefer FreeGLUT if the user has installed it, fallback to the included
            # GLUT if it is installed
            try:
                return ctypesloader.loadLibrary(
                    ctypes.windll, possible, mode = ctypes.RTLD_GLOBAL
                )
            except WindowsError:
                pass
        return None
「freeglut*.*.dll」もしくは「glut*.*.dll」が見つかったらそれらを使うようだなと…。

freeglut.dllを差し替えてみる。 :

試しに、前述の処理部分を以下のように修正してみた。
        for possible in ('freeglut%s.%s'%(size,vc,), 'glut%s.%s'%(size,vc,)):
# ↓
        for possible in ('freeglut', 'freeglut%s.%s' % (size,vc), 'glut%s.%s' % (size,vc)):

これで、freeglut??.vc*.dll ではなく、freeglut.dll を読み込んでくれるのではないかと…。

以下から、freeglut 3.0.0 MSVC Package (freeglut-MSVC-3.0.0-2.mp.zip) を入手。

_freeglut Windows Development Libraries

解凍すると、bin/x64/freeglut.dll が入ってる。

以下のフォルダにコピー。
(Python3インストールフォルダ)\Lib\site-packages\OpenGL\DLLS\
or
(Python3インストールフォルダ)\

加えて、freeglut64.vc16.dll を freeglut64.vc16.dll.orig にリネームした。これで freeglut64.vc16.dll は呼び出されない状態になって、別途導入した freeglut.dll が呼び出されてる状態になるはず。

手元のサンプルスクリプトを動かしてみたところ、この状態でも動いてくれた。

しかし、上記の freeglut.dll はバージョンが 3.0.0 と古い。

MSYS2 + MinGW-w64 で入手できる freeglut.dll (libfreeglut.dll) は 3.2.2 なので、そちらも試してみた。

以下のファイルをコピーしてくる。
(MSYS2インストールフォルダ)\mingw64\bin\libfreeglut.dll

これを、freeglut.dll にリネームして、前述の場所にコピー。この状態でも簡単なサンプルなら動いてくれた。

ところで。手元の環境では、Python 3.9.13 のインストールフォルダ直下に freeglut.dll を置いても動いているわけで…。もし、その場所に置いても問題無く動くのであれば…。巷の解説ページは C:\Windows\System32\ 以下にコピーしてる事例がほとんどだけど、Pythonインストールフォルダに入れたほうが、まだ安全だったりしないのだろうか…。

余談。glutIdleFunc()が動かなくて悩んだ。 :

PyOpenGL関係のサンプルファイルを眺めていると、glutIdleFunc() を使っている事例を結構見かけるのだけど。手元の環境、Windows10 x64 21H2 + Python3.9.13 + PyOpenGL 3.1.6 上ではエラーが出てしまってかなり悩んだ。

> py 03_teapot_draw_idle.py
Traceback (most recent call last):
  File "C:\Python\Python39-64\lib\site-packages\OpenGL\GLUT\special.py", line 130, in safeCall
    return function( *args, **named )
TypeError: idle() missing 1 required positional argument: 'value'
 GLUT Idle callback <function idle at 0x000002A6438D8B80> with (),{} failed: returning None idle() missing 1 required positional argument: 'value'

やりたいことは一定の時間間隔で処理を呼び出したいだけなので、glutIdleFunc() の代わりに glutTimerFunc() を使えば目的は果たせるのだけど…。若干気になる状態なわけで。ググってみても仕様が変わった等の話を見かけないし。この症状が気になって freeglut.dll を色々差し替えていたわけで。結局動作は変わらなかったのだけど。

ふと、渡していた関数に引数があることに気づいた。glutTimerFunc() に渡す関数は引数を一つ用意するのだけど、その関数を安易に流用して、glutIdleFunc() にそのまま渡していたのが問題だった。何の引数も取らない関数を渡したらエラーが解消された。そういうオチか…トホ。

def on_timer(value):
    """timer callback."""
    global yrot
    yrot += 0.5
    glutPostRedisplay()  # Redraw

    glutTimerFunc(int(1000 / FPS), on_timer, 0)


def idle():
    """idle callback."""
    global yrot
    yrot += 0.5
    glutPostRedisplay()  # Redraw

# ...

    if USE_IDLE:
        glutIdleFunc(idle)
    else:
        glutTimerFunc(int(1000 / FPS), on_timer, 0)

2022/09/10() [n年前の日記]

#1 [python][windows] PyOpenGLについて調べてる

Windows10 x64 21H2 + Python 3.9.13 x64 + PyOpenGL 3.1.6 で実験中。記述の仕方についていくつか分かったことがあるのでメモ。

glutMainLoop() の終了のさせ方。 :

GLUT を使っていた頃は glutMainLoop() を終了させる方法が無かったらしいけど、freeglut では glutLeaveMainLoop() が追加されて、glutLeaveMainLoop() を呼ぶと glutMainLoop() を終了させられるようになった模様。

def _keyfunc(key, x, y):

    ESCAPE = "\x1b"
    # ESCAPE = b'\x1b'

    key = key.decode("ascii", "ignore")
    print("Push key,", key, x, y)

    if key == ESCAPE or key == "q":
        print("Exit")
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()

# ...

def main():

    # ...

    glutKeyboardFunc(_keyfunc)
    
    glutMainLoop()

PyOpenGL-Demo 3.1.1 の中の、NeHe/ の中を眺めていて、そういう記述ができることが分かった。

_PyOpenGL-Demo - PyPI

ちなみに、PyOpenGL-Demo のインストールの仕方は以下。
pip install PyOpenGL-Demo
(Python3インストールフォルダ)\Lib\site-packages\OpenGL_Demo\ の中に展開される。

キーの読み取りその1。 :

PyOpenGL は、glutKeyboardFunc() や glutSpecialFunc() を使うことで、キーが押された時に呼ばれる関数を登録できるのだけど、どのキーが押されたか比較する記述で悩んだ。昔は key == 'q' といった感じで書けたようだけど…。

Python 3.x では、b'q' とか b'\x1b' といった感じで、最初に「b」をつける必要があるらしい。

def keyboard(key, x, y):
    """ キーボードが押された時に呼ばれるコールバック関数. """
    global xspeed, yspeed

    # ESC key
    ESCAPE = b'\x1b'

    # ESCAPE = 27
    # ESCAPE = chr(27)
    # ESCAPE = '\033'
    # ESCAPE = "\x1b"
    
    if key == ESCAPE or key == b'q':
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()
    elif key == GLUT_KEY_UP:
        xspeed -= 0.1
    elif key == GLUT_KEY_DOWN:
        xspeed += 0.1
    elif key == GLUT_KEY_LEFT:
        yspeed -= 0.1
    elif key == GLUT_KEY_RIGHT:
        yspeed += 0.1
    elif key == b'l':  # 照明のオンオフ
        if glIsEnabled(GL_LIGHTING):
            glDisable(GL_LIGHTING)
        else:
            glEnable(GL_LIGHTING)

# ...

def main():

    # ...

    glutKeyboardFunc(keyboard)
    glutSpecialFunc(keyboard)

ESCキーをどう記述すればいいのかで結構悩んだ…。Python 2.x 時代は、27、chr(27)、'\033'、'\x1b' 等を指定してたけど…。Python 3.x では b'\x1b' と書けばいいらしい。

カーソルキーが押されたかどうかは、GLUT_KEY_UP、GLUT_KEY_DOWN、GLUT_KEY_LEFT、GLUT_KEY_RIGHT と比較すれば判別できる。ただ、glutSpecialFunc() を使わないと、それらの特殊キーは読み取れない。

キーの読み取りその2。 :

前述の記述は、引数に何の処理もしないで読み取る際の記述だけど、.decode() を通してから比較する方法もある模様。

def _keyfunc(key, x, y):

    ESCAPE = "\x1b"
    # ESCAPE = b'\x1b'

    key = key.decode("ascii", "ignore")
    print("Push key,", key, x, y)

    if key == ESCAPE or key == "q":
        print("Exit")
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()

# ...

    glutKeyboardFunc(_keyfunc)

「b'\x1b'」という記述が、「"\x1b"」になってる。また、「b'q'」が「"q"」になってる。

2022/09/11() [n年前の日記]

#1 [python] PyOpenGLでテクスチャを表示

PyOpenGL を使ってテクスチャ画像を描画してみたい。

環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + PyOpenGL 3.1.6 + freeglut 3.0.0 64bit。

実行結果は以下。ポリゴン1枚(GL_QUADS)にテクスチャを貼り付けることができている。

ソース。 :

こんな感じのソースになった。

_01_draw_quads_tex.py
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
import sys
from PIL import Image


IMGS = [
    "img_rgb.png",
    "img_rgba.png",
    "img_grayscale.png",
    "img_palette.png",
]

IMG_IDX = 0

SCRW, SCRH = 512, 512
FPS = 60
PANGLE = 60.0

window = 0

# Rotation angle for the Quads
rotx = 0.0
roty = 0.0

texture = 0


def load_texture():
    global texture

    # load image by using PIL
    im = Image.open(IMGS[IMG_IDX])
    w, h = im.size
    print("Image: %d x %d, %s" % (w, h, im.mode))

    if im.mode == "RGB":
        # RGB convert to RGBA
        im.putalpha(alpha=255)
    elif im.mode == "L" or im.mode == "P":
        # Grayscale, Index Color convert to RGBA
        im = im.convert("RGBA")

    raw_image = im.tobytes()

    ttype = GL_RGBA
    if im.mode == "RGB":
        ttype = GL_RGB
    elif im.mode == "RGBA":
        ttype = GL_RGBA

    glBindTexture(GL_TEXTURE_2D, glGenTextures(1))

    # glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4)

    # set texture
    glTexImage2D(
        GL_TEXTURE_2D,      # target
        0,                  # MIPMAP level
        ttype,              # texture type (RGB, RGBA)
        w,                  # texture image width
        h,                  # texture image height
        0,                  # border width
        ttype,              # texture type (RGB, RGBA)
        GL_UNSIGNED_BYTE,   # data is unsigne char
        raw_image,          # texture data pointer
    )

    glClearColor(0, 0, 0, 0)
    glShadeModel(GL_SMOOTH)

    # set texture repeat
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)

    # set texture filter
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND)


def InitGL(width, height):
    glClearColor(0.125, 0.25, 0.5, 0.0)  # background color
    glClearDepth(1.0)  # Enables Clearing Of The Depth Buffer
    glDepthFunc(GL_LESS)  # The Type Of Depth Test To Do
    glEnable(GL_DEPTH_TEST)  # Enables Depth Testing
    glShadeModel(GL_SMOOTH)  # Enables Smooth Color Shading

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()  # Reset The Projection Matrix

    # Calculate The Aspect Ratio Of The Window
    gluPerspective(PANGLE, float(width) / float(height), 0.1, 100.0)

    glMatrixMode(GL_MODELVIEW)


def ReSizeGLScene(width, height):
    # Prevent A Divide By Zero If The Window Is Too Small
    if height == 0:
        height = 1

    # Reset The Current Viewport And Perspective Transformation
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(PANGLE, float(width) / float(height), 0.1, 100.0)
    glMatrixMode(GL_MODELVIEW)


def DrawGLScene():
    global rotx, roty

    glClearColor(0.125, 0.25, 0.5, 0.0)  # background color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity()  # Reset The View

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)
    glEnable(GL_TEXTURE_2D)

    glPushMatrix()
    glTranslatef(0.0, 0.0, -2.0)  # translate

    # Rotate
    glRotatef(roty, 0.0, 1.0, 0.0)
    glRotatef(rotx, 1.0, 0.0, 0.0)

    glColor4f(1.0, 1.0, 1.0, 1.0)  # color

    # draw quads
    x, y, z = 1.0, 1.0, 0.0
    glBegin(GL_QUADS)

    glTexCoord2f(0.0, 0.0)  # set u, v
    glVertex3f(-x, y, z)  # Top Left

    glTexCoord2f(1.0, 0.0)
    glVertex3f(x, y, z)  # Top Right

    glTexCoord2f(1.0, 1.0)
    glVertex3f(x, -y, z)  # Bottom Right

    glTexCoord2f(0.0, 1.0)
    glVertex3f(-x, -y, z)  # Bottom Left

    glEnd()

    glPopMatrix()
    glDisable(GL_TEXTURE_2D)
    glDisable(GL_BLEND)

    glutSwapBuffers()


def on_timer(value):
    global rotx, roty
    rotx = -10.0
    roty = roty + 1.0
    glutPostRedisplay()
    glutTimerFunc(int(1000 / FPS), on_timer, 0)


def keyPressed(key, x, y):
    # If escape is pressed, kill everything.
    ESCAPE = b"\x1b"
    if key == ESCAPE or key == b'q':
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()


def main():
    global window

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)

    glutInitWindowSize(SCRW, SCRH)
    # glutInitWindowPosition(0, 0)

    window = glutCreateWindow(b"Draw texture")

    glutDisplayFunc(DrawGLScene)
    glutReshapeFunc(ReSizeGLScene)
    # glutFullScreen()

    # glutIdleFunc(DrawGLScene)
    glutTimerFunc(int(1000 / FPS), on_timer, 0)

    glutKeyboardFunc(keyPressed)
    InitGL(SCRW, SCRH)

    load_texture()

    glutMainLoop()


if __name__ == "__main__":
    print("Hit ESC key to quit.")
    main()

使用画像は以下。

_img_rgb.png
_img_rgba.png
_img_grayscale.png
_img_palette.png

少し解説。 :

テクスチャ画像の読み込みと、OpenGL にテクスチャ画像を登録(?)するあたりは、load_texture() で処理している。

テクスチャ画像の読み込みには、PIL の Image を使った。更に、PIL (の Image) を使って、画像を必ず RGBA に変換してから画像を使う。

OpenGL に渡すためのテクスチャ画像はベタ画像データ(1ピクセル4バイトがピクセル数分ずらずらと並んでる感じのデータ)になってないといけないけれど、それについては PIL の .tobytes() で取得できる。

OpenGLにテクスチャ画像を登録するには、glTexImage2D() を利用する。

描画時にテクスチャを利用したい場合は、glEnable(GL_TEXTURE_2D) を呼んでから描画指定をする。逆に、テクスチャを利用しない場合は、glDisable(GL_TEXTURE_2D) を呼ぶ。

複数のテクスチャを描画。 :

前述のサンプルはテクスチャ画像を1つしか描画してないけれど、複数のテクスチャ画像を読み込んで描画できないか試してみた。

実行結果は以下。



複数のテクスチャを描画することができている。

ソースは以下。

_02_draw_quads_tex_m.py
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
import sys
from PIL import Image


IMGS = [
    [GL_TEXTURE0, "img_rgb.png"],
    [GL_TEXTURE1, "img_rgba.png"],
    [GL_TEXTURE2, "img_grayscale.png"],
    [GL_TEXTURE3, "img_palette.png"],
]

SCRW, SCRH = 512, 512
FPS = 60
PANGLE = 45.0

window = 0

# Rotation angle for the Quads
rotx = 0.0
roty = 0.0

textures = []


def load_texture(filename):
    # load image by using PIL
    im = Image.open(filename)
    w, h = im.size
    print("Image: %d x %d, %s" % (w, h, im.mode))

    if im.mode == "RGB":
        # RGB convert to RGBA
        im.putalpha(alpha=255)
    elif im.mode == "L" or im.mode == "P":
        # Grayscale and Index Color convert to RGBA
        im = im.convert("RGBA")

    raw_image = im.tobytes()

    ttype = GL_RGBA
    if im.mode == "RGB":
        ttype = GL_RGB
    elif im.mode == "RGBA":
        ttype = GL_RGBA

    id = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, id)

    # glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4)

    # set texture
    glTexImage2D(
        GL_TEXTURE_2D,      # target
        0,                  # MIPMAP level
        ttype,              # texture type (RGB, RGBA)
        w,                  # texture image width
        h,                  # texture image height
        0,                  # border width
        ttype,              # texture type (RGB, RGBA)
        GL_UNSIGNED_BYTE,   # data is unsigne char
        raw_image,          # texture data pointer
    )

    glClearColor(0, 0, 0, 0)
    glShadeModel(GL_SMOOTH)

    # set texture repeat
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)

    # set texture filter
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND)

    return id


def InitGL(width, height):
    glClearColor(0.15, 0.6, 0.3, 0.0)  # background color
    glClearDepth(1.0)  # Enables Clearing Of The Depth Buffer
    glDepthFunc(GL_LESS)  # The Type Of Depth Test To Do
    glEnable(GL_DEPTH_TEST)  # Enables Depth Testing
    glShadeModel(GL_SMOOTH)  # Enables Smooth Color Shading

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()  # Reset The Projection Matrix

    # Calculate The Aspect Ratio Of The Window
    gluPerspective(PANGLE, float(width) / float(height), 0.1, 100.0)

    glMatrixMode(GL_MODELVIEW)


def ReSizeGLScene(width, height):
    # Prevent A Divide By Zero If The Window Is Too Small
    if height == 0:
        height = 1

    # Reset The Current Viewport And Perspective Transformation
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(PANGLE, float(width) / float(height), 0.1, 100.0)
    glMatrixMode(GL_MODELVIEW)


def DrawGLScene():
    global rotx, roty, textures

    glClearColor(0.15, 0.6, 0.3, 0.0)  # background color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity()  # Reset The View

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)
    glEnable(GL_TEXTURE_2D)

    r = 1.05
    poslist = [
        [-r, r],
        [r, r],
        [-r, -r],
        [r, -r],
    ]

    ry = roty
    for i, tex_id in enumerate(textures):
        tx, ty = poslist[i]

        glPushMatrix()

        # change texture
        glBindTexture(GL_TEXTURE_2D, tex_id)

        # translate
        glTranslatef(tx, ty, -6.0)

        # Rotate
        glRotatef(ry, 0.0, 1.0, 0.0)
        glRotatef(rotx, 1.0, 0.0, 0.0)

        glColor4f(1.0, 1.0, 1.0, 1.0)  # color

        # draw quads
        x, y, z = 1.0, 1.0, 0.0
        glBegin(GL_QUADS)

        glTexCoord2f(0.0, 0.0)  # set u, v
        glVertex3f(-x, y, z)  # Top Left

        glTexCoord2f(1.0, 0.0)
        glVertex3f(x, y, z)  # Top Right

        glTexCoord2f(1.0, 1.0)
        glVertex3f(x, -y, z)  # Bottom Right

        glTexCoord2f(0.0, 1.0)
        glVertex3f(-x, -y, z)  # Bottom Left

        glEnd()

        glPopMatrix()

        ry *= 1.2

    glDisable(GL_TEXTURE_2D)
    glDisable(GL_BLEND)

    glutSwapBuffers()


def on_timer(value):
    global rotx, roty
    rotx = 0.0
    roty = roty + 1.0
    glutPostRedisplay()
    glutTimerFunc(int(1000 / FPS), on_timer, 0)


def keyPressed(key, x, y):
    # If escape is pressed, kill everything.
    ESCAPE = b"\x1b"
    if key == ESCAPE or key == b'q':
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()


def main():
    global window

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)

    glutInitWindowSize(SCRW, SCRH)
    # glutInitWindowPosition(0, 0)

    window = glutCreateWindow(b"Draw textures")

    glutDisplayFunc(DrawGLScene)
    glutReshapeFunc(ReSizeGLScene)
    # glutFullScreen()

    # glutIdleFunc(DrawGLScene)
    glutTimerFunc(int(1000 / FPS), on_timer, 0)

    glutKeyboardFunc(keyPressed)
    InitGL(SCRW, SCRH)

    for _, fn in IMGS:
        id = load_texture(fn)
        textures.append(id)
        glEnable(GL_TEXTURE_2D)

    glutMainLoop()


if __name__ == "__main__":
    print("Hit ESC key to quit.")
    main()

使用画像は以下。

_img_rgb.png
_img_rgba.png
_img_grayscale.png
_img_palette.png


少し解説。

glBindTexture() を使えば、何番目のテクスチャを利用してその後の処理をするかを指定できる。

ただ…。複数のテクスチャ画像を描画した後で気づいたけれど、考えてみたら、テクスチャ画像を1枚だけ渡して、そのテクスチャ内のここからここまで切り出して利用、といった形でも済むのだよな…。特にゲーム関係はそんな感じでテクスチャを利用しているはずだし…。いやまあ、テクスチャをリピートさせて描画する時は、またちょっと違ってくるのだろうか。

2022/09/12(月) [n年前の日記]

#1 [zatta] Googleサイト上の各ページを少し編集

PyOpenGL関係の情報をググって調べていたら、自分が昔、Googleサイトというサービス上に書き残したメモと遭遇した。

_mieki256 wiki

しかし、載せていた Pythonスクリプトや Script-Fu (TinyScheme) のソースがグチャグチャな状態で…。

Googleサイトというサービスは、ある時期にシステムが新しくなって、それまで扱ってたコンテンツデータが新システム用に自動変換されたのだけど。どうやらその際、「ここからここまで整形済みテキストだよ」という情報をバッサリ削除したまま変換していたようだなと…。

幸い、各ページではソースファイル群を zip の形でも置いてあったので、ダウンロードして、ソースを開いて、全部コピペして、貼り付け直した。選択範囲に対して「code」という属性を選んでやれば、整形済みテキストとして表示される模様。

ただ、プログラミング言語別に色分け表示はしてくれないし、画像も等倍表示になってくれない…。

現状の Googleサイトというサービスは、ちょっと使い勝手がよろしくないなと…。もしかすると、別サービスに各ページを移植(?)したほうがいいのかもしれない。そもそも、プログラミング言語のソースの類は、GitHub か GitHub Gist あたりに置いておいたほうがいいのではないかと思えてきた…。でも、例えば Gist で画像の類もアップロードするとなると、少し面倒なのだよな…。

ちなみに、元々これらのページは @Wiki というサービス上で公開していたのだけど、@Wikiがコンテンツの上に被さる形の広告を表示するようになってしまって、本文その他が読めなくなって Googleサイトに引っ越しをした経緯があって…。

今後もそういうアレコレで引っ越しする可能性を考えると、Wikiの類を使うことに拘らずに、静的サイトジェネレーターなどを導入して、単なる html としてどこかにアップロードしたほうがいいのかも。

2022/09/13(火) [n年前の日記]

#1 [gimp][windows] Windows版 GIMP 2.6 でPython-Fuを動かしたい

Windows10 x64 21H2上で、Python-Fu (GIMP-Python) を動かせる GIMP 2.6 環境を用意したい。

自作の Python-Fuスクリプトが GIMP 2.6上でも動くのか確認したかったので、動作確認環境として持っておかないとアレだなと。もっとも、今現在は GIMP 2.10 が現行版なので、今から GIMP 2.6 をあえて使う場面はほとんど無いだろうけど。

環境は、Windows10 x64 21H2 + Python 2.6.6 32bit + PyGTK all-in-one 2.24.2。ちなみに、普段は Python 3.9.13 64bit版をPATHに通しているので、Python と打ち込めば Python 3.9.13 64bit が実行される状態。

標準インストール版の GIMP 2.6.12、もしくは GIMP 2.6.12 Portable版でどうにかしたかったのだけど、それらの版ではPython-Fu を動作させることができなかったので、GIMP-painter- 2.6.11 を利用してみた。

Python 2.6のインストール。 :

GIMP 2.6 + Python-Fu は、Python 2.5 もしくは Python 2.6 を要求するので、インストールしておく。Python 2.6.6 は C:\Python\Python26\ にインストールした。

また、Python-Fu は PyGTK関係のモジュールも必要とするので、pygtk-all-in-one-2.24.2.win32-py2.6.msi もインストールした。

セットアップファイルの入手先は以下。

_Python Release Python 2.6.6 | Python.org
_Index of /binaries/win32/pygtk/2.24/

GIMP-painter- 2.6.11 でPython-Fuを動かす。 :

GIMP-painter- は、GIMP 2.6 にペンタブレット描画時の調整機能を追加した版、という説明でいいのだろうか。あるいは混色ブラシが使えるようになった版とでもいうか…。まあ、現行版の GIMP 2.10 ではそれらの機能が取り込まれていた気がするけど…。

GIMP-painter- 2.6.11 の入手は以下。

_ダウンロードファイル一覧 - [旧]GIMP-painter - OSDN

以下の2つを入手。
  • gimp-painter--2.6.11_100627-4_win32_starter.exe
  • gimp-painter--2.6.11_100627-4_win32_update.exe

実行して任意のフォルダに展開する。今回は、D:\Prog\GIMP-2.6.11-painter-\ に展開してみた。

中に入ってる run_gimp.wsf を実行すれば GIMP-painter- 2.6.11 が起動する。ただ、このままでは Python-Fu が利用できなかった。

自分の手元の環境では、C:\Python\Python26\ に Python 2.6.6 がインストールしてあるので…。この Python をGIMPから呼び出せるように、環境変数 PATH を若干変更するbatファイルを作成した。環境変数 GIMPPATH と PPATH だけ、各自の環境に合わせれば利用できるはず。

_gimppainetr.bat
@echo off
set GIMPPATH=D:\Prog\GIMP-2.6.11-painter-\gimp-painter-
set PPATH=C:\Python\Python26

set PATH=%PPATH%;%PPATH%\Scripts;%PATH%
set PYTHONPATH=%PPATH%\Lib\site-packages
set PYTHON_ROOT=%PPATH%

echo.
echo Add path [%PPATH%]
echo Add path [%PPATH%\Scripts]
echo PYTHONPATH=%PYTHONPATH%
echo PYTHON_ROOT=%PYTHON_ROOT%
echo.

cd /d %GIMPPATH%
run_gimp.wsf


加えて、 (GIMP-painter-インストールフォルダ)\home\.gimp-2.6\interpreters\ 以下に、pygimp.interp というテキストファイルを作成する。おそらく、GIMPから利用するインタプリタ言語の設定を記述するファイルだと思うけど…。内容は以下。pythonw.exe が置いてある場所を指定している。

_pygimp.interp
python=C:\Python\Python26\pythonw.exe
/usr/bin/python=C:\Python\Python26\pythonw.exe
:Python:E::py::python:

注意点。pygimp.interp は、改行コードを*NIXのソレに合わせてLFにしておくこと。Windowsの改行コード、CRLF になっていると、GIMPはこのファイルの解析に失敗する。

動作確認。gimppainetr.bat を実行して、フィルタ → Python-Fu → コンソール、を選んでコンソールが表示されれば Python-Fu が動いてる状態になっている。

通常インストール版 GIMP 2.6.12 でPython-Fuを利用。 :

もう少し色々試していたら、通常インストール版 GIMP 2.6.12 32bit でも、Python-Fu が利用できる状態になった。

GIMP 2.6 で Python-Fu を利用するための条件としては、おそらく以下を満たしておく必要があるのだろう…。これを踏まえつつ、GIMP 2.6 のインストール作業をしていけばよいのではないか。たぶん。

  • GIMP 2.6インストール時に、Python 2.6.x が利用できる状態であること (Python 2.6にPATHが通っていること)。
  • GIMP 2.6インストール時に、Python-Fu関係(GIMP-Python)のファイルもインストールすること。(「GIMP Python extension」にチェックを入れてインストール作業を進める。)
  • GIMP 2.6起動時に、Python 2.6 が利用できる状態であること。
  • GIMP 2.6インストールフォルダ\lib\gimp\2.0\interpreters\pygimp.interp で、Python 2.6への正しいパスが指定されていること。
  • GIMP 2.6インストールフォルダ\lib\gimp\2.0\plug-ins\ 内の *.py で /usr/bin/env が記述されていないこと。

ひとまず、GIMP 2.6 や GIMP 2.8 は、インストーラを実行すると、今までSSD/HDD内に存在していたユーザーデータ用フォルダを平気で削除してくれちゃうので…。~/.gimp-2.6 や ~/.gimp-2.8 等を、一旦別の場所に移動してから作業を始めた。

Pythn 2.6.6 にPATHが通っている状態にしてから、GIMP 2.6.12 のインストーラ、gimp-2.6.12-i686-setup-2.exe を実行した。

GIMPのインストーラが Python 2.6 を見つけたので、インストールの途中で Python-Fu関係のファイルもインストール項目(「GIMP Python extension」) として表示された。もし、Python 2.6 が利用できない状態でインストーラを実行すると、該当項目はグレーアウトして選べない状態になるはず。さておき、チェックを入れてインストールを続行。

GIMP 2.6.12 インストール後、GIMPを実行。ユーザーデータ用のフォルダ、~/.gimp-2.6/ が作られた。GIMP を終了。

GIMP 2.6インストールフォルダ\lib\gimp\2.0\interpreters\pygimp.interp の内容を確認。利用したい Python (pythonw.exe) の場所が正しく指定されているか、また、改行コードが CRLF ではなくて LF になっているか確認する。

python=C:\Python\Python26\pythonw.exe
/usr/bin/python=C:\Python\Python26\pythonw.exe
:Python:E::py::python:

GIMP 2.6インストールフォルダ\lib\gimp\2.0\plug-ins\*.py を確認。もし、1行目に 「#!/usr/bin/env python」と書いてあったら、「#!/usr/bin/python」に書き換える。Windowsに /usr/bin/env というプログラムは無いので…。

GIMP起動用のbatファイルを作成。GIMPインストールフォルダ\bin\ あたりに入れておく。

_gimp-2.6.bat
@echo off
set GIMPPATH=D:\Prog\GIMP-2.6.12\GIMP-2.0
set PYPATH=C:\Python\Python26

set GTKPATH=%PYPATH%\Lib\site-packages\gtk-2.0\runtime\bin
set GIMPFUPATH=%GIMPPATH%\lib\gimp\2.0\python

set PATH=%PYPATH%;%PYPATH%\Scripts;%GTKPATH%;%GIMPPATH%\bin;%PATH%
@rem set PATH=%PYPATH%;%PYPATH%\Scripts;%GIMPPATH%\bin;%PATH%

@rem set PYTHONPATH=%PYPATH%\Lib\site-packages
set PYTHONPATH=%GIMPFUPATH%
set PYTHON_ROOT=%PYPATH%

echo.
echo add path %PYPATH%
echo add path %PYPATH%\Scripts
echo add path %GTKPATH%
echo add path %GIMPPATH%\bin
echo.
echo PYTHONPATH=%PYTHONPATH%
echo PYTHON_ROOT=%PYTHON_ROOT%

@rem cd /d %GIMPPATH%
@rem cd /d %GIMPPATH%\bin
cd /d %USERPROFILE%
%GIMPPATH%\bin\gimp-2.6.exe %1 %2 %3 %4 %5 %6 %7 %8 %9

このbatファイルを実行すると、Python 2.6.6 をPATHに追加してから、GIMP 2.6.12 を起動する。

余談。カレントディレクトリを %USERPROFILE% にしてから GIMP 2.6.12 を起動しないと、何故か Python-Fu が動いてくれなかった…。なんでだろ…。

さておき。GIMPが起動したら、フィルタ → Python-Fu → コンソール、を選択。コンソールウインドウが表示されたら、Python-Fu は利用できる。

問題点。 :

これで一応、Python-Fu が動作する GIMP 2.6 環境は得られたわけだけど。不具合に遭遇した。

通常インストール版 GIMP 2.6.12、GIMP-papinter- 2.6.11、どちらでも不具合が発生してるのだけど…。Pythonコンソールウインドウや、Python-Fu で書いたフィルタのダイアログの入力欄で、何故かキー入力ができなかった。Enterキー、Deleteキー、Backspaceキーは反応するし、Ctrl + V で貼付けはできるのだけど…。

メモ帳でも開いておいて、そこに入力値を打ち込んでからコピーして、Python-Fu関係のダイアログに貼り付ける、といった感じで使っていくしかなさそう。

でもまあ、今時 GIMP 2.6 を使うことはまず無いと思うので…。GIMP 2.10 か 2.8 を使えばいいわけで…。

ちょっと記憶が怪しいけれど、たしか GIMP 2.8 からは Python-Fu が使える状態で配布されていた気がする。だから、Python-Fu を使いたいなら、最初から GIMP 2.8、2.10 を利用したほうがいい。GIMP 2.6 で Python-Fu を使おうとしたから、こんな面倒臭い作業をすることになってるわけで…。

2022/09/14(水) [n年前の日記]

#1 [prog] MinGWでGLFWを使えるように環境を整えた

OpenGLを勉強するにあたって、GLFW というライブラリが使えると便利らしいという話を見かけたので、MinGW (gcc 9.2.0) で GLFW を使ったC/C++ソースがコンパイルできるように GLFW をインストールしてみた。

環境は、Windows10 x64 21H2。

GLFWバイナリの入手とインストール。 :

以下からバイナリを入手。

_Download | GLFW

Windows pre-compiled binaries のところから、glfw-3.3.8.bin.WIN32.zip をダウンロードする。MinGW は32bit版なので、GLFW も32bit版を入手すればいいのだろう。たぶん。

glfw-3.3.8.bin.WIN32.zip を解凍すると、include\ や lib-mingw\ が入っていた。
  • include\ を、MinGWインストールフォルダ\include\ にコピー。
  • lib-mingw\*.a を、MinGWインストールフォルダ\lib\ にコピー。
  • lib-mingw\*.dll を、MinGWインストールフォルダ\bin\ にコピー。

GLEWもインストール。 :

GLEWというライブラリも必要になりそうなのでインストールしておく。

_GLEW: The OpenGL Extension Wrangler Library

glew-2.1.0-win32.zip を入手。

解凍すると、bin/、include/、lib/ と、お決まりのフォルダが見えるので、中身をそれぞれ、MinGWインストールフォルダ内にコピー、しようとしたけど、lib/ がちょっとアレだな…。たぶんコレ、Visual C++用なのではないかな…。

_MinGWでglewを入れる. - 考えるということ

上記ページによると、MinGW の場合もそのままコピーして構わないという話があるみたいなので、自分もそのようにしてみる。まあ、問題が出たらその時対処しよう…。たしか、*.lib を *.a に変換するツールがあったような気がするので、その時はそういうツールが使えないか試すことになるんじゃないかな…。

サンプルソースをコンパイル。 :

以下で紹介されてるサンプルソースをコンパイルできるか試してみる。

_Documentation | GLFW

_01_hello_glfw.c
#include <GLFW/glfw3.h>

int main(void)
{
  GLFWwindow* window;

  /* Initialize the library */
  if (!glfwInit())
    return -1;

  /* Create a windowed mode window and its OpenGL context */
  window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
  if (!window)
    {
      glfwTerminate();
      return -1;
    }

  /* Make the window's context current */
  glfwMakeContextCurrent(window);

  /* Loop until the user closes the window */
  while (!glfwWindowShouldClose(window))
    {
      /* Render here */
      glClear(GL_COLOR_BUFFER_BIT);

      /* Swap front and back buffers */
      glfwSwapBuffers(window);

      /* Poll for and process events */
      glfwPollEvents();
    }

  glfwTerminate();
  return 0;
}

以下でコンパイル。

_Makefile
gcc 01_hello_glfw.c -o 01_hello_glfw.exe -static -lglfw3 -lopengl32 -lwinmm -lgdi32 -mwindows

01_hello_glfw.exe が生成された。実行すると、真っ暗なウインドウが表示された。コンパイルできたのではないかな…。たぶん。

MSYS2 + MinGW-w64 の場合。 :

MSYS2 + MinGW-w64 (gcc 12.2.0) の場合、GLFWも、GLEWも、パッケージが用意されている。

_Base Package: mingw-w64-glew - MSYS2 Packages
_Base Package: mingw-w64-glfw - MSYS2 Packages

64bit版、もしくは 32bit版のインストールは以下。
pacman -S mingw-w64-x86_64-glfw mingw-w64-x86_64-glew 
pacman -S mingw-w64-i686-glfw mingw-w64-i686-glew

前述のサンプルソースもコンパイルできた。

参考ページ。 :


2022/09/15(木) [n年前の日記]

#1 [python] PyOpenGLで球を描画

Windows10 x64 21H2 + Python 3.9.13 64bit + PyOpenGL 3.1.6 で球を描画できるか実験。

結果画面は以下。

ソース。 :

ソースは以下。

_01_draw_sphere.py
import sys
import math
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

# SCRW, SCRH = 1280, 720
SCRW, SCRH = 512, 288
FPS = 60

ang = 0.0
window = 0


def draw_func():
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    # set light
    glDisable(GL_LIGHTING)
    glEnable(GL_LIGHTING)
    glEnable(GL_NORMALIZE)
    glLightfv(GL_LIGHT0, GL_POSITION, [0.25, 1.0, 0.5, 0])
    glLightfv(GL_LIGHT0, GL_DIFFUSE, [1, 1, 1, 1])
    glLightfv(GL_LIGHT0, GL_SPECULAR, [1, 1, 1, 1])
    glEnable(GL_LIGHT0)

    glLoadIdentity()
    
    # rotate camera position
    r = 30
    x = r * math.cos(math.radians(ang))
    y = r * 0.5
    z = r * math.sin(math.radians(ang))
    tx, ty, tz = 0, 0, 0
    gluLookAt(x, y, z, tx, ty, tz, 0, 1, 0)

    glScalef(1.0, 1.0, 1.0)

    # set color
    green = [0.0, 1.0, 0.0, 1.0]
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, green)

    # draw sphere
    quadric = gluNewQuadric()
    # radius = 0.25 * math.sin(math.radians(ang * 8)) + 1.0
    radius = 4 * math.sin(math.radians(ang * 8)) + 5.0
    slices = 32
    stacks = 16
    gluSphere(quadric, radius, slices, stacks)
        
    glDisable(GL_LIGHT0)
    glDisable(GL_LIGHTING)
    
    # set color
    c = 0.5 * math.cos(math.radians(ang * 2)) + 0.5
    glColor3f(0.0, c, 1.0 - c)
    # glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, [0.0, 1.0, 1.0, 1.0])

    # draw cube
    glutWireCube(20)

    glutSwapBuffers()


def init_GL():
    global window

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH)
    # glutInitWindowPosition(0, 0)
    glutInitWindowSize(SCRW, SCRH)
    window = glutCreateWindow("Draw sphere")

    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClearDepth(1.0)

    glDepthFunc(GL_LESS)
    glEnable(GL_DEPTH_TEST)

    glShadeModel(GL_SMOOTH)
    # glShadeModel(GL_FLAT)

    glDisable(GL_LIGHTING)
    glEnable(GL_LIGHTING)
    glEnable(GL_NORMALIZE)

    # glEnable(GL_CULL_FACE)
    # glCullFace(GL_BACK)

    glViewport(0, 0, SCRW, SCRH)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, float(SCRW) / float(SCRH), 0.1, 100.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()


def on_timer(value):
    global ang
    ang += 0.5
    glutPostRedisplay()
    glutTimerFunc(int(1000 / FPS), on_timer, 0)


def key_func(key, x, y):
    global window

    ESCAPE = b"\x1b"
    print("Push key,", key, x, y)
    if key == ESCAPE or key == b"q":
        print("Exit")
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()


def main():
    # main
    init_GL()
    glutKeyboardFunc(key_func)
    glutDisplayFunc(draw_func)
    glutTimerFunc(int(1000 / FPS), on_timer, 0)
    glutMainLoop()


if __name__ == "__main__":
    main()

python 01_draw_sphere.py で実行。

球の描画は何を使うのかググってみたところ、gluSphere() が使えるっぽい。

2022/09/16(金) [n年前の日記]

#1 [python] PyOpenGLを勉強中

PyOpenGLを使ってボールが箱の中で跳ね回るスクリプトを書いてるところ。ワイヤーフレームの箱だけでは寂しいので、グリッドを引いてみたり、エフェクトをつけてみたけれど、どうも壁に当たってる感じが弱い…。

2022/09/17() [n年前の日記]

#1 [prog] OpenGL勉強中

PyOpenGLを使って、ボールが箱の中で跳ね回るスクリプトを書いているところ。それっぽい感じの見た目になってきたので、せっかくだから OpenGLを使っているWindows用スクリーンセーバとして移植を試みているのだけど。ちょっと問題が…。

PyOpenGL版と比べると、スクリーンセーバ版は、見た感じ明らかにFPSが低い。なんでだろ。

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。

MinGW (gcc 9.2.0) を使うと遅くなるのだろうか。であればと、スクリーンセーバではない形で書いて動作確認。GLUT (freeglut 3.0.0) でフツーにウインドウを表示してから、同じ処理で描画。しかしコレだと PyOpenGL版と似た感じのフレームレートに見える…。ということは、スクリーンセーバ用に書くとフレームレートがおかしくなるということで…。

もしかすると、GLUT (freeglut) 使用版と、スクリーンセーバ版では、初期化時に行っている処理内容が違っていて、OpenGL のハードウェアアクセラレーションが効いていないのだろうか。スクリーンセーバ用は PIXELFORMATDESCRIPTOR を使って初期化するけれど、そこで何か指定を間違えているとか…?

それとも、OpenGL利用部分は正常に動作していて、スクリーンセーバとして動かすためのどこかしらでフレームレートが落ちているのだろうか…。

2022/09/18() [n年前の日記]

#1 [python] PyOpenGLでボールが跳ね回るスクリプトを作成

PyOpenGL を使って、ボールが箱の中で跳ね回るスクリプトを書いてみた。

動作確認環境は以下。
動かすとこんな感じ。




以下のキーが有効。 FPS表示時の赤い線は、期待した60FPSに対して実際何FPS出ているかを、箱の一辺の長さとの比較で表示している。60FPSが出ていれば、赤い線は箱の一辺と同じ長さになるし、それより少ないFPSなら、箱の一辺より短く表示される。

ソース。 :

ソースは GitHub Gist にアップロードしてみた。このページにも埋め込めるだろうか…。どうかな…。

_01_bound_ball.py (Gist)


以降、ソースの中身について、少しだけ解説。

FPSの計測について。 :

FPSの計測は、1秒間に何回画面を書き換えたかをカウントする方法で調べた。init_count_fps()、calc_fps() を見れば、どうやっているか分かるかなと…。

時間の取得には、time.time() を使った。import time をすれば使えるようになる。time.time() を呼ぶと、その時点の時刻が、秒単位(小数点以下も含む状態)で得られるらしい。

Python 2.7.18 32bitの場合、time.time() の返り値は小数点以下3桁まで含んでいる。ミリ秒まで分かるのだろう…。
>>> import time
>>> time.time()
1663505711.946
>>> time.time()
1663505714.521

Python 3.9.13 64bitの場合、time.time() の返り値は、小数点以下7桁まで得られる模様。もしかすると、マイクロ秒まで得られる…?
>>> import time
>>> time.time()
1663505644.9368925
>>> time.time()
1663505648.3456943

まあ、Python 2.7 の場合を考えて、time.time() は少なくともミリ秒まで分かると思って使えばいいのかなと。

ちなみに、最初は時間の取得に time.perf_counter() を使っていたのだけど。time.perf_counter() は Python 3.3 で実装されたメソッドだから Python 2.7 では動かなくて、time.time() で置き換えることになった。

文字描画について。 :

OpenGL で画面に文字を描画するあたりは、GLUT (freeglut) の glutBitmapCharacter() を利用した。GLUT (freeglut) を利用できる状況じゃないと使えないけれど…。

_情報メディア実験
_glutBitmapCharacter : PyOpenGL 3.1.0 GLUT Man Pages
_~mcfletch/openglcontext/trunk : contents of tests/glutbitmapcharacter.py at revision 699
_python - pyopengl How to render text - Stack Overflow

文字を描画する際の位置決めは、最初、glRasterPos3f(x, y, z) を使ってみたのだけど、glWindowPos2f(x, y) というメソッドもあると知って、そちらを使うようにしてみた。

_glRasterPos : PyOpenGL 3.1.0 GL Man Pages
_glWindowPos : PyOpenGL 3.1.0 GL Man Pages

この glWindowPos2f() を使えば、3D空間内の位置ではなく、ウインドウ上の2D的な感覚で座標指定ができる。

ただし、注意点が2つほどある。
  • 画面左下が (0, 0) になっている。
  • OpenGL 1.4 から追加されたメソッドなので、OpenGL 1.1 等では使えない。
例えば、Windows に標準で入ってる OpenGL は 1.1 対応だそうで、そのせいか、C/C++ で書いたプログラム等で glWindowPos2f() を使おうとすると「そんなAPIは無い」と言われてしまう。

もっとも、今時のPCなら、ビデオドライバはもっと後の OpenGLバージョンにも対応してるだろうから…。もしかすると、PyOpenGLを使う時はそのあたりを上手い具合に変更してくれて、それで今回 glWindowPos2f() が使えているのかもしれない。たぶん。分からんけど。

アニメーション処理について。 :

アニメーションをするためには一定の時間間隔で処理を呼ばないといけないけれど、そのあたりは glutTimerFunc() で指定している。

_glutTimerFunc : PyOpenGL 3.1.0 GLUT Man Pages
_GLUT のイベント

巷の解説ページでは glutIdleFunc() を使う場合が多いのだけど…。glutIdleFunc() に渡す関数は引数を取らないけれど、glutTimerFunc() に渡す関数は引数を1つ取るので、その点はちょっとだけ注意。

#2 [prog] C言語とOpenGLでボールが跳ね回るプログラムを作成

_PyOpenGLを使ってボールが箱の中で跳ね回るスクリプトを書いた わけだけど、これをC言語 + OpenGL で動くように移植してみた。

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0) + freeglut 3.0.0。MSYS2 + MinGW-w64 ではなくて、MinGW + MSYS でコンパイルしている。

MinGW用の freeglut 3.0.0 バイナリ (freeglut-MinGW-3.0.0-1.mp.zip) は、以下から入手させてもらった。ありがたや。

_freeglut Windows Development Libraries

動作画面は PyOpenGL版と大体同じ。

以下のキーを受け付ける。

ソース。 :

コンパイル。 :

Makefile があるので、make と打てばコンパイルできるだろうけど…。一応、コンパイルは以下。01_gl_bound_ball.exe が生成される。
gcc 01_gl_bound_ball.c -o 01_gl_bound_ball.exe -static -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lglu32 -lwinmm -lgdi32 -mwindows

01_gl_bound_ball.exe を実行すれば、PyOpenGL版と同じ見た目のウインドウが開くかと。

覚書。 :

FPS表示時に、一応「FPS: xx」の文字描画もしてみたけれど、glWindowPos2f() が使えなかったので、 *1 glRasterPos3f() を使うことになった。そのせいで、箱の上のほうに「FPS: xx」がくっついて動いてる表示になってしまった。ちょっとダサイ。ウインドウの特定の位置に固定して表示したかったのだけど、このあたり本来はどうやって解決すればいいのやら。

一般的に、C言語で円周率を使いたい時は、math.h をインクルードして(#include <math.h>)、M_PI を利用するらしいのだけど。試してみたら Visual Studio Code 上で「このシンボルは見当たらないよ」と言われてしまった。math.h を include する前に、「#define _USE_MATH_DEFINES」を書いておけば、その手のマクロが使えるようになるらしい。ただ、移植性は落ちるそうで…。

_定数M_PIが使えない - 需要のないページ
_プログラミングメモ日記 C++での数学定数の利用について
_M_PI ‐ 通信用語の基礎知識
_C言語(標準)にM_PIは無い - 簡潔なQ
_math.h の M_PI などは仕様外だった - GUST NOTCH? DIARY

*1: glWindowPos2f() は OpenGL 1.4 から使える。OpenGL 1.1 では使えない。

2022/09/19(月) [n年前の日記]

#1 [prog][windows] OpenGLを使ったWindows用スクリーンセーバをC++で書いてみた

_昨日、 C言語 + OpenGL を使ってボールが箱の中を跳ね回るプログラムを書いたけれど、せっかくだからWindows用スクリーンセーバにしてみた。

使用言語が、C言語じゃなくて C++ になってるけれど、たぶんソースはほとんどC言語の書き方になってるような気がする…。

環境は、Windows10 x64 21H2 + MinGW (g++ 9.2.0) + scrnsave.h + libscrnsave.a。

動作確認に使ったハードウェア環境は以下の2つ。どちらでも動いてくれた。
動作画面は以下。YouTubeにアップロードしてみた。

ソースその他。 :

ソースその他は、GitHub にアップロードしておいた。

_mieki256/glboundballscr: Bound ball animation screensaver win32 by using OpenGL.

Makefile もつけてあるので、MinGW が利用できる環境で make と打てばコンパイルできるのではないかと…。

実際に処理をしているソースは以下。

_glboundballscr/glboundball.cpp at main - mieki256/glboundballscr

インストール。 :

MinGW は32bit版なので、出来上がるスクリーンセーバ(.scr)も32bit版になる。
  • Windowsが64bit版、スクリーンセーバが32bit版なら、C:\Windows\SysWOW64\ 以下にコピー。
  • Windowsが64bit版、スクリーンセーバが64bit版なら、C:\Windows\System32\ 以下にコピー。
  • Windowsが32bit版なら、C:\Windows\System32\ 以下にコピー。

問題点。 :

一応スクリーンセーバにしてみたものの、何故かフレームレート(FPS、フレーム/秒)が低くなる問題に遭遇して結構ハマった。

_先日書いたGLUT(freeglut)使用版 は、ちゃんと60FPS出てるのだけど。スクリーンセーバ版を動かしたところ、どうも見た感じ、40FPS前後しか出てなくて…。

試しに目標FPS値を少しずつ上げていったところ、63FPSにして動作させた時、描画は60FPSよりちょっと低い程度になったので、その値のままにしてある。何故そうなるのかは分からないけど…。もし、動作がおかしい場合は、glboundball.cpp 内の #define FPS 63 が目標FPS値の指定なので、変更してコンパイル/動作確認してもらえればと…。

ちなみに、#define FPS_BAR 0 を 1 にしてビルドすれば、箱の一片の近くに赤い線が引かれて、FPSが大まかに分かるようになってる。
  • 箱の一辺の長さが目標FPSで、赤い線の長さが実際のFPS。
  • 例えば、赤い線の長さが、箱の一辺の0.95程度の長さなら、63 * 0.95 ≒ 60FPS が実際のFPS、ということになる。

原因を想像。 :

これは想像なのだけど、スクリーンセーバに一定の時間間隔で処理をさせるための、SetTimer() という関数の精度がよろしくないのかもしれないなと。

SetTimer() は、「xxミリ秒(msec)の間隔で WM_TIMERメッセージを送ってくれ」とOSに指示を出す関数。OSは、SetTimer() で指定されたミリ秒間隔でWM_TIMERメッセージを送ってくるので…。スクリーンセーバのプログラムは、WM_TIMERメッセージが届いたら、そのタイミングで描画処理をする作りにしておく。これで、一定の時間間隔で画面描画が行われるので、アニメーションが実現できる。

ただ、Windows NT系は、SetTimer() の精度が10ミリ秒程度だよ、という話もどこかで見かけたわけで…。 *1

60FPSで動かそうとすると、SetTimer() には 1000 / 60 ≒ 16ミリ秒(16msec)を指定することになるけれど。SetTimer() の精度が本当に10ミリ秒しかないのであれば、キッチリ16ミリ秒間隔でメッセージが送られてこなくて、20ミリ秒の間隔でメッセージが送られてきそうな気がする。それでは目標とするFPSが出ないよなと。

しかし、50FPS (1000 / 50 = 20ミリ秒)や、25FPS (1000 / 25 = 40ミリ秒) を指定しても、やっぱりフレームレートが低い…。目標の2/3ぐらいのFPSになってしまう。本当に10ミリ秒の精度なんだろうかという疑問が…。

63FPS なら、1000 / 63 ≒ 15ミリ秒になるなと…。ひょっとして、最近のWindowsは、15ミリ秒とか、あるいは5ミリ秒の精度だったりするんだろうか。

ただ、そもそも、「スクリーンセーバはバックグラウンドで動いてるプログラム群の邪魔をしない作りにするべし」という話もどこかで見かけたので…。60FPSでぬるぬる動かしたいと考えること自体がアウトなのかも。例えば10FPSぐらいでカクカクと動くのが正しいスクリーンセーバの姿、かもしれないよな、とも…。

何にせよ、これで OpenGL を使ったスクリーンセーバの作り方が少し分かってきたので、3D描画をするスクリーンセーバもC/C++で書けそうな気がしてきたなと…。

タイマーの精度について。 :

その後もググってたら、Windows7 はタイマー精度が15.6ミリ秒、という話を見かけた。

_ windows - MFCでの標準タイマーの精度をあげる方法ありますか? - スタック・オーバーフロー
Windows OSのシステムクロックは15.6ミリ秒の分解能しかもたないため(Windows 7)、記載のような事象が発生します。

_WindowsにおけるSleep(1)の精度について
_Rustのゲーム用ライブラリpistonがWindowsの上だと60fps出なかったのを直した - 名有りさんの日記
_タイマの精度 - お仕事メモ帳
この動作の原因は、Windows XP のハードウェア アブストラクションレイヤ モジュール:HAL.DLL の仕様の違いによるものであり、そのPCが使用しているHALの種類によってタイマ精度が決まります。

タイマ精度 約 10mS となるマシンは halacpi.dll が使用されており、タイマ精度 16mS となるマシンには halaacpi.dll が使用されております。

最近の機種では、割り込みコントローラに APIC (Advanced PIC) を 使用しておりまして、この場合、HAL は halaacpi.dll が使用されます。

タイマの精度 - お仕事メモ帳 より


Windows95系、NT系で違うだけ、ではないのだな…。使ってる割り込みコントローラの種類で、10ms と 15.6ms の違いが出てくると…。いやまあ、最近の機種は 15.6ms になってそうだけど。

SetTimer() に 15msec を指定するのと、16msec を指定するのでは、動作に差が出てきそう。16msec を指定すると、実際には次回のメッセージが送られてくるのが 15.6 x 2 = 31.2 msec 後になるのかも。とすると、FPSは 31〜33FPSぐらいになるのだろうか。60FPSを期待したのに40FPS前後しか出ないなーと困っていたけれど、実際は31〜33FPSだった可能性が高そう。

*1: ちなみに、Windows95系はもっと精度が荒くて、55ミリ秒程度らしい。

#2 [zatta] 日記をアップロード

2022/08/19を最後に日記をアップロードしてなかったのでアップロード。

2022/09/20(火) [n年前の日記]

#1 [python] PyOpenGLでビルボードが実現できそうか実験中

Python + PyOpenGL (OpenGL) を使って、ビルボードが実現できそうか実験中。

以下の解説記事が参考になった。ありがたや。

_床井研究室 - ビルボード

一番簡単そうな、ポリゴンをスクリーンと平行にする方法で ―― 変換行列の回転部分を単位行列で上書きしてしまう方法を試したところ、一見すると上手く行ったように思えたのだけど。テクスチャを貼った複数のポリゴンを表示してみたら、重なったポリゴンの、テクスチャの透明部分が妙な見た目になってしまって、解決策をググっているところ。

2022/09/21(水) [n年前の日記]

#1 [python] PyOpenGLでビルボードが実現できそうか実験中その2

Python + PyOpenGL (OpenGL) を使って、ビルボードが実現できそうか実験しているところ。環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + PyOpenGL 3.1.6。

この場合のビルボードというのは、立て看板みたいなもので、絶えずカメラのほうを向いているポリゴンというか…。それっぽいテクスチャをビルボードに貼っておくことで、一見するとそこに細かい何かが置いてあるように錯覚させることができるという…。まあ、ハードウェアスペックが低かった時代に多用されていた、頓智というか、インチキというか、そういうアレですけど…。以下の解説記事内のスクリーンショットが分かりやすい気がします。

_【Unity】ビルボードで常にカメラの方に向く木を作る - おもちゃラボ

昔のTVゲーム、「スペースハリアー」「アウトラン」「アフターバーナー」等もビルボードを活用した事例になるのだろうか。当時はポリゴンじゃなくてスプライトで実現してたと思うけど、発想は同じかなと。

最初に試したソース。 :

最初に書いたのは以下のソース。

_01_draw_billboard_alpha_bug.py
import sys
import math
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

IMG_NAME = "img_rgba.png"

SCRW, SCRH = 512, 512
FPS = 60

scr_w, scr_h = SCRW, SCRH
window = 0

# Rotation angle for the Quads
rotx = 0.0
roty = 0.0

texture = 0


def load_texture():
    global texture

    # load image by using PIL
    im = Image.open(IMG_NAME)
    w, h = im.size
    print("Image: %d x %d, %s" % (w, h, im.mode))

    if im.mode == "RGB":
        # RGB convert to RGBA
        im.putalpha(alpha=255)
    elif im.mode == "L" or im.mode == "P":
        # Grayscale, Index Color convert to RGBA
        im = im.convert("RGBA")

    raw_image = im.tobytes()

    ttype = GL_RGBA
    if im.mode == "RGB":
        ttype = GL_RGB
        print("Set GL_RGB")
    elif im.mode == "RGBA":
        ttype = GL_RGBA
        print("Set GL_RGBA")

    glBindTexture(GL_TEXTURE_2D, glGenTextures(1))

    # glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4)

    # set texture
    glTexImage2D(
        GL_TEXTURE_2D,      # target
        0,                  # MIPMAP level
        ttype,              # texture type (RGB, RGBA)
        w,                  # texture image width
        h,                  # texture image height
        0,                  # border width
        ttype,              # texture type (RGB, RGBA)
        GL_UNSIGNED_BYTE,   # data is unsigne char
        raw_image,          # texture data pointer
    )

    glClearColor(0, 0, 0, 0)
    glShadeModel(GL_SMOOTH)

    # set texture repeat
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)

    # set texture filter
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND)


def set_billboard_matrix():
    m = glGetDoublev(GL_MODELVIEW_MATRIX)
    m[0][0] = m[1][1] = m[2][2] = 1.0
    m[0][1] = m[0][2] = 0.0
    m[1][0] = m[1][2] = 0.0
    m[2][0] = m[2][1] = 0.0
    glLoadMatrixd(m)


def draw_gl():
    global rotx, roty

    glClearColor(0.125, 0.25, 0.5, 0.0)  # background color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    glLoadIdentity()  # Reset The View

    # move camera
    r = 12.0
    ex = r * math.cos(math.radians(roty + 90.0))
    ey = 5.0
    ez = r * math.sin(math.radians(roty + 90.0))
    tx, ty, tz = 0.0, 0.0, 0.0
    gluLookAt(ex, ey, ez, tx, ty, tz, 0, 1, 0)

    glEnable(GL_BLEND)
    glEnable(GL_TEXTURE_2D)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    glColor4f(1.0, 1.0, 1.0, 1.0)  # color

    # draw floor quads
    glPushMatrix()

    w = 5.0
    glBegin(GL_QUADS)

    glTexCoord2f(0.0, 0.0)  # set u, v
    glVertex3f(-w, 0.0, -w)  # Top Left

    glTexCoord2f(1.0, 0.0)
    glVertex3f(w, 0.0, -w)  # Top Right

    glTexCoord2f(1.0, 1.0)
    glVertex3f(w, 0.0, w)  # Bottom Right

    glTexCoord2f(0.0, 1.0)
    glVertex3f(-w, 0.0, w)  # Bottom Left

    glEnd()
    glPopMatrix()

    # draw object quads
    for i in range(0, 300, 20):
        x = 5.0 * math.cos(math.radians(i))
        y = 0.0
        z = 5.0 * math.sin(math.radians(i))

        glPushMatrix()
        glTranslatef(x, y, z)  # translate

        set_billboard_matrix()

        glBegin(GL_QUADS)

        glTexCoord2f(0.0, 0.0)  # set u, v
        glVertex3f(-1.0, 2.0, 0)  # Top Left

        glTexCoord2f(1.0, 0.0)
        glVertex3f(1.0, 2.0, 0.0)  # Top Right

        glTexCoord2f(1.0, 1.0)
        glVertex3f(1.0, 0.0, 0.0)  # Bottom Right

        glTexCoord2f(0.0, 1.0)
        glVertex3f(-1.0, 0.0, 0.0)  # Bottom Left

        glEnd()
        glPopMatrix()

    glDisable(GL_TEXTURE_2D)

    glutSwapBuffers()


def init_viewport_and_pers(width, height):
    # Prevent A Divide By Zero If The Window Is Too Small
    if height == 0:
        height = 1

    scr_w, scr_h = width, height

    # Reset The Current Viewport And Perspective Transformation
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()  # Reset The Projection Matrix

    # Calculate The Aspect Ratio Of The Window
    # gluPerspective(fovy, aspect, zNear, zFar )
    gluPerspective(60.0, float(width) / float(height), 0.1, 100.0)
    glMatrixMode(GL_MODELVIEW)


def InitGL(width, height):
    glClearColor(0.125, 0.25, 0.5, 0.0)  # background color
    glClearDepth(1.0)  # Enables Clearing Of The Depth Buffer
    glEnable(GL_DEPTH_TEST)  # Enables Depth Testing
    glDepthFunc(GL_LESS)  # The Type Of Depth Test To Do
    glShadeModel(GL_SMOOTH)  # Enables Smooth Color Shading

    init_viewport_and_pers(width, height)


def resize_gl(width, height):
    init_viewport_and_pers(width, height)


def on_timer(value):
    global rotx, roty
    rotx = 0.0
    roty = roty + 0.5
    glutPostRedisplay()
    glutTimerFunc(int(1000 / FPS), on_timer, 0)


def key_pressed(key, x, y):
    # If escape is pressed, kill everything.
    ESCAPE = b"\x1b"
    if key == ESCAPE or key == b'q':
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()


def main():
    global window

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)

    glutInitWindowSize(SCRW, SCRH)
    # glutInitWindowPosition(0, 0)

    window = glutCreateWindow(b"Draw texture")

    glutDisplayFunc(draw_gl)
    glutReshapeFunc(resize_gl)
    glutKeyboardFunc(key_pressed)
    # glutFullScreen()

    # glutIdleFunc(draw_gl)
    glutTimerFunc(int(1000 / FPS), on_timer, 0)

    InitGL(SCRW, SCRH)

    load_texture()

    glutMainLoop()


if __name__ == "__main__":
    print("Hit ESC key to quit.")
    main()


使用画像は以下。

_img_rgba.png


この版では、以下の解説記事を参考にして、変換行列の回転部分を単位行列にして試してみた。これは、ポリゴンをカメラに向かせる処理ではなくて、スクリーンと平行にする処理なのだとか。

_床井研究室 - ビルボード

そのあたりの処理をしているのは以下の部分。
def set_billboard_matrix():
    m = glGetDoublev(GL_MODELVIEW_MATRIX)
    m[0][0] = m[1][1] = m[2][2] = 1.0
    m[0][1] = m[0][2] = 0.0
    m[1][0] = m[1][2] = 0.0
    m[2][0] = m[2][1] = 0.0
    glLoadMatrixd(m)

PyOpenGL の場合、glGetDoublev(GL_MODELVIEW_MATRIX) を使うと、変換行列が二次元配列の形で取得できる。配列の並びは以下のような感じだった。
m[0][0]  m[1][0]  m[2][0]  m[3][0]
m[0][1]  m[1][1]  m[2][1]  m[3][1]
m[0][2]  m[1][2]  m[2][2]  m[3][2]
m[0][3]  m[1][3]  m[2][3]  m[3][3]

更に、glLoadMatrixd() を使えば、配列を変換行列に書き込む(変換する)ことができる。

ただ、この処理は、変換行列を書き換えてしまうので、そのままにしてしまうと後々の変換処理が軒並みおかしくなる可能性が高い。そこで、glPushMatrix() を呼んで変換行列を一時退避してから、こういった変換行列書き換え等の処理をして、処理が終わったら glPopMatrix() を呼んで変換行列の書き戻しをする、といった記述が必要になる。

さておき。実際に動かしてみたら、以下のような見た目になった。




一見すると上手く行ってるように思えたのだけど、よく見るとテクスチャの透明部分が色々おかしい。この症状は見覚えがある…。以前、three.js (WebGL) を勉強していた時にも遭遇した記憶が…。

_mieki256's diary - three.jsでビルボード

修正を試みたが変化無し。 :

ググっていたら、glDepthMask() や glDepthFunc() を使うべしという話を見かけたので試してみた。

_02_draw_billboard_depth_mask.py

追記したのは以下のような行。

    glDepthMask(GL_FALSE)
    glDepthFunc(GL_LEQUAL)

    # draw billboard ...

    glDepthMask(GL_TRUE)
    glDepthFunc(GL_LESS)

しかし、以下のような結果になった。ビルボードの前後関係がおかしくなってしまっている。いや、この場合、描画を指示した順番がちゃんと反映された状態で描画されるようになったと捉えるべきか…。

ALPHA_TESTを指定して解決。 :

自分の昔の日記を眺めていたら、alphaTest なるプロパティを変更したら改善された、とメモしてあった。

_mieki256's diary - three.jsでビルボード相当を実現その2

そんなわけで、そのあたりの記述を追加してみた版が以下。

_03_draw_billboard_alpha_test.py

実際には、以下のような行を追加している。
    glAlphaFunc(GL_GREATER, 0.5)
    glEnable(GL_ALPHA_TEST)

    # draw billboard ...

    glDisable(GL_ALPHA_TEST)

これはどういう指定かというと…。
  • glEnable(GL_ALPHA_TEST) や glDisable(GL_ALPHA_TEST) は、alpha test という処理を有効化/無効化している。
  • glAlphaFunc(GL_GREATER, 0.5) は、アルファチャンネルのしきい値を設定している。
この場合、アルファチャンネルの値が 0.5 より大きければそのドットは描画されるし、0.5より小さければそのドットは無視される。描画対象にするのか、しないのか、つまりは二値化している模様。

実行してみたところ、以下のような感じになった。




ちゃんとそれっぽく描画されている。この指定で、一応問題は解決すると分かった。

ただ、アルファチャンネルを2値化してるも同然なので…。アルファチャンネルがグラデーションを持っていても反映されないというか、縁の部分がジャギってるような見た目になりそうではあるなと…。

もっとも、画面を見た感じでは、それぞれ拡大縮小されるからジャギってるかどうかなんて正直よく分からん、という印象も受けた。これでも特に問題無さそうだよな…。

ビルボードを奥行きでソートして描画。 :

他の方法がないかググったところ、本来こういったものは、奥から手前に向けて重ね塗りしていくのが正しい(?)やり方、という話を見かけた。

そんなわけで、ビルボードの位置に基づいて、ソートしてから描画していく方法も試してみた。

_04_draw_billboard_sort.py

そのあたりの処理をしているのは、以下の部分。ちなみに、カメラとビルボードの距離を使ってソートする方法と、変換行列内の平行移動成分を使ってソートする方法、その2つを試してみた。この程度のサンプルなら、どちらを使っても結果はさほど変わらなかった。
    if USE_DEPTHMASK:
        glDepthMask(GL_TRUE)
        glDepthFunc(GL_LESS)
    else:
        glDepthMask(GL_FALSE)
        glDepthFunc(GL_LEQUAL)

    # record position
    pos = []
    for i in range(0, 300, 20):
        x = 5.0 * math.cos(math.radians(i))
        y = 0.0
        z = 5.0 * math.sin(math.radians(i))

        if GET_DISTANCE:
            # get distance from camera to billboard
            dx = x - cam_pos[0]
            dy = y - cam_pos[1]
            dz = z - cam_pos[2]
            dist = dx * dx + dy * dy + dz * dz
            pos.append([x, y, z, dist])
        else:
            # get z value
            glPushMatrix()
            glTranslatef(x, y, z)  # translate
            
            m = glGetDoublev(GL_MODELVIEW_MATRIX)
            tx, ty, tz = m[3][0], m[3][1], m[3][2]
            
            pos.append([x, y, z, tz])
            glPopMatrix()

    # sort billboard
    s_pos = sorted(pos, key=lambda x: x[3])
    
    if GET_DISTANCE:
        s_pos.reverse()

    for p in s_pos:
        x, y, z, _ = p
        
        # draw billboard

動作させてみたところ、以下のような見た目になった。




たしかに、描画しようとしているポリゴン群を、事前に奥行きでソートしておいて、奥のほうから手前に向かって描画していくことでも解決できそうだと分かった。

もっとも、これはまだビルボードの数が少ないから問題にならないけれど、枚数が多くなれば処理の負荷も増えてしまって、よろしくない状態になりそうではあるなと…。

透明部分がおかしくなる理由。 :

以下のページで、何故透明部分がおかしくなるのか、図で説明されていたのでメモ。

_Alpha/Transparency Sorting, Your Z-buffer, and You - Documentation - jMonkeyEngine Hub

また、以下のページで、描画がおかしい時に確認すべきことがまとめてあった。ありがたや。GL_ALPHA_TEST についても触れられている。

_DirectXとOpenGLで描画がおかしい時に考えること - TECOの技術研究室

2022/09/22(木) [n年前の日記]

#1 [python] PyOpenGLでビルボードにフォグをかけてみた

Python + PyOpenGL (OpenGL) を使ってビルボード関係の実験中。環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + PyOpenGL 3.1.6。

今回は、ビルボードにフォグをかけることができるのかどうかを試してみた。フォグというのは霧のこと。この場合、カメラとの距離に基づいてポリゴンの色を変えていくことで、遠近感があるように感じさせつつ、遠くのものははっきり見えない状態にして見た目を誤魔化す手法をフォグと呼んでいる。

動作させた結果は以下。フォグがかかっていることが分かるかと。




ちなみに、このサンプルは、ビルボードにとっては厳しい感じのカメラの動かし方をしている。回転させたり、上から見たりすると、立て看板らしさが実にハッキリと分かる…。小さいウインドウで見てる分には気にならないかもしれないけど、スクリプトを動かして、全画面表示にして眺めてみると、なかなか厳しいものが…。

ソース。 :

ソースは以下のような感じになった。python 05_draw_billboard_trees.py で実行できる。

_05_draw_billboard_trees.py
import sys
import math
import random
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

IMG_NAME = "tex.png"

# SCRW, SCRH = 1600, 900
SCRW, SCRH = 512, 512
FPS = 60

FLOOR_W = 10.0
TREES_MAX = 256

# USE_DEPTHMASK = True
USE_DEPTHMASK = False

# GET_DISTANCE = True
GET_DISTANCE = False

scr_w, scr_h = SCRW, SCRH
window = 0

# Rotation angle for the Quads
rotx = 0.0
roty = 0.0

objs = []

texture = 0

# lighting
light_ambient = [1.0, 1.0, 1.0, 1.0]
light_diffuse = [1.0, 1.0, 1.0, 1.0]
light_specular = [1.0, 1.0, 1.0, 1.0]
light_position = [5.0, 5.0, 10.0, 1.0]

# material
no_mat = [0.0, 0.0, 0.0, 1.0]
mat_ambient = [0.05, 0.1, 0.25, 1.0]
mat_diffuse = [0.0, 1.0, 0.0, 1.0]
mat_specular = [0.5, 0.5, 0.5, 1.0]
mat_emission = [0.2, 0.2, 0.2, 0.0]
no_shininess = [0.0]
low_shininess = [5.0]
high_shininess = [100.0]


class Obj:

    def __init__(self, x, y, z, scale, kind):
        self.x = x
        self.y = y
        self.z = z
        self.scale = scale

        uv = [
            (0.5, 0.0), (0.75, 0.0), (0.5, 0.25), (0.75, 0.25),
            (0.5, 0.5), (0.75, 0.5), (0.5, 0.75), (0.75, 0.75),
        ]
        self.u, self.v = uv[kind]

    def draw(self):
        glAlphaFunc(GL_GREATER, 0.5)
        glEnable(GL_ALPHA_TEST)

        glEnable(GL_BLEND)
        glEnable(GL_TEXTURE_2D)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glDisable(GL_LIGHTING)

        glColor4f(1.0, 1.0, 1.0, 1.0)  # color

        glPushMatrix()
        glTranslatef(self.x, self.y, self.z)  # translate

        m = glGetDoublev(GL_MODELVIEW_MATRIX)
        m[0][0] = m[1][1] = m[2][2] = 1.0
        m[0][1] = m[0][2] = 0.0
        m[1][0] = m[1][2] = 0.0
        m[2][0] = m[2][1] = 0.0
        glLoadMatrixd(m)

        w = 1.0 * self.scale
        u, v = self.u, self.v
        uw = 0.25

        glBegin(GL_QUADS)

        glTexCoord2f(u, v)
        glVertex3f(-w, w * 2, 0)  # Top Left

        glTexCoord2f(u + uw, v)
        glVertex3f(w, w * 2, 0.0)  # Top Right

        glTexCoord2f(u + uw, v + uw)
        glVertex3f(w, 0.0, 0.0)  # Bottom Right

        glTexCoord2f(u, v + uw)
        glVertex3f(-w, 0.0, 0.0)  # Bottom Left

        glEnd()
        glPopMatrix()

        glDisable(GL_ALPHA_TEST)
        glDisable(GL_TEXTURE_2D)


def init_objs():
    global objs
    w = FLOOR_W
    n = 4.0
    kind = 0
    i = 0
    while i < TREES_MAX:
        x = random.uniform(-w, w)
        y = 0
        z = random.uniform(-w, w)
        if (x * x + z * z <= n * n):
            continue

        scale = random.uniform(1.0, 1.5)
        objs.append(Obj(x, y, z, scale, kind))
        kind = (kind + 1) % 4
        i += 1


def draw_objs():
    global objs
    for obj in objs:
        obj.draw()


def load_texture():
    global texture

    # load image by using PIL
    im = Image.open(IMG_NAME)
    w, h = im.size
    print("Image: %d x %d, %s" % (w, h, im.mode))

    if im.mode == "RGB":
        # RGB convert to RGBA
        im.putalpha(alpha=255)
    elif im.mode == "L" or im.mode == "P":
        # Grayscale, Index Color convert to RGBA
        im = im.convert("RGBA")

    raw_image = im.tobytes()

    ttype = GL_RGBA
    if im.mode == "RGB":
        ttype = GL_RGB
        print("Set GL_RGB")
    elif im.mode == "RGBA":
        ttype = GL_RGBA
        print("Set GL_RGBA")

    glBindTexture(GL_TEXTURE_2D, glGenTextures(1))

    # glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4)

    # set texture
    glTexImage2D(
        GL_TEXTURE_2D,      # target
        0,                  # MIPMAP level
        ttype,              # texture type (RGB, RGBA)
        w,                  # texture image width
        h,                  # texture image height
        0,                  # border width
        ttype,              # texture type (RGB, RGBA)
        GL_UNSIGNED_BYTE,   # data is unsigne char
        raw_image,          # texture data pointer
    )

    glClearColor(0, 0, 0, 0)
    glShadeModel(GL_SMOOTH)

    # set texture repeat
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)

    # set texture filter
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
    # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND)


def draw_floor():
    """Draw floor quads."""

    w = FLOOR_W
    u, v = 0.0, 0.0
    uw = 0.5

    glDisable(GL_LIGHTING)

    glEnable(GL_BLEND)
    glEnable(GL_TEXTURE_2D)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    glColor4f(1.0, 1.0, 1.0, 1.0)  # color

    glPushMatrix()

    glBegin(GL_QUADS)

    glTexCoord2f(u, v)
    glVertex3f(-w, 0.0, -w)  # Top Left

    glTexCoord2f(u + uw, v)
    glVertex3f(w, 0.0, -w)   # Top Right

    glTexCoord2f(u + uw, v + uw)
    glVertex3f(w, 0.0, w)    # Bottom Right

    glTexCoord2f(u, v + uw)
    glVertex3f(-w, 0.0, w)   # Bottom Left

    glEnd()
    glPopMatrix()


def draw_cube():
    """ Draw cube."""
    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)

    glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient)
    glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse)
    glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular)
    glMaterialfv(GL_FRONT, GL_SHININESS, high_shininess)
    glMaterialfv(GL_FRONT, GL_EMISSION, no_mat)

    glPushMatrix()
    glDisable(GL_TEXTURE_2D)
    glColor3f(0.0, 1.0, 0.0)

    if False:
        glTranslatef(0.0, 2.0, 0.0)
        glutSolidCube(4.0)
    else:
        glTranslatef(0.0, 1.2, 0.0)
        glutSolidTeapot(2.0)

    glPopMatrix()


def draw_gl():
    global rotx, roty

    glClearColor(0.85, 0.85, 0.85, 0.0)  # background color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # set light
    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient)
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse)
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular)
    glLightfv(GL_LIGHT0, GL_POSITION, light_position)
    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)

    glLoadIdentity()  # Reset The View

    # move camera
    r = 16.0
    ex = r * math.cos(math.radians(roty + 90.0))
    ey = 6.0 + 4.0 * math.cos(math.radians(roty * 3))
    ez = r * math.sin(math.radians(roty + 90.0))
    tx, ty, tz = 0.0, 0.0, 0.0
    gluLookAt(ex, ey, ez, tx, ty, tz, 0, 1, 0)
    cam_pos = (ex, ey, ez)

    draw_floor()

    draw_cube()

    draw_objs()

    glutSwapBuffers()


def init_viewport_and_pers(width, height):
    # Prevent A Divide By Zero If The Window Is Too Small
    if height == 0:
        height = 1

    scr_w, scr_h = width, height

    # Reset The Current Viewport And Perspective Transformation
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()  # Reset The Projection Matrix

    # Calculate The Aspect Ratio Of The Window
    # gluPerspective(fovy, aspect, zNear, zFar )
    gluPerspective(45.0, float(width) / float(height), 0.1, 100.0)
    glMatrixMode(GL_MODELVIEW)


def InitGL(width, height):
    glClearColor(0.85, 0.85, 0.85, 0.0)  # background color
    glClearDepth(1.0)  # Enables Clearing Of The Depth Buffer
    glEnable(GL_DEPTH_TEST)  # Enables Depth Testing
    glDepthFunc(GL_LESS)  # The Type Of Depth Test To Do
    glShadeModel(GL_SMOOTH)  # Enables Smooth Color Shading

    # setiing fog
    fog_color = [0.85, 0.85, 0.85]
    glEnable(GL_FOG)
    glFogi(GL_FOG_MODE, GL_LINEAR)
    glFogfv(GL_FOG_COLOR, fog_color)
    glFogf(GL_FOG_DENSITY, 0.1)
    glHint(GL_FOG_HINT, GL_DONT_CARE)
    glFogf(GL_FOG_START, 8.0)
    glFogf(GL_FOG_END, 25.0)

    init_viewport_and_pers(width, height)


def resize_gl(width, height):
    init_viewport_and_pers(width, height)


def on_timer(value):
    global rotx, roty
    rotx = 0.0
    roty = roty + 0.25
    glutPostRedisplay()
    glutTimerFunc(int(1000 / FPS), on_timer, 0)


def key_pressed(key, x, y):
    # If escape is pressed, kill everything.
    ESCAPE = b"\x1b"
    if key == ESCAPE or key == b'q':
        if glutLeaveMainLoop:
            glutLeaveMainLoop()
        else:
            sys.exit()


def main():
    global window

    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)

    glutInitWindowSize(SCRW, SCRH)
    # glutInitWindowPosition(0, 0)

    window = glutCreateWindow(b"Draw texture")

    glutDisplayFunc(draw_gl)
    glutReshapeFunc(resize_gl)
    glutKeyboardFunc(key_pressed)
    # glutFullScreen()

    # glutIdleFunc(draw_gl)
    glutTimerFunc(int(1000 / FPS), on_timer, 0)

    InitGL(SCRW, SCRH)

    load_texture()

    init_objs()

    glutMainLoop()


if __name__ == "__main__":
    print("Hit ESC key to quit.")
    main()


使用画像は以下。CC0 / Public Domain ってことで。前述のスクリプトと同じ場所に置いておくこと。

_tex.png

覚書。 :

フォグをかけるように指定している部分は以下。
    # setiing fog
    fog_color = [0.85, 0.85, 0.85]
    glEnable(GL_FOG)
    glFogi(GL_FOG_MODE, GL_LINEAR)
    glFogfv(GL_FOG_COLOR, fog_color)
    glFogf(GL_FOG_DENSITY, 0.1)
    glHint(GL_FOG_HINT, GL_DONT_CARE)
    glFogf(GL_FOG_START, 8.0)
    glFogf(GL_FOG_END, 25.0)
  • glEnable(GL_FOG)、glDisable(GL_FOG) で、フォグの有効/無効を切り替えられる。
  • fog_color がフォグの色。[1.0, 1.0, 1.0] なら真っ白になるし、[0.0, 0.0, 0.0] なら真っ黒になる。
  • glFogfv(GL_FOG_COLOR, fog_color) で、フォグの色を指定している。
  • glFogi(GL_FOG_MODE, GL_LINEAR) で、フォグのかかり方の種類を指定している。
  • フォグのかかり方が GL_LINEAR の場合、GL_FOG_START、GL_FOG_END で、フォグがかかり始める距離を指定できるらしい。
  • GL_FOG_DENSITY の値は、フォグのかかり方 (GL_FOG_MODE) が、GL_EXP、GL_EXP2 の場合に絡んでくるらしい。今回は GL_LINEAR にしているから、関係してこないっぽい。

画像の入手先について。 :

今回、以下のblogで、CC0 として公開されてる木の画像を、テクスチャとして使わせてもらった。ありがたや。blenderでレンダリングした画像らしい。

_Tree 01 Free CC0 Image | Graphics Learning
_Tree 02 Free CC0 Image | Graphics Learning
_Tree 03 Free CC0 Image | Graphics Learning
_Tree 04 Free CC0 Image | Graphics Learning


余談。一応自分も木の画像を公開していたりするのだけど、見た目のスタイルがバラバラなものだから、自分で作っておきながら使い辛くて…。

_trees - OneDrive
_mieki256's diary - 木のテクスチャをレンダリング中

参考ページ :

#2 [gimp] GIMP 2.10の全レイヤーをレガシーモードにしたい

Windows10 x64 21H2 + GIMP 2.10.32 Portable samj 版で、テクスチャ画像の加工修正をしているのだけど、GIMP 2.10 は GIMP 2.8 とレイヤーの実装が変わったので、そのままだと .psd で保存(エクスポート)した際などにレイヤーが全部統合された状態でエクスポートされてしまう。

.psd でエクスポートしても、各レイヤーをバラバラの状態にしたいなら、各レイヤーを「デフォルト」モードから「レガシー」モードに変更して、GIMP 2.8時代のレイヤーの状態にしてからエクスポートしないといけない。

しかし、一つ一つ、レイヤーをレガシーモードに切り替えていくのが面倒臭い。一気に切り替えることはできないものか。

ググってみたら、全レイヤーをレガシーモードに切り替えてくれるプラグイン(Python-Fuスクリプト)を公開してくれてる方がいらっしゃった。ありがたや。使わせてもらおう…。

_To Legacy Mode Script/Plug-in - GIMP LEARN

to_legacy_modes(2).zip をダウンロード/解凍して、to_legacy_modes.py を GIMPユーザーディレクトリ/plug-ins/ 以下にコピーした。

オリジナル版は、Python-Fu というメニュー項目が増えるのだろうか…。自分の環境に合わせて、以下のように修正して使うことにした。レイヤーに対して処理をするのだから、「レイヤー」の下にあったほうが迷わなくて済むよな、みたいな。

        "<Image>/Python-Fu/To Legacy Modes...",             #Menu path
↓
        "<Image>/Layer/All Layers/To Legacy Modes...",

ちなみに、GIMP 2.10 のレイヤーの、「デフォルト」「レガシー」は、互換性がない合成モードがいくつかあるらしいので…。安易にモードを切り替えると見た目が違ってしまう可能性があることに注意。まあ、例えば「標準」で重ねてるだけの凝ったことをしていない画像であれば、問題無さそうではあるけれど…。

さておき。件のスクリプトの作者様は、他にも色々なスクリプトを公開してくれているようで。ありがたや。そのうちじっくり眺めてみよう…。

_GIMP 2.10 Scripts/Plug-ins - GIMP LEARN

2022/09/23(金) [n年前の日記]

#1 [python] OpenGLのGL_ALPHA_TESTについて動作を調べてた

Python + PyOpenGL を使ってOpenGLを勉強しているところ。ポリゴンに貼ったテクスチャ画像の透明部分について、妙な描画結果になることに気づいてしまって、そのあたりを少し調べてた。

環境は、Windows10 x64 21H2 + Python 3.9.13 + PyOpenGL 3.1.6。GPU/ビデオカードは、NVIDIA GeForce GTX 1060 6GB。

実験に使った画像は以下。
img_rgba2.png
_img_rgba2.png

実験に使ったソースは以下。

_06_draw_billboard_alpha_test_2.py

さて。アルファチャンネルを持ったテクスチャ画像をそのまま描画すると、以下のように、アレな見た目になってしまう。画面の右側は期待していた見た目になっているけれど、左側は、透明になってほしい部分の後ろにあるはずの色がちゃんと出てこない…。

ss_06_draw_billboard_alpha_test_2_01.png

ボールの向こう側にもボールがあるのだから、本来、透明部分には、奥側にあるボールの赤い色がそのまま出てほしいわけだけど…。実際は奥側のボールを素通りして、背景相当の緑の床が表示されてしまっている。

これはデプスバッファを使って描画する際に起きてしまう問題で、「一番手前にあるポリゴンの色だけをそこに描画すれば隠面消去ってできるよね」という考え方で描画するものだから、その奥にあるポリゴンの色は無視されて、こんな状態になってしまう、らしい。

左側では、まず、一番手前のポリゴンを描画して、そのポリゴンの奥行き情報がデプスバッファに書き込まれる。その後、奥にあるポリゴンを描画しようとしても、デプスバッファには一番手前のポリゴンの奥行き情報が既に書き込まれているので、「今から描こうとしてるポリゴンより手前になる奥行き情報がデプスバッファに書き込まれてるな。じゃあ、ここにはもう描かなくてええやろ」と無視されてしまうのだと思う。

GL_ALPHA_TEST を有効にすると、多少は改善されるけど。しかし、これもまだまだ問題が…。
        glEnable(GL_ALPHA_TEST)
        glAlphaFunc(GL_GREATER, 0.1)

ss_06_draw_billboard_alpha_test_2_02.png

この場合、アルファチャンネルの値を見て、0.1 より上か下かで、不透明もしくは透明に2値化してくれるはずだけど…。やはり奥側にあるボールの色が反映されず、床が見えてしまっている。


色々試してみたけれど、結局のところ、こういったテクスチャ画像を貼るポリゴンについては、奥行き情報でソートして、奥側から手前に向かって描画しないと解決しないようだなと分かってきた。その分、処理負荷が増えるだろうから、あまりやりたくはないのだけど…。

ss_06_draw_billboard_alpha_test_2_03.png


あるいは、テクスチャ画像そのものに対して、事前にアルファチャンネルを2値化しておくことでも解決できるだろうなと。実際、上記のボール群も、アルファチャンネルを2値化している下半分については、見た目で特に問題は無いわけで。もっとも、縁の部分でジャギが目立つだろうし、ぼんやり重ねた感じの表現ができなくなるから、見た目の品質は落ちてしまうのだろうけど、処理負荷については、おそらく軽く済むだろう…。

つまるところ、処理負荷を増やして見た目の品質を向上させるか、見た目を犠牲にして処理を軽くするか、トレードオフなのかもしれないなと…。

余談。このあたりの問題は、 _「opengl transparent artifacts」_「Order independent Transparency」 (OIT) でググるとそれらしいページが出てくる模様、とメモ。

2022/09/24() [n年前の日記]

#1 [nitijyou] 部屋に蚊が居る

部屋に蚊が居る…。目の前を時々横切る…。蚊取り線香をずっとつけっぱなしにしてたけど、自分がダメージを受けてる感じ。目が痛い…。

#2 [anime] 「竜とそばかすの姫」を視聴したことをメモ

昨日、金曜ロードショーで、「竜とそばかすの姫」が放映されていたので視聴、したことをメモしてなかったのでメモ。初見。

面白かった。「細田守監督作品の集大成」という評をどこかで見かけていた記憶があるけれど、まさしくそんな感じ。これはもう、スーパー細田アニメ大戦。ところどころ自作のセルフパロディだかセルフオマージュが盛り込まれていて、心の中でツッコミを入れる行為がちょっと楽しかった気もする。

CGの進歩も個人的には見所と感じた。CGキャラが手描きアニメのように動いていて、ここまでレベルが上がったのかと感心したりもして。昔だったら、というか「サマーウォーズ」の頃は、こういう動きをするキャラは手描きだったよなと…。よくぞここまで…。

仮想空間内のキャラデザも見ていて楽しかった。あの膨大なキャラ達は、一体一体、誰かがデザインして、誰かがモデリングしているはずで…。いやはや、よくまあここまで作ったなあ…。

「サマーウォーズ」の頃には存在しなかった、SNSをキッカケにバズる云々も盛り込まれていて、時代の違いを感じたりもして。おそらく10年後20年後には、ネットはまた違った光景を見せていて、それがアニメだの映画だのに盛り込まれたりもするのかなと。作家さんは大変だな…。そのあたり、これからずっとアンテナを立てておかないと…。

基本的には、「傷」をテーマにした作品なのかなと…。その傷がどういう流れで癒されていくのか、あるいは癒されることなく傷ついたままで過ぎていくのか、みたいな。

あるいは、ネット上に溢れる正義マンのアレコレも興味深かった。 「そんなの正義じゃない。他人を支配したがってるだけ」云々とか。自分ですら思い当たる…。気をつけないと…。

全然違う話だけど。これは「ウォーゲーム」Ver.3.0なのではと思ったりもして。例えば、「Microsoft製品はVer.3.0から使い物になる」という話があるし、「家は3回建て直すと満足できる家になる」という話があって。映画だって同じかもしれない、3回作り直せばかなりの完成度になるのではないか。実際、このアニメを目にして、「3回作れば〜」の仮説は本当かもしれないなと思えてきた。

つまり、過去に失敗作と思われてしまった各作品だって、そのネタでVer.2.0、Ver.3.0を作ってみたら化ける可能性があるのではないかと…。でもまあ、続編や外伝を作ってみたらアレなことになった作品も多々あるだろうから、一概には言えないか…。

誰が脚本を書くべきか。 :

細田監督は脚本家さんと一緒に脚本を書いたほうが、客観的な視点を脚本面で確保することができて、より良い映画ができるのではないか ―― などと今まで個人的には思ってたりもしたのだけど。たまたま、「オマツリ男爵」の脚本を担当した方のblogを目にして、ちょっと考えが変わってしまった。

_二年ぶりの約束: sometimes i speak!?放送作家伊藤正宏のノート?
_試写会: sometimes i speak!?放送作家伊藤正宏のノート?

脚本家さんが目指した方向とは全く違う方向に大改変、もしくは、脚本通りの流れのはずがその巧みな演出力で全く違う印象のシーンに激変してしまうことは、脚本家さんにとってあまりにも不本意であり、強烈な苦痛を伴う状況、らしい…。

となると、脚本家さんに一切不満を持たれることなく、しかし確実に新作映画を作るとしたら…。これはもう細田監督が最初から自分で脚本を書く以外に無いのだなと。それが各方面に対して、被害が最小限で済む制作スタイルになるわけで…。それ以外の方法なんて無いだろう…。

細田監督作品に対して、より良い脚本を望むこと自体が、贅沢というか、不可能というか、空虚な理想論、絵空事、現実を知らない無責任な発言の類、なのかもしれないなと思えてきたわけで。もしかすると、ある時期から細田監督は、そもそも脚本を誰にも書いてもらえない状態に追い込まれていて、仕方なく本人が脚本を書いている、そんな状況なのかも、とすら…。ここ最近は、ずっと崖っぷちギリギリの状態でどうにか映画を作ってる状況かもしれないのだなと。新作映画が出てくるだけでもお前等ありがたいと思え、本当なら映画なんか作れないのに、既に起こるはずのない奇跡が4回も起きてるんだぞ、みたいな。いや、4作品も脚本を書けてるならそれもう奇跡じゃないでしょ、堂々と脚本家を名乗れますわ、とも思いますが。

どこかに、「細田監督と一緒に仕事ができるだけでも俺は幸せです」「脚本改変? どんとこいですよ。監督の好きなようにバンバン変えてくださいよ」と言い出す奇特な脚本家さんは居ないものか…。まあ、そんなタフな脚本家さんは居るわけないよな…。

2022/09/25() [n年前の日記]

#1 [anime] 「さよなら銀河鉄道999」を視聴

BS12で放送されてたので視聴。たしか以前もBS12で放送されてた気がするけれど…。ちなみに先週は、「劇場版銀河鉄道999」が放送されてた。

やはり良い…。劇場版999シリーズは、良い…。いやまあ、脚本面だか設定面ではツッコミどころ満載な気もするけど。細田アニメの脚本に文句言ってる人達が「さよ銀」見たら泡吹いて卒倒するんじゃないの? ってレベルだけど。しかし、とにかくあちこちがスタイリッシュで…。この映像が…良い…。

松本零士先生はどのくらい関わっていたのか気になってググってみたのだけど、少なくとも企画段階で、りんたろう監督と話し合って戦争モノにするという方針が決まったという話を見かけた。どうやら全くノータッチというわけではなさそうだなと…。おそらく色々なアイデアを提供していたのではなかろうかと勝手に想像。そして脚本家さんはパズラーになってそう。たぶん。

整合性とかそっちのけで、「こういうシチュエーションを見せたいんじゃ」「こういう台詞を吐かせたいんじゃ」「こういうカット見せたいんじゃ」が伝わってくるというか。それはそれでヨシ。そういうのもアリ。と個人的には思うのです。色々あったほうが面白いよなと。

2022/09/26(月) [n年前の日記]

#1 [zatta] 言葉って便利

思考メモ。

ニュースサイトを見ていたら、安倍元総理の声をAIで復元したと称する追悼サイト、なるものが公開されたという記事を目にした。そのサイトで、音声を聞いているうちに、なんだかちょっと考え始めてしまったのでメモ。思考メモ。

「AI」とつければ何でもOKになるのかもしれない。 :

個人的には、件の音声を聞いて、「これは本当にAIを使ってるのだろうか…?」「既存の音声データを手作業で切り貼りしたとしてもこのくらいなら余裕で作れるのでは…」と疑問を持ってしまった。と言うのも、大昔に自分も既存の音声データを波形編集ソフトで切り貼りして音声MADを作れないか試していた時期があるので、その時の作業内容を鑑みると、フツーにやれてしまう程度の成果物だよなと感じたわけで。いや、本当にAIを使ってたらゴメンナサイって感じだけど。

そこでふと、「AI」とつけておけば何でも通ってしまう風潮がここ最近あるのかもしれんなあ、と思えてきたという…。

仮に、既存の泥臭い作業で何かしらを作ったとしても、そこに「AI」をつけて嘯いてみることで、受け手の印象は変わってきてしまうよなと。「AIか…。何だかよく分からない高度な実験をしているんだろう…。コレを叩いてしまったら日本国内の科学技術の進歩に釘を刺してしまうことになるのではないか…」という印象を持ってしまって、否定することも躊躇してしまうのではないか。

「AI」に限らず、そういうアレコレって巷に溢れているな、とも思えてきた。「新型小型原発の開発に注力する」とか。「DXに邁進する」とか。「骨太の方針」とか。中身がよく分からないけど「新型」とか「DX」とか「骨太」とか言ってるからきっと今までのソレとは違う良いものなんだろうと安易に思い込んで肯定しちゃう、みたいな。そう言えば「次世代ゲーム機」も若干ソレだったのかなという気もしてきた。「次世代」ってつけると良さそうなモノに思えてくるんだよな…。

何にせよ、「AI」とつけると通らなかったものも通るようになってしまうのかもしれない。便利だなと。

「追悼」とつければ何でもOKになるのかもしれない。 :

安倍元総理も、今の自民党議員も、例えばカルト宗教団体に対して何かしら対策が必要なのではないかと問われた時に、「それは各人の心の問題に絡むので…」等の言を使って具体的な策を打つことを逃避してきた印象があるのだけど。

「追悼」という言葉も、「心の問題」と似たような便利さがあるなと。

そこで行われている行為が何であれ、「追悼」と称しておけば、ちょっと叩くのが難しくなる、ような気がする。当人がソレを行うことの発端となったのは「追悼」という「心の問題」なので、そのことを否定するのはいかがなものか、という気分になってくるというか。

何にせよ、「追悼」とつけると通らなかったものも通るようになってしまうのかもしれない。便利だなと。

言動をトレースすることも追悼の一種、なのかもしれない。 :

個人的に、安倍元総理は、印象が良さそうな奇麗な言葉ばかりを多用してアレコレ進めていった人物、という印象があるのだけど。そういう面では、件の追悼サイトも、安倍元総理の生前の言動をトレースしているように感じたりもして…。

そこで実際に行われていることが何であれ、とにかくイイ感じの言葉を羅列しておく。それが、安倍元総理が好んで使っていた手口だったよなと。

そういう意味では、件のサイトも、たしかに「追悼」なのかもしれないなと思えてきた。内容はともかく、それっぽい言葉をたくさん並べて、それっぽく見せかけるソレ。「安倍さんってこういうメソッドを使いまくってた人でしたよね…。合掌」みたいな。

ここでもし、「陳腐な言い訳してるんじゃねえよ」的に叩いてしまうと、それは安倍元総理の生前の振る舞いも叩くことに繋がってしまうのではなかろうか。いや、生前、総理時代も、野党から散々ツッコまれてたから今更かもしれんけど。

安倍元総理がどんな人物だったのかを思い返すと…。自分は、件のサイトを否定するのがちょっと難しい気分になってくるなと。だって、やってることが、生前の安倍元総理と同じだから…。とにかくそれっぽい奇麗な言葉ばかり並べて体裁を繕ってた人だったよなあ…。

ところで。もし、件のサイトが叩かれて消えてしまうとしたら、そこにあるのは暗殺犯と同じメンタリティかもしれないな、とも思えてきた。つまるところ、「どんだけ奇麗な言葉を並べてみてもお前のやったことは許されねえんだよ!」と議論以外の方法で相手を消去してしまったのが件の暗殺の図式なわけで…。安倍元総理と同じことをしているサイトを、安倍元総理を葬ったのと同様、言葉以外の手段で消去するわけだから、やってることは同じだよなと…。だからちょっと、ここは慎重に振舞ったほうがいいのかもしれない。

バーチャルとは言え、安倍元総理を二度三度と暗殺する必要は無いのではないか、みたいな。いやまあ、その人が、安倍元総理を何度でも殺したいと思ってるなら話は別だろうけど…。

まあ、そんな感じで、ちょっと色々考えてしまいました、とメモ。思考メモです。オチは無いです。

2022/09/27(火) [n年前の日記]

#1 [anime] 「漁港の肉子ちゃん」を視聴

Eテレで放送されていたソレを録画していたので視聴。初見。

明石家さんまプロデュースを宣伝文句にしていた、小説を原作とするアニメ映画。実制作スタッフは、渡辺歩監督、キャラデザ総作監が小西賢一さん、制作はSTUDIO 4℃と錚々たるメンバー。実力派クリエイターが名を連ねてる。

冒頭から異様なテンションでキャラ紹介が始まって驚いた。とにかく動く。ひたすら動く。朝御飯を作るシーンすらゴリゴリ動く…。バターが熱で溶けていく様子。卵に浸された食パンがフライパン上にペタリと落ちる様子。そんなところまでキッチリ描く…。作画レベルがとんでもない…。いやはや、ここまでやるか…。最近のアニメってヤバイな…。

個人的には絵柄のスタイルがかなり好み。系統としては…何だろう…。どこか懐かしさを感じる絵柄なんだけど…。ざっくり描く部分と細かく描き込む部分がはっきり分かれてるというか…。この絵柄は誰の絵柄に近いのかな…。

主人公が女子小学生なのが良いなと。主人公が目にしたアレコレが、小学生らしい素直な感想でまとめられていくことで、印象がマイルドになるというか…。

タイトルにもなっている肉子というキャラを演じているのが大竹しのぶさんなのだけど。演技がスゴイ…。最初は聞いてて全然分からなくて、「この中の人、めちゃくちゃ上手いけど一体誰なんだろう…」とすら思ってた。監督さんが「怪演」と評していたらしいけど、まさしくそのレベル。実力のある役者さんは声優としてもフツーに上手いのだよなあ、と再認識。

話は…。さんまさんが原作に惚れ込んで映像化企画が始まったとされているだけあって、たしかにイイ感じの話だなと…。いやまあ、どこか捉えどころがなくて、人によっては「何を言いたいのか分からん」と言い出しそうな予感もあるけれど、対象年齢が高めの少女漫画のような雰囲気で、個人的にこういう話は好み。自分が「ぶ〜け」を読んでた時期をちょっと思い出したりもして。

これはイイ感じのアニメを見せてもらえたなと…。ただ、興行成績は良くなかったようで…。

さんまプロデュースがマイナスに働いたのではないかと邪推したけどどうなんだろう…。アニメオタクならその宣伝文句で「あ。結構です。遠慮します」になっちゃうし、一般層は「あのさんまさんが?」と好意的に受け止めてくれる予感がしないしで、どっち方面にも届かなくなったのでは、と…。「プペル」もそうだろうけど、芸人さんが絡むと大体はマイナスイメージになる印象があるなと…。しかし、さんまさんが動かないとこの企画は始まってなかったのだろうし、仮にさんまさんが絡んでなくてもこの内容でドカンと受けるかというとビミョーだろうし。さんまプロデュースは足枷になったのか、それとも追い風になったのか、ちょっと判断が難しそう。

何にせよ、興行成績の件を考えると、隠れた名作と言ってしまってもいいのかもしれない。海外の映画祭でいくつか賞を取ってるようでもあるし。でも、海外で賞を取った作品って、何故か日本国内で評価されないのだよな…。なんでだろ…。

2022/09/28(水) [n年前の日記]

#1 [nitijyou] 某所に行ってきた

某所から電話有り。PCのトラブル相談だったけど、現場で画面を見てみないと分からない感じの話だったので、電動自転車で行ってきた。自分が帰宅後、また別の質問内容を思い出したようで電話連絡があり、もう1度行ってきた。作業時間(?)は、12:45-13:25、15:30-15:50。詳細はGRPでメモ。

ところで、個人的にちょっと衝撃的な場面を目撃してしまったのだけど…。それについては別記事としてメモしておく。

#2 [pc] PCのUIはまだまだダメダメかもしれない

思考メモ。

今回、PC(Windows10 + Excel)の操作を教えてきた御相手は、結構年配の方だったのだけど。その方のPC操作(?)を目にして、ちょっと考え込んでしまったのでメモ。思考メモ。

どういう操作かというと…。Excelファイルを開いてじっと眺めた後、PCの電源コードが差さっている電源タップのスイッチを、いきなりバチバチと切り始めて(もちろんWindowsは電源断による強制終了状態)、そこからまたバチバチとスイッチを入れて、PCの電源スイッチを押してWindowsを立ち上げ始めて…。

見ているこちらはかなり困惑してしまった。やめて! そんなことをしたらWindowsが壊れる…!

そこでふとなんとなく気が付いた。ひょっとして、この方、Windowsの正しい終了の仕方を知らないのでは…?

とりあえず、「そのような終了のさせ方をするとPCが壊れてしまいます」「左下の旗みたいなマークをクリックするとメニューが開いて、そのちょっと上の電源っぽいマークをクリックするとまたメニューが出ますので、その中のシャットダウンと書かれてるところをクリックして終了してください」と伝えてみた。…説明が長いね。クソ長い。でも、実際そういう操作をMicrosoftから要求されているのだから仕方ない。 *1

これでWindowsの終了の仕方は分かったかな、と思いきや、今度は、Excelファイルを開くたびに、Windowsをシャットダウンさせて、またWindowsを起動して、デスクトップ上のExcelファイルを開いて…。なぜそんなことを…。

そこでふと気が付いた。この方、Excelファイルの閉じ方、あるいは、Excelのウインドウの閉じ方を知らないのでは…? だから、最初から、つまりはWindowsを起動させるところからやり直しているのでは…。

「開いたファイルを閉じたい時は、右上のバツのマークをクリックすると閉じることができますよ」と伝えてみた。いや、この操作、正確にはExcel自体を毎回終了させちゃってるのだけど…。 *2 相手の方は、Excelのウインドウが閉じられた瞬間、小さく「おお…」と漏らしてた。

なんだか考え込んでしまった。PCのUIって、まだまだ全然ダメダメなんじゃなかろうか。少なくとも、こうしないと作業ができないものと年配の方がすっかり思い込んでしまう程度には、未だに難解なまま放置されているのでは…。

笑ったり呆れたりする時点でもうダメ。 :

PCの操作に慣れてる人は、こんな話を聞いても、笑うか、もしくは呆れるのだろうなと想像するのだけど。でも、そこで笑ったり呆れたりする時点で、その人達自身もダメダメなんだろうなと思える…。

かつて、スティーブ・ジョブズは、iPhoneだかiPadだか何かの製品発表時に、「自分の母親でも使えるパーソナルコンピュータを作りたかった。そして出来上がったのが、コレです」的発言をして製品をアピールしたと聞く。 *3

実際に年配者でも使えるPCになったかどうかは、ちょっと、いや、かなり疑問が残るけど…。それでも、少なくともジョブズは、「今のPCはまだまだ操作が難解だ」「こんなUIでは年配者は使えないだろう」という問題意識を明確に持っていたことは間違いないし、「年配者でも使えるPCを作れたら、大ヒット間違い無し」「新しい製品市場を確立できるはずだ」ぐらいのことも考えていたのだろう。

そして実際、スマートフォンと言う製品ジャンルは、世界を席巻してみせた。

そういったことを鑑みると、この手の話を見て笑ったり呆れるようではダメだよなと…。問題意識すら持てない人が、問題を解決する名案を思いつくはずもなく。そこには停滞しかない。進歩する可能性がどこにも無い。

何にせよ、PCは、いや、PCに限らずデジタル家電全般がそうかもしれないけれど、まだまだUIの改良が足りないのではなかろうか。機械を人間に合わせるのではなく、人間が必死に機械に合わせている。人間が機械を使ってるんじゃなくて、人間が機械に使われてる。

などと書いてはみたけれど。昨今のWindowsは電源ボタンを押せばシャットダウン処理に入る仕様になってるので、例えば件の話は「電源ボタン押せばええやん?」で済んでしまうのだよなあ…。あ。しまった。そのやり方を伝えてくるのを忘れてた…。そっちをまず伝えるべきだったのでは…。どうやら自分、スイッチバチバチアクションを目にして、パニクってそのへんスポンと頭から抜けちゃった気がする…。自分もまだまだダメダメだなあ…。

終了ボタンはどこにあるのが妥当なのだろう。 :

Windowsの画面からは、いつの間にか「スタート」の文字が無くなってしまったけれど。本当にそれで良かったのだろうか…? 画面の左下にある、他のアイコンと同サイズの、Windowsのマークをクリックすればスタートメニューが開くという仕様は、分かってる人は分かってるけど、何も知らない人にとってそんなの分かるはずもなく…。そもそもコレって、本当にWindowsのマークなのだろうか。昔の形とは違うよな…。

そういえば自分も、PC-9801を使って仕事をしてた頃、Macの電源の切り方が分からなくてデザイナーさんに助けを求めたことがあったっけ…。説明を受けて分かってしまえばなんてことない操作だけど、その「なんてことない操作」という点が、そこに到達できない人達だって居るのですよ、という事実を軽視することに繋がっている可能性もありそう。初見で分かるわけがない操作ではあるのだよな…。

日本人相手に「スタート」と言われてもな、という気もする。本当なら「ここから始めます」とか「まずはここをクリック」と書いておいたほうがいいのでは。ダサいけど。めちゃくちゃダサいけど。自称デザイナーさん達がゲンナリした顔になりそうだけど。

終了操作をするための選択項目が、スタートメニューの中に入ってるというのもどうなんだろう。利用者は作業を終わらせたいと思っているのに、何かを始めるためのメニューを経由するというのは…それは自然な流れと言えるのか。「終了操作を『始める』のだから何も間違ってない」と言い出す人が多数だろうけど、そんなの屁理屈としか思えない。

しかし、「じゃあお前がUIをデザインしてみろや」と言われると…当たり前だけど頭を抱えるわけで…。

文字ばかり並べてもダメなんだよな…。ウチの親父さんなんてアプリウインドウの「ファイル」「編集」「表示」のメニューですらスルーしてるし…。文字列がずらずら並ぶと「俺には分からない何かが並んでるから触らないでおこう」という思考になるのだろうと邪推しているのだけど…。いやまあ、それはアイコンを並べても同じなんだけど。「何のアイコンか分からないからひとまず触らないでおこう」になるだけで…。

アプリウインドウと同様に、OS終了ボタンもバツマークにすればええやん。タスクバーを上に配置して、画面の右上にOS終了ボタン置いとけばええのや。なんて言おうものなら分かってる人達に「やめろー」と言われるだろうな…。どう考えてもアプリ終了のつもりでOSを終了して大事故発生間違い無し。だからOS終了ボタンはメニューの奥底に隠されているわけだけど…。その結果、電源タップのスイッチバチンを引き起こすわけで…。

初見でもスイスイと、かつ、間違いなく使えるUIなんて、幻想なんだろうか。でも、ジョブズはちょっと進んでみせたよな…。

「機器の終了のさせ方が分からない? じゃあ終了しなければいいじゃん」
「アプリの閉じ方が分からない? ならアプリ閉じなければいいじゃん」

実際スマホってそうなってるわけで。オイオイ、ジョブズ、天才かよ…。まさか異世界から転生してきた主人公? でも、それはもうUIの話ではなさそう。それとも、それもまたUIの話なのだろうか。

思考メモです。オチは無いです。

*1: コレを書いてる今になって気づいたけど。そこではうっかり「旗のマーク」って言っちゃったけど、今になってよくよく見てみたら、コレもう旗のマークじゃないな…。旗だったのは随分昔だろうよ…。本来、何のマークと言うべきだったのか…。窓? でもコレ、窓に見える?
*2: まあ、Excelのプログラム部分・バイナリがメインメモリ上のキャッシュに乗って次回のExcel起動はそんなに待たされないだろうからこれでも体感上は問題無いやろ、みたいなことを考えながら伝えたわけだけど…。
*3: という話を見かけた記憶があるのだけど、ググっても出てこない…。自分の偽記憶なんだろうか…。でもまあ、そういう話があったということにしておいたほうが説明しやすいので、裏は全然取れてないけどこのまま話を進めてみる。

2022/09/29(木) [n年前の日記]

#1 [python] Skyboxについて調べてる

SkyboxというかCubemapというかそのあたりについて少し調べてるところ。

Skyboxというのはゲーム関係の3DCGで使われることが多い手法、と言ってしまっていいのかな。カメラを取り囲むように箱を設置して、その箱に遠景のテクスチャを貼り付けることで、カメラの角度が変わった時に遠景が自然な見た目になる頓智の一種、という説明で合っているだろうか。

Skybox用のテクスチャとしては6枚の画像が必要になるけれど、一般的にそういう状態で遠景テクスチャが公開配布されていることは少ない。ただ、 _IBL(Image Based Lighting) 用に、 _HDRI(High Dynamic Range Image)_正距円筒図法 _(Equirectangular projection) で公開されていることは多いので、そのHDRIをSkybox用に変換する方法があちこちで解説されている模様。

このあたり、昔も調べてたな…。

_mieki256's diary - three.jsでSkybox
_mieki256's diary - 360度パノラマ画像を作成する際に便利なツールについてメモ

2022/09/30(金) [n年前の日記]

#1 [blender] blender勉強中

blender 2.93.9 x64 を使って、Skyboxとして利用できるテクスチャを作れないものかと試行錯誤。

Blendpeaksという無料のプラグインを使えば山っぽいモデルを生成できると知って、導入してアレコレ試してた。雪山っぽい感じの山ならたしかにイケそう。

フォグをかけるあたりで悩んでしまった。レイヤー設定にミストという項目があったり、ワールド設定のボリュームでボリューム散乱を選んだ上で、コンポジットノードでフォグをかけた感じにできるらしいけど、試してみてもしっくりこない…。

空と雲をどうやって再現すればいいのか、川 or 湖をどう表現するかも悩ましい…。

以上、30 日分です。

過去ログ表示

Prev - 2022/09 - Next
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project