Unityでストップウォッチを作る その3 目盛りのメッシュを作る

Pocket

今回は秒数目盛りのメッシュを表示するオブジェクトを作りたいと思います。こんな感じの1分で1周するアナログのストップウォッチ的な目盛りです。

unitysw_3_1.png

このように画面よりもかなり大きい目盛りの円(以後サークルと呼びます)を作りたいので、一枚の大きな画像を表示するのではなく、目盛りひとつの画像をたくさん並べて表示するという作り方をします。

スクリプトの作成

Projectウィンドウの「Script」フォルダの中に「BSWUtililty.cs」と「TimeCircle.cs」の2つのC#スクリプトを追加して、以下のコードをコピーしてください。

//
// BSWUtility.cs
//
using UnityEngine;
using System.Collections;
public class BSWUtility : MonoBehaviour {
  static public void DrawRect(Texture2D tex, Rect rect, Color col) {
   
    int minX = (int)rect.x;
    int minY = (int)rect.y;
    int maxX = (int)rect.xMax;
    int maxY = (int)rect.yMax;
   
    for (int x = minX; x < maxX; x++) {
      for (int y = minY; y < maxY; y++) {
        tex.SetPixel(x, y, col);
      }
    }
  }
 
  static public Rect CreateRectForClear(int originX, int originY, Vector2 lineSize, int padding, int border, int scale) {
   
    return new Rect(
      originX,
      originY,
      padding * 2 + (lineSize.x + border * 2) * scale,
      padding * 2 + (lineSize.y + border * 2) * scale);
  }
 
  static public Rect CreateRectForDraw(int originX, int originY, Vector2 lineSize, int padding, int border, int scale) {
   
    return new Rect(
      originX + padding + border * scale,
      originY + padding + border * scale,
      lineSize.x * scale,
      lineSize.y * scale);
  }
}
//
// TimeCircle.cs
//
using UnityEngine;
using System.Collections;
[ExecuteInEditMode()]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class TimeCircle : MonoBehaviour {
 
 public float lineRadius = 640.0f;
 public Vector2 largeLineSize = new Vector2(2.0f, 12.0f);
 public Vector2 smallLineSize = new Vector2(1.5f, 8.0f);
 public int largeLineCount = 60;
 public int partLineCount = 10;
 public Color largeLineColor = Color.white;
 public Color smallLineColor = Color.gray;
 public int textureOffsetX = 0;
 public int textureOffsetY = 0;
 
 float prevLineRadius = 0;
 Vector2 prevLargeLineSize = Vector2.zero;
 Vector2 prevSmallLineSize = Vector2.zero;
 int prevLargeLineCount = 0;
 int prevPartLineCount = 0;
 Color prevLargeLineColor = Color.clear;
 Color prevSmallLineColor = Color.clear;
 int prevTextureOffsetX = -1;
 int prevTextureOffsetY = -1;
 
 MeshFilter meshFilter;
 
 void Start () {
   
   UpdateLines();
   
 }
 
 void Update () {
   
   if (IsLinePropertyChanged()) {
     
     UpdateLines();
   }
 }
 
 bool IsLinePropertyChanged() {
   
   if (lineRadius != prevLineRadius ||
     largeLineSize != prevLargeLineSize ||
     smallLineSize != prevSmallLineSize ||
     largeLineCount != prevLargeLineCount ||
     partLineCount != prevPartLineCount ||
     largeLineColor != prevLargeLineColor ||
     smallLineColor != prevSmallLineColor ||
     textureOffsetX != prevTextureOffsetX ||
     textureOffsetY != prevTextureOffsetY) {
     
     return true;
   }
   
   return false;
 }
 
 void UpdateLines()
 {
   if (meshFilter == null) {
     meshFilter = GetComponent<MeshFilter>();
   }
   
   Mesh mesh = meshFilter.sharedMesh;
   if (mesh == null) return;
   
   mesh.Clear();
   
   Material mat = renderer.sharedMaterial;
   if (mat == null) return;
   
   Texture tex = mat.mainTexture;
   if (tex == null) return;
   
   Texture2D tex2d = (Texture2D)tex;
   if (tex2d == null) return;
   
   // Draw Texture
   
   int padding = 1;
   int border = 1;
   int scale = 2;
   int originX = textureOffsetX;
   int originY = textureOffsetY;
   int texWidth = tex2d.width;
   int texHeight = tex2d.height;
   
   Rect largeClearRect = BSWUtility.CreateRectForClear(originX, originY, largeLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, largeClearRect, Color.clear);
   
   Rect largeDrawRect = BSWUtility.CreateRectForDraw(originX, originY, largeLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, largeDrawRect, largeLineColor);
   
   Vector2[] largeUv = CreateUv(originX, originY, largeLineSize, texWidth, texHeight, padding, border, scale);
   
   originX += (int)(padding * 2 + (largeLineSize.x + border * 2) * scale);
   
   Rect smallClearRect = BSWUtility.CreateRectForClear(originX, originY, smallLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, smallClearRect, Color.clear);
   
   Rect smallDrawRect = BSWUtility.CreateRectForDraw(originX, originY, smallLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, smallDrawRect, smallLineColor);
   
   Vector2[] smallUv = CreateUv(originX, originY, smallLineSize, texWidth, texHeight, padding, border, scale);
   
   tex2d.Apply();
   
   renderer.sharedMaterial.mainTexture = tex2d;
   
   // Create Mesh
   
   int lineCount = largeLineCount * partLineCount;
   int vertexCount = lineCount * 4;
   int triangleCount = lineCount * 6;
   
   Vector3[] vertices = new Vector3[vertexCount];
   int[] triangles = new int[triangleCount];
   Vector2[] uv = new Vector2[vertexCount];
   
   Vector3[] largeLinePos = CreateLinePositions(largeLineSize, border, lineRadius);
   Vector3[] smallLinePos = CreateLinePositions(smallLineSize, border, lineRadius);
   
   for (int i = 0; i < lineCount; i++) {
     
     int vTopIndex = i * 4;
     int tTopIndex = i * 6;
     
     float angle = - ((float)i / lineCount) * 360.0f;
     var rot = Quaternion.Euler(0, 0, angle);
           var m = Matrix4x4.TRS(Vector3.zero, rot, Vector3.one);
     
     Vector3[] currentPos;
     Vector2[] currentUv;
     
     if (i % partLineCount == 0) {
       
       currentPos = largeLinePos;
       currentUv = largeUv;
     
     } else {
       
       currentPos = smallLinePos;
       currentUv = smallUv;
       
     }
     
     for (int j = 0; j < 4; j++) {
       
       int index = vTopIndex + j;
       vertices[index] = m.MultiplyPoint3x4(currentPos[j]);
       uv[index] = currentUv[j];
       
     }
     
     triangles[tTopIndex] = vTopIndex;
     triangles[tTopIndex + 2] = triangles[tTopIndex + 3] = vTopIndex + 1;
     triangles[tTopIndex + 1] = triangles[tTopIndex + 4] = vTopIndex + 2;
     triangles[tTopIndex + 5] = vTopIndex + 3;
   }
   
   mesh.vertices = vertices;
   mesh.triangles = triangles;
   mesh.uv = uv;
   
   mesh.RecalculateNormals();
   mesh.RecalculateBounds();
   mesh.Optimize();
   
   // Keep Properties
   
   prevLineRadius = lineRadius;
   prevLargeLineSize = largeLineSize;
   prevSmallLineSize = smallLineSize;
   prevLargeLineCount = largeLineCount;
   prevPartLineCount = partLineCount;
   prevLargeLineColor = largeLineColor;
   prevSmallLineColor = smallLineColor;
   prevTextureOffsetX = textureOffsetX;
   prevTextureOffsetY = textureOffsetY;
 }
 
 static public Vector3[] CreateLinePositions(Vector2 lineSize, int border, float radius) {
   
   float minX = - lineSize.x * 0.5f - border;
   float maxX = lineSize.x * 0.5f + border;
   float minY = - lineSize.y * 0.5f - border + radius;
   float maxY = lineSize.y * 0.5f + border + radius;
   
   return new Vector3[] {
     new Vector3(minX, minY, 0.0f),
     new Vector3(maxX, minY, 0.0f),
     new Vector3(minX, maxY, 0.0f),
     new Vector3(maxX, maxY, 0.0f)
   };
   
 }
 
 static public Vector2[] CreateUv(int originX, int originY, Vector2 lineSize, int texWidth, int texHeight, int padding, int border, int scale) {
   
   float minX = (float)(originX + padding) / texWidth;
   float maxX = (float)(originX + padding + (border * 2 + lineSize.x) * scale) / (float)texWidth;
   float minY = (float)(originY + padding) / texHeight;
   float maxY = (float)(originY + padding + (border * 2 + lineSize.y) * scale) / (float)texHeight;
   
   Vector2[] uv = new Vector2[] {
     new Vector2(minX, minY),
     new Vector2(maxX, minY),
     new Vector2(minX, maxY),
     new Vector2(maxX, maxY)
   };
   
   return uv;
 }
}

オブジェクトの作成と設定

スクリプトが作成できたら、シーンの中の「Root」オブジェクトの子に「Root/Circles/SecTimeCircle」という配置でゲームオブジェクトを2つ作成してください。それぞれのPositionを「Circles」は「X=0 / Y=-640 / Z=0」に、「SecTimeCircle」は「X=0 / Y=0 / Z=0」にしてください。

「SecTimeCircle」オブジェクトのComponentに「TimeCircle.cs」を追加します。

このままでは何も表示するメッシュやテクスチャがありませんので、前回のスクリプトを使って作成します。

ProjectウィンドウのCreateメニューから「Empty Mesh」を選択してメッシュを作成し、「SecTimeCircleMesh」と名前を変更してください。今後いくつかメッシュを作る事になるので「Mesh」というフォルダを作成してその中に移動しておいてください。

同じくProjectウィンドウのCreateメニューから、今度は「Texture...」を選択し、開いたウィンドウのなかの「Width」と「Height」が両方とも「128」、「Texture Format」が「ARGB32」になっている事を確認したら「Create」ボタンをクリックしてテクスチャを作成してください。テクスチャは今後もこの一枚しか使わないので適当に「Texture/Texture」な感じに置いておいてください。テクスチャ設定の「Filter Mode」は「Bilinear」にしておきます。

このテクスチャを使うのにはMaterialが必要なので作っておきます。Projectウィンドウの「Create/Material」で作成し、これも今後一つしか使わないので適当な名前ですが「Material/Material」のように置いておいてください。「Texture」をこのマテリアルにアサインし、shaderは「Unlit/Transparent」を選択します。このシェーダはラインティングの影響を受けず、テクスチャの透明部分を透過するシェーダーです。実はこのシェーダーを使うとちょっと問題があるのですが、その修正はチュートリアルの最後の方でやりますので、今の所はこれで我慢します。

先ほどシーン上に作った「SecTimeCircle」オブジェクトに「Material」と「SecTimeCircleMesh」をアサインします

この時点で画面上に秒数目盛りのラインが表示されていると思います。あと、タイムのテキストがサークルと被ってしまっているので、TimeTextのYを100くらいに移動しておいてください。

ここまで作業を終えると、プロジェクトの状態は以下のような感じになっていると思います。もし表示されていなかったら一旦Unityを再生してみると表示されるかもしれません。

unitysw_3_2.png

解説

「TimeCircle」のインスペクタのパラメータは以下のような感じです。

・lineRadius > サークルの半径
・largeLineSize > 長いラインのサイズ
・smallLineSize > 短いラインのサイズ
・largeLineCount > 長いラインの数(サークル全体が1分なら60で1秒)
・partLineCount > 長いラインの間を短いラインで分ける数(1秒につき0.1秒ごとなら10)
・largeLineColor > 長いラインの色
・smallLineColor > 短いラインの色
・textureOffsetX > テクスチャに描画する左の位置
・textureOffsetY > テクスチャに描画する下の位置

とりあえず良い感じに表示するようにパラメータの初期値を入れていますが、これらのパラメータを変更するとUnityを再生していなくてもリアルタイムで表示が変更されていると思います。これはスクリプトに「[ExecuteInEditMode]」と記述しているからで、これによって再生していないエディット中でもStart()やUpdate()が呼ばれます。ただ、毎アップデートごとにデータを更新するのはさすがに処理が重いので、データを更新したら「prev〜」という変数に保存して余計なアップデートをしない様にしています。実際にアプリをリリースするときにはStart()だけでやればいい処理なので、Updateはリリースビルド時に削除するようなマクロを仕込んでおいた方がいいかもしれません。

「UpdateLines()」というメソッドの中ではテクスチャへの描画とメッシュの作成をしています。やっていることは、2種類の目盛りの画像をテクスチャに描画するのと、その目盛りをたくさん並べた大きなサークルをひとつのメッシュとして作成する事です。Unityでテクスチャへ描画するのはドットを打つくらいしか出来ないのですが、BigStopWatchでは単純な四角くらいしか必要としないので十分です。

UnityのメッシュはOpenGLでいうところのGL_TRIANGLESで描画されます。頂点配列をmesh.verticesに、UV配列をmesh.uvに同じ要素数になるように作り、mesh.trianglesでその頂点番号の配列を作ってメッシュへ設定しています。

頂点配列(vertices)は必要な分だけ作れば良いのですが、頂点番号の配列(triangles)は、三角形ごとの頂点単位、つまり3つ単位でないといけません。たとえば四角形をひとつ作るなら、三角形を二つ組み合わせるけど2つの頂点は共通で使えるので、verticesとuvの配列の要素数は4、trianglesは6という事になります。

「UpdateLines()」の中の処理で、後々共通で使いたいものは「BSWUtility」の方に記述しています。四角をテクスチャに描画したり座標を取得するところあたりです。

目盛りはこんな感じで、四角に少しマージンを持たせて描画しています。

unitysw_3_3.png

四角いメッシュを単純に塗りつぶせばいいんじゃないかと思うかもしれませんが、やってみるとジャギーでひどい事になってしまいます。最近のiOSではマルチサンプリングが使えたりして多少ごまけたりもしますが、補間をテクスチャに任せたほうが遥かにきれいな線が表示できます。ただ、それは回転させるからの話で、回転させない四角は、ぴったりとピクセルが合う様に配置して補間させない方がきれいです。

メッシュは「lineRadius」の値だけY軸方向に移動させた位置に目盛りひとつ分の座標を「CreateLinePositions()」でつくって、それを必要な数だけMatrixで回転させてメッシュ全体を作っている感じです。

わざわざ1個のメッシュにしないで、目盛り1個のゲームオブジェクトを複製して作っても見た目は同じでコードも分かりやすいと思うのですが、やはり処理の負荷を考えると最初からまとめられるメッシュはまとめておいた方が良いと思います。UnityではDynamicBatchという機能があって勝手にメッシュをまとめてくれたりもしますが、一度にまとめられる頂点数に制限があったりとかあまり当てにしすぎると裏切られたりするので、自分でやっておくにこしたことはないです。

今回のWebPlayerビルド

以上で今回は終わりです。次回はこのサークルに数字を追加します。

前へ | 次へ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です