1. αブレンド
αブレンドというのは、ポリゴンや線・点に透明度を持たせて描画するときに使う手法です。また、設定を変えることで、透明度だけでなく、発光体や映り込みのようなものを描画することもできます。
テクスチャに赤緑青以外にαチャンネルを持たせれば透過テクスチャを表現することができます。透過テクスチャを使うことによりテクセル単位でα値を指定することができるため表現の幅が広がります。
αブレンドはピクセル単位で行われます。例えば画面に三角形を描いた場合、その三角形が描画される全ての点に対してαブレンドの計算が行われます。当然 計算時間がかかりますから、むやみに多くのポリゴンに対して使いすぎると描画が遅くなってしまいます。
またαブレンドを使う場合は描画順序が重要です。奥の物が手前の物を透けて見えないといけないので、奥から手前へ向かって描画します。
ところが、これだと全てのポリゴンを奥から手前にソートしてから描画しなければならず、しかも後ろに隠れてしまうような部分までわざわざ描画しなければならないので、時間がかかります。


そこで、不透明なものと透明なものを分けて描画することにしましょう。
まず最初に不透明なポリゴンの描画を行います。このときは描画順序はあまり気にしませんが、描画量の削減のためには手前から奥へ向かって描画するとよいでしょう (深度バッファのところで説明します)。不透明な物体の場合はポリゴン単位でなく物体単位でソートするほうが効率的です。
次にαブレンドを用いた描画を行います。このときは奥から手前へ向かって描画します。αブレンドを使う場合はポリゴン単位でソートしたほうが正確ですが、物体単位でソートしても目立たない場合はこちらでもOKです。
これを間違えて、透明度のあるものとないものを ごちゃ混ぜに描画したり、透明度のあるものを手前から描画したりすると変な感じの絵になるので注意が必要です。
OpenGLでαブレンドを設定する関数には、次のようなものがあります。
|
それではまず、αブレンドの式を書いておきましょう。
| R(d') = min( k(R), R(s)s(R)+R(d)d(R) ) |
| G(d') = min( k(G), G(s)s(G)+G(d)d(G) ) |
| B(d') = min( k(B), B(s)s(B)+B(d)d(B) ) |
| A(d') = min( k(A), A(s)s(A)+A(d)d(A) ) |
なんのこっちゃさっぱりですね。筆者もなんのこっちゃでした。それぞれの記号の意味は
- c(d'):結果の色成分。ポリゴンを描いたときに出力される色です。以下、成分が全部cで描いてありますが、これは上の式のR,G,B,Aに対応していて、Rは赤、Gは緑、Bは青、Aはαのことです。
- k(c):色成分の上限値。これは各成分に使っているビット数によって決まります。例えば5ビットだったらk(c)=31、8ビットだったらk(c)=255です。
- c(s):書き込もうとしている色成分。つまり、新たに描こうとしている三角形の色です。テクスチャを使っている場合は、テクスチャの色c(t)とディフューズc(C)の色を掛けた値になります。
- c(d):書き込まれるピクセルの元の色成分。つまり、既に何か書かれている部分の色です。
- s(c):色成分のソース係数。
- d(c):色成分のデスティネーション係数。
この中で、設定できるのは ソース係数s(c) と デスティネーション係数d(c) だけです。αブレンディングではこの2つの係数を設定することでいろんな効果を作り出します。
で、上の式を順に追って説明すると、
- 描画されるピクセルの元の値c(d)と、新たに書き込む値c(s)を求める。
- c(d)にはデスティネーション係数d(c)をかけ、c(s)にはソース係数s(c)をかける。
- 求めた2つの値を足す。
- 上限値k(c)を上回っていたら上限値に直す。(こういうのを「飽和」といいます。)
- 出てきた値c(d')をピクセルに書き込む。

それでは、関数の使い方に移りましょう。2つの係数を設定するには
| glBlendFunc(ソース係数,デスティネーション係数) |
|

最も一般的な使い方は
| glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ) |
| s(c) = ( A(s), A(s), A(s), A(s) ) |
| d(c) = ( 1-A(s), 1-A(s), 1-A(s), 1-A(s) ) |
まず、新たに書き込む値のうち、α値A(s) を求めます。この値を a とすると、結果の色は
| c(d') = (1-a)*c(d) + a*c(s) |
- a が 0 のときは 1-a は 1 になるので、元の色c(d) が結果の色
- a が 0.5 のときは、元の色と新しい色の平均の色が結果の色
- a が 1 のときは、新たに書き込んだ色c(s) が結果の色

さて、他にもパラメータはいろいろありますねー。筆者がよく使うものに、こんなのがあります。
| glBlendFunc( GL_SRC_ALPHA, GL_ONE ) |
| s(c) = ( A(s), A(s), A(s), A(s) ) |
| d(c) = (1,1,1,1) |
| c(d') = c(d) + a*c(s) |
- a が 0 のときは、元の色c(d) が結果の色
- a が 1 のときは、元の色と新しい色の和c(d)+c(s) が結果の色 (ただし、結果が1以上のときは1)
ここで重要なのは、c(d) に何もかかっていないので、a の値がどうであれ結果の色は元の色と同じかそれよりも明るくなるということです。α値を変化させれば発光の度合いが変化します。また、色値を変化させれば発光体の色が変化します。

他にも、こんなのがあります。
| glBlendFunc( GL_ZERO, GL_ONE_MINUS_SRC_COLOR ) |
| c(s) = (0,0,0,0) |
| c(d) = ( 1-R(s), 1-G(s), 1-B(s), 1-A(s) ) |
| c(d') = (1-c(s))*c(d) |
- c(s) が 0 のときは 1-c(s) は 1 になるので、元の色c(d) が結果の色
- c(s) が 1 のときは 1-c(s) は 0 になるので、結果の色は黒
ところで、GL_ONE_MINUS_SRC_COLOR の代わりに GL_SRC_COLOR を使えばわざわざテクスチャや色値を反転させなくてもいいじゃん、と思うかもしれません。しかし、その場合だと、
| c(d') = (c(s)/k(c))*c(d) |

このほかにも面白い組み合わせがあると思います。色々試してみてください。
2. 深度バッファ
深度バッファは、画面上の各ピクセルの奥行きの値を保存するメモリ領域のことです。深度バッファを使うと、ポリゴンを事前のソート無しに画面に描画しても正しい奥行き表示を得ることができます。また、互いに交差するようなポリゴンでも正しく表示することができます。
深度バッファテストは、ピクセル単位で処理されます。2つの深度値を比較して描くかどうかをテストしているだけなので、それほど時間はかかりません。むしろ実際に描画を行うほうが何倍も時間がかかります。このことを利用して、ポリゴンを手前から奥へ描画していけば、奥の隠れているポリゴンは深度バッファテストをパスしないので、それだけ高速に描画することができます。
OpenGLで深度バッファを設定する関数には、次のようなものがあります。
|
また、レンダリング前の初期化でglClearを使うときに、
| glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) |
3. サンプルプログラム
αブレンドのサンプルプログラムを作ってみました。かっこよさげなダメプログラムです。相変わらず長いです。もっとシンプルに生きたいです。
ところで、プログラム中にじわじわとベクトルっぽいコードが増えてきました。というわけで、次回は手抜きで、筆者がテキトーに作ったベクトルクラスを鑑賞する会です。

ソース 4.1
#include <stdlib.h>
#include <GL/glut.h>
#include <stdio.h>
#include <math.h>
#define for if(0);else for
const double PI=3.14159265358979;
const double D2R=PI/180;
//マウスドラッグ用
int tmp_mouse_x=-1000,tmp_mouse_y=0;
/////////////////////////////
// 回転変換
// x軸まわりの回転
void rotatex(float &x,float &y,float &z,float t)
{
float s=sin(t),c=cos(t);
float ay=y*c-z*s;
float az=y*s+z*c;
y=ay;z=az;
}
// y軸まわりの回転
void rotatey(float &x,float &y,float &z,float t)
{
float s=sin(t),c=cos(t);
float az=z*c-x*s;
float ax=z*s+x*c;
z=az;x=ax;
}
// z軸まわりの回転
void rotatez(float &x,float &y,float &z,float t)
{
float s=sin(t),c=cos(t);
float ax=x*c-y*s;
float ay=x*s+y*c;
x=ax;y=ay;
}
/////////////////////////////
// ベクトル演算
// 外積
void exprod(float p1[],float p2[],float r[])
{
r[0]=p1[1]*p2[2]-p2[1]*p1[2];
r[1]=p1[2]*p2[0]-p2[2]*p1[0];
r[2]=p1[0]*p2[1]-p2[0]*p1[1];
}
// 内積
float dotprod(float p1[],float p2[])
{
return p1[0]*p2[0]+p1[1]*p2[1]+p1[2]*p2[2];
}
// 正規化
void normalize(float p[])
{
float d=sqrt(p[0]*p[0]+p[1]*p[1]+p[2]*p[2]);
p[0]/=d;p[1]/=d;p[2]/=d;
}
//////////////////////////
// カメラクラス
//
class Camera
{
private:
double theta; //方位角(どっちから見るか)
double alpha; //偏角(上から見るか横から見るか)
double dist; //カメラと原点との距離
double px,py,pz; //カメラ位置
public:
Camera()
{
theta=0;
alpha=1.9;
dist=15;
}
//カメラを回転させる
void Move(int dx,int dy)
{
theta-=dx*0.005;
alpha+=dy*0.005;
Reflesh();
}
double GetAlpha(){return alpha;}
double GetTheta(){return theta;}
void GetPosition(double &x,double &y,double &z)
{
x=px;y=py;z=pz;
}
//カメラの設定をglに適用する
void Reflesh()
{
if(alpha>3.14)alpha=3.14;
if(alpha<1.58)alpha=1.58;
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
px=dist*sin(theta)*sin(alpha);
py=dist*cos(theta)*sin(alpha);
pz=dist*cos(alpha);
gluLookAt(
px, py, pz, // 視点
0.0, 0.0, 0.0, // 注視位置
0.0, 0.0, -1.0 // 頭の上の方向
);
}
};
Camera camera;
/////////////////////////////
// マウス入力
//
void mouse(int button, int state, int x, int y)
{
if(button==GLUT_LEFT_BUTTON && state==GLUT_DOWN)
{
}
if(button==GLUT_RIGHT_BUTTON)
{
if(state==GLUT_DOWN)//右ボタンを押した
{
tmp_mouse_x=x;tmp_mouse_y=y;
}
else//右ボタンを離した
{
camera.Move(x-tmp_mouse_x,y-tmp_mouse_y);
tmp_mouse_x=-1000;
}
}
}
void mousemove(int x,int y)
{
if(tmp_mouse_x!=-1000)
{
camera.Move(x-tmp_mouse_x,y-tmp_mouse_y);
tmp_mouse_x=x;tmp_mouse_y=y;
}
}
/////////////////////////////////
// アニメーション
//
void animate()
{
//再描画
glutPostRedisplay();
}
/////////////////////////
// 描画
//
// ボックスを描画する
void renderBox()
{
glBegin(GL_QUAD_STRIP);
glVertex3f(1,1,-1);
glVertex3f(-1,1,-1);
glVertex3f(1,1,1);
glVertex3f(-1,1,1);
glVertex3f(1,-1,1);
glVertex3f(-1,-1,1);
glVertex3f(1,-1,-1);
glVertex3f(-1,-1,-1);
glVertex3f(1,1,-1);
glVertex3f(-1,1,-1);
glEnd();
glBegin(GL_QUADS);
glVertex3f(1,1,-1);
glVertex3f(1,1,1);
glVertex3f(1,-1,1);
glVertex3f(1,-1,-1);
glVertex3f(-1,1,-1);
glVertex3f(-1,-1,-1);
glVertex3f(-1,-1,1);
glVertex3f(-1,1,1);
glEnd();
}
//グローボックス
void renderGlowBox(double tt)
{
glEnable(GL_BLEND);
glPushMatrix();
glDepthMask(0);
glBlendFunc(GL_SRC_ALPHA,GL_ONE);//加算ブレンド
glRotatef(tt,1,0,0);
glRotatef(tt*0.71,0,1,0);
double aa=sin(tt*0.04);
aa*=aa*0.03;
aa+=1.02;
for(int i=0;i<10;i++)
{
glColor4f(0.6,0.4,0.1,0.3-i*0.03);
renderBox();
glScalef(aa,aa,aa);
}
glPopMatrix();
}
double irid2(double t)
{
if(t*6<1)return t*6;
if(t*6<3)return 1;
if(t*6<4)return 4-t*6;
return 0;
}
void irid(double &r,double &g,double &b,double t)
{
g=irid2(fmod(t,1));
b=irid2(fmod(t+1/3.0,1));
r=irid2(fmod(t+2/3.0,1));
}
//グローボックスの発光体
void renderGlow(double tt)
{
glEnable(GL_BLEND);
glPushMatrix();
glDepthMask(0);
glBlendFunc(GL_SRC_ALPHA,GL_ONE);//加算ブレンド
glDisable(GL_DEPTH_TEST);
glPushMatrix();
glLoadIdentity();
gluLookAt(
0, 0, -15, // 視点
0.0, 0.0, 0.0, // 注視位置
0.0, 1.0, 0.0 // 頭の上の方向
);
double cx,cy,cz;
camera.GetPosition(cx,cy,cz);
float c[]={cx,cy,cz};
normalize(c);
double dc=1,cc=fabs(c[0]);
double rr=0,gg=0,bb=0;
glBegin(GL_TRIANGLE_FAN);
if(cc>0.85)
{
dc=(1-cc)/(1-0.85);
}
if(c[0]>0.90)
{
double a=(cc-0.90)/(1-0.90);
irid(rr,gg,bb,a*3.6);
rr+=a*0.5;gg+=a*0.5;bb+=a*0.5;
rr*=a;gg*=a;bb*=a;
}
if(dc>1)dc=1;
glColor4f(0.1*dc+rr,0.6*dc+gg,1*dc+bb,1);
glVertex3f(0,0,0);
glColor4f(0.1*dc+rr,0.6*dc+gg,1*dc+bb,0);
double rd=2+sin(tt*0.037)*0.3;
for(int i=0;i<=6;i++)
{
double a=2*PI*i/6.0;
glVertex3f(cos(a)*rd,-sin(a)*rd,0);
}
glEnd();
glPopMatrix();
}
//普通ボックス
void renderNormalBox(double tt)
{
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDepthMask(1);
glPushMatrix();
glTranslatef(-4,0,0);
glRotatef(tt,1,0,0);
glRotatef(tt*0.71,0,1,0);
static const float v[]={
1,1,1,
-1,1,1,
1,-1,1,
-1,-1,1,
1,1,-1,
-1,1,-1,
1,-1,-1,
-1,-1,-1,
};
static const int idx[]={
4,5,1,0,
0,1,3,2,
2,3,7,6,
6,7,5,4,
0,2,6,4,
5,7,3,1,
};
glBegin(GL_QUADS);
for(int i=0;i<6;i++)
{
//面の法線nを求める
float n[3];
float a[3],b[3];
int k=i*4;
a[0]=v[idx[k+2]*3]-v[idx[k+1]*3];
a[1]=v[idx[k+2]*3+1]-v[idx[k+1]*3+1];
a[2]=v[idx[k+2]*3+2]-v[idx[k+1]*3+2];
b[0]=v[idx[k]*3]-v[idx[k+1]*3];
b[1]=v[idx[k]*3+1]-v[idx[k+1]*3+1];
b[2]=v[idx[k]*3+2]-v[idx[k+1]*3+2];
exprod(a,b,n);
normalize(n);
rotatey(n[0],n[1],n[2],tt*0.71*PI/180);
rotatex(n[0],n[1],n[2],tt*PI/180);
//法線から色を作る
float c=n[0];
c+=0.2;
if(c<0)c=0;
glColor3f(c*0.8+0.1,c*0.8+0.2,c*0.8+0.3);
for(int j=0;j<4;j++)
{
glVertex3f(v[idx[k+j]*3],
v[idx[k+j]*3+1],
v[idx[k+j]*3+2]);
}
}
glEnd();
glPopMatrix();
}
//透明ボックス
void renderTransparentBox(double tt)
{
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glDepthMask(0);
//標準ブレンド(加重平均)
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
static const float v[]={
1,1,1,
-1,1,1,
1,-1,1,
-1,-1,1,
1,1,-1,
-1,1,-1,
1,-1,-1,
-1,-1,-1,
};
static const int idx[]={
4,5,1,0,
0,1,3,2,
2,3,7,6,
6,7,5,4,
0,2,6,4,
5,7,3,1,
};
glPushMatrix();
glTranslatef(4,0,0);
glRotatef(tt,1,0,0);
glRotatef(tt*0.71,0,1,0);
glBegin(GL_QUADS);
for(int i=0;i<6;i++)
{
//面の法線nを求める
float n[3];
float a[3],b[3];
int k=i*4;
a[0]=v[idx[k+2]*3]-v[idx[k+1]*3];
a[1]=v[idx[k+2]*3+1]-v[idx[k+1]*3+1];
a[2]=v[idx[k+2]*3+2]-v[idx[k+1]*3+2];
b[0]=v[idx[k]*3]-v[idx[k+1]*3];
b[1]=v[idx[k]*3+1]-v[idx[k+1]*3+1];
b[2]=v[idx[k]*3+2]-v[idx[k+1]*3+2];
exprod(a,b,n);
normalize(n);
rotatey(n[0],n[1],n[2],tt*0.71*PI/180);
rotatex(n[0],n[1],n[2],tt*PI/180);
//法線から色を作る
float c=-n[0];
c+=0.2;
if(c<0)c=0;
glColor4f(c*0.8+0.1,c*0.8+0.2,c*0.8+0.3,0.5);
for(int j=0;j<4;j++)
{
glVertex3f(v[idx[k+j]*3],
v[idx[k+j]*3+1],
v[idx[k+j]*3+2]);
}
}
glEnd();
glPopMatrix();
}
void disp()
{
// 画面を消去
glDepthMask(1);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
camera.Reflesh();
static int t=0;
t++;
double tt=t*0.4;
double cx,cy,cz;
camera.GetPosition(cx,cy,cz);
//不透明なボックスを描画
renderNormalBox(tt);
//αブレンドは遠いもの順に描画
//この場合はカメラの位置によってどっちが遠いかすぐに分かる。
if(cx<0)
{
//透明なボックスを描画
glCullFace(GL_FRONT);//まずは裏側
renderTransparentBox(tt);
glCullFace(GL_BACK);//次に表を描画
renderTransparentBox(tt);
//グローボックスを描画
renderGlowBox(tt);
renderGlow(tt);
}
else
{
renderGlowBox(tt);
glCullFace(GL_FRONT);
renderTransparentBox(tt);
glCullFace(GL_BACK);
renderTransparentBox(tt);
renderGlow(tt);
}
glutSwapBuffers();
}
/////////////////////////
// メイン
//
int main(int argc,char **argv)
{
// 初期化
glutInit(&argc,argv);
glutInitWindowPosition(100, 50);
glutInitWindowSize(640, 480);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutCreateWindow("3DCG Programming");
glutDisplayFunc(disp);
glutIdleFunc(animate);
glutMouseFunc(mouse);
glutMotionFunc(mousemove);
// 背景色
glClearColor(0.185f,0.199f,0.200f,0.0f);
// 奥行きテスト
glEnable(GL_DEPTH_TEST);
//αブレンド
glEnable(GL_BLEND);
// 裏面を表示しない
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
// カメラの設定
// 投影法の設定
glMatrixMode(GL_PROJECTION);
gluPerspective(
45, // 視野角
640.0/480.0, // アスペクト比
4, // 視角錘台の近いほうの距離
21 // 遠いほうの距離
);
// 座標の設定
camera.Reflesh();
// メインループ
glutMainLoop();
return 0;
}
次回
前回
いちばんはじめ