Kuina-chan

くいなちゃん2019年11月18日


プログラミング言語Kuin」の基本構文を学ぶチュートリアル7です。 今回は、3Dレースゲームを作ります。

13Dモデルのコンバート

今回は3Dレースゲームを作ります。 完成画面は図1-1の通りです。
完成画面
図1-1: 完成画面
まずは、ゲームで使う車の3Dモデルをお好みのツールで作り、「.fbx」形式で出力しておきます。 今回はサンプルとして「samples/free_resources/obj_car.fbx」を使います。
「.fbx」形式は3Dモデルの一般的な形式ですが、プログラムで読み込むには効率が悪いため、効率の良いKuinのバイナリ形式「.knobj」にコンバートして使います。
Kuinエディタを起動して、メニューから「ツール」「3Dモデルコンバータ...」を選び、「...」からfbxファイルを選んで「コンバート」ボタンを押すと、しばらくしてからfbxファイルと同じフォルダ内にknobjファイルが出力されます(図1-2)。
knobjへの変換
図1-2: knobjへの変換
注意点として、fbxファイルはすべてのポリゴンが三角形になっている必要があります。 三角分割をしてからfbxを保存してください。
また今回は使いませんが、3Dモデルコンバータのチェックボックスは表1-1の通りです。
表1-1: 変換オプション
オプション 説明
タンジェントとバイノーマルを書き込む UVからタンジェントとバイノーマルを計算し、knobjに書き込みます。 法線マップが使えるようになりますが、データが大きくなり、UVが縮退していると描画が崩れます。 法線マップを使わない場合はオフにしてください。
ジョイントを書き込む ボーンとボーンアニメーションをknobjに書き込みます。 各頂点が影響を受けるボーンの数は最大で2個です。 ボーンの総数は最大で256個です。 ボーンを動かさない場合はオフにしてください。
knobjファイルが出力されたら、「3Dモデルコンバータ」ダイアログを閉じ、「Ctrl+S」を押してメインのknファイルを保存し、「Ctrl+Shift+R」を押して表示される「res」フォルダ以下にknobjファイルを「obj_car.knobj」の名前で移動させます。
また、3Dモデルに貼るためのテクスチャもresフォルダ以下に「tex_car_albedo.png」の名前で用意します。 今回はサンプルとして「samples/free_resources/tex_car_albedo.png」をコピーして配置します。
ついでに、今回使うファイルをsamples/free_resourcesフォルダからコピーしましょう。 「tex_field_albedo.jpg」「tex_sky_albedo.png」「tex_wall_albedo.jpg」「map_sample_racing.txt」もコピーして配置します(図1-3)。
resフォルダ
図1-3: resフォルダ

2車の描画

それでは早速3Dモデルを描画しましょう。 第2回のようにスニペットから「mainとdrawコントロール」のコードを挿入し、図2-1のコードを追加します。
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3. var objCar: draw@Obj
  4. var texCarAlbedo: draw@Tex
  5.  
  6. func main()
  7.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  8.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  9.  
  10.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  11.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  12.   do draw@depth(true, true)
  13.  
  14.   while(wnd@act())
  15.     do @objCar.draw(0, 0.0, @texCarAlbedo, null, null)
  16.     do draw@render(60)
  17.   end while
  18. end func
図2-1: racing_game1.kn
3、4行目で3Dモデルとテクスチャ画像の変数を宣言し、10、11行目でファイルからロードしています。
12行目は、奥行きの前後関係を解決するためのZバッファの設定で、draw@depth関数の括弧内は順に「Zテストが有効か」「Z書き込みが有効か」です。 通常は、3Dを描画するときは有効化して「draw@depth(true,true)」とし、2Dを最前面に描画するときは「draw@depth(false,false)」とします。
15行目で、テクスチャを表面に貼った3Dモデルを画面に描画しています。 drawメソッドの括弧内は順に「メッシュのインデックス」「アニメーションのフレーム」「ディフューズマップテクスチャ(色の反射)」「スペキュラマップテクスチャ(光沢)」「法線マップテクスチャ(凹凸)」です。
カメラやライトは何も設定しなくても、ある程度使いやすい設定になっています。 実行すると、車が表示されます(図2-2)。
車の描画
図2-2: 車の描画

3地面と影の描画

次に、地面と影を描画します。
地面は単なる平面にテクスチャを貼って作ります。 平面のような単純な3Dモデルを作るだけなら「.knobj」ファイルを用意する必要はなく、関数から作れます。
影を描画するには、いったんdraw@Shadowクラスのインスタンスを作って、そこに影を落とす3Dモデルを登録しておき、先ほどのdrawメソッドの代わりにdrawWithShadowメソッドを使用します(図3-1)。
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3. var objCar: draw@Obj
  4. var texCarAlbedo: draw@Tex
  5. var objField: draw@Obj
  6. var texFieldAlbedo: draw@Tex
  7. var shadow: draw@Shadow
  8.  
  9. func main()
  10.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  11.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  12.  
  13.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  14.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  15.   do draw@depth(true, true)
  16.   do @objField :: draw@makePlane()
  17.   do @objField.pos(80.0, 1.0, 80.0, 0.0, 0.0, 0.0, 38.0, 0.0, 38.0)
  18.   do @texFieldAlbedo :: draw@makeTex("res/tex_field_albedo.jpg")
  19.   do @shadow :: draw@makeShadow(1024, 1024)
  20.  
  21.   while(wnd@act())
  22.     do @shadow.beginRecord(0.0, 0.0, 0.0, 5.0)
  23.     do @shadow.add(@objCar, 0, 0.0)
  24.     do @shadow.endRecord()
  25.  
  26.     do @objField.drawWithShadow(0, 0.0, @texFieldAlbedo, null, null, @shadow)
  27.     do @objCar.drawWithShadow(0, 0.0, @texCarAlbedo, null, null, @shadow)
  28.     do draw@render(60)
  29.   end while
  30. end func
図3-1: racing_game2.kn
27行目も忘れず変更してください。
16行目で地面を表す平面モデルを作成し、17行目で位置と大きさを調整しています。 posメソッドの括弧内は順に「拡縮X」「拡縮Y」「拡縮Z」「回転X」「回転Y」「回転Z」「位置X」「位置Y」「位置Z」です。
19行目で影のバッファを作成し、22から24行目で影を落とすモデルを登録しています。 このように、beginRecordとendRecordで囲んだ中で、モデルをaddメソッドで追加します。
22行目のbeginRecordの括弧内は、影を計算する範囲を球で指定します。 順に「球の位置X」「位置Y」「位置Z」「球の半径」です。 球を大きくしすぎると影が粗くなり、小さくしすぎると球の外の影は描画されません。
実行すると、図3-2のようになります。
地面と影の描画
図3-2: 地面と影の描画

4車の移動とカメラ

いよいよ、車を移動させます。
横視点アクションゲームのときのように、車の位置を、game@Rectを継承したクラスで管理します(図4-1)。
  1. class Car(game@Rect)
  2.   +var angle: float
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var objCar: draw@Obj
  8. var texCarAlbedo: draw@Tex
  9. var objField: draw@Obj
  10. var texFieldAlbedo: draw@Tex
  11. var shadow: draw@Shadow
  12. var car: @Car
  13.  
  14. func main()
  15.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  16.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  17.  
  18.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  19.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  20.   do draw@depth(true, true)
  21.   do @objField :: draw@makePlane()
  22.   do @objField.pos(80.0, 1.0, 80.0, 0.0, 0.0, 0.0, 38.0, 0.0, 38.0)
  23.   do @texFieldAlbedo :: draw@makeTex("res/tex_field_albedo.jpg")
  24.   do @shadow :: draw@makeShadow(1024, 1024)
  25.  
  26.   do @car :: #@Car
  27.   do @car.x :: 4.0 * 2.0
  28.   do @car.y :: 4.0 * 12.0
  29.   do @car.width :: 1.5
  30.   do @car.height :: 1.5
  31.   do @car.angle :: 0.0
  32.  
  33.   while(wnd@act())
  34.     if(input@pad(0, %a) >= 1)
  35.       do @car.veloX :- lib@sin(@car.angle) * 0.01
  36.       do @car.veloY :- lib@cos(@car.angle) * 0.01
  37.     end if
  38.     if(input@pad(0, %b) >= 1)
  39.       do @car.veloX :+ lib@sin(@car.angle) * 0.01
  40.       do @car.veloY :+ lib@cos(@car.angle) * 0.01
  41.     end if
  42.     if(input@pad(0, %left) >= 1)
  43.       do @car.angle :+ lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  44.     end if
  45.     if(input@pad(0, %right) >= 1)
  46.       do @car.angle :- lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  47.     end if
  48.     do @car.move(0.25)
  49.     do @car.backFriction(0.003)
  50.     do @car.update()
  51.     do draw@camera(@car.x + lib@sin(@car.angle) * 20.0, 5.0, @car.y + lib@cos(@car.angle) * 20.0, @car.x, 1.0, @car.y, 0.0, 1.0, 0.0)
  52.     do @objCar.pos(1.0, 1.0, 1.0, 0.0, @car.angle, 0.0, @car.x, 0.0, @car.y)
  53.  
  54.     do @shadow.beginRecord(@car.x, 0.0, @car.y, 5.0)
  55.     do @shadow.add(@objCar, 0, 0.0)
  56.     do @shadow.endRecord()
  57.  
  58.     do @objField.drawWithShadow(0, 0.0, @texFieldAlbedo, null, null, @shadow)
  59.     do @objCar.drawWithShadow(0, 0.0, @texCarAlbedo, null, null, @shadow)
  60.     do draw@render(60)
  61.   end while
  62. end func
図4-1: racing_game3.kn
54行目も忘れず変更してください。
2行目の「angle」は、車が向く角度を表します。 26から31行目でCarクラスのインスタンスを作成し、34から50行目で車の移動処理を行っています。 横視点アクションゲームのときと同様です。
51行目でカメラを設定しています。 draw@cameraの括弧内は順に「カメラの位置X」「位置Y」「位置Z」「カメラの注視点X」「注視点Y」「注視点Z」「カメラの上部が向く方向X」「Y」「Z」です。 ここでは常に車の斜め後ろから車を映すようにカメラを設定しています。
52行目で車の位置を設定しています。 また54行目で、影の作成を車の周辺、つまりカメラによく映る範囲に限って行うようにしています。
実行すると、図4-2のようになります。
車の移動
図4-2: 車の移動
ゲームパッドの「Aボタン」、もしくはキーボードの「Zキー」で発進します。 「Bボタン」もしくは「Xキー」で後退です。 走行中に左右ボタンもしくは左右キーを押すと、車は転回します。

5空と壁の描画

最後に、空と壁を描画します。 空も壁も、立方体で表現します。
壁は、横視点アクションゲームのときと同様、テキストファイルでマップを作り、game@Mapクラスで扱います。 衝突判定も横視点アクションゲームと同様です(図5-1)。
  1. class Car(game@Rect)
  2.   +var angle: float
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var objCar: draw@Obj
  8. var texCarAlbedo: draw@Tex
  9. var objField: draw@Obj
  10. var texFieldAlbedo: draw@Tex
  11. var shadow: draw@Shadow
  12. var car: @Car
  13. var objSky: draw@Obj
  14. var texSkyAlbedo: draw@Tex
  15. var objWall: draw@Obj
  16. var texWallAlbedo: draw@Tex
  17. var map: game@Map
  18.  
  19. func main()
  20.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  21.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  22.  
  23.   do @objCar :: draw@makeObj("res/obj_car.knobj")
  24.   do @texCarAlbedo :: draw@makeTex("res/tex_car_albedo.png")
  25.   do draw@depth(true, true)
  26.   do @objField :: draw@makePlane()
  27.   do @objField.pos(80.0, 1.0, 80.0, 0.0, 0.0, 0.0, 38.0, 0.0, 38.0)
  28.   do @texFieldAlbedo :: draw@makeTex("res/tex_field_albedo.jpg")
  29.   do @shadow :: draw@makeShadow(1024, 1024)
  30.   do @objSky :: draw@makeBox()
  31.   do @objSky.pos(320.0, 16.0, -320.0, 0.0, 0.0, 0.0, 40.0, 7.0, 40.0)
  32.   do @texSkyAlbedo :: draw@makeTex("res/tex_sky_albedo.png")
  33.   do @objWall :: draw@makeBox()
  34.   do @texWallAlbedo :: draw@makeTex("res/tex_wall_albedo.jpg")
  35.   do @map :: game@makeMap("res/map_sample_racing.txt", 4.0, 4.0)
  36.  
  37.   do @car :: #@Car
  38.   do @car.x :: 4.0 * 2.0
  39.   do @car.y :: 4.0 * 12.0
  40.   do @car.width :: 1.5
  41.   do @car.height :: 1.5
  42.   do @car.angle :: 0.0
  43.  
  44.   while(wnd@act())
  45.     if(input@pad(0, %a) >= 1)
  46.       do @car.veloX :- lib@sin(@car.angle) * 0.01
  47.       do @car.veloY :- lib@cos(@car.angle) * 0.01
  48.     end if
  49.     if(input@pad(0, %b) >= 1)
  50.       do @car.veloX :+ lib@sin(@car.angle) * 0.01
  51.       do @car.veloY :+ lib@cos(@car.angle) * 0.01
  52.     end if
  53.     if(input@pad(0, %left) >= 1)
  54.       do @car.angle :+ lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  55.     end if
  56.     if(input@pad(0, %right) >= 1)
  57.       do @car.angle :- lib@dist(@car.veloX, @car.veloY, 0.0, 0.0) * 0.15
  58.     end if
  59.     do @car.move(0.25)
  60.     do @car.backFriction(0.003)
  61.     do game@hitMapRect(@map, @car, getChipInfo, null)
  62.     do @car.update()
  63.     do draw@camera(@car.x + lib@sin(@car.angle) * 20.0, 5.0, @car.y + lib@cos(@car.angle) * 20.0, @car.x, 1.0, @car.y, 0.0, 1.0, 0.0)
  64.     do @objCar.pos(1.0, 1.0, 1.0, 0.0, @car.angle, 0.0, @car.x, 0.0, @car.y)
  65.  
  66.     do @shadow.beginRecord(@car.x, 0.0, @car.y, 5.0)
  67.     do @shadow.add(@objCar, 0, 0.0)
  68.     do @shadow.endRecord()
  69.  
  70.     do @objField.drawWithShadow(0, 0.0, @texFieldAlbedo, null, null, @shadow)
  71.     do @objCar.drawWithShadow(0, 0.0, @texCarAlbedo, null, null, @shadow)
  72.     do @objSky.drawFlat(0, 0.0, @texSkyAlbedo)
  73.  
  74.     for z(0, 19)
  75.       for x(0, 19)
  76.         var chip: int :: @map.get(x, z)
  77.         if(chip >= 0)
  78.           do @objWall.pos(4.0, 2.0, 4.0, 0.0, 0.0, 0.0, x $ float * 4.0, 1.0, z $ float * 4.0)
  79.           do @objWall.drawWithShadow(0, 0.0, @texWallAlbedo, null, null, @shadow)
  80.         end if
  81.       end for
  82.     end for
  83.     do draw@render(60)
  84.   end while
  85.  
  86.   func getChipInfo(chip: int, info: game@ChipInfo)
  87.     if(chip = 0)
  88.       do info.shape :: %rect
  89.       do info.solidFriction :: 0.5
  90.       do info.repulsion :: 0.3
  91.     else
  92.       do info.shape :: %none
  93.     end if
  94.   end func
  95. end func
図5-1: racing_game4.kn
30行目、33行目では、「draw@makeBox」関数で立方体モデルを作っています。
空は、巨大な立方体で世界を包むようにして表現しています。 このため、立方体のポリゴンの表面が内側を向くようにする必要があり、31行目でZ軸の拡縮をマイナスにして裏返しています。
実行すると、図5-2のようになります。
空と壁の描画
図5-2: 空と壁の描画
ちゃんと壁に衝突します。 速く1周走れるように遊んだり、ステージやプログラムを改造してみると良いでしょう。
以上で、3Dレースゲームの完成です。 次回は、競技プログラミングに挑戦します!
1574010631jaf