65.9K
CodeProject 正在变化。 阅读更多。
Home

Fountain OpenGL应用程序演练

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (52投票s)

2011年10月17日

CPOL

31分钟阅读

viewsIcon

91969

downloadIcon

4608

使用 OpenGL ES 1.1 创建一个基本的喷泉场景

引言

本教程将涵盖 OpenGL 应用程序的创建,包括以下主题:

  • 角度计算
  • 透视
  • 摄像机对准
  • 深度缓冲区
  • 多通道渲染
  • 动画
  • 加速度计
  • 触摸事件
  • 持久化用户设置

该应用程序允许用户:

  • 在场景中任意移动相机
  • 旋转场景或相机
  • 显示和隐藏场景中的对象
  • 显示 FPS
  • 更改摄像机对准方法
  • 使用手机角度设置视角

该项目使用 Eclipse 和 Android SDK 构建。

背景

我创建此应用程序是为了练习学习 OpenGL。我在 Android 上找不到喷泉应用程序,所以我觉得这是一个很好的起点。大约 10% 的 Android 用户仍在使用 OpenGL ES 1.1,所以我用该版本编写了这个应用程序。

本教程假定您已经设置好了 Eclipse 环境。如果您是 Eclipse 和 Android 开发新手,我建议您先学习温度转换器教程,该教程可以在此处找到。

Using the Code

您可以通过遵循以下步骤来创建项目。如果您希望加载整个项目,请下载并解压项目文件,然后打开 Eclipse,选择 File->Import..->General->Existing Projects,然后选择 FountainGL 项目的根文件夹。

开始吧

启动 Eclipse(我使用的是 Eclipse Classic 版本 3.6.2)。

选择 File -> New -> Project -> Android -> Android Project

单击“下一步”。

填写字段,如下图所示。您可以使用 Android 2.1 或更高版本的任何版本。

单击“完成”。

项目创建后,将此图标添加到 AutoRing\res\drawable-hdpi 文件夹。您可以直接将其拖到 Eclipse 中的文件夹,也可以使用 Windows 资源管理器。覆盖该文件夹中已有的文件。

icon.png

如果您没有使用高分辨率设备(您可能正在使用),也可以将图标复制到 drawable-mdpi 和 drawable-ldpi 文件夹。

右键单击 FountainGL 项目,选择 New->Class。

输入名称、包和父类,如下图所示。还请勾选指示的两个复选框(尽管我们将覆盖这些方法存根)。

单击“完成”。

编写 FountainGLRenderer 类

此类将包含我们应用程序的大部分代码。

打开 FountainGLRenderer.java

删除该文件中的所有现有代码。

添加我们的应用程序所需的包名和导入。

package droid.fgl;

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

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

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;
import android.os.Handler;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.TextView;

创建 FountainGLRenderer 类。我们的类将实现 Renderer,因此我们可以将渲染代码和 OpenGL 回调合并到一个类中。

//extend GLSurfaceView and implement Renderer to keep all code in single class
public class FountainGLRenderer extends GLSurfaceView implements Renderer
{

添加喷泉和球体动画所需的变量。elapsedRealtime() 返回自系统启动以来的毫秒数。

private static float mAngCtr = 0; //for animation
long mLastTime = SystemClock.elapsedRealtime();

添加处理触摸/拖动事件所需的变量。

//for touch event - dragging
float mDragStartX = -1;
float mDragStartY = -1;
float mDownX = -1;
float mDownY = -1;

添加用于存储相机角度和位置的变量。我们在初始值上加 .0001,因为精确的右(或 0)角度可能导致除以零错误。我们可以在每次计算时检查 0,但这更简单。

//we add the .0001 to avoid divide by 0 errors
//starting camera angles
static float mCamXang = 0.0001f;
static float mCamYang = 180.0001f;
//starting camera position
static float mCamXpos = 0.0001f;
static float mCamYpos = 60.0001f;
static float mCamZpos = 180.0001f; 

添加用于设置相机视图方向的变量。

//distance from camera to view target
float mViewRad = 100;
//target values will get set in constructor
static float mTargetY = 0;
static float mTargetX = 0;
static float mTargetZ = 0; 

添加用于设置场景旋转角度的变量。

//scene angles will get set in constructor
static float mSceneXAng = 0.0001f;
static float mSceneYAng = 0.0001f; 

添加用于存储屏幕信息的变量。

float mScrHeight = 0; //screen height
float mScrWidth  = 0; //screen width
float mScrRatio  = 0; //width/height
float mClipStart = 1; //start of clip region 

添加用于角度转换的常量。

final double mDeg2Rad = Math.PI / 180.0; //Degrees To Radians
final double mRad2Deg = 180.0 / Math.PI; //Radians To Degrees 

添加 mResetMatrix 标志。每当相机向前或向后移动时,都会设置此标志,以便我们可以更新剪裁区域。onSurfaceChanged 会执行实际的更新。

boolean mResetMatrix = false; //set to true when camera moves

添加用于 FPS(每秒帧数)计算和显示的变量。请注意,TextView 也可用于显示调试信息。

int[] mFrameTime = new int[20]; 		//frames used for avg fps
int mFramePos = 0; 			//current fps frame position
long mStartTime = SystemClock.elapsedRealtime(); //for fps
int mFPSDispCtr = 0; 			//fps display interval
float mFPS = 0; 				//actual fps value

TextView mTxtMsg = null; 			//for displaying FPS
final FountainGLRenderer mTagStore = this; 	//for SetTextMessage
Handler mThreadHandler = new Handler(); 	//used in SetTextMessage

添加对象索引常量和缓冲区长度数组。我们将顶点数组存储在 GPU 内存中,读取时需要索引和长度。我们不能使用 0 作为索引,因为它已被 OpenGL 保留。

//constants for scene objects in GPU buffer
final int mFLOOR = 1;
final int mBALL  = 2;
final int mPOOL  = 3;
final int mWALL  = 4;
final int mDROP  = 5;
final int mSPLASH = 6;

//need to store length of each vertex buffer
int[] mBufferLen = new int[] {0,0,0,0,0,0,0}; //0/Floor/Ball/Pool/Wall/Drop/Splash

添加用于对象创建的参数。这些参数已针对我的 Hauwei Ideos 进行了优化。mBallHSliceCnt 必须是偶数,因为我们将球体渲染为两半。

//ball parameters
int mBallRad = 10; //radius
int mBallVSliceCnt = 32; //slices vertically - latitude line count
int mBallHSliceCnt = 32;  //slices horizontally - longitude line count - must be even

//fountain parameters
int mStreamCnt = 10; //should divide evenly into 360
int mDropsPerStream = 30; //should divide evenly into 180
int mRepeatLen = 180/mDropsPerStream; //distance loop for drop
float mArcRad = 30; //stream arc radius
//for storing drop positions //3 floats per vertex [x/y/z]
float[][] dropCoords = new float[mStreamCnt*mDropsPerStream][3];

//pool parameters
int mPoolSliceCnt = mStreamCnt; //side count
float mPoolRad = 57f; //radius

添加用于存储加速度计值的变量。加速度计可用于设置相机视角。mOrientation 存储当前的手机方向。

//accelerometer value set by activity
public float AccelZ = 0;
public float AccelY = 0;
int mOrientation = 0; //portrait\landscape

添加用于存储用户选项的变量。

//options menu defaults
public boolean ShowBall = true;
public boolean ShowFloor = true;
public boolean ShowFountain = true;
public boolean ShowPool = true;
public boolean RotateScene = true;
public boolean UseTiltAngle = false;
public boolean MultiBillboard = true;
public boolean ShowFPS = true;
public boolean Paused = false;

添加 FountainGLRenderer 的构造函数。将 activity 传递进来,以便我们可以修改布局并添加一个 TextView 用于显示 fps。setRenderer() 告诉 OpenGl 该类将执行渲染并初始化表面。我们还创建了加速度计的监听器,以便可以根据手机倾斜度调整视角。请注意,加速度计返回的 X\Y 值与方向无关,因此我们需要选择要使用的传感器。

FountainGLRenderer(Activity pActivity)
{
	super(pActivity);

	//use FrameLayout so we can put a TextView on top of the openGL screen
	FrameLayout layout = new FrameLayout(pActivity);

	//create view for text message (fps)
	mTxtMsg = new TextView(layout.getContext());
	mTxtMsg.setBackgroundColor(0x00FFFFFF); //transparent
	mTxtMsg.setTextColor(0xFF777777); //gray

	layout.addView(this); //add openGL surface
	layout.addView(mTxtMsg); //add text view
	pActivity.setContentView(layout);
	setRenderer(this); //initialize surface view

	//create listener for accelerometer sensor
	((SensorManager)pActivity.getSystemService
		(Context.SENSOR_SERVICE)).registerListener(
		new SensorEventListener() {
			@Override
			public void onSensorChanged(SensorEvent event) {
				//accelerometer does not change orientation 
				//so need to switch sensors
				if (mOrientation == 
				Configuration.ORIENTATION_PORTRAIT)
					AccelY = event.values[1]; //use Y sensor
				else
					AccelY = event.values[0]; //use X sensor
				AccelZ = event.values[2]; //Z
			}
			@Override
			public void onAccuracyChanged
				(Sensor sensor, int accuracy) {} //ignore this event
		},
		((SensorManager)pActivity.getSystemService(Context.SENSOR_SERVICE))
		.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0),
				SensorManager.SENSOR_DELAY_NORMAL);
}

添加 onSurfaceCreated 回调。这仅在表面首次创建时调用一次。我们设置背景颜色并为我们的对象创建顶点数组。

//called once
@Override
public void onSurfaceCreated(GL10 gl1, EGLConfig pConfig)
{
	GL11 gl = (GL11)gl1; //we need 1.1 functionality
	//set background frame color
	gl.glClearColor(0f, 0f, 0f, 1.0f); //black
	//generate vertex arrays for scene objects
	BuildFloor(gl);
	BuildBall(gl);
	BuildPool(gl);
	BuildWall(gl);
	BuildDrop(gl);
	BuildSplash(gl);
}

添加 BuildFloor 方法。此方法生成构成地板的三角形的顶点。地板是一个 7x7 网格与一个 6x6 网格合并。为了创建棋盘格图案,我们只绘制交替的方块。其他方块是空的。创建顶点数组后,将其存储在 GPU 内存中。

void BuildFloor(GL11 gl)
{
	//7*7+6*6 = 85 quads = 170 triangles = 510 vertices = 1530 floats[x/y/z]
	int sqrSize = 20;
	float vtx[] = new float[1530];
	int vtxCtr = 0;
	//we use the offset to produce the checkered pattern
	for (int x=-130, offset=0; x<130; x+=sqrSize, offset=sqrSize-offset)
	{
		for (int y=-130+offset; y<130; y+=(sqrSize*2))
		{
			//each square is 2 triangles = 6 vertices = 18 floats [x/y/z]
			vtx[vtxCtr]    = x;
			vtx[vtxCtr+ 1] =-2; //floor is 2 points below 0
			vtx[vtxCtr+ 2] = y;
			vtx[vtxCtr+ 3] = x+sqrSize;
			vtx[vtxCtr+ 4] =-2;
			vtx[vtxCtr+ 5] = y;
			vtx[vtxCtr+ 6] = x;
			vtx[vtxCtr+ 7] =-2;
			vtx[vtxCtr+ 8] = y+sqrSize;
			vtx[vtxCtr+ 9] = x+sqrSize;
			vtx[vtxCtr+10] =-2;
			vtx[vtxCtr+11] = y;
			vtx[vtxCtr+12] = x;
			vtx[vtxCtr+13] =-2;
			vtx[vtxCtr+14] = y+sqrSize;
			vtx[vtxCtr+15] = x+sqrSize;
			vtx[vtxCtr+16] =-2;
			vtx[vtxCtr+17] = y+sqrSize;
			vtxCtr+=18;
		}
	}

	StoreVertexData(gl, vtx, mFLOOR); //store in GPU buffer
}

添加 BuildBall 方法。球体被创建为一个网格(经度/纬度)。该方法上半部分计算球体中的所有顶点。下半部分安排顶点以生成三角形(每个四边形是 2 个三角形)。我们只为交替的四边形生成顶点。绘制球体时,我们将渲染相同的顶点两次,中间旋转球体并更改颜色。请注意,顶部和底部行被创建为四边形(4 个角),尽管它们被渲染为三角形(3 个角)。这是因为顶部行中的每个四边形都具有相同的顶部顶点。OpenGL 会忽略没有面积的三角形,因此性能不是问题。

void BuildBall(GL11 gl)
{
	//need to add 1 to include last vertex
	float x[][] = new float[mBallVSliceCnt+1][mBallHSliceCnt+1];
	float y[][] = new float[mBallVSliceCnt+1][mBallHSliceCnt+1];
	float z[][] = new float[mBallVSliceCnt+1][mBallHSliceCnt+1];

	//create grid of vertices as if sphere was laid flat
	//start at top, go down by slice (180 degrees top to bottom)
	for (int vCtr = 0; vCtr <= mBallVSliceCnt; vCtr++)
	{
		double vAng = 180.0 / mBallVSliceCnt * vCtr;
		float sliceRad = (float) (mBallRad * Math.sin(vAng * mDeg2Rad));
		float sliceY = (float) (mBallRad * Math.cos(vAng * mDeg2Rad));
		float vertexY = sliceY;
		float vertexX = 0;
		float vertexZ = 0;
		//go around entire sphere, 360 degrees
		for (int hCtr = 0; hCtr <= mBallHSliceCnt; hCtr++)
		{
			double hAng = 360.0 / mBallHSliceCnt * hCtr;
			vertexX = (float) (sliceRad * Math.sin(hAng * mDeg2Rad));
			vertexZ = (float) (sliceRad * Math.cos(hAng * mDeg2Rad));
			y[vCtr][hCtr]=vertexY+60;
			x[vCtr][hCtr]=vertexX;
			z[vCtr][hCtr]=vertexZ;
		}
	}
	int hCnt = x[0].length;
	int vCnt = x.length;;

	//calculate triangle vertices for each quad
	//colors are drawn separately, only create vertices for one color
	//16*8 = 128 quads = 256 triangles = 768 vertices = 2304 floats [x/y/z]
	float vtx[] = new float[mBallVSliceCnt*mBallHSliceCnt/2*2*3*3];
	int vtxCtr = 0;
	for (int vCtr = 1; vCtr < vCnt; vCtr++)
		//use %2 to create checker pattern, hCtr+=2 to skip quads
		for (int hCtr = 1+vCtr%2; hCtr < hCnt; hCtr += 2)
		{
			vtx[vtxCtr]    = x[vCtr-1][hCtr-1];
			vtx[vtxCtr+ 1] = y[vCtr-1][hCtr-1];
			vtx[vtxCtr+ 2] = z[vCtr-1][hCtr-1];
			vtx[vtxCtr+ 3] = x[vCtr][hCtr-1];
			vtx[vtxCtr+ 4] = y[vCtr][hCtr-1];
			vtx[vtxCtr+ 5] = z[vCtr][hCtr-1];
			vtx[vtxCtr+ 6] = x[vCtr-1][hCtr];
			vtx[vtxCtr+ 7] = y[vCtr-1][hCtr];
			vtx[vtxCtr+ 8] = z[vCtr-1][hCtr];
			vtx[vtxCtr+ 9] = x[vCtr][hCtr-1];
			vtx[vtxCtr+10] = y[vCtr][hCtr-1];
			vtx[vtxCtr+11] = z[vCtr][hCtr-1];
			vtx[vtxCtr+12] = x[vCtr-1][hCtr];
			vtx[vtxCtr+13] = y[vCtr-1][hCtr];
			vtx[vtxCtr+14] = z[vCtr-1][hCtr];
			vtx[vtxCtr+15] = x[vCtr][hCtr];
			vtx[vtxCtr+16] = y[vCtr][hCtr];
			vtx[vtxCtr+17] = z[vCtr][hCtr];
			vtxCtr+=18;
		}

	StoreVertexData(gl, vtx, mBALL); //store in GPU buffer
}

添加 BuildPool 方法。此方法将水创建为三角形扇形,其中每个三角形都有一个共同的中心顶点。

void BuildPool(GL11 gl)
{
	//center+10+end vertices = 12 vertices = 36 floats[x/y/z]
	float vtx[] = new float[(mPoolSliceCnt+2)*3];
	int vtxCtr = 0;
	//center vertex
	vtx[vtxCtr]   = 0;
	vtx[vtxCtr+1] = 4f; //6 points above floor
	vtx[vtxCtr+2] = 0;
	for (float fAngY = 0;fAngY <= 360;fAngY += 360/mPoolSliceCnt)
	{
		//vertices that create triangle fan, first vertex is repeated (0=360)
		vtxCtr+=3;
		vtx[vtxCtr] = mPoolRad*(float)Math.sin(fAngY*mDeg2Rad); //X
		vtx[vtxCtr+1] = 4f; //Y
		vtx[vtxCtr+2] = mPoolRad*(float)Math.cos(fAngY*mDeg2Rad); //Z
	}

	StoreVertexData(gl, vtx, mPOOL); //store in GPU buffer
}

添加 BuildWall 方法。此方法将水池壁创建为三角形带,其中每个三角形与相邻的三角形共享一边。请注意,半径比水池大 2 个点,以防止 Z 轴冲突(三角形重叠)。我们将在本教程稍后讨论 Z 轴冲突。

void BuildWall(GL11 gl)
{
	int wallSliceCnt = mPoolSliceCnt; //divides nicely into 360
	float wallRad = mPoolRad+2; //2 points larger than water to prevent Z-fight
	//wall is a triangle strip
	//defines start line then each square has 2 vertices
	//startline+10 squares = 22 vertices = 66 floats[x/y/z]
	float vtx[] = new float[(wallSliceCnt+1)*2*3];
	int vtxCtr = 0;
	//start line (left side of first square)
	//bottom vertex
	vtx[vtxCtr]   = 0;
	vtx[vtxCtr+1] = -1; //bottom of wall is below 0
	vtx[vtxCtr+2] = wallRad;
	//top vertex
	vtxCtr+=3;
	vtx[vtxCtr]   = 0;
	vtx[vtxCtr+1] = 9; //wall is 10 units high
	vtx[vtxCtr+2] = wallRad;
	//rotate around fountain center
	for (float ftnAngY = 360/wallSliceCnt; 
		ftnAngY <= 360; ftnAngY += 360/wallSliceCnt)
	{
		//right side of each square (left side is from previous square)
		//bottom vertex
		vtxCtr+=3;
		vtx[vtxCtr] = wallRad*(float)Math.sin(ftnAngY*mDeg2Rad); //X
		vtx[vtxCtr+1] = -1; //Y
		vtx[vtxCtr+2] = wallRad*(float)Math.cos(ftnAngY*mDeg2Rad); //Z
		//top vertex
		vtxCtr+=3;
		vtx[vtxCtr] = wallRad*(float)Math.sin(ftnAngY*mDeg2Rad); //X
		vtx[vtxCtr+1] = 9; //Y
		vtx[vtxCtr+2] = wallRad*(float)Math.cos(ftnAngY*mDeg2Rad); //Z
	}

	StoreVertexData(gl, vtx, mWALL); //store in GPU buffer
}

添加 BuildDrop 方法。此方法创建喷泉中单个水滴的顶点。每个水滴都具有相同的坐标。绘制喷泉时,我们使用 glTranslateglRotate 来调整每个水滴的位置/角度。

void BuildDrop(GL11 gl)
{
	//every drop has the same coordinates
	//we glRotate and glTranslate when drawing
	float vtx[] = {
		// X,  Y, Z
		  0f, 0f, 0,
		 -1f,-1f, 0,
		  1f,-1f, 0
		};

	StoreVertexData(gl, vtx, mDROP); //store in GPU buffer
}

添加 BuildSplash 方法。此方法创建所有飞溅三角形的顶点。单个飞溅只是围绕水滴落入水中点的三角形环。飞溅三角形永远不会移动,但在绘制时会在水池中按比例放大。我们将在本教程稍后讨论。

void BuildSplash(GL11 gl)
{
	//splashes never move
	//all splash triangles stored together
	int triCnt = 6; //must divide into 180
	int vtxCnt = mStreamCnt*9*triCnt;
	float[] vtx = new float[vtxCnt];
	int vtxCtr = 0;
	//for each stream
	for (float ftnAngY = 0;ftnAngY < 360;ftnAngY += 360/mStreamCnt)
	{
		//get coordinates of fountain drop (end of stream)
		float dropX = mArcRad*1.5f*(float)Math.sin(ftnAngY*mDeg2Rad);
		float dropZ = mArcRad*1.5f*(float)Math.cos(ftnAngY*mDeg2Rad);
		float mid = 0; //toggle for edge\middle vertex
		int triCtr = 0;
		//get angle for triangle edges and centers
		for (float sAngY = 0;sAngY < 360;sAngY += 360/(2*triCnt))
		{
			float realAngY = sAngY+ftnAngY; //shift angle to match stream angle
			//middle vertex have larger radius then edge vertices
			//use mid to toggle radius length
			float sX = (float)Math.sin(realAngY*mDeg2Rad)*(1+2*mid)+dropX;
			float sZ = (float)Math.cos(realAngY*mDeg2Rad)*(1+2*mid)+dropZ;
			
			vtx[vtxCtr] = sX;
			vtx[vtxCtr+1] = 0+mid*3; //Y, middle vertex is higher then edges
			vtx[vtxCtr+2] = sZ;
			
			if (mid%2==0) //edge vertex
			{
				if (triCtr == 0) //first triangle for this drop
				{  //connect to last triangle in loop
					vtx[vtxCtr+triCnt*9-3] = sX;
					vtx[vtxCtr+triCnt*9-2] = 0; //Y
					vtx[vtxCtr+triCnt*9-1] = sZ;
				}
				else //next triangle shares a corner
				{
					vtx[vtxCtr+3] = sX;
					vtx[vtxCtr+4] = 0; //Y
					vtx[vtxCtr+5] = sZ;
					vtxCtr+=3; //we set 2 corners, so skip ahead
				}
				triCtr++; //keep track of which triangle we're creating
			}
			else
				if (triCtr == triCnt) vtxCtr+=3; //for loop skips last vtx
			vtxCtr+=3; //next corner
			mid = 1-mid; //toggle
		}
	}
	
	StoreVertexData(gl, vtx, mSPLASH); //store in GPU buffer
}

添加 StoreVertexData 方法。此方法将每个对象的顶点数据存储在 GPU 内存中。使用 GPU 内存可以极大地提高性能,因为我们无需每次渲染场景时都将顶点数据传递给 GPU。顶点数据使用对象索引存储在内存中。渲染对象时,我们将使用相同的索引。我们还存储缓冲区长度,检索数据时需要该长度。GL_STATIC_DRAW 表示顶点不会被更改。

void StoreVertexData(GL11 gl, float[] pVertices, int pObjectNum)
{
	FloatBuffer buffer = ByteBuffer.allocateDirect
			(pVertices.length * 4) //float is 4 bytes
	 .order(ByteOrder.nativeOrder())// use the device hardware's native byte order
	 .asFloatBuffer()  // create a floating point buffer from the ByteBuffer
	 .put(pVertices);    // add the coordinates to the FloatBuffer

	(gl).glBindBuffer(GL11.GL_ARRAY_BUFFER, pObjectNum); //bind as current object
	buffer.position(0); //reset buffer position to buffer start
	//allocate memory and write buffer data
	(gl).glBufferData(GL11.GL_ARRAY_BUFFER, 
		buffer.capacity()*4, buffer, GL11.GL_STATIC_DRAW);
	(gl).glBindBuffer(GL11.GL_ARRAY_BUFFER, 0); //unbind from buffer
	mBufferLen[pObjectNum] = buffer.capacity()/3; //store for retrieval
}

添加 onSurfaceCreated 回调。这在 onSurfaceCreated 之后以及每次手机方向更改时调用。我们初始化视口和投影矩阵。glLoadIdentity() 会清除我们设置的任何变换或旋转。我们计算相机与场景中心之间的距离,以便设置剪裁区域。glFrustumf(稍后讨论)设置投影视图的参数。然后,我们启用深度测试,以便前景对象绘制在背景对象之上。我们将 ModelView 添加到矩阵堆栈,以便可以使用标准笛卡尔坐标绘制对象。最后,我们用当前手机方向设置 mOrientation

//this is called when the user changes phone orientation (portrait\landscape)
@Override
public void onSurfaceChanged(GL10 gl, int pWidth, int pHeight)
{
	gl.glViewport(0, 0, pWidth, pHeight); //the viewport is the screen
	// make adjustments for screen ratio, default would be stretched square
	mScrHeight = pHeight;
	mScrWidth = pWidth;
	mScrRatio = mScrWidth/mScrHeight;

	//set to projection mode to set up Frustum
	gl.glMatrixMode(GL11.GL_PROJECTION);	// set matrix to projection mode
	gl.glLoadIdentity();	// reset the matrix to its default state
	//calculate the clip region to minimize the depth buffer range (more precise)
	float camDist = (float)Math.sqrt(mCamXpos*mCamXpos+mCamYpos*
			mCamYpos+mCamZpos*mCamZpos);
	mClipStart = Math.max(2, camDist-185); 	//max scene radius is 185 points 
						//at corners
	//set up the perspective pyramid and clip points
	gl.glFrustumf(
		-mScrRatio*.5f*mClipStart,
		mScrRatio*.5f*mClipStart,
		-1f*.5f*mClipStart,
		1f*.5f*mClipStart,
		mClipStart,
		mClipStart+185+Math.min(185, camDist));

	//foreground objects are bigger and hide background objects
	gl.glEnable(GL11.GL_DEPTH_TEST);

	//set to ModelView mode to set up objects
	gl.glMatrixMode(GL11.GL_MODELVIEW);
	mOrientation = getResources().getConfiguration().orientation;
}

开始 onDrawFrame 回调。我们在这里渲染场景。OpenGL 系统会持续调用此方法。OpenGL 假定存在持续的动画,需要持续更新屏幕。可以通过调用 setRenderMode(RENDERMODE_WHEN_DIRTY) 然后调用 requestRender() 来渲染场景来关闭持续渲染。

我们将 gl1 参数强制转换为 OpenGL 1.1,以便获得额外的 1.1 功能。如果设备不支持 1.1,此强制转换将失败。根据 Android 网站,所有 Android 设备现在都支持 OpenGL ES 1.1。

//this is called continuously
@Override
public void onDrawFrame(GL10 gl1)
{
	GL11 gl = (GL11)gl1; //we need 1.1 functionality

添加标志检查,以防用户移动了相机。如果相机距离发生变化,我们需要更新剪裁区域,使其与场景对齐。onSurfaceChanged 会执行实际的更新。

if (mResetMatrix) //camera distance changed
{
	//recalc projection matrix and clip region
	onSurfaceChanged(gl, (int)mScrWidth, (int)mScrHeight);
	mResetMatrix = false;
}

添加代码以清除颜色和深度缓冲区并重置矩阵。颜色和深度缓冲区为每个帧重新计算。

//reset color and depth buffer
gl.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();   //reset the matrix to its default state

添加代码以计算基于手机倾斜度的 X 轴角度。我们将在本教程的后面讨论角度计算。AccelYAccelZ 在构造函数中创建的传感器监听器中设置。请注意,我们不允许角度超过 90 度,因为场景会颠倒。

if (UseTiltAngle) //use phone tilt to determine X axis angle
{
	//float hyp = (float)Math.sqrt(AccelY*AccelY+AccelZ*AccelZ);
	if (RotateScene) //rotate camera around 0,0,0
	{
		//calculate new X angle
		float HypLen = (float)Math.sqrt
			(mCamXpos*mCamXpos+mCamZpos*mCamZpos); //across floor
		mSceneXAng = 90-(float)Math.atan2(AccelY,AccelZ)*(float)mRad2Deg;
		// stop at 90 degrees or scene will go upside down
		if (mSceneXAng > 89.9) mSceneXAng = 89.9f;
		if (mSceneXAng < -89.9) mSceneXAng = -89.9f;

		float HypZLen = (float)Math.sqrt(mCamXpos*mCamXpos+
				mCamYpos*mCamYpos+mCamZpos*mCamZpos); //across floor
		//HypZLen stays same with new angle
		//move camera to match angle
		mCamYpos = HypZLen*(float)Math.sin(mSceneXAng*mDeg2Rad);
		float HypLenNew = HypZLen*
				(float)Math.cos(mSceneXAng*mDeg2Rad); //across floor
		mCamZpos *= HypLenNew/HypLen;
		mCamXpos *= HypLenNew/HypLen;
	}
	else //rotate camera
	{
		mCamXang = (float)Math.atan2(AccelY,AccelZ)*(float)mRad2Deg - 90;
		//don't let scene go upside down
		if (mCamXang > 89.9) mCamXang = 89.9f;
		if (mCamXang < -89.9) mCamXang = -89.9f;
		ChangeCameraAngle(0, 0); //set target position
	}
}

添加 gluLookAt 调用。这会告诉 OpenGL 系统相机的位置及其观察方向。目标变量的实际值无关紧要;只有从相机发出的方向才重要(如果相机在 0,0,0 处,则目标 1,2,3 的结果与目标 2,4,6 相同)。100 值用于设置向上向量。在我们的场景中,正 Y 轴是向上的,所以我们将 Y 设置为 100。它可以是任何正数。

//gluLookAt tells openGL the camera position and view direction (target)
//target is 0,0,0 for scene rotate
//Y is up vector, so we set it to 100 (can be any positive number)
GLU.gluLookAt(gl, mCamXpos, mCamYpos, mCamZpos, mTargetX, mTargetY, 
		mTargetZ, 0f, 100.0f, 0.0f);

添加代码以计算自上一帧渲染以来经过的时间。mAngCtr 基于时间变化设置。我们这样做是因为某些帧比其他帧花费的时间长,并且我们希望维持平滑的动画。较大的时间间隔会导致较大的角度跳跃,从而使动画赶上。如果动画暂停,我们会跳过角度更改。请注意,即使在暂停时,onDrawFrame 仍在持续调用。

//use clock to adjust animation angle for smoother motion
//if frame takes longer, angle is greater and we catch up
long now = SystemClock.elapsedRealtime();
long diff = now - mLastTime;
mLastTime = now;

//if paused, animation angle does not change
if (!Paused)
{
	mAngCtr += diff/100.0;
	if (mAngCtr > 360) mAngCtr -= 360;
}

调用 DrawSceneObjects。这是我们场景中的所有对象绘制到屏幕的地方。

DrawSceneObjects(gl);

通过添加计算和显示 FPS(每秒帧数)的代码来完成 onDrawFrame 方法。mFrameTime 数组存储过去 20 帧的帧时间。为了获得平均帧时间,我们只需获取当前帧与 20 帧之前的帧之间的时间,然后除以 20。FPS 显示每 10 帧更新一次。我们将在稍后详细讨论此计算。

if (ShowFPS) //average fps across last 20 frames
{
	//elapsedRealtime() returns milliseconds since phone boot
	int thisFrameTime = (int)(SystemClock.elapsedRealtime()-mStartTime);
	//mFrameTime array stores times for last 20 frames
	mFPS = (mFrameTime.length)*1000f/(thisFrameTime-mFrameTime[mFramePos]);
	mFrameTime[mFramePos] = (int)(SystemClock.elapsedRealtime()-mStartTime);
	if (mFramePos < mFrameTime.length-1) //move pointer
		mFramePos++;
	else //end of array, jump to start
		mFramePos=0;
	if (++mFPSDispCtr == 10) //update fps display every 10 frames
	{
		mFPSDispCtr=0;
		SetStatusMsg(Math.round(mFPS*100)/100f+" fps"); //2 decimal places
	}
}
}

添加 DrawSceneObjects 方法。此处绘制所有场景对象。对于每个对象(喷泉除外),我们设置颜色,然后调用 DrawObject 来渲染对象的顶点。对于球体,我们使用 mAngCtr 来设置当前的旋转角度。我们只存储了半个球体的顶点,所以我们旋转球体一个切片,然后用不同的颜色重新渲染相同的顶点。对于飞溅,飞溅三角形是在 Y=0 处创建的。我们想在 Y=0 处进行缩放,然后将缩放后的飞溅移动到表面。这里操作的顺序似乎是错误的(先移动后缩放),但似乎 OpenGL 会以相反的顺序执行某些操作。mRepeatLen 用于使飞溅与水滴运动同步。飞溅三角形不使用摄像机对准,因为它们环绕水滴。只有当喷泉和水池显示时,飞溅才会显示。

void DrawSceneObjects(GL11 gl)
	{
		if (ShowBall) 
		{
			//draw first color
			gl.glPushMatrix();
			gl.glColor4f(.5f, .5f, .5f, 1); //gray
			gl.glRotatef(mAngCtr, 0.0f, 1.0f, 0f); 
			DrawObject(gl, GL11.GL_TRIANGLES, mBALL);
			gl.glPopMatrix();
			
			//rotate by one slice and draw second color
			gl.glPushMatrix();
			gl.glColor4f(0.7f, 1f, 0.7f, 1f); //light green
			gl.glRotatef(mAngCtr+360f/mBallHSliceCnt, 0.0f, 1.0f, 0f);
			DrawObject(gl, GL11.GL_TRIANGLES, mBALL);
			gl.glPopMatrix();
		}
		
		if (ShowFountain) 
			DrawFountain(gl);
		
		if (ShowPool) //pool and wall
		{ 
			gl.glColor4f(0.2f, 0.0f, 0.0f, 1f); //dark red
			DrawObject(gl, GL11.GL_TRIANGLE_STRIP, mWALL);
			gl.glColor4f(0.2f, 0.0f, 0.6f, 1f); //blue\red
			DrawObject(gl, GL11.GL_TRIANGLE_FAN, mPOOL);
		}
		
		if (ShowFountain && ShowPool) //splashes if both
		{
			gl.glPushMatrix(); //scale only the splash triangles
			gl.glColor4f(.9f, 0.9f, 0.9f, 1f); //off-white
			gl.glTranslatef(0, 3, 0); //move splash to pool surface
			//the splash scales up then down (3 ⇒ 0 ⇒ 3)
			//use abs value of (-3 ⇒ 0 ⇒ 3), scale Y only
			gl.glScalef(1f, Math.abs((
				mRepeatLen/2f-mAngCtr%(mRepeatLen))*0.4f), 1f);
			DrawObject(gl, GL11.GL_TRIANGLES, mSPLASH);
			gl.glPopMatrix();
		}
		
		if (ShowFloor) 
		{
			gl.glColor4f(0.0f, 0.0f, 0.4f, 1f); //dark blue
			DrawObject(gl, GL11.GL_TRIANGLES, mFLOOR);
		}
	}

添加 DrawObject 方法。此方法渲染 GPU 缓冲区中指定对象索引的顶点。传入的形状类型(GL_TRIANGLES/GL_TRIANGLE_STRIP/GL_TRIANGLE_FAN)告诉 OpenGL 顶点在内存中的组织方式。

void DrawObject(GL11 gl, int pShapeType, int pObjNum)
{
	//activate vertex array type
	gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
	//get vertices for this object id
	gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, pObjNum);
	//each vertex is made up of 3 floats [x\y\z]
	gl.glVertexPointer(3, GL11.GL_FLOAT, 0, 0);
	//draw triangles
	gl.glDrawArrays(pShapeType, 0, mBufferLen[pObjNum]);
	//unbind from memory
	gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
}

添加 SetStatusMsg 方法。此方法使用新文本更新 TextViewmTagStore 用于将新文本传递给 Runnable 类。如果我们使用非 final 的局部变量,编译器会报错。我们需要使用 Runnable 类,这样文本更新就不会阻塞渲染过程。

public void SetStatusMsg(String pMsg)
{
	//mTagStore = this. We just need an object to pass text to the anonymous method
	mTagStore.setTag(pMsg);
	mThreadHandler.post(new Runnable() {
		public void run() { mTxtMsg.setText(mTagStore.getTag().toString()); }
		});
}

添加 SetShowFPS 方法。此方法设置 ShowFPS 标志并清除 TextView(以防 ShowFPSfalse)。

//if user hides FPS, then clear text
public void SetShowFPS(boolean pShowFPS)
{
	ShowFPS = pShowFPS;
	SetStatusMsg(""); //clear message
}

添加 SwapCenter 方法。此方法在相机和场景之间交替旋转中心。如果旋转设置为场景,相机始终看向场景中心(0,0,0),相机绕中心移动(我们实际上并不旋转场景)。如果旋转设置为相机,相机会转动,我们移动视点。

//rotate scene or rotate camera
public void SwapCenter()
{
	RotateScene = !RotateScene;
	if (RotateScene) //rotate around fountain
	{
		//calculate scene angles based on camera position
		//hypotenuse using 2 dimensions
		float hypLen = (float)Math.sqrt(mCamXpos*mCamXpos+
				mCamZpos*mCamZpos); //across floor
		mSceneYAng = (float)Math.atan2(mCamXpos,mCamZpos)*(float)mRad2Deg;
		//3rd dimension
		mSceneXAng = (float)Math.atan2(mCamYpos,hypLen)*(float)mRad2Deg;

		mTargetX = mTargetY = mTargetZ = 0; //camera always looks at 0,0,0
	}
	else //rotate camera
	{
		//camera angle is reverse of scene angle
		mCamYang = mSceneYAng+180;
		mCamXang = -mSceneXAng;
		ChangeCameraAngle(0,0); //set camera view target
	}
}

添加 ChangeSceneAngle 方法。当 RotateScene 标志设置且用户旋转视图时,将调用此方法。我们绕场景中心(0,0,0)移动相机,保持相同的距离。我们将在本教程的后面讨论角度计算。

//rotate camera around fountain
void ChangeSceneAngle(float pChgXang, float pChgYang)
{
	//hypotenuse using 2 dimensions
	float hypLen = (float)Math.sqrt(mCamXpos*mCamXpos+
			mCamZpos*mCamZpos); //across floor
	//process X and Y angles separately
	if (pChgYang != 0)
	{
		mSceneYAng += pChgYang;
		if (mSceneYAng < 0) mSceneYAng += 360;
		if (mSceneYAng > 360) mSceneYAng -= 360;
		//move camera according to new Y angle
		mCamXpos = hypLen*(float)Math.sin(mSceneYAng*mDeg2Rad);
		mCamZpos = hypLen*(float)Math.cos(mSceneYAng*mDeg2Rad);
	}

	if (pChgXang != 0)
	{
		//hypotenuse using all 3 dimensions
		float hypZLen = (float)Math.sqrt
			(hypLen*hypLen+mCamYpos*mCamYpos); // 0,0,0 to camera
		mSceneXAng += pChgXang;
		if (mSceneXAng > 89.9) mSceneXAng = 89.9f;
		if (mSceneXAng < -89.9) mSceneXAng = -89.9f;
		//hypZLen stays same with new angle
		//move camera according to new X angle
		mCamYpos = hypZLen*(float)Math.sin(mSceneXAng*mDeg2Rad);
		float HypLenNew = 
		    hypZLen*(float)Math.cos(mSceneXAng*mDeg2Rad); //across floor
		mCamZpos *= HypLenNew/hypLen;
		mCamXpos *= HypLenNew/hypLen;
	}
}

添加 ChangeCameraAngle 方法。当 RotateScene 标志未设置且用户旋转视图时,将调用此方法。我们绕其中心点旋转相机。然后,我们根据更新后的角度更新相机目标视点。相机与目标之间的距离保持不变。

//change camera view direction
void ChangeCameraAngle(float pChgXang, float pChgYang)
{
	mCamXang += pChgXang;
	mCamYang += pChgYang;
	//keep angle within 360 degrees
	if (mCamYang > 360) mCamYang -= 360;
	if (mCamYang < 0) mCamYang += 360;
	//don't let view go upside down
	if (mCamXang > 89.9) mCamXang = 89.9f;
	if (mCamXang < -89.9) mCamXang = -89.9f;
	// move view target according to new angles
	mTargetY = mCamYpos+mViewRad*(float)Math.sin(mCamXang*mDeg2Rad);
	mTargetX = mCamXpos+mViewRad*(float)Math.cos(mCamXang*mDeg2Rad)*
			(float)Math.sin(mCamYang*mDeg2Rad);
	mTargetZ = mCamZpos+mViewRad*(float)Math.cos(mCamXang*mDeg2Rad)*
			(float)Math.cos(mCamYang*mDeg2Rad);
}

添加 MoveCamera 方法。当相机向前或向后移动时调用此方法。如果设置了 RotateScene 标志,相机始终朝向场景中心(0,0,0)移动。它永远不会穿过中心。如果未设置 RotateScene,相机会朝向相机目标前后移动,并调整目标以匹配(到目标的距离保持不变)。我们将 mResetMatrix 设置为 true,以便在下一次帧渲染期间更新剪裁区域。

void MoveCamera(float pDist)
{
	//move camera along line of sight toward target vertex
	if (RotateScene) //move towards\away from 0,0,0
	{
		//distance from 0,0,0
		float curdist = (float)Math.sqrt(
				mCamXpos*mCamXpos +
				mCamYpos*mCamYpos +
				mCamZpos*mCamZpos);
		//if camera will pass center than reduce distance
		if (pDist < 0 && curdist + pDist < 0.01) //can't go to exact center
			pDist = 0.01f-curdist;//0.01 closest distance
		float ratio = pDist/curdist;
		float chgCamX = (mCamXpos)*ratio;
		float chgCamY = (mCamYpos)*ratio;
		float chgCamZ = (mCamZpos)*ratio;
		mCamXpos += chgCamX;
		mCamYpos += chgCamY;
		mCamZpos += chgCamZ;
	}
	else //move towards\away from target
	{
		//mViewRad is 100, so do percentage
		float ratio = pDist/mViewRad;
		float chgCamX = (mCamXpos-mTargetX)*ratio;
		float chgCamY = (mCamYpos-mTargetY)*ratio;
		float chgCamZ = (mCamZpos-mTargetZ)*ratio;
		mCamXpos += chgCamX;
		mCamYpos += chgCamY;
		mCamZpos += chgCamZ;
		mTargetX += chgCamX;
		mTargetY += chgCamY;
		mTargetZ += chgCamZ;
	}

	mResetMatrix = true; //recalc depth buffer range
}

添加 onTouchEvent 回调。当用户触摸屏幕或在屏幕上拖动时调用此方法。如果用户在没有拖动的情况下触摸并释放(拖动 5 像素或更少),我们认为这是一个点击,然后前后移动相机。如果用户拖动,我们将根据拖动距离更新视角。点击屏幕顶部会使相机向前移动。点击屏幕底部会使相机向后移动。

public boolean onTouchEvent(final MotionEvent pEvent)
{
	if (pEvent.getAction() == MotionEvent.ACTION_DOWN) //start drag
	{
		//store start position
		mDragStartX = pEvent.getX();
		mDragStartY = pEvent.getY();
		mDownX = pEvent.getX();
		mDownY = pEvent.getY();
		return true; //must have this
	}
	else if (pEvent.getAction() == MotionEvent.ACTION_UP) //drag stop
	{
		//if user did not move more than 5 pixels, assume screen tap
		if ((Math.abs(mDownX - pEvent.getX()) <= 5) && 
			(Math.abs(mDownY - pEvent.getY()) <= 5))
		{
			if (pEvent.getY() < mScrHeight/2.0) //top half of screen
				MoveCamera(-5); //move camera forward
			else if (pEvent.getY() > 
				mScrHeight/2.0) //bottom half of screen
				MoveCamera(5); //move camera back
		}
		return true; //must have this
	}
	else if (pEvent.getAction() == MotionEvent.ACTION_MOVE) //dragging
	{
		//to prevent constant recalcs, only process after 5 pixels
		//if user moves less than 5 pixels, we assume screen tap, not drag
		//we divide by 3 to slow down scene rotate
		if (Math.abs(pEvent.getX() - mDragStartX) > 5) //process Y axis rotation
		{
			if (RotateScene) //rotate around fountain
				ChangeSceneAngle(0, 
				(mDragStartX - pEvent.getX())/3f); //Y axis
			else //rotate camera
				ChangeCameraAngle(0, 
				(mDragStartX - pEvent.getX())/3f); //Y axis
			mDragStartX = pEvent.getX();
		}
		if (Math.abs(pEvent.getY() - 
			mDragStartY) > 5) //process X axis rotation
		{
			if (RotateScene) //rotate around fountain
				ChangeSceneAngle(
					(pEvent.getY() - mDragStartY)/3f, 0); //X axis
			else //rotate camera
				ChangeCameraAngle(
					(mDragStartY - pEvent.getY())/3f, 0); //X axis
			mDragStartY = pEvent.getY();
		}
		return true; //must have this
	}
	return super.onTouchEvent(pEvent);
}

添加 DrawFountain 方法。此方法计算(0,0,0)处的摄像机对准角度,并计算每个水滴的位置。我们假设每个水滴都沿着弧线运动,所以我们只需将弧线(180 度)除以水滴数,然后将其用作水滴位置。每个水滴只移动一小段距离(mRepeatLen),然后重复。mAngCtr(在 onDrawFrame 中设置)用于在每一帧中增加角度偏移量,从而创建动画。您可以在此处添加一些随机性,使每个水滴具有略微不同的路径,但目前,所有水滴都将遵循相同的弧线。

void DrawFountain(GL11 gl)
{
	//get billboard angles for 0,0,0
	//calculate angle from 0,0,0 to camera, used if single billboard
	float angY = 270-(float)Math.atan2(mCamZpos,mCamXpos)*
			(float)mRad2Deg; //around Y axis

	float hypLen = (float)Math.sqrt(mCamXpos*mCamXpos+
			mCamZpos*mCamZpos); //across floor
	float angX = (float)Math.atan2(mCamYpos,hypLen)*(float)mRad2Deg; //X axis

	int dropCtr = 0;
	//rotate around fountain center
	for (float ftnAngY = 0;ftnAngY < 360;ftnAngY += 360/mStreamCnt)
	{
		//draw each arc
		//arcAng will cycle through single segment and repeat
		float arcAng = mAngCtr%(mRepeatLen);
		for (;arcAng < 180;arcAng += mRepeatLen)
		{
			//default arc is half circle
			//use 0.75 to reduce arc width
			float dropRad = 0.75f*(mArcRad-mArcRad*
				(float)Math.cos(arcAng*mDeg2Rad));
			//use 1.5 to increase arc height
			dropCoords[dropCtr][1] = 1.5f*mArcRad*
				(float)Math.sin(arcAng*mDeg2Rad); //Y
			dropCoords[dropCtr][0] = dropRad*
				(float)Math.sin(ftnAngY*mDeg2Rad); //X
			dropCoords[dropCtr][2] = dropRad*
				(float)Math.cos(ftnAngY*mDeg2Rad); //Z
			dropCtr++;
		}
	}
	gl.glColor4f(0.5f, 0.5f, 1f, 1f); //light blue
	DrawDropTriangles(gl, angX, angY, dropCoords); //draw all triangles at once
}

添加 DrawDropTriangles 方法。此方法将每个水滴渲染为单独的三角形。pDropCoords 数组只有每个三角形的顶顶点。如果设置了 MultiBillboard 标志,我们将为每个水滴重新计算摄像机对准角度,以便每个水滴看起来都是一个完美的面朝相机的三角形。如果未设置 MultiBillboard,我们只使用(0,0,0)的摄像机对准角度。我们稍后将讨论摄像机对准。

//each triangle has the same dimensions, only location and rotation are different
void DrawDropTriangles(GL11 gl, float pAngX, float pAngY, float[][] pDropCoords)
{
	//DropCoords array only contains top vertex of each drop triangle
	//for each triangle, just translate to top vertex and redraw 
	//same triangle each time
	int TriCnt = pDropCoords.length; //triangle count
	// initialize vertex Buffer for triangle
	gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
	gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mDROP);
	gl.glVertexPointer(3, GL11.GL_FLOAT, 0, 0);

	for (int ctr = 0;ctr < TriCnt;ctr++)
	{
		gl.glPushMatrix(); //translate\rotate only affects this single triangle
		gl.glTranslatef(
			pDropCoords[ctr][0], pDropCoords[ctr][1],pDropCoords[ctr][2]);
		if (MultiBillboard) //calc each triangle billboard angle separately
		{
			float hypLen = 0;
			float distX = mCamXpos-pDropCoords[ctr][0];
			float distY = mCamYpos-pDropCoords[ctr][1];
			float distZ = mCamZpos-pDropCoords[ctr][2];

			//hypotenuse in 2D
			hypLen = 
				(float)Math.sqrt(distX*distX+distZ*distZ); //across floor
			pAngY = 270-(float)Math.atan2(distZ,distX)*(float)mRad2Deg;
			//3rd dimension
			pAngX = (float)Math.atan2(distY,hypLen)*(float)mRad2Deg;
		}
		gl.glRotatef(pAngY, 0, 1, 0);
		gl.glRotatef(pAngX, 1, 0, 0);
		gl.glDrawArrays(GL11.GL_TRIANGLES, 0, mBufferLen[mDROP]); //single drop
		gl.glPopMatrix(); //done with this triangle
	}
	gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0); //unbind from buffer
}

添加 ShowMaxDepthBits 方法。此方法将确定您设备上深度缓冲区的最大尺寸。它在我们的应用程序中未调用,但可用于测试。

void ShowMaxDepthBits() //resolution of depth buffer
{
	EGL10 egl = (EGL10)EGLContext.getEGL();
	EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
	EGLConfig[] conf = new EGLConfig[100]; //buffer for surface configs
	//get all possible configs for this OpenGL surface
	egl.eglGetConfigs(dpy, conf, 100, null);
	int maxBits = 0;
	int[] value = new int[1]; //for return value
	//scan all possible configs for maximum depth bit count
	for(int i = 0; i < 100 && conf[i] != null; i++)
	{
		//get depth bit size for this config
		egl.eglGetConfigAttrib(dpy, conf[i], EGL10.EGL_DEPTH_SIZE, value);
		maxBits = value[0]>maxBits ? value[0] : maxBits;
	}
	SetStatusMsg("DepthBits "+maxBits); //display
}

使用两个测试方法完成 FountainGLRenderer 类。这些方法在测试期间使用,但不再由应用程序调用。它们可能有助于调试或向场景添加其他对象。为了获得最大性能,最好使用 StoreVertexData/DrawObject 方法,尽管这需要更复杂的设置。

	//utility function for drawing a square
	void DrawQuad(GL11 gl, float[] pX, float[] pY, 
		float[] pZ) //clockwise starting top left
	{
		float[] vtx = new float[12];
		int i = 0;
		vtx[i++]=pX[0]; vtx[i++]=pY[0]; vtx[i++]=pZ[0];
		vtx[i++]=pX[1]; vtx[i++]=pY[1]; vtx[i++]=pZ[1];
		vtx[i++]=pX[3]; vtx[i++]=pY[3]; vtx[i++]=pZ[3];
		vtx[i++]=pX[2]; vtx[i++]=pY[2]; vtx[i++]=pZ[2];

		FloatBuffer buffer;
		ByteBuffer vbb = 
			ByteBuffer.allocateDirect(vtx.length * 4); //float is 4 bytes
		//use the device hardware's native byte order
		vbb.order(ByteOrder.nativeOrder());	
		//create a floating point buffer from the ByteBuffer
		buffer = vbb.asFloatBuffer(); 	
		buffer.put(vtx); //add the coordinates to the FloatBuffer
		buffer.position(0); //set the buffer to read the first coordinate
		//3 values per vertex [x/y/z]
		gl.glVertexPointer(3, GL11.GL_FLOAT, 0, buffer); 
		gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, 0, 4); //4 vertices
	}

	//draw single point
	void DrawPoint(GL11 gl, float pVertexX, float pVertexY, float pVertexZ)
	{
		FloatBuffer buffer;
		float[] vtx = new float[3];
		int i=0;
		vtx[i++]=pVertexX; vtx[i++]=pVertexY; vtx[i++]=pVertexZ;
		ByteBuffer vbb = 
			ByteBuffer.allocateDirect(vtx.length * 4); //float is 4 bytes
		//use the device hardware's native byte order
		vbb.order(ByteOrder.nativeOrder());	
		//create a floating point buffer from the ByteBuffer
		buffer = vbb.asFloatBuffer(); 	
		buffer.put(vtx); //add the coordinates to the FloatBuffer
		buffer.position(0); //set the buffer to read the first coordinate
		//3 values per vertex [x/y/z]
		gl.glVertexPointer(3, GL11.GL_FLOAT, 0, buffer); 
		gl.glDrawArrays(GL11.GL_POINTS, 0, 1); //only one point
	}
}

编写 FountainGLActivity 类

这是应用程序首次启动时使用的类。对于我们的应用程序,它用于创建 FountainGLRenderer 类并处理选项菜单。

打开 FountainGLActivity.java

删除该文件中的所有现有代码。

添加 Activity 所需的 package 名称和 import

package droid.fgl;

import droid.fgl.FountainGLRenderer;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.Window;
import android.view.WindowManager.LayoutParams;

开始 FountainGLActivity 类并添加两个变量。mRenderer 将是 FountainGLRenderer 实例的指针,mMenuList 将用于存储选项菜单的项。

public class FountainGLActivity extends Activity
{
	FountainGLRenderer mRenderer = null;
	MenuItem[] mMenuList = new MenuItem[10]; //options menu

添加 onCreate 回调。这在应用程序首次启动时以及手机方向更改时(纵向/横向)调用。首先,我们将应用程序设置为全屏并禁用屏幕保护程序,然后调用父构造函数。我们创建 FountainGLRenderer 实例,并传递 Activity 的实例。然后,我们加载用户首选项。如果首选项不可用,则使用默认值。然后,我们调用 SwapCenter 两次,以确保相机和场景角度设置正确。

@Override
 public void onCreate(Bundle savedInstanceState) {
	requestWindowFeature(Window.FEATURE_NO_TITLE); //hide title bar
	getWindow().setFlags(0xFFFFFFFF, //hide status bar and keep phone awake
		LayoutParams.FLAG_FULLSCREEN|LayoutParams.FLAG_KEEP_SCREEN_ON);

	super.onCreate(savedInstanceState);

	//onCreate is called when phone orientation changes
	//no need to recreate render class
	if (mRenderer == null)
		mRenderer = new FountainGLRenderer(this); //openGL surface

	//retrieve options
	SharedPreferences sp = getSharedPreferences("FountainGL", 0);
	mRenderer.ShowBall = sp.getBoolean("ShowBall", mRenderer.ShowBall);
	mRenderer.ShowFountain = sp.getBoolean("ShowFountain", mRenderer.ShowFountain);
	mRenderer.ShowFloor = sp.getBoolean("ShowFloor", mRenderer.ShowFloor);
	mRenderer.ShowPool = sp.getBoolean("ShowPool", mRenderer.ShowPool);
	mRenderer.ShowFPS = sp.getBoolean("ShowFPS", mRenderer.ShowFPS);
	mRenderer.UseTiltAngle = sp.getBoolean("UseTiltAngle", mRenderer.UseTiltAngle);
	mRenderer.RotateScene = sp.getBoolean("RotateScene", mRenderer.RotateScene);
	//calculate angle and position of camera
	mRenderer.SwapCenter();
	mRenderer.SwapCenter();
}

添加 onPrepareOptionsMenu 回调。每次显示菜单时都会调用此方法,以便我们可以根据需要更改菜单。所有用户选项都是布尔值开关,因此我们只需根据当前的开关设置来设置每个菜单选项。请注意,菜单最多只能容纳 5 项,因此最后五项将转到溢出菜单(用户必须单击“更多”)。前五项应该是最常用的。

//this method called every time menu is shown
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
	menu.clear(); //reset menu
	//set menu items based on current settings
	mMenuList[0] = menu.add((mRenderer.ShowBall?"Hide":"Show")+" Ball");
	mMenuList[1] = menu.add((mRenderer.ShowFloor?"Hide":"Show")+" Floor");
	mMenuList[2] = menu.add((mRenderer.ShowFountain?"Hide":"Show")+" Fountain");
	mMenuList[3] = menu.add((mRenderer.ShowPool?"Hide":"Show")+" Pool");
	mMenuList[4] = menu.add("Rotate "+(mRenderer.RotateScene?"Camera":"Scene"));
	mMenuList[5] = menu.add("Use "+(mRenderer.UseTiltAngle?"Touch":"Tilt")+" Angle");
	mMenuList[6] = menu.add((mRenderer.MultiBillboard?"Single":"Multi")+" Billboard");
	mMenuList[7] = menu.add((mRenderer.ShowFPS?"Hide":"Show")+" FPS");
	mMenuList[8] = menu.add(mRenderer.Paused?"Unpause":"Pause");
	mMenuList[9] = menu.add("Exit");
	return super.onCreateOptionsMenu(menu);
}

通过添加 onOptionsItemSelected 回调来完成 FountainGLActivity 类。当用户选择菜单项时调用此方法。对于 RotateScene 选项,我们调用 SwapCenter,因为当旋转中心更改时,我们需要重新计算相机或场景角度。对于其他选项,我们只切换当前设置。对于退出,调用 finish 来关闭应用程序。更改设置后,这些设置将被持久化,以便在下一次应用程序运行时保持不变。

	//listener for menu item clicked
	@Override
	public boolean onOptionsItemSelected(MenuItem item)
	{
		if (item == mMenuList[0]) //Show\Hide Ball
			mRenderer.ShowBall = !mRenderer.ShowBall;
		else if (item == mMenuList[1]) //Show\Hide Floor
			mRenderer.ShowFloor = !mRenderer.ShowFloor;
		else if (item == mMenuList[2]) //Show\Hide Fountain
			mRenderer.ShowFountain = !mRenderer.ShowFountain;
		else if (item == mMenuList[3]) //Show\Hide Pool
			mRenderer.ShowPool = !mRenderer.ShowPool;
		else if (item == mMenuList[4]) //Rotate Camera\Scene
			mRenderer.SwapCenter();
		else if (item == mMenuList[5]) //Use Touch\Tilt Angle
			mRenderer.UseTiltAngle = !mRenderer.UseTiltAngle;
		else if (item == mMenuList[6]) //Single\Multi Billboard
			mRenderer.MultiBillboard = !mRenderer.MultiBillboard;
		else if (item == mMenuList[7]) //Show\Hide FPS
			mRenderer.SetShowFPS(!mRenderer.ShowFPS);
		else if (item == mMenuList[8]) //Pause\Unpause
			mRenderer.Paused = !mRenderer.Paused;
		else if (item == mMenuList[9]) //Exit
			finish();

		//store options
		getSharedPreferences("FountainGL", 0).edit()
		 .putBoolean("ShowBall", mRenderer.ShowBall)
		 .putBoolean("ShowFountain", mRenderer.ShowFountain)
		 .putBoolean("ShowPool", mRenderer.ShowPool)
		 .putBoolean("ShowFloor", mRenderer.ShowFloor)
		 .putBoolean("ShowFPS", mRenderer.ShowFPS)
		 .putBoolean("UseTiltAngle", mRenderer.UseTiltAngle)
		 .putBoolean("RotateScene", mRenderer.RotateScene)
		 .commit();
		return super.onOptionsItemSelected(item);
	 }
}

这样就完成了应用程序代码,现在我们可以运行应用程序并查看我们创建的场景了。

构建项目(Project->Build All)。如果您启用了自动构建,项目将在每次保存源文件时重建。

运行应用程序

在 Eclipse 中,按 Ctrl-F11 启动应用程序(或按 F11 进行调试)。

几秒钟后(如果一切顺利),应用程序应该会在模拟器(或已连接的手机)上启动。

要更改模拟器的方向,请使用数字键盘 9(必须关闭 NumLock)。要测试手机倾斜功能,您需要使用您的实际手机。模拟器不会倾斜。

要退出应用程序,请使用手机上的返回按钮(或“退出”)或在 Eclipse 中选择 Run->Terminate。

要通过 APK 文件将应用程序安装到手机上。

在手机上,在 Settings->Applications 中,启用“未知来源”以允许手机安装非市场应用程序。

在 Eclipse 中,选择 File->Export..->Android-> Export Android Application。

单击“下一步”。

输入 FountainGL 作为项目名称。

单击“下一步”。

如果您已经有密钥库,请选择 Use existing keystore。如果没有,请按照以下步骤创建密钥库。

选择 Create new keystore。输入文件名(不需要扩展名)和密码。

单击“下一步”。

对于 Alias 和 Password,您可以使用在上一屏幕中输入的值。将有效期设置为 100 年。在 Name 字段中输入任何名称。如果您计划使用此密钥库发布任何应用程序,您应该使用您的真实信息。

单击“下一步”。

输入您的 apk 文件的文件名。

单击“完成”。

要将 apk 文件安装到手机上,请使用 android-sdk\platform-tools 文件夹中的 adb 工具。如果您不知道文件夹位置,只需在计算机上搜索 adb.exe

要安装 apk 文件,请使用此命令行

adb install C:\FountainGL.apk

您还可以使用 Android Market 上的(免费)安装程序应用程序之一,该应用程序允许您从手机的 SD 卡安装 apk 文件。

安装完成后,FountainGL 应该可以在手机的应用程序列表中找到。

恭喜您完成了新应用程序。请务必测试选项,看看 FPS 如何受到影响以及摄像机对准的效果。

本教程的其余部分将讨论此应用程序中使用的一些概念

计算角度和坐标

对于那些自高中以来就没有接触过几何的人来说,这里有一个快速回顾。我将 arccos\arcsin\arctan 缩写为 acos\asin\atan 以匹配 Java 函数。

    给定一个直角三角形

     h=√(x² + y²)
     x=h*cos(θ) θ=acos(x/h)
     y=h*sin(θ) θ=asin(y/h)
     y=x*tan(θ) θ=atan(y/x)

Atan2 函数

上面用于计算 x 和 y 的方程在 360 度范围内都是准确的。用于计算角度(a*xxx*)的函数仅在 180 度范围内准确。另外 180 度会产生相同的角度。

考虑下图

     这里我们有两个角度,45度和225度。如果我们从角度计算坐标,结果是正确的

h = √(5² + 5²) = 7.07

x = 7.07*cos(45) = 5
y = 7.07*sin(45) = 5

x = 7.07*cos(225) = -5
y = 7.07*sin(225) = -5

如果我们从坐标计算角度,我们会遇到一个问题
θ = acos(5/7.07) = 45 正确
θ = acos(-5/7.07) = 135 错误!我们想要 225(或 -135)。

这是因为公式中只使用了一个坐标符号。另一个使用的变量是斜边(h),它总是正的。如果我们尝试使用 atan,也会出现同样的问题:atan(5/5) = atan(-5/-5)

我们可以通过在代码中添加一个检查来解决这个问题
if (y<0) Angle = -Angle;

幸运的是,大多数编程语言都包含 Atan2 函数来解决这个问题。Atan2 在计算角度时会同时考虑两个坐标符号
θ = atan2(y,x)
θ = atan2(5,5) = 45 正确
θ = atan2(-5,-5) = -135 正确

请注意,在 Excel 中,ATAN2 函数的参数是颠倒的(x,y)。

在 3D 中工作

我们 OpenGL 程序中的场景基于 3D,因此我们需要在三维空间中计算角度和坐标。

     以下是从相机坐标计算场景角度的过程。

β = atan2(cz, cx)
h = √(cx² + cy²)
α = atan2(cy, h)
hz = √(cx² + cy² + cz²)

要从场景角度(和 hz)计算相机坐标,我们只需反转过程。

h = hz * cos(α)
cx = h * sin(β)
cz = h * cos(β)

当我们在旋转相机时,计算方法相同,只是相机位于中心,相机目标绕相机移动。

请注意,在 Java 中,这些数学函数以弧度计算角度,其中 PI (3.141592) 弧度 = 180 度。

另请注意,在图中,Z 轴沿着地板延伸。这是因为 Android 屏幕(相机)是从侧面观察场景的,而在 OpenGL 中,Z 轴穿过屏幕。

顶点排序

当坐标(顶点)存储在 GPU 缓冲区中时,它们可以以几种方式组织以创建不同的形状。所有形状都由三角形组成,某些三角形可以共享顶点,从而减少存储并加快渲染速度。OpenGL 将根据 glDrawArrays 调用中传递的常量来渲染坐标。在 FountainGL 项目中,我们使用了三种顶点排序类型。

GL_TRIANGLE_STRIP   GL_TRIANGLE_FAN    GL_TRIANGLES
     

GL_TRIANGLE_STRIP 用于每个三角形与相邻的三角形共享一边的情况。此排序用于创建我们应用程序中的水池壁。

GL_TRIANGLE_FAN 用于每个三角形共享一个共同中心顶点的情况。此排序用于创建我们应用程序中的水池水面。

GL_TRIANGLES 用于创建不相互连接的三角形,因此没有任何共享。这三种排序类型需要最多的存储和渲染时间。此排序用于创建我们应用程序中的地板、球体和喷泉水滴。

摄像机对准

摄像机对准是一种使 2D 对象看起来像 3D 的方法。这会提高性能,因为 OpenGL 引擎不需要渲染完整的 3D 对象。例如,一个球体看起来就像一个面向相机的圆形,并且圆形渲染起来要快得多。摄像机对准的诀窍是旋转 2D 对象,使其始终面向相机,并看起来与 3D 对象相同。
在我们的程序中,我们通过两种方式实现了摄像机对准:单摄像机对准和多摄像机对准。

单摄像机对准

在这里,我们在喷泉中心(0,0,0)计算到相机的摄像机对准角度,然后将该角度用于每个喷泉水滴。

我们可以更快地渲染喷泉,因为我们只需要计算一次摄像机对准角度。从远处看,效果还可以,但近距离看,我们的捷径就很明显了。水滴会远离相机旋转,不再看起来像三角形。

Distance        近景
             

多摄像机对准

在这里,我们为每个水滴计算摄像机对准角度,这会增加渲染时间。从远处看,场景与单摄像机对准渲染几乎相同,但在近距离观察时,效果明显更好。水滴面向相机,看起来是完整的三角形。

Distance        近景
             

在喷泉始终位于背景中的场景中,单摄像机对准方法就足够了,并且可以提高渲染时间。由于我们的应用程序允许相机靠近喷泉,因此我们为用户提供了多摄像机对准选项。

飞溅

ErrolErrol 的要求,在场景中添加了飞溅效果。飞溅是通过在飞溅点周围使用一圈三角形创建的。

要创建三角形顶点,我们只需围绕水滴点旋转并计算每个三角形顶点的坐标。我们使用 6 个三角形,因此我们将圆分成 12。对于奇数步,我们使用较小的半径计算三角形的边缘顶点。对于偶数步,我们使用较大的半径计算三角形的中间顶点。中间顶点也比边缘顶点高(在 Y 轴上),因此三角形从水池表面向上指向。

通过创建向上倾斜的三角形,我们可以通过在 Y 轴上缩放三角形来创建飞溅效果
gl.glScalef(1f, Math.abs((mRepeatLen/2f - mAngCtr%(mRepeatLen)) * 0.4f), 1f);
如果 mRepeatLen 为 10,则缩放因子从 5 ⇒ 0 ⇒ 5(我们取 -5 ⇒ 0 ⇒ 5 的绝对值)。我们只在 Y 轴上缩放,所以飞溅变得更高,而不是更宽。mAngCtr 用于与水滴周期保持同步。

整个场景的所有飞溅三角形都存储在一起,并同时绘制。绘制飞溅时未使用摄像机对准,因为飞溅从大多数角度看都还可以,而且我们节省了 CPU 时间。

透视和 glFrustumf

透视

在我们的应用程序中,我们使用 glFrustumf 方法来设置相机的透视。透视基本上是相机的视野(或角度)。较大的 FOV 允许相机看到场景的更多内容,但对象看起来更小,近距离和远距离对象的大小差异也更明显。您可以将其视为在相机上安装广角镜头。较小的 FOV 会产生相反的效果;相机能看到的场景更少,近距离和远距离对象的大小变化也更小。这与使用相机的变焦镜头产生的效果相同。

在这两张屏幕截图中,场景角度相同,但 FOV 的差异产生了明显不同的视图。

    
视锥体长度 = 1
大 FOV
     视锥体长度 = 2
小 FOV
    

glFrustumf

glFrustumf 调用使用 5 个参数来设置透视(我们将稍后讨论 zFar)。这些参数定义了透视的锥体(视锥体)。

glFrustumf(left, right, bottom, top, zNear, zFar)

创建透视时,锥体的形状很重要,而不是大小。只要比例相同,透视就相同

glFrustumf(-2, 2, -4, 4, 100, 500)

创建的透视与

glFrustumf(-4, 4, -8, 8, 200, 500)

这两个命令之间的区别在于剪裁区域。zNear 有助于确定透视的形状,但它也指示了近剪裁区域。任何比这条线更近的像素都不会显示。任何比 zFar 剪裁区域更远的像素也不会显示。zNear 不能为零或负数。

深度缓冲区

当 OpenGL 渲染场景时,它使用深度缓冲区根据距离相机的距离对像素进行排序。一旦像素排序完毕,OpenGL 将从远到近渲染它们,以便近处的对象会隐藏远处的对象(如果 OpenGL 知道像素将被隐藏,它也可以跳过像素)。

深度缓冲区由从 zNearzFar 的存储桶组成,场景中的所有像素区域都将进入其中一个存储桶。然后,这些存储桶将从远到近渲染。同一存储桶中的像素被认为距离相机相等,并将被渲染为单个平面。存储桶的数量始终相同,它们被分成剪裁区域(zNearzFar)。大剪裁区域拥有的存储桶数量与小剪裁区域相同,但存储桶会更大。

深度缓冲区的精度(存储桶数量)可能因设备而异。我的 Huawei 有一个 16 位缓冲区,表示 65,536 个存储桶。某些设备将拥有 24 位或 32 位缓冲区,这将提供更高的精度。

Z 轴冲突

重要的是要知道缓冲区的存储桶大小并不相等。存储桶在 zNear 处非常密集(较小的存储桶),在 zFar 处则分散开。这是为了使靠近相机的对象具有更高的精度,减少像素重叠的风险。重叠问题称为 Z 轴冲突。

以下是 FountainGL 应用程序在模拟器上运行时截取的两张屏幕截图。相机位于喷泉下方向上看,水池比地板高 6 个单位。

剪裁区域 = 300
glFrustumf(-1, 1, -1, 1, 1, 300)
   剪裁区域 = 1000
glFrustumf(-1, 1, -1, 1, 1, 1000)
    

正如您所见,右侧的图像看起来不正确。看起来水池正在穿过地板。问题在于剪裁区域非常大(1000),存储桶更大,而且彼此靠近的像素会落入同一个存储桶并被渲染在同一平面上。左侧图像看起来是正确的,因为剪裁区域小得多(300),创建了更小的存储桶和更好的深度分辨率。

存储桶大小

如前所述,相机附近的存储桶大小非常小(zNear),而远处的存储桶大小则非常大(zFar)。随着距离相机距离的增加,存储桶大小呈指数增长。如果我们设置 zNear1zFar100,以下是每 10 个单位增量的相对存储桶大小。

 

第一个存储桶太小以至于在条形图中都无法显示。最后一个存储桶,涵盖 0.0015 个单位,比第一个存储桶大 10,000 倍,第一个存储桶涵盖微小的 0.00000015 个单位。对于 16 位深度缓冲区,将有 65,536(2^16)个存储桶。

从图中可以看出,随着对象远离 zNear,场景的深度分辨率会迅速下降。创建场景时,目标是使对象靠近 zNear,并将剪裁区域(zFar-zNear)保持尽可能小。

移动剪裁区域

不幸的是,我们的应用程序允许相机绕整个场景移动,并从任何距离查看喷泉。如果我们使用 300 作为剪裁区域,当相机向后移动时,场景就会开始被剪裁,而使用 1000 则会导致过度的 Z 轴冲突。为了解决这个问题,当相机向前或向后移动时,我们会移动剪裁区域,以便剪裁区域的长度(和深度分辨率)保持不变。

近剪裁区域

远剪裁区域

多通道渲染

在某些情况下,要渲染的场景很大,我们不希望牺牲深度分辨率来正确渲染场景。这就是多通道渲染的用武之地。这是指您将场景分成块进行渲染,从远距离对象开始,到近距离对象结束。每个块将使用单独的深度缓冲区,以便每个块都能更准确地渲染(Z 轴冲突更少)。代价是渲染完整场景所需的额外处理时间。

使用远剪裁区域渲染远距离对象。

重置深度缓冲区,然后使用近剪裁区域渲染近距离对象。

使用单独的深度缓冲区创建的完整场景。

如果您想在 FountainGL 应用程序中测试多通道渲染,请注释掉对 glMatrixMode(两者)和 DrawSceneObjects 的现有调用,然后在 onDrawFramegluLookAt 调用之后插入此代码。如果您想在渲染区域之间看到一个间隙,请在下面的代码块中将 glFrustumf 的远剪裁区域设置为 98。在此场景中,所有对象的中心点都相同,因此我们实际上将相同的对象渲染了两次(像素将根据每个剪裁区域进行剪裁)。

//=== Multipass Render ===
//remove other calls to glMatrixMode and DrawSceneObjects
// --- Draw far objects ---
gl.glPushMatrix();
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glClear(GL11.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();
//set clip region for 100 - 500 units from camera
gl.glFrustumf(-mScrRatio*100, mScrRatio*100, -1f*100, 1f*100, 1f*100, 500);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();   // reset the matrix to its default state
GLU.gluLookAt(gl, mCamXpos, mCamYpos, mCamZpos, mTargetX, mTargetY, 
		mTargetZ, 0f, 100.0f, 0.0f);
DrawSceneObjects(gl); // <----- Far objects
gl.glPopMatrix();

// --- Draw near objects ---
gl.glPushMatrix();
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glClear(GL11.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();
//set clip region for 1 - 100 units from camera
gl.glFrustumf(-mScrRatio, mScrRatio, -1f, 1f, 1f, 100); //set to 98 for gap
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();   // reset the matrix to its default state
GLU.gluLookAt(gl, mCamXpos, mCamYpos, mCamZpos, mTargetX, mTargetY, 
		mTargetZ, 0f, 100.0f, 0.0f);
DrawSceneObjects(gl); // <----- Near objects
gl.glPopMatrix();

计算 FPS(每秒帧数)

FountainGL 应用程序中,FPS 是过去 20 帧的平均渲染时间。这是通过将每帧的结束时间存储在一个数组中来完成的。20 帧之后,我们取当前帧的结束时间,减去第一帧(第 1 帧)的结束时间,然后除以 20。在应用程序运行 20 帧之前,FPS 结果将不正确。

为了简化起见,让我们假设我们正在基于 10 帧进行计算。在此示例中,我们假设每帧需要 5 秒(在现实生活中会快得多)。

应用程序启动时,帧数组中没有帧数据,帧指针指向槽 0。

帧指针
数组槽 0 1 2 3 4 5 6 7 8 9
帧时间  0   0   0   0   0   0   0   0   0   0 

5 帧之后,我们填充了 5 帧数据,并在每帧移动了指针。第一帧结束于启动时间+100 秒。每帧需要 5 秒。由于零值,FPS 计算仍然是错误的。

帧指针
数组槽 0 1 2 3 4 5 6 7 8 9
帧时间 100 105 110 115 120  0   0   0   0   0 

9 帧之后,我们填充了 9 帧数据。

帧指针
数组槽 0 1 2 3 4 5 6 7 8 9
帧时间 100 105 110 115 120 125 130 135 140  0 

10 帧之后,我们填充了整个数组。FPS 计算现在将是正确的。当前帧将在 150 秒,因此 FPS 平均值为 10/(150-100) = 0.2 帧每秒。在 FPS 计算之后,我们将帧指针处的值设置为当前帧时间,因此槽 0 将设置为 150。

帧指针
数组槽 0 1 2 3 4 5 6 7 8 9
帧时间 100 105 110 115 120 125 130 135 140 145

15 帧之后,我们绕过数组,但帧指针仍然正确指向 10 帧之前。FPS 平均值为 10/(175-125) = 0.2 帧每秒。在 FPS 计算之后,我们将帧指针处的值设置为当前帧时间,因此槽 5 将设置为 175。

帧指针
数组槽 0 1 2 3 4 5 6 7 8 9
帧时间 150 155 160 165 170 125 130 135 140 145

如前所述,实际代码使用 20 帧,但这里我们使用 10 帧来节省一些空间。在应用程序中,FPS 值每 10 帧显示一次。如果您在设备上获得很高的 FPS,您可能希望增加帧数,这样 FPS 显示就不会变成一连串的数字。

其他想法

  • 喷泉水滴会显著增加渲染时间。我找不到绕过这个问题的方法,因为所有的水滴都在每一帧中移动和旋转。
  • 多通道渲染可能有一种更有效的方法。这个应用程序并没有真正受益于它,因为所有的对象都具有相同的 Y 轴。
  • 模拟器具有糟糕的深度精度。总是存在 Z 轴冲突。在我实施剪裁区域移动后,我的手机表现好得多。
  • 使用 VBO(GPU 内存)存储顶点带来了惊人的性能提升。如果仅渲染地板,与使用主内存缓冲区相比,FPS 提高了一倍。
  • 存储桶大小图表基于此网站的准确数据。我使用 Excel 计算/创建了条形图。
  • 3D 图形使用 3D Studio Max 创建。2D 图形使用 Paint.Net(免费软件)创建。
  • 本教程顶部的动画使用 DropBox(屏幕截图)和 UnFREEz(gif 创建器)创建。两者都是免费软件。
  • 请投票/评论。我感谢您的任何反馈。

资源

“分享你的知识。这是获得永生的一种方式。” - 达赖喇嘛

我想我们完成了。希望您觉得本教程很有用。如果您觉得有任何地方令人困惑,或者认为我遗漏了什么,请告诉我,以便我更新此页面。

© . All rights reserved.