OPhone 3D开发之天空盒
UI设计, 2010-09-25 14:44:08
标签 : OPhone 3d
在3D世界中,我们可以通过精细的模型来表现近处场景的细节,但对于远距离场景,比如天空、高山、日月星辰等这些“遥不可及”的对象来说,我们可以使用高质量贴图环绕组合成一个封闭的场景,给玩家以始终处在内部的感觉。在这种以贴图来渲染远景的方式中,最常用的技术是天空盒。
天空盒设计思想
天空盒的思想非常简单,将远景渲染成6种纹理贴图,每个纹理应用到立方体的一面,同时确保摄像机始终位于立方体的中心。渲染时,映射到天空盒的各个面上的图像将拼接在一起,从而在视觉上形成一个完整的天空。
组成天空盒的6张纹理,分别对应立方体的前、后、左、右、顶、底这6个面,如图1所示:
图1 组成天空盒的6张纹理
使用时,将这6张纹理以下图2的形式拼接起来,组合成一个立方体,并在渲染时将观察相机放置于立方体内部中心位置,这样无论相机怎样旋转观察,都会感觉到始终是处在这个盒子当中,从而营造出一种视觉上无限远的感觉。
图2 天空盒拼接方式
创建天空盒渲染数据
对于天空盒立方体来说,每个面分别对应于一个四边形,每个四边形由4个顶点,两个三角形组成。渲染时,指定每一个面的渲染数据及对应纹理,循环渲染所有面即可。
首先我们需要指定立方体每个面的顶点位置数据和纹理顶点数据,这里我们对操作顶点缓存对象进行了传统的glVertex3f()和glTexCoord2f()封装,以便于大家更好的理解传统的OpenGL绑定顶点数据与OPhone 3D中的OpenGL ES使用顶点数组缓存对象的使用方法差异。相关代码如下:
- //总共六个面,前后左右顶底
- private static final int VALID_FACE = 6;
- //每个面由2个三角形,4个顶点构成
- private static final int VERTEX_PER_FACE = 4;
- //存储天空盒渲染位置数据
- private FloatBuffer mBufPosition;
- //存储天空盒纹理数据
- private FloatBuffer mBufUV;
- /**
- * 创建天空盒渲染数据
- */
- private void CreateSkyBoxRenderBuffer() {
- //创建天空盒立方体顶点缓存
- mBufPosition = IBufferFactory.newFloatBuffer(VALID_FACE
- * VERTEX_PER_FACE * 3);
- //创建天空盒立方体纹理缓存
- mBufUV = IBufferFactory
- .newFloatBuffer(VALID_FACE * VERTEX_PER_FACE * 2);
- mBufPosition.position(0);
- mBufUV.position(0);
- //背面
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(10, 10, 10);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(10, -10, 10);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-10, -10, 10);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-10, 10, 10);
- //正面
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-10, 10, -10);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-10, -10, -10);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(10, -10, -10);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(10, 10, -10);
- //右面
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(10, 10, -10);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(10, -10, -10);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(10, -10, 10);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(10, 10, 10);
- //左面
- glTexCoord2f(1.0f, 0.0f);
- glVertex3f(-10, 10, 10);
- glTexCoord2f(1.0f, 1.0f);
- glVertex3f(-10, -10, 10);
- glTexCoord2f(0.0f, 1.0f);
- glVertex3f(-10, -10, -10);
- glTexCoord2f(0.0f, 0.0f);
- glVertex3f(-10, 10, -10);
- //顶部
- glTexCoord2f(0, 1);
- glVertex3f(-10, 10, 10);
- glTexCoord2f(0, 0);
- glVertex3f(-10, 10, -10);
- glTexCoord2f(1, 0);
- glVertex3f(10, 10, -10);
- glTexCoord2f(1, 1);
- glVertex3f(10, 10, 10);
- //底部
- glTexCoord2f(0, 1);
- glVertex3f(-10, -10, 10);
- glTexCoord2f(0, 0);
- glVertex3f(-10, -10, -10);
- glTexCoord2f(1, 0);
- glVertex3f(10, -10, -10);
- glTexCoord2f(1, 1);
- glVertex3f(10, -10, 10);
- mBufPosition.position(0);
- mBufUV.position(0);
- }
- /**
- * 将纹理坐标写入渲染缓存
- * @param u
- * @param v
- */
- private void glTexCoord2f(float u, float v) {
- mBufUV.put(u);
- mBufUV.put(v);
- }
- /**
- * 将位置坐标写入缓存
- * @param x
- * @param y
- * @param z
- */
- private void glVertex3f(float x, float y, float z) {
- mBufPosition.put(x);
- mBufPosition.put(y);
- mBufPosition.put(z);
- }
在上面的代码中,我们将天空盒的六个面的渲染数据都统一放置在一起,因此使用时,只需要绑定一次渲染数据,提交渲染时,指定当前四边面的在渲染缓存中的偏移即可,这样可以避免频繁切换渲染数据而造成的性能损失。
创建天空盒纹理
天空盒所用到的六张纹理,每一张均已独立的PNG图像的形式放置于程序资源文件中。每一张纹理的尺寸必须均为2的N次方,而且若无特殊要求,纹理宽高应该相等。在这里,我们使用的是256x256的PNG图像作为纹理对象。在创建天空盒对象时,将纹理贴图从外部资源文件中分别载入,并申请绑定成为OPhone 3D中的纹理对象。相关操作如下:
- // 定义的天空盒纹理外部资源索引
- private int[] mpStrTexPath = { R.drawable.sk_0back, R.drawable.sk_1front,
- R.drawable.sk_2right, R.drawable.sk_3left, R.drawable.sk_4top,
- R.drawable.sk_5bottom };
- // 载入后的纹理对象句柄
- private int[] mpTextures;
- /**
- * 载入天空盒纹理
- * @param context
- * @param gl
- */
- public void loadSkyBoxTextures(Context context, GL10 gl) {
- mpTextures = new int[mpStrTexPath.length];
- //循环创建所有天空盒纹理
- for (int i = 0; i < mpTextures.length; i++) {
- try {
- mpTextures[i] = TextureFactory.getTexture(context, gl,
- mpStrTexPath[i]);
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[i]);
- // 设置环绕模式
- gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
- gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
- } catch (GLException ex) {
- ex.printStackTrace();
- }
- }
- }
这里需要注意的是,在创建天空盒纹理时,必须要确保纹理的环绕模式为GL_CLAMP_TO_EDGE,否则会造成天空盒立方体四边面之间存在明显的接缝。
渲染天空盒
在渲染天空盒之前,需要将观察相机放置于天空盒立方体内部中心位置,这样无论观察者如何渲染,所看到的视野均处于天空盒的内部,从而营造出一种视觉上的无限连续状态。
- /**
- * 渲染天空盒之前将观察相机放置于天空盒中心
- * @param gl -
- * @param camera - 当前相机
- */
- public void renderSkyBox(GL10 gl, ICamera camera) {
- gl.glPushMatrix();
- {
- //将相机放置于天空盒中心位置
- gl.glTranslatef(camera.getCamPosition().x, camera
- .getCamPosition().y, camera.getCamPosition().z);
- //渲染天空盒
- mSkybox.renderSkyBox(gl);
- }
- gl.glPopMatrix();
- }
由于天空盒始终处于无限远处,场景中的物体都不会与天空盒相交,因此在渲染时,需要禁止深度测试以及禁用深度写入,同时也要禁用光照、雾化等其他类似效果。下面的代码展示了渲染天空盒之前的必要设置:
- //禁用光照
- gl.glDisable(GL10.GL_LIGHTING);
- //禁用混合
- gl.glDisable(GL10.GL_BLEND);
- //禁用深度测试
- gl.glDisable(GL10.GL_DEPTH_TEST);
- //禁止深度写入
- gl.glDepthMask(false);
实际渲染时,绑定好统一的位置缓存和纹理坐标缓存后,针对每个面绑定对应的纹理,之后采用glDrawArrays()渲染GL_TRIANGLE_FAN的形式,指定好当前四边面在整体渲染数据中的起始偏移,来最终渲染天空盒的每个面。相关代码如下:
- /**
- * 渲染天空盒对象
- * @param gl
- */
- public void renderSkyBox(GL10 gl) {
- // 禁用光照
- gl.glDisable(GL10.GL_LIGHTING);
- // 禁用混合
- gl.glDisable(GL10.GL_BLEND);
- // 禁用深度测试
- gl.glDisable(GL10.GL_DEPTH_TEST);
- gl.glDisable(GL10.GL_CULL_FACE);
- // 禁止深度写入
- gl.glDepthMask(false);
- //启用纹理贴图
- gl.glEnable(GL10.GL_TEXTURE_2D);
- // 启用必要渲染状态
- gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
- gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
- // 绑定渲染数据
- gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufPosition);
- gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufUV);
- // 记录当前四边面在整体渲染数据缓存中的起始偏移
- int offset = 0;
- // 后
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[0]);
- gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, offset, VERTEX_PER_FACE);
- offset += VERTEX_PER_FACE;
- // 前
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[1]);
- gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, offset, VERTEX_PER_FACE);
- offset += VERTEX_PER_FACE;
- // 右
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[2]);
- gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, offset, VERTEX_PER_FACE);
- offset += VERTEX_PER_FACE;
- // 左
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[3]);
- gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, offset, VERTEX_PER_FACE);
- offset += VERTEX_PER_FACE;
- // 顶
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[4]);
- gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, offset, VERTEX_PER_FACE);
- offset += VERTEX_PER_FACE;
- // 底部
- gl.glBindTexture(GL10.GL_TEXTURE_2D, mpTextures[5]);
- gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, offset, VERTEX_PER_FACE);
- //重新启用必要状态
- gl.glEnable(GL10.GL_DEPTH_TEST);
- gl.glDepthMask(true);
- gl.glEnable(GL10.GL_CULL_FACE);
- }
总结
随着OPhone 2.0平台的发布和推广,移动设备的3D能力越来越强大。天空盒技术是3D开发中常用的一个组件,本文通过介绍如何使用OPhone中提供的3D API来实现天空盒,希望能引领更多开发者步入神奇的3D世界。
文章所附源码可以在http://www.ophonesdn.com/forum/viewthread.jsp?tid=832 中下载。
作者介绍
薛永,专注于移动3D技术开发,目前正在完善跨PC/Iphone/Android NDK的统一3D引擎。