概要
スマホに3Dの空間が描かれているとして、オブジェクトをタッチして移動させる計算方法を考えてみた。
移動は、cameraのある平面に対して平行に移動させる。
前提
文法はC++で記載しているが、計算方法のアイデアが主な目的なので、コードはあまり気にしなくてよいかも。
OpenGLやVulkanの座標系を想定し、cameraはこちら向きを正とする。
https://learnopengl.com/Getting-started/Camera
結論
忙しい人向け。
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);
座標系の用語
引用:https://learnopengl.com/Getting-started/Coordinate-Systems
The global picture
SCREEN空間 -> WORLD空間
SCREEN空間にあるタッチ位置をWORLD空間に変換する。
ただし、SCREEN空間は2次元でWORLD空間は3次元なので、変換して得られるものはWORLD空間内の、cameraを原点とするタッチ位置への方向ベクトルであることに注意する。
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を移動させることなので、diffを求めることである。
アイデアとしては、mの方向ベクトルだけは上記のCamera2TouchScreen()で求まるので、長さを内積を駆使して求める。
//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に垂直なベクトルnの長さがわかっていないので、これを求める。
dは正規化されたcameraの方向ベクトルとして、dとoの内積dotNOを求めて、これをoに掛けることでnの長さを求める。
方向ベクトルdirectionMは上記の関数から求まり、方向ベクトルが求まれば内積dotNMは計算できるので、これを計算する。
今度はmの長さを求めたいので、nの長さをdotNMで割る。
方向ベクトルがわかり、長さもわかったのでベクトルmが一意に定まる。
あとはmodel行列のtranslate行列として、位置の差分diffを計算する。
感想
要は平面P上にあって、求まる方向ベクトルとの交点を求めたいのだが、ググると直線と平面の交点を求める方程式がヒットする。
人間は方程式の方が扱いやすいが、コンピュータはベクトルをそのまま用いるほうが扱いやすい。
方程式の解を求めるプログラムを書くことは大変なので、何とかしてコンピュータが得意な演算で求められないか、を考えた結果が以上となっている。
Qiitaで検索してもヒットしなかったので、有名なゲームエンジンには内部関数として用意されているのかもしれないが、自力でやってみると以上の方法もある(もちろん別の方法もある)程度の記事でした。