- 投稿日:2022-02-24T19:49:12+09:00
テクスチャ配布コーナー
お疲れ様です。エンジニアインターンの松本です。 今回は、私が開発言語Processingで作成した著作権フリーのテクスチャを配布します。何か機会があれば使ってください。 0.その前にProcessingとは Processingは、Javaベースで動作している開発言語で、非常に簡単なプログラムで描画ができるツールです。扱いの簡単さから、某M大学のある学部の必修科目にも取り入れられています。 ダウンロード無料 https://processing.org/download Javaベースということもあり、非常にC#と扱い方は似ています。 Sample.cs public class Sample { void Start(){ //開始時1回のみ実行 } void Update(){ //毎フレーム実行 } } Sample.pde void setup(){ //開始時に1回のみ実行 } void draw(){ //毎フレーム実行 } Processingでは、メインクラスは用意する必要がありません。加えて、必要ならC#のようなクラスを作成してインスタンスを作成することもできます。 描画系では、 line(始点x, 始点y, 終点x, 終点y); circle(中心点x, 中心点y, 直径); fill(r, g, b); //circleなどの中を塗りつぶす、こちらを先に書く必要あり stroke(r, g, b); //先に書く必要 strokeWeight(太さ); //先に書く必要 などがあります。とても直感的ですね。 透過png対応のサンプルを作ったので、以下に記述しておきます。 DrawTransparentImage.pde //透過pngを作成するプログラム PGraphics gr; String name="sample";//ここに保存する画像のファイル名(拡張子抜き)を記入 void setup() { size(1, 1);//こっちは画像サイズに関係ない background(255, 255, 255, 0);//ここも画像の色に関係ない gr=createGraphics(300, 300, JAVA2D);//ここの値で画像サイズを調整 gr.beginDraw(); //<Write program below this border>ーーーーーーーーーーーーーーーーーーーーーーーーー switch(name) { case "sample"://最初に指定した画像ファイルの名前を指定 makeSample();//メソッドに切り出したが、lineなどを直接書き込むのもアリ break; case "": break; } //<Write program above this border>ーーーーーーーーーーーーーーーーーーーーーーーーーーーーー gr.endDraw(); gr.save(name+".png"); } //逐一gr.を書くのを忘れないように!変数自体の定義には不要。 void makeSample() { gr.fill(255,0,0); gr.stroke(0,255,0); gr.strokeWeight(15); gr.circle(150,150,120); gr.fill(#0000ff);//16進数もOK gr.stroke(50);//一つの数字なら(50,50,50)のようなrgb全て同じの略記になる gr.strokeWeight(3); gr.rect(20,20,40,100); int r=110; int g=20; int b=200; gr.fill(r,g,b); gr.noStroke(); gr.ellipse(200,250,150,90); } ちなみに上記のプログラムを実行すると、以下のような画像がpdeファイルのディレクトリに保存されます。 1.著作権フリー画像 以下、全部lineやcircleなどで幾何学的に描画しています。 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 ↑見えにくいですが、カウントマスターズの数字アイコンのような吹き出しです 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 ↑見えにくいですが、 >>> みたいなやつです 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 ↑見えにくいですが、リサイクルマークみたいなやつです 〜〜〜 ↑見えにくいですが、noAds的なマークです 〜〜〜 ↑見えにくいですが、歯車です。 〜〜〜 〜〜〜 ↑見えにくいですが、バイブレーションのイメージです 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 〜〜〜 2.まとめ いかがだったでしょうか。 実はsetupやdraw関数を抜いても動いてしまうなど、開発言語の中でも群を抜いてプログラムが簡単な言語です。 興味を持った方は、テクスチャ作りにProcessingを作成してみてください。
- 投稿日:2022-02-24T18:09:36+09:00
ML-Agents入門 4足歩行モデル
はじめに ※この記事はRICORA Advent Calendar 12/25 の記事になります。 本記事ではUnityの深層強化学習パッケージのML-Agentsによる4足歩行モデルの実装を行います。 ML-Agents完全初心者の方はML-Agents入門から読むことをおススメします。ML-Agentsのインストールから簡単なモデル作成まで行っています。 ここではモデルと環境の作成方法を紹介した後に、4足歩行モデルを実現するための報酬設計の考え方やトレーニングの高速化について主に紹介します。 モデルと環境の準備 モデルと環境をどのように作成したかについて説明します。 モデルの作成 以下のような4足2関節のモデルを作成しました。 今回はこのモデルに歩行を学習させるのがメインタスクとなります。具体的にはエージェントが目標に到達するというタスクを立てて、これを学習させていきます。 このモデルは次のような親子関係の3Dオブジェクトからなっています。 ここでBugAgentは空の3Dオブジェクトであり、Behavior ParametersとDecision RequesterをComponentとして付けます。 bodyは緑色のCapsule、l1,l2,r1,r2は赤色のSphere、各legは青色のCapsuleになります。このように親子関係を組むことで各部位が上位のパーツの動きを反映できるようになります。親子関係が無いと体が動いているのに足がついていかない、といった関連性のない挙動になってしまいます。 l1,l2,r1,r2と各legにはHinge Jointという動きに制限を掛けるComponentを付けています。今回は強化学習にこのComponentを操作させてモデルに歩行を学習させます。Connected Bodyに一つ上のオブジェクトを選択しましょう。またHingeの方向にも注意です。 色々なものが見えてますが今は気にしないでください(笑) Hinge JointのLimitsのmin maxはともに0にしておきましょう。上の写真のような操作を行うとオブジェクトがどの方向に回転できるかを示す円が出てきます。今回は回転方向が地面に垂直になるように調整しました。角度の調整はAxisの値を調整することでできます。 またBugAgentを除くオブジェクトはRigidBodyを持っています。基本的な重さは1ですが飛び跳ね防止のためbodyのみ重さを2.5にしています。単純に体積の大きい部位ほど重さを増やすといった仕様にしても面白いと思います。時間がある人は色々試してみましょう。 環境の作成 加えて地面に接地している各legと地面のPlaneに摩擦を設定しました。Projectウィンドを右クリックしてCreateからPhysic Materialを選択すると摩擦に使えるマテリアルを取得することができます。 マテリアルを選択してDynamic Friction(動摩擦)とStatic Friction(静止摩擦)をそれぞれ1に設定しましょう。 物体にこれを適用するためにはデフォルトでついているMesh ColliderというComponentにこれを付与します。Mesh CollierのMaterialのところにドラッグ&ドロップしてください。上で説明したように4本のlegとfloor(床のplane)にそれぞれ同じ操作を行います。 歩行の目標にするTragetをfloor上に配置します。今回はBugAgentとTargetの距離を20mにしてあります。 空のGame Objectを生成してそこにBugAreaという名前を付けました。次の階層構造になるように作ったオブジェクトが配置されるようにしましょう。 環境のプレハブ化 ここまでに作成した環境のプレハブ化というものを行います。プレハブ化を利用することで環境の量産と同時変更を行うことができるようになります。後に高速化を行うためのステップとなります。 今作成したBugAgentをProjectウィンドにドラッグ&ドロップします。画像ではBugAgentや諸々が水色の文字で表示されていますが、プレハブ化前は白色になっているはずです。プレハブ化ができると名前が水色になります。 Projectウィンド内にプレハブ化されたBugAreaが表示されているのでダブルクリックしてプレハブの作業エリアに移動してみましょう。 画像のような背景が青い空間に移動したと思います。ここでプレハブの編集を行うことができます。モデルや環境を変更したいときは個別のオブジェクトではなく、このプレハブから編集するようにしましょう。プレハブでコピーしたオブジェクト全体に同様の変更を適用することができます。 ML-Agent用コンポネントの設定 環境の最後の設定としてML-Agentsに必要なComponentの設定を行いましょう。 BugAgentにBehavior ParametersとScriptとDecision Requesterを付けます。スクリプトの名前はBugAgentとしました。 Bug Agent(Script)のTargetの部分は実際にプログラムを書くと出てくるので今は気にしないでください。プログラムを書いたらTargetのところに設置しているtargetオブジェクトをドラッグ&ドロップしてください。それぞれのComponentの数値は画像と同じ値にセットしましょう。 これで環境の準備が整いました。次はプログラムです。 スクリプトと報酬設計の考え方 BugAgent.csの中身を見ながらどのように報酬設計を行ったか解説します。 BugAgent.cs using System.Collections.Generic; using System; using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; public class BugAgent : Agent { public Transform target; private GameObject body; private GameObject[] thighs = new GameObject[4]; private GameObject[] legs = new GameObject[4]; private HingeJoint[] thighs_hinge = new HingeJoint[4]; private HingeJoint[] legs_hinge = new HingeJoint[4]; private JointMotor[] thighs_motor = new JointMotor[4]; private JointMotor[] legs_motor = new JointMotor[4]; private int phase = 0; private float rotate_phase = 1.0f; public override void Initialize(){ this.body = this.transform.Find("body").gameObject; this.thighs[0] = this.body.transform.Find("l1").gameObject; this.thighs[1] = this.body.transform.Find("l2").gameObject; this.thighs[2] = this.body.transform.Find("r1").gameObject; this.thighs[3] = this.body.transform.Find("r2").gameObject; int limit = 30; for(int i=0;i<4;i++){ this.legs[i] = this.thighs[i].transform.Find("leg").gameObject; this.thighs_hinge[i] = thighs[i].GetComponent<HingeJoint>(); this.legs_hinge[i] = legs[i].GetComponent<HingeJoint>(); this.thighs_motor[i] = this.thighs_hinge[i].motor; this.legs_motor[i] = this.legs_hinge[i].motor; JointLimits thigh_limits = thighs_hinge[i].limits; JointLimits leg_limits = legs_hinge[i].limits; thigh_limits.min = -limit; thigh_limits.max = limit; thigh_limits.bounciness = 0; thigh_limits.bounceMinVelocity = 0; thighs_hinge[i].limits = thigh_limits; thighs_hinge[i].useLimits = true; leg_limits.min = -limit; leg_limits.max = limit; leg_limits.bounciness = 0; leg_limits.bounceMinVelocity = 0; legs_hinge[i].limits = leg_limits; legs_hinge[i].useLimits = true; } } public override void OnEpisodeBegin(){ this.body.transform.localPosition = new Vector3(0,1.3f,-10); this.body.transform.localRotation = Quaternion.Euler(90, 0, 0); for(int i=0;i<4;i++){ this.thighs[i].transform.localRotation = Quaternion.Euler(-90, 0, 0); this.legs[i].transform.localRotation = Quaternion.Euler(0,0,0); this.thighs_motor[i].targetVelocity = 0; this.thighs_hinge[i].motor = this.thighs_motor[i]; this.legs_motor[i].targetVelocity = 0; this.legs_hinge[i].motor = this.legs_motor[i]; } this.phase = 0; this.rotate_phase = 1.0f; target.localPosition = new Vector3(0, 1.0f, 10.0f); } public override void CollectObservations(VectorSensor sensor){ sensor.AddObservation(target.localPosition); sensor.AddObservation(this.body.transform.localPosition); sensor.AddObservation(this.body.transform.localRotation); sensor.AddObservation(this.body.transform.up.z); sensor.AddObservation(this.body.transform.forward.y); for(int i=0;i<4;i++){ sensor.AddObservation(this.thighs[i].transform.localRotation.x); sensor.AddObservation(this.legs[i].transform.localRotation.x); } } public override void OnActionReceived(float[] vectorAction){ float force = 1500.0f; for(int i=0;i<4;i++){ this.thighs_motor[i].targetVelocity = vectorAction[i]*force; this.legs_motor[i].targetVelocity = vectorAction[i+4]*force; this.thighs_hinge[i].motor = this.thighs_motor[i]; this.legs_hinge[i].motor = this.legs_motor[i]; } AddReward(-0.001f); float distanceToTarget = Vector3.Distance(this.body.transform.localPosition, target.localPosition); if (distanceToTarget < 18.0f - this.phase && this.phase < 15){ AddReward(0.05f); this.phase += 1; } if (distanceToTarget < 2.0f){ AddReward(2.0f); EndEpisode(); } Vector3 rotate_angle = this.body.transform.localRotation.eulerAngles; float rotation = this.body.transform.up.z; bool is_rotate = rotation < Math.Cos(Math.PI/6.0f * rotate_phase); bool is_flip_z = this.body.transform.forward.y > 0; if (this.body.transform.localPosition.y < 0.0f || is_flip_z || rotation < Math.Cos(Math.PI/6.0f * 3.0f)){ EndEpisode(); } if(is_rotate && rotate_phase < 2){ AddReward(-0.1f); rotate_phase += 1.0f; } } public override void Heuristic(float[] actionsOut){ actionsOut[0] = Input.GetAxis("Horizontal"); actionsOut[1] = Input.GetAxis("Vertical"); } public void Update(){ } } UnityとML-Agentのスクリプトの書き方については長くなるので省略させてもらいます。 ここではOnActionRecived関数について説明していきます。この関数内では主に報酬の設定をAddReward関数の呼び出しで行っていて、全部で4か所あることが分かると思います。 それぞれのAddRewardの意味 最初の報酬は微量のマイナス報酬で、エージェントが行動をするたびに発生します。これはエージェントにできるだけ早くゴールすることを促すための報酬で、早くゴールした個体の方がマイナス報酬の累積が少なくなるのでより高い評価を受けることになります。これによって最適な移動を学習することができます。 次の報酬は少量のプラス報酬で、エージェントがターゲットと一定距離近づく度に発生します。具体的にはアメフトのフィールドを想像すると分かりやすいと思います。ここではターゲットに1m近づく度にプラス報酬をもらえるようにしました。これはエージェントのスムーズな学習を促すためのものです。 次にメインのゴールに対するプラス報酬です。これはエージェントがターゲットに到着した際に発生する報酬でそれと同時に環境のリセットが入ります。 最後は回転に対する少量のマイナス報酬です。これはエージェントが上から見て15度回転するたびに発生するマイナス報酬で15度と30度の2回発生します。方式は1つ目の距離報酬と同じです。これによってエージェントの不要な回転を抑制し真っすぐ歩くことを促します。 報酬設計の推移 4つの報酬について上で説明しました。なぜこのようになったのか詳細について説明します。 まず最初は3のエージェントがターゲットにゴールすることのみに報酬を設けていました。しかしこれだけでとそもそもゴールすることがなかなかできないので学習の進みが非常に悪くなります。そこで2の報酬を設定しました。 次に発生した問題はエージェントの移動経路です。2の報酬によってゴールができるようになりましたがこれだとどのような経路でもゴールすれば同じ報酬がもらえるため、最適でない経路や行動が発生していました。そこで1の微量なマイナス報酬を設定することでこれの最適化を試みました。マイナス報酬は設計が難しく値をあまり大きくしてしまうと、すぐにひっくり返って終了させるなどのマイナスを避ける望ましくない動きが発生してしまいます。この報酬によってかなりゴールまでの時間が短縮されました。 最後に発生していたのがエージェントが途中で180度旋回しながらゴールを目指していたというものです。4足歩行のエージェントにとって最も困難な問題は左右のバランスを取ることだったようで、実験を通して先ほどの微量なマイナス報酬の調整だけではこの問題を解決できないと気づきました。そこで直接的に旋回に対してマイナス報酬を設けることにしましたがこれがとても難しかったです。マイナスが大きいと旋回を避けるのがよほど難しいのか、スタート地点から一切動かないといった結果に何回もたどり着いてしまいました。最終的に4の形に落ち着いてこれによってできるだけ真っすぐとゴールを目指す最適な動きを生成することができました。 トレーニングの設定 トレーニングの設定は以下のようになっています。詳しくは前回の記事を参照してください。 BugAgent.yaml behaviors: BugAgent: trainer_type: ppo hyperparameters: batch_size: 10 buffer_size: 100 learning_rate: 0.0003 beta: 0.005 epsilon: 0.2 lambd: 0.95 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: true hidden_units: 128 num_leyers: 3 vis_encode_type: simple reward_signals: extrinsic: gamma: 0.99 strength: 1.0 keep_checkpoints: 5 checkpoint_interval: 100000 max_steps: 30000000 time_horizon: 64 summary_freq: 5000 threaded: true トレーニングの高速化 今回のタスクは前回のタスクと比べると格段に内容が難しくなっています。そのためある程度トレーニングを高速化する必要があります。ここでは最も簡単なプレハブから環境を量産する方法を紹介します。 環境の作成の際にプレハブ化を行いました。プレハブ化されたオブジェクトはSceneウィンドにドラッグ&ドロップすることでコピーすることができます。 自分は画像のように15個の環境をコピーして同時に学習をさせました。これによってモデルの収束を早めることができます。 特に操作はなくトレーニングを実行すればすべての環境で並列して学習が行われます。これ以外にもビルドしてアプリ化することでトレーニングを高速化することもできますが今回は使わなかったので紹介しません。気になる人は公式のドキュメントを参照してみてください。 トレーニング結果 以下がトレーニング結果になります。そこそこ4足歩行と言える動きを作れていると思います! 足の動かす順番が少しおかしかったり若干跳ねたりしているのでまだ少し改良の余地がありそうです。 自分は力尽きたのでやる気のある人は是非トライしてみてください! まとめ この記事では4足歩行のモデルをUnity ML-Agents上で学習する方法を紹介してみました。強化学習を3Dモデルで行いたいと考えている人の役に少しでもたてたら嬉しいです。 気づいていたかもしれませんが、この記事はすべての作業が完了してから書いたものなのでどこかで手順が抜け落ちていたりするかもしれません。そのようなことがあればコメントなどで教えていただけると幸いです。 ここまで読んでいただきありがとうございました。楽しいML-Agentsライフをお送りください! 参考 ML-Agents公式GitHub Unity ML-Agents 実践ゲームプログラミング v1.1対応版 (Unityではじめる機械学習・強化学習)
- 投稿日:2022-02-24T13:29:43+09:00
【Unity】AR FoundationのARCameraBackgroundでCustom Materialを使う
UnityのAR Foundationでカメラから取得した映像を背面に描画するためにARCameraBackgroundコンポーネントを使用します。この記事では、ARCameraBackgroundコンポーネントでCustom Materialを設定し、カメラから取得した映像にエフェクトをかける方法について解説します。 この記事の内容はURP、ARKit(iOS)のみで検証しています。使用しているUnityおよびパッケージのバージョンは以下のようになっています。 Unity: 2021.2.10f1 Universal RP: 12.1.4 AR Foundation: 4.2.2 ARKit XR Plugin: 4.2.2 まず、Custom Materialに使用するシェーダーを作成します。/Packages/ARKit XR Plugin/Assets/Shaders/ARKitBackground.shaderを雛形として使用するので、これをAssetディレクトリ以下の適当な箇所にコピーします。 コピーしたシェーダーを編集してエフェクトを加えます。ここでは、カメラ画像の色を反転させるようにしています。 ARKitBackground.shaderの194行目あたり fragment_output o; // o.color = videoColor; o.color = real4(1.0 - videoColor.rgb, videoColor.a); o.depth = depthValue; return o; ビルドする際にエラーとなるので、次のようにしてLWRP向けのシェーダーバリアントが作成されないようにします。 ARKitBackground.shaderの38行目 // #pragma multi_compile_local __ ARKIT_BACKGROUND_URP ARKIT_BACKGROUND_LWRP #pragma multi_compile_local __ ARKIT_BACKGROUND_URP このシェーダーを使用するマテリアルを作成します。ARCameraBackgroundコンポーネントのUse Custom Materialプロパティを有効化し、Custom Materialプロパティに作成したマテリアルを設定します。 ビルドして実機で確認するとカメラ映像の色が反転することが確認できます。
- 投稿日:2022-02-24T11:49:01+09:00
Terrainの地形を動的に自動生成する
Terrainの地形の保存先 Terrainの地形のデータはTerrainDataの中のheightmapにfloatの2次元配列で保存されている。 heightmapは地形の高さを表すテクスチャのようなものであり、要素の中には0.0f~1.0fの値が入っています。 この値はTerrainの高さの割合で、例えばTerrainの高さが5.0fの時、要素の値が0.5fだった場合高さは2.5fとなる。 今回は、このheightmapを変更して地形を作っていきます。 活用する変数とメソッド TerrainDataを取得する Terrain terrain = GetComponent<Terrain>(); TerrainData terrainData = terrain.terrainData; Terrainの大きさを変更する terrainData.size = new Vector3(x, height, z); heightmapの解像度を取得する int heightmapSize = terrainData.heightmapResolution; //地形の大きさ heightmapを渡す //地形のデータを渡す terrainData.SetHeights(0, 0, heightmap); サンプルプログラム パーリンノイズを利用した地形を生成するサンプルです。 インスペクターの生成か、Generate()を呼ぶことで生成できます。 TerrainGenerator.cs using System.Collections; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [RequireComponent(typeof(Terrain))] public class TerrainGenerator : MonoBehaviour { [SerializeField] Vector2Int terrainSize; //Terrainのサイズ [SerializeField] float height; //Terrainの高さ [SerializeField] float relief; //地形の滑らかさ float seedX, seedY; public void Generate() { //Terrain関連のコンポーネントを取得する Terrain terrain = GetComponent<Terrain>(); TerrainCollider terrainCollider = GetComponent<TerrainCollider>(); TerrainData terrainData = terrain.terrainData; //シード値を設定する seedX = Random.value * 100f; seedY = Random.value * 100f; //Terrainのサイズを変更する terrainData.size = new Vector3(terrainSize.x, height, terrainSize.y); //地形に関する変数を用意する int heightmapSize = terrainData.heightmapResolution; //地形の大きさ float[,] heightmap = new float[heightmapSize, heightmapSize]; //地形のデータ(0 ~ 1) //地形を変更する for(int y = 0; y < heightmapSize; y++) { for(int x = 0; x < heightmapSize; x++) { float sampleX = seedX + x / relief; float sampleY = seedY + y / relief; float noise = Mathf.PerlinNoise(sampleX, sampleY); heightmap[x, y] = noise; } } //地形のデータを渡す terrainData.SetHeights(0, 0, heightmap); //作ったTerrainDataを渡す terrain.terrainData = terrainData; terrainCollider.terrainData = terrainData; } } /// <summary> /// インスペクターに「生成」のボタンを作る /// </summary> #if UNITY_EDITOR [CanEditMultipleObjects] [CustomEditor(typeof(TerrainGenerator))] public class TerrainGeneratorEditor : Editor { public override void OnInspectorGUI() { TerrainGenerator terrainGenerator = target as TerrainGenerator; base.OnInspectorGUI(); EditorGUILayout.Space(); if (GUILayout.Button("生成")) { terrainGenerator.Generate(); } } } #endif
- 投稿日:2022-02-24T11:45:54+09:00
Unityのゲーム実行時UIはまだまだuGUIかなって思った/エディタ拡張はUI Toolkit(UIElements)になりそうだけど
TL; DR 情報も少ないし複雑で分かりづらいので、ゲーム内UIでは無理にUIElements(UI Toolkit)使わないでuGUI使っていけばいいと思う。UI Toolkitでできないことはまだまだたくさんあるし。 情報源 https://docs.unity3d.com/Manual/UIElements.html https://docs.unity3d.com/Manual/UI-system-compare.html 要点 https://docs.unity3d.com/Manual/UI-system-compare.html これを見れば分かる通り、 UI Toolkit ではまだまだ Planned となっている未実装機能がたくさんあります。つまり uGUI の UI 実装をそのまま移植するのには機能が足りていません。(だからこそ uGUI と共存できる、と UI Toolkit の説明に書いてあります) ゲーム実行時のUIというのは、(プロジェクトや文化にも依りますが)コードに沿った上品なUIの振る舞いを超えて、時々無茶な実装を求められることがあり、そういう例外的なワガママに応えようと思ったときにはどうしてもプログラム(スクリプト)であれこれいじくり回す必要が出てきます。 そういうエクストリーム実装に対してはまだまだ知見も安定性も足りてないと思うので、ゲーム中で使われるUIはまだまだuGUIで作っていくのが幸せなのかな、という感想です。 一方、エディタ拡張はそんな極端な動きを求められることは少なく、一定のパターンに沿った実装であるべきと思うので、こちらの実装はUI Toolkit化させていくように色々学んでいくところかな、と思います。
- 投稿日:2022-02-24T11:06:04+09:00
UnityでGitHubを使用していて、Sceneファイルでコンフリクトが起こった時の対処法
この記事について 身内向けの記事です。制作過程でどうしてもUnityのSceneファイルのコンフリクトを解消してマージしなくてはいけなくなったため、そのときのやり方をメモしておきます。筆者はGit初心者です。 あくまで身内向けなので、細かい手順、注意点、自分はどうしたかなどを細かく書いています。 必要なツールの導入 ①UnityYAMLMergeをマージツールに設定する UnityYAMLMergeという公式のツールを使えば上手くマージできるらしい。 の記事を参考にして設定を行った。 ローカルリポジトリ(自分のPC内にあるプロジェクトフォルダ)を開く。.git/configファイルをテキストエディタで開き、末尾に以下の文章をコピペする。(上記の記事より引用) .gitフォルダは隠しフォルダなので表示設定で見えるようにしておくこと。 [merge] tool = unityyamlmerge [mergetool "unityyamlmerge"] trustExitCode = false cmd = "/Applications/Unity/Unity.app/Contents/Tools/UnityYAMLMerge" merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED" ここから、"/Applications/.../UnityYAMLMerge"の部分を自分のUnityYAMLMergeのパスに変更する。 自分は"C:/Program Files/Unity/Hub/Editor/2020.3.23f1/Editor/Data/Tools/UnityYAMLMerge"になった。 Unityのバージョンごとに複数のUnityYAMLMergeが存在するので、適切なバージョン(今回は2020.3.23f1)を選択すること。 *パスを指定する上で、フォルダの区切りは必ず「/」であること。「\」や「¥」ではエラーになる。 ②FallBackツールの設定 FallBackツールとは、自動でマージしきれなかった分を手動でマージするときに使うツールである。これも設定する。 p4mergeのインストール p4mergeは代表的なFallBackツールの一つらしい。自分はこれをインストールすることにした。 p4mergeの公式サイトからダウンロードできる。ダウンロードするときにOSとプラットフォームとバージョンを聞かれるので、自分はWindows,x64,EXEを選択した。その後名前やメールアドレスなどの記入欄が出てくるが、「Skip Registration」でスキップできる。 autoファイルの作成 リポジトリルートディレクトリにautoという名前のファイルを作成する。リポジトリルートディレクトリとはリポジトリの一番上のフォルダのこと。.gitがあるところと同じ階層である。そこにautoファイルを作成する。コマンドラインで対象のフォルダに行き touch auto のコマンドを実行すれば作れる。 autoファイルをテキストエディタで開き、 * use "C:/Program Files/Perforce/p4merge.exe" "%b" "%l" "%r" "%d" をコピペする。この文も上記の記事を参考にした。"C:/Program Files/Perforce/p4merge.exe"の部分は自分のp4mergeのパスを入れること。 使用方法 ①プルリクエストをローカルに持ってくる GitHub公式の記事を参考に行った。(OpenWithドロップダウンが見つからなかったので下の方のやり方でやっている) ・上記の記事通りに、コンフリクトしているプルリクエストのページを開き、IDを確認する。 ・GitBushで専用のブランチを作成する。例えばIDが21でブランチの名前をConfFixにする場合は git fetch origin pull/21/head:ConfFix を実行する。 *ここのブランチの名前は適当でいいが、新しいものにすること。既に存在するブランチ名を使ってはいけない。 UnityYAMLMergeを使ってマージする ・さっき作ったブランチに切り替える。さっきの例なら、 git checkout ConfFix を実行する。 ・そのブランチと目的のブランチをマージする。今回はmasterブランチなので、 git merge master を実行する。これでブランチがマージ中になるため、 git mergetool を実行し、UnityYAMLMergeに自動でマージしてもらう。 自動マージが成功した場合は Normal merge conflict for 'Assets/TestScene.unity': {local}: modified file {remote}: modified file Conflicts: Conflict handling: Assets/cube.unity seems unchanged. Was the merge successful? [y/n] (UnityYAMLMergeの記事より引用)のような表示が出るため、問題なさそうならyを押す。 自動マージが成功しなかったらp4mergeが起動して手動でマージすることになるが、大半がマージされた後なので負担は少ないと思う。 手動マージはp4mergeで保存してウィンドウを閉じたら終了する。手動マージ後は[y/n]の確認は出ない。 自分のときは、手動マージ時はm_rootOrderが4つコンフリクトしているだけだった。m_rootOrderは多分、UnityのSceneにおけるオブジェクトの並び順を決めるだけの値なので、全部masterの値を選択するか又は全部自分のブランチの値を選択するかしておけば問題は発生しないと思う。 コンフリクトが解消したら、そのブランチからプルリクエストを作成してmasterにマージできるようになる。
- 投稿日:2022-02-24T09:07:10+09:00
Unity2021でUnityWebRequestでエラーが出る
Unity2021.2.10f1にアップデートしたところ、 UnityWebRequestでエラーが出力されるようになった。 (Unity2020ではエラー出ていなかった) A Native Collection has not been disposed, resulting in a memory leak. Allocated from: Unity.Collections.NativeArray`1:.ctor(Byte[], Allocator) (at /Users/bokken/buildslave/unity/build/Runtime/Export/NativeArray/NativeArray.cs:69) UnityEngine.Networking.UploadHandlerRaw:.ctor(Byte[]) (at /Users/bokken/buildslave/unity/build/Modules/UnityWebRequest/Public/UploadHandler/UploadHandler.bindings.cs:95) どうやら使い終わった時に明示的に Dispose() を呼ばないといけないようだった。 UnityWebRequest request = new UnityWebRequest(url); ... 通信処理 // 通信終了後明示的にDisposeを呼び出す request.Dispose();
- 投稿日:2022-02-24T01:30:31+09:00
スクラッチの境界線をシェーダで色付けしてみる
続きは土日に書くといいましたが、いざ続きを書き始めるとノリノリで書ききってしまいました。 ↓前回はこちら。 さて、前回は「シェーダで色合成を行う利点と欠点」と「0からスクラッチの合成を踏まえたシェーダを用意する流れ」を記載しました。 前回の記事の内容までだと、単純な「アルファマップ別持ち」処理に留まっているので、せっかくなので境界線への色付け方法も紹介します。 個人的にはこの辺りが一番シェーダ開発で楽しいポイントです。 更に1ステップアップで、境目に色を付けたい場合 前回のコーディングでスクラッチ表現の見た目を実装したい、というおおよその目的は達成されました。 ただ、それだけだとちょっと面白くない。 という事で「簡単に境目に色を付ける方法」も手順を紹介しておきます 境目とは何ぞや?を考える Pattern.pngを見直してみましょう よく見るとわかるのですが、こちらの画像、実は白黒2色ではなく、白と黒の間に微妙にグラデーションが入っているのですね。 つまり 黒(0) … 削った部分 黒でも白でもない部分(0 < color < 1) … 境目 白(1) … 削ってない部分 という考え方が出来るわけです。 つまり「0でも1でもない部分に色を流し込めば境目に色を塗れる!」という事です。 0でも1でもない部分を抽出する 簡単に思いつくならばif文です。 先ほどlerpで使ったのpatternCol.rの色を使って fixed4 col; if ( 0.0 < patternCol.r && patternCol.r < 1.0 ) { col = 境界線の色; } else { col = lerp(baseCol, overlayCol, patternCol.r); } とC#よろしく、分岐してしまえば簡単に対応できるでしょう。 ただ、前述した通りFragentシェーダはピクセル数分計算が走ってしまう = 512x512x1=262,144回のif文が発生してしまい(厳密にはコンパイラ挙動で違ったりもしますが)避けるべき事象となります。 ※ただ、最近はif文も結構禁忌ではなくなってきている。 ※今回のケースでは避けたいですが、SRPBatcher用に1シェーダで複数の見た目を制御する際には、下手にシェーダを分けるよりも分岐した方が良いケースもある。 という事で計算のみで0<color<1を抽出する計算の1例を紹介します カーブで考える 0~1のカーブを考えてみましょう シンプルな直線 y=x (縦y横x)です。 yを抽出後の値、xを元の値、と考えると y=xは前回のPattern.pngの色取得を引っ張って来ると、 fixed finalValue = patternCol.r; となります。 今回欲しいのは 0<color<1なので、patternCol.rが0の時に0。0超過かつ1未満の時に1、1の時に0という値が取得出来たら目的が達成できそうです。 カーブを加工する 正直ココからの計算は十人十色なのですが、自分がfrac()関数が好き!なので、frac()を使用する計算方法を紹介してみます。 frac()での加工 frac()は整数部を切り取り、少数部のみを繰り返す関数です。 つまり をy=frac(x)とすると というカーブに加工できます。 この時、xが1.0の時に少数部のみとなるので0。 つまり、xの値が0.9999999…まではyが0.999999…と比例していき、1の時に0になるギザ刃が出来上がるわけです。 で言うならば こんなカーブ。 早くも、xが0~0.99999…までは同値、1の時に0のカーブが出来上がりました。 ceil()での加工 次に使う ceil()関数は、小数点を繰り上げする関数です。 0は0のまま、0.0000001でも値が入ると1に繰り上げられます。 にy=ceil(x)とすると というカーブになります。 もう察しがついたでしょう、という事で目的の関数は y=ceil(frac(x)) となります をfrac(x)としてを更にceil()でくくってとなるわけです。 という事で 0、1は0 0、1以外は1 となる関数が出来ました! 早速y=ceil(frac(x))をシェーダに組み込みましょう! シェーダに組み込む 前回のシェーダ Scratch.shader Shader "Unlit/Scratch" { Properties { _MainTex ("Texture", 2D) = "white" {} _PatternTex ("Pattern", 2D) = "white" {} _OverlayTex ("Overlay", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _PatternTex; float4 _PatternTex_ST; sampler2D _OverlayTex; float4 _OverlayTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 baseCol = tex2D(_MainTex, i.uv); fixed4 patternCol = tex2D(_PatternTex, i.uv); fixed4 overlayCol = tex2D(_OverlayTex, i.uv); fixed4 col = lerp(baseCol, overlayCol, patternCol.r); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } } の、最終的に仕上がった色計算の後に、境界線の色を流し込んでみましょう fixed4 col = lerp(baseCol, overlayCol, patternCol.r); に、同じくlerp()で境界線の色を合成してみます fixed4 col = lerp(baseCol, overlayCol, patternCol.r); col = lerp(col, fixed4(1,0,0,1), ceil(frac(patternCol.r))); lerp()は前回の記事で紹介した通り lerp(valueが0の時の色、valueが1の時の色, value) なので ceil(frac(patternCol.r))が0ならcol(そのままの色)、1ならfixed(1,0,0,1)(赤)が入るような計算ですね。 さて、どういった見た目になるか… きったなっっ!なんと汚い境界線でしょう。 なぜこんなことになってしまったかというと、Pattern.pngに圧縮が掛かっているからです。 ceil(frac(x))はどんな微少の色変化も検知して繰り上げる計算なので圧縮の際のデータのチリが反応してしまっているのですね。 検証のためにPattern.pngの圧縮を非圧縮にしてみましょう。 ProjectツリービューからPattern.pngを選択し、FormatをRGBA32bitに変更 綺麗な境界線になりました!ただ、よく見ると、少しゴミが見えます。 これは、マッピング外(uv0~1以外)の繰り返し部分が侵食している場合に発生しているゴミですね。 という事でループ設定も切っておきます。 Pattern.pngのループ設定をRepeatからClampへ。 で、表示を見ると ゴミも無くなって、完璧に境界線が表示できました! 仕上げに 最後の仕上げとして、境界線の色をマテリアルから指定できるようにしておきましょう。 前回同様に、Propertiesと定義部を修正します Properties { _MainTex ("Texture", 2D) = "white" {} _PatternTex ("Pattern", 2D) = "white" {} _OverlayTex ("Overlay", 2D) = "white" {} _BorderColor ("Border", Color) = (1,1,1,1) } と sampler2D _MainTex; float4 _MainTex_ST; sampler2D _PatternTex; float4 _PatternTex_ST; sampler2D _OverlayTex; float4 _OverlayTex_ST; fixed4 _BorderColor;//←追加 そして、先ほど赤色固定していたfixed4(1,0,0,1)を_BorderColorに置き換えて col = lerp(col, _BorderColor, ceil(frac(patternCol.r))); で、出来上がったコードの完成形はこちら Scratch.shader Shader "Unlit/Scratch" { Properties { _MainTex ("Texture", 2D) = "white" {} _PatternTex ("Pattern", 2D) = "white" {} _OverlayTex ("Overlay", 2D) = "white" {} _BorderColor("Border", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _PatternTex; float4 _PatternTex_ST; sampler2D _OverlayTex; float4 _OverlayTex_ST; fixed4 _BorderColor; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 baseCol = tex2D(_MainTex, i.uv); fixed4 patternCol = tex2D(_PatternTex, i.uv); fixed4 overlayCol = tex2D(_OverlayTex, i.uv); fixed4 col = lerp(baseCol, overlayCol, patternCol.r); col = lerp(col, _BorderColor, ceil(frac(patternCol.r))); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } } マテリアルから境界線の色を変更する事が可能になりました! お疲れさまでした! ※BorderがEdgeという名前になっているのはスクショを撮ったタイミングの記載が違うからなのでお気になさらず。 最後に という事で、スクラッチ合成処理を行いつつ、くっきりとした境界線を描くシェーダでした。 個人的にはカーブを色々加工する辺りがとても好きなので、シンパシーを感じたならば是非色々な計算を実験してみて欲しいです。 「数学って美しい」という言葉をよく聞きますが、シェーダの演算についてはまさにそれを体現しているように思います。 さて、今回はパッキリした境界線を描く所で完了としましたが、 せっかく土台があるので、次回は圧縮が効いたままでも境界線をなじませるには?という記事が書けたら書こうかな、と思います。