Linuxで動作するCプログラムのデバッグ環境構築

投稿日: 更新日:


こんにちはー!

今回は、Linuxで動作するCのデバッグ環境の整え方をお話しします。

皆さんは、Raspberry PiとかのLinux上で動作するプログラムを作るときにどうしていますか、
Raspberry Pi 上で make して色々プログラムを作っていくのもいいですが、
コンパイルの速度が遅いですし、効率も落ちてしまいます。

では、どうすればいいのか。
Windows環境で、Linux上のプログラムを作ってテストしたいと思った時に
一番簡単なのは、仮想環境でLinuxを起動させてPCでmakeしていく方法です。
ただ、仮想環境は重たい切実な問題があります。

そこで、今回はできる限り軽い仮想環境を作って
Cプログラムをデバッグ出来る方法まで説明します。


作戦

まず、Linuxといってもたくさん派生しており、色々な環境があります。
そのため、かるーいOSもあるはずです。

Linux

ベースとなるLinuxを考えましょう。

Linuxの種類

OSの説明です。
私が知っている情報は、この4つしかありません(認識不足)

RedHat

RHEL(Red Hat Enterprise Linux) / CentOS / Fedora 等のサーバー向けのOSです。
RHEL は、有料のOSであるため、サポートや情報が多く、サーバーに使用されることがおおいです。
CentOS は、RHEL の無料版です。サポートが受けられませんが、信頼性が高い RHEL を使えるような形です。
Fedora は、新しい機能を追加していくOSです。最新の機能を試したい向けです。

Debian

昔からある OS です。
これをベースに多くのOSが派生しています。
このOSをベースに組み込み向けのLinuxなども作られることもあります。

Ubuntu

Devian ベースのOSで、個人向けだと有名なOSです。
デスクトップPC向けに様々なカスタマイズがされているOSです。
利用者が多いため非常に情報量が多いです。

BSD系

上の3種類と違ってLinuxではありませんが、よく耳にするOSなので紹介します。
Linux系のOSは、総じてGPLなのですが、BSD系のOSは、BSDライセンスとなります。
製品に組み込む際にBSDがよいといった場合に選ばれるOSです。

どのLinuxを選択するのか

結論からいいますと、Debian を今回選択しました。
というのもカスタイマイズされた Ubuntu は、有名なOSで情報量も多いのですが、
デバッグ環境だけに限れば、必要がないアプリがインストールされます。

そして、Debianは、Raspbianなど組み込みLinuxでベースとされることが多いため、
Debian での環境で何か開発すれば、それを他の組み込みに持っていくのが楽かと考えたためです。
Armadillo とかも Debian が動いています。

デスクトップ環境

Linuxを知っている方ならご存知ですが、デスクトップ環境は好きなものが選べます。
ここで選ぶものによって、非力なPCでは重たくなってしまいます。
Debian では、インストール中にいくつかデスクトップ環境を選ぶことができます。

デスクトップ環境の種類

Unity, GNOME

下記のに比べるとグラフィカルでUIが洗練されていますが、
その分メモリやパワーを使用します。
Ubuntu のデスクトップ環境を選ぶと標準で利用されます。
※Unity は Debian では選べません。

Xfce

当初、これを検討していました。
歴史があるデスクトップ環境のためか、
現在のPCでは軽快に動作する特徴を持っています。

LXDE

これは歴史は浅いですが、処理能力の低いPC向けに作られた環境です。
軽快に動作することを目的としています。

どのデスクトップ環境を選択するのか

今回は、LXDEを選択します。
目的が軽さというのがよいです。
デバッグ環境のみに使用するという目的とマッチしています。

デバッグ用ツール

ここはあんまり調査していませんでした。
Windows でもよく使用される Eclipse を使用したいと思います。


開発環境を整えよう

全てはかきませんが、流れを説明します。

前準備

次の環境を用意します。

・Oracle VM VirtualBox
仮想環境です。今回は 5.2.4 を使用します。
仮想環境は、スナップショットという機能を使うことで誤った設定をしても以前の段階に戻れるためおススメです。
VMware という選択肢もありますが、商用利用に制限があるためこちらを選択します。

・Debian
Debianのサイト」で
「Debian を入手する」→「ネットワークインストール」→「最小の CD を使って、ネットワークインストールする」を選択します。
「netinst CD イメージ (約 150-300 MB ですがアーキテクチャよって変わります)」の中にある
「i386」を選択して「debian-9.3.0-i386-netinst.iso」をダウンロードします。(x86用OSを想定)

・ネットワーク
プロクシなしで、ネットワークに接続できる環境を準備します。

VMで仮想環境を作成

新規作成する

1. 「Oracle VM VirtualBox」を起動して、「新規」を選択します。
2. タイプはLinux、バージョンはDebian(32-bit)を設定します
3. メモリサイズは緑の範囲で出来る限り多くとります。
4. ハードディスクでは「仮想ハードディスクを作成する」を選び「作成」。
5. ハードディスクはVDIを選択。サイズは可変サイズを選択
6. 「ファイルの場所とサイズ」になったら、HDDファイルの場所を設定してください。
場所やファイル名は変更がすこし大変なので注意。
また、ハードディスクのサイズは120GBほどに設定しましょう。
こちらは、変更が非常に困難なので極力大きめにします。

設定変更する

新規作成すると左側の領域に作成した仮想環境が表示されます。
これを選択した状態で「設定」を選ぶと、より設定変更が行えます。
次のように設定していきます。
1. 一般の高度で、クリップボードの共有を双方向にする
2. システムのプロセッサーで、CPU数を増やす
3. オーディオとUSBは無効化する(使用しないため)
4. ネットワークはNATとなっていること(外部にアクセスするため)
よくやる設定はこんなものです。

OSのインストール

1. 作成したプロジェクトを選び、起動を選びます。
2. 最初にISOを選択する画面が表示されるため「debian-9.3.0-i386-netinst.iso」を選択します
3. インストール方法は、グラフィカルじゃないインストール方法 Install を選択します。
※操作は、矢印キーとスペースキーで選択し、エンターキーで決定です。
4. あとは適当に進めていきます。途中で言語の設定が出た場合は「日本」を選びましょう
5. 途中で、初期インストールの設定が出たら、「LXDE」と「SSH サーバー」を選びましょう。
6. あとは適当に進めていけばOSのインストールが完了です。

OSの初期設定を行う

ターミナルを駆使して設定していきます。
スーパーユーザー権限で行うことが多いので「sudo su -」と入力しておくと楽です。

パスを英語化する

「デスクトップ」とかフォルダパスに
日本語が入っているとやり辛いので最初にやります。
1. xdg-user-dirs-gtk をインストールして、updateを実行します。

sudo apt-get install xdg-user-dirs-gtk
LANG=C xdg-user-dirs-gtk-update

Guest Additions CDをインストールする

Guest Additions CD とは、
VM上で動くOSでインストールすることで、
共有フォルダや画面解像度変更、クリップボード共有など
色々と使いやすくなるツールです。
1. VMのメニューバーにある「デバイス」→「Guest Additions CDイメージの挿入…」
2. CDが認識されます。このままだと実行できないので、一旦ディスクのフォルダをデスクトップへコピー(cdrom というフォルダにします)
3. 実行する前に、実行に必要なソフトをインストールします。

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install dkms
sudo apt-get install build-essential module-assistant 
sudo apt-get install linux-headers-686 linux-headers-4.9.0-4-686
※Linuxの部分は、DebianのLinuxカーネルのバージョンに依存する
そのため、下記のVBoxLinuxAdditions.runを実行すると
必要なインストールファイルが表示されるのでそれを参考にするよい

4. Guest Additionsをインストールして再起動

cd /home/***/Desktop/cdrom
sudo ./VBoxLinuxAdditions.run
sudo reboot

※もし「VBoxLinuxAdditions.run」でエラーが出た場合は表示内容をインストールする。
それでも駄目なら、VirtualBoxを最新版にする。
(Debianのリリース日より新しいバージョンだと確実。)
5. Guest Additions CD を取り出す
VMのメニューバーにある「デバイス」→「光学ドライブ」から除去的なのを選ぶ

共有フォルダの設定

Windows上とファイルの行き来する際に、共有フォルダを作ると便利です
1. VMのメニューバーにある「デバイス」→「共有フォルダー設定…」
2. フォルダの追加ボタンで、ダイアログを表示させる
3. フォルダのパスを入力する部分で、右端の▼を押して「その他…」を選び、フォルダを選択
4. 「自動マウント」と「永続化する」を選びOKを押す
5. OSを再起動
6. 共有フォルダ「/media/sf_***」への権限を付与

sudo gpasswd -a ユーザ名 vboxsf

7. OSを再起動

不要なファイルを削除する

いらないファイルは予め削除しちゃいましょう。
下記のコマンドで、オフィスツールや画像編集ソフトをアンインストールできます。

sudo apt remove libreoffice*
sudo apt remove gimp*
sudo apt-get autoremove

あると便利なソフトをインストールする

コンパイルに必要なmakeやファイルサーバーのsambaは必須!よく使うので。

sudo apt-get install samba
sudo apt-get install make

サーバーを自動起動させる

あとは、SSHやSAMBAの設定を好みでしておきましょう。

Eclipseのインストール

Eclipseをインストールしていきます。
ここからは「Synaptic パッケージマネージャ」を使用していきます。
「Synaptic パッケージマネージャ」というのは、アプリストアみたいなものです。
1. スタートメニューから「設定」→「Synaptic パッケージマネージャ」
2. 次の4つを選択した後に適用を押し、インストールします。
・eclipse(本体)
・eclipse-cdt(C/C++開発用)
・pleiades(eclipseの日本語化)
・gdb(gccのデバッグ支援機能)
3. スタートメニューから「プログラミング」→「eclipse」を選択して、起動させて終了させる。
4. 日本語化のために次のように「eclipse.ini 」を開く

sudo nano /usr/lib/eclipse/eclipse.ini

5. 最終行に次の1行を追加して上書きする。

-javaagent:/usr/lib/eclipse/plugins/jp.sourceforge.mergedoc.pleiades/pleiades.jar

6. ターミナルで次のコマンドをうつ。

eclipse -clean

7. これでC言語でデバッグ環境の準備を終えました。


デバッグしてみよう

ここからは、通常のeclipseの使い方と同じです。
ここについては私がもっと詳しくなったら記述したいと思います。


おまけ

配布用のサイズを縮小する

仮想環境で作るメリットとして、
一旦作ってしまえば、面倒な環境構築無しで、他のパソコンでもすぐに実行できるということがあります。
ただ1つ問題がありまして、この仮想環境のファイルシステムはどんどん大きくなっていきます。
可変サイズという設定にしているので、
使用しなくなったらサイズが減ると思っているかもしれませんが減りません。

配布する際にこのようにファイルサイズが巨大になったまま配布すると扱いが大変なので、
縮小させることを行います。
1. ゴミ箱は空にしておく
2. スナップショットを使用しているならば、統合するためにスナップショットを全て統合させる。
3. 仮想OS上(Debian)で次のコマンドをうつ

sudo dd if=/dev/zero of=zero bs=4k; \rm zero

4. 仮想OSをシャットダウン
5. Windows上で管理者権限でコマンドプロンプトを起動させる
6. VirtualBox のインストール先にカレントディレクトリを移動させる
5. 「VBoxManage list hdds」で縮小させるVDIのUUIDを調査する
6. 「VBoxManage odifyhd [UUID] –compact」で縮小させる
7. 圧縮ソフト7-zipを使って、7z形式で高圧縮させる

広告

HSPで高DPI対応ソフトを作ろう

投稿日:


こんにちは!

久しぶりにHSPの記事を書きたいと思います!
今回は、HSPの高DPI対応です。


はじめに

高DPIとはなんぞや、という説明をします。

昔のディスプレイは、1ドットのサイズが細かくないため、
1ピクセル=1ドットのような設計がほとんどでした。
そして、1ピクセルの大きさも、どのディスプレイもだいたい同じようなサイズ(96dpi)でした。
従って、ソフトを作る際に、1ドット=1ピクセルの前提で、GUIの設計をしていました。

ただ5、6年前ぐらいから、スマホが高繊細化してきており、
ディスプレイもRatinaディスプレイや4kディスプレイなど、高繊細なものが登場してきました。

例をあげると、次のようなものがあります。
Apple MacBook Pro 解像度 2560×1600、227dpi
Microsoft Surface Pro 解像度 2736×1824 、267 dpi
iiyama 4Kディスプレイ B2875UHSU-B1A 解像度 3840×2160、157 dpi

このように、96dpiより大きなものを高DPI(High DPI)と呼びます。

高DPIに対応していないソフトの問題点

このような高密度なディスプレイで、
これまで作成したソフトを実行するとどうなるでしょう。

高DPIで動作することを考えていないソフトの場合は、
OSがソフトのGUIを自動的にスケーリングして表示することとなります。

しかし、このスケーリングが問題でして、
ASCII.jp Windows 10+高解像度ディスプレイでのアプリのボケはRS2で解消される」のように
文字や画像がボケてしまうのです。

高DPI化に対応する方法と問題点

まず前提として、アプリ自身が「高DPIに対応してますよ!」と宣言しないといけません。
この宣言方法は、2種類あり、マニフェストで宣言するか、
アプリ内で「SetProcessDPIAware」を呼び出す方法があります。
これらの宣言方法については、「SetProcessDPIAware function」に記載されています。

一見、上記の方法だけで対応できるなら簡単じゃないかと思えるかもしれません。
実際の問題点は、高DPI対応宣言後の処理となります。
具体的には、座標、サイズ、位置などについて、
高DPIを考慮した数値で設定する必要が出てきます。

これから作るソフトならまだよいですが、今まで作ったソフトで高DPI化させようとした場合、
ソースコード上の全ての位置やサイズ情報を書き直す必要があるのです。

現実的な解決策

実は、今までのソースコード書き直しせず、簡単に解決することが可能です。
それは、標準関数の置き換えです。

以下に、いくつかの標準関数を置き換えたモジュールの例をおきます。
HIGHDPI モジュール
(このモジュールは、実際に私が作ったツールで利用しています。)

このように、標準関数を置き換えたモジュールを
ソースコードの最初でincludeすることで、
全コードが高DPI対応用の命令で実行させるということです。

おわりに

これで、高DPI対応に関する記事は終わりです。
ありがとうございました。

ちなみに、高DPI対応を試験する際の注意点として
ディスプレイの設定から、「拡大縮小とレイアウト」で「100%」から変更させるという方法があるのですが、
一旦、サインアウト・サインインをやり直す必要があります。

サインアウト・サインインをしないと、
GetDeviceCaps の HIGHDPI_LOGPIXELSX / HIGHDPI_LOGPIXELSY
で取得できる数値が96のままになってしまいます。
気が付かずに、すこしはまってしまいました……。

Raspberry Pi 3 に CentOS 7

投稿日: 更新日:


これまでも何度も Raspberry Pi を初期化していますが、
今回はサーバー運用に定評がある CentOS(for armv7hl) を入れてみました。
その時の設定メモです。

補足
armv7hl とは、文字通り ARMv7 向けかと思いますが、Raspberry Pi 3は、ARMv8 です。
なので、本来であれば v8用が必要なのかもしれませんが、
CentOSのサイトには Raspberry Pi 3 用であっても armv7hl とファイル名にかいてあるので、
この辺のARMのバージョンは気にしなくていいのかもしれません。

そしてですが、このCentOS用のイメージはテスト版であることと、ARM用ということなので、
普通のx86版のCentOSで行えるようなサードパーティのパッケージが用意されていない場合がありますので注意してください。
.
.
1. パーティションが存在するSDカードの初期化
1-1. コマンドプロンプトで「diskpart」を実行
1-2. 「list disk」で初期化したいSDカードのディスク番号を調べる
1-3. 「select disk 1」でディスク番号1を選択する
1-4. 「list partition」でパーティションを確認し選択しているのが正しいことを確認
1-5. 「clean」で内部を初期化
.
.
2. SDカードをフォーマットする
2-1. 「SD/SDHC/SDXC用SDメモリカードフォーマッター 5.0」を利用
.
.
3. CentOSのRaspbeyPi3用のimgファイルを準備
3-1. 「CentOS-Userland-7-armv7hl-Minimal-1611-test-RaspberryPi3.img.xz」をダウンロードして「7zip」などで展開
3-2. 「Win32DiskImager」を利用して、imgファイルをSDカードに入れる
3-3. これを差し込めば一応利用可能になる
.
.
4. SSHでログインできるまでの基礎設定
4-1. HDMI でディスプレイに接続して、USBキーボードを接続する
4-2. 次のコマンドをうち日本語キーボード入力を有効化する

localectl set-keymap jp106
localectl set-keymap jp-OADG109A
localectl LANG=ja_JP.utf8

4-3. タイムゾーンの設定を行う

timedetectl set-timezone Asia/Tokyo

4-4. SDカードの拡張を行う

/usr/local/bin/rootfs-expand

4-5. 次のコマンドでIPアドレスを調べる

ifconfig

4-6. sshで、ユーザー名 root、パスワード、centos でログイン
4-7. パスワードは次のコマンドで変更できるので必ず変更しておきましょう

passwd

.
.
5. 外部にネットワークで接続できるようにする(CentOS7 から後述の nmtui を使うと良い)
5-1. 「/etc/sysconfig/network」がないので、「touch」で新規作成する※後述
5-2. 中はネットワーク接続と、デフォルトゲートウェイの設定をする

NETWORKING="yes"
GATEWAY="192.168.11.1"(デフォルトゲートウェイ)

5-3. 「/etc/sysconfig/network-scripts/ifcfg-eth0」を編集して、固定IPアドレスにする

TYPE="Ethernet"
BOOTPROTO="dhcp"
NM_CONTROLLED="yes"
DEFROUTE="yes"
NAME="eth0"
UUID=xxx
ONBOOT="yes"

TYPE="Ethernet"
BOOTPROTO="static"
NM_CONTROLLED="yes"
DEFROUTE="yes"
NAME="eth0"
UUID=xxx(いじらない)
ONBOOT="yes"
IPADDR="192.168.11.200"(自分のIPアドレスにする)
NETMASK="255.255.255.0"
DNS1="8.8.8.8"(Google のDNSサーバーを設定する)
DNS2="8.8.4.4"

5-4. NetworkManagerを止める

systemctl stop NetworkManager

5-5. ネットワークの設定をリロードする

systemctl daemon-reload
systemctl restart network

5-6. ping で外部サーバーに接続できるかテストする

[root@centos-rpi3 sysconfig]# ping google.com
PING google.com (216.58.200.174) 56(84) bytes of data.
64 bytes from nrt12s11-in-f174.1e100.net (216.58.200.174): icmp_seq=1 ttl=49 time=20.3 ms
64 bytes from nrt12s11-in-f174.1e100.net (216.58.200.174): icmp_seq=2 ttl=49 time=20.5 ms
64 bytes from nrt12s11-in-f174.1e100.net (216.58.200.174): icmp_seq=3 ttl=49 time=20.2 ms

※「/etc/sysconfig/network」の設定がないと、次のようにエラーで立ち上がりません。

[root@centos-rpi3 sysconfig]# systemctl status network.service
● network.service - LSB: Bring up/down networking
   Loaded: loaded (/etc/rc.d/init.d/network; bad; vendor preset: disabled)
   Active: failed (Result: exit-code) since Thu 1970-01-08 06:27:21 JST; 8min ago
     Docs: man:systemd-sysv-generator(8)

Jan 08 06:27:21 centos-rpi3 systemd[1]: Starting LSB: Bring up/down networking...
Jan 08 06:27:21 centos-rpi3 systemd[1]: network.service: control process exited, code=exited status=6
Jan 08 06:27:21 centos-rpi3 systemd[1]: Failed to start LSB: Bring up/down networking.
Jan 08 06:27:21 centos-rpi3 systemd[1]: Unit network.service entered failed state.
Jan 08 06:27:21 centos-rpi3 systemd[1]: network.service failed.

【注意】
CentOS7 から手動でファイルをいじる方法は推奨されていません。
nmtui を使用して NetworkManager で設定するのを推奨されています。
nmtui のほうが簡単にIPアドレスを設定できるので、おすすめです。
コマンドで設定ができる nmcui というのもあります。
.
.
6. yum コマンドを使用できるようにする
6-1. 「/etc/yum.repos.d/CentOS-armhfp-kernel.repo」を下記のように編集する

[centos-kernel]
name=CentOS Kernels for armhfp
baseurl=http://mirror.centos.org/altarch/7/kernel/$basearch/kernel-$kvariant ★ここを変更する
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
       file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-AltArch-Arm32

[centos-kernel]
name=CentOS Kernels for armhfp
baseurl=http://mirror.centos.org/altarch/7/kernel/$basearch/kernel-rpi2
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
       file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-AltArch-Arm32

※なにもせずに「yum update」をすると、404エラーが発生します。

[root@centos-rpi3 ~]# yum update
Loaded plugins: fastestmirror
base | 3.6 kB 00:00:00
http://mirror.centos.org/altarch/7/kernel/armhfp/kernel-%24kvariant/repodata/repomd.xml: [Errno 14] HTTP Error 404 - Not Found
...
failure: repodata/repomd.xml from centos-kernel: [Errno 256] No more mirrors to try.
http://mirror.centos.org/altarch/7/kernel/armhfp/kernel-$kvariant/repodata/repomd.xml: [Errno 14] HTTP Error 404 - Not Found

6-2. yum の更新を行う

yum clean all
yum update

.
.
7. pi@centos に変更してみる
6-1. 現在使用しているのは「root@centos-rpi3」となりますが、rootを権限で常にいるのはよくないとききます。そのため、別のアカウントを作ってみましょう。
6-2. 次のようにすることで、簡単にpiユーザーを作成できます。

useradd pi
passwd pi

6-3. ホスト名もついでに変更する

hostname centos

6-4. 新規に作ったpiユーザーで、sudoを使用できるようにする

visudo

6-5. 「visudo」で「/etc/sudoers」を編集します。

## Allow root to run any commands anywhere 
root	ALL=(ALL) 	ALL

## Allow root to run any commands anywhere 
root	ALL=(ALL) 	ALL
pi	ALL=(ALL)	ALL

6-6. pi ユーザーで ssh ログインが出来るようになり、pi で sudo も利用が行えます。
.
.
7. 便利なものをインストールする。
7-1. nano のインストール

sudo yum install nano

7-2. make コマンドが使えるようなツールのインストール

sudo yum install make gcc gcc-c++

.
.
8. Sambaをインストールする。
8-1. インストール

yum install samba

8-2. アカウントを作成する(下記のどちらかのコマンドでよい)

smbpasswd -a pi
pdbedit -a pi

8-3. 設定ファイルを編集する

vi /etc/samba/smb.conf

8-4. 中身がおかしな部分があるので修正する

[global]
        workgroup = SAMBA
        security = user

        passdb backend = tdbsam

        printing = cups
        printcap name = cups
        load printers = yes
        cups options = raw
        encrypt_passwords = yes ← 実際は"encrypt passwords"

8-5. アカウントを確認する

pdbedit --list

8-6. サービスの起動と、自動開始の設定、ファイアーウォールの設定をする

systemctl start smb.service
systemctl start nmb.service
systemctl enable smb.service
systemctl enable nmb.service
firewall-cmd --permanent --add-service=samba --zone=public
firewall-cmd --reload

8-7. ホームディレクトリも見えるようにしておく

setsebool -P samba_enable_home_dirs on

何かしたら追記していきます
.
.


参考サイト
・2016/02/18 pb_tmz08 – [Raspberry Pi] RaspberryPi 2でCentOS7 ことはじめ
・2017/03/21 MSeeeeN – Raspberry Pi 3 で CentOS 7 を動かす
・2016/09/22 NAZA – raspberry Pi3にcentos7を入れてみた – Qiita
Raspberry Pi 用のSDカードを初期化する方法
Raspberry Pi 3へのCentOS7のインストール 基本編
NetworkManager 使用方法(nmtui編)
・2014/07/12 enakai00 – RHEL7/CentOS7でipコマンドをマスター
・2017/01/22 miyu – net-tools は使われなくなり iproute2 が標準的になるかも
・2016/04/14 k-sh – CnetOS 7 (on Raspberry Pi)でのrpmパッケージビルド環境の構築
・2015/06/28 CentOS 7 で ネットワーク(IPアドレス、ホスト名、ドメイン名など)の設定を行う

 

DirectXとOpenGLのカリングの設定方法の違い

投稿日: 更新日:


はじめに

こんにちはー!
今日は、3D描写するときのテストの話をします。

テストと聞いて何を思い浮かびますでしょうか。
試験とか、勉強とかそんなことをきっと思い浮かべるでしょう。

作成した3Dポリゴンの頂点を描写するときも、
本当に描写していいのか、無駄ではないのか試験をしています。

いくつかテスト方法を紹介しましょう。

背面カリング(裏面カリング)
モデルは多くの3角ポリゴンから構成されています。
モデルが回転すれば、裏を向いているポリゴンが出てきます。
そんな裏を向いている面を描写しないようにテストします。

クリッピング
視錐台(しすいだい)の中に、ポリゴンが収まっているかどうかテストします。
例えば、カメラより手前側にポリゴンが来ていないかといったチェックです。

バウンディングボリュームなど
見えている部分をより特定していく方法です。
衝突判定などにも、高速化のために使用するよ!

今回は、この中でも裏面カリングの設定方法について解説します。


カリングの仕組み

3Dオブジェクトは、細かな三角形ポリゴンによって構成されています。
それは三角形が最も小さな単位だからです。

では、この三角形の裏と表をどのように決めればいいのでしょうか。
まず前提条件として、ディスプレイに三角形を表示することを考えます。
三角形を構成するためには、3点の座標が必要です。
この3点の座標を囲めば、ポリゴンを作ることができるのです。

これに対して、裏と表を決める方法は、
三角形を結ぶ順番が、時計回りか、反時計回りかで決めればいいのです。
そう、決めの問題なのです!

例えば、表向きを時計回りと考えてみましょう。
3角形の頂点を裏返した場合は、
反時計回りの順序になるので裏と表が分かるわけなのです。


カリングの計算方法

3つの頂点が、時計回りか反時計回りかは簡単に計算できます。

下記の3つの頂点があり、a, b, c の3点を順番に通る三角形を考えます。
なお、2次元なので、 x と y 以外は 0 としています。

そして、
a, bへ向かうベクトルmを計算します。
a, cへ向かうベクトルnを計算します。

あとは、クロス積を計算しましょう。
計算にはmnの各ノルムと角度θを使用しますが、
ベクトルのまま計算することもできます。
なお、a, b, c の3次元目が 0 のため、下記のように簡略化されます。

クロス積の結果は、mnと直行するベクトルです。
従って3次元目の成分 、直行成分となっており、正負で方向が分かります。

ね!かんたん!


カリングの設定方法

各種エンジンでの設定方法をまとめてみます。
mk様の算譜記録帳にもまとまっております。

DirectX 9

次のように、裏面(描写しない面)が時計回りなのか、反時計回りなのかを設定します。
つまり、時計回りを表とみなす場合は、D3DCULL_CCWを設定する必要があります。

参考:D3DCULL

// 背面のカリングはしません。
SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

// 三角形の頂点が時計回りならば裏向きと見なし描画しません。
SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);

// 三角形の頂点が反時計回りならば裏向きと見なし描画しません。(デフォルト)
SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);

DirectX 11

FrontCounterClockwise で表面の定義をして、
CullMode で描写しない面の設定が可能です。

参考:D3D11_RASTERIZER_DESC

// 三角形の頂点が反時計回りならば三角形は前向き
FrontCounterClockwise = TRUE;

// 三角形の頂点が時計回りならば三角形は前向き(デフォルト)
FrontCounterClockwise = FALSE;

// 常にすべての三角形を描画します。
CullMode = D3D11_CULL_NONE;

// 前向きの三角形を描画しません。
CullMode = D3D11_CULL_FRONT;

// 後ろ向きの三角形を描画しません。(デフォルト)
CullMode = D3D11_CULL_BACK;

OpenGL

glFrontFace で表面の定義をして、
glCullFace で描写しない面の設定が可能です。

参考:glFrontFace, glCullFace

// 三角形の頂点が時計回りならば三角形は前向き
glFrontFace(GL_CW);

// 三角形の頂点が反時計回りならば三角形は前向き(デフォルト)
glFrontFace(GL_CCW);

// 常にすべての三角形を描画します。
glDisable(GL_CULL_FACE);

// 前向きの三角形を描画しません。
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);

// 後ろ向きの三角形を描画しません。(デフォルト)
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);

おわりに

今回は、描写テストでも基本的なカリングの解説をしました。

カリングは計算も簡単で、
お手軽に描写負荷を下げることができます。

レンダラーを作る方や、
頂点とインデックスで構成された
モデルデータのローダーを作成する方などの参考になると幸いです。

では、またね!


関連記事

3DCGの座標系の紹介
DirectXとOpenGLの回転行列、回転軸、回転方向
DirectXとOpenGLのビュートランスフォーム行列の違い
DirectXとOpenGLの射影トランスフォーム行列の違い
DirectXとOpenGLのビューポート行列の違い
DirectXとOpenGLのベクトル/行列演算の違い

DirectXとOpenGLのベクトル/行列演算の違い

投稿日: 更新日:


はじめに

こんにちー。
なたでです。

今日も引き続き勉強会です。

これまで、座標系とその変換を学んできました。
今回、実際にその計算を行うため、
具体的にはベクトルや行列演算について
どのようにソースコード上で書けばいいか学んでいきましょう。

ということで・・・
DirectX、OpenGL、各シェーディング言語の型や計算の特徴をまとめるぞー!(おー!)


言語ごとのベクトルと行列の演算

主流なエンジンでのベクトルと行列の型の例の紹介と、演算例を確認しましょう。
.

DirectX

D3DXVECTOR4
x, y, z, w成分を持つ横ベクトル
D3DXMATRIX
4行4列の行列
D3DXMatrixMultiply
行列同士の掛け算を行う

// ベクトル(横ベクトル)の宣言
D3DXVECTOR4 v4 = D3DXVECTOR4( x, y, z, w);
D3DXVECTOR4 v3 = D3DXVECTOR3( x, y, z);
// 行列の宣言
D3DXMATRIX M = D3DXMATRIX(
	_11, _12, _13, _14,   // row 1
	_21, _22, _23, _24,   // row 2
	_31, _32, _33, _34,   // row 3
	_41, _42, _43, _44 ); // row 4
// 行列同士の掛け算 A * B = C
D3DXMatrixMultiply(&C, &A, &B);
// ベクトルのドット積 a・b = c
c = D3DXVec3Dot(&a, &b);
// ベクトルのクロス積 a×b = c
D3DXVec3Cross(&c, &a, &b);
// 3次元ベクトルと4x4行列との積 aM = b
D3DXVec3TransformCoord(&b, &a, &M)

.

HLSL(High Level Shading Language)

DirectX用のシェーディング言語です。

float4
x, y, z, w / r, g, b, a 成分を持つ横ベクトル
float4x4
4行4列の行列
mul

// ベクトル(横ベクトル)の宣言
float4 v4 = float4( x, y, z, w);
float3 v3 = float3( x, y, z);
// 行列の宣言
float4x4 M = float4(
	_11, _12, _13, _14,   // row 1
	_21, _22, _23, _24,   // row 2
	_31, _32, _33, _34,   // row 3
	_41, _42, _43, _44 ); // row 4
float4x4 M = float4(
	_m00, _m01, _m02, _m03,   // row 1
	_m10, _m11, _m12, _m13,   // row 2
	_m20, _m21, _m22, _m23,   // row 3
	_m30, _m31, _m32, _m33 ); // row 4
	
// 行列同士の掛け算 A * B = C
C = A * B;
// ベクトルのドット積 a・b = c
c = dot(a, b)
// ベクトルのクロス積 a×b = c
c = cross(a, b)
// n次元ベクトルとnxX行列との積 aM = b
b = mul(a, M);

補足として成分ごとの演算 (DirectX HLSL)によると、
行列のコンストラクターのパッキング順は、常に行優先になるとのことです。

.

OpenGL

GLdouble v[4];
4つの値を持つ縦ベクトル(型はGLfloat / GLdouble の2種類)
GLdouble M[16];
4行4列の行列(型はGLfloat / GLdouble の2種類)
glMatrixMode + glMultMatrix
行列同士の掛け算を行う

// ベクトル(縦ベクトル)の宣言
GLfloat v4[4] = { x, y, z, w };
GLfloat v3[3] = { x, y, z };
// 行列の宣言
GLfloat M[16] = {
	m00, m10, m20, m30,	// column 1
	m01, m11, m21, m31,	// column 2
	m02, m12, m22, m32,	// column 3
	m03, m13, m23, m33	// column 4
};

// OpenGL単体では計算には向いていない
// OpenGLでは、あらかじめ設定した行列に対して操作を行うことができる

// 行列同士の掛け算 A * B = C
glMatrixMode(GL_MODELVIEW); // とりあえずモデルビュー変換用の行列で計算する
glLoadMatrixf(A);	// A をロードする
glMultMatrixf(B);	// A * B
glGetFloatv(GL_MODELVIEW, C)	// 計算結果を取り出す

注意点として、縦ベクトルのため行列との掛け算は Mv の順序で計算されます。
作成した行列を最終的にベクトルvに対して掛け算をするため、
行列を準備する際は、逆の順番で行列同士の掛け算が必要となります。
.

GLSL(OpenGL Shading Language)

OpenGL用のシェーディング言語です。

vec4
x, y, z, w / r, g, b, a / s, t, p, q 成分を持つ縦ベクトル
mat4
4行4列の行列

// ベクトル(縦ベクトル)の宣言
vec4 v4 = vec4( x, y, z, w);
vec3 v3 = vec3( x, y, z);
// 行列の宣言
mat4 M = mat4(
	m00, m10, m20, m30,	// column 1
	m01, m11, m21, m31,	// column 2
	m02, m12, m22, m32,	// column 3
	m03, m13, m23, m33	// column 4
);

// 行列同士の掛け算 A * B = C
C = A * B;
// ベクトルのドット積 a・b = c
c = dot(a, b)
// ベクトルのクロス積 a×b = c
c = cross(a, b)
// nxX行列とn次元ベクトルとの積 Ma = b
b = M * a;

OpenGLのヘルパー関数(glMatrixMode、glMultMatrix)と違って、
固定化された行列に対して操作していく必要はありません。
行列を掛け算していくときは、次のように1行に1つの掛け算を順番に書くことができます。

// 最後の計算が、aM = b の場合(ベクトルが縦行列なので実際はできない)
B = A * B;
C = B * C;
X = v * C;

// 最後の計算が、Ma = b の場合(ベクトルが縦なのでこの書き方できる。)
B = B * A;
C = C * B;
X = C * v;

.

WebGL

標準では用意されていないため、
次のようなJavaScriptライブラリを使用して管理します。(いろいろある)
glMatrix
vec3など、GLSLと似た感覚の型名をもてる(列管理)
Sylvester
古くからある行列用の一般的なライブラリ。WebGLでも拡張jsを追加すれば利用可能(行管理)

多くのライブラリで共通して言えることは、
OpenGLと同様に、行列のデータを列でもつことです。

[0,  1,  2,  3, // 1列目
 4,  5,  6,  7, // 2列目
 8,  9, 10, 11, // 3列目
12, 13, 14, 15] // 4列目

この理由は、あらかじめ列で持っていくことで、
変換行列を uniformMatrix4fv などでGPU内のシェーダーに
アップロードする際に余計な変換をする必要がないためです。


縦ベクトルと横ベクトルの違い

これまで、DirectXではベクトルは横ベクトルとなって
行列と掛け算するときは、vMとなる。
そして、OpenGLではベクトルは縦ベクトルとなり
行列と掛け算するときは、Mvとなることが分かりました。

しかも、行列を初期化する際の順序も
行で初期化するのか、列で初期化するのか、混乱してきます。

一度ここで、整理したいと思います。

次のような3×3行列を考えましょう。

DirectXの横ベクトルで演算した場合、次のようになります。

OpenGLの縦ベクトルで演算した場合、次のようになります。

そして、ソースコード上、行列を初期化する場合は次のようになることです。

ここまでを、まとめると、非常に複雑で混乱しそうです。
しかし、よくみると気が付くことがあります。

初期化時の配列の位置と、実際に掛け算したときの結果を色付けしました。

なんと、DirectXとOpenGLとで一見、違いがないではありませんか!
これは初期化の順序が異なりますが、掛け算する順序も変わるため
最終的には、コード上の見た目の順序でいえば、掛け算の結果は一致するわけです。

もちろん、行列同士を掛け算する際は、DirecXとOpenGLとで掛ける順序を気にする必要はありますが、
上記のことをしっていると少し混乱がおさまるかとおもいます。


おわりに

今回は、実際の行列/ベクトル演算の書き方を確認しました。

DirectXとOpenGLとで、掛け算をする順序がことなること、
行列の初期化方法が異なることが分かりました。
ただ、掛け算の順序と行列初期化方法の違いにより、
見た目上の掛け算は、一致している部分があることも分かりました。

以上、お疲れ様です。

DirectXとOpenGLのビューポート行列の違い

投稿日: 更新日:


はじめに

こんにちは!
3DCGの再勉強中のなたでです!

今まで、次の変換行列の違いをみてきました。

・DirectXとOpenGLのビュートランスフォーム行列の違い
・DirectXとOpenGLの射影トランスフォーム行列の違い

本日は変換の最後のビューポート行列の違いを調べてみましょう。
といっても、ここまでくると右手座標系・左手座標系という話はなくなっているので、
DirectX と OpenGL とで、きっと同じだと思いますが……。

おさらいですが、ビューポート変換は
正規化デバイス座標系から、スクリーン座標系への変換となります。

関連記事です。もし読んでないなら下から上に読んでいくといいよ!
DirectXとOpenGLの射影トランスフォーム行列の違い
DirectXとOpenGLのビュートランスフォーム行列の違い
3DCGの座標系の紹介
DirectXとOpenGLの回転行列、回転軸、回転方向


数式の比較

.

DirectX

D3DVIEWPORT9構造体でビューポートの定義をして、
SetViewportで設定できるようです。

行列というよりは設定値ですね。
ちなみにデフォルト値は次のようです。

D3DVIEWPORT9 vp;
vp.X = 0;
vp.Y = 0;
vp.Width = RenderTarget.Width;
vp.Height = RenderTarget.Height;
vp.MinZ = 0.0f;
vp.MaxZ = 1.0f;

vp.X, vp.Y が描写する左上の座標
vp.Width, vp.Height が描写幅
vp.MinZ, vp.MaxZ が深度値の変換です

なお、実際の行列自体は、ビューポートとクリッピング (Direct3D 9)に記載されています。

.

OpenGL

OpenGLでも行列というより関数になっています。
glViewportglDepthRangeで直接設定する形です。

void glViewport( GLint x, GLint y, GLsizei width, GLsizei height);
これの注意点として、ウィンドウで設定している場合は、左下が原点になります。
Windowsの原点は右上で、下に行けばy+にいくのに対して、
glViewportの原点は左下で、上に行けばy+になるわけです。

void glDepthRange( GLdouble nearVal, GLdouble farVal);
描写先の深度値変換の設定です。
0 – 1が初期値です。
※参考 [DEV][CG]DirectXのPerspectiveとOpenGL Frustumの違い

なお、行列は次のようになります。

※参考 ビューポート変換行列 – code snippets

行列は何を表しているのでしょうか

これまでのおさらいをしましょう。
カメラ座標系から射影トランスフォームをしたものがクリッピング座標系になり、
クリッピング座標系から x, y, zw で割った遠近除算したものが
正規化デバイス座標系です。この正規化デバイス座標系から、スクリーン座標系へ変換します。

この正規化デバイス座標系は、次の値の範囲に収まります。
-1 <= x <= 1
-1 <= y <= 1
-1 <= z <= 1 (IF OpenGL)
0 <= z <= 1 (IF DirectX)

これらをふまえて、まず DirectX の行列

この式が実際にどのようなことをしているか考えてみましょう。
実際に計算すると次のようになります。

行列の計算は次のようになるので、

ベクトルに行列を掛け算して、式変形すると次のようになります。

同じく、OpenGLの行列も計算してみましょう。

さきほどのおさらいで x, y,がとる範囲(-1~1)をみるとわかるように、
行列の計算によりオフセットの位置から、ウィンドウの幅分へ引き延ばしていることが分かります。
ここで、ウィンドウの大きさに引き延ばすからこそ、前回の 射影トランスフォーム行列 にて、
ウィンドウのアスペクト比を使用して補正していたのですね。

1つ気になるところといえば、DirectXでは が反転している点があります。
これはDirectXの出力先のスクリーン画面では、yが大きいほど下方向に描写されるためです。
3D空間上ではもちろん上がプラスため、ビューポート変換で反転しているわけです。

OpenGLの場合は、そもそも出力先のスクリーン画面が上がプラスの設計のため
行列を作っても y の反転は不要となっております。

z については、0がMinZ に 1がMaxZ になるような式になることが分かります。

DirectXとOpenGLとで、射影トランスフォーム後の 値の範囲が異なるため
微妙に式が違っています。実際に計算してあてはめると次のようになります。

OpenGLでも結局、最終的なz値は 0 <= z <= 1 の範囲に収めているようですね。

なお、この MinZMaxZ の設定を利用すると、たとえば、
1度目の描写は、MinZ = 0.0, MaxZ = 0.5
2度目の描写は、MinZ = 0.5, MaxZ = 1.0
のように、1度目の描写の後ろ面に、2度目の描写を行うといったことができます。


おわりに

ここまでのいくつかの回で、
3DCGに必要な座標系と、その座標系への変換をまとめました。
そして、右手系と左手系、OpenGLとDirectXとで
数式のどこに差が表れてくるのかが分かりました。

これまでDirectXを使っていた方が、WebGL(OpenGL)を始めたい。
逆に、WebGLを使っていた方が左手座標系のUnityを使いたいなどがあるかもしれません。
そんな時に、これらのまとめを見て、座標系や、
DirectX/OpenGLの各変換のこと動きを知っておくと
理解が進みやすいのかなと思います。

以上、ありがとうございました。

DirectXとOpenGLの射影トランスフォーム行列の違い

投稿日: 更新日:


はじめに

こんにちは!

引き続いて座標変換の話をします。
本日は射影トランスフォーム行列を覗いてみましょう。

前回の関連記事(予備知識として下から上に読んでおきましょう)
DirectXとOpenGLのビュートランスフォーム行列の違い
3DCGの座標系の紹介
DirectXとOpenGLの回転行列、回転軸、回転方向


数式の比較

.

DirectXとOpenGLのヘルパー関数

これまでと同様、行列作成用の関数が用意されています。

DirectX
matrix *D3DXMatrixPerspectiveFovLH( matrix *pOut, float fovy, float Aspect, float zn, float zf );
左手座標系のパースペクティブ射影行列を作成
matrix *D3DXMatrixPerspectiveFovRH( matrix *pOut, float fovy, float Aspect, float zn, float zf );
右手座標系のパースペクティブ射影行列を作成

OpenGL
void gluPerspective( double fovy, double aspect, double zNear, double zFar);

.

ヘルパー関数を使用したときに作成される行列

DirectXは、マイクロソフトのページの行列
OpenGLは、OpenGLプログラミングメモの「gluPerspectiveを置き換える」の行列を参考にします。

式は、DirectXのD3DXMatrixPerspectiveFovLHをベースにして、
ゲーム3D数学完全ホワイトボックスなパースペクティブ射影変換行列
を参考にしつつ、式変形したものを示します。

引数は次の通り
fovY  視体積の上下方向の視野角(0度から180度)
Aspect 近平面、遠平面のアスペクト比(Width / Height
Far  カメラから遠平面までの距離(ファークリッピング平面)
Near  カメラから近平面までの距離(ニアークリッピング平面)

一度これらを次のようにまとめます。
zoomYの逆関数、Aspectの求め方を補足で書きました。

上記の変数を使用して、DirectXとOpenGLの各行列を書くと次のようになります。

.

右手系と左手系の違い

D3DXMatrixPerspectiveFovLHとD3DXMatrixPerspectiveFovRHを比較すると分かりやすいのですが、
行列の3行目の正負が反転しています。
D3DXMatrixPerspectiveFovRHと同じ右手系のOpenGLのgluPerspectiveの3行目の正負が等しいです。
この正負の反転により、掛け算の最終結果のZ値の値を反転させています。

これまで右手座標系の計算では手前をZ値のプラスとして考えていましたが、
この反転の計算により、奥をプラスに変換させます。
つまり左手座標系へ変換させているわけです。
.

行列は一体何をしているのか

一度、このプロジェクショントランスフォーム行列の式を実際に使用した場合を少し考えてみましょう。
行列は、ベクトルに掛け算して使用します。

掛け算した結果は、クリッピング座標系・クリップ座標系と呼ばれます。そして、この座標系から、wで割ったものが正規化デバイス座標系と呼ばれます。

上記の式からx値とy値については、簡単に何をしているのか想像がつくようになります。ABzoomXzoomYですので、アスペクト比に関わる値を掛けて縦と横幅を調整することに繋がっています。

z値に関しては、上記の式変形ではまだ分かりにくいので、さらに値を代入して数式を変形していきましょう。
今回は、分かりやすくD3DXMatrixPerspectiveFovLHの値を代入しました。

このようにすると、zの値が、NearからFarまでの値をとると、0~1の結果になることが分かるかと思います。

さらにイメージしやすくなるように、1と10の値を代入してみましょう。

これをグラフに書くと次のようになります。

xが1から10をとると、f(x)が0から1までをとることが分かります。

射影行列を位置ベクトルに掛け算した結果のz値の値を、0未満と1以上の場合を表示させないと作ることで、
NearからFarまでにある座標のデータを表示させるという作りが可能となります。
なお空間のNearからFarまでを切り出すため、クリッピング座標系と呼ばれていると思われます。

ところで、数式から分かるように Near = Far と設定してしまうと、0割り算エラーが発生してしまいます。
ある程度値を持ったうえで、 Far > Near となるような値を設定する必要があります。

DirectXとOpenGLの式の違いの理由

右手系の行列を作成するD3DXMatrixPerspectiveFovRHと
右手系のOpenGLのgluPerspectiveとの式が微妙に異なることにお気づきでしょうか。
本来同一になるはずなのに、Near Far が関係するZ値の計算部分が少し違います。

なぜ、このようになっているのでしょうか。

先ほどと同じようにグラフを作ってみましょう。

さらに、NearFarの値を適当に決めてグラフを描きます。

なんと、OpenGLでは、xが1から10をとると、f(x)が-1から1までをとります。
OpenGLの射影変換後のz値は正負をとるということになります。

DirectXではz値が0から1なので、このような違いが行列に現れてきたのです。


おわりに

今回は、座標系の変換の中で最も?よくわからない
プロジェクショントランスフォーム行列をとりあげました。

右手座標系だったものはこの変換により左手座標系になることが分かりました。

DirectXの右手座標系のD3DXMatrixPerspectiveFovRHと
OpenGLの右手座標系のgluPerspectiveは基本的に同じですが、
DirectXのZ値は0~1までに正規化されるのに対して、
OpenGLのZ値は-1~1までに正規化されるため
微妙に行列が違っているということが分かりました。

よくわからない式もグラフをかくと、分かりやすくてよいですね。

以上。おつかれさまでした!