英文:
Translate a touch on android-opengl into a ray/vector and check if it hits a plane
问题
I'm rather new to Android app development and using OpenGL ES. My basic goal is to create 4 simple squares in my Surface View. When the user clicks on the screen, I want to check which of the squares they clicked on (if any). This square should then be marked and change its color. When the user clicks on a second (different) square, I want to draw an arrow from the first square to the second one. I used the android tutorial for OpenGL ES as a starting point and tried to adapt it for my purpose.
I have problems checking whether the user has clicked on a rectangle. I've worked through many StackOverflow questions and other guides regarding OpenGL in Android and linear algebra in general. I found the following resources to be the most useful:
This is what I've understood so far: My rendered squares are defined in a Model-View-Projection Matrix. In order to check if the user clicked on any of these squares, I have to translate the click into a ray in world space coordinates. After that, I need to check if this ray collides with any of my squares, which all lie on the same plane.
Here is the part of the code where I've made the most edits:
// Part of the MyGLRenderer class
public class MyGLRenderer implements GLSurfaceView.Renderer {
// ... (other code)
public void checkCollision(float touchX, float touchY) {
// Step 1: normalize coordinates
// ... (other code)
// Inverted matrices
float[] invertedProjectionMatrix = new float[16];
float[] invertedMViewMatrix = new float[16];
Matrix.invertM(invertedProjectionMatrix, 0, mProjectionMatrix, 0);
Matrix.invertM(invertedMViewMatrix, 0, mViewMatrix, 0);
// Calculation Matrices
float[] unviewMatrix = new float[16];
float[] mouse_worldspace = new float[4];
// Getting mouse position in world space
// ... (other code)
// Getting the camera position
float[] cameraPosition = {0, 0, -3};
// Subtract camera position from the mouse_worldspace
float[] ray_unnormalized = new float[4];
for (int i = 0; i < 3; i++) {
ray_unnormalized[i] = mouse_worldspace[i] / mouse_worldspace[3] - cameraPosition[i];
}
// Normalize ray_vector
// ... (other code)
LinePlaneIntersection linePlaneIntersection = new LinePlaneIntersection();
LinePlaneIntersection.Vector3D rv = new LinePlaneIntersection.Vector3D(ray_vector[0], ray_vector[1], ray_vector[2]);
LinePlaneIntersection.Vector3D rp = new LinePlaneIntersection.Vector3D(mouse_worldspace[0], mouse_worldspace[1], mouse_worldspace[2]);
LinePlaneIntersection.Vector3D pn = new LinePlaneIntersection.Vector3D(0.0, 0.0, 0.0);
LinePlaneIntersection.Vector3D pp = new LinePlaneIntersection.Vector3D(0.0, 0.0, 1.0);
LinePlaneIntersection.Vector3D ip = linePlaneIntersection.intersectPoint(rv, rp, pn, pp);
Log.i(TAG, "checkCollision-intersection point: " + ip);
}
// ... (other code)
}
I've also added a moveSquare
method in the Square
class to update the square's coordinates.
I'm not sure if my approach is correct or if I'm using the right transformations and steps. Additionally, I have questions about the naming of matrices (mMVPMatrix
, mProjectionMatrix
, mViewMatrix
) and the purpose of the Projection Matrix.
I hope this provides a clear overview of my challenges. If more information is needed to understand the problem, please let me know. I appreciate any assistance and guidance.
英文:
im rather new to android app development/using OpenGL ES. My basic goal is to create 4 simple Squares in my Surface View, when the user clicks on the screen i want to check which of the squares he clicked (if any). This square should then be marked and change its color, when the user clicks on a second (different) square i want to draw an arrow from square1 to square2. I used the android tutorial for opengl es as a starting point and tried to adapt it for my purpose.
I have problems checking whether the user has clicked on a rectangle.
I worked through quite a lot of stackoverflow questions and other guides regarding opengl in Android and linear Algebra in general. I found these to be the most useful:<br>
Opengl Tutorial <br>
Mouse picking with ray casting <br>
Implement Ray Picking <br>
<br>
This is what i got out of this so far: <br>
My rendered squares are defined in an Model-View-Projection Matrix, in order to check if the user clicked on of these square i have to translate the click into a ray in the world space coordinates. After that i would have to check if this ray collides with on of my squares, which all lie on the same plane. <br/>
<br/>
Here is where i edited the most, on surfaceCreated i add the four squares and move them to their positions. When the user taps on the screen the checkCollision
-Method is called with the absolute screen coordinates. What i tried then was to translate the instructions from these posts:<br>
Implement Ray Picking <br>
Intersection of a line and a plane
<br>
public class MyGLRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "MyGLRenderer";
private HashMap<String, Square> mySquares = new HashMap<>();
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
private final float[] mRotationMatrix = new float[16];
private int screenWidth = 0;
private int screenHeight = 0;
private float mAngle;
private int square_number = 65;
private final float[][] colors = {
{0.29f, 0.57f, 1.0f, 1.0f},
{0.8f, 0.0f, 0.0f, 1.0f},
{0.13f, 0.8f, 0.0f, 1.0f},
{1.0f, 0.84f, 0.0f, 1.0f}};
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
//Adding the 4 squares to the grid and move them to their positions
String square_key = "";
square_key = addSquare();
this.mySquares.get(square_key).moveSquare(0.5f, 0.5f);
square_key = addSquare();
this.mySquares.get(square_key).moveSquare(0.5f, -0.5f);
square_key = addSquare();
this.mySquares.get(square_key).moveSquare(-0.5f, 0.5f);
square_key = addSquare();
this.mySquares.get(square_key).moveSquare(-0.5f, -0.5f);
}
public void checkCollision(float touchX, float touchY) {
//Step 1: normalize coordinates
float[] touchClipMatrix = new float[]{
2.0f * touchX / this.screenWidth - 1.0f,
1.0f - touchY * 2 / this.screenHeight,
0,
1.0f
};
//inverted matrices
float[] invertedProjectionMatrix = new float[16];
float[] invertedMViewMatrix = new float[16];
Matrix.invertM(invertedProjectionMatrix,0, mProjectionMatrix, 0);
Matrix.invertM(invertedMViewMatrix,0, mViewMatrix, 0);
//Calculation Matrices
float[] unviewMatrix = new float[16];
float[] mouse_worldspace = new float[4];
//Getting mouse position in world space
Matrix.multiplyMM(unviewMatrix, 0, invertedMViewMatrix, 0, invertedProjectionMatrix,0);
Matrix.multiplyMV(mouse_worldspace, 0 , unviewMatrix, 0 , touchClipMatrix, 0);
Log.i(TAG, "checkCollision-touchClipMatrix: "+ Arrays.toString(touchClipMatrix));
Log.i(TAG, "checkCollision-invertedProjectionMatrix: "+ Arrays.toString(invertedProjectionMatrix));
Log.i(TAG, "checkCollision-invertedMViewMatrix: "+ Arrays.toString(invertedMViewMatrix));
Log.i(TAG, "checkCollision-mouse_worldspace: "+ Arrays.toString(mouse_worldspace));
//Getting the camera position
float [] cameraPosition = {0, 0, -3};
//subtract camera position from the mouse_worldspace
float [] ray_unnormalized = new float[4];
for(int i = 0; i < 3; i++){
ray_unnormalized[i] = mouse_worldspace[i] / mouse_worldspace[3] - cameraPosition[i];
}
//normalize ray_vector
float ray_length = Matrix.length(ray_unnormalized[0], ray_unnormalized[1], ray_unnormalized[2]);
float [] ray_vector = new float[4];
for(int i=0; i<3; i++){
ray_vector[i] = ray_unnormalized[i]/ray_length;
}
Log.i(TAG, "checkCollision - ray_vector: "+ Arrays.toString(ray_vector));
LinePlaneIntersection linePlaneIntersection = new LinePlaneIntersection();
LinePlaneIntersection.Vector3D rv = new LinePlaneIntersection.Vector3D(ray_vector[0], ray_vector[1], ray_vector[2]);
LinePlaneIntersection.Vector3D rp = new LinePlaneIntersection.Vector3D(mouse_worldspace[0], mouse_worldspace[1], mouse_worldspace[2]);
LinePlaneIntersection.Vector3D pn = new LinePlaneIntersection.Vector3D(0.0, 0.0, 0.0);
LinePlaneIntersection.Vector3D pp = new LinePlaneIntersection.Vector3D(0.0, 0.0, 1.0);
LinePlaneIntersection.Vector3D ip = linePlaneIntersection.intersectPoint(rv, rp, pn, pp);
Log.i(TAG, "checkCollision-intersection point: "+ip);
}
public String addSquare() {
String keyName = String.valueOf((char) this.square_number);
this.mySquares.put(keyName, new Square(keyName, colors[this.square_number-65]));
this.square_number += 1;
return keyName;
}
public void logMatrices() {
Log.i(TAG, "MVPMatrice: " + Arrays.toString(this.mMVPMatrix));
Log.i(TAG, "mProjectionMarice: " + Arrays.toString(this.mProjectionMatrix));
Log.i(TAG, "mViewMatrice: " + Arrays.toString(this.mViewMatrix));
}
@Override
public void onDrawFrame(GL10 unused) {
float[] scratch = new float[16];
// Draw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// Set the camera position (View matrix)
//mySquare.moveSquare(0.25f, 0.25f);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0.0f, 0f, 1.0f, 0.0f);
// Matrix.scaleM(mViewMatrix, 0, 0.5f,0.5f,0);
// Matrix.translateM(mViewMatrix, 0, 2f, 1f, 0);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Create a rotation for the square
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0.0f, 1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw squares
for (Map.Entry<String, Square> s : this.mySquares.entrySet()) {
s.getValue().draw(scratch);
}
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
this.screenWidth = width;
this.screenHeight = height;
// Adjust the viewport based on geometry changes,
// such as screen rotation
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
public static int loadShader(int type, String shaderCode) {
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
public static void checkGlError(String glOperation) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, glOperation + ": glError " + error);
throw new RuntimeException(glOperation + ": glError " + error);
}
}
}
I added a moveSquare-Methode, because all squares have the same coordinates when initialized. I'm not sure if this is the right way to do it, please tell me if this is wrong/messes up the other calculations.
public class Square {
private String squareID;
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 squarePosition;" +
"void main() {" +
// The matrix must be included as a modifier of gl_Position.
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * squarePosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 squareColor;" +
"void main() {" +
" gl_FragColor = squareColor;" +
"}";
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
private int mProgram;
private int mPositionHandle;
private int mColorHandle;
private int mMVPMatrixHandle;
private static final String TAG = "Square";
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
private float squareCoords[] = {
-0.1f, 0.1f, 0.0f, // top left
-0.1f, -0.1f, 0.0f, // bottom left
0.1f, -0.1f, 0.0f, // bottom right
0.1f, 0.1f, 0.0f}; // top right
private final short drawOrder[] = {0, 1, 2, 0, 2, 3}; // order to draw vertices
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
//Fallback color
private float color[] = {0.2f, 0.709803922f, 0.898039216f, 1.0f};
/**
* Sets up the drawing object data for use in an OpenGL ES context.
*/
public Square(String id, float [] color) {
this.squareID = id;
if(color.length == 4) {
this.color = color;
}
//Buffers need to updated with the new square coordinates
updateBuffers();
//Shaders (should) only be prepared once when initializing a square
prepareShadersAndOpenGL();
}
private void prepareShadersAndOpenGL() {
// prepare shaders and OpenGL program
int vertexShader = MyGLRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
mProgram = GLES20.glCreateProgram(); // create empty OpenGL Program
GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
GLES20.glLinkProgram(mProgram); // create OpenGL program executables
}
public void updateBuffers() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
//Updating the square coordinates and updating to buffers
public void moveSquare(float deltaX, float deltaY) {
this.squareCoords[0] += deltaX;
this.squareCoords[3] += deltaX;
this.squareCoords[6] += deltaX;
this.squareCoords[9] += deltaX;
this.squareCoords[1] += deltaY;
this.squareCoords[4] += deltaY;
this.squareCoords[7] += deltaY;
this.squareCoords[10] += deltaY;
updateBuffers();
}
/**
* Encapsulates the OpenGL ES instructions for drawing this shape.
*
* @param mvpMatrix - The Model View Project matrix in which to draw
* this shape.
*/
public void draw(float[] mvpMatrix) {
// Add program to OpenGL environment
// Log.i(TAG, "Square ("+squareID+") mProgram: "+mProgram);
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "squarePosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(
mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "squareColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// MyGLRenderer.checkGlError("glGetUniformLocation");
// Apply the projection and view transformation
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// MyGLRenderer.checkGlError("glUniformMatrix4fv");
// Draw the square
GLES20.glDrawElements(
GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
public class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
private static final String TAG = "MyGLSurfaceView";
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
public MyGLSurfaceView(Context context) {
super(context);
// Create an OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
// Set the Renderer for drawing on the GLSurfaceView
mRenderer = new MyGLRenderer();
setRenderer(mRenderer);
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mRenderer.logMatrices();
mRenderer.checkCollision(x, y);
// mRenderer.setAngle(mRenderer.getAngle()+45f);
requestRender();
}
return true;
}
}
I know this is quite a lot to read through, so I will try to express my main questions/issues:
- Is my idea correct in general or am I using the wrong transformations/steps?
- Does the squareCoord-Array in the Square-class represent my Model-Matrix and at which point are these converted into world-coordinates?
- Why is the matrix that i give to the draw-Method in the square-class called mMVPMatrix, for me this implies that this matrix contains all three Matrices (Model, View, Projection). But at the point I'm calling the draw-Method I've just multiplied the Projection- with the View-Matrix, so where should the Model-Part come from? Am I missing something here or am I mixing up the terms for the matrices?
- I'm still trying to understand what the Projection-Matrix does/describes. I understood that it basicly defines the area that shall be rendered, everything thats not in this area will not show up on the screen. Is this area always relative to the camera(View) Position?
<br>
<br>
I hope i explained my problem(s) properly, maybe there is even a simpler solution for my problem in general. Thanks in advance to all who have read so far. I hope someone can help me with this
PS: This is my first question on Stackoverflow and my spelling might not be perfect, so sorry for that. If you are missing information to understand the problem / answer one of my questions just ask, I will try to add them as quickly as possible.
Here is some debug information:
- Recognized touch on position x=940.94604| y=407.9297
- MVPMatrix: [-4.4, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 2.5, 1.0, 0.0, 0.0, -3.0, 3.0]
- mProjectionMarix: [4.4, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, -2.5, -1.0, 0.0, 0.0, -10.5, 0.0]
- mViewMatrix: [-1.0, 0.0, -0.0, 0.0, 0.0, 1.0, -0.0, 0.0, 0.0, -0.0, -1.0, 0.0, 0.0, 0.0, -3.0, 1.0]
- checkCollision-touchClipMatrix: [0.7424927, 0.48493725, -3.0, 1.0]
- checkCollision-invertedProjectionMatrix: [0.22727272, -0.0, -0.0, -0.0, -0.0, 0.3333333, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0952381, -0.0, -0.0, -1.0, 0.23809522]
- checkCollision-invertedMViewMatrix: [-1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, -0.0, -3.0, 1.0]
- checkCollision-unview-Matrix[-0.22727272, 0.0, 0.0, 0.0, 0.0, 0.3333333, 0.0, 0.0, 0.0, 0.0, 0.2857143, -0.0952381, 0.0, -0.0, 0.28571433, 0.23809522]
- checkCollision-mouse_worldspace: [-0.16874833, 0.16164574, -0.5714286, 0.52380955]
- checkCollision-ray_unnormalized: [-0.3221559, 0.3085964, 1.9090909, 0.0]
- checkCollision-ray_length: 1.9605213
- checkCollision - ray_vector: [-0.16432154, 0.15740527, 0.9737669, 0.0]
- checkCollision-intersection point: (NaN, NaN, NaN)
答案1
得分: 1
The computation of ray_unnormalized
seems to be wrong. You cannot subtract Homogeneous coordinates the way you do. Konvert the mouse_worldspace
to a Cartesian coordinate. The Cartesian coordinate are the Quotients of the x, y, and z component and the w component (see Perspective divide).
The ray direction is the vector from the Cartesian camera position to the Cartesian mouse position:
//Getting the camera position
float [] cameraPosition = {0, 0, -6};
//subtract camera position from the mouse_worldspace
float [] ray_unnormalized = new float[4];
for(int i = 0; i < 3; i++){
ray_unnormalized[i] = mouse_worldspace[i] / mouse_worldspace[3] - cameraPosition[i];
}
英文:
The computation of ray_unnormalized
seems to be wrong. You cannot subtract Homogeneous coordinates the way you do. Konvert the mouse_worldspace
to a Cartesian coordinate. The Cartesian coordinate are the Quotients of the x, y, and z component and the w component (see Perspective divide).
The ray direction is the vector from the Cartesian camera position to the Cartesian mouse position:
//Getting the camera position
float [] cameraPosition = {0, 0, -6};
//subtract camera position from the mouse_worldspace
float [] ray_unnormalized = new float[4];
for(int i = 0; i < 3; i++){
ray_unnormalized[i] = mouse_worldspace[i] / mouse_worldspace[3] - cameraPosition[i];
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论