床井研究室

フランス人帰る

こだわりのフランス人は、交換留学の期間を終えて、フランスに帰っていきました。外見はナイーブな感じでしたが、気さくで付き合いもよく、ほかの学生さんにはとてもいい刺激になったみたいです。彼は日本語がまったく話せない状態で日本に留学に来たわけで、そう思えば度胸のある人だったのかもしれません。ただ、そのために周りの学生さんや私も必然的に彼に合わせてコミュニケーションをとらざるを得ませんでした。みんなかなり四苦八苦していたのですが、それが結果的にいい「異文化コミュニケーション」になったと思います。彼は「日本の大学院に進学したい」と言い残して帰っていきました。

Point Sprite

そのフランス人に与えた課題の中で、彼は最初ビルボードにテクスチャを貼るという実装をしていたので、Point Sprite を使ったらもう少し高速化できるんじゃないかとアドバイスしました(この件に関しては pierrot さんアドバイスありがとうございました)。その後、卒業研究の学生さんのうちの一人が同じ大きさの球を大量に描いていることに気がついたので、その人にもそのうち Point Sprite を使うようアドバイスしようと思っていました。しかし、結局卒論の締め切りの方が先に来てしまいました。まあでも、その人は大学院に進学するので、その人の今後のためにもここにメモしておこうかと思います。

ところで Point Sprite とは、GL_POINTS で描かれる「点」にテクスチャをマッピングする機能です。この機能は、OpenGL 2.0 で OpenGL の基本機能に組み込まれました。GL_POINTS で描かれる点は、デフォルトでは1画素の大きさですが、glPointSize() を使って任意の大きさの正方形にすることができます(アンチエリアシングをかけると丸くなります)。ただ、これにそのままテクスチャを貼ろうとしても、テクスチャ内の1画素の色がこの正方形の全面に適用されてしまい、画像としては貼り付けられません。Point Sprite を有効にすると、この正方形に期待通りにテクスチャが貼り付けられます。

したがってこの機能は、パーティクルのようにポリゴンを使うと効率が悪いものを描画する際によく用いられます。また、最近では点の集合で表現された形状のレンダリング (Point Based Rendering) に盛んに用いられています1

点で図形を描く

それではまず、点で図形を描くプログラムを用意しましょう。ここでは点を噴水のように吹き上げるプログラムを雛形にします。なお、手元の Mac mini (Panther) では Point Sprite が使えなかったので、今回も Mac OS X 版は用意できませんでした。すみません。Tiger がまともに動く iMac か iBook 欲しいなぁ。

このプログラムをコンパイルして実行すると、こういう噴水のようなアニメーションが表示されます。マウスをドラッグすれば、視点を移動できます。

点の噴出

ただ、これだと点が小さすぎて見えにくいので、glPointSize() を使って点の大きさを指定します。

/*
** シーンの描画
*/
static void scene()
{
  /* パーティクルを生成する */
  if (particles.size() >= MAX_PARTICLES) {
    particles.pop_back();
  }
  particle p;
  particles.push_front(p);

  /* パーティクルを描く */
  glColor3d(1.0, 1.0, 1.0);
  glPointSize(20.0);
  glBegin(GL_POINTS);
  for (std::deque<particle>::iterator ip = particles.begin();
       ip != particles.end(); ++ip) {
    glVertex3dv(ip->getPosition());
    ip->update();
  }
  glEnd();
}

点の大きさを指定する

点にテクスチャをマッピングする

それでは、この点にテクスチャをマッピングしてみましょう。マッピングするテクスチャには、次のような球をレンダリングした画像を用います。

点にマッピングするテクスチャ

まず、プログラムの初期化部分で、テクスチャマッピングの設定を行います。今回はファイルの入出力に iostream / fstream を使いますので、プログラムの先頭部分に以下の2行を追加します。

/* 標準ライブラリ */
#include <stdlib.h>
#include <math.h>
#include <iostream>
#include <fstream>

次に、テクスチャとして使う画像ファイルを定義します。

/*
** テクスチャ
*/
#define TEXWIDTH  64                       /* テクスチャの幅    */
#define TEXHEIGHT 64                       /* テクスチャの高さ   */
static const char texture[] = "ball.raw";  /* テクスチャファイル名 */

初期化の関数 init() で画像ファイルを読み込み、テクスチャとしてマッピングします。このテクスチャには下地の色の影響を受けないよう、テクスチャ環境のモード GL_TEXTURE_ENV_MODEGL_REPLACE を指定しておきます。

/*
** 初期化
*/
static void init()
{
  /* テクスチャの読み込みに使う配列 */
  GLubyte image[TEXHEIGHT][TEXWIDTH][4];
  
  /* テクスチャ画像の読み込み */
  std::ifstream file(texture, std::ios::in | std::ios::binary);
  if (file) {
    file.read((char *)image, sizeof image);
    file.close();
  }
  else {
    std::cerr << texture << " が開けません" << std::endl;
  }
  
  /* テクスチャ画像はワード単位に詰め込まれている */
  glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
  
  /* テクスチャを拡大・縮小する方法の指定 */
  glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  
  /* テクスチャの繰り返し方法の指定 */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
  
  /* テクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

  /* テクスチャの割り当て */
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
               GL_RGBA, GL_UNSIGNED_BYTE, image);

  /* 初期設定 */
  glClearColor(0.3f, 0.3f, 1.0f, 0.0f);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);

  /* 陰影付けを無効にする */
  glDisable(GL_LIGHTING);

  /* 地面の高さ */
  particle::height(HEIGHT);
}

そしてシーンを描画する関数 scene() で、テクスチャマッピングを有効にして点を描画します。

/*
** シーンの描画
*/
static void scene()
{
  /* パーティクルを生成する */
  if (particles.size() >= MAX_PARTICLES) {
    particles.pop_back();
  }
  particle p;
  particles.push_front(p);

  /* テクスチャマッピング開始 */
  glEnable(GL_TEXTURE_2D);

  /* パーティクルを描く */
  glColor3d(1.0, 1.0, 1.0);
  glPointSize(20.0);
  glBegin(GL_POINTS);
  for (std::deque<particle>::iterator ip = particles.begin();
       ip != particles.end(); ++ip) {
    glVertex3dv(ip->getPosition());
    ip->update();
  }
  glEnd();

  /* テクスチャマッピング終了 */
  glDisable(GL_TEXTURE_2D);
}

このプログラムを実行すると、点がテクスチャ画像内の1画素の色で塗りつぶされてしまいます。

テクスチャがマッピングされない

Point Sprite を有効にする

点に画像全体を貼り付けるには、Point Sprite を有効にする必要があります。初期化関数 init() で、テクスチャ環境に GL_POINT_SPRITE を指定して、GL_COORD_REPLACEGL_TRUE に設定します。

/*
** 初期化
*/
static void init()
{
  ...

  /* テクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
  glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE);

そしてシーンを描画する関数 scene() で、GL_POINT_SPRITE を有効にして点を描画します。

/*
** シーンの描画
*/
static void scene()
{
  /* パーティクルを生成する */
  if (particles.size() >= MAX_PARTICLES) {
    particles.pop_back();
  }
  particle p;
  particles.push_front(p);

  /* テクスチャマッピング開始 */
  glEnable(GL_TEXTURE_2D);

  /* Point Sprite を有効にする */
  glEnable(GL_POINT_SPRITE);

  /* パーティクルを描く */
  glColor3d(1.0, 1.0, 1.0);
  glPointSize(20.0);
  glBegin(GL_POINTS);
  for (std::deque<particle>::iterator ip = particles.begin();
       ip != particles.end(); ++ip) {
    glVertex3dv(ip->getPosition());
    ip->update();
  }
  glEnd();

  /* Point Sprite を無効にする */
  glDisable(GL_POINT_SPRITE);

  /* テクスチャマッピング終了 */
  glDisable(GL_TEXTURE_2D);
}

これで点にテクスチャが貼り付けられるようになります。

Point Sprite を有効にした結果

テクスチャをアルファテストで丸くくりぬく

このテクスチャの画像には、テクスチャを丸くくりぬくようにアルファ値を設定しているので、アルファテストを使って透明部分を描画しないようにします。まず初期化関数 init() で、アルファテストの判別関数を設定します。

/*
** 初期化
*/
static void init()
{
  ...

  /* 初期設定 */
  glClearColor(0.3f, 0.3f, 1.0f, 0.0f);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);

  /* 陰影付けを無効にする */
  glDisable(GL_LIGHTING);

  /* アルファテストの判別関数 */
  glAlphaFunc(GL_GREATER, 0.5);

そしてシーンを描画する関数 scene() で、アルファテストを有効にします。

/*
** シーンの描画
*/
static void scene()
{
  /* パーティクルを生成する */
  if (particles.size() >= MAX_PARTICLES) {
    particles.pop_back();
  }
  particle p;
  particles.push_front(p);

  /* テクスチャマッピング開始 */
  glEnable(GL_TEXTURE_2D);

  /* Point Sprite を有効にする */
  glEnable(GL_POINT_SPRITE);

  /* アルファテストを有効にする */
  glEnable(GL_ALPHA_TEST);

  /* パーティクルを描く */
  glColor3d(1.0, 1.0, 1.0);
  glPointSize(20.0);
  glBegin(GL_POINTS);
  for (std::deque<particle>::iterator ip = particles.begin();
       ip != particles.end(); ++ip) {
    glVertex3dv(ip->getPosition());
    ip->update();
  }
  glEnd();

  /* アルファテストを無効にする */
  glDisable(GL_ALPHA_TEST);

  /* Point Sprite を無効にする */
  glDisable(GL_POINT_SPRITE);

  /* テクスチャマッピング終了 */
  glDisable(GL_TEXTURE_2D);
}

これで点を1個描くだけで球を描くようになりました。

アルファテストを有効にした結果

ウィンドウの大きさに合わせて点の大きさを変更する

現状では glPointSize() に定数を指定しているので、点の大きさは開いたウィンドウの大きさに関係なく一定になっています。点を形を持った図形として取り扱うなら、これはちょっと不自然です。そこで、ウィンドウの大きさに合わせて点の大きさを変更することにします。まず、点の大きさを決める変数 psize を用意します。

/*
** パーティクル
*/
#include "particle.h"
#include <deque>
#define MAX_PARTICLES 800
static std::deque<particle> particles;
static GLfloat psize;

この変数は開いたウィンドウの幅に比例するようにします。

static void resize(int w, int h)
{
  /* 点の大きさを設定する */
  psize = w * 0.1f;

  /* トラックボールする範囲 */
  trackballRegion(w, h);

glPointSize() の引数にこの変数を用います。

...
/*
** シーンの描画
*/
static void scene()
{
  ...

  /* パーティクルを描く */
  glColor3d(1.0, 1.0, 1.0);
  glPointSize(psize);
  glBegin(GL_POINTS);

点の大きさをウィンドウの大きさに合わせた結果

視点からの距離に応じて点の大きさを変更する

このプログラムは透視変換をおこなっているので、点であっても大きさを持っていれば、遠くの点は小さく、近くの点は大きく描かれる必要があります。そこで、点の大きさを視点からの距離に反比例するようにします。まず、次のような3要素の配列変数 distance を用意し、0 番目と 1 番目の要素に 0、2 番目の要素に 1 を入れておきます。

...

/*
** パーティクル
*/
#include "particle.h"
#include <deque>
#define MAX_PARTICLES 800
static std::deque<particle> particles;
static GLfloat psize;
static GLfloat distance[] = { 0.0f, 0.0f, 1.0f };

そして glPointParameterfv() を使って GL_POINT_DISTANCE_ATTENUATION にこの変数を指定します。ただし、この関数は OpenGL 1.4 以降の機能のため、標準では OpenGL 1.1 までしかサポートしていない Windows には用意されていません。そのため、これをビデオカードのメーカーが提供するドライバに組み込まれている機能を使えるようにします。まず、glPointParameterfv という関数のポインタ変数の宣言を追加します。このデータ型の PFNGLPOINTPARAMETERFVPROC は、glext.h で定義されています。

/* OpenGL */
#if defined(__APPLE__)
#  define GL_SILENCE_DEPRECATION
#  include <GLUT/glut.h>
#  include <OpenGL/glext.h>
#else
#  if defined(_MSC_VER)
//#    pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#    define _USE_MATH_DEFINES
#    define _CRT_SECURE_NO_WARNINGS
#  else
#    define GL_GLEXT_PROTOTYPES
#  endif
#  include <GL/glut.h>
#  include <GL/glext.h>
PFNGLPOINTPARAMETERFVPROC glPointParameterfv;
#endif

そして wglGetProcAddress() を使ってドライバ上にある関数 glPointParameterfv() の実体のポインタを取得し、変数 glPointParameterfv に代入します。これは Windows 特有の仕様なので、この処理は Windows の時だけ組み込まれるようにしておきます。そして glPointParameterfv() により GL_POINT_DISTANCE_ATTENUATION に先ほどの配列変数 distance を設定します。

/*
** 初期化
*/
static void init()
{
  ...

  /* 初期設定 */
  glClearColor(0.3f, 0.3f, 1.0f, 0.0f);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);

  /* 陰影付けを無効にする */
  glDisable(GL_LIGHTING);

  /* アルファテストの判別関数 */
  glAlphaFunc(GL_GREATER, 0.5);

#if defined(WIN32)
  glPointParameterfv =
    (PFNGLPOINTPARAMETERFVPROC)wglGetProcAddress("glPointParameterfv");
#endif

  /* 距離に対する点の大きさの制御 */
  glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, distance);

点の大きさを視点からの距離に反比例させた結果

実際に描かれる点の大きさ $pointSize$ は、次式により求められます。$size$ は glPointSize() で指定した点の大きさで、$d$ は視点座標系における視点から点までの距離です。$\mathrm{clampsize}()$ は点の大きさ $pointSize$ の上限値でクランプしますが、その上限値は実装依存です。

\[pointSize=\mathrm{clampSize}\left(size * \sqrt{\frac{1}{a + b * d + c * d^2}}\right)\]

GL_POINT_DISTANCE_ATTENUATION に指定した配列変数の各要素は、この式の $a$, $b$, $c$ に相当します。これに平方根がかかっているので、$a$, $b$ を 0 に、$c$ に 1 を設定すれば、点の大きさが視点からの距離 $d$ に反比例するようになります。

  1. 現在の Point Based Rendering では、Point Sprite を使わず(すなわち、ラスタライザを介さず)、Compute Shader で描く方法が採られるようです (2026 年 8 月 17 日追記)