To work with OpenGL on Android, you create an activity with a special view. And write the drawing code in the Render interface. This is a convenient separation.
To make code similar to the Android on other platforms let's create
AppOGL class that will responsible for setup a window in which the graphics are displayed. And a Render class, that will be responsible for rendering.
You can download
full sources of examples on GitHub for LWJGL 3, WebGL and Android.
WebGL
You can get WebGL context from any <canvas>
element like 2D context:
var canvas = document.getElementById(idCanvas);
var gl = canvas.getContext("webgl2"); // or "webgl"
Note. The OpenGL 3.0 features are available within WebGL 2.0.
WebGL example
class AppOGL{
canvas
gl
render
idRAF
constructor(idCanvas){
this.canvas = document.getElementById(idCanvas);
var gl = this.canvas.getContext("webgl2");
this.gl = gl;
gl.viewport(0, 0, gl.drawingBufferWidth,
gl.drawingBufferHeight);
this.render = new Render();
this.render.onSetup(this);
window.main = () => {
this.idRAF = window.requestAnimationFrame( main );
this.render.onDrawFrame(this)
};
main();
}
start(){
main();
}
stop(){
window.cancelAnimationFrame( this.idRAF );
this.idRAF=0;
}
setRender(r){
stop();
const old = this.render;
if (r == null) {
r = new Render();
}
r.onSetup(this);
this.render = r;
old.onDispose(this);
}
}class Render{
onSetup(appOGL){
const gl = appOGL.gl;
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
}
onDrawFrame(appOGL){
const gl = appOGL.gl;
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// ...
}
onDispose(appOGL){
}
}
// call our function when document ready
$(()=>{
const app = new AppOGL("myCanvas");
// app.setRender(new QuadRender());
})
LWJGL
Add necessary dependencies from dependency builder. For example, select your OS and option "Minimal OpenGL". It will include GLFW library, that provides a simple API for creating windows, contexts and surfaces, receiving input and events for OpenGL. Select JOML addon for work with matrices.
All GL APIs are declared in classes like GL33, so you can make static imports for the required version.
import static org.lwjgl.opengl.GL20.*;
// same thing for GLFW library
import static org.lwjgl.glfw.GLFW.*;
By default OpenGL 2.0 context will be created. You can change it via windows hints, for example to 3.3 core. It will allow you to use shaders with "330 core" version.
import org.lwjgl.glfw.*
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil.NULL
import org.lwjgl.system.Platform
open class AppOGL {
companion object {
@JvmStatic
var isV120 = false
}
protected var window = 0L
protected var render: Render
var width = 0
protected set
var height = 0
protected set
constructor(w: Int, h: Int) {
render = Render()
init(w, h)
}
constructor(r: Render, w: Int, h: Int) {
render = r
init(w, h)
}
fun setRenderer(r: Render?) {
val old = render
(r ?: Render()).also {
it.onSetup(this)
render = it
old.onDispose(this)
}
}
protected fun init(w: Int, h: Int) {
check(window == 0L) { "GLFW may be already initialized" }
GLFWErrorCallback.createPrint(System.err).set()
check(glfwInit()) { "Unable to initialize GLFW" }
createWindow(w, h)
setupCallbacks()
setupGL()
}
fun setupGL() {
MemoryStack.stackPush().use { stack ->
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
glfwGetWindowSize(window, pWidth, pHeight)
width = pWidth[0]
height = pHeight[0]
// Get the resolution of the primary monitor
val vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor())
// Center the window
glfwSetWindowPos(
window, (vidmode!!.width() - pWidth[0]) / 2,
(vidmode.height() - pHeight[0]) / 2
)
glfwMakeContextCurrent(window)
// Enable v-sync
glfwSwapInterval(1)
// Make the window visible
glfwShowWindow(window)
println("Version: " + glfwGetVersionString())
loop()
}
}
open fun createWindow(w: Int, h: Int) {
glfwDefaultWindowHints()
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE)
if (!isV120) {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)
if (Platform.get() === Platform.MACOSX) {
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE)
}
}
window = glfwCreateWindow(w, h, "Hello OpenGL!", NULL, NULL)
if (window == NULL) {
throw RuntimeException("Failed to create the GLFW window")
}
}
open fun setupCallbacks() {
glfwSetKeyCallback(
window
) { window: Long, key: Int, scancode: Int, action: Int, keyMods: Int ->
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true)
}
}
glfwSetWindowSizeCallback(window) { win, w, h ->
width = w
height = h
render.onSurfaceChanged(this@AppOGL, w, h)
}
glfwSetCursorPosCallback(window) { win, xpos, ypos ->
}
glfwSetMouseButtonCallback(window) { win, button, action, keyMods ->
}
glfwSetScrollCallback(window) { win, xoffset, yoffset ->
}
}
open fun dispose() {
render.onDispose(this)
if (window != 0L) {
Callbacks.glfwFreeCallbacks(window)
glfwDestroyWindow(window)
glfwTerminate()
glfwSetErrorCallback(null)!!.free()
window = 0
}
}
protected fun loop() {
GL.createCapabilities(false)
render.onSetup(this)
while (!glfwWindowShouldClose(window)) {
render.onDrawFrame(this)
glfwSwapBuffers(window) // swap the color buffers
glfwPollEvents()
}
dispose()
}
}
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.Platform;
import java.nio.IntBuffer;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL.createCapabilities;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;
public class AppOGL {
public static boolean isV120 = false;
protected long window = 0L;
protected Render render;
protected int width = 0;
protected int height = 0;
public AppOGL(int w, int h) {
render = new Render();
init(w, h);
}
public AppOGL(Render r, int w, int h) {
render = r;
init(w, h);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public void setRenderer(Render r) {
Render old = render;
if (r == null) {
r = new Render();
}
r.onSetup(this);
render = r;
old.onDispose(this);
}
protected void init(int w, int h) {
if (window != 0L) {
throw new IllegalStateException("GLFW may be already initialized");
}
GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}
createWindow(w, h);
setupCallbacks();
setupGL();
}
protected void setupGL() {
try (MemoryStack stack = stackPush()) {
IntBuffer pWidth = stack.mallocInt(1);
IntBuffer pHeight = stack.mallocInt(1);
glfwGetWindowSize(window, pWidth, pHeight);
width = pWidth.get(0);
height = pHeight.get(0);
// Get the resolution of the primary monitor
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
// Center the window
glfwSetWindowPos(window, (vidmode.width() - pWidth.get(0)) / 2,
(vidmode.height() - pHeight.get(0)) / 2
);
glfwMakeContextCurrent(window);
// Enable v-sync
glfwSwapInterval(1);
// Make the window visible
glfwShowWindow(window);
System.out.println("Version: " + glfwGetVersionString());
loop();
}
}
protected void createWindow(int w, int h) {
glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
if (!isV120) {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
if (Platform.get() == Platform.MACOSX) {
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
}
}
window = glfwCreateWindow(w, h, "Hello OpenGL!", NULL, NULL);
if (window == NULL) {
throw new RuntimeException("Failed to create the GLFW window");
}
}
protected void setupCallbacks() {
glfwSetKeyCallback(
window, (window, key, scancode, action, keyMods) -> {
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true);
}
}
);
glfwSetWindowSizeCallback(window, (win, w, h) -> {
width = w;
height = h;
render.onSurfaceChanged(AppOGL.this, w, h);
});
glfwSetCursorPosCallback(window, (win, xpos, ypos) -> {
});
glfwSetMouseButtonCallback(window, (win, button, action, keyMods) -> {
});
glfwSetScrollCallback(window, (win, xoffset, yoffset) -> {
});
}
protected void dispose() {
render.onDispose(this);
if (window != 0L) {
Callbacks.glfwFreeCallbacks(window);
glfwDestroyWindow(window);
glfwTerminate();
glfwSetErrorCallback(null).free();
window = 0;
}
}
protected void loop() {
createCapabilities(false);
render.onSetup(this);
while (!glfwWindowShouldClose(window)) {
render.onDrawFrame(this);
glfwSwapBuffers(window); // swap the color buffers
glfwPollEvents();
}
dispose();
}
}
MacOS users should start their application passing
"-XstartOnFirstThread" as a VM option.
Render class is a base class for other examples of this tutorial.
Render example
import org.lwjgl.opengl.GL33.*
open class Render {
protected var surfaceWidth = 0f
protected var surfaceHeight = 0f
protected var aspect = 0f
open fun onSetup(appOGL: AppOGL) {
setSurfaceSize(appOGL.width, appOGL.height)
glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
glEnable(GL_DEPTH_TEST)
// ... setup and prepare data
}
protected fun setSurfaceSize(w: Int, h: Int){
surfaceWidth = w.toFloat()
surfaceHeight = h.toFloat()
aspect = surfaceWidth/surfaceHeight
}
open fun onSurfaceChanged(appOGL: AppOGL, width: Int, height: Int) {
setSurfaceSize(width,height)
glViewport(0, 0, width, height)
}
open fun onDrawFrame(appOGL: AppOGL) {
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
// ... draw something
}
open fun onDispose(appOGL: AppOGL) {
}
companion object {
@JvmStatic
val PI_F = Math.PI.toFloat()
@JvmStatic
val TO_RAD = PI_F / 180.0f
@JvmStatic
val ALNGLE45 = 45f * TO_RAD
}
}
import com.darkraha.opengldemoj.gl.AppOGL;
import static org.lwjgl.opengl.GL33.*;
public class Render {
public static final float PI_F = (float) Math.PI;
public static final float TO_RAD = PI_F / 180.f;
public static final float ALNGLE45 = (float) Math.toRadians(45.0f);
protected float surfaceWidth = 0f;
protected float surfaceHeight = 0f;
protected float aspect = 0f;
protected void setSurfaceSize(int w, int h) {
surfaceWidth = w;
surfaceHeight = h;
aspect = surfaceWidth / surfaceHeight;
}
public void onSetup(AppOGL appOGL) {
setSurfaceSize(appOGL.getWidth(), appOGL.getHeight());
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glEnable(GL_DEPTH_TEST);
// ... setup and prepare data
}
public void onSurfaceChanged(AppOGL appOGL, int w, int h) {
setSurfaceSize(w, h);
glViewport(0, 0, w, h);
}
public void onDrawFrame(AppOGL appOGL) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// ... draw something
}
public void onDispose(AppOGL appOGL) {
}
}
Then you can run application with any your render.
companion object {
@JvmStatic
fun main(args: Array<String>) {
// AppOGL.isV120 = true
// val app = AppOGL(300, 300)
val app = AppOGL(Render(),300, 300)
}
} public static void main(String[] args) {
// AppOGL.isV120 = true;
// new AppOGL(300,300);
// new AppOGL(new QuadRender(), 300,300);
new AppOGL(new Render(), 300,300); //
}
Android
GLSurfaceView is a special view where you can draw and manipulate objects using OpenGL API calls.
GLSurfaceView.Renderer interface defines the methods required for drawing graphics. You must provide an implementation of this interface as a separate class and attach it to your GLSurfaceView.
When you are using GLSurfaceView, you don't need to handle the cleanup as the resources will be released when the context is lost. So in activity I don't call
render.onDispose().
You must specify version of OpenGL in AndroidManifest.xml
file.
<!--
0x00020000 - for OpenGL ES 2.0
0x00030000 - for OpenGL ES 3.0
0x00030001 - for OpenGL ES 3.1
-->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
Note. OpenGL ES 3.0 based on OpenGL 3.3 and OpenGL 4.2. You can test OpenGl API on emulator since Android API 29. Check an advance settings of emulator (must be SwiftShader or Native Desktop).
All GL APIs are declared in classes like android.opengl.GLES30.*, so you can make static imports for the required version.
Activity example
class MainActivity : AppCompatActivity() {
private lateinit var glSurfaceView: GLSurfaceView
private var render : Render? =null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(!isOpenGlSupported(3)){
Toast.makeText(this, "OpenGl ES 3.0 is not supported",
Toast.LENGTH_LONG).show()
finish()
return
}
// set view programmatically
glSurfaceView = GLSurfaceView(this)
glSurfaceView.setEGLContextClientVersion(3)
render = Render()
glSurfaceView.setRenderer(render)
setContentView(glSurfaceView)
}
override fun onPause() {
super.onPause()
glSurfaceView.onPause()
}
override fun onResume() {
super.onResume()
glSurfaceView.onResume()
}
private fun isOpenGlSupported(major: Int): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val configInfo: ConfigurationInfo = activityManager.deviceConfigurationInfo
// The GLES version used by an application.
// The upper order 16 bits of reqGlEsVersion represent the major version
// and the lower order 16 bits the minor version.
return if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
println("Version configInfo.reqGlEsVersion shr 16")
Toast.makeText(this, "Supported version OpenGl ${configInfo.reqGlEsVersion shr 16}",
Toast.LENGTH_LONG).show()
(configInfo.reqGlEsVersion shr 16) >= major
} else {
1 >= major
}
}
}
Now Android version of our Render class.
Render example
import android.opengl.GLES30.*
import android.opengl.GLSurfaceView
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
open class Render : GLSurfaceView.Renderer {
protected var surfaceWidth = 0f
protected var surfaceHeight = 0f
protected var aspect = 0f
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
glEnable(GL_DEPTH_TEST)
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
glViewport(0, 0, width, height)
surfaceWidth = width.toFloat()
surfaceHeight = height.toFloat()
aspect = surfaceWidth / surfaceHeight
}
override fun onDrawFrame(arg0: GL10?) {
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
// ... draw something
}
open fun onDispose() {
}
companion object {
@JvmStatic
val PI_F = Math.PI.toFloat()
@JvmStatic
val TO_RAD = PI_F / 180.0f
@JvmStatic
val ALNGLE45 = Math.toRadians(45.0).toFloat()
}
}