OpenGLES入门笔记:Rajawali学习(4)物体点击事件的实现

背景

前面我们分别分析了Rajawali中场景的创建与物体的绘制,这篇文章我们将梳理一下点击事件的实现。这里我们参照general中的拖动例程,看看一个物体如何实现点击事件的捕获与响应。

实现流程

IObjectPicker

这个接口定义了Picker类的功能,Picker用于最终确定实际选择的物体到底是哪个。

//设置选择监听器
public void setOnObjectPickedListener(OnObjectPickedListener objectPickedListener);
//为Picker传入当前选择的坐标
public void getObjectAt(float x, float y);

ObjectColorPicker

这个类实现了IObjectPicker接口,主要实现如下功能:

1.设置objectPickedListener,在回调接口中返回当前选中物体的引用

2.getObjectAt 获取点击的位置,用来传入坐标,计算那个物体被选择

3.registerObject 注册要被点击的物体,其中mObjectLookup列表中维
护要选择物体的列表

4.pickObject 静态方法,在Scene中调用,通过屏幕点击位置来获取被点击物体在mObjectLookup列表中索引,并通过objectPickedListener的onObjectPicked的参数返回这个物体的引用

现在我们具体看下这些功能的实现。

物体注册监听

响应触摸事件的物体列表

private final List<Object3D> mObjectLookup

将物体注册到Picker中

public void registerObject(Object3D object) {
    if (!mObjectLookup.contains(object)) {
        mObjectLookup.add(object);
        //注意此处,mColorIndex是mObjectLookup中物体的索引值,被以某个颜色的方式记录在物体的一个叫做mPickingColor属性中
        object.setPickingColor(mColorIndex);
        ++mColorIndex;
    }
}

下面是setPickingColor方法的实现

public void setPickingColor(int colorIndex) {
    mPickingIndex = colorIndex;
    mPickingColor[RED] = Color.red(colorIndex) / 255f;
    mPickingColor[GREEN] = Color.green(colorIndex) / 255f;
    mPickingColor[BLUE] = Color.blue(colorIndex) / 255f;
    mPickingColor[ALPHA] = Color.alpha(colorIndex) / 255f;
}

绘制pickingMaterial

我们会发现Object3D类中还有一个变量叫做mColor。这个颜色是物体真实显示的颜色,那么,mPickingColor是什么呢?此处不免让人感觉疑惑,我们从字面意思来看,这个颜色值应该是与物体选择有关的,带着这个问题,我们继续追踪。

我们接着看Object3D类的renderColorPicking方法,mPickingColor的值最终在这里被使用。

...
//pickingMaterial只初始化顶点和颜色
pickingMaterial.useProgram();
pickingMaterial.setVertices(mGeometry.getVertexBufferInfo());
pickingMaterial.setColor(mPickingColor);
pickingMaterial.applyParams();
...
//将绘制mMaterial的矩阵给了pickingMaterial
pickingMaterial.setMVPMatrix(mMVPMatrix);
pickingMaterial.setModelMatrix(mMMatrix);
pickingMaterial.setModelViewMatrix(mMVMatrix);

//绘制pickingMaterial     
int bufferType = mGeometry.getIndexBufferInfo().bufferType == Geometry3D.BufferType.SHORT_BUFFER ? GLES20.GL_UNSIGNED_SHORT : GLES20.GL_UNSIGNED_INT;
    GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, mGeometry.getIndexBufferInfo().bufferHandle);


GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);

通过实验打印信息我们可以知道,这段代码会在触摸到物体的时候调用一次。也就是说,这个pickingMaterial只有在被点击的时候才会被绘制,而它的颜色就是就是我们上文中所说,那个由索引值生成的颜色

我们可以继续跟踪这个颜色的传递,可以发现所有相关的颜色都被传递到shader的一个uniform中。

muColorHandle = getUniformLocation(programHandle, DefaultShaderVar.U_COLOR);

我们可以打印一下这种情况下的的顶点和片元着色器,来看一下这个color值是怎么处理的。
VertexShader

    precision mediump float;
    uniform mat4 uMVPMatrix;
    uniform vec4 uColor;
    uniform mat4 uModelMatrix;
    uniform mat3 uNormalMatrix;
    uniform mat4 uModelViewMatrix;
    attribute vec3 aNormal;
    attribute vec2 aTextureCoord;
    attribute vec4 aPosition;
    varying vec3 vNormal;
    varying vec4 vColor;
    varying vec2 vTextureCoord;
    varying vec3 vEyeDir;
    vec4 gColor;
    vec4 gPosition;
    vec3 gNormal;
    vec2 gTextureCoord;

    void main() {
        gPosition = aPosition;
        gNormal = aNormal;                                                                       gTextureCoord = aTextureCoord;
        gColor = uColor;
        gl_Position = uMVPMatrix * gPosition;
        vNormal = normalize(uNormalMatrix * gNormal);                                                                      vTextureCoord = gTextureCoord;
        vColor = gColor;
        vEyeDir = vec3(uModelViewMatrix * gPosition);
    }

fragment

    precision mediump float;
    uniform float uColorInfluence;
    varying vec3 vNormal;
    varying vec4 vColor;
    varying vec2 vTextureCoord;
    varying vec3 vEyeDir;
    float gShadowValue;
    float gSpecularValue;
    vec4 gColor;
    vec3 gNormal;
    vec2 gTextureCoord;

    void main() {
        gNormal = normalize(vNormal);
        gTextureCoord = vTextureCoord;
        gColor = uColorInfluence * vColor;
        gShadowValue = 0.0;
        gSpecularValue = 1.0;
        gl_FragColor = gColor;
    }

uColorInfluence 在FragmentShander中默认为1,所以,最终这个color是原封不动地给了gl_FragColor。这就是说,当我们点击的那一时刻,其实物体显示的是pickingMaterial,而其他时刻还是显示正常的Material。

获取点击物体

有了上面注册,我们接着整理点击事件时如何传递给物体的。

这里写图片描述

如上图所示,其实我们在上面分析的是Object3D的renderColorPicking方法,这个方法由Scene调用,在点击时,调用其中所有物体的renderColorPicking方法,也就是说给所有的物体换了张皮,而皮的颜色就是物体在Picker的物体列表中的索引值

下面我们就可以通过获取物体的颜色,来得到这个索引了。ObjectColorPicker的静态方法pickObject就是帮我们处理这个问题的。

public static void pickObject(ColorPickerInfo pickerInfo) {
        final ObjectColorPicker picker = pickerInfo.getPicker();
        OnObjectPickedListener listener = picker.mObjectPickedListener;
        if (listener != null) {
            final ByteBuffer pixelBuffer = ByteBuffer.allocateDirect(4);
            pixelBuffer.order(ByteOrder.nativeOrder());

            GLES20.glReadPixels(pickerInfo.getX(),
                    picker.mRenderer.getViewportHeight() - pickerInfo.getY(),
                    1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
            pixelBuffer.rewind();

            //使用pickerInfo中的信息生成生成模型的索引值
            final int r = pixelBuffer.get(0) & 0xff;
            final int g = pixelBuffer.get(1) & 0xff;
            final int b = pixelBuffer.get(2) & 0xff;
            final int a = pixelBuffer.get(3) & 0xff;
            final int index = Color.argb(a, r, g, b);

            if (0 <= index && index < picker.mObjectLookup.size()) {
                // Index may have holes due to unregistered objects
                Object3D pickedObject = picker.mObjectLookup.get(index);
                if (pickedObject != null) {
                    //将被触摸的模型作为回掉接口的参数传递回去,我们在renderer中就可以得到物体的引用。
                    listener.onObjectPicked(pickedObject);
                    return;
                }
            }
            listener.onNoObjectPicked();
        }
    }

可见,此处我们直接获取触摸点的颜色值,然后将其转换为数值并在列表中查找。如果列表中这个索引不为空,就将这个物体返回。

OnObjectPickedListener

回到我们的render中,由于需要进行触摸事件的监听,所以我们的render类实现了OnObjectPickedListener接口,这样直接在我们的render中实现public void onObjectPicked(@NonNull Object3D object)方法。这里参数就是我们当前所触摸的模型,所以我们可以从这里得到当前触摸模型的引用。

同时这里我们发现上面pickerInfo中的x,y其实是在getObjectAt方法中传入的,这里的x,y就是我们在touch事ACTION_DOWN时按下的触摸点。这里最终调用了下面方法,生成一个ColorPickerInfo,用来标识选中物体的信息。

mRenderer.getCurrentScene().requestColorPicking(new ColorPickerInfo(x, y, this));

这样,这个ColorPickerInfo被传入了Scene中。Scene的render方法调用doColorPicking(mPickerInfo);方法,而doColorPicking中调用ObjectColorPicker.pickObject(pickerInfo);,也就是我们文中讲到的,最终这里会通过回调接口返回那个被点击物体的引用。

而在接口的第三个方法stopMovingSelectedObject中,我们只需把被触摸模型的引用置为空即可。

实现模型随手指拖拽的效果

能获得模型的引用,我们只需修改模型的X,Y即可(一般情况手指平面上移动,不改变模型的Z坐标)。所以此处我们需要做的是将屏幕的X,Y坐标转换为模型在场景中的世界坐标。Demo中的处理方法如下,此处还需进一步理解。

    public void moveSelectedObject(float x, float y){
            if (mSelectedObject == null)
                return;

            //将当前xy在近平面的屏幕坐标转换为世界坐标,mNearPos4为输出坐标
            GLU.gluUnProject(x, getViewportHeight() - y, 0, mViewMatrix.getDoubleValues(), 0,
                    mProjectionMatrix.getDoubleValues(), 0, mViewport, 0, mNearPos4, 0);

            //将当前xy在远平面的屏幕坐标转换为世界坐标,mFarPos4为输出坐标
            GLU.gluUnProject(x, getViewportHeight() - y, 1.f, mViewMatrix.getDoubleValues(), 0,
                    mProjectionMatrix.getDoubleValues(), 0, mViewport, 0, mFarPos4, 0);

            //将坐标同时除以w
            mNearPos.setAll(mNearPos4[0] / mNearPos4[3], mNearPos4[1]
                    / mNearPos4[3], mNearPos4[2] / mNearPos4[3]);
            mFarPos.setAll(mFarPos4[0] / mFarPos4[3],
                    mFarPos4[1] / mFarPos4[3], mFarPos4[2] / mFarPos4[3]);

            //用归一化Z的值计算一个比率
            double factor = (Math.abs(mSelectedObject.getZ()) + mNearPos.z)
                    / (getCurrentCamera().getFarPlane() - getCurrentCamera()
                    .getNearPlane());

            mNewObjPos.setAll(mFarPos);
            mNewObjPos.subtract(mNearPos);
            mNewObjPos.multiply(factor);
            mNewObjPos.add(mNearPos);

            mSelectedObject.setX(mNewObjPos.x);
            mSelectedObject.setY(mNewObjPos.y);
        }

总结

上述流程比较复杂,在这里捋一下基本思路。首先,有一个Picker用作维护所有的要进行点击的物体列表。每个物体都有两张皮(两个Material,分别是mMaterial和pickingMatrial),其中mMaterial存储物体真实的材质,并且在一般情况下显示,pickingMatrial中存储一个颜色值,这个颜色值其实是这个物体在Picker中维护列表的索引值。当我们点击物体时,这一帧绘制其实绘制的是pickingMatrial这张皮,紧接着我们就把触摸点的颜色拿出来,还原成数字,并以此为索引在列表中寻找物体,如果找到,就把这个物体的引用返回,这样就获得了被点击的物体。

那么为什么要兜这么大的圈子来处理而不直接根据坐标判断触点呢?这里我猜想是这样的。对于一些简单的几何体我们很容易确定一个点是否包含在物体内,但是,脑补一个五角星,如何判断一个点是否在这种不规则图形内部呢?根据不同形状设置判断规则显然是不合适的。此处使用物体颜色作为索引就避开了这个问题,只要在触摸的那一帧进行颜色替换,就能直接拿到物体的索引。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值