pov-rayを使ったsimutrans建物アドオン開発について
- simu_poppo
- 2024年12月9日
- 読了時間: 12分
こんにちは。アドカレとしてははじめましてとなります。simu_poppoです。 この記事は、Simutrans Advent Calendar 2024 9日目の記事として投稿させていただきます。 最近本ホームページで公開した建物アドオンの中には、pov-rayという3DCG製作ソフトを活用したものがあります。本記事ではpov-rayを使ったsimutrans建物アドオン開発について、pov-rayの使い方とアドオン開発のためのレンダー用に開発したアプリの使い方を中心に取り上げていきます。かなりの長文となっていますが、お付き合いいただければと思います。
§1 pov-rayとは
pov-rayとは、コードベースで簡単に3DCGを作ることができる無料ソフトウェアです。windows, Mac, Linux等で使用できます。
基本的には、以下の3つの要素を記述したシーンファイルというファイルを作成し、pov-rayアプリケーションを用いて画像をレンダーすることで3DCGの描かれた画像ファイルを取得できるというものです。
カメラの設定
光源の設定
配置する物体(オブジェクト)の設定
この3要素について、テキストベースで記述したシーンファイルを、pov-rayソフトを利用してレンダーすることで、3DのCG画像を出力できます。この記事では、3DCG出力機能を使って、simutransのアドオン用画像を生成することを目指しています。
pov-rayの使い方は[英語(公式)]または[日本語マニュアル][日本語tips]などを参考にしてください。
§2 使用するアプリケーションについて
筆者は、pov-rayを用いて速やかにアドオン開発できるよう、次のことを行えるアプリケーションを公開しました。
アドオン画像生成に特化したpov-rayファイルの生成
編集したpov-rayシーンファイルのレンダー
レンダーしたアドオン画像の自動裁断(タイルカッター)
datの画像部分自動生成
必要となるアプリはpov-rayおよびsimutrans_building_addon_maker_with_pov-rayです。後者のアプリは、筆者がTILE CUTTERの機能を再現したアプリに、simutransアドオン開発向けにオプションを付与したうえでpov-rayの自動レンダーをする機能を付与したものです。
以上のアプリケーションをダウンロードし、pov-rayアプリをインストールしたら準備完了です。 ※simutrans_building_addon_maker_with_pov-rayのバージョン1.2未満では、pov-rayファイルと同じフォルダにアプリが存在しない場合レンダーができないバグがありました。v1.2をダウンロードしていただくか、同じフォルダにダウンロードしてください。
§3 アプリの機能について
まず、simutrans_building_addon_maker_with_pov-rayの使い方について簡単に説明します。

アプリを起動すると、このような画面が表示されると思います。何もない状態では、まず[.povファイルを作成]ボタンを押してpov-rayシーンファイルのテンプレートを作成しましょう。 テンプレートにオブジェクトを記述出来たら、[選択]から記述したpov-rayシーンファイルを選んでください。paksizeを指定し、建物の形状を記述し、その他必要なチェックを入れたら、[変換を実行]ボタンを押してください。ファイル関係やpov-rayシーンファイルにエラーが無ければ、設定された内容に従って自動的に全体の画像と、4面をそれぞれ切り出した画像が生成されます。エラーがある場合、途中でエラーが表示されて止まるか、切り出し機能のみが実行されます(レンダーが出来なくても切り出し機能が実行出来た場合はエラーが表示されない場合があります)。
[変換を実行]を押すと、以下のように画像が出力されます。通常は5枚、積雪画像やFront画像を生成すると最大20枚出力されます。


[datファイル生成]を選択すると、datファイルの画像指定部分だけが生成されます。
※画像指定部分のみを生成するため、そのままではpak化できません。必要事項を適宜加筆・修正してください。
§4 pov-rayシーンファイルのテンプレートと書き方について
ここでは、細かいpov-rayシーンファイルの記述方法については触れず、最低限アドオン製作に必要な部分について説明します。pov-rayシーンファイルの細かい文法についてはこちらのリンクから各サイトをご確認ください。
pov-rayシーンファイルのテンプレートを作成すると、次のような内容のファイルが生成されます(赤字は説明のための追記で、実際のファイル内には書かれていません!)。
シーンファイルのテンプレート
#include "snow.inc"/*このファイルは自動生成されます*/
#include "temp.inc"/*このファイルは自動生成されます*/
// ---add include files---
/*ここにincludeファイルを追加する*/
/*例*/#include "colors.inc"/*色についてのincludeファイル*/
// -----------------------
// The default tile scale in this pov-ray file (not for pak file)
#local paksize=64;
/*座標指定する際のpakサイズ*/
/*※画像出力する際のpakサイズではありません!*/
// ---camera setting---
camera {
orthographic
location <100,81.64965809277,100>*number_hight*paksize/128
look_at <0,0.5,0>*paksize/128 /*タイルの角がうまく調整されない場合はここの値を変更してください*/
right<1,0,-1> *paksize*number_width*2
up<1,0,1> *paksize*number_hight/2
}
// ---light setting---
light_source {/*通常の光源*/
<0,173,100>
color rgb 1
parallel
point_at<0,0,0>
}
// If winter==1, set a light to make the snow cover.
#if(winter)
light_source{winter_light}/*簡易的に積雪を再現する追加の光源*/
#end
// ----------------------------------
//
// the name of the object with all objects merged must be "obj"
//
// ---make objects below this line---
#declare obj=
/*ここにオブジェクトの情報を入力します*/
/*例:黒いタイル底面*/
object{
box{<0,0,0>,<32,0.0001,32>}
}
/*追加のオブジェクトを定義する場合は"#declare object_name="で同様に定義できます*/
// ---make objects above this line---
//
//
// ---put the obj---
#declare output_obj=
object{
obj
}
// ---output_area_set---/*出力範囲に関する定義*/
#declare output_area_set_x=
#if(make_front_image)
object{merge{box{<0-0.1,paksize/8,0-0.1>,<paksize*int_y/2+0.1,paksize*int_z,paksize*int_x/2+0.1>}box{<0-0.11,0+paksize/128,paksize*int_x/4>,<paksize*int_y/2+0.11,paksize*int_z,paksize*int_x/2+0.11>}}}
#else
object{box{<-paksize,-paksize*int_y,-paksize>,<paksize*(max(int_x,int_y)+2)/2+0.1,paksize*int_y,paksize*(max(int_z,int_y)+2)/2>}}
#end
#declare output_area_set_z=
#if(make_front_image)
object{merge{box{<-paksize,paksize/8,-paksize>,<paksize*(int_x+2)/2+0.1,paksize*int_z,paksize*(int_y+2)/2+0.1>}box{<paksize*int_x/4,0+paksize/128,0-0.11>,<paksize*int_x/2+0.11,paksize*int_z,paksize*int_y/2+0.11>}}}
#else
object{box{<-paksize,-paksize*int_z,-paksize>,<paksize*(max(int_x,int_y)+2)/2+0.1,paksize*int_z,paksize*(max(int_y,int_x)+2)/2>}}
#end
// Place objects in 4 directions/*4方向に回転させ、各方向のものを順番に配置し、まとめて出力します*/
object{merge{
object{
intersection{object{output_obj}
object{output_area_set_z}}
translate<-1,0,1>*paksize*number_width*3/4
}
object{
intersection{object{output_obj
rotate<0,90,0>
translate<0,0,1>*paksize*int_x/2}
object{output_area_set_x}}
translate<-1,0,1>*paksize*number_width*1/4
}
object{
intersection{object{output_obj
rotate<0,180,0>
translate<0,0,1>*paksize*int_y/2
translate<1,0,0>*paksize*int_x/2}
object{output_area_set_z}}
translate<-1,0,1>*paksize*number_width*(-1)/4
}
object{
intersection{object{output_obj
rotate<0,270,0>
translate<1,0,0>*paksize*int_y/2}
object{output_area_set_x}}
translate<-1,0,1>*paksize*number_width*(-3)/4
}}
scale<1,.8165,1> // To set 1 distance of y direction as 1px, rescaling the hight
}
このテンプレートを活用したシーンファイルの書き方の流れとしては、次のようになります。
アドオンの建物の形状や色等、オブジェクトの情報を"#declare obj="の次に追加する。
#declare obj=
object{
box{<0,0,0>,<32,10,32>}/*形状についての情報 boxは直方体で2つの頂点座標を指定*/
texture{/*色や模様等に関する情報*/
pigment{
color Red/*色をcolors.incから指定 今回は赤を指定*/
}
}
}
ここからは、オブジェクトの指定方法について解説していきます。
オブジェクトに行える基本的な操作は、形状の指定、色や模様等の指定、回転・移動・拡大縮小です。また、オブジェクト同士に対して行える基本的な操作は、結合(merge,union)、差分(difference)、共通部分の取り出し(intersection)です。詳しくは他のサイトをご確認いただければと思いますが、今回は重要なものをピックアップして解説していきます。
まずオブジェクトの形状ですが、これはobject{}の引数(括弧"{}"の中身)の一番初めに書くものです。基本的なものとしては、次のようなものがあります。
直方体:box{頂点座標,頂点座標}
球体 :sphere{中心座標、半径}
円柱 :cylinder{下底面の中心座標、上底面の中心座標、円の半径}
ほかにも様々な形状がありますが、simutransのアドオン向けにはこれだけで十分だと思います。
座標と半径という2種類の要素が基本的には必要になりますが、これは次のように書いていきます。
種類 | 書き方 | 意味 | 例 |
座標 | <x,y,z> | 3次元で、x,y,z方向の各座標がx,y,z | <3,1,2> |
半径や長さ | x | 大きさがx | 5 |
色はRGBのほか、colors.incを読み込むことで単語で指定が可能です。さらに、表面の模様は木や石、金属などさまざまな種類が用意されています。RGBで指定する場合は、次のようにします。
texture{
pigment{
color rgb<0.3,0.2,0.5>
}
}
RGBで指定する場合はR,G,B各要素について0から1の値を指定します。
回転や移動、拡大は基本的には次のようにします。
/*回転*/
rotate<0,90,0>/*y軸周りに90度回転*/
/*拡大*/
scale<1,4,1>/*y軸方向に4倍拡大*/
scale 5 /*全方向に5倍拡大*/
/*平行移動*/
translate<16,12,16>/*全体をx軸方向に16、y軸方向に12、z軸方向に16平行移動*/
また、オブジェクト同士の操作は、関数の引数にオブジェクトを追加していきます。
/*結合*/
merge{
object{
box{<0,0,0>,<10,10,10>}
}
object{
cylinder{<5,0,10>,<5,10,10>,10}
}
}
/*差分*/
difference{
object{
box{<0,0,0>,<10,10,10>}
}
object{
cylinder{<5,0,10>,<5,10,10>,10}
}
}
差分(difference)については2つのオブジェクトを引数に記述します。差分以外(merge,union,intersection)については3つ以上のオブジェクトを記述することもできます。
それでは、試しに書いてみましょう。一部を抜粋して表示しています。
// ---make objects below this line---
#declare obj=
object{
merge{
object{
box{<0,0,0>,<16,5,24>}
}
object{
cylinder{<8, 0, 28>, <8, 16, 28>, 4}
}
}
}
// ---make objects above this line---
出力した図は次のようになっています。

(グレーのタイルは、わかりくなるように足しました。)
座標の指定の仕方は、一番左側の向きにおいて、一番奥が原点<0,0,0>、右下向きの辺がx軸、左下向きの辺がz軸、高さ方向がy軸となっています。タイルはpaksizeの半分を1辺とする正方形とみなしています。目安としては、pov-rayシーンファイル内のpaksizeと出力画像のpaksizeが等しい時、横方向の1ドット分(縦方向0.5ドット分)がシーンファイル内のxおよびz座標の長さ1と対応しています。また、y軸はpaksizeがsimutrans_building_addon_maker_with_pov-rayとpov-rayシーンファイル内で等しい時にシーンファイル内のy軸方向長さ1と出力した画像の高さ1ドットが対応するようになっています。

たとえば、paksize=64のとき、辺の長さは32となります。今回は、<0,0,0>から<16,5,24>まで直方体を置き、その隣に<8,0,28>を下底面の中心、<8,16,28>を上底面の中心、半径を4とする円柱を置いたので、上のような図形が出来ました。直方体は高さ5ドットで辺の長さが横方向16ドットと24ドット、円柱は高さ16ドットで半径の横方向が4ドットです。
色もつけてみましょう。
// ---add include files---
#include "colors.inc"
// -----------------------
// (中略)
// ---make objects below this line---
#declare obj=
object{
merge{
object{
box{<0,0,0>,<16,5,24>}
texture{pigment{color Red}}
}
object{
cylinder{<8, 0, 28>, <8, 16, 28>, 4 }
texture{pigment{color Blue}}
}
}
}
// ---make objects above this line---

建物っぽくなりました。
影なども自動で計算されています。これらを組み合わせて、アドオンの建物を作っていくことができます。
ここまではsimutrans_building_addon_maker_with_pov-rayのpaksizeに64を入力しpak64向けの画像を作ってきましたが、同じソースを使って、pak128向けの画像を作ることもできます。simutrans_building_addon_maker_with_pov-rayのpaksizeに128を入力して変換するだけです。

また、同時に自動でタイルカッター機能が働き、4方向各方面のアドオン用画像が生成されています。基本的に、_0が南向き、_1が東向き、_2が北向き、_3が西向きのアドオン用画像となっています。
§5 トラブルシューティング
もしレンダーがうまくいかない場合は、次のことを試してみてください。
シーンファイル内の括弧の数を確認する。
シーンファイル内の,と.の数を確認する。
シーンファイルおよびアプリと同じフォルダにsnow.incが存在するかを確認する。(v1.2以降では、[.povファイルを生成]を押すとpov-rayファイルと同じフォルダに生成されます。v1.2未満では、カレントディレクトリに生成されています。)
インクルードファイルが適切に読み込まれているかを確認する。
simutrans_building_addon_maker_with_pov-ray.exeおよびシーンファイルがすべて同じフォルダにあるかを確認する(v1.2未満の場合のみ)。
フォルダやファイルが日本語名の場合にエラーが起こる可能性があります。
pov-rayシーンファイル内の変数等に日本語等の全角文字を使用することはできません(コメント内であれば問題ないようです)。
§6 作品例


ソースコード
#include "snow.inc"
#include "temp.inc"
// ---add include files---
#include "colors.inc"
#include "textures.inc"
#include "metals.inc"
// -----------------------
// The default tile scale in this pov-ray file (not for pak file)
#local paksize=64;
// ---camera setting---
camera {
orthographic
location <100,81.64965809277,100>*number_hight*paksize/128
look_at <0,0.425,0>*paksize/128
right<1,0,-1> *paksize*number_width*2
up<1,0,1> *paksize*number_hight/2
}
// ---light setting---
light_source {
<0,173,100>
color rgb 1
parallel
point_at<0,0,0>
}
// If winter==1, set a light to make the snow cover.
#if(winter)
light_source{winter_light}
#end
// ----------------------------------
//
// the name of the object with all objects merged must be "obj"
//
// ---make objects below this line---
#declare window_texture=
texture{pigment{color rgb<77,77,77>/256.}
finish{diffuse 0
ambient 0.25} }
#declare window=
object{
box{<0.5,0.5,0.5>,<-.5,-.5,-.5>}
texture{window_texture}
}
#declare tri=
intersection{
object{
box{<-1,-1.414,-1.414>,<1,1.414,1.414>}
rotate<45,0,0>
}
object{
box{<-1.1,0,-2>,<1.1,2,2>} }
}
#declare roof_color=<1.3,2.6,1.>/3.6;
#declare roof =
merge{
object{
box{<-1,-0.025,-1.414>,<1,0.025,1.414>}
rotate<45,0,0>
translate<0,-1,1>
texture {pigment{/*brick*/
rgb roof_color
}
finish {//diffuse 0.3
ambient 0.1}
scale.001
normal {
quilted
scale .3
} }
}
object{
box{<-1,-0.025,-1.414>,<1,0.025,1.414>}
rotate<-45,0,0>
translate<0,-1,-1>
texture {pigment{/*brick*/
rgb roof_color }
finish {//diffuse 0.3
ambient 0.1}
scale.001
normal {
quilted
scale .3
}}
}
}
#declare floor_texture=
texture{pigment{
bozo
color_map{
[0.0 color Gray40]
[0.6 color Gray50]
}
scale 0.3}}
#declare crossing_gate=
merge{
object{
box {
<-1, 1.5, -.75>, <-.5, 9.5, -.25>
}
texture{
pigment {
// onion
// color_map{
// [0 color Yellow]
// [1 color rgb <0,0,0>]
// }
checker color Yellow,color Black
}
finish {
ambient .1
}
scale .5
}
}
object{
box{
<-.5,.5,-1>,<1,2,1>
}
texture {
pigment {
checker color Yellow, color Black scale 1
}
finish {
ambient .2
}
scale .4
}
}
object{
cylinder {
<.24, -1, 0>, <.24, .5, 0>, 0.5 // center of one end, center of other end, radius
}
texture{
pigment {
onion
color_map{
[0 color Yellow]
[1 color rgb <0,0,0>]
}
}
finish {
ambient .3
}
scale 0.5
}
}
}
#declare obj=
merge{
object{box{<0,0,0>,<32,0,32>}texture{pigment{
bozo
color_map{
[0.0 color Gray10]
[0.6 color Gray20]
}
scale 0.3}}}
object{box{<6,0,2>,<26,1,12>}texture{pigment{
checker
color rgb<.8,.4,.5>,color rgb<.82,.46,.52>
scale 1}}}
object{box{<12,0,0>,<20,1,2>}texture{floor_texture}}
object{crossing_gate rotate<0,90,0> translate<21,1,.75>}
object{
difference{
object{box{<6,0,12>,<26,1,22>}}
object{box{<18,0,12>,<26.1,1.1,16.5>}}
} texture{floor_texture}}
object{box{<3.99,0,19>,<10,1.01,24.01>}texture{floor_texture}}
object{box{<5,1,22>,<5.5,12,21.5>}texture{Silver_Metal}}
object{box{<4,12,21.5><6.5,12.5,22>}texture{Silver_Metal}}
object{window scale<1,0.6,0.6> translate<4.5,11.75,21.75>}
object{window scale<1,0.6,0.6> translate<6,11.75,21.75>}
object{box{<4,0,12>,<6,1.01,12.5>}texture{floor_texture}}
object{box{<4,0,12>,<26,.5,24>}texture{floor_texture}}
object{box{<18,0,12>,<30,2,13>}texture{floor_texture}}
object{box{<18,0,16>,<26,2,16.5>}texture{floor_texture}}
object{box{<26,0,16>,<26.5,2,24>}texture{floor_texture}}
object{box{<30,0,12>,<30.5,2,24>}texture{floor_texture}}
object{intersection{object{tri rotate<0,90,0> scale<8,.5,2>/2 translate<18,.5,14>}box{<14,0,12.2>,<26.5,3,16.5>}}texture{floor_texture}}
object{box{<26,0,12.3>,<30.2,.5,16.5>}texture{floor_texture}}
object{intersection{object{tri scale<4,.5,8>/2 translate<28,-.1,16.5>}box{<26,0,16>,<30.2,3,24.5>}}texture{floor_texture}}
object{roof
scale<5,1.7,5>
rotate<0,90,0>
translate<16,11.72,6.5>}
object{tri
scale<4,1.36,4>
rotate<0,90,0>
translate<16,9,6.5>
texture{window_texture}}
object{box{<8,8.01,2.5>,<24,9,10.5>}texture{window_texture}}
object{merge{
object{box{<7.99,8,2.49>,<8.5,9,10.51>}}
object{box{<23.5,8,2.49>,<24.01,9,10.51>}}
object{box{<13.5,8,2.49>,<14,10.5,10.51>}}
object{box{<18,8,2.49>,<18.5,10.5,10.51>}}
object{box{<8,7.5,2.49>,<24,8,10.51>}}
}texture{pigment{color BakersChoc}}}
object{merge{
object{box{<14,7.5,10.5>,<18,8,11.5>}}
object{box{<14,7.5,1.5>,<18,8,2.5>}}
}texture{T_Chrome_3B}}
object{
merge{object{box{<8,1,2.5>,<14,7.5,10.5>}}
difference{
object{box{<18,1,2.5>,<24,7.5,10.5>}}
object{box{<23.5,1,2>,<20.5,6,3>}}}
}texture{
pigment{
quilted
color_map{
[0.0 color rgb<.5,.5,.5>]
[0.3 color rgb<.7,.7,.7>]
[1.0 color rgb<.9,.9,.9>]
}
control0 1
control1 0
scale 3
}}}
object{box{<23.5,1,2.9>,<20.5,6,3>}texture{pigment{White}}}
object{box{<18.5,6,10.5>,<22,7,10.51>}texture{pigment{color White}}}
object{box{<9,4,10.51>,<13,6,10.5>}texture{window_texture}}
object{box{<9,4,2.51>,<13,6,2.49>}texture{window_texture}}
object{merge{
object{tri scale<2,.3,.5> rotate<0,90,0> translate<8,3.5,4.6>}
object{box{<8,1,2.6>,<7,3.5,6.6>}}
texture{pigment{color rgb <.95,.95,.95>}}
}}
object{box{<17.9,2,10>,<18,6,7>}texture{pigment{MediumSeaGreen}}}
object{box{<17.9,3.5,3>,<18,5.5,5>}texture{pigment{White}}}
object{box{<14,1,4>,<14.1,6,9>}texture{pigment{White}}}
object{box{<14,4,4.5>,<14.11,5.5,8.5>}texture{window_texture}}
}
// ---make objects above this line---
//
//
//
// ---put the obj---
#declare output_obj=
object{
obj
}
// Place objects in 4 directions
object{merge{
object{output_obj
translate<-1,0,1>*paksize*number_width*3/4
}
object{output_obj
rotate<0,90,0>
translate<-1,0,1>*paksize*number_width*1/4
translate<0,0,1>*paksize*int_x/2
}
object{output_obj
rotate<0,180,0>
translate<-1,0,1>*paksize*number_width*(-1)/4
translate<0,0,1>*paksize*int_y/2
translate<1,0,0>*paksize*int_x/2
}
object{output_obj
rotate<0,270,0>
translate<-1,0,1>*paksize*number_width*(-3)/4
translate<1,0,0>*paksize*int_y/2
}}
scale<1,.8165,1> // To set 1 distance of y direction as 1px, rescaling the hight
}
このアドオンでは、各パーツを”#declare"を用いて定義し、それをmergeなどを組み合わせて配置しています。また、window_textureを定義して使用することで夜間に発光する窓や照明の色を再現しています。
複数タイルアドオンの出力例およびタイルカッターの実用例については、河口湖駅舎をご覧ください。 ほかにも、駅ホーム入り口、箱積み駅舎用入り口やバス営業所アクセサリなどもpov-rayを活用して作成しました。
§7 あとがき
ここまで長々とお付き合いいただきありがとうございました。pov-rayを使うことで絵が書けない筆者のような人でも3Dな建物アドオンを量産できるようになります。「絵は苦手だけどプログラミングはできる!アドオンも作りたい!」という方はぜひ参考にしてみてください。
筆者はこれまでも数作品pov-rayを用いたアドオンを作成してきました。公開されているものの中には、ソースコードを同封しているものもありますので、参考にしていただければと思います。
今後は、駅舎などの一般の建物だけでなく駅stopや道way、さらには乗り物vehicleなどもpov-rayで生成できないかと考えています。さらに、simutransアドオン用の色設定や複雑な図形を簡単に呼び出せる機能などをまとめたpov-rayインクルードファイルも公開しています。特に、simutransアドオン用の発光する窓の色を再現できるようにしています。
順次交流会議のDiscordで成果を報告していくつもりですので、興味のある方は参考にしてください。また、「こんな技術や方法がある!」という方も、ぜひ交流会議Discordやtwitterなどで教えていただければ幸いです。
最後までお読みいただきまして、誠にありがとうございました。
Comments