Skip to content

Commit e27544a

Browse files
committed
Implement OpenGL filter
1 parent 090f8b5 commit e27544a

File tree

15 files changed

+605
-36
lines changed

15 files changed

+605
-36
lines changed

app/build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
apply plugin: 'com.android.application'
22

33
apply plugin: 'kotlin-android'
4-
54
apply plugin: 'kotlin-android-extensions'
65

76
android {
87
compileSdkVersion 28
98
defaultConfig {
109
applicationId "com.camobap.imgmixer"
11-
minSdkVersion 15
10+
minSdkVersion 18 // GLESv3
1211
targetSdkVersion 28
1312
versionCode 1
1413
versionName "1.0"

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
34
package="com.camobap.imgmixer">
45

5-
<application
6+
<application tools:ignore="GoogleAppIndexingWarning,AllowBackup"
67
android:allowBackup="true"
78
android:icon="@mipmap/ic_launcher"
89
android:label="@string/app_name"
@@ -16,6 +17,16 @@
1617
<category android:name="android.intent.category.LAUNCHER"/>
1718
</intent-filter>
1819
</activity>
20+
21+
<provider
22+
android:name="androidx.core.content.FileProvider"
23+
android:authorities="${applicationId}.provider"
24+
android:exported="false"
25+
android:grantUriPermissions="true">
26+
<meta-data
27+
android:name="android.support.FILE_PROVIDER_PATHS"
28+
android:resource="@xml/provider_paths"/>
29+
</provider>
1930
</application>
2031

2132
</manifest>

app/src/main/cpp/CMakeLists.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ include(thirdparty/jni_hpp.cmake)
1414

1515
add_subdirectory(thirdparty/SOIL)
1616

17-
add_library(ndk-shader-mixer SHARED ImageMixer.cpp)
17+
add_library(ndk-shader-mixer SHARED ImageMixer.cpp oglshaderutils.cpp)
1818

1919
set_target_properties(ndk-shader-mixer PROPERTIES CXX_STANDARD 14 CMAKE_CXX_STANDARD_REQUIRED ON)
2020

21-
target_link_libraries(ndk-shader-mixer jni-hpp SOIL)
21+
target_include_directories(ndk-shader-mixer PRIVATE thirdparty/SOIL/src)
22+
23+
#find_library(liblog log)
24+
#find_library(libglesv3 GLESv3)
25+
26+
target_link_libraries(ndk-shader-mixer jni-hpp SOIL log EGL GLESv3)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// Created by CAMOBAP on 2019-05-05.
3+
//
4+
5+
#pragma once
6+
7+
#include <jni/jni.hpp>
8+
9+
namespace jni {
10+
class IllegalStateException {
11+
public:
12+
static constexpr auto Name() { return "java/lang/IllegalStateException"; }
13+
};
14+
15+
[[noreturn]] inline void ThrowIllegalStateException(JNIEnv &env, const char *message = nullptr) {
16+
ThrowNew(env, FindClass(env, IllegalStateException::Name()), message);
17+
}
18+
};

app/src/main/cpp/ImageMixer.cpp

Lines changed: 261 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,270 @@
44

55
#include "ImageMixer.hpp"
66

7+
#include <SOIL.h>
8+
9+
#include "IllegalStateException.hpp"
10+
#define ANDROID_LOG_TAG "ImageMixer++"
11+
#include "stddefs.h"
12+
13+
#include "oglshaderutils.hpp"
14+
715
namespace imgmixer {
816

9-
ImageMixer::ImageMixer(jni::JNIEnv &env)
10-
{}
17+
static constexpr std::size_t MAX_IMAGES = 4;
18+
static thread_local bool is_egl_context_creator_thread = false;
19+
20+
static const char gVertexShader[] =
21+
"#version 300 es\n"
22+
"precision mediump float;\n"
23+
"in vec2 aPosition;\n"
24+
"out vec2 vTexCoord;\n"
25+
"void main() {\n"
26+
" gl_Position = vec4(aPosition, 0.0, 1.0);\n"
27+
" vTexCoord = (aPosition + 1.0) / 2.0;\n"
28+
"}\n";
29+
30+
static const char gFragmentShader[] =
31+
"#version 300 es\n"
32+
"precision mediump float;\n"
33+
"uniform vec2 iResolution;\n"
34+
"uniform sampler2D iChannel0;\n"
35+
"uniform sampler2D iChannel1;\n"
36+
"uniform sampler2D iChannel2;\n"
37+
"uniform sampler2D iChannel3;\n"
38+
"in vec2 vTexCoord;\n"
39+
"out vec4 fragmentColor;\n"
40+
"void main() {\n"
41+
" vec2 uv = vTexCoord; //vec2(vTexCoord.x / 5.0, vTexCoord.y / 5.0);\n"
42+
" vec4 fragColor0 = vec4(texture(iChannel0, uv).rgb, 1.0);\n"
43+
" vec4 fragColor1 = vec4(texture(iChannel1, uv).rgb, 1.0);\n"
44+
" vec4 fragColor2 = vec4(texture(iChannel2, uv).rgb, 1.0);\n"
45+
" vec4 fragColor3 = vec4(texture(iChannel3, uv).rgb, 1.0);\n"
46+
" fragmentColor = mix(fragColor0, mix(fragColor1, mix(fragColor2, fragColor3, 0.5), 0.5), 0.5);\n"
47+
"}\n";
48+
49+
ImageMixer::ImageMixer(jni::JNIEnv &env, jni::jint width, jni::jint height)
50+
{
51+
// Step 1 - Get the default display.
52+
eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
53+
54+
// Step 2 - Initialize EGL.
55+
EGLint eglMajVers, eglMinVers;
56+
eglInitialize(eglDisplay, &eglMajVers, &eglMinVers);
57+
LOGI("EGL init with version %d.%d", eglMajVers, eglMinVers);
58+
59+
60+
// Step 3 - Make OpenGL ES the current API.
61+
eglBindAPI(EGL_OPENGL_ES_API);
62+
63+
// Step 4 - Specify the required configuration attributes.
64+
EGLint configAttr[] = {
65+
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, // very important!
66+
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, // we will create a pixelbuffer surface
67+
EGL_RED_SIZE, 8,
68+
EGL_GREEN_SIZE, 8,
69+
EGL_BLUE_SIZE, 8,
70+
EGL_NONE
71+
};
72+
73+
// Step 5 - Find a config that matches all requirements.
74+
int iConfigs;
75+
EGLConfig eglConfig;
76+
eglChooseConfig(eglDisplay, configAttr, &eglConfig, 1,
77+
&iConfigs);
78+
79+
if (iConfigs != 1) {
80+
jni::ThrowIllegalStateException(env, "Error: eglChooseConfig(): config not found");
81+
}
82+
83+
const EGLint surfaceAttr[] = {
84+
EGL_WIDTH, width,
85+
EGL_HEIGHT, height,
86+
EGL_NONE
87+
};
88+
eglSurface = eglCreatePbufferSurface(eglDisplay, eglConfig, surfaceAttr);
89+
90+
// Step 7 - Create a context.
91+
92+
const EGLint ctxAttr[] = {
93+
EGL_CONTEXT_CLIENT_VERSION, 3, // very important!
94+
EGL_NONE
95+
};
96+
eglContext = eglCreateContext(eglDisplay, eglConfig, NULL,
97+
ctxAttr);
98+
99+
// Step 8 - Bind the context to the current thread
100+
eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
101+
102+
const GLubyte *glslVersion = glGetString(GL_SHADING_LANGUAGE_VERSION);
103+
LOGI("%s", glslVersion);
104+
105+
is_egl_context_creator_thread = true;
106+
107+
glProgram = createProgram(gVertexShader, gFragmentShader);
108+
if (!glProgram) {
109+
jni::ThrowIllegalStateException(env, "Could not create program.");
110+
}
111+
gvPositionHandle = glGetAttribLocation(glProgram, "aPosition");
112+
checkGlError("glGetAttribLocation");
113+
LOGI("glGetAttribLocation(\"aPosition\") = %d\n", gvPositionHandle);
114+
115+
glViewport(0, 0, width, height);
116+
checkGlError("glViewport");
117+
118+
glGenBuffers(1, &quadBuffer);
119+
glBindBuffer(GL_ARRAY_BUFFER, quadBuffer);
120+
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
121+
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
122+
}
11123

12124
ImageMixer::~ImageMixer()
13-
{}
125+
{
126+
// TODO iterate over textures and unload them all
14127

15-
jni::Local<jni::String> ImageMixer::convert(jni::JNIEnv &env,
16-
jni::String& image0,
17-
jni::String& image1,
18-
jni::String& image2,
19-
jni::String& image3)
128+
if (eglDisplay != EGL_NO_DISPLAY) {
129+
eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
130+
if (eglContext != EGL_NO_CONTEXT) {
131+
eglDestroyContext(eglDisplay, eglContext);
132+
}
133+
if (eglSurface != EGL_NO_SURFACE) {
134+
eglDestroySurface(eglDisplay, eglSurface);
135+
}
136+
eglTerminate(eglDisplay);
137+
}
138+
139+
eglDisplay = EGL_NO_DISPLAY;
140+
eglContext = EGL_NO_CONTEXT;
141+
eglSurface = EGL_NO_SURFACE;
142+
}
143+
144+
void ImageMixer::loadTexture(jni::JNIEnv &env, jni::String &imagePath)
20145
{
21-
// TODO all opengl stuff here
146+
if (!is_egl_context_creator_thread) {
147+
jni::ThrowIllegalStateException(env, "Called from thread which is different to one who created eglContext");
148+
}
149+
150+
if (textures.size() >= MAX_IMAGES) {
151+
jni::ThrowIllegalStateException(env, "Attempt to add texture, but max supported textures exceed");
152+
}
153+
154+
std::string image_path = jni::Make<std::string>(env, imagePath);
155+
GLuint tex = SOIL_load_OGL_texture(image_path.c_str(), SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y);
156+
checkGlError("glTexImage2D");
157+
158+
if (!tex) {
159+
jni::ThrowIllegalStateException(env, "Image cannot be loaded");
160+
}
161+
162+
textures.push_back(tex);
163+
}
164+
165+
void ImageMixer::clearTextures(jni::JNIEnv &env)
166+
{
167+
if (!is_egl_context_creator_thread) {
168+
jni::ThrowIllegalStateException(env, "Called from thread which is different to one who created eglContext");
169+
}
170+
171+
for (auto tex : textures) {
172+
glDeleteTextures(1, &tex);
173+
checkGlError("glDeleteTextures");
174+
}
175+
176+
textures.clear();
177+
}
178+
179+
void ImageMixer::convert(jni::JNIEnv &env, jni::String &outPath)
180+
{
181+
if (!is_egl_context_creator_thread) {
182+
jni::ThrowIllegalStateException(env, "Called from thread which is different to one who created eglContext");
183+
}
184+
// commit the clear to the offscreen surface
185+
// eglSwapBuffers(eglDisplay, eglSurface);
186+
187+
glClearColor(1.0, 1.0, 1.0, 1.0);
188+
glClear(GL_COLOR_BUFFER_BIT);
189+
190+
glEnable(GL_BLEND);
191+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
192+
193+
checkGlError("glClear");
194+
195+
glUseProgram(glProgram);
196+
checkGlError("glUseProgram");
197+
198+
// bind textures
199+
for (GLuint unit = 0; unit < MAX_IMAGES; unit++) {
200+
if (unit < textures.size()) {
201+
std::string uniformNameStr = "iChannel" + std::to_string(unit);
202+
GLint uniformName = glGetUniformLocation(glProgram, uniformNameStr.c_str());
203+
checkGlError("glGetUniformLocation");
204+
205+
glActiveTexture(GL_TEXTURE0 + unit);
206+
checkGlError("glActiveTexture");
207+
glBindTexture(GL_TEXTURE_2D, textures[unit]);
208+
checkGlError("glBindTexture");
209+
glUniform1i(uniformName, unit);
210+
checkGlError("glUniform1i");
211+
212+
}
213+
}
214+
// bind resolution uniform
215+
GLint viewport[4];
216+
217+
glGetIntegerv(GL_VIEWPORT, viewport);
218+
GLsizei width = viewport[2], height = viewport[3];
219+
GLint resolutionName = glGetUniformLocation(glProgram, "iResolution");
220+
checkGlError("glGetUniformLocation");
221+
glUniform2f(resolutionName, (GLfloat)width, (GLfloat)height);
222+
checkGlError("glUniform2f");
223+
224+
glEnableVertexAttribArray(gvPositionHandle);
225+
checkGlError("glEnableVertexAttribArray");
226+
227+
glBindBuffer(GL_ARRAY_BUFFER, quadBuffer);
228+
checkGlError("glBindBuffer");
229+
glVertexAttribPointer(
230+
gvPositionHandle, // attribute
231+
2, // number of elements per vertex, here (x,y)
232+
GL_FLOAT, // the type of each element
233+
GL_FALSE, // take our values as-is
234+
0, // no extra data between each position
235+
0 // offset of first element
236+
);
237+
checkGlError("glVertexAttribPointer");
238+
239+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
240+
checkGlError("glDrawArrays");
241+
242+
glDisableVertexAttribArray(gvPositionHandle);
243+
checkGlError("glDisableVertexAttribArray");
244+
245+
for (GLuint unit = 0; unit < MAX_IMAGES; unit++) {
246+
if (unit < textures.size()) {
247+
glActiveTexture(GL_TEXTURE0 + unit);
248+
checkGlError("glActiveTexture");
249+
glBindTexture(GL_TEXTURE_2D, GL_NONE);
250+
checkGlError("glBindTexture");
251+
}
252+
}
253+
254+
// read
255+
256+
const auto readPixels = std::get_temporary_buffer<uint32_t>(width * height);
257+
glReadPixels(viewport[0], viewport[1], viewport[2], viewport[3], GL_RGB, GL_UNSIGNED_BYTE, readPixels.first);
258+
checkGlError("glReadPixels");
259+
260+
glUseProgram(GL_NONE);
261+
checkGlError("glUseProgram");
262+
263+
// save
264+
std::string result_path = jni::Make<std::string>(env, outPath);
265+
bool success = (bool) SOIL_save_screenshot(result_path.c_str(), SOIL_SAVE_TYPE_BMP, viewport[0], viewport[1], viewport[2], viewport[3]);
266+
std::return_temporary_buffer(readPixels.first);
22267

23-
return jni::Local<jni::String>();
268+
if (!success) {
269+
jni::ThrowIllegalStateException(env, "Image cannot be saved to disk");
270+
}
24271
}
25272

26273
};
@@ -35,13 +282,15 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
35282
jni::RegisterNativePeer<ImageMixer>(env,
36283
jni::Class<ImageMixer>::Find(env),
37284
"nativeHandle",
38-
jni::MakePeer<ImageMixer>,
285+
jni::MakePeer<ImageMixer, jni::jint, jni::jint>,
39286
"initialize",
40287
"finalize",
288+
METHOD(&ImageMixer::loadTexture, "load0"),
289+
METHOD(&ImageMixer::clearTextures, "clear"),
41290
METHOD(&ImageMixer::convert, "convert0")
42291
);
43292

44293
#undef METHOD
45294

46-
return jni::Unwrap(jni::jni_version_1_2);
295+
return jni::Unwrap(jni::jni_version_1_6);
47296
}

0 commit comments

Comments
 (0)