diff --git a/android/build.gradle b/android/build.gradle index d8c78636b7cbb3b87589420f4fbd3a6797e844b9..261d567faea21edda47c17d28c823bd3671cd1d9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -30,5 +30,6 @@ repositories { } dependencies { - compile 'com.facebook.react:react-native:0.11.+' + // FIXME: version shouldn't be hardcoded? how do to peerDep ? + compile 'com.facebook.react:react-native:0.14.+' } diff --git a/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java b/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java new file mode 100644 index 0000000000000000000000000000000000000000..a590ad0d35d921b16b866d13c81f3547281e0f7c --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java @@ -0,0 +1,605 @@ +package com.projectseptember.RNGL; + +import static android.opengl.GLES20.*; + +import android.graphics.PixelFormat; +import android.opengl.GLSurfaceView; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.logging.Logger; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class GLCanvas extends GLSurfaceView implements GLSurfaceView.Renderer { + + private static final Logger logger = Logger.getLogger(GLCanvas.class.getName()); + + private ReactContext reactContext; + private RNGLContext rnglContext; + private boolean preloadingDone = false; + private boolean deferredRendering = false; + private GLRenderData renderData; + private int[] defaultFBO; + + private int nbContentTextures; + private int renderId; + private boolean opaque; + private boolean autoRedraw; + private boolean eventsThrough; + private boolean visibleContent; + private int captureNextFrameId; + private GLData data; + private ReadableArray imagesToPreload; // TODO we need to make a List I guess? probably Uri should be resolved in advance. not in GLImage + + List preloaded; // TODO List + + private Map images = new HashMap<>(); + private List contentTextures = new ArrayList<>(); + + public GLCanvas(ThemedReactContext context) { + super(context); + reactContext = context; + this.rnglContext = context.getNativeModule(RNGLContext.class); + this.setEGLContextClientVersion(2); + this.setEGLConfigChooser(8, 8, 8, 8, 16, 0); + this.getHolder().setFormat(PixelFormat.RGBA_8888); + this.setRenderer(this); + this.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + this.requestRender(); + + preloadingDone = true; // TODO + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + // FIXME anything to do here? + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + // FIXME anything to do here? + } + + @Override + public void onDrawFrame(GL10 gl) { + runAll(mRunOnDraw); + + syncEventsThrough(); // FIXME, really need to do this ? + + if (!preloadingDone) { + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + return; + } + + boolean needsDeferredRendering = nbContentTextures > 0 && !autoRedraw; + if (needsDeferredRendering && !deferredRendering) { + deferredRendering = true; + this.requestRender(); // FIXME is this working correctly? + /* + dispatch_async(dispatch_get_main_queue(), ^{ + if (!weakSelf) return; + deferredRendering = true; + [weakSelf setNeedsDisplay]; + }); + */ + } + else { + this.render(); + deferredRendering = false; + } + } + + public void setNbContentTextures(int nbContentTextures) { + this.nbContentTextures = nbContentTextures; + // TODO: resize uniform content textures + } + + public void setRenderId(int renderId) { + this.renderId = renderId; + if (nbContentTextures > 0) + this.requestRender(); + } + + public void setOpaque(boolean opaque) { + this.opaque = opaque; + /* // FIXME: how to do ? + if (opaque) { + this.getHolder().setFormat(PixelFormat.RGB_565); + } + else { + this.getHolder().setFormat(PixelFormat.TRANSLUCENT); + } + */ + this.requestRender(); // FIXME is this required? + } + + public void setAutoRedraw(boolean autoRedraw) { + this.autoRedraw = autoRedraw; + this.setRenderMode(autoRedraw ? GLSurfaceView.RENDERMODE_CONTINUOUSLY : GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + + public void setEventsThrough(boolean eventsThrough) { + this.eventsThrough = eventsThrough; + syncEventsThrough(); + } + + public void setVisibleContent(boolean visibleContent) { + this.visibleContent = visibleContent; + syncEventsThrough(); + } + + public void setCaptureNextFrameId(int captureNextFrameId) { + // FIXME move away from this pattern. just use a method, same to ObjC impl + this.captureNextFrameId = captureNextFrameId; + this.requestRender(); + } + + private boolean ensureCompiledShader (List data) { + for (GLData d: data) { + if (!ensureCompiledShader(d)); + return false; + } + return true; + } + + private boolean ensureCompiledShader (GLData data) { + GLShader shader = rnglContext.getShader(data.shader); + if (shader == null) return false; + shader.ensureCompile(); + return ensureCompiledShader(data.children) && ensureCompiledShader(data.contextChildren); + } + + public void setData (GLData data) { + this.data = data; + this.requestSyncData(); + } + + + public void setImagesToPreload(ReadableArray imagesToPreload) { + logger.info("setImageToPreload, working correctly?"); // TODO + if (preloadingDone) return; + if (imagesToPreload.size() == 0) { + dispatchOnLoad(); + preloadingDone = true; + } + else { + preloadingDone = false; + } + + this.imagesToPreload = imagesToPreload; + } + + // Sync methods + + private final Queue mRunOnDraw = new LinkedList<>(); + protected void runOnDraw (final Runnable runnable) { + synchronized (mRunOnDraw) { + mRunOnDraw.add(runnable); + requestRender(); + } + } + private void runAll(Queue queue) { + synchronized (queue) { + while (!queue.isEmpty()) { + queue.poll().run(); + } + } + } + + public void requestSyncData () { + runOnDraw(new Runnable() { + public void run() { + if (ensureCompiledShader(data)) + syncData(); + else + requestSyncData(); + } + }); + } + + public void syncContextTextures () { + int i = 0; + for (GLTexture texture: contentTextures) { + View view = ((ViewGroup) this.getParent()).getChildAt(i); + texture.setPixelsWithView(view); + i ++; + } + } + + public void resizeUniformContentTextures (int n) { + int length = contentTextures.size(); + if (length == n) return; + if (n < length) { + contentTextures = contentTextures.subList(0, n); + } + else { + for (int i = contentTextures.size(); i < n; i++) { + contentTextures.add(new GLTexture()); + } + } + } + + + private int countPreloaded () { + int nb = 0; + for (int i=0; i images) { + Map prevImages = this.images; + + GLShader shader = rnglContext.getShader(data.shader); + Map uniformsInteger = new HashMap<>(); + Map uniformsFloat = new HashMap<>(); + Map uniformsIntBuffer = new HashMap<>(); + Map uniformsFloatBuffer = new HashMap<>(); + Map textures = new HashMap<>(); + List contextChildren = new ArrayList<>(); + List children = new ArrayList<>(); + + shader.bind(); + + for (GLData child: data.contextChildren) { + contextChildren.add(recSyncData(child, images)); + } + + for (GLData child: data.children) { + children.add(recSyncData(child, images)); + } + + Map uniformTypes = shader.getUniformTypes(); + + int units = 0; + ReadableMapKeySetIterator iterator = data.uniforms.keySetIterator(); + while (iterator.hasNextKey()) { + String uniformName = iterator.nextKey(); + int type = uniformTypes.get(uniformName); + + ReadableMap dataUniforms = data.uniforms; + + if (type == GL_SAMPLER_2D || type == GL_SAMPLER_CUBE) { + uniformsInteger.put(uniformName, units++); + + if (dataUniforms.isNull(uniformName)) { + GLTexture emptyTexture = new GLTexture(); + emptyTexture.setPixelsEmpty(); + textures.put(uniformName, emptyTexture); + } + else { + ReadableMap value = dataUniforms.getMap(uniformName); + String t = value.getString("type"); + if (t.equals("content")) { + int id = value.getInt("id"); + if (id >= contentTextures.size()) { + this.resizeUniformContentTextures(id + 1); + } + textures.put(uniformName, contentTextures.get(id)); + } + else if (t.equals("fbo")) { + int id = value.getInt("id"); + GLFBO fbo = rnglContext.getFBO(id); + textures.put(uniformName, fbo.color.get(0)); + } + else if (t.equals("uri")) { + final String src = srcResource(value); + if (src==null || src.equals("")) { + logger.severe("Shader '"+shader.getName()+": texture uniform '"+uniformName+"': Invalid uri format '"+value+"'"); + } + + GLImage image = images.get(src); + if (image == null) { + image = prevImages.get(src); + if (image != null) + images.put(src, image); + } + if (image == null) { + image = new GLImage(reactContext.getApplicationContext(), new Runnable() { + public void run() { + onImageLoad(src); + } + }); // TODO bind on load + /* + [[GLImage alloc] initWithBridge:_bridge withOnLoad:^{ + if (weakSelf) [weakSelf onImageLoad:src]; + }]; + */ + image.setSrc(src); + images.put(src, image); + } + textures.put(uniformName, image.getTexture()); + } + else { + logger.severe("Shader '"+shader.getName()+": texture uniform '"+uniformName+"': Unexpected type '"+type+"'"); + } + } + } + else { + switch (type) { + case GL_INT: + uniformsInteger.put(uniformName, dataUniforms.getInt(uniformName)); + break; + + case GL_BOOL: + uniformsInteger.put(uniformName, dataUniforms.getBoolean(uniformName) ? 1 : 0); + break; + + case GL_FLOAT: + uniformsFloat.put(uniformName, (float) dataUniforms.getDouble(uniformName)); + break; + + case GL_FLOAT_VEC2: + case GL_FLOAT_VEC3: + case GL_FLOAT_VEC4: + case GL_FLOAT_MAT2: + case GL_FLOAT_MAT3: + case GL_FLOAT_MAT4: + ReadableArray arr = dataUniforms.getArray(uniformName); + if (arraySizeForType(type) != arr.size()) { + throw new Error("Shader '"+shader.getName()+ + "' uniform '"+uniformName+ + "': Invalid array size: "+arr.size()+ + ". Expected: "+arraySizeForType(type)); + } + uniformsFloatBuffer.put(uniformName, parseAsFloatArray(arr)); + break; + + case GL_INT_VEC2: + case GL_INT_VEC3: + case GL_INT_VEC4: + case GL_BOOL_VEC2: + case GL_BOOL_VEC3: + case GL_BOOL_VEC4: + ReadableArray arr2 = dataUniforms.getArray(uniformName); + if (arraySizeForType(type) != arr2.size()) { + throw new Error("Shader '"+shader.getName()+ + "' uniform '"+uniformName+ + "': Invalid array size: "+arr2.size()+ + ". Expected: "+arraySizeForType(type)); + } + uniformsIntBuffer.put(uniformName, parseAsIntArray(arr2)); + break; + + default: + throw new Error("Shader '"+shader.getName()+ + "' uniform '"+uniformName+ + "': type not supported: "+type); + } + + } + } + + int[] maxTextureUnits = new int[1]; + glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, maxTextureUnits, 0); + if (units > maxTextureUnits[0]) { + logger.severe("Shader '"+shader.getName()+": Maximum number of texture reach. got "+units+" >= max "+maxTextureUnits); + } + + for (String uniformName: uniformTypes.keySet()) { + if (!uniformsFloat.containsKey(uniformName) && + !uniformsInteger.containsKey(uniformName) && + !uniformsFloatBuffer.containsKey(uniformName) && + !uniformsIntBuffer.containsKey(uniformName)) { + logger.severe("Shader '"+shader.getName()+": All defined uniforms must be provided. Missing '"+uniformName+"'"); + } + } + + return new GLRenderData( + shader, + uniformsInteger, + uniformsFloat, + uniformsIntBuffer, + uniformsFloatBuffer, + textures, + data.width, + data.height, + data.fboId, + contextChildren, + children); + } + + private FloatBuffer parseAsFloatArray(ReadableArray array) { + int size = array.size(); + FloatBuffer buf = ByteBuffer.allocateDirect(size * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + for (int i=0; i images = new HashMap<>(); + renderData = recSyncData(data, images); + this.images = images; + this.requestRender(); // FIXME don't do it here since syncData is called in render phase + } + + public void recRender (GLRenderData renderData) { + DisplayMetrics dm = reactContext.getResources().getDisplayMetrics(); + + int w = new Float(renderData.width.floatValue() * dm.density).intValue(); + int h = new Float(renderData.height.floatValue() * dm.density).intValue(); + + for (GLRenderData child: renderData.contextChildren) + recRender(child); + + for (GLRenderData child: renderData.children) + recRender(child); + + if (renderData.fboId == -1) { + glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO[0]); + glViewport(0, 0, w, h); + } + else { + GLFBO fbo = rnglContext.getFBO(renderData.fboId); + fbo.setShape(w, h); + fbo.bind(); + } + + renderData.shader.bind(); + + for (String uniformName: renderData.textures.keySet()) { + GLTexture texture = renderData.textures.get(uniformName); + int unit = renderData.uniformsInteger.get(uniformName); + texture.bind(unit); + } + + Map uniformTypes = renderData.shader.getUniformTypes(); + for (String uniformName: renderData.uniformsInteger.keySet()) { + renderData.shader.setUniform(uniformName, renderData.uniformsInteger.get(uniformName)); + } + for (String uniformName: renderData.uniformsFloat.keySet()) { + renderData.shader.setUniform(uniformName, renderData.uniformsFloat.get(uniformName)); + } + for (String uniformName: renderData.uniformsFloatBuffer.keySet()) { + renderData.shader.setUniform(uniformName, renderData.uniformsFloatBuffer.get(uniformName), uniformTypes.get(uniformName)); + } + for (String uniformName: renderData.uniformsIntBuffer.keySet()) { + renderData.shader.setUniform(uniformName, renderData.uniformsIntBuffer.get(uniformName), uniformTypes.get(uniformName)); + } + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + public void render () { + if (renderData == null) return; + syncContextTextures(); + + defaultFBO = new int[1]; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, defaultFBO, 0); + glEnable(GL_BLEND); + recRender(renderData); + glDisable(GL_BLEND); + glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO[0]); + } + + public void syncEventsThrough () { + // TODO: figure out how to do this (Obj C code below) + //self.userInteractionEnabled = !(_eventsThrough); + //self.superview.userInteractionEnabled = !(_eventsThrough && !_visibleContent); + } + + + private void dispatchOnProgress(double progress, int count, int total) { + WritableMap event = Arguments.createMap(); + event.putDouble("progress", progress); + event.putInt("count", count); + event.putInt("total", total); + ReactContext reactContext = (ReactContext)getContext(); + reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( + getId(), + "progress", + event); + } + + public void dispatchOnLoad () { + WritableMap event = Arguments.createMap(); + ReactContext reactContext = (ReactContext)getContext(); + reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( + getId(), + "load", + event); + } + +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java b/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java index ce0070a5e699a2515c6a86a5b1fb1afc9e32106f..028a3779f587ba5d9cd18deba5625e45e6c917bd 100644 --- a/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java +++ b/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java @@ -1,53 +1,73 @@ package com.projectseptember.RNGL; -import android.opengl.GLSurfaceView; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ConfigurationInfo; +import android.support.annotation.Nullable; +import android.util.Log; -import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ReactProp; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.opengles.GL10; - -public class GLCanvasManager extends SimpleViewManager { +public class GLCanvasManager extends SimpleViewManager { public static final String REACT_CLASS = "GLCanvas"; - // TODO... props + @ReactProp(name="nbContentTextures") + public void setNbContentTextures (GLCanvas view, int nbContentTextures) { + view.setNbContentTextures(nbContentTextures); + } + @ReactProp(name="renderId") + public void setRenderId (GLCanvas view, int renderId) { + view.setRenderId(renderId); + } - @Override - public String getName() { - return REACT_CLASS; + @ReactProp(name="opaque") + public void setOpaque(GLCanvas view, boolean opaque) { + view.setOpaque(opaque); } - @Override - public GLSurfaceView createViewInstance(ThemedReactContext context) { - GLSurfaceView view = new GLSurfaceView(context); - view.setRenderer(new GLSurfaceView.Renderer() { - @Override - public void onSurfaceCreated(GL10 gl, EGLConfig config) { + @ReactProp(name="autoRedraw") + public void setAutoRedraw(GLCanvas view, boolean autoRedraw) { + view.setAutoRedraw(autoRedraw); + } - } + @ReactProp(name="eventsThrough") + public void setEventsThrough(GLCanvas view, boolean eventsThrough) { + view.setEventsThrough(eventsThrough); + } - @Override - public void onSurfaceChanged(GL10 gl, int width, int height) { - gl.glViewport(0, 0, width, height); - } + @ReactProp(name="visibleContent") + public void setVisibleContent(GLCanvas view, boolean visibleContent) { + view.setVisibleContent(visibleContent); + } - @Override - public void onDrawFrame(GL10 gl) { - gl.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); - gl.glClear(GL10.GL_COLOR_BUFFER_BIT); - } - }); - return view; + @ReactProp(name="captureNextFrameId") + public void setCaptureNextFrameId(GLCanvas view, int captureNextFrameId) { + view.setCaptureNextFrameId(captureNextFrameId); + } + + @ReactProp(name="data") + public void setData(GLCanvas view, @Nullable ReadableMap glData) { + view.setData(glData == null ? null : GLData.fromMap(glData)); + } + + @ReactProp(name="imagesToPreload") + public void setImagesToPreload(GLCanvas view, @Nullable ReadableArray imageToPreload) { + view.setImagesToPreload(imageToPreload); + } + + @Override + public String getName() { + return REACT_CLASS; } @Override - public void updateView(final GLSurfaceView view, final CatalystStylesDiffMap props) { - super.updateView(view, props); - // TODO... call setters with props - // view.requestRender(); + public GLCanvas createViewInstance(ThemedReactContext context) { + return new GLCanvas(context); } } diff --git a/android/src/main/java/com/projectseptember/RNGL/GLData.java b/android/src/main/java/com/projectseptember/RNGL/GLData.java new file mode 100644 index 0000000000000000000000000000000000000000..c0c042001efc3c1cc0ab38c803dcfda2364752e4 --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLData.java @@ -0,0 +1,47 @@ +package com.projectseptember.RNGL; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +import java.util.ArrayList; +import java.util.List; + +public class GLData { + + final Integer shader; + final ReadableMap uniforms; + final Integer width; + final Integer height; + final Integer fboId; + final List contextChildren; + final List children; + + public GLData(Integer shader, ReadableMap uniforms, Integer width, Integer height, Integer fboId, List contextChildren, List children) { + this.shader = shader; + this.uniforms = uniforms; + this.width = width; + this.height = height; + this.fboId = fboId; + this.contextChildren = contextChildren; + this.children = children; + } + + public static List fromArray (ReadableArray arr) { + ArrayList list = new ArrayList<>(); + for (int i=0; i < arr.size(); i++) { + list.add(fromMap(arr.getMap(i))); + } + return list; + } + + public static GLData fromMap (ReadableMap map) { + Integer shader = map.getInt("shader"); + ReadableMap uniforms = map.getMap("uniforms"); + Integer width = map.getInt("width"); + Integer height = map.getInt("height"); + Integer fboId = map.getInt("fboId"); + List children = fromArray(map.getArray("children")); + List contextChildren = fromArray(map.getArray("contextChildren")); + return new GLData(shader, uniforms, width, height, fboId, contextChildren, children); + } +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLFBO.java b/android/src/main/java/com/projectseptember/RNGL/GLFBO.java new file mode 100644 index 0000000000000000000000000000000000000000..d5b6b4f4a1ffa7a174bfe18bf2e37ad011401942 --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLFBO.java @@ -0,0 +1,134 @@ +package com.projectseptember.RNGL; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import static android.opengl.GLES20.*; + +public class GLFBO {// TODO + private static final Logger logger = Logger.getLogger(GLFBO.class.getName()); + + public final List color = new ArrayList<>(); + private int handle; + private int width = 0; + private int height = 0; + + private static GLTexture initTexture (int width, int height, int attachment) { + GLTexture texture = new GLTexture(); + texture.bind(); + texture.setShape(width, height); + glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, texture.handle, 0); + return texture; + } + + int initRenderBuffer (int width, int height, int component, int attachment) + { + int[] handleArr = new int[1]; + glGenRenderbuffers(1, handleArr, 0); + int handle = handleArr[0]; + glBindRenderbuffer(GL_RENDERBUFFER, handle); + glRenderbufferStorage(GL_RENDERBUFFER, component, width, height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, handle); + return handle; + } + + class FBOState { + + private int fbo; + private int rbo; + private int tex; + + public FBOState() { + int[] fbo = new int[1], rbo = new int[1], tex = new int[1]; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, fbo, 0); + glGetIntegerv(GL_RENDERBUFFER_BINDING, rbo, 0); + glGetIntegerv(GL_TEXTURE_BINDING_2D, tex, 0); + this.fbo = fbo[0]; + this.rbo = rbo[0]; + this.tex = tex[0]; + } + + private void restore() { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glBindRenderbuffer(GL_FRAMEBUFFER, rbo); + glBindTexture(GL_FRAMEBUFFER, tex); + } + } + + public GLFBO() { + FBOState state = new FBOState(); + + int[] handleArr = new int[1]; + glGenFramebuffers(1, handleArr, 0); + handle = handleArr[0]; + + int numColors = 1; + + glBindFramebuffer(GL_FRAMEBUFFER, handle); + + for(int i=0; i maxFBOSize[0] || h < 0 || h > maxFBOSize[0]) { + logger.severe("Can't resize framebuffer. Invalid dimensions"); + return; + } + width = w; + height = h; + + FBOState state = new FBOState(); + + for (GLTexture clr: color) { + clr.setShape(w, h); + } + + glBindFramebuffer(GL_FRAMEBUFFER, handle); + checkStatus(); + + state.restore(); + } +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLImage.java b/android/src/main/java/com/projectseptember/RNGL/GLImage.java new file mode 100644 index 0000000000000000000000000000000000000000..7015ef7cdc60bb4806b4f6878cdb813ef51215b5 --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLImage.java @@ -0,0 +1,240 @@ +package com.projectseptember.RNGL; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.support.annotation.Nullable; + +import com.facebook.common.util.UriUtil; +import com.facebook.drawee.interfaces.DraweeController; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/* +This class is maintained and inspired from +https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +also inspired from +https://github.com/CyberAgent/android-gpuimage/blob/master/library/src/jp/co/cyberagent/android/gpuimage/GPUImage.java + */ +public class GLImage { + private final Context context; + private String src; + private GLTexture texture; + + private Uri uri; + private boolean isLocalImage; + private boolean isDirty; + private AsyncTask task; + private Runnable onload; + + public GLImage (Context context, Runnable onload) { + this.context = context; + this.onload = onload; + this.texture = new GLTexture(); + } + + public void setSrc(String src) { + if (this.src == src) return; + this.src = src; + reloadImage(); + } + + private void reloadImage () { + uri = null; + if (src != null) { + try { + uri = Uri.parse(src); + // Verify scheme is set, so that relative uri (used by static resources) are not handled. + if (uri.getScheme() == null) { + uri = null; + } + } catch (Exception e) { + // ignore malformed uri, then attempt to extract resource ID. + } + if (uri == null) { + uri = getResourceDrawableUri(context, src); + isLocalImage = true; + } else { + isLocalImage = false; + } + } + isDirty = true; + } + + public void onLoad (Bitmap bitmap) { + texture.setPixels(bitmap); + this.onload.run(); + } + + public GLTexture getTexture() { + if (isDirty) { + if (task != null) task.cancel(true); + task = new LoadImageUriTask(this, uri).execute(); + isDirty = false; + } + return texture; + } + + private static @Nullable Uri getResourceDrawableUri(Context context, @Nullable String name) { + if (name == null || name.isEmpty()) { + return null; + } + name = name.toLowerCase().replace("-", "_"); + int resId = context.getResources().getIdentifier( + name, + "drawable", + context.getPackageName()); + return new Uri.Builder() + .scheme(UriUtil.LOCAL_RESOURCE_SCHEME) + .path(String.valueOf(resId)) + .build(); + } + + + private class LoadImageUriTask extends LoadImageTask { + + private final Uri mUri; + + public LoadImageUriTask(GLImage gpuImage, Uri uri) { + super(gpuImage); + mUri = uri; + } + + @Override + protected Bitmap decode(BitmapFactory.Options options) { + try { + InputStream inputStream; + if (mUri.getScheme().startsWith("http") || mUri.getScheme().startsWith("https")) { + inputStream = new URL(mUri.toString()).openStream(); + } else { + inputStream = context.getContentResolver().openInputStream(mUri); + } + return BitmapFactory.decodeStream(inputStream, null, options); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @Override + protected int getImageOrientation() throws IOException { + Cursor cursor = context.getContentResolver().query(mUri, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null); + + if (cursor == null || cursor.getCount() != 1) { + return 0; + } + + cursor.moveToFirst(); + int orientation = cursor.getInt(0); + cursor.close(); + return orientation; + } + } + + /* + private class LoadImageFileTask extends LoadImageTask { + + private final File mImageFile; + + public LoadImageFileTask(GLImage gpuImage, File file) { + super(gpuImage); + mImageFile = file; + } + + @Override + protected Bitmap decode(BitmapFactory.Options options) { + return BitmapFactory.decodeFile(mImageFile.getAbsolutePath(), options); + } + + @Override + protected int getImageOrientation() throws IOException { + ExifInterface exif = new ExifInterface(mImageFile.getAbsolutePath()); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1); + switch (orientation) { + case ExifInterface.ORIENTATION_NORMAL: + return 0; + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return 0; + } + } + } + */ + + private abstract class LoadImageTask extends AsyncTask { + + private GLImage glImage; + + public LoadImageTask (GLImage glImage) { + this.glImage = glImage; + } + + @Override + protected Bitmap doInBackground(Void... params) { + return loadResizedImage(); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + super.onPostExecute(bitmap); + glImage.onLoad(bitmap); + } + + protected abstract Bitmap decode(BitmapFactory.Options options); + + private Bitmap loadResizedImage() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + decode(options); + options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.RGB_565; + options.inTempStorage = new byte[32 * 1024]; + Bitmap bitmap = decode(options); + if (bitmap == null) { + return null; + } + bitmap = rotateImage(bitmap); + return bitmap; + } + + + private Bitmap rotateImage(final Bitmap bitmap) { + if (bitmap == null) { + return null; + } + Bitmap rotatedBitmap = bitmap; + try { + int orientation = getImageOrientation(); + if (orientation != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(orientation); + rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), + bitmap.getHeight(), matrix, true); + bitmap.recycle(); + } + } catch (IOException e) { + e.printStackTrace(); + } + return rotatedBitmap; + } + + protected abstract int getImageOrientation() throws IOException; + } +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLRenderData.java b/android/src/main/java/com/projectseptember/RNGL/GLRenderData.java new file mode 100644 index 0000000000000000000000000000000000000000..b345013dcbb340e63baa8c6c3f29297ad921dd3d --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLRenderData.java @@ -0,0 +1,46 @@ +package com.projectseptember.RNGL; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.List; +import java.util.Map; + +public class GLRenderData { + + final GLShader shader; + final Map uniformsInteger; + final Map uniformsFloat; + final Map uniformsIntBuffer; + final Map uniformsFloatBuffer; + final Map textures; + final Integer width; + final Integer height; + final Integer fboId; + final List contextChildren; + final List children; + + public GLRenderData( + GLShader shader, + Map uniformsInteger, + Map uniformsFloat, + Map uniformsIntBuffer, + Map uniformsFloatBuffer, + Map textures, + Integer width, + Integer height, + Integer fboId, + List contextChildren, + List children) { + this.shader = shader; + this.uniformsInteger = uniformsInteger; + this.uniformsFloat = uniformsFloat; + this.uniformsIntBuffer = uniformsIntBuffer; + this.uniformsFloatBuffer = uniformsFloatBuffer; + this.textures = textures; + this.width = width; + this.height = height; + this.fboId = fboId; + this.contextChildren = contextChildren; + this.children = children; + } +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLShader.java b/android/src/main/java/com/projectseptember/RNGL/GLShader.java new file mode 100644 index 0000000000000000000000000000000000000000..6fc37fda1618d0336ee90169ffa4a848a9975654 --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLShader.java @@ -0,0 +1,205 @@ +package com.projectseptember.RNGL; + +import static android.opengl.GLES20.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +public class GLShader { + + private static final Logger logger = Logger.getLogger(GLShader.class.getName()); + + private final String name; + private final String vert; + private final String frag; + private Map uniformTypes; + private int program; // Program of the shader + private int buffer[]; // the buffer currently contains 2 static triangles covering the surface + private int pointerLoc; // The "pointer" attribute is used to iterate over vertex + private Map uniformLocations; // The uniform locations cache + + public GLShader(String name, String vert, String frag) { + this.name = name; + this.vert = vert; + this.frag = frag; + makeProgram(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (buffer != null) { + glDeleteProgram(program); + glDeleteBuffers(1, buffer, 0); + } + } + + public void bind () { + if (!glIsProgram(program)) { + logger.severe("Shader '"+name+"': not a program!"); + return; + } + glUseProgram(program); + glBindBuffer(GL_ARRAY_BUFFER, buffer[0]); + glEnableVertexAttribArray(pointerLoc); + glVertexAttribPointer(pointerLoc, 2, GL_FLOAT, false, 0, 0); + } + + public void validate () { + glValidateProgram(program); + int[] validSuccess = new int[1]; + glGetProgramiv(program, GL_VALIDATE_STATUS, validSuccess, 0); + if (validSuccess[0] == GL_FALSE) { + glGetProgramInfoLog(program); + logger.severe("Shader '" + name + "': Validation failed " + glGetProgramInfoLog(program)); + } + } + + public void setUniform (String name, Integer i) { + glUniform1i(uniformLocations.get(name), i); + } + public void setUniform (String name, Float f) { + glUniform1f(uniformLocations.get(name), f); + } + public void setUniform (String name, FloatBuffer buf, int type) { + switch (type) { + case GL_FLOAT_VEC2: + glUniform2fv(uniformLocations.get(name), 1, buf); + break; + case GL_FLOAT_VEC3: + glUniform3fv(uniformLocations.get(name), 1, buf); + break; + case GL_FLOAT_VEC4: + glUniform4fv(uniformLocations.get(name), 1, buf); + break; + case GL_FLOAT_MAT2: + glUniformMatrix2fv(uniformLocations.get(name), 1, false, buf); + break; + case GL_FLOAT_MAT3: + glUniformMatrix3fv(uniformLocations.get(name), 1, false, buf); + break; + case GL_FLOAT_MAT4: + glUniformMatrix4fv(uniformLocations.get(name), 1, false, buf); + break; + default: + throw new Error("Unsupported case: uniform '" + name + "' type: " + type); + } + } + public void setUniform (String name, IntBuffer buf, int type) { + switch (type) { + case GL_INT_VEC2: + case GL_BOOL_VEC2: + glUniform2iv(uniformLocations.get(name), 1, buf); + break; + case GL_INT_VEC3: + case GL_BOOL_VEC3: + glUniform3iv(uniformLocations.get(name), 1, buf); + break; + case GL_INT_VEC4: + case GL_BOOL_VEC4: + glUniform4iv(uniformLocations.get(name), 1, buf); + break; + default: + throw new Error("Unsupported case: uniform '"+name+"' type: "+type); + } + } + + public String getName() { + return name; + } + + public Map getUniformTypes() { + return uniformTypes; + } + + + private static int compileShader (String name, String code, int shaderType) { + int shaderHandle = glCreateShader(shaderType); + glShaderSource(shaderHandle, code); + glCompileShader(shaderHandle); + int compileSuccess[] = new int[1]; + glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, compileSuccess, 0); + if (compileSuccess[0] == GL_FALSE) { + logger.severe("Shader '"+name+"' failed to compile: "+glGetShaderInfoLog(shaderHandle)); + return -1; + } + return shaderHandle; + } + + private void computeMeta () { + Map uniforms = new HashMap<>(); + Map locations = new HashMap<>(); + + int[] nbUniforms = new int[1]; + int[] type = new int[1]; + int[] size = new int[1]; + glGetProgramiv(program, GL_ACTIVE_UNIFORMS, nbUniforms, 0); + for (int i=0; i < nbUniforms[0]; i++) { + String uniformName = glGetActiveUniform(program, i, size, 0, type, 0); + int location = glGetUniformLocation(program, uniformName); + uniforms.put(uniformName, type[0]); + locations.put(uniformName, location); + } + this.uniformTypes = uniforms; + this.uniformLocations = locations; + } + + private void makeProgram () { + int vertex = compileShader(name, vert, GL_VERTEX_SHADER); + if (vertex == -1) return; + + int fragment = compileShader(name, frag, GL_FRAGMENT_SHADER); + if (fragment == -1) return; + + program = glCreateProgram(); + glAttachShader(program, vertex); + glAttachShader(program, fragment); + glLinkProgram(program); + + int[] linkSuccess = new int[1]; + glGetProgramiv(program, GL_LINK_STATUS, linkSuccess, 0); + if (linkSuccess[0] == GL_FALSE) { + logger.severe("Shader '"+name+"': Linking failed "+glGetProgramInfoLog(program)); + return; + } + + glUseProgram(program); + + computeMeta(); + + pointerLoc = glGetAttribLocation(program, "position"); + + buffer = new int[1]; + glGenBuffers(1, buffer, 0); + glBindBuffer(GL_ARRAY_BUFFER, buffer[0]); + + float buf[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + -1.0f, 1.0f, + 1.0f, -1.0f, + 1.0f, 1.0f + }; + FloatBuffer bufferData = ByteBuffer.allocateDirect(buf.length * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + bufferData.put(buf).position(0); + + glBufferData(GL_ARRAY_BUFFER, buf.length * 4, bufferData, GL_STATIC_DRAW); + } + + public boolean isReady () { + return buffer != null && uniformLocations != null; + } + + public boolean ensureCompile() { + if (!isReady()) makeProgram(); + return isReady(); + } +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLTexture.java b/android/src/main/java/com/projectseptember/RNGL/GLTexture.java new file mode 100644 index 0000000000000000000000000000000000000000..c57fd87bb1c654b94d8df8ca4a1f338e3982450f --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/GLTexture.java @@ -0,0 +1,81 @@ +package com.projectseptember.RNGL; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.opengl.GLUtils; +import android.view.View; + +import static android.opengl.GLES20.*; + +public class GLTexture { + public int handle; + public Bitmap bitmapCurrentlyUploaded = null; + + private void dealloc () { + int[] handleArr = new int[] { handle }; + glDeleteTextures(1, handleArr, 0); + bitmapCurrentlyUploaded = null; + } + + private void makeTexture () { + int[] handleArr = new int[1]; + glGenTextures(1, handleArr, 0); + handle = handleArr[0]; + glBindTexture(GL_TEXTURE_2D, handle); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + public int bind (int unit) { + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, handle); + return unit; + } + + public void bind () { + glBindTexture(GL_TEXTURE_2D, handle); + } + + + public void setPixels (Bitmap bitmap) { + if (bitmap != bitmapCurrentlyUploaded) { + bitmapCurrentlyUploaded = bitmap; + bind(); + GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0); + } + } + + public void setPixelsRandom (int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + bitmap.setPixel(x, y, Color.rgb( + (int)(255.0 * Math.random()), + (int)(255.0 * Math.random()), + (int)(255.0 * Math.random()))); + } + } + setPixels(bitmap); + } + + public void setPixelsEmpty () { + Bitmap bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888); + setPixels(bitmap); + } + + public void setPixelsWithView (View view) { + Bitmap bitmap = Bitmap.createBitmap( view.getLayoutParams().width, view.getLayoutParams().height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + view.draw(canvas); + setPixels(bitmap); + } + + public void setShape (int width, int height) { + bind(); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null); + } +} diff --git a/android/src/main/java/com/projectseptember/RNGL/RNGLContext.java b/android/src/main/java/com/projectseptember/RNGL/RNGLContext.java index b29b07a18c1241d1d405391a98780a881fecad45..7a393ababb58ab64010e9d943b07eaa88f21d3e0 100644 --- a/android/src/main/java/com/projectseptember/RNGL/RNGLContext.java +++ b/android/src/main/java/com/projectseptember/RNGL/RNGLContext.java @@ -1,12 +1,35 @@ package com.projectseptember.RNGL; +import android.opengl.GLSurfaceView; + import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; +import java.util.HashMap; +import java.util.Map; + +import javax.microedition.khronos.egl.EGLContext; + public class RNGLContext extends ReactContextBaseJavaModule { + // Share GL Context ? + // http://developer.android.com/training/graphics/opengl/environment.html + // http://stackoverflow.com/questions/8845491/sharing-the-egl2-0-context-between-2-glsurfaceviews-caused-egl-bad-access-on-and + // http://stackoverflow.com/questions/5675355/sharing-the-gles20-context-and-textures-between-different-glsurfaceviews + + private static String STATIC_VERT = + "attribute vec2 position;"+ + "varying vec2 uv;"+ + "void main() {"+ + "gl_Position = vec4(position,0.0,1.0);"+ + "uv = vec2(0.5, 0.5) * (position+vec2(1.0, 1.0));"+ + "}"; + + private Map shaders = new HashMap<>(); + private Map fbos = new HashMap<>(); + public RNGLContext (ReactApplicationContext reactContext) { super(reactContext); } @@ -16,10 +39,21 @@ public class RNGLContext extends ReactContextBaseJavaModule { return "RNGLContext"; } + public GLShader getShader (Integer id) { + return shaders.get(id); + } + + public GLFBO getFBO (Integer id) { + if (!fbos.containsKey(id)) { + fbos.put(id, new GLFBO()); + } + return fbos.get(id); + } + @ReactMethod - public void addShader (Integer id, ReadableMap config) { - String frag = config.getString("frag"); - String name = config.getString("name"); - System.out.println("TODO... addShader: "+id+" "+name); + public void addShader (final Integer id, final ReadableMap config) { + final String frag = config.getString("frag"); + final String name = config.getString("name"); + shaders.put(id, new GLShader(name, STATIC_VERT, frag)); } } diff --git a/android/src/main/java/com/projectseptember/RNGL/RNGLPackage.java b/android/src/main/java/com/projectseptember/RNGL/RNGLPackage.java index 96784e34af14ad11cbdc733c58aed10055939625..19b47ddba8796f61c889fbd8f5aa8bca4baf6b3a 100644 --- a/android/src/main/java/com/projectseptember/RNGL/RNGLPackage.java +++ b/android/src/main/java/com/projectseptember/RNGL/RNGLPackage.java @@ -13,7 +13,7 @@ import com.facebook.react.bridge.JavaScriptModule; public class RNGLPackage implements ReactPackage { - @Override + @Override public List createNativeModules(ReactApplicationContext reactApplicationContext) { List modules = new ArrayList<>(); modules.add(new RNGLContext(reactApplicationContext)); diff --git a/android/src/main/java/com/projectseptember/RNGL/UniformValue.java b/android/src/main/java/com/projectseptember/RNGL/UniformValue.java new file mode 100644 index 0000000000000000000000000000000000000000..98fb479adaca1601a25d3bb3a4826240d7b85f2a --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/UniformValue.java @@ -0,0 +1,7 @@ +package com.projectseptember.RNGL; + +import com.facebook.react.bridge.ReadableMap; + +public class UniformValue { + +}