20211202のUnityに関する記事は14件です。

コードをめんどくさがるな!

はじめに Unity幼稚園のじょにです? 今回は私が初めてプログラムを触った時にやらずに後で苦労したこと、 やっておけばよかったなと後悔したことを書こうと思います? 当時の学習スタイル いきなりですが、みなさんは面倒くさがりですか? 私はかなり怠惰な性格をしており、動画で学習している時でも 「ここは自分にはいらないから飛ばそう」 「写すの面倒だからとりあえず眺めとこう」 という学習方法をしていました。 わかりやすく例を出すと、「Debug.Logで確認する」なんかは 一切書かずに動画の中のログを眺めて確認していました。 だって見ながら写してるし、同じ結果になるでしょ? なんて思ってたわけなんです。 そんな学習方法で上達するわけないじゃん!って思いません? 私も今考えると酷いなって思います? 実際に困ったこと さて、その方法で私がどうなったかというと、 「Debug.Log」を書く癖がつかなくなってしまいました。 エラーが起こってもどこまで動画を遡れば良いのかわからない Debug.Logを書くという発想が自分の中に生まれないので どこまでコードが動いてるのか確認ができない こうなると何がいけないのか判断できないので 動画を見返す時間や手間、 他の人に質問しようと思ってもどこまで動いてるのか不明なので 答える側にとっても時間や手間がかかるという最悪のパターンになります。 Debug.Logの大切さ もしかしたらこの記事を読んでる方の中には そんなの当たり前じゃん!なんて思ってる人も多いかもしれません ただ、プログラミングを学び始めた頃は何もわからず 「Hello Worldを書くためのもの」という認識でした 大丈夫だろうと思ってるコードでもこまめにログを出して ちゃんと動いてるかどうかを確認するのは面倒です ただ、それをせずエラーを起こして対処する方がもっと面倒です どれだけ便利で大切なことなのか、 私の拙い日本語では伝わりにくいかもしれませんが 日頃からログを確認してる人にとってはそれが当たり前となるように とっても便利で大切な1行なんです。 もし当時の私のような人がこの記事を読んでいたら 同じ手間と時間を費やす前にログを出す癖をつけてください そしていつかまたこの記事に触れる機会があったら 何言ってんだこいつ、当たり前じゃんハハッ っと笑い飛ばしてください この記事が誰かの参考になりますように?✨ Unity幼稚園? 幼稚園児でもわかる解説!をモットーに、 初心者の方向けにUnityやC#の用語を解説するチャンネルを運営しています?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity Barracuda を利用して透明人間になるマン

こんにちは、Psychic VR Lab で Web エンジニアをしている ku6ryo です。今回 Unity の機械学習の推論ライブラリ Barracuda を使って透明人間になる方法をご紹介いたします。 カメラに映る人間をこちらの画像ように消す(うっすら見える)状態にしてみたいと思います。 Barracuda をすでに知っている方、使ったことがある方は Barracuda についての項目を飛ばして読んでください。 コードの内容、機械学習モデルに関して 基本的な部分のコードは IKEP さんのこちらのリポジトリを参考にさせてもらっています(Apache-2.0 License)。使用したモデルもこちらのリポジトリからとってきたものですが、もともとPINTO0309/PINTO_model_zoo で MIT License で公開されているものですので、使用可能と判断しています。こちらの記事のコードは github で公開していますので、この記事と合わせて御覧ください。 Barracuda とは Unity 上で機械学習の学習済みモデルを実行する事のできるライブラリです。今回は人が写っている画像を、人の領域だけ判定してテクスチャとして出力してくれるモデルを使用しています。Barracuda についてはすでにいろいろな記事や動画があり、参考になるので聞いたことがない方はざっくり概要を知るために調べてみてください。 ニューラルネットで何でもできちゃう!? Unity Barracudaで遊ぼう! - Unityステーション qiita 上で barracuda の検索結果 人を消す仕組み Webcam のビデオデータに対して人の写っている領域を判定し、1フレーム前の画像を現在のフレームの人が写っている部分にだけ当て込みます。この仕組からわかるように、写ったことのないものを生成するのではなく、写ったことのあるもので人の領域を埋めているので、入力は連続的な画像でかつ、背景が動いておらず、人間が動いている必要があります。 Unity プロジェクトの作成、Barracuda の導入 今回は Unity 2021.2.2f1 を使用しました。Unity プロジェクトを作成し、こちらの install 方法を参考に、Barracuda を導入します。注意点として、Unity version が 2020 の場合には Packages/manifest.json に直接依存を書き込むか、github の URL で直接追加する必要があります。 Unity シーンのセットアップ 今回は以下のようなシーン内オブジェクトとファイル構成 Canvas とその中に配置された RawImage (最終的結果が毎フレーム RawImage に書き込まれます) スクリプトを貼り付ける空のオブジェクト Processor.cs という処理をコントロールするスクリプト Preprecess.compute というカメラ画像を機械学習に食わせるための前処理の compute shader。Processor.cs のインジェクション要素 Segmentation.onnx という Barracuda 上で実行する人の領域を白色で吐き出す機械学習モデル。Processor.cs のインジェクション要素 Combine.compute という前のフレーム画像と今のフレーム画像を合わせる compute shader。Processor.cs のインジェクション要素 ヒエラルキー Processor オブジェクトの Processor.cs スクリプト設定 具体的な処理 2つの図で説明します。以下の処理が毎フレームごと(Unity の Update() 関数)に走っていると考えてください。 最初の図では、機械学習を使って人の部分を黒塗りすることを考えます。(次の図で黒塗りした部分に背景画像を当てはめることを説明します) まず、とってきたカメラ画像を①の段階で compute shader を使い、256 x 256 の画像に変換します。(input は 1920 x 1080 でなくてもよいです)次に②の段階で機械学習モデルに通して人間の部分だけ白くなった画像にします(mask とよびます)。③で input と mask をつかい、人間の部分だけ切り取った画像を作成します。 以下が①の preprocess compute shader の内容です。ほぼこちらのリポジトリにあるままです。こちらでやっていることは、入力画像を 256 x 256 のサイズに変更し、また、上下を逆さまにしています。逆さまにする理由は、使用している機械学習モデルが Unity のテクスチャフォーマットと上下逆の情報を入力としてとる仕様のためです。注意するポイントは、最後の方にある Result[offs + 0] = rgb.r; から続く3行で、Compute Buffer は Unity の Compute shader では一次元配列として扱われるため、render texture に書き込むときのような float4 を使うのではなく、RGB を配列に順番に入れ込んでいく操作が必要です。 #pragma kernel Preprocess // Input image size defined by neural network model. #define NN_INPUT_SIZE 256 sampler2D Input; RWStructuredBuffer<float> Result; [numthreads(8, 8, 1)] void Preprocess(uint3 id : SV_DispatchThreadID) { // Caluculate vertically flipped UV. float2 uv = float2(0.5 + id.x, NN_INPUT_SIZE - 0.5 - id.y) / NN_INPUT_SIZE; // Caluculate vertically flipped UV gradients. float2 duv_dx = float2(1.0 / NN_INPUT_SIZE, 0); float2 duv_dy = float2(0, -1.0 / NN_INPUT_SIZE); // Texture sample float3 rgb = tex2Dgrad(Input, uv, duv_dx, duv_dy).rgb; // Generate output buffer uint offs = (id.y * NN_INPUT_SIZE + id.x) * 3; Result[offs + 0] = rgb.r; Result[offs + 1] = rgb.g; Result[offs + 2] = rgb.b; } ①、②に相当する C# スクリプトです。 public void GenerateSegment(Texture cameraTexture) { // Convert the camera image to compute buffer. preprocessShader.SetTexture(0, "Input", cameraTexture); preprocessShader.SetBuffer(0, "Result", mlInputBuffer); preprocessShader.Dispatch(0, ML_INPUT_SIZE / 8, ML_INPUT_SIZE / 8, 1); // Creates a input tesor with the buffer and execute the model. var inputTensor = new Tensor(1, ML_INPUT_SIZE, ML_INPUT_SIZE, ML_IN_CH, mlInputBuffer); woker.Execute(inputTensor); inputTensor.Dispose(); // Copy output to the mask texture. var shape = new TensorShape(1, ML_INPUT_SIZE, ML_INPUT_SIZE, ML_OUT_CH); var tmpOutput = RenderTexture.GetTemporary(ML_INPUT_SIZE, ML_INPUT_SIZE, 0, RenderTextureFormat.ARGB32); var tensor = woker.PeekOutput("activation_10").Reshape(shape); tensor.ToRenderTexture(tmpOutput); tensor.Dispose(); Graphics.Blit(tmpOutput, segmentMask); RenderTexture.ReleaseTemporary(tmpOutput); } こちらで preprocess の compute shader にカメラ画像を渡して compute buffer (mlInputBuffer) に結果を格納します。 preprocessShader.SetTexture(0, "Input", cameraTexture); preprocessShader.SetBuffer(0, "Result", mlInputBuffer); preprocessShader.Dispatch(0, ML_INPUT_SIZE / 8, ML_INPUT_SIZE / 8, 1); 次に以下のコードで Barracuda でモデルを実行します。 var inputTensor = new Tensor(1, ML_INPUT_SIZE, ML_INPUT_SIZE, ML_IN_CH, mlInputBuffer); woker.Execute(inputTensor); inputTensor.Dispose(); 最後に、実行結果を RenderTexture にコピーします。 var shape = new TensorShape(1, ML_INPUT_SIZE, ML_INPUT_SIZE, ML_OUT_CH); var tmpOutput = RenderTexture.GetTemporary(ML_INPUT_SIZE, ML_INPUT_SIZE, 0, RenderTextureFormat.ARGB32); var tensor = woker.PeekOutput("activation_10").Reshape(shape); tensor.ToRenderTexture(tmpOutput); tensor.Dispose(); Graphics.Blit(tmpOutput, segmentMask); RenderTexture.ReleaseTemporary(tmpOutput); ここで activation_10 という文言が出てきますが、そちらは使用している機械学習のモデルの最終層の名前です。モデルを Inspector で確認すると、Input / Output という欄に入力と出力の名前とデータ形式が書かれています。(最終結果だけでなく、途中の層の結果も取得できるか気になりますね。要調査です。) ③については、処理のイメージをしてもらうために書いていますが、実際には絵とは少し異なる処理をしています。次の画像と合わせて説明します。 人を消して、背景画像で埋めるために、この画像のようなことをしています。例えば最初のフレームでは背景は人に隠れてみえないので、人の位置に埋める画像がなく、黒くなります。次のフレームで人が動いた場合、次のフレームでは前のフレームで写っていた背景画像で人の領域を埋めることができます。こちらの処理のコードは以下のような形になります。 public void Combine(Texture current, Texture previous, Texture mask, Texture output) { var kernel = combineShader.FindKernel("CSMain"); combineShader.SetTexture(kernel, "Current", current); combineShader.SetTexture(kernel, "Previous", previous); combineShader.SetTexture(kernel, "Output", output); combineShader.SetTexture(kernel, "Mask", mask); combineShader.Dispatch( kernel, current.width / 8, current.height / 8, 1 ); } #pragma kernel CSMain Texture2D<float4> Current; Texture2D<float4> Previous; Texture2D<float4> Mask; RWTexture2D<float4> Output; [numthreads(8, 8, 1)] void CSMain (uint3 id : SV_DispatchThreadID) { if (Mask[id.xy].x > 0) { Output[id.xy] = Previous[id.xy]; } else { Output[id.xy] = Current[id.xy]; } } C# のスクリプトは Compute shader を実行しているだけなので深く触れません。Compute shader の内容で登場する要素は以下です。 Current:今のカメラ画像フレーム Previous:前回の結果 Mask: 人の領域のマスク Output: 結果の render texture 肝は if (Mask[id.xy].x > 0) { の部分だけですが、マスクの値が 0 より大きい、つまり人の領域には前回の結果を、それ以外の部分は今のカメラ画像を出力するようにしています。(0 より大きいとは真っ黒でないという意味で、真っ白であるという条件で判定しないのは、機械学習の出す結果が人と背景との境界値で、 0 - 1 の間の曖昧な値をだしており、0 より大きいとしたほうが見た目がきれいな結果となったからです。) 以上が処理の流れとなります。Unity の Start() 関数で処理化処理などを行っていますが解説は割愛させていただきます。詳しくはこちらのリポジトリをご覧ください。 感想など Barracuda は学習済みモデルさえ手に入れば気軽に使えるもので、コード量も少なくて良いと思いました。しかし、上記の Unity station の動画で触れられているように、ONNX のモデルであればなんでも動くわけでもないのでそこらへんはエンジニアどうして知識を共有していく必要があると感じました。今回は他人の作ったモデルを使用しましたが、自分のトレーニングしたモデルでも挑戦したいなと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GenvidでUnityからストリーミング動画と同期したデータ配信を行う

はじめに: Genvid SDKについて Genvidは、動画ストリーミングとブラウザを介したインタラクティブな体験を組み合わせた「大規模インタラクティブ・ライブ・イベント」(Massive Interactive Live Events)を実現するSDKです。 https://www.genvidtech.com/ja/mile%E3%81%A8%E3%81%AF%EF%BC%9F/ ゲーム技術をベースに、リアルタイムで進行する動画番組に対して、動画視聴者が能動的に参加できるシステムを提供します。 動画番組内のキャラクターが次に何をするかを投票で決めたり、ミニゲームをプレイしてポイントをため、特定のキャラクターを応援するなどの活動を経て、物語が変化していきます。 MILEは、Unityを使いFacebook上で配信されている「Rival Peak」と、Unreal Engine 4を使った「Project Raven」があります。 『RIVAL PEAK』が示す次世代の視聴者参加型デジタルエンタテインメント https://news.yahoo.co.jp/byline/onokenji/20210326-00229353 Genvid SDKの導入については、Genvidディベロッパーサイトの日本語マニュアルからご確認ください。 https://www.genvidtech.com/for-developers/ 今回はGenvid SDK for Unityを使っていますが、Unreal Engineでも利用可能です。 ゲームの動画配信で、ブラウザ上でゲームの動画に合わせた演出を行う Genvidは、UnityやUnreal Engine、その他リアルタイムで動作するコンテンツをクラウドサーバー上で動作させ、そこから動画を生成してTwitchやYouTubeで配信します。 動画の視聴者は、ブラウザ上に描画される視聴者専用UIからコンテンツに介入できます。 コメント欄で視聴者と遊ぶタイプのゲームもありますが、Genvidのシステムでは動画の上に視聴者専用のUIを表示するため、コメント欄は使用しません。 動画視聴者→動画コンテンツの通信をGenvidでは「Genvid Event」と呼んでいます。 Eventの基礎と実装については次の記事をご参考ください。 Genvidにおける動画視聴者からの送信データ「Event」を集計処理するjsonスキーマの読み方 https://qiita.com/Takaaki_Ichijo/items/33e46d7849107f0bcfa7 Genvidにはもうひとつ、クラウドサーバー上で動作しているリアルタイムコンテンツから動画と同期したデータ配信ができます。 これを「Genvid Stream」と呼んでいます。テレビのデータ放送にイメージが近いです。 たとえば、動画視聴者専用のUIの表示・非表示をコンテンツ側の進行に合わせてオンオフしたり、動画側の要素をクリックして選択できる、といった演出に使えます。 こちらのgifはGenvidシステムが動作している様子です。Unityで描画されている白い板と黒い球があります。これらはクラウドサーバー上でレンダリングされ、ブラウザには動画としてストリーミングされています。ブラウザ上ではいっさいゲームの描画はしておらず、ただ動画を再生しているだけです。 そこに、「黒い球の位置」情報を同時にデータ配信しています。黄色いラベル「BallPosition」はゲーム内で描画されておらず、このブラウザ上で合成しています。 この「同期」がGenvidの重要なポイントです。同期したデータをもとにインタラクションを作ることで、「触れる動画コンテンツ」を実現できます。 本投稿では、ブラウザ側でどのように情報を受け取るか、Unityからどうやって情報を送信するかを紹介します。 なお、今回はボールの2D座標の計算をUnity側で実行していますが、変換行列(Matrix4x4)をそのままブラウザ側に送って、ブラウザで2D座標の計算をするアプローチもあります。 Genvid mathを利用したマトリックス変換行列による位置情報のデータ配信と動画同期 https://qiita.com/Takaaki_Ichijo/items/8a79cfa630238b22591c ブラウザ側(js)の実装 index.html htmlファイルは動画再生部分と、ラベルを定義するのみになります。 ソースとしてGenvid動作ライブラリであるgenvid.umd.jsをインクルードします。 (Genvidインストールフォルダのapi\web\distにあります) overlay.jsでGenvidサーバーからの情報を受け取ったり、その情報を加工してラベルの位置情報を更新する処理を行います。 index.html <!doctype html> <html> <head> <title>Genvid Overlay</title> <link rel="stylesheet" href="style.css"> </head> <body style="background:black"> <div id="video_player"></div> <div class="label" id="ballPosition">ballPosition</div> <script src="genvid.umd.js"></script> <script src="overlay.js"></script> </body> </html> Genvidの初期化とGenvid Stream受け取りイベントの設定 genvidClientの初期化後、onStreamsReceivedでGenvidStreamが来た時にJSONとしてパースする設定と、onDrawで描画時に指定の名前のGenvidStreamが来ていたら描画関数を実行する手続きを設定します。 drawの実装はこの後説明します。 overlay.js var genvidClient; fetch("/api/public/channels/join", { method: "post" }) .then(function (data) { return data.json() }) .then(function (response) { genvidClient = genvid.createGenvidClient(response.info, response.uri, response.token, "video_player"); genvidClient.onStreamsReceived(function (dataStreams) { for (let stream of [...dataStreams.streams, ...dataStreams.annotations]) { for (let frame of stream.frames) { try { frame.user = JSON.parse(frame.data); } catch (e) { console.log(e, frame.data); } } } }); genvidClient.onDraw(function (frame) { let gameDataFrame = frame.streams["ball"]; if (gameDataFrame && gameDataFrame.user) { draw(gameDataFrame.user); } }); genvidClient.start(); }) .catch(function (e) { console.log(e) }); 描画実行 draw関数では、Genvid Streamによって送られてきたデータgameDataから、ボールの位置情報posXとposYを取り出し、ラベル(ballPosition)に直接指定します。 後ほどUnity側の実装を説明しますが、位置情報はスクリーン座標に対する0~1の値で正規化されていますので、まずはGenvidで描画されている動画のアス比とスクリーンのサイズを調べ、座標を割り出します。 draw.js function draw(gameData) { let videoHeight = visualViewport.width / this.genvidClient.videoAspectRatio; let heightRaito = videoHeight / visualViewport.height; ballPosition.style.left = Math.floor(gameData.posX * 100) + "vw"; ballPosition.style.top = Math.floor((1 - gameData.posY)* heightRaito * 100) + "vh"; } cssファイルでラベルの定義をします。left,bottom要素をjsから操作します。 style.css .label { position: absolute; left:0vw; bottom:0vw; opacity: 0.7; border-radius: 8px; background-color:rgb(255, 200, 47); } Unity側の実装 Unity側の実装を見ていきましょう。本投稿ではGenvidの初期化関連は省略します。 Genvidの初期化を行うプレハブの配置については、Genvid Cube Sampleを参照していただければと思います。 RectTransformUtility.WorldToScreenPoint関数を使ってスクリーンのカメラから2Dの座標を割り出し、スクリーンサイズで割って正規化します。 その後、SubmitGameDataJSONでブラウザ側に送信します。 BallPositionBroadcaster.cs public class BallPositionBroadcaster : MonoBehaviour { public GameObject currentBallGameObject; // translation matrix used to display tank position private Camera mainCam; private void Awake () { mainCam = Camera.main; } public void SubmitBallPosition(string streamId) { if (GenvidSessionManager.IsInitialized && GenvidSessionManager.Instance.enabled) { var pos = RectTransformUtility.WorldToScreenPoint (mainCam, currentBallGameObject.transform.position); GameData gameData = new GameData () { posX = pos.x / Screen.width, posY = pos.y / Screen.height }; GenvidSessionManager.Instance.Session.Streams.SubmitGameDataJSON (streamId, gameData); } } [System.Serializable] public struct GameData { [SerializeField] public float posX, posY; } } Genvid StreamプレハブからSubmitBallPosition関数を呼ぶ シーン内でGenvid StreamプレハブをGenvidSessionプレハブ下に配置し、先ほど用意したSubmitBallPositionを呼びます。このとき、インスペクターで指定するIDの名前とブラウザ側で受け取るIDの名前が一致するようにしてください。 この例ではID「ball」を指定し、ブラウザ側でlet gameDataFrame = frame.streams["ball"];としてデータを取り出しています。 動画内の要素と位置同期するHTML要素で動画をインタラクティブに 今回はシンプルに位置同期の手順を開設しましたが、「動画内のオブジェクトをクリック選択可能にする」「位置同期した要素を奥行き計算をして他のHTML要素の後ろに隠す」といった活用が可能です。 これによって、動画視聴者をコンテンツに能動的に参加し、よりのめり込めるコンテンツを作ることができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity画像のFilter Modeについて

はじめに 2Dゲームの、特にドット感の強いゲームをUnityで作る際には、画像の素材に対してFilter Modeというプロパティを変更したほうが良い場合が多いです。 下の画像をご覧ください。これはFilter Modeを「Point(No filter)」と「Bilinear」に変更した際の比較画像です。Bilinearだとぼやけた感じがあると思います。 グラフィック素材:Wolf RPGエディタ ウルファール より Unityのフィルターモードとは Unity 公式の説明 3D 変形で伸長される際に、テクスチャをどのようにフィルタリングするかを選択します。デフォルトは Bilinear です。 https://docs.unity3d.com/ja/2019.4/Manual/class-TextureImporter.html 分かりやすく言い換えると、フィルターモードは画像を大きくした際に大きくした分の画素情報をどのように決定するかを決めるプロパティです。 例えば、左の白黒画像の画像サイズを縦横2倍にした場合、?の部分はどのように埋めるべきでしょうか? Point(No filter) Point(No filter)ですと、一番近い画素を?に割り当てます。そのためドット感が残ります。 Bilinear Bilinearの場合は近くの画素と線形に補完します。例えば、左に白、右に黒がある場合だと、中心の画素はグレーになります。そのため、特にドットの感じの残る画像ではぼかしたような印象を受けるでしょう。 参考資料 https://docs.unity3d.com/ja/2019.4/Manual/class-TextureImporter.html https://www.silversecond.com/WolfRPGEditor/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WindowsMicrophoneStreamDemoやーる(HoloLens2、Unity2020.3.0f1、MRTK2.7.2)

HoloLensアドベントカレンダー2021の6日目の記事です。 MicStreamSelectorを使いたいという要望があったので、MRTKのサンプルにあるWindowsMicrophoneStreamDemoをやってみました。 開発環境 HoloLens2 Unity 2020.3.0f1 MRTK 2.7.2 MicStreamSelector commit d7cb7dcd4ce18437cc81789e6e364f4beb56dea8 実装 1.MRTKをunitypackageから入れます Microsoft.MixedReality.Toolkit.Unity.Examples.2.7.2.unitypackage Microsoft.MixedReality.Toolkit.Unity.Extensions.2.7.2.unitypackage Microsoft.MixedReality.Toolkit.Unity.Foundation.2.7.2.unitypackage Microsoft.MixedReality.Toolkit.Unity.TestUtilities.2.7.2.unitypackage Microsoft.MixedReality.Toolkit.Unity.Tools.2.7.2.unitypackage 2.シーンのWindowsMicrophoneStreamDemoを開きます 2.MicStreamSelectorをVS2019、Release、ARM64でビルドします 3.できたDLLとMicStreamSelector->UnityAddonにある3つのスクリプトをUnityプロジェクトに追加します 4.SceneContentのInspectorビューにあるMicrophoneAmplitudeDemo.csを編集します #if MICSTREAM_PRESENT #end をコメントアウトします。 wireThicknessの値をDebugTextに表示しています MicrophoneAmplitudeDemo.cs // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Audio; using Microsoft.MixedReality.Toolkit.SpatialAwareness; using UnityEngine; using TMPro; namespace Microsoft.MixedReality.Toolkit.Examples { /// <summary> /// Demonstration class using WindowsMicrophoneStream (from com.microsoft.mixedreality.toolkit.micstream) to select the /// voice microphone and adjust the spatial awareness mesh based on the amplitude of the user's voice. /// </summary> [RequireComponent(typeof(AudioSource))] public class MicrophoneAmplitudeDemo : MonoBehaviour { // #if MICSTREAM_PRESENT public TextMeshProUGUI debugText; [SerializeField] [Tooltip("Gain to apply to the microphone input.")] [Range(0, 10)] private float inputGain = 1.0f; [SerializeField] [Tooltip("Factor by which to boost the microphone amplitude when changing the mesh display.")] [Range(0, 50)] private int amplitudeBoostFactor = 10; [SerializeField] [Tooltip("Color to use for the wireframe mesh.\nIt is recommended to use a color with an alpha of 255.")] private Color meshColor = Color.blue; private IMixedRealitySpatialAwarenessMeshObserver spatialMeshObserver = null; private Material visibleMaterial = null; /// <summary> /// Class providing microphone stream management support on Microsoft Windows based devices. /// </summary> private WindowsMicrophoneStream micStream = null; /// <summary> /// The average amplitude of the sound captured during the most recent microphone update. /// </summary> private float averageAmplitude = 0.0f; /// <summary> /// Cached material values used to restore initial settings when running the demo in the editor. /// </summary> private Color defaultMaterialColor = Color.black; private int defaultWireThickness = 0; private void Awake() { // We do not wish to play the ambient room sound from the audio source. gameObject.GetComponent<AudioSource>().volume = 0.0f; spatialMeshObserver = (CoreServices.SpatialAwarenessSystem as IMixedRealityDataProviderAccess)?.GetDataProvider<IMixedRealitySpatialAwarenessMeshObserver>(); visibleMaterial = spatialMeshObserver?.VisibleMaterial; if (visibleMaterial != null) { // Cache the initial material settings. defaultMaterialColor = visibleMaterial.GetColor("_WireColor"); defaultWireThickness = visibleMaterial.GetInt("_WireThickness"); visibleMaterial.SetColor("_WireColor", meshColor); } micStream = new WindowsMicrophoneStream(); if (micStream == null) { Debug.Log("Failed to create the Windows Microphone Stream object"); } micStream.Gain = inputGain; // Initialize the microphone stream. WindowsMicrophoneStreamErrorCode result = micStream.Initialize(WindowsMicrophoneStreamType.HighQualityVoice); if (result != WindowsMicrophoneStreamErrorCode.Success) { Debug.Log($"Failed to initialize the microphone stream. {result}"); return; } // Start the microphone stream. // Do not keep the data and do not preview. result = micStream.StartStream(false, false); if (result != WindowsMicrophoneStreamErrorCode.Success) { Debug.Log($"Failed to start the microphone stream. {result}"); } } private void OnDestroy() { if (micStream == null) { return; } // Stop the microphone stream. WindowsMicrophoneStreamErrorCode result = micStream.StopStream(); if (result != WindowsMicrophoneStreamErrorCode.Success) { Debug.Log($"Failed to stop the microphone stream. {result}"); } // Uninitialize the microphone stream. micStream.Uninitialize(); micStream = null; // Restore the initial material settings. if (visibleMaterial != null) { visibleMaterial.SetColor("_WireColor", defaultMaterialColor); visibleMaterial.SetInt("_WireThickness", defaultWireThickness); } } private void OnDisable() { if (micStream == null) { return; } // Pause the microphone stream. WindowsMicrophoneStreamErrorCode result = micStream.Pause(); if (result != WindowsMicrophoneStreamErrorCode.Success) { Debug.Log($"Failed to pause the microphone stream. {result}"); } } private void OnEnable() { if (micStream == null) { return; } // Resume the microphone stream. WindowsMicrophoneStreamErrorCode result = micStream.Resume(); if (result != WindowsMicrophoneStreamErrorCode.Success) { Debug.Log($"Failed to resume the microphone stream. {result}"); } } private static int maxWireThickness = 750; private void Update() { if (micStream == null) { return; } // Update the gain, if changed. if (micStream.Gain != inputGain) { micStream.Gain = inputGain; } if (visibleMaterial != null) { // Artificially increase the amplitude to make the visible effect more pronounced. int wireThickness = (int)(averageAmplitude * amplitudeBoostFactor * maxWireThickness); wireThickness = Mathf.Clamp(wireThickness, 0, maxWireThickness); visibleMaterial.SetInt("_WireThickness", wireThickness); debugText.text = wireThickness.ToString(); } } private void OnAudioFilterRead(float[] buffer, int numChannels) { if (micStream == null) { return; } // Read the microphone stream data. WindowsMicrophoneStreamErrorCode result = micStream.ReadAudioFrame(buffer, numChannels); if (result != WindowsMicrophoneStreamErrorCode.Success) { Debug.Log($"Failed to read the microphone stream data. {result}"); } float sumOfValues = 0; // Calculate this frame's average amplitude. for (int i = 0; i < buffer.Length; i++) { if (float.IsNaN(buffer[i])) { buffer[i] = 0; } buffer[i] = Mathf.Clamp(buffer[i], -1.0f, 1.0f); sumOfValues += Mathf.Clamp01(Mathf.Abs(buffer[i])); } averageAmplitude = sumOfValues / buffer.Length; } // #endif // MICSTREAM_PRESENT } } 5.Main Cameraの下にTextMeshProのTextを作成して、SceneContentのDebugTextにアタッチしてください 6.MicStreamSelectorの設定 7.Build Settingsの設定 8.ProjectSettings->Player->Publishing Settings->Capabilitiesのチェック項目(デプロイ前にPackage.appxmanifestの機能に反映されているかもチェック) InternetClient WebCam Microphone SpatialPerception GazeInput 9.XR Plug-in Managementの設定(※今回、OpenXRは使いません) 10.ビルドして、Release、ARM64でHoloLensにデプロイしましょう デモ ボイスの大きさに合わせて、空間メッシュのワイヤーフレームが太くなるみたいです! *音量注意WindowsMicrophoneStreamDemo#HoloLens2 #MRTK pic.twitter.com/WEM12bUJKn— がちもとさん@メタバース熊本 (@sotongshi) December 3, 2021 ただし、アプリ内で音出しちゃうとマイク機能が使えなくなるバグがあります。録画不可です。一度マイク機能が使えなくなると、アプリ再起動しても無理です。本体を再起動したらいけます。たぶん起動時にmicStreamのリセットが必要だと思います。 お疲れ様でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Debug.Log()の全文をファイル出力し、<message truncated>で省略された内容をすぐに確認できるようにする

PONOS Advent Calendar 2021の3日目の記事です。 昨日は私@e73ryoの「SystemInfo.operatingSystemからバージョンを取得するときの注意(iPadOS15.1以降のiPad端末の挙動について)」でした。 はじめに 先日、Unityプロジェクトへ外部SDKの導入をしていたところ、久しぶりにConsoleウインドウ上で<message truncated>に遭遇しました。 Unityには、Consoleウインドウに出力しようとしているログの文章が一定の文字数(私の手元で確認した限りでは16,300文字)を超えると<message truncated>が末尾に挿入されてログが終了してしまう仕様があります。 すごーーく長いエラーログが出力された場合に、その全文をConsoleウインドウ上ですぐに確認できないのは不便ですね。 一応、ConsoleウインドウのOpen Editor Logメニューから参照できるログには全文が記録されていますので、 そちらを開いて該当のログを検索することで、ログの全文を取得することが可能ではあります。 <message truncated>になってしまうのはあくまでConsoleウインドウで表示される上での仕様なので、ログ情報としては全文が残っています。 しかし、ここで確認できるログはプレーンテキストですべてのログが1ファイルに保存されているため、目的のログだけを抽出するには少し手間がかかります。 ということで、ログの全文取得を改善するディタ拡張を考えてみました。 確認環境はUnity 2019.4.32f1です。 実装の方針 では、どういった拡張機能を作りましょうか。 最初はUnityエディタのConsoleウインドウ上でログの全文が表示される機能にしようと考えていました。 やはり、Consoleウインドウ上ですぐにログの全文が読めるのが一番効率良いですからね。 しかし、 Consoleウインドウに表示されているメッセージ要素 Debug.Log()で出力されたログ この2つを関連付ける方法がどうしても見つからず、断念することに… (UnityEditor.LogEntryあたりを色々触ってみましたがうまくいきませんでした…機会があればリベンジしたいです) 今回は、Consoleウインドウと連携させることは諦め、とにかく作業者がログの全文情報が取得しやすい環境を作ることを目指しました。 機能要件は以下としました。 プロジェクトフォルダ以下にログの全文を、1ログ出力1ファイルとして保存する(作業者がすぐに開くことができる場所に置く) ファイル名にはログ出力のタイムスタンプを含め、目的のログを探しやすくする そのままだとログのファイルが無尽蔵に増え続けてしまうため、最大保存件数を設け、超えた場合は古いログから削除していく ログの全文を取得するには ログの全文はDebug.Log()へリクエストされたログの文字列から取得することができます。 Debug.Log()によるログの発行はApplication.logMessageReceivedイベントに登録することでハンドリングすることができます。 Application-logMessageReceived - Unity スクリプトリファレンス 以下のように使用します。 [InitializeOnLoad] public static class LogReceiver { static LogReceiver() { // ログの発行イベントを登録。 Application.logMessageReceived += OnReceived; } static void OnReceived(string condition, string stackTrace, LogType type) { // condition … 発行されたログの全文。 // stackTrace … 発行されたログのスタックトレース。 // type … ログの種別。 } } 実装したもの 実装したコードはこちら。 using System; using System.IO; using System.Linq; using UnityEngine; using UnityEditor; /// <summary> /// Debug.Log()で出力されたログを、ログファイルとして書き出すクラス。 /// </summary> [InitializeOnLoad] public static class DebugLogWriter { // ログを保存するフォルダ名。 const string DirectoryName = "DebugLogs"; // 最大件数。 const int LogFileLimit = 100; static DebugLogWriter() { Application.logMessageReceived += OnReceived; } static void OnReceived(string condition, string stackTrace, LogType type) { if (!Directory.Exists(DirectoryName)) { Directory.CreateDirectory(DirectoryName); } // ファイル名にタイムスタンプを含めてログ内容を書き出す。 var now = DateTime.Now; var fileName = now.ToString("yyyy-MM-dd-HH-mm-ss-fffffff") + "_" + type.ToString() + ".txt"; File.WriteAllText(DirectoryName + "/" + fileName, condition + "\n\n" + stackTrace); // 最大件数以上を超えるようであれば古いログから削除する。 // ファイルのメタ情報でソートしたい場合はDirectoryInfo、FileInfoが便利。 var directoryInfo = new DirectoryInfo(DirectoryName); var fileInfos = directoryInfo.GetFiles("*.txt"); while (LogFileLimit < fileInfos.Length) { var oldestFileInfo = fileInfos.OrderBy(fileInfo => fileInfo.CreationTime).FirstOrDefault(); File.Delete(oldestFileInfo.FullName); fileInfos = directoryInfo.GetFiles("*.txt"); } } } 実行すると、以下のようにDebug.Log()の呼び出しごとにファイルが作成されていきます。 では、試しに<message truncated>となるログを出力してみます。 今回は以下のようなコードを用いてテストしてみました。 static void LogLongMessage() { var stringBuilder = new System.Text.StringBuilder(); for(int i = 0; i < 10000; ++i) { stringBuilder.AppendFormat("{0:D5},", i); } stringBuilder.AppendLine("End"); // ログの最後には「End」が出力される。 Debug.Log(stringBuilder.ToString()); } Consoleウインドウ上だと以下のように途中で<message truncated>にされて、ログの最後の「End」が確認できませんが、 書き出されたログのファイル上では全文が参照でき、最後の「End」まで確認できます。 まとめ 不具合の調査等でDebug.Log()の内容は重要なヒントになりますので、なるべく少ない手間で正確な情報が取得できるように環境を整備しておきたいですね。 (Unity公式でなにか機能を用意してほしい…) 明日は@FW14Bさんです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スプライト(Transparent)にもDoF(被写界深度)を効かせたい【Unity】【URP】

この投稿はグレンジ Advent Calendar 2021の2日目の記事です。 概要 こんにちは! 株式会社グレンジでクライアントエンジニアをしているGamu(@AblerBiri)です。 みなさん、SpriteRenderer等で描画しているキャラにDepth of Fieldが上手く効かなくて困ったという経験はありませんか? 例えば、上の動画のようにキャラの足元だけDoFが効いて、ほとんどがボヤっとしてしまうとか。 今回はUnityのURP(Universal RenderPipeline)で解決する方法を紹介します。 環境 OS : MacOS Big Sur(11.4) Unity : 2020.3.12f1 URP : 10.5.0 GL : Metal スプライトにDoFが効かない原因 結論からすると、スプライトの描画で使用しているシェーダが深度値をデプスバッファに書き込まないからです。 DoFはデプスバッファに書き込まれている深度値を用いてブラーを掛ける範囲を決めています。 なので、深度値を書き込んでいなければ正しくブラーが掛からないのは当然です。 例えば、SpriteRendererにデフォルトで付くマテリアルのシェーダ(Sprites/Default)やURP用のライティングを考慮するスプライト用のシェーダ(Universal Render Pipeline/2D/Sprite-Lit-Default)を見ると、深度値を書き込む設定(ZWrite)がOFFになっています。 では、それらのシェーダのZWriteをONにすれば解決!...とはいきませんでした。 スプライトを使ってキャラを描画する場合の問題とURPのForwardRendererを使う場合の問題が存在しています。 問題点1 : Zファイティング スプライトを使ってキャラを描画する時に深度値も書き込むようにすると、次の画像のようになることがあります。 この現象はZファイティングと呼ばれます。 Zファイティングとは、カメラからの距離がほぼ同じ2つの平面がある時、どちらが手前に描画されるか定まらない現象です。 Zファイティング自体は3D描画で一般的に発生するものですが、スプライトを使ってパーツごとにキャラを描画している場合は特に発生しやすいです。 スプライトの描画でZファイティングを引き起こさないためにはZTestをAlways(深度値を考慮せず常に描画)等にするか、深度値を書き込まない等の解決方法があります。 今回はZTestをAlwaysにせず、かつ、キャラのスプライトの深度値を上手く書き込む方法として、キャラの色を描画した後に別のパスでキャラの深度値を書き込む方法を紹介します。 URPはRendererFeatureというものを使って実装してみます。 スプライトのシェーダに専用パスを定義する URPのスプライト用シェーダ(Universal Render Pipeline/2D/Sprite-Lit-Default)をコピペし、新しいシェーダを作成します。 そして作成したシェーダのPassというブロックが並んでいる場所の一番最後に、次のコードを置きます。 Sprite-Lit-Custom Pass { Name "TransparentDepthOnly" Tags { "LightMode" = "TransparentDepthOnly" "Queue"="Geometry" "RenderType"="Opaque"} ZWrite On ColorMask 0 HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment // ------------------------------------- // Material Keywords #pragma shader_feature_local_fragment _ALPHATEST_ON #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A //-------------------------------------- // GPU Instancing #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON struct Attributes { float4 position : POSITION; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv : TEXCOORD0; float4 positionCS : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _MainTex_ST; Varyings DepthOnlyVertex(Attributes input) { Varyings output = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); output.uv = TRANSFORM_TEX(input.texcoord, _MainTex); output.positionCS = TransformObjectToHClip(input.position.xyz); return output; } half4 DepthOnlyFragment(Varyings input) : SV_TARGET { clip(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a - 0.5); return 0; } ENDHLSL } これがスプライトの深度値だけを書き込む専用パスになります。 URPにスプライトの深度値だけを書き込む機能を追加する URPのRendererFeatureを使って、先ほどの専用パスを使ってスプライトの深度値をデプスバッファに書き込む機能を実装します。 以下、RendererFeatureとRenderPassのコードです。 DrawTransparentDepthRendererFeature.cs using UnityEngine.Rendering.Universal; public class DrawTransparentDepthRendererFeature : ScriptableRendererFeature { private DrawTransparentDepthRenderPass pass; public override void Create() { pass ??= new DrawTransparentDepthRenderPass(); pass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (pass != null) { renderer.EnqueuePass(pass); } } } DrawTransparentDepthRenderPass.cs using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class DrawTransparentDepthRenderPass : ScriptableRenderPass { private readonly ShaderTagId shaderTagId = new ShaderTagId("TransparentDepthOnly"); public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get("Draw Transparent Depth"); cmd.SetRenderTarget(renderingData.cameraData.renderer.cameraDepthTarget); context.ExecuteCommandBuffer(cmd); var drawSettings = CreateDrawingSettings(shaderTagId, ref renderingData, SortingCriteria.CommonOpaque); var filterSettings = new FilteringSettings(RenderQueueRange.transparent, renderingData.cameraData.camera.cullingMask); context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings); CommandBufferPool.Release(cmd); } } RenderPassは拡張する描画機能の実装本体です。 RendererFeatureはRenderPassに渡すパラメータを設定したりRenderPassのインスタンスを生成したりする役割があります。 そして、このRendererFeatureを有効にするため、ForwardRenderer.assetというScriptableObjectを表示し、AddRendererFeatureからDrawTransparentDepthRendererFeatureを選択します。 RenderFeature追加前 RenderFeature追加後 これでZファイティングを起こさずにキャラにもDoFが効くように...なりません。 ここでもう1つの問題が立ちはだかります。 問題点2 : ポストエフェクト用のデプスバッファが別に存在している 先ほどまでの実装でZファイティングを起こさずにスプライトの深度値は確かに書き込まれます。 しかし、実はURPのForwardRendererのポストエフェクトで使われているデプスバッファとは別のものに書き込まれています。 FrameDebuggerやDebug.Logで確認してみると、 renderingData.cameraData.renderer.cameraDepthTargetで参照できるデプスバッファは_CameraDepthAttachment 実際にポストエフェクトで使われているデプスバッファは_CameraDepthTexture ということが確認できます。 _CameraDepthTextureは直接参照できない仕組みになっているので、GetTemporaryRTを使って無理やり参照してみます。 DrawTransparentDepthRenderPassクラスのコードを以下に置き換えて下さい。 DrawTransparentDepthRenderPass.cs using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class DrawTransparentDepthRenderPass : ScriptableRenderPass { private readonly ShaderTagId shaderTagId = new ShaderTagId("TransparentDepthOnly"); private readonly int depthId = Shader.PropertyToID("_CameraDepthTexture"); public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get("Draw Transparent Depth"); var descriptor = renderingData.cameraData.cameraTargetDescriptor; descriptor.colorFormat = RenderTextureFormat.Depth; descriptor.depthBufferBits = 32; descriptor.msaaSamples = 1; cmd.GetTemporaryRT(depthId, descriptor, FilterMode.Point); cmd.SetRenderTarget(depthId, depthId); context.ExecuteCommandBuffer(cmd); var drawSettings = CreateDrawingSettings(shaderTagId, ref renderingData, SortingCriteria.CommonOpaque); var filterSettings = new FilteringSettings(RenderQueueRange.transparent, renderingData.cameraData.camera.cullingMask); context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings); cmd.Clear(); cmd.ReleaseTemporaryRT(depthId); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } 最後にReleaseTemporaryRTするのでダメかと思いきや、この方法で上手く書き込めました。 これで晴れてスプライトを使ったキャラにもDoFを効かせることが出来るようになりました! 最後に Zファイティングを発生させずにスプライト(Transparent)にもDoFを効かせる方法を紹介しました。 3D空間にスプライトのキャラを配置するというのは珍しいシチュエーションだと思いますが、オクトパストラベラーのような表現のゲームを作ろうとした時は使えるかもしれません。 グレンジのアドベントカレンダーには引き続き記事を投稿していきます! 明日は、flankidsさんの記事が投稿される予定です! 参考 https://blog.amagi.dev/entry/2019/05/09/174926 https://forum.unity.com/threads/solved-urp-depth-of-field-transparency-render-queue-problem.859936/ https://qiita.com/t-matsunaga/items/09343ae7c683269374c4
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLite meets Unity 〜Unityでローカルなデータベースを使おう〜

はじめまして。 CYBIRD Advent Calendar 2021 18日目の@whitemage_yuです。 先日17日目は@cy-tatsuya-sakaiさんの Unity1週間ゲームジャムで毎回やるいい加減な実装 でした。 この記事のゴール この記事では、UnityでSQLiteを使えるようにするまでの手順を紹介します。 対象プラットフォームはWindowsとmacOSです。 この記事で説明していないこと Unityの導入方法については説明しません。 SQLiteの文法にを説明する記事でもありません。 SQLiteとは SQLiteとは、C言語で実装された、軽量かつ信頼性の高いSQLデータベースエンジンです。一般的なRDBMSで使われているデータベースエンジンとは異なり、複雑な設定が必要なく手軽に利用することができます。またライブラリとしてアプリケーションやゲームにも組み込めます。 この記事では、UnityへSQLiteを組み込む方法についてご紹介できればと思います。 使用シチュエーション例 ・ゲームの設定情報の保存先として使う ・マスタデータの保存先として使う etc... 私は音楽ゲームで、読み込んだ楽曲の情報をキャッシュするために導入しています。 前提バージョン Unity 2020.3.14f1 SQLite Version 3.36.0 Unityに導入してみる 以下、すでにUnityプロジェクトが作成されている前提で記述します。 導入したいプラットフォームに合わせてプラグインを取得する SQLiteのライブラリが必要なので、導入したいプラットフォームに合わせてプラグインを取得します。 Windows sqlite-dll-win64-x64-{数字}.zip をダウンロードすればOKです。解凍すると、中にDLLファイルが入っています。 macOS プラグインファイルが公式で配布されていないので、gcc コマンドが使える環境で自力でビルドします(そこまで難しくないので大丈夫)。ここではmacOSでのビルド方法をご紹介します。 1 sqlite-amalgamation-{数字}.zip をダウンロードします。 2 以下のコードを実行します。 $ unzip sqlite-amalgamation-{数字}.zip $ cd sqlite-amalgamation-{数字} $ gcc -o libsqliteX.dylib sqlite3.c -dynamiclib $ echo "macOS以外の環境だと共用ライブラリの場所が違うかもしれないので、その場合は-Lオプションの引数を変える" > /dev/null $ gcc -o shell shell.c libsqliteX.dylib -L /usr/lib 3 完成! SQLiteUnityKit拡張ライブラリを導入する UnityでSQLiteを扱う際の橋渡しをしてくれるライブラリを導入します。 1. tetr4lab/SQLiteUnityKitをZIPファイルでダウンロード、またはgit clone でクローンします。 2. ダウンロードしてきたSQLiteUnityKitのAssets/Scripts にあるSQLite.cs とSQLiteUtility.cs をAssets 以下の任意の場所に配置します。 3. Assets/Plugins にx86_64 というフォルダを作り、前項で取得したプラグインを配置します。 4. 3.で配置したDLLファイルおよびDYLIBファイルについて、名前をlibsqliteX.dll もしくはlibsqliteX.dylib に変更します。 5. SQLite.cs 25から48行目のsqlite3 をlibsqliteXに変更します。 6. 配置したプラグインの設定をInspectorで画像のように変更します。 7. 完了! SQLiteのクライアントを入れる UnityでSQLiteを用いるのに必須ではありませんが、SQLiteのクライアントを入れると開発時に便利です。WindowsやMac、Linuxにも対応しています。 DB Browser for SQLite DBにアクセスしてみる テーブルを作成してDBインスタンスを作成してみる using UnityEngine; using System.IO; using SQLiteUnity; public class SQLiteTest : MonoBehaviour { private string path = ""; private const string databaseFileName = "instance.db"; private SQLite sqlite; public void Start() { var createTableQuery = $@" PRAGMA foreign_keys=true; CREATE TABLE personnel ( id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING NOT NULL ); "; #if UNITY_EDITOR path = Directory.GetCurrentDirectory(); #else path = System.AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\'); #endif sqlite = new SQLite(databaseFileName, createTableQuery, path); } } DBを作成すると同時にテーブルも作成します。 これを実行すると、Unityプロジェクトのルートディレクトリにinstance.db というファイルが生成されます。これがSQLiteのDBのインスタンスとなります。 これをコピーしてどこかに保存しておくだけでバックアップが完了します。 作ったテーブルにレコードを追加してみる using UnityEngine; using System.IO; using SQLiteUnity; public class SQLiteTest : MonoBehaviour { private string path = ""; private const string databaseFileName = "instance.db"; private SQLite sqlite; public void Start() { var createTableQuery = $@" PRAGMA foreign_keys=true; CREATE TABLE personnel ( id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING NOT NULL ); "; #if UNITY_EDITOR path = Directory.GetCurrentDirectory(); #else path = System.AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\'); #endif sqlite = new SQLite(databaseFileName, createTableQuery, path); var personalNames = new string[] { "kisaragi", "minami_nitta", "riko_sakurauchi", "rinze_morino", "sayoko_takayama", "kaede_kujo" }; foreach(var name in personalNames) { sqlite.ExecuteQuery(string.Format("INSERT INTO `personnel` VALUES (null, '{0}')", name)); } } } これを実行し、DBのインスタンスファイルをクライアントソフトで開くとデータが入っていることが確認できます。 投入したデータを取得してみる using UnityEngine; using System.IO; using SQLiteUnity; public class SQLiteTest : MonoBehaviour { private string path = ""; private const string databaseFileName = "instance.db"; private SQLite sqlite; public void Start() { var createTableQuery = $@" PRAGMA foreign_keys=true; CREATE TABLE personnel ( id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING NOT NULL ); "; #if UNITY_EDITOR path = Directory.GetCurrentDirectory(); #else path = System.AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\'); #endif sqlite = new SQLite(databaseFileName, createTableQuery, path); var personalNames = new string[] { "kisaragi", "minami_nitta", "riko_sakurauchi", "rinze_morino", "sayoko_takayama", "kaede_kujo" }; foreach(var name in personalNames) { sqlite.ExecuteQuery(string.Format("INSERT INTO `personnel` VALUES (null, '{0}')", name)); } var selectQuery = "SELECT * FROM `personnel`"; var result = sqlite.ExecuteQuery(selectQuery); Debug.Log(result); } } SQLiteを使う上での注意点 SQLiteのデータベースファイルは暗号化されていない SQLiteのデータベースのインスタンスであるファイルは暗号化されておらず、上記にもある通りファイルさえあればクライアントソフトで中身を見られてしまいます。もし商用ソフトで用いるなら暗号化が必須になると思われます。 複数書き込みができない SQLite公式のドキュメントより SQLite supports multiple simultaneous read transactions coming from separate database connections, possibly in separate threads or processes, but only one simultaneous write transaction. 拙訳 SQLiteのデータベースは複数のコネクション(いくつかのスレッドもしくはプロセスに分かれている場合を含む)からの同時読み込みのトランザクションはサポートしていますが、同時に1つのトランザクションしか書き込むことはできません。 同時に書き込もうとするとエラーになります。 データベースの性能はストレージの読み書きの速度に依存する SQLiteはファイル=データベースなので、その性能は単純にストレージの性能に依存します(といっても、最近はSSDなどのフラッシュメモリのストレージを搭載しているデバイスが増えてきているので、これは考慮しなくてもいいかもしれませんが)。 参考文献 この記事を書くにあたり、参考にさせていただいたwebページは以下の通りです。 UnityでSQLiteを使う ~ SQLiteUnityKit拡張ライブラリ(@tetr4lab様) https://qiita.com/tetr4lab/items/0e8b8cf0f20049e46c7d UnityでSQLiteを扱う方法(@hiroyuki7様) https://qiita.com/hiroyuki7/items/5335e391c9ed397aee50 Mac OSXでダイナミックライブラリdylibを作ったり使ったりする方法。(katty0324様) https://blog.katty.in/4346 Transaction(SQLite公式ホームページ) https://www.sqlite.org/lang_transaction.html 最後に 明日19日目は@koronekoさんの記事ですので、そちらもぜひご覧ください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Genvid mathを利用したマトリックス変換行列による位置情報のデータ配信と動画同期

はじめに: Genvid SDKについて Genvidは、動画ストリーミングとブラウザを介したインタラクティブな体験を組み合わせた「大規模インタラクティブ・ライブ・イベント」(Massive Interactive Live Events)を実現するSDKです。 https://www.genvidtech.com/ja/mile%E3%81%A8%E3%81%AF%EF%BC%9F/ ゲーム技術をベースに、リアルタイムで進行する動画番組に対して、動画視聴者が能動的に参加できるシステムを提供します。 コンテンツ内のキャラクターが次に何をするかを投票で決めたり、ミニゲームをプレイしてポイントをため、特定のキャラクターを応援するなどの活動を経て、物語が変化していきます。 MILEは、Unityを使いFacebook上で配信されている「Rival Peak」と、Unreal Engine 4を使った「Project Raven」があります。 『RIVAL PEAK』が示す次世代の視聴者参加型デジタルエンタテインメント https://news.yahoo.co.jp/byline/onokenji/20210326-00229353 Genvid SDKの導入については、Genvidディベロッパーサイトの日本語マニュアルからご確認ください。 https://www.genvidtech.com/for-developers/ 今回はGenvid SDK for Unityを使っていますが、Unreal Engineでも利用可能です。 変換行列(Matrix4x4)を使って動画と同期した座標情報を送る さて、Genvid経由でゲームから動画と同期した「オブジェクトの位置」を送信して、そのデータをもとにHTML側のラベルを動かす処理を考えてみましょう。 次のgifでは、黄色いラベルが黒い球に追従しています。 白い板と黒い球の部分は動画です。Unityで描画された内容を動画としてブラウザ上でストリーミング再生しています。 黒い球の座標を動画と一緒にブラウザに送り、ブラウザ側で黄色いラベルを描画する際に座標を使って位置を同期させています。 こうした演出をGenvidで行う場合、Unity側でオブジェクトの2D座標を計算してXY座標を送る方法が最も楽です。その手法については以下の手順で紹介しました。 GenvidでUnityからストリーミング動画と同期したデータ配信を行う https://qiita.com/Takaaki_Ichijo/items/4fbc70e7efdbdef85459 これでもかまわないのですが、Unityインスタンス側に一定の処理コストがかかります。 そこで、今回は変換行列(Matrix4x4)を使って位置情報を送り、ブラウザ側で2D座標に変換する処理を考えましょう。 変換行列(Matrix4x4)では、位置を示すVector3, 回転を示すRotationとサイズの、3次元空間上の位置・回転・大きさが一意の値で表現できます。中身は16個の数値の配列です。 Matrix4x4 https://docs.unity3d.com/ja/current/ScriptReference/Matrix4x4.html ゲームからMatrix4x4で位置情報を送りますが、その座標データを2Dに変換するために、カメラのマトリックス情報も同時に送ります。 Genvid Mathの活用 Genvidには、ブラウザ側で各種演算計算を行うための便利ライブラリとして「Genvid Math」が付属しています。 ベクトル演算や閾値チェック、距離の計算など3D座標系を取り扱うことができます。 Genvid Math API https://www.genvidtech.com/doc/ja/SDK-1.32.0/reference/web_sdk/js/math.html ブラウザ側(js)の実装 index.html 今回はGenvid Mathライブラリを使うので、ソースとしてgenvid.umd.jsと一緒にgenvid-math.umd.jsもインクルードします。 index.html <!doctype html> <html> <head> <title>Genvid Overlay</title> <link rel="stylesheet" href="style.css"> </head> <body style="background:black"> <div id="video_player"></div> <div class="label" id="ballPosition">ballPosition</div> <script src="genvid.umd.js"></script> <script src="genvid-math.umd.js"></script> <script src="overlay.js"></script> </body> </html> Genvidの初期化とGenvid Stream受け取りイベントの設定 overlay.jsは前回記事と同様です。drawの中身でgenvid-mathを使用します。 overlay.js var genvidClient; fetch("/api/public/channels/join", { method: "post" }) .then(function (data) { return data.json() }) .then(function (response) { genvidClient = genvid.createGenvidClient(response.info, response.uri, response.token, "video_player"); genvidClient.onStreamsReceived(function (dataStreams) { for (let stream of [...dataStreams.streams, ...dataStreams.annotations]) { for (let frame of stream.frames) { try { frame.user = JSON.parse(frame.data); } catch (e) { console.log(e, frame.data); } } } }); genvidClient.onDraw(function (frame) { let gameDataFrame = frame.streams["ball"]; if (gameDataFrame && gameDataFrame.user) { draw(gameDataFrame.user); } }); genvidClient.start(); }) .catch(function (e) { console.log(e) }); 描画実行 draw関数では、Genvid Streamによって送られてきたデータgameDataから、ボールのマトリックスを取り出し、位置情報のVector3のみを取り出します。 また、カメラのマトリックスは後述するconvertMatrix関数でmat4データに変換後、genvidMath.projectPositionを使ってスクリーンのマトリックスとボールの位置から2D座標を計算します。 draw.js function draw(gameData) { let m = gameData.ballMatrix; let p = genvidMath.vec3(m.e03, m.e13, m.e23); let mat = this.convertMatrix(gameData.matProjView); let pos_2d = genvidMath.projectPosition(mat, p); this.center_at(ballPosition, pos_2d, blankSize); } 送られてきたデータを4行からなる4x4 の行列データに加工する Genvid経由でゲームから座標情報が来た時、実体的なデータは大きさが16の配列です。まずはそれをmat4の形に整形します。 convertMatrix.js function convertMatrix(rawmat) { return genvidMath.mat4(genvidMath.vec4(rawmat.e00, rawmat.e01, rawmat.e02, rawmat.e03), genvidMath.vec4(rawmat.e10, rawmat.e11, rawmat.e12, rawmat.e13), genvidMath.vec4(rawmat.e20, rawmat.e21, rawmat.e22, rawmat.e23), genvidMath.vec4(rawmat.e30, rawmat.e31, rawmat.e32, rawmat.e33)); } 2D座標からラベルの描画位置を計算する draw関数で最後に呼び出しているcenter_atでは、中央の座標を計算してHTML要素の位置を書き換えます。引数にhtml要素、2d座標、ブラウザ内の動画以外の余白サイズ(デモでは縦方向)の3つを取ります。 まず-1~1の数値を0~1にノーマライズし、その後0から画面サイズとノーマライズされた位置情報を掛けて実際のサイズに変換します。 さらに、this.genvidClient.videoAspectRatioから現在のストリーミング動画のアス比から、ビデオが描画されていない縦方向の空白部分を計算します。 そしてセンタリング処理として移動する要素の中央ポイントになるよう調整したのち、left / bottomに対して座標を指定します。 center_at.js function center_at(html_element, pos_2d) { // Convert from [-1, 1] range to [0, 1]. let vh = genvidMath.vec2(0.5, 0.5); let pos_2d_n = genvidMath.mad2D(pos_2d, vh, vh); // Convert from [0, 1] range to [0, w]. let p = html_element.parentElement; let p_size = genvidMath.vec2(p.clientWidth, p.clientHeight); let pos_in_parent = genvidMath.mul2D(pos_2d_n, p_size); // Calculate Video Height. let videoHeight = p.width / this.genvidClient.videoAspectRatio; let blankSize = p.height - videoHeight; // Adjust for centering element. let e_size = genvidMath.vec2(html_element.clientWidth, html_element.clientHeight); let e_offset = genvidMath.muls2D(e_size, -0.5); let pos_centered = genvidMath.add2D(pos_in_parent, e_offset); // Apply. html_element.style.left = pos_centered.x+"px"; html_element.style.bottom = pos_centered.y + blankSize+"px"; html_element.style.position = "absolute"; } Unity側実装 Unity側の実装を見ていきましょう。本投稿ではGenvidの初期化関連は省略します。 前回記事と比較して、Genvid Streamで送るGameDataの構造が違います。  ボールの位置を送るSubmitBallPosition関数を作る 生成されたボールのマトリックス情報を定期的にGenvid Streamとして配信します。マトリックスはtransform.localToWorldMatrixから取得できます。 同様にカメラのプロジェクションビューをmainCam.projectionMatrix * mainCam.worldToCameraMatrixで計算し、「GameData」として同時に送信します。 BallPositionBroadcaster.cs public class BallPositionBroadcaster : MonoBehaviour { public GameObject currentBallGameObject; // translation matrix used to display tank position private Camera mainCam; private void Awake () { mainCam = Camera.main; } public void SubmitBallPosition(string streamId) { if (GenvidSessionManager.IsInitialized && GenvidSessionManager.Instance.enabled) { GameData gameData = new GameData () { matProjView = mainCam.projectionMatrix * mainCam.worldToCameraMatrix, ballMatrix = currentBallGameObject.transform.localToWorldMatrix, }; GenvidSessionManager.Instance.Session.Streams.SubmitGameDataJSON (streamId, gameData); } } [System.Serializable] public struct GameData { [SerializeField] public Matrix4x4 matProjView; [SerializeField] public Matrix4x4 ballMatrix; } } Genvid StreamプレハブからSubmitBallPosition関数を呼ぶ この手順は前回記事と全く同一です。 Matrix4x4とGenvid Mathの活用 描画される内容は前回と全く一緒ですが、Unity側の処理負荷が若干下がることと、奥行き情報を含めたデータとしてブラウザ側に渡されるため、ほかのHTML要素のソート(ある地点では後ろに隠れるなど)といった処理が可能になります。 次回はより凝った演出について紹介します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity uGUIで使える無料/OSSなカラーピッカー

Unity uGUIで使える無料/OSSなカラーピッカーを調べました。 Github Star数の大きい順に並べています。 HSV Color Picker Github Star 409 最終更新 2ヶ月前 Unity Color Picker Github Star 69 最終更新 3年前 Color Picker for Unity UI in a single script/prefab Github Star 57 最終更新 5年前 UnityColor Github Star 25 最終更新 5年前 ACP Github Star 12 最終更新 2年前 結論 GitHub Starが一番多く、更新も新しいHSV Color Pickerが有力。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】 RewiredでInputManagerをPrefabで使用する方法

0.アセットの紹介 Unity Asset Storeで販売されている「Rewired」です。 スマホ、PC、コントローラー(XBox360 , XBoxOne , PS4 , Nintendo Switch)の様々なコントローラーに対応できる最強アセットになります。 https://assetstore.unity.com/packages/tools/utilities/rewired-21676 1.セットアップ 基本的なセットアップは以下のサイトをご参照ください。 Rewiredという入力管理で最強のアセット https://gentome.com/gentomeblog/2287/userewired/ 2.疑問点 Rewiredは使用するときにInputManagerを生成する必要があります。 生成する方法を【in scene】と【prefab】の2つがあり、【prefab】での使用方法を調べました。 3.解決方法 入力を取るScene内でInitializerを生成 Window -> Rewired -> Create -> Initializer Hierarcyの【Rewired Initializer】Initializer(Script)がアタッチされているので、生成したPrefabをアタッチする 4. 入力を検知 以下のコードで入力を検知できます。 InputSample.cs using Rewired; public void Update() { var player = ReInput.players.GetPlayer(0); //action名の検知 if (player.GetButtonDown("Jump")) { Debug.Log("Jumpキーの入力を検知しました。"); } } 5. まとめ Rewiredは日本語での解説が非常に少なく英語の公式ドキュメントを翻訳しつつ読み解く必要があります。 英語は苦手ですが少しづつ日本語記事が増やしていきたいですね。がんばります!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】AddComponentが重い件への対処

はじめに こUnityのAddComponentの呼び出しの負荷が高いことは度々話題になります. Unity AddComponent Very Inefficient and Slow (StackOverflow) この記事では, AddComponentが重い件への1つの対処法について解説します. 概要 ざっくり結論としては,一度に大量のGameObjectを生成しAddComponentをするようなケース においてはAddComponentを呼び出す回数をなるべく減らすため, すでにAddComponentが行われたGameObjectをInstantiateするとよい というものです. 実行環境 実行時間などの参考に検証をした環境を明示しておきます. Ryzen 9 5900X Unity 2020.3.8f1 Windows 10 64bit Performance testing API 2.8.0-preview 検証 今回の記事では,UnityのPerformance testing APIを使った簡単なベンチマークテストで以下の2パターンの速度について検証します. 100000回GameObjectを生成し,都度AddComponentした場合 1度AddComponentしたGameObjectをInstantiateで複製した場合 // GameObject生成 -> AddComponent // x 10000 [Performance] [Test] public void AddComponentProfilingTest() { Measure.Method(() => { for (int i = 0; i < 10000; i++) { var obj = new GameObject(); obj.AddComponent<EmptyComponent>(); } }) .WarmupCount(1) .IterationsPerMeasurement(1) .MeasurementCount(20) .Run(); } // AddComponent -> Instantiate // x 10000 [Performance] [Test] public void InstantiateProfilingTest() { Measure.Method(() => { var template = new GameObject(); template.AddComponent<EmptyComponent>(); for (int i = 0; i < 10000 - 1; i++) { var obj = Object.Instantiate(template); } }) .WarmupCount(1) .IterationsPerMeasurement(1) .MeasurementCount(20) .Run(); } 結果 測定した結果, 都度AddCompnent : 481.79ms (Median) Instantiateで複製 : 93.67ms (Median) とおよそ5倍高速化される結果となりました. 実行環境によっては更に差が出ることもありそうに思えます. AddCompnentはComponentの重複チェックなどの処理が行われるためオーバーヘッドが大きく,大量に行う場合はInstantiateで複製したほうが効率が良さそうです. 最後に AddComponentは重いので気をつけよう,といった趣旨の記事でした. そもそもMonoBehaviourの使用やAddComponent自体が比較的負荷の高い処理となるので,大量に生成する必要があるオブジェクトなどにはなるべく使用しないのが得策と言えそうです.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity]インタラクティブな水面の表現をやってみる

みなもは しずかに ゆれている ....なみのりを つかいますか? スィ〜〜〜〜〜〜 概要 グレンジ Advent Calendar 2021 3日目担当の、flankids です。 普段はインプット管理、カメラワーク作り、アニメーション制御などなどで遊び心地を作ることを主にやってるエンジニアをしています。 今回はUnityで 光の性質を加味した水面の表現 波動方程式を使った波の表現 をやってみました。 もし気に入っていただけたら、LGTM(いいね)やストックをしてもらえるとスゴく嬉しいです! 参考にした記事を先に紹介しておきます! モバイルアプリにおけるアーティストフレンドリーな水面表現戦略 - slideshare 水面を作ってみた - しゅみぷろ 両方をほとんどそのまま踏襲してマージしたのが今回の表現になります。 ありがたい記事に感謝・・・! 各記事内のコードやサンプルプロジェクトがそのまま参考になるため、本記事ではざっくり概要の解説をします。 光の性質を加味した水面の表現 まずは光の性質を加味して、クオリティ高く見える水面の表現です。 今回は以下の3つのテクニックを使ってビジュアルを作りました。 キューブマップを使って映り込みを表現 ノーマルマップを使って水面の凹凸を表現 フレネル反射という光の性質を再現して水面の透け具合をリアルに表現 キューブマップを使って映り込みを表現 キューブマップとは、周囲の環境のリフレクションを表現する正方形テクスチャが 6 つで一組になったもの。 モデルへの周囲の風景の映り込みなどを表現するために使われることが多いです。 今回は、青空と雲が広がるキューブマップを使って水面に映る空を表現します。 WaterSurface.shader // 視点からのベクトルと法線から反射方向のベクトルを計算する half3 reflDir = reflect(-i.toEye, i.normal); float4 col = texCUBE(_Cube, reflDir); reflectを使って、視線から頂点へのベクトルを反射させたベクトル計算します。 それを使ってキューブマップをサンプリングすることで、映り込みを反映しています。 反射ベクトルを使ってピクセルシェーダーの色を決めているので、普通にテクスチャを反映するのと違って、角度を変えることで映り込みなりの色の動きがあることが確認できます。 ノーマルマップを使って水面の凹凸を表現 次にノーマルマップを使ってフラットなポリゴンに凹凸を与えます。 WaterSurface.shader // ノーマルマップから法線情報を取得する float3 localNormal = UnpackNormalWithScale(tex2D(_BumpMap, i.uvNormal), _BumpScale); // タンジェントスペースの法線をワールドスペースに変換する i.normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z; tangentやbinormalを使ってモデルの持つ法線にノーマルマップを上乗せする形で計算しています。 これだけで水面っぽい凹凸は生まれるのですが、水流を表すためにノーマルの位置をUVスクロールで動かしてみましょう。 WaterSurface.shader float2 scroll = float2(_ScrollSpeedX, _ScrollSpeedY) * _Time; // ノーマルマップから法線情報を取得する float3 localNormal = UnpackNormalWithScale(tex2D(_BumpMap, i.uvNormal + scroll), _BumpScale); 少しずつ水面っぽくなってきました。 フレネル反射を再現して水面の透け具合をリアルに表現 フレネル反射とは、水面に対して垂直に覗き込むと大部分が透けて見えて、覗き込む角度が平行に近づくほど透明度が減って見えるという水面に見られる現象です。 先に実装結果を出します。 カメラの角度によって水面の透け具合が変化し、視点が水面に対して並行に近づくほど透明度が減り、空の映り込みが強く反映されることがわかると思います。 ノーマルで表現された凹凸の角度もこの現象に影響するため、透明度は視点と法線の角度によって決まります。 フレネル反射を実現する実装は以下のとおりです。 WaterSurface.shader // 水のF0値(正面から見たときの反射率)は0.02 #define F0 0.02 ~~中略~~ // フレネル反射率を反映 half vdotn = dot(i.toEye, i.normal); half fresnel = F0 + (1 - F0) * pow(1 - vdotn, 5); col.a = fresnel; 視線から頂点へのベクトルと法線の内積を使って、フレネル反射を反映するためのアルファ値を計算します。 水面に対して垂直に覗き込んだときの透明率(F0値)は定数で0.02=2%として定義しています。 これでフレネル反射を再現することができました。 ノーマルによる凹凸もあり、透明度がまばらだったり、角度を変えると透明度が変化したりと、かなり現実の水面に近い質感が表現できたと思います! TIPS: さらなる水面の表現 モバイルアプリにおけるアーティストフレンドリーな水面表現戦略 - slideshare ここまでの実装の参考にした上記公演スライドでは、さらに 深さに合わせた水中の色 コースティクス(水面によって屈折した光が水底に投影されてできる独特の模様) について解説されています。 本記事ではひとまず水面だけを考えた表現に留めているため、興味がある方は是非上記スライドをご覧ください! 波動方程式を使った波の表現 ここまでは水面の質感を表現する工程でした。 インタラクティブな水面にするためには、外からの影響で水面の凹凸が変化する必要があります。 そのために「波動方程式」を使って波の動きをシミュレートします。 波動方程式は水面の波紋以外にも、音波、電磁波などのいろんな振動・波動現象を計算する方程式です。 波動方程式 波動方程式自体の解説は割愛します。(というか僕もちゃんと分かってません・・・) 下記サイトがパラメータを調整できるデモもついていて分かりやすそうです。 https://ryukau.github.io/filter_notes/waveequation/waveequation.html 上記Githubのプロジェクトで、波動方程式を使って波を作る実装が紹介されていたので、そちらを参考にしました。 同開発者さんによるInkPainterというアセットで、指定した位置のテクスチャを塗り、波発生源の入力として扱う 波の動きのシミュレートする Wave.shader に波発生源の入力を渡す Wave.shader でシミュレートの結果を赤色で表現されるハイトマップとして出力 ハイトマップを法線として反映 という流れで、ここまでで作った水面に波の動きを追加しています。 Wave.shader float2 stride = float2(_Stride, _Stride) * _PrevTex_TexelSize.xy; half4 prev = (tex2D(_PrevTex, i.uv) * 2) - 1; half value = (prev.r * 2 - (tex2D(_Prev2Tex, i.uv).r * 2 - 1) + ( (tex2D(_PrevTex, half2(i.uv.x+stride.x, i.uv.y)).r * 2 - 1) + (tex2D(_PrevTex, half2(i.uv.x-stride.x, i.uv.y)).r * 2 - 1) + (tex2D(_PrevTex, half2(i.uv.x, i.uv.y+stride.y)).r * 2 - 1) + (tex2D(_PrevTex, half2(i.uv.x, i.uv.y-stride.y)).r * 2 - 1) - prev.r * 4) * _C); float4 input = tex2D(_InputTex, i.uv); value += input.r; value *= _Attenuation; value = (value + 1) * 0.5; value += _RoundAdjuster * 0.01; return fixed4(value, 0, 0, 1); WaterSurface.shader //波動方程式の解を_WaveTexで受け取り、波による歪み具合を法線に反映 //_WaveTexは波の高さなので、高さの変化量から法線を求める float2 shiftX = { _WaveTex_TexelSize.x, 0 }; float2 shiftZ = { 0, _WaveTex_TexelSize.y }; shiftX *= _ParallaxScale * _NormalScaleFactor; shiftZ *= _ParallaxScale * _NormalScaleFactor; float3 texX = 2 * tex2Dlod(_WaveTex, float4(i.uvNormal.xy + shiftX,0,0)) - 1; float3 texx = 2 * tex2Dlod(_WaveTex, float4(i.uvNormal.xy - shiftX,0,0)) - 1; float3 texZ = 2 * tex2Dlod(_WaveTex, float4(i.uvNormal.xy + shiftZ,0,0)) - 1; float3 texz = 2 * tex2Dlod(_WaveTex, float4(i.uvNormal.xy - shiftZ,0,0)) - 1; float3 du = { 1, 0, _NormalScaleFactor * (texX.x - texx.x) }; float3 dv = { 0, 1, _NormalScaleFactor * (texZ.x - texz.x) }; float3 waveNormal = normalize(cross(du, dv)); i.normal = i.tangent * waveNormal.x + i.binormal * waveNormal.y + i.normal * waveNormal.z; これで外からの影響で波が発生する水面が作れました! 波を発生させる方法 今回、波の動きのシミュレーションの参考先にした記事を踏襲して、記事の著者が開発した InkPainter というアセットを使って波の発生位置を指定しています。 InkPainterとは、Unityでテクスチャペイントを行えるようになるアセットです。 波の発生位置を表すテクスチャを塗り、波動方程式を行うシェーダーの入力テクスチャとして扱っています。 クリック位置が波の発生位置となるように実装しましたが、応用すればキャラクターの足に判定を仕込み、歩く動きに合わせて波を発生させることもできます。 まとめ 光の性質を加味した水面の表現 波動方程式を使った波の表現 を使ってインタラクティブな水面の表現をやってみました。 どちらも現実世界の物理現象を再現する内容で、リアルな表現には物理や科学、気象などについて向き合わないといけないことが多いなと改めて感じました…引き続き精進します! 来週と再来週の金曜日も同じアドベントカレンダーに記事を投稿する予定です。 もしよろしければ、引き続き読んでいただけると嬉しいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity 2021.2からProject Windowでもコピー・カット・ペーストができるようになった

Unity 2021.1までは、Project Windowにおいては、「Ctrl/Cmd+D」による複製(Duplicate)しかできませんでした。 Unity 2021.2からは、Project Windowにおいて、 コピー : 「Ctrl/Cmd+C」 カット : 「Ctrl/Cmd+X」 ペースト : 「Ctrl/Cmd+V」 ができるようになりました。 次のGIFはProject Windowにおいて、コピー&ペーストを行う様子を録画したものです。 次のGIFはProject Windowにおいて、カット&ペーストを行う様子を録画したものです。 Unity 2021.2はまだLTSになっていないので、これから使うという人も多いのではないでしょうか。 直近のUnityは、Editor機能において細かい改善が多数なされています。昔からUnityを使っている人には「これ、不便だったんんだよ−」という点が改善されたり、「ここ、こんなに便利になっていたのかー」という改善も多いと思います。ぜひ、新しいUnityにアップデートして、これらの改善を体験し、開発生活の質を向上させてみてください。 関連記事・動画 Unity for Pro 「Unity 2020 LTS / Unity 2021.1におけるUnity Editorの小さな機能改善集」 Unity Learning Materials 「Unityをもっと使いやすく!Unityカイゼン委員会!」 Unity Learning Materials 「Unity 2021 アップデートまとめ」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む