为了方便没有准备好梯子的同学,我把项目在CSDN上打包下载,不过更新会慢一些
如何用OpenGL绘制一个球呢?其实方法很多,网上一搜就能搜到一大把。
坐标系
要想知道怎么画,当然要把坐标系先搞清楚啦,上图:
这是世界坐标(看懂这张图需要一定想象力),Y轴是和重力方向相反的,X轴和水平面齐平,我们(相机)站在Z轴上,观察点是在球内的。
这是世界坐标(3D)对应的纹理坐标(2D),不过我们现在还不需要考虑这个,等第五章再来讨论纹理映射的事。
原理
绘制球的原则其实和绘制平面并没有什么区别,那么球的坐标要如何表示呢?我们知道三角形是OpenGL的基本形状,所以我们就将球面划分成许多个三角形,当我们划分的个数足够多时,整个球看起来就比较圆润了,像这样:
最后一张图是用矩形表示的终极形状(是不是很像一个地球仪),我们还需要把矩阵切分成两个三角形进行绘制。
坐标推导公式就不上了,估计也没人看,直接上代码吧。
下面是一张百度上爬来的图(坐标系标的不符合我们的要求,凑合着看吧):
题外话
在贴出球的代码之前,我们先将之前绘制平面视频的代码块提取出来,像这样:
package com.martin.ads.panoramaopengltutorial;
import android.opengl.GLES20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import static com.martin.ads.panoramaopengltutorial.ShaderUtils.checkGlError;
public class Plain {
private final FloatBuffer mVerticesBuffer;
private FloatBuffer mTexCoordinateBuffer;
private final float[] vertexData = {
1f,-1f,0f,
-1f,-1f,0f,
1f,1f,0f,
-1f,1f,0f
};
private final float[] textureVertexData = {
1f,0f,
0f,0f,
1f,1f,
0f,1f
};
public Plain() {
mVerticesBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
mVerticesBuffer.position(0);
mTexCoordinateBuffer = ByteBuffer.allocateDirect(textureVertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureVertexData);
mTexCoordinateBuffer.position(0);
}
public void uploadVerticesBuffer(int positionHandle){
FloatBuffer vertexBuffer = getVerticesBuffer();
if (vertexBuffer == null) return;
vertexBuffer.position(0);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
checkGlError("glVertexAttribPointer maPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
checkGlError("glEnableVertexAttribArray maPositionHandle");
}
public void uploadTexCoordinateBuffer(int textureCoordinateHandle){
FloatBuffer textureBuffer = getTexCoordinateBuffer();
if (textureBuffer == null) return;
textureBuffer.position(0);
GLES20.glVertexAttribPointer(textureCoordinateHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
checkGlError("glVertexAttribPointer maTextureHandle");
GLES20.glEnableVertexAttribArray(textureCoordinateHandle);
checkGlError("glEnableVertexAttribArray maTextureHandle");
}
public FloatBuffer getVerticesBuffer() {
return mVerticesBuffer;
}
public FloatBuffer getTexCoordinateBuffer() {
return mTexCoordinateBuffer;
}
public void draw() {
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
}
为什么要这样做呢?因为就像之前说的,绘制平面和绘制球体并没有本质的区别,所以我们完全可以抽象出一个类,只要把Plain换成Sphere,我们就完成了绘制模式的转变。
球体绘制
package com.martin.ads.panoramaopengltutorial;
import android.opengl.GLES20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import static com.martin.ads.panoramaopengltutorial.ShaderUtils.checkGlError;
public class SphereNoTexture {
private static final int sPositionDataSize = 3;
private FloatBuffer mVerticesBuffer;
private ShortBuffer indexBuffer;
private int mNumIndices;
public SphereNoTexture(float radius, int rings, int sectors) {
final float PI = (float) Math.PI;
final float PI_2 = (float) (Math.PI / 2);
float R = 1f/(float)rings;
float S = 1f/(float)sectors;
short r, s;
float x, y, z;
int numPoint = (rings + 1) * (sectors + 1);
float[] vertexs = new float[numPoint * 3];
short[] indices = new short[numPoint * 6];
//矩形的四个点
int t = 0, v = 0;
for(r = 0; r < rings + 1; r++) {
for(s = 0; s < sectors + 1; s++) {
x = (float) (Math.cos(2*PI * s * S) * Math.sin( PI * r * R ));
y = (float) Math.sin( -PI_2 + PI * r * R );
z = (float) (Math.sin(2*PI * s * S) * Math.sin( PI * r * R ));
vertexs[v++] = x * radius;
vertexs[v++] = y * radius;
vertexs[v++] = z * radius;
}
}
//球体绘制坐标索引,用于 glDrawElements
int counter = 0;
int sectorsPlusOne = sectors + 1;
for(r = 0; r < rings; r++){
for(s = 0; s < sectors; s++) {
indices[counter++] = (short) (r * sectorsPlusOne + s); //(a)
indices[counter++] = (short) ((r+1) * sectorsPlusOne + (s)); //(b)
indices[counter++] = (short) ((r) * sectorsPlusOne + (s+1)); // (c)
indices[counter++] = (short) ((r) * sectorsPlusOne + (s+1)); // (c)
indices[counter++] = (short) ((r+1) * sectorsPlusOne + (s)); //(b)
indices[counter++] = (short) ((r+1) * sectorsPlusOne + (s+1)); // (d)
}
}
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
vertexs.length * 4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(vertexs);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
indices.length * 2);
dlb.order(ByteOrder.nativeOrder());
indexBuffer = dlb.asShortBuffer();
indexBuffer.put(indices);
indexBuffer.position(0);
mVerticesBuffer=vertexBuffer;
mNumIndices=indices.length;
}
public void uploadVerticesBuffer(int positionHandle){
FloatBuffer vertexBuffer = getVerticesBuffer();
if (vertexBuffer == null) return;
vertexBuffer.position(0);
GLES20.glVertexAttribPointer(positionHandle, sPositionDataSize, GLES20.GL_FLOAT, false, 0, vertexBuffer);
checkGlError("glVertexAttribPointer maPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
checkGlError("glEnableVertexAttribArray maPositionHandle");
}
public FloatBuffer getVerticesBuffer() {
return mVerticesBuffer;
}
public void draw() {
indexBuffer.position(0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, mNumIndices, GLES20.GL_UNSIGNED_SHORT, indexBuffer);
}
}
和之前绘制平面的代码对比一下,除了构造函数以外,是不是满满的套路。。
public SphereNoTexture(float radius, int rings, int sectors)
我们给构造函数传入半径,纬度切分数,经度切分数,一般sectors应该是rings的两倍(当然不是也看不出明显的区别),我们按照经纬度切分出多个矩形,然后把矩形再划分成两个小的三角形(使用下标索引的方式)
为什么还要rings + 1呢(sector类似),因为我们的最后矩形的右边界还应该和第一个矩形的左边界重合。
除了上面所说的,球体的绘制还有很多需要注意的小细节,大家可以对着代码慢慢揣摩
因为我们还没有做纹理映射,方便起见,我们将球的颜色设置为白色。
如果我们已经设置好了MVP矩阵(第五章),那么我们就能看到这样子的一个球:
呵呵,这真的是一个球,不是个圆啊!!!算了,加点噪声好了,这样明显一些:
当然,如果用棋盘纹理来表示也是棒棒哒(虽然这不是重点):

本文介绍了如何使用OpenGL ES 2.0在Android平台上绘制一个360度全景视频播放器的球体。通过将球面划分为多个三角形并利用坐标推导,实现球体的圆润效果。文章提到了坐标系的理解、绘制原理,并给出了代码示例,展示了最终的渲染效果。

1万+

被折叠的 条评论
为什么被折叠?



