読者です 読者をやめる 読者になる 読者になる

Unityでサーキットを自動生成する

簡単なサーキットを自動生成するスクリプトを作ったのでまとめてみました。完成系はこんな感じです。
f:id:usiatan2:20170415225448p:plain
f:id:usiatan2:20170415225559p:plain

処理の流れ

ランダムに点を配置し、スプライン曲線で補完し点を増やします。そして増やした点をこのサイトのような方法でメッシュを生成させるとサーキットっぽくなるのではないかと考えました。

ランダムに点を配置

処理の内容から考えてこの点がサーキットのコーナー部分になるはずです。つまりコースの形がこの点によって決まります。今回は適当に円周上に配置しましたが、手動で配置してもいいかもしれません。

List<Vector3> points1 = new List<Vector3>();
for (int i = 0; i < _pointCount; i++) {
    float radian = Mathf.PI * 2f * ((float)i / (float)_pointCount) + Random.Range(-0.1f, 0.1f);
    float radius = Random.Range(_minRadius, _maxRadius);
    points1.Add(new Vector3(Mathf.Cos(radian) * radius, 0f, Mathf.Sin(radian) * radius));
}

点を補完

先ほど配置した点を補完して点の数を増やします。補完にはcatmull-rom曲線を使いました。この曲線はコントロールポイントを必要としないのでとても便利です。


a=p_{1}(t^{2}(2-t)-t)\\
b=p_{2}(t^{2}(3t-5)+2)\\
c=p_{3}(t^{2}(4-3t)+t)\\
b=p_{4}(t^{2}(t-1))\\
\displaystyle p_{t}=\frac{a+b+c+d}{2}

Vector3 Spline(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float t) {
    Vector3 a = p1 * (t * t * (2 - t) - t);
    Vector3 b = p2 * (t * t * (3 * t - 5) + 2);
    Vector3 c = p3 * (t * t * (4 - 3 * t) + t);
    Vector3 d = p4 * (t * t * (t - 1));
    return (a + b + c + d) / 2f;
}
List<Vector3> points2 = new List<Vector3>();
for (int i = 0; i < points1.Count; i++) {
    Vector3 p1 = points1[(i + points1.Count - 1) % points1.Count];
    Vector3 p2 = points1[i];
    Vector3 p3 = points1[(i + 1) % points1.Count];
    Vector3 p4 = points1[(i + 2) % points1.Count];
    for (float t = 0f; t < 1f; t += 0.05f) {
        points2.Add(Spline(p1, p2, p3, p4, t));
    }
}

セクションを作る

このサイトを参考にしました。まず方向を計算します。ある点p_{i}の方向は、p_{i-1}p_{i+1}の差を正規化したものとします。一つ前と一つ先の点を使うことでちょうどよい中間の方向を得ることができます。そして方向に対して直角の2点を計算します。この2点が道の両脇になります。

public class Section {
    public Vector3 _direction;
    public Vector3 _left;
    public Vector3 _right;
}
List<Section> sections = new List<Section>();
for (int i = 0; i < points2.Count; i++) {
    Vector3 prev = points2[(i + points2.Count - 1) % points2.Count];
    Vector3 curr = points2[i];
    Vector3 next = points2[(i + 1) % points2.Count];
    Section section = new Section();
    section._direction = (next - prev).normalized;
    Vector3 orthogonal = Quaternion.AngleAxis(90f, -Vector3.up) * section._direction;
    section._left = curr - orthogonal * (_roadWidth / 2f);
    section._right = curr + orthogonal * (_roadWidth / 2f);
    sections.Add(section);
}

セクションからにメッシュを作る

同じくこのサイトを参考にしました。それぞれのセクションが保持する方向に対して直角な2点を頂点としてメッシュを構築します。作ったメッシュはMeshColliderにも渡して当たり判定をとれるようにしておきます。

List<Vector3> vertices = new List<Vector3>();
for (int i = 0; i < sections.Count; i++) {
    vertices.Add(sections[i]._left);
    vertices.Add(sections[i]._right);
}
List<int> triangles = new List<int>();
int n = vertices.Count;
for (int i = 0; i < vertices.Count; i += 2) {
    triangles.Add((i + 1) % vertices.Count);
    triangles.Add((i + 3) % vertices.Count);
    triangles.Add((i + 0) % vertices.Count);
    triangles.Add((i + 0) % vertices.Count);
    triangles.Add((i + 3) % vertices.Count);
    triangles.Add((i + 2) % vertices.Count);
}
Mesh mesh = GetComponent<MeshFilter>().mesh;
mesh.Clear();
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
mesh.RecalculateNormals();
MeshCollider meshCollider = GetComponent<MeshCollider>();
meshCollider.sharedMesh = mesh;

全体のソースコード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer), typeof(MeshCollider))]
public class Circuit : MonoBehaviour {
    public int _pointCount = 20;
    public float _minRadius = 100f;
    public float _maxRadius = 200f;
    public float _roadWidth = 20f;

    [System.Serializable]
    public class Section {
        public Vector3 _direction;
        public Vector3 _left;
        public Vector3 _right;
    }

    void Start () {
        Generate();
    }

    void Update() {
        if (Input.GetMouseButtonDown(0)) {
            Generate();
        }
    }

    void Generate() {
        List<Vector3> points1 = new List<Vector3>();
        for (int i = 0; i < _pointCount; i++) {
            float radian = Mathf.PI * 2f * ((float)i / (float)_pointCount) + Random.Range(-0.1f, 0.1f);
            float radius = Random.Range(_minRadius, _maxRadius);
            points1.Add(new Vector3(Mathf.Cos(radian) * radius, 0f, Mathf.Sin(radian) * radius));
        }
        List<Vector3> points2 = new List<Vector3>();
        for (int i = 0; i < points1.Count; i++) {
            Vector3 p1 = points1[(i + points1.Count - 1) % points1.Count];
            Vector3 p2 = points1[i];
            Vector3 p3 = points1[(i + 1) % points1.Count];
            Vector3 p4 = points1[(i + 2) % points1.Count];
            for (float t = 0f; t < 1f; t += 0.05f) {
                points2.Add(Spline(p1, p2, p3, p4, t));
            }
        }
        List<Section> sections = new List<Section>();
        for (int i = 0; i < points2.Count; i++) {
            Vector3 prev = points2[(i + points2.Count - 1) % points2.Count];
            Vector3 curr = points2[i];
            Vector3 next = points2[(i + 1) % points2.Count];
            Section section = new Section();
            section._direction = (next - prev).normalized;
            Vector3 orthogonal = Quaternion.AngleAxis(90f, -Vector3.up) * section._direction;
            section._left = curr - orthogonal * (_roadWidth / 2f);
            section._right = curr + orthogonal * (_roadWidth / 2f);
            sections.Add(section);
        }
        List<Vector3> vertices = new List<Vector3>();
        for (int i = 0; i < sections.Count; i++) {
            vertices.Add(sections[i]._left);
            vertices.Add(sections[i]._right);
        }
        List<int> triangles = new List<int>();
        int n = vertices.Count;
        for (int i = 0; i < vertices.Count; i += 2) {
            triangles.Add((i + 1) % vertices.Count);
            triangles.Add((i + 3) % vertices.Count);
            triangles.Add((i + 0) % vertices.Count);
            triangles.Add((i + 0) % vertices.Count);
            triangles.Add((i + 3) % vertices.Count);
            triangles.Add((i + 2) % vertices.Count);
        }
        Mesh mesh = GetComponent<MeshFilter>().mesh;
        mesh.Clear();
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();
        MeshCollider meshCollider = GetComponent<MeshCollider>();
        meshCollider.sharedMesh = mesh;
    }

    Vector3 Spline(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float t) {
        Vector3 a = p1 * (t * t * (2 - t) - t);
        Vector3 b = p2 * (t * t * (3 * t - 5) + 2);
        Vector3 c = p3 * (t * t * (4 - 3 * t) + t);
        Vector3 d = p4 * (t * t * (t - 1));
        return (a + b + c + d) / 2f;
    }
}

まとめ

アイディアの元となったサイトに感謝です。