と言いつつ、それ以外の情報の方が多い・・・
端末の方向
- Androidの人は
deviceorientationabsolute
イベントを引っ掛ければOKの模様
- Y軸の正方向が北のはず
- 両方のイベントハンドラを登録し、
deviceorientationabsolute
が来るようならdeviceorientation
の方を削除する感じか
両方のイベントをそのまま表示
- とりあえず数値だけ表示、座標球はなし(つまりWebGLなしのただのJS)
絶対位置が使えないなら相対位置を使う
deviceorientationabsolute
イベントが有効な値を保持しているようならば、deviceorientation
のハンドラを削除する
- Androidエミュレータが変な値を報告してくることがあり、また、macOS上のChromeは一発だけ
deviceorientationabsolute
を送ってくる(alpha
・beta
・gamma
はnull
)
$AEL
が実は「ハンドラを削除するための関数」を返しているので、この関数を呼び出せばハンドラを削除できる
- 詳しくは無名関数イベントハンドラの削除を参照
- 何かの拍子に変な
deviceorientationabsolute
が来て、それ以降黙ってしまうと何も起きなくなるが、その時はリロードで対処してもらうことにしよう(闇が深い)
というわけで、これ以降の部分はAndroidの人は読む必要がありませんが、3D-CGのお約束は色々出てくるので、読んでみるのもいいでしょう。
iOSのwebkitCompassHeading
- iOSでは
deviceorientationabsolute
は使えない
- 代わりに
deviceorientation
でもらったイベントオブジェクトにwebkitCompassHeading
というのがあり、これが使える
- 仕様自体は簡単で、「端末が向いている方角」を度で示す
- 0が北、90が東
webkitCompassHeading
が135ならば、端末は南東の方向を向いている
- が、南東を向いているのが端末のどの部分なのかが判然としない
- 画面の向きはとりあえず縦位置でロックする
- 端末を垂直に立てて、縦位置で持つ
- 以後、この状態の上下左右を端末の上下左右と呼び、端末のこちら側(ディスプレイ側)の面を表、向こう側の面を裏と呼ぶ
- 「端末が向いている方向」は端末の裏方向=自分から見て向こう側
- そのまま端末の上を向こう側方向へ倒していく
- 端末の表(ディスプレイ面)が徐々に上を向き、最終的に水平になる
- 「端末が向いている方向」は端末の裏もしくは上方向=自分から見て向こう側
- そのまま端末を右に回す
- 端末の上が徐々に右を向き、最終的に水平・横位置になる
- 「端末が向いている方向」は端末の上方向=自分から見て右側に回転していく
- そのまま端末の左側を起こす
- 端末の上下を軸に回転し、最終的に垂直・横位置になる
- 「端末が向いている方向」は端末の裏方向ではなく上方向=自分から見て右側のまま
- ここまでは直感と大きく違わないだろう
- さらに端末を縦位置に変更する
- 端末の上が徐々に上を向き、最終的に垂直・縦位置に戻る
- 「端末が向いている方向」は端末の上方向から裏方向=自分の右側から向こう側に徐々に回転し(上図右点線)、最初の状態に戻る
- もう一つ問題がある
- このまま天を仰ぐように端末の裏を上に向けていく
- しばらく端末の方向は変わらないが、どこかでクルッっと急にwebkitCompassHeadingの値が変わる
- この挙動は標準のコンパスアプリでも同じ
- 実際に確認してみる
- 座標球は表示するが、端末の動きには追従させず、少し傾けた位置で固定してある
- 端末がビュー座標系でY軸正方向を向いていると仮定して、
webkitCompassHeading
が示している北の方向を表示する
webkitCompassHeading
が0ならばY軸正方向(XY平面で90度の方向)が北
webkitCompassHeading
が90ならば端末は東を向いているので、Y軸正方向は東、よって北はX軸負方向(XY平面で180度の方向)
- つまり、Z軸を軸にして、
90 + webkitCompassHeading
度だけ回ったところに指示線を表示する
- コードの解説が恐ろしく長くなってしまったので折りたたみました
- 2D空間で一定の幅になる線は少し修正している
- 以前はVSでXY座標と太さを射影変換後の\(W\)で割ってしまい、\(Z=0\)、\(W=1\)を出力していた
- \(W\)をきちんと出力すれば固定パイプラインが勝手に\(W\)で割ってくれるので、XY座標・太さとも\(W\)で割らずに射影変換した値をそのまま出力している
- この結果、\(Z\)がまともな値になるため、深度テストができるようになる(あとで使う)
- 細かいことだが、\(W\)の除算は絶対値を使っている
- 太さの計算に視点よりも手前の点を使うことがあり、この場合\(W\)がマイナスになって、XYの符号が反転してしまうため
- 直線を準備する関数と直線を引く関数を用意しておく
- 直線はモデル座標系で(0, 0, 0)-(1, 0, 0)という座標で固定になっている
- 直線の中心自体は1次元空間に収まっているので、頂点バッファにはX座標だけ載せ、座標変換で適切な位置に出力する
- 直線は0.01手前から0.01奥までで太さの計算をする
- この点は描画されないが、Zが0になってしまうと除算ができなくなり、関係するセグメントが表示されなくなる(ハマったらしい)
- 描画準備ができるまで描画ができないので、
draw
関数を変数にして、初期化時は何もしないアロー関数にしてある
- これでプロミス満了前に
deviceorientation
が来てもリサイズが来てもへっちゃら
- でも、レンダラーに渡す関数が生の
draw
関数ではなく、draw
を呼び出すアロー関数になっている
- レンダラーの
create
呼び出し時、引数にdraw
を指定すると、実引数はdraw
が指している空の関数を指す
- レンダラーには
draw
という変数だったという情報は伝わらないので、あとでdraw
の中身をすげかえても、レンダラーが握った関数が書き換わらない
- アロー関数にしておくと、レンダラーはアロー関数それ自体を握り、アロー関数が呼び出されると
draw
変数が指している関数を呼び出すため、呼び出し時点で設定されている(更新された)関数が呼び出される
- JavaScriptの変数とはなんぞやというのを正確に理解しているかが鍵
promise_all
でまたなにか黒魔術をやってます
$DCL
とかはあまり気にしないように
- 赤道のデータを準備する関数と描画する関数は
thik-line.js
に追い出しました
- 実際にやってみると、端末を立てて水平方向に(地球座標系のZ軸、端末座標系のY軸を軸として)回した場合は北をちゃんと表示し、そこから端末を起こしたり寝かせたり(端末のX軸を軸にして回転)しても指示は動かない(下図左)
- 同様に、端末を寝かせて水平方向(地球も端末もZ軸)に回した場合も北をちゃんと表示し、そこから端末の長辺方向(Y軸)を軸にして回転しても指示は動かない(下図右)
- 端末を立てた状態から横方向に倒して(地球座標系のY軸、端末座標系のZ軸を軸として回転させて)横向き(垂直・横位置)にしていくとなんだか不思議な動きをする(下図左)
- 指示が動かない回転では、共通して回転軸が水平である
- 試しに、不思議な動きをする状態で端末をナナメに保持し、ディスプレイを(地球座標系で)水平に横切るラインを軸に回転させると指示が動かない(下図右)
- ここから推察できる「端末の方向」の意味は、ディスプレイ面(端末座標系のXY平面)上にあり、地球座標系で水平な直線を軸として、端末が水平になるまで回転した時に、端末Y軸が指している方向である
- 水平には表が上になる場合と、裏が上になる場合の2通りがあるので、適切な方を選ぶ必要がある
- どちらが適切かは端末の姿勢によって決まるが、これが
webkitCompassHeading
の値が飛ぶ原因である
もう少し端末の回転が分かりやすいデモも作ったが、想定を超えて大きくなってしまい、プログラム的にも色々と覚え書きを書いておかないとマズい状態になってしまったので、しばし待たれたし。
太さのある線、再び 31 Jul 2024
太さのある線は赤道を書いた時にはじめて出てきたのだが、ここでもう一度太さのある線を描画する関数を書いたら、両者の太さの指定の仕方が違ってしまって、自分でも混乱している。
こういう行き当たりばったりの開発はよくないというお手本のような状態になってしまった。
このままでは今後さらなる混乱が予想されるため、先に進む前にここできっちり整理しておく。
- まず、当時とシェーダーが少し変わっている
- 以前はシェーダーの中で
w
で割っていたが、今回は固定パイプライン側で割っている
- 影響があるのは深度値が正しく扱えるかどうかで、太さの指定には影響しない
- このシェーダーの太さの指定は、\(W=1\)(言い換えると\(Z=-1\))でキャンバスのピクセルサイズの1/2が単位になっている
- 例えば、
gl.canvas.height
が1000のときに、太さ100と指定すると、キャンバス高さの1/20の太さの線が表示される(1/10ではない)
- なんでサイズが半分になるかというと、ビュー座標系の-1〜+1がキャンバスの左右あるいは上下に対応するから
- シェーダー内では単純に渡された幅と高さを乗じているだけなので、キャンバスサイズをそのまま渡すと、キャンバスの幅が200ならば、左端が-200、右端が+200という座標系になって、座標系の幅は400になる
- このまま特に細工をしなければ、キャンバスサイズに関わらず、一定の太さの線が表示される
- 赤道を書いた時は、
drawEquatorThickLine
関数は引数で指定された値をそのままシェーダーに流している
- 関数を呼ぶ側で
canvas.height * 0.01
を指定しているので、キャンバスの高さに比例して太さが変わる(幅は影響しない)
- モデルもキャンバスの高さに比例して拡大・縮小しているので、モデルの大きさと比例した太さになる
- 一方、今回作った
drawThickLine
関数では、関数の中でcanvas.height * 2
を乗じている
- したがって、太さとして1を指定すると、\(Z=-1\)の点でキャンバスの高さと同じ太さの線が表示される
- これは明らかによくない
- 今回はモデルがキャンバス高さに比例するようになっているので、線の太さもモデルに比例するが、もし、モデルをキャンバスの幅に比例させていたら、モデルのサイズと線の太さがちぐはぐになってしまう
- しかし、いちいちモデルと比例する値を計算するのは面倒なので、ある程度の選択肢を設けて呼び出し側に指定してもらうことにする
- モードを指定する場合は幅に対する実引数として2要素の配列を指定する
- 最初の要素はモードで、線の太さの基準をどの寸法にするかを示す
- ふたつめの要素は線の太さで、基準寸法に対する割合を1/1024単位で示す
- 太さが配列ではなかった場合は過去と互換の動作をする
drawEquatorThickLine
の場合は指定された数値をそのままシェーダーに流す
drawThickLine
の場合はキャンバス高さの2倍を乗じてシェーダーに流す
- モードは文字列で、次のとおり
- abs
- 絶対指定モード。線の太さはキャンバスサイズに影響しない。単位はZ=1におけるピクセルサイズ。
- min
- キャンバスの幅と高さのいずれか小さい方を基準とする
- max
- キャンバスの幅と高さのいずれか大きい方を基準とする
- horz
- キャンバスの幅を基準とする
- vert
- キャンバスの高さを基準とする
- diag
- キャンバスの対角線を基準とする
- \(Z=-1\)の場所に、縦横に線を引いている
- 適当に選択肢を選んで、ウィンドウサイズを変更してみると動作を確認できる
aspectFactor
は「アスペクト」という名がついているが、あとで透視投影行列のアスペクト比を指定するときに流用できる
prepareThickLine
はdrawThickLine
とdrawEquatorThickline
で共通に使う