Рубрики
Без рубрики

Начало работы с собственным приложением OpenGL для Android

собственная разработка OpenGL на Android. С тегами java, android, opengl.

Этот пост – еще одна передача от Medium. Некоторое время назад я решил удалить свой аккаунт там, но до этого было бы пустой тратой времени не перенести (и не улучшить) некоторые из моих постов оттуда. Это один из них.

Во время праздников (Рождество 2018 года) Я хотел попробовать что-то другое. Я никогда в жизни особо не занимался разработкой мобильных устройств. Но поскольку у меня было немного времени, я подумал, что должен попробовать.

Но что интересного в том, чтобы делать что-то простое, например приложение для форм. Я хотел чему-то научиться, поэтому решил создать простое приложение OpenGL без использования каких-либо сторонних библиотек. Все с нуля.

Имейте в виду, что когда-то давно мне было довольно комфортно с разработкой OpenGL (с использованием C++).

Я огляделся в поисках руководств, нашел кое-какие фрагменты тут и там и, наконец, сумел создать то, что хотел.

Это было то, чего я хотел достичь:

  • Нарисуйте круг
  • Заставьте круг двигаться, когда вы наклоняете свой телефон.

Вот что я имел в виду

Это была отличная возможность узнать две вещи

  • Как работают датчики
  • Как OpenGL работает на Android.

Итак, я скачал Android Studio и создал пустой проект.

На данный момент я прочитал несколько статей и руководств, поэтому у меня было некоторое смутное представление о том, что я должен был делать. Мне пришлось создать View и Средство визуализации .

Мой OpenGL View необходимо расширить GLSurfaceView .

public class OpenGLView extends GLSurfaceView {

    public OpenGLView(Context context) {
        super(context);
        init();
    }

    public OpenGLView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init(){
        setEGLContextClientVersion(2);  // OpenGL ES Version
        setPreserveEGLContextOnPause(true);
        setRenderer(new OpenGLRenderer());
    }
}

Затем мой OpenGLRenderer/| должен был реализовать GLSurfaceView. Средство визуализации .

public class OpenGLRenderer implements GLSurfaceView.Renderer {
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(0.9f, 0.9f,0.9f,1f);
    }
   @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }
}

Средство визуализации на этом этапе ничего не делает, кроме настройки цвета фона.

Затем нам нужно перейти к нашему макету и добавить только что созданный вид. Так, чтобы элемент просмотра заполнял наш экран.


Наконец, в нашем MainActivity (который я назвал MainGameActivity ) должен выглядеть следующим образом:

public class MainGameActivity extends AppCompatActivity {
private OpenGLView openGLView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_game);
        openGLView = findViewById(R.id.openGLView);
    }
    @Override
    protected void onResume(){
        super.onResume();
        openGLView.onResume();
    }
    @Override
    protected void onPause(){
        super.onPause();
        openGLView.onPause();
    }
    @Override
    public void onPointerCaptureChanged(boolean hasCapture) {
    }
}

…и если мы выполним код. мы получили бы представление с любым цветом, который мы определили. Цвет, который я определил, почти белый, так что там ничего не должно быть. 🎉

Прежде чем мы нарисуем наш круг, давайте настроим SensorEventListener для прослушивания акселерометра. Хорошо, акселерометр, возможно, не лучший датчик для того, чего мы пытаемся достичь (потому что он будет работать только тогда, когда вы не двигаетесь), поэтому мы могли бы переключиться на гироскоп, но пока, я думаю, все в порядке.

public class MainGameActivity extends AppCompatActivity implements SensorEventListener {
...
    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
        }
    }
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
}

Акселерометр имеет 3 значения

  • значение[0] влияет на ось x
  • значение[1] влияет на ось y
  • значение[2] влияет на ось z

Мы будем использовать только x и y. Мы также округлим значения со второго десятичного знака, поскольку, в частности, нам не нужна высокая точность. Наша функция будет выглядеть следующим образом:

@Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
          float x = Math.round(event.values[0] * 100.0) / 100f;
          float y = Math.round(event.values[1] * 100.0) / 100f;
    }
}

Итак, у нас есть некоторые координаты для нашего круга. Теперь нам нужно использовать возможности OpenGL, чтобы нарисовать круг.

  1. Мы собираемся создать Класс Circle со следующими функциями.
public class Circle {
    // basically a circle is a linestring so we need its centre
    // radius and how many segments it will consist of
    public Circle(float cx, float cy, float radius, int segments) {
    }

    // calculate the segments
    public void calculatePoints(float cx, float cy, float radius, int segments) {
    }
    // actuall openGL drawing
    public void draw() {
    }
}
  1. В наш класс Circle мы собираемся добавить функцию, которая компилирует наш шейдер формы. В основном шейдеры должны быть скомпилированы с помощью OpenGL.
public static int loadShader(int type, String shaderCode){
    int shader = GLES20.glCreateShader(type);
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);
    return shader;
}
  1. В классе Circle мы определим два шейдера (которые мало что делают).
  2. Вершинный шейдер : для рендеринга вершин фигуры.
  3. Фрагментный шейдер : для рендеринга грани фигуры с помощью цветов или текстур.
private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "}";

private final String fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}";
  1. Пришло время вычислить точки окружности. Мы сохраним эти точки в буфере с плавающей запятой.

Простой взгляд на следующий код покажет что-то немного странное. Это DisplayMetrics . Проблема здесь в том, что это связано с тем, что холст OpenGL имеет квадратную форму, а отображение координат экрана варьируется от -1 до 1. Если бы мы просто нарисовали круг, он бы в конечном итоге исказился. Нам нужны ширина и высота экрана, чтобы рассчитать соотношение сторон, чтобы мы могли сжать одно измерение и создать фактический круг.

private FloatBuffer vertexBuffer;
private static final int COORDS_PER_VERTEX = 3;
public void CalculatePoints(float cx, float cy, float radius, int segments) {
    DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();

    float[] coordinates = new float[segments * COORDS_PER_VERTEX];

    for (int i = 0; i < segments * 3; i += 3) {
        float percent = (i / (segments - 1f));
        float rad = percent * 2f * (float) Math.PI;

        //Vertex position
        float xi = cx + radius * (float) Math.cos(rad);
        float yi = cy + radius * (float) Math.sin(rad);

        coordinates[i] = xi;
        coordinates[i + 1] = yi / (((float) dm.heightPixels / (float) dm.widthPixels));
        coordinates[i + 2] = 0.0f;
    }

    // initialise vertex byte buffer for shape coordinates
    ByteBuffer bb = ByteBuffer.allocateDirect(coordinates.length * 4);
    // use the device hardware's native byte order
    bb.order(ByteOrder.nativeOrder());

    // create a floating point buffer from the ByteBuffer
    vertexBuffer = bb.asFloatBuffer();
    // add the coordinates to the FloatBuffer
    vertexBuffer.put(coordinates);
    // set the buffer to read the first coordinate
    vertexBuffer.position(0);
}
  1. Время для реализации конструктора. В случаях:

давайте просто нарисуем фигуру и покончим с этим.

Нам не нужно было бы проверять, инициализировано ли приложение, но потому, что мы намерены обновлять координаты объекта каждый раз, когда получаем событие датчика. Мы не должны загружать шейдер/приложение/ссылку более одного раза.

private int app = -1;
public Circle(float cx, float cy, float radius, int segments) {
    CalculatePoints(cx, cy, radius, segments);
    if (app == -1) {
        int vertexShader = OpenGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = OpenGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);

        // create empty OpenGL ES Program
        app = GLES20.glCreateProgram();

        // add the vertex shader to program
        GLES20.glAttachShader(app, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(app, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(app);
    }
}
  1. Функция рисования
public void draw() {

    int vertexCount = vertexBuffer.remaining() / COORDS_PER_VERTEX;

    // Add program to the environment
    GLES20.glUseProgram(app);

    // get handle to vertex shader's vPosition member
    int mPositionHandle = GLES20.glGetAttribLocation(app, "vPosition");

    // Enable a handle to the triangle vertices
    GLES20.glEnableVertexAttribArray(mPositionHandle);

    // Prepare the triangle coordinate data
    GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
        GLES20.GL_FLOAT, false,
        vertexStride, vertexBuffer);

    // get handle to fragment shader's vColor member
    mColorHandle = GLES20.glGetUniformLocation(app, "vColor");

    // Draw the triangle, using triangle fan is the easiest way
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(mPositionHandle);

    // Set color of the shape (circle)
    GLES20.glUniform4fv(mColorHandle, 1, new float[]{0.5f, 0.3f, 0.1f, 1f}, 0);
}
  1. Наконец, мы возвращаемся к нашему средству визуализации и добавляем новый объект circle. Сначала мы нарисуем наш круг в точке,,.1 с. * * объекты готовы *
public class OpenGLRenderer implements GLSurfaceView.Renderer {
    private Circle circle;
    public boolean objectsReady = false;
    public Circle getCircle() {
        return circle;
    }
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(0.9f, 0.9f,0.9f,1f);
        circle = new Circle(0,0, 0.1f, 55);
        objectsReady = true;
    }
...
}

На этом этапе, если мы запустим наше приложение, мы должны получить коричневый круг в середине нашего экрана. Таким образом, наш onSensorChanged станет. МАСШТАБ используется для сопоставления необходимых нам данных датчика (-4, 4) с нашим представлением OpenGL (-1,1).

private final static int SCALE = 4;
@Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
          float x = Math.round(event.values[0] * 100.0) / 100f;
          float y = Math.round(event.values[1] * 100.0) / 100f;
if (openGLView.renderer.objectsReady) {
              openGLView.renderer.getCircle().CalculatePoints(x /    SCALE, y / SCALE, 0.1f, 55);
              openGLView.requestRender();
          }
    }
}

Наконец-то наш круг жив и здоров, но его движение нервное.

SensorEventListener выдает тысячи событий в секунду с огромной точностью, поэтому, чтобы сделать движение плавным, нам нужно нормализовать наши данные с помощью некоторого статистического метода. Очевидный выбор в решении этой проблемы (по крайней мере, очевидный для меня) – использовать скользящую среднюю.

Скользящая средняя – это не что иное, как среднее значение x последних показаний. Это легко сделать. Нам нужно только добавить следующее к нашему Основному виду деятельности .

private final static int OVERFLOW_LIMIT = 20;
private float[][] movingAverage = new float[2][OVERFLOW_LIMIT];
@Override
public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
        float x = Math.round(event.values[0] * 100.0) / 100f;
        float y = Math.round(event.values[1] * 100.0) / 100f;

        movingAverage[0][overflow] = x;
        movingAverage[1][overflow] = y;

        float s1 = calculateAverage(movingAverage[0]);
        float s2 = calculateAverage(movingAverage[1]);

        if (openGLView.renderer.objectsReady) {
            openGLView.renderer.getCircle().CalculatePoints(s1 / SCALE, s2 / SCALE, 0.1f, 55);
            openGLView.requestRender();
         }
    }
    overflow += 1;
    if (overflow >= OVERFLOW_LIMIT) {
        overflow = 0;
    }
}
private float calculateAverage(float[] input) {
    DoubleStream io = IntStream.range(0, input.length)
            .mapToDouble(i -> input[i]);
    float sum = (float)io.sum();
    return sum/OVERFLOW_LIMIT;
}

Запустив приложение снова, мы теперь получаем более плавное движение нашей фигуры.

Оригинал: “https://dev.to/elasticrash/getting-started-with-native-opengl-android-app-19e7”