0
votes

Using Android Studio, my code renders an array of floats as a texture passed to GLSL with one float per texel in the range of 0 to 1, like a grayscale texture. For that i use GL_LUMINANCE as the internalFormat and format for glTexImage2D and GL_FLOAT for type. Running the app on an android device emulator works fine (which uses my PC's GPU), but on a real device (Samsung Galaxy S7) calling glTexImage2D gives error 1282, GL_INVALID_OPERATION. I thought it might be a problem with non power of two textures, but the width and height are certainly powers of two.

The code uses Jos Stam fluid simulation C code (compiled with the NDK, not ported) which outputs density values for a grid.

mSizeN is the width (same as height) of the fluid simulation grid, although 2 is added to it by the fluid sim for boundary conditions, so the width of the array returned is mSizeN + 2; 128 in this case.

The coordinate system is set up as an orthographic projection with 0.0,0.0 the top left of the screen, 1.0,1.0 is the bottom right. I just draw a full screen quad and use the interpolated position across the quad in GLSL as texture coordinates to the array containing density values. Nice easy way to render it.

This is the renderer class.

public class GLFluidsimRenderer implements GLWallpaperService.Renderer {

    private final String TAG = "GLFluidsimRenderer";

    private FluidSolver mFluidSolver = new FluidSolver();

    private float[] mProjectionMatrix = new float[16];

    private final FloatBuffer mFullScreenQuadVertices;

    private Context mActivityContext;

    private int mProgramHandle;
    private int mProjectionMatrixHandle;
    private int mDensityArrayHandle;
    private int mPositionHandle;
    private int mGridSizeHandle;

    private final int mBytesPerFloat = 4;
    private final int mStrideBytes = 3 * mBytesPerFloat;
    private final int mPositionOffset = 0;
    private final int mPositionDataSize = 3;

    private int mDensityTexId;

    public static int mSizeN = 126;

    public GLFluidsimRenderer(final Context activityContext) {
        mActivityContext = activityContext;

        final float[] fullScreenQuadVerticesData = {
                0.0f, 0.0f, 0.0f,
                0.0f, 1.0f, 0.0f,
                1.0f, 0.0f, 0.0f,
                1.0f, 1.0f, 0.0f,
        };

        mFullScreenQuadVertices = ByteBuffer.allocateDirect(fullScreenQuadVerticesData.length * mBytesPerFloat)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        mFullScreenQuadVertices.put(fullScreenQuadVerticesData).position(0);
    }

    public void onTouchEvent(MotionEvent event) {

    }

    @Override
    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
        GLES20.glDisable(GLES20.GL_DEPTH_TEST);
        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);

        String vertShader = AssetReader.getStringAsset(mActivityContext, "fluidVertShader");
        String fragShader = AssetReader.getStringAsset(mActivityContext, "fluidFragDensityShader");

        final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertShader);
        final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragShader);

        mProgramHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
                new String[] {"a_Position"});

        mDensityTexId = TextureHelper.loadTextureLumF(mActivityContext, null, mSizeN + 2, mSizeN + 2);
    }


    @Override
    public void onSurfaceChanged(GL10 glUnused, int width, int height) {
        mFluidSolver.init(width, height, mSizeN);

        GLES20.glViewport(0, 0, width, height);
        Matrix.setIdentityM(mProjectionMatrix, 0);
        Matrix.orthoM(mProjectionMatrix, 0, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f);
    }

    @Override
    public void onDrawFrame(GL10 glUnused) {
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

        GLES20.glUseProgram(mProgramHandle);

        mFluidSolver.step();

        TextureHelper.updateTextureLumF(mFluidSolver.get_density(), mDensityTexId, mSizeN + 2, mSizeN + 2);

        mProjectionMatrixHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_ProjectionMatrix");
        mDensityArrayHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_aDensity");
        mGridSizeHandle = GLES20.glGetUniformLocation(mProgramHandle, "u_GridSize");
        mPositionHandle = GLES20.glGetAttribLocation(mProgramHandle, "a_Position");

        double start = System.nanoTime();
        drawQuad(mFullScreenQuadVertices);
        double end = System.nanoTime();
    }

    private void drawQuad(final FloatBuffer aQuadBuffer) {
        // Pass in the position information
        aQuadBuffer.position(mPositionOffset);
        GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
                mStrideBytes, aQuadBuffer);
        GLES20.glEnableVertexAttribArray(mPositionHandle);

        // Attach density array to texture unit 0
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mDensityTexId);
        GLES20.glUniform1i(mDensityArrayHandle, 0);

        // Pass in the actual size of the grid.
        GLES20.glUniform1i(mGridSizeHandle, mSizeN + 2);

        GLES20.glUniformMatrix4fv(mProjectionMatrixHandle, 1, false, mProjectionMatrix, 0);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }
}

Here's the texture helper functions.

public static int loadTextureLumF(final Context context, final float[] data, final int width, final int height) {
    final int[] textureHandle = new int[1];

    GLES20.glGenTextures(1, textureHandle, 0);

    if (textureHandle[0] != 0) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);

        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
        GLES20.glPixelStorei(GLES20.GL_PACK_ALIGNMENT, 1);

        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
                (int) width, (int) height, 0, GLES20.GL_LUMINANCE, GLES20.GL_FLOAT,
                (data != null ? FloatBuffer.wrap(data) : null));
    }

    if (textureHandle[0] == 0)
        throw new RuntimeException("Error loading texture.");

    return textureHandle[0];
}

public static void updateTextureLumF(final float[] data, final int texId, final int w, final int h) {
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
    GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, (int)w, (int)h, GLES20.GL_LUMINANCE, GLES20.GL_FLOAT, (data != null ? FloatBuffer.wrap(data) : null));
}

Fragment shader.

precision mediump float;

uniform sampler2D u_aDensity;
uniform int u_GridSize;

varying vec4 v_Color;
varying vec4 v_Position;

void main()
{
    gl_FragColor = texture2D(u_aDensity, vec2(v_Position.x, v_Position.y));
}

Is the combination of GL_FLOAT and GL_LUMINANCE unsupported in OpenGL ES 2?

android emulator pic.

edit: To add, am i right in saying that each floating point value will be reduced to an 8-bit integer component when transferred with glTexImage2D (etc), so the majority of the floating point precision will be lost? In that case, it might be best to rethink the implementation of the simulator to output fixed point. That can be done easily, Stam even describes it in his paper.

1

1 Answers

0
votes

Table 3.4 of the spec shows the "Valid pixel format and type combinations" for use with glTexImage2D. For GL_LUMINANCE, the only option is GL_UNSIGNED_BYTE.

OES_texture_float is the relevant extension you'd need to check for.

An alternative approach which would work on more devices is to pack your data in multiple channels of an RGBA. Here is some discussion about packing a float value into an 8888. Note, however, that not all OpenGLES2 devices even support 8888 render targets, you might have to pack into a 4444.

Or you could use OpenGLES 3. Android is up to 61.3% support of OpenGLES3 according to this.

EDIT: On re-reading more carefully, there probably isn't any benefit in using any higher than an 8-bit texture, because when you write the texture to gl_FragColor in your fragment shader you are copying into a 565 or 8888 framebuffer, so any extra precision is lost anyway at that point.