近況、家を買った話など

 人生は爆速で過ぎて行き、気づいたら今月で20代の折り返し地点となる25歳になった。

 私は小学生の時からメイプルストーリーをはじめとするいくつかのオンラインゲームをやっていたし、社会に出たのも20歳だったので、学校以外で身の回りの人間が年上というのは自身にとっての当たり前だった。
しかしこの頃、会社には次々と年下の新入社員が入社してくるし、FF14のレイド攻略の固定メンバーは年下ばかりだ。否応がなく、自分がもう最若年齢気取りではいられないことを実感させられる。

 社会に出た当時は、2~3年おきに転職を繰り返し、やりたいことをやりまくる刹那的な人生を送るんだと漠然と考えていたが、蓋を開けてみれば大企業に根を張ってのんびり生きている。
 収入は安定しているし、仕事は忙しすぎることもなく、プライベートではゲームであったり読書であったり、好きな時間を十分に過ごすことができているので、特に現状に不満がない。向こう数年は転職することもないだろう。
 C++とかいう古の言語で仕事をしているせいで、新鮮な技術に触れる機会が少なく、20代というもっともインプットを大事にすべき時間を棒に振っているきらいはあって、多少の危機感を感じなくはないが......まあ会社員として生きてればそういうこともあるだろうを割り切って生きている。
 ある種の意識高い系の界隈からは軽蔑されるような生き方かもしれない。


 ところで、最近よくゲームプログラマーは労働量の割に給料が安いみたいなツイートをよく見かける。
 これはコンシューマーの世界の話だと思っていて、Webベンチャーの派生であるソーシャルゲーム業界では、あまりそのような傾向はない。AppStoreランキングの上位常連の企業はすべからく社員に適切な給料を払っているだろう。一般的に揶揄されるメテオフォール開発が行われている現場も滅多に見ない。忙しいところは忙しいが、プロジェクトマネジメントの努力は最低限なされていると思う。
 
 だが世間的にもソシャゲはゲームじゃねえ!という声も少なからずあり、それなら我々はゲームプログラマーの定義に当てはまらないのかもしれない。悲しいことだ。
 ガチャが非常に頻繁に批判に晒されているのを見るとやるせない気持ちになる。我々開発者だって、別に集金だけがしたいわけではなくて、価値のあるものを提供したいという思いでゲームを作っている。
 ただ、日本のゲーム業界のビジネスモデルとして、もっともガチャというシステムが優れているだけだ。感情よりもビジネスが大事にされる場面なんて、世界のいたるところに溢れているというのに。

 もちろん、コンシューマーでも大きく成功し続けている企業はある。
 ただ、現在、ユーザーがゲームに求めるクオリティは高くなりすぎて、ゲームの開発規模は際限なく肥大化していく。一本のゲームを作るために何十億の開発費やマーケティング費がかかっている。
 しかしゲームそのものの値段は今も昔も変わらず、6000~8000円程度だ。

 もはやコンシューマーのゲーム業界は巨大な資本を持つ企業しか参入することができないだろう。それを是としたのは、ソーシャルゲームを「低クオリティ」「クソゲー」と揶揄するメンタリティに他ならないと思う。

 
 もちろん、大企業ではない会社がリリースしていて、成功したコンシューマーゲームもある、最近だと、サクナヒメが挙げられるだろう。
 ただそのサクセスストーリーの裏には、代表が借金をしながら開発をしたり、パブリッシャーを外部に置き販売権を譲渡するなど、多大な苦労が見え隠れする。

 それでも尚サクナヒメは無事リリースされ、ヒットした。本当に泣ける話だ。そしてやるせなさも感じる。きっとこのような成功の陰では、開発が断念されたもの、リリースしたがヒットしなかったもの、多くのゲームクリエイターの悲鳴が多くあるのだろう。
 
 まあ、そういったリスクを背負うことが出来ず、会社員に甘んじている私がこんな話をするのは、卑怯なのだろうが......


 さて、去年の秋ごろ、リモートワークの影響で会社から徒歩圏内のマンションに高い金を払って住む理由がなくなり、引越しを決意した。
 次に住む家はペットがOKなところが良いと考えていた。家に時間がいる増え、ペット的な存在が恋しくなったからだ。ただ、都内でペットOKのマンションというのはかなり敷居が高く、絶望的だった。
 そのため、安定志向に心変わりした背景もあり、一戸建てを購入することにした。



 周りの人間は「家を買う」という字面に面を喰らっていたが、実は家を買うというのはそんなにハードルの高いものではない。
 私は不動産投資家ではないので自分の発言に一切の責任は持てないが、都内で家やマンションを買うことは、高い賃貸に住み続けるよりよっぽど建設的だ。(ギャグではない)
 ワンルームマンションみたいなところで安い家賃で暮らすことが苦にならないのであれば、家なんか買う必要は一切ないと思うが、家賃に10万円とか払うくらいなら、家を買った方がいいと思う。
 都内ないしその近辺では住宅の需要は非常に高く、築20~30年の一戸建てでも、新築一戸建てより多少安いくらいでしかなく、有事の際の売却を視野に入れることも可能だ。20年後単位になってくると日本がどうなっているかわからないけれど。
(ちなみに田舎の中古の一軒家の需要はゴミらしい。少子化が進めば尚更だろう。)

 更に、現在は低金利時代と呼ばれていて、恐ろしいほど住宅ローンの金利が安い。
 また、新築一戸建ての住宅ローンを組んだ場合、10年間税金控除が受けられる。その金額は借入残高の1%で、最大年間40万。これは固定資産税を考慮しても余りある莫大な金額だ。
 つまり最大4000万円の家まで控除を受けることができ、その場合35年ローンを組んだとしても返済額は月に10万円ほどにしかならない。そして、10年後には木造住宅の固定資産価値はほぼゼロになるので土地のみの固定資産税になっているためそれほどの負担にはならないはず。

 つまり、家を買うということは、賃貸よりも安い家賃で広い家に住むことが出来、税金控除を受け、そして最終的に資産が残るというメリットを享受できるということだ。
 賃貸のメリットとしては、天災などのリスクがない、引越しを繰り返せば常に新しい住居に住むことができる、転勤や転職などに対応できる、あるいはワンルームマンションなどで極限までコストを削減できる、固定資産税を払わなくてよい、などがあるだろうか。


 ちなみに、家の値段は3680万円で、もろもろの諸費用を合わせて3900万ほどになった。私は優良な不動産屋を選んだため、仲介手数料がかかってないが、実際には仲介手数料の相場となる「家の価格3%+6万円」である120万円がこの上に乗ることになる。
 不動産屋選びは慎重にした方がいい、たいていの大手はこの「家の価格3%+6万円」という決して少なくない金額を請求してくるためだ。
 そして、一度物件を内覧するときに仲介してもらった不動産屋を後から変えることはできない。これは暗黙の了解的な問題のようだ。すべての不動産屋は共通の物件のデータベースの閲覧権限をもっており、物件の仲介自体はどんな物件でもすべての不動産屋で可能なので、物件よりも先に不動産屋を選ぶべきだとは思う。

 自己資金は私は400万ほど用意したが、これが十分かは本人の社会的信用による。銀行は一般に自己資金が少ない顧客は信用してくれないため、スムーズに住宅ローンを借り入れるためには、ある程度の自己資金が必要だ。
 ちなみに、私は「独身」「持病もち」「勤続年数が数年」という役満が揃っていたので借入で死ぬほど苦労した。どれくらい苦労したのかというと、銀行7社くらいと交渉した。特に持病の問題が深刻で、銀行の審査が下りても、団体信用生命保険と呼ばれる保険会社の審査が通らないことが多々あった。かなり絶望的な状態であったが、不動産屋の努力の甲斐あって無事に現在は借入に成功している。


 まあ、別に私は不動産購入を布教したいわけではないのでこの辺りにしておく。各々好きな人生を生きたらよいと思う。 


 かくして、私は、持ち家を獲得し、新たな人生のスタートを切った。
 これからは積み立てNISAや、ふるさと納税個人年金保険など、非常にリスクの低い投資や、税金対策などを行いつつ、独身生活を満喫していく予定である。こういった資産運用については、またそのうち別途記事を立てたい。
 積み立てNISAはコロナウィルスパンデミックの影響で、世界的に株価が下落した影響でめっちゃリターンが増えた。なぜ株価が下落したのにリターンが増えたのかというと、毎月同じ金額投資しているため、株価が下落すると買える量が増えるからだ。そして、人類が滅びない限り、経済は発展し続けるため、インデックスファンドのような商品に対して投資している場合、かならず評価価値は上がっていくためだ。

 f:id:riyaaaaasan:20210207185830p:plain

 これは私のNISAの保有商品一覧だが、商品によっては30%増えている。適当にファンドを選んで一定金額クレカから引き落とされているだけなのに、金が増えていく様は感動すら覚える。


 あとは結婚さえすれば人生のゴールともいえるのだが、残念ながらその予定は全くない。
 タイムイズマネーの原則に従えば、恋愛は恐ろしいほどハイリスクだ。空から美少女が落ちてくることを願うしかない。
 
 
 2021年も良い年でありますように。

蛇を飼い始めて1年余りが経った

皆さん蛇を飼っていますか?
蛇は令和で今最も注目されているペットです。ソースはありません。

蛇を飼っていると話すとギョっとされることが多々あるのですが、ほ乳類よりもよっぽど扱いやすく、初心者向けのペットだと思います。
もちろん種類によって様々ですが、私の飼っているコーンスネークを例に挙げると、以下の特徴が挙げられます。

・エサ代が安い(月五百~千円程度
成体だと週一ペースでしかご飯を食べないので、月に4匹マウスを与えるだけです。マウスは冷凍のものをアマズゥーンで買うことができます。

・静か
基本的にとぐろを巻いてじっとしています。声帯もないので声を発しません。

・世話が楽
キッチンペーパーをしいたケージで飼育し、フンが生じたらキッチンペーパーを取り換えるだけです。
温度管理はコーンスネークは意外と適当でよく、全身漬かれる水場をおいておけば、冬場だけケージの半分にパネルヒーターがあたるように設置するだけで、あとは勝手に体温調整してくれます。

・かわいい
かわいいです。


たまに勘違いされるのですが

>生きてる餌しか食べないんじゃないの?
カエルやカメレオンなどは一部そういう種類が要るみたいですが、ヘビは生餌しか食べないみたいな話は聞いたことないです。

>噛みついてこない?
コーンスネークは非常に大人しい子が多いです。

>湿度・温度管理大変なんじゃないの?
一部爬虫類は厳密な管理がいるそうですが、コーンスネークは適当で大丈夫です。



購入直後のヘビ
年齢はべビー~ヤングくらいな感じです

youtu.be

f:id:riyaaaaasan:20200806184254p:plain

かわいいね


これは最初のえさやりです
かなり苦戦してますね、今ではエサを与えたら1秒で喰いつくんですけどね

youtu.be


これはヘビのあくびです
性格には「あご直し」と呼ばれるもので、ご飯のあととかにたまにやってます
一回生で見てみたいと思っていたので、撮影できたときは歓喜でしたね

youtu.be

youtu.be



これはある日家に帰ったらヘビがえらいことになってたという動画です
呑気に動画撮ってる場合か!って感じですが、シェルターの重さごときで死ぬようなヤワな体ではないです むしろ今では私がヘビを首に巻いたりして遊んでるとそのまま絞殺されそうになるくらいなんですよね

youtu.be


これは奇跡の一枚ですね ヘビが人間の手の上でこんなに大人しくなるなんてほぼないです
多分使い捨ての手袋をつけているのが大きいんでしょうね 熱をもった巨大な手というものはヘビにとっては危険因子ですから。 でもバカだから手袋つけただけで熱を感じなくなって床にいるような気持ちになっているんじゃないですかね

f:id:riyaaaaasan:20200806190037j:plain


ヘビは懐くの?ということをたまに聞かれますが、人に慣れることはあっても懐くことはないと思います。結局ヘビにとって人間というのは強大で危険な存在ですから、対等な関係になることは難しいでしょうね
「虫は人間に懐くか?」という問いにはほとんどの人がNoと答えるでしょう。多分ヘビは知能のレベルがほ乳類と虫どっち寄りかと言われれば、多分虫寄りなんですよね

とはいえ、たまにこうやって袖になかにもぐりこんで落ち着いてる姿を見たりすると、ヘビがまるで懐いているかのような気持ちになってうれしくなっちゃうわけですね

f:id:riyaaaaasan:20200806191622p:plain


これはヘビの水飲みです ヘビだってもちろん水を飲みますよ かわいいね

youtu.be


ヘビがある程度大きくなってからはファジーマウスをあげるようにしました
ファジーはまあ毛が生えたばかりの若い個体くらいな感じですかね

youtu.be



ところでうちのヘビには名前はないんですが、普通ペットには名前をつけるでしょみたいなことをよく言われます
でもヘビには耳はないんですよね 体全体で振動を感じる機能はあるのですが、それが人間の定義する「音」と同義なのはよくわかっていません
当然ヘビは自分の名前を識別することはできないので、名前をつけることに一切の合理性を見いだせないんですが、マジョリティの人々はそれでも名前をつけるみたいですね やっぱ皆お気に入りの家電とかにも名前をつけるのかな


コーンスネークは10年近く生きるので、私が30代のおっさんになっても隣にいてくれるわけです 健やかに生きろよな


おわり

0からRaspberry Pi 4でさくっとマイクラサーバーを建てるまで

知人からマイクラサーバーを建ててくれと頼まれたから仕方なく0から環境構築した話をするよ。

私はケチな上にパソコンの寿命管理にうるさいので(すぐ壊れるから)、自分のメインマシンを24時間稼働させるのは絶対嫌だし、電気代もかけたくなかった。
こういうときはやっぱり消費電力が超少ないシングルボードコンピュータRaspberry Piに限る。

昔私は2系を使っていたけど、最新のラズパイは4だそうだ。CPUも1.5GHzのクアッドで、メモリも4GBついてるらしい。
最大消費電力は6Wらしいので、24時間ずっと稼働させても電気代は月100円くらい。

というわけでこれを買った↓
LABISTS Raspberry4 4B-32GB(技適マーク入)MicroSDHCカード32G/NOOBSシステムプリインストール/カードリーダ /5.1V/3A Type-C スイッチ付電源/MicroHDMI-to-HDMIケーブルライン/三つヒートシンク/簡単に取り付けケース/日本語取扱説明書/24ヶ月保証
@amazonJPさんから

とりあえず悩んだら絶対これを買った方がいい。ファン、ヒートシンク、OSプリインストール済Micro SD、HDMIケーブルがついてくる。

OSプリインストール済みSDがあるので、起動してちゃちゃっと環境構築するだけ。普通に起動するとGUIが立ち上がるけどコンピューターリソースの無駄なのでさっさとCLIで起動しなおしたほうがいい。

とりあえずエディタはあったほうがいいので

sudo apt-get install vim

はやっておこう。(個人差アリ)

ローカルIPを固定する

多分デフォルトではDHCPを使ってると思うから、/etc/dhcpcd.confを弄る。

例えば、私のネットワーク環境はネットワークアドレスが192.168.11.0で、ルーターのアドレスが192.168.11.1で、サブネットマスクが255.255.255.0で、ローカルIPのホストの数字は(なんでもいいけど)25を割り当てたかったので、以下のようにした。

interface wlan0
static ip_address=192.168.11.25/24
static routers=192.168.11.1
static domain_name_servers=192.168.11.1

wlan0は無線のこと。無線の場合、そもそもインターネットにつなぐ必要があるので

wpa_passphrase  [SSID] [pass]

しておくこと。ちなみにケーブル管理が絶望的に不得意じゃないのなら有線にしたほうがいいです。マジで。サーバーのレイテンシ全然違うよ(無線の人間が何言ってるんだって話だが)
DNS(domain name servers)アドレスってなんやねんって思った人は、まず間違いなくルーターのアドレスいれれば動くと思う。

(03/15 追記) サーバーの遅延について抗議されたので有線接続にした。IP固定の方法はinterfaceをeth0にするだけで他は同じ

rebootすればIPが目的のものになっているはず。

公開鍵を登録してSSH接続できるようにする

ラズビアン(ラズパイのデフォルトOS)はデフォルトでSSH接続ができるから、もうすでにSSH接続できるっちゃできるけど、セキュリティ的にも公開鍵を使ったほうが良い。

公開鍵の作り方はいくらでも資料があるのでぐぐってください。

自分のSSH接続したいメインPCで公開鍵を作ったら、scpコマンドでラズパイのホームディレクトリに転送する。
ユーザーネームはpiとすると

scp ./.ssh/id_rsa.pub pi@192.168.11.25:~/

次、ラズパイのコンソールに入って

mkdir .ssh
cat id_rsa.pub >> .ssh/authorized_keys
chmod 700 .ssh
chmod 600 .ssh/authorized_keys

chmodしているのは念のため。sshはセキュリティ上この辺のファイルのパーミッションがガバガバだと弾かれるので。

これで公開鍵接続の準備は整ったので、パスワード接続を禁止する。
/etc/ssh/sshd_configを開いて、

PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile  .ssh/authorized_keys

と記載しておく。

メインPCに戻って、./ssh/configで、ラズパイのホストの名前をつけてあげる。
名前はなんでもいいけど私はpi4にした。タイプ数少ないの最高!

Host pi4
    HostName     192.168.11.25
    IdentityFile    ~/.ssh/id_rsa
    User            pi

もうこれですぐつながる。

ssh pi4

ポートを開ける

イクラサーバーのデフォルトのポートは25565。自分のルーターのマニュアル見ながら開放しよう。
ちなみに私はv6プラス接続なので、開放できるポートには制限がある。その場合もポート変換をかませば可能。
ここは各自ルーターのベンダーにお問い合わせくださいといった感じなので割愛する。

イクラサーバーのダウンロード、起動

ラズビアンには多分JDKが最初から入ってたはずなので、ダウンロードしてすぐ起動できるはず。

適当にディレクトリ作って、 Download server for Minecraft | Minecraft ここからダウンロードリンクを取得したら

mkdir minecraft
cd minecraft
sudo wget DOWNLOAD_LINK

すると、ディレクトリにサーバー関連ファイルと、利用規約の同意書(eula.txt)が展開されるので
エディタでeula.txtをtrueに書き換える。(公式ページの利用規約を読んでから)

これで準備は完了。
ラズパイ4はメモリ4GBなので2GBくらいアサインするとして

sudo java -Xms2G -Xms2G -jar server.jar nogui

で起動。これでサーバーが全世界に公開されました。

ログ公開サーバーを作る

私はニートの知人にサーバーを貸してるので、仕事中に昼間からマイクラを遊んでる様子をみて励みにしたいから、ログが見れるHTTPサーバーを建てた。

ログサーバーなんて適当でいい、nodejsでちゃちゃっと書こう。ちなみにnodejsはラズビアンに既に入ってた。すげーなおい!

適当にディレクトリ作って、適当に.js拡張子でファイルを作って

var http = require('http');
var fs = require("fs");

const port = XXXX; // 適当なポート
const server = http.createServer((req, res) => {
        if (req.method == "GET") {
                var data = fs.readFileSync(".path/to/minecraft/logs/latest.log");
                res.writeHead(200, {"Content-Type": "text/plain"});
                res.end(data);
        } else {
                res.writeHead(404, {"Content-Type": "text/plain"});
                res.end("Sushi is not found");
        }
}).listen(port);

はい終わり。これはひどい! GETされるとリクエストパス関係なく常にマイクラサーバーの最新ログを返すだけのサーバー。それ以外のメソッドは404!
いいんだよこういうので。最高に堅牢なサーバーは何もしないサーバーだからね。

ちなみに私は例によってv6プラスなのでHTTPポートが開けられないから、適当なポート番号でlistenしてるよ。

このサーバーはデーモン化させたかったのでforeverを使った。

sudo npm install -g forever 
forever start app.js  

これでどこにいてもサーバーが元気に稼働してる様子が見られる。

終わり。

結局オブジェクト指向とはいったいなんなのかとか、ゲームの設計に関して最近思うこと

2兆年ぶりにブログ記事を書いている気がする。レンダリングエンジンの開発に飽きたからこのブログはほぼ用なしになってしまった。ただ、最近プライベートでプログラムを書いてなさすぎるので、リハビリに再開するのもいいかもしれない。プログラムを書くのは好きだが、致命的に目的意識や目標となるエンジニア像が存在しないため、具体的なマイルストーンを設置しないと絶望的に怠惰になりがちだ。

しかし、最近転職してから「設計」というものを考える機会が莫大に増え、それに関しての自身のナレッジ、あるいはモヤモヤなどを文章化することで、自分の中で情報を整理しようと思い立った。

私はゲームプログラマーだ。作っているゲームはリリースされてから数年が経ち、ソースコードの量は膨大で、そしてありがたいことに収益が安定しているため、数年後もちゃんと保守できることを前提として設計しなくてはならない。

私のチームのクライアントの開発の要素として大きく上げられるものとしては、オブジェクト指向設計アジャイル開発の2つがある。巷ではどちらもある種のバズワードのような扱われ方をしている。確かにすべてが上手くいく魔法のような開発手法というわけではないことは確かだが、果たして数年続き、大人数で行われる私のチームにとっての開発スタイルとして正しいのだろうか、そういつも考えている。といっても、アジャイル開発に関してはマネジメントの領域なので、その是非については1エンジニアに過ぎない私には判断できない。

オブジェクト指向設計に話をフォーカスしよう。

で、実際のところ、「オブジェクト指向」を言語化して説明することは無理なんじゃないかと思っている。多態性とかカプセル化とかそういう言葉の定義を説明することは可能だが、いざプログラムを見せられた時に、「これはオブジェクト指向で設計されているか?」という質問に対して回答することはできない。オブジェクト指向プログラミング言語で書かれてますね、とは言えるかもしれないけれど。

この時点で、少なくともプログラムを評価したり、あるいは指導する際に「オブジェクト指向設計」という単語を使うことは不適切に感じる。定量化ができず、共通認識も取れない単語なんて地雷にしかならない。

結局、適切なケースにおいて適切なデザインパターンを適用する、適切な抽象化をする、適切なデータ隠蔽をする、そういった「ある局所的なケースにおいて常に適切な」選択を続けていく結果、再利用性や可読性が最強になる桃源郷のようなプログラムが生まれるという仮定のもと、それをオブジェクト指向設計と定義しているんだろう。だから、オブジェクト指向設計を目指すというのは見えてないゴールに向けて適当に走り出すようなもので、実際に私たちがやるべきなのは、もっと「良い設計」とやらを個々の問題に分解して、見えているゴールを目指すことだと思う。その結果、きっと私たちのプログラムはいつかオブジェクト指向設計にたどり着くんじゃないんかな。知らんけど。

設計に関する個々の問題についてなら、私もそれなりに思想がある。
以下に、私が経験則的に上手くいった設計方針の備忘録みたいなものをまとめる。もちろん個人的な思想なのでその辺は留意してほしい。


...思いついたら書き足す。

クラスは常にミニマムに設計する

モジュールは細かく要素分解されているべきだ。もちろん、一連の手続きをラップする上位的なインターフェースはあってもいいけれど、内部の構造としてはミニマムなクラスや関数の組み合わせによって実現されていてほしい。

例えば、AssetManagerというクラスを考えてみる。もうだめだ。作った時はいいけれど、担当者が変わるたびにインターフェイスが増えて、メンバ変数が増えて、そしていつか神にへと昇華するだろう。最初に設計した人はこのManagerクラスは美しいほどによくできたクラスだったのかもしれない。でも担当者が変わった瞬間に、この「Manager」という単語を都合よくとらえて、好き勝手に機能を継ぎ足ししてしまうに違いない。

「このクラスはこの仕事をします!」という設計者の意図を正しく伝えるためには、クラス名を適切にするしかない。このAssetManagerも適切な名前にすべきだ。といっても、リネームでは解決しない。クラスがミニマムではないから。

このManagerクラスがゲームで頻繁に要求される要件をすべて含んでいるとして、それらはAssetDownloder, AssetDependencyResolver, AssetCachePoolといった個々のクラスに分離できるはずだ。もちろん無理にクラスにしなくてもいい。ただの手続きであるのならば、関数でもいい。大事なのは、全ての部分問題を独立させて、適切な名前を割り当てることだ。そうすれば、後任者も機能を追加するとき、「え!? そこにコード加えるの!?」みたいなトンデモ改修をすることはなくなることが期待できる。

できるだけ副作用のないconst属性のインターフェイスを採用する

処理を記述するときは、結果を返り値で返して、内部の状態には変更を加えないようなメンバ関数を中心に定義すべきだ。
どうしてか? それは、ユニットテストが書きやすいから。これに尽きる。特に、処理に必要な情報を引数で受け取るように記述されていれば、もうテストが書くのが楽すぎて最高になる。テストの書きやすさは品質に直結する。そして、テストフレンドリーに設計されたクラスは、自然と「命名が適切で」「個々の関数の役割が明確な」クラス/関数になる。信じてもらえないかもしれないけど、経験則的にそうなると思っている。

class Hoge {
  void setData(Data data);
  void calculate();
  int getResult() const;
};

よりも、

class Hoge {
  int calculate(Data data) const;
};

のほうがいいよねって話だ。もちろん、こんな分かりやすいヘンテコなコードはめったに見ないが、本質的に同じコードはよく見かける。大事なのは「ユニットテスト」の「ユニット」とは、「一つの関数」であるべきということ。前者のコードの場合、このcalculate関数のテストを記述するためには3つの関数を実行する必要がある。それはユニットではない。

これは愚痴に近い雑談だが、テスト駆動開発はある種の理想だけど、ゲームではそれが難しいことも多い。ビューが根深く絡むし、この分野は仕様変更がかなり多い傾向にあって、仕様変更の影響で膨大な量のユニットテストを修正していると本当に虚無の感情となる。

セッター関数を減らす

セッター関数はあればあるほど設計者も使用者も混乱する。複数のプロパティの整合性、あるいは単なるデータのセットし忘れなど、落とし穴を生みがちだ。オブジェクトとして十分に振る舞うために必要なデータは全てコンストラクターや生成関数で受け取って、それ以降は変更を加えないべき。もしくは、本質的にはSetterでも、setという命名ではなく、そのデータの格納によってどのようにオブジェクトが振舞うのかを明示した関数名を定義する。簡単な例だと、setStateよりもchangeStateの方がインターフェイスとして明瞭ということ。

1つのクラスでフラグ変数を2つ以上定義しない

フラグ複数持つのは典型的なアンチパターンだ。(2^フラグの数)だけ状態が生まれるから。そして、必ずそれは設計者の想定の範疇を超えてバグを生み出す。
例を挙げてみよう。isTouchable、isVisibleのような二つのフラグを持つとする。この時、「タッチ可能で見える状態」、「タッチ可能だが見えない状態」、「タッチ不可能だが見える状態」、「タッチ不可能でかつ見えない状態」の4つの状態をこのオブジェクトは持ちうる。だが、多くの場合、この4つの状態全ての要件を満たす必要性があることはとても少ない。設計者も、暗黙的に「isVisibleがfalseの時は必ずisTouchableもfalseになるようにしよう」という思想で設計しているだろう。だが、「想定しない状態になりうる」それだけでバグを生み出すオブジェクトの脆弱性になる。見えないけどタッチできてしまうとかありそーなバグだ。設計者は完全に想定された状態を明示的に定義しよう。この場合、enumなどで Active(タッチもできるし見える), Deactive(タッチはできないが見える), Invisible(タッチもできないし見えない) のようなステートを定義するのが懸命だ。すると、件の起こりそうなバグは未然に防がれる。

if文はテスト可能な関数に集約し、ビュー側では書かない

これも突き詰めるとテストカバレッジの問題になってくるが、提言しておく。前提として、ビューのコードはテスト困難だ。というか、ビューのテストを実現できているゲームプロジェクトがあるのならばそのナレッジを共有して欲しい。まじでなんでもします。靴も舐めます。
さて、if文があるということは、その関数は毎回同じ動作をしないということだ。それは、つまりバグを生み出す。同じことばっかり言ってる気がするが、プログラマは複雑なロジックを書いた時必ずバグを出す。そしてそれを後任者が改修した時もっと顕著に現れる。それをどう防ぐか? ユニットテストだ。

幸い、ビューが絡まないロジックの場合はテストを書くことは困難ではない。if文があっても、カバレッジが100%になるようにテストを書こう。(もちろんカバレッジが100%だからといってテストケースが十分である保証は全くない)

例えばこんなコードがあるとする。

void initView() {
  /* ... */
  int64_t remainTimeSec = _model->getRemainTimeSec();
  if (remainTimeSec >= SECOND_PER_DAY) {
    _text->setString(format("残り%d日", remainTimeSec / SECOND_PER_DAY));
  } else if (remainTimeSec >= SECOND_PER_DAY) {
    _text->setString(format("残り%d時間", remainTimeSec / SECOND_PER_HOUR));
  } else {
    _text->setString(format("残り%d時間", remainTimeSec / SECOND_PER_MINUTE));
  }
 /* .. */
}

別に何が悪いというわけではない。ただ、ロジックがあるのに、この関数はビューにアクセスしているためテストできない。それは、不健全だ。

std::string formatRemainTimeText(int64_t remainTimeSec) {
  if (remainTimeSec >= SECOND_PER_DAY) {
    return format("残り%d日", remainTimeSec / SECOND_PER_DAY);
  } else if (remainTimeSec >= SECOND_PER_DAY) {
    return format("残り%d時間", remainTimeSec / SECOND_PER_HOUR);
  } else {
    return format("残り%d時間", remainTimeSec / SECOND_PER_MINUTE);
  }
}


void initView() {
  /* .. */

  int64_t remainTimeSec = _model->getRemainTimeSec();
  _test->setString(formatRemainTimeText(remainTimeSec));

 /* .. */
}


これならば、ビューの初期化コードは常に一貫性のある動作をし、そしてロジックはテスト可能なビューが絡まない関数に集約されているため、品質が保証できる。

protectedメンバー変数は邪悪

クラスというものは、インターフェイスを通じてやり取りするもの。だからこそ、オブジェクトはブラックボックスとして振舞うことができる。公開しているインターフェイスは、「想定された操作」だからだ。さて、ここで継承クラスについて考えよう。これはあんまり浸透している感じがしないのだが、継承クラスと基底クラスは 赤の他人 なのだ。もう少し具体的に言うと、基底クラスは、継承クラスが作られ、どのように拡張されるかを想定して設計することは困難ということ。たいていの場合、overrideやprotectedメンバー変数を悪用すると、基底クラスが「想定された操作」の範疇を超えて継承クラスは設計されてしまう。その状態になると、基底クラスの内部実装をリファクタリングしたりするだけで継承クラスの振る舞いが変わってしまい、不具合を引き起こす。他にも、ある基底クラスから派生したAとBという2つの継承クラスがあるときに、基底クラスはAとBの共通部分であることが期待できるが、本質的にはそうなっておらず、共通機能を実装するときに基底クラスを弄るだけで解決しないことがある。

これらを回避するために一番大事なことは、protectedメンバー変数を定義しないこと。継承先で自由にアクセスできる変数を基底クラスに置くことは、基底クラスが一切カプセル化されていないのと同義だ。たとえ継承先のクラスであっても、全てメンバー関数を通してならばある程度制御が効く。(ここで継承クラスのためだけのGetterとSetterを無理やり定義してしまうと、本質的な問題が解決しないので注意すること)

共通の要件を持つ複数のクラスを設計するとき、継承ではなく内含で設計する

これは前の項の「protectedメンバー変数は邪悪」の話の延長戦上にある。そもそも、継承で「共通の要件」を括りだすことは、保守的な面で非常に困難だ。特に、仮想関数を持ったりしている時点で、基底クラスがそれらの共通要件であり続けることはとても困難である。何度も言うが、将来的に第三者の手で改修されることを常に想定すべきだ。基底クラスで解決するという選択も、作りきりのプログラムではうまくいくかもしれないけれど、保守や改修が必ず困難になる。

内含は、共通要件である機能を完全なカプセルオブジェクトとして設計できる。設計者は、publicインターフェイスがどのように呼ばれるかだけ考慮すればよい。これは、仮想関数を持つ基底クラスを設計することよりも非常に簡単だ。

私は、動的ポリモーフィズムを用いて多態的に振舞わせたいという要件においてのみ、継承を使うべきだと思っている。

グローバルイルミネーションのためのフォトンマップ実装①

 グローバルイルミネーション(Global Illumination: GI)とは、現実世界における間接光を表現するためのアプローチである。(あるいは、間接光そのものを指す)

 本来、現実のライティングというものは、無限にも等しい膨大な量のフォトンと、制限のないバウンスという膨大な物理的現象の結果起きている事象であるが、それをコンピュータで再現するというのは非常に難しい。リアルタイムレンダリングであるならばなおさらだ。
 ゆえに、古きよきレンダリングエンジンというものは、頂点の座標と法線、そして光源の距離と方向から高速に計算することができる「直接光(Direct Lighting)」と、間接光をすべて均一に入射する光として大幅に近似する「環境光(Ambient Lighting)」の二つでライティングは実装されていることが多かった。

 しかし、近年は、オフラインライティングとリアルタイムライティングを組み合わせた近似的GI表現が流行っている。その一つがフォトンマッピングだ。これは、広義的にはレイトレーシングの一種で、狭義のレイトレーシングは視線からレイを飛ばし色を追跡するのに対し、フォトンマッピングは光源からフォトンを放ちその軌道をシミュレーション、キャッシュするアルゴリズムである。
 そして、テクセルベースで特定数のキャッシュされたフォトンを収集、そしてその収集したフォトンの分布から密度を推定、放射照度を計算する。
 この手法は、放射するフォトンの量と、テクセルあたりのフォトン量にしか計算量は依存せず、古典的GI実装手法であったラジオシティ法よりも計算量の見積もりが容易いという利点がある。また、フォトンの放射量及び密度を、関心の高いオブジェクト(例えば、コースティクスを形成する透過材質のオブジェクトなど)に対し調整を加えることで、ほかの手法では難しかったシーンの品質の調整が可能になっている。特に、透過材質に対するコースティクスの生成は、フォトンマッピングの優秀なところを語る際には欠かすことができない。これは最も一般的なラスタライズ法とは根本的に異なるアプローチではあるが、リアルタイムレンダリングでは組み合わせて使われることも多いようだ。

 実際に、GPUレンダラ―における実装を行ってみよう。手順は以下のようなものが考えられる。

①光源からフォトンを照射する
②非完全鏡面に当たるまでバウンス、拡散面に当たった場合はメタリック値、RGB反射能から確率を計算し、ロシアンルーレット法でバウンスするかどうかを決定する
③全てのフォトンがmiss(サーフェイスに吸収されないままどっかいった)、吸収、あるいは数値計算的限界(あらかじめ定めたバウンスオーダーのオーバー)等により計算結果が確定したら、吸収が起きたフォトンの座標の空間分布よりKD-Treeを形成する(平衡条件を満たすように構築するとよい)
④次に、LightMapを生成するために、GIの影響下にあるオブジェクトをテクスチャにUV展開し、テクセルごとにワールド座標やワールドタンジェント等の情報を書き込んでおく。
⑤LightMapをテクセル単位で走査し、各テクセルについてKD-Treeを使ってフォトンのN近傍探索を行い、放射照度を推定する。
⑥シェーダーでLightMapをフェッチしてライティングに使用する。

 結構泥臭い。とくに、LightMapの生成に関しては、ラスタライザーはもちろん、リソースの肥大化を防ぐためにもアトラステクスチャを作って大きいライトマップに複数のオブジェクトベイクするべきであるので、ある種の平面ナップザック問題を解かなくてはならない。メッシュのUV展開アルゴリズムも必要である。
 アルゴリズムそのもの複雑さの割に、なかなかに面倒な代物だ。一方で、CPUで計算する場合はKd-treeのフォトン分布をそのまま放射輝度推定に使えるのでLightMapを作る必要はなく、レンダラは割とシンプルに作れる。

 
 ざっくりレンダリングしてみると以下のような結果が得られる。放射フォトン数は10000、放射輝度推定フォトン数は10、フォトン探索半径は3(このコーネルボックス自体のサイズが30くらい)


f:id:riyaaaaasan:20180519230732p:plain

 にじみがすさまじい。まあ、フォトンの数的にこんなものだろうか。

f:id:riyaaaaasan:20180519230727p:plain

 ボールの右側が、近くの青い壁から反射してくる間接光によりうっすら青くなっている様子も確認できる。

 
 ちなみに、これは直接光も間接光も両方フォトンマップから放射輝度を推定しているが、前述の通り直接光は入射ベクトルと法線ベクトルから高速に計算できる。しかし、その場合の光の物理量の辻褄をどう合わせるのか分かっていない。
 今回の実装では、光のIntensity(強度)をポイントライトのパラメータにもち、それを放射束に変換して、フォトンの密度とその放射束の和から放射輝度を計算している。
 一方で、古典的なポイントライティングのライティング手法では、「減衰係数ζ1、ζ2、ζ3」をパラメータとし、そして入射ベクトルと法線ベクトルの内積から放射輝度を推定する。
 
 まったく異なるアプローチのため、直接光と間接光を二つのパスに分けるとしても、同じポイントライトの光の強さから、物理量としての整合性を保つ方法が分からない。うーむ。減衰係数という概念をライティングに持ち出すことをやめたほうがいいのかもしれない。
 あまり実用レベルにならない上に、ちょっと資料の不足により改善の目途が立っていないので、いったんこのGI機能はお蔵入りにしようか。敗北した気分だが。

 どうでもいいが、放射輝度推定に使うフォトン数を1個にすると、ボロノイ図のようなキモイものが浮かび上がる。

 f:id:riyaaaaasan:20180519232408p:plain
 

 




 
 



 

8分木空間でのレイトレースのコリジョンリスト収集処理を見直して10倍速くした

 フォトンマッピングの実装で、フォトンを1万~10万くらいバラまく処理が死ぬほど重かったので、8分木空間におけるレイトレースの処理を見直した。

 8分木はもとより衝突判定を高速にするための空間分割アルゴリズムだが、レイトレースをする際に、衝突判定をすべき空間の収集に最適化の余地があったのでいろいろコードを弄っていたら、10倍速くなった。

 まず簡潔に8分木の分割のロジックを説明する。が、簡略化のために2次元で考える。つまり、4分木として考える。
 例えば、分割レベル3の空間では、ルート空間、インデックス0~3の空間レベル1、インデックス0~15の空間レベル2の3つのレベルが作られる。この時、あるオブジェクトPが空間レベル2のインデックス3に属するとする。Pの衝突判定を行う場合、その空間と、そして親空間である、空間レベル1のインデックス0、ルート空間、この3つの空間に属するオブジェクトと比較をすればよい。これがN分木空間における衝突判定の高速化の原理である。ちなみに、空間レベルのグリッドをまたぐ場合は属する空間が親空間に移る。この辺の説明は以前の記事(Hashed-Octree(ハッシュ化八分木)による空間分割を実装した - My life accelerated)を参照されたい。
 
f:id:riyaaaaasan:20180508214830p:plain

 さて、図を示すと上記のようなインデックスになる。特殊な配番に注意すること。これはシフト演算による処理の高速化のための、モートンオーダーを使った配番である。衝突処理の仕組みについては、前述の通りだが、ここで、レイトレースを行う場合の衝突処理を考える。原理的には、レイのポジションに対し、レイの方向ベクトルを加算し、そのたびにレイのポジションからN分木空間のインデックスを算出、衝突判定処理をすればよいが、それは効率的ではない。図を挙げてみよう。

 
 f:id:riyaaaaasan:20180508220510p:plain



 右上のオブジェクトはレイに衝突しないのは自明である。右下のオブジェクトは、グリッドをまたいでいるため、空間レベル2ではなく、空間レベル1のインデックス3に属している。私の従来の実装では、レイの衝突判定は、空間レベル2ベースでグリッドを移動する。インデックスのビジット順序は、空間レベル2の「0,2,3,9,12,14,15」、空間レベル1の「0,2,3」、そしてルート空間である。
 
 このような衝突リスト収集処理をエンジンから抜粋する。

std::set<uint32_t> GetColliderMortonList(SpaceOctree::OctreeFactoryBase* factory, Ray ray) {
    auto size = factory->GetMinBoxSize(); // 最大の空間レベルの分割サイズ
    auto rayForward = Vector3D(ray.dir.x * size.w, ray.dir.y * size.h, ray.dir.z * size.d); // レイが1ステップに進む距離
    auto rootAABB = factory->GetRootAABB(); // ルート空間
    
    _Vector3D<int16_t> grid = factory->CalculateGridCoordinate(ray.pos); // レイの初期位置から空間のグリッド座標を算出
    _Vector3D<int8_t> gridForward = _Vector3D<int8_t>( // レイ方向ベクトルの符号から1ステップにおけるグリッドの移動データを算出
        ray.dir.x >= 0.0f ? 1 : -1,
        ray.dir.y >= 0.0f ? 1 : -1,
        ray.dir.z >= 0.0f ? 1 : -1
        );

    Vector3D pos = Vector3D(grid.x * size.w, grid.y * size.h, grid.z * size.h) + rootAABB.bpos; // 初期位置
    _Vector3D<int16_t> nextGrid = grid;
    std::set<uint32_t> colliderList; // 衝突リスト(リストの中身は空間ハッシュ)

    while (rootAABB.Contains(pos)) {
   // グリッドから空間ハッシュ算出
        uint32_t number = SpaceOctree::Get3DMortonOrder(grid);

        // 空間ハッシュを、ルート空間まで遡って、衝突リストに格納していく(存在する場合のみ)
        for (int i = 0; i <= factory->GetSplitLevel(); i++) {
            uint32_t idx = static_cast<uint32_t>((number >> i * 3) + PrecomputedConstants::PowNumbers<8, 8>::Get(factory->GetSplitLevel() - i) / 7);
            if (factory->BoxExists(idx)) {
                colliderList.insert(idx);
            }
        }

        // 次のグリッド
        nextGrid = grid + gridForward;
        // 次の座標
        Vector3D nextpos = Vector3D(nextGrid.x * size.w, nextGrid.y * size.h, nextGrid.z * size.h) + rootAABB.bpos;

        // レイベクトルから、X方向、Y方向、Z方向のグリッドに到達する時のレイベクトルの係数を算出 
        float ax = ray.dir.x != 0.0f ? std::abs((nextpos.x - pos.x) / rayForward.x) : FLT_MAX;
        float ay = ray.dir.y != 0.0f ? std::abs((nextpos.y - pos.y) / rayForward.y) : FLT_MAX;
        float az = ray.dir.z != 0.0f ? std::abs((nextpos.z - pos.z) / rayForward.z) : FLT_MAX;

        // 最短で到達するグリッドの探索
        if (ax < ay && ax < az) {
            pos += rayForward * ax;
            grid.x += gridForward.x;
        }
        else if (ay < ax && ay < az) {
            pos += rayForward * ay;
            grid.y += gridForward.y;
        }
        else if (az < ax && az < ay) {
            pos += rayForward * az;
            grid.z += gridForward.z;
        }
        else {
            pos += rayForward;
            grid += gridForward;
        }
    }

    return colliderList;
}

 
 これはうまく動く。ただし、レイの1ステップにおける移動距離が、必ず最大空間レベル(例えば、上の4分木の例だとレベル2)の分割サイズにしかならない。これは、最適ではない。

 (少なくとも私の実装では)オブジェクトの存在しない空間は、ハッシュリストに登録されない。(あるオブジェクトを登録するとき、その親空間、更にその親空間...と遡って登録はする)。
 つまり、上記の例だと、ハッシュリストに登録されている空間は、空間レベル2の「5」、空間レベル1の「1,3」、そしてルート空間だけである。ならば、空間レベル1のインデックス0、左上の空間はまとめて無視できることがハッシュリストの構造から推測ができて、レイの距離は一気に9まで進めていいことがわかる。これを具体的にロジックで考えるならば、あるレイの点が属するすべての空間レベルのうち、「実際にハッシュリストに登録されている空間のレベル+1」の「分割サイズ」の距離だけレイを進めることができる。
 上記の例の場合、レイの始点がレベル2インデックス0空間とすると、レベル2のインデックス0及びレベル1のインデックス0はハッシュリストに存在せず、ルート空間(空間レベル0)のみが存在する。そのため、レイは空間レベル1の分割サイズだけレイを進めることができる。

 上記の疎空間におけるレイのステップ距離の最適化を施した後のコードが以下。

std::set<uint32_t> GetColliderMortonList(SpaceOctree::OctreeFactoryBase* factory, Ray ray) {
    auto min_size = factory->GetMinBoxSize();
    auto rootAABB = factory->GetRootAABB();
    
    _Vector3D<int16_t> grid = factory->CalculateGridCoordinate(ray.pos);
    _Vector3D<int16_t> gridForward = _Vector3D<int16_t>(
        ray.dir.x >= 0.0f ? 1 : -1,
        ray.dir.y >= 0.0f ? 1 : -1,
        ray.dir.z >= 0.0f ? 1 : -1
        );

    Vector3D pos = Vector3D(grid.x * min_size.w, grid.y * min_size.h, grid.z * min_size.h) + rootAABB.bpos;
    Vector3D next_pos = pos;
    std::set<uint32_t> colliderList;

    while (true) {
        uint32_t number = SpaceOctree::Get3DMortonOrder(grid);

        int exists_max_split_level = 0;
        for (int i = 0; i <= factory->GetSplitLevel(); i++) {
            int split_level = factory->GetSplitLevel() - i;
            uint32_t idx = static_cast<uint32_t>((number >> i * 3) + PrecomputedConstants::PowNumbers<8, 8>::Get(split_level) / 7);
            if (factory->BoxExists(idx)) {
                colliderList.insert(idx);
                // 存在していた空間レベルを保存
                exists_max_split_level = std::max(exists_max_split_level, split_level);
            }
        }

        // 探索すべき空間レベルの決定(+1する)
        exists_max_split_level = std::min(exists_max_split_level + 1, factory->GetSplitLevel());
        // 探索空間レベル基準の座標系で次のグリッド座標を決定する 
        auto next_grid = gridForward + factory->CalculateGridCoordinate(pos, exists_max_split_level);
        // 探索空間における分割サイズを計算する
        auto size = rootAABB.size() / static_cast<float>(1 << exists_max_split_level);
        // 次のグリッドサイズから、探索空間における座標を算出する
        next_pos = Vector3D(next_grid.x * size.w, next_grid.y * size.h, next_grid.z * size.h) + rootAABB.bpos;

        // 次のグリッドの座標がシーンから出ていたら終了
        if (!rootAABB.Contains(next_pos)) {
            break;
        }

        float ax = ray.dir.x != 0.0f ? std::abs((next_pos.x - pos.x) / ray.dir.x) : FLT_MAX;
        float ay = ray.dir.y != 0.0f ? std::abs((next_pos.y - pos.y) / ray.dir.y) : FLT_MAX;
        float az = ray.dir.z != 0.0f ? std::abs((next_pos.z - pos.z) / ray.dir.z) : FLT_MAX;

        if (ax < ay && ax < az) {
            pos += ray.dir * ax;
            grid.x = next_grid.x;
        }
        else if (ay < ax && ay < az) {
            pos += ray.dir * ay;
            grid.y = next_grid.y;
        }
        else if (az < ax && az < ay) {
            pos += ray.dir * az;
            grid.z = next_grid.z;
        }
        else {
            pos += Vector3D(ray.dir.x * ax, ray.dir.y * ay, ray.dir.z * az);
            grid = next_grid;
        }
    }

    return colliderList;
}

 

 これは小さなロジックの変更だが、非常に疎な空間(特に、空に向けて放たれるようなレイ)を進むレイのステップを著しく減らすことができる。これにより、私の開発中のレンダリングエンジンのベースのシーンであるコーネルボックスにおける、フォトン散布処理は10倍程度高速化された。

 しかし依然として現在はフォトンが1万個、バウンス制限3回という制約下でも、フォトン散布処理に10sec、ライトマップベイク処理に40secかかっている。特に、シーンの複雑度が増す場合、ライトマップのベイク処理はより増加することが想定される。高い品質のフォトンマッピングのために、まだまだチューニングの余地はあるだろう。(事前計算処理とはいえ、毎度数分待たされるとげんなりする)


 フォトンマッピングの実装は大体終わっているが、ノイズというかにじみがとれない。フォトンの量を十分に増やせばいずれなくなるのか、それとも何かミスがあるのか、判断がつかないでいる。全然わからない。私は雰囲気でGI実装をしている。

その気になれば前提知識無し機材無しの無の状態から2週間で美少女VTuberになれたという話


 最近、美少女になりました。
 いつもは、このブログでは文語体を用いてプログラミングに関する記事を投稿しているのですが、今回はあえて口語体で書くことにします。

 なんやかんやあって、美少女のアバターを使ってYoutubeに動画を投稿してしまうという事件を起こしました。
 私的には、情報発信の媒体がブログから動画共有サイトに移っただけとも言えると思うですが、世間一般的にはそれをVTuberと呼ぶらしいです。

 ことの発端は、とあるフォロワーの絵師ぎゃー氏が、とあるバーチャル幼女プログラマにドハマりしたのがきっかけでした。

www.youtube.com

ぎゃー氏(貝塚 (@gyaaaaaaaaaaaa) | Twitter)
きりみんちゃん(きりみん (@kirimin) | Twitter)

 「きりみんちゃんねる」はVTuberでは異色ともいえる、技術発信系VTuberです。ほかにもいるのかな? こう言ってしまうのもなんですが男性Youtuberです。

 ぎゃー氏は、プログラマでもなんでもなく、さらに言うなら動画の内容は基本的に全然わかっていないとのことですが、淡々としゃべり続けるラジオのような動画が非常に好きらしく、きりみんちゃんの動画を死ぬほど摂取する日々を送っていたらしいです。(作業用BGMとして)

 そんな中、私はいつものようにレンダリングエンジンの開発ブログの更新(まさにここ)をして、適当に日々を送っていたのですが。ある日フォロワーとの交流として、確か「美少女になりたいよね~」みたいな話をぎゃー氏としました。
 「美少女になりたい」なんて言葉は、Twitter民ならだれでも口にするようなありきたりなセリフで、ラップで言うならYO!と同レベルの中身のない発言なわけですが...


朝起きたら美少女にされていた

 フォロワーを女体化するなんて正気の沙汰ではないと思うのですが、女体化されてしまいました。

 その後も女体化の攻防は続く。




 ぶっちゃけるとあまりの可愛さ(性癖にドストライクだった)に普通に自分(?)に惚れてしまった。



そしてその時はやってきた




奴の目的は....私をVTuberにすることだったのだ!!!(テテドン!!)



 どうやら、私が定期的にブログで情報発信しているのを見て、きりみんちゃんと同存在になれると思ったらしい。





 ド直球で「VTuberになってもらえませんか?」と言われたとき、私は非常に悩みました。なにせ、男性のしかも別に声が優れているわけでもない人間がVTuberになって一体どうするというのだ、という気持ちが強かったからですね。

 でも、それ以上に、「この可愛いキャラクターをこの先も観測したい」という気持ちが強かったので、私は「OK」と答えました。


3日後

りやちゃんLive2Dモデルが完成する




 3日です。彼はもちろんLive2Dなんて触ったことがないお絵かきマン。Live2D凄すぎる。



 正直私も面食らってしまいましたが、その完成度に驚愕。恥をかき捨て動画作成に踏み切りました。

 即座に必要な機材・ソフトウェアを購入。

・マイク
Webカメラ(ところでなんでWebカメラというのだろう)
USBオーディオインターフェース
・FaceRig (顔を認識してアバターを動かすソフトウェア)
・FaceRig Live2Dプラグイン
・Live2Dサブスクリプション

 合計で1万円くらい?

 急いで動画の内容を考える。コンセプトは「美少女が意味不明な高度な技術を淡々を喋る動画」

 平日は普通に仕事なので、休日メインで作業。自分の声を録音して編集するという苦痛に慣れるのに非常に時間を要しました。
 私も動画撮影・編集は完全に素人。手探りでいろいろと試行錯誤。

そして

 闇がこの世に生れ落ちた。


ろりやちゃん on Twitter: "動画を投稿しました。本編は~0:10までで、それ以降おまけにC++ SFINAEの解説をしています。/

りやちゃんプログラミング講座①「C++ SFINAE」 https://t.co/75Xjk2AiXJ @YouTubeさんから"



 はっきり言って非常に完成度が低いです。ターゲット層も意味不明で、音量がめちゃくちゃ小さいという痛恨のミスも犯したり。

 でも、確かにこの瞬間、私はVTuberという世界に足を踏み入れたのでした。

 その期間、「ボクと契約してVTuberになってよ」と持ち掛けられた4月16日からおよそ13日。

 2週間で美少女VTuberになってしまいました。


 1個投稿すれば編集も慣れたもので、GWを活用し、怒涛のペースで動画を仕上げていきます。
2作目は、見てるだけで楽しいグラフィクスプログラミング×ライブコーディングを組み合わせた動画をつくって、まあそこそこ面白い動画になったんじゃないかなあと思います。BGMとかもいれたり。

ろりやちゃん on Twitter: "動画を投稿しました。本編00:10までで、おまけにGLSLでピクセルシェーダによるグラフィクスプログラミングを解説しています。プログラミングが分からない人にも楽しめる内容にしました。

りやちゃんプログラミング講座②「GLSL ピクセルシェーダ 前編」 https://t.co/azo05FW51k @YouTubeさんから"


 そして今日、GW最終日、3作目を投稿しました。


ろりやちゃん on Twitter: "GW最後の動画を投稿しました。本編は0:20までで、おまけにGLSLによるレイマーチ... https://t.co/SDqHtVsMAZ"



 字幕や通釈を加えた、一番動画らしい動画になりました。



 そんなこんなで、振り返ってみれば非常に密度の高い一か月で、よく頑張ったなあとしみじみ感じています。

 個人的にはノリと勢いで始めた活動にしては本気で、そして楽しく取り組めたことを凄いうれしく思っていて、まるでサークル活動のようだなと思いました。

 GWが終われば私も普通に仕事が待っているし、ぎゃー氏も自身の創作活動に時間を割かなくてはいけません。
 なので、とりあえず「バーチャル美少女プログラマ りやちゃん」としての活動は、小休止となります。
 1か月くらい期間をおいて、機会に恵まれれば、また誰向けかもわからない謎な動画を、まずはぎゃー氏と二人で楽しむことに重点を置いて作れていけたらなと思います。


 最後に、全ての始まりであるきりみんちゃんの言葉を引用しておわります。

VTuberは特別なものじゃなく、ブログなどと同じようなネット上でのアウトプットの新しいプラットフォームとしての可能性があるんじゃないかなと思っているので、いろいろと試してみたいという想いがあります。


 私もそう思います。別に、VTuberである必要はないと思うのですが、美少女のアバターを借りたエンジニアが技術動画を配信するという形態、そんな不思議なアウトプットをきっかけとして、インターネット上で自身の技術について共有するということがより一般的に広まって、技術のコミュニティが活性化されれば、それは素敵なことだなと思います。