Dockerの使い方

イントロダクション

ラズパイにRedmineをインストールして使用しようと考えております。
しかし、記事を検索しながら、いろいろとやってみると、どれもこれもエラーがでて使えない。。。結局Dockerをちゃんと学習することにしました。

参考サイト

DockerDocsを参考にします。

タイトルは「Raspberry Pi OS (32 ビット) に Docker エンジンをインストールする」です。

Dockerとは?

いろいろと記事を斜め読みしてみると下のようなものということです。

Dockerはコンテナ仮想化を用いたOSレベルの仮想化によりアプリケーションを開発・実行環境から隔離し、アプリケーションの素早い提供を可能にする。

色々な説明があるけど、結局はこういうことだと思う。

つまり、「コンテナ」にまとめたアプリケーションを仮想マシンを使用して実行できる。ということらしい

Docker事始め

次のコマンドで、不要なもの、古いDockerをアンインストールします。もしかしたらインストールしているかもしれないからです。

よくあるのが、デフォルトインストールしているかもしれません。。。

for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done

DockerのAPTリポジトリセットアップ

コードを書くと下のようになります。

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Set up Docker's APT repository:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/raspbian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

ラズパイを更新

下のコマンドで、ラズパイの更新します。

sudo apt-get update

コマンドのインストール

下のコマンドをインストールします。既にインストールしているときは「すでに最新バージョン」というメッセージを確認できます。

  • ca-certificates
  • gnupg
  • curl
sudo apt-get install ca-certificates curl gnupg

Dockeraptリポジトリをセットアップ準備

GPGコマンドで暗号化・復号を行えるようにする?

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Dockeraptリポジトリをセットアップ

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/raspbian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

最後に更新

sudo apt-get update

Dockerパッケージインストール

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

インストールできたら、起動確認「ハローワールド」を実行する

sudo service docker start
sudo docker run hello-world

Dockerコマンドを使う

Dockerをコマンドで扱うときの基本構文は下のようになっています。※こちらのページ参照

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

DockerFileを使う

こちらのページを参照しています。※英語だったので翻訳してみました。
下のようなコマンドがあるのでそれを使用するということらしい。。。
Dockerのコマンド入力の基本としてコマンド名は大文字、引数は小文字で書くようです。

docker ADD argument...
コマンド名 意味
ADD ホスト上のソースから、設定された宛先にあるコンテナー独自のファイルシステムにファイルをコピーします。
CMD コンテナー内で特定のコマンドを実行するために使用できます。
ENTRYPOINT は、イメージを使用してコンテナーが作成されるたびに使用されるデフォルトのアプリケーションを設定します。
ENV 環境変数を設定します。
EXPOSE 特定のポートを関連付けて、コンテナと外部の間のネットワークを可能にします。
FROM ビルド プロセスの開始に使用されるベース イメージを定義します。
MAINTAINER イメージ作成者のフルネームと電子メール アドレスを定義します。
RUN Dockerfile の中心的な実行ディレクティブです。
USER コンテナを実行する UID (またはユーザー名) を設定します。
VOLUME コンテナからホスト マシン上のディレクトリへのアクセスを可能にするために使用されます。
WORKDIR CMD で定義されたコマンドが実行されるパスを設定します。
LABEL Docker イメージにラベルを追加できます。

試しにコマンドを実行する

下のようにディレクトリを作成します。作成先は初期ディレクトリ(ユーザーのディレクトリ)から「dockerbuild」というディレクトリを作成しました。

mkdir ~/dockerbuild

Dockerfileを作成します。

nano Dockerfile

下の内容をコピペします。

FROM ubuntu:latest
MAINTAINER NAME EMAIL

RUN apt-get -y update
RUN apt-get -y upgrade
RUN apt-get install -y build-essential

Where NAME is your full name and EMAIL is your email address.

そして、以下のようにコマンドをたたきます。

docker build -t "NAME:Dockerfile" .


エラーがあるけど、イメージ作製はできているようです。

「NAME」には好きな名前を入れてよいです。最後の「.」を忘れないようにしましょう。

これができたら下のようなコマンドをたたきます。

docker images

使用できるイメージの一覧が表示されます。

Redmineのインストール

こちらのサイトを参考にします。
手順は以下の通りです。

  1. docker-compose.ymlを作成する
  2. docker-composeコマンドを実行する
    docker-compose up -d

しかしうまくいかない。。。

no matching manifest for linux/arm/v7 in the manifest list entries

DBの指定とYMLの作成

結局docker-compose.ymlの記述がまずかったみたいだ。「hypriot/rpi-mysql」を指定してやれば問題なかった。
つまり、下の様に書くとOK
<docker-compose.yml>

version: '3.8'
services:
  redmine:
    image: redmine
    container_name: redmine
    ports:
      - 3000:3000
    volumes:
      - ./data/plugins:/usr/src/redmine/plugins
      - ./data/themes:/usr/src/redmine/public/themes
    environment:
      REDMINE_DB_MYSQL: redmine-db
      REDMINE_DB_PASSWORD: redmine
    depends_on:
      - redmine-db
    restart: always

  redmine-db:
    image: hypriot/rpi-mysql
    container_name: redmine-db
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: redmine
      MYSQL_DATABASE: redmine
    volumes:
      - ./data/db:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode>
    restart: always

しかしポート番号が。。。

for redmine-db  Cannot start service redmine-db: driver failed programming external connectivity on endpoint redmine-db (af1d40![](http://zenryokuservice.com/wp/wp-content/uploads/2023/11/Docker4.png)9b7e96abcb6b0d803518c400dd6f2bca79ef35fced14fd6a20b3c2b9cf): Error starting userland proxy: listen tcp4 0.0.0.0:3306: bind: address already in use

つまりは、「DBが使用するポート番号が使用されているため、DBが動かせない」ということでした。

使用ポートを調べる

「ss」コマンドで調べます。実行すると下のような結果が出ました。

pi@raspberrypi:~/dockerbuild/redmine $ ss -atn
State   Recv-Q  Send-Q   Local Address:Port        Peer Address:Port   Process  
LISTEN  0       5              0.0.0.0:5900             0.0.0.0:*               
LISTEN  0       128          127.0.0.1:631              0.0.0.0:*               
LISTEN  0       128            0.0.0.0:22               0.0.0.0:*               
LISTEN  0       80           127.0.0.1:3306             0.0.0.0:*               
ESTAB   0       0            127.0.0.1:60324          127.0.0.1:34449           
ESTAB   0       0            127.0.0.1:34449          127.0.0.1:60324           
ESTAB   0       0        169.254.37.79:5900     169.254.221.230:55375           
LISTEN  0       5                 [::]:5900                [::]:*               
LISTEN  0       511                  *:80                     *:*               
LISTEN  0       128               [::]:22                  [::]:*               
LISTEN  0       128              [::1]:631                 [::]:*  

まずは、使用しているポートが80番で待機(LISTENING)しているサービスと言えば「apache2」があやしい。。。ウェブサーバーは大体80を使っていることが多いので。。。下のコマンドでサービスを確認

service --status-all


そんなわけで、アパッチ(apache2)を停止し用と思ったけど、ポートを変更して対応します。

YMLファイルの編集

ポート番号を3306から3307に変更しました。

  redmine-db:
    image: hypriot/rpi-mysql
    container_name: redmine-db
    ports:
      - 3307:3307
    environment:
      MYSQL_ROOT_PASSWORD: redmine
      MYSQL_DATABASE: redmine
    volumes:
      - ./data/db:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode>
    restart: always

起動することができました。

しかし、現状では不要なアプリケーションが沢山動いているようなので、ブラウザで開けませんでした。。。
他のサービスなどを停止すればよいのですが、また後日。。。

ラズパイの準備

Redmineが動いたので、今度はラズパイの運用準備を行います。
大まかに行うことは以下の通りです。

  • 使用しないGPUのメモリ割り当てを減らす
  • 使わないアプリの停止

メモリ使用量確認

  • free -hコマンド:メモリの利用状況を確認
  • df -h: ファイル・システム内のフリー・スペースの量を表示する

デーモンを停止する

デーモンとは、画面上に見えないアプリケーションのことです。停止するのは以下のデーモンです。

  • alsa-utils
  • dbus
  • triggerhappy
  • cups
プロセルの確認方法
ps aux

上記のコマンドで確認できます。

サービス一覧を取得する

下のコマンドで一覧できます。

service --status-all

ここで表示されたサービスの名前を停止するのには下のように行います。

[ - ]  alsa-utils
 [ - ]  apache-htcacheclean
 [ + ]  apache2
 [ - ]  apparmor
 [ + ]  avahi-daemon
 [ + ]  binfmt-support
 [ + ]  bluetooth
 [ - ]  cgroupfs-mount
 [ - ]  console-setup.sh
 [ + ]  cron
 [ + ]  cups
 [ + ]  cups-browsed
 [ + ]  dbus
 [ + ]  docker
 [ + ]  dphys-swapfile
 [ + ]  fake-hwclock
 [ - ]  fio
 [ - ]  hwclock.sh
 [ - ]  keyboard-setup.sh
 [ + ]  kmod
 [ + ]  lightdm
 [ + ]  mariadb
 [ + ]  networking
 [ - ]  nfs-common
 [ - ]  paxctld
 [ - ]  plymouth
 [ + ]  plymouth-log
 [ + ]  procps
 [ - ]  pulseaudio-enable-autospawn
 [ + ]  raspi-config
 [ + ]  rng-tools-debian
 [ - ]  rpcbind
 [ - ]  rsync
 [ + ]  rsyslog
 [ - ]  saned
 [ + ]  ssh
 [ - ]  sudo
 [ - ]  triggerhappy
 [ + ]  udev
 [ - ]  x11-common

サービスの停止方法

下記コマンドでサービスの停止

sudo systemctl サービス名

無効にする

sudo systemctl disable サービス名

実行したコマンド

<サービスの停止>

sudo systemctl stop alsa-utirls
sudo systemctl stop dbus
sudo systemctl stop triggerhappy.service triggerhappy.service
sudo systemctl stop triggerhappy.socket
sudo systemctl stop cups

<サービス自動起動無効>

sudo systemctl disable alsa-utirls
sudo systemctl disable dbus
sudo systemctl disable triggerhappy.service triggerhappy.service
sudo systemctl disable triggerhappy.socket
sudo systemctl disable cups

設定の反映を行うのに再起動します。

sudo reboot

結果

<サービス停止前>

               total        used        free      shared  buff/cache   available
Mem:           921Mi       328Mi        55Mi        10Mi       538Mi       526Mi
Swap:          384Mi        68Mi       316Mi

<サービス停止後>

               total        used        free      shared  buff/cache   available
Mem:           921Mi       376Mi        78Mi       8.0Mi       466Mi       479Mi
Swap:          376Mi       1.0Mi       375Mi

起動スクリプトを作る

まずは、Shellスクリプトである宣言をする

# !/bin/sh

処理を作成する(自分の場合です。)

cd ~/dockerbuild/redmine/

起動する設定を行う

参考サイトはこちらです。
/etc/systemd/system/ ディレクトリに autorun.serviceを、以下のように作成する

[Unit]
Description=Execute at OS startup and terminates
After=network.target
[Service]
Type=oneshot
ExecStart=/home/pi/autorun.sh
ExecStop=/home/pi/os_term.sh
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

After=network.target はネットワーク関連のプロセスが起動した後に当サービスを起動するという意味らしいです。
サービスの有効化

sudo systemctl daemon-reload
sudo systemctl enable autorun.service

Redmineの起動確認

Redmineの使い方

まずは、初期設定を行う。

チケットのトラッカー

チケットのトラッカーを追加してやります。
上部のメニューにある「管理」をクリックしてから、下のような画面を開くことができます。

ここをクリックして、下のような画面を表示できますので「新しいトラッカー」をクリックして作成します。
ここでは、下のように今後作成するチケットの種類(カテゴリ)を作成します。

チケットのステータス

同様に、上部のメニューにある「管理」をクリックしてから、下のような画面を開くことができます。

同様に、赤枠の部分をクリックしてやると下のような画面が開けます。

これはすでに入力した後なのですが、こんな感じで、「ステータス(状態)」を登録することで、作成するチケットの状態を指定します。
つまりは、「このチケットは<ステータス>に対応した工程の作業」という意味です。

優先度

これも同様に、「選択肢の値」を選択します。

下のように、「新しい値」をクリックして追加してやります。

チケットの作成

チケットのトラッカーを追加してやると下のように、「新しいチケット」をクリックすることができます。これが見当たらないときは設定がたりない状態ということです。

これで、チケットを作成して、アイティアと行いたい作業が混同することを防げるであろう。
まぁこれ流行ってみないことにはどうしようもないな。。。

でわでわ。。。

Java Bag fix ~ArithmeticException in BigDecimal~

ArithmeticExceptionが出ました。

これは、割り切れない処理を行ったときにです用です。
JavaDocを参照すると「1 / 3」を行うと出力されるようです。

【実行コード】

BigDecimal three = new BigDecimal(1);
BigDecimal four = new BigDecimal(3);
System.out.println(three.divide(four));

【エラーログ】

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

    at java.math.BigDecimal.divide(BigDecimal.java:1693)
    at jp.zenryoku.sample.statics.BigDecimalSample.test01(BigDecimalSample.java:14)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)

解決策

スケール指定と、まるめ算の指定を行うことで解決しました。

※注意スケールをセットしたら、そのBigDecimalクラスを返却します。

BigDecimal left = new BigDecimal(1);
BigDecimal right = new BigDecimal(3);
BigDecimal setScale = left.setScale(2);
BigDecimal res = setScale.divide(right, BigDecimal.ROUND_HALF_UP);
System.out.println(res.toString());

下のコードだとスケールを設定していない形になるのでエラーが出る。

BigDecimal left = new BigDecimal(1);
BigDecimal right = new BigDecimal(3);
left.setScale(2);
BigDecimal res = setScale.divide(right, BigDecimal.ROUND_HALF_UP);
System.out.println(res.toString());

Java Swing ゲーム作りや学習に画面をつくる

イントロダクション

Javaを学習して、基本文法がわかると今度は、Java API、つまりはListインターフェースなど既存のクラスを使用する事を学習すると思います。
ここら辺から、学習がうまく進まなくなる。自分がそうでした。。。

今思い返すと、何かしらのアプリケーションを作ってみるのが一番良いのだけど、「何かを作るほどの理解がない」「イマイチ自身が。。。」などと感じる人のもいるかもしれません。

しかし、まずはプログラミングを楽しみましょうという気持ちでSwingしてみませんか?
ちなみに、Jazzの世界ではカッコイイ演奏をすることを「Swingしてるねぇ!」といいます。関連は全くありません(笑)

ちなに、インスタンス=コンポーネントです。つまりラベルが表示されているというのはラベルがインスタンス化されているということです。
目に見えないインスタンスが目に見えマス。※目に見えないものもありますので注意が必要です。

Java Swing

Oracleのページでは、下のように書いていました。
Swingはグラフィカル・ユーザー・インタフェース(GUI)を構築し、豊富なグラフィック機能および双方向性をJavaアプリケーションに追加するコンポーネントのセットを実装しています。

Swingコンポーネントは、全体がJavaプログラミング言語により実装されています。プラガブルなルック・アンド・フィールにより、プラットフォーム間で同じ外観になるGUI、または現在のOSプラットフォーム(Microsoft Windows、Solaris(tm)、Linuxなど)のルック・アンド・フィールを想定したGUIが作成できます。

そして、チュートリアルもあるようです。

まずは、ここのチュートリアルを行い自分で学習していく土台を作っていくのも一つだと思います。

Swingの仕組み

Swingフレームワークでは、コンテナー(Container)にコンポーネント(Component)をセットして、それを表示するという仕組みを持っています。
単純に、画面上に「Hello World」と表示するプログラムを書くと下のようになります。

<プログラムコード1>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.getContentPane().add(new JLabel("Hello World."));
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

処理内容

JFrame frame = new JFrame();の部分は、トップレベルコンテナーのJFrameクラスをインスタンス化しています。
そして、トップレベルコンテナーは以下の3つがあります。

JFrameについて

このうちJFrameクラスを使用するサンプルコードが良く見受けられます。上記のサンプルもこのJFrameを使用しています。
ちょっと長いですが、オラクルのドキュメントの説明が良いと思います。


Frame は、タイトルと境界線を持つトップレベル ウィンドウです。フレームのサイズには、ボーダー用に指定されたすべての領域が含まれます。この方法を使用して境界領域の寸法を得ることができるgetInsets。境界領域はフレーム全体のサイズに含まれているため、境界はフレームの一部を効果的に覆い隠し、サブコンポーネントのレンダリングおよび/または表示に使用できる領域を、左上隅の位置が 、 、および の長方形に制限し(insets.leftますinsets.top)。width - (insets.left + insets.right)のサイズは ですheight - (insets.top + insets.bottom)。
クラスのインスタンスとして実装されるフレームは JFrame、境界線やタイトルなどの装飾を持ち、ウィンドウを閉じるかアイコン化するボタン コンポーネントをサポートするウィンドウです。通常、GUI を使用するアプリケーションには、少なくとも 1 つのフレームが含まれます。アプレットもフレームを使用することがあります。
別のウィンドウに依存するウィンドウを作成するには (たとえば、別のウィンドウがアイコン化されると非表示になります)、dialogの代わりにa を使用しますframe.。別のウィンドウ内に表示されるウィンドウを作成するには、内部フレームを使用します。

まぁなんとなくの理解でもよいと思います。

コンポーネントの追加

frame.getContentPane().add(new JLabel("Hello World."));はラベル、テキストフィールド、ボタンなどのコンポーネントを追加する
時のコードです。つまり、このコードの引数にラベル以外のものもセットできるというわけです。

ちなみに、色付きのコードと下のコードは同じことを行っています。

Container cont = frame.getContentPane();
cont.add(new Label("Hello World."));

一行で書くか2行で書くかの違いです。細かいところでは、Containerのインスタンスを変数「cont」にセットしています。
複数回同じ処理をするのであれば、変数に取り出したインスタンスを使用するのがエコなコードになります。

処理の内容としては、以下の通りです。

  1. JFrameからコンテナーを取得する
  2. 取得したコンテナーにコンポーネント(ラベル)を追加する

閉じるときの処理設定

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);画面を閉じるときの処理です。
のコードを書くことで画面を閉じるとアプリケーションが終わります。

単純に追加

単純にコンポーネントを追加すると下のようになります。
<プログラムコード2>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."));
        cont.add(new JLabel("*************"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("*************"));

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

<実行結果>

一番最後に追加したコンポーネントのみが表示されています。おそらく、コンテナーに一つのコンポーネントしか登録できないのでしょう。
なので、JPanelクラスを使用してコンポーネントを複数追加します。

JPanelを使う

上のコードを下の様に書き換えます。
<プログラムコード3>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();

        Container cont = frame.getContentPane();
        JPanel panel = new JPanel();
        cont.add(panel);

        panel.add(new JLabel("Hello World."));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

<実行結果>

コードのように四角っぽいのを表示したかったのですが、横並びですね。。。

これは、デフォルトで設定されているレイアウトマネージャ(XXXLayoutクラス)がFlowLayoutになっているので、横並びになります。

BorderLayoutについて

デフォルトで設定されているレイアウトクラスBorderLayoutクラスです。下の図のようにレイアウトを組むことができます。
<レイアウト図1>

上のコードでは、レイアウトの指定(NORTH, CENTERなど)を使用していないので、余計変な形になっています。

レイアウトを使う

<プログラムコード4>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

BoxLayoutというクラスも出てきましたが、このクラスに関しては、さておきにして下のように表示されました。
<実行結果>

ポイントは、次のような部分です。

  1. JFrameのコンストラクターの引数に「Window Test」と追加したのでウィンドウのタイトル部分に表示されている。
  2. JPanelにBoxレイアウトを設定して、縦に追加する形を作ったので壮丁通りの表示になった。
  3. setSize()メソッドを使用して、JFrameのサイズを指定している。

レイアウトを色々試す

上記の表示コンポーネントに、いろいろと装飾を加えてみることにします。

コンポーネントにボーダーを入れてみる。

コンポーネント(表示している部品)に、ボーダーを入れるには次のメソッドを使用します。
JComponent#setBorder()を使用してボーダーを追加します。しかし表示しているJPanelのメソッドではありません。
それでも問題はありません。JavaDocを見ればわかるように、JPanelはJComponentのサブクラス(子供)になっています。
つまり、JComponentのメソッドを使用することができます。※privateのメソッドは使えません。

パネルにボーダーを入れる

プログラムコード4を修正して次のプログラムコード5を作成します。

<プログラムコード5>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

実行結果は下の通りです。

ボーダーが真ん中らへんに1本入っただけです。。。これは、レイアウト図1のNORTHとCENTERにそれぞれ、ラベルとパネルを設定しているためNORTHとCENTERの境目にボーダーが票う辞された程度になっています。もっとわかりやすくほかのものも追加してみます。

<プログラムコード6>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        JLabel west = new JLabel("東");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);

        JLabel east = new JLabel("西");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

上記のように、東西南北と中心の5つの領域にそれぞれのコンポーネントをセットしてやる形の表示ができました。

簡単なリファクタリング

今度はBoxLayoutを使用してみます。ここで、プログラムのコード量が増えてきたので、コードを分割してみようと思います。
コードを分割する手段として使えるのは「メソッドを使う」というところです。
具体的には次のようにやります。

1.メソッドの処理を言葉で表現する。

筆者の表現したものなので、「これが正解!」ってことではないので注意してください。それぞれの人にそれぞれの言葉があるように表現の仕方は無限大にあります

  1. JFrameをインスタンス化
  2. BorderLayoutをセット
  3. 北(NORTH)にJLabel「Hello World」をセット
  4. 中央にJPanelをセット
  5. 東。。。
  6. 西。。。
  7. 南。。。

※性格が出るんですね。。。

このように、東西南北、中央の5つに各描画処理を行っているので、そのように処理を分割しようと思います。
メソッド名は、わかりやすいとおもうので「中央を描く」「北を描く」。。。のようにつけることにします。

2.表現したように分割

東西南北、中央の5つに分割することにしたので、次のようなメソッドを作成しました。まだからの状態です。引数および返り値もありません。
そのため、ビルドエラーが出ます。説明の段階を踏むためなので、ご了承ください。
<プログラムコード7>

public class SwingMain {
    ...
    /** 北を描く */
    private void darwNorth() {
    }
    /** 中央を描く */
    private void darwNCenter() {
    }
    /** 西を描く */
    private void darwWest() {
    }
    /** 東を描く */
    private void darwEast() {
    }
    /** 南を描く */
    private void drawSouth() {
    }
}

そして、単純に既存のコードを移植します。
<プログラムコード8>

public class SwingMain {
    public static void main(String[] args) {
        SwingMain main = new SwingMain();
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        main.darwNorth();
        main.darwNCenter();
        main.darwWest();
        main.darwEast();
        main.drawSouth();

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }

    /** 北を描く */
    private void darwNorth() {
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);
    }
    /** 中央を描く */
    private void darwNCenter() {
        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);
    }
    /** 西を描く */
    private void darwWest() {
        JLabel west = new JLabel("西");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);
    }
    /** 東を描く */
    private void darwEast() {
        JLabel east = new JLabel("東");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

    }
    /** 南を描く */
    private void drawSouth() {
        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

    }
}

この状態でもエラーが出ます。それは、Containerが宣言されているのは、メインメソッドだからです。

ならば、引数にContainerをわたしてやればよいのでわ?とおもうのでそのようにします。
<プログラムコード9>

package jp.zenryoku.swing;

import javax.swing.*;
import java.awt.*;

public class SwingMain {
    public static void main(String[] args) {
        SwingMain main = new SwingMain();
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        main.darwNorth(cont);
        main.darwNCenter(cont);
        main.darwWest(cont);
        main.darwEast(cont);
        main.drawSouth(cont);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }

    /** 北を描く */
    private void darwNorth(Container cont) {
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);
    }
    /** 中央を描く */
    private void darwNCenter(Container cont) {
        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);
    }
    /** 西を描く */
    private void darwWest(Container cont) {
        JLabel west = new JLabel("西");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);
    }
    /** 東を描く */
    private void darwEast(Container cont) {
        JLabel east = new JLabel("東");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

    }
    /** 南を描く */
    private void drawSouth(Container cont) {
        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

    }
}

実行結果は、下のようになります。

はじめに表示したものと同じです。そうならなくてはなりません。リファクタリングした後は、処理結果が全く同じになる事が大切です。
なので、単体テストクラスがないとリファクタリングするのがとても大変になリます。
逆に単体テストクラスがない場合はリファクタリングができません。

HTMLを読み込む

単純にHTMLファイルを作成して、それをSwingで表示するということができます。

<プログラムコード10>

    public static void main(String[] args) {
        SwingHtml main = new SwingHtml();
//        JEditorPane editorPane = new JEditorPane("text/html", HTML);
        JEditorPane editorPane = null;
        try  {
            URL url = new File("PracticeJava1/resources/012.html").toPath().toUri().toURL();
            editorPane = new JEditorPane(url);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
        editorPane.setEditable(false);
        editorPane.setPreferredSize(new Dimension(200, 150));
        JScrollPane scrollPane = new JScrollPane(editorPane);

        JPanel panel = new JPanel();

        panel.setLayout(new BorderLayout());
        panel.add(scrollPane, BorderLayout.CENTER);
        main.getContentPane().add(panel);
        main.setSize(new Dimension(400, 300));
        main.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        main.setVisible(true);
    }

Java Swing テキストRPGを作る ~Swingを使って画面を作る~

イントロダクション

テキストRPGを作成しようと思います。どちらかというとリメイクに近いのですが、前回作成したものが完成していないので。。。
兎にも角にも、Java Swingでの実装になりますので、クラスの扱い方の学習がしやすい、かつ、視覚的にクラス関係を理解できると思います。

IDE(開発ツール)はBlueJを使用しています。

Swingを使って画面を作る

以前、テキストRPGを作成しました。
Gitにアップしてあります。

しかし、これはコマンドプロンプト上で実行するもので「ゲーム」って感じのしないものでした。画面のリロードとかうまくいきません。。。
なので、いろいろと考えた末、Java Swingで実装したらよいと考えなおしました。

余談

実際、筆者はJavaのオブジェクト指向プログラミングという部分をSwingで学びました。つまり、クラスの扱い方を理解しました。
「オブジェクト指向」という言葉を使うと「staticおじさん」や「オブジェクト指向おじさん」よろしく。。。混沌の世界に足を踏み入れることになるので言葉を変えていきたいと思います。

クラスの扱い方を理解する

まとめると、筆者はSwingの実装を通してクラスの扱い方を理解しました。というところを言いたかった次第です。
そして、最近覚えたBlueJを使用して、テキストRPGを作成していきたいと思います。

画面を作る

Swingを使用して、画面を作成していきます。まずは、テキストRPGを実行して「テキスト」を表示する領域が必要になります。

初めのコード

この領域には次の機能が必要になります。

  1. 文字を表示する
  2. 文字をクリア(削除)する

これらを実現するためにプログラム的には、以下のものを使用します。

  • JFrame: アプリの表示する領域フレームを表現するクラス
  • JPanel: フレーム上にコンポーネントを配置するパネル
  • 各種コンポーネント: ラベル(JLabel)、テキストエリア(JTextArea)など

クラス継承について

クラスの継承関係を見てみるとわかりやすいです。

これは、JFrameクラスの親は、Frameクラス、そしてその親は。。。とそれぞれの継承関係を示しています。
つまり、クラスキャストも行うことができるということです。

JFrame frame = new JFrame();
Frame superFrame = (Frame) frame;
superFrame.XXXX;

言葉を変えると、親クラスが必要な時は、上記のようにキャストして使用することができます。
そして、親クラスのメソッドを呼び出すこともできます。

JFrame frame = new JFrame();
frame.addNotify(); // java.awt.Frameのメソッド

コードについて

作成したコードは、下のような表示を行います。どの部分がフレームなのか?も記述しました。

コード

TextRPGMainクラスは、JFrameを継承しているところに注意してください。

package jp.zenryoku.rpg;

import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JPanel;
import javax.swing.JLabel;
import java.awt.Container;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;

/**
 * クラス TextRPGMain の注釈をここに書きます.
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TextRPGMain extends JFrame
{
    public static void main(String[] args) {
        // JFrameを継承しているのでJFrameクラスのメソッドを使える
        TextRPGMain main = new TextRPGMain();
        main.run("Text RPG");
    }

    public void run(String title) {
        // タイトルをセット
        setTitle(title);
        // 画面の表示位置と画面サイズをセット
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        // 画面を閉じたときアプリを終了する設定
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // ラベル
        JLabel titleLabel = new JLabel("Text RPG");
        JTextArea textarea = new JTextArea();
        // テキストエリア
        textarea.setColumns(40);
        textarea.setRows(10);

        // ラベル用のパネル
        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        // テキストエリア用のパネル
        JPanel textPanel = new JPanel();
        textPanel.add(textarea);

        // パネルをセットするコンテナ
        Container contentPane = getContentPane();
        // コンテナにパネルをセット
        contentPane.add(titlePanel, BorderLayout.NORTH);
        contentPane.add(textPanel, BorderLayout.CENTER);
        // 表示する設定
        setVisible(true);
    }
}

ちなみに、クラス図で見ると下のようになります。

メインメソッドを持っているクラスのみになります。

次は、クラスの拡張実装を行ってみようと思います。

クラス継承の実装

クラスの継承方法は下のように「extends クラス名」と書くだけです。

public class ChildClass extends ParentClass {
   ....
}

JLabelを拡張する

「拡張」という言葉に戸惑うかもしれません。ズバリ「JLabelを継承して新しいクラスを作成する」という意味です。

新しく「TitleLabel」クラスを追加します。このクラスは上記のTextRPGMainクラスのrun()メソッドで行っている処理を少なくするように実装しています。
別な言い方をすると「タイトルラベルの処理はTitleLabelに任せましょう。というところです。

では、どのようになるのか?というところです。

TitleLabelの実装

  1. TitleLabelクラスを作成します。
  2. JLabelを継承します。
  3. 現状はコンストラクタの実装のみで事足ります。

実際のコードです。

package jp.zenryoku.rpg;

import javax.swing.JLabel;
import java.awt.Dimension;
import java.awt.Color;

/**
 * クラス TitleLabel の注釈をここに書きます.
 * JLabelを拡張して、テキストRPGのタイトルをセットするラベルを作成する。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TitleLabel extends JLabel
{
    public TitleLabel(String title, Dimension windowSize) {
        super(title);
        int width = (int) windowSize.getWidth() / 4;
        int height = (int) windowSize.getHeight() / 16;
        Dimension labelSize = new Dimension(width, height);
        setOpaque(true);
        setPreferredSize(labelSize);
        setBackground(Color.GREEN);
        setHorizontalAlignment(JLabel.CENTER);
    }
}
  1. JLabelを継承しているので、親クラス(JLabel)のコンストラクタを呼び出します。super(title);
  2. ラベルのサイズ(縦横の幅指定)をします。
  3. ラベルの領域がわかるように、緑色の背景を付けます。

ちなみに、ラベルのサイズは、毎回値を変更するのは、面倒なのでPCの画面サイズに合わせてサイズを変更するように実装しました。

そして、run()メソッドと今回作成したクラスの処理の関係を示します。

TextRPGMain#run()

JLabelを生成して、タイトルをセットしただけで、幅や背景などはセットしていませんでした。

// ラベル
JLabel titleLabel = new JLabel("Text RPG");

なので、この「TitleLabel」クラスを作成していなかったらTextRPGMainクラスにJLabelの処理を書くことになります。
このTextRPGMainクラスにJLabelの処理を書くことがプログラム的に美しくないのでTitleLabelを作成しタイトルラベルのことはこのクラスにコードを書きましょう。という風に考えてプログラムを作りました。

TextRPGMain#run()の修正

ズバリ下のように修正しました。1行です。
JLabel titleLabel = new JLabel("Text RPG");TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
になりました。表示の結果は以下の通り
<元々の表示>

<修正後の表示>

次は、プログラム・コードを見てみましょう。
<元々の処理>

public void run(String title) {
    // タイトルをセット
    setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    JLabel titleLabel = new JLabel("Text RPG");
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    // テキストエリア用のパネル
    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    // パネルをセットするコンテナ
    Container contentPane = getContentPane();
    // コンテナにパネルをセット
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);
    // 表示する設定
    setVisible(true);
}

<修正後>

public void run(String title) {
    // タイトルをセット
   setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    // テキストエリア用のパネル
    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    // パネルをセットするコンテナ
    Container contentPane = getContentPane();
    // コンテナにパネルをセット
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);
    // 表示する設定
    setVisible(true);
}

こんな感じです。
次は、テキストエリアをタイトルラベルと同じように拡張しようと思います。

JTextAreaの拡張

まずは、現状のクラス作成状況を確認します。

次は、画面の白い部分「テキストエリア」を拡張して文字列の表示領域を作成します。

今回も、テキストエリアを担当するクラスを作成します。ネーミングセンスが問われますが、目的と役割を明確にすることを最優先にするので。。。

RpgTextクラスを作成

RpgTextAreaクラスとします。作成はまずJTextAreaを継承します。

import javax.swing.JTextArea;

/**
 * クラス RpgTextArea の注釈をここに書きます.
 * テキストRPGの表示する文字列をこの領域に出力(描画)する。
 * 背景は黒、イメージはドラ○エのような感じにしたい。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {

    }
}

そして、テキストの表示を担当するので、メインクラスに書いている次の部分が不要になります。

JTextArea textarea = new JTextArea();
textarea.setColumns(40);
textarea.setRows(10);

同様に、次のようにコードをRpgTextAreaに追加します。

public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {
        setColumns(40);
        setRows(10);
    }
}

そして、TextRPGMain#run()を修正

    public void run(String title) {
        setTitle(title);
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
        RpgTextArea textarea = new RpgTextArea();

        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        JPanel textPanel = new JPanel();
        textPanel.add(textarea);

        Container contentPane = getContentPane();
        contentPane.add(titlePanel, BorderLayout.NORTH);
        contentPane.add(textPanel, BorderLayout.CENTER);

        setVisible(true);
    }

JPanelをRpgTextAreaに修正、不要なコードを削除しました。

この状態でプログラム実行すると下のようになります。

全く変わりません。その代わり、run()メソッドのコードの量は(少しですが)減りました。
ここから、テキストエリアのおしゃれをしていきます。
参照するのはJavaDocのJTextAreaです。
他にも次のクラスを参照します。

Fontクラスを見ると、フォントファイルを指定することでオリジナルのフォントも使えるようです。

<実行結果>

TextAreaのサイズ設定

画面のサイズ指定に関して、文字入力することも考えてPCの画面サイズから文字の数、行の数を設定するようにプログラムを組みました。
理論的なところが、はっきりと決まらなかったのですが、縦横の「~分の~」という形で実装しました。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        ...
    }
    ...
}

実行結果は、下のような形です。

とりあえずは、これで、画面が作成できたのでここでひと段落になります。

まとめ

クラスを継承すると親クラスのメソッドなどは、自分のクラス内のメソッドのように使用することができる。
なので、下のようなコードが書ける。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        // 背景の描画準備
        setOpaque(true);
        // フォントの設定
        setFont(createTextFont());
        // 背景を黒にする
        setBackground(Color.BLACK);
        // 白い文字の設定
        setForeground(Color.WHITE);
        // 白いボーダーの設定
        Border border = BorderFactory.createLineBorder(Color.GREEN);
        setBorder(BorderFactory.createCompoundBorder(border,
            BorderFactory.createEmptyBorder(10, 10, 10, 10)));

        setWrapStyleWord(true);
        setLineWrap(true);
    }
    ....
}

つまるところは、親クラスのpublic, (packaged, )protectedのメソッドを子クラスが使用することができるので「setXXX」のようなメソッドを直接呼び出すことができる。

今回は、コンストラクタのみを使用した形で実装しました。
次は、テキストの表示などを行っていきたいと思います。

次回 >>>

Java 3D LWJGL GitBook: 第 19 章 – ディファード シェーディング

第 19 章 - ディファード シェーディング

これまで、3D シーンをレンダリングする方法はフォワード レンダリングと呼ばれていました。最初に 3D オブジェクトをレンダリングし、フラグメント シェーダーでテクスチャと照明効果を適用します。この方法は、多数のライトと複雑なエフェクトを含む複雑なフラグメント シェーダー パスがある場合、あまり効率的ではありません。それに加えて、これらの効果を後で深度テストのために破棄される可能性のあるフラグメントに適用することになる場合があります (ただし、初期のフラグメント テストを有効にした場合、これは正確には当てはまりません)。

上記の問題を軽減するために、ディファード シェーディングと呼ばれる技術を使用してシーンをレンダリングする方法を変更することがあります。ディファード シェーディングでは、後の段階で (フラグメント シェーダーで) 必要となるジオメトリ情報を最初にバッファーにレンダリングします。フラグメント シェーダーに必要な複雑な計算は、これらのバッファーに格納されている情報を使用する際に、後の段階に延期されます。

コンセプト

Deferred では、2 つのレンダリング パスを実行する必要があります。1 つ目はジオメトリ パスで、次の情報を含むバッファにシーンをレンダリングします。

・深さの値。
・各位置の拡散色と反射係数。
・各位置のスペキュラー コンポーネント。
・各位置の法線 (ライト ビュー座標系でも)。
そのすべての情報は、G-Buffer と呼ばれるバッファーに格納されます。

2 番目のパスは、ライティング パスと呼ばれます。このパスは、すべての画面を埋めるクワッドを取得し、G バッファーに含まれる情報を使用して各フラグメントの色情報を生成します。ライティング パスを実行するとき、深度テストでは、表示されないすべてのシーン データが既に削除されています。したがって、実行する操作の数は、画面に表示されるものに制限されます。

追加のレンダリング パスを実行するとパフォーマンスが向上するかどうかを尋ねられる場合があります。答えは、場合によるということです。ディファード シェーディングは通常、多数の異なるライト パスがある場合に使用されます。この場合、追加のレンダリング手順は、フラグメント シェーダーで実行される操作の削減によって補われます。

G-バッファ

それでは、コーディングを始めましょう。最初に行う作業は、G-Buffer の新しいクラスを作成することです。という名前のクラスは、GBuffer次のように定義されます。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryStack;
import org.lwjglb.engine.Window;

import java.nio.*;
import java.util.Arrays;

import static org.lwjgl.opengl.GL30.*;

public class GBuffer {

    private static final int TOTAL_TEXTURES = 4;

    private int gBufferId;
    private int height;
    private int[] textureIds;
    private int width;
    ...
}

このクラスは、使用されるバッファーの最大数をモデル化する定数を定義します。G バッファー自体に関連付けられた識別子と、個々のバッファーの配列。テクスチャのサイズも保存されます。

コンストラクターを確認しましょう。

public class GBuffer {
    ...
    public GBuffer(Window window) {
        gBufferId = glGenFramebuffers();
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBufferId);

        textureIds = new int[TOTAL_TEXTURES];
        glGenTextures(textureIds);

        this.width = window.getWidth();
        this.height = window.getHeight();

        for (int i = 0; i < TOTAL_TEXTURES; i++) {
            glBindTexture(GL_TEXTURE_2D, textureIds[i]);
            int attachmentType;
            if (i == TOTAL_TEXTURES - 1) {
                glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT,
                        (ByteBuffer) null);
                attachmentType = GL_DEPTH_ATTACHMENT;
            } else {
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, (ByteBuffer) null);
                attachmentType = GL_COLOR_ATTACHMENT0 + i;
            }
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

            glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentType, GL_TEXTURE_2D, textureIds[i], 0);
        }

        try (MemoryStack stack = MemoryStack.stackPush()) {
            IntBuffer intBuff = stack.mallocInt(TOTAL_TEXTURES);
            for (int i = 0; i < TOTAL_TEXTURES; i++) {
                intBuff.put(i, GL_COLOR_ATTACHMENT0 + i);
            }
            glDrawBuffers(intBuff);
        }

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }
    ...
}

最初に行うことは、フレーム バッファーを作成することです。フレーム バッファは、画面にレンダリングする代わりに操作をレンダリングするために使用できる単なる OpenGL オブジェクトであることに注意してください。次に、フレーム バッファに関連付けられる一連のテクスチャ (4 つのテクスチャ) を生成します。

その後、for ループを使用してテクスチャを初期化します。次のタイプがあります。

・位置、法線、拡散コンポーネントなどを格納する「通常のテクスチャ」。
・深度バッファを格納するためのテクスチャ。これが最後のテクスチャになります。
テクスチャが初期化されたら、テクスチャのサンプリングを有効にして、フレーム バッファにアタッチします。各テクスチャは、 で始まる識別子を使用してアタッチされGL_COLOR_ATTACHMENT0ます。各テクスチャはその id によって 1 ずつ増加するため、位置は を使用してアタッチされGL_COLOR_ATTACHMENT0、拡散コンポーネントはGL_COLOR_ATTACHMENT1( GL_COLOR_ATTACHMENT0 + 1) を使用するなどです。

すべてのテクスチャが作成されたら、レンダリングのためにフラグメント シェーダで使用できるようにする必要があります。これはglDrawBuffers呼び出しで行われます。使用するカラー アタッチメントの識別子を含む配列を渡すだけです (GL_COLOR_ATTACHMENT0にGL_COLOR_ATTACHMENT5)。
クラスの残りの部分は、getter メソッドとクリーンアップ メソッドだけです。

public class GBuffer {
    ...
    public void cleanUp() {
        glDeleteFramebuffers(gBufferId);
        Arrays.stream(textureIds).forEach(GL30::glDeleteTextures);
    }

    public int getGBufferId() {
        return gBufferId;
    }

    public int getHeight() {
        return height;
    }

    public int[] getTextureIds() {
        return textureIds;
    }

    public int getWidth() {
        return width;
    }
}

ジオメトリ パス

ジオメトリ パスを実行するときに適用する必要がある変更を調べてみましょう。これらの変更をSceneRenderクラスと関連するシェーダーに適用します。クラスをSceneRender見て、ライト定数とライト ユニフォームを削除する必要があります。これらはこのパスで使用されることに注意してください (単純化するためにマテリアルにアンビエント カラーも使用しません。そのユニフォームも削除する必要があり、選択したものも削除します)。実体のユニフォーム):

public class SceneRender {

    private ShaderProgram shaderProgram;
    private UniformsMap uniformsMap;
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("projectionMatrix");
        uniformsMap.createUniform("modelMatrix");
        uniformsMap.createUniform("viewMatrix");
        uniformsMap.createUniform("bonesMatrices");
        uniformsMap.createUniform("txtSampler");
        uniformsMap.createUniform("normalSampler");
        uniformsMap.createUniform("material.diffuse");
        uniformsMap.createUniform("material.specular");
        uniformsMap.createUniform("material.reflectance");
        uniformsMap.createUniform("material.hasNormalMap");
    }
    ...
}

メソッドは次のrenderように定義されます。

public class SceneRender {
    ...
    public void render(Scene scene, GBuffer gBuffer) {
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBuffer.getGBufferId());
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0, 0, gBuffer.getWidth(), gBuffer.getHeight());
        glDisable(GL_BLEND);

        shaderProgram.bind();

        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        uniformsMap.setUniform("viewMatrix", scene.getCamera().getViewMatrix());

        uniformsMap.setUniform("txtSampler", 0);
        uniformsMap.setUniform("normalSampler", 1);

        Collection<Model> models = scene.getModelMap().values();
        TextureCache textureCache = scene.getTextureCache();
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();

            for (Material material : model.getMaterialList()) {
                uniformsMap.setUniform("material.diffuse", material.getDiffuseColor());
                uniformsMap.setUniform("material.specular", material.getSpecularColor());
                uniformsMap.setUniform("material.reflectance", material.getReflectance());
                String normalMapPath = material.getNormalMapPath();
                boolean hasNormalMapPath = normalMapPath != null;
                uniformsMap.setUniform("material.hasNormalMap", hasNormalMapPath ? 1 : 0);
                Texture texture = textureCache.getTexture(material.getTexturePath());
                glActiveTexture(GL_TEXTURE0);
                texture.bind();
                if (hasNormalMapPath) {
                    Texture normalMapTexture = textureCache.getTexture(normalMapPath);
                    glActiveTexture(GL_TEXTURE1);
                    normalMapTexture.bind();
                }

                for (Mesh mesh : material.getMeshList()) {
                    glBindVertexArray(mesh.getVaoId());
                    for (Entity entity : entities) {
                        uniformsMap.setUniform("modelMatrix", entity.getModelMatrix());
                        AnimationData animationData = entity.getAnimationData();
                        if (animationData == null) {
                            uniformsMap.setUniform("bonesMatrices", AnimationData.DEFAULT_BONES_MATRICES);
                        } else {
                            uniformsMap.setUniform("bonesMatrices", animationData.getCurrentFrame().boneMatrices());
                        }
                        glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
                    }
                }
            }
        }

        glBindVertexArray(0);
        glEnable(GL_BLEND);
        shaderProgram.unbind();
    }
}

GBufferメソッドのパラメーターとしてインスタンスを受け取っていることがわかります。そのバッファはレンダリングを実行する場所であるため、最初に を呼び出してそのバッファをバインドしglBindFramebufferます。その後、そのバッファをクリアしてブレンドを無効にします。遅延レンダリングを使用する場合、透明なオブジェクトは少し注意が必要です。アプローチは、それらをライト パスでレンダリングするか、ジオメトリ パスで破棄することです。ご覧のとおり、ライトの統一設定コードをすべて削除しました。

頂点シェーダーの唯一の変更点 ( scene.vert) は、ビューの位置が 4 つのコンポーネントのベクターになったことです ( vec4)。

#version 330
...
out vec4 outViewPosition;
...
void main()
{
    ...
    outWorldPosition = modelMatrix * initPos;
    outViewPosition  = viewMatrix * outWorldPosition;
    gl_Position   = projectionMatrix * outViewPosition;
    outNormal     = normalize(modelViewMatrix * initNormal).xyz;
    outTangent    = normalize(modelViewMatrix * initTangent).xyz;
    outBitangent  = normalize(modelViewMatrix * initBitangent).xyz;
    outTextCoord  = texCoord;
}

フラグメント シェーダー ( scene.frag) は大幅に簡素化されています。

#version 330

in vec3 outNormal;
in vec3 outTangent;
in vec3 outBitangent;
in vec2 outTextCoord;
in vec4 outViewPosition;
in vec4 outWorldPosition;

layout (location = 0) out vec4 buffAlbedo;
layout (location = 1) out vec4 buffNormal;
layout (location = 2) out vec4 buffSpecular;

struct Material
{
    vec4 diffuse;
    vec4 specular;
    float reflectance;
    int hasNormalMap;
};

uniform sampler2D txtSampler;
uniform sampler2D normalSampler;
uniform Material material;

vec3 calcNormal(vec3 normal, vec3 tangent, vec3 bitangent, vec2 textCoords) {
    mat3 TBN = mat3(tangent, bitangent, normal);
    vec3 newNormal = texture(normalSampler, textCoords).rgb;
    newNormal = normalize(newNormal * 2.0 - 1.0);
    newNormal = normalize(TBN * newNormal);
    return newNormal;
}

void main() {
    vec4 text_color = texture(txtSampler, outTextCoord);
    vec4 diffuse = text_color + material.diffuse;
    if (diffuse.a < 0.5) {
        discard;
    }
    vec4 specular = text_color + material.specular;

    vec3 normal = outNormal;
    if (material.hasNormalMap > 0) {
        normal = calcNormal(outNormal, outTangent, outBitangent, outTextCoord);
    }

    buffAlbedo   = vec4(diffuse.xyz, material.reflectance);
    buffNormal   = vec4(0.5 * normal + 0.5, 1.0);
    buffSpecular = specular;
}

最も関連性の高い行は次のとおりです。

...
layout (location = 0) out vec4 buffAlbedo;
layout (location = 1) out vec4 buffNormal;
layout (location = 2) out vec4 buffSpecular;
...

これは、このフラグメント シェーダーが書き込むテクスチャを参照している場所です。ご覧のとおり、拡散反射光カラー (マテリアルのコンポーネントに関連付けられたテクスチャのカラー)、スペキュラー コンポーネント、法線、およびシャドウ マップの深度値をダンプするだけです。テクスチャに位置を保存していないことに気付くかもしれません。これは、深度値を使用してフラグメントの位置を再構築できるためです。ライティング パスでこれを行う方法について説明します。

補足:Materialアンビエント カラー コンポーネントを削除して、クラス定義を簡略化しました。

OpenGL デバッガー (RenderDoc など) を使用してサンプルの実行をデバッグすると、ジオメトリ パス中に生成されたテクスチャを表示できます。アルベド テクスチャは次のようになります。

法線の値を保持するテクスチャは次のようになります。

スペキュラ カラーの値を保持するテクスチャは次のようになります。

最後に、深度テクスチャは次のようになります。

照明パス

LightsRenderライティング パスを実行するために、次のように始まる名前の新しいクラスを作成します。

package org.lwjglb.engine.graph;

import org.joml.*;
import org.lwjglb.engine.scene.*;
import org.lwjglb.engine.scene.lights.*;

import java.util.*;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL14.*;
import static org.lwjgl.opengl.GL30.*;

public class LightsRender {
    private static final int MAX_POINT_LIGHTS = 5;
    private static final int MAX_SPOT_LIGHTS = 5;

    private final ShaderProgram shaderProgram;

    private QuadMesh quadMesh;
    private UniformsMap uniformsMap;

    public LightsRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/lights.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/lights.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        quadMesh = new QuadMesh();
        createUniforms();
    }

    public void cleanup() {
        quadMesh.cleanup();
        shaderProgram.cleanup();
    }
    ...
}

新しいシェーダー プログラムの作成に加えて、QadMeshクラスの新しい属性 (まだ定義されていません) を定義していることがわかります。render メソッドを分析する前に、ライトをどのようにレンダリングするかについて少し考えてみましょう。G-Buffer の内容を使用する必要がありますが、それらを使用するには、まず何かをレンダリングする必要があります。しかし、私たちはすでにシーンを描いているので、何をレンダリングしようとしています. 今?答えは簡単です。すべての画面を満たすクワッドをレンダリングするだけです。そのクワッドの各フラグメントに対して、G バッファーに含まれるデータを使用して、正しい出力カラーを生成します。ここでQuadMeshクラスが機能します。ライティング パスでレンダリングするために使用されるクワッドを定義するだけで、次のように定義されます。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjgl.system.*;

import java.nio.*;
import java.util.*;

import static org.lwjgl.opengl.GL30.*;

public class QuadMesh {

    private int numVertices;
    private int vaoId;
    private List<Integer> vboIdList;

    public QuadMesh() {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            vboIdList = new ArrayList<>();
            float[] positions = new float[]{
                    -1.0f, 1.0f, 0.0f,
                    1.0f, 1.0f, 0.0f,
                    -1.0f, -1.0f, 0.0f,
                    1.0f, -1.0f, 0.0f,};
            float[] textCoords = new float[]{
                    0.0f, 1.0f,
                    1.0f, 1.0f,
                    0.0f, 0.0f,
                    1.0f, 0.0f,};
            int[] indices = new int[]{0, 2, 1, 1, 2, 3};
            numVertices = indices.length;

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            // Positions VBO
            int vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
            positionsBuffer.put(0, positions);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

            // Texture coordinates VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer textCoordsBuffer = MemoryUtil.memAllocFloat(textCoords.length);
            textCoordsBuffer.put(0, textCoords);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, textCoordsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);

            // Index VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            IntBuffer indicesBuffer = stack.callocInt(indices.length);
            indicesBuffer.put(0, indices);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }

    public void cleanup() {
        vboIdList.stream().forEach(GL30::glDeleteBuffers);
        glDeleteVertexArrays(vaoId);
    }

    public int getNumVertices() {
        return numVertices;
    }

    public int getVaoId() {
        return vaoId;
    }
}

ご覧のとおり、必要なのは位置とテクスチャの座標属性だけです (G-Buffer テクスチャに適切にアクセスするため)。クラスに戻るとLightsRender、ユニフォームを作成するメソッドが必要です。これにより、以前にクラスで使用されていたライト ユニフォームに加えて、G バッファ テクスチャ ( 、、および)SceneRenderをマップするための新しいユニフォームのセットが復元されます。それに加えて、やなどの深度値からフラグメント位置を計算するための新しいユニフォームが必要になります。シェーダー コードで、それらがどのように使用されるかを確認します。albedoSamplernormalSamplerspecularSamplerdepthSamplerinvProjectionMatrixinvViewMatrix

public class LightsRender {
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("albedoSampler");
        uniformsMap.createUniform("normalSampler");
        uniformsMap.createUniform("specularSampler");
        uniformsMap.createUniform("depthSampler");
        uniformsMap.createUniform("invProjectionMatrix");
        uniformsMap.createUniform("invViewMatrix");
        uniformsMap.createUniform("ambientLight.factor");
        uniformsMap.createUniform("ambientLight.color");

        for (int i = 0; i < MAX_POINT_LIGHTS; i++) {
            String name = "pointLights[" + i + "]";
            uniformsMap.createUniform(name + ".position");
            uniformsMap.createUniform(name + ".color");
            uniformsMap.createUniform(name + ".intensity");
            uniformsMap.createUniform(name + ".att.constant");
            uniformsMap.createUniform(name + ".att.linear");
            uniformsMap.createUniform(name + ".att.exponent");
        }
        for (int i = 0; i < MAX_SPOT_LIGHTS; i++) {
            String name = "spotLights[" + i + "]";
            uniformsMap.createUniform(name + ".pl.position");
            uniformsMap.createUniform(name + ".pl.color");
            uniformsMap.createUniform(name + ".pl.intensity");
            uniformsMap.createUniform(name + ".pl.att.constant");
            uniformsMap.createUniform(name + ".pl.att.linear");
            uniformsMap.createUniform(name + ".pl.att.exponent");
            uniformsMap.createUniform(name + ".conedir");
            uniformsMap.createUniform(name + ".cutoff");
        }

        uniformsMap.createUniform("dirLight.color");
        uniformsMap.createUniform("dirLight.direction");
        uniformsMap.createUniform("dirLight.intensity");

        uniformsMap.createUniform("fog.activeFog");
        uniformsMap.createUniform("fog.color");
        uniformsMap.createUniform("fog.density");

        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            uniformsMap.createUniform("shadowMap_" + i);
            uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".projViewMatrix");
            uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".splitDistance");
        }
    }
    ...
}

メソッドは次のrenderように定義されます。

public class LightsRender {
    ...
    public void render(Scene scene, ShadowRender shadowRender, GBuffer gBuffer) {
        shaderProgram.bind();

        updateLights(scene);

        // Bind the G-Buffer textures
        int[] textureIds = gBuffer.getTextureIds();
        int numTextures = textureIds != null ? textureIds.length : 0;
        for (int i = 0; i < numTextures; i++) {
            glActiveTexture(GL_TEXTURE0 + i);
            glBindTexture(GL_TEXTURE_2D, textureIds[i]);
        }

        uniformsMap.setUniform("albedoSampler", 0);
        uniformsMap.setUniform("normalSampler", 1);
        uniformsMap.setUniform("specularSampler", 2);
        uniformsMap.setUniform("depthSampler", 3);

        Fog fog = scene.getFog();
        uniformsMap.setUniform("fog.activeFog", fog.isActive() ? 1 : 0);
        uniformsMap.setUniform("fog.color", fog.getColor());
        uniformsMap.setUniform("fog.density", fog.getDensity());

        int start = 4;
        List<CascadeShadow> cascadeShadows = shadowRender.getCascadeShadows();
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glActiveTexture(GL_TEXTURE0 + start + i);
            uniformsMap.setUniform("shadowMap_" + i, start + i);
            CascadeShadow cascadeShadow = cascadeShadows.get(i);
            uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".projViewMatrix", cascadeShadow.getProjViewMatrix());
            uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".splitDistance", cascadeShadow.getSplitDistance());
        }
        shadowRender.getShadowBuffer().bindTextures(GL_TEXTURE0 + start);

        uniformsMap.setUniform("invProjectionMatrix", scene.getProjection().getInvProjMatrix());
        uniformsMap.setUniform("invViewMatrix", scene.getCamera().getInvViewMatrix());

        glBindVertexArray(quadMesh.getVaoId());
        glDrawElements(GL_TRIANGLES, quadMesh.getNumVertices(), GL_UNSIGNED_INT, 0);

        shaderProgram.unbind();
    }
    ...
}

ライトを更新した後、ジオメトリ パスの結果を保持するテクスチャをアクティブにします。その後、フォグとカスケード シャドウのユニフォームを設定し、クワッドだけを描画します。

では、ライト パスの頂点シェーダーはどのように見えるのでしょうか ( lights.vert)?

#version 330

layout (location=0) in vec3 inPos;
layout (location=1) in vec2 inCoord;

out vec2 outTextCoord;

void main()
{
    outTextCoord = inCoord;
    gl_Position = vec4(inPos, 1.0f);
}

上記のコードは、頂点を直接ダンプし、テクスチャ座標をフラグメント シェーダーに渡すだけです。フラグメント シェーダー ( lights.frag) は次のように定義されます。

#version 330

const int MAX_POINT_LIGHTS = 5;
const int MAX_SPOT_LIGHTS = 5;
const float SPECULAR_POWER = 10;
const int NUM_CASCADES = 3;
const float BIAS = 0.0005;
const float SHADOW_FACTOR = 0.25;

in vec2 outTextCoord;
out vec4 fragColor;

struct Attenuation
{
    float constant;
    float linear;
    float exponent;
};
struct AmbientLight
{
    float factor;
    vec3 color;
};
struct PointLight {
    vec3 position;
    vec3 color;
    float intensity;
    Attenuation att;
};
struct SpotLight
{
    PointLight pl;
    vec3 conedir;
    float cutoff;
};
struct DirLight
{
    vec3 color;
    vec3 direction;
    float intensity;
};
struct Fog
{
    int activeFog;
    vec3 color;
    float density;
};
struct CascadeShadow {
    mat4 projViewMatrix;
    float splitDistance;
};

uniform sampler2D albedoSampler;
uniform sampler2D normalSampler;
uniform sampler2D specularSampler;
uniform sampler2D depthSampler;

uniform mat4 invProjectionMatrix;
uniform mat4 invViewMatrix;

uniform AmbientLight ambientLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform SpotLight spotLights[MAX_SPOT_LIGHTS];
uniform DirLight dirLight;
uniform Fog fog;
uniform CascadeShadow cascadeshadows[NUM_CASCADES];
uniform sampler2D shadowMap_0;
uniform sampler2D shadowMap_1;
uniform sampler2D shadowMap_2;

vec4 calcAmbient(AmbientLight ambientLight, vec4 ambient) {
    return vec4(ambientLight.factor * ambientLight.color, 1) * ambient;
}
vec4 calcLightColor(vec4 diffuse, vec4 specular, float reflectance, vec3 lightColor, float light_intensity, vec3 position, vec3 to_light_dir, vec3 normal) {
    vec4 diffuseColor = vec4(0, 0, 0, 1);
    vec4 specColor = vec4(0, 0, 0, 1);

    // Diffuse Light
    float diffuseFactor = max(dot(normal, to_light_dir), 0.0);
    diffuseColor = diffuse * vec4(lightColor, 1.0) * light_intensity * diffuseFactor;

    // Specular Light
    vec3 camera_direction = normalize(-position);
    vec3 from_light_dir = -to_light_dir;
    vec3 reflected_light = normalize(reflect(from_light_dir, normal));
    float specularFactor = max(dot(camera_direction, reflected_light), 0.0);
    specularFactor = pow(specularFactor, SPECULAR_POWER);
    specColor = specular * light_intensity  * specularFactor * reflectance * vec4(lightColor, 1.0);

    return (diffuseColor + specColor);
}

vec4 calcPointLight(vec4 diffuse, vec4 specular, float reflectance, PointLight light, vec3 position, vec3 normal) {
    vec3 light_direction = light.position - position;
    vec3 to_light_dir  = normalize(light_direction);
    vec4 light_color = calcLightColor(diffuse, specular, reflectance, light.color, light.intensity, position, to_light_dir, normal);

    // Apply Attenuation
    float distance = length(light_direction);
    float attenuationInv = light.att.constant + light.att.linear * distance +
    light.att.exponent * distance * distance;
    return light_color / attenuationInv;
}

vec4 calcSpotLight(vec4 diffuse, vec4 specular, float reflectance, SpotLight light, vec3 position, vec3 normal) {
    vec3 light_direction = light.pl.position - position;
    vec3 to_light_dir  = normalize(light_direction);
    vec3 from_light_dir  = -to_light_dir;
    float spot_alfa = dot(from_light_dir, normalize(light.conedir));

    vec4 color = vec4(0, 0, 0, 0);

    if (spot_alfa > light.cutoff)
    {
        color = calcPointLight(diffuse, specular, reflectance, light.pl, position, normal);
        color *= (1.0 - (1.0 - spot_alfa)/(1.0 - light.cutoff));
    }
    return color;
}

vec4 calcDirLight(vec4 diffuse, vec4 specular, float reflectance, DirLight light, vec3 position, vec3 normal) {
    return calcLightColor(diffuse, specular, reflectance, light.color, light.intensity, position, normalize(light.direction), normal);
}
vec4 calcFog(vec3 pos, vec4 color, Fog fog, vec3 ambientLight, DirLight dirLight) {
    vec3 fogColor = fog.color * (ambientLight + dirLight.color * dirLight.intensity);
    float distance = length(pos);
    float fogFactor = 1.0 / exp((distance * fog.density) * (distance * fog.density));
    fogFactor = clamp(fogFactor, 0.0, 1.0);

    vec3 resultColor = mix(fogColor, color.xyz, fogFactor);
    return vec4(resultColor.xyz, color.w);
}

float textureProj(vec4 shadowCoord, vec2 offset, int idx) {
    float shadow = 1.0;

    if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0) {
        float dist = 0.0;
        if (idx == 0) {
            dist = texture(shadowMap_0, vec2(shadowCoord.xy + offset)).r;
        } else if (idx == 1) {
            dist = texture(shadowMap_1, vec2(shadowCoord.xy + offset)).r;
        } else {
            dist = texture(shadowMap_2, vec2(shadowCoord.xy + offset)).r;
        }
        if (shadowCoord.w > 0 && dist < shadowCoord.z - BIAS) {
            shadow = SHADOW_FACTOR;
        }
    }
    return shadow;
}

float calcShadow(vec4 worldPosition, int idx) {
    vec4 shadowMapPosition = cascadeshadows[idx].projViewMatrix * worldPosition;
    float shadow = 1.0;
    vec4 shadowCoord = (shadowMapPosition / shadowMapPosition.w) * 0.5 + 0.5;
    shadow = textureProj(shadowCoord, vec2(0, 0), idx);
    return shadow;
}
void main()
{
    vec4 albedoSamplerValue = texture(albedoSampler, outTextCoord);
    vec3 albedo  = albedoSamplerValue.rgb;
    vec4 diffuse = vec4(albedo, 1);

    float reflectance = albedoSamplerValue.a;
    vec3 normal = normalize(2.0 * texture(normalSampler, outTextCoord).rgb  - 1.0);
    vec4 specular = texture(specularSampler, outTextCoord);

    // Retrieve position from depth
    float depth = texture(depthSampler, outTextCoord).x * 2.0 - 1.0;
    if (depth == 1) {
        discard;
    }
    vec4 clip      = vec4(outTextCoord.x * 2.0 - 1.0, outTextCoord.y * 2.0 - 1.0, depth, 1.0);
    vec4 view_w    = invProjectionMatrix * clip;
    vec3 view_pos  = view_w.xyz / view_w.w;
    vec4 world_pos = invViewMatrix * vec4(view_pos, 1);

    vec4 diffuseSpecularComp = calcDirLight(diffuse, specular, reflectance, dirLight, view_pos, normal);

    int cascadeIndex;
    for (int i=0; i<NUM_CASCADES - 1; i++) {
        if (view_pos.z < cascadeshadows[i].splitDistance) {
            cascadeIndex = i + 1;
            break;
        }
    }
    float shadowFactor = calcShadow(world_pos, cascadeIndex);

    for (int i=0; i<MAX_POINT_LIGHTS; i++) {
        if (pointLights[i].intensity > 0) {
            diffuseSpecularComp += calcPointLight(diffuse, specular, reflectance, pointLights[i], view_pos, normal);
        }
    }

    for (int i=0; i<MAX_SPOT_LIGHTS; i++) {
        if (spotLights[i].pl.intensity > 0) {
            diffuseSpecularComp += calcSpotLight(diffuse, specular, reflectance, spotLights[i], view_pos, normal);
        }
    }
    vec4 ambient = calcAmbient(ambientLight, diffuse);
    fragColor = ambient + diffuseSpecularComp;
    fragColor.rgb = fragColor.rgb * shadowFactor;

    if (fog.activeFog == 1) {
        fragColor = calcFog(view_pos, fragColor, fog, ambientLight.color, dirLight);
    }
}

ご覧のとおり、見慣れた機能が含まれています。これらは、シーン フラグメント シェーダーの前の章で使用されていました。ここで注意すべき重要な点は、次の行です。

uniform sampler2D albedoSampler;
uniform sampler2D normalSampler;
uniform sampler2D specularSampler;
uniform sampler2D depthSampler;

最初に、現在のフラグメント座標に従って、アルベド、法線マップ ([0, -1] から [-1, 1] の範囲に変換)、およびスペキュラー アタッチメントをサンプリングします。それに加えて、見慣れないコード フラグメントがあります。光の計算を実行するには、フラグメントの位置が必要です。ただし、役職のアタッチメントはありません。ここで、深度アタッチメントと逆投影行列が機能します。その情報を使用して、位置を保存する別のアタッチメントを必要とせずに、世界の位置 (ビュー空間座標) を再構築できます。他のチュートリアルでは、位置に特定のアタッチメントを設定していることがわかりますが、この方法で行う方がはるかに効率的です。遅延アタッチメントによって消費されるメモリが少ないほど良いことを常に覚えておいてください。

コードの残りの部分は、シーン レンダリングのフラグメント シェーダーのものと非常によく似ています。

最後にRender、新しいクラスを使用するようにクラスを更新する必要があります。

public class Render {
    ...
    private GBuffer gBuffer;
    ...
    private LightsRender lightsRender;
    ...
    public Render(Window window) {
        ...
        lightsRender = new LightsRender();
        gBuffer = new GBuffer(window);
    }

    public void cleanup() {
        ...
        lightsRender.cleanup();
        gBuffer.cleanUp();
    }

    private void lightRenderFinish() {
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }

    private void lightRenderStart(Window window) {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0, 0, window.getWidth(), window.getHeight());

        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFunc(GL_ONE, GL_ONE);

        glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer.getGBufferId());
    }

    public void render(Window window, Scene scene) {
        shadowRender.render(scene);
        sceneRender.render(scene, gBuffer);
        lightRenderStart(window);
        lightsRender.render(scene, shadowRender, gBuffer);
        skyBoxRender.render(scene);
        lightRenderFinish();
        guiRender.render(scene);
    }

    public void resize(int width, int height) {
        guiRender.resize(width, height);
    }
}

<サンプルコードの実行>