Thursday, November 26, 2020

Learn Postgres in 3 days

PostgreSQL Getting Started
TABLE OF CONTENTS (HIDE)

Getting Started with PostgreSQL

I assume that you are familiar with SQL commands (such as CREATE TABLE, DROP TABLE, INSERT, SELECT, UPDATE and DELETE). Otherwise, read ...

Introduction

PostgreSQL is an open-source Object-Relational Database Management System (ORDBMS). It supports a large part of the SQL standards such as foreign keys, triggers, views and transactions; and object-oriented features such as inheritance.

The project started in UC Berkeley in 1986, and is now developed by a group of volunteers called "PostgreSQL Global Development Group".

The mother site for PostgreSQL is http://www.postgresql.org; with manual @ http://www.postgresql.org/docs/manuals/.

Installing PostgreSQL 10.6

Ubuntu
// Refresh the apt-get repository
$ sudo apt-get update
// Install PostgreSQL
$ sudo apt-get install postgresql postgresql-contrib

// Verify the installation
$ dpkg --status postgresql
Package: postgresql
Version: 10+190
......
$ whereis postgresql
postgresql: /etc/postgresql /usr/lib/postgresql /usr/share/postgresql ...
$ which psql     // psql is an interactive PostgreSQL client
/usr/bin/psql
$ ll /usr/bin/psql
lrwxrwxrwx 1 root root 37 xxx xx  xxxx /usr/bin/psql -> ../share/postgresql-common/pg_wrapper*

The following packages will be installed:

  • postgresql, postgresql-common, postgresql-10: Core database server.
  • postgresql-client-common, postgresql-client-10: Client binaries and libraries.
  • postgresql-contrib-10: Additional supplied modules.
PostgreSQL Configuration Files

The PostgreSQL configuration files are stored in directory /etc/postgresql/10/main (for Ubuntu). The main configuration file is called "postgresql.conf".

$ less /etc/postgresql/10/main/postgresql.conf 
......
data_directory = '/var/lib/postgresql/10/main'
hba_file = '/etc/postgresql/10/main/pg_hba.conf'     # host-based authentication file
ident_file = '/etc/postgresql/10/main/pg_ident.conf' # ident configuration file
#listen_addresses = 'localhost'   # what IP address(es) to listen on;
                                  # comma-separated list of addresses;
                                  # defaults to 'localhost', '*' = all
port = 5432
max_connections = 100
......

Notes:

  • data_directory directive specifies where the databases are stored.
  • hba_file directive specifies the host-based authentication file.
  • ident_file directive specifies the ident authentication file.
  • The port directive specifies the TCP port number. The default is 5432.

Getting Started

Reference: PostgreSQL Documentation - Tutorial @ http://www.postgresql.org/docs/10/static/tutorial.html.

PostgreSQL Client-Server System

PostgreSQL runs as a client-server system.

[TODO] more explanation.

PostgreSQL Server

In Ubuntu, the server is run as a service called postgresql (configured in /etc/init.d/postgresql). The postgresql service is started automatically upon startup. Like all other services, you could:

$ sudo service postgresql stop     // Stop the service
$ sudo service postgresql start    // Start the service
$ sudo service postgresql restart  // Stop and restart the service
$ sudo service postgresql reload   // Reload the configuration without stopping the service

// OR
$ sudo systemctl start postgresql
$ sudo systemctl stop postgresql
$ sudo systemctl restart postgresql
$ sudo systemctl reload postgresql
$ sudo systemctl status postgresql   // Show th status

The PostgreSQL server accepts connection by client program over the TCP/IP network. The default TCP port number is 5432.

The PostgreSQL server program is called postgres (in directory /usr/lib/postgresql/10/bin). There is also a symlink called postmaster in the same directory. You could display the PostgreSQL server process via:

$ ps aux | grep postgres
postgres  7136  0.0  0.2 134068  9608 ?        S    Dec13   0:01 
/usr/lib/postgresql/10/bin/postgres -D /var/lib/postgresql/10/main 
-c config_file=/etc/postgresql/10/main/postgresql.conf
......
Command-line Client Program "psql"

The PostgreSQL installation provides a command-line client program called psql, which can be used to login to the server.

$ which psql
/usr/bin/psql
$ ll /usr/bin/psql
lrwxrwxrwx 1 root root 37 xxx xx xxxx /usr/bin/psql -> ../share/postgresql-common/pg_wrapper*

psql is a symlink located in /usr/bin (a PATH entry). The actual program and utilities are located at /usr/lib/postgresql/10/bin and linked via pg_wrapper.

Utilities createdb, createuser

The directory /usr/lib/postgresql/10/bin also contains many utilities such as createdb (create database), dropdb (drop database), createuser (create PostgreSQL user), dropuser (drop PostgreSQL user), among others, with symlinks defined in /usr/bin.

$ ll /usr/bin/create*
lrwxrwxrwx 1 root root 37 xxx xx  xxxx /usr/bin/createdb -> ../share/postgresql-common/pg_wrapper*
lrwxrwxrwx 1 root root 37 xxx xx  xxxx /usr/bin/createuser -> ../share/postgresql-common/pg_wrapper*
Graphical Clients

Besides the command-line client psql, many graphical clients are available to access PostgreSQL, e.g., pgAdmin3, PhpPgAdmin (for PHP, model after PhpMyAdmin), etc. I will describe these tools in later sections.

Login to the PostgreSQL Server

Default PostgreSQL Superuser "postgres"

The PostgreSQL installation creates a "UNIX USER" called postgres, who is ALSO the "Default PostgreSQL's SUPERUSER". The UNIX USER postgres cannot login interactively to the system, as its password is not enabled.

Login as PostgreSQL Superuser "postgres" via "psql" Client

To run psql using "UNIX USER" postgres, you need to invoke "sudo -u postgres psql", which switches to "UNIX USER" postgres and run the psql command.

$ sudo -u postgres psql
   -- Run command "psql" as UNIX USER "postgres".
   -- Enter the CURRENT SUPERUSER password for sudo.
psql (10.6 (Ubuntu 10.6-0ubuntu0.18.04.1))
Type "help" for help.

postgres=# help 
You are using psql, the command-line interface to PostgreSQL.
Type:  \copyright for distribution terms
       \h for help with SQL commands
       \? for help with psql commands
       \g or terminate with semicolon to execute query
       \q to quit

-- Display version
postgres=# SELECT version();
     version                                                   
---------------------------------------------------
 PostgreSQL 10.6 (Ubuntu 10.6-0ubuntu0.18.04.1) ...
press q to quit display

-- Quit
postgres=# \q

Notes:

  • The PostgreSQL's prompt is in the format of databaseName=# for superuser or databaseName=> for regular user. In this case, the current database is also called postgres, which is the same as the username.
  • The prompt changes to -# (superuser) or -> (regular user) for command continuation.
  • Type "help" to see the help menu.
  • Type \q to quit.
  • Type \? to see all the psql commands.
  • You need to end a SQL statement with a semicolon (;) or \g (for GO) to execute the statement. For example, in "SELECT version();".
  • Comments: In a SQL script or command, an end-of-line comment begins with --; a block comment is enclosed by /* and */

Create Database, Create Table, CURD (Create-Update-Read-Delete) Records

-- Login to server
$ sudo -u postgres psql
......

-- List all databases via \l (or \list), or \l+ for more details
postgres=# \l
   Name    | ...
-----------+-----------
postgres   | ...
template0  | ...
template1  | ...
 
-- Help on SQL command syntax
postgres=# \h CREATE DATABASE
......
 
-- Create a new database called mytest
postgres=# CREATE DATABASE mytest;
CREATE DATABASE
   -- By default, the owner of the database is the current login user.
 
-- List all databases via \l (or \list), or \l+ for more details
postgres=# \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 mytest    | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 ......

   -- By default, CREATE DATABASE copies the "template1", which you could customize.
   -- "template0" has the same contents as "template1", but you should not change.
 
-- Connect to mytest database via \c (or \connect)
-- Take note of the change of database name in the command prompt.
postgres=# \c mytest 
You are now connected to database "mytest" as user "postgres".

-- Display all tables (aka relations) via \dt or \dt+ for more details
mytest=# \dt
Did not find any relations.
 
-- We will be using an enumeration (TYPE) in our table, let's define it.
mytest=# CREATE TYPE cat_enum AS ENUM ('coffee', 'tea');
CREATE TYPE

-- Display all types via /dT or /dT+ for more details.
mytest=# \dT+
                        List of data types
 Schema |   Name   | Internal name | Size | Elements | Description 
--------+----------+---------------+------+----------+-------------
 public | cat_enum | cat_enum      | 4    | coffee  +| 
        |          |               |      | tea      | 
 
-- Create a new table.
mytest=# CREATE TABLE IF NOT EXISTS cafe (
  id SERIAL PRIMARY KEY,        -- AUTO_INCREMENT integer, as primary key
  category cat_enum NOT NULL,   -- Use the enum type defined earlier
  name VARCHAR(50) NOT NULL,    -- Variable-length string of up to 50 characters
  price NUMERIC(5,2) NOT NULL,  -- 5 digits total, with 2 decimal places
  last_update DATE              -- 'YYYY-MM-DD'
);
NOTICE:  CREATE TABLE will create implicit sequence "cafe_id_seq" for serial column "cafe.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "cafe_pkey" for table "cafe"
CREATE TABLE
 
-- Display all tables in the current database, via \dt or \dt+ for more details
mytest=# \dt+
                    List of relations
 Schema | Name | Type  |  Owner   |  Size   | Description 
--------+------+-------+----------+---------+-------------
 public | cafe | table | postgres | 0 bytes | 

-- Display details of a particular table.
mytest=# \d+ cafe
                                               Table "public.cafe"
   Column    |         Type          |                     Modifiers                     | Storage  | Description 
-------------+-----------------------+---------------------------------------------------+----------+-------------
 id          | integer               | not null default nextval('cafe_id_seq'::regclass) | plain    | 
 category    | cat_enum              | not null                                          | plain    | 
 name        | character varying(50) | not null                                          | extended | 
 price       | numeric(5,2)          | not null                                          | main     | 
 last_update | date                  |                                                   | plain    | 
Indexes:
    "cafe_pkey" PRIMARY KEY, btree (id)
Has OIDs: no

-- Insert rows.
mytest=# INSERT INTO cafe (category, name, price) VALUES
  ('coffee', 'Espresso', 3.19),
  ('coffee', 'Cappuccino', 3.29),
  ('coffee', 'Caffe Latte', 3.39),
  ('coffee', 'Caffe Mocha', 3.49),
  ('coffee', 'Brewed Coffee', 3.59),
  ('tea', 'Green Tea', 2.99),
  ('tea', 'Wulong Tea', 2.89);
INSERT 0 7
-- The output shows the OID (Object Identification Number) and the rows affected.
-- Each row has an hidden OID. OID is not a standard SQL feature and not recommended.
   
-- Query (SELECT)
mytest=# SELECT * FROM cafe;
 id | category |     name      | price | last_update 
----+----------+---------------+-------+-------------
  1 | coffee   | Espresso      |  3.19 | 2013-12-14
  2 | coffee   | Cappuccino    |  3.29 | 2013-12-14
  3 | coffee   | Caffe Latte   |  3.39 | 2013-12-14
  4 | coffee   | Caffe Mocha   |  3.49 | 2013-12-14
  5 | coffee   | Brewed Coffee |  3.59 | 2013-12-14
  6 | tea      | Green Tea     |  2.99 | 2013-12-14
  7 | tea      | Wulong Tea    |  2.89 | 2013-12-14
(7 rows)

mytest=# SELECT name, price FROM cafe WHERE category = 'coffee' AND price < 3.3; 
    name    | price 
------------+-------
 Espresso   |  3.19
 Cappuccino |  3.29
(2 rows)

-- Update
mytest=# UPDATE cafe SET price = price * 1.1 WHERE category = 'tea';
UPDATE 2
 
mytest=# SELECT * FROM cafe WHERE category = 'tea';
 id | category |    name    | price | last_update 
----+----------+------------+-------+-------------
  6 | tea      | Green Tea  |  3.29 | 2013-12-14
  7 | tea      | Wulong Tea |  3.18 | 2013-12-14
(2 rows)

-- Delete
mytest=# DELETE FROM cafe WHERE id = 6;
DELETE 1
 
mytest=# SELECT * FROM cafe WHERE category = 'tea';
 id | category |    name    | price | last_update 
----+----------+------------+-------+-------------
  7 | tea      | Wulong Tea |  3.18 | 2013-12-14
(1 row)

-- Quit
mytest=# \q

Notes:

  • Take note that there are two sets of commands:
    • psql commands: such as \c, \l, \dt.
    • SQL commands: such as CREATE DATABASE, CREATE TABLE and SELECT. SQL command KEYWORDS are not case sensitive. I show them in uppercase for clarity. PostgreSQL automatically converts all names (identifiers) to lowercase (even you type in uppercase or mixed case). To use uppercase or mixed case, you need to double-quote the names.
  • You need to end your SQL commands with a semi-colon (;) or \g. If you forget to enter a semi-colon, the command-prompt changes to "dbname-#" to indicate continuation (A SQL command can span many lines). You can enter the semi-colon on the new line.
More on psql commands
  • \?: show all psql commands.
  • \h sql-command: show syntax on SQL command.
  • \c dbname [username]: Connect to database, with an optional username (or \connect).
  • Display Commands: You can append + to show more details.
    • \l: List all database (or \list).
    • \d: Display all tables, indexes, views, and sequences.
    • \dt: Display all tables.
    • \di: Display all indexes.
    • \dv: Display all views.
    • \ds: Display all sequences.
    • \dT: Display all types.
    • \dS: Display all system tables.
    • \du: Display all users.
  • \x auto|on|off: Toggle|On|Off expanded output mode.
More on SQL commands

See ....

Using Utility "createuser" and "createdb"

As an example, let's create a PostgreSQL user called testuser, and a new database called testdb owned by testuser.

# Create a new PostgreSQL user called testuser, allow user to login, but NOT creating databases
$ sudo -u postgres createuser --login --pwprompt testuser
Enter password for new role: xxxx

# Create a new database called testdb, owned by testuser.
$ sudo -u postgres createdb --owner=testuser testdb

Tailor the PostgreSQL configuration file /etc/postgresql/10/main/pg_hba.conf to allow non-default user testuser to login to PostgreSQL server, by adding the following entry:

# TYPE  DATABASE    USER        ADDRESS          METHOD
local   testdb      testuser                     md5

Restart PostgreSQL server:

$ sudo service postgresql restart

Login to PostgreSQL server:

# Login to PostgreSQL: psql -U user database
$ psql -U testuser testdb
Password for user testuser: ......
psql (9.3.10)
Type "help" for help.

# List all databases (\list or \l)
testdb=>\list
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 testdb    | testuser | UTF8     | en_SG.UTF-8 | en_SG.UTF-8 |
......

# Create table
testdb=> create table if not exists cafe (
           name varchar(50),
           price numeric(5,2),
           primary key (name));
CREATE TABLE

# Insert rows
testdb=> insert into cafe values
           ('Espresso', 3.99),
           ('Green Tea', 2.99);
INSERT 0 2

# Select query
testdb=> select * from cafe;
   name    | price 
-----------+-------
 Espresso  |  3.99
 Green Tea |  2.99
(2 rows)

# To change password for the current user
testdb=> \password
......

testdb=> \q

You can also use other utilities like dropuser, dropdb, and etc.

Commonly-used SQL Data Types

As illustrated in the above example, the commonly-used SQL data types in PostgreSQL are:

  1. INT, SMALLINT: whole number. There is no UNSIGNED attribute in PostgreSQL.
  2. SERIAL: auto-increment integer (AUTO_INCREMENT in MySQL).
  3. REAL, DOUBLE: single and double precision floating-point number.
  4. CHAR(n) and VARCHAR(n): fixed-length string of n characters and variable-length string of up to n characters. String literals are enclosed by single quotes, e.g., 'Peter', 'Hello, world'.
  5. NUMERIC(m,n): decimal number with m total digits and n decimal places (DECIMAL(m,n) in MySQL).
  6. DATE, TIME, TIMESTAMP, INTERVAL: date and time.
  7. User-defined types.
  8. NULL: A special value indicates unknown value or no value (of an optional field), which is different from 0 and empty string (that represent known value of 0 and empty string). To test for NULL value, use operator IS NULL or IS NOT NULL (e.g., email IS NULL). Comparing two NULLs with = or != results in unknown.

PostgreSQL Administration

Default Superuser "postgres"

During installation, a "UNIX USER" (who cannot login to the system interactively) called postgres is created. You can verify by checking the entry in /etc/passwd and /etc/shadow:

$ sudo less /etc/passwd | grep postgres
postgres:x:120:130:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
 
$ sudo less /etc/shadow | grep postgres
postgres:*:16049:0:99999:7:::  
   // * indicates that password is not enabled (this user cannot login interactively).

The user postgres belongs to the following groups:

$ groups postgres
postgres : postgres ssl-cert

An entry is also defined in PostgreSQL's HBA (Host-Based Authentication) configuration file for this user (PostgreSQL 9.5, NOT 10?!):

$ sudo less /etc/postgresql/10/main/pg_hba.conf
......
# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             postgres                                peer
   // UNIX user "postgres" can access all databases from the localhost.
   // The "peer" authentication method means that if "foo" is a UNIX user and "foo" is also an PostgreSQL user,
   // then "foo" can login to PostgreSQL server from localhost without password.
 
# "local" is for Unix domain socket connections only
local   all             all                                     peer
# IPv4 local connections:
host    all             all             127.0.0.1/32            md5
# IPv6 local connections:
host    all             all             ::1/128                 md5

Authentication Methods

PostgreSQL supports a number of authentication methods. The commonly-used are:

  • ident, peer: identical (or fully match) to the OS account, with an optional name mapping defined in pg_ident.conf file. ident is applicable to TCP/IP; while peer for "local" connection.
  • md5: require md5-hashed password (most common).
  • password: require clear-text password.
  • trust: no password, as long as meeting the IP, user, and database criteria defined in the HBA.
  • reject: reject login immediately.
  • others, such as GSSAPI, SSPI, Kerberos, LDAP, RADIUS, Certificate, PAM.

There are two ways to login PostgreSQL:

  1. By running the "psql" command as a UNIX user which is also configured as PostgreSQL user using so-called IDENT/PEER authentication, e.g., "sudo -u postgres psql".
  2. Via TCP/IP connection using PostgreSQL's own managed username/password (using so-called MD5 authentication).

Set a Password for User postgres

To set a password, login to PostgreSQL server using postgres via psql and issue command "\password username", as follows:

-- Login in to server via "psql" with user "postgres"
$ sudo -u postgres psql
......
 
-- Change password for current user "postgres"
postgres=# \password postgres
Enter new password: xxxx
Enter it again: xxxx
  
-- Display the user table
postgres=# SELECT * FROM pg_user;
 usename  | usesysid | usecreatedb | usesuper | usecatupd | userepl |  passwd  | valuntil | useconfig 
----------+----------+-------------+----------+-----------+---------+----------+----------+-----------
 postgres |       10 | t           | t        | t         | t       | ******** |          |
 
-- Quit
postgres=# \q

To test the password login, you need to change the the authentication method from "peer" to "md5" in pg_hba.conf. Restart the server, and login via sudo -u postgres sql. The system will prompt you for the password.

Add your UNIX user as PostgreSQL user

Using user "postgres" (which is a UNIX user as well as PostgreSQL user) to run "psql" involves switching user (via "sudo -u username" or "su - username"). You can simply the process by configuring your current UNIX userID as PostgreSQL user, as follows:

-- Switch to default superuser "postgres",
-- run utility "createuser" to create a superuser same name as current login.
-- "$USER" is an environment variable denoting the current login user.
$ sudo -u postgres createuser --superuser $USER

-- Create the default database which shall be the same as the username.
$ sudo -u postgres createdb $USER

-- Now, you can invoke "psql" from your user account.
$ psql
......
yourusername=# SELECT * FROM pg_user;
   usename    | usesysid | usecreatedb | usesuper | usecatupd | userepl |  passwd  | valuntil | useconfig 
--------------+----------+-------------+----------+-----------+---------+----------+----------+-----------
 yourusername |    16424 | t           | t        | t         | t       | ******** |          |            
 
-- Perform database operations.
.......

Create Group and User

The latest PostgreSQL treats both group and user as role. Some roles can login (i.e. user), some roles have member of other roles (i.e., group). You should use CREATE ROLE to create both users and groups (CREATE USER and CREATE GROUP are meant for compatibility).

-- Create a login user role
CREATE ROLE user1 LOGIN PASSWORD 'xxxx' CREATEDB VALID UNTIL 'infinity';

-- Create a login superuser role
CREATE ROLE user2 LOGIN PASSWORD 'xxxx' SUPERUSER VALID UNTIL '2019-12-31';

-- Create a group role
CREATE ROLE group1 INHERIT;
-- Add a user (or group) to this group
GRANT group1 TO user1;

Backup and Restore

PostgreSQL provides two utilities for backup:

  1. pg_dump: backup a specific database.
  2. pg_dumpall: backup all databases and server globals. Need to be run by superuser.

For example,

-- Create a compressed backup for a database
pg_dump -h localhost -p 5432 -U username -F c -b -v -f mydatabase.backup mydatabase

-- Create a plain-text backup for a database, including the CREATE DATABASE
pg_dump -h localhost -p 5432 -U username -C -F p -b -v -f mydatabase.backup.sql mydatabase

To restore a database from backup generated by pg_dump or pg_dumpall:

  1. Use "psql" to run the plain-text backup.
    -- Run SQL script
    $ psql -U username -f filename.sql
  2. Use utility "pg_restore" to restore compressed backup.

Admin Tools

pgAdmin 3

The mother site is http://www.pgadmin.org.

To install pgAdmin 3 (on Ubuntu):

$ sudo apt-get install pgadmin3

To launch: Run "pgadmin3" from terminal; or launch "pgAdmin III" form the dash. Push the "Connection" button. In "Properties" tab: enter a description in "Name"; "localhost" or "127.0.0.1" in Host; "postgres" in Username and its password.

[TODO] more

PHP-PostgreSQL

Install Driver

Install PDO PostgreSQL driver:

$ sudo apt-get install php5-pgsql

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?php
/**
 * Testing PDO MySQL Database Connection, query() and exec().
 * For CREATE TABLE, INSERT, DELETE, UPDATE:
 *   exec(): returns the affected rows.
 * For SELECT:
 *   query(): returns a result set.
 */
define('DB_HOST', 'localhost');  // MySQL server hostname
define('DB_PORT', '5432');       // MySQL server port number (default 3306)
define('DB_NAME', 'mytest');       // MySQL database name
define('DB_USER', 'postgres');   // MySQL username
define('DB_PASS', 'xxxx');       // password
 
// Create a database connection to PostgreSQL server.
try {
   // new PDO('pgsql:host=hostname;port=number;dbname=database;user=username;password=pw')
   $dbConn = new PDO('pgsql:host=' . DB_HOST . ';'
                     . 'port=' . DB_PORT . ';'
                     . 'dbname=' . DB_NAME . ';'
                     . 'user=' . DB_USER . ';'
                     . 'password=' . DB_PASS);
   $dbConn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Set error mode to exception
   echo 'Connected', '<br />';
 
} catch (PDOException $e) {
   $fileName = basename($e->getFile(), ".php"); // File that triggers the exception
   $lineNumber = $e->getLine();          // Line number that triggers the exception
   die("[$fileName][$lineNumber] Database connect failed: " . $e->getMessage() . '<br />');
}
 
// Run SQL statements
try {
   // Use exec() to run a CREATE TABLE, DROP TABLE, INSERT, DELETE and UPDATE,
   // which returns the affected row count.
   $rowCount = $dbConn->exec('DROP TABLE IF EXISTS test');
   echo 'DROP TABLE: ', $rowCount, ' rows', '<br />';
 
   $rowCount = $dbConn->exec(
         'CREATE TABLE IF NOT EXISTS test (
           id SERIAL,
           name VARCHAR(20),
           PRIMARY KEY (id))');
   echo 'CREATE TABLE: ', $rowCount, ' rows', '<br />';
 
   $rowCount = $dbConn->exec("INSERT INTO test (id, name) VALUES (1001, 'peter')");
   echo 'INSERT INTO: ', $rowCount, ' rows', '<br />';
// Cannot called lastInsertId as nextval() was not called in the previous insert.
// echo 'LAST_INSERT_ID (of the AUTO_INCREMENT column) is ', $dbConn->lastInsertId('test_id_seq'), '<br />';
 
   $rowCount = $dbConn->exec("INSERT INTO test (name) VALUES ('paul'),('patrick')");
   echo 'INSERT INTO: ', $rowCount, ' rows', '<br />';
   echo 'LAST_INSERT_ID (of the AUTO_INCREMENT column) is ', $dbConn->lastInsertId('test_id_seq'), '<br />';
 
   // Use query() to run a SELECT, which returns a resultset.
   $sql = 'SELECT * FROM test';
   $resultset = $dbConn->query($sql);
   foreach ($resultset as $row) {
      echo 'Using column name: id=', $row['id'], ' name=', $row['name'], '<br />';
      echo 'Using column number: id=', $row[0], ' name=', $row[1], '<br />';
      print_r($row); // for illustrating the contents of resultset's row
                     // indexed by both column-name and 0-indexed column-number
      echo '<br />';
   }
 
   // Run again with FETCH_ASSOC.
   $resultset = $dbConn->query($sql, PDO::FETCH_ASSOC);
   foreach ($resultset as $row) {  // by column-name only
      echo 'Using column name: id=', $row['id'], ' name=', $row['name'], '<br />';
      print_r($row); // for illustrating the contents of resultset's row
                     // indexed by column-name only
      echo '<br />';
   }
 
   // PostgreSQL supports "INSERT ... RETURNING ID", which returns a resultset of IDs.
   $sql = "INSERT INTO test (name) VALUES ('john'), ('jimmy') RETURNING ID";
   $resultset = $dbConn->query($sql, PDO::FETCH_ASSOC);
   foreach ($resultset as $row) {
      echo "Last insert id is {$row['id']}<br />";
   }
 
   // Close the database connection  (optional).
   $dbConn = NULL;
 
} catch (PDOException $e) {
   $fileName = basename($e->getFile(), ".php"); // File that trigger the exception
   $lineNumber = $e->getLine();         // Line number that triggers the exception
   die("[$fileName][$lineNumber] Database error: " . $e->getMessage() . '<br />');
}
?>

More Examples

See "PHP PDO" (some modifications needed from MySQL to PostgreSQL).

Python-PostgreSQL

Goto "Python-PostgreSQL Database Programming".

REFERENCES & RESOURCES

  1. PostgreSQL mother site @ http://www.postgresql.org; documentation @ http://www.postgresql.org/docs/; manual @ http://www.postgresql.org/docs/manuals/.

Wednesday, November 4, 2020

Android Programming 3D Graphics with OpenGL ES (Including Nehe's Port)

Android 3D with OpenGL ES with Nehe's Port
TABLE OF CONTENTS (HIDE)

Android Programming

3D Graphics with OpenGL ES
(Including Nehe's Port)

Introduction

Read

  1. Android Training "Displaying Graphics with OpenGL ES" @ http://developer.android.com/training/graphics/opengl/index.html.
  2. Android API Guides "OpenGL ES" @ http://developer.android.com/guide/topics/graphics/opengl.html.
  3. Android Reference "Package android.opengl" @ http://developer.android.com/reference/android/opengl/package-summary.html.

Getting Started with 3D Graphics on Android

OpenGL ES

Android supports OpenGL ES in packages android.opengl, javax.microedition.khronos.opengles and javax.microedition.khronos.egl.

GLSurfaceView

For 3D graphics programming, you need to program you own custom view, instead using XML-layout. Fortunately, a 3D OpenGL ES view called GLSurfaceView is provided, which greatly simplifies our tasks.

I shall use the Nehe's Lessons (http://nehe.gamedev.net) to illustrate Android 3D programming

Example 1: Setting up OpenGL ES using GLSurfaceView (Nehe Lesson 1: Setting Up)

Create an android application called "Nehe 01", with project name "Nehe01", package name "com.test". Create a blank activity called "MyGLActivity".

The following program sets up the GLSurfaceView, and show a blank (dark green) screen.

MyGLActivity.java
package com.test;
   
import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
/**
 * Our OpenGL program's main activity
 */
public class MyGLActivity extends Activity {
   
   private GLSurfaceView glView;   // Use GLSurfaceView
  
   // Call back when the activity is started, to initialize the view
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      glView = new GLSurfaceView(this);           // Allocate a GLSurfaceView
      glView.setRenderer(new MyGLRenderer(this)); // Use a custom renderer
      this.setContentView(glView);                // This activity sets to GLSurfaceView
   }
   
   // Call back when the activity is going into the background
   @Override
   protected void onPause() {
      super.onPause();
      glView.onPause();
   }
   
   // Call back after onPause()
   @Override
   protected void onResume() {
      super.onResume();
      glView.onResume();
   }
}
Dissecting MyActivity.java

We define MyActivity by extending Activity, so as to override onCreate(), onPause() and onResume(). We then override onCreate() to allocate a GLSurfaceView, set the view's renderer to a custom renderer (to be defined below), and set this activity to use the view.

MyGLRenderer.java

This is our custom OpenGL renderer class.

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
/**
 *  OpenGL Custom renderer used with GLSurfaceView 
 */
public class MyGLRenderer implements GLSurfaceView.Renderer {
   Context context;   // Application's context
   
   // Constructor with global application context
   public MyGLRenderer(Context context) {
      this.context = context;
   }
   
   // Call back when the surface is first created or re-created
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // You OpenGL|ES initialization code here
      // ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      if (height == 0) height = 1;   // To prevent divide by zero
      float aspect = (float)width / height;
   
      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);
  
      // Setup perspective projection, with aspect ratio matches viewport
      gl.glMatrixMode(GL10.GL_PROJECTION); // Select projection matrix
      gl.glLoadIdentity();                 // Reset projection matrix
      // Use perspective projection
      GLU.gluPerspective(gl, 45, aspect, 0.1f, 100.f);
  
      gl.glMatrixMode(GL10.GL_MODELVIEW);  // Select model-view matrix
      gl.glLoadIdentity();                 // Reset
  
      // You OpenGL|ES display re-sizing code here
      // ......
   }
   
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers using clear-value set earlier
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
     
      // You OpenGL|ES rendering code here
      // ......
   }
}
Dissecting MyGLRenderer.java

Our custom rendering class implements interface GLSurfaceView.Renderer, which is responsible to make the OpenGL calls to render a frame. It declare 3 methods to be called back by the Android graphics sub-system upon specific GL events.

  1. onSurfaceCreated(GL10 gl, EGLConfig config): Called when the surface is first created or recreated. It can be used to perform one-time initialization tasks such as setting the clear-value for color and depth, enabling depth-test, etc.
  2. onSurfaceChanged(GL10 gl, int width, int height): Called when the surface is first displayed and after window's size changes. It is used to set the view port and projection mode.
    In our OpenGL renderer, we set the Android's view port (display area) to cover the entire screen from (0,0) to (width-1, height-1):
    gl.glViewport(0, 0, width, height);
    We also choose the perspective projection and set the projection volume, with aspect ratio matches the view port, as follows:
    // OpenGL uses two transformation matrices: projection matrix and model-view matrix
    // We select the projection matrix to setup the projection
    gl.glMatrixMode(GL10.GL_PROJECTION); // Select projection matrix
    gl.glLoadIdentity();                 // Reset projection matrix
    // Use perspective projection with the projection volume defined by
    //   fovy, aspect-ration, z-near and z-far
    GLU.gluPerspective(gl, 45, aspect, 0.1f, 100.f);
      
    // Select the model-view matrix to manipulate objects (Deselect the projection matrix)
    gl.glMatrixMode(GL10.GL_MODELVIEW);  // Select model-view matrix
    gl.glLoadIdentity();                 // Reset
  3. onDrawFrame(GL10 gl): Called to draw the current frame. You OpenGL rendering codes here.
    In our OpenGL renderer, We clear the color and depth buffers (using the clear-values set via glClear* earlier).
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

In the Activity class, we construct a custom renderer, and use setRenderer() to set it for the view:

glView = new GLSurfaceView(this);       // Allocate a GLSurfaceView
glView.setRenderer(new MyGLRenderer()); // Set the renderer for the view

Run the program. You shall see a blank screen.

Example 2: Drawing 2D Shapes (Nehe Lesson 2: Your First Polygon)

Let us get started by drawing 2D polygons as illustrated (Push Ctrl-F11 to switch the emulator in landscape orientation):

Create an android application called "Nehe 02", with project name "Nehe02", package name "com.test". Create a blank activity called "MyGLActivity".

Triangle.java

We first define a class called Triangle.

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
/*
 * A triangle with 3 vertices.
 */
public class Triangle {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private ByteBuffer indexBuffer;    // Buffer for index-array
  
   private float[] vertices = {  // Vertices of the triangle
       0.0f,  1.0f, 0.0f, // 0. top
      -1.0f, -1.0f, 0.0f, // 1. left-bottom
       1.0f, -1.0f, 0.0f  // 2. right-bottom
   };
   private byte[] indices = { 0, 1, 2 }; // Indices to above vertices (in CCW)
 
   // Constructor - Setup the data-array buffers
   public Triangle() {
      // Setup vertex-array buffer. Vertices in float. A float has 4 bytes.
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert byte buffer to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
    
      // Setup index-array buffer. Indices in byte.
      indexBuffer = ByteBuffer.allocateDirect(indices.length);
      indexBuffer.put(indices);
      indexBuffer.position(0);
   }
  
   // Render this shape
   public void draw(GL10 gl) {
      // Enable vertex-array and define the buffers
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      
      // Draw the primitives via index-array
      gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
Dissecting Triangle.java

In OpenGL ES, you cannot define individual vertex via glVertex command (this command is not supported in ES due to inefficiency). Instead, you have to use a vertex array to define a group of vertices. This is done in two steps:

  1. We first define the (x, y, z) location of the vertices in a Java array:
    private float[] vertices = {  // Vertices of the triangle
        0.0f,  1.0f, 0.0f, // 0. top
       -1.0f, -1.0f, 0.0f, // 1. left-bottom
        1.0f, -1.0f, 0.0f  // 2. right-bottom
    };
  2. We then allocate the vertex-array buffer, and transfer the data into the buffer. We use nio's buffer because they are placed on the native heap and are not garbage-collected.
    private FloatBuffer vertexBuffer;
    ......
    // Allocate a raw byte buffer. A float has 4 bytes
    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
    // Need to use native byte order
    vbb.order(ByteOrder.nativeOrder());
    // Convert the byte buffer into float buffer
    vertexBuffer = vbb.asFloatBuffer();
    // Transfer the data into the buffer
    vertexBuffer.put(vertices);
    // Rewind
    vertexBuffer.position(0);

To render from the vertex-array, we need to enable client-state vertex-array:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

We can then use method glDrawArrays() to render from the vertex array directly, or glDrawElements() to render via an index array.

In the above example, we set up an index array, which indexes into the vertex array (and its associated color array), as follows. Take note that the vertices are arranged in counter-clockwise (CCW) manner, with normal pointing out of the screen (or positive z-direction).

private ByteBuffer indexBuffer;
......
byte[] indices = { 0, 1, 2 };   // CCW
......
indexBuffer = ByteBuffer.allocateDirect(indices.length);  // Allocate raw byte buffer
indexBuffer.put(indices);   // Transfer data into buffer
indexBuffer.position(0);    // rewind

We render the triangle in the draw() method, with the following steps:

  1. We first enable vertex-array client states.
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    
  2. We then specify the location of the buffers via:
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);   
      // gl*Pointer(int size, int type, int stride, Buffer pointer)
      //   size: number of coordinates per vertex (must be 2, 3, or 4).
      //   type: data type of vertex coordinate, GL_BYTE, GL_SHORT, GL_FIXED, or GL_FLOAT
      //   stride: the byte offset between consecutive vertices. 0 for tightly packed.
    
  3. Finally, we render the primitives using glDrawElements(), which uses the index array to reference the vertex and color arrays.
    gl.glDrawElements(GL10.GL_TRIANGLES, numIndices, GL10.GL_UNSIGNED_BYTE, indexBuffer);
      // glDrawElements(int mode, int count, int type, Buffer indices)
      //   mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, or GL_TRIANGLES
      //   count: the number of elements to be rendered.
      //   type: data-type of indices (must be GL_UNSIGNED_BYTE or GL_UNSIGNED_SHORT).
      //   indices: pointer to the index array. 
    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
Square.java

Similarly, let's define a quad. Take note that OpenGL ES does not support quad as a primitive. We need to draw two triangles instead. We shall use the same color for all the vertices of the quad. The color can be set via glColor.

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * A square drawn in 2 triangles (using TRIANGLE_STRIP).
 */
public class Square {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
  
   private float[] vertices = {  // Vertices for the square
      -1.0f, -1.0f,  0.0f,  // 0. left-bottom
       1.0f, -1.0f,  0.0f,  // 1. right-bottom
      -1.0f,  1.0f,  0.0f,  // 2. left-top
       1.0f,  1.0f,  0.0f   // 3. right-top
   };
  
   // Constructor - Setup the vertex buffer
   public Square() {
      // Setup vertex array buffer. Vertices in float. A float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Render the shape
   public void draw(GL10 gl) {
      // Enable vertex-array and define its buffer
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      // Draw the primitives from the vertex-array directly
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
Dissecting Square.java

OpenGL ES 1.0 does not support quad as primitive. We shall draw a quad using TRIANGLE_STRIP, composing of 2 triangles v0v1v2 and v2v1v3, in counter-clockwise orientation.

For the triangle, we use glDrawElements() which uses an index array to reference the vertex and color array. For the quad, we shall use glDrawArrays() to directly render from the vertex-array, as follows:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);           // Enable vertex array
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);  // Set the location of vertex array
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, numVertices);
  // glDrawArrays(int mode, int first, int count)
  //   mode: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, or GL_TRIANGLES
  //   first: the starting index in the enabled arrays.
  //   count: the number of indices to be rendered.
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
GL Renderer

Now, modify the renderer to draw the triangle and quad.

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   Triangle triangle;     // ( NEW )
   Square quad;           // ( NEW )
   
   // Constructor
   public MyGLRenderer(Context context) {
      // Set up the data-array buffers for these shapes ( NEW )
      triangle = new Triangle();   // ( NEW )
      quad = new Square();         // ( NEW )
   }

   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // NO CHANGE - SKIP
      ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }

   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers using clear-values set earlier 
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      gl.glLoadIdentity();                 // Reset model-view matrix ( NEW )
      gl.glTranslatef(-1.5f, 0.0f, -6.0f); // Translate left and into the screen ( NEW )
      triangle.draw(gl);                   // Draw triangle ( NEW )
  
      // Translate right, relative to the previous translation ( NEW )
      gl.glTranslatef(3.0f, 0.0f, 0.0f);
      quad.draw(gl);                       // Draw quad ( NEW )
   }
}

We run the shapes' setup codes in the renderer's constructor, as they only have to run once. We invoke the shapes' draw() in renderer's onDrawFrame() which renders the shapes upon each frame refresh.

GL Activity

There is no change to the Activity codes in MyGLActivity.

Example 3: Color (Nehe Lesson 3: Color)

Triangle.java

Modify the triangle.java as follow:

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
/*
 * A triangle with 3 vertices. Each vertex has its own color.
 */
public class Triangle {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private FloatBuffer colorBuffer;   // Buffer for color-array (NEW)
   private ByteBuffer indexBuffer;    // Buffer for index-array
  
   private float[] vertices = {  // Vertices of the triangle
       0.0f,  1.0f, 0.0f, // 0. top
      -1.0f, -1.0f, 0.0f, // 1. left-bottom
       1.0f, -1.0f, 0.0f  // 2. right-bottom
   };
   private byte[] indices = { 0, 1, 2 }; // Indices to above vertices (in CCW)
   private float[] colors = { // Colors for the vertices (NEW)
      1.0f, 0.0f, 0.0f, 1.0f, // Red (NEW)
      0.0f, 1.0f, 0.0f, 1.0f, // Green (NEW)
      0.0f, 0.0f, 1.0f, 1.0f  // Blue (NEW)
   };
  
   // Constructor - Setup the data-array buffers
   public Triangle() {
      // Setup vertex-array buffer. Vertices in float. A float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert byte buffer to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   
      // Setup color-array buffer. Colors in float. A float has 4 bytes (NEW)
      ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
      cbb.order(ByteOrder.nativeOrder()); // Use native byte order (NEW)
      colorBuffer = cbb.asFloatBuffer();  // Convert byte buffer to float (NEW)
      colorBuffer.put(colors);            // Copy data into buffer (NEW)
      colorBuffer.position(0);            // Rewind (NEW)
    
      // Setup index-array buffer. Indices in byte.
      indexBuffer = ByteBuffer.allocateDirect(indices.length);
      indexBuffer.put(indices);
      indexBuffer.position(0);
   }
  
   // Render this shape
   public void draw(GL10 gl) {
      // Enable arrays and define the buffers
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_COLOR_ARRAY);          // Enable color-array (NEW)
      gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);  // Define color-array buffer (NEW)
      
      // Draw the primitives via index-array
      gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_COLOR_ARRAY);   // Disable color-array (NEW)
   }
}
Dissecting Triangle.java

During rendering, the vertex-array will be rendered together with other attributes (such as color, texture and normal), if these attributes are enabled.

In the above example, we define the colors of the vertices and copy them into a color-array buffer. We enable color-array client-state. The colors will be rendered together with the vertices in glDrawElements().

Square.java

Modify Square.java as follows:

package com.test;
   
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * A square drawn in 2 triangles (using TRIANGLE_STRIP). This square has one color.
 */
public class Square {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private float[] vertices = {  // Vertices for the square
      -1.0f, -1.0f,  0.0f,  // 0. left-bottom
       1.0f, -1.0f,  0.0f,  // 1. right-bottom
      -1.0f,  1.0f,  0.0f,  // 2. left-top
       1.0f,  1.0f,  0.0f   // 3. right-top
   };
  
   // Constructor - Setup the vertex buffer
   public Square() {
      // Setup vertex array buffer. Vertices in float. A float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Render the shape
   public void draw(GL10 gl) {
      // Enable vertex-array and define its buffer
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);      // Set the current color (NEW)
      // Draw the primitives from the vertex array directly
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
   }
}
Dissecting Square.java

Our square has one color. That is, all vertices are rendered using the same color. Hence, there is no need to define a color-array. Instead, we added a glColor* command before rendering the square using glDrawArrays().

GL Renderer and GL Activity

No change.

Example 4: Rotation (Nehe Lesson 4: Rotation)

To rotate the shapes created in the previous example, we need to make some minor modifications to our renderer.

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   private Triangle triangle;
   Square quad;
   
   // Rotational angle and speed (NEW)
   private float angleTriangle = 0.0f; // (NEW)
   private float angleQuad = 0.0f;     // (NEW)
   private float speedTriangle = 0.5f; // (NEW)
   private float speedQuad = -0.4f;    // (NEW)
   
   // Constructor
   public MyGLRenderer(Context context) {
      // Set up the buffers for these shapes
      triangle = new Triangle();
      quad = new Square();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // NO CHANGE - SKIP
      ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers using clear-values set earlier
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    
      gl.glLoadIdentity();                 // Reset model-view matrix
      gl.glTranslatef(-1.5f, 0.0f, -6.0f); // Translate left and into the screen
      gl.glRotatef(angleTriangle, 0.0f, 1.0f, 0.0f); // Rotate the triangle about the y-axis (NEW)
      triangle.draw(gl);                   // Draw triangle
   
      gl.glLoadIdentity();                 // Reset the mode-view matrix (NEW)
      gl.glTranslatef(1.5f, 0.0f, -6.0f);  // Translate right and into the screen (NEW)
      gl.glRotatef(angleQuad, 1.0f, 0.0f, 0.0f); // Rotate the square about the x-axis (NEW)
      quad.draw(gl);                       // Draw quad

      // Update the rotational angle after each refresh (NEW)
      angleTriangle += speedTriangle; // (NEW)
      angleQuad += speedQuad;         // (NEW)
   }
}

A glRotate* command was added to rotate the shape, with angle of rotation updated after each refresh.

Example 5: 3D Shapes - Rotating Color Cube and Pyramid (Nehe Lesson 5: 3D Shapes)

Pyramid.java

Set up the color pyramid as follows:

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
public class Pyramid {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private FloatBuffer colorBuffer;   // Buffer for color-array
   private ByteBuffer indexBuffer;    // Buffer for index-array
    
   private float[] vertices = { // 5 vertices of the pyramid in (x,y,z)
      -1.0f, -1.0f, -1.0f,  // 0. left-bottom-back
       1.0f, -1.0f, -1.0f,  // 1. right-bottom-back
       1.0f, -1.0f,  1.0f,  // 2. right-bottom-front
      -1.0f, -1.0f,  1.0f,  // 3. left-bottom-front
       0.0f,  1.0f,  0.0f   // 4. top
   };
          
   private float[] colors = {  // Colors of the 5 vertices in RGBA
      0.0f, 0.0f, 1.0f, 1.0f,  // 0. blue
      0.0f, 1.0f, 0.0f, 1.0f,  // 1. green
      0.0f, 0.0f, 1.0f, 1.0f,  // 2. blue
      0.0f, 1.0f, 0.0f, 1.0f,  // 3. green
      1.0f, 0.0f, 0.0f, 1.0f   // 4. red
   };
  
   private byte[] indices = { // Vertex indices of the 4 Triangles
      2, 4, 3,   // front face (CCW)
      1, 4, 2,   // right face
      0, 4, 1,   // back face
      4, 0, 3    // left face
   };
  
   // Constructor - Set up the buffers
   public Pyramid() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
  
      // Setup color-array buffer. Colors in float. An float has 4 bytes
      ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
      cbb.order(ByteOrder.nativeOrder());
      colorBuffer = cbb.asFloatBuffer();
      colorBuffer.put(colors);
      colorBuffer.position(0);
  
      // Setup index-array buffer. Indices in byte.
      indexBuffer = ByteBuffer.allocateDirect(indices.length);
      indexBuffer.put(indices);
      indexBuffer.position(0);
   }
  
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);  // Front face in counter-clockwise orientation
  
      // Enable arrays and define their buffers
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
      gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);
      
      gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE,
            indexBuffer);
      
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
   }
}
Cube.java

Similarly, set up the color cube as follows:

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
  
public class Cube {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   private int numFaces = 6;
   
   private float[][] colors = {  // Colors of the 6 faces
      {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
      {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
      {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
      {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
      {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
      {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
   };
  
   private float[] vertices = {  // Vertices of the 6 faces
      // FRONT
      -1.0f, -1.0f,  1.0f,  // 0. left-bottom-front
       1.0f, -1.0f,  1.0f,  // 1. right-bottom-front
      -1.0f,  1.0f,  1.0f,  // 2. left-top-front
       1.0f,  1.0f,  1.0f,  // 3. right-top-front
      // BACK
       1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
      -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
       1.0f,  1.0f, -1.0f,  // 7. right-top-back
      -1.0f,  1.0f, -1.0f,  // 5. left-top-back
      // LEFT
      -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
      -1.0f, -1.0f,  1.0f,  // 0. left-bottom-front 
      -1.0f,  1.0f, -1.0f,  // 5. left-top-back
      -1.0f,  1.0f,  1.0f,  // 2. left-top-front
      // RIGHT
       1.0f, -1.0f,  1.0f,  // 1. right-bottom-front
       1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
       1.0f,  1.0f,  1.0f,  // 3. right-top-front
       1.0f,  1.0f, -1.0f,  // 7. right-top-back
      // TOP
      -1.0f,  1.0f,  1.0f,  // 2. left-top-front
       1.0f,  1.0f,  1.0f,  // 3. right-top-front
      -1.0f,  1.0f, -1.0f,  // 5. left-top-back
       1.0f,  1.0f, -1.0f,  // 7. right-top-back
      // BOTTOM
      -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
       1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
      -1.0f, -1.0f,  1.0f,  // 0. left-bottom-front
       1.0f, -1.0f,  1.0f   // 1. right-bottom-front
   };
        
   // Constructor - Set up the buffers
   public Cube() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display)
  
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

      // Render all the faces
      for (int face = 0; face < numFaces; face++) {
         // Set the color for each of the faces
         gl.glColor4f(colors[face][0], colors[face][1], colors[face][2], colors[face][3]);
         // Draw the primitive from the vertex-array directly
         gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, face*4, 4);
      }
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
}

The vertices of the color cube is labeled as follows.

The vertices of all the faces are arranged in counter-clockwise orientation with normal pointing outwards in a consistent manner. This enables us to cull the back face with the following codes:

gl.glFrontFace(GL10.GL_CCW);    // Set the front face
gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
gl.glCullFace(GL10.GL_BACK);    // Cull the back face
GL Renderer

The renderer is as follows:

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   private Pyramid pyramid;    // (NEW)
   private Cube cube;          // (NEW)
   
   private static float anglePyramid = 0; // Rotational angle in degree for pyramid (NEW)
   private static float angleCube = 0;    // Rotational angle in degree for cube (NEW)
   private static float speedPyramid = 2.0f; // Rotational speed for pyramid (NEW)
   private static float speedCube = -1.5f;   // Rotational speed for cube (NEW)
   
   // Constructor
   public MyGLRenderer(Context context) {
      // Set up the buffers for these shapes
      pyramid = new Pyramid();   // (NEW)
      cube = new Cube();         // (NEW)
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // NO CHANGE - SKIP
      ......
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    
      // ----- Render the Pyramid -----
      gl.glLoadIdentity();                 // Reset the model-view matrix
      gl.glTranslatef(-1.5f, 0.0f, -6.0f); // Translate left and into the screen
      gl.glRotatef(anglePyramid, 0.1f, 1.0f, -0.1f); // Rotate (NEW)
      pyramid.draw(gl);                              // Draw the pyramid (NEW)
    
      // ----- Render the Color Cube -----
      gl.glLoadIdentity();                // Reset the model-view matrix
      gl.glTranslatef(1.5f, 0.0f, -6.0f); // Translate right and into the screen
      gl.glScalef(0.8f, 0.8f, 0.8f);      // Scale down (NEW)
      gl.glRotatef(angleCube, 1.0f, 1.0f, 1.0f); // rotate about the axis (1,1,1) (NEW)
      cube.draw(gl);                      // Draw the cube (NEW)
      
      // Update the rotational angle after each refresh (NEW)
      anglePyramid += speedPyramid;   // (NEW)
      angleCube += speedCube;         // (NEW)
   }
}
Cube

There are many ways to render a cube. You could define all the vertices of the 6 faces as in the above example. You could also define one representative face, and render the face 6 times with proper translation and rotation.

Example 1: Cube1.java

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * Define the vertices for only one face (the front face).
 * Render the cube by translating and rotating the face.
 */
public class Cube1 {
   private FloatBuffer vertexBuffer;  // Buffer for vertex-array
   
   private float[][] colors = {  // Colors of the 6 faces
      {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
      {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
      {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
      {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
      {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
      {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
   };
  
   private float[] vertices = {  // Vertices for the front face
      -1.0f, -1.0f, 1.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 1.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 1.0f,  // 2. left-top-front
       1.0f,  1.0f, 1.0f   // 3. right-top-front
   };
   
   // Constructor - Set up the buffers
   public Cube1() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
  
   // Draw the color cube
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display)
   
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

      // Front
      gl.glColor4f(colors[0][0], colors[0][1], colors[0][2], colors[0][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      
      // Right - Rotate 90 degree about y-axis
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glColor4f(colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      // Back - Rotate another 90 degree about y-axis
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glColor4f(colors[2][0], colors[2][1], colors[2][2], colors[2][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      // Left - Rotate another 90 degree about y-axis
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glColor4f(colors[3][0], colors[3][1], colors[3][2], colors[3][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      // Bottom - Rotate 90 degree about x-axis
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glColor4f(colors[4][0], colors[4][1], colors[4][2], colors[4][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      
      // Top - Rotate another 180 degree about x-axis
      gl.glRotatef(180.0f, 1.0f, 0.0f, 0.0f);
      gl.glColor4f(colors[5][0], colors[5][1], colors[5][2], colors[5][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
}

Example 2: Cube2.java

package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/*
 * Define the vertices for a representative face.
 * Render the cube by translating and rotating the face.
 */
public class Cube2 {
   private FloatBuffer vertexBuffer; // Buffer for vertex-array
  
   private float[] vertices = { // Vertices for a face at z=0
      -1.0f, -1.0f, 0.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 0.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 0.0f,  // 2. left-top-front
       1.0f,  1.0f, 0.0f   // 3. right-top-front
   };
  
   private float[][] colors = {  // Colors of the 6 faces
      {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
      {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
      {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
      {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
      {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
      {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
   };
  
   // Constructor - Set up the buffers
   public Cube2() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
   }
   
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display)
  
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[0][0], colors[0][1], colors[0][2], colors[0][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[2][0], colors[2][1], colors[2][2], colors[2][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[3][0], colors[3][1], colors[3][2], colors[3][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
 
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[4][0], colors[4][1], colors[4][2], colors[4][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
 
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glColor4f(colors[5][0], colors[5][1], colors[5][2], colors[5][3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
}

Example 6: Texture (Nehe Lesson 6: Texture)

Let's convert our color-cube into a texture-cube.

TextureCube.java

Let's modify our earlier Cube2.java to set up the texture array.

package com.test;
   
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/*
 * A cube with texture. 
 * Define the vertices for only one representative face.
 * Render the cube by translating and rotating the face.
 */
public class TextureCube {
   private FloatBuffer vertexBuffer; // Buffer for vertex-array
   private FloatBuffer texBuffer;    // Buffer for texture-coords-array (NEW)
  
   private float[] vertices = { // Vertices for a face
      -1.0f, -1.0f, 0.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 0.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 0.0f,  // 2. left-top-front
       1.0f,  1.0f, 0.0f   // 3. right-top-front
   };
  
   float[] texCoords = { // Texture coords for the above face (NEW)
      0.0f, 1.0f,  // A. left-bottom (NEW)
      1.0f, 1.0f,  // B. right-bottom (NEW)
      0.0f, 0.0f,  // C. left-top (NEW)
      1.0f, 0.0f   // D. right-top (NEW)
   };
   int[] textureIDs = new int[1];   // Array for 1 texture-ID (NEW)
     
   // Constructor - Set up the buffers
   public TextureCube() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
  
      // Setup texture-coords-array buffer, in float. An float has 4 bytes (NEW)
      ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4);
      tbb.order(ByteOrder.nativeOrder());
      texBuffer = tbb.asFloatBuffer();
      texBuffer.put(texCoords);
      texBuffer.position(0);
   }
   
   // Draw the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display) 
   
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);  // Enable texture-coords-array (NEW)
      gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer); // Define texture-coords buffer (NEW)
      
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);  // Disable texture-coords-array (NEW)
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
  
   // Load an image into GL texture
   public void loadTexture(GL10 gl, Context context) {
      gl.glGenTextures(1, textureIDs, 0); // Generate texture-ID array

      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]);   // Bind to texture ID
      // Set up texture filters
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
  
      // Construct an input stream to texture image "res\drawable\nehe.png"
      InputStream istream = context.getResources().openRawResource(R.drawable.nehe);
      Bitmap bitmap;
      try {
         // Read and decode input as bitmap
         bitmap = BitmapFactory.decodeStream(istream);
      } finally {
         try {
            istream.close();
         } catch(IOException e) { }
      }
  
      // Build Texture from loaded bitmap for the currently-bind texture ID
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
      bitmap.recycle();
   }
}
Dissecting TextureCube.java

The vertices of all the 6 faces are arranged in a consistent manner (inverted-Z). Hence, we can use the same texture coordinates for all 6 face. We define the texture coords once, and put into the texture buffer 6 times. Take note that texture coordinates' origin is at the top-left corner. The coordinates are normalized to [0, 1].

private FloatBuffer texBuffer;     // Texture Coords Buffer
......
float[] texCoords = {  // Define the texture coord, applicable to all 6 faces
   // FRONT
   0.0f, 1.0f,  // A. left-bottom
   1.0f, 1.0f,  // B. right-bottom
   0.0f, 0.0f,  // C. left-top
   1.0f, 0.0f   // D. right-top
};
   
// Allocate texture buffer
ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4 * 6);
tbb.order(ByteOrder.nativeOrder());
texBuffer = tbb.asFloatBuffer();
// All the 6 faces have the same texture coords, repeat 6 times
for (int face = 0; face < 6; face++) {
   texBuffer.put(texCoords);
}
texBuffer.position(0);     // Rewind

To render the texture, we simply enable client-state texture-coords-array (together with vertex-array). The vertices and texture-coords will be rendered together.

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer);
   
for (int face = 0; face < 6; face++) {
// Render each face in TRIANGLE_STRIP using 4 vertices
   gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, face*4, 4);
}

We store the texture image "nehe.png" into folder "res\drawable".

The following steps are needed to setup texture and load an image:

  1. [TODO]

Take note that we have removed all the color information.

GL Renderer

We need to modify our renderer to setup texture as follows:

package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;

public class MyGLRenderer implements GLSurfaceView.Renderer {
   
   private Context context;   // Application context needed to read image (NEW)
   private TextureCube cube;
   private static float angleCube = 0;     // rotational angle in degree for cube
   private static float speedCube = -1.5f; // rotational speed for cube
   
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;   // Get the application context (NEW)
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // Setup Texture, each time the surface is created (NEW)
      cube.loadTexture(gl, context);    // Load image into Texture (NEW)
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture (NEW)
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      .......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
      
      // ----- Render the Cube -----
      gl.glLoadIdentity();                  // Reset the current model-view matrix
      gl.glTranslatef(0.0f, 0.0f, -6.0f);   // Translate into the screen
      gl.glRotatef(angleCube, 0.1f, 1.0f, 0.2f); // Rotate
      cube.draw(gl);
      
      // Update the rotational angle after each refresh.
      angleCube += speedCube;
   }
}

Example 6a: Photo-Cube

Let's convert the texture cube into photo cube with different images for each of the 6 faces.

PhotoCube.java
package com.test;
  
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/*
 * A photo cube with 6 pictures (textures) on its 6 faces.
 */
public class PhotoCube {
   private FloatBuffer vertexBuffer;  // Vertex Buffer
   private FloatBuffer texBuffer;     // Texture Coords Buffer
   
   private int numFaces = 6;
   private int[] imageFileIDs = {  // Image file IDs
      R.drawable.caldera,
      R.drawable.candice,
      R.drawable.mule,
      R.drawable.glass,
      R.drawable.leonardo,
      R.drawable.tmsk
   };
   private int[] textureIDs = new int[numFaces];
   private Bitmap[] bitmap = new Bitmap[numFaces];
   private float cubeHalfSize = 1.2f;
        
   // Constructor - Set up the vertex buffer
   public PhotoCube(Context context) {
      // Allocate vertex buffer. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(12 * 4 * numFaces);
      vbb.order(ByteOrder.nativeOrder());
      vertexBuffer = vbb.asFloatBuffer();
  
      // Read images. Find the aspect ratio and adjust the vertices accordingly.
      for (int face = 0; face < numFaces; face++) {
         bitmap[face] = BitmapFactory.decodeStream(
               context.getResources().openRawResource(imageFileIDs[face]));
         int imgWidth = bitmap[face].getWidth();
         int imgHeight = bitmap[face].getHeight();
         float faceWidth = 2.0f;
         float faceHeight = 2.0f;
         // Adjust for aspect ratio
         if (imgWidth > imgHeight) {
            faceHeight = faceHeight * imgHeight / imgWidth; 
         } else {
            faceWidth = faceWidth * imgWidth / imgHeight;
         }
         float faceLeft = -faceWidth / 2;
         float faceRight = -faceLeft;
         float faceTop = faceHeight / 2;
         float faceBottom = -faceTop;
         
         // Define the vertices for this face
         float[] vertices = {
            faceLeft,  faceBottom, 0.0f,  // 0. left-bottom-front
            faceRight, faceBottom, 0.0f,  // 1. right-bottom-front
            faceLeft,  faceTop,    0.0f,  // 2. left-top-front
            faceRight, faceTop,    0.0f,  // 3. right-top-front
         };
         vertexBuffer.put(vertices);  // Populate
      }
      vertexBuffer.position(0);    // Rewind
  
      // Allocate texture buffer. An float has 4 bytes. Repeat for 6 faces.
      float[] texCoords = {
         0.0f, 1.0f,  // A. left-bottom
         1.0f, 1.0f,  // B. right-bottom
         0.0f, 0.0f,  // C. left-top
         1.0f, 0.0f   // D. right-top
      };
      ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4 * numFaces);
      tbb.order(ByteOrder.nativeOrder());
      texBuffer = tbb.asFloatBuffer();
      for (int face = 0; face < numFaces; face++) {
         texBuffer.put(texCoords);
      }
      texBuffer.position(0);   // Rewind
   }
   
   // Render the shape
   public void draw(GL10 gl) {
      gl.glFrontFace(GL10.GL_CCW);
      
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer);
  
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0f, 1f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[1]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 4, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0f, 1f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[2]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 8, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0f, 1f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[3]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 12, 4);
      gl.glPopMatrix();
  
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1f, 0f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[4]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 16, 4);
      gl.glPopMatrix();
  
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1f, 0f, 0f);
      gl.glTranslatef(0f, 0f, cubeHalfSize);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[5]);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 20, 4);
      gl.glPopMatrix();
   
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
   }
  
   // Load images into 6 GL textures
   public void loadTexture(GL10 gl) {
      gl.glGenTextures(6, textureIDs, 0); // Generate texture-ID array for 6 IDs
  
      // Generate OpenGL texture images
      for (int face = 0; face < numFaces; face++) {
         gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[face]);
         // Build Texture from loaded bitmap for the currently-bind texture ID
         GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap[face], 0);
         bitmap[face].recycle();
      }
   }
}
MyGLRenderer.java
package com.test;
   
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
   
public class MyGLRenderer implements GLSurfaceView.Renderer {
   private PhotoCube cube;     // (NEW)
   private static float angleCube = 0;     // rotational angle in degree for cube
   private static float speedCube = -1.5f; // rotational speed for cube
   
   // Constructor
   public MyGLRenderer(Context context) {
      cube = new PhotoCube(context);    // (NEW)
   }
   
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
    
      // Setup Texture, each time the surface is created (NEW)
      cube.loadTexture(gl);             // Load images into textures (NEW)
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture (NEW)
   }
  
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      // ----- Render the Cube -----
      gl.glLoadIdentity();                  // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, -6.0f);   // Translate into the screen
      gl.glRotatef(angleCube, 0.15f, 1.0f, 0.3f); // Rotate
      cube.draw(gl);
      
      // Update the rotational angle after each refresh.
      angleCube += speedCube;
   }
}

Example 7a: User Inputs (Nehe Lesson 7 Part 1: Key-Controlled)

Nehe lesson 7 is far too complex, I shall break it into 3 parts: Key-controlled, Texture Filters, and Lighting.

TextureCube.java

No change, except using image "crate.png".

MyGLRenderer.java

We modify our renderer by adding variables and transformation methods to control the cube z-position, x and y rotational angles and speeds.

package com.test;
   
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
   
public class MyGLRenderer implements GLSurfaceView.Renderer {
    
   private Context context;
   private TextureCube cube;
   // For controlling cube's z-position, x and y angles and speeds (NEW)
   float angleX = 0;   // (NEW)
   float angleY = 0;   // (NEW)
   float speedX = 0;   // (NEW)
   float speedY = 0;   // (NEW)
   float z = -6.0f;    // (NEW)
   
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // Setup Texture, each time the surface is created
      cube.loadTexture(gl, context);    // Load image into Texture
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen (NEW)
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate (NEW)
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate (NEW)
      cube.draw(gl);
      
      // Update the rotational angle after each refresh (NEW)
      angleX += speedX;  // (NEW)
      angleY += speedY;  // (NEW)
   }
}
MyGLSurfaceView.java

In order to capture the user inputs, we need to customize the GLSurfaceView by extending a subclass, so as to override the event handlers (such as onKeyUp(), onTouchEvent()).

package com.test;
   
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.KeyEvent;
import android.view.MotionEvent;
/*
 * Custom GL view by extending GLSurfaceView so as
 * to override event handlers such as onKeyUp(), onTouchEvent()
 */
public class MyGLSurfaceView extends GLSurfaceView {
   MyGLRenderer renderer;    // Custom GL Renderer
   
   // For touch event
   private final float TOUCH_SCALE_FACTOR = 180.0f / 320.0f;
   private float previousX;
   private float previousY;

   // Constructor - Allocate and set the renderer
   public MyGLSurfaceView(Context context) {
      super(context);
      renderer = new MyGLRenderer(context);
      this.setRenderer(renderer);
      // Request focus, otherwise key/button won't react
      this.requestFocus();  
      this.setFocusableInTouchMode(true);
   }
   
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         case KeyEvent.KEYCODE_DPAD_LEFT:   // Decrease Y-rotational speed
            renderer.speedY -= 0.1f;
            break;
         case KeyEvent.KEYCODE_DPAD_RIGHT:  // Increase Y-rotational speed
            renderer.speedY += 0.1f;
            break;
         case KeyEvent.KEYCODE_DPAD_UP:     // Decrease X-rotational speed
            renderer.speedX -= 0.1f;
            break;
         case KeyEvent.KEYCODE_DPAD_DOWN:   // Increase X-rotational speed 
            renderer.speedX += 0.1f;
            break;
         case KeyEvent.KEYCODE_A:           // Zoom out (decrease z)
            renderer.z -= 0.2f;
            break;
         case KeyEvent.KEYCODE_Z:           // Zoom in (increase z)
            renderer.z += 0.2f;
            break;
      }
      return true;  // Event handled
   }

   // Handler for touch event
   @Override
   public boolean onTouchEvent(final MotionEvent evt) {
      float currentX = evt.getX();
      float currentY = evt.getY();
      float deltaX, deltaY;
      switch (evt.getAction()) {
         case MotionEvent.ACTION_MOVE:
            // Modify rotational angles according to movement
            deltaX = currentX - previousX;
            deltaY = currentY - previousY;
            renderer.angleX += deltaY * TOUCH_SCALE_FACTOR;
            renderer.angleY += deltaX * TOUCH_SCALE_FACTOR;
      }
      // Save current x, y
      previousX = currentX;
      previousY = currentY;
      return true;  // Event handled
   }
}

Clearly, we use key 'A' and 'Z' for zoom out and zoom in. Touch events for modifying x and y rotational angles. Left, right, up and down buttons to control the x and y rotational speeds.

MyGLActivity.java

We need to modify our GL Activity to use the custom GL View.

package com.test;
  
import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
/*
 * OpenGL Main Activity.
 */
public class MyGLActivity extends Activity {
   private GLSurfaceView glView;  // Use subclass of GLSurfaceView (NEW)
   
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      // Allocate a custom subclass of GLSurfaceView (NEW)
      glView = new MyGLSurfaceView(this);
      setContentView(glView);  // Set View (NEW)
   }
   
   @Override
   protected void onPause() {
      super.onPause();
      glView.onPause();
   }
   
   @Override
   protected void onResume() {
      super.onResume();
      glView.onResume();
   }
}

You can, similarly, capture and process other events. [MORE]

Example 7b: Texture Filters (Nehe Lesson 7 Part 2: Texture Filter)

TextureCube.java
package com.test;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
/*
 * A cube with texture.
 * Three texture filters are to be set up. 
 */
public class TextureCube {
   private FloatBuffer vertexBuffer; // Buffer for vertex-array
   private FloatBuffer texBuffer;    // Buffer for texture-coords-array
  
   private float[] vertices = { // Vertices for a face
      -1.0f, -1.0f, 0.0f,  // 0. left-bottom-front
       1.0f, -1.0f, 0.0f,  // 1. right-bottom-front
      -1.0f,  1.0f, 0.0f,  // 2. left-top-front
       1.0f,  1.0f, 0.0f   // 3. right-top-front
   };
  
   float[] texCoords = { // Texture coords for the above face
      0.0f, 1.0f,  // A. left-bottom
      1.0f, 1.0f,  // B. right-bottom
      0.0f, 0.0f,  // C. left-top
      1.0f, 0.0f   // D. right-top
   };
   int[] textureIDs = new int[3];  // Array for 3 texture-IDs (NEW)
     
   // Constructor - Set up the buffers
   public TextureCube() {
      // Setup vertex-array buffer. Vertices in float. An float has 4 bytes
      ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
      vbb.order(ByteOrder.nativeOrder()); // Use native byte order
      vertexBuffer = vbb.asFloatBuffer(); // Convert from byte to float
      vertexBuffer.put(vertices);         // Copy data into buffer
      vertexBuffer.position(0);           // Rewind
  
      // Setup texture-coords-array buffer, in float. An float has 4 bytes
      ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4);
      tbb.order(ByteOrder.nativeOrder());
      texBuffer = tbb.asFloatBuffer();
      texBuffer.put(texCoords);
      texBuffer.position(0);
   }
   
   // Draw the shape
   public void draw(GL10 gl, int textureFilter) {  // Select the filter (NEW)
      gl.glFrontFace(GL10.GL_CCW);    // Front face in counter-clockwise orientation
      gl.glEnable(GL10.GL_CULL_FACE); // Enable cull face 
      gl.glCullFace(GL10.GL_BACK);    // Cull the back face (don't display) 
   
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);  // Enable texture-coords-array
      gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texBuffer); // Define texture-coords buffer

      // Select the texture filter to use via texture ID (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[textureFilter]);
  
      // front
      gl.glPushMatrix();
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // left
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // back
      gl.glPushMatrix();
      gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // right
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // top
      gl.glPushMatrix();
      gl.glRotatef(270.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      // bottom
      gl.glPushMatrix();
      gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
      gl.glTranslatef(0.0f, 0.0f, 1.0f);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
      gl.glPopMatrix();
  
      gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisable(GL10.GL_CULL_FACE);
   }
  
   // Load an image and create 3 textures with different filters (NEW)
   public void loadTexture(GL10 gl, Context context) {
      // Construct an input stream to texture image "res\drawable\crate.png"
      InputStream istream = context.getResources().openRawResource(R.drawable.crate);
      Bitmap bitmap;
      try {
         // Read and decode input as bitmap
         bitmap = BitmapFactory.decodeStream(istream);
      } finally {
         try {
            istream.close();
         } catch(IOException e) { }
      }

      gl.glGenTextures(3, textureIDs, 0);  // Generate texture-ID array for 3 textures (NEW)

      // Create Nearest Filtered Texture and bind it to texture 0 (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

      // Create Linear Filtered Texture and bind it to texture 1 (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[1]);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

      // Create mipmapped textures and bind it to texture 2 (NEW)
      gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[2]);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
      gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR_MIPMAP_NEAREST);
      if(gl instanceof GL11) {
         gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_GENERATE_MIPMAP, GL11.GL_TRUE);
      }
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

      bitmap.recycle();
   }
}
Dissecting TextureCube.java

[TODO]

MyGLRenderer.java
......
public class MyGLRenderer implements GLSurfaceView.Renderer {
   ......
   int currentTextureFilter = 0;  // Texture filter (NEW)
   ......
   
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the current model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate
      
      cube.draw(gl, currentTextureFilter);    // (NEW)
      
      // Update the rotational angle after each refresh
      angleX += speedX;
      angleY += speedY;
   }
}
MyGLSurfaceView.java
......
public class MyGLSurfaceView extends GLSurfaceView {
   ......
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         ......
         case KeyEvent.KEYCODE_DPAD_CENTER:  // Select texture filter (NEW)
            renderer.currentTextureFilter = (renderer.currentTextureFilter + 1) % 3;
            break;
      }
}

Example 7c: Lighting (Nehe Lesson 7 Part 3: Lighting)

MyGLRenderer.java
package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
    
   private Context context;
   private TextureCube cube;
   // For controlling cube's z-position, x and y angles and speeds
   float angleX = 0;
   float angleY = 0;
   float speedX = 0;
   float speedY = 0;
   float z = -6.0f;
   
   int currentTextureFilter = 0;  // Texture filter

   // Lighting (NEW)
   boolean lightingEnabled = false;   // Is lighting on? (NEW)
   private float[] lightAmbient = {0.5f, 0.5f, 0.5f, 1.0f};
   private float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
   private float[] lightPosition = {0.0f, 0.0f, 2.0f, 1.0f};
  
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance

      // Setup Texture, each time the surface is created
      cube.loadTexture(gl, context);    // Load image into Texture
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture
      
      // Setup lighting GL_LIGHT1 with ambient and diffuse lights (NEW)
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_AMBIENT, lightAmbient, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_DIFFUSE, lightDiffuse, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_POSITION, lightPosition, 0);
      gl.glEnable(GL10.GL_LIGHT1);   // Enable Light 1 (NEW)
      gl.glEnable(GL10.GL_LIGHT0);   // Enable the default Light 0 (NEW)
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      .......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      // Enable lighting? (NEW)
      if (lightingEnabled) {
         gl.glEnable(GL10.GL_LIGHTING);
      } else {
         gl.glDisable(GL10.GL_LIGHTING);
      }
      
      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate
      cube.draw(gl, currentTextureFilter);
      
      // Update the rotational angle after each refresh
      angleX += speedX;
      angleY += speedY;
   }
}
MyGLSurfaceView.java
......
public class MyGLSurfaceView extends GLSurfaceView {
   .......
  
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         .......
         case KeyEvent.KEYCODE_L:  // Toggle lighting on/off (NEW) 
            renderer.lightingEnabled = !renderer.lightingEnabled;
            break;
      }
      ......
   }
}

Example 8: Blending (Nehe Lesson 8: Blending)

TextureCube.java

Use texture image "glass.png". Remove the culling of back face.

MyGLRenderer.java
package com.test;
  
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
  
public class MyGLRenderer implements GLSurfaceView.Renderer {
   private Context context;
   private TextureCube cube;
   // For controlling cube's z-position, x and y angles and speeds
   float angleX = 0;
   float angleY = 0;
   float speedX = 0;
   float speedY = 0;
   float z = -6.0f;
   
   int currentTextureFilter = 0;  // Texture filter
  
   // Lighting
   boolean lightingEnabled = false;
   private float[] lightAmbient = {0.5f, 0.5f, 0.5f, 1.0f};
   private float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
   private float[] lightPosition = {0.0f, 0.0f, 2.0f, 1.0f};
  
   // Blending (NEW)
   boolean blendingEnabled = false;  // Is blending on? (NEW)
  
   // Constructor
   public MyGLRenderer(Context context) {
      this.context = context;
      cube = new TextureCube();
   }
  
   // Call back when the surface is first created or re-created.
   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  // Set color's clear-value to black
      gl.glClearDepthf(1.0f);            // Set depth's clear-value to farthest
      gl.glEnable(GL10.GL_DEPTH_TEST);   // Enables depth-buffer for hidden surface removal
      gl.glDepthFunc(GL10.GL_LEQUAL);    // The type of depth testing to do
      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);  // nice perspective view
      gl.glShadeModel(GL10.GL_SMOOTH);   // Enable smooth shading of color
      gl.glDisable(GL10.GL_DITHER);      // Disable dithering for better performance
  
      // Setup Texture, each time the surface is created
      cube.loadTexture(gl, context);    // Load image into Texture
      gl.glEnable(GL10.GL_TEXTURE_2D);  // Enable texture
      
      // Setup lighting GL_LIGHT1 with ambient and diffuse lights
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_AMBIENT, lightAmbient, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_DIFFUSE, lightDiffuse, 0);
      gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_POSITION, lightPosition, 0);
      gl.glEnable(GL10.GL_LIGHT1);   // Enable Light 1
      gl.glEnable(GL10.GL_LIGHT0);   // Enable the default Light 0
      
      // Setup Blending (NEW)
      gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);           // Full brightness, 50% alpha (NEW)
      gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE); // Select blending function (NEW)
   }
   
   // Call back after onSurfaceCreated() or whenever the window's size changes.
   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      // NO CHANGE - SKIP
      ......
   }
  
   // Call back to draw the current frame.
   @Override
   public void onDrawFrame(GL10 gl) {
      // Clear color and depth buffers
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  
      // Enable lighting?
      if (lightingEnabled) {
         gl.glEnable(GL10.GL_LIGHTING);
      } else {
         gl.glDisable(GL10.GL_LIGHTING);
      }
      
      // Blending Enabled? (NEW)
      if (blendingEnabled) {
         gl.glEnable(GL10.GL_BLEND);       // Turn blending on (NEW)
         gl.glDisable(GL10.GL_DEPTH_TEST); // Turn depth testing off (NEW)
         
      } else {
         gl.glDisable(GL10.GL_BLEND);      // Turn blending off (NEW)
         gl.glEnable(GL10.GL_DEPTH_TEST);  // Turn depth testing on (NEW)
      }
      
      // ----- Render the Cube -----
      gl.glLoadIdentity();              // Reset the model-view matrix
      gl.glTranslatef(0.0f, 0.0f, z);   // Translate into the screen
      gl.glRotatef(angleX, 1.0f, 0.0f, 0.0f); // Rotate
      gl.glRotatef(angleY, 0.0f, 1.0f, 0.0f); // Rotate
      cube.draw(gl, currentTextureFilter);
        
      // Update the rotational angle after each refresh
      angleX += speedX;
      angleY += speedY;
   }
}
MyGLSurfaceView.java
......
public class MyGLSurfaceView extends GLSurfaceView {
   .......
  
   // Handler for key event
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent evt) {
      switch(keyCode) {
         .......
         case KeyEvent.KEYCODE_B:  // Toggle Blending on/off (NEW)
            renderer.blendingEnabled = !renderer.blendingEnabled;
            break;
      }
      ......
   }
}

Example 8a: Bouncing Ball in Cube

[TODO] No primitive to draw a sphere in OpenGL ES.

Android Port for Nehe's Lessons

I have ported some of the Nehe's lessons into Android. Refer to Nehe for the problem descriptions.

Setting Up:

  • Nehe's Lesson #1: Setting up OpenGL's window. (Refer to above examples.)

OpenGL Basics: I consider Lessons 2-8 as OpenGL basic lessons, that are extremely important! (Refer to above examples.)

  • Nehe's Lesson #2: Your first polygon
  • Nehe's Lesson #3: Adding Color
  • Nehe's Lesson #4: Rotation
  • Nehe's Lesson #5: 3D Shape
  • Nehe's Lesson #6: Texture
  • Nehe's Lesson #7: Texture Filter, Lighting, and key-controlled
  • Nehe's Lesson #8: Blending

Intermediate: [TODO]

 

REFERENCES & RESOURCES