1 javaによるグラフィクス - lecture.ecc.u-tokyo.ac.jpyama/text/o_cover.pdf1 第1章...

90
1 1 Java によるグラフィクス 1.1 グラフィクス グラフィクス (graphics)(graph) すが, (コンピュータグラフィ クス) りつ んだ ディジタル画像: によって された ( ) する「 する「 によって して される 標本化 (discretization, sampling)を格 んだ によって するこ ピクセル (pixel)扱う 1 された ,ラスタ (raster) れる.ピクセル (0, 0) し,x き,y きに 量子化 (quantization)各ピクセル するこ ラスタ () ディスプレイ: ピクセル びによって されるディスプレイ (モニタ) ラスタディスプレイ CRT(Cathode-Ray Tube) あったが, ディスプレイ (LCD: Liquid-Crystal Display) ってきている. ラスタグラフィックス: ラスタディスプレイを したグラフィックス 解像度 (空間解像度)ピクセル ( × ) かさ ( / らかさ) iMac 1024 × 768 っている.こ 他, ころ,800 × 600, 1152 × 864, 1280 × 1024 いられる Frame クラス: ィンド クラス Frame クラス オブジェクト (インスタンス) OS ィンド システムを じて, スクリーン ィンド する 位クラス java.awt.Frame java.awt.Window java.awt.Container java.awt.Component java.lant.Object Canvas クラス: クラス Canvas クラス オブジェクト (インスタンス) 域を確 する に,

Upload: others

Post on 02-Apr-2020

3 views

Category:

Documents


0 download

TRANSCRIPT

1

第1章 Javaによるグラフィクス

1.1 グラフィクス

グラフィクス (graphics):図形や文字などの描画語源 (graph)的には線図形を描く手法を指すが,計算機的な用語 (コンピュータグラフィクス)では,塗りつぶしや画像なども含んだ描画全般を指す

ディジタル画像:離散値によって表現された画像画像面情報を離散化 (離散値表現化)する「標本化」と色情報を離散化する「量子化」によって離散値として表現される

標本化 (discretization, sampling):画像を格子状に並んだ細かな点によって表現すること

ピクセル (pixel):計算機の扱う画像の 1点標本化された画像の各点,ラスタ (raster)や画素とも呼ばれる.ピクセルの座標は,左上を原点 (0, 0) とし,x軸は右向き,y軸は下向きになる

量子化 (quantization):各ピクセルの色情報を離散値で表現すること

ラスタ (型)ディスプレイ:ピクセルの並びによって構成されるディスプレイ (モニタ)

従来はラスタディスプレイはCRT(Cathode-Ray Tube)が主体であったが,最近は液晶ディスプレイ (LCD: Liquid-Crystal Display)が中心になってきている.

ラスタグラフィックス:ラスタディスプレイを前提としたグラフィックス理論や技術

解像度 (空間解像度):ピクセルの数 (水平方向の数 × 垂直方向の数)

画面の細かさ (精密さ/滑らかさ)を表すもので,情報棟の iMacは 1024 × 768 の解像度を持っている.この他,現在のところ,800 × 600, 1152 × 864, 1280 × 1024 などの解像度が良く用いられる

Frameクラス:ウィンドウ生成のクラスFrameクラスのオブジェクト (インスタンス)は,OSのウィンドウシステムを通じて,スクリーン上にウィンドウを生成する上位クラスは,java.awt.Frame → java.awt.Window → java.awt.Container →java.awt.Component → java.lant.Object

Canvasクラス:描画領域のクラスCanvasクラスのオブジェクト (インスタンス)は,描画領域を確保するとともに,描

2 第 1章 Javaによるグラフィクス

図 1.1: ×線の表示

画のためのインタンスメソッド (paint, update)提供する.授業のプログラムでは,Frameオブジェクトによってウィンドウを生成し,そのウィンドウ内部に Canvas領域を確保して描画に利用する上位クラスは,java.awt.Canvas → java.awt.Component → java.lang.Object

paint 描画メソッド必要に応じてオーバライドするメソッドで,Canvas内の描画内容を規定する.引数として渡される Graphicsオブジェクトを用いて,描画命令を実現する

update 画像更新メソッド画面上で Canvasが隠された後に,再度描画し直す必要があるときなどに,画面更新要求に伴って自動的に呼び出されるメソッドデフォールト (標準)では,背景色で全体を塗りつぶした後に,paintメソッドを呼び出して描画を行なう

Graphicsクラス:描画情報および描画のクラスGraphicsクラスのオブジェクト (インスタンス)は,描画領域,描画のための色やフォントなどの情報を保持するともに,描画のためのインタンスメソッド提供する.Canvasは Graphicsオブジェクトを自動的に生成するので,それを用いて描画を行なう上位クラスは,java.awt.Graphics → java.lang.Object

例1:×線の表示 - Crossline.java

表示領域において対角線 (2本)と文字列を描画する.

import java.awt.*;

import java.awt.event.*;

public class CrossLine extends Canvas {

private static final int width = 400; // Canvasの幅

1.1. グラフィクス 3

private static final int height = 300; // Canvasの高さ

private CrossLine() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

setForeground(Color.black); // 描画色の設定 (黒)

Frame f = new Frame("CrossLine"); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

f.setVisible(true); // Frameの表示}

public void paint(Graphics g) {

g.drawLine(0, 0, width-1, height-1); // 直線: 左上→右下g.drawLine(0, height-1, width-1, 0); // 直線: 左下→右上g.drawString("Hello, Graphics.", width/2, height/2);

// 文字列: 中央付近}

public static void main(String[] args) {

new CrossLine(); // Canvasオブジェクトの作成}

}

例2:直線群の描画 - Lines.java

原点から放射状に直線を描画する.

import java.awt.*;

import java.awt.event.*;

public class Lines extends Canvas {

private static final int width = 600; // Canvasの幅private static final int height = 600; // Canvasの高さprotected static final int[][] points = // 直線群の終端点座標値

4 第 1章 Javaによるグラフィクス

600

600

(599,599)

(599,300)

(599,150)

(599,75)

(300,599)

(0,0)

図 1.2: 直線群の描画

{{10, 599}, {30, 599}, {75, 599}, {150, 599}, {300, 599}, {599, 599},

{599, 300}, {599, 150}, {599, 75}, {599, 30}, {599, 10}

};

protected Lines(String name) {

super(); // Canvasのコンストラクタ実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

setForeground(Color.black); // 描画色の設定 (黒)

Frame f = new Frame(name); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

f.setVisible(true); // Frameの表示}

public void paint(Graphics g) {

for (int i = 0; i < points.length; i++) {// 終端点数だけ直線描画g.drawLine(0, 0, points[i][0], points[i][1]);

} // 原点→終端点}

1.1. グラフィクス 5

public static void main(String[] args) {

new Lines("Lines"); // Canvasオブジェクトの作成}

}

例3:点による直線群の描画 - DotLines.java

デジタル画像はピクセルの集合によって表現される.したがって,直線も連続したピクセルによって構成されている.直線描画のメソッドではなく,ピクセルの描画メソッドを繰り返し利用することで,直線の描画を試みる

import java.awt.*;

public class DotLines extends Lines {

private DotLines(String name) {

super(name); // Linesのコンストラクタ実行}

public void paint(Graphics g) {

for (int i = 0; i < points.length; i++) {// 終端点数だけ直線描画if (points[i][0] >= points[i][1]) { // x >= y (横長の直線)

int n = points[i][0]; // 表示点の個数: n = x

double d = ((double)points[i][1])/n; // 1点ごとの差分for (int x = 0; x <= n; x++) { // 点描画 n回

g.fillRect(x, (int)(d*x+0.5), 1, 1); //点の描画}

}

else { // x < y (縦長の直線)

int n = points[i][1]; // 表示点の個数: n = y

double d = ((double)points[i][0])/n; // 1点ごとの差分for (int y = 0; y <= n; y++) { // 点描画 n回

g.fillRect((int)(d*y+0.5), y, 1, 1); //点の描画}

}

}

}

public static void main(String[] args) {

new DotLines("DotLines"); // Canvasオブジェクトの作成}

}

例 4:点による直線群の描画 - DDALines.java

6 第 1章 Javaによるグラフィクス

本来,ピクセルの格子は整数値で指定できる.高速な描画を実現するために,浮動小数点数を用いずに整数のみで描画ピクセルを算出することが望ましい.DDA(Digital

Differential Analyzer)によって,整数演算 (とシフト演算)のみで直線を描画する

import java.awt.*;

public class DDALines extends Lines {

private DDALines(String name) {

super(name); // Linesのコンストラクタ実行}

public void paint(Graphics g) {

for (int i = 0; i < points.length; i++) {// 終端点数だけ直線描画if (points[i][0] >= points[i][1]) { // x >= y (横長の直線)

int n = points[i][0]; // 表示点の個数: n = x

int dn = points[i][1] << 1; // 差分 * 2n

int dr = dn - (n << 1); // (差分-1) * 2n

int e = dn - n; // (初期ズレ-0.5) * 2n

for (int x = 0, y = 0; x <= n; x++) { // 点描画 n回g.fillRect(x, y, 1, 1); // 点の描画if (e > 0) { // ズレが正(yが1増える)

y++; e += dr; // ズレの加算 & 補正}

else { // ズレが負(yはそのまま)

e += dn; // ズレの加算}

}

}

else { // x < y (縦長の直線)

int n = points[i][1]; // 表示点の個数: n = y

int dn = points[i][0] << 1; // 差分 * 2n

int dr = dn - (n << 1); // (差分-1) * 2n

int e = dn - n; // (初期ズレ-0.5) * 2n

for (int x = 0, y = 0; y <= n; y++) { // 点描画 n回g.fillRect(x, y, 1, 1); // 点の描画if (e > 0) { // ズレが正(xが1増える)

x++; e += dr; // ズレの加算 & 補正}

else { // ズレが負(xはそのまま)

e += dn; // ズレの加算}

1.1. グラフィクス 7

x

y

r

θ(x , y )o o

図 1.3: 円の描画

}

}

}

}

public static void main(String[] args) {

new DDALines("DDALines"); // Canvasオブジェクトの作成}

}

例 5:円 (曲線)の描画 - Circle.java

もともとデジタル画像では,滑らかな曲線を描くことは不可能であり,曲線は細かな線分の集合 (よく細かくいえばピクセルの集合)として描画する.中心 (xo, yo),半径 r

の円は,次式で表せる.

x = r cos θ + xo, y = r sin θ + yo (θ ∈ [−π, π])

そこで,円上の頂点を線分列で結んで表示する.コマンド引数によって頂点の個数を指定できるので,頂点の個数を変更してみること

import java.awt.*;

import java.awt.event.*;

public class Circle extends Canvas {

private static final int width = 600; // Canvasの幅private static final int height = 600; // Canvasの高さprivate static final int centerX = 300; // 円中心の x座標private static final int centerY = 299; // 円中心の y座標private static final int radius = 250; // 円の半径protected int[][] points; // 円周上の点列

8 第 1章 Javaによるグラフィクス

protected Circle(String name, int numberOfPoints) {

super(); // Canvasのコンストラクタ実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

setForeground(Color.black); // 描画色の設定 (黒)

points = new int[numberOfPoints+1][]; // 円周上の点の配列: n+1

for (int i = 0; i <= numberOfPoints; i++) {// P_0 = p_n

double theta = 2.0 * Math.PI * i / numberOfPoints;

// 角度の計算: n等分points[i] = new int[2]; // 円周上の点の座標値points[i][0] = (int)(radius * Math.cos(theta)) + centerX;

points[i][1] = (int)(radius * Math.sin(theta)) + centerY;

}

Frame f = new Frame(name); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

f.setVisible(true); // Frameの表示}

public void paint(Graphics g) {

for (int i = 0; i < points.length - 1; i++) {// 点個数の直線描画g.drawLine(points[i][0], points[i][1], // 点 P_i → 点 P_i+1

points[i+1][0], points[i+1][1]);

}

}

public static void main(String[] args) {

if (args.length == 0) // コマンド引数の確認System.err.println("Usage: java Circle #");

else

new Circle("Circle", Integer.parseInt(args[0]));

// Canvasオブジェクトの作成

1.2. 色 9

}

}

1.2 色

光は波としての性質を持っており,各波長の光が異なった色として知覚されることは,プリズムや虹などの現象から明らかである.一方で,光の 3原色とか色の 3原色という言葉は,3

種類の光 (色)の混合によって様々な色が再現できることを意味している.

円錐体 (視)細胞 (cone):色を感知する視細胞視細胞には,弱い光に反応する棹状体 (rod)と,ある程度強い光に反応する円錐体 (cone)

がある.円錐体にはさらに 3種類あり,それぞれ光の波長に対する反応が異なる.通常,目で観察される光は幾つかの波長の光が集まったものであり,その成分に応じて 3

種類の円錐体がそれぞれ独立した反応を示す.3つの刺激値を脳が再構成して「色」として知覚される.

(条件)等色:等しい色として知覚される色およびその関係全く同じ組成 (周波数成分)を持つ光でなくても,円錐体の 3つの刺激値が等しくなれば,人間にとっては等しい色として知覚される.

表色系:色の表現方法色を 3つの値の組として表す「混色系」と色見本など具体的な色表を用いて表す「顕色系」がある

混色系: 3つの色成分の混合による色の表現法光の 3原色や色の 3原色は,混色系の一種とみなせる.

加法混色:光を原色として,3原色の組によって色を表す方法RGB(Red Green Blue)表現 (光の 3原色)は加法混色系の一種である.光の無い状態が「黒」であり,そこにRGBの各成分を加えていくことで色が構成される.すべての成分が最大 (通常は 1.0)になると,色は「白」になる.RGB表現では,主な色は次のように表現される.

色 Color ( R G B ) 成分赤 Red ( 1 0 0 )

緑 Green ( 0 1 0 )

青 Blue ( 0 0 1 )

シアン Cyan ( 0 1 1 )

マゼンタ Magenta ( 1 0 1 )

黄 Yellow ( 1 1 0 )

白 White ( 1 1 1 )

黒 Black ( 0 0 0 )

10 第 1章 Javaによるグラフィクス

x=300, y=225

x=386.6, y=375x=213.4, y=375

Red(1 0 0)

Green(0 1 0)

Blue(0 0 1)

Yellow(1 1 0)

Cyan(0 1 1)

Magenta(1 0 1)

White(1 1 1)

Black(0 0 0)

図 1.4: 光の 3原色 (RGB表現)

減法混色:フィルタを原色として,3原色の組によって色を表す方法CMY (Cyan Magenta Yellow)表現 (色の 3原色)は減法混色系の一種である.フィルタの無い状態が「白」であり,そこにCMY の各成分を混ぜる (光の成分は減ぜられる)

ことで色が構成される.すべての成分が最大 (通常は 1.0)になると,色は「黒」になる.CMY 表現では,主な色は次のように表現される.

色 Color ( C M Y ) 成分シアン Cyan ( 1 0 0 )

マゼンタ Magenta ( 0 1 0 )

黄 Yellow ( 0 0 1 )

赤 Red ( 0 1 1 )

緑 Green ( 1 0 1 )

青 Blue ( 1 1 0 )

黒 Black ( 1 1 1 )

白 White ( 0 0 0 )

RGB表現とCMY 表現の間には,以下の関係式が成り立つ.

( R G B ) = ( 1 1 1 ) − ( C M Y ) (1.1)

HSB(HSV )表現:H,S,Bの 3値によって色を表す方法H,S,Bは,それぞれ色相 (Hue),彩度 (Saturation),明度 (Brightness/Value)を表している.RGB表現とHSB表現の変換は 1つの式では表現できない (それほど難しくはない)が,Javaには変換のためのメソッドが用意されている

例1:光の 3原色図 1.4のように,画面上に 3つの円 (中心座標が (300,225),(213.4,375),(386.6,375),半

1.2. 色 11

径が 180)を考える.それぞれの円の内側に R = ( 1 0 0 ), G = ( 0 1 0 ), B =

( 0 0 1 ) の 3色を割り当て,円の重なった部分は単純に色を足し合わせる.すると,良く見かける「光の 3原色」の絵が作られる.

import java.awt.*;

import java.awt.event.*;

public class AdditiveColor extends Canvas {

protected static final int width = 600; // Canvasの幅protected static final int height = 600; // Canvasの高さprotected static final double radius2 = 180.0 * 180.0;

// 円の半径の 2乗protected double[][] centers = // 円の中心

{{300.0, 225.0}, {213.4, 375.0}, {386.6, 375.0}};

protected AdditiveColor(String name) {

super(); // Canvasのコンストラクタ実行setSize(width, height); // 幅と高さの設定

Frame f = new Frame(name); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

f.setVisible(true); // Frameの表示}

public void paint(Graphics g) {

for (int y = 0; y < height; y++) { // y軸方向のスキャンfor (int x = 0; x < width; x++) { // x軸方向のスキャン

float[] color = new float[3]; // RGBの配列作成for (int i = 0; i < 3; i++) { // RGB各成分

double dist2 = // 中心からの距離(x - centers[i][0]) * (x - centers[i][0]) +

(y - centers[i][1]) * (y - centers[i][1]);

color[i] = (dist2 > radius2) ? 0.0f : 1.0f;

// 距離と半径の大小で 1 / 0

12 第 1章 Javaによるグラフィクス

R=1G=0~1B=0R=1~0

G=1B=0

R=0G=1B=0~1

R=0G=1~0B=1

R=0~1G=0B=1 R=1

G=0B=1~0

Red(1 0 0)

Yellow(1 1 0)

Green(0 1 0)

Cyan(0 1 1)

Blue(0 0 1)

Magenta(1 0 1)

x

y

図 1.5: 色相と円

}

g.setColor(new Color(color[0], color[1], color[2]));

// 色の設定 (RGB値)

g.fillRect(x, y, 1, 1); // 1ピクセル描画}

}

}

public static void main(String[] args) {

new AdditiveColor("Additive Color");// Canvasオブジェクトの作成}

}

例2:色相円1.1節の例4の円を色相を変えて表示する.図 1.5のように,円全体を 6つの区間に区切って,徐々に色を変えていく.

θ ∈ [0, π3) : Red ( 1 0 0 ) → Yellow ( 1 1 0 )

θ ∈ [π3, 2π

3) : Yellow ( 1 1 0 ) → Green ( 0 1 0 )

θ ∈ [2π3

, π) : Green ( 0 1 0 ) → Cyan ( 0 1 1 )

θ ∈ [π, 4π3

) : Cyan ( 0 1 1 ) → Blue ( 0 0 1 )

θ ∈ [4π3

, 5π3

) : Blue ( 0 0 1 ) → Magenta ( 1 0 1 )

θ ∈ [5π3

, 2π] : Magenta ( 1 0 1 ) → Red ( 1 0 0 )

import java.awt.*;

1.2. 色 13

import java.awt.event.*;

public class ColorRingRGB extends Canvas {

protected static final int width = 600; // Canvasの幅protected static final int height = 600; // Canvasの高さprotected static final int centerX = 300; // 円中心の x座標protected static final int centerY = 299; // 円中心の y座標protected static final int radius = 250; // 円の半径protected Color[] colors; // 色の配列protected int[][] points; // 円周上の点列

protected ColorRingRGB(String name, int numberOfPoints) {

super(); // Canvasのコンストラクタ実行setSize(width, height); // 幅と高さの設定setBackground(Color.black); // 背景色の設定 (白)

setData(numberOfPoints); // 点と色データの作成

Frame f = new Frame(name); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

f.setVisible(true); // Frameの表示}

protected void setData(int numberOfPoints) {

final float oneSixth = 1.0f / 6.0f;

final float twoSixth = 2.0f / 6.0f;

final float threeSixth = 3.0f / 6.0f;

final float fourSixth = 4.0f / 6.0f;

final float fiveSixth = 5.0f / 6.0f;

final float six = 6.0f;

colors = new Color[numberOfPoints+1]; // 円周上の色の配列: n+1

points = new int[numberOfPoints+1][]; // 円周上の点の配列: n+1

for (int i = 0; i <= numberOfPoints; i++) { // 色と座標の計算float ratio = (float)i / numberOfPoints;

14 第 1章 Javaによるグラフィクス

// 角度の比if (ratio <= oneSixth) { // 0 <= t <= 1/6

colors[i] = new Color(1.0f, ratio*six, 0.0f);

} else if (ratio <= twoSixth) { // 1/6 < t <= 2/6

colors[i] = new Color((twoSixth-ratio)*six, 1.0f, 0.0f);

} else if (ratio <= threeSixth) { // 2/6 < t <= 3/6

colors[i] = new Color(0.0f, 1.0f, (ratio-twoSixth)*six);

} else if (ratio <= fourSixth) { // 3/6 < t <= 4/6

colors[i] = new Color(0.0f, (fourSixth-ratio)*six, 1.0f);

} else if (ratio <= fiveSixth) { // 4/6 < t <= 5/6

colors[i] = new Color((ratio-fourSixth)*six, 0.0f, 1.0f);

} else { // 5/6 < t <= 1

colors[i] = new Color(1.0f, 0.0f, (1.0f-ratio)*six);

}

double theta = 2.0 * Math.PI * ratio; // 角度 (radian)

points[i] = new int[2]; // 円周上の点の座標値points[i][0] = (int)(radius * Math.cos(theta)) + centerX;

points[i][1] = (int)(radius * Math.sin(theta)) + centerY;

}

}

public void paint(Graphics g) {

for (int i = 0; i < points.length - 1; i++) { // 点個数の直線描画

g.setColor(colors[i]); // 色の設定 (RGB値)

g.drawLine(points[i][0], points[i][1],

points[i+1][0], points[i+1][1]);

} // 点 P_i → 点 P_i+1

}

public static void main(String[] args) {

if (args.length == 0) // コマンド引数の確認System.err.println("Usage: java ColorRingRGB #");

else

new ColorRingRGB("ColorRingRGB", Integer.parseInt(args[0]));

}

}

例3:HSB表現と色円盤例2の円の内部も表示する.円の中心に近付くにしたがって彩度が落ちて白っぽくな

1.2. 色 15

り,中心では白色になる.HSB表現を用いることで簡単に計算できる

import java.awt.*;

import java.awt.event.*;

public class ColorDisk extends Canvas {

protected static final int width = 600; // Canvasの幅protected static final int height = 600; // Canvasの高さprotected static final int centerX = 300; // 円中心の x座標protected static final int centerY = 299; // 円中心の y座標protected static final int radius = 250; // 円の半径

protected ColorDisk(String name) {

super(); // Canvasのコンストラクタ実行setSize(width, height); // 幅と高さの設定

Frame f = new Frame(name); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

f.setVisible(true); // Frameの表示}

public void paint(Graphics g) {

for (int y = 0; y < height; y++) { // y軸方向のスキャンfor (int x = 0; x < width; x++) { // x軸方向のスキャン

double dx = (double)(x-centerX); // 中心との差 x成分double dy = (double)(y-centerY); // 中心との差 y成分float h = (float)(Math.atan2(dy, dx) / (2.0 * Math.PI));

// x軸との角 / 2PI

float s = (float)(Math.sqrt(dx*dx + dy*dy) / radius);

// 中心との距離float b = (s > 1.0f) ? 0.0f : 1.0f;

// 色の有無で 1 / 0

if (s > 1.0f) s = 0.0f; // s を 1以下にするg.setColor(Color.getHSBColor(h, s, b));

// 色の設定 (HSB値)

16 第 1章 Javaによるグラフィクス

g.fillRect(x, y, 1, 1); // 1ピクセル描画}

}

}

public static void main(String[] args) {

new ColorDisk("ColorDisk"); // Canvasオブジェクトの作成}

}

1.3 グラフィクスとイベント処理

グラフィクスを用いた対話的な (interactive; 人間と計算機との対話を実現する)プログラムでは,人間の入力に応じて動作が変わる.人間の入力のように,いつ生じるかわからない状態変更を処理する手法として,イベントの考え方が用いられる.

イベント (event): (入力などによって生じた)状態変更マウスのボタンをクリックしたり,キーボードから文字を入力したりすると,その状態の変更がイベントとして通知される.グラフィクスを用いた対話的なプログラムでは,イベントの通知を受けて,別の作業へと実行が移る.このようにイベントに応じてプログラムが実行される方式をイベント駆動型 (event driven)のプログラムと呼ぶ

イベントソース (event source):イベントが生じたオブジェクトイベントは,様々な場所で,様々な機会に生じる.イベントが生じた場所 (オブジェクト)がイベントソースになる.たとえば,ある Canvas内でマウスボタンがクリックされた場合には,その Canvasがイベントソースとなり,マウス (がクリックされたという)イベントが通知される

イベントリスナ,EventListener:イベントを受けとるオブジェクト (インタフェース)

イベントが生じると,イベントソースから,登録されているEventListener(複数)にイベントが通知され,該当するEventListenerのメソッドが起動される.すなわち,予めCanvasオブジェクト,より正確にはComponentオブジェクト,にaddEventListenerメソッドを用いて EventListenerを登録しておく必要がある.Javaでは EventListener

はインタフェースとして実現されている

イベントアダプタ,EventAdapter:イベントを受けとるオブジェクト (クラス)

インタフェースである EventListenerに代わるクラスとして,EventAdapterがある.この授業では,インタフェースについて学習していないので,EventAdapterを用いてイベント処理プログラムを作成する.イベントが生じると,イベントソースであるCanvasに登録されているすべての EventAdapterにイベントが通知され,イベントの種類に応じたメソッド起動される.ただし,本来の使い方とは若干異なることに注意すること

1.3. グラフィクスとイベント処理 17

マウスイベント,MouseEvent:マウス関連のイベントオブジェクトJavaでは,イベント自体も継承を用いたオブジェクト (クラス)として定義されている.この授業ではマウス関連のイベントである MouseEventのみを学ぶ.MouseEventには,以下の種類がある

MouseEventの種類 状態変更 EventListener/Adapter 起動メソッドMOUSE CLICKED ボタンのクリック MouseListener/Adapter mouseClicked

MOUSE ENTERED カーソルの進入 MouseListener/Adapter mouseEntered

MOUSE EXITED カーソルの退出 MouseListener/Adapter mouseExited

MOUSE PRESSED ボタンのプレス MouseListener/Adapter mousePressed

MOUSE RELEASED ボタンのリリース MouseListener/Adapter mouseReleased

MOUSE DRAGGED マウスのドラッグ MouseMotionListener/Adapter mouseDragged

MOUSE MOVED カーソルの移動 MouseMotionListener/Adapter mouseMoved

また MouseEventは修飾子によって,イベントが発生したボタン (左中右)やその時のキーボードの状態を調べられる.修飾子は getModifiersメソッドによって獲得できる.また MouseEventが発生した際のマウスカーソルの位置は,getXメソッドと getY

メソッドによって調べられる

例1:マウスイベントによる背景色の変更 - Background.java

Canvas内でマウスボタンをクリックすると背景色が変化する.ここで MouseEventのイベントソースは Canvasクラスを拡張した Backgroundであり,EventListenerはMouseAdapterクラスを拡張した BAMouseAdapterである.発生した MouseEventは,まず BAMouseAdapterに通知され,mousePressedメソッドないし mouseReleasedメソッドが起動される.これらのメソッドはBackgroundの同名のメソッドmousePressed

ないし mouseReleasedに MouseEventをそのまま通知する.BAMouseAdapterは,通知先の Backroundオブジェクトをインスンタンス変数 baに保持している

import java.awt.*;

import java.awt.event.*;

public class Background extends Canvas {

private static final int width = 200; // Canvasの幅private static final int height = 200; // Canvasの高さ

private Background() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.green); // 背景色の設定 (緑)

addMouseListener(new BgMouseAdapter(this));

Frame f = new Frame("Background"); // Frameの作成f.add(this); // Frameに Canvaを登録

18 第 1章 Javaによるグラフィクス

f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

protected void mousePressed(MouseEvent me) {

setBackground(Color.red); // 背景色の設定 (赤)

repaint(); // 画像更新の要求(update)

}

protected void mouseReleased(MouseEvent me) {

setBackground(Color.green); // 背景色の設定 (緑)

repaint(); // 画像更新の要求(update)

}

public static void main(String[] args) {

new Background(); // Canvasオブジェクトの作成}

}

class BgMouseAdapter extends MouseAdapter {

private Background bg;

public BgMouseAdapter(Background bg) {

this.bg = bg;

}

public void mousePressed(MouseEvent me) {

bg.mousePressed(me);

}

public void mouseReleased(MouseEvent me) {

bg.mouseReleased(me);

}

}

例2:マウスイベントによる背景色の変更 (インタフェース版) - BackgroundListener.java

例1とまったく同じプログラムだが,MouseAdapterクラスではなく MouseLinstener

インタフェースを用いている.インタフェース (詳細はプログラミングのプリントの

1.3. グラフィクスとイベント処理 19

「インタフェース」を見よ)によって,Backgroundは,イベントソースかつ描画領域であるとともに,同時に MouseListenerすなわち EventListenerとなる.このためプログラムは非常にすっきりしている.ただし,インタフェースのメソッドはすべて抽象メソッドであるため,何もしないメソッドにも定義が必要となる

import java.awt.*;

import java.awt.event.*;

public class BackgroundListener extends Canvas

implements MouseListener {

private static final int width = 200; // Canvasの幅private static final int height = 200; // Canvasの高さ

private BackgroundListener() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.green); // 背景色の設定 (緑)

addMouseListener(this);

Frame f = new Frame("BackgroundListener"); // Frameの作成

f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

public void mousePressed(MouseEvent me) {

setBackground(Color.red); // 背景色の設定 (赤)

repaint(); // 画像更新の要求(update)

}

public void mouseReleased(MouseEvent me) {

setBackground(Color.green); // 背景色の設定 (緑)

repaint(); // 画像更新の要求(update)

}

public void mouseEntered(MouseEvent me) { }

public void mouseExited(MouseEvent me) { }

20 第 1章 Javaによるグラフィクス

public void mouseClicked(MouseEvent me) { }

public static void main(String[] args) {

new BackgroundListener(); // Canvasオブジェクトの作成}

}

例3:マウスイベントによる背景色の変更 (内部クラス版) - BackgroundInner.java

例1とまったく同じプログラムだが,MouseAdapterクラスを拡張したBgMouseAdapter

クラスをBackgroundInnerクラスの内側で定義している.すなわち,BackgroundInnerクラスのスコープ内にあるため,BackgroundInnerクラスの setBackgroundメソッドや repaintメソッドを BgMouseAdapterクラスから直接呼び出すことが可能となっている.この分,プログラムはかなりすっきりしている.

import java.awt.*;

import java.awt.event.*;

public class BackgroundInner extends Canvas {

private static final int width = 200; // Canvasの幅private static final int height = 200; // Canvasの高さ

private BackgroundInner() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.green); // 背景色の設定 (緑)

addMouseListener(new BgMouseAdapter());

Frame f = new Frame("BackgroundInner"); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

class BgMouseAdapter extends MouseAdapter { // 内部クラスの定義public void mousePressed(MouseEvent me) {

setBackground(Color.red); // 背景色の設定 (赤)

1.3. グラフィクスとイベント処理 21

repaint(); // 画像更新の要求(update)

}

public void mouseReleased(MouseEvent me) {

setBackground(Color.green); // 背景色の設定 (緑)

repaint(); // 画像更新の要求(update)

}

}

public static void main(String[] args) {

new BackgroundInner(); // Canvasオブジェクトの作成}

}

例4:マウスイベントによる背景色の変更 (無名クラス版) - BackgroundAnonymous.java

例1とまったく同じプログラムだが,MouseAdapterクラスを拡張した内部クラスを無名で定義し,そのままオブジェクトを生成している.例3のプログラムと比較すると,クラス名が省略されていることが確認できる.この分,プログラムは大変すっきりしたものになっている.f.addWindowListenerメソッドの引数も,WindowAdapterクラスを拡張した無名クラスであることが分かる.

import java.awt.*;

import java.awt.event.*;

public class BackgroundAnonymous extends Canvas {

private static final int width = 200; // Canvasの幅private static final int height = 200; // Canvasの高さ

private BackgroundAnonymous() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.green); // 背景色の設定 (緑)

addMouseListener(new MouseAdapter() { // 無名オブジェクト作成public void mousePressed(MouseEvent me) {

setBackground(Color.red); // 背景色の設定 (赤)

repaint(); // 画像更新の要求(update)

}

public void mouseReleased(MouseEvent me) {

setBackground(Color.green); // 背景色の設定 (緑)

repaint(); // 画像更新の要求(update)

}

});

22 第 1章 Javaによるグラフィクス

Frame f = new Frame("BackgroundAnonymous"); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

public static void main(String[] args) {

new BackgroundAnonymous(); // Canvasオブジェクトの作成}

}

例5:カーソル位置の獲得と描画 - Marker.java

getModifiersメソッドによって修飾子を獲得できる.修飾子は int型の値であり,各種のマスク値を比較することで,イベントの生じたボタンの種類やキーボードの状態を調べられる.また,getXメソッドと getYメソッドを用いると,イベント発生時のカーソル位置を調べられる.このプログラムはボタンが押されるたびに,カーソル位置に正方形を描画する.押されたボタンによって正方形の色が変わる.前の例のように repaintメソッドによって updateしていないので,一度描いた正方形は消えずに画面上に残っている

import java.awt.*;

import java.awt.event.*;

public class Marker extends Canvas {

private static final int width = 400; // Canvasの幅private static final int height = 400; // Canvasの高さprivate static final int SIZE = 10; // 正方形の 1辺

private Marker() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

addMouseListener(new MouseAdapter () { // 無名オブジェクト作成public void mousePressed(MouseEvent me) {

int modifiers = me.getModifiers(); // 修飾子の獲得

1.3. グラフィクスとイベント処理 23

Graphics g = getGraphics(); // Graphicsの獲得if ((modifiers & MouseEvent.BUTTON3_MASK) != 0)

g.setColor(Color.blue); // ボタン 3(右)で青// else if ((modifiers & MouseEvent.BUTTON2_MASK) != 0)

// g.setColor(Color.blue); // ボタン2(中)で青 Mac不可else

g.setColor(Color.red); // その他 (左)で赤int x = me.getX(); // カーソルの x座標int y = me.getY(); // カーソルの y座標g.fillRect(x - (SIZE/2), y - (SIZE/2), SIZE, SIZE);

} // 正方形の描画});

Frame f = new Frame("Marker"); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

public static void main(String[] args) {

new Marker(); // Canvasオブジェクトの作成}

}

例6:マウスによる線分の描画 - Rubberband.java

MouseListenerだけではなく MuoseMotionListenerを用いることで,マウスをドラッグ (ボタンをプレスしながら移動)する間も,マウスの動きに合わせて線分を描ける.このような線分は,一般にラバーバンド (早い話がゴム紐)と呼ばれる

import java.awt.*;

import java.awt.event.*;

public class Rubberband extends Canvas {

private static final int width = 400; // Canvasの幅private static final int height = 400; // Canvasの高さprivate int startX; // 始点の x座標

24 第 1章 Javaによるグラフィクス

private int startY; // 始点の y座標

private Rubberband() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

setForeground(Color.black); // 描画色の設定 (黒)

addMouseListener(new MouseAdapter() { // 無名オブジェクト作成public void mousePressed(MouseEvent me) {

startX = me.getX(); // 始点のx座標(プレス時)

startY = me.getY(); // 始点のy座標(プレス時)

}

});

addMouseMotionListener(new MouseMotionAdapter() {

public void mouseDragged(MouseEvent me) {

Graphics g = getGraphics(); // Graphicsの獲得update(g); // 画面の消去int x = me.getX(); // カーソルの x座標int y = me.getY(); // カーソルの y座標g.drawLine(startX, startY, x, y); // 線分の描画

}

});

Frame f = new Frame("Rubberband"); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

public static void main(String[] args) {

new Rubberband(); // Canvasオブジェクトの作成}

}

例7:マウスを用いたお絵描き - Draw.java

マウスの移動に伴って,次々と短い線分を描画することで,自由な線が描ける.

1.3. グラフィクスとイベント処理 25

import java.awt.*;

import java.awt.event.*;

public class Draw extends Canvas {

private static final int width = 400; // Canvasの幅private static final int height = 400; // Canvasの高さprivate int oldX; // 始点の x座標private int oldY; // 始点の y座標

private Draw() {

super(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

setForeground(Color.black); // 描画色の設定 (黒)

addMouseListener(new MouseAdapter() { // 無名オブジェクト作成public void mousePressed(MouseEvent me) {

oldX = me.getX(); // 始点のx座標(プレス時)

oldY = me.getY(); // 始点のy座標(プレス時)

}

});

addMouseMotionListener(new MouseMotionAdapter() {

public void mouseDragged(MouseEvent me) {

int x = me.getX(); // カーソルの x座標int y = me.getY(); // カーソルの y座標getGraphics().drawLine(oldX, oldY, x, y);// 線分の描画oldX = x;

oldY = y;

}

});

Frame f = new Frame("Draw"); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.setVisible(true); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

26 第 1章 Javaによるグラフィクス

public static void main(String[] args) {

new Draw(); // Canvasオブジェクトの作成}

}

1.4 表示空間 (平面)と2次元座標変換

常に Canvas の大きさを意識しながら,図形を描くのは適切ではない.すなわち,600× 600

程度の大きさに合うようにプログラムの数値を書き換えるのではなく,プログラムの側でウィンドウにあった大きさに変換すれば良い.また,ベクトルの計算や座標変換などをクラスとしてライブラリ化すると,プログラムの見通しが良くなる.

許容差 (tolerance):実数値の同値判定の閾値実数は近似値であるために,多少の誤差を見込んで同値判定を行なう.2つ値が同値(同じ値)であるか否かの判定の閾値を許容差という.

例1:許容差の定義 - Tolerance.java

public class Tolerance {

public final static double EPSILON = 1.0e-6;

}

2次元ベクトル: 2つの値 (x, y)からなるベクトル量ベクトルの和,差,内積など,さまざまな演算がある

例2: 2次元ベクトルの定義 - Vector2D.java

import java.util.*;

public class Vector2D {

private double x, y; // ベクトルの成分

public Vector2D(double x, double y) { // コンストラクタ (成分指定)

this.x = x;

this.y = y;

}

public Vector2D(Vector2D v) { // コンストラクタ (コピー)

this.x = v.x;

this.y = v.y;

}

public Vector2D(String str) { // コンストラクタ (文字列)

StringTokenizer st = new StringTokenizer(str);

1.4. 表示空間 (平面)と 2次元座標変換 27

this.x = Double.parseDouble(st.nextToken());

this.y = Double.parseDouble(st.nextToken());

}

public double x() { // x成分return this.x;

}

public double y() { // y成分return this.y;

}

public double norm() { // ノルム (大きさ)

return Math.sqrt(this.x * this.x + this.y * this.y);

}

public Vector2D add(Vector2D v) { // ベクトルの和return new Vector2D(this.x + v.x, this.y + v.y);

}

public Vector2D subtract(Vector2D v) { // ベクトルの差return new Vector2D(this.x - v.x, this.y - v.y);

}

public Vector2D scale(double d) { // スカラ倍 (拡大縮小)

return new Vector2D(this.x * d, this.y * d);

}

public Vector2D normalize() { // 正規化double norm = this.norm();

if (norm > Tolerance.EPSILON)

return this.scale(1.0 / norm);

else

return (new Vector2D(0.0, 0.0));

}

public String toString() { // 文字列化return "(" + x + ", " + y + ")";

}

}

アフィン変換 (affine transformation):正方行列とベクトルによる変換p′ = Mp + v の形式で表現される変換.ただし,M は正方行列 (たとえば 2 × 2行列など)で,vはベクトル.スケール変換 (拡大縮小)や回転変換,平行移動,それらの複合変換などを含む.2次元の場合は,以下のとおり.

28 第 1章 Javaによるグラフィクス

• スケール変換 - x, y成分を sx, sy倍する

M =

[sx 0

0 sy

]

• 回転変換 - 原点を中心に θ回転する

M =

[cos θ − sin θ

sin θ cos θ

]

• 平行移動 - v = [ tx ty ]Tだけ平行移動する

v =

[txty

]

例3: 2 × 2行列の定義 - Matrix2X2.java

public class Matrix2X2 {

private double xx, xy, yx, yy; // 行列の成分

public Matrix2X2(double xx, double xy, double yx, double yy) {

this.xx = xx; this.xy = xy; // コンストラクタ (成分指定)

this.yx = yx; this.yy = yy;

}

public Matrix2X2(Matrix2X2 m) { // コンストラクタ (コピー)

this.xx = m.xx; this.xy = m.xy;

this.yx = m.yx; this.yy = m.yy;

}

public Vector2D apply(Vector2D v) { // ベクトルとの積return new Vector2D(xx*v.x() + xy*v.y(), yx*v.x() + yy*v.y());

}

public Matrix2X2 multiply(Matrix2X2 m) { // 行列同士の積return new Matrix2X2((xx*m.xx + xy*m.yx), (xx*m.xy + xy*m.yy),

(yx*m.xx + yy*m.yx), (yx*m.xy + yy*m.yy));

}

public static Matrix2X2 rotate(double t) {// 回転行列の生成return new Matrix2X2(Math.cos(t), -Math.sin(t),

Math.sin(t), Math.cos(t));

}

public static Matrix2X2 scale(double s) { // スケール行列の生成return new Matrix2X2(s, 0.0, 0.0, s);

}

}

1.4. 表示空間 (平面)と 2次元座標変換 29

例4:仮想の表示空間を持つ Canvas - CGCanvas.java

CGCanvasを拡張することで,2次元の描画プログラムを簡潔に書ける.標準では,600×600のウィンドウ内に,原点を中心にした−1 ≤ x, y ≤ 1の範囲を描画する.

import java.awt.*;

import java.awt.event.*;

public class CGCanvas extends Canvas {

private Frame f; // Frameオブジェクトprivate double x, y; // ベクトルの成分private int width = 600; // Canvasの幅private int height = 600; // Canvasの高さprivate int originX = 300; // 原点位置の x座標private int originY = 299; // 原点位置の y座標protected double range = 2.0; // 表示空間の広さ (長さ)

protected double scale = width/range; // Canvasへの拡大縮小率protected int markerSize = 5; // マーカ (小四角形)の大きさ

public CGCanvas(String name) { // コンストラクタsuper(); // Canvasコンストラクタの実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

setForeground(Color.black); // 描画色の設定 (黒)

f = new Frame(name); // Frameの作成f.add(this); // Frameに Canvaを登録f.pack(); // 配置の確定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

public void showFrame() {

f.setVisible(true); // Frameの表示}

public void setOrigin(int x, int y) { // 原点位置の設定originX = x;

originY = y;

}

public void setRange(double r) { // 仮想表示空間の設定

30 第 1章 Javaによるグラフィクス

Dimension d = getSize(); // Canvasサイズの獲得width = d.width;

height = d.height;

range = r; // 表示領域の設定scale = Math.min(width, height) / range; // 拡大縮小率の変更

}

public void drawLine(Graphics g, Vector2D from, Vector2D to) {

g.drawLine(x(from), y(from), x(to), y(to)); // 線分の描画}

public void drawPoint(Graphics g, Vector2D point) {

g.fillRect(x(point), y(point), 1, 1); // 点の描画}

public void drawPolygon(Graphics g, Vector2D[] points) {

int[] x = new int[points.length]; // 多角形 (辺)の描画int[] y = new int[points.length];

for (int i = 0; i < points.length; i++) {

x[i] = x(points[i]);

y[i] = y(points[i]);

}

g.drawPolygon(x, y, points.length);

}

public void drawCircle(Graphics g, Vector2D point, int radius) {

int dr = (int) (radius * scale); // 円の描画g.drawOval(x(point) - dr, y(point) - dr, dr * 2, dr * 2);

}

public void drawString(Graphics g, Vector2D point, String str) {

g.drawString(str, x(point), y(point)); // 文字列の描画}

public void fillMarker(Graphics g, Vector2D point) {

g.fillRect(x(point) - markerSize / 2, y(point) - markerSize / 2,

markerSize, markerSize); // マーカ (小四角形)の描画}

public void fillPolygon(Graphics g, Vector2D[] points) {

int[] x = new int[points.length]; // 多角形 (内部)の描画int[] y = new int[points.length];

for (int i = 0; i < points.length; i++) {

x[i] = x(points[i]);

y[i] = y(points[i]);

}

g.fillPolygon(x, y, points.length);

1.4. 表示空間 (平面)と 2次元座標変換 31

}

public Vector2D point(int x, int y) {

return new Vector2D((x - originX) / scale, (originY - y) / scale);

} // Canvas上の位置から表示空間への変換protected final int x(Vector2D point) {

return (int) (scale * point.x()) + originX;

} // 表示空間から Canvas上の位置の x座標へ変換protected final int y(Vector2D point) {

return -(int) (scale * point.y()) + originY;

} // 表示空間から Canvas上の位置の y座標へ変換protected final boolean inside(Vector2D point) {

int x = (int) (scale * point.x()) + originX;

if (x < 0 || x >= width)

return false;

int y = -(int) (scale * point.y()) + originY;

if (y < 0 || y >= height)

return false;

return true;

} // 表示空間の内外判定 (曲線描画用)

}

例5:ダイアモンドパターンの描画 - CGDiamondPattern.java

CGCanvasを利用することで,簡潔なプログラムになる

import java.awt.*;

public class CGDiamondPattern extends CGCanvas {

private final static double radius = 0.8; // 円の半径private Vector2D[] points; // 円周上の点の配列

private CGDiamondPattern(int numberOfPoints) {

super("CGDiamondPattern"); // CGCanvasコンストラクタの実行points = new Vector2D[numberOfPoints]; // 点の配列for (int i = 0; i < numberOfPoints; i++) { // 点の座標計算

double theta = 2.0 * Math.PI * i / numberOfPoints;

points[i] = new Vector2D(radius*Math.cos(theta),

radius*Math.sin(theta));

}

}

public void paint(Graphics g) { // 描画プログラム

32 第 1章 Javaによるグラフィクス

for (int i = 0; i < points.length - 1; i++)

for (int j = i + 1; j < points.length; j++)

drawLine(g, points[i], points[j]);

} // 全ての 2点で線分描画public static void main(String[] args) {

if (args.length == 0) // コマンド引数の確認System.err.println("Usage: java CGDiamondPattern #");

else

(new CGDiamondPattern(Integer.parseInt(args[0]))).showFrame();

}

}

1.5 フラクタル

自然界には海岸線や岩山などの複雑な形状が見られるが,フラクタルと呼ばれる形状と似通った性質を持つと言われている.

フラクタル (Fractal):自己相似性を持つ形状微分不可能で,位相次元と異なるハウスドルフ次元を持つ

自己相似性 (self-similarity):異なるスケールで自らに相似な形状適当な拡大縮小を行なうと元の形状に相似した形状が見られること

ハウスドルフ次元 (Hausdorff dimension):形状の量 (大きさ)の次元曲線ならば長さ (1次元),曲面ならば面積 (2次元),立体ならば体積 (3次元)というように,スケールに関する形状の次数のこと.ハウスドルフ次元が nの形状は,スケール (長さの単位)を 1/s倍にすると,その量は sn倍になる.フラクタル次元と呼ばれることもある

反復関数系 (Iterated Function System: IFS):フラクタル生成の一手法アフィン変換などで表せる縮小写像の反復システム.図 1.6のコッホ曲線,シェルピンスキーガスケット,ドラゴン曲線などのフラクタルを生成できる

例1:コッホ曲線 - Koch.java

1辺を 3等分し,中央の 3分の 1を除去して,正三角形の 2辺を加える.1/3の長さの辺が 4本になるので,ハウスドルフ次元は log34 = 1.26186となる.

import java.awt.*;

public class Koch extends CGCanvas {

private int times;

private Vector2D initP0;

private Vector2D initP1;

1.5. フラクタル 33

図 1.6: コッホ曲線,シェルピンスキーガスケット,ドラゴン曲線

private Matrix2X2[] m;

private Vector2D[] v;

private Koch(int times) {

super("Koch"); // CGCanvasコンストラクタの実行this.times = times; // 再帰の回数setOrigin(50, 299); // 原点位置の設定setRange(1.2); // 描画空間の設定initP0 = new Vector2D(0.0, 0.0); // 始点 (原点にすると楽)

initP1 = new Vector2D(1.0, 0.0); // 終点

m = new Matrix2X2[4]; // 縮小写像の行列v = new Vector2D[4]; // 縮小写像のベクトルm[0] = Matrix2X2.scale(1.0/3.0); // 行列の定義m[1] = Matrix2X2.scale(1.0/3.0).

multiply(Matrix2X2.rotate(Math.PI/3.0));

m[2] = Matrix2X2.scale(1.0/3.0).

multiply(Matrix2X2.rotate(-Math.PI/3.0));

m[3] = Matrix2X2.scale(1.0/3.0);

34 第 1章 Javaによるグラフィクス

v[0] = new Vector2D(0.0, 0.0); // ベクトルの定義v[1] = m[0].apply(new Vector2D(1.0, 0.0)).add(v[0]);

v[2] = m[1].apply(new Vector2D(1.0, 0.0)).add(v[1]);

v[3] = m[2].apply(new Vector2D(1.0, 0.0)).add(v[2]);

}

public void paint(Graphics g) { // 描画プログラムdrawSegment(g, initP0, initP1, times);

}

private void drawSegment(Graphics g, Vector2D p0, Vector2D p1, int l) {

if (l > 0) // 再帰レベルfor (int i = 0; i < m.length; i++) // 縮小写像の適用

drawSegment(g, m[i].apply(p0).add(v[i]),

m[i].apply(p1).add(v[i]), l-1);

else // 線分の描画drawLine(g, p0, p1);

}

public static void main(String[] args) {

if (args.length != 1) // コマンド引数の確認System.err.println("Usage: Koch #iteration");

else

(new Koch(Integer.parseInt(args[0]))).showFrame();

}

}

例2:シェルピンスキーガスケット - Sierpinski.java

三角形の各辺を 2等分して全体を 4つの三角形とし,そのうちの中央の三角形を取り除く.1/2の大きさの三角形が 3つになるので,ハウスドルフ次元は log23 = 1.58496

となる.

import java.awt.*;

public class Sierpinski extends CGCanvas {

private int times;

private Vector2D[] initPoints;

private Matrix2X2[] m;

private Vector2D[] v;

private Sierpinski(int times) {

1.5. フラクタル 35

super("Sierpinski"); // CGCanvasコンストラクタの実行this.times = times; // 再帰の回数setOrigin(300, 84); // 原点位置の設定setRange(2.4); // 描画空間の設定initPoints = new Vector2D[3]; // 初期の 3頂点initPoints[0] = new Vector2D( 0.0, 0.0);

initPoints[1] = new Vector2D(-1.0, -Math.sqrt(3.0));

initPoints[2] = new Vector2D( 1.0, -Math.sqrt(3.0));

m = new Matrix2X2[3]; // 縮小写像の行列v = new Vector2D[3]; // 縮小写像のベクトルm[0] = Matrix2X2.scale(1.0/2.0); // 行列の定義m[1] = Matrix2X2.scale(1.0/2.0);

m[2] = Matrix2X2.scale(1.0/2.0);

v[0] = new Vector2D( 0.0, 0.0); // ベクトルの定義v[1] = new Vector2D(-0.5, -Math.sqrt(3.0)/2.0);

v[2] = new Vector2D( 0.5, -Math.sqrt(3.0)/2.0);

}

public void paint(Graphics g) { // 描画プログラムdrawTriangle(g, initPoints, times);

}

private void drawTriangle(Graphics g, Vector2D[] points, int l) {

if (l > 0) { // 再帰レベルfor (int i = 0; i < m.length; i++) { // 縮小写像の適用

Vector2D[] newPoints = new Vector2D[3];

for (int j = 0; j < 3; j++)

newPoints[j] = m[i].apply(points[j]).add(v[i]);

drawTriangle(g, newPoints, l-1);

}

}

else { // 三角形の描画g.setColor(Color.lightGray);

fillPolygon(g, points);

g.setColor(Color.black);

drawPolygon(g, points);

}

}

36 第 1章 Javaによるグラフィクス

public static void main(String[] args) {

if (args.length != 1) // コマンド引数の確認System.err.println("Usage: Sierpinski #iteration");

else

(new Sierpinski(Integer.parseInt(args[0]))).showFrame();

}

}

1.6 パラメトリック曲線

パラメータ (媒介変数)tの値に応じて,(平面/空間内の)座標値が 1つ定まる形式の曲線.コンピュータグラフィクスではパラメトリック形式の曲線や曲面がよく用いられる.

パラメトリック曲線 (parametric curve):パラメータ値に対して座標が定まる曲線x = g(t), y = h(t) という形式をしている.例: 円の式 {

x = r cos t

y = r sin tまたは

x = 2rt1+t2

y = r(1−t2)1+t2

曲線上の点列を計算しやすいので,CGでよく利用される.

陰関数曲線 (implicit curve):座標値間の制約式で与えられる曲線f(x, y) = 0 という形式をしている.例: 円の式

x2 + y2 − r2 = 0

関数曲線 (function curve): y座標が x座標の関数で与えられる曲線y = f(x) という形式をしている.例: 円の式

y =√

r2 − x2

例1:パラメトリック曲線 - ParametricCurve.java

public interface ParametricCurve {

public Vector2D evaluate(double t); // 座標値の計算public double begin(); // 始点のパラメータ値public double end(); // 終点のパラメータ値

}

例2:パラメトリック曲線の描画プログラム - CurveCanvas.java

import java.awt.*;

public class CurveCanvas extends CGCanvas {

1.6. パラメトリック曲線 37

final static int NOP = 128; // 描画の標準点数final static double RATIO = 8.0; // 範囲の標準閾値protected ParametricCurve[] curves; // 曲線の配列

protected CurveCanvas(String name){

super(name); // CGCanvasコンストラクタの実行}

protected void drawCurve(Graphics g, ParametricCurve curve, double ts,

double te, int nOfPoints, double borderRatio) {

double delta = (te - ts)/nOfPoints; // te -> ts

Vector2D prev = null; // 初期設定 (prev なし)

boolean prevIn = false; // 初期設定 (prev 外部)

for (int i = 0; i <= nOfPoints; i++) { // nOfPoints回反復Vector2D point = curve.evaluate(ts + delta * i);

// 曲線上の点boolean in = inside(point); // 内外判定if (prev != null && (prevIn || in))

drawLine(g, prev, point); // 1点でも内ならば描画prev = point; // 点の更新prevIn = in;

}

}

protected void drawCurve(Graphics g, ParametricCurve curve) {

drawCurve(g, curve, curve.begin(), curve.end(), NOP, RATIO);

} // 標準値での描画

public void paint(Graphics g) { // 描画プログラムif (curves != null) // curvesの数だけ描画

for (int i = 0; i < curves.length; i++)

if (curves[i] != null)

drawCurve(g, curves[i]);

}

}

例3:三角関数の描画 - DiagonalCanvas.java

import java.awt.*;

public class DiagonalCanvas extends CurveCanvas {

38 第 1章 Javaによるグラフィクス

public DiagonalCanvas() {

super("Diagonal Canvas"); // CurveCanvasコンストラクタの実行setRange(8.0); // 描画空間の設定curves = new ParametricCurve[3]; // パラメトリック曲線curves[0] = new Sine(-Math.PI, 0.0);

curves[1] = new Cosine(-Math.PI, 0.0);

curves[2] = new Tangent(-Math.PI, 0.0);

}

public void paint(Graphics g) { // 描画プログラムg.setColor(Color.blue); // 描画色の設定 (青)

super.paint(g); // 曲線の描画g.setColor(Color.black); // 描画色の設定 (黒)

drawLine(g, new Vector2D(-Math.PI, 4.0), new Vector2D(-Math.PI, -4.0));

drawLine(g, new Vector2D(-4.0, 0.0), new Vector2D(4.0, 0.0));

} // 座標軸の描画

public static void main(String[] args) {

new DiagonalCanvas().showFrame();

}

}

abstract class Diagonal implements ParametricCurve {

protected Vector2D translation; // 原点位置の指定

Diagonal(double x, double y) { // コンストラクタtranslation = new Vector2D(x, y); // 原点位置の設定

}

public double begin() { // 始点のパラメータ値return -0.5 * Math.PI;

}

public double end() { // 終点のパラメータ値return 2.5 * Math.PI;

}

}

class Sine extends Diagonal {

Sine(double x, double y) {

super(x, y); // ParametricCurveコンストラクタの実行}

1.6. パラメトリック曲線 39

public Vector2D evaluate(double t) { // 座標値の計算return (new Vector2D(t, Math.sin(t))).add(translation);

}

}

class Cosine extends Diagonal {

Cosine(double x, double y) {

super(x, y); // ParametricCurveコンストラクタの実行}

public Vector2D evaluate(double t) { // 座標値の計算return (new Vector2D(t, Math.cos(t))).add(translation);

}

}

class Tangent extends Diagonal {

Tangent(double x, double y) {

super(x, y); // ParametricCurveコンストラクタの実行}

public Vector2D evaluate(double t) { // 座標値の計算return (new Vector2D(t, Math.tan(t))).add(translation);

}

}

ベジエ曲線 (Bezier curve):多項式による自由曲線制御点P0,P1, · · · ,Pnによって形状を自由に変えられる3次 (多項式の次数が 3)のベジエ曲線は,以下の式で与えられる.

P(t) = (1 − t)3P0 + 3(1 − t)2tP1 + 3(1 − t)t2P2 + t3P3 ただし 0 ≤ t ≤ 1

• 多項式部分の和は,常に 1 → 制御点の加重平均

(1 − t)3 + 3(1 − t)2t + 3(1 − t)t2 + t3 = ((1 − t) + t)3 = 1

• t = 0, 1 で特殊な条件 → 両端の位置と向き

P(0) = P0, P(1) = P3,dP

dt(0) = 3(P1 − P0),

dP

dt(1) = 3(P3 − P2)

例4: 3次ベジエ曲線 - CubicBezier.java

public class CubicBezier implements ParametricCurve {

private Vector2D[] points; // 制御点

40 第 1章 Javaによるグラフィクス

public CubicBezier(Vector2D[] points) { // コンストラクタthis.points = points; // 制御点の設定

}

public Vector2D[] points() { // 制御点return points;

}

public Vector2D evaluate(double t) { // 座標値の計算double[] ts = new double[4]; // 多項式部分の値 (重み)

double u = 1.0 - t;

ts[0] = u*u*u; ts[1] = 3.0*u*u*t;

ts[2] = 3.0*u*t*t; ts[3] = t*t*t;

Vector2D point = new Vector2D(0.0, 0.0);

for (int j = 0; j <= 3; j++) // 4点の加重平均point = point.add(points[j].scale(ts[j]));

return point;

}

public double begin() { // 始点のパラメータ値return 0.0;

}

public double end() { // 始点のパラメータ値return 1.0;

}

}

例5:ベジエ曲線の描画プログラム - CubicBezierCanvas.java

import java.awt.*;

public class CubicBezierCanvas extends CurveCanvas {

public CubicBezierCanvas() { // コンストラクタ(制御点なし)

super("Cubic Bezier Canvas");

}

public CubicBezierCanvas(Vector2D[] points) {

super("Cubic Bezier Canvas"); // コンストラクタ(制御点あり)

curves = new ParametricCurve[1];

curves[0] = new CubicBezier(points);

1.6. パラメトリック曲線 41

}

public void paint(Graphics g) { // 描画プログラムsuper.paint(g); // 曲線の描画if (curves != null) // 制御点の描画

for (int i = 0; i < curves.length; i++)

drawPoints(g, ((CubicBezier)curves[i]).points());

}

private void drawPoints(Graphics g, Vector2D[] points) {

if (points != null) { // 制御点の描画Color tmpColor = g.getColor(); // 描画色の獲得 (tmp)

g.setColor(Color.BLUE); // 描画色の設定 (青)

fillMarker(g, points[0]); // 第 1制御点の描画for (int i = 1; i < points.length; i++) {

fillMarker(g, points[i]); // 制御点と線分の描画drawLine(g, points[i - 1], points[i]);

}

g.setColor(tmpColor); // 描画色の設定 (tmp)

}

}

public static void main(String[] args) {

if (args.length != 8) // コマンド引数の確認System.err.println(

"Usage: java CubicBezierCanvas x0 y0 x1 y1 x2 y2 x3 y3");

else { // 制御点→ベジエ曲線Vector2D[] points = new Vector2D[4];

for (int i = 0; i < 4; i++)

points[i] = new Vector2D(Double.parseDouble(args[2*i]),

Double.parseDouble(args[2*i+1]));

(new CubicBezierCanvas(points)).showFrame();

}

}

}

42 第 1章 Javaによるグラフィクス

1.7 JOGLによるグラフィクス

OpenGL: 3次元グラフィクス (ハードウェア)用ライブラリ現在,最も広く用いられているグラフィックスライブラリである.本来はCないしC++

用のライブラリだったが,Javaからも利用できるようになった.

ライブラリ:プログラムの集まりライブラリに登録された関数を用いることによって,様々な作業を簡単に実行できる.

JOGLの利用:クラスライブラリと JNI(Java Native Interface)

JOGLは標準の Java環境 (SDK)には含まれていない.JOGLのクラスライブラリである jogl.jar とC/C++ライブラリとのリンクをとる JNI (OSによって異なる)が必要となる

OS JNI-jarファイル JNIライブラリMacOS X jogl-natives-macosx.jar libjogl.jnilib, libjogl cg.jnilib

Windows jogl-natives-win32.jar jogl.dll, jogl cg.dll

いずれも http://jogl.dev.java.net/からダウンロードできる∗.また JNIライブラリファイルは,jarコマンドによって JNI-jarファイルから取り出せる.MacOS環境では,これら jogl.jarと JNIライブラリをホームディレクトリの下の, /Library

/Java/Extensionsに置く.

ca10101$ jar xf jogl-natives-macosx.jar¶ca10101$ mkdir ~/Library/Java¶ca10101$ mkdir ~/Library/Java/Extensions¶ca10101$ cp jogl.jar ~/Library/Java/Extensions¶ca10101$ cp libjogl.jnilib ~/Library/Java/Extensions¶ca10101$ cp libjogl_cg.jnilib ~/Library/Java/Extensions¶

これで,コンパイル実行が可能となる.

ca10101$ javac File.java¶ca10101$ java File¶

1.8 3次元グラフィクスの基礎

3次元形状を 2次元平面である画面上に表示するには,いくつかの座標変換を施す必要がある.特に,3次元座標を 2次元座標に変換する平行投影や透視投影 (射影)が重要である.

ビューイングパイプライン: 3次元データから画像生成までの過程OpenGL/JOGLでのビューイングパイプラインは,図1.7のように 7段階で構成される.

∗CFIVEに jogl.jarと JNIライブラリ (MacOS用)を置いておく.

1.8. 3次元グラフィクスの基礎 43

図 1.7: ビューイングパイプライン

1. モデル変換/視野変換視点の位置や視線の向きなどに合わせて,3次元データに,平行移動や回転,拡大縮小などを施し,適当な位置,向き,大きさに変換する.

2. クリッピング (clipping)

3次元データから表示すべき部分として,視体積 (viewing volume)と呼ばれる領域を切り出す.透視変換が施される場合には,元の 3次元空間において視体積は四角錘台状になることから,視錐台 (viewing frustum)と呼ばれる.

3. 投影変換投影変換によって,3次元データを投影面上の 2次元空間のデータに変換する.3

次元グラフィクスでは多くの場合,透視変換 (perspective transformation)が用いられる.

4. 隠線・隠面消去複数の物体と視点との位置関係によって,物体は部分的に遮蔽されて,視点から観察されない場合もある.この遮蔽などに伴う可視・不可視を判定して,不可視部分を削除する隠線・隠面消去を実施する.

5. 陰影処理視点と光源,面の位置関係から明るさや色を計算する陰影処理行なう.局所的な光の反射を考慮して陰を計算するシェーディングモデルや,より大域的な光の遮閉や間接反射などによって生じる影を計算する大域照明モデルなどがある.

6. ウィンドウ・ビューポート変換 (window-viewport transformation)

画面上で実際に表示する領域として,ビューポートと呼ばれる矩形領域に画像を当てはめる.

44 第 1章 Javaによるグラフィクス

図 1.8: 平行投影と透視投影

7. ラスタ化 (rasterization)

2次元情報を標本化して,画素情報に変換する.

平行投影:投影方向が平行な投影方法図 1.8左のように 3次元座標を平行に投影する.(JOGL/OpenGLでは,投影方向がスクリーンに垂直となる「直投影」が用いられる).3次元空間で平行な線がスクリーン上でも平行になる.

透視投影:視点を中心とした投影方法図 1.8右のように 3次元座標を視点中心に投影する.カメラや人間の目と似た投影方法であり,自然な画像が得られる.3次元空間で平行な線がスクリーン上では平行にならずに 1点 (消失点)に集まる (どこまでもまっすぐに続く電車のレールを想像してみるとよい).

変換行列機能:透視投影などの変換行列生成機能1.8節では,透視投影や視点変換のために行列を作成したが,OpenGL では次のような関数によって行列を意識せずに変換を実現できる.

void gluPerspective(double fieldOfView, double aspect,

double near, double far);

透視投影の行列を生成・設定する (GL_PROJECTION 行列用)

fieldOfView y方向 (縦方向)の視野角 (画角)

aspect xyのアスペクト比near 手前クリッピング面までの距離far 奥クリッピング面までの距離

void gluLookAt(double ex, double ey, double ez,

double rx, double ry, double rz,

double upx, double upy, double upz);

視点位置と視線方向を設定する (GL_MODEL_VIEW 行列用)

1.8. 3次元グラフィクスの基礎 45

ex,ey,ez 視点の座標rx,ry,rz 注視点 (参照点)の座標upx,upy,upz 画面の上方向ベクトル

基本図形:描画の基本的な図形要素glBegin(図形種類) と glEnd() の間に,頂点 (場合によって,色や法線など)を与えて図形を構築する.図形種類は以下のとおり.

点 GL_POINTS 与えられた点の集合線 GL_LINES 2つ 1組の線分の集合

GL_LINE_STRIP 開いた折れ線GL_LINE_LOOP 閉じた折れ線 (ループ)

面 GL_TRIANGLES 3つ 1組の三角形の集合GL_QUADS 4つ 1組の四角形の集合GL_POLYGON 多角形GL_QUAD_STRIP 四角形 (梯子風)のリボン形状GL_TRIANGLE_STRIP 三角形 (互い違い)のリボン形状GL_TRIANGLE_FAN 三角形の扇型形状

頂点,色,法線などの指定方法

gl{Vertex,Color,Normal}{3,4}{i,f,d}[v]

Vertex,Color,Normal: 頂点座標, 色, 法線ベクトル3,4 : 3次元ベクトル,4次元ベクトルi,f,d : int, float, double

v : 配列利用の有無

Zバッファアルゴリズム:隠面消去の一手法フレームバッファ(各画素メモリ)に色情報 (RGB)とともに,その奥行きの値 (Z)を格納する.フレームバッファ情報の更新 (色塗り)にあたっては,新しい奥行き値と既にフレームバッファに書き込まれている奥行き値とを比較する.新しい奥行き値がフレームバッファの値よりも小さい (手前にある)場合には上書きし,そうでない場合には何もしない.JOGL(OpenGL) では,グラフィクスハードウェアのZバッファアを用いて隠面消去を実現しており,プログラムを書く際に隠面消去を意識する必要はあまりない.たとえば,glEnable(GL DEPTH TEST) (と glEnable(GL CULL FACE)) をコメントアウトすると隠面消去を実行しなくなるので試してみること.ちなみに,線画表示で面を塗りつぶさない場合,Zバッファアルゴリズムは,そのまま利用できない.

背面除去:面の向きと視点との位置関係で可視性を判定する方法図 1.9のように視点を向いた面のみを表示する.この判定は法線ベクトルと視線方向ベクトルとの内積によって実現できる.対象が凸多面体 1個であれば,背面除去で充分である.背面除去も JOGL(OpenGL) に組み込まれている.

46 第 1章 Javaによるグラフィクス

法線ベクトル

法線ベクトル

P0

P1

P2

P3

P0

P1

P2

P3

視点 PV

図 1.9: 背面除去

図 1.10: 立方体のデータ

ベクトルの内積 (·): 2つのベクトルから 1つのスカラーを求める演算3次元ベクトルV0 = [x0 y0 z0]

T , V1 = [x1 y1 z1]T の内積は,次式で与えられる.

V0 · V1 = x0x1 + y0y1 + z0z1

内積には,次の性質がある.

V0 · V1

> 0 V0,V1のなす角がπ

2より小さい

= 0 V0,V1のなす角がπ2に等しい

< 0 V0,V1のなす角がπ2より大きい

ベクトルの外積 (×): 2つの 3次元ベクトルから 1つの 3次元ベクトルを求める演算3次元ベクトルV0 = [x0 y0 z0]

T , V1 = [x1 y1 z1]T の外積は,次式で与えられる.

V0 × V1 =

y0z1 − y1z0

z0x1 − z1x0

x0y1 − x1y0

外積の結果のベクトルは,もとのベクトルV0,V1の張る平面に垂直で,右手の中指に相当する向きのベクトルとなる.

例1:透視投影による立方体の描画 - CubePosition.java

図 1.10のように原点を中心とし一辺の長さが 2.0 の立方体を描画する.頂点番号は,

1.8. 3次元グラフィクスの基礎 47

vertices配列の添字に対応している.各面のデータは faces配列に頂点番号の列として表現されているが,外から見て左回りの順で与えられていることに注意せよ.y軸の正の向きが,画面の上方になるようにしている.視点位置を変えて,様々な方向から眺めた画像を作ってみること.

import java.awt.*;

import java.awt.event.*;

import net.java.games.jogl.*; // joglパッケージのインポート

public class CubePosition implements GLEventListener {

private Frame f; // Frameオブジェクトprotected GL gl; // GLオブジェクトprotected GLU glu; // GLUオブジェクトprotected double eye_x = 4.0, eye_y = 3.0, eye_z = 7.0;

// 視点位置protected CubePosition(String name) { // コンストラクタ

GLCanvas canvas = // JOGL用 GLCanvas作成GLDrawableFactory.getFactory().

createGLCanvas(new GLCapabilities());

canvas.addGLEventListener(this); // GLイベント登録f = new Frame(name); // Frame作成f.add(canvas); // Frameに GLCanvasを登録f.setSize(500, 500); // 幅と高さの設定f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

System.exit(0);

}

});

}

protected CubePosition(String name, String[] args) {

this(name); // 標準コンストラクタの起動eye_x = Double.parseDouble(args[0]); // 視点 (x座標)

eye_y = Double.parseDouble(args[1]); // 視点 (y座標)

eye_z = Double.parseDouble(args[2]); // 視点 (z座標)

}

protected void showFrame() {

f.setVisible(true); // Frameの表示}

48 第 1章 Javaによるグラフィクス

public void init(GLDrawable drawable) { // 初期化gl = drawable.getGL(); // GLオブジェクトglu = drawable.getGLU(); // GLUオブジェクトgl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 背景色gl.glEnable(GL.GL_DEPTH_TEST); // 奥行テスト利用gl.glEnable(GL.GL_CULL_FACE); // 背面除去利用

}

public void reshape(GLDrawable drawable, // ウィンドウサイズ変更時int x, int y, int w, int h) {

final double fieldOfView = 25.0, near = 1.0, far = 20.0;

// カメラ (透視投影)のパラメタ 画角, 前方/後方クリッピング面double aspect = (double) w / (double) h; // 縦横比gl.glMatrixMode(GL.GL_PROJECTION); // 投影変換行列gl.glLoadIdentity(); // 恒等行列設定glu.gluPerspective(fieldOfView, aspect, near, far);

// 透視投影変換行列作成gl.glMatrixMode(GL.GL_MODELVIEW); // 幾何変換行列gl.glLoadIdentity(); // 恒等行列設定glu.gluLookAt(eye_x, eye_y, eye_z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

} // 視点位置設定行列作成

public void display(GLDrawable drawable) { // 描画要求時gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

cube(); // バッファクリア (画面消去)の後,立方体描画}

void cube() { // 立方体の描画float[][] vertices = { { -1.0f, -1.0f, -1.0f },

{ 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f },

{ -1.0f, 1.0f, -1.0f }, { -1.0f, -1.0f, 1.0f },

{ 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f },

{ -1.0f, 1.0f, 1.0f } }; // 頂点座標値int[][] faces = { { 1, 2, 6, 5 }, { 2, 3, 7, 6 }, { 4, 5, 6, 7 },

{ 0, 4, 7, 3 }, { 0, 1, 5, 4 }, { 0, 3, 2, 1 } };

// 各面の頂点番号列float[][] colors = { { 0.0f, 1.0f, 1.0f }, { 1.0f, 0.0f, 1.0f },

{ 1.0f, 1.0f, 0.0f }, { 0.0f, 0.5f, 0.5f },

{ 0.5f, 0.0f, 0.5f }, { 0.5f, 0.5f, 0.0f } };

// 各面の描画色

1.9. 同次座標 (homogeneous coordinates)と行列計算 49

gl.glBegin(GL.GL_QUADS); // 四角形描画開始for (int i = 0; i < faces.length; i++) {

gl.glColor3fv(colors[i]); // 描画色の指定for (int j = 0; j < faces[i].length; j++)

gl.glVertex3fv(vertices[faces[i][j]]);

} // 四角形頂点列の指定gl.glEnd(); // 四角形描画終了

}

public void displayChanged(GLDrawable drawable,

boolean modeChanged, boolean deviceChanged) {

} // 描画モードなどの変更時 (将来)

public static void main(String[] args) {

if (args.length == 3) // 視点指定時(new CubePosition("CubePosition Demo", args)).showFrame();

else // 標準視点(new CubePosition("CubePosition Demo")).showFrame();

}

}

1.9 同次座標 (homogeneous coordinates)と行列計算

3次元グラフィクスでは,様々な座標変換が行なわれるが,同次座標表現や行列計算が有効であり,実際に多用されている.

同次座標:実座標空間の次元を 1つ上げて,同値関係を導入した座標表現3次元空間 P(3) = [x(3) y(3) z(3)]

T ∈ R3 の場合には,4次元座標 P = [x y z w]T ∈ R4

と以下の同値関係∼で構成される.

P(3) ∼ P ⇔ x = x(3)w, y = y(3)w, y = y(3)w, w 6= 0

すなわち,2つの同次座標 [x y z w]T と [ xw

yw

zw

1]T は同値であり,3次元実空間の座

標 [ xw

(3) yw

(3) zw

(3)]Tに等しい.

同次座標を用いると,スケール変換 (拡大縮小)や回転変換だけでなく,平行移動や透視変換 (射影変換)も行列 M で表現可能である.

スケール変換 (拡大縮小):各成分ごとに拡大縮小する行列表現は次のようになる.

P′ =

x′

y′

z′

1

= MP =

sx 0 0 0

0 sy 0 0

0 0 sy 0

0 0 0 1

x

y

z

1

=

sxx

syy

szz

1

50 第 1章 Javaによるグラフィクス

回転変換:各座標軸ごとに θ 回転する回転は,軸の正の側から見て,反時計回りを正の向きとする.行列表現は次のようになる.

x軸回り P′ =

x′

y′

z′

1

= MP =

1 0 0 0

0 cos θ − sin θ 0

0 sin θ cos θ 0

0 0 0 1

x

y

z

1

=

x

y cos θ − z sin θ

y sin θ + z cos θ

1

y軸回り P′ =

x′

y′

z′

1

= MP =

cos θ 0 sin θ 0

0 1 0 0

− sin θ 0 cos θ 0

0 0 0 1

x

y

z

1

=

x cos θ + z sin θ

y

−x sin θ + z cos θ

1

z軸回り P′ =

x′

y′

z′

1

= MP =

cos θ − sin θ 0 0

sin θ cos θ 0 0

0 0 1 0

0 0 0 1

x

y

z

1

=

x cos θ − y sin θ

x sin θ + y cos θ

z

1

平行移動:各成分ごとに平行移動する (ズラす)

行列表現は次のようになる.

P′ =

x′

y′

z′

1

= MP =

1 0 0 tx0 1 0 ty0 0 1 tz0 0 0 1

x

y

z

1

=

x + txy + tyz + tz

1

(透視)射影変換:特定の軸についてスケールを変える

通常は,z座標に応じて x座標と y座標のスケールを反比例させる.行列表現は次のようになる.

z軸方向 P′ =

x′

y′

z′

1

= MP =

1 0 0 0

0 1 0 0

0 0 1 −1d

0 0 1d

0

x

y

z

1

=

x

y

z − 1d

zd

xdzydz

d − 1z

1

結合法則:行列計算は結合法則が成り立つ行列の並ぶ順序さえ変えなければ,どこから計算しても構わない.すなわち,実際に変換する座標値が判明する前に,予め行列部分の計算ができる.

M1M2M3P = M1(M2(M3P)) = (M1M2M3)P

単位行列 (identity matrix):対角成分のみすべて 1の行列単位行列を施した計算結果は,まったくもとの行列となる.その性質から「恒等行列」

1.9. 同次座標 (homogeneous coordinates)と行列計算 51

とも呼ばれる.

IP =

1 0 0 0

0 1 0 0

0 0 1 0

0 0 0 1

x

y

z

1

=

x

y

z

1

= P

例1:透視投影による立方体の描画 - CubeMatrix.java

1.8節の例1のプログラム CubePositionを拡張しており,実際に内容はまったく同じだが,GL_MODEL_VIEW行列 を 1つずつ計算している.

import net.java.games.jogl.*; // joglパッケージのインポート

public class CubeMatrix extends CubePosition {

protected CubeMatrix(String name) {

super(name); // 上位コンストラクタの起動}

protected CubeMatrix(String name, String[] args) {

super(name, args); // 上位コンストラクタの起動}

public void reshape(GLDrawable drawable, // ウィンドウサイズ変更時int x, int y, int w, int h) {

final double fieldOfView = 25.0, near = 1.0, far = 20.0;

// カメラ (透視投影)のパラメタ 画角, 前方/後方クリッピング面double aspect = (double) w / (double) h; // 縦横比gl.glMatrixMode(GL.GL_PROJECTION); // 投影変換行列gl.glLoadIdentity(); // 恒等行列設定glu.gluPerspective(fieldOfView, aspect, near, far);

// 透視投影変換行列作成gl.glMatrixMode(GL.GL_MODELVIEW); // 幾何変換行列gl.glLoadIdentity(); // 恒等行列設定double eye_xz = Math.sqrt(eye_x * eye_x + eye_z * eye_z);

double eye_xyz = Math.sqrt(eye_xz * eye_xz + eye_y * eye_y);

double[] translate = { // 平行移動行列 (z軸)

1.0f, 0.0f, 0.0f, 0.0f,

0.0f, 1.0f, 0.0f, 0.0f,

0.0f, 0.0f, 1.0f, 0.0f,

0.0f, 0.0f, -eye_xyz, 1.0f

};

gl.glMultMatrixd(translate); // 平行移動行列の適用

52 第 1章 Javaによるグラフィクス

double sinP = eye_y / eye_xyz, cosP = eye_xz / eye_xyz;

double[] rotateX = { // 回転行列 (x軸)

1.0f, 0.0f, 0.0f, 0.0f,

0.0f, cosP, sinP, 0.0f,

0.0f, -sinP, cosP, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

gl.glMultMatrixd(rotateX); // 回転行列の適用double sinT = eye_x / eye_xz, cosT = eye_z / eye_xz;

double[] rotateY = { // 回転行列 (y軸)

cosT, 0.0f, sinT, 0.0f,

0.0f, 1.0f, 0.0f, 0.0f,

-sinT, 0.0f, cosT, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

gl.glMultMatrixd(rotateY); // 回転行列の適用}

public static void main(String[] args) {

if (args.length == 3) // 視点指定時(new CubeMatrix("CubeMatrix Demo", args)).showFrame();

else // 標準視点(new CubeMatrix("CubeMatrix Demo")).showFrame();

}

}

変換行列の退避 (複写):スタックに現在の行列を退避するOpenGLでは行列を 1つずつ (GL_PROJECTION 行列 と GL_MODEL_VIEW 行列)しか持たない.複数の物体に対して異なる変換を施したい場合には,行列をスタックに一時退避することで実現する.OpenGLの行列は,左側から (物体に作用させる場合は後のものから)積をとるので,個別の部品の配置などを調整でる.たとえば,

1. 恒等行列2. 全体 (部品A + 部品B) の変換行列3. 行列の push → 部品Aの局所配置の行列 → 行列の pop

4. 行列の push → 部品Bの局所配置の行列 → 行列の pop

とすると,部品Aには「部品Aの局所配置行列 × 全体の変換行列」,部品Bには「部品Bの局所配置行列 × 全体の変換行列」が施される.実際の利用法については,1.10

節の例3を参照して欲しい.

1.9. 同次座標 (homogeneous coordinates)と行列計算 53

例2:角度指定による立方体の描画 - CubeAngle.java

例1のプログラムと似ているが,必ず距離 10だけ離れることとし,角度を与えて表示している.

import net.java.games.jogl.*; // joglパッケージのインポート

public class CubeAngle extends CubePosition {

private double fieldOfView = 25.0, near = 1.0, far = 20.0;

// カメラ (透視投影)のパラメタ 画角, 前方/後方クリッピング面private float rotx = 20.0f, roty = -30.0f, rotz = 0.0f;

// x軸,y軸,z軸回りの回転角CubeAngle(String name) { // コンストラクタ

super(name); // 上位コンストラクタの起動}

CubeAngle(String name, String[] args) {

super(name); // 上位コンストラクタの起動switch (args.length) {

case 1: // 画角指定fieldOfView = Double.parseDouble(args[0]); break;

case 2: // クリッピング面指定near = Double.parseDouble(args[0]);

far = Double.parseDouble(args[1]); break;

case 3: // 回転角指定rotx = Float.parseFloat(args[0]); // x軸回りroty = Float.parseFloat(args[1]); // y軸回りrotz = Float.parseFloat(args[2]); // z軸回り

}

}

public void reshape(GLDrawable drawable, // ウィンドウサイズ変更時int x, int y, int w, int h) {

double aspect = (double) w / (double) h; // 縦横比gl.glMatrixMode(GL.GL_PROJECTION); // 投影変換行列gl.glLoadIdentity(); // 恒等行列設定glu.gluPerspective(fieldOfView, aspect, near, far);

// 透視投影変換行列作成gl.glMatrixMode(GL.GL_MODELVIEW); // 幾何変換行列gl.glLoadIdentity(); // 恒等行列設定gl.glTranslatef(0.0f, 0.0f, -10.0f); // 平行移動 (z軸方向)

}

54 第 1章 Javaによるグラフィクス

public void display(GLDrawable drawable) { // 描画要求時gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

// バッファクリア (画面消去)

gl.glPushMatrix(); // 行列の退避 (複写)

gl.glRotatef(rotx, 1.0f, 0.0f, 0.0f); // 回転 (x軸回り)

gl.glRotatef(roty, 0.0f, 1.0f, 0.0f); // 回転 (y軸回り)

gl.glRotatef(rotz, 0.0f, 0.0f, 1.0f); // 回転 (z軸回り)

cube(); // 立方体描画gl.glPopMatrix(); // 退避行列の回復

}

public static void main(String[] args) {

if (args.length > 0 && args.length < 4) // 投影法指定時(new CubeAngle("CubeAngle Demo", args)).showFrame();

else // 標準の投影法(new CubeAngle("CubeAngle Demo")).showFrame();

}

}

1.10 アニメーションとマウス入力

JOGLでは高速な描画能力を利用して,アニメーションを実現できる.

Animator:アニメーションのためのオブジェクトAnimatorオブジェクトを実行することで,連続した描画要求が発せられて (displayメソッドが起動されて),アニメーションが実現される.

マウス入力: JOGLではアニメーションとして実現するマウス入力と描画は別のスレッド (プログラムの流れ)で処理されるが,マウス入力スレッドから JOGLにはアクセスできないし,repaint要求を出すこともできない.マウス入力に相当した回転や移動情報を状態変数 (インスタンス変数)として持ち,アニメーションすることによって回転や移動を反映させる.

ディスプレイリスト:描画命令のハードウェア格納複雑な形状だと (非常に頂点数が多かったり)すると,形状データをグラフィクスハードウェアに転送するオーバヘッドが大きくなる.予め形状データをハードウェアに格納 (登録)し,それを呼び出すことで高速な描画が実現できる

例1:物体の回転プログラム - ObjectRotate.java

さまざまな物体を表示し,マウスで回転させる抽象クラスであり,抽象メソッドobject()

を実装することで実行可能となる.

1.10. アニメーションとマウス入力 55

import java.awt.*;

import java.awt.event.*;

import net.java.games.jogl.*; // joglパッケージのインポート

public abstract class ObjectRotate implements GLEventListener {

private Frame f; // Frameオブジェクトprotected GL gl; // GLオブジェクトprotected GLU glu; // GLUオブジェクトprivate double fieldOfView = 25.0, near = 1.0, far = 20.0;

// カメラ (透視投影)のパラメタ 画角, 前方/後方クリッピング面protected float rotx = 0.0f, roty = 0.0f, rotz = 0.0f;

// x軸,y軸,z軸回りの回転角private int prevMouseX, prevMouseY; // 直前のマウス位置private int object; // ディスプレイリスト番号

protected abstract void object(); // 具体的な描画命令

public ObjectRotate(String name) { // コンストラクタGLCanvas canvas = // JOGL用 GLCanvas作成

GLDrawableFactory.getFactory().

createGLCanvas(new GLCapabilities());

canvas.addGLEventListener(this); // GLイベント登録f = new Frame(name); // Frame作成f.add(canvas); // Frameに GLCanvasを登録f.setSize(500, 500); // 幅と高さの設定final Animator animator = new Animator(canvas);

// Animator作成f.addWindowListener(new WindowAdapter() {// 無名オブジェクト作成

public void windowClosing(WindowEvent e) {

animator.stop(); // Animator終了System.exit(0);

}

});

animator.start(); // Animator実行}

protected void showFrame() {

f.setVisible(true); // Frameの表示}

56 第 1章 Javaによるグラフィクス

public void init(GLDrawable drawable) { // 初期化gl = drawable.getGL(); // GLオブジェクトglu = drawable.getGLU(); // GLUオブジェクトgl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 背景色gl.glEnable(GL.GL_DEPTH_TEST); // 奥行テスト利用gl.glEnable(GL.GL_CULL_FACE); // 背面除去利用

object = gl.glGenLists(1); // ディスプレイリスト作成gl.glNewList(object, GL.GL_COMPILE); // 登録開始object(); // 描画命令gl.glEndList(); // 登録終了

drawable.addMouseListener(new MouseAdapter() {

public void mousePressed(MouseEvent e) {

prevMouseX = e.getX(); // マウスボタンプレス時prevMouseY = e.getY(); // マウス位置の記録

}

});

drawable.addMouseMotionListener(new MouseMotionAdapter() {

public void mouseDragged(MouseEvent e) {

int x = e.getX(); // マウスドラッグ時int y = e.getY(); // 現マウス位置取得Dimension size = e.getComponent().getSize();

// ウィンドウサイズ取得float thetaY = 360.0f*((float)(x-prevMouseX)/size.width);

float thetaX = 360.0f*((float)(prevMouseY-y)/size.height);

rotx -= thetaX; // 相対移動量からroty += thetaY; // 回転角を算出して加算prevMouseX = x; // マウス位置の更新prevMouseY = y;

}

});

}

public void reshape(GLDrawable drawable, // ウィンドウサイズ変更時int x, int y, int w, int h) {

double aspect = (double) w / (double) h; // 縦横比gl.glMatrixMode(GL.GL_PROJECTION); // 投影変換行列gl.glLoadIdentity(); // 恒等行列設定glu.gluPerspective(fieldOfView, aspect, near, far);

1.10. アニメーションとマウス入力 57

// 透視投影変換行列作成gl.glMatrixMode(GL.GL_MODELVIEW); // 幾何変換行列gl.glLoadIdentity(); // 恒等行列設定gl.glTranslatef(0.0f, 0.0f, -10.0f); // 平行移動 (z軸方向)

}

public void display(GLDrawable drawable) { // 描画要求時gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

// バッファクリア (画面消去)

gl.glPushMatrix(); // 行列の退避 (複写)

gl.glRotatef(rotx, 1.0f, 0.0f, 0.0f); // 回転 (x軸回り)

gl.glRotatef(roty, 0.0f, 1.0f, 0.0f); // 回転 (y軸回り)

gl.glRotatef(rotz, 0.0f, 0.0f, 1.0f); // 回転 (z軸回り)

gl.glCallList(object); // ディスプレイリスト呼出しgl.glPopMatrix(); // 退避行列の回復

}

public void displayChanged(GLDrawable drawable,

boolean modeChanged, boolean deviceChanged) {

} // 描画モードなどの変更時 (将来)

}

例2:立方体の回転プログラム - CubeRotate.java

例1のプログラム ObjectRotateを利用することで,立方体をマウスによって簡単に回転できる

import net.java.games.jogl.*; // joglパッケージのインポート

public class CubeRotate extends ObjectRotate {

public CubeRotate(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出しrotx = 20.0f; // 初期回転角 (x軸)

roty = -30.0f; // 初期回転角 (y軸)

}

protected void object() { // 立方体の描画float[][] vertices = { { -1.0f, -1.0f, -1.0f },

{ 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f },

{ -1.0f, 1.0f, -1.0f }, { -1.0f, -1.0f, 1.0f },

{ 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f },

{ -1.0f, 1.0f, 1.0f } }; // 頂点座標値

58 第 1章 Javaによるグラフィクス

int[][] faces = { { 1, 2, 6, 5 }, { 2, 3, 7, 6 }, { 4, 5, 6, 7 },

{ 0, 4, 7, 3 }, { 0, 1, 5, 4 }, { 0, 3, 2, 1 } };

// 各面の頂点番号列float[][] colors = { { 0.0f, 1.0f, 1.0f }, { 1.0f, 0.0f, 1.0f },

{ 1.0f, 1.0f, 0.0f }, { 0.0f, 0.5f, 0.5f },

{ 0.5f, 0.0f, 0.5f }, { 0.5f, 0.5f, 0.0f } };

// 各面の描画色gl.glBegin(GL.GL_QUADS); // 四角形描画開始for (int i = 0; i < faces.length; i++) {

gl.glColor3fv(colors[i]); // 描画色の指定for (int j = 0; j < faces[i].length; j++)

gl.glVertex3fv(vertices[faces[i][j]]);

} // 四角形頂点列の指定gl.glEnd(); // 四角形描画終了

}

public static void main(String[] args) {

(new CubeRotate("Cube Rotate")).showFrame();

}

}

例3:複合形状の回転プログラム - ConesWire.java

gl.glPushMatrix()と gl.glPopMatrix()を利用することで,複数の形状の相対的な配置を定めることが可能となる.また,ワイヤーフレーム (線画)の場合には,Zバッファアルゴリズムでは隠線消去できないことに注意せよ.

import net.java.games.jogl.*; // joglパッケージのインポート

public class ConesWire extends ObjectRotate {

public ConesWire(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出し

}

protected void object() { // 複合形状の描画final int NOP = 16; // 配置する個数float[] foreground = { 1.0f, 1.0f, 0.0f }; // 描画色 (黄)

gl.glColor3fv(foreground); // 描画色の設定for (int i = 0; i < NOP; i++) {

double t = 2.0 * Math.PI * i / NOP; // 2PIの n等分gl.glPushMatrix(); // 行列の退避 (複製)

gl.glTranslated(1.6*Math.cos(t), 1.6*Math.sin(t), 0.0);

1.10. アニメーションとマウス入力 59

// 半径 1.6の円周上に配置gl.glScalef(0.3f, 0.3f, 1.4f); // スケーリングconeWire(); // ワイヤフレームの円錐gl.glPopMatrix(); // 退避行列の回復

}

}

protected void coneWire() {

final int NOP = 16; // 円錐底面の頂点数float[] apex = { 0.0f, 0.0f, 1.0f }; // 円錐先端頂点の座標float[][] circle = new float[NOP][]; // 円錐底面の頂点座標for (int i = 0; i < NOP; i++) {

double t = 2.0 * Math.PI * i / NOP; // 2PIの n等分circle[i] = new float[3]; // 底面頂点座標の計算circle[i][0] = (float)Math.cos(t);

circle[i][1] = (float)Math.sin(t);

circle[i][2] = -1.0f;

}

gl.glBegin(GL.GL_LINE_LOOP); // 円錐底面の描画開始for (int i = 0; i < NOP; i++) // 線画の順序は無意味

gl.glVertex3fv(circle[NOP-i-1]); // 一応反時計回りgl.glEnd(); // 円錐底面の描画終了gl.glBegin(GL.GL_LINES); // 円錐側面の描画開始for (int i = 0; i < NOP; i++) {

gl.glVertex3fv(apex); // 先端頂点gl.glVertex3fv(circle[i]); // 底面頂点

}

gl.glEnd(); // 円錐側面の描画終了}

public static void main(String[] args) {

(new ConesWire("Cones Wire")).showFrame();

}

}

Zバッファによる隠線消去:面と線を描くことによって隠線を消去する描画色で線を描く一方で,背景色で面を描く.Zバッファには面の奥行情報が保持されるため,遠くの線は消去される.実際には線と面が同じ奥行値の場合,数値誤差の影響で線が消えてしまう可能性があるため,面はやや奥にオフセットして描画する.

例4:隠線消去のプログラム - ConesHiddenLine.java

60 第 1章 Javaによるグラフィクス

ConesWireを拡張し,面を背景色で描くことで,隠線消去を実現する.背面除去のために,法線方向 (外側)から見て,反時計回りになるように頂点を与えていることに注意せよ.

import net.java.games.jogl.*; // joglパッケージのインポート

public class ConesHiddenLine extends ConesWire {

public ConesHiddenLine(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出し

}

public void init(GLDrawable drawable) { // 初期化super.init(drawable); // 上位クラスの初期化gl.glEnable(GL.GL_POLYGON_OFFSET_FILL); // 面のオフセットgl.glPolygonOffset(1.0f, 1.0f); // 面のオフセット量

}

protected void object() { // 複合形状の描画final int NOP = 16; // 配置する個数float[] background = { 0.0f, 0.0f, 0.0f }; // 背景色 (黒)

float[] foreground = { 1.0f, 1.0f, 0.0f }; // 描画色 (黄)

for (int i = 0; i < NOP; i++) {

double t = 2.0 * Math.PI * i / NOP; // 2PIの n等分gl.glPushMatrix(); // 行列の退避 (複製)

gl.glTranslated(1.6*Math.cos(t), 1.6*Math.sin(t), 0.0);

// 半径 1.6の円周上に配置gl.glScalef(0.3f, 0.3f, 1.4f); // スケーリングgl.glColor3fv(foreground); // 描画色の設定coneWire(); // ワイヤフレームの円錐gl.glColor3fv(background); // 描画色の設定conePolygon(); // 面による円錐gl.glPopMatrix(); // 退避行列の回復

}

}

private void conePolygon() {

final int NOP = 16; // 円錐底面の頂点数float[] apex = { 0.0f, 0.0f, 1.0f }; // 円錐先端頂点の座標float[][] circle = new float[NOP+1][]; // 円錐底面の頂点座標for (int i = 0; i <= NOP; i++) {

double t = 2.0 * Math.PI * i / NOP; // 2PIの n等分

1.10. アニメーションとマウス入力 61

circle[i] = new float[3]; // 底面頂点座標の計算circle[i][0] = (float)Math.cos(t);

circle[i][1] = (float)Math.sin(t);

circle[i][2] = -1.0f;

}

gl.glBegin(GL.GL_POLYGON); // 円錐底面の描画開始for (int i = 0; i < NOP; i++)

gl.glVertex3fv(circle[NOP-i-1]); // 反時計回りgl.glEnd(); // 円錐底面の描画終了gl.glBegin(GL.GL_TRIANGLE_FAN); // 円錐側面の描画開始gl.glVertex3fv(apex); // 先端頂点for (int i = 0; i <= NOP; i++) { // 底面頂点

gl.glVertex3fv(circle[i]); // 反時計回り}

gl.glEnd(); // 円錐側面の描画終了}

public static void main(String[] args) {

(new ConesHiddenLine("Cones HiddenLine")).showFrame();

}

}

色の補間: JOGL(OpenGL)は色を補間する頂点ごとに色を指定すると,線や面を描く際に,色を連続に補間する.

色立体:色の知覚は 3自由度であり,立体に対応するRGBやCMY表色系であれば,それぞれの値が独立かつ直交すると考えて,立方体に対応づけられる.一方,HSB(HSV)表色系の場合には,色相が円周状になるので色相円盤が積み重なった円錐形状に対応づけられる.

例5:RGB色立体の描画 - ColorCube.java

import net.java.games.jogl.*; // joglパッケージのインポート

public class ColorCube extends ObjectRotate {

public ColorCube(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出し

}

protected void object() { // RGB色立体の描画float[][] vertices = { { -1.0f, -1.0f, -1.0f },

{ 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f },

62 第 1章 Javaによるグラフィクス

{ -1.0f, 1.0f, -1.0f }, { -1.0f, -1.0f, 1.0f },

{ 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f },

{ -1.0f, 1.0f, 1.0f } }; // 頂点座標値float[][] colors = { { 0.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f },

{ 1.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f },

{ 0.0f, 0.0f, 1.0f }, { 1.0f, 0.0f, 1.0f },

{ 1.0f, 1.0f, 1.0f }, { 0.0f, 1.0f, 1.0f } };

// 各面の頂点番号列int[][] faces = { { 1, 2, 6, 5 }, { 2, 3, 7, 6 }, { 4, 5, 6, 7 },

{ 0, 4, 7, 3 }, { 0, 1, 5, 4 }, { 0, 3, 2, 1 } };

// 各面の描画色gl.glBegin(GL.GL_QUADS); // 四角形描画開始for (int i = 0; i < faces.length; i++) {

for (int j = 0; j < faces[i].length; j++){

gl.glColor3fv(colors[faces[i][j]]);

gl.glVertex3fv(vertices[faces[i][j]]);

} // 四角形頂点列の指定}

gl.glEnd(); // 四角形描画終了}

public static void main(String[] args) {

(new ColorCube("Color Cube")).showFrame();

}

}

1.11 シェーディング

これまでは色を直接に指定していたが,実際の環境では対象の材質や光の当たり方で色が変わってくる.映画館では白いスクリーンに色のついた光が当たることによって,映画の画面が表示される.仮に赤いスクリーンであれば赤のモノクロ画面,黒いスクリーンであれば単に真っ黒な画面となってしまう.光の反射を考慮して色をつける作業をシェーディング (陰づけ)と呼ぶ.

反射特性:光を反射する性質実在の物質の反射特性は,「環境成分」「拡散成分」「鏡面成分」の3つの要素に分けて考えられる.実際の計算は,それぞれの成分ごとにRGB値を算出して和をとる.

環境成分 (ambient):間接光によるぼんやりとした明るさに伴う色空間には光が満ちている.すなわち,光源からの直接光だけではなく,他の物質からの

1.11. シェーディング 63

n

LV

光源

視点

法線ベクトル

θα M

図 1.11: シェーディング

反射によって与えられる光 (反射光)による色あいがこれに相当する.環境成分は,次式で与えられる.

Iaka

ただし,Iaは光の環境成分,kaは材質の環境成分の反射率.

拡散成分 (diffuse):光源に対する向きで決定する色映画館のスクリーンのように光の当たり方 (強さと向き)によって決定し,観察する位置に依存しない色.光源への方向ベクトルLと面の法線ベクトル nのなす角 θ の余弦cos θによって定まる (図 1.11参照).2つのベクトル L,nが正規化されていれば (単位ベクトルであれば),ベクトルの内積を求めれば良い.ランバート (Lambert)則と呼ばれ,次式で与えられる.

Idkd cos θ = IdkdnL

ただし,Idは光の拡散成分,kdは材質の拡散成分の反射率.

鏡面成分 (specular):面と光源,視点の関係で決定する色金属やプラスチックなどの光沢に相当するもので,光の当たる向きと観察する位置に依存する.光源への方向ベクトルLと面の法線ベクトル nによって決まる正反射の方向を中心に比較的狭い領域で強くなる (図 1.11参照).そこで,光源方向と視点方向の中間方向のベクトルM = (L + V)/|L + V|と法線ベクトル nのなす角 αの余弦 cos α

の冪乗で与えられる.2つのベクトルが正規化されていれば,次式で計算される.

Iskscosnα = Isks(nM)n

ただし,Isは光の拡散成分,ksは材質の拡散成分の反射率,nは減衰を表す定数 (shininess

に相当する).

輝度 (色)計算:OpenGLでは輝度 (色)の計算を自動的に実行する各点における輝度は,RGB ごとに次式で計算される.

Iaka + IdkdnL + Isks(nM)n

次のようなメソッドが用意されている.

64 第 1章 Javaによるグラフィクス

void glLightfv(enum light, enum pname, const float *params);

ライトを設定するlight 照明の指定 (GL_LIGHTi)

pname 光の種類 (GL.GL_AMBIENT, GL.GL_DIFFUSE, GL.GL_SPECULARなど)

params 指定値の配列

void glMaterialfv(enum face, enum pname, const float *params);

材質を設定する (これ以降の vertex の反射特性を指定する)

face 面の指定 (GL.GL_FRONT, GL.GL_BACK, GL.GL_FRONT_AND_BACK)

pname 反射の種類 (GL.GL_AMBIENT, GL.GL_DIFFUSE, GL.GL_SPECULARなど)

params 指定値の配列

void glNormal3dv(double v[3]);

法線ベクトルを指定する (これ以降の vertex の法線を指定する)

v 法線ベクトル (基本的には単位ベクトルを与える)

gl.glEnable(GL.GL_NORMALIZE) 指定をすれば正規化しなくてもよい

例1:シェーディングによる表示 - ObjectShade.java

ObjectRotateクラスを拡張し,さまざまな物体をシェーディング表示する抽象クラス.抽象メソッド object()を実装して利用する.

import net.java.games.jogl.*; // joglパッケージのインポート

public abstract class ObjectShade extends ObjectRotate {

protected float[] lightPosition = { 5.0f, 5.0f, 10.0f, 0.0f };

// 光源位置protected float[] lightAmbient = { 0.2f, 0.2f, 0.2f, 1.0f };

// 光源の環境成分protected float[] lightDiffuse = { 1.0f, 1.0f, 1.0f, 1.0f };

// 光源の拡散成分protected float[] lightSpecular = { 0.9f, 0.9f, 0.9f, 1.0f };

// 光源の鏡面成分public ObjectShade(String name) { // コンストラクタ

super(name); // 上位コンストラクタ呼出し}

public void init(GLDrawable drawable) { // 初期化super.init(drawable); // 上位クラス initの呼出しgl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, lightPosition);

// 光源位置指定gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, lightAmbient);

1.11. シェーディング 65

// 環境成分指定gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, lightDiffuse);

// 拡散成分指定gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, lightSpecular);

// 鏡面成分指定gl.glEnable(GL.GL_LIGHTING); // シェーディング有効化gl.glEnable(GL.GL_LIGHT0); // ライト有効化gl.glEnable(GL.GL_NORMALIZE); // ベクトル正規化

}

}

例2:立方体のシェーディング表示 - CubeShade.java

ObjectShadeクラスを拡張している.形状とともに,法線ベクトルと反射特性 (material)

を指定している.

import net.java.games.jogl.*; // joglパッケージのインポート

public class CubeShade extends ObjectShade {

public CubeShade(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出しrotx = 20.0f; // 初期回転角 (x軸)

roty = -30.0f; // 初期回転角 (y軸)

}

protected void object() { // 立方体の描画float[][] vertices = { { -1.0f, -1.0f, -1.0f },

{ 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f },

{ -1.0f, 1.0f, -1.0f }, { -1.0f, -1.0f, 1.0f },

{ 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f },

{ -1.0f, 1.0f, 1.0f } }; // 頂点座標値int[][] faces = { { 1, 2, 6, 5 }, { 2, 3, 7, 6 }, { 4, 5, 6, 7 },

{ 0, 4, 7, 3 }, { 0, 1, 5, 4 }, { 0, 3, 2, 1 } };

// 各面の頂点番号列float[][] normals = { { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f },

{ 0.0f, 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f },

{ 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } };

// 各面の法線ベクトルfloat[] diffuse = { 0.8f, 0.8f, 0.2f, 1.0f };

// 反射特性の拡散成分float[] specular = { 0.9f, 0.9f, 0.9f, 1.0f };

// 反射特性の鏡面成分

66 第 1章 Javaによるグラフィクス

gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, diffuse);

// 拡散成分の指定gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specular);

// 鏡面成分の指定gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 100.0f);

// shininessの指定gl.glShadeModel(GL.GL_FLAT); // フラットシェーディングgl.glBegin(GL.GL_QUADS); // 四角形描画開始for (int i = 0; i < faces.length; i++) {

gl.glNormal3fv(normals[i]); // 法線ベクトル (面ごと)

for (int j = 0; j < faces[i].length; j++)

gl.glVertex3fv(vertices[faces[i][j]]);

} // 四角形頂点列の指定gl.glEnd(); // 四角形描画終了

}

public static void main(String[] args) {

(new CubeShade("Cube Shade")).showFrame();

}

}

例3:正 20面体のシェーディング表示 - IcosahedronShade.java

ObjectShadeクラスを拡張している.

import net.java.games.jogl.*; // joglパッケージのインポート

public class IcosahedronShade extends ObjectShade {

public IcosahedronShade(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出し

}

protected void object() { // 正 20面体の描画float[] diffuse = { 0.8f, 0.3f, 0.8f, 1.0f };

// 反射特性の拡散成分float[] specular = { 0.9f, 0.9f, 0.9f, 1.0f };

// 反射特性の鏡面成分gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, diffuse);

// 拡散成分の指定gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specular);

// 鏡面成分の指定gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 100.0f);

1.11. シェーディング 67

// shininessの指定final float R = (float)Math.sqrt(2.0); // 外接球の半径final float X = (float)((Math.sqrt(5.0) + 1.0) / 2.0);

final float Y = (float)(R / Math.sqrt(1.0 + X*X));

final float Z = (float)(X * Y);

float[][] vertices = // 正 20面体の頂点座標{ { 0.0f, Y, Z }, { 0.0f, -Y, Z }, { 0.0f, -Y, -Z },

{ 0.0f, Y, -Z }, { Z, 0.0f, Y }, { Z, 0.0f, -Y },

{ -Z, 0.0f, -Y }, { -Z, 0.0f, Y }, { Y, Z, 0.0f },

{ -Y, Z, 0.0f }, { -Y, -Z, 0.0f }, { Y, -Z, 0.0f } };

int[][] faces = // 各面の頂点番号列{ { 0, 1, 4 }, { 0, 4, 8 }, { 0, 8, 9 }, { 0, 9, 7 },

{ 0, 7, 1 }, { 1, 10, 11 }, { 1, 11, 4 }, { 4, 11, 5 },

{ 4, 5, 8 }, { 8, 5, 3 }, { 8, 3, 9 }, { 9, 3, 6 },

{ 9, 6, 7 }, { 7, 6, 10 }, { 7, 10, 1 }, { 2, 10, 6 },

{ 2, 6, 3 }, { 2, 3, 5 }, { 2, 5, 11 }, { 2, 11, 10} };

gl.glShadeModel(GL.GL_FLAT); // フラットシェーディングgl.glBegin(GL.GL_TRIANGLES); // 三角形描画開始for (int i = 0; i < faces.length; i++) {

gl.glNormal3fv(normal(vertices[faces[i][0]],

vertices[faces[i][1]], vertices[faces[i][2]]));

// 法線ベクトル (面ごと)

for (int j = 0; j < faces[i].length; j++)

gl.glVertex3fv(vertices[faces[i][j]]);

} // 三角形頂点列の指定gl.glEnd(); // 三角形描画終了

}

private float[] normal(float[] pnt0, float[] pnt1, float[] pnt2) {

return crossProduct(subtract(pnt1, pnt0), subtract(pnt2, pnt0));

} // 法線ベクトルの計算 - 2辺の方向ベクトルの外積

private float[] subtract(float[] vec0, float[] vec1) {

float[] answer = new float[3]; // ベクトルの差分計算for (int i = 0; i < 3; i++)

answer[i] = vec0[i] - vec1[i];

return answer;

}

private float[] crossProduct(float[] vec0, float[] vec1) {

68 第 1章 Javaによるグラフィクス

float[] answer = new float[3]; // ベクトルの外積計算answer[0] = vec0[1] * vec1[2] - vec0[2] * vec1[1];

answer[1] = vec0[2] * vec1[0] - vec0[0] * vec1[2];

answer[2] = vec0[0] * vec1[1] - vec0[1] * vec1[0];

return answer;

}

public static void main(String[] args) {

(new IcosahedronShade("Icosahedron Shade")).showFrame();

}

}

例4:近似球面のシェーディング表示 - SubdivideShade.java

正 20面体をもとに三角形を再帰的に分割することによって球面を近似する.三角形の各辺を 2等分するとともに,その点を外接球にまで持ち上げる.1回の分割で三角形は4つの小三角形に分割される.形は球面に近づくが,人間の目のマッハバンド効果 (隣接の差分をとる効果)によって,あくまでも多面体として見えてしまう.

import net.java.games.jogl.*; // joglパッケージのインポート

public class SubdivideShade extends ObjectShade {

static int depth = 0; // 再帰レベルの深さ

public SubdivideShade(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出し

}

protected void object() { // 近似球面の描画float[] diffuse = { 0.8f, 0.3f, 0.8f, 1.0f };

// 反射特性の拡散成分float[] specular = { 0.9f, 0.9f, 0.9f, 1.0f };

// 反射特性の鏡面成分gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, diffuse);

// 拡散成分の指定gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specular);

// 鏡面成分の指定gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 100.0f);

// shininessの指定final float R = (float)Math.sqrt(2.0); // 外接球の半径final float X = (float)((Math.sqrt(5.0) + 1.0) / 2.0);

1.11. シェーディング 69

final float Y = (float)(R / Math.sqrt(1.0 + X*X));

final float Z = (float)(X * Y);

float[][] vertices = // 20面体の頂点座標{ { 0.0f, Y, Z }, { 0.0f, -Y, Z }, { 0.0f, -Y, -Z },

{ 0.0f, Y, -Z }, { Z, 0.0f, Y }, { Z, 0.0f, -Y },

{ -Z, 0.0f, -Y }, { -Z, 0.0f, Y }, { Y, Z, 0.0f },

{ -Y, Z, 0.0f }, { -Y, -Z, 0.0f }, { Y, -Z, 0.0f } };

int[][] faces = // 各面の頂点番号列{ { 0, 1, 4 }, { 0, 4, 8 }, { 0, 8, 9 }, { 0, 9, 7 },

{ 0, 7, 1 }, { 1, 10, 11 }, { 1, 11, 4 }, { 4, 11, 5 },

{ 4, 5, 8 }, { 8, 5, 3 }, { 8, 3, 9 }, { 9, 3, 6 },

{ 9, 6, 7 }, { 7, 6, 10 }, { 7, 10, 1 }, { 2, 10, 6 },

{ 2, 6, 3 }, { 2, 3, 5 }, { 2, 5, 11 }, { 2, 11, 10} };

for (int i = 0; i < faces.length; i++) {

subdivide(vertices[faces[i][0]], vertices[faces[i][1]],

vertices[faces[i][2]], depth);

} // 正 20面体を再帰的に分割して近似球を得る}

protected void subdivide

(float[] pnt0, float[] pnt1, float[] pnt2, int depth) {

if (depth == 0) { // 最終レベルgl.glShadeModel(GL.GL_FLAT); // フラットシェーディングgl.glBegin(GL.GL_TRIANGLES); // 三角形の描画開始gl.glNormal3fv(normal(pnt0, pnt1, pnt2)); // 法線ベクトルgl.glVertex3fv(pnt0); // 頂点座標値gl.glVertex3fv(pnt1);

gl.glVertex3fv(pnt2);

gl.glEnd(); // 三角形の描画終了}

else { // 更に再帰する場合float[] pnt01 = split(pnt0, pnt1); // 辺を 2等分してシフトfloat[] pnt12 = split(pnt1, pnt2);

float[] pnt20 = split(pnt2, pnt0);

subdivide(pnt0, pnt01, pnt20, depth-1);

subdivide(pnt1, pnt12, pnt01, depth-1);

subdivide(pnt2, pnt20, pnt12, depth-1);

subdivide(pnt01, pnt12, pnt20, depth-1);

} // 4つの小三角形に分割}

70 第 1章 Javaによるグラフィクス

private float[] normal(float[] pnt0, float[] pnt1, float[] pnt2) {

float[] answer = new float[3]; // 簡易型法線ベクトル計算for (int i = 0; i < 3; i++)

answer[i] = pnt0[i] + pnt1[i] + pnt2[i];

return answer; // 三角形の重心方向}

protected float[] split(float[] pnt0, float[] pnt1) {

float[] ans = new float[3]; // 辺の分割for (int i = 0; i < 3; i++) // 中点方向の計算

ans[i] = pnt0[i] + pnt1[i];

float ratio = (float)(Math.sqrt(2.0) /

Math.sqrt(ans[0]*ans[0]+ans[1]*ans[1]+ans[2]*ans[2]));

for (int i = 0; i < 3; i++) // 大きさを外接球半径にans[i] *= ratio;

return ans;

}

public static void main(String[] args) {

if (args.length == 1)

depth = Integer.parseInt(args[0]);

(new SubdivideShade("Subdivide Shade")).showFrame();

}

}

スムーズシェーディング:平面を曲面であるかのように描画する技術描画色は面の法線ベクトルによって決定されるため,多面体 (多角形)の頂点ごとに法線を与えて,個々の点での色を補間すると多角形の内部 (の法線)が滑らかに変化しているように表示される.スムーズシェーディングには,各頂点の法線を補間して面上のすべての点で色 (輝度)を計算するフォン (Phong)補間と,各頂点で色 (輝度)を計算してそれを補間するグロー(Gouraud)補間の 2通りがある.画像の質としては光沢が鮮やかに出る点でフォン補間の方が優れているが,計算速度の観点ではグロー補間の方が優れており,OpenGLの標準設定では後者 (グロー補間)を用いている.

例5:近似球のスムーズシェーディング - SphereShade.java

例 4の正 20面体の細分割表示をもとに,各頂点に法線ベクトルを指定して,球のスムーズシェーディング表示を得る.原点を中心とする球面上の法線は,球面上の座標がそのまま利用できる.

import net.java.games.jogl.*;

1.11. シェーディング 71

public class SphereShade extends SubdivideShade {

public SphereShade(String name) { // コンストラクタsuper(name); // 上位コンストラクタ呼出し

}

protected void subdivide

(float[] pnt0, float[] pnt1, float[] pnt2, int depth) {

if (depth == 0) { // 最終レベルgl.glShadeModel(GL.GL_SMOOTH); // スムーズシェーディングgl.glBegin(GL.GL_TRIANGLES); // 三角形の描画開始gl.glNormal3fv(pnt0); // 法線ベクトル (頂点ごと)

gl.glVertex3fv(pnt0); // 頂点座標値gl.glNormal3fv(pnt1);

gl.glVertex3fv(pnt1);

gl.glNormal3fv(pnt2);

gl.glVertex3fv(pnt2);

gl.glEnd(); // 三角形の描画終了}

else { // 更に再帰する場合float[] pnt01 = split(pnt0, pnt1); // 辺を 2等分してシフトfloat[] pnt12 = split(pnt1, pnt2);

float[] pnt20 = split(pnt2, pnt0);

subdivide(pnt0, pnt01, pnt20, depth-1);

subdivide(pnt1, pnt12, pnt01, depth-1);

subdivide(pnt2, pnt20, pnt12, depth-1);

subdivide(pnt01, pnt12, pnt20, depth-1);

} // 4つの小三角形に分割}

public static void main(String[] args) {

if (args.length == 1)

depth = Integer.parseInt(args[0]);

(new SphereShade("Sphere Shade")).showFrame();

}

}

73

第2章 情報システム科学V課題

2.1 例題プログラムの実行

プリント中の以下のプログラムを実行して,実行結果について考察しなさい.1.1節 例 1, 2, 3, 4, 5

2.2 ダイアモンドパターンの描画

図 2.1のように正 n角形の全対角線を描いた図形をダイヤモンドパターンと呼ぶ.ダイヤモンドパターンの表示するには,円周上に均等に置かれた n点から全ての 2点の組を選んで線分を描けばよい.ヒント:円を描くプログラム (1.1節 例4を参考に,円周上の点の座標値 x と y を一旦すべて配列に格納してから,線分を描画するとプログラムは簡単になる.

図 2.1: ダイアモンドパターン

74 第 2章 情報システム科学V課題

2.3. 例題プログラムの実行 75

2.3 例題プログラムの実行

プリント中の以下のプログラムを実行して,実行結果について考察しなさい.1.2節 例 1, 2, 3

2.4 色の3原色 (減法混色/CMY 表現)

1.2節の例1「光の 3原色」のプログラムを参考に,「色の 3原色」のプログラムを作成せよ.「色の 3原色 (CMY 表現)」の図は,図 2.2のようになるはずである.この絵は「光の 3原色(RGB表現)」の図 1.4に酷似している.(ヒント:(1.1)式を参考に,プログラムの変更は 5

行未満で済むはず)

x=300, y=225

x=386.6, y=375x=213.4, y=375

Cyan(1 0 0)

Magenta(0 1 0)

Yellow(0 0 1)

Blue(1 1 0)

Red(0 1 1)

Green(1 0 1)

Black(1 1 1)

White(0 0 0)

図 2.2: 色の 3原色 (CMY 表現)

2.5 HSB表現を用いた色相円

色相円の円周上での色の変化は,色相の変化に他ならない.HSB表現を用いる (S = 1, B = 1

に固定する)ことで,色相円を簡単に描けるはずである.1.1節の例4「円の描画」,1.2節の例2「色相円」,1.2節の例3「HSB表現と色円盤」などのプログラムを参考に,HSB表現を利用した色相円のプログラムを作成せよ.

2.6 明度を変化させた色円盤

1.2節の例3「HSB表現と色円盤」のプログラムでは,中心に向かって彩度が落ちていき,中心で白色になっていた.ここでは図 2.3のように,中心に向かって明度が落ちていき,中心で黒色になる色円盤のプログラムを作成せよ.

76 第 2章 情報システム科学V課題

図 2.3: 明度を変化させた色円盤

2.7. 例題プログラムの実行 77

2.7 例題プログラムの実行

1.3節 例 2, 4, 5, 6, 7

2.8 マウスによる円の変更

1.3節の例 4「マウスによる線分の描画」を参考に,次のような動きをする Canvasプログラムを作成せよ.

1. マウスボタンをプレスしてドラッグすると円を描く2. ボタンをプレスした位置が円の中心となる3. マウスをドラッグすると,円はカーソル位置を通過するように拡大縮小する4. 円の描画には,次のように drawOval メソッドを用いるとよい

getGraphics().drawOval(左上 x座標, 左上 y座標, 直径, 直径)

参考:プログラムの解読 (余力のある者だけで良い)

以下のプログラム (CFIVEからダウンロードできる)を解読して,その働きを説明せよ.また,余力のあるものは,プログラムを改良せよ.

import java.awt.*;

import java.awt.event.*;

public class ClickSpeed extends Canvas implements MouseListener {

private static final int width = 400; // Canvasの幅private static final int height = 400; // Canvasの高さprivate static final int SIZE = 10; // 正方形の 1辺static final int TIMES = 10;

static long fastest = -1;

int count = 0;

long time;

int x, y;

boolean error = false;

private ClickSpeed() {

super(); // Canvasのコンストラクタ実行setSize(width, height); // 幅と高さの設定setBackground(Color.white); // 背景色の設定 (白)

addMouseListener(this); // Listenerの設定

Frame f = new Frame("ClickSpeed"); // Frameの作成f.add(this); // Frameに Canvaを登録

78 第 2章 情報システム科学V課題

f.pack(); // 配置の確定f.show(); // Frameの表示f.addWindowListener(new WindowAdapter() {// 無名オブジェクトの作成

public void windowClosing(WindowEvent e) {

System.exit(0); // ウィンドウ閉鎖で終了}

});

}

public void paint(Graphics g) {

if (count > 0) { // 残りカウント有Dimension d = getSize(); // Canvasサイズ獲得x = (int) ((d.width - SIZE) * Math.random());

y = (int) ((d.height - SIZE) * Math.random());

// マーカ位置の決定 (乱数)

if (error)

g.setColor(Color.RED); // 描画色 - 赤 (ミス)

else

g.setColor(Color.BLACK); // 描画色 - 黒 (ヒット)

g.fillRect(x, y, SIZE, SIZE); // マーカ描画}

}

public void mousePressed(MouseEvent me) {

if (count > 0) { // 残りカウント有if ((me.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {

// ボタン 1(左)押下int xc = me.getX(); // カーソル位置の x座標int yc = me.getY(); // カーソル位置の y座標if ((x < xc) && (xc < (x + SIZE)) && (y < yc)

&& (yc < (y + SIZE))) { // マーカ内count--; // 残りカウント減error = false; // ヒット

}

else { // マーカ外count++; // 残りカウント増error = true; // ミス

}

if (count > 0) { // 残りカウント有System.out.println("" + count + " more!!"); // 残り数表示

2.8. マウスによる円の変更 79

repaint(); // 画像更新要求}

else { // 残りカウント無time += System.currentTimeMillis(); // 時間測定if ((fastest < 0) || (fastest > time))

fastest = time; // 最速判定System.out.println("Finished in " + (time / 1000.0) + " secs. "

+ "Fastest time: " + (fastest / 1000.0) + " secs.");

} // 時間表示}

}

else { // ボタン 1(左)以外押下if (fastest < 0) // 最速記録の有無

System.out.println("Click right button to start.");

else

System.out.println("Click right button to start. Fastest time: "

+ (fastest / 1000.0) + " secs.");

}

}

public void mouseReleased(MouseEvent me) {

if ((me.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {

// ボタン 3(右)離上→開始count = TIMES; // カウント設定System.out.println("Start clicking ... " + TIMES + " more!!");

// 開始表示error = false; // ヒット状態time = -System.currentTimeMillis(); // 開始時刻確認repaint(); // 画像更新要求

}

}

public void mouseEntered(MouseEvent me) { }

public void mouseExited(MouseEvent me) { }

public void mouseClicked(MouseEvent me) { }

public static void main(String[] args) {

new ClickSpeed(); // Canvasオブジェクトの作成}

}

80 第 2章 情報システム科学V課題

2.9. 例題プログラムの実行 81

2.9 例題プログラムの実行

1.4節 例 1, 2, 3, 4, 5 1.5節 例 1, 2

2.10 ドラゴン曲線のプログラム

1.5節の例 1「コッホ曲線」を参考に,ドラゴン曲線を描くプログラムを作成せよ.なおドラゴン曲線は 1ステップごとに,1辺が長さ...の 2辺になっている.2辺の並び方 (それぞれの向き)に注意すること.また,ドラゴン曲線のハウスドルフ次元を計算し,そこから想像されることを述べよ.

82 第 2章 情報システム科学V課題

2.11. 例題プログラムの実行 83

2.11 例題プログラムの実行

1.6節 例 1, 2, 3, 4, 5

2.12 cycloid曲線と trochoid曲線

図 2.4中央のように直線上を円が転がるとき,円周上の点が移動する軌跡 (曲線)を cycloid

曲線と呼び,円周からややズレた点が移動する軌跡 (曲線)を trochoid曲線と呼ぶ.trochoid

曲線の媒介変数表現は,次のようになる.

x = aθ − b sin θ

y = a − b cos θ

各変数 a, b, θ が表す値については,図 2.5を参照せよ.cycloid曲線は a = b となる trochoid

曲線の特殊な場合と考えられる.1.6節の例 3「三角関数の描画」を参考に,cycloid曲線と trochoid曲線 (b = 0.8a, a, 1.2a)を描くプログラムを作成せよ.

2.13 マウスによるベジエ曲線の変更

次のようにマウスで自由に形状を変更できる 3次ベジエ曲線のプログラムを書くこと.

• マウスの左ボタンを 4回押して,制御点の位置を定める.

• 制御点の位置が決まると 3次ベジエ曲線と制御多角形 (制御点を線分で結んだもの)を表示する.

• マウスの左ボタンで制御点をドラッグして,ベジエ曲線を変形する.

• マウスの右ボタンでクリアする.(あってもなくても可)

(すでにベジエ曲線を表示するメソッドがあるので)BezierCanvasを拡張すると良い.

84 第 2章 情報システム科学V課題

図 2.4: cycloid曲線と trochoid曲線

x

y

thetaa b

b

図 2.5: trochoid曲線の表現

2.14. 例題プログラムの実行 85

2.14 例題プログラムの実行

1.8節 例 1 1.9節 例 1, 2

2.15 視点位置とデプステスト

1.8節の例1「透視投影法による立方体の描画」において,さまざまな視点からの画像を作成してみよ.また,gl.glEnable(GL_DEPTH_TEST)と gl.glEnable(GL_CULL_FACE)をコメントアウトしてから,さまざま視点からの画像を表示してみよ.

2.16 立体データの意味

1.8節の例1「透視投影法による立方体の描画」において,立方体の面情報 facesを書き換えてみよ.たとえば,最初の { 1, 2, 6, 5 } を { 1, 2, 5, 6 } や { 5, 6, 2, 1 } に変更すると何が起きるか確認してみよ.

2.17 画角とクリッピング面の効果

1.9節の例2「角度指定による立方体の描画」において,画角 fieldOfViewとクリッピング面 near, farの指定を変えてみよ.特にクリッピング面については,前方/後方のクリッピング面の位置として 9.5, 10, 10.5 などの値を試してみよ.

86 第 2章 情報システム科学V課題

2.18. 例題プログラムの実行 87

2.18 例題プログラムの実行

1.10節 例 1, 2, 3, 4, 5

2.19 立方体の隠線表示

1.10節の例4「隠線消去のプログラム」を参考に,立方体を隠線消去表示してみよ.

2.20 HSB(HSV)色立体の描画

1.10節の例5「RGB色立体の描画」を参考に,HSB(HSV)色立体を描画してみよ.HSB(HSV)

色立体は円錐状になるが,底面は中央部を白(R=1, G=1, B=1)としたいので,GL.GL_POLYGONではなく GL.GL_TRIANGLE_FANを用いよ.

88 第 2章 情報システム科学V課題

2.21. 例題プログラムの実行 89

2.21 例題プログラムの実行

1.11節 例 1, 2, 3, 4, 5

2.22 複雑な立体の描画

複数の立体を組み合わせて,複雑な形を作ってみよう.GLUTライブラリを用いると,トーラス (ドーナッツ型)など基本的な形状を簡単に作ることができる.たとえば,次のような短いプログラムを試してみよ.

import net.java.games.jogl.*;

import net.java.games.jogl.util.*;

public class GlutToriShade extends ObjectShade {

public GlutToriShade(String name) {

super(name);

}

protected void object() {

float[] diffuse = { 0.2f, 0.7f, 0.8f, 1.0f };

float[] specular = { 0.9f, 0.9f, 0.9f, 1.0f };

gl.glShadeModel(GL.GL_FLAT);

gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE, diffuse);

gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specular);

gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 100.0f);

GLUT glut = new GLUT();

gl.glPushMatrix();

gl.glTranslatef(1.1f, 0.0f, 0.0f);

glut.glutSolidTorus(gl, 0.20, 0.80, 16, 32);

gl.glPopMatrix();

gl.glPushMatrix();

gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);

glut.glutSolidTorus(gl, 0.20, 0.80, 16, 32);

gl.glPopMatrix();

gl.glPushMatrix();

gl.glTranslatef(-1.1f, 0.0f, 0.0f);

glut.glutSolidTorus(gl, 0.20, 0.80, 16, 32);

gl.glPopMatrix();

}

90 第 2章 情報システム科学V課題

public static void main(String[] args) {

new GlutToriShade("GlutTori Shade");

}

}