- 投稿日:2019-11-24T22:56:27+09:00
1杯のラーメンの写真をいっぱい撮って3Dモデルにした話
1杯のラーメンをいっぱい写真撮ってZephyrで3Dモデルにした話
これは Photogrammetry Advent Calendar 2019 の1日目の記事です。
ラーメンの写真からラーメンの3Dモデルを作成したときの話を書こうと思います。
ラーメンをフォトグラメトリでメッシュ化したらどうなるの?
フォトグラメトリ という技術があります。これを使えば、複数の静止画から3Dモデルを作成できます。
ラーメンをフォトグラメトリでメッシュ化したらどうなるの?
これが、フォトグラメトリという技術に触れて最初に思った疑問でした。
2019年7月7日にTIMEMACHINEさんで開催された STYLY Photogrammetry Workshop で、はじめてフォトグラメトリに触れました。
あまりにも簡単に実写風のメッシュが生成できて感動したので、さっそく、帰り道でラーメン屋に立ち寄り、ラーメンの写真を12枚ほど撮影しました。(念のため、店員さんにお許しをいただきました。)
そうしてできたのが、こちらです。
さっそくラーメンでフォトグラやってみましたw
— せぎゅ (@segur_vita) July 7, 2019
写真の枚数が全然足りなくて、穴だらけのメッシュですが、懸念していたスープの透過や反射は、それほど悪影響はなさそうです。 pic.twitter.com/HU9Mx6zig6穴だらけではありますが、想像以上に綺麗にできてびっくりしました。
使用したツールはこんな感じです。
- 撮影機材:iPhone Xのカメラ(静止画12枚)
- ソフト: 3DF Zephyr Free
3DF Zephyrの詳しい使い方はこちらを参考にさせていただきました。
これをきっかけに、ラーメンや食器を日々メッシュ化するようになりました。
撮影枚数を増やすためにテーブル席のあるラーメン屋を探した
丼の裏側は撮影できていないため、きちんとモデルが作れていません。完成度を上げるためには、死角ができないように上下左右前後から、ラーメンをくまなく撮影する必要があります。
そうなると、カウンター席ではなくテーブル席で撮影することが望ましいです。(テーブルの周りをぐるぐる撮影します)
しかし、ある問題が浮上しました。
テーブル席のあるラーメン屋って、結構少ないんです!(東京の場合)
テーブル席のあるラーメン屋を探す日々が始まりました。
なんとかテーブルのあるお店を見つけてできたのが、こちらです。
今度は家系ラーメンで #フォトグラメトリ やってみました!@mononou さんと協力して撮りまくった280枚の写真から、ピンボケしたのを取り除いた120枚でやりました。
— せぎゅ (@segur_vita) July 17, 2019
海苔のメッシュが想像よりも綺麗で嬉しいです!
次の課題は丼とレンゲかなぁ#Photogrammetry pic.twitter.com/14r2Z9O6eu死角のないようにとにかく撮りまくった結果、280枚になりましたw(その内使ったのは120枚です。)
おかげさまで、海苔が重なっている部分とかの精度がかなり向上しました!
ちなみに、静止画120枚をZephyrに取り込もうとしたところ、Free版は最大50枚までだったため、思い切って
3DF Zephyr Lite を購入しましたw
これで500枚まで取り込めるようになりました。
Unityで湯気を再現してみた
作ったものを友人や知り合いに見せて回ったところ、とある人から言われました。
湯気があるともっといいよねーw
衝撃を受けました。
早速、湯気の再現に取り掛かりました。
パーティクルシステムで実現できないか調査したところ、以下の記事を見つけました。
Unity ParticleSystem Lesson:舞い上がる煙
こちらを参考にして、無事にパーティクルシステムで湯気を出すことに成功しました。
アワードに応募することにした
ちょうどその頃、 STYLY Photogrammetryu Awards 2019 というイベントがVR作品を応募していました。
これに応募してみることにしました。
レンゲや箸といったラーメン関連物を片っ端から3Dモデル化し、VR空間に設置してみました。
そうしてできたのが、 ラーメン背牛 です。ぜひご覧ください。
STYLYさんにアップロードしたのはこちらです。
— せぎゅ (@segur_vita) July 21, 2019
丼とかごますり器とかいろいろ詰め込めこんだら、すごく重たいシーンになってしまった。。。https://t.co/wyXnAckbQ5 #STYLY残念ながらファイナリストには選ばれませんでしたが、自分としては、大好きなラーメンに囲まれて非常に楽しい空間が作れたと思ってますw
今後の課題:もっとメッシュを綺麗にしたい
メッシュがガタガタなので、これらを綺麗にすれば完成度がぐっと上がると思いました。この辺りはBlender等の技術を磨く必要があるなと感じました。
たとえば、丼なんかは円柱や円錐を組み合わせて加工すればもっと綺麗なメッシュになると思います。(その場合、フォトグラメトリとは呼べない気もしますが・・・)
また、ポリゴン数が多すぎるのも課題です。特徴が失われない範囲でポリゴン数を減らし、データ量を抑えたいなと思います。
さいごに
本記事作成にあたり、以下のページを参考にさせていただきました。ありがとうございました。
- 投稿日:2019-11-24T20:22:09+09:00
Unity DOTSとUnity Physicsでただ単純に球を動かしてみる
はじめに
たのしいDOTS〜初級から上級まで〜 - Unite Tokyo 2019 を見て、
「Unity DOTS凄い!」
「Unity Physics凄い!」
「使えるようになりたい!」
と思ったのですが、何から始めたら良いか分からなかったのでとりあえず「ただ球を動かす」ところから始めようと思い、その過程を本記事にまとめました。
私と同じような思いを持っている人の助けになれば幸いです。注意点
- preview packageを多く使用しています。今後、本記事のコードが使えなくなる可能性が高いのでご注意下さい。
- GameObject/Componentを併用した Hybrid ECS を前提としています。
- Unity DOTS(Unity ECS, C# Job System, Burst Compiler) やUnity Physics に関する詳しい説明は行わないのでご注意下さい。
環境
- macOS Catalina 10.15.1
- Unity 2019.2.12f1
- Entities preview 0.1.1
- Burst 1.1.2
- Jobs preview 0.1.1
- Mathematics 1.1.0
- Hybrid Renderer preview 0.1.1
- Unity Physics preview 0.2.4
PackagesのInstall
Window > Package Manager で Packages タブを開きます。
Entities や Hybrid Renderer はこの記事を書いている時点(2019年12月)では preview package なのでリストには表示されていません。
なので Advanced > Show preview packages にチェックを入れ、preview package を選択できるようにします。
Entitiesがリストに表示されたのでInstallします。
EntitiesをInstallすると、Burst, Jobs, Mathematicsも自動的に一緒にInstallされるので、新たにBurst, Jobs, MathematicsをInstallする必要はありません。
最後にEntitiesと同様に Hybrid Renderer と Unity Physics をInstallします。
Unity Physicsとは
簡単に説明しておくとUnity PhysicsとはDOTSで作られた、C#の物理エンジンです。
主に次のような特徴があります。
- ステートレスで決定論的
- 高パフォーマンス
詳しくは たのしいDOTS〜初級から上級まで〜 - Unite Tokyo 2019 をご覧ください。
地面を作る
3D Objectの作成
まず土台となる地面を作ります。
Cubeでも良いのですが、ここではPlaneの3D Objectを作成して Stage とでも名前を付けておきましょう。
Positionは (0, 0, 0)、Scaleは (5, 1, 5) くらいにしておきます。Colliderを交換する
ここでStageにアタッチされているMesh Collider(CubeならBox Collider)はUnity Physicsでは使えないのでRemoveします。
代わりにUnity Physics版のColliderである Physics Shape をAdd Componentします。ちゃんとAdd Componentできました。(Add ComponentするとInspectorには Physics Shape Authoring と表示されます)
しかし、Inspectorの上部に何か警告が出ています。The PhysicsShapeAuthoring component on `Stage` is meant for entity conversion, but it is not part of a SubScene or ConvertToEntity component. Please move the game object to a SubScene or add the ConvertToEntity component.この警告はStageオブジェクトをEntityに変換する処理を設定していないことが原因で表示されています。
(余談) [RequiresEntityConversion] の意味
そこでPhysicsShapeAuthoringの実装を見てみると、
[AddComponentMenu("DOTS/Physics/Physics Shape")] [DisallowMultipleComponent] [RequiresEntityConversion] public sealed partial class PhysicsShapeAuthoring : MonoBehaviour, IInheritPhysicsMaterialProperties, ISerializationCallbackReceiver { /* ... */ }このようにクラスの頭に RequireEntityConversion Attribute が付いています。
これを試しにコメントアウトしてみると、
警告が消えました。
どうやら RequireEntityConversion Attributeは、GameObjectがEntityにきちんと変換されるかどうかを監視する役割があるようです。
GameObjectがEntityに変換されるかどうか、は万が一のために監視して欲しいので、コメントアウトしたところを元に戻しておきます。GameObjectをEntityに変換する
StageオブジェクトをEntityに変換します。
GameObjectをEntityに変換する方法はいくつかありますが、ここでは ConvertToEntity コンポーネントを使います。
Stageに ConvertToEntity をAdd Componentします。
先ほどの警告は消え、これで実行時にStageはGameObjectからEntityに変換されるはずです。Entity Debuggerを使って確かめる
ConvertToEntity が実際にきちんと働いたか確かめてみます。
Window > Analysis > Entity Debugger でEntity Debuggerを開きます。
そしてUnityエディタを実行します。
すると、まず左のHierarchyビューからはStageオブジェクトが消えます。
また、Matching Chunksの所(上の画像で右の赤い四角に囲われた所)に、StageオブジェクトにアタッチされていたComponentから変換されたComponentDataの情報が表示されています。
試しにPhysicsShapeAuthoringを削除してから実行し、もう一度Entity Debuggerを確認してみます。
すると、PhysicsColliderが消えました。(他にもCompositeScale が NonUniformScale になっていたりしています)
つまり、3D ObjectにPhysicsShapeAuthoringをアタッチしてEntity化することによって、PhysicsColliderというComponentDataが自動的にEntityに追加される、ということが分かりました。Colliderの形を変更する
PhysicsShapeAuthoringの Shape type を Plane に変更して、 Fit to Enabled Render Meshes > Apply を選択します。
球を作る
Sphereの3D Objectを作成して、Position を (0, 3, 0)くらいにしておきます。
Sphere Collider を削除して Physics Shape と ConvertToEntity をAdd Componentします。
そしてPhysicsShapeAuthoring の Shape Type を Sphere に設定し、Fit To Enabled Render Meshes > Apply を選択します。
次に Sphere に Physics Body をAdd Componentします。(Add ComponentするとPhysics Body Authoringと表示されます)
PhysicsBodyAuthoringはRigidbodyのUnity Physics版のようなものです。
試しに実行してみます。
確かに Sphere が落下し Stage と衝突することが確認できました。
Entity Debuggerを見てみます。
PhysicsDamping, PhysicsMass, PhysicsVelocity という3つのComponentDataが新たに増えています。
つまり、PhysicsBodyAuthoringをアタッチしてEntity化することによって、PhysicsDamping, PhysicsMass, PhysicsVelocity という3つのComponentDataが自動的にEntityに追加される、ということが分かりました。Sphereを動かす
ここでようやくSphereを動かしてみます。
Unity ECSで何らかの処理を行うには ComponentSystem を継承したクラスを作成し、OnUpdate()
メソッド内に具体的な処理を書きます。
今回は次のような MoveSphereSystem というクラスを作成しました。MoveSphereSystem.csusing Unity.Entities; using Unity.Physics; public class MoveSphereSystem : ComponentSystem { protected override void OnUpdate() { Entities.ForEach((ref PhysicsVelocity physicsVelocity) => { physicsVelocity.Linear.x = 1.0f; }); } }
OnUpdate()
メソッド内にあるEntities.ForEach()
では、引数でWorldに存在するEntityの中から操作の対象を絞り込み、絞り込んだEntityに付随しているComponentDataの値を変更することによって目的の処理を実現しています。
ここでは PhysicsVelocity をComponentDataとして持っている(関連付けられている)Entity全てに対して処理を行っています。
今は PhysicsVelocity をComponentDataとして持っているEntityはSphere(をEntity化したもの)だけなので、結局Sphereの速度(のx成分)を設定していることになります。実際に実行してみると、確かにSphereが動いていることが確認できます。
Sphere「だけ」を動かす
ただ、PhysicsVelocityが付いているEntityが今はたまたまSphereしか無かったのでSphereだけが動きましたが、他にもPhysicsVelocityが付いているEntityがある場合、それらに対してもMoveSphereSystemは動作してしまいます。
例えば、Cubeの3D Objectを作成し、ConvertToEntity, Physics Shape Authoring, Physics Body Authoringをアタッチして実行してみると、
このようにSphereと一緒にCubeも動いてしまいます。複数のモノを動かすことが目的なら勿論これで全く構わないのですが、例えば「Sphereだけを動かしたい!」というような場合だと困りますよね。
何故このようなことになってしまったのかというと、MoveSphereSystem内の
Entities.ForEach()
の引数で操作の対象となるEntityを絞り込む際の絞り込みが甘かったからです。
CubeもSphereと同様にPhysicsBodyAuthoringがアタッチされていますから、PhysicsVelocityがCubeのEntityに付いています。
そこでEntities.ForEach()
の引数による絞り込みの条件を「PhysicsVelocityが付いていること」だけにしてしまうとSphereだけでなくCubeも処理の対象としてヒットしてしまう、というわけです。なので絞り込みの条件をキツくして、絞り込んだ結果Sphereだけを処理の対象とするようにするために、次のようなComponentDataを作成し、SphereのEntityに追加することにします。
SphereTagComponentData.csusing System; using Unity.Entities; [Serializable] public struct SphereTagComponentData : IComponentData { }単に絞り込みの条件をキツくするためだけのComponentDataなので中身は空でOKです。
そして次のようなSphereAuthoringクラスを作り、Sphereオブジェクトにアタッチします。
SphereAuthoring.csusing UnityEngine; using Unity.Entities; [RequiresEntityConversion] public class SphereAuthoring : MonoBehaviour, IConvertGameObjectToEntity { public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new SphereTagComponentData()); } }これで実行時にSphereのEntityにSphereTagComponentDataが追加されます。
MoveSphereSystemを次のように変更します。
MoveSphereSystem.csusing Unity.Entities; using Unity.Physics; public class MoveSphereSystem : ComponentSystem { protected override void OnUpdate() { Entities.ForEach((ref PhysicsVelocity physicsVelocity, ref SphereTagComponentData sphereTagComponentData) => { physicsVelocity.Linear.x = 1.0f; }); } }これによりEntityの絞り込み条件が「PhysicsVelocityとSphereTagComponentDataを両方持つこと」になったのでSphereのみがこの条件にヒットし、結果的にSphereのみを動かすことができます。
C# Job Systemを使う
MoveSphereSystemをJob Component Systemを使って書き直すことにより並列化することができます。
ComponentSystemではなくJobComponentSystemを継承したクラスを作成し、そのクラス内にさらにIJobForEachを実装した構造体(ここではMoveSphereJob)を作成し、その構造体内のExecute()
メソッド内に具体的な処理を書きます。
Execute()
メソッドの引数で先ほどのEntities.ForEach()
と同じように、操作の対象を絞り込んでいます。
OnUpdate()
メソッド内で、たった今定義した処理(Job)を他のスレッドに投げる、ということを行っています。MoveSphereSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Physics; public class MoveSphereSystem : JobComponentSystem { private struct MoveSphereJob : IJobForEach<PhysicsVelocity, SphereTagComponentData> { public void Execute(ref PhysicsVelocity physicsVelocity, [ReadOnly] ref SphereTagComponentData sphereTagComponentData) { physicsVelocity.Linear.x = 1.0f; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveSphereJob(); return job.Schedule(this, inputDeps); } }Burst Compilerにより高速化する
コードはほとんど同じでMoveSphereJobの直前に
[BurstCompile]
をつけるだけです。MoveSphereSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Physics; public class MoveSphereSystem : JobComponentSystem { [BurstCompile] private struct MoveSphereJob : IJobForEach<PhysicsVelocity, SphereTagComponentData> { public void Execute(ref PhysicsVelocity physicsVelocity, [ReadOnly] ref SphereTagComponentData sphereTagComponentData) { physicsVelocity.Linear.x = 1.0f; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveSphereJob(); return job.Schedule(this, inputDeps); } }Sphereに力を加えて動かす
ここまでSphereを一定の速度で動かしていましたが、ここではSphereに力を加えて加速させたいと思います。
$ m $ : Sphereの質量
$ \textbf{v} $ : Sphereの速度
$ \textbf{F} $ : Sphereが受ける外力
$ t $ : 時刻とするとニュートンの運動方程式より
m \frac{d\textbf{v}}{dt} = \textbf{F}が成り立ちます。
両辺に$dt / m$をかけるとd\textbf{v} = \frac{1}{m}\textbf{F} dtとなります。
左辺にd\textbf{v} = \textbf{v}(t + dt) - \textbf{v}(t)を代入して整理すると、
\textbf{v}(t + dt) = \textbf{v}(t) + \frac{1}{m}\textbf{F}dtとなります。
Sphereの質量$m$はPhysicsMassから取得できます。
これをMoveSphereSystemに実装すると、次のようになります。MoveSphereSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Physics; using UnityEngine; public class MoveSphereSystem : JobComponentSystem { [BurstCompile] private struct MoveSphereJob : IJobForEach<PhysicsVelocity, PhysicsMass, SphereTagComponentData> { public float3 F; public float DeltaTime; public void Execute(ref PhysicsVelocity physicsVelocity, [ReadOnly] ref PhysicsMass physicsMass, [ReadOnly] ref SphereTagComponentData sphereTagComponentData) { physicsVelocity.Linear += physicsMass.InverseMass * F * DeltaTime; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveSphereJob { F = math.float3(5, 0, 0), DeltaTime = Time.deltaTime }; return job.Schedule(this, inputDeps); } }非常に分かりづらいですが、力が加わって加速して動いているように見えなくもないです。
キーボードの入力に応じてSphereを動かす
次にキーボード入力によりSphereを動かす方向を変えられるようにします。
まずForceというComponentDataを新たに作成し、SphereAuthoring内でSphereのEntityに追加します。ForceComponent.csusing System; using Unity.Entities; using Unity.Mathematics; [Serializable] public struct Force : IComponentData { public float3 value; }SphereAuthoring.csusing UnityEngine; using Unity.Entities; [RequiresEntityConversion] public class SphereAuthoring : MonoBehaviour, IConvertGameObjectToEntity { public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new SphereTagComponentData()); dstManager.AddComponentData(entity, new Force()); } }これに伴い、MoveSphereSystemも次のように変更します。
MoveSphereSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Physics; using UnityEngine; public class MoveSphereSystem : JobComponentSystem { [BurstCompile] private struct MoveSphereJob : IJobForEach<PhysicsVelocity, PhysicsMass, SphereTagComponentData, Force> { public float DeltaTime; public void Execute(ref PhysicsVelocity physicsVelocity, [ReadOnly] ref PhysicsMass physicsMass, [ReadOnly] ref SphereTagComponentData sphereComponentData, ref Force force) { physicsVelocity.Linear += physicsMass.InverseMass * force.value * DeltaTime; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveSphereJob() { DeltaTime = Time.deltaTime, }; return job.Schedule(this, inputDeps); } }以前は外力$\textbf{F}$の値をMoveSphereSystemの
OnUpdate()
内で具体的に設定していたのに対し、それをここではForceというComponentDataに切り出しています。そしてForceに対する処理を行うChangeForceSystemを作成します。
ChangeForceSystem.csusing Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Physics; using UnityEngine; public class ChangeForceSystem : JobComponentSystem { [BurstCompile] private struct ChangeForceJob : IJobForEach<PhysicsVelocity, PhysicsMass, SphereTagComponentData, Force> { public float3 Force; public void Execute(ref PhysicsVelocity physicsVelocity, [ReadOnly] ref PhysicsMass physicsMass, [ReadOnly] ref SphereTagComponentData sphereComponentData, ref Force force) { force.value = Force; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new ChangeForceJob(); if (Input.GetKey(KeyCode.LeftArrow)) { job.Force = math.float3(-10, 0, 0); } if (Input.GetKey(KeyCode.RightArrow)) { job.Force = math.float3(10, 0, 0); } if (Input.GetKey(KeyCode.UpArrow)) { job.Force = math.float3(0, 0, 10); } if (Input.GetKey(KeyCode.DownArrow)) { job.Force = math.float3(0, 0, -10); } return job.Schedule(this, inputDeps); } }キーボードの矢印の入力に従ってSphereを動かすことができました。
今回のサンプルはこちらに置いてあるので良ければ参考にしてみてください。おわりに
Unity DOTS初心者を対象としているのに、DOTS(ECS, C# Job System, Burst Compiler)自体の詳しい説明はしない、という一体誰をターゲットにしているのかよく分からない中途半端な記事になってしまったことをお許しください。
DOTSを勉強する際は参考に挙げている記事や動画が特にオススメです。
- 追記 -
この記事の続編にあたる Unity DOTSでRoll a Ball(玉転がし)を作る を書きました。
少しでも参考になれば幸いです。参考
- たのしいDOTS〜初級から上級まで〜 - Unite Tokyo 2019
- Introduction to Unity Physics
- 【Unity】新しい物理演算、Unity Physicsについて
- 【Unity】 ECS まとめ(前編) - エフアンダーバー
- 【Unity】 ECS まとめ(後編) - エフアンダーバー
- 【Unity】 ECSへ 思考の移行ガイド - エフアンダーバー
- 【Unity】Unity 2018のEntity Component System(通称ECS)について(1) - テラシュールブログ
- 大量のオブジェクトを含む広いステージでも大丈夫、そうDOTSならね - Unite Tokyo 2019
- EntityComponentSystemSamples
- 投稿日:2019-11-24T18:38:14+09:00
【Unity】UIで押させたいボタンだけフォーカスさせる
背景
デジゲー博に展示して試遊してる様子を見て気づいたんですが、押させたい場所にポップを表示させるだけでは人は押してくれませんでした(見ててまじかよと思った)。
そこで、押させたい場所しか押せない仕組みが必須と分かり実装してみました。
概要
UIで押させたいボタンだけフォーカスさせる実装方法
— アズマゴロー@デジゲー博B-03a (@azumagoro) November 24, 2019
①Stopper用Canvas(+Image)を通常のUIのCanvasより前に表示させる
②押させたいボタンにAddComponentで動的にCanvasを追加し、SortOrderをStopperCanvasより前に表示させる。
③するとUIのレイヤー関係を壊さずに最前面に表示できる。 pic.twitter.com/TRnADbzk1Lコードとか
StopperController.csusing UnityEngine; public class StopperController : MonoBehaviour { [SerializeField] private Canvas _stopperCanvas; public static StopperController Instance; private void Awake() { Instance = this; } public void Focus(GameObject go) { _stopperCanvas.gameObject.SetActive(true); var canvas = go.AddComponent<Canvas>(); // ここtrueにしてからでないとsortingOrderの値を変更できない! canvas.overrideSorting = true; canvas.sortingOrder = _stopperCanvas.sortingOrder + 1; canvas.renderMode = RenderMode.ScreenSpaceOverlay; } public void RemoveFocus(GameObject go) { var canvas = go.GetComponent<Canvas>(); if (canvas !=null) { Object.Destroy(canvas); } _stopperCanvas.gameObject.SetActive(false); } #if UNITY_EDITOR // 動作テスト用のコードは#if UNITY_EDITORで囲ってビルドに含まれないようにするのをよくやります [SerializeField] private GameObject testGameObject; [ContextMenu("TestFocus")] void TestFocus() { Focus(testGameObject); } [ContextMenu("TestRemoveFocus")] void TestRemoveFocus() { RemoveFocus(testGameObject); } #endif }StopperCanvas
StopperController
参考になった場合はよろしければいいねお願いします?
おわり
- 投稿日:2019-11-24T12:19:04+09:00
【VFX】ポイントキャッシュの読み取り方法をTextrureで模倣する
【VFX】ポイントキャッシュの読み取り方法をTextrureで模倣する
キーワード
Unity, Visual Effect Graph, Point Cache, Texture
これは何
Textureを利用してVisual Effect Graphのパーティクル出現位置を調整する方法の紹介です。
こんな感じのTexture画像を読み込むと、
こんな感じに表現されます。
経緯
Visual Effect Graphで、TextureをPoint Cacheに変換して利用する記事を見つけました。
神屋電算 さんの記事です。 【unity】visual-effect-graphで文字がサラサラっと消えるやつを作るただ、Point Cacheはパラメータを外部から設定できない?ようでした。Textureなら設定できるので、それっぽい情報を探していたところ、次の記事がUnityフォーラムにありました。
Generation Of A Point Cache For Vfx Graph
フォーラム上のRemyさんの回答がほぼほぼ答えなのですが、一部詰まった所がありましたので、一通り導入する方法をまとめました。設定(概要)
下記手順で動作するようになります。
あらかじめ、Visual Effect Graphで表現したい図形の表示座標をRGBAで表現したpng画像があるとします。Texture
読み込み対象のTextureは次の制約を課されます。
- Mitmapを持たないこと(1)
- Wrap ModeがClampではないこと(2)
- とりあえずRepeatにしておけばいいと思います
- Filter ModeがPointであること(3)
- デフォルトはBilinearが設定されていると思います(多分)。Binaryですと場合によっては得られる座標点が補完されなめらかになります。
- Float値であること(4)
- そうでない場合正しく座標が読み込まれません。Automaticもダメでした。
- sRGB, Alpha Is Transparencyについても、falseやNoneの方がいいようです。
- 以下の状態です。
Visual Effect Graphの操作
- TextureはSample Texture2Dより読み込みます。(1)
- こちらの s ノードをSet Positionボックスの座標系に接続します。(2)
- 任意のUV座標を読ませるよう設定します(3)
- Remyさんの回答は丁寧に座標を取得していますが、ざっくりRandom Numberを使っても大丈夫そうです。
- 以下の状態です。 ※Random numberはConstantフラグをOffにするか、Seedを別値にしておきます。(4)
詳細
概要通りの設定をすれば動作します。
ここからは、Textureがどのように座標情報として利用されるかなどをまとめました。Textureと座標の関係
読み込む画像の各ピクセル座標のr,g,bが、Unity空間上のx,y,zと対応しています。
r,g,bは今回の場合0-255で作成しています。
これをUnityが0-1のfloat値として利用していますが、スクリプト上でそれ以上の値を設定できるかもしれません(要検証)
Remyさんの添付グラフ
Generation Of A Point Cache For Vfx Graphにて回答されているRemyさんの手法についてまとめました。
まず外部に三つの変数を公開しておきます。
それぞれ、参照したいテクスチャ、幅、高さ、になります。
Get Attribute: ParticleIdノードを用いて、生成されるパーティクルのIDを求めます。
以降、このIDに対して計算を行うことで、パーティクル毎に位置を決めていくようです。
Moduloのノードで、1次元での参照位置を計算しています。
次の部分では、参照位置を幅、高さの各軸になるよう求めなおしています。
ここまでで参照するピクセル座標は決まりましたが、最終的にはUV座標でアクセスする必要があります。
下の計算部分でUV座標に直しています。
ここで、それぞれのDivideがfloatの結果を求めることを確認してください。
a,bともにfloatとし、再度設定ボタンを押してください。
結果で得られたUV座標は、それぞれの色ピクセルの端を指しています。
こちらで参照位置を各色の中心に変更します。
(理解が間違っていたらご指摘ください)
こちらのDivideの項目も、Vector2でない場合は先ほどと同様に設定しなおしておきます。
最後にSet Positionと接続します。
ここではInitializeブロック内にSet Positionを追加し、そこに接続しました。
このようにグラフを接続することで、読み込み画像の左下のピクセルから順に数値を読み取り、
各パーティクルの座標として利用できるようです。例
- 参照ピクセルが(r,g,b)=(255,0,0)つまり赤だった場合、(x,y,z)=(1,0,0)となるので、Unity空間上の1,0,0にパーティクルは生成されます。
参照ピクセルが(r,g,b)=(255,2555,0)つまり黄だった場合、(x,y,z)=(1,1,0)となるので、Unity空間上の1,1,0にパーティクルは生成されます。
次の画像は赤から黒へのグラデーションなので、左の画像をグラフの入力とすると、(0,0,0)-(1,0,0)の範囲内に16個の点が打たれます。
-->ピクセル座標から色情報への変換イメージ
その他
ランダム打点な座標取得とRemyさんの手法の差
- Remyさんの手法を取らずとも、Random NumberノードをSample 2DのUVに接続すれば似たような表示が可能です。ただ、Remyさんの手法の場合パーティクル座標は画像の左下から順に値を追うことになるので、表示を調整すると書き順の再現的なことができます。
PNGにおける透明ピクセルの扱い
- Textureの設定でAlpha Is TransparencyをTrueにした場合は無視され、Falseの場合は白として扱われるようです。
- 微妙にUnity上での結果が変わりました。
わかっていないこと
w……あなたは何者でどこへ向かうのでしょうか……
グラフ全体
参考
Unityフォーラム JakHussainさんの質問
Generation Of A Point Cache For Vfx Graph
- 投稿日:2019-11-24T11:45:48+09:00
Unity玉転がしチュートリアル 3-4.ゲームのビルド(終)
この記事の対象者
- Unity入門したい人
- 最初の一歩が踏み出せない人
OSとか環境とか
- Windows 10 Pro
- macOS Mojave
- Unity 2019.2.8f1
- Rider 2019.2.2
補足
- 公式動画にて利用しているのはMacなので、Windowsユーザーはある程度脳内変換して見る事
- 筆者はWindows、Macの両方の環境で確認。Ubuntuとかでは検証してない。
- 基本Unityは英語メニューで利用
- 間違いがあったらツッコミ大歓迎
公式
ゲームのビルド
そんなに書くことが無い
Build Setttingsウィンドウから行う
ビルドターゲットは現在はPCなので、PCのままでOK
AndroidとかiOS等に出力したい場合は今回は割愛ビルドするゲームに必要なScenesを取り込みます。
Add Open Scenesから追加しても良いし、直接Projectタブからドラッグして追加しても良いとりあえず変更せずにBuildボタンを押下
フォルダを選択するとビルドがはじまるので終わるまで待機
ビルドが終わるとプレイに必要なファイル群がフォルダに生成される
生成された「アプリ名.exe」を実行するとゲームが動くはずです!最後に
という感じでUnity超基本デビューは幕を閉じるのであった。
折角ある程度触ったし、次は自分でAssetStoreとか漁って小さくてもいいから形にしようと思う今日この頃でした。
オラタンか格ゲーが作りたいね!なおここまでのソースは下記にアップしておりますのでご自由にどうぞ。
- 投稿日:2019-11-24T11:17:25+09:00
Unity玉転がしチュートリアル 3-3.スコアとテキストの表示
この記事の対象者
- Unity入門したい人
- 最初の一歩が踏み出せない人
OSとか環境とか
- Windows 10 Pro
- macOS Mojave
- Unity 2019.2.8f1
- Rider 2019.2.2
補足
- 公式動画にて利用しているのはMacなので、Windowsユーザーはある程度脳内変換して見る事
- 筆者はWindows、Macの両方の環境で確認。Ubuntuとかでは検証してない。
- 基本Unityは英語メニューで利用
- 間違いがあったらツッコミ大歓迎
公式
https://learn.unity.com/tutorial/collecting-scoring-and-building-the-game#5c7f8529edbc2a002053b78a
アイテムのカウント
PlayerControllerスクリプトにアイテムのカウント機能を追加
単純にスクリプトにint型のcount変数を準備するだけ
ただしこのままでは内部的に数値が増加するだけでプレイヤーに伝わらないUnityでのテキスト表示
UnityのUIツールセットを利用する
UI>Textを追加
追加するとTextの親としてCanvas、EventSystemが追加されているこれはUnityの仕様で、全てのUI要素はCanvasの子要素として機能させなければならないという鉄の掟がある(多分)
Textを「Count Text」とリネーム
追加されたが、色が黒くて地味で見ずらいので、白に変更
Textオブジェクトには他のオブジェクトと違いRect Transformが存在する
スコア表示の位置調整
Anchor Presetsを表示して
Shift + Altキーを押しながら、一番左上のアイコンを押下
画面の左上にテキストが配置されるが、このままだとテキストがギリギリ過ぎてめり込んで見えてしまう
なのでPosX,PosYを調整して空白をもたせるとそれっぽくなるcount値の反映
PlayerControllerスクリプトの中でUnityEngine.UIのTextを用いて紐付ける
これでPlayerController側にフィールドが増えるので、そこにCountTextをドラッグして紐付ける
※最終的なソースは最後に書きますので動きはそちらを参考にする事↓紐付けるとこの様になる
ゲームの終了
UI>Textで終了用のテキストオブジェクトを追加
自動的にCanvasの下に追加されるので、下記のように調整
要素 値 Pos Y 75 Font Size 24 色 白 Text Win Text これで下記の状態になるので、実際に終了条件を満たした際に「Win Text」が更新されるスクリプトを実装
スクリプトを実装したら、PlayerオブジェクトのWin Textフィールドにドラッグして紐付ければ完成
最終的なPlayerController.cs
PlayerController.csusing System; using UnityEngine; using UnityEngine.UI; public class PlayerController : MonoBehaviour { public float speed; public Text countText; public Text winText; private Rigidbody rb; private int count; // See also:https://docs.unity3d.com/ja/2019.1/Manual/ExecutionOrder.html private void Start() { rb = GetComponent<Rigidbody>(); count = 0; SetCountText(); winText.text = ""; } private void FixedUpdate() { float moveHorizontal = Input.GetAxis("Horizontal"); float moveVertical = Input.GetAxis("Vertical"); Vector3 movement = new Vector3(moveHorizontal,0.0f,moveVertical); rb.AddForce(movement * speed); } private void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag("Pick Up")) { other.gameObject.SetActive(false); count++; SetCountText(); } } private void SetCountText() { countText.text = "Count :" + count.ToString(); if (count >= 12) { winText.text = "You Win!"; } } }
- 投稿日:2019-11-24T10:45:28+09:00
20年付き合ったデザイナーの肩書と別れて、エンジニアになった全裸中年男性の話
みなさまこんにちは。Qiita初投稿者です。
いずれ技術的な話も書いていってみたいですが、まずは初投稿記念に自己紹介とQiita記事を投稿しようと思った経緯など書いてみます。Qiita怖い。簡潔にいうと、隙あれば自語りという内容です。Qiita怖い。
ですが、どなたかの何かしらの参考になればという思いもございます。Qiita怖い。はじめに
タイトル通りですが、私は20年ほどデザイナー(少し具体的には、紙媒体やDTPデザイナーからWebデザイナーへ転向したマン)という肩書で雇われた会社で業務に従事してまいりましたが、どうも『デザイン』という言葉の定義を履き違えていたようなのと、何も考えてなかった、及び将来見据えてエンジニアという道を選択しましたので、デザイナーだった自分へのはなむけみたいなとりとめのない話です。Qiita怖い。
20年以上使い込み慣れ親しんだイラレやフォトショとはここでオサラバです。(会社のしか使ったことないけど)最近よく見る投稿で「ゼロからエンジニアになった話」とか「未経験から」とか「(全く違う職種)から」などありますが、自分に関しては全くそうではなく、絵を描くのが小さい頃は好きで多少なり絵に関わるデザイナーみたいな仕事を選んだにも関わらず、よくよく思い返してみると、高校くらいから初めて買ってもらったMac(←Macというのはデザイナーぽい)に付属してたハイパーカードでRPG作ったり、デザイン専門学校のグラフィック科に入ったのに、学科的に本当は専攻できないディレクター?ショックウェーブ?だっけ?でのゲーム作り講義になぜか入り込んでいたり(半年だかで事務局からあなたこの授業受講できませんって言われた)、Webデザイン始めてからは当然絵作りはもちろんhtml, cssでのコーディング(初期は懐かしのテーブルコーディング)はもちろん、javascript, 今は亡き?ActionScript, 商用のCMSで謎記述したりと割とそもそもお前プログラミングとか好きでは?的な生き方でありました。
さらにざっくり10年ほど前にiPhone 3Gの発売の衝撃から、『ゲーム作りたい!』マインドが爆発し、ググって出てきたObjective-Cのコードを丸々コピペしながら何も考えずに80本ほどApp Store/Google playにてリリースしてきました。(直近はもっぱらUnityでの制作ですが)もちろん会社仕事とは別です。
そんなですから多くの技術者様に石を投げられそうな(最近だとマサカリっていうんですかね?村田兆治?)クソコードを量産してきており、とてもこちらに投稿できるようなレベルの記事が書けるとも思えないのですが、自身の研鑽のためにもとにかくやってみようと思った次第です。
おそらく『未経験から〜』の方々の方がしっかり大学やらで一番大事な基礎の学問を学ばれてその下地により経験1年でも1ヶ月でも僕なんかよりよほど優れたコードをお書きになられていると存じます。Qiita怖い。本文
なんやかんやありまして。
いがかでたしでしうょか?
導入だけで終わるという、いかにも表層だけ、見た目だけの中身のないデザイナー(という肩書の依頼者が気に入る絵を組む人)を続けてきた人間の書く初投稿といった感じではないでしょうか?Qiita怖い。
正直どんな仕事であれ、頭使ってしっかり考えなきゃダメ、という自戒でした。まとめると、
・何も考えずともデザイナーという肩書きで適当に働ける。
・何も考えずとも10年で80本くらいはアプリをリリースできる。
・何も考えていないと仕事も、アプリもいくら数撃っても当たらない。
(まぐれ当たりはノーカウント。)おわりに
よろしければ20年の集大成とも言える、一番最近、私のリリースしたアプリをぜひ遊んでみてください。
Lunch Time Fish(日本語タイトル:シロクマランチFish)
https://softfunk.com/app/rd_ltf.html
プラットフォーム:iOS, Androidありがたい事に今年開催されたGoogle主催のインディーゲームコンテスト、『Google IndieGames Festival 2019』にて、数多くの応募の中から栄えあるトップ10に選出していただきました!
デザイナー肩書との決別とともに、40代突入の大変素晴らしいスタートを切らせていただきました。
...が、現状おそろしくダウンロードされてませんのでまだまだここから、今後はしっかり遊ぶ人がいかに楽しめるかを考えて、アップデートでコンテンツ拡充を頑張りたいと思っています。最後までお読みいただき、ありがとうございました。
※本記事に関しては完全に単なる自分語りですので、何か思うところあれば本記事へのコメントはせず、ご自身で記事を投稿いただけますようお願いいたします。非常に怯えながら書いております。Qiita怖い。
ちなみにこの記事も特に何も考えず書いています。ただ書いてみたいと思って。・・・こういうとこやぞ!
- 投稿日:2019-11-24T03:54:42+09:00
【Unity】【シェーダ】ライティング入門
デフォルトライティング関数
・Unityではあらかじめ下記のライティング関数が利用されている。
・構造体の詳細については※4 公式リファレンス参照。
関数名 ライティング関数へ渡す構造体 タイプ Lambert SurfaceOutput 非物理ベース BlinnPhong SurfaceOutput 非物理ベース Standard SurfaceOutputStandard 物理ベース ライティングの切り替え
・下記2か所を設定する。
#pragma surface surf 関数名 void surf (Input IN, inout ライティング関数へ渡す構造体 o) { }自作ライティング関数
①定義
・関数名は「Lighting」で始める。
・関数の引数は以下の通り。(その他の引数については ※1公式リファレンスを参照)
タイプ 引数 カメラビューなし half4 LightingMyName(SurfaceOutput s, half3 lightDir, half atten) カメラビューあり half4 LightingMyName(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten) void LightingAnyMethodName(SurfaceOutput s, half3 lightDir, half atten) { return 描画色; }②宣言
#pragma surface surf 「Lighting」を除いた関数名 void surf (Input IN, inout ライティング関数へ渡す構造体 o) { }ライティングをOFFにする
・シェーダの作成に当たり、ライトを切りたい場合があるは以下の設定を行う。
1. Window -> Rendering -> Lighting Settingを選択する。
2. Intensity Multiplierの値を「0」に設定する。物理ベースと非物理ベース
特徴について
・物理ベースレンダリング (PBR : Physically - Based Rendering)
・非物理ベースに比べて、より物理学のモデル(光の屈折や反射)に近い計算を行うことで、より現実に近い描写を行うことができる。
- energy conservation (エネルギー保存則) - 受けた光量以上は反射できない。
- microsurface scatteringstate-of-the-art (微小表面反射) - 荒い面はなめらかな面に比べて不安定な光を反射する
- Fresnel reflectance (フレネル反射) - スペキュラ反射は「かすめ角」に現れる。
- surface occlusion (面閉塞) - 角の暗がりや反射しにくい面
・イメージする質感を直感的に作成できる。
(例えば、Metallicというパラメタに1を設定すると金属っぽくなり, 0を設定すると非金属っぽくなる。)
・非物理ベースに比べて、高負荷である。種類
・Materialのインスペクタビューからシェーダを「Standard」「Standard (Specular)」を選択する。
・どちらを選択しても同一のマテリアルを作成できるため、どちらを選ぶかは好みの問題である。
・詳細な違いについては※2 公式リファレンス参照
・マテリアルのサンプルは※3 公式リファレンス参照透過の設定
・Materialのインスペクタビューから「Render Mode」を切り替える。
Type 2 3 Transparent 半透過 α=0でも光は反射するため透明にはならない。鏡のようなオブジェを作りたいときに使用 Fade 完全透過 αが透明度となる。 CutOff 部分透過 α < C となる部分の描画をスキップする。 参考サイト
1. Custom lighting models in Surface Shaders
2. Metallic と Specular のワークフロー
3. マテリアルチャート
4. サーフェスシェーダーの記述
- 投稿日:2019-11-24T02:38:36+09:00
【Unity】【シェーダ】Shader Graph入門
Shader Graphの導入
3.「render-pipelines.lightweight」をインストールする。
4.Projectタブ上でCreate -> Rendering -> LightWeight Render Pipelineを選択。
5.メニュー上でEdit -> Project Settingsを選択する。
6.Graphics -> Scriptable Render Pipeline Settingsに4で作成したファイルをアタッチする。
Shader Graphの利用
インスペクタ上にプロパティを追加する。
- +ボタンを押し、使用したい型を選択する。
MyTexture : インスペクタ上に表示される名前
Exposed : インスペクタ上に表示するかどうか
Reference : シェーダ内での変数名
- 投稿日:2019-11-24T02:13:10+09:00
【Unity】
はじめに
Unityに関連した記事はなるべくここに記載してゆく
シェーダ
Surface Shader入門
シェーダ サンプル集PostEffect サンプル集
Lighting サンプル集
アニメーション
- 投稿日:2019-11-24T02:13:10+09:00
【Unity】【シェーダ】
はじめに
Unityのシェーダ関連はすべてここの記事にまとめていくよ。
Surface Shader入門
シェーダ サンプル集PostEffect サンプル集
Lighting サンプル集
- 投稿日:2019-11-24T00:41:25+09:00
PackageManagerから必要なPackageをインポートする
はじめに
はやく民主化して
こんにちは、アドベントカレンダー19日目担当の避雷です。unitypackageを配布したことのある人の中には、相手方と自分のプロジェクトに入っているリソースの乖離に苦しんだことがある人もいると思います。
例えばこっちがPPSv2でイイ感じの画を提供していても、インポート先のProjectにPackageManagerのPostProcessingが入っていなければその魅力を伝えることが出来ません。
今回はEditor拡張を用いてPackageManagerを操作する方法を調べてみましょう。PackageManagerを操作する
PackageManagerに対する基本的な操作は
UnityEditor.PackageManager
で行います。
https://docs.unity3d.com/ja/2017.4/ScriptReference/PackageManager.Client.htmlインポートされているパッケージの一覧を取得
インポートされているパッケージの一覧を取得するには、
Client.List
を使います。Listの返り値はListRequestです。
https://docs.unity3d.com/ja/2017.4/ScriptReference/PackageManager.Client.List.html
返り値の名称からもわかるように、この関数は非同期で実行されます。ListRequestはStatus,Resultなどの変数を持つので、これらを参照して非同期的に書きましょう。
↓サンプルコード(インポートされたすべてのpackageを出力)using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEditor; using UnityEditor.PackageManager; using UnityEngine; public class ListPackage { [MenuItem("PackageManager/List")] static async void List() { var listRequest = Client.List(); while (listRequest.Status == StatusCode.InProgress) { await Task.Delay(100); } if (listRequest.Status == StatusCode.Success) { var result = ""; foreach (var package in listRequest.Result) { result += package.packageId + "\n"; } Debug.Log(result); } if (listRequest.Status == StatusCode.Failure) { Debug.Log("Error!"); } } }MenuItemアトリビュートによってUnityEditorのメニューバーにPackageManagerの欄が追加されていると思うので、Listをクリックしてみましょう。一瞬のディレイの後、以下のようなログが出力されます。
Hoge@huga
と言うのがそれぞれのpackageのpackageIDです。
@より前がpackageの名前、@より後がpackageのversionです。
現在はcom.unity.hoge
となっている(つまりUnity公式の)packageしかありませんが、将来的にPackageManagerが使えるようになれば、他の人がPackageをインポートすることが出来るようになるはずです。パッケージを指定してインポートする
新しくpackageをインポートするときは
Client.Add
を使います。Addの返り値はAddRequestです。
これを実行するとプログレスバーが表示され、通常の方法でのpackageのinstallと同じようにインポートされます。
https://docs.unity3d.com/ja/2017.4/ScriptReference/PackageManager.Client.Add.html
Listと同様非同期で処理されます。引数のpackageIdOrNameは、nam(@の前)だけを指定すると最新版をインポートしてくれるようになっています。packageIDによってバージョン(@の後ろ)まで指定すれば、古いバージョンのpackageをインポートしてくれます。
↓サンプルコード(適当なpackageをインポートする)同様にメニューバーにAddがあるはずなので押してみます。今回はPostProcessingを直打ちして最新版をインストールするようにしています。しばらく経過すると下記のようなプログレスバーが出てきて、インポートの進捗を伝えてくれます。
更に暫くすれば、AddRequest.StatusがSuccessとなり、以下のようなログが出力されます。
Packageを見るとちゃんとPostProcessingがインストールされていることが分かります。
既にインストールされているpackageについては何も起こりません。(特に処理が行われず、Succeededとなる)
unity packageをインポートした直後にPackageManagerを呼びたい
[InitializeOnLoad]
アトリビュートを付けることによって、UnityEditor起動時とそのクラス自身のコンパイル時にクラスのコンストラクタが走るようになります。
ここに先ほどの処理を書けばunitypackageインポート直後にpackageをインストールすることが出来ます(UnityPackageとPackageManagerのpackageで紛らわしいですね…)
ただし、コンストラクタ自身はasyncを付けることが出来ないため、コンストラクタ内で別の非同期メソッドを走らせるようにしましょう。おわりに
他のIDEにおけるNugetのような役割を果たしているPackageManagerですが、現在はpublicなrepositoryをjson直書きで登録できるみたいですが、そのうち民主化されて楽にPackageが公開されるようになるといいですね。
個人的にはPackageで追加されるアセット類はgitで共有せずに済むところが気に入っています。
- 投稿日:2019-11-24T00:09:30+09:00
円形のゲージを自力で作る
はじめに
この記事は2013年頃に、uGUIもなく、NGUIにお金を出すのも厳しかった時に、どうにかして円形のゲージを作りたくて作ったプログラムを、投稿用に手直しをしたものになります。
現在のUnityではuGUIでお手軽に実装できるのでそちらを使ったほうが確実に良いです。
UIではなく3D空間に表示したいだとか、Unityじゃない他のプログラムで実装するのには少し役立つかもしれません。丸い画像はいらすとやさんからお借りしました。
お世話になってます。
丸のマークのイラスト「○」実装
CircleGauge.csusing UnityEngine; [RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))] [ExecuteInEditMode] public class CircleGauge : MonoBehaviour { private const float TWO_PI = Mathf.PI * 2; private static readonly int[] Triangles = new int[] { 4,3,5, 5,3,0, 3,2,0, 0,2,1, 5,0,6, 6,0,7, 0,9,7, 7,9,8, }; private static readonly int[] TrianglesClockwise = new int[] { 2,3,1, 1,3,0, 3,4,0, 0,4,5, 9,0,8, 8,0,7, 0,5,7, 7,5,6, }; private enum StartPosition { Right = 0, Top, Left, Bottom, } private Mesh _mesh = null; private Vector3[] _vertices = null; private Vector2[] _uv = null; [SerializeField, Range(0.0f, 1.0f)] private float _value = 0f; [SerializeField] private StartPosition _startPosition = StartPosition.Right; [SerializeField] private bool _clockwise = false; [SerializeField] private Texture2D _texture = null; [SerializeField] private bool _isUpdate = false; void Start() { CreateMesh(); } void Update() { if (_isUpdate) { _value += Time.deltaTime / 5; if (_value > 1) { _value = _value - Mathf.Floor(_value); } } UpdateMesh(); } /// <summary> /// Mesh情報更新 /// </summary> private void UpdateMesh() { for (int i = 0; i < _vertices.Length; i++) { if (i != 0) { var val = Mathf.Clamp(_value, 0, 0.125f * (i - 1)); var rad = val * TWO_PI * (_clockwise ? -1 : 1) + Mathf.PI * ((int)_startPosition * 0.5f); // normalized rad rad = rad - TWO_PI * (int)(rad / TWO_PI); if (rad < 0.0f) { rad += TWO_PI; } // rad in top if (ValueInRange(rad, Mathf.PI * 0.25f, Mathf.PI * 0.75f)) { _vertices[i].y = 0.5f; _vertices[i].x = _vertices[i].y / Mathf.Tan(rad); } // rad in left else if (ValueInRange(rad, Mathf.PI * 0.75f, Mathf.PI * 1.25f)) { _vertices[i].x = -0.5f; _vertices[i].y = Mathf.Tan(rad) * _vertices[i].x; } // rad in bottom else if (ValueInRange(rad, Mathf.PI * 1.25f, Mathf.PI * 1.75f)) { _vertices[i].y = -0.5f; _vertices[i].x = _vertices[i].y / Mathf.Tan(rad); } // rad in right else { _vertices[i].x = 0.5f; _vertices[i].y = Mathf.Tan(rad) * _vertices[i].x; } } _uv[i].x = _vertices[i].x + 0.5f; _uv[i].y = _vertices[i].y + 0.5f; } _mesh.vertices = _vertices; _mesh.uv = _uv; _mesh.triangles = _clockwise ? TrianglesClockwise : Triangles; } /// <summary> /// 値がmin~maxの範囲内にあるかチェック。 /// value が min, max と同じ値の場合もtrue。 /// </summary> private bool ValueInRange(float value, float min, float max) { return min <= value && value <= max; } /// <summary> /// 描画用Mesh生成 /// </summary> [ContextMenu("Reset Mesh")] private void CreateMesh() { var renderer = gameObject.GetComponent<MeshRenderer>(); var meshFilter = gameObject.GetComponent<MeshFilter>(); int length = 10; _vertices = new Vector3[length]; _uv = new Vector2[length]; var material = new Material(Shader.Find("Mobile/Particles/Alpha Blended")) { name = "material" }; material.SetTexture("_MainTex", _texture); _mesh = meshFilter.sharedMesh = new Mesh(); renderer.sharedMaterial = material; UpdateMesh(); } }解説
考え方
まず円形の画像を円形に表示したい場合はどうしたら良いかと考えたときに、中心点と円周上を360に分割した点でポリゴンを作れば良いかとも考えました。
ただ、少し重そうな気がしたのと、そのポリゴンに収まる余白のある元画像を作る必要がありそうだったのでもっと簡素化できないかと考えたときに、画像は四角形に描画されるので、四角形の外周を円運動と同じように角度によって等速で移動できないかと考えました。
上記のプログラムはそれをその通りに実装したものです。四角形の外周を角度によって移動するには
※四角形のサイズは縦横1(0を中心としたxy共に-0.5~0.5の範囲)とします。
※角度は正規化されている(0~360°内にある)ものとします。角度によってx,yのどちらかの値が決まる
まずは現在の角度によって、xまたyのどちらかの値が確定します。
- 角度が0°~45°、または225°~360°の時 : xは右辺上(0.5)に固定
- 角度が45°~135°の時 : yは常に上辺上(0.5)に固定
- 角度が135°~225°の時 : xは常に左辺上(-0.5)に固定
- 角度が225°~315°の時 : yは常に下辺上(-0.5)に固定
確定したxまたはyの値から、確定していないほうのx,yの値を算出
これにはTangentを用います。
覚えていますか?Tangent。
45°、135°、225°、315°に近いほど1または-1に近づき、水平に近いほど0、垂直に近いほど∞に近づきます。
式はtanθ = y / x
です。
つまり
x が確定している場合 :y = tanθ * x
y が確定している場合 :x = y / tanθ
という風に確定した値から確定しいない方を算出します。
これで角度によって四角形のどの外周上にいるか算出できました。Meshの作成と操作
必須コンポーネント
MeshRendererやMeshFilterなどのコンポーネントを用いて、自前でMeshの操作を行います。
[RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]で描画に必要なコンポーネントが必ず付随するようにします。
Meshの作成
verticesとuvは、中心点+外周を移動する9つの点の計10点で構成します。
位置は後々計算で算出されるので初期化時はすべてVector3.zero(Vector2.zero)で大丈夫です。
こんなイメージです。
※振っている番号にも意味があります。
verticesの各点の位置
※右側を開始点とした場合の説明になります。
四角形の外周上の位置を角度によって決めることができましたが、次はそれをverticesの各点に反映します。
中心点は必ず(0,0)なので計算はスキップします。
それ以外の点ですが、入力された角度が何度であろうと各点の範囲は決まっているので、各点の計算時に角度をその点の最大値に制限する必要があります。
①の点は必ず0°、②の点は0°~45°、③の点は0°~90°... といった具合です。
それがvar val = Mathf.Clamp(_value, 0, 0.125f * (i - 1));の部分です。
入力されるゲージの値_value
の値を制限することで、自動的に角度についても値が制限されることになります。uvの各点の位置
uvについてはverticesの同じindexのx,y値に、それぞれ0.5をプラスした値が0~1になるのでそれで大丈夫です。
_uv[i].x = _vertices[i].x + 0.5f; _uv[i].y = _vertices[i].y + 0.5f;もしTexture全体ではなく、特定の範囲を使いたい等といった場合には、この値に範囲を掛け合わせて計算すればいけるはずです。
triangles
trianglesはカメラ側から見た際に、各ポリゴンが時計回りになるようになるように設定されていれば順番は特に関係ありません。
とりあえず左上から1枚ずつ三角形を構成するような作りにしています。
このコンポーネントでは反時計回り、時計回りを選べるようにしていますが、verticesの計算上、同じTrianglesを使ってしまうとゲージを時計回りにしたときにポリゴンが反時計回り(裏向き)になってしまうので、別々のTrianglesを用意しています。
その他余談的なもの
isUpdateフラグについて
_isUpdateのチェックはデバッグ用なので実際に使うときは消してください。
角度の正規化
rad = rad - TWO_PI * Mathf.Floor(rad / TWO_PI);とすれば1行で書けますがほんの少しだけ処理速度が劣ります。
と言っても誤差程度なのでシビアな状況じゃなければこちらの方がスマートだと思います。コメント
// rad in right
がif分の最後の分岐になっている理由角度が右辺の範囲にあるかどうかの判定ですが、これをifで記述すると
if(ValueInRange(rad, Mathf.PI * 0, Mathf.PI * 0.25f) || ValueInRange(rad, Mathf.PI * 1.75f, Mathf.PI * 2.0f))のようにこれだけ2回チェックになってしまうので意図的にelseにしてます。
最後に
最初にも書きましたが、UIとして使いたい場合はuGUIなどを使った方が良いです。
そちらだと360°以外にも180°、90°、Horizontal、Verticalなど色々なモードが選べます。