Skip to content

Latest commit

 

History

History
293 lines (235 loc) · 12 KB

File metadata and controls

293 lines (235 loc) · 12 KB

VRMC_vrm.lookAt

本文書では、 VRMC_vrm 拡張のうち lookAt フィールドについての仕様を示します。

Table of Contents generated with DocToc

概要

LookAtは、VRMモデルに対して視線のアニメーションを行うためのコンポーネントです。

初期姿勢時の Head ボーンをオフセットして得られる LookAt空間 で視線を定義します。 視線値は、LookAt空間 での 上下左右 の Degree 値です。 本文章では、右手系の Euler 角に準じた正の回転方向をもつ Yaw と Pitch で説明します。

Euler角については、プラットフォーム毎に、右手・左手、 Y-UP・Z-UP などをふまえて一貫性のある実装をしてください。

一組の yaw, pitch により両目が同じ方向を見ることを想定しています。 このことより、寄り目等の両目が異なる方向を向く表現はできません。

詳細

extensions.VRMC_vrm.lookAt = {
  "offsetFromHeadBone": [
    0,
    0.06,
    0
  ],
  "rangeMapHorizontalInner": {
    "inputMaxValue": 90,
    "outputScale": 10
  },
  "rangeMapHorizontalOuter": {
    "inputMaxValue": 90,
    "outputScale": 10
  },
  "rangeMapVerticalDown": {
    "inputMaxValue": 90,
    "outputScale": 10
  },
  "rangeMapVerticalUp": {
    "inputMaxValue": 90,
    "outputScale": 10
  },
  "type": "bone"
}
名前 備考
type bone または expression
offsetFromHeadBone lookAtの基準位置(両目の間が目安)へのヘッドボーンからの位置offsetです
rangeMapHorizontalInner 水平内側の目の可動範囲
rangeMapHorizontalOuter 水平外側の目の可動範囲(ExpressionのLookLeft, LookRightはこれを使用)
rangeMapVerticalDown 下方向の目の可動範囲
rangeMapVerticalUp 上方向の目の可動範囲

LookAtType

下記の2種類を定義しています。

名前 対象
bone Humanoid leftEyeボーンとrightEyeボーン の LocalRotation EulerAngles
expression Expression のLookAt, LookDown, LookLeft, LookRight ExpressionWeight

expression は、MorphTarget, MaterialColor, TextureTransform が可能です。 LookAt では、おもに MorphTarget による頂点移動 と TextureTransform による 目のテクスチャーの offset 移動により視線が表現されることが想定されています。

LookAt 空間 (offsetFromHeadBone)

モデルがある対象物体を見るような場合において、視線方向を決定するのに用いる「LookAt 空間」を定義します。

LookAt 空間は、ワールド上のあるトランスフォームからの相対的な空間として定義され、このトランスフォームを以下のように定義します。

  • トランスフォームの親はheadであり、headの動きに追従して動く
  • トランスフォームのheadからのローカル位置は、プロパティ offsetFromHeadBone によって定められる
  • トランスフォームのheadからのローカル回転は、headのモデル空間におけるレスト回転の逆である

headがモデル空間におけるレスト回転によって、 offsetFromHeadBone による視点の位置の移動方向がモデル空間の軸と一致しない場合があります。 また、headがモデル空間におけるレスト回転を持っている場合も、視線の前方向はモデル座標系における+Z軸と一致します。

本文章では、glTF の右手系, Y-Up, Z-Forward 座標系を用いて説明します。 Yaw, Pitch の正の方向は下記のとおりです。

  • Yaw: Z->X方向 => 左
  • Pitch: Y->Z方向 => 下
      Y  Forward
      ^  Z
      | /
      |/
X<----+
Left      Right

offsetFromHeadBone は、VR向けHMDの位置を想定しています。 モデルの一人称視点の位置の取得・反映に用いることができます。

Implementation note: モデルに offsetFromHeadBone が存在しない場合は、実装ごとに適切な値にフォールバックを行うことが推奨されます。

範囲マップ

LookAt空間 で評価された視線値 YawPitchbone または expression に適用する前に、値を加工できます。

range_map

name 機能
inputMaxValue Yaw または Pitch の上限値。この値が小さいほど、同じ視線値に対して視線は大きく動きます。
outputScale bone の回転 または Expression の Weight の最大値。

type が bone のときの 解釈

yaw, pitch の視線値から、leftEye ボーンと rightEye ボーンに対する local rotation を生成します。 水平内側, 水平外側, 垂直上側, 垂直下側 の4つの区分があります。

上下左右の rangeMap の使い分けは下記のようになります。

  + yaw -     + yaw -
     ^           ^
     |           |
outer|inner inner|outer
  left eye    right eye
leftEye rangeMap rightEye rangeMap
Yaw>0(左) rangeMapHorizontalOuter rangeMapHorizontalInner
Yaw<0(右) rangeMapHorizontalInner rangeMapHorizontalOuter
Pitch>0(下) rangeMapVerticalDown rangeMapVerticalDown
Pitch<0(上) rangeMapVerticalUp rangeMapVerticalUp

範囲マップは、視線値によって得られたEuler角 (degree) の絶対値に対して行います。 出力の単位は、目のボーンのEuler角 (degree) となります。

const boneLocalEulerAngle = min(fabs(value), inputMaxValue)/inputMaxValue * outputScale;

type が expression のときの解釈

lookUp Expression, lookDown Expression, lookLeft Expression, lookRight Expression に対する weight を生成します。 水平, 垂直上側, 垂直下側 の3つの区分があります。 ひとつの Expression で両目がまとめて変化するため、水平内側, 水平外側 の区別が無いことに注意してください。 rangeMapHorizontalOuter を使います。

expression rangeMap
Yaw>0(左) lookLeft rangeMapHorizontalOuter
Yaw<0(右) lookRight rangeMapHorizontalOuter
Pitch>0(下) lookDown rangeMapVerticalDown
Pitch<0(上) lookUp rangeMapVerticalUp

範囲マップは、視線値によって得られたEuler角 (degree) の絶対値に対して行います。 出力の単位は、目のexpressionのweight値となります。

const expressionWeight = min(fabs(value), inputMaxValue)/inputMaxValue * outputScale;

LookAtのアルゴリズム

Yaw and Pitch in lookAt space

public static (float Yaw, float Pitch) CalcYawPitch(this Matrix4x4 lookAtSpace, Vector3 target)
{
    var localTarget = lookAtSpace.inverse.MultiplyPoint(target);

    var z = Vector3.Dot(localPosition, Vector3.forward);
    var x = Vector3.Dot(localPosition, Vector3.right);
    var yaw = (float)Math.Atan2(x, z) * Mathf.Rad2Deg;

    // x+y z plane
    var xz = Mathf.sqrt(x * x + z * z);
    var y = Vector3.Dot(localPosition, Vector3.up);
    var pitch = (float)Math.Atan2(-y, xz) * Mathf.Rad2Deg;

    return (yaw, pitch);
}

Apply Yaw and Pitch to bone

function applyLeftEyeBone(vrm, yawDegrees, pitchDegrees)
{    
  var yaw = 0;
  if(yawDegrees>0)
  {
    // left => outer
    yaw = min(fabs(yawDegrees), rangeMapHorizontalOuter.inputMaxValue) / rangeMapHorizontalOuter.inputMaxValue * rangeMapHorizontalOuter.outputScale;
  }
  else{
    // right => inner
    yaw = -min(fabs(yawDegrees), rangeMapHorizontalInner.inputMaxValue) / rangeMapHorizontalInner.inputMaxValue * rangeMapHorizontalInner.outputScale;
  }

  var pitch = 0;
  if(pitchDegrees>0)
  {
    // down
    pitch = min(fabs(pitchDegrees), rangeMapVerticalDown.inputMaxValue) / rangeMapVerticalDown.inputMaxValue * rangeMapVerticalDown.outScale;
  }
  else{
    // up
    pitch = -min(fabs(pitchDegrees), rangeMapVerticalUp.inputMaxValue) / rangeMapVerticalUp.inputmaxValue * rangeMapVerticalUp.outScale;
  }

  vrm.humanoid.leftEye.localRotation = Quaternion.from_YXZEuler(yaw, pitch, 0);
}

function applyRightEyeBone(vrm, yawDegrees, pitchDegrees)
{    
  var yaw = 0;
  if(yawDegrees>0)
  {
    // left => inner
    yaw = min(fabs(yawDegrees), rangeMapHorizontalInner.inputMaxValue) / rangeMapHorizontalInner.inputMaxValue * rangeMapHorizontalInner.outputScale;
  }
  else{
    // right => outer
    yaw = -min(fabs(yawDegrees), rangeMapHorizontalOuter.inputMaxValue) / rangeMapHorizontalOuter.inputMaxValue * rangeMapHorizontalOuter.outputScale;
  }

  var pitch = 0;
  if(pitchDegrees>0)
  {
    // down
    pitch = min(fabs(pitchDegrees), rangeMapVerticalDown.inputMaxValue) / rangeMapVerticalDown.inputMaxValue * rangeMapVerticalDown.outScale;
  }
  else{
    // up
    pitch = -min(fabs(pitchDegrees), rangeMapVerticalUp.inputMaxValue) / rangeMapVerticalUp.inputmaxValue * rangeMapVerticalUp.outScale;
  }

  vrm.humanoid.rightEye.localRotation = Quaternion.from_YXZEuler(yaw, pitch, 0);
}

Apply Yaw and Pitch to expression

function applyExpression(vrm, yawDegrees, pitchDegrees)
{
  // horizontal
  if(yawDegrees>0)
  {
    // left
    const yawWeight = min(fabs(yawDegrees), rangeMapHorizontalOuter.inputMaxValue)/rangeMapHorizontalOuter.inputMaxValue * rangeMapHorizontalOuter.outputScale;
    vrm.expression.setWeight(LOOK_LEFT, yawWeight);
    vrm.expression.setWeight(LOOK_RIGHT, 0);
  }
  else{
    // right
    const yawWeight = min(fabs(yawDegrees), rangeMapHorizontalOuter.inputMaxValue)/rangeMapHorizontalOuter.inputMaxValue * rangeMapHorizontalOuter.outputScale;
    vrm.expression.setWeight(LOOK_LEFT, 0);
    vrm.expression.setWeight(LOOK_RIGHT, yawWeight);
  }

  // vertical
  if(pitchDegrees>0)
  {
    // down
    const pitchWeight = min(fabs(pitchDegrees), rangeMapVerticalDown.inputMaxValue)/rangeMapVerticalDown.inputMaxValue * rangeMapVerticalDown.outputScale;
    vrm.expression.setWeight(LOOK_DOWN, pitchWeight);
    vrm.expression.setWeight(LOOK_UP, 0);
  }
  else{
    // up
    const pitchWeight = min(fabs(pitchDegrees), rangeMapVerticalUp.inputMaxValue)/rangeMapVerticalUp.inputMaxValue * rangeMapVerticalUp.outputScale;
    vrm.expression.setWeight(LOOK_DOWN, 0);
    vrm.expression.setWeight(LOOK_UP, pitchWeight);
  }
}