アクセルとブレーキは別のキーにしてほしいというリクエストがあったので別バージョンも作成しました。Zキー:アクセル、Xキー:ブレーキです。⇒ 動作確認はこちらから
前回では車の描画をしました。今回はコースの描画をおこないます。前回同様にTypeScript/JavaScriptで使えるコードをC#で生成するのですが、OpenTKと同じように画像ファイルから読み取ったピクセルのデータをそのまま使おうとすると処理が重たくなってしまいます。ちょっと工夫が必要です。
画像ファイルから点情報を取得するクラス
まずコースが書かれたPNGファイルからGetPixelメソッドで取得したColor.ToArgb()がColor.Black.ToArgb()と同じになる点のリストを取得し、ここからコースの境界線とコースの内部にある点のリストを取得します。これは画像の輪郭を取得するにはとほとんどかわりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
public class Course { List<Point> InsidePoints = new List<Point>(); public List<Point> BorderPoints1 = new List<Point>(); public List<Point> BorderPoints2 = new List<Point>(); public Course(string path) { using (Bitmap bitmap = new Bitmap(path)) { InsidePoints = GetPointsInside(bitmap, Color.Black); List<List<Point>> ptsList = GetContourPointsListFromBitmap(bitmap, Color.Black); BorderPoints1 = ptsList[0]; BorderPoints2 = ptsList[1]; } } List<Point> GetPointsInside(Bitmap bitmap, Color color) { List<Point> points = new List<Point>(); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color pixelColor = bitmap.GetPixel(x, y); if (pixelColor.ToArgb() == color.ToArgb()) { points.Add(new Point(x, y)); } } } return points; } Bitmap GetBitmapContour(Bitmap bitmap, Color color) { Bitmap newBitmap = new Bitmap(bitmap.Width, bitmap.Height); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color pixelColor = bitmap.GetPixel(x, y); if (pixelColor.ToArgb() == color.ToArgb()) { bool b1 = bitmap.GetPixel(x + 1, y).ToArgb() != color.ToArgb(); bool b2 = bitmap.GetPixel(x - 1, y).ToArgb() != color.ToArgb(); bool b3 = bitmap.GetPixel(x, y + 1).ToArgb() != color.ToArgb(); bool b4 = bitmap.GetPixel(x, y - 1).ToArgb() != color.ToArgb(); if (b1 || b2 || b3 || b4) newBitmap.SetPixel(x, y, color); } } } return newBitmap; } List<List<Point>> GetContourPointsListFromBitmap(Bitmap bitmap, Color color) { Bitmap bitmap2 = GetBitmapContour(bitmap, color); List<List<Point>> pointsList = GetPointListContour(bitmap2, color); bitmap2.Dispose(); return pointsList; } Point GetPointFromColor(Bitmap bitmap, Color color) { for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color color1 = bitmap.GetPixel(x, y); if (color1.ToArgb() == color.ToArgb()) { return new Point(x, y); } } } return Point.Empty; } List<List<Point>> GetPointListContour(Bitmap bitmap, Color color) { // 渡されたBitmapには輪郭部分しか残されていないのが前提 List<List<Point>> pointsList = new List<List<Point>>(); while (true) { List<Point> points = new List<Point>(); Point point = GetPointFromColor(bitmap, color); if (point == Point.Empty) break; Point tempPoint = point; points.Add(tempPoint); while (true) { Point nextPoint = Point.Empty; bitmap.SetPixel(tempPoint.X, tempPoint.Y, Color.Red); if (bitmap.GetPixel(tempPoint.X + 1, tempPoint.Y).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X + 1, tempPoint.Y); else if (bitmap.GetPixel(tempPoint.X - 1, tempPoint.Y).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X - 1, tempPoint.Y); else if (bitmap.GetPixel(tempPoint.X, tempPoint.Y - 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X, tempPoint.Y - 1); else if (bitmap.GetPixel(tempPoint.X, tempPoint.Y + 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X, tempPoint.Y + 1); else if (bitmap.GetPixel(tempPoint.X + 1, tempPoint.Y + 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X + 1, tempPoint.Y + 1); else if (bitmap.GetPixel(tempPoint.X - 1, tempPoint.Y - 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X - 1, tempPoint.Y - 1); else if (bitmap.GetPixel(tempPoint.X + 1, tempPoint.Y - 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X + 1, tempPoint.Y - 1); else if (bitmap.GetPixel(tempPoint.X - 1, tempPoint.Y + 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X - 1, tempPoint.Y + 1); if (nextPoint == Point.Empty) break; tempPoint = nextPoint; points.Add(tempPoint); } pointsList.Add(points); } return pointsList; } List<Rectangle> _insideRectangles = new List<Rectangle>(); public List<Rectangle> InsideRectangles { get { if (_insideRectangles.Count == 0) { foreach (Point pt in InsidePoints) { if (!BorderPoints1.Any(x => x == pt) && !BorderPoints2.Any(x => x == pt)) { Point point = new Point(pt.X, pt.Y); Size size = new Size(1, 1); _insideRectangles.Add(new Rectangle(point, size)); } } } return _insideRectangles; } } } |
境界線の点のリストを取得する
次にこのクラスをつかって境界線の点のリストを取得する処理を示します。やっていることはTypeScriptで書く場合、そのままコピペすればコースの両側の境界線を取得するGetCourseBorder1関数とGetCourseBorder2関数として使える文字列の取得です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Course course = new Course(Application.StartupPath + "\\course.png"); var pointFs1 = course.BorderPoints1; var pointFs2 = course.BorderPoints2; StringBuilder sb = new StringBuilder(); sb.Append("function GetCourseBorder1() {\n"); sb.Append(" let border = [];\n"); foreach (var a in pointFs1) { string str = String.Format(" border.push(new Point({0}, {1}));\n", a.X, a.Y); sb.Append(str); } sb.Append(" return border;\n"); sb.Append("}\n"); sb.Append("function GetCourseBorder2() {\n"); sb.Append(" let border = [];\n"); foreach (var a in pointFs2) { string str = String.Format(" border.push(new Point({0}, {1}));\n", a.X, a.Y); sb.Append(str); } sb.Append(" return border;\n"); sb.Append("}\n"); // これを取得する return sb.ToString(); |
コースの内部を取得する
同様にコースの内部かどうかを調べる関数も同様に生成できると考えたのですが、これをやってみると配列が大きくなりすぎて処理も時間がかかるため、読み込みに時間がかかるうえにゲーム中に何度も止まってしまったため、別の方法を考えることにします。
まずコースの内側を黒く塗りつぶす処理ですが、無数の正方形をシーンのなかに敷き詰めるのではなく、帯状になった平面をシーンのなかに追加します。
以下のコードはコースのなかを黒く塗りつぶすために用いる平面の中心座標と奥行きをセットにしたBandクラスの配列を取得するためのGetCourseInside関数を生成するコードです。
TypeScript側で定義するBandクラスは以下のようなものです。
1 2 3 4 5 6 7 8 9 10 11 |
class Band { CenterX: number = 0; CenterY: number = 0; Depth: number = 0; constructor(x, y, depth) { this.CenterX = x; this.CenterY = y; this.Depth = depth; } } |
C#側ではループの中で取得した値とそれに1を加えた数をそれぞれ保存しておき、次のループで取得した値とこれらと比較してみます。startが0ではないときにnextよりも大きな数が取得されたときは連続していない点を取得したことになるので、それまでに連続した点があるのであれば終了させ、新たな開始点をつくります。開始点のY座標と終了点のY座標の平均値が平面の中心のX座標になり、終了点のY座標から開始点のY座標を引いて1を加えたものが平面の奥行きになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
Course course = new Course(Application.StartupPath + "\\course.png"); List<Rectangle> rects = course.InsideRectangles; StringBuilder sb = new StringBuilder(); sb.Append("function GetCourseInside() {\n"); sb.Append(" let bands = [];"); var groups = rects.GroupBy(x => x.X); foreach (var group in groups) { int key = group.Key; string str = ""; List<string> vs = new List<string>(); int start = 0; int next = 0; int cur = 0; foreach (var rect in group) { int y = rect.Y; vs.Add("y == " + y); if (next != y && next < y) { if (start > 0) { str = String.Format(" bands.push(new Band({0}, {1}, {2}));\n", key, (start + cur) / 2, (cur - start + 1)); sb.Append(str); } start = y; } next = y + 1; cur = y; } str = String.Format(" bands.push(new Band({0}, {1}, {2}));\n", key, (start + cur) / 2, (cur - start + 1)); sb.Append(str); } sb.Append("\treturn bands;\n"); sb.Append("}\n"); // これを取得する return sb.ToString(); |
座標はコースの内側かどうか?
次に車がある座標はコースの内側かどうかを取得する関数を生成するメソッドを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
Course course = new Course(Application.StartupPath + "\\course.png"); var rectangles = course.InsideRectangles; StringBuilder sb = new StringBuilder(); sb.Append("function IsCourceInside(x, y) {\n"); var groups = rectangles.GroupBy(x => x.X); foreach (var group in groups) { int key = group.Key; string str = ""; str = String.Format(" if(x == {0}) {{\n", key); sb.Append(str); List<string> vs = new List<string>(); foreach (var rect in group) vs.Add("y == " + rect.Y); str ="\t\tif(" + String.Join(" || ", vs.ToArray()) + ")\n\t\t\treturn true;\n"; sb.Append(str); sb.Append("\t\telse\n"); sb.Append("\t\t\treturn false;\n"); sb.Append(str = "\t}\n"); } sb.Append("\treturn false;\n"); sb.Append("}\n"); // これを取得する return sb.ToString(); |
TypeScriptで使う
これらを使ってTypeScriptでコースをシーンに追加します。
AddSceneCourseはコースを生成してシーンに追加する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 |
let expansionRate = 1.6; function AddSceneCourse() { let geometry = new THREE.BoxGeometry(10000, 0.01, 10000); let material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); scene.add(new THREE.Mesh(geometry, material)); scene.add(CreateLeft()); scene.add(CreateRight()); scene.add(CreateInside()); } |
CreateLeftは進行方向にむかって左側にあるコースの境界線を構成するオブジェクトを生成する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function CreateLeft() { // border1はグローバル変数 let border:Point[] = GetCourseBorder1(); border1 = border.map(point => { return new Point(point.X * expansionRate, point.Y * expansionRate); }); //@ts-ignore let geometry = new THREE.Geometry(); let material = new THREE.MeshLambertMaterial({ color: 0xff0000 }); border1.forEach(point => { let geometryTemp = new THREE.CylinderGeometry(0.5, expansionRate, 1, 30); let meshTemp = new THREE.Mesh(geometryTemp); meshTemp.position.set( point.X, 0.5, point.Y ); geometry.mergeMesh(meshTemp); }); return new THREE.Mesh(geometry, material); } |
CreateRightは進行方向にむかって右側にあるコースの境界線を構成するオブジェクトを生成する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function CreateRight() { // border2はグローバル変数 let border: Point[] = GetCourseBorder2(); border2 = border.map(point => { return new Point(point.X * expansionRate, point.Y * expansionRate); }); //@ts-ignore let geometry = new THREE.Geometry(); let material = new THREE.MeshLambertMaterial({ color: 0x0000ff }); border2.forEach(point => { let geometryTemp = new THREE.CylinderGeometry(0.5, expansionRate, 1, 30); let meshTemp = new THREE.Mesh(geometryTemp); meshTemp.position.set(point.X, 0.5, point.Y); geometry.mergeMesh(meshTemp); }); return new THREE.Mesh(geometry, material); } |
CreateInsideはコースの内部にある平面の集まりを生成する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function CreateInside() { let insideCourcePoints: Band[] = GetCourseInside(); //@ts-ignore let geometry = new THREE.Geometry(); insideCourcePoints.forEach(point => { let geometryTemp = new THREE.BoxGeometry(expansionRate, 0.1, point.Depth * expansionRate); let meshTemp = new THREE.Mesh(geometryTemp); // メッシュを追加 // XYZ座標を設定 meshTemp.position.set(point.CenterX * expansionRate, 0.05, point.CenterY * expansionRate); geometry.mergeMesh(meshTemp); }); let material = new THREE.MeshBasicMaterial({ color: 0x333333 }); return new THREE.Mesh(geometry, material); } |
IsCarInsideは車がコースアウトしていないか調べる関数です。
1 2 3 4 5 6 7 8 |
function IsCarInside(car) { let x1 = Math.round(car.position.x / expansionRate); let y1 = Math.round(car.position.z / expansionRate); if (IsCourceInside(x1, y1)) return true; return false; } |