How to deal with orientation change in Open GL ES - android

I'm trying out Open GL 2 for android. So far I have been able to initialize GLES20 and draw some simple shapes on it. The problem is that I can't find information on how should be GLES20 configured for screen orientation change, as for now I just have a black screen after device rotation. It is even possible for GLSurfaceView to rotate after screen has been rotated, or do I have to do it manually using some matrix?
Open GL init code:
object OpenGLBuilder {
val vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}"
val fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}"
var mProgram = -1;
init {
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram().also {
// add the vertex shader to program
GLES20.glAttachShader(it, vertexShader)
// add the fragment shader to program
GLES20.glAttachShader(it, fragmentShader)
// creates OpenGL ES program executables
GLES20.glLinkProgram(it)
}
}
fun loadShader(type: Int, shaderCode: String): Int {
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
return GLES20.glCreateShader(type).also { shader ->
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
}
}
}
My shape class:
class Triangle(val triangleCoords: FloatArray) {
// Set color with red, green, blue and alpha (opacity) values
val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
private var vertexBuffer: FloatBuffer =
// (number of coordinate values * 4 bytes per float)
ByteBuffer.allocateDirect(triangleCoords.size * 4).run {
// use the device hardware's native byte order
order(ByteOrder.nativeOrder())
// create a floating point buffer from the ByteBuffer
asFloatBuffer().apply {
// add the coordinates to the FloatBuffer
put(triangleCoords)
// set the buffer to read the first coordinate
position(0)
}
}
private val COORDS_PER_VERTEX = 3
private var positionHandle = 0
private var mColorHandle = 0;
private val vertexCount = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride = COORDS_PER_VERTEX * 4
fun draw(program: Int) {
GLES20.glUseProgram(program)
positionHandle = GLES20.glGetAttribLocation(program, "vPosition").also {
GLES20.glEnableVertexAttribArray(it)
GLES20.glVertexAttribPointer(
it,
COORDS_PER_VERTEX,
GLES20.GL_FLOAT,
false,
vertexStride,
vertexBuffer
)
}
mColorHandle = GLES20.glGetUniformLocation(program, "vColor").also{
GLES20.glUniform4fv(mColorHandle, 1, color, 0)
}
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
GLES20.glDisableVertexAttribArray(positionHandle)
}
}
Then I just create and use single instances of shapes like this:
object SceneObjects {
val triangle1 = Triangle(floatArrayOf(
0f, 0.5f, 0f,
-0.5f, -0.3f, 0f,
0.5f, -0.3f, 0f
))
val square1 = Square2(floatArrayOf(
-0.5f, 0.5f, 0f,
-0.5f, -0.5f, 0f,
0.5f, -0.5f, 0f,
0.5f, 0.5f, 0f
))
}
And GLSurfaceView:
class MyGLSurfaceView(context: Context) : GLSurfaceView(context), GLSurfaceView.Renderer {
init{
setEGLContextClientVersion(2)
setRenderer(this)
// Render the view only when there is a change in the drawing data
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
}
override fun onDrawFrame(unused: GL10) {
// Redraw background color
SceneObjects.triangle1.draw(OpenGLBuilder.mProgram)
}
override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
}

I think that there's two things you want to do to handle orientation change neatly.
Firstly, by default your OpenGL context and all created objects are destroyed when entering the background. There's an argument that you should just let that happen and add code to recreate all the resources, but if you'd rather have an easy life then just use setPreserveEGLContextOnPause and never worry about it again.
You might be thinking "but it was an orientation change, my app didn't enter the background". Well, by default an Android activity gets restarted after an orientation change which might be messing things up. This behaviour is detailed here. You probably want to use this lot in the manifest for your activity: android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"

Related

Only seeing glClear color with no geometry on Android OpenGL ES

I am attempting to write a very simple Android application that uses OpenGL to display a green background and render a single cyan triangle over it, to verify that my code properly displays some geometry. I am running into an issue where the call to glClearColor/glClear to set the background is visible and makes the view green as intended, but I see no visible change from calling glDrawArrays after pointing the 0th attribute array to a FloatBuffer containing vertex coords. All my vertex shader does is pass the position as a vec4 to the fragment shader, which always just sets the output color to cyan, so I would expect to see the view green with one cyan triangle, but instead I see only the green background, and I am unsure why.
Main activity file (imports omitted):
setupDefaultProgram()
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES30.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
glRenderer.renderFunc()
}
}
Renderer code:
class GlRenderer {
companion object {
// Pass through position and UV values
val vertexSource = """
#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, -0.5, 1.0);
}
""".trimIndent()
// Eventually get the texture value, for now, just make it cyan so I can see it
val fragmentSource = """
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(0.0, 1.0, 1.0, 1.0);
}
""".trimIndent()
}
private var vertexBuffer: FloatBuffer
private var defaultProgram: Int = -1
private var vertexLocation: Int = -1
private fun checkGlError(msg: String) {
val errCodeEgl = EGL14.eglGetError()
val errCodeGl = GLES30.glGetError()
if (errCodeEgl != EGL14.EGL_SUCCESS || errCodeGl != GLES30.GL_NO_ERROR) {
throw RuntimeException(
"$msg - $errCodeEgl(${GLU.gluErrorString(errCodeEgl)}) : $errCodeGl(${
GLU.gluErrorString(
errCodeGl
)
})"
)
}
}
init {
// Flat square
// Am I allocating and writing to these correctly?
val vertices = floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)
vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4).asFloatBuffer().also {
it.put(vertices)
it.position(0)
}
}
fun setupDefaultProgram() {
defaultProgram = makeProgram(
mapOf(
GLES30.GL_VERTEX_SHADER to vertexSource,
GLES30.GL_FRAGMENT_SHADER to fragmentSource
)
)
vertexLocation = GLES30.glGetAttribLocation(defaultProgram, "position")
checkGlError("Getting uniform")
}
private fun compileShader(source: String, shaderType: Int): Int {
val shaderId = GLES30.glCreateShader(shaderType)
checkGlError("Create shader")
if (shaderId == 0) {
Log.e("Native", "Could not create shader for some reason")
checkGlError("Could not create shader")
}
GLES30.glShaderSource(shaderId, source)
checkGlError("Setting shader source")
GLES30.glCompileShader(shaderId)
val statusBuffer = IntArray(1)
GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, statusBuffer, 0)
val shaderLog = GLES30.glGetShaderInfoLog(shaderId)
Log.d("Native", "Compiling shader #$shaderId : $shaderLog")
if (statusBuffer[0] == 0) {
GLES30.glDeleteShader(shaderId)
checkGlError("Failed to compile shader $shaderId")
}
return shaderId
}
private fun makeProgram(sources: Map<Int, String>): Int {
val program = GLES30.glCreateProgram()
checkGlError("Create program")
sources.forEach {
val shader = compileShader(it.value, it.key)
GLES30.glAttachShader(program, shader)
}
val linkBuffer = IntArray(1)
GLES30.glLinkProgram(program)
GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkBuffer, 0)
if (linkBuffer[0] == 0) {
GLES30.glDeleteProgram(program)
checkGlError("Failed to link program $program")
}
return program
}
// Called to actually draw to the surface. When fully implemented it should draw whatever is
// on the associated texture, but for now, to debug, I just want to verify I can draw vertices,
// but it seems I cannot?
fun renderFunc() {
GLES30.glClearColor(0f, 1f, 0.5f, 1f)
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
checkGlError("Clearing")
GLES30.glUseProgram(defaultProgram)
checkGlError("Use program")
GLES30.glEnableVertexAttribArray(vertexLocation)
vertexBuffer.position(0)
FloatArray(2 * 4).apply {
vertexBuffer.get(this)
vertexBuffer.position(0)
Log.d("Native", "Vertex buffer ${contentToString()}")
}
GLES30.glVertexAttribPointer(vertexLocation, 2, GLES30.GL_FLOAT, false, 0, vertexBuffer)
checkGlError("Attribute 0")
// Just render a triangle
GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 3)
GLES30.glFinish()
checkGlError("Finished GL")
}
}
My debug output logs what I would expect:
D/Native: Compiling shader #2 :
D/Native: Compiling shader #3 :
D/Native: Vertex buffer [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]
I am including <uses-feature android:glEsVersion="0x00020000" android:required="true" /> in my manifest. Where am I going wrong that the geometry of the triangle is either not rendered to the screen or is not visible?
When you create Buffer for GL, you need to specify the native byte-order.
val vertices = floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)
vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().also {
it.put(vertices)
it.position(0)
}
To expand on #ardget's answer: A byte-order is the internal order of the ByteBuffer's data, which basically determines how the data is stored in memory.
There are two 'byte-orders' possible: little-endian and big-endian. I won't go in to too much detail here, but big-endian stores data from the most significant to least significant value and little-endian stores data from least significant to most significant value. Your machine has a native order, which can be either little-endian or big-endian.
When you call ByteBuffer.allocateDirect, Java automatically sets the byte-order to be big-endian. But when you then use glBufferData, (if I'm not mistaken) OpenGL expects the byte-order to be the machine's native order. If the native-order is not big-endian, but instead is little-endian (as in your machine), then problems occur.
You can fix this by directly specifying the byte-order as the native byte order by using .order(ByteOrder.nativeOrder()).

Cannot get OpenGL ES Android plugin to show drawn vertex array

I am attempting to write a Flutter plugin for Android to allow me to directly write pixels using a Texture, so I need to make a SurfaceTexture available, and I want to be able to draw arbitrary pixel data to it using a single textured quad. For now, for debugging, I am simply trying to draw a single cyan triangle over a magenta background to verify my vertices are being drawn correctly, but it appears they are not. The glClear call is doing what I expect, as the magenta background is being shown instead of the black color that would otherwise be behind it, and I can change that color by changing what I pass to glClearColor, so in some way, the texture is being rendered, but I see no evidence that calling glDrawArrays is accomplishing anything. The code containing all of my interfacing with OpenGL ES is in the file below, and the drawTextureToCurrentSurface method is where both glClear and glDrawArrays are being called:
class EglContext {
companion object {
// Pass through position and UV values
val vertexSource = """
#version 300 es
precision mediump float;
/*layout(location = 0)*/ in vec2 position;
/*layout(location = 1)*/ in vec2 uv;
out vec2 uvOut;
void main() {
gl_Position = vec4(position, -0.5, 1.0);
uvOut = uv;
}
""".trimIndent()
// Eventually get the texture value, for now, just make it cyan so I can see it
val fragmentSource = """
#version 300 es
precision mediump float;
in vec2 uvOut;
out vec4 fragColor;
uniform sampler2D tex;
void main() {
vec4 texel = texture(tex, uvOut);
// Effectively ignore the texel without optimizing it out
fragColor = texel * 0.0001 + vec4(0.0, 1.0, 1.0, 1.0);
}
""".trimIndent()
var glThread: HandlerThread? = null
var glHandler: Handler? = null
}
private var display = EGL14.EGL_NO_DISPLAY
private var context = EGL14.EGL_NO_CONTEXT
private var config: EGLConfig? = null
private var vertexBuffer: FloatBuffer
private var uvBuffer: FloatBuffer
//private var indexBuffer: IntBuffer
private var defaultProgram: Int = -1
private var uniformTextureLocation: Int = -1
private var vertexLocation: Int = -1
private var uvLocation: Int = -1
var initialized = false
private fun checkGlError(msg: String) {
val errCodeEgl = EGL14.eglGetError()
val errCodeGl = GLES30.glGetError()
if (errCodeEgl != EGL14.EGL_SUCCESS || errCodeGl != GLES30.GL_NO_ERROR) {
throw RuntimeException(
"$msg - $errCodeEgl(${GLU.gluErrorString(errCodeEgl)}) : $errCodeGl(${
GLU.gluErrorString(
errCodeGl
)
})"
)
}
}
init {
// Flat square
// Am I allocating and writing to these correctly?
val vertices = floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)
vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4).asFloatBuffer().also {
it.put(vertices)
it.position(0)
}
val uv = floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f)
uvBuffer = ByteBuffer.allocateDirect(uv.size * 4).asFloatBuffer().also {
it.put(uv)
it.position(0)
}
// Not being used until I can figure out what's currently not working
/*val indices = intArrayOf(0, 1, 2, 2, 1, 3)
indexBuffer = ByteBuffer.allocateDirect(indices.size * 4).asIntBuffer().also {
it.position(0)
it.put(indices)
it.position(0)
}*/
if (glThread == null) {
glThread = HandlerThread("flutterSoftwareRendererPlugin")
glThread!!.start()
glHandler = Handler(glThread!!.looper)
}
}
// Run OpenGL code on a separate thread to keep the context available
private fun doOnGlThread(blocking: Boolean = true, task: () -> Unit) {
val semaphore: Semaphore? = if (blocking) Semaphore(0) else null
glHandler!!.post {
task.invoke()
semaphore?.release()
}
semaphore?.acquire()
}
fun setup() {
doOnGlThread {
Log.d("Native", "Setting up EglContext")
display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
if (display == EGL14.EGL_NO_DISPLAY) {
Log.e("Native", "No display")
checkGlError("Failed to get display")
}
val versionBuffer = IntArray(2)
if (!EGL14.eglInitialize(display, versionBuffer, 0, versionBuffer, 1)) {
Log.e("Native", "Did not init")
checkGlError("Failed to initialize")
}
val configs = arrayOfNulls<EGLConfig>(1)
val configNumBuffer = IntArray(1)
var attrBuffer = intArrayOf(
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16,
//EGL14.EGL_STENCIL_SIZE, 8,
//EGL14.EGL_SAMPLE_BUFFERS, 1,
//EGL14.EGL_SAMPLES, 4,
EGL14.EGL_NONE
)
if (!EGL14.eglChooseConfig(
display,
attrBuffer,
0,
configs,
0,
configs.size,
configNumBuffer,
0
)
) {
Log.e("Native", "No config")
checkGlError("Failed to choose a config")
}
if (configNumBuffer[0] == 0) {
Log.e("Native", "No config")
checkGlError("Got zero configs")
}
Log.d("Native", "Got Config x${configNumBuffer[0]}: ${configs[0]}")
config = configs[0]
attrBuffer = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE
)
context = EGL14.eglCreateContext(display, config, EGL14.EGL_NO_CONTEXT, attrBuffer, 0)
if (context == EGL14.EGL_NO_CONTEXT) {
Log.e("Native", "Failed to get any context")
checkGlError("Failed to get context")
}
Log.d("Native", "Context = $context\n 'Current' = ${EGL14.eglGetCurrentContext()}")
initialized = true
}
}
// Called by my plugin to get a surface to register for Texture widget
fun buildSurfaceTextureWindow(surfaceTexture: SurfaceTexture): EGLSurface {
var _surface: EGLSurface? = null
doOnGlThread {
val attribBuffer = intArrayOf(EGL14.EGL_NONE)
val surface =
EGL14.eglCreateWindowSurface(display, config, surfaceTexture, attribBuffer, 0)
if (surface == EGL14.EGL_NO_SURFACE) {
checkGlError("Obtained no surface")
}
EGL14.eglMakeCurrent(display, surface, surface, context)
Log.d("Native", "New current context = ${EGL14.eglGetCurrentContext()}")
if (defaultProgram == -1) {
defaultProgram = makeProgram(
mapOf(
GLES30.GL_VERTEX_SHADER to vertexSource,
GLES30.GL_FRAGMENT_SHADER to fragmentSource
)
)
uniformTextureLocation = GLES30.glGetUniformLocation(defaultProgram, "tex")
vertexLocation = GLES30.glGetAttribLocation(defaultProgram, "position")
uvLocation = GLES30.glGetAttribLocation(defaultProgram, "uv")
Log.d("Native", "Attrib locations $vertexLocation, $uvLocation")
checkGlError("Getting uniform")
}
_surface = surface
}
return _surface!!
}
fun makeCurrent(eglSurface: EGLSurface, width: Int, height: Int) {
doOnGlThread {
GLES30.glViewport(0, 0, width, height)
if (!EGL14.eglMakeCurrent(display, eglSurface, eglSurface, context)) {
checkGlError("Failed to make surface current")
}
}
}
fun makeTexture(width: Int, height: Int): Int {
var _texture: Int? = null
doOnGlThread {
val intArr = IntArray(1)
GLES30.glGenTextures(1, intArr, 0)
checkGlError("Generate texture")
Log.d("Native", "${EGL14.eglGetCurrentContext()} ?= ${EGL14.EGL_NO_CONTEXT}")
val texture = intArr[0]
Log.d("Native", "Texture = $texture")
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texture)
checkGlError("Bind texture")
val buffer = ByteBuffer.allocateDirect(width * height * 4)
GLES30.glTexImage2D(
GLES30.GL_TEXTURE_2D,
0,
GLES30.GL_RGBA,
width,
height,
0,
GLES30.GL_RGBA,
GLES30.GL_UNSIGNED_BYTE,
buffer
)
checkGlError("Create texture buffer")
_texture = texture
}
return _texture!!
}
private fun compileShader(source: String, shaderType: Int): Int {
val currentContext = EGL14.eglGetCurrentContext()
val noContext = EGL14.EGL_NO_CONTEXT
val shaderId = GLES30.glCreateShader(shaderType)
Log.d("Native", "Created $shaderId\nContext $currentContext vs $noContext")
checkGlError("Create shader")
if (shaderId == 0) {
Log.e("Native", "Could not create shader for some reason")
checkGlError("Could not create shader")
}
GLES30.glShaderSource(shaderId, source)
checkGlError("Setting shader source")
GLES30.glCompileShader(shaderId)
val statusBuffer = IntArray(1)
GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, statusBuffer, 0)
val shaderLog = GLES30.glGetShaderInfoLog(shaderId)
Log.d("Native", "Compiling shader #$shaderId : $shaderLog")
if (statusBuffer[0] == 0) {
GLES30.glDeleteShader(shaderId)
checkGlError("Failed to compile shader $shaderId")
}
return shaderId
}
private fun makeProgram(sources: Map<Int, String>): Int {
val currentContext = EGL14.eglGetCurrentContext()
val noContext = EGL14.EGL_NO_CONTEXT
val program = GLES30.glCreateProgram()
Log.d("Native", "Created $program\nContext $currentContext vs $noContext")
checkGlError("Create program")
sources.forEach {
val shader = compileShader(it.value, it.key)
GLES30.glAttachShader(program, shader)
}
val linkBuffer = IntArray(1)
GLES30.glLinkProgram(program)
GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkBuffer, 0)
if (linkBuffer[0] == 0) {
GLES30.glDeleteProgram(program)
checkGlError("Failed to link program $program")
}
return program
}
// Called to actually draw to the surface. When fully implemented it should draw whatever is
// on the associated texture, but for now, to debug, I just want to verify I can draw vertices,
// but it seems I cannot?
fun drawTextureToCurrentSurface(texture: Int, surface: EGLSurface) {
doOnGlThread {
// Verify I have a context
val currentContext = EGL14.eglGetCurrentContext()
val noContext = EGL14.EGL_NO_CONTEXT
Log.d("Native", "Drawing, Context = $currentContext vs $noContext")
checkGlError("Just checking first")
GLES30.glClearColor(1f, 0f, 1f, 1f)
GLES30.glClearDepthf(1f)
GLES30.glDisable(GLES30.GL_DEPTH_TEST)
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT or GLES30.GL_DEPTH_BUFFER_BIT)
checkGlError("Clearing")
GLES30.glUseProgram(defaultProgram)
checkGlError("Use program")
GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
checkGlError("Activate texture 0")
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texture)
checkGlError("Bind texture $texture")
GLES30.glUniform1i(uniformTextureLocation, 0)
checkGlError("Set uniform")
GLES30.glEnableVertexAttribArray(vertexLocation)
vertexBuffer.position(0)
GLES30.glVertexAttribPointer(vertexLocation, 2, GLES30.GL_FLOAT, false, 0, vertexBuffer)
Log.d("Native", "Bound vertices (shader=$defaultProgram)")
checkGlError("Attribute 0")
GLES30.glEnableVertexAttribArray(uvLocation)
uvBuffer.position(0)
GLES30.glVertexAttribPointer(uvLocation, 2, GLES30.GL_FLOAT, false, 0, uvBuffer)
checkGlError("Attribute 1")
//indexBuffer.position(0)
//GLES30.glDrawElements(GLES30.GL_TRIANGLES, 4, GLES30.GL_UNSIGNED_INT, indexBuffer)
// I would expect to get a triangle of different color than the background
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 3)
GLES30.glFinish()
checkGlError("Finished GL")
EGL14.eglSwapBuffers(display, surface)
checkGlError("Swapped buffers")
}
}
...currently unused other methods
}
The general flow of the above code is that the init block executes when initializing the context, of which there is only one. setup is called when the plugin is registered, and buildSurfaceTextureWindow is called when initializing a SurfaceTexture for a Flutter Texture. The first time this is called, it compiles the shaders. When the plugin wants to render the texture, it calls makeCurrent then drawTextureToCurrentSurface, which is where the magenta background becomes visible but without any cyan triangle. Calls to GL functions are done in a separate thread using doOnGlThread.
If you need to see all of the code including the full plugin implementation and example app using it, I have it on Github, but as far as I can tell the above code should be the only relevant region to not seeing any geometry rendered in the effectively hardcoded color from my fragment shader.
tl;dr My background color from glClear shows up on screen, but my expected result of calling glDrawArrays, a cyan triangle, does not, and I am trying to understand why.
Apparently I needed to call .order(ByteOrder.nativeOrder()) on my buffers. Without this, the vertex array data is not set up properly. Also I needed to set glTexParameteri(GL_TEXTURE_2D, ...) for GL_TEXTURE_MAG/MIN_FILTER and GL_TEXTURE_WRAP_S/T. Without that, all textures are all-black

Drawing a sky grid (sphere) using OPENGLES in android

I am trying to create a sphere in OPENGL in Android.
For now, I am just drawing equator, but when I add multiple vertices (stp=10), app crashes with this error :
A/libc: Fatal signal 11 (SIGSEGV), code 2, fault addr 0x751b5360 in tid 2588 (GLThread 3395)
But when I lower the number of vertices (stp=20), it displays the equator:
This is my code :
import COORDS_PER_VERTEX
import android.opengl.GLES20
import android.opengl.GLES20.GL_FLOAT
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import kotlin.math.*
class SkyGrid {
private var vertexCount: Int = 0
private var vertexBuffer: FloatBuffer
val stp=20
private val 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 vPosition;" +
"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 * vPosition;" +
"}"
// Use to access and set the view transformation
private var vPMatrixHandle: Int = 0
private val fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}"
fun loadShader(type: Int, shaderCode: String): Int {
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
return GLES20.glCreateShader(type).also { shader ->
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
}
}
private var mProgram: Int
init {
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram().also {
// add the vertex shader to program
GLES20.glAttachShader(it, vertexShader)
// add the fragment shader to program
GLES20.glAttachShader(it, fragmentShader)
// creates OpenGL ES program executables
GLES20.glLinkProgram(it)
}
var triangleCoords: MutableList<Float> = mutableListOf();
for (a in 0..360 step stp)
{
val r=0.5f
val alpha=Math.PI* a/180.0
val x=r*cos(alpha).toFloat()
val y=r*sin(alpha).toFloat()
var p1=arrayOf(x,y,0.0f)
val alpha2=Math.PI* (a+stp)/180.0
val x2=r*cos(alpha2).toFloat()
val y2=r*sin(alpha2).toFloat()
var p2=arrayOf(x2,y2,0.0f)
triangleCoords.addAll(p1)
triangleCoords.addAll(p2)
}
vertexCount = triangleCoords.size
vertexBuffer =
// (number of coordinate values * 4 bytes per float)
ByteBuffer.allocateDirect(triangleCoords.size * 4).run {
// use the device hardware's native byte order
order(ByteOrder.nativeOrder())
// create a floating point buffer from the ByteBuffer
asFloatBuffer().apply {
// add the coordinates to the FloatBuffer
put(triangleCoords.toFloatArray())
// set the buffer to read the first coordinate
position(0)
}
}
}
// Set color with red, green, blue and alpha (opacity) values
val color = floatArrayOf(0.13671875f, 0.76953125f, 0.22265625f, 1.0f)
private var positionHandle: Int = 0
private var mColorHandle: Int = 0
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex
fun draw(mvpMatrix: FloatArray) {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram)
// get handle to shape's transformation matrix
vPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mvpMatrix, 0)
// get handle to vertex shader's vPosition member
positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(it)
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(
it,
COORDS_PER_VERTEX,
GLES20.GL_FLOAT,
false,
vertexStride,
vertexBuffer
)
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->
// Set color for drawing the triangle
GLES20.glUniform4fv(colorHandle, 1, color, 0)
}
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_LINES, 0, vertexCount)
// Disable vertex array
GLES20.glDisableVertexAttribArray(it)
}
}
}
The third augment of glDrawArrays should be number of vertices (not #vertex-components). Calculate the parameter appropriately.
//vertexCount = triangleCoords.size
vertexCount = triangleCoords.size / COORDS_PER_VERTEX
:
GLES20.glDrawArrays(GLES20.GL_LINES, 0, vertexCount)

OpenGL ES how to apply transformations from android.graphics.Matrix

I have a 3x3 android.graphics.Matrix, and i want to apply all transformation to a 4x4 OpenGL Matrix for 2D transformations only. So far I have manage to apply rotation and scaling I am using the example from the android team HERE to render a triangle. I used this class for generating the android.graphics.Matrix, from finger gestures made by the user for scale, move and translate transformations.
After that I attach the MatrixGestureDetector on the onTouchEvent from the View. In the MyGLSurfaceView class:
class MyGLSurfaceView : GLSurfaceView {
...
private val matrixGestureDetector = MatrixGestureDetector()
override fun onTouchEvent(e: MotionEvent): Boolean {
matrixGestureDetector.onTouchEvent(e)
requestRender()
return true
}
}
Then I used it to convert the android.graphics.Matrix to a OpenGL Matrix in the onDrawFrame method in
MyGLRenderer class
...
lateinit var matrixGestureDetector: MatrixGestureDetector
override fun onDrawFrame(unused: GL10) {
...
// get graphics matrix values
val m = FloatArray(9)
matrixGestureDetector.matrix.getValues(m)
// set rotation and scaling from graphics matrix to form new 4x4 OpenGL matrix
val openGLMatrix = floatArrayOf(
m[0], m[3], 0f, 0f,
m[1], m[4], 0f, 0f,
0f, 0f, 1f, 0f,
0f, 0f, 0f, 1f
)
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, openGLMatrix, 0)
// draw shape, where scaling and rotation work
mTriangle.draw(scratch)
}
To apply the translation I have to add the m[2] and m[5] from the android.graphics.Matrix values and change the openGLMatrix to:
val openGLMatrix = floatArrayOf(
m[0], m[3], 0f, 0f,
m[1], m[4], 0f, 0f,
0f, 0f, 1f, 0f,
m[2], m[5], 0f, 1f
)
Now the problem is that the OpenGL viewbox size is formed by coordinates in range [-1,1], look at the image below:
But the translation X and Y values from the android.graphics.Matrix are not in that range, to do that I changed it to:
val scaleX: Float = m[android.graphics.Matrix.MSCALE_X]
val skewY: Float = m[android.graphics.Matrix.MSKEW_Y]
val translateX = m[android.graphics.Matrix.MTRANS_X]
val translateY = m[android.graphics.Matrix.MTRANS_Y]
val ratio = width.toFloat() / height
val openGLMatrix = floatArrayOf(
m[0], m[3], 0f, 0f,
m[1], m[4], 0f, 0f,
0f, 0f, 1f, 0f,
-ratio * (translateX / width * 2), -(translateY / height * 2), 0f, 1f
)
Now translation work, but scale and rotation are not done on the pivot point(center point of rotation between the two fingers). How to apply all the transformation and is there a example code for 2D transformations for finger gestures that I can find anywhere?
Well, I figure out that there is miscalculations in the conversion from Graphic coordinate system to a OpenGL coordinate system for the translation. Here is the code for getting the accurate translation in a OpenGL coordinate system, set as separate functions:
fun normalizeTranslateX(x: Float): Float {
val translateX = if (x < width / 2f) {
-1f + (x / (width / 2f))
} else {
(x - (width / 2f)) / (width / 2f)
}
return -translateX * OpenGLRenderer.NEAR * ratio
}
fun normalizeTranslateY(y: Float): Float {
val translateY = if (y < height / 2f) {
1f - (y / (height / 2f))
} else {
-(y - (height / 2f)) / (height / 2f)
}
return translateY * OpenGLRenderer.NEAR
}
I have also updated the whole finger gesture transformation class, for generating the OpenGL matrix, with the applied transformations from the finger gestures here is the class OpenGLFingerGestureTransformations.
To get the OpenGL matrix first create your own OpenGLMatrixGestureDetector object, using the same way as creating MatrixGestureDetector:
class MyGLSurfaceView : GLSurfaceView {
...
private val matrixGestureDetector = OpenGLMatrixGestureDetector()
override fun onTouchEvent(e: MotionEvent): Boolean {
matrixGestureDetector.onTouchEvent(e)
requestRender()
return true
}
}
Then in the MyGLRenderer class just generate the matrix with the method transform()
...
lateinit var matrixGestureDetector: OpenGLMatrixGestureDetector
private val transformedMatrixOpenGL: FloatArray = FloatArray(16)
override fun onDrawFrame(unused: GL10) {
...
// get OpenGL matrix with the applied transformations, from finger gestures
matrixGestureDetector.transform(mMVPMatrix, transformedMatrixOpenGL)
// draw shapes with apply transformations from finger gestures
mTriangle.draw(transformedMatrixOpenGL)
}
I have uploaded the full source code HERE.
Here is the final result:

OpenGL ES 2.0 not drawing in Android Lollipop (5.1)

I'm new in openGL ES programming, so I followed the guide provided by Android Developers site. It shows how draw a simple green triangle that rotate on the screen following the touch point. I tried it on my Tablet (Galaxy Tab A10, Android Oreo - 8) and it worked, also on a Huawei (Android Marhmallow - 6.0); the problem is that the triangle is not shown on my Galaxy J3 (Android Lollipop - 5.1) without log errors and 0 as return of all glGetError() call, the only thing I can see is the color change of the background.
I couldn't find similar problems here in SO and on the web, has someone had the same problem?
(The language used is Kotlin, but I think it's a conceptual question, so please take a look also if the code is slightly different from Java).
Game Activity:
import android.content.Context
import android.opengl.GLSurfaceView
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.MotionEvent
class GameActivity : AppCompatActivity() {
private lateinit var mGLView: GLSurfaceView
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
mGLView = MyGLSurfaceView(this)
setContentView(mGLView)
}
class MyGLSurfaceView(context: Context) : GLSurfaceView(context) {
private val mRenderer: MyGLRenderer
init {
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2)
mRenderer = MyGLRenderer()
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(mRenderer)
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
}
private val TOUCH_SCALE_FACTOR: Float = 180.0f / 320f
private var previousX: Float = 0f
private var previousY: Float = 0f
override fun onTouchEvent(e: MotionEvent): Boolean {
// 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.
val x: Float = e.x
val y: Float = e.y
when (e.action) {
MotionEvent.ACTION_MOVE -> {
var dx: Float = x - previousX
var dy: Float = y - previousY
// reverse direction of rotation above the mid-line
if (y > height / 2) {
dx *= -1
}
// reverse direction of rotation to left of the mid-line
if (x < width / 2) {
dy *= -1
}
mRenderer.angle += (dx + dy) * TOUCH_SCALE_FACTOR
requestRender()
}
}
previousX = x
previousY = y
return true
}
}
}
Custom Renderer Class:
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
class MyGLRenderer : GLSurfaceView.Renderer {
val TAG = MyGLRenderer::class.java.name
#Volatile
var angle: Float = 0f
private lateinit var mTriangle: Triangle
private val mRotationMatrix = FloatArray(16)
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private val mMVPMatrix = FloatArray(16)
private val mProjectionMatrix = FloatArray(16)
private val mViewMatrix = FloatArray(16)
override fun onSurfaceCreated(unused: GL10, config: EGLConfig) {
// Set the background frame color
GLES20.glClearColor(0.8f, 0.2f, 0.2f, 1.0f)
// initialize a triangle
mTriangle = Triangle()
}
override fun onDrawFrame(unused: GL10) {
val scratch = FloatArray(16)
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// enable face culling feature
//GLES20.glEnable(GL10.GL_CULL_FACE)
// specify which faces to not draw
//GLES20.glCullFace(GL10.GL_BACK)
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0f, 0f, -3f, 0f, 0f, 0f, 0f, 1.0f, 0.0f)
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0)
// Create a rotation transformation for the triangle
/*val time = SystemClock.uptimeMillis() % 4000L
val angle = 0.090f * time.toInt()*/
Matrix.setRotateM(mRotationMatrix, 0, -angle, 0f, 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 shape
mTriangle.draw(scratch)
}
override fun onSurfaceChanged(unused: GL10, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
val ratio: Float = width.toFloat() / height.toFloat()
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1f, 1f, 3f, 7f)
}
}
Triangle Class:
import android.opengl.GLES20
import android.util.Log
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
class Triangle {
val TAG = Triangle::class.java.name
// number of coordinates per vertex in this array
val COORDS_PER_VERTEX = 3
var triangleCoords = floatArrayOf( // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
)
private val fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}"
private val 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 vPosition;" +
"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 * vPosition;" +
"}"
// Use to access and set the view transformation
private var mMVPMatrixHandle: Int = 0
// Set color with red, green, blue and alpha (opacity) values
val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)
private var vertexBuffer: FloatBuffer =
// (number of coordinate values * 4 bytes per float)
ByteBuffer.allocateDirect(triangleCoords.size * 4).run {
// use the device hardware's native byte order
order(ByteOrder.nativeOrder())
// create a floating point buffer from the ByteBuffer
asFloatBuffer().apply {
// add the coordinates to the FloatBuffer
put(triangleCoords)
// set the buffer to read the first coordinate
position(0)
}
}
private var mProgram: Int
init {
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram().also {
// add the vertex shader to program
GLES20.glAttachShader(it, vertexShader)
Log.d(TAG, "glAttachShader: ${GLES20.glGetError()}")
// add the fragment shader to program
GLES20.glAttachShader(it, fragmentShader)
Log.d(TAG, "glAttachShader: ${GLES20.glGetError()}")
// creates OpenGL ES program executables
GLES20.glLinkProgram(it)
Log.d(TAG, "glLinkProgram: ${GLES20.glGetError()}")
}
Log.d(TAG, "glCreateProgram: ${GLES20.glGetError()}")
}
fun loadShader(type: Int, shaderCode: String): Int {
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
return GLES20.glCreateShader(type).also { shader ->
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
}
}
private var mPositionHandle: Int = 0
private var mColorHandle: Int = 0
private val vertexCount: Int = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride: Int = COORDS_PER_VERTEX * 4 // 4 bytes per vertex
fun draw(mvpMatrix: FloatArray) { // pass in the calculated transformation matrix
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram)
Log.d(TAG, "glUseProgram: ${GLES20.glGetError()}")
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition").also {
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(it)
Log.d(TAG, "glEnableVertexAttribArray: ${GLES20.glGetError()}")
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(
it,
COORDS_PER_VERTEX,
GLES20.GL_FLOAT,
false,
vertexStride,
vertexBuffer
)
Log.d(TAG, "glVertexAttribPointer: ${GLES20.glGetError()}")
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor").also { colorHandle ->
// Set color for drawing the triangle
GLES20.glUniform4fv(colorHandle, 1, color, 0)
}
Log.d(TAG, "glGetUniformLocation: ${GLES20.glGetError()}")
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")
Log.d(TAG, "glGetUniformLocation: ${GLES20.glGetError()}")
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0)
Log.d(TAG, "glUniformMatrix4fv: ${GLES20.glGetError()}")
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
Log.d(TAG, "glDrawArrays: ${GLES20.glGetError()}")
// Disable vertex array
GLES20.glDisableVertexAttribArray(it)
Log.d(TAG, "glDisableVertexAttribArray: ${GLES20.glGetError()}")
}
Log.d(TAG, "glGetAttribLocation: ${GLES20.glGetError()}")
}
}
Of course I added this line in the manifest.xml:
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
UPDATE:
I discovered that if I try to run the app while the phone (the Lollipop) is disconnected from PC, it'll fail to run, with the toast message: "Authorization denied" (the Italian message is "Autorizzazione negata", so in English it could be also "Permission denied"), and I don't know what it means, but maybe it could be helpful for someone.

Categories

Resources