From 3bfc62f675cf87bb097711785f40389a80996577 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 25 Jan 2020 10:45:10 -0300 Subject: [PATCH] Various improvements (#14) * Uniform method names for GlRect and GlRoundRect * Split GlTextureProgram and GlSimpleTextureProgram * Refactor texture programs * Create GlTexture class * Make texture variable * Refactoring and new features * Fix GlTexture constructors * Create GlDrawable.vertexArrayVersion for optimizations * Create Gl2dMesh * Improve texture program * Create geometry package * Small changes --- .../opengl/demo/VideoActivity.kt | 62 +++-- docs/_docs/programs.md | 40 ++- docs/_docs/scenes.md | 2 +- docs/_docs/textures.md | 44 ++++ library/build.gradle | 51 ++-- .../opengl/core/EglContextFactory.kt | 1 - .../com/otaliastudios/opengl/core/Egloo.kt | 2 +- .../otaliastudios/opengl/core/GlBindable.kt | 12 + .../{viewport => core}/GlViewportAware.kt | 2 +- .../com/otaliastudios/opengl/draw/Gl2dMesh.kt | 123 +++++++++ .../otaliastudios/opengl/draw/GlDrawable.kt | 13 +- .../otaliastudios/opengl/draw/GlPolygon.kt | 18 +- .../com/otaliastudios/opengl/draw/GlRect.kt | 33 ++- .../otaliastudios/opengl/draw/GlRoundRect.kt | 1 + .../opengl/extensions/Buffers.kt | 27 +- .../opengl/geometry/IndexedPointF.kt | 5 + .../opengl/geometry/IndexedSegmentF.kt | 19 ++ .../otaliastudios/opengl/geometry/SegmentF.kt | 77 ++++++ .../otaliastudios/opengl/program/GlProgram.kt | 88 +++---- .../{GlHandle.kt => GlProgramLocation.kt} | 6 +- .../opengl/program/GlTextureProgram.kt | 237 +++++++++++------- .../com/otaliastudios/opengl/scene/GlScene.kt | 3 +- .../opengl/texture/GlFramebuffer.kt | 43 ++++ .../otaliastudios/opengl/texture/GlTexture.kt | 64 +++++ 24 files changed, 757 insertions(+), 216 deletions(-) create mode 100644 docs/_docs/textures.md create mode 100644 library/src/main/java/com/otaliastudios/opengl/core/GlBindable.kt rename library/src/main/java/com/otaliastudios/opengl/{viewport => core}/GlViewportAware.kt (94%) create mode 100644 library/src/main/java/com/otaliastudios/opengl/draw/Gl2dMesh.kt create mode 100644 library/src/main/java/com/otaliastudios/opengl/geometry/IndexedPointF.kt create mode 100644 library/src/main/java/com/otaliastudios/opengl/geometry/IndexedSegmentF.kt create mode 100644 library/src/main/java/com/otaliastudios/opengl/geometry/SegmentF.kt rename library/src/main/java/com/otaliastudios/opengl/program/{GlHandle.kt => GlProgramLocation.kt} (72%) create mode 100644 library/src/main/java/com/otaliastudios/opengl/texture/GlFramebuffer.kt create mode 100644 library/src/main/java/com/otaliastudios/opengl/texture/GlTexture.kt diff --git a/demo/src/main/java/com/otaliastudios/opengl/demo/VideoActivity.kt b/demo/src/main/java/com/otaliastudios/opengl/demo/VideoActivity.kt index 8137e8d..50e2fde 100644 --- a/demo/src/main/java/com/otaliastudios/opengl/demo/VideoActivity.kt +++ b/demo/src/main/java/com/otaliastudios/opengl/demo/VideoActivity.kt @@ -1,39 +1,36 @@ package com.otaliastudios.opengl.demo -import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Intent -import android.graphics.Color import android.graphics.PixelFormat +import android.graphics.PointF import android.graphics.RectF import android.graphics.SurfaceTexture import android.net.Uri +import android.opengl.GLES11Ext import android.opengl.GLES20 import android.opengl.GLSurfaceView import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import android.os.Handler import android.view.* import android.widget.Toast -import androidx.interpolator.view.animation.FastOutSlowInInterpolator import com.google.android.exoplayer2.ExoPlayerFactory import com.google.android.exoplayer2.SimpleExoPlayer -import com.google.android.exoplayer2.source.ExtractorMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.video.VideoListener import com.otaliastudios.opengl.core.EglConfigChooser import com.otaliastudios.opengl.core.EglContextFactory -import com.otaliastudios.opengl.core.EglCore import com.otaliastudios.opengl.draw.* -import com.otaliastudios.opengl.program.GlFlatProgram +import com.otaliastudios.opengl.extensions.makeIdentity +import com.otaliastudios.opengl.extensions.scaleX +import com.otaliastudios.opengl.extensions.scaleY import com.otaliastudios.opengl.program.GlTextureProgram import com.otaliastudios.opengl.scene.GlScene -import com.otaliastudios.opengl.surface.EglWindowSurface +import com.otaliastudios.opengl.texture.GlTexture import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.opengles.GL10 -import kotlin.math.roundToInt class VideoActivity : AppCompatActivity(), GLSurfaceView.Renderer { @@ -45,7 +42,26 @@ class VideoActivity : AppCompatActivity(), GLSurfaceView.Renderer { private val scene = GlScene() private val glRect = GlRect() private val glRoundRect = GlRoundRect().apply { setCornersPx(200) } - private var glDrawable: GlDrawable = glRect + private val glMesh = Gl2dMesh().also { + it.setPoints(listOf( + PointF(0F, 0F), + PointF(0F, 1F), + PointF(1F, 0F), + PointF(0F, -1F), + PointF(-1F, 0F), + PointF(0.9F, 0.7F), + PointF(0.7F, 0.9F), + PointF(-0.9F, -0.7F), + PointF(-0.7F, -0.9F), + PointF(0.9F, -0.7F), + PointF(0.7F, -0.9F), + PointF(-0.9F, 0.7F), + PointF(-0.7F, 0.9F) + )) + } + + private val glDrawables = listOf(glRect, glRoundRect, glMesh) + private var glDrawable = 0 private lateinit var player: SimpleExoPlayer private var videoWidth = -1 @@ -96,7 +112,10 @@ class VideoActivity : AppCompatActivity(), GLSurfaceView.Renderer { // On touch, change the current drawable. glSurfaceView.setOnTouchListener { v, event -> - glDrawable = if (glDrawable == glRect) glRoundRect else glRect + glDrawable++ + if (glDrawable > glDrawables.lastIndex) { + glDrawable = 0 + } false } Toast.makeText(this, "Touch the screen to change shape.", Toast.LENGTH_LONG).show() @@ -104,10 +123,12 @@ class VideoActivity : AppCompatActivity(), GLSurfaceView.Renderer { override fun onSurfaceCreated(gl: GL10, config: EGLConfig) { // Configure GL + val glTexture = GlTexture() glTextureProgram = GlTextureProgram() + glTextureProgram!!.texture = glTexture // Configure the player - surfaceTexture = SurfaceTexture(glTextureProgram!!.textureId) + surfaceTexture = SurfaceTexture(glTexture.id) surfaceTexture!!.setOnFrameAvailableListener { glSurfaceView.requestRender() } @@ -131,7 +152,7 @@ class VideoActivity : AppCompatActivity(), GLSurfaceView.Renderer { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) texture.updateTexImage() texture.getTransformMatrix(program.textureTransform) - scene.draw(program, glDrawable) + scene.draw(program, glDrawables[glDrawable]) } private fun onSurfaceDestroyed() { @@ -149,24 +170,27 @@ class VideoActivity : AppCompatActivity(), GLSurfaceView.Renderer { if (surfaceWidth == -1 || surfaceHeight == -1) return val videoRatio = videoWidth.toFloat() / videoHeight val surfaceRatio = surfaceWidth.toFloat() / surfaceHeight - val rect: RectF + val viewport: RectF if (videoRatio > surfaceRatio) { // Video is wider. We should collapse height. val surfaceRealHeight = surfaceWidth / videoRatio val surfaceRealHeightEgloo = surfaceRealHeight / surfaceHeight * 2 - rect = RectF(-1F, surfaceRealHeightEgloo / 2F, + viewport = RectF(-1F, surfaceRealHeightEgloo / 2F, 1F, -surfaceRealHeightEgloo / 2F) } else if (videoRatio < surfaceRatio) { // Video is taller. We should collapse width val surfaceRealWidth = surfaceHeight * videoRatio val surfaceRealWidthEgloo = surfaceRealWidth / surfaceWidth * 2 - rect = RectF(-surfaceRealWidthEgloo / 2F, 1F, + viewport = RectF(-surfaceRealWidthEgloo / 2F, 1F, surfaceRealWidthEgloo / 2F, -1F) } else { - rect = RectF(-1F, 1F, 1F, -1F) + viewport = RectF(-1F, 1F, 1F, -1F) } - glRect.setVertexArray(rect) - glRoundRect.setRect(rect) + glRect.setRect(viewport) + glRoundRect.setRect(viewport) + glMesh.modelMatrix.makeIdentity() + glMesh.modelMatrix.scaleX(viewport.width() / 2F) + glMesh.modelMatrix.scaleY(-viewport.height() / 2F) } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/docs/_docs/programs.md b/docs/_docs/programs.md index e46de4f..2461e4f 100644 --- a/docs/_docs/programs.md +++ b/docs/_docs/programs.md @@ -44,23 +44,51 @@ program.draw(drawable2) ### Texture program -The `GlTextureProgram` program can be used to render texture frames. It will automatically generate -a `textureId` for you, which is meant to be used to create `SurfaceTexture`s. This means that it -uses the `GLES11Ext.GL_TEXTURE_EXTERNAL_OES` texture target. +The `GlTextureProgram` program can be used to render textures. To use it, you will need to create +a `GlTexture` first and call `program.texture = texture`: this will make sure that texture is +correctly bound before rendering. See [textures](textures) to learn about this object. + +The texture program has built-in support for: +- Adapting the texture to the `GlDrawable` it is being drawn into. This means that the drawable and the texture should have the same aspect ratio to avoid distortion. +- Apply a matrix transformation to the texture by modifying `GlTextureProgram.textureTransform` See the sample below: ```kotlin +val texture = GlTexture() val program = GlTextureProgram() -val programTexture = SurfaceTexture(program.textureId) +program.texture = texture +val surfaceTexture = SurfaceTexture(texture.id) // Pass this surfaceTexture to Camera, for example val camera: android.hardware.Camera = openCamera() -camera.setPreviewTexture(programTexture) +camera.setPreviewTexture(surfaceTexture) // Now the program texture receives the camera frames // And we can render them using the program val rect = GlRect() // Draw the full frame -programTexture.getTransformMatrix(program.textureTransform) +surfaceTexture.getTransformMatrix(program.textureTransform) program.draw(rect) ``` + +If, for some reason, you do not want to call `program.texture = texture` (which gives the program +the ownership of the texture), you can still call `texture.bind()` and `texture.unbind()` manually: + +```kotlin +// Option 1 +texture.bind() +program.draw(drawable) +texture.unbind() + +// Option 2 +texture.use { + program.draw(drawable) +} + +// Option 3 +program.texture = texture +program.draw(drawable) +``` + +These options are equivalent. Note, however, that when passing the texture to `GlTextureProgram`, +the texture will automatically be released when you call `program.release()`. \ No newline at end of file diff --git a/docs/_docs/scenes.md b/docs/_docs/scenes.md index 1345491..5fc9a76 100644 --- a/docs/_docs/scenes.md +++ b/docs/_docs/scenes.md @@ -2,7 +2,7 @@ layout: page title: "Scenes" description: "How to control the view and projection matrix" -order: 4 +order: 5 disqus: 1 --- diff --git a/docs/_docs/textures.md b/docs/_docs/textures.md new file mode 100644 index 0000000..a7fa5d0 --- /dev/null +++ b/docs/_docs/textures.md @@ -0,0 +1,44 @@ +--- +layout: page +title: "Textures" +description: "APIs for textures and framebuffer objects" +order: 4 +disqus: 1 +--- + +### The GlTexture object + +The `GlTexture` object will generate and allocate an OpenGL texture. The texture can then be used to +read from it, render into it, attach to a framebuffer object and much more. + +By default, the `GlTexture` is created with the `GLES11Ext.GL_TEXTURE_EXTERNAL_OES` texture target. +This means that it is suitable for using it as the output of a `SurfaceTexture`: + +```kotlin +val texture = GlTexture() +val surfaceTexture = SurfaceTexture(texture.id) +// Anytime the surface texture is passed new data, its contents are put into our GlTexture +// For example, we can receive the stream of video frames: +videoPlayer.setOutputSurface(surfaceTexture) +videoPlayer.play() +``` + +However, different targets can be specified within the texture constructor. When using `GLES20.GL_TEXTURE_2D`, +you will probably want to use the constructor that accepts a `width` and `height` so that the buffer +is actually allocated. + +> To render texture contents, just use a `GlTextureProgram` and pass the texture to it. +See the [programs](programs#texture-program) page for details. + +### The GlFramebuffer object + +The `GlFramebuffer` object will generate an OpenGL framebuffer object. +You can attach textures to it by using `GlFramebuffer.attach()`, like so: + +```kotlin +val texture = GlTexture() +val fbo = GlFramebuffer() +fbo.attach(texture, GLES20.GL_COLOR_ATTACHMENT0) +``` + +The attached textures will now receive the framebuffer contents. \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 6266757..bf253d7 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -40,12 +40,12 @@ dependencies { } task sourcesJar(type: Jar) { - classifier = 'sources' + archiveClassifier.set('sources') from android.sourceSets.main.java.srcDirs } task dokkaJar(type: Jar, dependsOn: dokka) { - classifier = 'javadoc' + archiveClassifier.set('javadoc') from dokka.outputDirectory } @@ -85,41 +85,34 @@ install.repositories.mavenInstaller.pom.project { def bintrayUser def bintrayKey -def hasBintray = false if (System.getenv('TRAVIS') == 'true') { - if (System.getenv('TRAVIS_SECURE_ENV_VARS') == 'true') { - bintrayUser = System.getenv("BINTRAY_USER") - bintrayKey = System.getenv("BINTRAY_KEY") - hasBintray = true - } + bintrayUser = System.getenv("BINTRAY_USER") + bintrayKey = System.getenv("BINTRAY_KEY") } else { Properties props = new Properties() props.load(project.rootProject.file('local.properties').newDataInputStream()) bintrayUser = props.getProperty('bintray.user') bintrayKey = props.get('bintray.key') - hasBintray = true } -if (hasBintray) { - bintray { - // https://github.com/bintray/gradle-bintray-plugin - user = bintrayUser - key = bintrayKey - configurations = ['archives'] - publish = true - override = true - pkg { - repo = 'android' - name = libName - licenses = [libLicenseName] - vcsUrl = githubGit - desc = libDescription - version { - name = libVersion - desc = libName + ' v' + libVersion - released = new Date() - vcsTag = 'v' + libVersion - } +bintray { + // https://github.com/bintray/gradle-bintray-plugin + user = bintrayUser + key = bintrayKey + configurations = ['archives'] + publish = true + override = true + pkg { + repo = 'android' + name = libName + licenses = [libLicenseName] + vcsUrl = githubGit + desc = libDescription + version { + name = libVersion + desc = libName + ' v' + libVersion + released = new Date() + vcsTag = 'v' + libVersion } } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/core/EglContextFactory.kt b/library/src/main/java/com/otaliastudios/opengl/core/EglContextFactory.kt index 77faf08..a4e484f 100644 --- a/library/src/main/java/com/otaliastudios/opengl/core/EglContextFactory.kt +++ b/library/src/main/java/com/otaliastudios/opengl/core/EglContextFactory.kt @@ -23,7 +23,6 @@ object EglContextFactory { @JvmField val GLES3: GLSurfaceView.EGLContextFactory = Factory(3) - private class Factory(private val version: Int) : GLSurfaceView.EGLContextFactory { override fun createContext(egl: EGL10, display: EGLDisplay, eglConfig: EGLConfig): EGLContext { val attributes = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, version, EGL14.EGL_NONE) diff --git a/library/src/main/java/com/otaliastudios/opengl/core/Egloo.kt b/library/src/main/java/com/otaliastudios/opengl/core/Egloo.kt index fa5e62b..4f8595b 100644 --- a/library/src/main/java/com/otaliastudios/opengl/core/Egloo.kt +++ b/library/src/main/java/com/otaliastudios/opengl/core/Egloo.kt @@ -17,7 +17,7 @@ object Egloo { /** * Identify matrix for general use. */ - @JvmStatic + @JvmField val IDENTITY_MATRIX = FloatArray(16).apply { makeIdentity() } diff --git a/library/src/main/java/com/otaliastudios/opengl/core/GlBindable.kt b/library/src/main/java/com/otaliastudios/opengl/core/GlBindable.kt new file mode 100644 index 0000000..4e81881 --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/core/GlBindable.kt @@ -0,0 +1,12 @@ +package com.otaliastudios.opengl.core + +interface GlBindable { + fun bind() + fun unbind() +} + +fun GlBindable.use(block: () -> Unit) { + bind() + block() + unbind() +} \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/viewport/GlViewportAware.kt b/library/src/main/java/com/otaliastudios/opengl/core/GlViewportAware.kt similarity index 94% rename from library/src/main/java/com/otaliastudios/opengl/viewport/GlViewportAware.kt rename to library/src/main/java/com/otaliastudios/opengl/core/GlViewportAware.kt index ca5325b..7d67477 100644 --- a/library/src/main/java/com/otaliastudios/opengl/viewport/GlViewportAware.kt +++ b/library/src/main/java/com/otaliastudios/opengl/core/GlViewportAware.kt @@ -1,4 +1,4 @@ -package com.otaliastudios.opengl.viewport +package com.otaliastudios.opengl.core import android.opengl.GLES20 diff --git a/library/src/main/java/com/otaliastudios/opengl/draw/Gl2dMesh.kt b/library/src/main/java/com/otaliastudios/opengl/draw/Gl2dMesh.kt new file mode 100644 index 0000000..2f74f0a --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/draw/Gl2dMesh.kt @@ -0,0 +1,123 @@ +package com.otaliastudios.opengl.draw + +import android.graphics.PointF +import android.opengl.GLES20 +import com.otaliastudios.opengl.core.Egloo +import com.otaliastudios.opengl.extensions.floatBufferOf +import com.otaliastudios.opengl.extensions.toBuffer +import com.otaliastudios.opengl.geometry.IndexedSegmentF +import java.nio.ByteBuffer +import java.nio.FloatBuffer +import kotlin.IllegalArgumentException +import kotlin.math.* + + +open class Gl2dMesh: Gl2dDrawable() { + + override var vertexArray: FloatBuffer = floatBufferOf(6) + private var vertexIndices: ByteBuffer? = null + + fun setPoints(points: List) { + setPoints(points.map { it.x }, points.map { it.y }) + } + + fun setPoints(x: List, y: List) { + if (x.size != y.size) throw IllegalArgumentException("x.size != y.size") + val points = x.size + val coords = points * 2 + if (vertexArray.capacity() < coords) { + vertexArray = floatBufferOf(coords) + } else { + vertexArray.clear() + } + val segments = mutableListOf() + for (i in 0 until points) { + val xi = x[i] + val yi = y[i] + vertexArray.put(xi) + vertexArray.put(yi) + for (j in (i + 1) until points) { + val xj = x[j] + val yj = y[j] + segments.add(IndexedSegmentF(i, j, xi, yi, xj, yj)) + } + } + vertexArray.flip() + notifyVertexArrayChange() + + // Sort segments and iterate over them to select. + segments.sortBy { it.length } + val accepted = mutableListOf() + for (s in segments) { + if (accepted.none { it.intersects(s) }) { + accepted.add(s) + } + } + computeIndicesFromIndexedSegments(accepted) + } + + // Segments must be non intersecting and sorted by ascending length + private fun computeIndicesFromIndexedSegments(segments: List) { + // Now we have all segments. Each of this can participate in many triangles, + // but we actually want to take at most two of them, one on each side. + // NOTE: consider sorting to make the inner loop faster and to make sure + // that we take the smaller triangles instead of big ones + val indices = mutableListOf() + for (si in 0 until segments.size) { + val s1 = segments[si] + var hasPositiveTriangle = false + var hasNegativeTriangle = false + for (sj in (si + 1) until segments.size) { + if (hasPositiveTriangle && hasNegativeTriangle) break + val s2 = segments[sj] + var s2UnsharedIndex: Int + var s2UnsharedX: Float + var s2UnsharedY: Float + if (s1.hasIndex(s2.i)) { + // s2i is the shared value! s2j is the other. + s2UnsharedIndex = s2.j + s2UnsharedX = s2.jx + s2UnsharedY = s2.jy + } else if (s1.hasIndex(s2.j)) { + // s2j is the shared value! s2i is the other. + s2UnsharedIndex = s2.i + s2UnsharedX = s2.ix + s2UnsharedY = s2.iy + } else { + // Keep searching + continue + } + // Since we have two segments, and they are sorted by length, in theory we don't + // need to search for the last one, it MUST exist. We could assume it does. + // However this might create duplicate triangles when going on with the outer loop. + // I don't think we should search for the last one. It MUST exist. + // So we could simply create it. + val orientation = s1.orientation(s2UnsharedX, s2UnsharedY) + if (orientation == 0) continue + if (orientation > 0 && hasPositiveTriangle) continue + if (orientation < 0 && hasNegativeTriangle) continue + for (sk in (sj + 1) until segments.size) { + val s3 = segments[sk] + if (s3.hasIndex(s2UnsharedIndex) && (s3.hasIndex(s1.i) || s3.hasIndex(s1.j))) { + // Shares a point with s1, and shares the correct point with s2. + indices.add(s1.i.toByte()) + indices.add(s1.j.toByte()) + indices.add(s2UnsharedIndex.toByte()) + if (orientation > 0) hasPositiveTriangle = true + if (orientation < 0) hasNegativeTriangle = true + break + } + } + } + } + vertexIndices = indices.toByteArray().toBuffer() + } + + override fun draw() { + vertexIndices?.let { + Egloo.checkGlError("glDrawElements start") + GLES20.glDrawElements(GLES20.GL_TRIANGLES, it.limit(), GLES20.GL_UNSIGNED_BYTE, it) + Egloo.checkGlError("glDrawElements end") + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/draw/GlDrawable.kt b/library/src/main/java/com/otaliastudios/opengl/draw/GlDrawable.kt index e66a1ad..f208a52 100644 --- a/library/src/main/java/com/otaliastudios/opengl/draw/GlDrawable.kt +++ b/library/src/main/java/com/otaliastudios/opengl/draw/GlDrawable.kt @@ -3,7 +3,7 @@ package com.otaliastudios.opengl.draw import com.otaliastudios.opengl.core.Egloo import com.otaliastudios.opengl.program.GlProgram -import com.otaliastudios.opengl.viewport.GlViewportAware +import com.otaliastudios.opengl.core.GlViewportAware import java.nio.FloatBuffer abstract class GlDrawable : GlViewportAware() { @@ -16,12 +16,12 @@ abstract class GlDrawable : GlViewportAware() { /** * Returns the array of vertices. - * To avoid allocations, this returns internal state. The caller must not modify it. + * To avoid allocations, this returns internal state. The caller must not modify it. */ abstract var vertexArray: FloatBuffer /** - * Returns the number of position coordinates per vertex. This will be 2 or 3. + * Returns the number of position coordinates per vertex. This will be 2 or 3. */ abstract val coordsPerVertex: Int @@ -43,4 +43,11 @@ abstract class GlDrawable : GlViewportAware() { * Instead, this drawable should be passed to some [GlProgram]. */ abstract fun draw() + + var vertexArrayVersion: Int = 0 + private set + + protected fun notifyVertexArrayChange() { + vertexArrayVersion++ + } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/draw/GlPolygon.kt b/library/src/main/java/com/otaliastudios/opengl/draw/GlPolygon.kt index 91da234..4e7c07b 100644 --- a/library/src/main/java/com/otaliastudios/opengl/draw/GlPolygon.kt +++ b/library/src/main/java/com/otaliastudios/opengl/draw/GlPolygon.kt @@ -1,11 +1,11 @@ package com.otaliastudios.opengl.draw +import android.graphics.PointF import android.opengl.GLES20 import com.otaliastudios.opengl.core.Egloo import com.otaliastudios.opengl.extensions.floatBufferOf import com.otaliastudios.opengl.extensions.scale import com.otaliastudios.opengl.extensions.translate -import java.nio.FloatBuffer import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin @@ -43,14 +43,21 @@ open class GlPolygon(private val sides: Int): Gl2dDrawable() { set(value) { field = value updateArray() - onViewportSizeChanged() + onViewportSizeOrCenterChanged() } var centerY = 0F set(value) { field = value updateArray() - onViewportSizeChanged() + onViewportSizeOrCenterChanged() + } + + var center: PointF + get() = PointF(centerX, centerY) + set(value) { + centerX = value.x + centerY = value.y } override var vertexArray = floatBufferOf((sides + 2) * coordsPerVertex) @@ -74,10 +81,15 @@ open class GlPolygon(private val sides: Int): Gl2dDrawable() { array.put(array.get(2)) // Close the fan array.put(array.get(3)) // Close the fan array.flip() + notifyVertexArrayChange() } override fun onViewportSizeChanged() { super.onViewportSizeChanged() + onViewportSizeOrCenterChanged() + } + + private fun onViewportSizeOrCenterChanged() { // Undo the previous modifications. modelMatrix.scale(x = 1F / viewportScaleX, y = 1F / viewportScaleY) modelMatrix.translate(x = -viewportTranslationX, y = -viewportTranslationY) diff --git a/library/src/main/java/com/otaliastudios/opengl/draw/GlRect.kt b/library/src/main/java/com/otaliastudios/opengl/draw/GlRect.kt index 4a06601..8fe5643 100644 --- a/library/src/main/java/com/otaliastudios/opengl/draw/GlRect.kt +++ b/library/src/main/java/com/otaliastudios/opengl/draw/GlRect.kt @@ -23,6 +23,7 @@ open class GlRect: Gl2dDrawable() { override var vertexArray: FloatBuffer = floatBufferOf(*FULL_RECTANGLE_COORDS.clone()) @Suppress("unused") + @Deprecated("Use setRect", ReplaceWith("setRect(rect)")) open fun setVertexArray(array: FloatArray) { if (array.size != 4 * coordsPerVertex) { throw IllegalArgumentException("Vertex array should have 8 values.") @@ -30,27 +31,41 @@ open class GlRect: Gl2dDrawable() { vertexArray.clear() vertexArray.put(array) vertexArray.flip() + notifyVertexArrayChange() } + @Deprecated("Use setRect", ReplaceWith("setRect(rect)")) open fun setVertexArray(rect: RectF) { + setRect(rect) + } + + @Suppress("unused") + fun setRect(rect: RectF) { + setRect(rect.left, rect.top, rect.right, rect.bottom) + } + + @Suppress("MemberVisibilityCanBePrivate") + fun setRect(left: Float, top: Float, right: Float, bottom: Float) { vertexArray.clear() // 1 - vertexArray.put(rect.left) - vertexArray.put(rect.bottom) + vertexArray.put(left) + vertexArray.put(bottom) // 2 - vertexArray.put(rect.right) - vertexArray.put(rect.bottom) + vertexArray.put(right) + vertexArray.put(bottom) // 3 - vertexArray.put(rect.left) - vertexArray.put(rect.top) + vertexArray.put(left) + vertexArray.put(top) // 4 - vertexArray.put(rect.right) - vertexArray.put(rect.top) + vertexArray.put(right) + vertexArray.put(top) vertexArray.flip() + notifyVertexArrayChange() } override fun draw() { + Egloo.checkGlError("glDrawArrays start") GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertexCount) - Egloo.checkGlError("glDrawArrays") + Egloo.checkGlError("glDrawArrays end") } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/draw/GlRoundRect.kt b/library/src/main/java/com/otaliastudios/opengl/draw/GlRoundRect.kt index 0999e8d..b5b69d6 100644 --- a/library/src/main/java/com/otaliastudios/opengl/draw/GlRoundRect.kt +++ b/library/src/main/java/com/otaliastudios/opengl/draw/GlRoundRect.kt @@ -136,6 +136,7 @@ open class GlRoundRect : Gl2dDrawable() { array.put(array.get(2)) array.put(array.get(3)) array.flip() + notifyVertexArrayChange() } private fun addCornerArc(array: FloatBuffer, diff --git a/library/src/main/java/com/otaliastudios/opengl/extensions/Buffers.kt b/library/src/main/java/com/otaliastudios/opengl/extensions/Buffers.kt index c77c1e4..bd54b8f 100644 --- a/library/src/main/java/com/otaliastudios/opengl/extensions/Buffers.kt +++ b/library/src/main/java/com/otaliastudios/opengl/extensions/Buffers.kt @@ -1,13 +1,14 @@ package com.otaliastudios.opengl.extensions import android.opengl.Matrix +import com.otaliastudios.opengl.core.Egloo import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.FloatBuffer fun FloatArray.toBuffer(): FloatBuffer { val buffer = ByteBuffer - .allocateDirect(size * 4) + .allocateDirect(size * Egloo.SIZE_OF_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(this) @@ -21,9 +22,31 @@ fun floatBufferOf(vararg elements: Float): FloatBuffer { fun floatBufferOf(size: Int): FloatBuffer { return ByteBuffer - .allocateDirect(size * 4) + .allocateDirect(size * Egloo.SIZE_OF_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer().also { it.limit(it.capacity()) } +} + +fun ByteArray.toBuffer(): ByteBuffer { + val buffer = ByteBuffer + .allocateDirect(size) + .order(ByteOrder.nativeOrder()) + .put(this) + buffer.flip() + return buffer +} + +fun byteBufferOf(vararg elements: Byte): ByteBuffer { + return byteArrayOf(*elements).toBuffer() +} + +fun byteBufferOf(size: Int): ByteBuffer { + return ByteBuffer + .allocateDirect(size) + .order(ByteOrder.nativeOrder()) + .also { + it.limit(it.capacity()) + } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/geometry/IndexedPointF.kt b/library/src/main/java/com/otaliastudios/opengl/geometry/IndexedPointF.kt new file mode 100644 index 0000000..e3ab2df --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/geometry/IndexedPointF.kt @@ -0,0 +1,5 @@ +package com.otaliastudios.opengl.geometry + +import android.graphics.PointF + +class IndexedPointF(val index: Int, x: Float, y: Float) : PointF(x, y) \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/geometry/IndexedSegmentF.kt b/library/src/main/java/com/otaliastudios/opengl/geometry/IndexedSegmentF.kt new file mode 100644 index 0000000..08a4735 --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/geometry/IndexedSegmentF.kt @@ -0,0 +1,19 @@ +package com.otaliastudios.opengl.geometry + +class IndexedSegmentF(val i: Int, val j: Int, ix: Float, iy: Float, jx: Float, jy: Float) + : SegmentF(ix, iy, jx, jy) { + + constructor(i: IndexedPointF, j: IndexedPointF) : this(i.index, j.index, i.x, i.y, j.x, j.y) + + override fun intersects(other: SegmentF): Boolean { + if (other is IndexedSegmentF) { + if (other.hasIndex(i) && other.hasIndex(j)) return true + if (other.hasIndex(i) || other.hasIndex(j)) return false + } + return super.intersects(other) + } + + fun hasIndex(index: Int): Boolean { + return index == i || index == j + } +} \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/geometry/SegmentF.kt b/library/src/main/java/com/otaliastudios/opengl/geometry/SegmentF.kt new file mode 100644 index 0000000..ee64ca3 --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/geometry/SegmentF.kt @@ -0,0 +1,77 @@ +package com.otaliastudios.opengl.geometry + +import android.graphics.PointF +import com.otaliastudios.opengl.draw.Gl2dMesh +import kotlin.math.* + +open class SegmentF(val ix: Float, val iy: Float, val jx: Float, val jy: Float) { + + constructor(i: PointF, j: PointF) : this(i.x, i.y, j.x, j.y) + + val length: Float by lazy { + sqrt((ix - jx).pow(2) + (iy - jy).pow(2)) + } + + /** + * Not an easy task. Some references: + * https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java + * https://stackoverflow.com/questions/3838329/how-can-i-check-if-two-segments-intersect + */ + open fun intersects(other: SegmentF): Boolean { + // Check the envelope for a quick fail. + val thisMinX = min(ix, jx) + val thisMaxX = max(ix, jx) + val otherMinX = min(other.ix, other.jx) + val otherMaxX = max(other.ix, other.jx) + if (thisMinX > otherMaxX) return false + if (thisMaxX < otherMinX) return false + val thisMinY = min(iy, jy) + val thisMaxY = max(iy, jy) + val otherMinY = min(other.iy, other.jy) + val otherMaxY = max(other.iy, other.jy) + if (thisMinY > otherMaxY) return false + if (thisMaxY < otherMinY) return false + + // Compute orientations. + // Fail if relative position is the same (right - right). + val thisToOtherI = orientation(other.ix, other.iy) + val thisToOtherJ = orientation(other.jx, other.jy) + if (thisToOtherI > 0 && thisToOtherJ > 0) return false + if (thisToOtherI < 0 && thisToOtherJ < 0) return false + // Same for this with respect to the other + val otherToThisI = other.orientation(ix, iy) + val otherToThisJ = other.orientation(jx, jy) + if (otherToThisI > 0 && otherToThisJ > 0) return false + if (otherToThisI < 0 && otherToThisJ < 0) return false + + // Check for collinear segments + if (thisToOtherI == 0 && thisToOtherJ == 0 && otherToThisI == 0 && otherToThisJ == 0) { + // From first check we know that the two envelopes are intersecting or touching + // themselves. From the check above we know that segments are collinear. + // There are 4 adjacency cases. We want to return false because it's a shared point. + if (thisMinX == otherMaxX && thisMinY == otherMaxY) return false + if (thisMinX == otherMaxX && thisMaxY == otherMinY) return false + if (thisMaxX == otherMinX && thisMinY == otherMaxY) return false + if (thisMaxX == otherMinX && thisMaxY == otherMinY) return false + return true + } + + // Envelopes intersect and relative orientations are compatible: we have a single point + // intersection. However we want to return false if the point is a shared endpoint. + if (ix == other.ix && iy == other.iy) return false + if (jx == other.jx && jy == other.jy) return false + if (ix == other.jx && iy == other.jy) return false + if (jx == other.ix && jy == other.iy) return false + return true + } + + /** + * Returns -1 if the point is clockwise (right) from us. + * Returns +1 if the point is counterclockwise (left) from us. + * 0 if the point is collinear. + * https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithmsDD.java#L47-L53 + */ + fun orientation(x: Float, y: Float): Int { + return ((jx - ix) * (y - jy) - (jy - iy) * (x - jx)).sign.toInt() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/program/GlProgram.kt b/library/src/main/java/com/otaliastudios/opengl/program/GlProgram.kt index f997943..6c21fd8 100644 --- a/library/src/main/java/com/otaliastudios/opengl/program/GlProgram.kt +++ b/library/src/main/java/com/otaliastudios/opengl/program/GlProgram.kt @@ -19,46 +19,23 @@ import com.otaliastudios.opengl.draw.GlDrawable * * The vertex shader should then use the two to compute the gl_Position. */ -abstract class GlProgram( - @Suppress("MemberVisibilityCanBePrivate") val vertexShader: String, - @Suppress("MemberVisibilityCanBePrivate") val fragmentShader: String -) { - - protected var handle = createProgram() - private set - - // Creates a program with given vertex shader and pixel shader. - private fun createProgram(): Int { - val pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader) - if (pixelShader == 0) throw RuntimeException("Could not load fragment shader") - val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShader) - if (vertexShader == 0) throw RuntimeException("Could not load vertex shader") - - val program = GLES20.glCreateProgram() - Egloo.checkGlError("glCreateProgram") - if (program == 0) { - throw RuntimeException("Could not create program") - } - GLES20.glAttachShader(program, vertexShader) - Egloo.checkGlError("glAttachShader") - GLES20.glAttachShader(program, pixelShader) - Egloo.checkGlError("glAttachShader") - GLES20.glLinkProgram(program) - val linkStatus = IntArray(1) - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) - if (linkStatus[0] != GLES20.GL_TRUE) { - val message = "Could not link program: " + GLES20.glGetProgramInfoLog(program) - GLES20.glDeleteProgram(program) - throw RuntimeException(message) - } - return program - } +abstract class GlProgram protected constructor( + val handle: Int, + private val ownsHandle: Boolean) { + + constructor(vertexShader: String, fragmentShader: String) + : this(create(vertexShader, fragmentShader), true) + + constructor(handle: Int) + : this(handle, false) + + private var isReleased = false @Suppress("unused") open fun release() { - if (handle != -1) { + if (!isReleased && ownsHandle) { GLES20.glDeleteProgram(handle) - handle = -1 + isReleased = true } } @@ -77,24 +54,24 @@ abstract class GlProgram( Egloo.checkGlError("draw end") } - protected open fun onPreDraw(drawable: GlDrawable, modelViewProjectionMatrix: FloatArray) {} + open fun onPreDraw(drawable: GlDrawable, modelViewProjectionMatrix: FloatArray) {} - protected open fun onDraw(drawable: GlDrawable) { + open fun onDraw(drawable: GlDrawable) { drawable.draw() } - protected open fun onPostDraw(drawable: GlDrawable) {} + open fun onPostDraw(drawable: GlDrawable) {} - protected fun getAttribHandle(name: String) = GlHandle.getAttrib(handle, name) + protected fun getAttribHandle(name: String) = GlProgramLocation.getAttrib(handle, name) - protected fun getUniformHandle(name: String) = GlHandle.getUniform(handle, name) + protected fun getUniformHandle(name: String) = GlProgramLocation.getUniform(handle, name) companion object { @Suppress("unused") internal val TAG = GlProgram::class.java.simpleName // Compiles the given shader, returns a handle. - private fun loadShader(shaderType: Int, source: String): Int { + private fun createShader(shaderType: Int, source: String): Int { val shader = GLES20.glCreateShader(shaderType) Egloo.checkGlError("glCreateShader type=$shaderType") GLES20.glShaderSource(shader, source) @@ -108,5 +85,32 @@ abstract class GlProgram( } return shader } + + @JvmStatic + fun create(vertexShaderSource: String, fragmentShaderSource: String): Int { + val pixelShader = createShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource) + if (pixelShader == 0) throw RuntimeException("Could not load fragment shader") + val vertexShader = createShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource) + if (vertexShader == 0) throw RuntimeException("Could not load vertex shader") + + val program = GLES20.glCreateProgram() + Egloo.checkGlError("glCreateProgram") + if (program == 0) { + throw RuntimeException("Could not create program") + } + GLES20.glAttachShader(program, vertexShader) + Egloo.checkGlError("glAttachShader") + GLES20.glAttachShader(program, pixelShader) + Egloo.checkGlError("glAttachShader") + GLES20.glLinkProgram(program) + val linkStatus = IntArray(1) + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) + if (linkStatus[0] != GLES20.GL_TRUE) { + val message = "Could not link program: " + GLES20.glGetProgramInfoLog(program) + GLES20.glDeleteProgram(program) + throw RuntimeException(message) + } + return program + } } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/program/GlHandle.kt b/library/src/main/java/com/otaliastudios/opengl/program/GlProgramLocation.kt similarity index 72% rename from library/src/main/java/com/otaliastudios/opengl/program/GlHandle.kt rename to library/src/main/java/com/otaliastudios/opengl/program/GlProgramLocation.kt index dbf7559..3de981b 100644 --- a/library/src/main/java/com/otaliastudios/opengl/program/GlHandle.kt +++ b/library/src/main/java/com/otaliastudios/opengl/program/GlProgramLocation.kt @@ -6,7 +6,7 @@ import com.otaliastudios.opengl.core.Egloo /** * A simple helper class for holding handles to program variables. */ -class GlHandle private constructor( +class GlProgramLocation private constructor( program: Int, type: Type, @Suppress("CanBeParameter") val name: String @@ -24,7 +24,7 @@ class GlHandle private constructor( } companion object { - fun getAttrib(program: Int, name: String) = GlHandle(program, Type.ATTRIB, name) - fun getUniform(program: Int, name: String) = GlHandle(program, Type.UNIFORM, name) + fun getAttrib(program: Int, name: String) = GlProgramLocation(program, Type.ATTRIB, name) + fun getUniform(program: Int, name: String) = GlProgramLocation(program, Type.UNIFORM, name) } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/program/GlTextureProgram.kt b/library/src/main/java/com/otaliastudios/opengl/program/GlTextureProgram.kt index 1362742..a7b3c05 100644 --- a/library/src/main/java/com/otaliastudios/opengl/program/GlTextureProgram.kt +++ b/library/src/main/java/com/otaliastudios/opengl/program/GlTextureProgram.kt @@ -1,138 +1,188 @@ package com.otaliastudios.opengl.program - import android.graphics.RectF -import android.opengl.GLES11Ext import android.opengl.GLES20 import com.otaliastudios.opengl.core.Egloo import com.otaliastudios.opengl.draw.Gl2dDrawable import com.otaliastudios.opengl.draw.GlDrawable import com.otaliastudios.opengl.extensions.floatBufferOf +import com.otaliastudios.opengl.texture.GlTexture import java.lang.RuntimeException + /** - * An [GlProgram] that uses a simple vertex shader and a texture fragment shader. + * Base implementation for a [GlProgram] that draws textures. */ @Suppress("unused") -open class GlTextureProgram @JvmOverloads constructor( - private val textureUnit: Int = GLES20.GL_TEXTURE0 -) : GlProgram(SIMPLE_VERTEX_SHADER, SIMPLE_FRAGMENT_SHADER) { - - private val textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES +open class GlTextureProgram protected constructor( + handle: Int, + ownsHandle: Boolean, + /* An attribute vec4 within the vertex shader that will contain the vertex position. */ + vertexPositionName: String, + /* A uniform mat4 within the vertex shader that will contain the MVP matrix. */ + vertexMvpMatrixName: String, + textureCoordsName: String?, // enforce not null? + textureTransformName: String? // enforce not null? +): GlProgram(handle, ownsHandle) { + + @JvmOverloads + constructor( + vertexShader: String = SIMPLE_VERTEX_SHADER, + fragmentShader: String = SIMPLE_FRAGMENT_SHADER, + vertexPositionName: String = "aPosition", + vertexMvpMatrixName: String = "uMVPMatrix", + textureCoordsName: String? = "aTextureCoord", + textureTransformName: String? = "uTexMatrix" + ) : this( + create(vertexShader, fragmentShader), + true, + vertexPositionName, + vertexMvpMatrixName, + textureCoordsName, + textureTransformName + ) + + @JvmOverloads + constructor( + handle: Int, + vertexPositionName: String = "aPosition", + vertexMvpMatrixName: String = "uMVPMatrix", + textureCoordsName: String? = "aTextureCoord", + textureTransformName: String? = "uTexMatrix" + ) : this( + handle, + false, + vertexPositionName, + vertexMvpMatrixName, + textureCoordsName, + textureTransformName + ) - private val vertexPositionHandle = getAttribHandle("aPosition") - private val vertexMvpMatrixHandle = getUniformHandle("uMVPMatrix") - private val textureCoordsHandle = getAttribHandle("aTextureCoord") - private val textureTransformHandle = getUniformHandle("uTexMatrix") + var textureTransform: FloatArray = Egloo.IDENTITY_MATRIX.clone() + private val textureTransformHandle = textureTransformName?.let { getUniformHandle(it) } - private val drawableBounds = RectF() private var textureCoordsBuffer = floatBufferOf(8) + private val textureCoordsHandle = textureCoordsName?.let { getAttribHandle(it) } - private fun ensureTextureCoordsBuffer(size: Int) { - if (textureCoordsBuffer.capacity() < size) { - textureCoordsBuffer = floatBufferOf(size) - } - textureCoordsBuffer.clear() - textureCoordsBuffer.limit(size) - } + private val vertexPositionHandle = getAttribHandle(vertexPositionName) + private val vertexMvpMatrixHandle = getUniformHandle(vertexMvpMatrixName) - @Suppress("MemberVisibilityCanBePrivate") - val textureId: Int - init { - val textures = IntArray(1) - GLES20.glGenTextures(1, textures, 0) - Egloo.checkGlError("glGenTextures") - textureId = textures[0] - - GLES20.glActiveTexture(textureUnit) - GLES20.glBindTexture(textureTarget, textureId) - Egloo.checkGlError("glBindTexture $textureId") - - GLES20.glTexParameterf(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat()) - GLES20.glTexParameterf(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat()) - GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) - GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) - Egloo.checkGlError("glTexParameter") - - GLES20.glBindTexture(textureTarget, 0) - GLES20.glActiveTexture(GLES20.GL_TEXTURE0) - Egloo.checkGlError("init end") - } + private val lastDrawableBounds = RectF() + private var lastDrawableVersion = -1 + private var lastDrawable: Gl2dDrawable? = null - @Suppress("MemberVisibilityCanBePrivate") - var textureTransform: FloatArray = Egloo.IDENTITY_MATRIX.clone() + /** + * If not null, [GlTextureProgram] will care about the texture lifecycle: binding, + * unbinding and destroying. + */ + var texture: GlTexture? = null override fun onPreDraw(drawable: GlDrawable, modelViewProjectionMatrix: FloatArray) { super.onPreDraw(drawable, modelViewProjectionMatrix) - GLES20.glActiveTexture(textureUnit) - GLES20.glBindTexture(textureTarget, textureId) + if (drawable !is Gl2dDrawable) { + throw RuntimeException("GlTextureProgram only supports 2D drawables.") + } - // Copy the modelViewProjectionMatrix over. - GLES20.glUniformMatrix4fv(vertexMvpMatrixHandle.value, 1, false, - modelViewProjectionMatrix, 0) - Egloo.checkGlError("glUniformMatrix4fv") + texture?.bind() - // Copy the texture transformation matrix over. - GLES20.glUniformMatrix4fv(textureTransformHandle.value, 1, false, - textureTransform, 0) - Egloo.checkGlError("glUniformMatrix4fv") + // Pass the MVP matrix. + vertexMvpMatrixHandle.let { + GLES20.glUniformMatrix4fv(it.value, 1, false, modelViewProjectionMatrix, 0) + Egloo.checkGlError("glUniformMatrix4fv") + } - // Enable the "aPosition" vertex attribute. - // Connect vertexBuffer to "aPosition". - if (drawable !is Gl2dDrawable) { - throw RuntimeException("GlTextureProgram only supports 2D drawables.") + // Pass the texture transformation matrix. + textureTransformHandle?.let { + GLES20.glUniformMatrix4fv(it.value, 1, false, textureTransform, 0) + Egloo.checkGlError("glUniformMatrix4fv") + } + + // Pass the vertices position. + vertexPositionHandle.let { + GLES20.glEnableVertexAttribArray(it.value) + Egloo.checkGlError("glEnableVertexAttribArray") + GLES20.glVertexAttribPointer(it.value, 2, + GLES20.GL_FLOAT, + false, + drawable.vertexStride, + drawable.vertexArray) + Egloo.checkGlError("glVertexAttribPointer") } - val vertexStride = 2 * Egloo.SIZE_OF_FLOAT - GLES20.glEnableVertexAttribArray(vertexPositionHandle.value) - Egloo.checkGlError("glEnableVertexAttribArray") - GLES20.glVertexAttribPointer(vertexPositionHandle.value, 2, - GLES20.GL_FLOAT, - false, - vertexStride, - drawable.vertexArray) - Egloo.checkGlError("glVertexAttribPointer") // We must compute the texture coordinates given the drawable vertex array. // To do this, we ask the drawable for its boundaries, then apply the texture // onto this rect. - // TODO cache so that we only do this if drawable bounds have changed. - drawable.getBounds(drawableBounds) - val coordinates = drawable.vertexCount * 2 - ensureTextureCoordsBuffer(coordinates) - for (i in 0 until coordinates) { - val isX = i % 2 == 0 - val drawableValue = drawable.vertexArray.get(i) - val drawableMinValue = if (isX) drawableBounds.left else drawableBounds.bottom - val drawableMaxValue = if (isX) drawableBounds.right else drawableBounds.top - val drawableFraction = (drawableValue - drawableMinValue) / (drawableMaxValue - drawableMinValue) - val textureValue = 0F + drawableFraction * 1F // tex value goes from 0 to 1 - textureCoordsBuffer.put(i, textureValue) + textureCoordsHandle?.let { + // Compute only if drawable changed. If the version has not changed, the + // textureCoordsBuffer should be in a good state already - just need to rewind. + if (drawable != lastDrawable || drawable.vertexArrayVersion != lastDrawableVersion) { + lastDrawable = drawable + lastDrawableVersion = drawable.vertexArrayVersion + drawable.getBounds(lastDrawableBounds) + val coordinates = drawable.vertexCount * 2 + if (textureCoordsBuffer.capacity() < coordinates) { + textureCoordsBuffer = floatBufferOf(coordinates) + } + textureCoordsBuffer.clear() + textureCoordsBuffer.limit(coordinates) + for (i in 0 until coordinates) { + val isX = i % 2 == 0 + val value = drawable.vertexArray.get(i) + val min = if (isX) lastDrawableBounds.left else lastDrawableBounds.bottom + val max = if (isX) lastDrawableBounds.right else lastDrawableBounds.top + val texValue = computeTextureCoordinate(i / 2, drawable, value, min, max, isX) + textureCoordsBuffer.put(i, texValue) + } + } else { + textureCoordsBuffer.rewind() + } + + GLES20.glEnableVertexAttribArray(it.value) + Egloo.checkGlError("glEnableVertexAttribArray") + GLES20.glVertexAttribPointer(it.value, 2, + GLES20.GL_FLOAT, + false, + drawable.vertexStride, + textureCoordsBuffer) + Egloo.checkGlError("glVertexAttribPointer") } + } - GLES20.glEnableVertexAttribArray(textureCoordsHandle.value) - Egloo.checkGlError("glEnableVertexAttribArray") - GLES20.glVertexAttribPointer(textureCoordsHandle.value, 2, - GLES20.GL_FLOAT, - false, - vertexStride, - textureCoordsBuffer) - Egloo.checkGlError("glVertexAttribPointer") + // Returns the texture value for a given drawable vertex coordinate, + // considering that texture values go from 0 to 1. + protected open fun computeTextureCoordinate(vertex: Int, + drawable: Gl2dDrawable, + value: Float, + min: Float, + max: Float, + horizontal: Boolean): Float { + val fraction = (value - min) / (max - min) + return 0F + fraction * 1F // in tex coords } override fun onPostDraw(drawable: GlDrawable) { super.onPostDraw(drawable) - GLES20.glDisableVertexAttribArray(vertexPositionHandle.value) - GLES20.glDisableVertexAttribArray(textureCoordsHandle.value) - GLES20.glBindTexture(textureTarget, 0) - GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + vertexPositionHandle.let { + GLES20.glDisableVertexAttribArray(it.value) + } + textureCoordsHandle?.let { + GLES20.glDisableVertexAttribArray(it.value) + } + texture?.unbind() Egloo.checkGlError("onPostDraw end") } + override fun release() { + super.release() + texture?.release() + texture = null + } + companion object { @Suppress("unused") internal val TAG = GlTextureProgram::class.java.simpleName - private const val SIMPLE_VERTEX_SHADER = + const val SIMPLE_VERTEX_SHADER = "" + "uniform mat4 uMVPMatrix;\n" + "uniform mat4 uTexMatrix;\n" + @@ -144,7 +194,7 @@ open class GlTextureProgram @JvmOverloads constructor( " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" + "}\n" - private const val SIMPLE_FRAGMENT_SHADER = + const val SIMPLE_FRAGMENT_SHADER = "" + "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + @@ -154,5 +204,4 @@ open class GlTextureProgram @JvmOverloads constructor( " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + "}\n" } - } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/scene/GlScene.kt b/library/src/main/java/com/otaliastudios/opengl/scene/GlScene.kt index 80eb664..c6fc4bb 100644 --- a/library/src/main/java/com/otaliastudios/opengl/scene/GlScene.kt +++ b/library/src/main/java/com/otaliastudios/opengl/scene/GlScene.kt @@ -1,12 +1,11 @@ package com.otaliastudios.opengl.scene -import android.opengl.GLES20 import android.opengl.Matrix import com.otaliastudios.opengl.core.Egloo import com.otaliastudios.opengl.draw.GlDrawable import com.otaliastudios.opengl.program.GlProgram -import com.otaliastudios.opengl.viewport.GlViewportAware +import com.otaliastudios.opengl.core.GlViewportAware /** * Scenes can be to draw [GlDrawable]s through [GlProgram]s. diff --git a/library/src/main/java/com/otaliastudios/opengl/texture/GlFramebuffer.kt b/library/src/main/java/com/otaliastudios/opengl/texture/GlFramebuffer.kt new file mode 100644 index 0000000..9f834f7 --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/texture/GlFramebuffer.kt @@ -0,0 +1,43 @@ +package com.otaliastudios.opengl.texture + +import android.opengl.GLES20 +import com.otaliastudios.opengl.core.GlBindable +import com.otaliastudios.opengl.core.Egloo +import com.otaliastudios.opengl.core.use + +class GlFramebuffer(id: Int? = null) : GlBindable { + + val id = id ?: run { + val array = IntArray(1) + GLES20.glGenFramebuffers(1, array, 0) + Egloo.checkGlError("glGenFramebuffers") + array[0] + } + + @JvmOverloads + fun attach(texture: GlTexture, attachment: Int = GLES20.GL_COLOR_ATTACHMENT0) { + use { + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, + attachment, + texture.target, + texture.id, + 0) + val status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + throw RuntimeException("Invalid framebuffer generation. Error:$status") + } + } + } + + override fun bind() { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, id) + } + + override fun unbind() { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) + } + + fun release() { + GLES20.glDeleteFramebuffers(1, intArrayOf(id), 0) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/opengl/texture/GlTexture.kt b/library/src/main/java/com/otaliastudios/opengl/texture/GlTexture.kt new file mode 100644 index 0000000..15cd72f --- /dev/null +++ b/library/src/main/java/com/otaliastudios/opengl/texture/GlTexture.kt @@ -0,0 +1,64 @@ +package com.otaliastudios.opengl.texture + +import android.opengl.GLES11Ext +import android.opengl.GLES20 +import com.otaliastudios.opengl.core.GlBindable +import com.otaliastudios.opengl.core.Egloo +import com.otaliastudios.opengl.core.use + +class GlTexture private constructor( + val unit: Int, + val target: Int, + id: Int?, + width: Int?, + height: Int?, + format: Int?) : GlBindable { + + @JvmOverloads + constructor(unit: Int = GLES20.GL_TEXTURE0, target: Int = GLES11Ext.GL_TEXTURE_EXTERNAL_OES, id: Int? = null) + : this(unit, target, id, null, null, null) + + @JvmOverloads + constructor(unit: Int, target: Int, width: Int, height: Int, format: Int = GLES20.GL_RGBA) + : this(unit, target, null, width, height, format) + + val id = id ?: run { + val textures = IntArray(1) + GLES20.glGenTextures(1, textures, 0) + Egloo.checkGlError("glGenTextures") + textures[0] + } + + init { + if (id == null) { + use { + if (width != null && height != null && format != null) { + GLES20.glTexImage2D(target, 0, + format, width, height, 0, + format, GLES20.GL_UNSIGNED_BYTE, null) + } + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat()) + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat()) + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) + Egloo.checkGlError("glTexParameter") + } + } + } + + override fun bind() { + GLES20.glActiveTexture(unit) + GLES20.glBindTexture(target, id) + Egloo.checkGlError("bind") + } + + override fun unbind() { + GLES20.glBindTexture(target, 0) + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + Egloo.checkGlError("unbind") + } + + fun release() { + GLES20.glDeleteTextures(1, intArrayOf(id), 0) + } +} \ No newline at end of file