豚コマ肉とキャベツの中華風パスタ

五香粉を使ってみたくて、中華風パスタを即興で作った。

食べたときは美味しいと感じたのだが、それは味が完璧だったからなのか、それとも今まで味わったことのない新鮮さを感じたからなのか、はたまたオリジナル料理を作れたという達成感からなのか、よくわからなかった。

後日味を検証するために、とりあえずレシピを記録しておく。

 

材料

ペンネ:100g

サラダ油:大さじ1

にんにく:1片、包丁で潰しておく

豚コマ肉:100g、片栗粉と塩と五香粉を適量まぶしておく

玉ねぎ:1/4個、粗めのみじん切り

キャベツ:2、3枚、一口大にちぎる、硬い部分は薄切り

豆板醤:小さじ2

オイスターソース:小さじ1

五香粉:適量(2振り程度?)

 

手順

  1. フライパンにサラダ油と潰したにんにくを入れ、弱火でにんにくの香りを油に移す
  2. 水1Lに塩小さじ1を入れ、ペンネをしっかりめに茹でる
  3. フライパンからにんにくを取り出し、豚コマ肉を炒める。少し色がついてきたら、玉ねぎとキャベツの硬いところも炒める。
  4. 玉ねぎが透き通ってきたらキャベツの葉の部分も加え、少ししんなりするまで炒める
  5. フライパンを傾け、豆板醤とオイスターソース、五香粉を入れ、ゆで汁で溶かしてあんかけを作る。粘度は、少しとろみのある程度にする。
  6. 茹で上がったペンネを加えて和える。

 

備考

  • 本当は合いびき肉を使いたかった。
  • ペンネだけ口に入れたときに少し浮いている感じがした。茹でるときに味の素を入れればよかった?
  • 見た目の鮮やかさ、フレッシュさが足りていなかった。大葉をかけたらよかったかもしれない。パセリでも意外と合うかも?
  • 玉ねぎは薄切りにして、フォークに引っかかるようにしても良かったかもしれない。
  • にんにくはみじん切りにしてがっつり効かせた方がよかったかもしれない。

 

スーファミコントローラーを雑にUSB化する

スーパーファミコンのコントローラーをUSBデバイス化したときの写真が眠っていたので公開します。

 

たしか、昔のゲーム機のコントローラーを捨てるのはもったいないし、せっかくだから左手デバイスプログラマブルキーボードって言うのか?)として使えないかと思ってやったはずです。

f:id:Ebit:20200419083927j:plain

ゲーム機のコントローラーをUSBデバイス化する人はそこそこいるようで、ググると出てきます。USB変換基板を買う方法もありますが、私はArduino Pro Microを使うことにしました。理由として、市販されている変換基板より安い(当時はArduino互換機が1個600円くらい?)、かつプログラムの書き換えが簡単だったからです。

 

Arduino Pro Microはこんな感じです。小さいので、コントローラーの中に突っ込めるというわけです。

f:id:Ebit:20200419083201j:plain

スーファミコントローラーを分解して、

f:id:Ebit:20200419083948j:plain

f:id:Ebit:20200419081244j:plain

f:id:Ebit:20200419081349j:plain

中の基板とArduinoをはんだ付けして、

f:id:Ebit:20200419084859j:plain

邪魔な真ん中の棒を切って、気合でねじこみました。

f:id:Ebit:20200419094635j:plain

あとは、ボタンの押下を判定するライブラリ(https://github.com/Kotakku/SFCcontroller)と、キー入力を行うライブラリ(https://www.arduino.cc/reference/en/language/functions/usb/keyboard/)を組み合わせて、例えばAボタンが押されたときに、PCに「Ctrl + Z」を送信するようなプログラムをArduinoに書き込めば完成です。

 

ゲーム機のコントローラーってとても使いやすいインタフェースですし、人によっては愛着もあるでしょうから、こういう形で使い続けられると楽しいと思います。

 

皆さんもぜひ試してみたらどうでしょうか、みたいなことを書こうと思ったのですが、Pro Microの値段が以前の2倍近くになっててびっくりしました。もともと電子工作は金のかかる趣味と言われていますが、その傾向が強くなっててちょっと悲しいですね。

Quset Linkでラップトップの内蔵GPUが使用される問題(Windows11)

問題

ラップトップでQuest Link(有線接続)を使用するときに,Oculusアプリが外部GPUではなくCPU内蔵GPUを使うため酷いラグが発生する.

環境

PC:ASUS TUF Gaming A15 FA506QM

CPU:AMD Ryzen 7 5800H

GPUNVIDIA GeForce RTX 3060 Laptop

OS:Windows11

Oculusアプリのバージョン:56.0.0.109.155

HMD:Meta Quest Pro

解決方法

ここで説明されている,Windowsの設定アプリからグラフィックスの基本設定を変更する方法で解決した.

手順は以下の通り.

  1. 設定アプリのシステム > ディスプレイ > グラフィックに移動
  2. アプリのリストから OVRServer_x64.exe,OculusClient.exe,OculusDash.exeのオプションを選択し,グラフィックスの基本設定を高パフォーマンスに設定し,保存する.

    ※プログラムがリストに表示されていない場合,「アプリを追加する」の参照から追加する.デフォルトのインストール先は,
    OVRServer_x64.exe:C:\Program Files\Oculus\Support\oculus-runtime
    OculusClient.exe:C:\Program Files\Oculus\Support\oculus-client
    OculusDash.exe:C:\Program Files\Oculus\Support\oculus-dash\dash\bin
  3. PCを再起動する

 

備考

  • Nvidiaコントロールパネルの「3D設定の管理」から特定のプログラムの設定を変更してみたが,何も変わらなかった.
  • バイスマネージャーから内蔵GPUを無効にするのは以前やったことがあるが,一応動くものの十分なパフォーマンスが出ないし,PCの挙動が不安定になるのでやめた方がいい.
  • 去年Rift Sを繋ぐときにこれと同じ操作をした覚えがあるが,そのときはWindows10だったので,Windowsアップデートで設定がリセットされたのかもしれない.

参考

Oculus Link is laggy with my Zephyrus G14 : r/OculusLink (reddit.com)

 

【Unity】 Animation Riggingでカスタムコンストレイントを実装し、ランタイムでアニメーションを修正してみた

はじめに

モーキャプでとったアニメーションをAnimation Riggingでプロシージャルに修正する必要があったのですが、用意されているコンストレイントだけでは上手くいかなかったのでカスタムコンストレイントを実装してみました。

 

Animation Riggingとは

アニメーション作成時やランタイムで使えるリグを組めるやつです(Animation Rigging でアニメーションをレベルアップさせる方法を学ぼう | Unity Blog )。独自のコンストレイントを作れるなど拡張性も売りにしています。

似たような使い方をされるFinalIKと比較すると、アニメーション編集時でもIKやAimコンストレイントなどを使えるのが明確な利点だと思います。あと無料です。ただ、必要最小限のコンポーネントしか提供されていないため、例えばFinalIKのVRIKに相当するものをAnimation Riggingで実装しようとすると手間がかかります。

また、Animation RiggingはAnimation C# Jobs(Animation C# Jobs | Unity Blog)を使用していてパフォーマンスが良い一方で、コンストレイントの処理の間にMonobehaviorスクリプトの処理は挟めないようになっています(Question - Script Execution Order - Unity Forum)。ここらへんはちょっとややこしかったです。

UnityフォーラムにAnimation Riggingの情報まとめ(Official - Compilation of resources for Animation Rigging - Unity Forum)が投稿されていたのを今見つけたのですが、もっと早く見つけたかった……

使用場面

モーキャプのデータでキャラクターを動かす必要があったのですが、このように腕が机を貫通する問題が発生していました。(このgifで使ってるモーションはMixamoのやつで、実際に使ったやつとは違いますが)

モーションを入れただけ

前から

モーションを撮り直したり手作業で修正する手もありましたが、実際に動かすキャラクターの総数は数十体で、かつ仕様もはっきりと決まっていなかったため、Animation Riggingのコンストレイントを使ってプロシージャルに貫通を防ぐリグを作ろうと考えました。

しかし、用意されているコンストレイントの組み合わせでは上手くいかず、なぜかメモリリークが発生するリグさえ出来上がってしまいました。そこで、専用のカスタムコンストレイントを作りました。

カスタムコンストレイントの実装

TwoBoneIKコンストレイントを拡張した、HeightLimitedTwoBoneIKConstraintというコンストレイントを実装しました。次のような処理を行っています。

  1. TwoBoneIKのtargetが制限高より低くならないように位置を修正する
  2. TwoBoneIKの処理を行う
  3. midが制限高より高くなるように、tipの位置を維持したまま(tip-rootを軸にして)rootを回転させる

横から見た図

 

コード(最後に載せています)の説明は省略しますが、カスタムコンストレイントの書き方はこの動画で説明されています。

youtu.be

 

作ったカスタムコンストレイントを使ってリグを組みます。

リグの構成

HeightLimitedTwoBoneIKConstraintのプロパティは、Limit Heightを机と同じくらいの高さにし、RootやTarget等はTwoBoneIKConstraintと同じように設定します。ただし、このTargetはMulti-PositionConstraintでHandボーンと同じ位置になるように拘束しています。

コンストレイントの実行順は上から下の順で処理が行われるので、Multi-PositionConstraintを上に、HeightLimitedTwoBoneIKConstraintを下にすることで、(Animatorで計算された手の位置を取得)→(手の位置の修正)という風に適切に実行できます。

 

という感じで、腕が机を貫通しないリグを組むことができました。

カスタムコンストレイントを使った場合

前から

体格の違うキャラクターであっても、異なるアニメーションであってもこのリグを流用するだけでいいので便利です。

おわりに

最初はAnimation RiggingをFinalIKのようなものと考えていたので苦戦しましたが、使いこなせたらかなり強いツールだと感じました。

参考

Animation Rigging でアニメーションをレベルアップさせる方法を学ぼう | Unity Blog

Animation C# Jobs | Unity Blog

Question - Script Execution Order - Unity Forum

(79) Extending the Animation Rigging package with C# - Unite Copenhagen - YouTube

Official - Compilation of resources for Animation Rigging - Unity Forum

Mixamo

おまけ(コード)

汚くて恥ずかしいですが、一応コードを載せときます。

rootの回転角度の計算はもっと賢いやり方がありそうですが思いつきませんでした。

 

using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Animations.Rigging;


[DisallowMultipleComponent, AddComponentMenu("Animation Rigging/Height Limited Two Bone IK Constraint")]
public class HeightLimitedTwoBoneIKConstraint : RigConstraint<
HeightLimitedTwoBoneIKConstraintJob,
HeightLimitedTwoBoneIKConstraintData,
HeightLimitedTwoBoneIKConstraintJobBinder<HeightLimitedTwoBoneIKConstraintData>
>
{
protected override void OnValidate()
{
base.OnValidate();
m_Data.hintWeight = Mathf.Clamp01(m_Data.hintWeight);
m_Data.targetPositionWeight = Mathf.Clamp01(m_Data.targetPositionWeight);
m_Data.targetRotationWeight = Mathf.Clamp01(m_Data.targetRotationWeight);
}
}


[Unity.Burst.BurstCompile]
public struct HeightLimitedTwoBoneIKConstraintJob : IWeightedAnimationJob
{
public ReadWriteTransformHandle root;
public ReadWriteTransformHandle mid;
public ReadWriteTransformHandle tip;

public ReadOnlyTransformHandle hint;
public ReadOnlyTransformHandle target;

public AffineTransform targetOffset;

public FloatProperty targetPositionWeight;
public FloatProperty targetRotationWeight;
public FloatProperty hintWeight;

public FloatProperty limitHeight;

public bool isLeftArm;

public FloatProperty jobWeight { get; set; }

public void ProcessRootMotion(AnimationStream stream) { }

public void ProcessAnimation(AnimationStream stream)
{
float w = jobWeight.Get(stream);
if (w > 0f)
{
HeightLimitedSolveTwoBoneIK(
stream, root, mid, tip, target, hint,
targetPositionWeight.Get(stream) * w,
targetRotationWeight.Get(stream) * w,
hintWeight.Get(stream) * w,
targetOffset,
limitHeight.Get(stream),
isLeftArm
);
}
else
{
AnimationRuntimeUtils.PassThrough(stream, root);
AnimationRuntimeUtils.PassThrough(stream, mid);
AnimationRuntimeUtils.PassThrough(stream, tip);
}
}

public static void HeightLimitedSolveTwoBoneIK(
AnimationStream stream,
ReadWriteTransformHandle root,
ReadWriteTransformHandle mid,
ReadWriteTransformHandle tip,
ReadOnlyTransformHandle target,
ReadOnlyTransformHandle hint,
float posWeight,
float rotWeight,
float hintWeight,
AffineTransform targetOffset,
float limitHeight,
bool isLeftArm
)
{
Vector3 aPosition = root.GetPosition(stream);
Vector3 bPosition = mid.GetPosition(stream);
Vector3 cPosition = tip.GetPosition(stream);
target.GetGlobalTR(stream, out Vector3 targetPos, out Quaternion targetRot);

//targetの高さが指定よりも低ければ調整
if (targetPos.y < limitHeight)
{
targetPos.y = limitHeight;
}

//TwoBoneIKの処理
Vector3 tPosition = Vector3.Lerp(cPosition, targetPos + targetOffset.translation, posWeight);
Quaternion tRotation = Quaternion.Lerp(tip.GetRotation(stream), targetRot * targetOffset.rotation, rotWeight);
bool hasHint = hint.IsValid(stream) && hintWeight > 0f;

Vector3 ab = bPosition - aPosition;
Vector3 bc = cPosition - bPosition;
Vector3 ac = cPosition - aPosition;
Vector3 at = tPosition - aPosition;

float abLen = ab.magnitude;
float bcLen = bc.magnitude;
float acLen = ac.magnitude;
float atLen = at.magnitude;

float oldAbcAngle = TriangleAngle(acLen, abLen, bcLen);
float newAbcAngle = TriangleAngle(atLen, abLen, bcLen);

Vector3 axis = Vector3.Cross(ab, bc);
float k_SqrEpsilon = 1e-8f;
if (axis.sqrMagnitude < k_SqrEpsilon)
{
axis = hasHint ? Vector3.Cross(hint.GetPosition(stream) - aPosition, bc) : Vector3.zero;

if (axis.sqrMagnitude < k_SqrEpsilon)
axis = Vector3.Cross(at, bc);

if (axis.sqrMagnitude < k_SqrEpsilon)
axis = Vector3.up;
}
axis = Vector3.Normalize(axis);

float a = 0.5f * (oldAbcAngle - newAbcAngle);
float sin = Mathf.Sin(a);
float cos = Mathf.Cos(a);
Quaternion deltaR = new Quaternion(axis.x * sin, axis.y * sin, axis.z * sin, cos);
mid.SetRotation(stream, deltaR * mid.GetRotation(stream));

cPosition = tip.GetPosition(stream);
ac = cPosition - aPosition;
root.SetRotation(stream, QuaternionExt.FromToRotation(ac, at) * root.GetRotation(stream));

if (hasHint)
{
float acSqrMag = ac.sqrMagnitude;
if (acSqrMag > 0f)
{
bPosition = mid.GetPosition(stream);
cPosition = tip.GetPosition(stream);
ab = bPosition - aPosition;
ac = cPosition - aPosition;

Vector3 acNorm = ac / Mathf.Sqrt(acSqrMag);
Vector3 ah = hint.GetPosition(stream) - aPosition;
Vector3 abProj = ab - acNorm * Vector3.Dot(ab, acNorm);
Vector3 ahProj = ah - acNorm * Vector3.Dot(ah, acNorm);

float maxReach = abLen + bcLen;
if (abProj.sqrMagnitude > (maxReach * maxReach * 0.001f) && ahProj.sqrMagnitude > 0f)
{
Quaternion hintR = QuaternionExt.FromToRotation(abProj, ahProj);
hintR.x *= hintWeight;
hintR.y *= hintWeight;
hintR.z *= hintWeight;
hintR = QuaternionExt.NormalizeSafe(hintR);
root.SetRotation(stream, hintR * root.GetRotation(stream));
}
}
}

tip.SetRotation(stream, tRotation);

//midが制限高より低い場合に、tipの位置を維持してrootを回転させる

if (mid.GetPosition(stream).y >= limitHeight) return;

Vector3 rootPosition = root.GetPosition(stream);
Vector3 midPosition = mid.GetPosition(stream);
Vector3 tipPosition = tip.GetPosition(stream);

Vector3 mPosition = (rootPosition + tipPosition) * 0.5f;

Vector3 normal = (tipPosition - rootPosition).normalized;

Vector3 rVector = midPosition - mPosition;
float radius = rVector.magnitude;

Vector3 xVector = rVector;
Vector3 yVector = xVector.magnitude > Mathf.Epsilon
? Vector3.Cross(normal, xVector)
: Vector3.Cross(normal, Vector3.up);

xVector.Normalize();
yVector.Normalize();

float rotateDirection = isLeftArm ? -1 : 1;

float angle = 0;
Vector3 point = mPosition + radius * xVector;
while (point.y < limitHeight && angle <= 180f)
{
angle += 0.1f;
float theta = rotateDirection * Mathf.Deg2Rad * angle;
point = mPosition + radius * (xVector * Mathf.Cos(theta) + yVector * Mathf.Sin(theta));
}

Quaternion rotation = Quaternion.AngleAxis(rotateDirection * angle, normal);
root.SetRotation(stream, rotation * root.GetRotation(stream));

Quaternion inverse = Quaternion.Inverse(rotation);
tip.SetRotation(stream, inverse * tip.GetRotation(stream));
}

private static float TriangleAngle(float aLen, float aLen1, float aLen2)
{
float c = Mathf.Clamp((aLen1 * aLen1 + aLen2 * aLen2 - aLen * aLen) / (aLen1 * aLen2) / 2.0f, -1.0f, 1.0f);
return Mathf.Acos(c);
}
}

public interface IHeightLimitedTwoBoneIKConstraintData
{
Transform root { get; }
Transform mid { get; }
Transform tip { get; }
Transform target { get; }
Transform hint { get; }

bool maintainTargetPositionOffset { get; }
bool maintainTargetRotationOffset { get; }

string targetPositionWeightFloatProperty { get; }
string targetRotationWeightFloatProperty { get; }
string hintWeightFloatProperty { get; }
string limitHeightFloatProperty { get; }
bool isLeftArm { get; }
}

[System.Serializable]
public struct HeightLimitedTwoBoneIKConstraintData : IAnimationJobData, IHeightLimitedTwoBoneIKConstraintData
{
[SerializeField] Transform m_Root;
[SerializeField] Transform m_Mid;
[SerializeField] Transform m_Tip;

[SyncSceneToStream, SerializeField] Transform m_Target;
[SyncSceneToStream, SerializeField] Transform m_Hint;
[SyncSceneToStream, SerializeField, Range(0f, 1f)] float m_TargetPositionWeight;
[SyncSceneToStream, SerializeField, Range(0f, 1f)] float m_TargetRotationWeight;
[SyncSceneToStream, SerializeField, Range(0f, 1f)] float m_HintWeight;

[NotKeyable, SerializeField] bool m_MaintainTargetPositionOffset;
[NotKeyable, SerializeField] bool m_MaintainTargetRotationOffset;

[SyncSceneToStream, SerializeField] float m_LimitHeight;
[SerializeField] private bool m_IsLeftArm;

public Transform root { get => m_Root; set => m_Root = value; }
public Transform mid { get => m_Mid; set => m_Mid = value; }
public Transform tip { get => m_Tip; set => m_Tip = value; }
public Transform target { get => m_Target; set => m_Target = value; }
public Transform hint { get => m_Hint; set => m_Hint = value; }

public float targetPositionWeight { get => m_TargetPositionWeight; set => m_TargetPositionWeight = Mathf.Clamp01(value); }
public float targetRotationWeight { get => m_TargetRotationWeight; set => m_TargetRotationWeight = Mathf.Clamp01(value); }
public float hintWeight { get => m_HintWeight; set => m_HintWeight = Mathf.Clamp01(value); }

public bool maintainTargetPositionOffset { get => m_MaintainTargetPositionOffset; set => m_MaintainTargetPositionOffset = value; }
public bool maintainTargetRotationOffset { get => m_MaintainTargetRotationOffset; set => m_MaintainTargetRotationOffset = value; }

public bool isLeftArm { get => m_IsLeftArm; set => m_IsLeftArm = value; }

string IHeightLimitedTwoBoneIKConstraintData.targetPositionWeightFloatProperty => ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(m_TargetPositionWeight));
string IHeightLimitedTwoBoneIKConstraintData.targetRotationWeightFloatProperty => ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(m_TargetRotationWeight));
string IHeightLimitedTwoBoneIKConstraintData.hintWeightFloatProperty => ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(m_HintWeight));

string IHeightLimitedTwoBoneIKConstraintData.limitHeightFloatProperty => ConstraintsUtils.ConstructConstraintDataPropertyName(nameof(m_LimitHeight));

bool IAnimationJobData.IsValid() => (m_Tip != null && m_Mid != null && m_Root != null && m_Target != null && m_Tip.IsChildOf(m_Mid) && m_Mid.IsChildOf(m_Root));

void IAnimationJobData.SetDefaultValues()
{
m_Root = null;
m_Mid = null;
m_Tip = null;
m_Target = null;
m_Hint = null;
m_TargetPositionWeight = 1f;
m_TargetRotationWeight = 0f;
m_HintWeight = 1f;
m_MaintainTargetPositionOffset = false;
m_MaintainTargetRotationOffset = false;
m_LimitHeight = 0f;
m_IsLeftArm = true;
}
}

public class HeightLimitedTwoBoneIKConstraintJobBinder<T> : AnimationJobBinder<HeightLimitedTwoBoneIKConstraintJob, T>
where T : struct, IAnimationJobData, IHeightLimitedTwoBoneIKConstraintData
{
public override HeightLimitedTwoBoneIKConstraintJob Create(Animator animator, ref T data, Component component)
{
var job = new HeightLimitedTwoBoneIKConstraintJob();

job.root = ReadWriteTransformHandle.Bind(animator, data.root);
job.mid = ReadWriteTransformHandle.Bind(animator, data.mid);
job.tip = ReadWriteTransformHandle.Bind(animator, data.tip);
job.target = ReadOnlyTransformHandle.Bind(animator, data.target);

if (data.hint != null)
job.hint = ReadOnlyTransformHandle.Bind(animator, data.hint);

job.targetOffset = AffineTransform.identity;
if (data.maintainTargetPositionOffset)
job.targetOffset.translation = data.tip.position - data.target.position;
if (data.maintainTargetRotationOffset)
job.targetOffset.rotation = Quaternion.Inverse(data.target.rotation) * data.tip.rotation;

job.targetPositionWeight = FloatProperty.Bind(animator, component, data.targetPositionWeightFloatProperty);
job.targetRotationWeight = FloatProperty.Bind(animator, component, data.targetRotationWeightFloatProperty);
job.hintWeight = FloatProperty.Bind(animator, component, data.hintWeightFloatProperty);

job.limitHeight = FloatProperty.Bind(animator, component, data.limitHeightFloatProperty);
job.isLeftArm = data.isLeftArm;

return job;
}

public override void Destroy(HeightLimitedTwoBoneIKConstraintJob job)
{
}
}