[C++]タッチで3Dオブジェクトを移動させる計算方法

# 概要
スマホに3Dの空間が描かれているとして、オブジェクトをタッチして移動させる計算方法を考えてみた。
移動は、cameraのある平面に対して平行に移動させる。

# 前提
文法はC++で記載しているが、計算方法のアイデアが主な目的なので、コードはあまり気にしなくてよいかも。
OpenGLやVulkanの座標系を想定し、cameraはこちら向きを正とする。
https://learnopengl.com/Getting-started/Camera

# 結論
忙しい人向け。
``` c++
glm::vec4 Camera2TouchScreen(glm::vec2* touchPos)
{
  //screen space
  float x = touchPos[0].x / width;
  float y = touchPos[0].y / height;
  //screen space to clip space
  x = x * 2.0f - 1.0f;
  y = y * 2.0f - 1.0f;
  //clip space to view space
  glm::mat4 invp = glm::inverse(proj);
  glm::vec4 sview = invp * glm::vec4(x, y, 1.0f, 1.0f);
  //view space to world space
  return glm::inverse(view) * glm::vec4(glm::vec3(sview), 0.0f);
}

//calc diff(translate matrix)
glm::vec3 o = object - cameraPos;
float dotNO = glm::dot(-d, glm::normalize(o));
float nLength = dotNO * glm::length(o);
glm::vec3 directionM = Camera2TouchScreen(touchPos);
float dotNM = glm::dot(-d, glm::normalize(directionM));
float mLength = nLength / dotNM;
glm::vec3 m = cameraPos + mLength * glm::normalize(directionM);
mat4 diff = glm::translate(diff, m - object);
```


# 座標系の用語
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1446998/3434087f-348d-905f-37b0-c23149e21945.png)
引用:https://learnopengl.com/Getting-started/Coordinate-Systems
<i>The global picture</i>

# SCREEN空間 -> WORLD空間
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1446998/b828907a-69f6-d0c7-5113-6cb79c1b89b0.png)

SCREEN空間にあるタッチ位置をWORLD空間に変換する。
ただし、SCREEN空間は2次元でWORLD空間は3次元なので、変換して得られるものはWORLD空間内の、cameraを原点とするタッチ位置への方向ベクトルであることに注意する。
``` c++
glm::vec4 Camera2TouchScreen(glm::vec2* touchPos)
{
  //screen space
  float x = touchPos[0].x / width;
  float y = touchPos[0].y / height;
  //screen space to clip space
  x = x * 2.0f - 1.0f;
  y = y * 2.0f - 1.0f;
  //clip space to view space
  glm::mat4 invp = glm::inverse(proj);
  glm::vec4 sview = invp * glm::vec4(x, y, 1.0f, 1.0f);
  //view space to world space
  return glm::inverse(view) * glm::vec4(glm::vec3(sview), 0.0f);
}
```

AndroidではAMotionEvent_getで取ってこれるタッチ位置はピクセルで返されるので、取得したピクセル位置からWORLD空間内の、camera -> タッチ位置の4次元方向ベクトルが上記で得られる。
ここで、
touchPos : タッチ位置(ピクセル)
width : スマホ画面の横の長さ(ピクセル)
height : スマホ画面の縦の長さ(ピクセル)
proj : projection行列(mat4)
view : view行列(mat4)
である。

# WORLD空間
計算したいベクトルがすべてWORLD空間に持ってこれたので、WORLD空間内で計算を行う。
行いたいことはcameraの向きに垂直な平面に平行になるように、タッチ位置の分だけobjectを移動させることなので、<i>diff</i>を求めることである。
アイデアとしては、<i>m</i>の方向ベクトルだけは上記のCamera2TouchScreen()で求まるので、長さを<b>内積</b>を駆使して求める。
``` c++
//length n
glm::vec3 o = object - cameraPos;
float dotNO = glm::dot(-d, glm::normalize(o));
float nLength = dotNO * glm::length(o);
//direction of m
glm::vec3 directionM = Camera2TouchScreen(touchPos);
float dotNM = glm::dot(-d, glm::normalize(directionM));
float mLength = nLength / dotNM;
glm::vec3 m = cameraPos + mLength * glm::normalize(directionM);
mat4 diff = glm::translate(diff, m - object);
```

objectの位置は分かっているが、objectがあってcameraの向きに垂直な平面Pに垂直なベクトル<i>n</i>の長さがわかっていないので、これを求める。
<i>d</i>は正規化されたcameraの方向ベクトルとして、<i>d</i>と<i>o</i>の内積dotNOを求めて、これを<i>o</i>に掛けることで<i>n</i>の長さを求める。
方向ベクトル<i>directionM</i>は上記の関数から求まり、方向ベクトルが求まれば内積dotNMは計算できるので、これを計算する。
今度は<i>m</i>の長さを求めたいので、<i>n</i>の長さをdotNMで割る。
方向ベクトルがわかり、長さもわかったのでベクトル<i>m</i>が一意に定まる。
あとはmodel行列のtranslate行列として、位置の差分<i>diff</i>を計算する。

# 感想
要は平面P上にあって、求まる方向ベクトルとの交点を求めたいのだが、ググると直線と平面の交点を求める方程式がヒットする。
人間は方程式の方が扱いやすいが、コンピュータはベクトルをそのまま用いるほうが扱いやすい。
方程式の解を求めるプログラムを書くことは大変なので、何とかしてコンピュータが得意な演算で求められないか、を考えた結果が以上となっている。
Qiitaで検索してもヒットしなかったので、有名なゲームエンジンには内部関数として用意されているのかもしれないが、自力でやってみると以上の方法もある（もちろん別の方法もある）程度の記事でした。