Журнал LinuxFormat - перейти на главную

LXF165-166: Android

Материал из Linuxformat
Перейти к: навигация, поиск

Android

Программирование: Работа с графикой при помощи библиотеки OpenGL

Содержание

Соз­да­ем объ­ем с OpenGL

Джуль­ет­та Кемп еще пом­нит вре­ме­на, ко­гда в фо­то­ап­па­ра­тах бы­ла на­стоя­щая плен­ка. Сей­час она де­ла­ет гораздо поболее сним­ков, чем то­гда.

(thumbnail)
Наш эксперт Джуль­ет­та Кемп по­ка­зы­ва­ет, как «на­тя­нуть» эк­ран те­ле­фо­на с Android на лю­без­ный мно­гим вра­щаю­щий­ся куб.

OpenGL (Open Graphics Library – от­кры­тая гра­фи­че­­ская биб­лио­те­ка) – ши­ро­ко ис­поль­зуе­мая спе­ци­фи­ка­ция кросс­плат­фор­мен­но­го API для соз­дания дву­мер­ной и трех­мер­ной гра­фи­ки. Пу­тем вве­дения стан­дарт­но­го на­бо­ра воз­мож­но­стей и ин­тер­фей­са она уп­ро­ща­ет ра­бо­ту с раз­лич­ны­ми ап­па­рат­ны­ми плат­фор­ма­ми и 3D-уско­ри­те­ля­ми для раз­ра­бот­чи­ков. Су­ще­ст­ву­ют при­вяз­ки и реа­ли­за­ции OpenGL для ог­ром­но­го ко­ли­че­­ст­ва язы­ков про­грам­ми­ро­вания, и, к сча­стью, в Android API есть встро­ен­ная под­держ­ка OpenGL. С пер­во­го ре­ли­за Android под­дер­жи­ва­ют­ся OpenGL ES 1.0 и 1.1, с Android 2.2 (API уров­ня 8) под­держи­ва­ет­ся OpenGL ES 2.0.

На этом уро­ке мы бу­дем поль­зо­вать­ся OpenGL ES 2.0, так как се­го­дня поч­ти на 85 % уст­ройств ис­поль­зу­ет­ся Android 2.2 и вы­ше, но ес­ли вам важ­на об­рат­ная со­вмес­ти­мость с бо­лее ста­ры­ми вер­сия­ми, мож­но про­ве­рять уро­вень API в ко­де и при необ­хо­ди­мо­сти ис­поль­зо­вать OpenGL ES 1.1.

В этой ста­тье я бу­ду пред­по­ла­гать, что вы никогда не ра­бо­тали с OpenGL или с ее реа­ли­за­ци­ей в Android API, и объ­яс­ню неко­то­рые основ­ные идеи. Мы начнем со ста­тич­ной дву­мер­ной фи­гу­ры, за­тем пе­рей­дем к вра­щаю­ще­му­ся ку­бу. На сле­дую­щем уроке мы про­дол­жим ра­бо­ту с этим при­ме­ром, до­ба­вив в него ра­бо­ту с сен­сор­ным эк­ра­ном.

На­чи­на­ем зна­ком­ст­во с OpenGL

Под­держ­ка OpenGL встрое­на в Android по умол­чанию, и что­бы восполь­зо­вать­ся этим в сво­ем про­ек­те, ниче­го осо­бен­но­го де­лать не нуж­но. Од­на­ко надо до­ба­вить в манифест стро­ку (по­мес­ти­те ее по­сле uses-sdk), го­во­ря­щую о том, что для за­пуска на уст­рой­ст­ве при­ло­жение долж­но под­дер­жи­вать OpenGL2.0:

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

Ос­нов­ное За­ня­тие [Activity] при­ло­же­ния не тре­бу­ет по­яс­не­ний:

public class CubeInSpaceActivity extends Activity {

private GLSurfaceView glView;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

glView = new GLSurfaceView(this);

glView.setEGLContextClientVersion(2);

glView.setRenderer(new CISGLRenderer());

setContentView(glView);

}

@Override

protected void onResume() {

super.onResume();

glView.onResume();

}

@Override

protected void onPause() {

super.onPause();

glView.onPause();

}

}

onCreate() соз­да­ет ис­поль­зуе­мый да­лее GLSurfaceView и кон­текст OpenGL ES 2.0 для него, и на­страи­ва­ет Renderer, ко­то­рый мы сей­час напишем. Для приоста­нов­ки и во­зоб­нов­ления при­ло­жения важ­но ис­поль­зо­вать ме­то­ды Пред­став­ления onPause() и onResume(). OpenGL ве­ли­ко­ле­пен, но по­треб­ля­ет мно­го ре­сур­сов, и нам неза­чем, что­бы он про­дол­жал ра­бо­тать, по­ка поль­зо­ва­тель не смот­рит на на­шу ши­кар­ную гра­фи­ку.

Боль­шая часть ра­бо­ты вы­пол­ня­ет­ся клас­сом CISGLRenderer; вот его пер­вая вер­сия, в ней за­да­ет­ся толь­ко цвет фо­на:

public class CISGLRenderer implements GLSurfaceView.

Renderer {

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

GLES20.glClearColor(61f/255, 89f/255, 171f/255, 1.0f);

}

public void onDrawFrame(GL10 gl) {

GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

}

public void onSurfaceChanged(GL10 gl, int width, int height) {

GLES20.glViewport(0, 0, width, height);

}

}

(thumbnail)
Дву­мер­ная сис­те­ма ко­ор­ди­нат OpenGL. В трех­мер­ной сис­те­ме ось Z будет пер­пен­ди­ку­ляр­на эк­ра­ну.

Для реа­ли­за­ции GLSurfaceView.Renderer, нуж­но реа­ли­зо­вать три сле­дую­щих ме­то­да. onSurfaceCreated() вы­зы­ва­ет­ся один раз при соз­дании и за­да­ет па­ра­мет­ры объ­ек­та; onDrawFrame() вы­зы­ва­ет­ся при ка­ж­дой пе­ре­ри­сов­ке кад­ра (по­сто­ян­но); а onSurfaceChanged() вы­зы­ва­ет­ся при гео­мет­ри­че­­ских из­менениях в Пред­став­лении (ча­ще все­го при из­менении ори­ен­та­ции эк­ра­на).

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

glClearColor уста­нав­ли­ва­ет цвет фо­на, ко­то­рый бу­дет при­ме­нять­ся ка­ж­дый раз при вы­зо­ве glClear – здесь это при­ят­ный ко­баль­то­во-синий. Па­ра­мет­ры ме­то­да – крас­ная (R), зе­ле­ная (G), си­няя (B) со­став­ляю­щая и аль­фа (она управ­ля­ет про­зрач­но­стью), ка­ж­дый от 0 до 1. Для пре­об­ра­зо­вания из стан­дарт­ных 255 RGB мож­но, как и я, раз­де­лить стан­дарт­ное зна­чение на 255, или раз­де­лить вруч­ную и под­ста­вить сю­да ре­зуль­тат.

Един­ст­вен­ный ар­гу­мент glClear() – би­то­вая мас­ка очи­ст­ки бу­фе­ра: здесь это цвет и глу­би­на (под­робнее о бу­фе­ре глу­би­ны поз­же, когда дой­дем до трех­мер­ных фи­гур).

На­конец, в ме­то­де onSurfaceChanged() мы за­да­ем раз­ме­ры пря­мо­уголь­ной об­лас­ти про­смот­ра в со­от­вет­ст­вии с ши­ри­ной и вы­со­той по­верх­но­сти Пред­став­ления.

Но это толь­ко фон. Да­вай­те оп­ре­де­лим квад­рат, ко­то­рый на­ри­су­ем на нем, с по­мо­щью клас­са Square:

public class Square {

private FloatBuffer squareBuffer;

float vertices[] = {

-0.5f, -0.5f, 0.0f, //bottom left

0.5f, -0.5f, 0.0f, //bottom right

-0.5f, 0.5f, 0.0f, //top left

0.5f, 0.5f, 0.0f //top right

};

public Square() {

ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);

vbb.order(ByteOrder.nativeOrder());

squareBuffer = vbb.asFloatBuffer();

squareBuffer.put(vertices);

squareBuffer.position(0);

}

}

В мас­си­ве vertices за­да­ет­ся рас­по­ло­жение ка­ж­до­го уг­ла (вер­ши­ны) в ко­ор­ди­на­тах X, Y и Z (наш квад­рат дву­мер­ный, по­это­му ко­ор­ди­на­та Z вез­де рав­на ну­лю). Точ­ка от­сче­та сис­те­мы ко­ор­ди­нат OpenGL на­хо­дит­ся в цен­тре эк­ра­на, как по­ка­за­но на ри­сун­ке (Z по­ло­жи­тель­ные) и ухо­дит вглубь эк­ра­на (Z от­ри­ца­тель­ные).

Соз­да­ем ByteBuffer

При за­дании па­ра­мет­ров квад­ра­та сна­ча­ла соз­да­ет­ся бай­то­вый бу­фер ByteBuffer, с по­мо­щью че­ты­рех бай­тов для ка­ж­дой точ­ки в обыч­ном по­ряд­ке бай­тов уст­рой­ст­ва. За­тем с его по­мо­щью соз­да­ет­ся бу­фер с пла­ваю­щей точ­кой squareBuffer; по­ме­ща­ем ту­да вер­ши­ны и за­да­ем на­чаль­ную по­зи­цию на пер­вой ко­ор­ди­на­те.

Вернем­ся в CISGLRenderer.java, что­бы на­стро­ить вер­шин­ный и фраг­мент­ный шей­де­ры. Вер­шин­ный шей­дер управ­ля­ет раз­ме­щением и про­ри­сов­кой вер­шин (уг­лов) фи­гур в OpenGL. Фраг­мент­ный шей­дер управ­ля­ет тем, что ри­су­ет­ся ме­ж­ду вер­ши­на­ми. Для их соз­дания нам по­на­до­бит­ся объ­ект Strings, ко­то­рый бу­дет пе­ре­дан OpenGL:

private final String vertexShaderCode =

“attribute vec4 vPosition; \n” +

“void main(){ \n” +

“ gl_Position = vPosition; \n” +

“} \n”;

private final String fragmentShaderCode =

“precision mediump float; \n” +

“void main(){ \n” +

“ gl_FragColor = vec4 (0.63671875, 0.76953125, 0.22265625, 1.0); \n” +

“} \n”;

private Square square;

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

GLES20.glClearColor(0.5f, 0.5f, 05.f, 1.0f);

square = new Square();

int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);

int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

}

private int loadShader(int type, String shaderCode) {

int shader = GLES20.glCreateShader(type);

GLES20.glShaderSource(shader, shaderCode);

GLES20.glCompileShader(shader);

return shader;

}

Что­бы соз­дать шей­дер, нуж­но за­гру­зить код в ви­де стро­ки String и ском­пи­ли­ро­вать его, как в ме­то­де loadShader().

Для про­ри­сов­ки в OpenGL ES 2.0 ис­поль­зу­ет­ся Program; это объ­ект, ко­то­рый бе­рет шей­де­ры (ис­пол­няе­мый код) и свя­зы­ва­ет их друг с дру­гом, что­бы с их по­мо­щью на­ри­со­вать объ­ек­ты. По­это­му нам нуж­но соз­дать про­грам­му и свя­зать с ней шей­де­ры:

private int program; private int positionHandle;

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

[ ... ]

program = GLES20.glCreateProgram();

GLES20.glAttachShader(program, vertexShader);

GLES20.glAttachShader(program, fragmentShader);

GLES20.glLinkProgram(program);

positionHandle = GLES20.glGetAttribLocation(program, “vPosition”);

}

В этом ко­де соз­да­ет­ся но­вый пустой объ­ект Program, с ним свя­зы­ва­ют­ся оба на­ших шей­де­ра, за­тем ме­тод glLinkProgram() соз­да­ет необ­хо­ди­мый ис­пол­няе­мый код.

На­конец, в по­следней стро­ке мы по­лу­ча­ем де­ск­рип­тор пе­ре­мен­ной vPosition в ко­де вер­шин­но­го шей­де­ра. Это по­зво­ля­ет нам пе­ре­дать рас­по­ло­жение ка­ж­дой вер­ши­ны из на­ше­го ко­да для Android в про­грам­му OpenGL.

За­дав па­ра­мет­ры про­грам­мы OpenGL, до­бавь­те сле­дую­щие стро­ки в ме­тод onDrawFrame(), что­бы на­ри­со­вать квад­рат:

GLES20.glUseProgram(program);

square.draw(positionHandle);

а это __draw()__ method to __Square__:

public void draw(int positionHandle) {

GLES20.glVertexAttribPointer(positionHandle, 3, GLES20. GL_FLOAT, false, 0, squareBuffer);

GLES20.glEnableVertexAttribArray(positionHandle);

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertices.length/3);

}

glVertexAttribPointer() оп­ре­де­ля­ет мас­сив дан­ных вер­шин. positionHandle – ин­декс ото­бра­жае­мой вер­ши­ны, ко­то­рая здесь со­сто­ит из трех ком­понен­тов (X, Y, Z). Тип дан­ных – GL_FLOAT, зна­чения не долж­ны нор­ма­ли­зо­вать­ся (false), ме­ж­ду зна­чения­ми мас­си­ва вер­шин нет сме­щения (0), а ин­фор­ма­ция о вер­шине на­хо­дит­ся в squareBuffer.

glEnableVertexAttribArray() ак­ти­ви­ру­ет мас­сив, а glDrawArrays() пре­вра­ща­ет мас­сив дан­ных в ви­зуа­ли­зи­ро­ван­ные гео­мет­ри­че­­ские при­ми­ти­вы. Мы поль­зу­ем­ся ти­пом GL_TRIANGLE_STRIP, на­чи­ная ин­декс с ну­ля, и ви­зуа­ли­зи­ру­ем че­ты­ре вер­ши­ны, так как в мас­си­ве vertices для ка­ж­дой вер­ши­ны есть три зна­чения (X, Y, Z). Ском­пи­ли­руй­те и за­пусти­те про­грам­му, и вы долж­ны уви­деть свою фи­гу­ру... од­на­ко это не со­всем квад­рат?

Де­ла­ем квад­рат квад­рат­ным

Про­бле­ма с неквад­рат­ным квад­ра­том поя­ви­лась из-за пред­по­ло­жения OpenGL, что об­ласть про­смот­ра пред­став­ля­ет со­бой квад­рат, а на уст­рой­ст­вах с Android эк­ран обыч­но со­всем не квад­рат­ный. Что­бы это ис­пра­вить, нам по­на­до­бят­ся про­ек­ци­он­ная мат­ри­ца и мат­ри­ца об­зо­ра ка­ме­ры, с ко­то­ры­ми мы пра­виль­но вы­чис­лим ко­ор­ди­на­ты и при­вя­жем их к эк­ра­ну ва­ше­го уст­рой­ст­ва. Бо­лее под­роб­но о мат­ри­цах на­пи­са­но во врез­ке; про­ек­ци­он­ная мат­ри­ца по су­ти свя­зы­ва­ет «иде­аль­ный квад­рат» OpenGL с ре­аль­ным эк­ра­ном, а мат­ри­ца об­зо­ра ка­ме­ры пред­став­ля­ет ви­зуа­ли­зи­руе­мые объ­ек­ты так, как их ви­дит ка­ме­ра с за­дан­но­го по­ло­жения. На­ша ка­ме­ра на­хо­дит­ся по цен­тру эк­ра­на.

Все эти дей­ст­вия вы­пол­ня­ют­ся в CISGLRenderer.java. Для на­ча­ла нуж­но из­менить код вер­шин­но­го шей­де­ра, до­ба­вив ту­да ссыл­ку на един­ст­вен­ную мат­ри­цу uMVPMatrix.

Мы объ­е­ди­ни­ли обе мат­ри­цы в од­ну:

private final String vertexShaderCode =

“uniform mat4 uMVPMatrix; \n” +

“attribute vec4 vPosition; \n” +

“void main(){ \n” +

“ gl_Position = uMVPMatrix * vPosition; \n” +

“} \n”;

Здесь до­бав­ля­ет­ся но­вая стро­ка с мат­ри­цей, за­тем она ум­но­жа­ет­ся на vPosition для вы­чис­ления glPosition.

Так­же до­ба­вим несколь­ко при­ват­ных чле­нов мат­ри­цы для хранения раз­ных дан­ных:

private int mvpMatrixHandle; // matrix handle

private float[] uMVPMatrix = new float[16]; // объ­е­ди­нен­ная матрица

private float[] vMatrix = new float[16]; // матрица об­зо­ра ка­ме­ры

private float[] projMatrix = new float[16]; // про­ек­ци­он­ная мат­ри­ца

Те­перь до­пишем немно­го ко­да в onSurfaceCreated() и в onSurfaceChanged():

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

[ ... ]

mvpMatrixHandle = GLES20.

glGetUniformLocation(program, “uMVPMatrix”);

[ ... ]

}

public void onSurfaceChanged(GL10 gl, int width, int height)

{

GLES20.glViewport(0, 0, width, height);

float ratio = (float) width/height;

Matrix.frustumM(projMatrix, 0, -ratio, ratio, -1, 1, 2, 7);

Matrix.setLookAtM(vMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f,0.0f);

}

Под­клю­ча­ем­ся к Мат­ри­це

Ме­то­ды клас­са Matrix по­зво­ля­ют вы­пол­нять раз­лич­ные манипу­ля­ции с мат­ри­ца­ми. frustumM() за­да­ет на­шу про­ек­ци­он­ную мат­ри­цу в ви­де шес­ти плоско­стей от­се­чения. Плоскость от­се­чения в трех­мер­ной гра­фи­ке го­во­рит ви­зуа­ли­за­то­ру, «на ка­ком рас­стоянии» от на­блю­да­те­ля он дол­жен пре­кра­тить вы­чис­ления по­верх­но­стей. Это оз­на­ча­ет, что для са­мых да­ле­ких по­верх­но­стей вы­чис­ления не вы­пол­ня­ют­ся; таким образом, мы эко­но­мим вы­чис­ли­тель­ные ре­сур­сы.

Ле­вая и пра­вая плоско­сти от­се­чения за­да­ют­ся пе­ре­мен­ной ratio, это ле­вый и пра­вый края эк­ра­на. Ниж­няя и верх­няя граница эк­ра­на за­да­ют­ся ко­ор­ди­на­та­ми -1 и 1. Два по­следних зна­чения – «ближ­няя» и «даль­няя» граница, у нас это 2 и 7 (они долж­ны быть по­ло­жи­тель­ны­ми). Мы по­лу­ча­ем унифи­ци­ро­ван­ный де­ск­рип­тор мат­ри­цы для по­сле­дую­ще­го ис­поль­зо­вания, а за­тем за­да­ем мат­ри­цу об­зо­ра ка­ме­ры ме­то­дом setLookAtM. Она бу­дет хранить­ся в vMatrix со сме­щением 0. Точ­ка на­блю­дения – (0, 0, -3), x = y = 0 и z = -3, т. е. точ­ка на­блю­дения вы­хо­дит «за» эк­ран на три единицы сис­те­мы ко­ор­ди­нат.


(thumbnail)
> Те­перь квад­рат и вправ­ду квад­рат.

Центр об­лас­ти за­дан в (0f, 0f, 0f), т. е. сов­па­да­ет с цен­тром эк­ра­на, а восхо­дя­щий век­тор – в (0f, 1.0f, 0.0f). Восхо­дя­щий век­тор за­да­ет на­прав­ление «вверх»; здесь это, как обыч­но, ось Y.

На­конец, из­меним ме­тод onDrawFrame() и восполь­зу­ем­ся все­ми но­вы­ми мат­ри­ца­ми:

public void onDrawFrame(GL10 gl) {

GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |

GLES20.GL_DEPTH_BUFFER_BIT);

Matrix.multiplyMM(uMVPMatrix, 0, projMatrix, 0, vMatrix, 0);

GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, uMVPMatrix, 0);

GLES20.glUseProgram(program);

square.draw(positionHandle);

}

Ме­тод multiplyMM ум­но­жа­ет vMatrix на projMatrix и за­пи­сы­ва­ет ре­зуль­тат в uMVPMatrix (все ну­ли – сме­щения в ре­зуль­ти­рую­щем мас­си­ве: у нас их нет). Ме­тод glUniformMatrix4fv об­нов­ля­ет mvpMatrixHandle, ко­то­рый мы по­лу­чи­ли из вер­шин­но­го шей­де­ра в ме­то­де onSurfaceChanged() с по­мо­щью uMVPMatrix, не транс­понируя ника­ких эле­мен­тов (false).

Все го­то­во! Пе­ре­ком­пи­ли­руй­те про­грам­му, и вы долж­ны уви­деть на­стоя­щий квад­рат.

Будь­те осто­рож­ны! Лю­бые ошиб­ки в стро­ках шей­де­ров OpenGL при­ве­дут к то­му, что код пе­ре­станет ра­бо­тать, но ком­пи­ля­тор Android их не уви­дит, так как для него это про­сто стро­ки.

Ес­ли код ра­бо­та­ет не так, как ожи­да­лось, внима­тель­но про­верь­те за­глав­ные бу­к­вы, про­бе­лы и кор­рект­ность ис­поль­зуе­мых опе­ра­то­ров в стро­ках.

До­ба­вим цве­та

Сей­час мы за­да­ем цвет квад­ра­та вруч­ную, как часть пе­ре­мен­ной fragmentShaderCode. Луч­ше пе­ре­да­вать его из­вне. Из­мените пе­ре­мен­ные fragmentShaderCode и vertexShaderCode:

private final String vertexShaderCode =

“uniform mat4 uMVPMatrix; \n” +

“attribute vec4 vPosition; \n” +

“attribute vec4 aColour; \n” +

“varying vec4 vColour; \n” +

“void main(){ \n” +

“ vColour = aColour; \n” +

“ gl_Position = uMVPMatrix * vPosition; \n” +

“} \n”;

private final String fragmentShaderCode =

“precision mediump float; \n” +

“varying vec4 vColour; \n” +

“void main(){ \n” +

“ gl_FragColor = vColour; \n” +

“} \n”;

Цвет стал пе­ре­да­вать­ся в aColour и vColour. Пе­ре­мен­ные с из­ме­няе­мы­ми зна­чения­ми слу­жат в ка­че­­ст­ве ин­тер­фей­са ме­ж­ду вер­шин­ным и фраг­мент­ным шей­де­ра­ми.

Цвет вер­шин за­да­ет­ся в пе­ре­мен­ной aColour, ко­то­рая не из­ме­ня­ет­ся. Пе­ре­мен­ная vColour (ко­то­рая из­ме­ня­ет­ся) в точ­ках вер­шин рав­на aColour, а за­тем во фраг­мент­ном шей­де­ре ин­тер­по­ли­ру­ет­ся до цве­та пик­се­лей, ко­то­рый ме­ня­ет­ся ме­ж­ду вер­ши­на­ми; так мы по­лу­ча­ем раз­но­цвет­ную фи­гу­ру.

За­тем в ме­то­де onSurfaceCreated() соз­да­дим объ­ект Program, ко­то­рый ис­поль­зу­ет эти зна­чения:

private int colourHandle;

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

[ ... ]

program = GLES20.glCreateProgram();

GLES20.glAttachShader(program, vertexShader);

GLES20.glAttachShader(program, fragmentShader);

GLES20.glBindAttribLocation(program, 0, “vPosition”);

GLES20.glBindAttribLocation(program, 1, “aColour”);

GLES20.glLinkProgram(program);

positionHandle = GLES20.glGetAttribLocation(program, “vPosition”);

colourHandle = GLES20.glGetAttribLocation(program, “aColour”);

}

Данный код свя­зы­ва­ет де­ск­рип­тор по­ло­жения и де­ск­рип­тор цве­та с со­от­вет­ст­вую­щи­ми пе­ре­мен­ны­ми в вер­шин­ном шей­де­ре (vPosition и aColour) и по­лу­ча­ет де­ск­рип­то­ры для обе­их. Те­перь из­мените вы­зов square.draw() в onDrawFrame(), пе­ре­дав в ка­че­­ст­ве ар­гу­мен­тов де­ск­рип­то­ры по­ло­жения и цве­та.

В но­вой вер­сии square.draw() бу­дет все­го две но­вых стро­ки:

GLES20.glVertexAttribPointer(colourHandle, 4, GLES20.GL_ FLOAT, false, 0, squareColourBuffer);

GLES20.glEnableVertexAttribArray(colourHandle);

Об­ра­ти­те внимание, что мы здесь ис­поль­зу­ем пе­ре­мен­ную squareColourBuffer, ко­то­рая свя­за­на с colourHandle.

По­нят­ное де­ло, имен­но в ней хра­нят­ся цве­та вер­шин:

private FloatBuffer squareColourBuffer; float verticesColour[] =

1.0f, 0.0f, 1.0f, 1.0f,

0.0f, 1.0f, 0.0f, 1.0f,

0.0f, 0.0f, 1.0f, 1.0f,

0.0f, 0.0f, 0.0f, 1.0f,

};

Для ка­ж­дой вер­ши­ны ука­зан цвет в фор­ма­те RGBA, и пик­се­ли в этой вер­шине бу­дут в него ок­ра­ше­ны. По ме­ре пе­ре­ме­щения к сле­дую­щей вер­шине ви­зуа­ли­за­тор бу­дет ин­тер­по­ли­ро­вать цве­та ме­ж­ду цве­та­ми вер­шин, и один цвет бу­дет плав­но пе­ре­те­кать в дру­гой (как на ри­сун­ке).

Ес­ли все стро­ки (вер­ши­ны) бу­дут од­но­го цве­та в verticesColour, мы по­лу­чим од­но­тон­ный квад­рат.

squareColourBuffer за­да­ет­ся в кон­ст­рук­то­ре точ­но так же, как и squareBuffers. Ском­пи­ли­руй­те и за­пусти­те про­грам­му, и вы долж­ны уви­деть раз­но­цвет­ный квад­рат.

Квад­рат пре­вра­ща­ет­ся в куб

Квад­ра­ты – это пре­крас­но, но ис­тин­ная при­вле­ка­тель­ность OpenGL – в умении ра­бо­тать с трех­мер­ны­ми фи­гу­ра­ми. Соз­да­дим класс Cube, что­бы на­ри­со­вать куб вме­сто на­ше­го квад­ра­та:

public class Cube {

private FloatBuffer cubeBuffer;

private FloatBuffer cubeColourBuffer;

private ShortBuffer cubeIndexBuffer;

float vertices[] = {

-0.5f, -0.5f, -0.5f,

// Ко­ор­ди­на­ты 8 вер­шин; под­роб­но­сти см. на DVD

};

float verticesColour[] = {

0.0f, 0.0f, 0.0f, 1.0f,

// Ко­ор­ди­на­ты 8 вер­шин; под­роб­но­сти см. на DVD

};

short[] cubeIndices = {

0, 4, 5,

// Все­го 36 ин­дек­сов в 12 груп­пах по 3 (см. текст); под­роб­но­сти см. на DVD

};

public Cube() {

// Ии­циа­ли­зи­ру­ем cubeBuffer и cubeColourBuffer как де­ла­лось для Square.java

cubeIndexBuffer = ByteBuffer.allocateDirect(

cubeIndices.length * 4).

order(ByteOrder.nativeOrder()).asShortBuffer();

cubeIndexBuffer = cbb.asShortBuffer().put(cubeIndices).position(0);

}

public void draw(int positionHandle, int colourHandle) {

// Ус­та­но­вим по­зи­цию и цвет VertexAttribPointers как в Square.java

GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, cubeIndices.length, GLES20.GL_UNSIGNED_SHORT, cubeIndexBuffer);

}

}

Глав­ное от­ли­чие в том, что здесь ис­поль­зу­ют­ся ко­ор­ди­на­ты вер­шин ку­ба. В мас­си­ве vertices оп­ре­де­ля­ет­ся 8 вер­шин ку­ба, ко­то­рые мож­но про­ну­ме­ро­вать от 0 до 7.

В мас­си­ве cubeIndices они груп­пи­ру­ют­ся по три; ка­ж­дая груп­па за­да­ет тре­угольник раз­ме­ром в по­ло­ви­ну грани ку­ба. Это де­ла­ет­ся по­то­му, что для ри­со­вания ку­ба мы восполь­зу­ем­ся при­ми­ти­вом GL_TRIANGLE_STRIP (к со­жа­лению, Android не под­дер­жи­ва­ет GL_QUADS, с ко­то­рым мы мог­ли бы оп­ре­де­лить грани ку­ба, сгруп­пи­ро­вав вер­ши­ны по че­ты­ре).

Та­кое струк­ту­ри­ро­вание дан­ных о вер­ши­нах/гра­нях и ме­тод glDrawElements() по­зво­ля­ют за­дать куб с мень­шим ко­ли­че­­ст­вом вы­зо­вов.

Вы­звав cube.draw() в CISGLRenderer.onDrawFrame(), вы все еще уви­ди­те квад­рат – пло­скую пе­ред­нюю грань ку­ба, на ко­то­рую вы смот­ри­те спе­ре­ди. Что­бы уви­деть трех­мер­ность ку­ба, его нуж­но немно­го по­вер­нуть.

Для это­го восполь­зу­ем­ся еще од­ной мат­ри­цей mMatrix и до­ба­вим несколь­ко строк в onSurfaceCreated() и од­ну стро­ку в onDrawFrame():

private float mMatrix = new float[16];

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

[ ... rest of method as stands ... ]

Matrix.setIdentityM(mMatrix, 0);

Matrix.rotateM(mMatrix, 0, -40, 1, -1, 0);

}

(thumbnail)
Раз­но­цвет­ный вра­щаю­щий­ся куб.

public void onDrawFrame(GL10 gl) {

GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

Matrix.setIdentityM(uMVPMatrix, 0);

Matrix.multiplyMM(uMVPMatrix, 0, mMatrix, 0, uMVPMatrix, 0);

Matrix.multiplyMM(uMVPMatrix, 0, vMatrix, 0, uMVPMatrix, 0);

Matrix.multiplyMM(uMVPMatrix, 0, projMatrix, 0, uMVPMatrix, 0);

GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, uMVPMatrix, 0);

GLES20.glUseProgram(program);

cube.draw(positionHandle, colourHandle);

}

В ме­то­де onSurfaceCreated() мы инициа­ли­зи­ру­ем эту мат­ри­цу как единич­ную, за­тем по­во­ра­чи­ва­ем ее на мес­те на -40 гра­ду­сов во­круг оси XYZ (1, -1, 0) (по­про­буй­те из­менить эти чис­ла и по­смот­ри­те, что про­ис­хо­дит, ес­ли вы не зна­ко­мы с трех­мер­ным по­во­ро­том).

Те­перь в ме­то­де onDrawFrame() нуж­но ум­но­жить эту мат­ри­цу на мат­ри­цу об­зо­ра ка­ме­ры (vMatrix) и на про­ек­ци­он­ную мат­ри­цу (projMatrix). Это оз­на­ча­ет, что нуж­но вы­де­лить стро­ки и ум­но­жать ка­ж­дую из них по оче­ре­ди для по­лу­чения объ­е­динен­ной мат­ри­цы, но по су­ти мы де­ла­ем здесь то же, что и рань­ше (объ­е­ди­ня­ем раз­лич­ные мат­ри­цы в од­ну, что­бы при­менить ее к на­шей фи­гу­ре), толь­ко с еще од­ной мат­ри­цей. По­том мы ри­су­ем наш куб.

У нас долж­но по­лу­чить­ся нечто по­хо­жее на куб, но его грани вы­гля­дят немно­го стран­но. Что­бы они смот­ре­лись бо­лее со­лид­но, до­бавь­те еще две стро­ки в ме­тод onSurfaceCreated() по­сле вы­зо­ва glClearColor():

GLES20.glEnable(GLES20.GL_DEPTH_TEST);

GLES20.glDepthFunc(GLES20.GL_LEQUAL);

В пер­вой стро­ке вклю­ча­ют­ся про­вер­ка глу­би­ны и бу­фер глу­би­ны. С вклю­чен­ным бу­фе­ром глу­би­ны ка­ж­дый раз при про­ри­сов­ке пик­се­ля его зна­чение глу­би­ны сравнива­ет­ся с со­хранен­ным зна­чением глу­би­ны.

Мы поль­зу­ем­ся па­ра­мет­ром GL_LEQUAL, по­это­му но­вый пик­сель бу­дет на­ри­со­ван толь­ко в том слу­чае, ес­ли он бли­же к на­блю­да­те­лю, чем ста­рый, и, та­ким об­ра­зом, ви­ден ему.

Без это­го, так как OpenGL ри­су­ет пик­се­ли в лю­бом по­ряд­ке, мож­но по­лу­чить причудливые ре­зуль­та­ты и час­тич­но ви­деть объ­ек­ты «на­сквозь». Ском­пи­ли­руй­те и за­пусти­те про­грам­му и по­лю­буй­тесь сво­им ку­бом! |

Куб в дви­же­нии

Что­бы за­кон­чить на­шу ис­то­рию, за­ста­вим куб дви­гать­ся. Для это­го нам сно­ва дос­та­точ­но лишь мат­ри­цы дви­же­ния, но на сей раз в ме­то­де onDrawFrame():

Matrix.rotateM(mMatrix, 0, 1, 6, 2, 3);

Matrix.setIdentityM(uMVPMatrix, 0);

Ка­ж­дый раз при пе­ре­ри­сов­ке кад­ра (пе­рио­дич­ность пе­ре­ри­сов­ки бу­дет за­ви­сеть от ап­па­рат­ной на­чин­ки уст­рой­ст­ва и от то­го, что еще про­ис­хо­дит в сис­те­ме) куб бу­дет по­во­ра­чи­вать­ся на один гра­дус во­круг оси (6, 2, 3). Опять же, по­про­буй­те из­менить эти чис­ла, что­бы по­нять, что про­ис­хо­дит (на­при­мер, уве­личь­те «гра­ду­сы», что­бы куб за­вер­тел­ся бы­ст­рее). Уч­ти­те, что по­ря­док мно­жи­те­лей при ум­но­жении мат­риц в onDrawFrame() име­ет зна­чение; по­про­буй­те по­ме­нять мес­та­ми стро­ку multiplyMM mMatrix со стро­ка­ми vMatrix и projMatrix, и вы пой­ме­те, что я имею в ви­ду!

Персональные инструменты
купить
подписаться
Яндекс.Метрика