20210801のUnityに関する記事は7件です。

単独でも活用できる!CorgiEngine/TopDownEngineに同梱されているMMFeedbacksの紹介

この記事は、Unity アセットストア 真夏のアドベントカレンダー 2021 Summer のために書かれた記事です。 Corgi Engine/Topdown Engineに同梱されているMMFeedbacksというアセットが便利なので活用しようという話です。 Corgi Engine/Topdown Engineとは? Corgi EngineはパブリッシャーMore Mountainsが作った2D Platformerのテンプレートエンジンです。 TopDown EngineはパブリッシャーMore Mountainsが作ったTopDownビューゲームのテンプレートエンジンです。 有名なアセットなのでご存じな方も多いと思います。 TopDown Engineについては、「Unityアセット真夏のアドベントカレンダー2021」25日目、のぼるさんの記事でも紹介されるようです。 MMFeedbacksとは? Corgi Engine/TopDown Engine内でゲームのフィードバックをジューシー(Juicy)にしているコア機能になります。Corgi Engine/TopDown Engine内のフィードバックは全てMMFeedbacksで実装されています。 MMFeedbacksはAssetStoreで単体でも販売されています。MMFeedbacksは最近バージョン2.0にメジャーアップデートされて、FEELという名前になっています。MMFeedbacks 2.0がFEELといえます。 MMFeedbacksの最新版はCorgi Engine/TopDown Engineに同梱されているので、別途FEELを買う必要はありません。 FEELは以下の4つで構成されています。 MMFeedbacks MMTools 20個のデモ Nice Vibrationsというアセット MMFeedbacksとMMToolsはCorgi Engine/TopDown Engineに含まれているので、既にCorgi Engine/TopDown Engineのいずれかを所持している人は20個のデモもしくはNice Vibrationsが欲しいときのみFEELを購入するといいでしょう。 ちなみに、Nice Vibrations は PC、コンソール、iPhone、Android ゲームに振動とハプティックフィードバックを追加するためのシンプルかつパワフルなソリューションです。これも単独で販売されています。 以上、説明してきたFEELですが「Unityアセット真夏のアドベントカレンダー2021」21日目のxrdnkさんの記事でも紹介されるようです。より詳しくFEELについて知りたい人は是非チェックしてみてください。 MMFeedbacksを体験する ↓のページにデモがあります。 ↑のデモには3Dを用いたものもあり2Dプラットフォーマー、TopDownゲーム以外にも様々な用途で活用可能であることが分かります。これらのデモはFEELのアセット内にあります。 ↑このデモは特別にCorgi Engine/TopDown Engineにも含まれています。 MMFeedbacksを使ってみる では、実際にMMFeedbacksを使ってみたいと思います。 今回はCorgi Engineに同梱のMMFeedbacksを使ってみたいと思います。TopDown Engineに同梱のMMFeedbacksでも、FEELに同梱のMMFeedbacksでも同じことができます。 まずはCorgi Engine内のアセットをMMFeedbacksとMMToolsだけを残し他のアセットを削除してみます。これでMMFeedbacksだけで動作することが分かると思います。MMToolsは、More Moutainsのアセットを支えるユーティリティツール群なので残しておきます。 Packagesも図示しておきます。CinemachineやPostProcessingは、MMFeedbacks内で使うことがあるのでインストールしておいたほうがいいでしょう。 MMFeedbacksフォルダ内のシーンSequencerDemoを見てみます。 このシーンでは、下図のように立方体がスタイリッシュにジャンプ+回転+音+テクスチャ変更するサンプルが含まれています。 このジャンプ+回転+音+テクスチャ変更のフィードバック内容は、ゲームオブジェクトMMFeedbacks1にアタッチされているMMFeedbacksというスクリプトに記載されています。 このMMFeedbacksにはPosition、Rotation、Note Sound、Material、Chromatic Aberration ... などのフィードバックがすでに追加されています。Positionは移動フィードバック、Rotationは回転フィードバック、Chromatic Aberrationはポストプロセッシングのフィードバックなどです。これらのフィードバックを右側の数字のタイミングで順次再生することでジューシーな見た目にします。 スクリプトの下にはフィードバックを随時再生して確認できるように再生ボタンが備わっています。 では、MMFeedbacks内の各種フィードバックの中のPositionフィードバックについて見てみます。 Active, Label, Chance, Timingまでが全Feedback共通の設定項目です。 Active: このフィードバックを実行するかをOn/Offで設定します。一時的にフィードバックをオフにしたいときに便利です。 Label:このフィードバックの名前を設定します。わかりやすい名前を付けましょう。 Chance:このフィードバックを一定の確率で実行したい場合に設定します。常に実行したい場合は100%にしておきましょう。 Timing:フィードバックを実行するタイミングをここで調整します。ここの調整がフィードバックのジューシーさに影響するのでしっかり設定しましょう。(Timingの設定詳細は割愛します。タイミングの調整はInitial Delayで調整します。) Timingより下の設定項目は各フィードバック固有の設定項目です。Positionでれば物体をどう動かすか、Materialであればどのようなマテリアルを物体に適用するかなどを設定します。 次に、MMFeedbacks内のSettingsの項目を見てみます。このSettingsではMMFeedbacks全体の設定を行います。 Initialization > Safe Mode MMFeedbacksが内部的に行っているSerializationのエラーチェックをいつ行うかを表します。Fullが推奨されています。 Initialization > Initialization Mode Runtime時の初期化をいつ行うかを表します。Awake実行時、Start実行時、Scriptでの3パターンがあります。Scriptを選ぶと自分で初期化する必要があります。 Initialization > Auto Play On Start Start実行時にフィードバックを発動させたい場合にオンにします。 Initialization > Auto Play On Enable OnEnable実行時にフィードバックを発動させたい場合にオンにします。 Direction > Direction 実行方向を指定します。上から下へ(Top To Bottom)、下から上へ(Bottom To Top)の2種類あります。 Direction > Auto Change Direction On End チェックを入れると実行ごとに実行方向が入れ替わります。(Top To Bottom ←→ Bottom To Top) Intensitiy > Feedbacks Intensity フィードバック全体の強さを調整します。強さの概念がないフィードバックや強さの意味合いが意図するものと違ったりするので使えないこともあります。 Timing > Duration Multiplier フィードバック全体の時間をスケールします。2を指定すると通常の2倍の時間をかけてフィードバックを実行します(スローになります)。 Timing > Full Duration Details チェックを入れると、Durationの情報表示がより詳細に表示されます。Delay時間+再生時間など分かれて表示されます。 Timing > Cooldown Duration フィードバック全体が実行された後に、再度実行可能になるまでの時間を設定します。 Timing > Initial Delay フィードバック全体を遅延実行するときの遅延時間を設定します。 Events > Trigger MM Feedbacks Events チェックを入れると、MMFeedbacksEventsを使ったEvent処理を追加できます。MMFeedbacksEventsはデリゲートベースのEvent処理を提供しています。詳しくはMMFeedbacksEvents.csを参照してください。 Events > Trigger Unity Events チェックを入れると、フィードバック実行時にUnityイベントが設定されていれば実行します。チェック項目の下にOnPlay()、OnPause()、OnResume()、OnRevert()、OnComplete()のUnityEventが用意されています。ここにUnityEventを設定すれば実行されます。 MMFeedbacksのフィードバックリスト MMFeedbacksには100以上のフィードバックが既に備わっています。どのようなフィードバックが利用可能なのかは↓のページをご確認ください。 MMFeedbacksをスクリプトから実行する MMFeedbacksをスクリプトから実行するには、MMFeedbacksの関数PlayFeedbacks()を実行するだけです。 以下の様なコードを書くことで実行可能です。 MMFeedbacksTest.cs using UnityEngine; using MoreMountains.Feedbacks; public class MMFeedbacksTest : MonoBehaviour { [SerializeField] private MMFeedbacks mmFeedbacks; public void Test() { mmFeedbacks.PlayFeedbacks(); } } MMFeedbackの機能を自作する 自分が作った機能や他のアセットをMMFeedbacksに組み込みたいということもあるでしょう。そのような場合は自分でMMFeedbackを自作します。 MMFeedbackの自作はとても簡単です。MMFeedbackを継承したクラスを作り(MMFeedbacksでないことに注意。sはつけない)、必要な関数をオーバーライドすればいいだけです。 qiita.rb using UnityEngine; using MoreMountains.Feedbacks; [AddComponentMenu("")] [FeedbackPath("ChosenPath/MyFeedbackNameGoesHere")] public class MMFeedbackMyFeedbackNameGoesHere : MMFeedback { // declare your variables here // TODO:ここにフィードバックに必要な変数を定義する protected override void CustomInitialization(GameObject owner) { base.CustomInitialization(owner); if (Active) { // Put custom initialization code here // TODO:ここにフィードバックの初期化コードを書く } } protected override void CustomPlayFeedback(Vector3 position, float attenuation = 1.0f) { if (Active) { // Put custom play code here // ここにフィードバックの実行内容を書く } } protected override void CustomStopFeedback(Vector3 position, float attenuation = 1) { base.CustomStopFeedback(position, attenuation); if (Active) { // Put custom stop code here // ここにフィードバックが停止した時のコードを書く } } protected override void CustomReset() { base.CustomReset(); if (Active) { // Put custom reset code here // ここにフィードバックをリセットした時のコードを書く // リセットはフィードバックをPlayした直後に呼ばれるようです } } } まとめ というわけでCorgi Engine/TopDown Engineに同梱されているMMFeedbacksというアセットについて解説してみました。 Corgi EngineやTopDown Engineを持っているけどまだ使ってないという方でも、MMFeedbacksだけでも3Dのプロジェクト等のどんなプロジェクトでも活用できますので是非使ってみてください! 「Unity アセット真夏のアドベントカレンダー 2021 Summer! 」明日はたなかゆうさんによる「Voxel ImporterでMagicaVoxelのボクセルを読み込んで透過させたり光らせたり」です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ScriptableRenderPass.OnCameraSetup() と ScriptableRenderPass.Configure() の呼び出し比較

Unity2021.1.16f1で ふとタイトルの OnCameraSetup() と Configure() を調べてみると困ったことに、コメントの記述がほぼ同じになっていた /// <summary> /// This method is called by the renderer before rendering a camera /// Override this method if you need to to configure render targets and their clear state, and to create temporary render target textures. /// If a render pass doesn't override this method, this render pass renders to the active Camera's render target. /// You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>. /// </summary> /// <param name="cmd">CommandBuffer to enqueue rendering commands. This will be executed by the pipeline.</param> /// <param name="renderingData">Current rendering state information</param> /// <seealso cref="ConfigureTarget"/> /// <seealso cref="ConfigureClear"/> public virtual void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {} /// <summary> /// This method is called by the renderer before executing the render pass. /// Override this method if you need to to configure render targets and their clear state, and to create temporary render target textures. /// If a render pass doesn't override this method, this render pass renders to the active Camera's render target. /// You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>. /// </summary> /// <param name="cmd">CommandBuffer to enqueue rendering commands. This will be executed by the pipeline.</param> /// <param name="cameraTextureDescriptor">Render texture descriptor of the camera render target.</param> /// <seealso cref="ConfigureTarget"/> /// <seealso cref="ConfigureClear"/> public virtual void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) {} 引数のコメントは違うけどsummaryはほぼ同じ…。違いと言えば、 OnCameraSetup は This method is called by the renderer before rendering a camera   → カメラの描画前 Configure は This method is called by the renderer before executing the render pass. → パスの実行前 せっかくなので呼ばれている箇所を比較してみた。 ScriptableRenderPass.OnCameraSetup() の呼び出しスタックトレース UniversalRenderPipeline.RenderSingleCamera() ScriptableRenderer.Execute() ScriptableRenderer.InternalStartRendering() ScriptableRenderPass.OnCameraSetup() ScriptableRenderPass.Configure() の呼び出しスタックトレース UniversalRenderPipeline.RenderSingleCamera() ScriptableRenderer.Execute() ScriptableRenderer.ExecuteBlock() ScriptableRenderer.ExecuteRenderPass ScriptableRenderPass.Configure() 呼び出される順番を見る お互いが呼び出されている ScriptableRenderer.Execute() を見てみます ScriptableRenderer.csの一部抜粋 public void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { ://コマンドバッファの初期化など using (new ProfilingScope(cmdScope, profilingExecute)) { InternalStartRendering(context, ref renderingData);//←OnCameraSetup()! ://deltaTime周りの準備 ://RenderStateの準備など ://Lightの準備など ://RenderStateの準備など using (new ProfilingScope(cmd, Profiling.RenderBlock.beforeRendering)) { // Before Render Block. This render blocks always execute in mono rendering. // Camera is not setup. Lights are not setup. // Used to render input textures like shadowmaps. ExecuteBlock(RenderPassBlock.BeforeRendering, in renderBlocks, context, ref renderingData); //←ここでConfigure()! } ://この後もRenderPassBlock毎にExecuteBlock()が連なっている InternalFinishRendering(context, cameraData.resolveFinalTarget);//←OnCameraSetup()と対のOnCameraCleanup()はここで呼ばれる } } という事で、順番としては OnCameraSetup() RenderPassBlock.BeforeRendering     に含まれるRenderPassEventのPassのConfigure() RenderPassBlock.MainRenderingOpaque   に含まれるRenderPassEventのPassのConfigure() RenderPassBlock.MainRenderingTransparent に含まれるRenderPassEventのPassのConfigure() RenderPassBlock.AfterRendering      に含まれるRenderPassEventのPassのConfigure() OnCameraCleanup() な感じ。 描画時最初に行いたいカメラ設定はOnCameraSetup()で、その後にRenderPassBlockごとに行う調整はConfigure()で行うのが良さそうです 追記 RenderPassBlockについては別の記事を書きました↓
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityカスタムシェーダでPOINTSIZEを変更する。

UnityカスタムシェーダでPOINTSIZEを変更する。 DirectからOpenGL系に変更をする。 Shader "Custom/VertexColor" { Properties{ _Size("ShaderSize", Float) = 1.000000 _PointSize("PointSize", Float) = 5.0 } SubShader{ Pass { LOD 200 CGPROGRAM #pragma vertex vert #pragma fragment frag uniform float _PointSize; struct VertexInput { float4 v : POSITION; float4 color: COLOR; }; struct VertexOutput { float4 pos : SV_POSITION; float4 col : COLOR; float psize : PSIZE; }; VertexOutput vert(VertexInput v) { VertexOutput o; o.pos = UnityObjectToClipPos(v.v); o.col = v.color; o.psize = _PointSize; return o; } float4 frag(VertexOutput o) : COLOR { return o.col; } ENDCG } } } using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class PointCloud : MonoBehaviour { private Mesh mesh; int numPoints = 600000; // Use this for initialization void Start() { mesh = new Mesh(); GetComponent<MeshFilter>().mesh = mesh; CreateMesh(); } void CreateMesh() { Vector3[] points = new Vector3[numPoints]; int[] indecies = new int[numPoints]; Color[] colors = new Color[numPoints]; for (int i = 0; i < points.Length; ++i) { points[i] = new Vector3(Random.Range(-10, 10), Random.Range(-10, 10), Random.Range(-10, 10)); indecies[i] = i; colors[i] = new Color(0,0,1, 1.0f); } mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; mesh.vertices = points; mesh.colors = colors; mesh.SetIndices(indecies, MeshTopology.Points, 0); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】NatShareでツイート後に報酬を付与する #アセットアドカレ2021

はじめに 「Unity アセット真夏のアドベントカレンダー 2021 Summer!」 2日目の記事です。1日目はラズさんによる「【アセット紹介】Grabbitでオブジェクトを配置する【Unity】」でした。 今回紹介するアセットはこちら。簡単にSNS投稿や画等付きツイートが実装でき、無料で使えます(記事執筆時点)。 このNatShareを使って、ツイートをした時点で報酬を付与する方法を紹介します。 よくある実装ではツイート画面を開いた時点で付与自体が確定するのでキャンセルしても付与されてしまうのですが、NatShareではシェアの成否に応じた処理の切り分けをすることができます。そこまで厳密な方法ではないのですが、簡単に実装できるのでおすすめです。 環境 NatShare 1.2.3 Asset Store ドキュメント GitHub macOS 11.4 iOS 14.6 UniTask 2.2.5(サンプルで使用) 実装 payload.Commit()でSNS共有ができるのですが、非同期で実装されていて成否がbool値で返ってきます。キャンセルの時はfalseが返ってくるので、trueの時だけ報酬付与処理を実行すればOKです。下記が実装例です。 // 実装例 shareButton.onClick.AddListener(async () => { var shareText = $"共有するテキスト"; #if !UNITY_EDITOR var payload = new SharePayload(); payload.AddText(shareText); var result = await payload.Commit(); // キャンセルされた場合は何もしない if (!result) return; #else // Editor上では決め打ちで実行 await UniTask.Delay(TimeSpan.FromSeconds(3f)); #endif // SNS共有後のリワード付与処理など }); 共有先の指定ができないので実際にはツイッター以外でも成否判定がなされてしまうのですが、かなりライトに実装ができるので活用できる場面は多いんじゃないかなと思います。 最後に 最後に宣伝なのですが、この記事で紹介した方法は「ワンナイト人狼オンライン」というアプリの開発で使用しています。よかったらぜひ遊んでみてください! Twitterもやっているのでよかったらフォローお願いします! https://twitter.com/nkjzm 「Unity アセット真夏のアドベントカレンダー 2021 Summer!」 明日の担当はさとやさんによる「単独でも活用できる!Corgi Engine/TopDown Engineに同梱されているMMFeedbacksの紹介」です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Symbolアカウントにメタデータ登録するバックエンドをNapkinで自作する※Unity用

はじめに 本記事ではタイトル通り、Symbolアカウントにメタデータ登録するためのバックエンドをNapkinで自作する方法を解説します。Symbolとはブロックチェーン技術の一つで、APIを使って簡単にデータの取得、または各種SDKを使ってトランザクション等を容易に作成できるブロックチェーンエンジニア以外におすすめするブロックチェーンです。 私の目的はUnityとSymbolを掛け合わせ、ブロックチェーンゲームを作るためにこれを作成しましたが、(C#のSDKは開発中で今はまだない※2021年7月時点)どんな言語でもHttp通信ができれば使えるかと思います。 そもそもの目的は、アクションゲームやRPGなど、何かしらパラメータを持った武器や防具をハクスラのようにランダムで作成、もしくは、何かしらのレアアイテム所持をブロックチェーンに刻む。かつその武器を譲渡や販売などができる仕組み作成のためです。また、譲渡を容易にすることで、そのアイテムが無ければクリアできないステージなどを友達と協力することでクリアが近づくことも可能です。ツヨツヨ武器をプレゼントする、ステージクリアの証明書を自慢する、、など。 もともとの着想を得た記事はこちらになりますのでぜひご一読いただければと思います。 ちょっと事前理解がないとこれから作成するものが何者なのか分からないと思うので、少し説明します。 Symbolではアカウントにメタデータを登録することができます。また、そのメタデータには何かのアカウントから署名することができるので、例えばゲーム本体のアカウントが署名すれば、その武器などはゲームが認めたものとなり、ゲーム上で使用することを許可できます。つまり、メタデータ自体は誰でも登録できますが、ゲームアカウントの署名が無ければ認めなければいい、それだけです。これはゲームに限らず、例えば卒業証書などにも活用できる事例かと思います。 続いて、マルチシグと呼ばれる機能を使えば、その武器アカウントの所有者を決めることができます。武器アカウント自体は意思を持たず、その所有者であるプレイヤーアカウントのみが譲渡等を行えます。 ゲーム側としては、プレイヤーに紐づくマルチシグアカウントを全て取得し、かつゲーム本体が署名した武器や防具を武器庫に入れる、といったことが可能になります。 そして本記事では、ゲーム上で生み出された武器や防具のパラメータ、プレイヤーの秘密鍵を暗号化して送信することで、(ほかにもAPIKeyなど) ・ゲームアカウントの署名付きパラメータをSymbolブロックチェーンに刻む ・その武器アカウントの所有者をプレイヤーとする。 ところまでを解説します。 実は、Symbolの機能であるアグリゲートトランザクションを使えば、これらのトランザクションを一つにまとめ一度のアナウンスで完結することができます。また手数料の支払いもどこかのアカウントにまとめることができます。なお、今回はプレイヤーが全ての手数料を負担することとします。 準備するもの ■Symbolデスクトップウォレット こちらの記事を参考にお持ちでない場合はインストールしてください https://note.com/nembear/n/n56d2c9a28e8a ※ただし、本番稼働するまではテストネットで進めましょう テストネット上で、 ・Gameアカウント ・Playerアカウント をそれぞれ準備しておきましょう。 ■Napkinアカウント こちらを参考に作成しておいてください https://paiza.hatenablog.com/entry/2021/07/21/130000 当初、FireBaseFunctionsで作成していましたが、このNapkinに出会い、くっそ簡単にブラウザだけでAPIができたので、こちらを使うことにしました。 Napkinが準備できたら以下のモジュールをインストールしてください ・symbol-sdk ・crypto 続いて、 ・APIKEY ・GAMEPRIVATEKEY ・AESKEY をそれぞれ環境変数としてNapkinに登録してください。方法は、先ほどのNapkin紹介記事に書かれてあります。 APIKEYは、誰でもこのAPIを使えると良くないのでGETなどでこのAPIキーを渡された場合のみ処理するようにします。特にこだわりはありませんのでパスワード生成ツールなどである程度複雑な文字列を用意してください。 GAMEPRIVATEKEYは、Symbolウォレットからゲームアカウントの秘密鍵を取得し、登録してください。 最後にAESKEYですが、ゲーム側からプレイヤーの秘密鍵をhttp通信で送るので、ちょっと怖いなと思いますし、暗号化して送ります。そのためそれを複合するためのキーで、16桁の英数字で作成してください。 準備が長くなりましたが、以下コードの解説に進みます。 コード解説 まずは全文を貼り付けますので、この後簡単にブロックごとに解説します。 const symbol = require('symbol-sdk'); const crypto = require('crypto'); const AESKEY = process.env.AESKEY; const IV = 'abcdef1234567890' const key = symbol.KeyGenerator.generateUInt64Key('game'); const nodeUrl = 'https://sym-test-01.opening-line.jp:3001'; const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl); const networkGenerationHash = '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155'; function createCipher(mode) { return crypto[mode]('aes-128-cbc', KEY, IV) } function decrypt(text) { const buf = Buffer.from(text, 'base64') const cipher = createCipher('createDecipheriv') const decrypted = cipher.update(buf); return Buffer.concat([decrypted, cipher.final()]).toString('utf-8') } export default async (req, res) => { if(req.query.apikey !== process.env.APIKEY) { throw TypeError("Wrong APIKEY ERROR"); } else { let networkType = symbol.NetworkType.MAIN_NET; if (req.query.mode == "TEST_NET") { networkType = symbol.NetworkType.TEST_NET; } const gamePrivateKey = process.env.GAMEPRIVATEKEY; const gameAccount = symbol.Account.createFromPrivateKey(gamePrivateKey, networkType); const playerPrivatekey = decrypt(req.query.pkey); const playerAccount = symbol.Account.createFromPrivateKey(playerPrivatekey, networkType); const weaponAccount = symbol.Account.generateNewAccount(networkType); const type = req.query.type; const id = req.query.id; const value = JSON.parse(req.query.value); const metaData = JSON.stringify({ "type": type, "id": id, "value": value }) const epochAdjustment = await repositoryFactory .getEpochAdjustment() .toPromise() const multisigAccountModificationTransaction = symbol.MultisigAccountModificationTransaction.create( symbol.Deadline.create(epochAdjustment), 1, 1, [playerAccount.address], [], networkType, ); const gameAccountMetadataTransaction = symbol.AccountMetadataTransaction.create( symbol.Deadline.create(epochAdjustment), weaponAccount.address, key, metaData.length, metaData, networkType, ) const aggregateTransaction = symbol.AggregateTransaction.createComplete( symbol.Deadline.create(epochAdjustment), [ multisigAccountModificationTransaction.toAggregate(weaponAccount.publicAccount), gameAccountMetadataTransaction.toAggregate(gameAccount.publicAccount) ], networkType, [], symbol.UInt64.fromUint(2000000), ); const signedTransactionNotComplete = playerAccount.sign( aggregateTransaction, networkGenerationHash, ); const cosignedTransactionWeapon = symbol.CosignatureTransaction.signTransactionPayload( weaponAccount, signedTransactionNotComplete.payload, networkGenerationHash, ); const cosignedTransactionGame = symbol.CosignatureTransaction.signTransactionPayload( gameAccount, signedTransactionNotComplete.payload, networkGenerationHash, ); const cosignatureSignedTransactions = [ new symbol.CosignatureSignedTransaction( cosignedTransactionGame.parentHash, cosignedTransactionGame.signature, cosignedTransactionGame.signerPublicKey, ), new symbol.CosignatureSignedTransaction( cosignedTransactionWeapon.parentHash, cosignedTransactionWeapon.signature, cosignedTransactionWeapon.signerPublicKey, ), ]; const rectreatedAggregateTransactionFromPayload = symbol.TransactionMapping.createFromPayload( signedTransactionNotComplete.payload, ); const signedTransactionComplete = playerAccount.signTransactionGivenSignatures( rectreatedAggregateTransactionFromPayload, cosignatureSignedTransactions, networkGenerationHash, ); const transactionHttp = repositoryFactory.createTransactionRepository(); const result = await transactionHttp.announce(signedTransactionComplete).toPromise(); console.log(result) } } 以上が全文です。そのまま貼り付けても使えるかとは思います。 PostManなどで動作確認をしてみてください。 ただし、Playerの秘密鍵を復号(関数decrypt)することが前提なので暗号化せずにテストする場合は、 const playerPrivatekey = req.query.pkey; のようにしてそのまま秘密鍵を受け取るようにしてください。 また私の場合は、type id valueの3つのパラメータを受け取り、それを文字列に変換しメタデータとして登録しましたので、そこは自由に変更していただければと思います。このまま使う場合はパラメータは pkey プレイヤーのプライベートキー暗号化、もしくは生のまま apikey 環境変数で設定したAPIKEY mode メインネットかテストネットで使用するか type ゲームの仕様によりますが、私の場合は例えばweapon,item,certificateなどを想定しています。 id 同じく仕様次第です。例えば武器名やクリアしたステージ名など。 value こちらは武器の攻撃力など。射程距離など2つ以上の場合は、JSONで渡します。 例) "value": { "power": 58, "reach": 5.12 } このように英数字以外を使用する場合、URLエンコードをしてから送信することを忘れずに。 以下、簡単にブロックごとに解説。 const symbol = require('symbol-sdk'); const crypto = require('crypto'); const AESKEY = process.env.AESKEY; const IV = 'abcdef1234567890' const key = symbol.KeyGenerator.generateUInt64Key('game'); const nodeUrl = 'https://sym-test-01.opening-line.jp:3001'; const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl); const networkGenerationHash = '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155'; 使用するモジュールはsymbol-sdkとcryptoのみ。 復号のためのAESKEYは環境変数にあります。 IVとは暗号化、復号するときの方向?らしいです。これは公開されても良いもので、暗号化するときと復号は同一のIVである必要があります。 const key = symbol.KeyGenerator.generateUInt64Key('game'); Symbolのメタデータ登録はKEYとVALUEがセットで登録でき、KEYは今回は統一しています。これは使い方を考えれば色々できそうですが、自分で使う時に極力シンプルに使いたくこのようにしました。 NODEはOpeningLine社のテストネットをお借りしています。 function createCipher(mode) { return crypto[mode]('aes-128-cbc', KEY, IV) } function decrypt(text) { const buf = Buffer.from(text, 'base64') const cipher = createCipher('createDecipheriv') const decrypted = cipher.update(buf); return Buffer.concat([decrypted, cipher.final()]).toString('utf-8') } こちらは復号のための関数ですが、おまじないと思って使えば良いと思います。(私も完璧に理解していません) if(req.query.apikey !== process.env.APIKEY) { throw TypeError("Wrong APIKEY ERROR"); } else { let networkType = symbol.NetworkType.MAIN_NET; if (req.query.mode == "TEST_NET") { networkType = symbol.NetworkType.TEST_NET; } const gamePrivateKey = process.env.GAMEPRIVATEKEY; const gameAccount = symbol.Account.createFromPrivateKey(gamePrivateKey, networkType); const playerPrivatekey = decrypt(req.query.pkey); const playerAccount = symbol.Account.createFromPrivateKey(playerPrivatekey, networkType); const weaponAccount = symbol.Account.generateNewAccount(networkType); const type = req.query.type; const id = req.query.id; const value = JSON.parse(req.query.value); const metaData = JSON.stringify({ "type": type, "id": id, "value": value }) APIキーが正しく無ければエラーを返します。 APIキーが正しければ、テストネットかメインネットを判別します。デフォルトはメインネットにしています。 それぞれ、渡されたパラメータを変数に格納します。その際に、ゲームアカウントとプレイヤーアカウントは秘密鍵から作成し、武器アカウントは新たに作成しています。 さて、ここからはSymbolの話です。正直、私も理解するまでに時間はかかりましたが、それでもやってることの凄さを考えれば非常にシンプルだと思います。特に最初は理解しがたいことも多いと思いますが、根気よく以下WEBサイトとにらめっこしていれば段々理解できてきますので、ぜひ何度も読まれることをおすすめします。とは言え、ちんぷんかんぷんで手を出さないぐらいなら、読むのをやめてコピペで遊ぶことを推奨します、もちろんテストネットで!! ↑ガイドのあたりおすすめ const multisigAccountModificationTransaction = symbol.MultisigAccountModificationTransaction.create( symbol.Deadline.create(epochAdjustment), 1, 1, [playerAccount.address], [], networkType, ); まずはプレイヤーアカウントが武器アカウントの署名者になるのですが、(武器アカウントをマルチシグアカウントとする)そのための準備です。この段階ではプレイヤーアカウントが一人だけとあるアカウントの署名者になるよ!って宣言しているイメージです。そのとあるアカウントはのちほど決定します。 1,1ってのはそのマルチシグアカウントに必要な意思決定数は一人で、かつ管理者も一人という意味でいわゆる1of1というやつです。(このへん意味不明なら今は飛ばして良し) const gameAccountMetadataTransaction = symbol.AccountMetadataTransaction.create( symbol.Deadline.create(epochAdjustment), weaponAccount.address, key, metaData.length, metaData, networkType, ) 武器アカウントに対してメタデータを登録します。読めば理解できる箇所。ただし署名者は↑と同じく後ほど。 const aggregateTransaction = symbol.AggregateTransaction.createComplete( symbol.Deadline.create(epochAdjustment), [ multisigAccountModificationTransaction.toAggregate(weaponAccount.publicAccount), gameAccountMetadataTransaction.toAggregate(gameAccount.publicAccount) ], networkType, [], symbol.UInt64.fromUint(2000000), ); さて、上の2つのトランザクションを一つのトランザクションにまとめるためにアグリゲートトランザクションとします。ここがSymbolの強みの一つで、最大100個のトランザクションをとりまとめることができます。(記事を探すとだいたい1000ってなってるけど多分100が最大) 上の2つのトランザクションを配列で格納していますが、それぞれ署名者を引数としていることを確認してください。また、この段階では正しくは署名していません。publicAccountとなっています。つまり、ここまでのトランザクションは秘密鍵を知っていなくても誰でもできます。 ちょっとここからは私も理解度が100%じゃないところに入っていきます。フォローできる方がいればフォローして欲しい。。。特に言葉としての表現が正しいかが不安。 const signedTransactionNotComplete = playerAccount.sign( aggregateTransaction, networkGenerationHash, ); const cosignedTransactionWeapon = symbol.CosignatureTransaction.signTransactionPayload( weaponAccount, signedTransactionNotComplete.payload, networkGenerationHash, ); const cosignedTransactionGame = symbol.CosignatureTransaction.signTransactionPayload( gameAccount, signedTransactionNotComplete.payload, networkGenerationHash, ); 上記3つはそれぞれのアカウントの署名。 一番上は先ほど作成したアグリゲートトランザクションに対して手数料を負担するプレイヤーの署名。 下2つは武器アカウントとゲームアカウントがプレイヤーアカウントが署名した不完全なトランザクションに署名。つまりこの時点でここで出てくる3つのアカウントの署名が揃ったことになります。 const cosignatureSignedTransactions = [ new symbol.CosignatureSignedTransaction( cosignedTransactionGame.parentHash, cosignedTransactionGame.signature, cosignedTransactionGame.signerPublicKey, ), new symbol.CosignatureSignedTransaction( cosignedTransactionWeapon.parentHash, cosignedTransactionWeapon.signature, cosignedTransactionWeapon.signerPublicKey, ), ]; const rectreatedAggregateTransactionFromPayload = symbol.TransactionMapping.createFromPayload( signedTransactionNotComplete.payload, ); const signedTransactionComplete = playerAccount.signTransactionGivenSignatures( rectreatedAggregateTransactionFromPayload, cosignatureSignedTransactions, networkGenerationHash, ); ここで最後にプレイヤーアカウントの管理のもと、全てのトランザクションをコンプリートとしてぜーんぶひとまとめになりました。(手数料についてはここでのアカウントが負担するのかもしれない、、) signedTransactionComplete これですね。 const transactionHttp = repositoryFactory.createTransactionRepository(); const result = await transactionHttp.announce(signedTransactionComplete).toPromise(); console.log(result) 最後にネットワークにアナウンスしておしまいです。 承認されれば、 ・武器アカウントを作成し、プレイヤーアカウント管理のもとマルチシグアカウントとする ・武器アカウントに、攻撃力などのメタデータが、ゲームアカウントの署名のもと登録される これらが実現します。 さいごに 以上、こちらの記事を終えます。おそらく間違っている表現などがあるかもしれませんので随時加筆修正したいと思います。ちょっと不安に思っているのはセキュリティ面です。まず、プレイヤーについては多くXYMを所持しないことは大切かと思いますし、そのあたりもっといい方法を思いつく方がいらっしゃいましたらぜひアドバイスいただければ幸いです。 また、こちらで登録したメタデータをどのように取得するかは次回の記事にします。あとは、ゲーム上で取得した武器のパラメータをこのAPIに投げる。そして、登録された武器を次回書く記事を使って取得すればゲームとブロックチェーンの融合は最低限完成します。 なお、もしC#のSDKが開発されれば、バックエンドをわざわざ作成する必要はなく、Unity内のみでこれらが完結する予定です。そもそも自分で必要な箇所だけを開発すればいいのかもしれませんが私にそんなスキルはありません。 この記事にたどり着いた方がいらっしゃいましたら、自分なりの活用を色々考えてみてください。 もし、あるステージをクリアした方のみが割引を受けられるサービスがあったら面白いと思いませんか?このゲームで取得した武器のパラメータやレア度を違うゲームで引き継げたら?またはリアルとの融合で、脱出ゲームやその他ゲームで使えたら? SymbolならHttp通信さえできれば、データの取得は非常に容易なため、こういった派生は比較的簡単にできるかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

23万ポリという数字自体に囚われるのは良くない――かといって重くないわけでもないという話

今北産業 VRChat向けの23万ポリの衣装が「重いのではないか」と騒がれている でも正直「23万ポリ」のインパクトで騒いでる人も居る印象を受けますし、実際私も初めは印象論でツイートしました(ごめんなさい) 印象論で騒ぐのは良くないので建設的な形で意見をまとめて送りたかったですし、間違った認識の元「重い」という評判が広まるのはよろしくないと思います。重いなら重いで「何が、どのように重いのか」を指摘した方が建設的です まとめていたら思いの他知見として有用そうだったので、折角なので公開記事としてまとめます(4行目) 以下、スクリーンショット等に含まれる3Dモデルは以下の製品か、それを私的に加工したものです。 Only4U 「【リアル服】only4Uクラファン開催記念無料配布衣装【セフィラ・幽狐族のお姉様・ここあ用】ver1.01」 https://only4u.booth.pm/items/3140313 2021/8/21時点でのVRChatの指定UnityバージョンであるUnity2018.4.20f1を利用しています。 結論から 高負荷なモデルですが、「23万ポリ」の他に、より大きな負荷の原因があります 全体的に、Unityでのリアルタイムレンダリングの知見が不足していることに起因するのではないかなあと思っています 折角のフォトリアルなお洋服なのに、人の多いインスタンスで使うと使用者が嫌われかねない現状はあまり嬉しくありません Clothコンポーネントは本来リアルタイムレンダリングでの使用に耐えうるほど高速ですが、8000ポリゴンはちょっと多いと思います 目安は2000頂点, Solver Frequency 300Hzで12個だそうです。インスタンスに何人居るかにもよりますが、12人以上が居るインスタンスは珍しくないのでこれより低く抑えることが望ましいでしょう この基準がどのような環境を意図したものはわかりませんが、一般にVR向けの表現は通常のデスクトップアプリケーションよりも厳しいパフォーマンス要件を課されます その他、いくつか見た目を全く変化させずにパフォーマンスを向上させられる要素があったのでそちらも記載します ただこれらはこのモデルが特に悪いとかではなく、同様の要素は他の作者のモデルでも目にすることがあります Marvelousで吐いたモデルをリトポしていないのはちょっとよくわからなかったです(工数の問題?) 2021/8/3追記:お仕事としてこのモデルの改善に携わることになりました! Qiitaを出した後即日お声掛け頂き、色々ご説明をさせていただいておりました。いかんせんまだ1営業日しか経っていないのでどういった関わり方になるかはちょっと不確定でございます。お話しした感じやはりと言うべきかリアルタイムレンダリング環境でのクオリティの上げ方に対して認識違いがあっただけでクオリティに対する誠意は非常に感じましたので、これから良くなっていくのではないかなと思っています。 前提 出来るだけソースを当たり正確性の担保に努めていますが、不正確な内容が含まれるかもしれません Twitterまで教えて頂けると大変助かります(@AomeeVR) そもそも私は最適化がそんなに好きではありません 私のアバターは大体Poorで、殆ど最適化されていません 面倒ですよね Avatar Performance Rankingの指標自体が必ずしも適切ではない現状もありますから、適切な設定が報われるわけでもない…… 私は行列や線形代数を理解していないため、Unityのレンダリングに関する知識は表層的なものに留まります アバター改変が初めての読者の方でも読めるよう、基本的な内容から解説を行うよう気を付けています パラレルマーケットというバーチャル展示会を主催している人間なので、たまにその入稿ルールの話が出てきます。何故かというと、各種バーチャル展示会の入稿ルールは要は会場を重くしないために最低限必要な内容が設定されるものだからです。 Profiler 簡単に言うと、「何に、どれだけ」計算時間がかかっているのかがわかるUnityの便利機能です Profilerを活用したパフォーマンス確認 こんな感じでMain Cameraの前に当該衣装を置き、Realtime Directional Light1灯で照らしただけのSceneを用意しました。 既に画面にいくつか専門用語が出てきているので特に重要な二つについて簡単に説明すると、 「Batches」は「描画命令が1F(フレーム)に何セット飛んだか」 「SetPass calls」は「描画環境の切り替えが1Fに何回行われているか」 という数値です。これらは描画負荷に関する簡便な指標で、ざっくり言えばこれらの数値が大きい程重いわけです。 といっても数値の感覚がわからないと思うので例を出すと、パラケットやVket、クロマケのブースの制限はBatches:30、SetPass calls:20です。このお洋服は大体ブース1/3~1/2個分ということですね。 早速Editor上で実行して、Profilerの表示のうち一部を切り出して張り付けてみました。CPU処理についての部分です。 重要なのはこれらの処理に何ms掛かったのかという絶対値ではなく、他と比べてその処理がどのくらい長いのか短いのかという相対値です。本質的には「何がボトルネックなのか」が重要なのであり、その項目を改善するのが効果的なわけですね。 右側の「EditorLoop」はUnityエディタ自体の負荷なので実際の環境では消滅します。ですから、「PlayerLoop」の中で何に時間がかかっているの? ということを見れば良いようです。 もうお分かりと思いますが、Physics.UpdateClothがPlayerLoopのうち約82%を占めています。これはClothコンポーネントの処理です。じゃあ「Clothは重い」ということで済ませてしまっても良いのでしょうか? Clothは重い? 本当でしょうか。Clothには様々な設定項目がありますが、試しにこれを変化させてみましょう。 たくさんの項目がありますね。それぞれの項目についての解説は以下の記事がとても参考になりました。 記事を読む限り、 Use Continuous Collision Solver Frequency Self Collision これらの3項目が特にパフォーマンスに影響しやすいようです。 まずSelf Collisionは編集画面の時点でUnity Editorがカクついてまともに操作できませんでした。 上記記事によれば 計算の大部分を占める可能性がある だそうです(孫引き)。少なくともこのようなハイポリゴンなClothでは利用しない方がいいでしょう。 Use Continuous Collisionはオンにすることで計算量が2倍になる項目です。 高速で移動する物体の計算精度が向上するようですが、「高速」の定義がよくわかりませんね。 切ると平均で0.5ms程度計算時間が短縮されました。試しに衣装を高速移動させてみましたが、特に見た目の違いがあったようには見えませんでした。衣装の形状にもよるのかもしれません。 Solver Frequencyは通常は120~300で設定されるようで、320という値はそう離れていないように思えます。 単位がHzということは、一秒間に何度計算が走るかというパラメーターなのでしょう。 試しに120まで落としてみたところ、平均して2msの改善が見られました。元が大体3~8msの処理ですから、中々の改善です。 ただ、このモデルの場合はそれなりに見た目への影響がありました。左が変化前(320Hz)、右が変化後(120Hz)です。 右側の見た目はちょっと頂けないですね。計算頻度が変わったことにより、前開きの広さが変わってしまっています。 この値を下げるにはメッシュを編集する必要がありそうです。 なので、どうせならということでリトポをしてみました。 リトポロジーとは、メッシュをより立体構造に沿った形状にすることで、より効率的なデータとすることを言います。 左半分をリトポしてミラーしただけなので向かって右半分が破綻していますが、左半分だけでも負荷検証には事足りるでしょう。 左がリトポ前、右がリトポ後のパフォーマンスです。今回は頂点数、ポリゴン数共におよそ1/4まで削減を行いました。 Solver Frequencyは120を設定しています。 この例では頂点数を一般的な水準まで減少させた反面、Clothで表現されていたヨレが消えてしまいました。リトポをするならばその際に一緒にメッシュで表現してあげるべきでしたね。反省。 番外編:リトポロジーってどうやってやるの? 私は今回このアドオンをBlenderに入れてリトポを行いました。Blenderのバージョンは2.83を利用。 今回掛かったのはアドオン探しから通算で3時間くらいでしょうか。私はモデラーではないのですが、慣れたモデラーさんならもっと早く(そしてハイクオリティに)行えるのかもしれません。気質もあると思うのですが結構楽しかったです。 閑話休題。 ここで提示したかったのは以下の二点です。 ポリゴン数=ハイクオリティ、ではない 特にトポロジー(四角面の流れ)はレンダリング結果にも影響を及ぼすので、リトポした方がクオリティが高くなる場合も多い Clothコンポーネントは、適切に使えば計算負荷を大幅に抑えることができる Unity2019以降はPxClothからNvClothに変更されるので、細かな変更点はあるかもしれません。ただ「2019に持っていくと壊れる」現象はFBXインポーターに対する変更によるものでNvClothへの変更によるものではないので、この記事で記載した点についてはさほど変わらないのかなと思っています。 ちなみに、ClothはVRChatのAvatar Performance Rankingで不当に低く評価されたままだったりします。これはUnity2017までの間にClothが異常に重くなるバグがあったからなのですが、これが2018になって修正されたにも関わらずAvatar Performance Rankingの基準が低いままになっているんですね。以下のcannyから詳しく見ることが出来ます(VRChatアカウントでのログインが必要です/英語) 2020/8/1 16時補足(Graphics Jobs, Normalの利用による利点, 欠点, 注意点) izmさん(@izm)より大変素晴らしいご指摘をいただきましたので補足します。 ツイートの内容が非常によくまとまっているのですが、Graphics Jobsについて聞いたことが無い方もいるかと思うので説明します。英語に抵抗が無ければ以下のリンク先で専門的な内容ながらわかりやすく説明されています。以下、本項の画像は全て下記の記事から引用します。 一言で言ってしまえば、これは「マルチスレッドを活用してグラフィック関連の命令をGPUに出そう」という仕組みのうち一つです。以下の図は最もシンプルな形で、メインスレッド(CPUのスレッドのうち一つ)からGfxDevice(GPU)に描画の命令が出されています。しかし、メインスレッドはその計算をしている間他の処理ができません。 ちょっとこれは非効率的だということで、Unityはデフォルトで一つのRenderthread(CPUのスレッドのうち一つ)にその計算を投げています。でも、最近のパソコンのCPUは基本的にマルチコアですよね。 なので、それをもっと効率的に使おうというのがGraphics Jobsです。メインスレッドから命令を出す先が増えていますね。これで効率的に描画できそうです。 でも実はこれ、Renderthreadが消えてWorkerthreadが増えていることにお気付きでしょうか。Unity Learnの当該記事が執筆された時点(2020/11/27)ではUnityのGraphics JobsにはRenderthreadがなく、Workerthreadに命令を出す分の負荷が少しだけMainthreadに掛かります。また、Experimentalな機能の為デフォルトでは有効化されないようです。 念のためこの記事で利用したProjectで確認したところGraphics Jobsは有効、Graphics APIはDirect3D11を指定していました。 補足終わり。ありがとうございます! ポリゴン数について 今回よく騒がれたポリゴン数ですが、わかりやすい指標ではあるものの、Clothコンポーネントの負荷と比べるとそんなに大したことはないです。というより不適切なClothは超重い。(適切に設定すれば大丈夫だよ! というか揺れ物全般が元々重い処理で、Dynamic Boneも不必要に多く使うのはあまり良くありません) Skinned Mesh Rendererなので、純粋な描画処理に加えてボーンの計算も見る必要があります。ちょっといい方法が思いつかなかったのでここでは具体的な結果をもとに語ることが出来ません。すみません。 古いUnityのドキュメンテーションには目安の値として「1500~4000ポリゴン」みたいな値がデスクトップ向けに書いてあったりするんですが、5年以上前のものであまり参考になりません。 ただ、最新のUnity Manualに非常に良い文章があったので引用します。 以下2つの競合する事実は等しく当てはまります。 メッシュで使用するポリゴンが少ないほど、アプリケーションの実行が速くなります。これは、すべての頂点、エッジ、面が計算リソースを必要とするためです。 メッシュのポリゴン数が多いほど、より細かく有機的なゲームオブジェクトの外観が得られます。なぜなら、ポリゴンが小さいほど形状をより良く制御できるためです。 当然これは無駄なポリゴンを割かないための努力を放棄してよいという意味ではないですが、その努力には当然工数が掛かります。 もちろん、そのような努力の為されたモデルの方が高品質であることを疑う人はいないでしょう。ただ高品質なものには工数がかかるという当たり前の事実と、ビジネスとして考えた時に無限に品質を追求することはできないことを忘れてはいけませんね。 Skinned Mesh Renderer Skinned Mesh Renderer(ボーンと紐付いたメッシュ。キャラクターモデルは原則そう)は、一部の例外を除いて原則として1キャラクターに1つであるべきです。 Unityのレンダリングにおいて、Skinned Mesh Rendererを分割することのメリットはほとんどありません。 1つのメッシュの代わりに2つのスキンメッシュを使用すると、モデルのレンダリング時間がおよそ2倍になる可能性があります。 今回取り扱った製品は2つのSkinned Mesh Rendererを利用しています。「メイン」と「cloth部分」がそうです。 不必要にClothコンポーネントの頂点数を増やすことが致命的であることは前述した通りなので、この場合はCloth部分のみSkinned Mesh Rendererを分割するべきです。つまり、適切な形での結合/分割が行われていることになると思います。 VRSNS向けアバターの多くはUnity上での改変の利便の為にSkinned Mesh Rendererを分割しています(実際とても便利で、私も助かっています)。もちろんその方が改変はしやすいのですが、計算量は非常に多くなります。 今回取り扱った製品の非常に最適化された部分だと思います。 また、ここで当該キャラクターのClothコンポーネントのついたSkinned Mesh Rendererを見てみましょう。 「Update When Offscreen」という項目がオンになっているのがわかるかと思います。 これは「画面外でも更新する」という機能で、この服は画面外でも処理が走るということになります。これはちょっとイケてないですね。各種バーチャル展示会では負荷対策のためにオフを必須にしていることが多いです。 特に理由も見当たらないので、オフにした方が良いかと思います。Clothの場合はBounds(画面内なのか画面外なのかを計算するための値)が自動で計算されるので、特に調整も必要ないようです。 単色テクスチャの解像度 このモデルファイルでは、単色のテクスチャが2枚使われています。 1枚目はHSV(0,0,0)の黒のAlbedoとして、2枚目はHSV(0,0,75)の薄いグレーでMetalicのテクスチャです。 それぞれに2Kの解像度が割かれていますが、その必要はないでしょう。 まずAlbedoに単色のテクスチャを入れる場合、代わりにAlbedoのColorにその色を設定します。 左:変更前 右:変更後 またColorの設定ができない箇所に単色のテクスチャを入れる場合、その解像度は出来るだけ下げておくと良いと思います。 これらの設定はレンダリングの見た目を一切変えないので、純粋に無駄を省くテクニックとして有用です。 不必要なテクスチャはVRAMに乗る無駄データになってしまうので気を付けたいですね。 Materialの結合 同じシェーダーで同じ設定値を採用したマテリアルが複数ある場合、「アトラス化」という工程を挟むことにより、上の方で述べたBatchesとSetPass callsを削減することができます。 このモデルの場合、New MaterialとNew Material 1がどちらも全く同じ設定値を利用しているため、この恩恵を受けることが出来ます。 2バイト文字の利用 一般論として、2バイト文字(全角文字)は思わぬバグの原因になりやすいので避けた方が良いと思います。 ただまあ分かりやすいですよね。 その他、見かけた検証 本記事はあくまでもUnityのEditor上で見た値や各種ドキュメンテーションに立脚した言ってしまえば机上論なので、本番環境での検証とは異なる可能性があります。いくつかVRChatでの検証を見かけたのでリンクを貼っておきます。 公式も検証するらしいです この記事に不足している内容 ライティング/シェーダーについての知見が私では不足しているため、その点については触れていません。この衣装はStandardシェーダーを利用していますが(Autodesk InteractiveはRoughness付きStandardです)、VRSNSのような環境ではライティングが各ワールドにより大幅に異なるので、必ずしも「リアル」な描画結果は得られません。例えばUTS2にはVRChat向けの項目があったりUnlitWFは白飛びし辛いようになっていたり等その辺を考慮しているシェーダーもあるようなのですが、それらの内容の理解には至っていないので「そういうものもあるようだ」という提示のみ行います。 おわりに よくある誤解として、「最適化とは、クオリティを下げることだ」というものがあります。そして実際そのような最適化手法を必須かのように語るエンジニアも存在します。 ですが実際のところそんなことはなく、最適化とはクオリティを最大化する為の手法であるべきです。それはレンダリング結果の美麗さもそうですし、必要なフレームレートを確保することも含まれるでしょう。カクカクの画面では折角の作り込みも台無しです。 このモデルで言えば、よく言われた「ポリゴン数が多い」ことは本質的では無く、正確な指摘とするには「リトポロジーが行われていない」「しかもそのメッシュにClothが適用されている」とするべきでしょう。またテクスチャファイルを見ると単色の他単純な図柄にも2Kテクスチャが割り当てられていたり、しかし「Only 4U」のロゴ部分にはそんなに大きなUV領域を割いておらず、結果ピクセルが目立つ等なんだかちぐはぐとも思える部分もあります。 一方、「Skinned Mesh Rendererがまとめられている」ことは他のVRSNS向けダウンロード商品と比べて最適化された要素です。こういったことから個人的には最適化の意思が無いわけではなく、単にリアルタイムレンダリング向けの知識や技術力が追い付いていないだけではないかなと考えています。まだ始まったばかりのブランドのようですし、これからの発展が楽しみです。 あと個人的にちょっと気になっているのは利用規約の個別条項にある「品位を著しく害する~」の項なのですが、「5.1.aに基づく」ということは5.1.aに含まれる行為だけが規制されるということなんでしょうか? 一方2.2.には「個別条件の定めと基本条項の定めに矛盾や抵触がある場合、前者が優先するものとします」とあり、5.1.aは基本条項ですからなんだか循環参照感があります。解釈次第で広く取れる文章ではありますがバスケット条項は5.1.gに既に定めがあることもあり、ちょっとよくわからないので法務に自信ニキは教えてくださると助かります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SOLID原則を勉強する その3~リスコフの置換原則(LSP)~

目次 SOLID原則を勉強する その1~単一責任の原則 (SRP)~ SOLID原則を勉強する その2~オープン・クローズド原則(OCP)~ SOLID原則を勉強する その3~リスコフの置換原則(LSP)~ ←いまここ SOLID原則を勉強する その4~インターフェース分離の原則(ISP)~ SOLID原則を勉強する その5~オ 依存性逆転の原則(DIP)~ 前置き 書籍を読んだり、ググったりして、自分に分かりやすいようにまとめた記事です。 より詳しく知りたい方は、下記の参考文献を読んでみてください。 after に記載されているクラス図の変数に誤りがあります。近日修正予定です。 参考文献 Clean Architecture 達人に学ぶソフトウェアの構造と設計 | Amazon Adaptive Code ~ C#実践開発手法 | Amazon C#の設計の基本【SOLID原則】まとめ Unity開発で使える設計の話+Zenjectの紹介 C# で SOLID の原則に違反する危険性 リスコフの置換原則(LSP) 派生クラスは基底クラスと置き換えても、正常にプログラムが動作しなければならない 事前条件を派生クラスで強化してはいけない 事前条件 = ガード節 = メソッドを失敗させずに実行するために必要な条件 事後条件を派生クラスで緩くしてはいけない 事後条件 = メソッド終了時に処理結果が有効化チェックするための条件 アクセス修飾子を上書きしてはいけない C# の new キーワードはこのLSP原則を破る可能性が高い メソッドの上書きは virtual と overrideを使う LSP違反するとどうなるか? 間違った使い方をしてもコンパイルエラーにならなくなる(型安全性が壊れる) 実際にプログラムを動作させないと分からない 「この派生クラスだったら…」という条件分岐ができる コード例 長方形クラス Rectangle と正方形クラス Square を作ります。 (Unity 感はないですが、LSP違反の例として有名らしいので) 正方形クラスは長方形クラスを継承するべきかという問題です。 「正方形 is a 長方形」と言い換えると、正しいように思えるが…… before 長方形クラス Rectangle を継承して、正方形クラス Squareを作る 「正方形 is a 長方形」 User クラス は長方形クラスを使って、辺の長さを定義して面積を計算する Rectangle.cs public class Rectangle { public virtual int Height { get; set; } public virtual int Width { get; set; } public int Area() { return Height * Width; } } Square.cs public class Square : Rectangle { private int _height; private int _width; public override int Height { get { return _height; } set { _height = value; _width = value; } } public override int Width { get { return _width; } set { _width = value; _height = value; } } } User.cs using UnityEngine; public class User : MonoBehaviour { void Start() { Rectangle rectangle = new Rectangle(); rectangle.Width = 5; rectangle.Height = 2; Debug.Assert(rectangle.Area() == 10); } } ダメなところ Rectangleクラスを Square クラスに置き換えできない Square は Rectangle のサブクラスなのに… 置き換えたら意図しない結果が返ってくる User クラスの new Rectangle() を new Square() にするだけで Assert に引っかかるようになる User.cs using UnityEngine; public class User : MonoBehaviour { void Start() { // new Square(); に変更したら Assert に引っかかるようになる。しかもコンパイルエラーにならない。 Rectangle rectangle = new Square(); rectangle.Width = 5; rectangle.Height = 2; Debug.Assert(rectangle.Area() == 10); } } after 共通の基底クラスを作成する( IShape クラス) Square クラスは Rectangle クラスを継承しない Square クラスと Rectangle クラスは Shape クラスを継承する IShape.cs public interface IShape { int Area(); } Rectangle.cs public class Rectangle : IShape { public int Height { get; set; } public int Width { get; set; } public int Area() { return Height * Width; } } Square.cs public class Square : IShape { public int Sides { get; set; } public int Area() { return Sides * Sides; } } User.cs using UnityEngine; public class User : MonoBehaviour { void Start() { var rectangle = new Rectangle(); rectangle.Width = 5; rectangle.Height = 2; Debug.Assert(rectangle.Area() == 10); } } 良いところ new Rectangle() を new Square() に変更したらコンパイルエラーになるので、使い方を間違っていてもすぐに問題に気づける 終わりに 現実の世界では「正方形 is a 長方形」は正しいけど、「ソフトウェアの世界」という立場に変えたら、正しくないことが分かりました。 もし変なところがあったら教えて下さい。 (特にクラス図とか…)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む