- PVSM.RU - https://www.pvsm.ru -

Изучаем OpenGL ES2 для Android Урок №3. Освещение

Перед тем как начать
Если вы новичок в OpenGL ES, рекомендую сначала изучить уроки №1 и №2, так как данный урок опирается на знания предыдущих уроков.
Основы кода, используемого в этой статье, взяты отсюда:
1. http://andmonahov.blogspot.com/2012/10/opengl-es-20.html [1]
2. http://www.learnopengles.com/android-lesson-two-ambient-and-diffuse-lighting/ [2]
В результате мы получим такую картинку на экране устройства или эмулятора.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 1

Немного о свете и видах источников света
Свет может рассматриваться либо как электромагнитная волна определенной частоты, либо как поток фотонов — частиц, обладающих определённой энергией. В зависимости от того, какие фотоны упали на определенные места сетчатки глаза, наш мозг [3] создает зрительные ощущения.
Ощущения цвета, зависят от энергии фотона или частоты электромагнитного излучения. В качестве коротковолновой границы спектрального диапазона, занимаемого светом, принят участок с длинами волн в вакууме 380—400 нм (фиолетовый), а в качестве длинноволновой границы — участок 760—780 нм (красный).
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 2
Кроме ощущения цвета для нас очень важным является понятие яркости света, которое определяется количеством фотонов, которое падает на сетчатку за единицу времени или интенсивности электромагнитной волны.
Все предметы окружающего мира можно условно разделить на те, которые излучают свет и те, которые его отражают. Но мы воспринимаем мир во всем его многообразии, нам важна не только яркость света или цвет, но и отражающие способности самих предметов, тени от них и много чего другого. Компьютерное моделирование окружающего мира достигло таких высот, что иногда только специалист может отличить фотографию реального объекта от компьютерной модели. Существует множество классификаций и систем моделирования освещения объектов, рассмотрим лишь некоторые из них.

Фоновое освещение (ambient lighting)
Когда объекты освещены одинаково со всех сторон, мы говорим о фоновом освещении. В природе такое обычно бывает в пасмурный день. Если мы находимся в помещении, то такое освещение возникает при многократном отражении света от разных объектов или при наличии множества источников света распределенных особым образом, в результате чего наша модель будет освещена со всех сторон. На фото слева вы видите картинку, где смикшировано направленное и фоновое освещение, а справа – только фоновое. При расчете фонового освещения не учитываются ни нормали поверхностей (под каким углом луч падает на поверхность), ни текущее положение камеры.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 3

Как описать фоновое освещение программно?
Пусть наш объект является красным, а наш фоновый свет будет тускло-белый. Предположим, что мы сохраняем цвет, как массив из трех цветов: красного, зеленого и синего, используя цветовую модель RGB. Тогда, нам нужно перемножить весовые коэффициенты цветов на некую константу освещенности.
final color = {1, 0, 0} * {0.1, 0.1, 0.1} = {0.1, 0.0, 0.0}
Теперь окончательный цвет объекта станет тускло-красным, что соответствует тому, что мы увидим в реальной жизни, если осветим красный объект тусклым белым светом.

Рассеянное освещение
Рассеянное освещение (diffuse lighting) — свет от источника рассеивается после попадания в заданную точку. В зависимости от угла, под которым падает свет, освещение становится сильнее или слабее. Здесь учитываются нормали к поверхности, но не положение камеры;
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 4
Представим себе, что луч света падает под углом 45 градусов (угол между лучом и нормалью к поверхности) на горизонтальную поверхность. В результате диффузного (рассеянного) отражения световая энергия равномерно отразится во все стороны, поэтому все три камеры снимут поверхность одинаковой яркости. Понятно, что если свет будет падать под углом 0 градусов (перпендикулярно к поверхности), поверхность будет освещена лучше всего. А если луч света будет падать под углом близким к 90 градусам, то лучи будут скользить по поверхности и не осветят её.

Программное моделирование диффузного отражения
Чтобы смоделировать диффузное отражение, применяют коэффициент Ламберта (lambert factor).
light vector = light position - object position
cosine = dot product(object normal, normalize(light vector))
lambert factor = max(cosine, 0)

Поясним эти три строчки кода.
Сначала вычисляют вектор света путем вычитания позиции объекта из положения источника света.
light vector = light position - object position

Тогда мы сможем рассчитать косинус угла между вектором света и нормалью, найдя скалярное произведение между нормалью к поверхности и нормированного вектора света. Нормировать вектор света – значит изменить его длину так, чтобы она равнялась единице, но направление вектора при этом остается неизменным. Модуль нормали к поверхности у нас уже равен единице. Определив скалярное произведение двух нормированных векторов, найдем косинус угла между ними.
cosine = dot product(object normal, normalize(light vector))

Поскольку скалярное произведение может иметь диапазон от -1 до 1, мы должны ограничить его в диапазоне от 0 до 1. Функция max(х, у) дает «х», если «х» больше «у» и наоборот.
lambert factor = max(cosine, 0)

Чтобы было понятно, рассмотрим на примере.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 5

Пусть наш источник света находится в т.S (0,10,10), а мы хотим вычислить освещенность горизонтальной поверхности (это может быть единственный пиксель), у которого нормаль смотрит вертикально вверх (0, 1, 0). Тогда вектор OS (light vector) и вектор нормали (object normal)
light vector = {0, 10, 10} - {0, 0, 0} = {0, 10, 10}
object normal = {0, 1, 0}

Найдем длину вектора OS
light vector length = square root(0*0 + 10*10 + 10*10) = square root(200) = 14.14

Нормализуем его, т.е. сделаем так, что его длина станет равной единице.
normalized light vector = {0, 10/14.14, 10/14.14} = {0, 0.707, 0.707}

Затем мы вычисляем скалярное произведение:
dot product({0, 1, 0}, {0, 0.707, 0.707}) = (0 * 0) + (1 * 0.707) + (0 * 0.707) = 0 + 0.707 + 0 = 0.707

Кто подзабыл математику, напомню, что скалярное произведение двух векторов равно произведению их модулей, помноженному на косинус угла между ними. Если модули у нас равны по единице, то скалярное произведение равно косинусу.
Есть маленькая проблема, которая заключается в том, что в принципе косинус может принимать и отрицательные значения, если угол альфа от 90 до 180 градусов. Поэтому мы ограничиваем диапазон значений коэффициента Ламберта от 0 до 1.
lambert factor = max(0.707, 0) = 0.707

Отраженное или бликовое освещение
Отраженное освещение (specular lighting) — это свет от источника, отраженный после попадания в заданную точку. Отраженный свет виден только, если он попадает в камеру. Поэтому здесь учитываются как нормали, так и положение камеры.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 6

Программная реализация блика
При расчете диффузного освещения был определен вектор единичной длины, проходящий из освещаемой точки к источнику света normalized light vector. Чтобы определить вектор падающего света от источника света к точке на поверхности, нужно просто поменять знак на минус.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 7

Для вычисления отраженного вектора (направление OS1) в GLSL существует специальная функция reflect:
reflectvector = reflect(- normalized light vector, object normal);

Координаты камеры передаём во фрагментный шейдер, как униформу u_camera:
uniform vec3 u_camera;

Теперь вычислим вектор OК, указывающий из точки освещения на камеру и нормализуем его:
lookvector = normalize(u_camera - object position);

Далее нам нужно вычислить косинус угла бета между отраженным вектором OS1 и направлением на камеру ОК. Раньше мы уже показали, что косинус угла это скалярное произведение двух единичных векторов: dot(lookvector,reflectvector). Чем меньше значение угла бета, тем ярче будет блик.
Также, как для lambert factor, отсекаем отрицательные значения скалярного произведения при помощи функции max:
max(dot(lookvector,reflectvector),0.0)

Размер самого блика можно регулировать при помощи параметра блеска, для этого вычисленное значение скалярного произведения нужно возвести в степень блеска. Для возведения в степень в GLSL предусмотрена функция pow. Обычно значение блеска берут несколько десятков единиц. При увеличении блеска размер блика уменьшается, но яркость его увеличивается. И наоборот, чем меньше блеск, тем больше размер блика, но яркость его становится меньше.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 8

Пусть, для примера, блеск будет равен 30. Тогда возведем полученное скалярное произведение в степень 30:
pow( max(dot(lookvector,reflectvector),0.0), 30.0 )

Умножим полученное значение на коэффициент зеркального освещения k_specular и получим яркость зеркального освещения для данного пикселя:
float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 30.0 );

Суммарное освещение
Чтобы смоделировать освещение объекта в реальной жизни, нужно суммировать в определенных пропорциях разные типы освещения объекта. Например, чтобы получить цвет пикселя с учетом освещения нужно сложить фоновую, диффузную и зеркальную части освещения ambient + diffuse + specular и умножить на вектор цвета пикселя, полученного при интерполяции цветов вершин v_color.
gl_FragColor = (ambient+diffuse+specular)*v_color;

Если мы не хотим разукрашивать пиксели интерполированными цветами вершин достаточно определить вектор белого цвета:
vec4 one=vec4(1.0,1.0,1.0,1.0);

и умножить на него яркость освещения:
gl_FragColor = (ambient+diffuse+specular)*one;

Вы можете оценить, как меняется картинка, если взять сначала ambient = 1, diffuse = 0, specular = 0. Картинка в центре для случая ambient = 0, diffuse = 1, specular = 0, соответственно картинка справа для таких значений ambient = 0, diffuse = 0, specular = 1.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 9

В этой строке происходит микширование освещенности и цвета.
gl_FragColor = mix(lightColor,v_color,0.6)

Посмотрите, как выглядит картинка (слева), если коэффициент микширования сделать 0, фактически мы видим распределение по яркости света. Если коэффициент сделать равным 1, то мы увидим чистые цвета.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 10

Прежде чем перейти к исходникам, маленькие комментарии к ним.
1. Еще на прошлом уроке вы заметили, что в файле Манифеста появились такие строки

<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

Теперь Google Play не будет показывать наше приложение на устройствах, которые не поддерживают OpenGL ES.
2. Во втором уроке мы рисовали треугольники, используя только метод GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
В этом уроке появился новый для нас метод GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
С помощью его удобней рисовать четырехугольники и сложные рельефы. Именно его мы применили для прорисовки моря, неба и лодки. Хороший урок по применению этого метода вы найдете здесь
http://www.learnopengles.com/tag/triangle-strips/

3. Наш кораблик нарисован немного выступающим по оси 0Z. Изменяя положение камеры, можно увидеть это смещение, работу матриц и изменение освещения поверхности. Слева камеру подняли на 5 единиц по 0У, справа – сместили на 4 единицы по 0Х.
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 11
Остальные пояснения вы найдете в самом коде.
Рекомендую сначала запустить проект, а потом играться и экспериментировать с разными коэффициентами.

Создаем проект
Создайте в Android Studio проект OpenGLESLighting
Главную активити назовите Opengles3Activity.
Создайте еще три java файла:
MyClassSurfaceView.java
MyClassRenderer.java
Shader.java
Изучаем OpenGL ES2 для Android Урок №3. Освещение - 12

Скопируйте код и вставьте в файлы вместо того, что сгенерировалось автоматически.
Наслаждайтесь картинкой.:)
Если в статье закрались ошибки, рад буду их исправить и научиться у вас.
Всего хорошего!

Исходники кода

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.adc2017gmail.opengleslighting"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="19" />
    <uses-feature android:glEsVersion="0x00020000" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".Opengles3Activity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Opengles3Activity.java

public class Opengles3Activity extends Activity {
    // создадим ссылку на экземпляр нашего класса MyClassSurfaceView
    private MyClassSurfaceView mGLSurfaceView;

    // переопределим метод
    // onCreate
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

//создадим экземпляр нашего класса MyClassSurfaceView
        mGLSurfaceView = new MyClassSurfaceView(this);

//вызовем экземпляр нашего класса MyClassSurfaceView
        setContentView(mGLSurfaceView);
// на экране появится поверхность для рисования в OpenGl ES
    }
    @Override
    protected void onPause() {
        super.onPause();
        mGLSurfaceView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGLSurfaceView.onResume();
    }
}

MyClassSurfaceView.java

package com.adc2017gmail.opengleslighting;

import android.content.Context;
import android.opengl.GLSurfaceView;

//Опишем наш класс MyClassSurfaceView расширяющий GLSurfaceView
public class MyClassSurfaceView extends GLSurfaceView{
    //создадим ссылку для хранения экземпляра нашего класса рендерера
    private MyClassRenderer renderer;

    // конструктор
    public MyClassSurfaceView(Context context) {
        // вызовем конструктор родительского класса GLSurfaceView
        super(context);
        setEGLContextClientVersion(2);
        // создадим экземпляр нашего класса MyClassRenderer
        renderer = new MyClassRenderer(context);
        // запускаем рендерер
        setRenderer(renderer);
        // установим режим циклического запуска метода onDrawFrame
        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
        // при этом запускается отдельный поток
        // в котором циклически вызывается метод onDrawFrame
        // т.е. бесконечно происходит перерисовка кадров
    }
}

MyClassRenderer.java

package com.adc2017gmail.opengleslighting;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;


public class MyClassRenderer implements GLSurfaceView.Renderer{



    // интерфейс GLSurfaceView.Renderer содержит
    // три метода onDrawFrame, onSurfaceChanged, onSurfaceCreated
    // которые должны быть переопределены
    // текущий контекст
    private Context context;
    //координаты камеры
    private float xСamera, yCamera, zCamera;
    //координаты источника света
    private float xLightPosition, yLightPosition, zLightPosition;
    //матрицы

    private float[] modelMatrix= new float[16];
    private float[] viewMatrix= new float[16];
    private float[] modelViewMatrix= new float[16];;
    private float[] projectionMatrix= new float[16];;
    private float[] modelViewProjectionMatrix= new float[16];;
    //буфер для координат вершин
    private final FloatBuffer vertexBuffer;
    private final FloatBuffer vertexBuffer1;
    private final FloatBuffer vertexBuffer2;
    private final FloatBuffer vertexBuffer3;
    private final FloatBuffer vertexBuffer4;
    //буфер для нормалей вершин
    private FloatBuffer normalBuffer;
    private FloatBuffer normalBuffer1;
    //буфер для цветов вершин
    private final FloatBuffer colorBuffer;
    private final FloatBuffer colorBuffer1;
    private final FloatBuffer colorBuffer2;
    private final FloatBuffer colorBuffer4;
    //шейдерный объект
    private Shader mShader;
    private Shader mShader1;
    private Shader mShader2;
    private Shader mShader3;
    private Shader mShader4;


    //конструктор
    public MyClassRenderer(Context context) {
        // запомним контекст
        // он нам понадобится в будущем для загрузки текстур
        this.context=context;

        //координаты точечного источника света
        xLightPosition=0.3f;
        yLightPosition=0.2f;
        zLightPosition=0.5f;


        //мы не будем двигать объекты
        //поэтому сбрасываем модельную матрицу на единичную
       Matrix.setIdentityM(modelMatrix, 0);
        //координаты камеры
        xСamera=0.0f;
        yCamera=0.0f;
        zCamera=3.0f;
        //пусть камера смотрит на начало координат
        //и верх у камеры будет вдоль оси Y
        //зная координаты камеры получаем матрицу вида
        Matrix.setLookAtM(
                viewMatrix, 0, xСamera, yCamera, zCamera, 0, 0, 0, 0, 1, 0);
        // умножая матрицу вида на матрицу модели
        // получаем матрицу модели-вида
        Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
        //координаты вершины 1
        float x1=-1;
        float y1=-0.35f;
        float z1=0.0f;
        //координаты вершины 2
        float x2=-1;
        float y2=-1.5f;
        float z2=0.0f;
        //координаты вершины 3
        float x3=1;
        float y3=-0.35f;
        float z3=0.0f;
        //координаты вершины 4
        float x4=1;
        float y4=-1.5f;
        float z4=0.0f;
        //запишем координаты всех вершин в единый массив
        float vertexArray [] = {x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4};
        //coordinates for sky
        float vertexArray1 [] = {-1.0f,1.5f,0.0f,  -1.0f,-0.35f,0.0f, 1.0f,1.5f,0.0f, 1.0f,-0.35f,0};
        //coordinates for main sail
        float vertexArray2 [] = {-0.5f,-0.45f,0.4f,  0.0f,-0.45f,0.4f, 0.0f,0.5f,0.4f};
    //coordinates for small sail
        float vertexArray3 [] = {0.05f,-0.45f,0.4f, 0.22f,-0.5f,0.4f, 0.0f,0.25f,0.4f };
        //coordinates for boat
        float vertexArray4 [] = {-0.5f,-0.5f,0.4f,  -0.5f,-0.6f,0.4f, 0.22f,-0.5f,0.4f,  0.18f,-0.6f,0.4f};

        //создадим буфер для хранения координат вершин
        ByteBuffer bvertex = ByteBuffer.allocateDirect(vertexArray.length*4);
        bvertex.order(ByteOrder.nativeOrder());
        vertexBuffer = bvertex.asFloatBuffer();
        vertexBuffer.position(0);

        ByteBuffer bvertex1 = ByteBuffer.allocateDirect(vertexArray1.length*4);
        bvertex1.order(ByteOrder.nativeOrder());
        vertexBuffer1 = bvertex1.asFloatBuffer();
        vertexBuffer1.position(0);

        ByteBuffer bvertex2 = ByteBuffer.allocateDirect(vertexArray2.length*4);
        bvertex2.order(ByteOrder.nativeOrder());
        vertexBuffer2 = bvertex2.asFloatBuffer();
        vertexBuffer2.position(0);

        ByteBuffer bvertex3 = ByteBuffer.allocateDirect(vertexArray3.length*4);
        bvertex3.order(ByteOrder.nativeOrder());
        vertexBuffer3 = bvertex3.asFloatBuffer();
        vertexBuffer3.position(0);

        ByteBuffer bvertex4 = ByteBuffer.allocateDirect(vertexArray4.length*4);
        bvertex4.order(ByteOrder.nativeOrder());
        vertexBuffer4 = bvertex4.asFloatBuffer();
        vertexBuffer4.position(0);

        //перепишем координаты вершин из массива в буфер
        vertexBuffer.put(vertexArray);
        vertexBuffer.position(0);
        vertexBuffer1.put(vertexArray1);
        vertexBuffer1.position(0);
        vertexBuffer2.put(vertexArray2);
        vertexBuffer2.position(0);
        vertexBuffer3.put(vertexArray3);
        vertexBuffer3.position(0);
        vertexBuffer4.put(vertexArray4);
        vertexBuffer4.position(0);
        //вектор нормали перпендикулярен плоскости квадрата
        //и направлен вдоль оси Z
        float nx=0;
        float ny=0;
        float nz=1;
        //нормаль одинакова для всех вершин квадрата,
        //поэтому переписываем координаты вектора нормали в массив 4 раза
        float normalArray [] ={nx, ny, nz,   nx, ny, nz,   nx, ny, nz,   nx, ny, nz};
        float normalArray1 [] ={0, 0, 1,   0, 0, 1,   0, 0, 1,   0, 0, 1};
        //создадим буфер для хранения координат векторов нормали
        ByteBuffer bnormal = ByteBuffer.allocateDirect(normalArray.length*4);
        bnormal.order(ByteOrder.nativeOrder());
        normalBuffer = bnormal.asFloatBuffer();
        normalBuffer.position(0);

        //перепишем координаты нормалей из массива в буфер
        normalBuffer.put(normalArray);
        normalBuffer.position(0);

        //разукрасим вершины квадрата, зададим цвета для вершин
        float red1=0;
        float green1=1;
        float blue1=1;
        //цвет второй вершины 
        float red2=0;
        float green2=0;
        float blue2=1;
        //цвет третьей вершины 
        float red3=0;
        float green3=1;
        float blue3=1;
        //цвет четвертой вершины 
        float red4=0;
        float green4=0;
        float blue4=1;
        //перепишем цвета вершин в массив
        //четвертый компонент цвета (альфу) примем равным единице
        float colorArray [] = {
                red1, green1, blue1, 1,
                red2, green2, blue2, 1,
                red3, green3, blue3, 1,
                red4, green4, blue4, 1,
        };
        float colorArray1[] = {
                0.2f, 0.2f, 0.8f, 1,
                0.5f, 0.5f, 1, 1,
                0.2f, 0.2f, 0.8f, 1,
                0.5f, 0.5f, 1, 1,
        };
        float colorArray2[] = {
                1, 0.1f, 0.1f, 1,
                1, 1, 1, 1,
                1, 0.1f, 0.1f, 1,
        };

        float colorArray4[] = {
                1, 1, 1, 1,
                0.2f, 0.2f, 0.2f, 1,
                1, 1, 1, 1,
                0.2f, 0.2f, 0.2f, 1,
        };

        //создадим буфер для хранения цветов вершин
        ByteBuffer bcolor = ByteBuffer.allocateDirect(colorArray.length*4);
        bcolor.order(ByteOrder.nativeOrder());
        colorBuffer = bcolor.asFloatBuffer();
        colorBuffer.position(0);
        //перепишем цвета вершин из массива в буфер
        colorBuffer.put(colorArray);
        colorBuffer.position(0);

        ByteBuffer bcolor1 = ByteBuffer.allocateDirect(colorArray1.length*4);
        bcolor1.order(ByteOrder.nativeOrder());
        colorBuffer1 = bcolor1.asFloatBuffer();
        colorBuffer1.position(0);
        colorBuffer1.put(colorArray1);
        colorBuffer1.position(0);

        ByteBuffer bcolor2 = ByteBuffer.allocateDirect(colorArray1.length*4);
        bcolor2.order(ByteOrder.nativeOrder());
        colorBuffer2 = bcolor2.asFloatBuffer();
        colorBuffer2.position(0);
        colorBuffer2.put(colorArray2);
        colorBuffer2.position(0);

        ByteBuffer bcolor4 = ByteBuffer.allocateDirect(colorArray4.length*4);
        bcolor4.order(ByteOrder.nativeOrder());
        colorBuffer4 = bcolor4.asFloatBuffer();
        colorBuffer4.position(0);
        colorBuffer4.put(colorArray4);
        colorBuffer4.position(0);

    }//конец конструктора

    //метод, который срабатывает при изменении размеров экрана
    //в нем мы получим матрицу проекции и матрицу модели-вида-проекции
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        // устанавливаем glViewport
        GLES20.glViewport(0, 0, width, height);
        float ratio = (float) width / height;
        float k=0.055f;
        float left = -k*ratio;
        float right = k*ratio;
        float bottom = -k;
        float top = k;
        float near = 0.1f;
        float far = 10.0f;
        // получаем матрицу проекции
        Matrix.frustumM(projectionMatrix, 0, left, right, bottom, top, near, far);
        // матрица проекции изменилась,
        // поэтому нужно пересчитать матрицу модели-вида-проекции
        Matrix.multiplyMM(
                modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0);
    }

    //метод, который срабатывает при создании экрана
    //здесь мы создаем шейдерный объект
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        //включаем тест глубины
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        //включаем отсечение невидимых граней
        GLES20.glEnable(GLES20.GL_CULL_FACE);
        //включаем сглаживание текстур, это пригодится в будущем
        GLES20.glHint(
                GLES20.GL_GENERATE_MIPMAP_HINT, GLES20.GL_NICEST);
        //записываем код вершинного шейдера в виде строки
        String vertexShaderCode=
                "uniform mat4 u_modelViewProjectionMatrix;"+
                        "attribute vec3 a_vertex;"+
                        "attribute vec3 a_normal;"+
                        "attribute vec4 a_color;"+
                        "varying vec3 v_vertex;"+
                        "varying vec3 v_normal;"+
                        "varying vec4 v_color;"+
                        "void main() {"+
                        "v_vertex=a_vertex;"+
                        "vec3 n_normal=normalize(a_normal);"+
                        "v_normal=n_normal;"+
                        "v_color=a_color;"+
                        "gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);"+
                        "}";
        //записываем код фрагментного шейдера в виде строки
        String fragmentShaderCode=
                "precision mediump float;"+
                        "uniform vec3 u_camera;"+
                        "uniform vec3 u_lightPosition;"+
                        "varying vec3 v_vertex;"+
                        "varying vec3 v_normal;"+
                        "varying vec4 v_color;"+
                        "void main() {"+
                        "vec3 n_normal=normalize(v_normal);"+
                        "vec3 lightvector = normalize(u_lightPosition - v_vertex);"+
                        "vec3 lookvector = normalize(u_camera - v_vertex);"+
                        "float ambient=0.2;"+
                        "float k_diffuse=0.3;"+
                        "float k_specular=0.5;"+
                        "float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);"+
                        "vec3 reflectvector = reflect(-lightvector, n_normal);"+
                        "float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );"+
                        "vec4 one=vec4(1.0,1.0,1.0,1.0);"+
                        "vec4 lightColor = (ambient+diffuse+specular)*one;"+
                        "gl_FragColor = mix(lightColor,v_color,0.6);"+
                        "}";
        //создадим шейдерный объект
        mShader=new Shader(vertexShaderCode, fragmentShaderCode);
        //свяжем буфер вершин с атрибутом a_vertex в вершинном шейдере
        mShader.linkVertexBuffer(vertexBuffer);
        //свяжем буфер нормалей с атрибутом a_normal в вершинном шейдере
        mShader.linkNormalBuffer(normalBuffer);
        //свяжем буфер цветов с атрибутом a_color в вершинном шейдере
        mShader.linkColorBuffer(colorBuffer);
        //связь атрибутов с буферами сохраняется до тех пор,
        //пока не будет уничтожен шейдерный объект
      
        mShader1 = new Shader(vertexShaderCode, fragmentShaderCode);
        mShader1.linkVertexBuffer(vertexBuffer1);
        mShader1.linkNormalBuffer(normalBuffer);
        mShader1.linkColorBuffer(colorBuffer1);

        mShader2 = new Shader(vertexShaderCode, fragmentShaderCode);
        mShader2.linkVertexBuffer(vertexBuffer2);
        mShader2.linkNormalBuffer(normalBuffer);
        mShader2.linkColorBuffer(colorBuffer2);

        mShader3 = new Shader(vertexShaderCode, fragmentShaderCode);
        mShader3.linkVertexBuffer(vertexBuffer3);
        mShader3.linkNormalBuffer(normalBuffer);
        mShader3.linkColorBuffer(colorBuffer2);

        mShader4 = new Shader(vertexShaderCode, fragmentShaderCode);
        mShader4.linkVertexBuffer(vertexBuffer4);
        mShader4.linkNormalBuffer(normalBuffer);
        mShader4.linkColorBuffer(colorBuffer4);

    }

    //метод, в котором выполняется рисование кадра
    public void onDrawFrame(GL10 unused) {
        //очищаем кадр
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        //передаем в шейдерный объект матрицу модели-вида-проекции
        mShader.useProgram();
        mShader.linkVertexBuffer(vertexBuffer);
        mShader.linkColorBuffer(colorBuffer);
        mShader.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
        mShader.linkCamera(xСamera, yCamera, zCamera);
        mShader.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        mShader1.useProgram();
        mShader1.linkVertexBuffer(vertexBuffer1);
        mShader1.linkColorBuffer(colorBuffer1);
        mShader1.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
        mShader1.linkCamera(xСamera, yCamera, zCamera);
        mShader1.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        mShader2.useProgram();
        mShader2.linkVertexBuffer(vertexBuffer2);
        mShader2.linkColorBuffer(colorBuffer2);
        mShader2.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
        mShader2.linkCamera(xСamera, yCamera, zCamera);
        mShader2.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

        mShader3.useProgram();
        mShader3.linkVertexBuffer(vertexBuffer3);
        mShader3.linkColorBuffer(colorBuffer2);
        mShader3.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
        mShader3.linkCamera(xСamera, yCamera, zCamera);
        mShader3.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

        mShader4.useProgram();
        mShader4.linkVertexBuffer(vertexBuffer4);
        mShader4.linkColorBuffer(colorBuffer4);
        mShader4.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
        mShader4.linkCamera(xСamera, yCamera, zCamera);
        mShader4.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

}//конец класса

Shader.java

package com.adc2017gmail.opengleslighting;

import android.opengl.GLES20;
import java.nio.FloatBuffer;

public class Shader {
    //будем хранить ссылку на шейдерную программу
    //внутри класса как локальное поле
    private int program_Handle;

    //при создании объекта класса передаем в конструктор
    //строки кода вершинного и фрагментного шейдера
    public Shader(String vertexShaderCode, String fragmentShaderCode){
        //вызываем метод, создающий шейдерную программу
        //при этом заполняется поле program_Handle
        createProgram(vertexShaderCode, fragmentShaderCode);
    }
    // метод, который создает шейдерную программу, вызывается в конструкторе
    private void createProgram(String vertexShaderCode, String fragmentShaderCode){
        //получаем ссылку на вершинный шейдер
        int vertexShader_Handle =
                GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        //присоединяем к вершинному шейдеру его код
        GLES20.glShaderSource(vertexShader_Handle, vertexShaderCode);
        //компилируем вершинный шейдер
        GLES20.glCompileShader(vertexShader_Handle);
        //получаем ссылку на фрагментный шейдер
        int fragmentShader_Handle =
                GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        //присоединяем к фрагментному шейдеру его код
        GLES20.glShaderSource(fragmentShader_Handle, fragmentShaderCode);
        //компилируем фрагментный шейдер
        GLES20.glCompileShader(fragmentShader_Handle);
        //получаем ссылку на шейдерную программу
        program_Handle = GLES20.glCreateProgram();
        //присоединяем к шейдерной программе вершинный шейдер
        GLES20.glAttachShader(program_Handle, vertexShader_Handle);
        //присоединяем к шейдерной программе фрагментный шейдер
        GLES20.glAttachShader(program_Handle, fragmentShader_Handle);
        //компилируем шейдерную программу
        GLES20.glLinkProgram(program_Handle);
    }

    //метод, который связывает
    //буфер координат вершин vertexBuffer с атрибутом a_vertex
    public void linkVertexBuffer(FloatBuffer vertexBuffer){
        //устанавливаем активную программу
        GLES20.glUseProgram(program_Handle);
        //получаем ссылку на атрибут a_vertex
        int a_vertex_Handle = GLES20.glGetAttribLocation(program_Handle, "a_vertex");
        //включаем использование атрибута a_vertex
        GLES20.glEnableVertexAttribArray(a_vertex_Handle);
        //связываем буфер координат вершин vertexBuffer с атрибутом a_vertex
        GLES20.glVertexAttribPointer(
                a_vertex_Handle, 3, GLES20.GL_FLOAT, false, 0,vertexBuffer);
    }

    //метод, который связывает
    //буфер координат векторов нормалей normalBuffer с атрибутом a_normal
    public void linkNormalBuffer(FloatBuffer normalBuffer){
        //устанавливаем активную программу
        GLES20.glUseProgram(program_Handle);
        //получаем ссылку на атрибут a_normal
        int a_normal_Handle = GLES20.glGetAttribLocation(program_Handle, "a_normal");
        //включаем использование атрибута a_normal
        GLES20.glEnableVertexAttribArray(a_normal_Handle);
        //связываем буфер нормалей normalBuffer с атрибутом a_normal
        GLES20.glVertexAttribPointer(
                a_normal_Handle, 3, GLES20.GL_FLOAT, false, 0,normalBuffer);
    }

    //метод, который связывает
    //буфер цветов вершин colorBuffer с атрибутом a_color
    public void linkColorBuffer(FloatBuffer colorBuffer){
        //устанавливаем активную программу
        GLES20.glUseProgram(program_Handle);
        //получаем ссылку на атрибут a_color
        int a_color_Handle = GLES20.glGetAttribLocation(program_Handle, "a_color");
        //включаем использование атрибута a_color
        GLES20.glEnableVertexAttribArray(a_color_Handle);
        //связываем буфер нормалей colorBuffer с атрибутом a_color
        GLES20.glVertexAttribPointer(
                a_color_Handle, 4, GLES20.GL_FLOAT, false, 0, colorBuffer);
    }

    //метод, который связывает матрицу модели-вида-проекции
    // modelViewProjectionMatrix с униформой u_modelViewProjectionMatrix
    public void linkModelViewProjectionMatrix(float [] modelViewProjectionMatrix){
        //устанавливаем активную программу
        GLES20.glUseProgram(program_Handle);
        //получаем ссылку на униформу u_modelViewProjectionMatrix
        int u_modelViewProjectionMatrix_Handle =
                GLES20.glGetUniformLocation(program_Handle, "u_modelViewProjectionMatrix");
        //связываем массив modelViewProjectionMatrix
        //с униформой u_modelViewProjectionMatrix
        GLES20.glUniformMatrix4fv(
                u_modelViewProjectionMatrix_Handle, 1, false, modelViewProjectionMatrix, 0);
    }

    // метод, который связывает координаты камеры с униформой u_camera
    public void linkCamera (float xCamera, float yCamera, float zCamera){
        //устанавливаем активную программу
        GLES20.glUseProgram(program_Handle);
        //получаем ссылку на униформу u_camera
        int u_camera_Handle=GLES20.glGetUniformLocation(program_Handle, "u_camera");
        // связываем координаты камеры с униформой u_camera
        GLES20.glUniform3f(u_camera_Handle, xCamera, yCamera, zCamera);
    }

    // метод, который связывает координаты источника света
    // с униформой u_lightPosition
    public void linkLightSource (float xLightPosition, float yLightPosition, float zLightPosition){
        //устанавливаем активную программу
        GLES20.glUseProgram(program_Handle);
        //получаем ссылку на униформу u_lightPosition
        int u_lightPosition_Handle=GLES20.glGetUniformLocation(program_Handle, "u_lightPosition");
        // связываем координаты источника света с униформой u_lightPosition
        GLES20.glUniform3f(u_lightPosition_Handle, xLightPosition, yLightPosition, zLightPosition);
    }

    // метод, который делает шейдерную программу данного класса активной
    public void useProgram(){
        GLES20.glUseProgram(program_Handle);
    }
    // конец класса
}

Источники:
http://andmonahov.blogspot.com/2012/10/opengl-es-20.html [1]
http://www.learnopengles.com/android-lesson-two-ambient-and-diffuse-lighting/ [2]
http://www.learnopengles.com/tag/triangle-strips/ [4]
http://eax.me/opengl-lighting/ [5]
http://www.john-chapman.net/content.php?id=3 [6]

Автор: AlwaysDream

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/165986

Ссылки в тексте:

[1] http://andmonahov.blogspot.com/2012/10/opengl-es-20.html : http://andmonahov.blogspot.com/2012/10/opengl-es-20.html

[2] http://www.learnopengles.com/android-lesson-two-ambient-and-diffuse-lighting/ : http://www.learnopengles.com/android-lesson-two-ambient-and-diffuse-lighting/

[3] мозг: http://www.braintools.ru

[4] http://www.learnopengles.com/tag/triangle-strips/ : http://www.learnopengles.com/tag/triangle-strips/

[5] http://eax.me/opengl-lighting/ : http://eax.me/opengl-lighting/

[6] http://www.john-chapman.net/content.php?id=3 : http://www.john-chapman.net/content.php?id=3

[7] Источник: https://habrahabr.ru/post/306928/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best