安卓-OpenGL-ES-高级教程-全-

安卓 OpenGL ES 高级教程(全)

原文:Pro OpenGL ES for Android

协议:CC BY-NC-SA 4.0

零、简介

1985 年,我带回家一台崭新闪亮的海军准将 Amiga 1000,大约在它们发布一周后。它配备了巨大的 512K 内存、可编程色彩映射表、摩托罗拉 68K CPU 和现代多任务操作系统,写满了“令人敬畏”的字样。当然是打个比方。我想它可能会成为一个天文学项目的良好*台,因为我现在可以控制那些恒星的颜色,而不是不得不满足于像 Hercules 或 C64 这样的人强加给我的蹩脚的固定调色板。所以我编写了一个 24 行的基本程序来绘制一个随机的星域,关掉灯,心想,“哇!我打赌我能为那东西写一个很酷的天文学程序!”26 年后,我仍在研究它,并希望有一天能搞清楚。那时候,我的梦想设备是一种我可以放入口袋,需要时拿出来,对准天空告诉我正在看什么星星或星座的东西。

这叫智能手机。

我先想到的。

虽然这些东西很适合播放音乐、打电话或向小猪扔小鸟,但当你接触到 3D 东西时,它真的会发光。毕竟,3D 就在我们周围——除非你是一个海盗,并且戴上了眼罩,在这种情况下,你的深度感知将非常有限。啊啊啊。

另外,向人们炫耀 3D 应用很有趣。他们会“明白”事实上,他们将会得到比所有孩子都在谈论的地膜购买指南应用更多的东西。(除非他们在 3D 中展示他们的覆盖物,但那会浪费一个完美的维度。)

因此,3D 应用看起来很有趣,互动起来很有趣,编程起来也很有趣。这让我想到了这本书。我绝不是这方面的专家。真正的大师是那些可以在早餐前击倒几个 NVIDIA 驱动程序,在午餐前击倒 4 维超立方体模拟器,并在晚上的 SyFy 上的 Firefly 马拉松之前将光晕移植到 TokyoFlash 手表的人。我做不到。但我是一个体面的作家,对这个主题有足够的工作知识使我无害,并且知道如何拼写“3D”所以我们在这里。

首先,这本书是为那些想要至少学习一点 3D 语言的有经验的 Android 程序员准备的。至少在下一次游戏程序员的鸡尾酒会上,你也可以和他们中最好的人一起笑四元数笑话。

这本书涵盖了 3D 理论和使用行业标准 OpenGL ES toolkit 实现小型设备的基础知识。虽然 Android 可以支持这两种风格——版本 1.x 用于简单的方式,版本 2.x 用于那些喜欢获得细节的人——但我主要介绍前者,除了在最后一章介绍后者和可编程着色器的使用。

第一章沿着漫长而曲折的计算机图形学历史介绍 OpenGL ES。第二章是基本 3D 渲染背后的数学,而第三章到第八章将带你慢慢了解所有图形程序员最终会遇到的各种问题,比如如何投射阴影、渲染多个 OpenGL 屏幕、添加镜头光晕等等。最终这变成了一个简单的(S-I-M-P-L-E!)由太阳、地球和一些恒星组成的太阳系模型——传统的 3D 练习。第九章着眼于最佳实践和开发工具,第十章作为 OpenGL ES 2 和着色器使用的简要概述。

所以,玩得开心点,给我发一些 M & Ms,当你在玩的时候,可以随意看看我自己的应用:遥远的太阳 3。是的,这就是 1985 年在 Commodore Amiga 1000 上启动的同一应用,它是一个 24 行的 basic 程序,在屏幕上随机画了几百颗星星。

现在更大了。

迈克·史密斯威克

一、计算机图形学:从那时到现在

要预测未来,欣赏现在,你必须了解过去。

——可能是某个时候某个人说的

计算机图形一直是软件世界的宠儿。外行人更容易欣赏计算机图形,比如说,把排序算法的速度提高 3%,或者给电子表格程序增加自动色调控制。你可能会听到更多的人说“Coooool!”比起 Microsoft Word 中的 Visual Basic 脚本(当然,除非 Microsoft Word 中的 Visual Basic 脚本可以渲染土星;那真的会很酷)。当这些渲染图放在一个你可以放在后兜里随身携带的设备上时,酷的因素就更大了。让我们面对现实吧——硅谷的人们让科幻电影的艺术导演的日子变得非常艰难。毕竟,想象一下设计一个看起来比三星 Galaxy Tab 或 iPad 更具未来感的道具有多难。(甚至在苹果 iPhone 上市销售之前,美国广播公司 Lost 的道具部门就借用了一些苹果的屏幕图标,用于一名神秘的直升机飞行员携带的双向无线电中。)

如果你正在读这本书,那么你很可能已经有了一台基于 Android 的设备,或者正在考虑在不久的将来买一台。如果你有一个,现在就把它放在你的手里,想想这是 21 世纪工程学的一个奇迹。数百万小时的工作时间,数十亿美元的研究,数百年的加班,大量的通宵工作,以及大量喝着烈酒,穿着 t 恤,热爱漫画的工程师在夜晚的寂静中编写代码,这些都是为了制作那个小小的玻璃和塑料奇迹盒,这样你就可以在重播《流言终结者》时玩《愤怒的小鸟》。

你的第一个 OpenGL ES 程序

一些软件指南书籍会仔细地为他们的特定主题(“无聊的东西”)建立案例,但在第 655 页左右就会看到编码和例子(“有趣的东西”)。其他人会立即开始做一些练习来满足你的好奇心,把无聊的事情留到以后再做。本书将试图属于后一类。

注: OpenGL ES 是一种基于 OpenGL 库的 3D 图形标准,于 1992 年出现在硅图形实验室。它被广泛应用于整个行业,从运行游戏的便携式机器到为 NASA 运行流体动力学模拟的超级计算机(以及玩非常非常快的游戏)。ES 系列代表嵌入式系统,意为小型、便携、低功耗的设备。

安装后,Android SDK 附带了许多非常好的简明示例,从*场通信(NFC)到 UI,再到 OpenGL ES 项目。我们最早的例子将利用您将在广泛的 ApiDemos 代码库中找到的那些例子。不像它的 Apple-lovin 表亲 Xcode,它有一个很好的项目向导选择,包括一个 OpenGL 项目,不幸的是,Android dev 系统几乎没有。因此,与库比蒂诺的人们相比,我们不得不从一点点劣势开始。所以,你需要创建一个通用的 Android 项目,我相信你已经知道如何去做。完成后,添加一个名为Square.java的新类,由清单 1–1 中的代码组成。下面是详细的分析清单。

清单 1–1。 一个使用 OpenGL ES 的 2D 广场

`package book.BouncySquare;

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

import javax.microedition.khronos.opengles.GL10;                            //1
import javax.microedition.khronos.opengles.GL11;

/**
 * A vertex shaded square.
 */
class Square
{
    public Square()
    {
        float vertices[] =                                                   //2 {
            -1.0f, -1.0f,
             1.0f, -1.0f,
            -1.0f,  1.0f,
             1.0f,  1.0f
        };

byte maxColor=(byte)255;

byte colors[] =                                                      //3
        {
            maxColor,maxColor,       0,maxColor,
            0,       maxColor,maxColor,maxColor,
            0,              0,       0,maxColor,
            maxColor,       0,maxColor,maxColor
        };

byte indices[] =                                                     //4
        {
            0, 3, 1,
            0, 2, 3
        };

ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);     //5
        vbb.order(ByteOrder.nativeOrder());
        mFVertexBuffer = vbb.asFloatBuffer();
        mFVertexBuffer.put(vertices);
        mFVertexBuffer.position(0);

mColorBuffer = ByteBuffer.allocateDirect(colors.length);
        mColorBuffer.put(colors);
        mColorBuffer.position(0);

mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
        mIndexBuffer.put(indices);
        mIndexBuffer.position(0);

}

public void draw(GL10 gl)                                                //6
    {
        gl.glFrontFace(GL11.GL_CW);                                          //7
        gl.glVertexPointer(2, GL11.GL_FLOAT, 0, mFVertexBuffer);             //8
        gl.glColorPointer(4, GL11.GL_UNSIGNED_BYTE, 0, mColorBuffer);        //9
        gl.glDrawElements(GL11.GL_TRIANGLES, 6,                              //10
        GL11.GL_UNSIGNED_BYTE, mIndexBuffer);
        gl.glFrontFace(GL11.GL_CCW);                                         //11
    }     private FloatBuffer mFVertexBuffer;
    private ByteBuffer   mColorBuffer;
    private ByteBuffer  mIndexBuffer;
}`

在我进入下一阶段之前,我将分解清单 1–1 中构建多色正方形的代码:

  • Java 拥有几种不同的 OpenGL 接口。父类仅仅叫做GL,而 OpenGL ES 1.0 使用的是GL10,1.1 版本导入为GL11,如第 1 行所示。如果您的图形硬件支持,您还可以通过 GL11ExtensionPack 提供的GL10Ext package访问一些扩展。后来的版本仅仅是早期版本的子类;然而,仍然有一些调用被定义为只接受GL10对象,但是如果你正确地转换对象,这些调用也可以工作。
  • 在第 2 行中,我们定义了我们的正方形。你很少会这样做,因为许多对象可能有数千个顶点。在这些情况下,您可能会从任意数量的 3D 文件格式导入它们,如 Imagination Technologies 的 POD 文件、3D Studio 的.3ds文件等等。在这里,因为我们描述的是一个 2D 正方形,所以只需要指定 x 和 y 坐标。如你所见,这个正方形边长是两个单位。
  • 颜色的定义类似,但在这种情况下,在第 3ff 行中,每种颜色有四种成分:红色、绿色、蓝色和 alpha(透明度)。这些直接映射到前面显示的四个顶点,所以第一个颜色与第一个顶点相配,依此类推。您可以使用颜色的浮点或固定或字节表示,如果要导入非常大的模型,后者可以节省大量内存。因为我们使用字节,颜色值从 0 到 255,这意味着第一个颜色设置红色为 255,绿色为 255,蓝色为 0。这将使一个可爱的,否则眩目的黄色阴影。如果使用浮点或定点,它们最终会在内部转换为字节值。与它的桌面兄弟不同,它可以渲染四边的对象,OpenGL ES 仅限于三角形。在第 4ff 行中,创建了连接性数组。这将顶点匹配到特定的三角形。第一个三元组表示顶点 0、3 和 1 构成三角形 0,而第二个三角形由顶点 0、2 和 3 组成。
  • 一旦创建了颜色、顶点和连接性数组,我们可能需要处理这些值,将它们的内部 Java 格式转换成 OpenGL 可以理解的格式,如第 5ff 行所示。这主要确保字节的排序是正确的;否则,根据硬件的不同,它们的顺序可能会相反。
  • 第 6 行中的draw方法由SquareRenderer.drawFrame()调用,稍后介绍。
  • 第 7 行告诉 OpenGL 顶点如何排列它们的面。为了让你的软件发挥出最佳性能,顶点排序是至关重要的。这有助于在整个模型中保持统一的顺序,这可以表明三角形是朝向还是背离您的视点。后者被称为背面三角形物体的背面,所以它们可以被忽略,大大减少渲染时间。因此,通过指定三角形的正面是GL_CW,或顺时针,所有逆时针三角形被剔除。注意,在第 11 行,它们被重置为GL_CCW,这是默认值。
  • 在第 8、9 和 10 行中,指向数据缓冲区的指针被移交给渲染器。对glVertexPointer()的调用指定了每个顶点的元素数量(在本例中是两个),数据是浮点的,并且“步幅”是 0 字节。数据可以是八种不同的格式,包括浮点、固定、整数、短整数和字节。后三种有签名和无签名两种形式。Stride 是一种方便的方法,只要数据结构不变,就可以将 OpenGL 数据与您自己的数据交错。Stride 仅仅是打包在 GL 数据之间的用户信息的字节数,因此系统可以跳过它到它将理解的下一位。
  • 在第 9 行,颜色缓冲区以四个元素的大小发送,RGBA 四元组使用无符号字节(我知道,Java 没有无符号的东西,但 GL 不需要知道),它也有一个 stride=0。
  • 最后,给出实际的draw命令,它需要连接性数组。第一个参数表示几何图形的格式,换句话说,三角形、三角形列表、点或线。
  • 第 11 行让我们成为一个好邻居,并在先前的对象使用默认值的情况下将正面排序重置回GL_CCW

现在我们的广场需要一个驱动程序和方式来在屏幕上展示它多彩的自我。创建另一个名为SquareRenderer.java的文件,并用清单 1–2 中的代码填充它。

清单 1–2。 我们第一个 OpenGL 项目的驱动

`package book.BouncySquare;

import javax.microedition.khronos.egl.EGL10;                                 //1
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView;                                         //2
import java.lang.Math; class SquareRenderer implements GLSurfaceView.Renderer
{
    public SquareRenderer(boolean useTranslucentBackground)
    {
        mTranslucentBackground = useTranslucentBackground;
        mSquare = new Square();                                              //3
    }

public void onDrawFrame(GL10 gl)                                         //4
    {

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);     //5

gl.glMatrixMode(GL10.GL_MODELVIEW);                                  //6
        gl.glLoadIdentity();                                                 //7
        gl.glTranslatef(0.0f,(float)Math.sin(mTransY), -3.0f);               //8

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);                        //9
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

mSquare.draw(gl);                                                    //10

mTransY += .075f;
    }

public void onSurfaceChanged(GL10 gl, int width, int height)             //11
    {
         gl.glViewport(0, 0, width, height);                                 //12

float ratio = (float) width / height;
         gl.glMatrixMode(GL10.GL_PROJECTION);                                //13
         gl.glLoadIdentity();
         gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);                         //14
    }

public void onSurfaceCreated(GL10 gl, EGLConfig config)                  //15
    {
        gl.glDisable(GL10.GL_DITHER);                                        //16

gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,                       //17
                 GL10.GL_FASTEST);

if (mTranslucentBackground)                                     //18
         {
             gl.glClearColor(0,0,0,0);                       
         }
             else
         {              gl.glClearColor(1,1,1,1);
         }
         gl.glEnable(GL10.GL_CULL_FACE);                                     //19
         gl.glShadeModel(GL10.GL_SMOOTH);                                    //20
         gl.glEnable(GL10.GL_DEPTH_TEST);                                    //21
    }
    private boolean mTranslucentBackground;
    private Square mSquare;
    private float mTransY;
    private float mAngle;
}`

这里发生了很多事情:

  • 第 1 行中的 EGL 库将 OpenGL 绘图表面绑定到系统,但是在本例中被隐藏在GLSurfaceview中,如第 2 行所示。EGL 主要用于分配和管理绘图表面,是 OpenGL ES 扩展的一部分,因此它是*台无关的。
  • 在第 3 行,分配并缓存了 square 对象。
  • 第 4 行中的onDrawFrame()是根刷新方法;每次都是这样构建图像,每秒钟很多次。第一个调用通常是清除整个屏幕,如第 5 行所示。考虑到一个帧可以由几个组件构成,您可以选择每帧清除哪些组件。颜色缓冲区保存所有的 RGBA 颜色数据,而深度缓冲区用于确保较*的项目正确地遮挡了较远的项目。
  • 第 6 行和第 7 行开始使用实际的 3D 参数;这些细节将在后面介绍。这里所做的只是设置值,以确保示例几何图形立即可见。
  • 接下来,第 8 行将盒子上下*移。为了获得漂亮、*滑的运动,实际的*移值基于正弦波。值mTransY仅用于产生范围从 1 到+1 的最终上下值。每次通过drawFrame(),*移增加. 075。因为我们取的是正弦值,所以没有必要将值返回给它自己,因为正弦会为我们做这件事。尝试将mTransY的值增加到. 3,看看会发生什么。
  • 第 9f 行告诉 OpenGL 期待顶点和颜色数据。
  • 最后,在所有这些设置代码之后,我们可以调用您之前见过的mSquare的实际绘制例程,如第 10 行所示。
  • 第 11 行中的 onSurfaceChanged(),每当屏幕改变尺寸或在启动时创建时被调用。在这里,它也被用来设置观看截锥,这是空间的体积,定义了你实际可以看到的东西。如果你的任何场景元素位于截锥之外,它们被认为是不可见的,因此被剪切或剔除,以防止对它们进行进一步的操作。
  • 仅允许您指定 OpenGL 窗口的实际尺寸和位置。这通常是主屏幕的大小,位置为 0。
  • 在第 13 行,我们设置了矩阵模式。这样做的目的是设置当前的工作矩阵,当您进行任何通用矩阵管理调用时,将对其进行操作。在这种情况下,我们切换到GL_PROJECTION矩阵,也就是将 3D 场景投影到你的 2D 屏幕的矩阵。glLoadIdentity()将矩阵重置为初始值,清除之前的所有设置。
  • 现在,您可以使用纵横比和六个裁剪*面来设置实际的截锥:*/远、左/右和顶/底。
  • 在清单 1–2 的最后一个方法中,一些初始化是在表面创建线 15 上完成的。第 16 行确保任何抖动被关闭,因为它默认为开。OpenGL 中的抖动使得有限调色板的屏幕看起来更好,但当然是以牺牲性能为代价的。
  • 第 17 行中的glHint()用于通过接受某些折衷来推动 OpenGL ES 做它认为最好的事情:通常是速度与质量。其他可提示的设置包括雾和各种*滑选项。
  • 我们可以设置的许多状态中的另一个是背景被清除时呈现的颜色。在这种情况下,如果背景是半透明的,则为黑色;如果背景不是半透明的,则为白色(所有颜色的最大值为 1)。继续,稍后更改这些,看看会发生什么。
  • 最后,这个清单的结尾设置了一些其他方便的模式。第 19 行说剔除那些背对我们的面(三角形)。第 20 行告诉它使用*滑阴影,使颜色在表面上混合。唯一的另一个值是GL_FLAT,当它被激活时,将以最后绘制的顶点的颜色显示面。第 21 行启用深度测试,也称为 z 缓冲,稍后介绍。

最后,需要修改活动文件,使其看起来像清单 1–3 中的。

清单 1–3。 活动文件

package book.BouncySquare;
`import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.WindowManager;
import book.BouncySquare.*;

public class BouncySquareActivity extends Activity
{
    /** Called when the activity is first created. /
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
       getWindow().setFlags(WindowManager.LayoutParams.
FLAG_FULLSCREEN,
          WindowManager.LayoutParams.
FLAG_FULLSCREEN*);
          GLSurfaceView view = new GLSurfaceView(this);
          view.setRenderer(new SquareRenderer(true));
          setContentView(view);
    }
}`

我们的活动文件与默认文件相比修改很少。这里,GLSurfaceView实际上被分配并绑定到我们的自定义渲染器SquareRenderer

现在编译并运行。您应该会看到类似于 Figure 1–1 的东西。

Image

图 1–1。 一个有弹性的广场。如果这是你看到的,给自己一个击掌。

作为工程师,我们都喜欢摆弄和调整我们的作品,看看会发生什么。因此,让我们通过将顶点数组中的第一个数字替换为-2.0 而不是-1.0 来改变欢乐弹跳方块的形状。并用 0 替换颜色数组中的第一个值maxcolor。这将使左下角的顶点突出相当的方式,应该把它变成绿色。编译,敬畏地退后。你应该有类似图 1-2 的东西。

Image

图 1–2。 扭捏过后

不要担心第一个练习的简单性;在某个时候,你会造出比跳动的彩虹色果冻方块更奇特的东西。主要项目是基于《遥远的太阳 3》中使用的一些代码构建一个简单的太阳系模拟器。但是现在,是时候进入无聊的话题了:计算机图形学从何而来,又将走向何方。

注意:Android 模拟器是出了名的漏洞百出,速度也是出了名的慢。强烈建议您在真实的硬件上完成所有的 OpenGL 工作,尤其是当练习变得稍微复杂一些的时候。你会省去很多悲伤。

一部不稳定的计算机图形学史

说 3D 在今天风靡一时是一种保守的说法。虽然“3D”图像的形式可以追溯到一个多世纪以前,但它似乎终于成熟了。首先让我们看看什么是 3D,什么不是。

好莱坞的 3D

1982 年,迪斯尼发行了第一部广泛使用电脑图形描绘电子游戏中的生活的电影。尽管这部电影在评论界和财务上都是失败的,但它最终还是加入了与《??》和《洛基恐怖电影秀》齐名的最受欢迎的行列。好莱坞已经把苹果咬了一口,没有回头路了。

追溯到 19 世纪,我们今天所说的“3D”更通常被称为立体视觉。流行的维多利亚时代的立体幻灯在当时的许多客厅里都能找到。将这种技术视为早期的 Viewmaster。用户将把立体视觉仪举到他们的面前,将立体照片放入远端,并看到一些远处土地的景色,但是是立体的,而不是*面的 2D 图片。每只眼睛只能看到卡片的一半,上面有两张几乎一模一样的照片,相距只有几英寸。

立体视觉给了我们视野深度分量的概念。我们的两只眼睛向大脑传递两个略有不同的图像,然后大脑以一种我们称为深度感知的方式解释它们。一张图片不会有这种效果。最终这种情况转移到了电影中,早在 1903 年就有过短暂而不成功的调情(据说短暂的L ' arrivé渡厄火车曾让观众从电影院跑出来,以避开明显开往他们的方向的火车)并在 20 世纪 50 年代初复兴,其中 Bwana Devil 可能是最著名的。

*3D 电影的原始形式通常使用“立体”技术,要求观众戴上廉价的塑料眼镜,一只眼睛戴上红色滤光片,另一只眼睛戴上蓝色滤光片。偏振系统在 20 世纪 50 年代早期出现,并允许彩色电影以立体声观看,它们仍然与今天非常相似。由于担心电视会扼杀电影业,好莱坞需要一些在电视上不可能实现的噱头来继续卖票,但由于所需的摄像机和放映机都太不切实际和昂贵,这种形式不再受欢迎,电影业挣扎着勉强度日。

随着 20 世纪 90 年代数字投影系统的出现,以及《玩具总动员》等完全渲染的电影,立体电影以及最终的电视终于变得既实用又实惠,足以超越噱头阶段。特别是,全长 3D 动画功能(玩具总动员是第一个)使它成为一个没有大脑转换成立体。所有人需要做的只是简单地从一个稍微不同的角度重新放映整部电影。这是立体和三维计算机图形混合的地方。

计算机图形学的曙光

关于计算机图形和一般计算机的历史的一个迷人的事情是,这项技术仍然是如此的新,以至于许多巨人仍然大步走在我们中间。很难追查到底是谁发明了马鞭,但如果你想直接了解 20 世纪 60 年代阿波罗登月舱计算机是如何编程的,我知道该找谁。

计算机图形(通常称为 CG)有三种总体风格:用户界面的 2D,飞行或其他形式的模拟以及游戏的实时 3D,以及非实时使用的质量胜过速度的 3D 渲染。

1961 年,一位名叫伊凡·苏泽兰的麻省理工学院工程系学生为他的博士论文创建了一个名为画板的系统,使用了矢量示波器、一支简陋的光笔和一台定制的林肯 TX-2 计算机(TX-2 小组的一个分支将成为 DEC)。Sketchpad 革命性的图形用户界面展示了许多现代 UI 设计的核心原则,更不用说面向对象架构的巨大帮助了。

注:关于画板运行的视频,去 YouTube 搜索画板或者伊凡·苏泽兰

萨瑟兰的一个同学史蒂夫·拉塞尔发明了也许是有史以来最大的时间接收器之一,电脑游戏。Russell 在 1962 年创造了传奇游戏 Spacewar,该游戏在 PDP-1 上运行,如图 1–3 所示。

Image

图 1–3。1962 年的太空战游戏在加州山景城的计算机历史博物馆复活,在一个老式的 PDP-1 上。照片由 Joi Itoh 拍摄,根据知识共享署名 2.0 通用许可([creativecommons.org/licenses/by/2.0/deed.en](http://creativecommons.org/licenses/by/2.0/deed.en))进行许可。

到 1965 年,IBM 将发布被认为是第一个广泛使用的商业图形终端,2250。与低成本的 IBM-1130 计算机或 IBM S/340 配对,该终端主要用于科学界。

也许最早在电视上使用计算机图形的例子之一是 1965 年 12 月 CBS 新闻报道双子座 6 号和双子座 7 号载人航天任务时使用的 a 2250(IBM 建造了双子座的机载计算机系统)。这个终端被用来在电视直播中演示从发射到会合的几个阶段。在 1965 年,它的价格约为 10 万美元,相当于一栋漂亮的房子。参见图 1–4。

Image

图 1–4。1965 年的 IBM-2250 终端。由美国宇航局提供。

犹他大学

萨瑟兰于 1968 年被犹他大学招入计算机科学项目,他自然专注于图形学。在接下来的几年里,许多接受训练的计算机图形梦想家将会通过大学的实验室。

例如,艾德·卡姆尔热爱经典动画,但对自己不会画画感到沮丧——这是当时艺术家的必备条件。意识到计算机可能是制作电影的途径,Catmull 制作了第一个计算机动画,是他的手张开和合拢的动画。这个片段会出现在 1976 年的电影《未来世界》中。

在此期间,他开创了两项主要的计算机图形学创新:纹理映射和双三次曲面。前者可以通过使用纹理图像来增加简单形状的复杂性,而不是使用离散的点和表面来创建纹理和粗糙度,如图图 1–5 所示。后者用于生成算法化的曲面,比传统的多边形网格要高效得多。

Image

图 1–5。 有纹理和无纹理的土星

卡特莫尔最终找到了去卢卡斯影业和皮克斯的路,并最终成为迪士尼动画工作室的总裁,在那里他终于可以制作他想看的电影。不错的演出。

行业中的许多其他顶级品牌也同样会通过犹他大学的大门,并受到萨瑟兰的影响:

  • 约翰·沃诺克,他在开发一种称为 PostScript 和可移植文档格式(PDF)的独立于设备的显示和打印图形的方法中起了重要作用,是 Adobe 的创始人之一。
  • 吉姆·克拉克是 Silicon Graphics 的创始人,该公司为好莱坞提供了当时最好的图形工作站,并创建了现在被称为 OpenGL 的 3D 框架。在 SGI 之后,他共同创立了网景通信公司,这将带领我们进入万维网的领域。
  • 吉姆·布林,凹凸贴图和环境贴图的发明者,凹凸贴图是给物体添加真实 3D 纹理的有效方法,环境贴图用于创建真正闪亮的东西。也许他最出名的是为美国宇航局的旅行者项目创作革命性的动画,描绘了外行星的飞越,如图 1-6 所示(与使用现代设备的图 1-7 相比)。对于布林,萨瑟兰会说,“大约有十几个伟大的计算机图形人,吉姆·布林是其中之一。”Blinn 后来领导创建了微软的 OpenGL 的竞争对手,即 Direct3D。

Image

图 1–6。 吉姆·布林描绘了 1981 年 8 月旅行者 2 号与土星的相遇。注意穿过环面时由冰粒形成的条纹。由美国宇航局提供。

Image

图 1–7。 与比较图 1–6,使用一些当时最好的图形计算机和软件,在 500 美元的 iPad 上运行从遥远的太阳 3 看土星的类似视图。

好莱坞的成年

由于好莱坞和功能越来越强大同时成本越来越低的机器,计算机图形学在 20 世纪 80 年代开始真正发挥作用。例如,1985 年推出的广受欢迎的 Commodore Amiga 价格不到 2,000 美元,它为消费者市场带来了先进的多任务操作系统和彩色图形,而这在以前是价格超过 10 万美元的工作站的领域。参见图 1–8。

Image

图 1–8。 阿米加 1000,1985 年左右。照片由 Kaivv 提供,根据知识共享署名 2.0 通用许可([creativecommons.org/licenses/by/2.0/deed.en](http://creativecommons.org/licenses/by/2.0/deed.en))进行许可。

相比之下,不到 18 个月前发布的黑白原版 Mac 电脑的价格也差不多。它配备了非常原始的操作系统、*面文件系统和 1 位显示器,是各种阵营之间爆发的“宗教战争”的沃土,这场战争是关于谁的机器更好(战争也包括 Atari ST)。

注意:原始 Amiga 上的一种特殊图形模式可以将 4,096 种颜色压缩到一个系统中,通常最大值为 32 种颜色。它被称为保持和修改(HAM 模式),最初由设计师 Jay Miner 出于实验原因包含在一个主要芯片上。虽然他想去除公认的产生大量色彩失真图像的杂牌,但结果会在芯片上留下一个大的空白点。考虑到未使用的芯片景观是任何一个自尊的工程师都无法容忍的,他把它留了下来,让 Miner 非常惊讶的是,人们开始使用它。

堪萨斯州的一家名为 NewTek 的公司率先使用 Amigas 来渲染高质量的 3D 图形,并将其与名为视频烤面包机的特殊硬件相结合。结合一个叫做 Lightwave 3D 的复杂的 3D 渲染软件包,NewTek 为任何有几千美元花费的人打开了廉价的网络质量图形的领域。这一发展为精心制作的科幻节目打开了大门,如巴比伦 5海上任务考虑到他们广泛的特效需求,在经济上是可行的。

在 20 世纪 80 年代,更多的技术和创新在 CG 领域得到了普遍应用:

  • 洛伦·卡彭特开发了一种技术,利用一种叫做分形的东西,通过算法生成高度精细的景观。卡彭特受雇于卢卡斯影业,为一家名为皮克斯的新公司制作渲染包。结果是 REYES,它代表着渲染你所见过的一切。
  • 特纳·惠特德开发了一种叫做光线追踪的技术,可以产生高度逼真的场景(以显著的 CPU 成本),特别是当它们包括具有各种反射和折射属性的对象时。玻璃物品是各种早期光线追踪工作中的常见对象,如图图 1–9 所示。
  • Frank Crow 开发了计算机图形学中第一个实用的反走样方法。混叠是由于显示器分辨率相对较低而产生锯齿状边缘(锯齿)的现象。Crow 的方法可以*滑从线条到文本的一切,使其看起来更加自然和令人愉悦。注意卢卡斯影业的早期游戏之一叫做《分形上的营救》。这些坏家伙被命名为美洲豹
  • 《星际迷航 II:可汗之怒》带来了第一个完全由计算机生成的序列,用来说明一个叫做创世机器的设备如何在一个没有生命的星球上产生生命。那个模拟被称为“不会死的效果”,因为它在火焰和粒子动画以及分形景观方面的开创性技术。

Image

图 1–9。 像这样复杂的图像是开源 POV-Ray 等程序的爱好者所能接受的。照片由 Gilles Tran 拍摄,2006 年。

20 世纪 90 年代带来了终结者 2:审判日中的 ??“液态金属”终结者、玩具总动员的第一部完全由计算机生成的全长故事片、侏罗纪公园中的可信动画恐龙,以及詹姆斯·卡梅隆的泰坦尼克号,所有这些都有助于巩固 CG 作为好莱坞导演军火库中的常用工具。

到这个十年结束时,很难找到任何电影在实际效果或后期制作中没有计算机图形来帮助清理各种场景。新技术仍在以更加壮观的方式被开发和应用,就像迪斯尼令人愉快的 Up!或者詹姆斯·卡梅隆美丽的头像

现在,再一次,拿出你的智能设备,意识到这是一个多么小的技术奇迹。随意用安静的、尊重的语气说“哇”。

工具包

如果没有软件,前面提到的所有 3D 魔法都不可能实现。很多 CG 软件程序是高度专业化的,还有一些是比较通用的,比如本书重点介绍的 OpenGL ES。因此,下面是许多可用工具包中的几个。

OpenGL

开放图形库(OpenGL)来自高端图形工作站和大型机制造商硅图形(SGI)的开拓性努力。它自己的专有图形框架 IRIS-GL 已经发展成为业界事实上的标准。随着竞争的加剧,为了留住客户,SGI 选择将 IRIS-GL 转变为一个开放的框架,以加强其作为行业领导者的声誉。IRIS-GL 剥离了与图形无关的功能和硬件相关的特性,更名为 OpenGL,并于 1992 年初发布。在撰写本文时,版本 4.1 是最新的。

随着小型手持设备变得越来越普遍,嵌入式系统 OpenGL(OpenGL ES)被开发出来,它是桌面版本的精简版本。它删除了许多冗余的 API 调用,并简化了其他元素,使其能够在市场上的低功耗 CPU 上高效运行。因此,它已经在许多*台上被广泛采用,如 Android、iOS、惠普的 WebOS、任天堂 3DS 和黑莓(OS 5.0 及更新版本)。

OpenGL ES 主要有两种风格,1。 x 和 2。 x 。许多设备都支持这两者。版本 1。 x 是更高级的变体,基于最初的 OpenGL 规范。版本 2。 x (是的,我知道这令人困惑)的目标是更专业的渲染杂务,可以由可编程图形硬件处理。

Direct3D

Direct3D (D3D)是微软对 OpenGL 的回应,主要面向游戏开发者。1995 年,微软收购了一家名为 RenderMorphics 的小公司,该公司专门为编写游戏创建一个名为 RealityLab 的 3D 框架。RealityLab 变成了 Direct3D,并于 1996 年夏天首次发布。尽管它是基于 Windows 系统的专利,但它在微软的所有*台上都有庞大的用户群:Windows、Windows 7 Mobile,甚至 Xbox。OpenGL 和 Direct3D 阵营之间一直在争论哪个更强大、更灵活、更易于使用。其他因素包括硬件制造商更新其驱动程序以支持新功能的速度、易于理解性(Direct3D 使用微软的 COM 接口,这对新手来说可能非常混乱)、稳定性和行业支持。

其他人

虽然 OpenGL 和 Direct3D 在采用和功能方面仍然处于领先地位,但图形领域充斥着许多其他框架,其中许多框架在当今的设备上都受到支持。

在计算机图形世界中,图形库有两种非常广泛的风格:以 OpenGL 和 Direct3D 为代表的低级渲染机制,以及通常在游戏引擎中发现的高级系统,这些系统专注于资源管理,并具有扩展到常见游戏元素(声音、网络、得分等)的特殊附加功能。后者通常建立在前者之上,用于 3D 部分。如果做得好的话,更高层次的系统甚至可能被足够抽象,使得同时与 GL 和 D3D 一起工作成为可能。

快速绘制 3D

高级通用库的一个例子是 QuickDraw 3D (QD3D)。作为苹果 2D QuickDraw 的 3D 兄弟,QD3D 有一种优雅的方式,以一种易于理解的分层方式生成和链接对象(一个场景图)。它同样有自己的文件格式来加载 3D 模型和标准查看器,并且是独立于*台的。QD3D 的高级部分将计算场景,并确定每个对象以及每个对象的每个部分如何在 2D 绘图表面上显示。在 QD3D 下面有一个非常薄的层,叫做 RAVE,它将处理这些位的特定于设备的呈现。

用户可以使用 RAVE 的标准版本,这将按照预期渲染场景。但是更有野心的用户可以编写自己的程序,以更艺术的方式显示场景。例如,一家公司生成 RAVE 输出,以便看起来像他们的对象是在洞穴的一侧手绘的。当你可以拿着这幅现代版的洞穴画并旋转它的时候,那真是太酷了。插件架构也使得 QD3D 高度可移植到其他机器。当潜在用户因为 QD3D 在 PC 上没有硬件解决方案而不愿使用 QD3D 时,RAVE 的一个版本问世了,它将通过实际使用其竞争对手作为其光栅化器来使用 Direct3D 可用的硬件加速。可悲的是,QD3D 在史蒂夫·乔布斯第二次到来时几乎立即被扼杀,他确定 OpenGL 应该是未来 MAC 的 3D 标准。这是一个奇怪的说法,因为 QD3D 不是另一个的竞争对手,而是一个使程序员的生活更容易的附加产品。在乔布斯拒绝让 QD3D 开源的请求后,Quesa 项目成立了,以尽可能多地重新创建原始库,在撰写本文时仍在支持原始库。毫无疑问,Quesa 使用 OpenGL 作为渲染引擎。

这里声明:我写 QD3D 的 RAVE/Direct3D 层只是为了在“gold master”(准备发货)几天后取消项目。呸。

食人魔

另一个场景图形系统是面向对象的渲染引擎(OGRE)。OGRE 于 2005 年首次发布,可以同时使用 OpenGL 和 Direct3D 作为底层光栅化解决方案,同时为用户提供许多商业产品中使用的稳定和免费的工具包。用户群体的规模令人印象深刻。在撰写本文时,快速浏览一下论坛就会发现,仅一般讨论部分就有超过 6500 个主题。

OpenSceneGraph

最*为 iOS 设备发布的 OpenSceneGraph 大致与 QuickDraw 3D 一样,提供了一种在更高层次上创建对象、将它们链接在一起、在 OpenGL 层之上执行场景管理任务和额外效果的方法。其他功能包括导入多种文件格式、文本支持、粒子效果(用于火花、火焰或云),以及在 3D 应用中显示视频内容的能力。强烈推荐了解 OpenGL,因为许多 OSG 函数仅仅是它们的 OpenGL 对应物的薄薄的包装。

三维统一

与 OGRE、QD3D 或 OpenSceneGraph 不同,Unity3D 是一个跨*台的成熟游戏引擎,可以在 Android 和 iOS 上运行。区别在于产品的范围。虽然前两者集中在围绕 OpenGL 创建一个更抽象的包装,但游戏引擎走得更远,提供了游戏通常需要的大多数(如果不是全部)其他支持功能,如声音、脚本、网络扩展、物理、用户界面和记分模块。此外,一个好的引擎可能有工具来帮助生成素材,并且是独立于*台的。

Unity3D 具备所有这些特性,因此对于许多较小的项目来说是多余的。此外,作为一种商业产品,其来源是不可获得的,并且它不是免费使用的,而是仅花费适中的数量(与过去可能要花费 100,000 美元或更多的其他产品相比)。

还有其他人

我们也不要忽略 A6,冒险游戏工作室,C4,水晶空间,VTK,Coin3D,SDL,QT,Delta3D,Glint3D,Esenthel,FlatRedBall,Horde3D,Irrlicht,Leadwerks3D,Lightfeather,Raydium,Panda3D(来自迪士尼工作室和 CMU),Torque 等等。虽然它们很强大,但是使用游戏引擎的一个缺点是,你的世界通常是在它们的环境中运行的。所以,如果你需要一种特定的微妙行为,而这种行为是不可获得的,那你可能就不走运了。

OpenGL 架构

现在,既然我们已经对一个简单的 OpenGL 程序进行了深入的分析,那么让我们来简单地看一下在图形管道中到底发生了什么。

术语管道通常用于说明一个紧密结合的事件序列是如何相互关联的,如图图 1–10 所示。在 OpenGL ES 的例子中,这个过程在一端接受一串数字,在另一端输出一些看起来很酷的东西,可能是土星的图像,也可能是核磁共振成像的结果。

Image

图 1–10。OpenGL ES 1 的基本概述。 x 管道

  • 第一步是获取描述一些几何图形的数据,以及如何处理照明、颜色、材质和纹理的信息,并将其发送到管道中。

  • 接下来数据被移动和旋转,之后每个物体上的光照被计算和存储。场景——比如说一个太阳系模型——必须根据你设置的视点进行移动、旋转和缩放。视点采用了一个截头圆锥体的形式,一种矩形的圆锥体,它将场景限制在理想的可管理的水*。

  • Next the scene is clipped, meaning that only stuff that is likely to be visible is actually processed. All of the other stuff is culled out as early as possible and discarded. Much of the history of real-time graphics development has to do with object culling techniques, some of which are very complex.

    让我们回到太阳系的例子。如果你在看地球,而月亮在你的视角后面,就没有必要处理月亮数据。剪裁级别就是这样做的,一端是对象级别,另一端是顶点级别。当然,如果您能在提交到管道之前自己预先挑选对象,那就更好了。也许最简单的方法就是简单地判断你身后是否有物体,让它完全可以被跳过。如果对象太远而看不见或者被其他对象完全遮挡,也可以进行剔除。

  • 剩下的物体现在被投影到“视口”上,这是一种虚拟显示。

  • 这里是光栅化发生的地方。光栅化将图像分割成实际上是单个像素的片段

  • 现在,碎片可以应用纹理和雾效果。例如,如果雾遮住了更远的碎片,同样可以进行额外的剔除。

  • 最后一个阶段是将幸存的碎片写入帧缓冲区,但前提是它们满足一些最后的操作。这里是片段的 alpha 值应用于半透明的地方,还有深度测试以确保最*的片段绘制在更远的片段之前,以及用于渲染非矩形视口的模板测试。

完成后,你可能会看到类似于图 1–11b 所示茶壶的东西。

注:你对计算机图形学研究得越多,你就会越多地看到一个小茶壶出现在从书籍到电视和电影的例子中(辛普森一家,玩具总动员)。茶壶的传说,有时被称为犹他州茶壶(一切都可以追溯到犹他州),始于 1975 年一位名叫马丁·纽维尔的博士生。他需要一个具有挑战性的形状,但除此之外,这是他博士工作中的一个普通对象。他的妻子推荐了他们的白色茶壶,在这一点上,纽维尔费力地用手工将它数字化。当他将数据发布到公共领域时,它很快就成为了“Hello World!”图形编程。甚至苹果开发者网站上的一个早期 OpenGL ES 例子也有一个茶壶演示。最初的茶壶现在存放在加州山景城的计算机历史博物馆,离谷歌只有几个街区。参见图 1–11 的左侧。

Image

图 1–11a,b. 左边是纽厄尔用过的真正的茶壶,目前陈列在加利福尼亚州山景城的计算机历史博物馆。照片由史蒂夫·贝克拍摄。右边是苹果开发者网站上的一个 OpenGL 应用示例。

总结

在这一章中,我们讲述了一点计算机图形学的历史,一个简单的示例程序,以及最重要的,犹他茶壶。接下来是对 3D 图像背后的数学的深入的、无疑是过分详细的研究。*

二、所有那些数学爵士

如果没有至少一章关于 3D 转换背后的数学知识,任何一本关于 3D 编程的书都是不完整的。如果你对此毫不在乎,那就继续前进——这里没什么可看的。毕竟 OpenGL 不是会自动处理这些东西吗?当然可以。但是熟悉里面发生的事情是有帮助的,如果没有什么比理解 3D 语言更有帮助的话。

让我们先定义一些术语:

  • *移:从初始位置移动物体(见图 2–1,左)
  • 旋转:围绕原点中心点旋转物体(见图 2–1,右图)
  • 缩放:改变对象的大小
  • 转换:以上全部

Image

图 2–1。 *移(左)和旋转(右)

2D 变换

在不知道的情况下,你可能已经以简单翻译的形式使用了 2D 变换。如果您创建了一个 UIImageView 对象,并希望根据用户触摸屏幕的位置来移动它,您可以抓取它的框架并更新原点的 x 和 y 值。

翻译

你有两种方法来想象这个过程。第一是物体本身相对于一个共同的原点运动。这叫做几何变换。第二种方法是移动世界原点,而对象保持静止。这叫做坐标变换。在 OpenGL ES 中,两种描述通常一起使用。

*移操作可以这样表达:

x′=x+Tx**y′=y+Ty

原来的坐标是 xy ,而*移后的 T ,会把这些点移动到一个新的位置。很简单。如你所知,翻译自然会很快。

注:小写字母如 xyz 为坐标,大写字母如 XYZ 为参考轴。

旋转

现在让我们来看看旋转。在这种情况下,我们将首先围绕世界原点旋转,以保持简单(见图 2–2)。

Image

图 2–2。 围绕共同原点旋转

当我们必须重温高中的三角函数时,事情自然会变得更加复杂。所以,现在的任务是找出任意旋转后正方形的角在哪里。目光呆滞地注视着大地。

注:按照惯例,逆时针旋转被认为是正的,而顺时针旋转是负的。

所以,把 xy 作为我们正方形的一个顶点的坐标,这个正方形就被归一化了。不旋转的话,任何顶点都会自然地直接映射到我们的坐标系中,即 xy 。很公*。现在我们要将正方形旋转一个角度 a 。虽然它的角在正方形自己的局部坐标系中仍然在“相同”的位置,但在我们的坐标系中它们是不同的,如果我们想要实际绘制该对象,我们需要知道新的坐标 xy

现在我们可以直接跳到可靠的旋转方程,因为最终这就是代码要表达的内容:

x′=xcos(a)–ysin(a)y′=xsin(a)+ycos(a)

做一个真正快速的检查,你可以看到如果 a 是 0 度(无旋转), xy 减少到原来的 xy 坐标。如果旋转 90 度,那么 sin(a)=1cos(a)=0 ,所以x′=-y,并且y′= x。果然不出所料。

数学家总是喜欢用尽可能简洁的形式来表达事物。因此,2D 旋转可以用矩阵符号来“简化”:

Image

注:星际迷航中使用最多的一个词是母体。这里是模式矩阵,那里是缓冲矩阵,“第一,我头疼,需要小睡一会儿。”(不要让我开始使用 24 中的协议。)每一个自重的星际迷航喝酒游戏(好像任何喝酒游戏都会自重)在选词上都要用 matrix 。除非你的一个朋友有酗酒问题,在这种情况下,用 matrix 代替 rabid badger。我几乎可以肯定,在《星际迷航》的万神殿里,从来没有提到过獾,不管是不是狂犬病。

Ra是我们的 2D 旋转矩阵的简写。虽然矩阵可能看起来很忙,但它们实际上非常简单,易于编码,因为它们遵循精确的模式。在这种情况下, xy 可以表示为一个微小的矩阵:

Image

翻译也可以以矩阵形式编码。因为*移仅仅是移动点,所以*移后的值 xy 来自于增加点的移动量。如果你想在同一个物体上做旋转和*移会怎么样?翻译矩阵只需要一点点非显而易见的想法。这里显示的第一个和第二个,哪个是正确的?

Image

答案是显然第二个,也可能没那么明显。第一个结尾如下,没有太大意义:

x′=x+yTxy′=x+yTy

因此,为了创建一个用于翻译的矩阵,我们需要我们的 2D 点的第三个组件,通常写成 (x,y,1) ,如第二个表达式中的情况。暂时忽略 1 的来源,请注意,它可以很容易地简化为:

x′=x+Txy′=y+Ty

1 的值不要与第三维的 z 相混淆;更确切地说,它是一种用来表达直线方程的方法(在本例中为 2D 空间),与我们在小学学过的斜率/截距略有不同。这种形式的一组坐标被称为齐次坐标,在这种情况下,它有助于创建一个 3×3 矩阵,该矩阵现在可以与其他 3×3 矩阵合并或连接。我们为什么要这么做?如果我们想一起做旋转和*移呢?每个点可以使用两个独立的矩阵,这样就很好了。相反,我们可以使用矩阵乘法(也称为串联)预先计算出几个矩阵中的一个矩阵,这反过来表示了各个转换的累积效果。这不仅可以节省一些空间,而且可以大大提高性能。

在 Java2D 中,您会在某个时候偶然发现 Java . awt . geom . affinite transform。所有可能的 2D 仿射变换都可以表示为x′=ax+cy+ey=bx+dy+f。这构成了一个非常好的矩阵,一个可爱的矩阵:

Image

下面是一段简单的代码,展示了如何使用 AffineTransform 进行转换和缩放。如您所见,这非常简单。

public void paint(Graphics g)
{
    AffineTransform transform = new AffineTransform();
    transform.translate(5,5);
    transform.scale(2,2);
    Graphics2D g2d = (Graphics2D)g;
    g2d.setTransform(transform);
}

缩放

对于其他两种变换,让我们来看看对象的缩放或简单的大小调整:

x′=xSx??y=ySy

在矩阵形式中,这变成如下:

Image

与其他两种变换一样,缩放的顺序在应用到几何体时非常重要。比方说,你想旋转和移动你的对象。根据你是先翻译还是后翻译,结果会明显不同。更常见的顺序是先旋转物体,然后*移,如图 2–3 左侧所示。但是如果你颠倒顺序,你会得到类似于图 2–3 中右图的东西。在这两种情况下,旋转都是围绕原点进行的。如果你想让物体围绕它自己的原点旋转,那么第一个例子就是为你准备的。如果你想让它和其他东西一起旋转,第二个就可以了。(一个典型的情况可能是让你将物体*移到世界原点,旋转它,然后再*移回来。)

Image

图 2–3。 绕原点旋转后*移(左)vs *移后旋转(右)

那么,这和 3D 有什么关系呢?简单!如果不是全部的话,大部分原理可以应用于 3D 变换,并且用一个更少的维度更清楚地说明。

3D 转换

当你将所学的一切转移到 3D 空间(也称为3-空间)时,你会发现,就像在 2D 一样,3D 变换同样可以表示为一个矩阵,因此可以与其他矩阵连接。 Z 的额外维度现在是进出屏幕的场景深度。OpenGL ES 有 +Z 出来了

和-Z进去。其他系统可能会有相反的情况,甚至有 Z 是垂直的, Y 现在假设深度。我将继续使用 OpenGL 惯例,如图图 2–4 所示。

注意:从一个参照系到另一个参照系来回移动是除了试图找出福克斯为什么取消萤火虫之外最快的疯狂之路。1973 年出版的经典著作《交互式计算机图形的 ?? 原理》中有 Z 上升和+ Y 进入屏幕。在他的书中,微软飞行模拟器的创造者 Bruce Artwick 展示了观察*面中的 X 和 Y,但是+Z 将进入屏幕。然而,另一本书已经(得到这个!) Z 向上, Y 向右前进,X向观者走来。应该有一部法律…

Image

图 2–4。z 轴朝向观察者。

首先我们来看看 3D 变换。正如 2D 的变化仅仅是在原来的位置上增加所需的增量一样,同样的事情也适用于 3D。描述这一点的矩阵如下所示:

Image

当然,这会产生以下结果:

x′=x+Txy′=y+Tyz′=z+Tz

请注意添加的额外 1;这和 2D 的情况一样,所以我们的点位置现在是均匀的形式。

那么,让我们来看看旋转。人们可以有把握地假设,如果我们绕着 z 轴旋转(图 2–5),方程将直接映射到 2D 版本。使用矩阵来表达这一点,下面是我们得到的结果(注意新的符号,其中 R(z,a) 用于明确哪个轴被寻址)。请注意,z 保持不变,因为它要乘以 1:

Image

Image

图 2–5绕 z 轴旋转

这看起来几乎和 2D 的一模一样,只不过多了一个 z=z。但是现在我们也可以绕着 x 或者 y 旋转。对于 x ,我们得到如下:

*Image

当然,对于 y 我们得到如下:

Image

但是一个在另一个之上的多个转换呢?现在我们在谈论丑陋。幸运的是,您不必太担心这一点,因为您可以让 OpenGL 来完成繁重的工作。这就是它的用途。

假设我们想先绕着 y- 轴旋转,接着是 x ,然后是 z 。得到的矩阵可能如下所示(使用 a 作为围绕 x 的旋转, b 代表 y ,而 c 代表 z ):

Image

简单,嗯?难怪 3D 引擎作者的口头禅是优化,优化,优化。事实上,在最初的 Amiga 版本《遥远的太阳》中,我的一些内部循环需要在 68K 汇编中。请注意,这甚至不包括缩放或*移。

现在让我们来看看写这本书的原因:所有这些都可以通过下面三行代码来完成:

glRotatef(b,0.0,1.0,0.0);
glRotatef(a,1.0,0.0,0.0);
glRotatef(c,0.0,0.0,1.0);

注意:OpenGL ES 1.1 中有很多功能是 2.0 中没有的。后者面向底层操作,为了灵活性和控制牺牲了一些易于使用的工具。转换函数已经消失,留给开发者去计算他们自己的矩阵。幸运的是,有许多不同的库来模拟这些操作并简化转换任务。

在处理 OpenGL 时,这个特殊的矩阵被称为 modelview,因为它适用于你绘制的任何东西,无论是模型还是灯光。稍后我们将处理另外两种类型:投影矩阵和 ?? 纹理矩阵。

需要重申的是,当试图让这个东西工作时,旋转的实际顺序是绝对重要的。例如,一个常见的任务是用完整的六个自由度(三个*移分量和三个旋转分量)对飞机或航天器进行建模。转动部分通常称为滚转俯仰偏航 (RPY)。滚转是绕着 z 轴旋转,俯仰是绕着 x- 轴旋转(换句话说,瞄准机头向上或向下),偏航当然是绕着 y 轴旋转,左右移动机头。图 2–6 显示了 20 世纪 60 年代阿波罗飞船登月时的工作情况。正确的顺序是偏航、俯仰和滚动,或者绕着 y、x 旋转,最后绕着 z 旋转。(这需要 12 次乘法和 6 次加法,而对三个旋转矩阵进行预乘可以将其减少到 9 次乘法和 6 次加法。)变换将是递增的,包括自上次更新以来 RPY 角度的变化,而不是从开始的全部变化。在过去,舍入误差可能会使矩阵复合变形,导致非常酷但出乎意料的结果(但仍然很酷)。

Image

图 2–6阿波罗的参照系、操纵杆和人工地*线的图示

想象一下:将物体投射到屏幕上

咻,即使做了这一切,我们还没有完全完成。一旦你完成了对象的所有旋转、缩放和*移,你仍然需要将它们投影到你的屏幕上。自从他在洞穴墙壁上画出第一只猛犸象草图以来,将 3D 场景转换到 2D 表面就一直困扰着人类。但是,与转换相反,它实际上很容易掌握。

这里主要有两种投射在起作用:透视*行。透视投影是我们在 2D 视网膜上看到三维世界的方式。透视图由消失点和透视缩小组成。消失点是所有*行线在远处汇聚的地方,提供了深度感(想象铁轨朝着地*线)。结果是越靠*的东西看起来越大,反之亦然,如图图 2–7 所示。*行变体,也称为正交投影,通过有效地将每个顶点的 z 分量设置为 0(我们观察*面的位置),简单地消除了距离的影响,如图图 2–8 所示。

Image

图 2–7透视投影

Image

图 2–8*行投影

在透视投影中,距离分量 z 用于缩放最终将成为屏幕 x 和屏幕 y 的值。所以, z,或者离观看者的距离越大,作品在视觉上就越小。我们需要的是视窗 (OpenGL 版本的窗口或显示屏)的尺寸及其中心点,通常是 XY *面的原点。

这最后一个阶段包括设置视图视锥。*截头体建立了六个裁剪*面(顶部、底部、左侧、右侧、*侧和远侧),以精确确定用户应该看到什么,以及如何将其投影到他们的视口,这是 OpenGL 版本的窗口或屏幕。这就像是你的 OpenGL 虚拟世界中的一个镜头。通过更改这些值,您可以放大或缩小并剪切很远的内容或根本不剪切,如图图 2–9 和图 2–10 所示。这些值定义了透视矩阵。

Image

图 2–9。 窄边框的*截头体给你一个高倍镜头。

Image

图 2–10更宽的边界像一个广角镜头

随着这些边界的建立,最后一个转换是到视窗,你的屏幕的 OpenGL 版本。这是 OpenGL 接收屏幕尺寸、显示区域尺寸和原点(可能是屏幕的左下角)的地方。在手机或*板电脑等小型设备上,你可能会填满整个屏幕,因此会使用屏幕的整个宽度。但是如果您想将图像放在主显示的子窗口中,您可以简单地将较小的值传递给 viewport。相似三角形法则在这里发挥作用。

在图 2–11 中,我们想要找出投影的x′是什么,给定模型上任意顶点的 x 。考虑两个三角形,一个由角 CBA 形成,另一个由 COA' 形成(其中 0 表示原点)。从 C(眼睛所在的位置)到 0 的距离为 d 。从 C 到 B 的距离是 d+z 。所以,就拿这些的比率来说,如下:

Image

产生以下结果:

Image

Image

图 2–11使用相似三角形法则将一个顶点映射到视口

图 2–12 显示了最终的翻译。那些可以加到x′y′:

Image

Image

图 2–12将 x 和 y 投影到设备屏幕上。您可以将此想象为将您的设备*移到对象的坐标(左),或将对象*移到设备的坐标(右)。

当像素尘埃落定,我们有一个很好的矩阵形式:

Image

通常需要一些最终的缩放,例如,如果视口被规格化。但这取决于你。

现在穿着高跟鞋倒着做

据说这是金格尔·罗杰斯在谈到她与伟大的弗雷德·阿斯泰尔共舞的感受时所说的一句话。他的回答是,虽然他很棒,但她必须做他做的每件事倒着穿高跟鞋做。(罗杰斯显然从未说过这句话,因为它的用法可以追溯到连环漫画《弗兰克和欧内斯特》中的一句插科打诨的台词。)

那么,这和转换有什么关系呢?假设您想通过触摸屏幕来判断是否有人拿走了您的某个对象。你怎么知道你的哪个对象被选中了?您必须能够进行逆变换,将屏幕坐标“解映射”回 3D 空间中可识别的位置。但是由于 z 值在这个过程中会丢失,所以有必要在对象列表中进行搜索,以找出哪个是最有可能的目标。不改变某些东西需要你向后做所有的事情(如果你喜欢这种事情,还可以穿高跟鞋)。这是通过以下方式完成的:

  1. 将模型视图矩阵与投影矩阵相乘。
  2. 反转结果。
  3. 将触摸点的屏幕坐标转换为视窗的参考框架。
  4. 取其结果并乘以步骤 2 中的逆矩阵。

不要担心,这将在本书的后面有更详细的介绍。

四元数呢?

四元数是超复数,可以将 RPY 信息存储在四维向量类型的东西中。它们在性能和空间上都非常有效,通常用于在飞行模拟中模拟飞机或航天器的瞬时航向。它们是一种奇特的生物,具有很好的特性,但留待以后使用,因为 OpenGL 不直接支持它们。

总结

在本章中,您学习了 3D 数学的基础知识。本章首先介绍了 2D 变换(旋转、*移和缩放),然后介绍了 3D,还介绍了投影。虽然你可能不需要自己编写任何转换代码,但是熟悉这一章是理解大部分 OpenGL 术语的关键。我头疼。*

三、从 2D 到 3D:增加一个额外的维度

在前两章,我们讨论了酷的东西和数学的东西(可能很酷也可能很无聊)。在第三章中,我们将把弹跳立方体的例子从 2D 版本转移到 3D 版本(4D 超立方体不在本文讨论范围之内)。在这个过程中,更多的关于投影、旋转等的 3D 理论将被加入进来。但是,请注意,OpenGL 不仅仅用于 3D,还可以很容易地用于将 2D 控件放在 3D 可视化的前面。

首先,多一点理论

记住 OpenGL ES 对象是 3D 空间中的点的集合;也就是说,它们的位置由三个值定义。这些值连接在一起形成面,面是看起来非常像三角形的*面。这些三角形然后被连接在一起形成物体或物体的碎片。

为了得到一串形成顶点的数字,形成颜色的其他数字,以及在屏幕上组合顶点和颜色的其他数字,有必要告诉系统它的图形环境。完成 3D 电路需要视点位置、接收图像的窗口(或视口)、长宽比以及其他各种数字碎片。更具体地说,我将介绍 OpenGL 的坐标,它们如何与*截头体相关,如何从场景中裁剪或挑选对象,以及如何在设备的显示器上绘图。

注意:你可能想知道我们什么时候会谈到酷酷的行星。很快,小蚱蜢,很快。

OpenGL 坐标

如果你曾经在任何系统上绘制过任何类型的图形,你就会熟悉普通的 X-Y 坐标系统。x 始终是水*轴,右为正,而 Y 始终是垂直轴,下为正,将原点放在左上角。被称为屏幕坐标,它们很容易与数学坐标混淆,后者将原点放在左下角,对于 Y,up 为正。

现在跳到 OpenGL 3D 坐标,我们有一个稍微不同的系统,使用笛卡尔坐标,空间中表示位置的标准。通常,对于 2D 显示器上的屏幕坐标,原点在左上角,X 向右,Y 向下。但是,OpenGL 的原点在左下角,+Y 向上。但是现在我们增加了一个第三维度,表示为 Z。在这种情况下,+Z 指向你,如图图 3–1 所示。

Image

图 3–1。 OpenGL ES 3D 笛卡尔坐标系(图片由 Jorge Stolfi 提供)

事实上,在 OpenGL 中我们有几种坐标系,或者说空间,每个空间都被转换到下一个空间:

  • 对象空间,相对于你的每一个对象。
  • 相机,或,空间,局部于你的视点。
  • 投影,或剪辑,空间,这是显示最终图像的 2D 屏幕或视窗。
  • 切线空间,用于更高级的效果,如凹凸贴图,这将在后面介绍。
  • 标准化设备坐标(NDCs),表示从-1 到 1 的标准化 xyz 值。也就是说,该值(或一组值)被规范化,以使其适合边长为 2 个单位的立方体。
  • 窗口,或屏幕,坐标,这是你的场景在实际屏幕上显示时的最终位置(无疑会赢得热烈的掌声)。

自然,前者可以用流水线形式表示,如图图 3–2 所示。

Image

图 3–2。 顶点变换流水线

对象、视点和剪辑空间是你通常需要担心的三个问题。例如,用本地原点生成物体坐标,然后移动并旋转到眼空间。例如,如果你有一堆战斗游戏的飞机,每一架都有自己的本地来源。你应该能够通过移动,或者*移,将*面移动到你世界的任何地方,只移动原点,让其余的几何体跟着移动。在这一点上,对象的可见性是针对视见*截头体进行测试的,视见*截头体是定义虚拟相机实际可以看到什么的空间体积。如果它们位于*截头体之外,它们被认为是不可见的,因此被剪切或剔除,以便不对它们进行进一步的操作。你可能还记得《??》第一章,图形引擎设计的大部分工作都集中在引擎的裁剪部分,以便尽早转储尽可能多的对象,从而产生更快更高效的系统。

最后,在所有这些之后,OpenGL 面向屏幕的部分准备好转换,或者说投影,剩余的对象。这些对象是你的飞机、齐柏林飞艇、导弹、路上的卡车、海上的船只、投石机,以及任何你想塞进应用的东西。

注意: OpenGL 并没有真正定义任何东西为“世界空间”然而,眼睛坐标是下一个最好的东西,因为你可以定义与你的位置相关的一切。

眼睛坐标

OpenGL 中没有神奇的眼点对象。因此,不是移动眼点,而是移动与眼点相关的所有对象。是的,这很容易混淆,你会发现自己不断改变数值的符号,画有趣的图表,并以奇怪的方式伸出手,试图找出为什么你的投石机是颠倒的。在眼点相对坐标中,对象实际上是远离您,而不是远离对象。想象一下,你正在制作一个汽车飞驰而过的视频。在 OpenGL 下,汽车会静止不动;你和你周围的一切都会被它感动。这主要是通过glTranslate*()glRotate*()调用来完成的,稍后您将会看到。正是在这一点上,上一章提到的 OpenGL 的模型视图矩阵发挥了作用。您可能还记得模型视图矩阵处理基本的 3D 转换(与投影矩阵相对,投影矩阵将 3D 视图投影到屏幕的 2D 空间,或者纹理矩阵,帮助将图像应用到您的对象)。你会经常提到它。

查看视锥和投影矩阵

在几何学中,*截头体是(典型的)棱锥或圆锥体的一部分,由两个*行*面切割而成。换句话说,想想顶部三分之一被砍掉的吉萨大金字塔(我不是在纵容对埃及古物的破坏)。在图形中,视见*截头体定义了我们的虚拟相机可以实际看到的世界部分,如图图 3–3 所示。

Image

图 3–3。 视锥

与 OpenGL 中的许多东西不同,视见*截头体的定义非常简单,并且通过简单地定义空间中的一个体积,即视见金字塔,来紧密地遵循概念图形。只要不被任何更*的物体遮挡,任何整体或部分在*截头体内的物体最终都可能找到它们到屏幕的路径。

*截头体也用于指定你的视野(FOV),就像你的相机的广角镜头与长焦镜头。侧*面与中心轴相比形成的角度越大(即它们如何散开),FOV 就越大。一个更大的 FOV 将允许你的世界的更多部分可见,但也会导致更低的帧速率。

到目前为止,*移和旋转使用 ModelView 矩阵,使用调用 gl 很容易设置。glMatrixMode(GL_MODELVIEW);。但是现在在渲染管道的这个阶段,您将定义并使用投影矩阵。这很大程度上是通过第二章的“图片”一节中的视锥定义来完成的。而且这也是一个惊人的紧凑的手段,可以做很多操作。

将变换后的顶点转换成 2D 图像的最后步骤如下:

  1. *截头体内的 3D 点被映射到归一化立方体,以将 XYZ 值转换为 NDC。NDC 代表归一化设备坐标,这是一个描述位于*截头体内的坐标空间的中间系统,与分辨率无关。这在将每个顶点和每个对象映射到设备屏幕时非常有用,无论它有多大或多少像素,无论它是手机、*板电脑还是屏幕尺寸完全不同的新产品。一旦有了这种形式,坐标就“移动”了,但仍然保持着它们之间的相对关系。当然,在 NDC,它们的值在-1 到 1 之间。请注意,Z 值在内部翻转。现在 Z 向你走来,而+Z 正在离开,但谢天谢地,那巨大的不愉快都被隐藏起来了。
  2. 然后这些新的 NDC 被映射到屏幕上,考虑到屏幕的纵横比和顶点到屏幕的“距离”,如由*裁剪*面指定的。结果是,越远的东西越小。大多数数学只是用来确定截锥内这个或那个的比例。

前面的步骤描述了透视投影,这是我们*常看待世界的方式。也就是说,越远的东西,看起来就越小。当那些固有的扭曲被移除时,我们得到了正投影。此时,无论物体有多远,它仍然显示相同的大小。当任何透视变形会破坏原始艺术品的意图时,正交渲染通常用于机械工程图中。

注意:你将经常需要直接处理你正在处理的矩阵。对gl.glMatrixMode()的调用用于指定当前矩阵,所有后续操作都将应用于当前矩阵,直到改变为止。忘记哪个矩阵是当前矩阵是一个容易犯的错误。

回到有趣的事情:超越蹦蹦跳跳的广场

现在我们可以回到在第一章中使用的例子。既然我们已经开始认真对待 3D,那么需要添加一些东西来处理 Z 维,包括立方体几何和颜色的更大数据集,将数据传递给 OpenGL 的方法,更复杂的*截头体定义,任何需要的面剔除技术,以及旋转而不仅仅是*移。

注: 翻译是指在你的世界中上下左右前后移动物体,而旋转是指绕任意轴旋转物体。并且两者都被认为是变换

添加几何图形

从第一章的开始,您将记住清单 3–1 中定义的数据。首先是四个角的位置,顶点,顶点是如何连接在一起的,以及它们是如何着色的。

清单 3–1。 定义 2D 广场

`float vertices[] =
        {
                    -1.0f, -1.0f,
                     1.0f, -1.0f,
                    -1.0f,  1.0f,
                     1.0f,  1.0f
        };

byte maxColor=(byte)255;

byte colors[] =
        {
                    maxColor,maxColor,                0,maxColor,
                    0,               maxColor, maxColor,maxColor,
                    0,                               0,                  0,maxColor,
                    maxColor,               0, maxColor, maxColor
        };

byte indices[] =
        {
                0, 3, 1,
                0, 2, 3
        };`

现在这可以扩展到包括 z 组件,从额外的顶点开始,如清单 3–2 的第 1 行所示。代码的其他细节将在列表后讨论。

清单 3–2。 定义 3D 立方体

`float vertices[] =
        {
                    -1.0f,  1.0f, 1.0f,                                          //1
                     1.0f,  1.0f, 1.0f,
                     1.0f, -1.0f, 1.0f,
                    -1.0f, -1.0f, 1.0f,

-1.0f,  1.0f,-1.0f,
                     1.0f,  1.0f,-1.0f,
                     1.0f, -1.0f,-1.0f,
                    -1.0f, -1.0f,-1.0f
        };

byte maxColor=(byte)255;

byte colors[] =                                                      //2
        {
                    maxColor,maxColor,       0,maxColor,
                    0,       maxColor,maxColor,maxColor,
                    0,              0,       0,maxColor,
                    maxColor,       0,maxColor,maxColor,

maxColor,       0,       0,maxColor,
                    0,       maxColor,       0,maxColor,
                    0,              0,maxColor,maxColor,
                    0,              0,       0,maxColor
        };

byte tfan1[] =
        {
                    1,0,3,
                    1,3,2,
                    1,2,6,
                    1,6,5,
                    1,5,4,
                    1,4,0

};

byte tfan2[] =
        {
                    7,4,5,
                    7,5,6,
                    7,6,2,
                    7,2,3,
                    7,3,0,
                    7,0,4
        };`

第 1 行将顶点扩展到三维,而颜色数组,第 2ff 行,对颜色做同样的事情。

图 3–4 显示了顶点排序的方式。在正常情况下,您将永远不必以这种方式定义几何图形。您可能会从以一种标准 3D 数据格式存储的文件中加载对象,例如 3D Studio 或 Modeler 3D 所使用的格式。考虑到这样的文件可能有多复杂,不建议您自己编写,因为大多数主要格式的导入程序都是可用的。

Image

图 3–4。 注意不同的轴:X 向右,Y 向上,Z 朝向观察者。

颜色数组的大小,如清单 3–2 的第 2 行所示,因为顶点的数量加倍而加倍;在其他方面,它们与第一个示例中的相同,只是背面的颜色有所不同。

现在需要一些新数据来告诉 OpenGL 顶点的使用顺序。对于正方形来说,手动排序或排序数据是显而易见的,这样四个顶点就可以代表两个三角形。立方体使这变得相当复杂。我们可以用单独的顶点数组来定义立方体的六个面,但是对于更复杂的对象来说,这种方法不太适用。而且它的效率比不得不通过图形硬件传输六组数据要低。从内存和速度的角度来看,将所有数据保存在一个数组中是最有效的。那么,我们如何告诉 OpenGL 数据的布局呢?在这种情况下,我们将使用如图图 3–5 所示的三角扇的绘图模式。

Image

图 3–5。 三角形扇形与所有三角形有一个公共点。

有许多不同的方法可以将数据存储并呈现给 OpenGL ES。一种格式可能更快,但使用更多的内存,而另一种格式可能使用更少的内存,但会有一点额外的开销。如果您要从其中一个 3D 文件导入数据,很可能它已经针对其中一种方法进行了优化,但是如果您真的想要手动调整系统,您可能需要在某些时候将顶点重新打包为您的应用喜欢的格式。

除了三角扇,你会发现其他方式可以存储或表示数据,称为模式

  • 点和线只是说明:点和线。OpenGL ES 可以把你的顶点仅仅渲染成可定义大小的点,或者可以渲染点之间的线来显示线框版本。分别使用GL10.GL_POINTSGL10.GL_LINES
  • 线条、GL10.GL_LINE_STRIP是 OpenGL 在一个镜头中绘制一系列线条的一种方式,而线条循环、GL10.GL_LINE_LOOP类似于线条,但总是将第一个和最后一个顶点连接在一起。
  • 三角形,三角条,三角扇,圆出 OpenGL ES 图元列表:GL10.GL_TRIANGLES,GL10。GL_TRIANGLE_STRIPGL10.GL_TRIANGLE_FAN。OpenGL 本身可以处理额外的模式,如四边形(有四个顶点/边的面)、四边形带和多边形。

注:术语图元表示图形系统中数据的基本形状或形式。基本体的例子包括立方体、球体和圆锥体。该术语也可用于更简单的形状,如点、线,以及在 OpenGL ES 中的三角形和三角扇。

当使用这些低级对象时,你可能会想起在《??》第一章第一个例子中,有一个索引数组来告诉你哪些顶点与哪些三角形匹配。在定义三角形数组(在清单 3–2 中称为tfan1tfan2)时,您使用了类似的方法,除了所有的索引集合都从同一个顶点开始。例如,数组tfan1中的前三个数字是 1、0 和 3。这意味着第一个三角形依次由顶点 1、0 和 3 组成。于是,回到数组 vertices ,顶点 1 位于 x=1.0f,y=1.0f,z=1.0f,顶点 0 是 x=-1.0f,y=1.0f,z=1.0f 的点,而我们的三角形的第三个角位于 x=-1.0,y=-1.0,z=1.0。好处是这使得创建数据集变得容易得多,因为实际的顺序现在无关紧要了,而坏处是它使用了更多的内存来存储额外的信息。

立方体可以分成两个不同的三角形扇形,这就是为什么有两个索引数组。第一个包含正面、正面和顶面,而第二个包含背面、底面和左面,如图图 3–6 所示。

Image

图 3–6。 第一个三角形扇面的顶点 1 为公共顶点。

将它们缝合在一起

现在必须修改呈现代码来处理新数据。清单 3–3 展示了新constructor方法的其余部分,就在清单 3–2 中的数据定义下面。这将复制第一章示例的大部分内容,除了使用三角形扇形代替连接数组和两个对gl.glDrawArray()的调用。这是必需的,因为立方体分为两部分,必须分别绘制,一部分用于定义两个三角形扇的三个面或六个三角形。

清单 3–3。 其余的构造函数方法

`        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
        vbb.order(ByteOrder.nativeOrder());
        mFVertexBuffer = vbb.asFloatBuffer();
        mFVertexBuffer.put(vertices);
        mFVertexBuffer.position(0);

mColorBuffer = ByteBuffer.allocateDirect(colors.length);
        mColorBuffer.put(colors);
        mColorBuffer.position(0);

mTfan1 = ByteBuffer.allocateDirect(tfan1.length);
        mTfan1.put(tfan1);
        mTfan1.position(0);

mTfan2 = ByteBuffer.allocateDirect(tfan2.length);
        mTfan2.put(tfan2);
        mTfan2.position(0);`

注意:你会注意到很多 OpenGL ES 调用都以 f 结尾,比如gl.glScalef()gl.glRotatef()等等。 f 表示传递的参数是浮点数。OpenGL ES 中唯一需要特殊调用的其他参数类型是定点值,所以glScale现在应该是gl.glScalex()。定点对于旧的和较慢的设备是有用的,但是对于较新的硬件,建议使用浮点调用。您会注意到,颜色数组和其他属性可以作为字节、整数、长整型等等来收集。但是他们没有考虑拥有一套专用的 API 调用。

清单 3–4 展示了前一个例子中更新的draw方法。这与第一章中的基本相同,但是它自然有了我们的新朋友,三角粉丝,而不是索引数组。

清单 3–4。 更新了draw方法

`public void draw(GL10 gl)
        {
           gl.glVertexPointer(3, GL11.GL_FLOAT, 0, mFVertexBuffer);
           gl.glColorPointer(4, GL11.GL_UNSIGNED_BYTE, 0, mColorBuffer);

gl.glDrawElements( gl.GL_TRIANGLE_FAN, 6 * 3, gl.GL_UNSIGNED_BYTE, mTfan1);
           gl.glDrawElements( gl.GL_TRIANGLE_FAN, 6 * 3, gl.GL_UNSIGNED_BYTE, mTfan2);
        }`

清单 3–5 显示了对CubeRenderer.java的调整onDrawFrame

清单 3–5。 略加修改后的onDrawFrame

`public void onDrawFrame(GL10 gl)
        {
           gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
           gl.glClearColor(0.0f,0.5f,0.5f,1.0f);                                  //1

gl.glMatrixMode(GL10.GL_MODELVIEW);
           gl.glLoadIdentity();
           gl.glTranslatef(0.0f,(float)Math.sin(mTransY), -7.0f);                 //2
           gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
           gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

mCube.draw(gl);

mTransY += .075f;
        }`

这里没有真正的戏剧性变化:

  • 在第 1 行,我已经添加了gl.glClearColor到这个组合中。这指定了清除框架时应该使用的颜色。这里的颜色是迷人的蓝绿色。
  • 在第 2 行中,翻译中的 z 值被提高到-7,只是为了使立方体的描绘看起来更自然一些。

最后一组变化如清单 3–6 所示,它定义了视见*截头体。这里有更多的信息,以便处理不同的显示大小,并使代码更容易理解。

清单 3–6。 新的*截体代码

`public void onSurfaceChanged(GL10 gl, int width, int height)
        {
            gl.glViewport(0, 0, width, height);

float aspectRatio;
            float zNear =.1f;
            float zFar =1000;
            float fieldOfView = 30.0f/57.3f;                                     //1
            float size;

gl.glEnable(GL10.GL_NORMALIZE);

aspectRatio=(float)width/(float)height;                              //2

gl.glMatrixMode(GL10.GL_PROJECTION);                                 //3

size = zNear * (float)(Math.tan((double)(fieldOfView/2.0f)));        //4

gl.glFrustumf(-size, size, -size /aspectRatio,                       //5
                                        size /aspectRatio, zNear, zFar);

gl.glMatrixMode(GL10.GL_MODELVIEW);                                  //6
        }`

下面是正在发生的事情:

  • 第 1 行用给定的 FOV 定义了*截头体,在选择值时更加直观。按照 Java 数学库的要求,30 度的字段被转换成弧度,而 OpenGL 坚持使用度。
  • 第 2 行中的纵横比基于宽度除以高度。因此,如果宽度为 1024×768,长宽比为 1.33。这有助于确保图像的比例适当缩放。否则,如果视图不考虑长宽比,它的对象看起来会被压扁。
  • 接下来,第 3 行确保当前矩阵模式被设置为投影矩阵。
  • 第 4 行的任务是计算尺寸值,该值需要指定观察体积的左/右和上/下限值,如图图 3–3 所示。这可以被认为是你进入三维空间的虚拟窗口。以屏幕中心为原点,您需要在两个维度上从-size 到+size。这就是磁场被一分为二的原因——因此,对于 60 度的磁场,窗口将从-30 度变为+30 度。将尺寸乘以zNear仅仅是增加了某种缩放暗示。最后,用长宽比除上下限,以确保你的正方形是真正的正方形。
  • 现在我们可以将这些数字输入到gl.glFrustum(),如第 5 行所示,然后将当前矩阵重置回GL10.GL_MODELVIEW

如果工作正常,您应该会看到一些看起来完全像原来的弹性立方体!等等,你没被打动?好的,如果你要那样,让我们给它添加一些旋转。

带她出去兜风

现在是时候给场景添加一些更有趣的动画了。除了上下弹跳之外,我们还要慢慢旋转它。在 glondrawFrames()的底部添加以下一行:

        mAngle+=.4;

右侧GL 之前。glTranslatef()调用,添加以下内容:

        gl.glRotatef(mAngle, 0.0f, 1.0f, 0.0f);         gl.glRotatef(mAngle, 1.0f, 0.0f, 0.0f);

并且自然地将private float mAngle;和其他定义一起添加到文件的底部。

现在再跑一次。“嘿!嗯?”将是最有可能的反应。立方体似乎没有旋转,而是围绕你的视点旋转(同时弹跳),如图 3–7 所示。这说明了基本 3D 动画中最令人困惑的元素之一:获得正确的*移和旋转顺序。(还记得第二章里的讨论吗?)

考虑我们的立方体。如果你想让一个立方体在你面前旋转,正确的顺序是什么?旋转然后*移?还是*移然后旋转?追溯到五年级的数学课,你可能记得学过加法和乘法是可交换的。也就是说,运算的顺序并不重要:a+b=b+a,或者 ab=ba。嗯,3D 变换是而不是可交换的(最后,一个我从未想过我会需要的东西的用途!).也就是说,旋转*移和*移旋转不一样。参见图 3–8。

右边是你现在在旋转立方体例子中看到的。立方体首先被*移,然后被旋转,但是因为旋转是围绕“世界”原点(视点的位置)进行的,所以你会看到它好像在你的头周围旋转。

现在到了明显的不计算时刻:在示例代码中,旋转没有放在*移之前吗?

Image

图 3–7。 *移第一,旋转第二

所以,这应该是导致全国突然皱起眉头的原因:

`gl.glRotatef(mAngle, 0.0f, 1.0f, 0.0f);
        gl.glRotatef(mAngle, 1.0f, 0.0f, 0.0f);

gl.glTranslatef(0.0f, (float)(sinf(mTransY)/2.0f), z);` Image

图 3–8。 先旋转还是先*移?

然而,在现实中,变换的顺序实际上是从最后到第一个应用的。现在将gl.glTranslatef() 放在两个旋转的前面,你应该会看到类似于图 3–9 的东西,这正是我们最初想要的。下面是完成这项工作所需的代码:

`        gl.glTranslatef(0.0f, (float)(sinf(mTransY)/2.0f), z);

gl.glRotatef(mAngle, 0.0f, 1.0f, 0.0f);
        gl.glRotatef(mAngle, 1.0f, 0.0f, 0.0f);`

有两种不同的方法来可视化变换排序:局部坐标和世界坐标方法。在前一种情况下,你将物体移动到它们最终的静止位置,然后进行旋转。由于坐标系是局部的,所以对象将围绕自己的原点旋转,这使得从上到下时前面的序列有意义。如果您选择世界方法,这实际上是 OpenGL 正在做的,您必须在执行*移之前首先围绕对象的局部轴执行旋转。这样,转换实际上是自下而上发生的。最终结果是一样的,代码也是一样的,两者都令人困惑,很容易出现混乱。这就是为什么你会看到许多 3D 男孩或女孩拿着一臂长的东西,同时四处移动,以帮助他们弄清楚为什么他们漂亮的弹射器模型在地面下飞行。我称之为 3D 洗牌

Image

图 3–9。 让立方体旋转

现在需要注意的最后一个转换命令是 gl。glScale(),用于沿所有三个轴调整模型大小。假设你需要将立方体的高度增加一倍。您可以使用行glScalef(1,2,1)。记住高度是和 Y 轴对齐的,而宽度和深度是 X 和 Z,这是我们不想碰的。

现在的问题是,在调用onDrawFrame()中的gl.glRotatef()之前或之后,你会把线放在哪里,以确保立方体的几何形状是唯一受影响的东西,如图图 3–10 中的左图所示?

如果你说了之后,比如这样:

`        gl.glTranslatef(0.0f, (GLfloat)(sinf(mTransY)/2.0f), z);

gl.glRotatef(mAngle, 0.0, 1.0, 0.0);
        gl.glRotatef(mAngle, 1.0, 0.0, 0.0);

gl.glScalef(1,2,1);`

你是对的。这样做的原因是,由于列表中的最后一个变换实际上是第一个执行的变换,如果您想要调整对象几何体的大小,必须将缩放放在任何其他变换之前。把它放在其他任何地方,你可能会得到类似于图 3–10 中右图的东西。那么,那里发生了什么?这是由代码片段生成的:

`        gl.glTranslatef(0.0f, (float)(sinf(mTransY)/2.0f), z);

gl.glScalef(1,2,1);

gl.glRotatef(mAngle, 0.0f, 1.0f, 0.0f);
        gl.glRotatef(mAngle, 1.0f, 0.0f, 0.0f);` Image

图 3–10。 执行旋转前的缩放(左)和旋转后的缩放(右)

首先旋转几何体,然后旋转立方体的局部轴,该轴不再与原点的轴对齐。随后缩放,它沿着世界的 Y 轴伸展,而不是它自己的 Y 轴。这就好像你已经从一个旋转了一半的立方体的顶点列表开始,并且只缩放了它。所以,如果你在最后做了缩放,你的整个世界也缩放了。

调整数值

现在更多的乐趣来了,当我们可以开始玩不同的值。这一节将展示许多不同的原则,这些原则不仅与 OpenGL ES 相关,而且几乎在你可能偶然发现的每一个 3D 工具包中都可以找到。

裁剪区域

有了工作演示,我们可以通过调整一些值和观察变化来获得乐趣。首先,我们将通过将zFar的值从 1000 降低到 6.0 来改变*截头体中的远裁剪*面。为什么呢?记住立方体的本地原点是 7.0,它的大小是 2.0。因此,当直接面对我们时,最接*的点将是 6.0,因为每一边都将跨在原点上,每一边都有 1.0。因此,通过将zFar的值更改为 6.0,当立方体正对着我们时,它将被隐藏。但是有些部分会透过来,看起来就像一片漂浮在水面上的漂浮物。原因是当它旋转时,角自然会更靠*观察*面,如图图 3–11 所示。

Image

图 3–11。 躲猫猫!当立方体的任何部分位于比zFar更远的地方时,立方体被剪切。

那么,当我将*剪裁*面移动得更远时会发生什么呢?将zFar重置为 1000(一个足够大的任意数字,以确保我们可以看到我们练习中的所有内容),并将zNear从. 1 设置为 6.0。你认为它会是什么样子?这将与前面的例子相反。结果见图 3–12。

Image

图 3-12。zFar*面被重置,而zNear*面被移回以剪切立方体中任何过于靠*的部分。

像这样的 z 裁剪在处理大型复杂的世界时非常有用。您可能不希望所有您可能“正在看”的对象都被渲染,因为大多数对象可能离得太远而无法真正看到。设置zFarzNear来限制可视距离可以加快系统速度。然而,这并不是在对象进入管道之前预切割对象的最佳替代方法。

视野

记住,观众的 FOV 也可以在视锥设置中改变。再次回到我们有弹性的朋友那里,确保你的zNearzFar设置回到正常值 0.1 和 1000。现在将gl.onDrawFrame()中的z值改为-20 并再次运行。图 3–13 中最左边的图像是你应该看到的。

接下来我们要放大。转到setClipping(),将fieldOfView=10的度数从 30 度更改为。结果显示在图 3–13 的中间图像中。请注意,与最右边的图像相比,立方体没有明显的消失点或透视效果。当你在相机上使用变焦镜头时,你会看到同样的效果,因为缩短效果是不存在的,使事物看起来像正交投影。

Image

图 3–13。 将物体移开(左),然后用 10 FOV(中)放大。最右边的图像的默认 FOV 值设置为 50,立方体仅在 4 个单位之外。

人脸剔除

让我们回到几页前你可能记得的一行代码:

        gl.glEnable(GL_CULL_FACE);

这使得背面剔除成为可能,在第一章中有介绍,这意味着物体背面的面不会被画出来,因为它们无论如何都不会被看到。它最适用于凸面对象和基本体,如球体或立方体。该系统计算每个三角形的面法线,这是一种判断一个面是朝向我们还是远离我们的方法。默认情况下,面绕组为逆时针方向。因此,如果一个 CCW 面是针对我们的,它将被渲染,而所有其他的将被剔除。如果您的数据是非标准的,您可以用两种不同的方法来改变这种行为。您可以指定正面三角形按顺时针方向排序,或者剔除转储正面而不是背面。要查看这一点,请将下面一行添加到onSurfaceCreated()方法中:gl.glCullFace(GL10.GL_FRONT);

图 3–14 显示了移除前面的三角形,只显示后面的三角形的结果。

注意: gl.glEnable()是一个频繁调用,用于改变各种状态,从消除背面,如前所示,到*滑点(GL10.GL_POINT-SMOOTH)到执行深度测试(GL10.GL_DEPTH_TEST)。

Image

图 3–14。 背面现在可见,而正面被剔除。

建造一个太阳系

有了这些 3D 武器库中的基本工具,我们实际上可以开始建造一个小型太阳系模型的主要项目。太阳系之所以如此理想,是因为它有一个非常基本的简单形状,几个必须都围绕彼此运动的物体,以及一个单一的光源。最初使用立方体示例的原因是,它的形状是 3D 所能得到的最基本的形状,所以代码中没有多余的几何图形。当你到达像一个球体这样的东西时,正如你将看到的,大部分代码将会创建这个对象。

尽管 OpenGL 是一个很好的底层*台,但当涉及到任何更高层次的东西时,它仍有许多不足之处。正如你在第一章中看到的,当谈到建模工具时,许多可用的第三方框架最终可以用来完成这项工作,但目前我们只是坚持使用基本的 OpenGL ES。

注:除了 OpenGL 本身,还有一个流行的助手工具包叫做 GL Utility Toolkit(GLUT) GLUT 为基本的窗口 UI 任务和管理功能提供了可移植的 API 支持。它可以构造一些基本的原语,包括一个球体,因此在做小项目时非常方便。不幸的是,在撰写本文时,还没有官方的 Android 或 iOS 库,尽管目前正在进行一些努力。

首先要做的是创建一个新项目或导入一个以前的项目,该项目建立了通常的 OpenGL 框架,包括渲染器和几何对象。在这种情况下,它是一个由清单 3–7 描述的球体。

清单 3–7。 建造我们的 3D 星球

`package book.SolarSystem;

import java.util.;
import java.nio.
;
import javax.microedition.khronos.opengles.GL10;

public class Planet
{
    FloatBuffer m_VertexData;
    FloatBuffer m_NormalData;
    FloatBuffer m_ColorData;

float m_Scale;
    float m_Squash;
    float m_Radius;
    int m_Stacks, m_Slices;

public Planet(int stacks, int slices, float radius, float squash)
    {
        this.m_Stacks = stacks;                                                     //1
        this.m_Slices = slices;
        this.m_Radius = radius;
        this.m_Squash=squash;

init(m_Stacks,m_Slices,radius,squash,"dummy");
    }

private void init(int stacks,int slices, float radius, float squash, String textureFile)
    {
             float[] vertexData;
             float[] colorData;                                                         //2
             float colorIncrement=0f;

float blue=0f;
             float red=1.0f;
             int numVertices=0;
             int vIndex=0;                          //vertex index
             int cIndex=0;                          //color index

m_Scale=radius;
             m_Squash=squash;

colorIncrement=1.0f/(float)stacks;                                         //3

{
             m_Stacks = stacks;
             m_Slices = slices;

//vertices

vertexData = new float[ 3((m_Slices2+2) * m_Stacks)];           //4

//color data

colorData = new float[ (4(m_Slices2+2) * m_Stacks)];            //5

int phiIdx, thetaIdx;

//latitude

for(phiIdx=0; phiIdx < m_Stacks; phiIdx++)                                  //6
        {
                 //starts at -90 degrees (-1.57 radians) goes up to +90 degrees
                              (or +1.57 radians)

//the first circle
                                                                                    //7
                 float phi0 = (float)Math.PI * ((float)(phiIdx+0) *
                              (1.0f/(float)(m_Stacks)) - 0.5f);

//the next, or second one.
                                                                                    //8
                 float phi1 = (float)Math.PI * ((float)(phiIdx+1) *
                              (1.0f/(float)(m_Stacks)) - 0.5f);

float cosPhi0 = (float)Math.cos(phi0);                             //9
                 float sinPhi0 = (float)Math.sin(phi0);
                 float cosPhi1 = (float)Math.cos(phi1);
                 float sinPhi1 = (float)Math.sin(phi1);

float cosTheta, sinTheta;

//longitude

for(thetaIdx=0; thetaIdx < m_Slices; thetaIdx++)                      //10
             {
                 //increment along the longitude circle each "slice"

float theta = (float) (-2.0f*(float)Math.PI * ((float)thetaIdx) *
                     (1.0/(float)(m_Slices-1)));
                 cosTheta = (float)Math.cos(theta);
                 sinTheta = (float)Math.sin(theta);

//we're generating a vertical pair of points, such
                 //as the first point of stack 0 and the first point of stack 1
                 //above it. This is how TRIANGLE_STRIPS work,
                 //taking a set of 4 vertices and essentially drawing two triangles
                 //at a time. The first is v0-v1-v2 and the next is v2-v1-v3. Etc.

//get x-y-z for the first vertex of stack

vertexData[vIndex+0] = m_ScalecosPhi0cosTheta;               //11
                          vertexData[vIndex+1] = m_Scale(sinPhi0m_Squash);
                          vertexData[vIndex+2] = m_Scale(cosPhi0sinTheta);

vertexData[vIndex+3] = m_ScalecosPhi1cosTheta;
                  vertexData[vIndex+4] = m_Scale(sinPhi1m_Squash);
                  vertexData[vIndex+5] = m_Scale(cosPhi1sinTheta);

colorData[cIndex+0] = (float)red;                              //12
                  colorData[cIndex+1] = (float)0f;
                  colorData[cIndex+2] = (float)blue;
                  colorData[cIndex+4] = (float)red;
                  colorData[cIndex+5] = (float)0f;
                  colorData[cIndex+6] = (float)blue;
                  colorData[cIndex+3] = (float)1.0;
                  colorData[cIndex+7] = (float)1.0;

cIndex+=24;                                                   //13
                          vIndex+=2
3;                                           //14
         }

blue+=colorIncrement;                                            //15
        red-=colorIncrement;

// create a degenerate triangle to connect stacks and maintain winding order

//16
        vertexData[vIndex+0] = vertexData[vIndex+3] = vertexData[vIndex-3];
        vertexData[vIndex+1] = vertexData[vIndex+4] = vertexData[vIndex-2];
        vertexData[vIndex+2] = vertexData[vIndex+5] = vertexData[vIndex-1];
           }

}
            m_VertexData = makeFloatBuffer(vertexData);                          //17
            m_ColorData = makeFloatBuffer(colorData);
}`

好的,所以创建一个像球体一样基本的东西需要很多代码。在标准 OpenGL 中,使用三角形列表比使用四边形列表更复杂,但这就是我们必须要做的。

基本算法是计算的边界,一次两个作为伙伴。堆栈*行于地面,X-Z *面,它们形成了三角形带的边界。因此,计算堆栈 A 和 B,并根据围绕圆的切片数将其细分为三角形。下一次通过将采取堆栈 B 和 C,然后冲洗和重复。两个边界条件适用:

  • 第一个和最后一个堆栈包含我们的伪行星的两极,在这种情况下,他们更像一个三角形的风扇,而不是一个带。然而,为了简化代码,我们将把它们视为条带。
  • 每个条带的末端必须与起点相连,以形成一组连续的三角形。

所以,让我们来分解一下:

  • 在第 1 行中,您可以看到初始化例程使用堆栈和切片的概念来定义球体的分辨率。拥有更多的切片和堆栈意味着一个更*滑的球体,但会使用更多的内存,更不用说额外的处理时间了。将切片想象成类似于苹果楔,代表球体从底部到顶部的一部分。堆叠是横向的切片,定义纬度的部分。参见图 3–15。

    radius参数是比例因子的一种形式。你可以选择规格化你的所有对象并使用glScalef(),但是这确实增加了额外的 CPU 开销,所以在这种情况下 radius 被用作预缩放的一种形式。 Squash 用于创建木星和土星所必需的扁*球体。因为它们都有很高的转速,所以它们被压*了一点。木星的一天只有十个小时左右,而它的直径是地球的十多倍。因此,它的极地直径大约是赤道直径的 93%。土星更加扁*,极地直径只有赤道直径的 90%。挤压值是相对于赤道直径测量的极地直径。值为 1.0 意味着该对象是完美的球形,而土星的挤压值为 0.90,即 90%。

  • 在第 2 行中,我引入了一个颜色数组。因为我们想看一些有趣的东西,直到我们在第五章中看到很酷的纹理,让我们从上到下改变颜色。上面是蓝色的,下面是红色的。第 3 行中计算的颜色增量仅仅是从一个堆栈到另一个堆栈的颜色增量。红色从 1.0 开始向下,蓝色从 0.0 开始向上。

Image

图 3–15。 栈上下,切片周而复始,面细分成三角条。

  • 第 4 行和第 5 行为顶点和颜色分配内存。稍后将需要其他数组来保存光照所需的纹理坐标和面法线,但现在让我们保持简单。请注意,我们使用的是 32 位颜色,就像立方体一样。三个值形成 RGB 三元组,而第四个值用于 alpha (半透明),但在本例中并不需要。m_Slices*2值考虑了由切片和堆栈边界限定的每个面需要两个三角形的事实。正常情况下,这应该是一个正方形,但这里必须是两个三角形。+2 处理这样一个事实,即前两个顶点也是最后一个顶点,所以是重复的。当然,我们需要价值m_Stacks的所有这些东西。

  • 第 6 行开始外环,从最底部的堆栈(或我们星球的南极地区或海拔-90 度)到北极,在+90 度。

    这里用了一些希腊标识符来表示球面坐标。 Phi 通常用于类纬度值,而 theta 用于经度。

  • 第 7 行和第 8 行生成特定条带边界的纬度。首先,当phiIdx为 0 时,我们希望phi0为-90,或-1.57 弧度。. 5 把所有东西向下推 90 度。否则,我们的值会从 0 到 180。

  • 在第 9ff 行中,一些值是预先计算好的,以最小化 CPU 负载。

  • 线 10ff 形成从 0°到 360°的内环,并定义切片。数学是相似的,所以没有必要进入极端的细节,除了我们通过第 11ff 行计算圆上的点。m_Scalem_Squash都在这里发挥作用。但是现在,就假设他们都是 1.0 的数据规范化。

    注意这里寻址的是顶点 0 和顶点 2。vertexData[0]x ,而vertexData[2]z——处理两个*行于地面的部件。由于顶点 1 与 y 相同,所以它对于每个循环都保持不变,并与其表示的纬度一致。因为我们是成对循环,数组元素 3、4 和 5 指定的顶点覆盖了下一个更高的堆栈边界。

    实际上,我们正在生成成对的点,即每个点和它上面的另一个点。这是 OpenGL 期望的三角形条带的格式,如图 Figure 3–16 所示。

Image

图 3–16。 由六个顶点组成的三角形带

  • 在第 12ff 行中,生成了颜色数组,并且与顶点一样,它们是成对生成的。红色和蓝色组件被藏在这里的数组中,暂时没有绿色组件。第 13 行增加了颜色索引,考虑到我们每个循环生成两个四元素颜色条目。
  • 与颜色索引一样,顶点索引也是递增的,如第 14 行所示,但这次只针对三个组件。
  • 在第 15ff 行中,我们增加蓝色,减少红色,确保底部的“极点”是纯红色,而顶部是纯蓝色。
  • 在每个条带的末尾,我们需要创建一些“退化”的三角形,如第 16ff 行所示。术语退化指定三角形实际上由三个相同的顶点组成。实际上,它只是一个点;从逻辑上讲,它是一个三角形,用来连接当前堆栈。
  • 最后,当该说的都说了,该做的都做了,第 17f 行的顶点和颜色数据被转换成 OpenGL 在渲染时可以理解的字节数组。清单 3–8 是完成这项工作的辅助函数。

清单 3–8。 辅助函数生成 OpenGL 能理解的浮点数组

protected static FloatBuffer makeFloatBuffer(float[] arr) {    ByteBuffer bb = ByteBuffer.*allocateDirect*(arr.length*4);    bb.order(ByteOrder.*nativeOrder*());    FloatBuffer fb = bb.asFloatBuffer();    fb.put(arr);    fb.position(0);    return fb; }

既然几何图形已经不碍事了,我们需要把注意力集中在绘制方法上。参见清单 3–9。

清单 3–9。 渲染星球

`public void draw(GL10 gl)
{

gl.glFrontFace(GL10.GL_CW);                                               //1
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, m_VertexData);                    //2
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glColorPointer(4, GL10.GL_FLOAT, 0, m_ColorData);
    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

//3
    gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, (m_Slices+1)2(m_Stacks-1)+2);
}`

现在,您应该能够识别多维数据集示例中的许多元素:

  • 首先在第 1 行,我们指定顺时针方向的面是前面的面。
  • 第 2ff 行将颜色和顶点数据提交给渲染器,确保它能够接受这些数据。
  • 最后(!!)我们现在可以画出我们的小球体了。哎呀,嗯,还没有,还是要分配的。

既然行星对象对于这个例子来说已经足够完整了,让我们来做驱动程序。您可以将弹跳立方体渲染器重命名为类似于SolarSystemRenderer的名称,这将是GLSurfaceView.Renderer接口的一个实例。更改构造函数,看起来像清单 3–10。这会将行星分配到一个相当粗略的分辨率,即十个堆栈和十个切片,半径为 1.0,挤压值为 1.0(即,完美的圆形)。确保声明mPlanet,当然,还要导入 GL10 库。

清单 3–10。 建造师SolarSystemRenderer

public SolarSystemRenderer() {      mPlanet=new Planet(10,10,1.0f, 1.0f); }

顶层刷新方法gl.onDrawFrame(),与立方体方法没有太大区别,如清单 3–11 所示。

清单 3–11。 主绘制方法,位于SolarSystemRenderer

`private float mTransY;
private float mAngle;

public void onDrawFrame(GL10 gl)
{
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    gl.glClearColor(0.0f,0.0f,0.0f,1.0f);
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();

gl.glTranslatef(0.0f,(float)Math.sin(mTransY), -4.0f);

gl.glRotatef(mAngle, 1, 0, 0);
    gl.glRotatef(mAngle, 0, 1, 0);

mPlanet.draw(gl);

mTransY+=.075f;
    mAngle+=.4;
}`

这里没有什么奇特的,因为它实际上与渲染立方体的方法是一样的。从立方体的代码中复制onSurfaceChanged()onSurfaceCreated(),同时暂时注释掉initLighting()initGeometry()。您现在应该能够编译和运行它了。图 3–17 应该是结果。

Image

图 3–17。 未来星球地球

当它旋转时,没有任何特征,你很难看到它的运动。

和前面的例子一样,让我们试验一些参数,看看会发生什么。首先,让我们在SolarSystemRenderer  构造函数中改变堆栈和切片的数量,从 10 到 20。您应该会看到类似于图 3–18 的内容。

Image

图 3–18。 拥有双栈和切片的星球

如果你想让弯曲的物体看起来更*滑,通常有三种方法可以实现:

  • 有尽可能多的三角形。
  • 使用 OpenGL 内置的一些特殊的照明和阴影工具。
  • 使用纹理。

下一章将讨论第二种选择。但是现在,看看需要多少切片和堆叠才能形成一个真正光滑的球体。(两者数量相等时效果最佳。)每个 100 就真的开始好看了。现在,回到 20 人一组。

如果你想看看球体的实际线框结构,将行星的绘制方法中的GL10.GL_TRIANGLE_STRIP改为 GL10。GL_LINE_STRIP。您可能希望将背景颜色改为中灰色,以使线条更加突出(图 3–19,左侧)。作为练习,看看怎样才能得到图 3–19 中的右图。现在问问你自己,为什么我们在那里没有看到三角形,而是看到了那个奇怪的螺旋图案。它只是 OpenGL 绘制和连接线条的方式。我们可以通过指定一个连接数组来呈现三角形轮廓。但是对于我们的最终目标来说,这是不必要的。

Image

图 3–19。 线框模式下的星球

自行将GL10.GL_LINE_STRIP改为GL10.GL_POINTS。你会看到每个顶点都被渲染成一个点。

然后再次尝试截锥。将zNear从. 1 设置为 3.2。(为什么不是 4?物体的距离?)你就会得到图 3–20。

Image

图 3–20。 有人把zNear剪裁*面设置得太*了。

最后一个练习:怎样才能得到看起来像 Figure 3–21 的东西?(这是木星和土星所需要的;因为它们旋转如此之快,它们不是球形而是扁球形。)

Image

图 3–21。 需要什么才能得到这个?

最后,为了加分,让它像立方体一样弹跳。

总结

在这一章中,我们从把 2D 立方体变成三维立方体开始,然后学习如何旋转和*移它。我们还了解了视见*截头体,以及如何使用它来剔除对象和放大缩小我们的场景。最后,我们构建了一个更加复杂的物体,它将成为太阳系模型的基础。下一章将介绍阴影、照明和材质,并将添加第二个物体。

四、开灯

你现在必须坚强。你绝不能放弃。当人们(或代码)让你哭泣,你害怕黑暗时,不要忘记光明一直在那里。

—作者未知

光是画家之首。没有什么东西是如此肮脏,以至于强烈的光线也不会使它变得美丽。

拉尔夫·瓦尔多·爱默生 Ralph Waldo Emerson

船长,一切都很闪亮。不要担心。

——凯莉·弗莱,《萤火虫》

这一章将涵盖 OpenGL ES 的一个最大的主题:照亮、着色和着色虚拟景观的过程。我们在上一章中提到了颜色,但是因为它对照明和阴影都是不可或缺的,所以我们将在这里更深入地讨论它。

光与色的故事

没有光,世界将是一个黑暗的地方(咄)。如果没有颜色,就很难区分交通信号灯。

我们都认为光的奇妙本质是理所当然的——从晨雾中柔和柔和的照明到航天飞机主引擎的点火,再到仲冬雪原上满月羞涩苍白的光芒。关于光的物理学、它的本质和感知已经写了很多。一个艺术家可能要花一辈子的时间才能完全理解如何将悬浮在油中的彩色颜料涂在画布上,在瀑布底部创造出可信的彩虹。这就是 OpenGL ES 在打开场景中的灯光时的任务。

没有诗意,光仅仅是我们的眼睛敏感的全部电磁光谱的一部分。同样的光谱还包括我们的 iPhones 使用的无线电信号,帮助医生的 X 射线,数十亿年前从一颗垂死的恒星发出的伽马射线,以及可以用来加热上周四 Wii 保龄球之夜剩下的一些披萨的微波。

据说光有四个主要特性:波长、强度、偏振和方向。波长决定了我们感知的颜色,或者说我们是否能首先看到任何东西。可见光谱从波长约为 380 纳米的紫色范围开始,一直到波长约为 780 纳米的红色范围。紧接在它下面的是紫外线,在可见光范围的正上方你会发现红外线,我们不能直接看到它,但可以以热的形式间接探测到它。

我们感知物体颜色的方式与物体或其材质吸收的波长有关,否则会干扰迎面而来的光。除了吸收,它还可能被散射(给我们天空的蓝色或日落的红色)、反射和折射。

如果有人说他们最喜欢的颜色是白色,他们一定是说所有的颜色都是他们最喜欢的,因为白色是可见光谱中所有颜色的总和。如果是黑色,他们不喜欢任何颜色,因为黑色就是没有颜色。事实上,这就是为什么你不应该在一个温暖的晴天穿黑色。你的衣服吸收了如此多的能量,却反射了如此少的能量(以光和红外线的形式),以至于其中的一些最终转化为热量。

注:当太阳直射头顶时,它可以发出每*方米约 1 千瓦的辐照度。其中,超过一半的是红外线,感觉非常温暖,而不到一半是可见光,只有少得可怜的 32 瓦用于紫外线。

据说亚里士多德发展了第一个已知的颜色理论。他考虑了四种颜色,每种颜色对应于气、土、水、火四种元素中的一种。

然而,当我们观察可见光谱时,你会注意到从一端的紫色到另一端的红色有一个很好的连续分布,其中既没有水也没有火。你也看不到红色、绿色或蓝色的离散值,它们通常被用来定义不同的色调。19 世纪初,英国学者托马斯·杨开发了三色模型,用三种颜色来模拟所有可见的色调。杨提出视网膜是由成束的神经纤维组成的,它们会对不同强度的红光、绿光或紫光做出反应。德国科学家赫尔曼·赫尔姆霍茨后来在 19 世纪中叶扩展了这一理论。

注:扬是个特别有趣的家伙。(总得有人说。)他不仅是生理光学领域的创始人,在业余时间他还发展了光的波动理论,包括发明了经典的双缝实验,这是大学物理的主要内容。但是等等!还有呢!他还提出了毛细现象理论,第一个使用现代意义上的术语“??”能量“?? ”,部分破译了罗塞塔石碑的埃及部分,并设计了一种改进的乐器调音方法。这位女士肯定严重睡眠不足。

如今,颜色最常通过红绿蓝(RGB)三元组及其相对强度来描述。每种颜色在强度为零的情况下逐渐变为黑色,并随着强度的增加而显示出不同的色调,最终被感知为白色。因为三种颜色需要加在一起才能产生整个光谱,所以这个系统是一个加法模型。

除了 RGB 模式,打印机还使用一种被称为 CMYK 的减色模式,用于青色-品红色-黄色-黑色()。因为这三种颜色不能产生真正深的黑色,所以添加黑色作为深阴影或图形细节的强调。

另一个常见的模型是 HSV 的色调-饱和度-值,你会经常发现它是许多图形包或颜色选择器中 RGB 的替代物。HSV 是 20 世纪 70 年代专门为计算机图形开发的,它将颜色描绘成一个 3D 圆柱体(图 4–1)。饱和度从内到外,值从下到上,色调围绕边缘。这方面的一个变体是 HSL,用值代替亮度。图 4–2 显示了多种版本的 Mac OS X 拾色器。

Image

图 4–1。 HSV 色轮或圆柱体(来源:维基共享资源)

Image

图 4–2。 苹果的 OS-X 标准拾色器——RGB、CMYK、HSV,以及一直流行的克雷奥拉模型

让那里有光

在现实世界中,光从四面八方以各种颜色射向我们,当结合起来时,可以创造日常生活的细节和丰富的场景。OpenGL 并不试图复制任何像真实世界的照明模型,因为这些模型非常复杂和耗时,并且通常保留给迪士尼的渲染农场。但是它可以以一种对实时游戏动作来说足够好的方式来*似它。

OpenGL ES 中使用的光照模型允许我们在场景中放置不同类型的灯光。我们可以随意打开或关闭它们,指定方向、强度、颜色等等。但这还不是全部,因为我们还需要描述模型的各种属性,以及它如何与入射光相互作用。照明定义了光源与物体互动的方式以及创建这些物体的材质。阴影根据照明和材质具体决定像素的颜色。请注意,一张白纸反射的光线与一件粉红色的镜面圣诞装饰品完全不同。综合起来,这些特性被捆绑成一种叫做材质的物体。将材质属性和灯光属性混合在一起会生成最终的场景。

OpenGL 灯光的颜色最多可以由三种不同的成分组成:

  • 传播
  • 环境的
  • 镜子的

漫射光可以说是来自一个方向,如太阳或手电筒。它撞击一个物体,然后向四面八方散射,产生一种令人愉快的柔软感。当漫射光照射到表面时,反射量很大程度上取决于入射角。当直接面对光线时,它会最亮,但随着倾斜得越来越远,它会越来越暗。

环境光是来自没有特定方向的光,被构成环境的所有表面反射。环顾你所在的房间,从天花板、墙壁和家具上反射回来的光线组合在一起形成了环境光。如果你是一名摄影师,你会知道环境照明对于拍摄比单一光源更真实的场景有多重要,特别是在人像摄影中,你会用柔和的“补光”来抵消较亮的主光。

镜面光是从光亮的表面反射回来的光。它来自一个特定的方向,但是在一个表面上以一种更加定向的方式反弹。它会成为我们在迪斯科球或刚清洗上蜡的汽车上看到的热点。当观察点与光源成直线时,光线最亮,当我们绕着物体移动时,光线迅速减弱。

当谈到漫反射和镜面反射照明时,它们通常是相同的颜色。但是,即使我们被限制在八个灯光对象,每个组件有不同的颜色实际上意味着一个单一的 OpenGL“光”可以同时表现得像三个不同的。在这三种颜色中,你可以考虑让环境光有不同的颜色,通常是与主色相反的颜色,以使场景在视觉上更有趣。在太阳系模型中,暗淡的蓝色环境光有助于照亮行星的黑暗面,并赋予其更高的 3D 质量。

注意:你不必为一个给定的灯光指定所有三种类型。漫反射通常在简单的场景中工作得很好。

回到有趣的事情(暂时)

我们还没有完成理论,但是让我们暂时回到编码上来。在那之后,我将会涉及更多关于光线和阴影的理论。

在前面的例子中,你看到了用标准 RGB 版本逐顶点定义的颜色如何让我们在没有任何光照的情况下看到它。现在,我们将创建各种类型的灯,并将它们放置在我们所谓的星球周围。OpenGL ES 必须总共支持至少八个灯光,这是 Android 的情况。但是你当然可以创建更多的,并根据需要添加或删除它们。如果你真的很挑剔,你可以在运行时检查一个特定的 OpenGL 实现支持多少个灯光,方法是使用glGet*的许多变体之一来检索这个值:

    ByteBuffer byteBuffer = ByteBuffer.allocateDirect( 4 );     byteBuffer.order( ByteOrder.LITTLE_ENDIAN );     IntBuffer intBuffer = byteBuffer.asIntBuffer();     drawable.getGL().glGetIntegerv( GL10.GL_MAX_LIGHTS, intBuffer);

注: OpenGL ES 有很多效用函数,glGet*()是最常用的家族之一。glGet*调用可以让你查询各种参数的状态,比如当前的 modelview 矩阵到当前的线宽。确切的调用取决于请求的数据类型。

让我们回到第三章中的示例代码,其中有一个压扁的红色和蓝色星球上下跳动,并进行以下更改:

  1. 从太阳系控制器的方法initGeometry()中更改地球的squash值,从. 7f 更改为 1.0f 以再次圆化球体,并将分辨率(堆叠和切片)更改为 20。

  2. In Planet.java, comment out the line blue+=colorIncrment at the end of the init() method.

    你应该看到什么?来吧,不要偷看。掩盖图 4–3 并猜测。明白了吗?现在你可以编译和运行了。图 4–3 中左边的图像是你应该看到的。现在回到initGeometry方法,将切片和堆栈的数量分别增加到 100。这应该会产生右边的图像。因此,通过简单地改变一些数字,我们有一个粗略的照明和阴影方案。但这只是一个固定的照明方案,当你想要开始移动东西时,它就会坏掉。这时,我们让 OpenGL 来承担重任。

    不幸的是,添加灯光的过程比仅仅调用类似于glMakeAwesomeLightsDude()的东西要复杂一些,我们将会看到。

    Image

    图 4–3。 从下面模拟光照(左)和一个更高的多边形数来模拟明暗处理(右)

  3. 回到第三章的中太阳系渲染器的onSurfaceCreated方法,修改成这样:public void onSurfaceCreated(GL10 gl, EGLConfig config) {        gl.glDisable(GL10.*GL_DITHER*);        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.*GL_FASTEST*);        gl.glEnable(GL10.*GL_CULL_FACE*);        gl.glCullFace(GL10.*GL_BACK*);        gl.glShadeModel(GL10.*GL_SMOOTH*);        gl.glEnable(GL10.*GL_DEPTH_TEST*);        gl.glDepthMask(false);        initGeometry(gl);        initLighting(gl); }

  4. 并添加到渲染器:public final static int *SS_SUNLIGHT* = GL10.*GL_LIGHT0*;

  5. 然后添加清单 4–1 中的代码来打开灯。

清单 4–1。 初始化灯光

private void initLighting(GL10 gl) {     float[] diffuse = {0.0f, 1.0f, 0.0f, 1.0f};                                 //1     float[] pos = {0.0f, 10.0f, -3.0f, 1.0f};                                   //2     gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(pos));          //3     gl.glLightfv(SS_SUNLIGHT, GL10.GL_DIFFUSE, makeFloatBuffer(diffuse));       //4     gl.glShadeModel(GL10.GL_FLAT);                                              //5     gl.glEnable(GL10.GL_LIGHTING);                                              //6     gl.glEnable(SS_SUNLIGHT);                                                   //7 }

这是怎么回事:

  • 照明组件采用标准的 RGBA 标准化形式。所以在这种情况下,没有红色,没有完整的绿色,也没有蓝色。alpha 的最终值现在应该保持在 1.0,因为后面会有更详细的介绍。
  • 线 2 是灯的位置。它的 y 轴为+10,z 轴为-3,行星也是如此。所以,它会在我们的上空盘旋。
  • 在第 3 行和第 4 行中,我们将灯光的位置和漫射组件设置为漫射颜色。glLightfv()是一个新调用,用于设置各种与灯光相关的参数。您可以在以后使用glGetLightfv()来检索这些数据,它可以从特定的灯光中检索任何参数。
  • 在第 5 行我们指定了一个阴影模型。*坦意味着一个面是单一的纯色,而将其设置为GL_SMOOTH将使颜色在整个面上以及面与面之间*滑混合。
  • 最后,第 6 行告诉系统我们想要使用灯光,而第 7 行启用我们创建的一个灯光。

注意:glLightfv()的最终参数取四个GLfloat值的数组;fv 后缀表示“浮点向量”还有一个glLightf()调用来设置单值参数。

现在编译并运行。呃?你说那是什么?你在星系 M31 的中心只看到一个超大质量黑洞大小的黑色东西?哦,对了,我们忘了点东西,抱歉。如前所述,各种版本的 OpenGL 仍然是一个相对低级的库,所以由程序员来处理各种各样的内务处理任务,这些任务是你期望更高级别的系统来管理的(这在 OpenGL ES 2.0 上变得更糟)。一旦灯光打开,预定义的顶点颜色被忽略,所以我们得到黑色。考虑到这一点,我们的球体模型需要一个额外的数据层来告诉系统如何照亮它的小*面,这是通过每个顶点的一组法线来完成的。

什么是顶点法线?法线是与*面或面正交(垂直)的归一化向量。但是在 OpenGL 中,使用顶点法线来代替,因为它们提供了更好的着色。听起来很奇怪,一个顶点可以有自己的“法线”。说到底,一个顶点的“方向”是什么?这实际上在概念上很简单,因为顶点法线仅仅是与顶点相邻的面的法线的归一化总和。参见图 4–4。

Image

图 4–4面法线显示在右边,而三角形扇形的顶点法线显示在左边。

OpenGL 需要所有这些信息来判断顶点的“方向”,这样它就可以计算出有多少光照落在它上面。当它直接瞄准光源时最亮,当它开始倾斜时最暗。这意味着我们需要修改我们的行星生成器来创建一个法线数组以及顶点和颜色数组,如清单 4–2 所示。

清单 4–2。将普通发电机添加到 Planet.java

`private void init(int stacks,int slices, float radius, float squash, String textureFile)
    {
            float[] vertexData;
            float[] colorData;      
            float[] normalData;
            float   colorIncrement=0f;
            float blue=0f;
            float red=1.0f;
            int numVertices=0;
            int vIndex=0;                                                   //Vertex index
            int cIndex=0;                                                   //Color index
            int nIndex =0;                                                  //Normal index

m_Scale=radius;                        
            m_Squash=squash;

colorIncrement=1.0f/(float)stacks;                                  
            m_Stacks = stacks;
            m_Slices = slices;

//Vertices
                    vertexData = new float[ 3((m_Slices2+2) * m_Stacks)];

//Color data
                    colorData = new float[ (4(m_Slices2+2) * m_Stacks)];                  
            // Normalize data
                    normalData = new float [ (3(m_Slices2+2)* m_Stacks)];     //1

int phiIdx, thetaIdx;

//Latitude
            for(phiIdx=0; phiIdx < m_Stacks; phiIdx++)
        {
            //Starts at -90 degrees (-1.57 radians) and goes up to +90 degrees
                   (or +1.57 radians).
            //The first circle
               float phi0 = (float)Math.PI * ((float)(phiIdx+0) * (1.0f/(float)
                   (m_Stacks)) - 0.5f);

//The next, or second one.
               float phi1 = (float)Math.PI * ((float)(phiIdx+1) * (1.0f/(float)
                   (m_Stacks)) - 0.5f);

float cosPhi0 = (float)Math.cos(phi0); float sinPhi0 = (float)Math.sin(phi0);
               float cosPhi1 = (float)Math.cos(phi1);
               float sinPhi1 = (float)Math.sin(phi1);

float cosTheta, sinTheta;

//Longitude
               for(thetaIdx=0; thetaIdx < m_Slices; thetaIdx++)
             {
                    //Increment along the longitude circle each "slice."

float theta = (float) (2.0f*(float)Math.PI * ((float)thetaIdx) *
                        (1.0/(float)(m_Slices-1)));
                    cosTheta = (float)Math.cos(theta);        
                    sinTheta = (float)Math.sin(theta);

//We're generating a vertical pair of points, such
                   //as the first point of stack 0 and the first point of stack 1
                   //above it. This is how TRIANGLE_STRIPS work,
                   //taking a set of 4 vertices and essentially drawing two triangles
                   //at a time. The first is v0-v1-v2 and the next is v2-v1-v3, etc.

//Get x-y-z for the first vertex of stack.

vertexData[vIndex+0] = m_ScalecosPhi0cosTheta;
                   vertexData[vIndex+1] = m_Scale(sinPhi0m_Squash);
                   vertexData[vIndex+2] = m_Scale(cosPhi0sinTheta);

vertexData[vIndex+3] = m_ScalecosPhi1cosTheta;
                   vertexData[vIndex+4] = m_Scale(sinPhi1m_Squash);
                   vertexData[vIndex+5] = m_Scale(cosPhi1sinTheta);

colorData[cIndex+0] = (float)red;
                   colorData[cIndex+1] = (float)0f;
                   colorData[cIndex+2] = (float)blue;
                   colorData[cIndex+4] = (float)red;
                   colorData[cIndex+5] = (float)0f;
                   colorData[cIndex+6] = (float)blue;
                   colorData[cIndex+3] = (float)1.0;
                   colorData[cIndex+7] = (float)1.0;

// Normalize data pointers for lighting.
                   normalData[nIndex + 0] = cosPhi0*cosTheta;             //2
                   normalData[nIndex + 1] = sinPhi0;
                   normalData[nIndex + 2] = cosPhi0*sinTheta;

normalData[nIndex + 3] = cosPhi1*cosTheta;             //3                    normalData[nIndex + 4] = sinPhi1;
                   normalData[nIndex + 5] = cosPhi1*sinTheta;

cIndex+=2*4;
                   vIndex+=23;             nIndex+=23;
          }

//Blue+=colorIncrement;                               
            red-=colorIncrement;

//Create a degenerate triangle to connect stacks and maintain winding order.
        vertexData[vIndex+0] = vertexData[vIndex+3] = vertexData[vIndex-3];
        vertexData[vIndex+1] = vertexData[vIndex+4] = vertexData[vIndex-2];
        vertexData[vIndex+2] = vertexData[vIndex+5] = vertexData[vIndex-1];
           }
        m_VertexData = makeFloatBuffer(vertexData);                                 
        m_ColorData = makeFloatBuffer(colorData);
        m_NormalData = makeFloatBuffer(normalData);

}`

  • 在第 1 行中,法线数组的分配与顶点内存相同,这是一个简单的数组,每个法线包含 3 个组件。
  • 第二部分和第三部分生成正常数据。它看起来并不像前面提到的任何花哨的正常*均方案,那么是什么原因呢?由于我们处理的是一个非常简单的对称形式的球体,法线与没有任何缩放值的顶点相同(以确保它们是单位向量,即长度为 1.0)。请注意,vPtr值和nPtrs值的计算结果实际上是相同的。

注意:你很少需要生成你自己的法线。如果您在 OpenGL ES 中进行任何实际工作,您可能会从第三方应用(如 3D-Studio 或 Strata)中导入模型。他们将为您生成普通数组和其他数组。

还有最后一步,那就是修改Planet.java中的draw()方法,使其看起来像清单 4–3。

清单 4–3。draw套路

public void draw(GL10 gl)        {               gl.glMatrixMode(GL10.GL_MODELVIEW);               gl.glEnable(GL10.GL_CULL_FACE);               gl.glCullFace(GL10.GL_BACK); `              gl.glNormalPointer(GL10.GL_FLOAT, 0, m_NormalData);                   //1
              gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);                         //2

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, m_VertexData);                 
              gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glColorPointer(4, GL10.GL_FLOAT, 0, m_ColorData);        
              gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, (m_Slices+1)2(m_Stacks-1)+2);
       }`

除了添加了第 1 行和第 2 行,用于将普通数据与颜色和顶点信息一起发送到 OpenGL 管道之外,它与原始代码没有太大的不同。如果你有一个非常简单的模型,其中许多顶点都共享相同的法线,你可以转储法线数组并使用glNormal3f()来代替,在这个过程中节省一点内存和 CPU 开销。

让我们做最后一个调整。对于本例,确保分配行星时将堆栈和切片值设置回 10(从第三章的结尾使用的 100)。这样更容易看到一些灯光是如何工作的。现在你可以真正编译和运行它,如果你得到类似于图 4–5 的东西,放松一下,停下来喝杯清凉饮料。

Image

图 4–5*面照明

现在你回来了,我相信你已经发现了一些奇怪的东西。据说几何学是基于一条条三角形,那么为什么脸是那些奇怪的四边三角形呢?

当设置为*面着色时,OpenGL 仅从单个顶点(相应三角形的最后一个顶点)拾取照明提示。现在,不是从水*成对的三角形中画出条带,而是把它们想象成垂直成对的松散耦合,正如你在图 4–6 中看到的。

Image

图 4–6“堆叠”的三角形对

在条带 0 中,将使用顶点 0、1 和 2 绘制三角形 1,顶点 2 用于着色。三角形 2 将使用 2、1 和 3。泡沫,冲洗,并重复其余的地带。接下来,对于条带 1,将绘制具有顶点 4、0 和 5 的三角形 41。但是三角形 42 将使用顶点 5、0 和 2,与三角形 1 具有相同的顶点用于其着色。这就是为什么垂直对组合起来形成一个“弯曲的”四边形。

如今很少有理由使用*面阴影,所以在initLighting()中,将GL_FLAT替换为GL_SMOOTH,在glShadeModel()中,通过改变以下内容来改变灯光的位置:

    float[] pos = {0.0f, 10.0f, -3.0f, 1.0f};

对此:

    float[] pos = {0.0f, 5.0f, 0.0f, 1.0f};    

这将显示更多被照亮的一面。现在你可能知道该怎么做了:编译、运行和比较。然后为了好玩,将球体的分辨率从 20 个切片和分段降低到 5 个。回到*面阴影,然后与*滑阴影进行比较。参见图 4–7。图 4–7 中最右边的图像特别有趣,因为阴影模型开始崩溃,显示出一些沿着脸部边缘的赝像。

Image

图 4–7。 从左到右:*滑着色一个 20 叠 20 片的球体;只有 5 个堆栈和切片的球体上的*面阴影;*滑阴影

光和材质的乐趣

现在,既然我们有一个光滑的球体可以玩,我们可以开始修补其他的灯光模型和材质。但是首先做一个思维实验:假设你有一个绿色的球体,如前所示,但是你的漫射光是红色的。球体会是什么颜色?(暂停危险主题。)准备好了吗?在现实世界中会是什么呢?红绿?绿红色?淡紫色的粉红色?让我们试一试,找出答案。再次修改initLighting(),如清单 4–4 所示。请注意,灯光向量已被重命名为其特定的颜色,以使其更具可读性。

清单 4–4。 添加更多的灯光类型和材质

private void initLighting(GL10 gl) {     float[] diffuse = {0.0f, 1.0f, 0.0f, 1.0f};     float[] pos = {0.0f, 5.0f, -3.0f, 1.0f};     float[] white = {1.0f, 1.0f, 1.0f, 1.0f};     float[] red={1.0f, 0.0f, 0.0f, 1.0f};     float[] green={0.0f,1.0f,0.0f,1.0f}; `    float[] blue={0.0f, 0.0f, 1.0f, 1.0f};
    float[] cyan={0.0f, 1.0f, 1.0f, 1.0f};
    float[] yellow={1.0f, 1.0f, 0.0f, 1.0f};
    float[] magenta={1.0f, 0.0f, 1.0f, 1.0f};
    float[] halfcyan={0.0f, 0.5f, 0.5f, 1.0f};

gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(pos));
    gl.glLightfv(SS_SUNLIGHT, GL10.GL_DIFFUSE, makeFloatBuffer(green));

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, makeFloatBuffer(red));  //1

gl.glShadeModel(GL10.GL_SMOOTH);
    gl.glEnable(GL10.GL_LIGHTING);
    gl.glEnable(SS_SUNLIGHT);
    gl.glLoadIdentity();                                                             //2
}`

如果你看到我们的老朋友,来自 M31 的超大质量黑洞,你做得很好。那么,为什么是黑色的呢?那很简单;还记得本章开始时关于颜色和反射率的讨论吗?只有当照射到红色物体上的光线含有红色成分时,红色物体才会看起来是红色的,而我们的绿光却没有。如果你在一个黑暗的房间里有一个红色的气球,用绿色的光照亮它,它看起来会是黑色的,因为绿色不会回到你身边。如果有人问你在一个黑暗的房间里拿着一个红色的气球在做什么,就吼一声“物理学!”然后用轻蔑的语气告诉他们,他们不会理解的。

因此,有了这样的理解,用绿色替换第一行中的红色漫反射材质。你应该得到什么?对,绿色球体再次被照亮。但是你可能会注意到一些非常有趣的事情。绿色现在看起来比添加材质之前亮了一点。在图 4–8 中左边的图像显示它没有指定任何材质,右边的图像显示它添加了绿色漫射材质。

Image

图 4–8。 无绿色物质定义(左)和有它定义(右)

让我们再做一个实验。让我们使漫射光是一个更传统的白色。现在绿色该怎么办?红色?蓝色怎么样?因为白光有所有这些成分,所以彩色材质应该都显示得一样好。但是如果你再次看到黑球,你改变了材质的颜色,而不是灯光。

镜面照明

那么,镜面反射的东西呢?将下面一行添加到 lights 部分:

    gl.glLightfv(SS_SUNLIGHT,GL10.GL_SPECULAR, makeFloatBuffer(red));

在“材质”部分,添加以下内容:

    gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, makeFloatBuffer(red));

并将灯光的位置更改为如下所示:

    float[] pos={10.0f,0.0f,3.0f,1.0f};

注意:glMaterial*的第一个值必须始终是GL_FRONT_AND_BACK。在普通的 OpenGL 中,你可以在一个面的两面使用不同的材质,但是在 OpenGL ES 中却不行。但是,您仍然必须使用 OpenGL ES 中的前值和后值,否则材质将无法正常工作。

将漫射材质重置回绿色。你应该会看到一些东西,看起来像一大堆黄红色的东西。简而言之,我们可以用另一个值来玩灯光。称为闪亮度,它指定物体表面有多亮,范围从 0 到 128。该值越高,反射越集中,因此看起来越亮。但是由于它默认为 0,它将镜面财富分布在整个星球上。它压倒了绿色,以至于当与红色混合时,它显示为黄色。因此,为了控制这种混乱,添加以下代码行:

    gl.glMaterialf(GL10.GL_FRONT_AND_BACK,GL10.GL_SHININESS, 5);

我将很快解释这背后的数学原理,但是现在先看看值 5 会发生什么。接下来尝试 25,并将其与图 4–9 进行比较。光泽度值从 5 到 10 大致对应塑料;再大一点,我们就进入了严肃的金属领域。

Image

图 4–9。 光泽度分别设置为 0、5.0 和 25.0

环境照明

是时候享受一下环境照明了。在initLighting()中增加以下一行:然后编译并运行:

    gl.glLightfv(SS_SUNLIGHT, GL10.GL_AMBIENT, makeFloatBuffer(blue));

看起来像不像图 4–10 中左边的图像?你应该怎么做才能得到右边的图像?您需要添加下面一行:

    gl.glMaterialfv(GL10.*GL_FRONT_AND_BACK*, GL10.*GL_AMBIENT*, *makeFloatBuffer*(blue)); Image

图 4–10仅蓝色环境光(左),环境光和环境光材质(右)

除了每个灯光的环境属性,还可以设置一个世界环境值。与所有灯光参数一样,基于灯光的值也是变量,因此它们作为距离、衰减等的函数而变化。世界值在整个 OpenGL ES 世界中是一个常数,可以通过在initLighting()例程中添加以下内容来设置:

    float[] colorVector={r, g, b, a};     gl.glLightModelfv(GL10.*GL_LIGHT_MODEL_AMBIENT*, *makeFloatBuffer*(colorVector));

默认值是由红色=.2f、绿色=.2f 和蓝色=.2f 组成的暗灰色。这有助于确保无论发生什么情况,您的对象总是可见的。当我们这样做的时候,glLightModelfv()还有一个值,它是由GL_LIGHT_MODEL_TWO_SIDE的参数定义的。该参数实际上是一个布尔浮点数。如果是 0.0,只照亮一边;否则,两者都会。默认值为 0.0。如果出于任何原因,你想改变哪些面是正面,你可以用和glFrontFace()指定顺时针或逆时针排序的三角形代表正面。默认为 CCW。

后退一步

那么,这里到底发生了什么?实际上,相当多。有三种通用的着色模型用于实时计算机图形。OpenGL ES 1.1 使用了其中的两个,这两个我们都见过。第一种是*面模型,简单地用一个常量值给每个三角形着色。你已经在图 4–5 中看到了它的样子。在过去的好日子里,这是一个有效的选择,因为它比其他任何方式都要快得多。然而,当你口袋里的 iPhone 大致相当于手持 Cray-1 时,那些速度技巧真的是过去的事情了。*滑模型使用插值阴影,计算每个顶点的颜色,然后在面上进行插值。OpenGL 实际使用的是一种特殊形式的着色,叫做古劳着色。这是基于所有相邻面的法线生成顶点法线的地方。

第三种着色被称为 Phong ,因为 CPU 开销高,所以在 OpenGL 中不使用。它不是在面上插值颜色值,而是插值法线,为每个片段(即像素)生成一个法线。这有助于消除由高曲率定义的边缘上的一些人为因素,这些因素会产生非常尖锐的角度。Phong 可以减少这种效果,但是使用更多的三角形来定义你的对象也可以。

还有许多其他模型。20 世纪 70 年代,JPL/航海家动画公司的吉姆·布林创造了一种改进的 Phong 着色形式,现在称为 Blinn-Phong 模型。如果光源和观看者可以被视为在无限远处,那么计算强度可以更小。

Minnaert 模型倾向于给漫反射材质增加一点对比度。柳文欢-纳耶在漫反射模型中添加了一个“粗糙度”组件,以便更好地匹配现实。

发光材质

还有一个我们需要考虑的影响最终颜色的重要参数是GL_EMISSION。与漫反射、环境光和镜面反射不同,GL_EMISSION仅用于材质,并指定材质在质量上具有发射。一个发射物体有它自己的内部光源,比如太阳,这在太阳系模型中会派上用场。要查看实际效果,在initLighting()的其他材质代码中添加下面一行,并删除周围材质:

    gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, makeFloatBuffer(yellow));

因为黄色是最强烈的,你期望看到什么?大概就像图 4–11 中左边的图像。接下来将这些值减半,这样就有了:

    float[] yellow={.5f, .5f, 0.0f, 1.0f};

现在你看到了什么?我敢打赌它看起来有点像图 4–11 中的右图。

Image

图 4–11一个发射材质设置为黄色全强度的物体(左);同样的场景,但是只有 50%的亮度(右)

从表面上看,发光材质可能看起来就像使用环境照明的结果。但与环境光不同,场景中只有一个对象会受到影响。附带的好处是,它们不会用尽额外的轻物体。然而,如果你的发射物体确实代表了任何种类的真实灯光,比如太阳,在里面放一个灯光物体肯定会给场景增加一层真实性。

关于材质的另一个注意事项:如果你的对象已经指定了颜色顶点,就像我们的立方体和球体一样,这些值可以用来代替设置材质。你必须使用gl.glEnable(GL10.GL_COLOR_MATERIAL);。这将把顶点颜色应用到阴影系统,而不是那些由glMaterial *调用指定的颜色。

衰减

当然,在现实世界中,物体离光源越远,光线越弱。OpenGL ES 也可以使用以下三个衰减因子中的一个或多个来模拟该因子:

  • GL _ 常数 _ 衰减
  • GL _ 线性 _ 衰减
  • GL _ 二次衰减

所有这三个值组合在一起形成一个值,然后该值会计算到模型每个顶点的总照度中。使用 gLightf (GLenum light, GLenum pname, GLfloat param)设置它们,其中 light 是您的光源 ID,例如GL_LIGHT0, pname是前面列出的三个衰减参数之一,实际值使用param传递。

线性衰减可用于模拟由雾等因素引起的衰减。随着距离的增加,二次衰减模拟光的natural衰减,这呈指数变化。当灯光的距离增加一倍时,照明会减少到原来的四分之一。

我们暂且只看一个,GL_LINEAR_ATTENUATION。这三者背后的数学原理将在稍后揭晓。将下面一行添加到initLighting():

gl.glLightf(SS_SUNLIGHT, GL10.GL_LINEAR_ATTENUATION, .025f);

为了让事情在视觉上更加清晰,请确保关闭发光材质。你看到了什么?现在,在位置向量中将 x 轴上的距离从 10 增加到 50。图 4–12 显示了结果。

Image

图 4–12。 光线的 x 距离为 10(左)和 50(右),衰减常数为 0.025。

聚光灯

标准灯光默认为各向同性模型;也就是说,它们就像一盏没有灯罩的台灯,向四面八方均匀地(而且是刺眼地)照射着。OpenGL 提供了三个额外的照明参数,可以将普通光转化为*行光:

  • GL _ SPOT _ 方向
  • GL _ SPOT _ 指数
  • GL _ SPOT _ CUTOFF

因为它是*行光,所以由你使用GL_SPOT_DIRCTION矢量来瞄准它。默认为 0,0,-1,指向-z轴,如图图 4–13 所示。否则,如果您想要更改它,您可以使用类似下面的调用,将它沿着+x 轴移动:

    GLfloat direction[]={1.0,0.0,0.0};     gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPOT_DIRECTION, direction); Image

图 4–13聚光灯对准默认方向

GL_SPOT_CUTOFF指定聚光灯光束从聚光灯圆锥体中心渐变到 0 强度的角度,自然是整个光束角直径的一半。对于 90 度的光束宽度,默认值为 45 度。该值越低,光束越窄。

第三个也是最后一个聚光灯参数GL_SPOT_EXPONENT,确定了光束强度的下降率,这也是另一种形式的衰减。OpenGL ES 将取光束的中心轴和任意顶点的中心轴所形成的角度的余弦。,并将其提升到GL_SPOT_EXPONENT的幂。因为其默认值为 0,所以灯光的强度在照明区域的所有部分都是相同的,直到达到截止值,然后降到零。

灯光参数进场

表 4–1 总结了本节涵盖的各种灯光参数。

Image

阴影背后的数学原理

正如你所看到的,漫反射阴影模型给物体一个非常*滑的外观。它使用一种叫做兰伯特照明模型的东西。朗伯照明简单地说,一个特定的面越直接对准光源,它就越亮。天空中的太阳越高,你脚下的土地就越明亮。或者在更晦涩但精确的技术版本中,反射光随着入射光、 I 和面部法线、 N 之间的角度Image从 0 度增加到 1 度,基于 cos Image从 90 度减少到 0 度。参见图 4–14。这里有一个快速思维实验:当Image是 90 度时,它是从侧面过来的;cos(90)为 0,那么沿 N 的反射光自然也要为 0。当它垂直向下时,*行于 N,cos(0)将为 1,因此最大量将被反射回来。这可以更正式地表达如下:

Id= kdIIcos(Image)

Id是漫反射的强度,KI是入射光线的强度, k d 表示与物体材质的粗糙度松散耦合的漫反射率不严格地说是指在许多真实世界的材质中,实际的表面可能有些抛光,但仍然是半透明的,而下面的层执行散射。像这样的材质可能同时具有强漫射和镜面反射成分。此外,在现实生活中,每个色带可能都有自己的 k 值,因此红色、绿色和蓝色都有一个。**

Image

图 4–14对于完全漫射的表面,入射光束的反射强度将是该光束的垂直分量,或者是入射光束和表面法线之间角度的余弦。

镜面反射

如前所述,除了更普通的漫反射表面之外,镜面反射还为你的模型提供了闪亮的外观。很少有东西是完全*坦或完全闪亮的,大多数都介于两者之间。事实上,地球的海洋是很好的镜面反射体,在远距离拍摄的地球图像上,可以清楚地看到海洋中的太阳反射。

与所有方向都相等的漫反射不同,镜面反射高度依赖于观察者的角度。我们被告知入射角=反射角。这是真的足够完美的反射器。但是除了镜子,51 年的斯图贝克的鼻子,或者那个赛昂百夫长在把你轰进 15 万年前擦亮的前额,很少有东西是完美的反射器。因此,入射光线会有轻微的散射;参见图 4–15。

Image

图 4–15。 对于镜面反射来说,入射光线是散射的,但只围绕其反射部分的中心。

镜面分量的等式可以表示如下:

I 镜面=W(q)I光线cosnImage

其中:

I 光线是入射光线的强度。

W ( q )是基于 I 的角度的表面反射程度。

n的闪亮因子(听着耳熟?).

Image是反射光线与射向眼睛的光线之间的角度。

这实际上是基于所谓的菲涅耳反射定律,这就是 W(q)值的来源。虽然 W(q)不直接用于 OpenGL ES 1.1,因为它随入射角变化,因此比镜面照明模型稍微复杂一些,但它可以用于 OpenGL ES 2.0 版本的着色器中。在这种情况下,它将特别有用,例如,在水面上做反射。取而代之的是一个基于材质设置的高光值的常数。

闪亮因子,也称为镜面指数,是我们之前用过的。然而,在现实生活中 n 可以远远高于最大值 128。

衰减

现在回到前面列出的三种衰减:常数、线性和二次衰减。总衰减计算如下,其中kc为常数, k l 为线性值, k q 为二次分量, d 代表光源与任意顶点的距离:

Image

总结这一切

所以,现在你可以看到,有许多因素在起作用,仅仅是为我们场景中的任何模型的任何顶点生成颜色和颜色强度。其中包括以下内容:

  • 距离衰减
  • 漫射灯光和材质
  • 镜面光和材质
  • 聚光灯参数
  • 环境光和材质
  • 发光
  • 材质的发射率

您可以将所有这些视为作用于整个颜色矢量或颜色的每个单独的 R、G 和 B 分量。

因此,为了说明一切,最终的顶点颜色将如下:

颜色 = 环境世界模型环境材质 + 发光材质+强度光线**

其中:

Image

换句话说,一旦我们考虑到衰减,漫射,镜面反射和聚光灯元素,颜色等于一些不受灯光控制的东西加上所有灯光的强度。

计算时,这些值分别作用于相关颜色的 R、G 和 B 分量。

那么,这一切是为了什么?

理解幕后发生的事情很方便的一个原因是它有助于使 OpenGL 和相关工具不那么神秘。就像你学外语一样,说克林贡语(如果你,亲爱的读者,是克林贡语,majQa ' nuq DAQ ' oH puch pa ' ' e '!),它不再是它曾经是神秘的;在咆哮是咆哮,咆哮是咆哮的地方,现在你可能会认为它们是一首关于好茶的可爱的诗。

另一个原因是,正如前面提到的,所有这些好的“高级”工具在 OpenGL ES 2.0 中都没有。大多数早期的着色算法都必须由你用一点点代码来实现,这些代码叫做着色器。幸运的是,关于最常见着色器的信息可以在互联网上获得,并且复制了以前的信息,相对简单。

更多有趣的东西

现在,有了所有这些光子的优点,是时候回去编码并引入不止一种光了。次要灯光可以不费吹灰之力就让场景的真实性产生惊人的巨大差异。

回到initLighting(),让它看起来像清单 4–5。这里我们再添加两盏灯,分别命名为SS_FILLLIGHT1SS_FILLLIGHT2。将其定义添加到渲染器的类中:

     public final static int *SS_SUNLIGHT1* = GL10.*GL_LIGHT1*;      public final static int *SS_SUNLIGHT2* = GL10.*GL_LIGHT2*;

现在编译并运行。你看到图 4–16 中左边的图像了吗?如前所述,这是 Gouraud 着色模型崩溃的地方,暴露了三角形的边缘。解决方案是什么?此时,只需将每个切片和堆叠的数量从 20 个增加到 50 个,您将获得更加令人满意的图像,如图 4–16 右侧所示。

清单 4–5。 增加两个补光灯

`private void initLighting(GL10 gl)
{
            float[] posMain={5.0f,4.0f,6.0f,1.0f};
            float[] posFill1={-15.0f,15.0f,0.0f,1.0f};
            float[] posFill2={-10.0f,-4.0f,1.0f,1.0f};

float[] white={1.0f,1.0f,1.0f,1.0f};
            float[] red={1.0f,0.0f,0.0f,1.0f};
            float[] dimred={.5f,0.0f,0.0f,1.0f};

float[] green={0.0f,1.0f,0.0f,0.0f};
            float[] dimgreen={0.0f,.5f,0.0f,0.0f};
            float[] blue={0.0f,0.0f,1.0f,1.0f};
            float[] dimblue={0.0f,0.0f,.2f,1.0f};

float[] cyan={0.0f,1.0f,1.0f,1.0f};
            float[] yellow={1.0f,1.0f,0.0f,1.0f};
            float[] magenta={1.0f,0.0f,1.0f,1.0f};
            float[] dimmagenta={.75f,0.0f,.25f,1.0f};

float[] dimcyan={0.0f,.5f,.5f,1.0f};

//Lights go here.

gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(posMain));
            gl.glLightfv(SS_SUNLIGHT, GL10.GL_DIFFUSE, makeFloatBuffer(white));
            gl.glLightfv(SS_SUNLIGHT, GL10.GL_SPECULAR, makeFloatBuffer(yellow));

gl.glLightfv(SS_FILLLIGHT1, GL10.GL_POSITION, makeFloatBuffer(posFill1));
            gl.glLightfv(SS_FILLLIGHT1, GL10.GL_DIFFUSE, makeFloatBuffer(dimblue));
            gl.glLightfv(SS_FILLLIGHT1, GL10.GL_SPECULAR, makeFloatBuffer(dimcyan));

gl.glLightfv(SS_FILLLIGHT2, GL10.GL_POSITION, makeFloatBuffer(posFill2));
            gl.glLightfv(SS_FILLLIGHT2, GL10.GL_SPECULAR, makeFloatBuffer(dimmagenta));
            gl.glLightfv(SS_FILLLIGHT2, GL10.GL_DIFFUSE, makeFloatBuffer(dimblue));

gl.glLightf(SS_SUNLIGHT, GL10.GL_QUADRATIC_ATTENUATION, .005f);

//Materials go here.

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, makeFloatBuffer(cyan));             gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, makeFloatBuffer(white));

gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 25);

gl.glShadeModel(GL10.GL_SMOOTH);
            gl.glLightModelf(GL10.GL_LIGHT_MODEL_TWO_SIDE, 1.0f);

gl.glEnable(GL10.GL_LIGHTING);
            gl.glEnable(SS_SUNLIGHT);
            gl.glEnable(SS_FILLLIGHT1);
            gl.glEnable(SS_FILLLIGHT2);

gl.glLoadIdentity();
}` Image

图 4–16。 三灯,一主二补。左图是一个低分辨率的球体,而右图是高分辨率的。

在前面的示例中,涵盖了许多新的 API 调用,这些调用汇总在 Table 4–2 中。了解他们——他们是你的朋友,你会经常用到他们。

回到太阳系

现在我们有足够的工具回到太阳系项目。等等,这里有很多材质要讲。特别是 OpenGL 的一些其他方面,它们与照明或材质无关,但需要在太阳系模型变得更加复杂之前解决。

首先,我们需要向 renderer 类添加一些新的方法声明和实例变量。参见清单 4–6。

清单 4–6。 为太阳和地球做准备。

`public final static int X_VALUE = 0;

public final static int Y_VALUE = 1;

public final static int Z_VALUE = 2;

Planet m_Earth;     Planet m_Sun;
    float[] m_Eyeposition = {0.0f, 0.0f, 0.0f};`

接下来,需要生成第二个对象,在本例中是我们的太阳,并调整其大小和位置。当我们这样做的时候,改变地球的大小,使它比太阳小。因此,用清单 4–7 中的替换渲染器构造函数中的初始化代码。

清单 4–7。 添加第二个物体并初始化观察者的位置

`    m_Eyeposition[X_VALUE] = 0.0f;                                     //1
    m_Eyeposition[Y_VALUE] = 0.0f;
    m_Eyeposition[Z_VALUE] = 5.0f;

m_Earth = new Planet(50, 50, .3f, 1.0f);                           //2
    m_Earth.setPosition(0.0f, 0.0f, -2.0f);                            //3

m_Sun = new Planet(50, 50,1.0f, 1.0f);                             //4
    m_Sun.setPosition(0.0f, 0.0f, 0.0f);                               //5`

事情是这样的:

  • 我们的眼点,线 1,现在在 z 轴上有一个明确定义的+5 位置。
  • 在第二行,地球的直径缩小到 0.3。
  • 初始化地球的位置,从我们的角度看,在太阳的后面,在 z=-2,第 3 行。
  • 现在,我们可以创建太阳,并将其放置在相对虚假的太阳系的中心,即 4 号线和 5 号线。

initLighting()需要看起来像在清单 4–8 中,清除了前面例子中的所有混乱。

清单 4–8。 太阳系模型的扩展照明

`private void initLighting(GL10 gl)
{
    float[] sunPos={0.0f, 0.0f, 0.0f, 1.0f};
    float[] posFill1={-15.0f, 15.0f, 0.0f, 1.0f};
    float[] posFill2={-10.0f, -4.0f, 1.0f, 1.0f};
    float[] white={1.0f, 1.0f, 1.0f, 1.0f};
    float[] dimblue={0.0f, 0.0f, .2f, 1.0f};
    float[] cyan={0.0f, 1.0f, 1.0f, 1.0f};
    float[] yellow={1.0f, 1.0f, 0.0f, 1.0f};
    float[] magenta={1.0f, 0.0f, 1.0f, 1.0f};
    float[] dimmagenta={.75f, 0.0f, .25f, 1.0f};
    float[] dimcyan={0.0f, .5f, .5f, 1.0f};

//Lights go here.     gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(sunPos));
    gl.glLightfv(SS_SUNLIGHT, GL10.GL_DIFFUSE, makeFloatBuffer(white));
    gl.glLightfv(SS_SUNLIGHT, GL10.GL_SPECULAR, makeFloatBuffer(yellow));

gl.glLightfv(SS_FILLLIGHT1, GL10.GL_POSITION, makeFloatBuffer(posFill1));
    gl.glLightfv(SS_FILLLIGHT1, GL10.GL_DIFFUSE, makeFloatBuffer(dimblue));
    gl.glLightfv(SS_FILLLIGHT1, GL10.GL_SPECULAR, makeFloatBuffer(dimcyan));

gl.glLightfv(SS_FILLLIGHT2, GL10.GL_POSITION, makeFloatBuffer(posFill2));
    gl.glLightfv(SS_FILLLIGHT2, GL10.GL_SPECULAR, makeFloatBuffer(dimmagenta));
    gl.glLightfv(SS_FILLLIGHT2, GL10.GL_DIFFUSE, makeFloatBuffer(dimblue));

//Materials go here.

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, makeFloatBuffer(cyan));
    gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, makeFloatBuffer(white));

gl.glLightf(SS_SUNLIGHT, GL10.GL_QUADRATIC_ATTENUATION,.001f);
    gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 25);    
    gl.glShadeModel(GL10.GL_SMOOTH);    
    gl.glLightModelf(GL10.GL_LIGHT_MODEL_TWO_SIDE, 0.0f);

gl.glEnable(GL10.GL_LIGHTING);
    gl.glEnable(SS_SUNLIGHT);
    gl.glEnable(SS_FILLLIGHT1);
    gl.glEnable(SS_FILLLIGHT2);
}`

自然地,顶层的 execute 方法必须彻底修改,同时添加一个小的实用函数,如清单 4–9 所示。

清单 4–9新的渲染方式

`static float angle = 0.0f;

private void onDrawFrame(GL10 gl)
{
    float paleYellow[]={1.0f, 1.0f, 0.3f, 1.0f};                                       //1
    float white[]={1.0f, 1.0f, 1.0f, 1.0f};
    float cyan[]={0.0f, 1.0f, 1.0f, 1.0f};
    float black[]={0.0f, 0.0f, 0.0f, 0.0f};                                            //2

float orbitalIncrement= 1.25f;                                                     //3
    float[] sunPos={0.0f, 0.0f, 0.0f, 1.0f};

gl.glEnable(GL10.GL_DEPTH_TEST);
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);     gl.glClearColor(0.0f,0.0f,0.0f,1.0f);
    gl.glPushMatrix();                                                                 //4

gl.glTranslatef(-m_Eyeposition[X_VALUE], -m_Eyeposition[Y_VALUE],-
    m_Eyeposition[Z_VALUE]);                                                           //5

gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(sunPos));              //6
    gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, makeFloatBuffer(cyan));
    gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, makeFloatBuffer(white));

gl.glPushMatrix();                                                                 //7
    angle+=orbitalIncrement;                                                           //8
    gl.glRotatef(angle, 0.0f, 1.0f, 0.0f);                                             //9
    executePlanet(m_Earth, gl);                                                        //10
    gl.glPopMatrix();                                                                  //11

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, makeFloatBuffer(paleYellow));  
                                                                                       //12
    gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, makeFloatBuffer(black)); //13
    executePlanet(m_Sun, gl);                                                          //14

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, makeFloatBuffer(black)); //15

gl.glPopMatrix();                                                                  //16
}

private void executePlanet(Planet m_Planet, GL10 gl)
{
        float posX, posY, posZ;
        posX = m_Planet.m_Pos[0];                                                      //17
        posY = m_Planet.m_Pos[1];
        posZ = m_Planet.m_Pos[2];

gl.glPushMatrix();
        gl.glTranslatef(posX, posY, posZ);                                             //18
        m_Planet.draw(gl);                                                             //19
        gl.glPopMatrix();
}`

事情是这样的:

  • 第 1 行创建了一个较浅的黄色阴影。这只是给太阳涂上了更精确的颜色。
  • 如果需要,我们需要一个黑色来“关闭”一些材质特性,如第 2 行所示。
  • 在第 3 行中,需要轨道增量来使地球绕太阳运行。
  • 第 4 行的glPushMatrix()是一个新的 API 调用。当与glPopMatrix()结合时,它有助于将世界的一部分与另一部分的转换隔离开来。在这种情况下,第一个glPushMatrix实际上阻止了随后对glTranslate()的调用向自身添加新的翻译。你可以转储glPush/PopMatrix对,把onDrawFrame()中的glTranslate放到初始化代码中,只要它只被调用一次。
  • 第 5 行的*移确保对象从我们的视点“移开”。请记住,OpenGL ES 世界中的一切都是围绕眼点进行的。我更喜欢有一个不依赖于观察者位置的公共原点,在这种情况下,它是太阳的位置,用视点的偏移量来表示。
  • 第 6 行仅仅强调了太阳的位置在原点。
  • 哦!7 号线另一个glPushMatrix()。这确保了地球上的任何变化都不会影响太阳。
  • 第 8 行和第 9 行使地球绕太阳运行。怎么会?在第 10 行中,调用了一个小的实用函数。它执行任何过渡,并在需要时将对象从原点移开。正如您所记得的,转换可以被认为是最后调用/第一次使用的。因此,executePlanets()中的翻译实际上是首先执行的,然后是glRotation。请注意,这种方法将使地球以正圆轨道运行,然而在现实中,没有行星会有正圆轨道,所以将使用glTranslation
  • 第 11 行转储任何地球独有的转换。
  • 第 12 行设置太阳的物质是发射性的。注意对glMaterialfv的调用没有绑定到任何特定的对象。它们设置所有后续对象使用的当前材质,直到进行下一次调用。第 13 行关闭任何用于地球的镜面反射设置。
  • 第 14 行再次调用我们的工具,这次是太阳。
  • 发射材质属性被关闭,在第 15 行,接着是另一个glPopMatrix()。请注意,每次使用 push 矩阵时,它都必须与 pop 配对使用。OpenGL ES 可以处理高达 16 层的堆栈。此外,由于 OpenGL 中使用了三种矩阵(模型视图、投影和纹理),请确保您正在推送/弹出正确的堆栈。您可以通过记住使用glMatrixMode()来确保这一点。
  • 现在在executePlanet()中,第 17 行获得行星的当前位置,因此第 18 行可以将行星*移到适当的位置。在这种情况下,它实际上从未改变,因为我们让glRotatef()处理轨道任务。否则,xyz 会随着时间不断变化。
  • 最后在第 19 行调用星球自己的drawing例程。

对于 Planet.java,将下面一行添加到实例变量中:

    public float[] m_Pos = {0.0f, 0.0f, 0.0f};

在 execute 方法之后,添加清单 4–10 中的代码,定义新的方法。

清单 4–10。m _ Pos 的设定者

public void setPosition(float x, float y, float z) {     m_Pos[0] = x;     m_Pos[1] = y;     m_Pos[2] = z; }

当你这么做的时候,让我们调低背景的灰色。应该是太空,太空不是灰色的。返回渲染器 onDrawFrame(),将对glClearColor的调用更改如下:

    gl.glClearColor(0.0f,0.0f, 0.0f, 1.0f);

现在编译并运行。您应该会看到类似于图 4–17 的内容。

Image

图 4–17。 中间发生了什么?

这里有点奇怪。跑步的时候,你应该看到地球从太阳的左侧后面出来,朝着我们的方向绕行到太阳前面穿过,然后移开,再次重复绕行。那么,为什么我们看不到中间图像中太阳前方的地球(图 4–17)?

在所有的图形中,无论是计算机还是其他,绘制的顺序起着很大的作用。如果你在画肖像,你先画背景。如果你正在生成一个小太阳系,那么,应该先画太阳(呃,也许不是…或者不总是)。

渲染顺序,或深度排序,以及如何确定什么对象遮挡其他对象一直是计算机图形学的一大部分。在添加太阳之前,渲染顺序是无关紧要的,因为只有一个对象。但是随着世界变得越来越复杂,你会发现有两种方法可以解决这个问题。

第一种叫做画家算法。这意味着简单地先画最远的物体。对于像一个球体绕另一个球体旋转这样简单的事情来说,这是非常容易的。但是,当你拥有像《魔兽世界》或《第二人生》这样非常复杂的 3D 沉浸式世界时,会发生什么呢?这些实际上会使用 painter 算法的一个变体,但是会预先计算一些信息来确定所有可能的遮挡顺序。该信息然后被用来形成一个二进制空间划分 (BSP)树。3D 世界中的任何地方都可以映射到树中的一个元素,然后可以遍历树以获取查看者位置的最佳顺序。这在执行上非常快,但是设置起来很复杂。幸运的是,对于我们简单的宇宙来说,这是一种过度杀戮。深度排序的第二种方法根本不是排序,而是实际上使用每个单独像素的 z 分量。屏幕上的一个像素有一个 x 和 y 值,但它也可以有一个 z 值,即使我面前的优派是一个*面 2D 表面。当一个像素准备在另一个像素上绘制时,z 值被比较,两者中较*的胜出。它被称为 z 缓冲,非常简单明了,但对于非常复杂的场景,它会占用额外的 CPU 时间和图形内存。我更喜欢后者,OpenGL 使得 z 缓冲非常容易实现。

在方法 OnSurfaceCreated 中,找到:

    gl.glDepthMask(false);

并替换为

    gl.glDepthMask(true);

如果它工作正常,你现在应该看到地球在前面时遮住太阳,或者在后面时被遮住。参见图 4–18。

Image

图 4–18。 使用 z 缓冲器

乐队继续演奏

在 Android 下,GLSurfaceView 对象没有默认为“真正的”32 位颜色,即红色、绿色、蓝色和 alpha 各 8 位。相反,它选择了一种质量稍低的模式,称为“RGB565”,每像素仅使用 16 位,或者红色使用 5 位,绿色使用 6 位,蓝色使用 5 位。使用像这样的低分辨率颜色模式,您可能会在*滑着色的对象上看到“带状”伪像,如图 Figure 4–19 中左侧的图像所示。这仅仅是因为没有足够的颜色可用。但是,您可以指示 GLSurfaceView 使用更高分辨率的模式,从而生成图 4–19 中右侧的图像。在 activity 对象的 onCreate()处理程序中使用以下代码:

    GLSurfaceView view = new GLSurfaceView(this);     view.setEGLConfigChooser(8,8,8,8,16,0);                       //The new line     view.setRenderer(new SolarSystemRenderer());     setContentView(view);

新行(view.setEGLConfigChooser(8,8,8,8,16,0)告诉视图每种颜色使用 8 位,此外还有 16 位用于深度或 z 缓冲,如上所述。最终值是模板的,将在第七章的中介绍。

Image

图 4–19。 左边 16 位颜色,右边 32 位颜色。

自动使用 32 位颜色模式时要小心,因为一些较旧的设备不支持它们,可能会导致崩溃。建议您在使用默认模式之外的任何模式之前,先检查哪些模式可用。这可以通过创建一个自定义的“ColorChooser”对象来完成,该对象将枚举所有可能的模式。是的,这很痛苦,而且有很多代码。网站上将提供一个示例。

总结

本章介绍了场景照明和着色的各种方法,以及用于确定每个顶点颜色的数学算法。您还学习了漫射、镜面反射、发射和环境照明,以及与将灯光转化为聚光灯相关的各种参数。太阳系模型被更新以支持多个对象,并使用 z 缓冲来正确处理对象遮挡。

五、纹理

一个人的真正价值不在于他自己,而在于他身上的色彩和质地。

—阿尔贝特·施韦泽

人们的生活会变得相当乏味,没有质感。去除那些有趣的小缺点和怪癖,会给我们的日常生活增添一点光彩,不管它们是奇怪的但却是迷人的小习惯还是意想不到的才能。想象一下,一个高中看门人碰巧是一个优秀的舞厅舞者,一个著名的喜剧演员每天必须只穿新的白色袜子,一个非常成功的游戏工程师害怕手写信件——所有这些都可以让我们微笑,并在一天中增加一点点奇迹。在创造人造世界时也是如此。计算机可以产生的视觉完美可能很漂亮,但如果你想给你的场景创造一种真实感,那就感觉不太对了。这就是纹理产生的原因。

质感让完美成为真实。《美国传统词典》这样描述它:“某物独特的物理组成或结构,尤其是其各部分的大小、形状和排列。”很有诗意,是吧?

在 3D 图形的世界里,纹理在创建引人注目的图像时和灯光一样重要,而且现在可以不费吹灰之力地加入进来。图形芯片行业的大部分工作都植根于以比前一代硬件更高的速度渲染越来越精细的纹理。

因为 OpenGL ES 中的纹理是一个很大的主题,这一章将局限于基础知识,更高级的主题和技术留待下一章。记住这一点,让我们开始吧。

纹理化的语言

假设你想在你正在开发的游戏中创建一个飞机跑道。你会怎么做?很简单,拿几个黑色三角形,把它们拉长。砰!现在你有你的着陆跑道了!别急,伙计。画在长条中心的线条呢?一堆小白脸怎么样?这可能行得通。但是不要忘记最后那些黄色的人字形。嗯,添加一些额外的面孔,并把它们涂成黄色。别忘了数字。通向停机坪的曲线怎么样?很快你可能会有数百个三角形,但这仍然无助于解决油渍、修理、刹车痕或交通事故。现在事情开始变得复杂了。获取所有的细节可能需要成千上万张脸。与此同时,你的伙伴亚瑟也在创作一部漫画。你在比较笔记,告诉他多边形的数量,你甚至还没有谈到路杀。亚瑟说他只需要几个三角形和一个图像。你看,他使用纹理地图,使用纹理地图可以创建一个非常详细的表面,如飞机跑道,砖墙,装甲,云,吱吱作响的风化木门,一个遥远星球上的坑坑洼洼的地形,或一辆 56 年别克生锈的外表。

在计算机图形学的早期,纹理,或者说纹理映射,消耗了两种最珍贵的资源:CPU 周期和内存。它很少被使用,各种各样的小技巧都被用来节省这两种资源。与 20 年前相比,现在的内存几乎是免费的,现代芯片似乎有无限的速度,使用纹理不再是一个需要整夜熬夜和挣扎的决定。

关于纹理的一切(大部分)

纹理有两大类型:程序图像。程序纹理是基于某种算法动态生成的。木材、大理石、沥青、石头等等都有“方程式”。几乎任何种类的材质都可以简化成一种算法,从而绘制到一个物体上,如图 Figure 5–1 所示。

Image

图 5–1。 左边的圣杯是抛光的黄金,而右边使用程序纹理看起来像金矿石,而圆锥体看起来像大理石。

程序纹理非常强大,因为它们可以产生无限多种可缩放的图案,这些图案可以被放大以显示越来越多的细节,如图 5–2 所示。否则,这将需要大量的静态图像。

Image

图 5–2。 图 5–1 中右边的高脚杯特写。请注意需要非常大的图像才能完成的细节。

用于之前图像的 3D 渲染应用 Strata Design 3D-SE 支持程序纹理和基于图像的纹理。图 5–3 显示了用于指定图 5–2 中描述的金矿纹理参数的对话框。

Image

图 5–3。 图 5–2 中用于产生金矿纹理的所有可能设置

程序纹理,以及在较小程度上的图像纹理,可以按照从随机到结构化的复杂程度进行分类。随机或随机纹理可以被认为是“看起来像噪音”,就像沙子、灰尘、砾石、纸张中的颗粒等细粒度材质。接*随机的可能是火焰、草地或湖面。另一方面,结构化纹理具有广泛的可识别特征和图案。砖墙、柳条篮、格子花呢或一群壁虎将被构建。

图像纹理

如前所述,图像纹理就是这样。它们可以作为表面或材质纹理,如红木、钢板或散落在地上的树叶。如果做得好,这些可以无缝*铺,覆盖比原始图像更大的表面。因为他们来自现实生活,他们不需要复杂的程序性软件。Figure 5–4 展示了圣杯的场景,但这一次使用了木质纹理,圣杯使用桃花心木,圆锥使用桤木,而立方体仍然是金色的。

Image

图 5–4。 使用真实世界的图像纹理

除了使用图像纹理作为材质,它们本身也可以在你的 3D 世界中作为图片使用。Galaxy Tab 的渲染图像可以将纹理放到屏幕上。一个 3D 城市可以使用真实的照片作为建筑物的窗户、广告牌或客厅里的家庭照片。

OpenGL ES 和纹理

当 OpenGL ES 渲染一个物体时,比如《??》第四章中的迷你太阳系,它会绘制每个三角形,然后根据组成每个面的三个顶点对其进行光照和着色。之后,它愉快地走向下一个。纹理只不过是一个图像。正如您在前面了解到的,它可以动态生成以处理上下文相关的细节(比如云模式),也可以是.jpg.png或其他任何东西。当然,它是由像素组成的,但是当作为纹理操作时,它们被称为纹理元素。你可以把一个 OpenGL ES 纹理想象成一堆小的彩色“面”(纹理元素),每一个都有相同的大小,并且缝合在一张纸上,比如说,一面上有 256 个这样的“面”。每个面的大小都一样,可以拉伸或挤压,以便在任何大小或形状的表面上工作。它们没有浪费存储 xyz 值的内存的角几何,可以有多种可爱的颜色,并且非常划算。当然,它们的用途非常广泛。

像你的几何体一样,纹理也有自己的坐标空间。几何图形使用可靠的笛卡尔坐标表示其许多部分的位置,称为 x,yz ,纹理使用 st 。将纹理应用到某个几何对象的过程称为 UV 映射。( st 仅用于 OpenGL 世界,其他使用 uv 。去想想。)

那么,这是如何应用的呢?假设你有一块正方形的桌布,你必须把它拼成一张长方形的桌子。你需要沿着一边把它固定住,然后沿着另一边用力拉,直到它刚好盖住桌子。您可以只连接四个角,但如果您真的希望它“适合”,您可以沿着边缘甚至在中间连接其他部分。这就是一个纹理如何适应一个表面的一点点。

纹理坐标空间归一化;也就是说, st 的范围都是从 0 到 1。它们是无单位的实体,被抽象为不依赖于源或目的地的维度。因此,要进行纹理处理的面的顶点 st 值将在 0.0 到 1.0 之间,如图图 5–5 所示。

Image

图 5–5。 纹理坐标从 0 到 1.0,不管纹理是什么。

在最简单的例子中,我们可以将一个矩形纹理应用到一个矩形面上,然后完成它,如图 5–5 所示。但是如果你只想要纹理的一部分呢?你可以提供一个只有你想要的位的.png,如果你想拥有这个东西的许多变体,这就不太方便了。然而,还有另一种方法。仅仅改变目的面的 st 坐标。假设你想要的只是复活节岛雕像的左上角,我称之为海德利。你需要做的只是改变目的地的坐标,而那些坐标是基于你想要的图像部分的比例,如图 Figure 5–6 所示。也就是说,因为您希望图像被裁剪到沿 S 轴的一半,所以 s 坐标将不再从 0 到 1,而是从 0 到. 5。然后 t 坐标将从 0.5 变为 1.0。如果你想要左下角,你可以使用与 s 坐标相同的 0 到 0.5 的范围。

还要注意,纹理坐标系是独立于分辨率的。也就是说,边长为 512 的图像的中心将是(. 5,. 5),就像边长为 128 的图像的中心一样。

Image

图 5–6。 通过改变纹理坐标裁剪掉一部分纹理

纹理不限于直线物体。通过仔细选择目标面上的第坐标,你可以做出一些更加丰富多彩的形状,如图图 5–7 所示。

Image

图 5–7。 将图像映射成不寻常的形状

如果您在目的地的顶点上保持图像坐标不变,图像的角将跟随目的地的角,如图 Figure 5–8 所示。

Image

图 5–8。 扭曲的图像可以在 2D 表面产生 3D 效果。

纹理也可以*铺以便复制描绘壁纸、砖墙、沙滩等的图案,如图图 5–9 所示。请注意,坐标实际上超过了 1.0 的上限。所做的只是开始纹理重复,例如,0.6 的 s 等于 1.6 的 s ,2.6,等等。

Image

图 5–9。 *铺图像对于重复的图案非常有用,例如用于壁纸或砖墙的图案。

除了在图 5–9 中显示的*铺模型,纹理*铺也可以被“镜像”或夹紧。两者都是处理 0 到 1.0 范围之外的 s 和 t 的机制。

镜像*铺类似重复,但只是翻转交替图像的列/行,如图图 5–10a 所示。钳制图像意味着纹理元素的最后一行或最后一列重复,如图 Figure 5–1bb 所示。在我的示例图像中,钳制看起来一塌糊涂,但是当图像具有中性边框时,钳制很有用。在这种情况下,如果 sv 超出其正常界限,您可以防止任何图像在一个或两个轴上重复。

Image

图 5–10。 左边(a)显示的是一个镜像-重复,只针对 S 轴,而右边纹理(b)被钳制。

注意:图 5–10 中右侧图像右边缘的问题表明,设计用于夹紧的纹理应该有一个 1 像素宽的边界,以匹配它们所绑定的对象的颜色——除非你认为它真的很酷,那么当然,这几乎胜过一切。

正如你现在所知道的,OpenGL ES 不做四边形——也就是有四个边的面(相对于它的桌面兄弟)。所以,我们必须用两个三角形来制作它们,给我们一些结构,比如我们在第三章实验过的三角形带和扇形。将纹理应用到这个“假”四边形是一件简单的事情。一个三角形的纹理坐标为(0,0)、(1,0)和(0,1),而另一个三角形的坐标为(1,0)、(1,1)和(0,1)。如果你研究一下图 5–11,会更有意义。

Image

图 5–11。 在两个面上放置纹理

最后,让我们看看一个纹理是如何被拉伸到一大堆人脸上的,如图 Figure 5–12 所示,然后我们可以做一些有趣的事情,看看这是不是真的。

Image

图 5–12。 将一个纹理拉伸到多个面上

图像格式

OpenGL ES 支持很多不同的图像格式,我说的不是.png vs. .jpg,我指的是内存中的形式和布局。标准是 32 位,为红色、绿色、蓝色和 alpha 各分配 8 位内存。被称为 RGBA,它是大多数练习使用的标准。它也是“最漂亮的”,因为它提供了超过 1600 万种颜色和透明度。但是,您通常可以使用 16 位甚至 8 位图像。这样做,你可以节省大量的内存和曲柄速度相当多,仔细选择图像。参见表 5–1 了解一些更流行的格式。

Image

Image

另外,各种格式的要求是,一般来说,OpenGL 只能使用边长为 2 的幂的纹理图像。一些系统可以绕过这一点,比如有一定限制的 iOS,但目前,只要坚持标准就行了。

所以,所有这些东西都解决了,是时候开始编码了。

回到充满弹性的正方形

让我们后退一步,再次创建通用的弹性正方形,这是我们在第一章中第一次做的。我们将对它应用一个纹理,然后操纵它来展示前面详述的一些技巧,比如重复、动画和扭曲。

随意回收第一个项目。

接下来是纹理的实际创建。这将读入 Android 支持的任何类型的图像文件,并将其转换为 OpenGL 可以使用的格式。

在我们开始之前,还需要一个步骤。Android 需要被通知将被用作纹理的图像文件。这可以通过将图像文件hedly.png添加到/res/drawable文件夹来完成。如果 drawable 文件夹不存在,您可以现在创建它并将其添加到项目中。我们将在整数数组中存储纹理信息。在Square.java中增加以下一行:

    private int[] textures = new int[1];

添加以下导入内容:

    import android.graphics.*;celar     import android.opengl.*;

接下来将列表 5–1 添加到Square.java来创建纹理。

清单 5–1。 创建 OpenGL 纹理

public int createTexture(GL10 gl, Context contextRegf, int resource)        {                Bitmap image = BitmapFactory.decodeResource(contextRegf.getResources(),                resource);                                                           // 1            gl.glGenTextures(1, textures, 0);                                        // 2            gl.glBindTexture(GL10.*GL_TEXTURE_2D*, textures[0]);                       // 3 `           GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, image, 0);                     // 4

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
               GL10.GL_LINEAR);                                                     // 5a

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
               GL10.GL_LINEAR);                                                     // 5b

image.recycle();                                                         //6
           return resource;
    }`

让我们来分解一下:

  • 第 1 行加载 Android 位图,让加载器处理 Android 可以读取的任何图像格式。表 5–2 列出了支持的格式。
  • 第 2 行得到一个未使用的纹理“名称”,它实际上只是一个数字。这确保了您使用的每个纹理都有一个唯一的标识符。如果您想重用标识符,那么您应该调用glDeleteTextures()
  • 之后,纹理被绑定到下一行当前的 2D 纹理,用新的纹理替换之前的纹理。在 OpenGL ES 中,这些纹理目标只有一个,所以它必须总是GL_TEXTURE_2D,而成熟的 OpenGL 有几个。所有可用参数参见表 5–3。绑定也会使该纹理处于活动状态,因为一次只有一个纹理处于活动状态。这也指导 OpenGL 在哪里放置任何新的图像数据。参见表 5–2。
  • 在这里的第 4 行,GLUtils使用了绑定 OpenGL ES 和 Android APIs 的 Android 工具类。这个工具为我们在第 1 行创建的位图指定了 2D 纹理图像。图像(纹理)是基于创建的位图以其原始格式在内部创建的。
  • 最后,第 5a 和 5b 行设置了 Android 上需要的一些参数。没有它们,纹理有一个默认的“过滤器”值,这是不必要的。最小和最大过滤器告诉系统如何在某些情况下处理纹理,在这些情况下,纹理必须缩小或放大以适应给定的多边形。表 5–3 显示了 OpenGL ES 中可用的参数类型。
  • 为了成为好邻居,第 6 行告诉 Android 显式回收位图,因为位图会占用大量内存。

Image

在我们调用createTexture方法之前,我们需要获取图像的上下文和资源 ID(headly.png)。要获得上下文,请修改BouncySquareActivity.java中的onCreate()方法,如下所示:

    view.setRenderer(new SquareRenderer(true));

致以下内容:

    view.setRenderer(new SquareRenderer(true, this.getApplicationContext()));

这也需要将SquareRenderer.java中的构造器定义更改为以下内容:

public SquareRenderer(boolean useTranslucentBackground, Context context) {     mTranslucentBackground = useTranslucentBackground;     this.context = context;                                             //1     this.mSquare = new Square(); }

您需要添加以下导入:

    import android.content.Context;

并添加一个实例变量来支持新的上下文。稍后,当加载图像并将其转换为 OpenGL 兼容纹理时,会用到上下文。

现在,为了获得资源 ID,在SquareRenderer.javaonSurfaceCreated()方法中添加以下内容:

        int resid = book.BouncySquare.R.drawable.*hedly*;                               //1         mSquare.createTexture(gl, this.context, resid);                               //2

  • 第 1 行获取我们添加到 drawable 文件夹中的图像资源(hedly.png)。当然,你可以使用任何你想要的图像。
  • 在第 2 行,我们使用了Square类的对象,并用正确的上下文和图像的资源 ID 调用了createTexture()方法。

然后将以下内容添加到Square.java中的接口实例变量中:

    public FloatBuffer mTextureBuffer;     float[] textureCoords =          {                                                 0.0f, 0.0f,         1.0f, 0.0f,         0.0f, 1.0f,         1.0f, 1.0f                     };

这定义了纹理坐标。现在在 square 的构造函数中创建类似于我们在第一章中创建的vertBuffertextureBuffer

`    ByteBuffer tbb = ByteBuffer.allocateDirect(textureCoords.length * 4);

tbb.order(ByteOrder.nativeOrder());

mTextureBuffer = tbb.asFloatBuffer();

mTextureBuffer.put(textureCoords);

mTextureBuffer.position(0);`

我的图片是太*洋复活节岛上一个神秘的巨大石头头像的照片。为了便于测试,请使用 32 位 RGBA 的 2 的幂(POT)图像。

注意:默认情况下,OpenGL 要求图像数据中的每一行纹理元素都在一个 4 字节的边界上对齐。我们的 RGBA 纹理坚持这一点;对于其他格式,考虑使用调用 glPixelStorei(GL _ PACK _ ALIGNMENT,x),其中 x 可以是 1、2、4 或 8 个字节用于对齐。使用 1 来涵盖所有情况。

请注意,纹理通常有大小限制,这取决于实际使用的图形硬件。您可以通过调用以下代码来确定特定*台可以使用多大的纹理,其中maxSize是一个在运行时进行补偿的整数:

     gl.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE,maxSize);

最后,需要修改draw()例程,如清单 5–2 所示。大部分你已经见过了。我已经将渲染器模块中的glEnableClientState()调用迁移到这里,使方形对象更加包容。

清单 5–2。 用纹理渲染几何体

`public void draw(GL10 gl)
    {
            gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mFVertexBuffer);
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
            gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, mColorBuffer);
            gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

gl.glEnable(GL10.GL_TEXTURE_2D);                                     //1
            gl.glEnable(GL10.GL_BLEND);                                          //2

gl.glBlendFunc(GL10.GL_ONE, GL10.GL_SRC_COLOR);                      //3
            gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);                   //4

gl.glTexCoordPointer(2, GL10.GL_FLOAT,0, mTextureBuffer);            //5
            gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);                 //6

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);                       //7

gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
            gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
            gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);                //8
    }`

这是怎么回事?

  • 在第 1 行中,GL_TEXTURE_2D目标被启用。桌面 OpenGL 支持 1D 和 3D 纹理,但不支持 es。
  • 在第 2 行中,可以启用混合。混合是指颜色和目标颜色根据第 3 行中打开的公式进行混合。
  • 混合功能决定如何将源和目标像素/片段混合在一起。最常见的形式是源覆盖目标,但其他形式可以创建一些有趣的效果。由于这是一个很大的话题,所以后面会讲到。
  • 第 4 行确保我们想要的纹理是当前的。
  • 第 5 行是纹理坐标被传递给硬件的地方。
  • 正如您必须告诉客户端处理颜色和顶点一样,您需要对第 6 行中的纹理坐标做同样的事情。
  • 第 7 行你会认识,但这一次除了绘制颜色和几何图形,它现在从当前纹理(texture_2d)获取信息,将四个纹理坐标匹配到由vertices[]数组指定的四个角(纹理对象的每个顶点都需要分配一个纹理坐标),并使用第 3 行中指定的值混合它。
  • 最后,禁用纹理的客户端状态,就像禁用颜色和顶点一样。

如果一切正常,您应该会看到类似于图 5–13 中左图的东西。注意纹理是如何从顶点获取颜色的?注释掉该行glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, mColorBuffer),现在您应该会看到图 5–13 中的右图。如果您没有看到任何图像,请仔细检查您的文件,确保它的大小确实是 2 的幂,例如 128x128 或 256x256。

Image

图 5–13。 对有弹性的正方形应用纹理。左边使用顶点颜色;右派没有。

你说什么?纹理颠倒了?这很可能是一个问题,取决于各自的操作系统如何对待位图。OpenGL 希望左下角作为原点,而一些图像格式或驱动程序选择左上角作为原点。有两种主要方法可以解决这个问题:您可以选择更改代码,或者您可以提供一个预先裁剪的图像。因为这是一个非常简单的项目,我选择用图像编辑器翻转图形。

所以现在我们可以复制本章第一部分中的一些例子。第一种方法是只挑选一部分纹理来显示。将textureCoords更改如下:

`float[] textureCoords =

{
           0.0f, 0.0f,
           0.5f, 0.0f,
           0.0f, 0.5f,
           0.5f, 0.5f
};`

你拿到图 5–14 了吗?

Image

图 5–14。 使用 s 和 t 坐标裁剪图像

纹理坐标到真实几何坐标的映射看起来像图 5–15。如果你还不太清楚,花几分钟去理解这里发生了什么。简单地说,数组中的纹理坐标与几何坐标是一对一的映射。

Image

图 5–15。 纹理坐标与几何坐标一一对应。

现在改变纹理坐标如下。你能猜到会发生什么吗?(图 5–16):

    float[] textureCoords =     {           0.0f, 2.0f,           2.0f, 2.0f,           0.0f, 0.0f,           2.0f, 0.0f     }; Image

图 5–16。 当需要做壁纸等重复图案时,重复图像很方便。

现在让我们通过改变顶点的几何形状来扭曲纹理,为了让事情看起来更清楚,恢复原始的纹理坐标来关闭重复:

    float  vertices[] = {                     -1.0f, -0.7f,                     1.0f, -0.30f,                     -1.0f, 0.70f,                     1.0f,  0.30f, };

这应该会夹住正方形的右侧,并带走纹理,如图图 5–17 所示。

Image

图 5–17。 捏紧多边形的右边

有了这些知识,如果你动态改变纹理坐标会发生什么?将下面的代码添加到draw()—任何地方都应该可以工作。

*`    textureCoords[0]+=texIncrease;

textureCoords[2]+=texIncrease;

textureCoords[4]+=texIncrease;

textureCoords[6]+=texIncrease;

textureCoords[1]+=texIncrease;

textureCoords[3]+=texIncrease;

textureCoords[5]+=texIncrease;

textureCoords[7]+=texIncrease;`

并使textureCoordstexIncrease都成为实例变量。

这将逐帧增加一点纹理坐标。敬畏地奔跑和站立。这是一个非常简单的获得动画纹理的技巧。3D 世界中的字幕可能会用到这个。您可以创建一个纹理,就像一个卡通人物正在做某事的电影胶片,并更改 st 的值,像一本小动画书一样从一帧跳到另一帧。另一种是创建基于纹理的字体。由于 OpenGL 没有原生字体支持,这取决于我们,世界上长期受苦的工程师,自己添加它。唉。这可以通过将所需字体的字符放置在单个马赛克纹理上,然后通过仔细使用纹理坐标来选择它们来实现。

mipmap

小中见大贴图是一种为给定纹理指定多个细节级别的方法。这可以在两个方面有所帮助:当纹理对象到视点的距离变化时,它可以*滑纹理对象的外观;当纹理对象很远时,它可以节省资源的使用。

例如,在遥远的太阳中,我可能会为木星使用 1024x512 的纹理。但是,如果木星离我们很远,只有几个像素宽,那将是对内存和 CPU 的浪费。这就是 mipmapping 发挥作用的地方。那么,什么是 mipmap?

源自拉丁语短语“multum in parvo”(字面意思:小中有大),一个 mipmap 是一系列细节层次不同的纹理。你的根图像的边长可能是 128,但是当它是一个纹理贴图的一部分时,它的边长可能也是 64,32,16,8,4,2 和 1 像素,如图图 5–18 所示。

Image

图 5–18。 海德利,mipmapped 版

回到本章最初的练习,有纹理的弹性正方形,你将通过测试进行 mipmapping。

首先,创建一系列纹理,从 1x1 到 256x256,使每个纹理的大小是前一个的两倍,同时给它们涂上不同的颜色。这些颜色使您能够很容易地分辨出一个图像何时变成另一个图像。将它们添加到您的项目中的/res/drawable/下,然后在onSurfaceCreated()SquareRendered.java中,用清单 5–3 替换掉对createTexture()的单个调用。注意,最后使用的是最后一个参数,即细节层次索引。如前所述,如果您只有一个图像,则使用 0 作为默认值。任何大于 0 的都将是 mipmap 图像族的剩余部分。所以,第一张是海德利的,其余的我用的是彩色方块,这样当它们弹出和弹出时,很容易看到不同的图像。请注意,如果您像这样手动生成 mipmaps,您需要为每个级别指定图像,并且它们必须是正确的尺寸,因此您不能仅仅为了节省几行代码而跳过 1、2 和 4 像素图像。否则,什么都不会显示。并确保原始图像的边长为 256,以便从 1 到 256 的图像之间有一个完整的链。

为了方便打开或关闭 mipmapping,我在Square.java中添加了以下实例变量。

    public boolean m_UseMipmapping = true;

将清单 5–3 添加到SquareRenderer.java中的onSurfaceCreated()方法。

清单 5–3。 设置自定义预过滤 Mipmap

`            int resid = book.BouncySquare.R.drawable.hedly256;                        
            mSquare.createTexture(gl, this.context, resid, true);

if (mSquare.m_UseMipmapping)
        {
                    resid = book.BouncySquare.R.drawable.mipmap128;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap64;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap32;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap16;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap8;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap4;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap2;
                    mSquare.createTexture(gl, this.context, resid, false);
                    resid = book.BouncySquare.R.drawable.mipmap1;
                    mSquare.createTexture(gl, this.context, resid, false);
         }`

在此之后,Square.java中的createTexture()需要替换为清单 5–4 中的内容。

清单 5–4。 生成 Mipmap 链

private int[] textures = new int[1];         static int *level* = 0; `        public int createTexture(GL10 gl, Context contextRegf, int resource, boolean imageID)
    {
               Bitmap tempImage = BitmapFactory.decodeResource(
                               contextRegf.getResources(), resource);             // 1
               if (imageID == true) {
                       gl.glGenTextures(1, textures, 0);
                       gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
          }

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, level, tempImage, 0);      // 4
                       level++;

if (m_UseMipmapping == true) {
                       gl.glTexParameterx(GL10.GL_TEXTURE_2D,
                                   GL10.GL_TEXTURE_MIN_FILTER,
                                   GL10.GL_LINEAR_MIPMAP_NEAREST);
                       gl.glTexParameterx(GL10.GL_TEXTURE_2D,
                                   GL10.GL_TEXTURE_MAG_FILTER,
                                   GL10.GL_LINEAR_MIPMAP_NEAREST);
          } else {
                       gl.glTexParameterf(GL10.GL_TEXTURE_2D,
                                   GL10.GL_TEXTURE_MIN_FILTER,
                                   GL10.GL_LINEAR);
                       gl.glTexParameterf(GL10.GL_TEXTURE_2D,
                                   GL10.GL_TEXTURE_MAG_FILTER,
                                   GL10.GL_LINEAR);
          }

tempImage.recycle();// 6
            return resource;
    }`

自然也需要一些改变。将以下实例变量添加到SquareRenderer.java:

`    float z = 0.0f;

boolean flipped=false;

float delz_value=.040f;

float delz = 0.0f;

float furthestz=-20.0f;

static float rotAngle=0.0f;`

现在,使用清单 5–5 代替当前的onDrawFrame()。这将导致 z 值来回振荡,以便您可以观察到小中见大贴图的运行。

清单 5–5。 随 z 值变化的 onDrawFrame()

`    public void onDrawFrame(GL10 gl) {

gl.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

gl.glMatrixMode(GL11.GL_MODELVIEW);
           gl.glLoadIdentity();

if(z<furthestz)

{
                   if(!flipped)
               {
                           delz=delz_value;
                           flipped=true;
               } else {
                           flipped=false;
               }
       } else if(z > -.01f) {
                   if(!flipped) {
                           delz=-delz_value;
                           flipped=true;
               } else {
                           flipped=false;
               }                
       }
          z=z+delz;
          gl.glTranslatef(0.0f, (float) (Math.sin(mTransY) / 2.0f), z);
          gl.glRotatef(rotAngle, 0, 0, 1.0f);
          rotAngle+=.5f;

mSquare.draw(gl);

mTransY += .15f;
        }`

最后,确保调用glColorPonter()从 square 的draw()方法中移除。

如果编译和运行正常,您应该会看到类似于 Figure 5–19 的内容,当 OpenGL ES 为给定的距离选择最佳颜色时,不同的颜色会出现和消失。

Image

图 5–19。 根据到眼点的距离,每个 mipmap 级别的不同图像会弹出和弹出。

让 OpenGL 为您生成小中见大贴图是可能的,如下一节所示。

过滤

用作纹理的图像在投影到屏幕上时,根据其内容和最终大小,可能会出现各种人为效果。非常详细的图像可能会出现令人讨厌的闪烁效果。然而,可以通过一个叫做过滤的过程来动态修改图像以最小化这些影响。

假设你有一个 128x128 的纹理,但是纹理面的边长是 500 像素。你应该看到什么?显然,图像的原始像素,现在称为纹理像素,将比任何屏幕像素大得多。这是一个被称为放大的过程。相反,你可能会遇到纹理元素比一个像素小得多的情况,这被称为缩小。过滤是用于确定如何将像素的颜色与底层纹理元素相关联的过程。表 5–4 和 5–5 显示了这种情况的可能变化。

Image

有三种主要的过滤方法:

  • 点采样(在 OpenGL lingo 中调用):像素的颜色是基于离像素中心最*的纹理元素。这是最简单、最快的,自然产生最不令人满意的图像。
  • 双线性采样,也称为线性像素的着色基于距离像素中心最*的 2×2 纹理元素阵列的加权*均值。这可以大大*滑图像。
  • 三线性采样:这需要小中见大贴图,将两个最接*的小中见大贴图级别用于屏幕上的最终渲染,对每个级别执行双线性选择,然后对两个单独的值进行加权*均。

您可以通过再次查看第一个练习来了解这一点。在您的 mipmapping 实验中,将以下行添加到createTexture()的最末尾,同时删除创建所有先前 mipmap 图像的初始化行(当然,图像#0 除外):

    gl.glHint(GL11.GL_GENERATE_MIPMAP,GL10.GL_NICEST);     gl.glTexParameterf(GL10.GL_TEXTURE_2D,GL11.GL_GENERATE_MIPMAP,GL10.GL_TRUE);

第二个调用,在前面已经提到过,将自动从你唯一的图像中创建一个 mipmap,并交给渲染器。并且确保createTexture()只被调用一次,因为不需要为不同层次的细节使用我们自己的定制图像。对glHint()的第一次调用告诉系统使用它所拥有的任何算法来生成看起来最好的图像。还可以选择GL_FASTESTGL_DONT_CARE。后者将选择它认为最适合这种情况的方法。

在 Figure 5–20 中,左边的图像显示了关闭过滤功能的 Hedly 的特写,而右边的图像显示了过滤功能的开启。

Image

图 5–20。 左侧已经全部过滤关闭。右侧打开了双线性过滤。

OpenGL 扩展

尽管 OpenGL 是一个标准,但它在设计时就考虑到了可扩展性,允许各种硬件制造商使用extension strings将他们自己的特殊调料添加到 3D 汤里。在 OpenGL 中,开发人员可以轮询可能的扩展,然后使用它们,如果它们存在的话。要了解这一点,请使用下面一行代码:

    String extentionList=gl.glGetString(GL10.*GL_EXTENSIONS*);

这将返回一个空格分隔的列表,列出 Android 中用于 OpenGL ES 的各种额外选项,如下所示(来自 Android 2.3):

    GL_OES_byte_coordinates GL_OES_fixed_point GL_OES_single_precision     GL_OES_read_format GL_OES_compressed_paletted_texture GL_OES_draw_texture     GL_OES_matrix_get GL_OES_query_matrix GL_OES_EGL_image     GL_OES_compressed_ETC1_RGB8_texture GL_ARB_texture_compression     GL_ARB_texture_non_power_of_two GL_ANDROID_user_clip_plane     GL_ANDROID_vertex_buffer_object GL_ANDROID_generate_mipmap

这有助于找到一部手机可能比另一部手机拥有的定制功能。一种可能是使用一种称为 PVRTC 的特殊图像压缩格式,这种格式只为使用 PowerVR 类图形芯片的设备定制。PVRTC 与 PowerVR 硬件紧密结合,可以改善渲染和加载时间。三星 Galaxy S、摩托罗拉的 Droid X、黑莓 playbook 以及本文撰写时的所有 iOS 设备都可以利用 PVRTC。非 PowerVR 设备,如使用 Ardreno 或 Tegra 内核的设备,也可能有自己的特殊格式。

如果字符串GL_IMG_texture_compression_pvrtc出现在之前的扩展列表中,您可以判断您的设备是否支持 PVRTC。其他 GPU 可能有类似的格式,所以如果你想走自定义路线,我们鼓励你查看开发者论坛和 SDK。

最后,更多太阳系的善良

现在我们可以回到上一章的太阳系模型,给地球添加一个纹理,让它看起来更像地球。对SolarSystemActivity.java进行类似于我们之前对BouncySquareActivity.java所做的修改,将 setter 修改为:

    view.setRenderer(new SolarSystemRenderer(this.getApplicationContext());

还要修改SolarSystemRenderer.java中的构造函数来处理传递的上下文。

    import android.content.Context;     public Context myContext;     public SolarSystemRenderer(Context context)     {         this.myContext = context;     }

我们需要将上下文存储在一个公共变量中,因为我们将在创建图像纹理时将它传递给init()函数。接下来,检查Planet.java,并将init()替换为清单 5–6;变化已被突出显示。对于地球的纹理,有很多例子。只要在谷歌上搜索一下。或者你可能想在[maps.jpl.nasa.gov/](http://maps.jpl.nasa.gov/)先检查一下 NASA。

清单 5–6。 修改了球体生成器,增加了纹理支持

private void init(int stacks,int slices, float radius, float squash, GL10 gl,               Context context, boolean imageId, int resourceId)              // 1     {               float[] vertexData; `float[] normalData;
              float[] colorData;
              float[] textData=null;

float colorIncrement=0f;

float blue=0f;
              float red=1.0f;

int vIndex=0;                                //vertex index
              int cIndex=0;                                //color index
              int nIndex=0;                                //normal index
              int tIndex=0;                                //texture index

if(imageId == true)
              createTexture(gl, context, resourceId);                         //2
              m_Scale=radius;                                                
              m_Squash=squash;

colorIncrement=1.0f/(float)stacks;

m_Stacks = stacks;
              m_Slices = slices;

//Vertices

vertexData = new float[ 3((m_Slices2+2) * m_Stacks)];

//Color data

colorData = new float[ (4(m_Slices2+2) * m_Stacks)];

//Normal pointers for lighting

normalData = new float[3((m_Slices2+2) * m_Stacks)];
                       if(imageId == true)                                      //3
                       textData = new float [2 * ((m_Slices*2+2) * (m_Stacks))];

int      phiIdx, thetaIdx;

//Latitude

for(phiIdx=0; phiIdx < m_Stacks; phiIdx++)
            {
                    //Starts at -1.57 and goes up to +1.57 radians.

///The first circle.

float phi0 = (float)Math.PI * ((float)(phiIdx+0) *
                     (1.0f/(float)(m_Stacks)) - 0.5f);

//The next, or second one. float phi1 = (float)Math.PI * ((float)(phiIdx+1) *
                    (1.0f/(float)(m_Stacks)) - 0.5f);

float cosPhi0 = (float)Math.cos(phi0);                                
                    float sinPhi0 = (float)Math.sin(phi0);
                    float cosPhi1 = (float)Math.cos(phi1);
                    float sinPhi1 = (float)Math.sin(phi1);

float cosTheta, sinTheta;

//Longitude

for(thetaIdx=0; thetaIdx < m_Slices; thetaIdx++)
                {
                            //Increment along the longitude circle each "slice."
                            float theta = (float) (2.0f(float)Math.PI* *
                            ((float)thetaIdx) * (1.0/(float)(m_Slices-1)));
                            cosTheta = (float)Math.cos(theta);
                            sinTheta = (float)Math.sin(theta);

//We're generating a vertical pair of points, such
                            //as the first point of stack 0 and the first point of
                            //stack 1 above it. This is how TRIANGLE_STRIPS work,
                            //taking a set of 4 vertices and essentially drawing two
                            //triangles at a time. The first is v0-v1-v2, and the next
                            //is v2-v1-v3, etc.

//Get x-y-z for the first vertex of stack.

vertexData[vIndex]   = m_ScalecosPhi0cosTheta;
                            vertexData[vIndex+1] = m_Scale(sinPhi0m_Squash);
                            vertexData[vIndex+2] = m_Scale(cosPhi0sinTheta);

vertexData[vIndex+3]   = m_ScalecosPhi1cosTheta;
                            vertexData[vIndex+4] = m_Scale(sinPhi1m_Squash);
                            vertexData[vIndex+5] = m_Scale(cosPhi1sinTheta);

//Normal pointers for lighting

normalData[nIndex+0] = (float)(cosPhi0 * cosTheta);
                            normalData[nIndex+2] = cosPhi0 * sinTheta;
                            normalData[nIndex+1] = sinPhi0;

//Get x-y-z for the first vertex of stack N.
                            normalData[nIndex+3] = cosPhi1 * cosTheta;
                            normalData[nIndex+5] = cosPhi1 * sinTheta;
                            normalData[nIndex+4] = sinPhi1;

if(textData != null)                                         //4 {
                                    float texX = (float)thetaIdx *
                                  (1.0f/(float)(m_Slices-1));
                                    textData [tIndex + 0] = texX;
                                    textData [tIndex + 1] = (float)(phiIdx+0) *
                                  (1.0f/(float)(m_Stacks));
                                    textData [tIndex + 2] = texX;
                                    textData [tIndex + 3] = (float)(phiIdx+1) *
                                  (1.0f/(float)(m_Stacks));
                               }

colorData[cIndex+0] = (float)red;        
                               colorData[cIndex+1] = (float)0f;
                               colorData[cIndex+2] = (float)blue;
                               colorData[cIndex+4] = (float)red;
                               colorData[cIndex+5] = (float)0f;
                               colorData[cIndex+6] = (float)blue;
                               colorData[cIndex+3] = (float)1.0;
                               colorData[cIndex+7] = (float)1.0;

cIndex+=24;
                               vIndex+=2
3;
                               nIndex+=2*3;

if(textData!=null)                                        //5
                               tIndex+= 2*2;

blue+=colorIncrement;
                               red-=colorIncrement;

//Degenerate triangle to connect stacks and maintain
                              //winding order.

vertexData[vIndex+0] = vertexData[vIndex+3] =
                             vertexData[vIndex-3];
                              vertexData[vIndex+1] = vertexData[vIndex+4] =
                             vertexData[vIndex-2];
                              vertexData[vIndex+2] = vertexData[vIndex+5] =
                             vertexData[vIndex-1];

normalData[nIndex+0] = normalData[nIndex+3] =
                             normalData[nIndex-3];
                              normalData[nIndex+1] = normalData[nIndex+4] =
                             normalData[nIndex-2];
                              normalData[nIndex+2] = normalData[nIndex+5] =
                             normalData[nIndex-1];

if(textData!= null)                                           //6
                      {
                          textData [tIndex + 0] = textData [tIndex + 2] =
                               textData [tIndex -2];
                          textData [tIndex + 1] = textData [tIndex + 3] =
                                textData [tIndex -1];                       }
              }
    }
        m_Pos[0]= 0.0f;
        m_Pos[1]= 0.0f;
        m_Pos[2]= 0.0f;

m_VertexData = makeFloatBuffer(vertexData);
       m_NormalData = makeFloatBuffer(normalData);
       m_ColorData = makeFloatBuffer(colorData);

if(textData!= null)
        m_TextureData = makeFloatBuffer(textData);                
    }`

所以,事情是这样的:

  • 图像的 GL 对象、上下文、图像 ID 和资源 ID 被添加到第 1 行的参数列表的末尾。
  • 在第 2 行,创建了纹理。
  • 在第 3ff 行中,分配了纹理的坐标数组。
  • 接下来,计算第 4ff 行的纹理坐标。由于球体有 x 个切片和 y 个堆栈,坐标空间仅从 0 到 1,我们需要将每个值增加 s 的增量1/m_slicest 的增量1/m_stacks。注意,这覆盖了两对坐标,一对在另一对之上,匹配三角形条带的布局,也产生了堆叠的坐标对。
  • 在第 5 行,推进坐标数组以保存下一组值。
  • 最后,一些松散的线程被绑在一起,准备进入第 6 行的下一个堆栈。

确保将以下内容添加到实例数据中:

    FloatBuffer m_TextureData;

将第一个例子中的createTexture()方法复制到Planet.java,并根据需要进行修改。如果您愿意,可以随意移除 mipmap 支持,但是保留它没有坏处,因为它对于本练习来说并不重要。确保glTexParameterf()GL10.GL_LINEAR作为参数。

对于一个地球纹理,请注意,这将环绕整个球体模型,所以不是任何图像都可以,因此它应该类似于图 5–21。

Image

图 5–21。 纹理通常填充整个帧,边到边。行星使用墨卡托投影(圆柱形地图)。

一旦你找到一个合适的.png,将它添加到你的项目中/res/drawable-nodpi/下,并在分配时将它交给行星对象。因为你不需要太阳的纹理,你可以传递一个 0 作为资源 ID。所以我们可以把false设定为imageId太阳的行星物体,而把true设定为地球。接下来,我们修改行星的构造函数,使其看起来像清单 5–7 中的。

清单 5–7。 为 Planet.java 增加几个新参数

public Planet(int stacks, int slices, float radius, float squash, GL10 gl, Context context, boolean imageId, int resourceId)      {      this.m_Stacks = stacks;      this.m_Slices = slices;      this.m_Radius = radius;      this.m_Squash = squash;      init(m_Stacks,m_Slices,radius,squash, gl, context, imageId, resourceId);      }

自然地,initGeometry()需要被修改以支持额外的参数,如清单 5–8 中的所示。

清单 5–8。initGeometry()将新参数传递给 Planet.java

`private void initGeometry(GL10 gl) {
        int resid;
m_Eyeposition[X_VALUE] = 0.0f;
m_Eyeposition[Y_VALUE] = 0.0f;
m_Eyeposition[Z_VALUE] = 10.0f;

resid =  com.SolarSystem.R.drawable.earth_light;
m_Earth = new Planet(50, 50, .3f, 1.0f, gl, myContext, true, resid);
m_Earth.setPosition(0.0f, 0.0f, -2.0f); m_Sun = new Planet(50, 50, 1.0f, 1.0f, gl, myContext, false, 0);
m_Sun.setPosition(0.0f, 0.0f, 0.0f);
}`

当然,我们需要更新Planet.java中的draw()方法,如清单 5–9 所示。

清单 5–9。 准备处理新的纹理

`    public void draw(GL10 gl)
    {
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glEnable(GL10.GL_CULL_FACE);
        gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

if(m_TextureData != null)        
    {
        gl.glEnable(GL10.GL_TEXTURE_2D);                //1
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, m_TextureData);
    }

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, m_VertexData);
        gl.glNormalPointer(GL10.GL_FLOAT, 0, m_NormalData);
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, m_ColorData);
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, (m_Slices+1)2(m_Stacks-1)+2);

gl.glDisable(GL10.GL_BLEND);
        gl.glDisable(GL10.GL_TEXTURE_2D);
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

}`

在第 1ff 行中,您将识别出带有正方形的示例中的相同调用。首先启用纹理支持,然后调用一个glBindTexture()来确保当前纹理可用,然后提醒系统期待一个纹理坐标数组,然后将数据传递给它。

编译并运行,理想情况下,您会看到类似于 Figure 5–22 的内容。

注:在 Android 环境中遇到仿真器 bug 并不少见(比如在准备这个例子的时候)。如果你看到了一些你没有预料到的东西,并且它违反了任何逻辑解释,那么试着在硬件上运行代码,看看会发生什么。

Image

图 5–22。 太阳和地球

总结

这一章是对纹理及其用途的基本介绍。涵盖了以下主题:基本纹理理论,如何表达纹理坐标,提高保真度的小中见大贴图,以及如何过滤纹理以使其*滑。太阳系模型已经更新,所以现在地球看起来真的像使用纹理贴图的地球。table 5–7 总结了涵盖的所有新 API 调用。在下一章,我们将继续纹理,使用 Android 的多重纹理单元,以及混合技术。*

六、会混合吗?

是的!它混合了!

—汤姆·迪克森,Blendtec 搅拌机公司的老板

2006 年,汤姆·迪克森在 YouTube 上发布了一个愚蠢的视频,展示了他公司的搅拌机是如何通过将一些弹珠混合成粉末来搅拌的。从那时起,他频繁的视频被观看了超过 1 亿次,并展示了从 Tiki 火炬和激光笔到贾斯汀比伯娃娃和新摄像机的所有东西。汤姆的这种混合与我们的混合没有任何关系,除非虐待狂和无情地粉碎几个 Android 触摸板和手机也算在内。毕竟,它们是 OpenGL ES 设备:有自己的混合形式的设备,尽管不那么具有破坏性。(是的,这是一种延伸。)

混合在 OpenGL ES 应用中起着重要的作用。这是一个用来创造半透明物体的过程,这些物体可以用于像窗户这样简单的东西,也可以用于像池塘这样复杂的东西。其他用途包括添加大气,如雾或烟,*滑锯齿线,以及模拟各种复杂的灯光效果。OpenGL ES 2.0 有一个复杂的机制,它使用称为着色器的小模块来做专门的混合效果等。但在着色器之前,有混合功能,这是不那么多才多艺,但更容易使用。

在这一章中,你将学习混合功能的基础,以及如何将它们应用于颜色和 alpha 混合。之后,你将使用一种不同的混合方式,包括多种纹理,用于更复杂的效果,比如阴影。最后,我会想出如何在太阳系项目中应用这些效果。

阿尔法混合

你一定注意到了“RGBA”的彩色四胞胎如前所述, A 部分是 alpha 通道,它通常用于指定图像中的透明度。在用于纹理的位图中,alpha 层形成各种各样的 8 位图像,它可以在一个部分半透明,在另一个部分透明,在第三个部分完全不透明。如果一个对象没有使用纹理,而是通过其顶点,照明或整体全局着色来指定其颜色,alpha 将使整个对象或场景具有半透明属性。值 1.0 表示对象或像素完全不透明,而值 0 表示完全不可见。

要使 alpha 与任何混合模型一起工作,您需要同时处理源图像和目标图像。因为这个主题最好通过例子来理解,所以我们现在从第一个开始。

抓住你的第一章练习,然后用清单 6–1 代替原来的方法。这里首先使用的是纯色方块,而不是纹理方块,因为这是一个更简单的例子。

清单 6–1。 新改进的onDrawFrame()方法

`    public void onDrawFrame(GL10 gl)
{
        gl.glClearColor(0.0f,0.0f,0.0f,1.0f);                                //1
        gl.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

gl.glMatrixMode(GL11.GL_MODELVIEW);
        gl.glEnableClientState(GL11.GL_VERTEX_ARRAY);

//SQUARE 1

gl.glLoadIdentity();
        gl.glTranslatef(0.0f,(float)Math.sin(mTransY), -3.0f);               //2
        gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f);
        mSquare.draw(gl);

//SQUARE 2

gl.glLoadIdentity();                                                 //3
        gl.glTranslatef( (float)(Math.sin(mTransY)/2.0f),0.0f, -2.9f);
        gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
        mSquare.draw(gl);

mTransY += .075f;
    }`

和以前一样,让我们仔细看看代码:

  • 在第 1ff 行中,缓冲区被清除为黑色,以便稍后更容易看到任何混合。
  • 在 2ff 行中,我们可以画一个上下移动 3 个单位的正方形,同时给它一个蓝色。因为没有逐顶点着色,所以对glColor4f()的调用会将整个正方形设置为蓝色。但是,请注意 1.0 的最后一个组件。那是阿尔法,它将很快被处理。紧随gl.glColor4f()之后的是实际绘制正方形的调用。
  • 第 3ff 行寻址第二个方块,将它涂成红色并左右移动。将它移动 2.9 个单位而不是 3.0 个单位可以确保红色方块在蓝色方块的前面。

如果一切正常,你应该得到类似于 Figure 6–1 的东西。

Image

图 6–1。 蓝色方块上下起伏;红色的走左边和右边。

看起来没什么,但这将是接下来几个实验的框架。第一个将打开默认的混合功能。

和许多其他 OpenGL 特性一样,通过调用gl.glEnable(GL10.GL_BLEND)打开混合。在第一次调用mSquare.draw()之前的任何地方添加。重新编译,你会看到什么?什么都没有,或者至少什么都没有改变。它看起来仍然像图 6–1。那是因为混合不仅仅是说“混合,你!”我们还必须指定一个混合函数 ,它描述了源颜色(通过其片段或像素表示)如何与目标颜色混合。当然,默认情况下,当深度提示关闭时,源片段总是替换目的片段。事实上,只有当z-缓冲关闭时,才能确保正确的混合。

混合功能

要改变默认的混合,我们必须求助于使用glBlendFunc(),它有两个参数。第一个说明如何处理源,第二个说明目的地。为了描述接下来发生的事情,请注意,最终发生的事情只是将每个 RGBA 源组件与每个目标组件相加、相减或进行其他操作。也就是说,源的红色通道与目的的红色通道混合,源的绿色与目的的绿色混合,依此类推。这通常是这样表示的:把源 RGBA 值 Rs、Gs、Bs、称为,把目的值 Rd、Gd、Bd、Ad 。但是我们还需要源和目的地的混合因子,表示为 Sr,Sg,Sb,Sa ,和 Dr,Dg,Db,Da 。(没看起来那么复杂,真的)。这是最终合成颜色的公式:

*(R,G,B) = ((Rs* Sr) + (Rd* Dr),(Gs* Sg) + (Gd*Dg),(Bs* Sb) + (Bd*Db))*

换句话说,将源颜色乘以其混合因子,并将其添加到乘以其混合因子的目标颜色。

最常见的混合形式之一是在已经绘制好的东西(即目的地)上覆盖一个半透明的面。像以前一样,这可以是模拟的窗玻璃,飞行模拟器的*视显示器,或者其他图形,当与现有的图像混合时可能会看起来更好。(后者在遥远的太阳中被大量用于许多元素,如星座名称、轮廓等。)根据目的的不同,您可能希望覆盖图接*不透明,使用接* 1.0 的 alpha,或者非常模糊,使用接* 0.0 的 alpha。

在这个基本的混合任务中,源的颜色首先乘以 alpha 值,即它的混合因子。因此,如果源红色的最大值为 1.0,alpha 为 0.75,那么结果就是 1.0 乘以 0.75。这同样适用于绿色和蓝色。另一方面,目标颜色乘以 1.0 减去源的 alpha 。为什么呢?这有效地产生了永远不会超过最大值 1.0 的复合颜色;否则,可能会发生各种颜色失真。或者这样想象:源的 alpha 值是允许源填充的颜色“宽度”1.0 的比例。剩余空间变成 1.0 减去源的 alpha。alpha 越大,可以使用的源颜色的比例就越大,保留给目标颜色的比例就越小。因此,alpha 越接* 1.0,复制到帧缓冲区的源颜色就越多,从而替换目标颜色。

注意:在这些例子中,使用了标准化的颜色值,因为它们比使用无符号字节(表示从 0 到 255 的颜色)更容易理解这个过程。

现在我们可以在下一个例子中检验这一点。要设置前面描述的混合函数,可以使用下面的调用:

gl.glBlendFunc(GL10)。*GL_SRC_ALPHA*gl10。*GL_ONE_MINUS_SRC_ALPHA*

GL_SRC_ALPHAGL_ONE_MINUS_SRC_ALPHA是之前描述的混合因子。请记住,第一个参数是源的混合,即当前正在编写的对象。将该行放在启用混合的位置之后。红色,编译并运行。你看到图 6–2 了吗?

Image

图 6–2。 红色方块的 alpha 值为 0.5,蓝色方块的 alpha 值为 1.0。

发生了什么事?蓝色的 alpha 值为 1.0,因此每个蓝色片段会完全替换背景中的任何内容。那么 alpha 为 0.5 的红色表示 50%的红色被写入目标。黑色区域将是暗红色,但只有glColor4f()中给出的指定值 1.0 的 50%。目前为止,一切顺利。现在在蓝色之上,50%的红色值与 50%的蓝色值混合:

混合颜色=颜色源源的 Alpha+(1.0-源的 Alpha)目标的颜色。或者根据上一个示例中的值查看每个组件:

红色=1.00.5+(1.0-0.5)0.0

绿色=0.00.5+(1.0-0.5)0.0

蓝色=0.00.5+(1.0-0.5)1.0

因此,片段像素的最终颜色应该是 0.5、0.0、0.5 或洋红色。现在红色和由此产生的洋红色有点偏暗。如果你想让它变得更亮,你会怎么做?如果有一种混合各种颜色全部强度的方法就好了。你会使用 1.0 的 alpha 值吗?没有。为什么呢?好吧,以蓝色为目标,源 alpha 为 1.0,前面的蓝色通道等式就是 0.01.0+(1.0-1.0)1.0。这等于 0,而红色是 1.0,或者是纯色。你想要的是在黑色背景上书写时有最亮的红色,蓝色也是一样。为此,你可以使用一个混合函数,以最大强度写出两种颜色,比如GL_ONE。这意味着:

     gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

回到使用源三元组红色= 1、绿色=0、蓝色=0 和目的地红色= 0、绿色=0、蓝色= 1(alpha 默认为 1.0)的等式,计算如下:

红色=11+01

绿色=0(1+(0-0)1

蓝色=01+(1-0)1

这就产生了一种颜色,其中红色=1,绿色=0,蓝色=1。而我的朋友,是洋红色的(见图 6–3)。

Image

图 6–3混合红色和蓝色的全部强度

现在是时候进行另一种实验了。以上例中的代码为例,将两个 alphas 设置为 0.5,并将混合函数重置为传统的透明度值:

    gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);

运行修改后的代码后,请注意组合的颜色,并注意在-4.0 处的另一个正方形是蓝色的,也是第一个被渲染的,第二个是红色的。现在颠倒颜色的顺序,运行。怎么了?您应该得到类似于图 6–4 的东西。

Image

图 6–4。 左边先画蓝色,右边先画红色。

交叉点颜色略有不同。这显示了 OpenGL 中一个令人困惑的问题:与大多数 3D 框架一样,渲染时根据面和颜色的顺序,混合会略有不同。在这种情况下,弄清楚发生了什么其实很简单。在图 6–4 的左图中,蓝色方块首先以 0.5 的 alpha 绘制。因此,即使蓝色三元组被定义为 0,0,1,当写入帧缓冲区时,alpha 值也会将其降低到 0,0,. 5。现在添加具有相似属性的红色方块。自然,红色将以与蓝色相同的方式写入帧缓冲区的黑色部分,因此最终值将是. 5,0,0。但是注意当红色写在蓝色上面时会发生什么。由于蓝色已经是其强度的一半,混合函数将进一步将其削减到. 25,这是混合函数的目标部分的结果,(1.0-源 alpha)蓝色+目标*,或(1.0-.5).5+0,或. 25。最后的颜色然后是 .5,0,. 25。蓝色的强度越低,它对复合色的贡献就越小,红色占主导地位。现在在图 6–4 的右图中,顺序颠倒了,所以蓝色占主导,最终颜色为. 25,0,. 5。

Table 6–1 包含了所有允许的 OpenGL ES 混合因子,尽管源和目标并不都支持。正如你所看到的,有足够的空间来修补,没有固定的规则来创造最好的效果。这将高度依赖于你的个人品味和需求。尽管尝试不同的价值观很有趣。确保用暗淡的灰色填充背景,因为一些组合在黑色背景上书写时只会产生黑色。

Image

这里最后一个可能在一些混合操作中非常方便的方法是glColorMask()。此功能允许您阻止一个或多个颜色通道被写入目标。要查看实际效果,请将红色方块的颜色修改为 1,1,0,1;将两个混合功能设置回GL_ONE;又注释掉了一行glglBlendEquation(GL10.GL_FUNC_SUBTRACT);。运行时,您应该会看到类似于图 6–5 中左边的图像。红色方块现在是黄色的,当与蓝色混合时,在交叉点产生白色。现在添加下面一行:

          gl.glColorMask(true, false, true, true);

前面的行在被拉到帧缓冲器时屏蔽或关闭绿色通道。运行时,您应该会在图 6–5 中看到右图,该图与图 6–3 非常相似。事实上,逻辑上它们是相同的。

Image

图 6–5。 左边不使用glColorMask,所以所有颜色都在起作用,而右边屏蔽掉绿色通道。

多色混合

现在,我们可以花几分钟来看看当用每个顶点的单独颜色定义正方形时,混合函数的效果。将清单 6–2 中的添加到正方形的构造函数中。第一组颜色定义黄色、品红色和青色。标准红绿蓝的互补色在第二组中指定。

清单 6–2。 两个正方形的顶点颜色

`    float squareColorsYMCA[] =
{
             1.0f, 1.0f, 0.0f, 1.0f,
             0.0f, 1.0f, 1.0f, 1.0f,
             0.0f, 0.0f, 0.0f, 1.0f,
             1.0f, 0.0f, 1.0f, 1.0f
};
    float squareColorsRGBA[] =
{

1.0f, 0.0f, 0.0f, 1.0f,
             0.0f, 1.0f, 0.0f, 1.0f,
             0.0f, 0.0f, 1.0f, 1.0f,
             1.0f, 1.0f, 1.0f, 1.0f
};`

将第一个颜色数组分配给第一个方块(到目前为止一直是蓝色的),将第二个颜色数组分配给之前的红色方块。我在SquareRenderer.java中这样做,并通过 square 的构造函数传递颜色数组。当然,现在我们需要两个方块,每种颜色一个,而不是只有一个。不要忘记启用颜色数组的使用。

你现在应该很熟悉了,知道该怎么做了。另外,请注意,数组现在被规范化为一组浮点数,而不是以前使用的无符号字节,所以您必须调整对glColorPointer()的调用。解决方案由学生自己决定(我一直想这么说)。禁用混合后,您应该会看到图 6–6 中最左边的图像,当使用传统的透明功能启用时,结果应该是图 6–6 中中间的图像。什么事?不是吗?你说它看起来仍然像第一个图形?为什么会这样?

回头看看颜色数组。请注意每行的最后一个值 alpha 是如何达到最大值的。请记住,在这种混合模式下,任何目标值都要乘以(1.0-源 alpha),或者更确切地说,是 0.0,这样源颜色就占主导地位,如前面的示例所示。看到一些真正的透明度的一个解决方案是使用下面的:

         gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);

这是可行的,因为它完全抛弃了阿尔法通道。如果您希望 alpha 具有“标准”函数,只需将 1.0 值更改为其他值,如. 5,并将混合函数更改为以下值:

        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);

而结果就是图 6–6 中最右边的图像。

Image

图 6–6无混合、GL_ONE混合和阿尔法混合,分别为

纹理混合

现在,战战兢兢地,我们可以接*纹理的混合了。最初,这看起来很像前面描述的阿尔法混合,但是通过使用多重纹理可以做各种有趣的事情。

首先,让我们重新编写前面的代码来同时支持两个纹理,并进行顶点混合。你必须修改第五章例子中的Square.draw()createImage()。正方形也需要支持纹理坐标,并且正方形的每个实例都需要自己独特的纹理。图 6–7 中最右边的图像是您禁用混合后应该得到的图像。如果你激活上一个练习中的颜色,并使用本章前面的GL_ONE功能启用混合,就可以生成中间的那个。

那么,正确的图像是如何产生的呢?

使用单一位图并着色是节省内存的常见做法。如果你在 OpenGL 层做一些 UI 组件,考虑使用一个单一的图像,并使用这些技术着色。你可能会问为什么它是纯红的,而不仅仅是浅红色,允许一些颜色的变化。这里所发生的是顶点的颜色被每个片段的颜色相乘。对于红色,我使用了 1.0,0.0,0.0 的 RGB 三元组。因此,当每个片段在通道乘法中计算时,绿色和蓝色通道将乘以 0,因此它们被完全过滤掉,只留下红色通道。如果想要让一些其他颜色透过,可以指定顶点偏向更中性的色调,所需的色调颜色比其他颜色稍高,如 1.0、0.7、0.7。

Image

图 6–7在左边,只显示纹理。在中间,它们与颜色混合,在右边的是纯红。

你也可以很容易地给纹理添加透明度,图 6–8。为了实现这一点,我将在这里引入一个小的简化因子。你可以通过简单的使用glColor4f()用一种单一的颜色给纹理化的表面着色,并且完全消除了创建顶点颜色数组的需要。所以,对于第二个方块,最*的一个,用glColor4f(1, 1, 1, .75)给它着色,并确保重置第一个方块的颜色;否则会随着第二个变暗。此外,确保混合已打开,并且混合功能使用了SRC_ALPHA/ONE_MINUS_SRC_ALPHA组合。

Image

图 6–8。 左边的图像 alpha 为 0.5,而右边的为 0.75。

多重纹理

现在我们已经讨论了颜色混合和纹理颜色混合模式,但是把两个纹理混合在一起做第三个怎么样呢?这样的技术被称为多重纹理。多重纹理可用于在执行某些数学运算时将一个纹理叠加到另一个纹理上。更复杂的应用包括简单的图像处理。但是让我们先去摘低垂的果实。

多重纹理需要使用纹理组合器纹理单元。纹理组合器让你可以组合和操作绑定到硬件纹理单元的纹理,这些纹理单元是图形芯片的特定部分,将图像包裹在对象周围。如果您希望大量使用合并器,您可能希望通过gl来验证支持的总数。glGetIntegerv(GL10.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, numberTextureUnits),其中numberTextureUnits被定义为一个整数。

为了建立一个管道来处理多重纹理,我们需要告诉 OpenGL 使用什么纹理以及如何将它们混合在一起。这个过程与之前处理 alpha 和颜色混合操作时定义混合函数没有太大的不同(至少在理论上是这样)。它确实大量使用了glTexEnvf()调用,这是 OpenGL 的另一个超负荷方法。(如果你不相信我,可以在 OpenGL 网站上查看它的官方参考页面。)这将设置纹理环境,定义多重纹理处理的每个阶段。

图 6–9 显示了组合器链。每个组合器引用第一个组合器的前一个纹理片段(P0Pn)或输入片段。然后,它从一个“源”纹理(图中的S0)中取出一个片段,将其与P0组合,如果需要的话,将它交给下一个组合器C1,循环重复。

Image

图 6–9。 纹理合并器链

解决这个问题的最好方法和其他任何问题一样:查阅代码。在下面的示例中,两个纹理一起加载,绑定到各自的纹理单元,并合并成一个输出纹理。我们尝试了几种不同的方法来组合两幅图像,并对每幅图像的结果进行了深入的展示和检查。

首先,我们重访我们的老朋友。我们回到了只有一个纹理,上升和下降。颜色支持也被关闭。因此,您应该有类似于清单 6–3 的东西。确保你还在加载第二个纹理。

清单 6–3。 Square.draw()改版,修改为多文支持

`public void draw(GL10 gl)

{
        gl.glEnable(GL10.GL_TEXTURE_2D);
        gl.glBindTexture(GL10.GL_TEXTURE_2D,mTexture0);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);         gl.glFrontFace(GL11.GL_CW);
        gl.glVertexPointer(2, GL11.GL_FLOAT, 0, mFVertexBuffer);
        gl.glColorPointer(4, GL11.GL_FLOAT, 0, mColorBuffer);

gl.glClientActiveTexture(GL10.GL_TEXTURE0);                       //1
        gl.glTexCoordPointer(2, GL10.GL_FLOAT,0,mTextureCoords);

gl.glClientActiveTexture(GL10.GL_TEXTURE1);                       //2
        gl.glTexCoordPointer(2, GL10.GL_FLOAT,0,mTextureCoords);

multiTexture(gl,mTexture0,mTexture1);                             //3

gl.glDrawElements(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_BYTE, mIndexBuffer);
        gl.glFrontFace(GL11.GL_CCW);
}`

这里有一个新的调用,如第 1 行和第 2 行所示。是glClientActiveTexture(),设置操作什么纹理单元。这是在客户端,而不是硬件方面的事情,并指示哪个纹理单元将接收纹理坐标数组。不要把这个和glActiveTexture()混淆,后者用在清单 6–4 中,它实际上打开了一个特定的纹理单元。第 3 行调用配置纹理单元的方法。

这是一个非常简单的默认情况。精彩的东西在后面。

清单 6–4。 设置纹理合并器

`        public void multiTexture(GL10 gl, int tex0, int tex1)
{
                float combineParameter= GL10.GL_MODULATE;                        //1

// Set up the First Texture.
                gl.glActiveTexture(GL10.GL_TEXTURE0);                            //2
                gl.glBindTexture(GL10.GL_TEXTURE_2D, tex0);                      //3

// Set up the Second Texture.
                gl.glActiveTexture(GL10.GL_TEXTURE1);
                gl.glBindTexture(GL10.GL_TEXTURE_2D, tex1);

// Set the texture environment mode for this texture to combine.
                gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,
                    combineParameter);                                           //4
}`

  • 第 1 行指定了组合器应该做什么。表 6–1 列出了所有可用的可能值。

  • glActiveTexture()在第 2 行激活一个特定的硬件纹理单元。

  • 第 3 行不应该是个谜,因为你以前见过。在这个例子中,第一纹理被绑定到特定的硬件纹理单元。下面两行对第二个纹理做了同样的处理。

  • 现在告诉系统如何处理最后一行的纹理。在该表中,P 是 previous,S 是 source,下标 a 是 alpha,c 是 color,仅在必须单独考虑 color 和 alpha 时使用。

Image

现在编译并运行。您的显示表面上应该类似于 Figure 6–10 的结果。

Image

图 6–10。 左边的海德利是“先前的”纹理,而杰克森·波拉克的画是“源”使用GL_MODULATE时,结果在右边。

现在是时候玩其他组合器设置了。尝试用GL_ADD替换清单 63 中的combineParameter中的GL_MODULATE。然后通过GL_BLENDGL_DECAL跟随这个。结果如图 6–11 所示。另外,注意叠加纹理的白色部分是不透明的。因为白色对于所有三种颜色都是 1.0,所以它将总是产生 1.0 的颜色,以便遮挡下面的任何东西。对于非白色的阴影,你应该可以看到一点海德利纹理穿透。中间图像中的GL_BLEND,图 6–11 不太明显。为什么青色代替了红色?很简单。说红色值是 1.0,它的最高。考虑GL_BLEND的等式:

output =Pn(1—Sn)+Sn×C

对于红色,第一部分将是零,因为红色的值 1 被等式中的 1 减去,天哪,第二部分也将是零,假设使用默认的黑色环境颜色。考虑绿色通道。假设背景图像的绿色值为 0.5,这是“先前”的颜色,同时保持 splat 颜色(源)为纯红色(因此 splat 中没有蓝色或绿色)。现在等式的第一部分变成了. 5(1.0-0.0),或. 5。也就是说,前一个*纹理 Hedly 中绿色的. 5 值与源纹理中的“1 减绿色”相乘。由于源的红色斑点中的绿色和蓝色通道都是 0.0,这意味着没有任何红色的绿色和蓝色的组合会产生青色阴影,因为青色是红色的反转。如果你仔细观察图 6–11 中的中间图像,你可以分辨出一块突出的碎片。这同样适用于洋红色和黄色斑点。在图 6–11 的最右侧图像中,使用了GL_DECAL,它可以起到许多与塑料模型贴花相同的作用,即应用标志或符号来遮挡其后面的任何东西。因此,对于贴花,通常纹理的实际图像部分的 alpha 通道将设置为 1.0,而不是所需图像的任何部分的 alpha 通道将设置为 0.0。通常背景是黑色的,在你的画图程序中,你可以让它根据亮度或者图像中非零颜色的部分生成一个 alpha 通道。在 splat 的情况下,因为背景是白色的,我必须先反转颜色,使其变成黑色,生成遮罩,并将其与正常的正片图像合并。一些略小于 1 的 alpha 是为绿色通道生成的,因此,您可以看到一小部分 Hedly 显示出来。

Image

图 6-11。使用左边的GL_ADDGL_BLEND为中心,右边的GL_DECAL

最后一个任务是制作第二个纹理的动画。你将需要创建一个textureCoordinatesmTextureCoods0mTextureCoords1的副本,每个纹理一个,因为我们不能再共享它们。接下来公开用于在构造函数中生成 Java 字节缓冲区的“原始”坐标。这样,我们可以在Square.draw()方法中修改它们。然后将以下内容添加到draw()中,仅更新贴花纹理的坐标:

`    for (i=0;i<8;i++)
  {
         mTextureCoordsAnimated[i]+=.01;
  }

mTextureCoords1.position(0);
     mTextureCoords1.put(mTextureCoordsAnimated);`

调用mTextureCoords1.position()来重置缓冲区的内部指针。否则,下面对put()的调用将在下次通过时追加数据并溢出缓冲区。

像这样的效果可以用来在卡通般的环境中或行星周围的云层中制作雨或雪的动画。后者会很酷,如果你有两个额外的纹理,一个用于上层云,一个用于下层云,以不同的速度移动。

如前所述,环境参数GL_COMBINE需要一系列额外的设置才能工作,因为它让您可以在更精确的水*上操作合并器方程。如果你只想使用GL_COMBINE,它默认为GL_MODULATE,所以你看不出两者有什么不同。使用Arg0Arg1代表输入源,它们是纹理组合器。它们是通过使用类似下面的行来设置的,其中GL_SOURCE0_RGB是在表 6–3 中引用的参数 0 或Arg0:

    gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_SOURCE0_RGB, GL10.GL_TEXTURE);

同样,你可以用GL_SOURCE1_RGB来代表Arg1:

    gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_SOURCE1_RGB, GL10.GL_TEXTURE);

Image

用凹凸贴图

你可以用纹理做很多非常复杂的事情;凹凸贴图只是其中之一。因此,接下来是对“凸起”到底是什么以及为什么任何人都应该关注映射它们的讨论。

正如前面所指出的,计算机图形学中的大部分挑战是在幕后使用巧妙的黑客技术来制作看起来复杂的视觉效果。凹凸贴图只是其中的一个技巧,在 OpenGL ES 1.1 中,它可以用纹理合并器来实现。

就像纹理是在简单的面上增加复杂性的“技巧”一样,凹凸贴图是一种给纹理添加第三维的技术。它用于生成物体整体表面的粗糙度,当被照亮时,会产生一些令人惊讶的真实高光。它可能用于模拟湖面、网球表面或行星表面的波浪。

一个物体表面的粗糙度是通过它处理光线和阴影的方式来感知的。例如,考虑满月和凸月,如图图 6–12 所示。当太阳在月亮正前方时,月亮是圆的,因此,月亮表面只不过是深浅不一的灰色。看不到任何影子。和你背对太阳看地面没太大区别。在你头部阴影的周围,表面看起来是*的。现在,如果把光源移到事物的侧面,突然各种细节都蹦了出来。图 6–12 中的右图显示了一个凸月,太阳朝向左侧,即月亮的东翼。这是一个完全不同的故事,不是吗?

Image

图 6–12。 相对较少的细节显示在左边,而用斜向照明,则更多的细节显示在右边。

理解高光和阴影是如何一起工作的,对于训练优秀的艺术家和插图画家来说是绝对重要的。

添加真实的表面位移来复制整个月球表面可能需要数千兆字节的数据,从内存和 CPU 的角度来看,这对于当前一代的小型手持设备来说是不可能的。因此进入了相当优雅的凹凸贴图到中心阶段。

你可能还记得在第四章中,你必须给球体模型添加一组“面法线”。法线仅仅是垂直于面的向量,显示面指向的方向。任何光源的法线角度在很大程度上决定了人脸的明暗程度。脸部朝向光线越直接,光线就越亮。那么,如果你有一种紧凑的方法来编码法线,而不是基于逐面,因为一个模型可能有相对较少的面,而是基于,比如说,一个像素一个像素?如果您可以将编码的法线数组与真实的图像纹理相结合,并根据入射光的方向以某种方式处理它,使图像中的像素变亮或变暗,会怎么样?

这让我们回到了纹理合成。在表 6–3 中,注意最后两种组合器类型:GL_DOT3_RGBGL_DOT3_RGBA。现在,回到你高中的几何课上。还记得两个向量的点积吗?点积和叉积都是那些的东西,你用抱怨“老师,对吗??为什么我需要知道这个?”好吧,现在你会得到你的答案。

点积是基于另外两个向量的角度的向量的长度。还不明白吗?考虑图 6–13 中左侧的图表。点积是指向灯光的法向量的“量”,该值用于直接照亮面部。在图 6–13 的右图中,脸部与太阳方向成直角,因此没有被照亮。

Image

图 6–13。 左边,脸被照亮;右边就不是这样了。

记住这一点,凹凸贴图使用的“欺骗”如下。使用你想要使用的真实纹理,并添加一个特殊的第二个辅助纹理。第二个纹理编码普通信息,而不是 RGB 颜色。因此,它没有使用每个都是 4 个字节的浮点数,而是使用 1 个字节的值作为法向量的 xyz 值,这样可以方便地放入一个 4 个字节的像素中。由于向量通常不需要非常精确,8 位分辨率就可以了,而且非常节省内存。因此,这些法线以一种直接映射到您想要突出显示的垂直特征的方式生成。

因为法线可以有正值也可以有负值(背向太阳时为负值),所以 xyz 值在 0 到 1 的范围内居中。也就是说,-127 到+127 必须映射到 0 到 1 之间的任何位置。因此,“红色”分量通常是矢量的 x 部分,计算如下:

    *red* = (*x* +1) /2.0

当然,绿色和蓝色位的情况类似。

现在看看在表 6–3 的GL_DOT3_RGB条目中表示的公式。这将 RGB 三元组作为向量,并返回其长度。n 是法向量,L 是光向量,所以长度求解如下:

长度】= 4×4(rn——5)×(rl

因此,如果面沿着 x 轴直接朝向灯光,法线的红色将是 1.0,灯光的红色或 x 值也将是 1.0。绿色和蓝色位是 0 的编码形式 0.5。将它代入前面的等式会是这样的:

长度= 4×4(1**——5)×(1】5)+(【5】**

**长度 = 4×(.25+0+0) =1.0

这正是我们所期待的。如果法线在 z 方向上指向上并远离表面,用蓝色字节编码,答案应该是 0,因为法线主要指向远离纹理的 X 和 Y *面。在图 6–14 中左边的图像显示了我们地球地图的一部分,而右边的图像显示了它对应的法线贴图。

Image

图 6–14。 左边是我们的形象;右边是匹配的法线贴图。

为什么法线贴图主要是紫色的?指向远离地球表面的垂直矢量被编码为红色=.5,绿色=.5,蓝色=1.0。(记住. 5 其实是 0。)

当纹理合并器设置为 DOT3 模式时,它使用法线和光照向量来确定每个纹理元素的强度。然后,该值用于调制真实图像纹理的颜色。

现在是时候回收之前的多纹理项目了。这一次,第二个纹理需要由可从 Apress 站点获得的凹凸贴图组成。接下来,设置合并器来处理法线贴图和任何从过去的练习中剩余的动画。

在这个例子中加载法线贴图,然后添加新的例程,multiTextureBumpMap(),如清单 6–5 中的所示。

清单 6–5。 为凹凸贴图设置组合器

`static float lightAngle=0.0f;
        public void multiTextureBumpMap(GL10 gl, int mainTexture, int normalTexture)
    {
            float x,y,z;

lightAngle+=.3f;                                                                //1

if(lightAngle>180)
            lightAngle=0;

// Set up the light vector.
            x = (float) Math.sin(lightAngle * (3.14159 / 180.0f));                          //2
            y = 0.0f;
            z = (float) Math.cos(lightAngle * (3.14159 / 180.0f));

// Half shifting to have a value between 0.0f and 1.0f.
            x = x * 0.5f + 0.5f;                                                            //3
            y = y * 0.5f + 0.5f;
            z = z * 0.5f + 0.5f;

gl.glColor4f(x, y, z, 1.0f);                                                    //4

//The color and normal map are combined.
            gl.glActiveTexture(GL10.GL_TEXTURE0);                                           //5
            gl.glBindTexture(GL10.GL_TEXTURE_2D, mainTexture);

gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL11.GL_COMBINE);   //6
            gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_DOT3_RGB);       //7
            gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL11.GL_SRC0_RGB, GL11.GL_TEXTURE);           //8
            gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_PREVIOUS);          //9             // Set up the Second Texture, and combine it with the result of the Dot3
    combination.

gl.glActiveTexture(GL10.GL_TEXTURE1);                                           //10
            gl.glBindTexture(GL10.GL_TEXTURE_2D, normalTexture);

gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);  //11
}`

前述操作使用两个进行。第一个将凹凸或法线贴图与原色混合,这是使用glColor4f()调用建立的。第二个使用我们的老朋友GL_MODULATE将结果与彩色图像结合起来。

所以让我们一点一点地检查一下:

  • 在第 1 行中,我们定义了lightAngle,它将围绕纹理在 0 到 180 度之间循环,以显示高光在不同光照条件下的外观。
  • 计算第 2 行中灯光矢量的 xyz 值。
  • 在第 3 行,xyz 组件需要被缩放以匹配凹凸贴图的组件。
  • 现在使用第 4 行中的光线矢量组件给片段上色。
  • 先设置并绑定凹凸贴图,就是 5f 行的 tex0
  • 第 6 行的GL_COMBINE告诉系统预期一个组合类型跟随其后。
  • 在第 7 行,我们指定我们将使用GL_DOT3_RGB操作只组合 RGB 值(GL_DOT3_RGBA包括 alpha,但在这里并不需要)。
  • 这里我们设置了“阶段 0”,这是两个阶段中的第一个。第 8 行指定了第一位数据的来源。这表示使用当前纹理单元(GL_TEXTURE0)的纹理作为第 5 行分配的凹凸贴图的来源。
  • 然后我们必须告诉它与之前的颜色混合——在这个例子中,是通过第 4 行的glColor()设置的。对于阶段 0,GL_PREVIOUSGL_PRIMARY_COLOR相同,因为没有之前的纹理可以使用。
  • 现在在第 10 行和下面的行中设置阶段 1。参数tex1是彩色图像。
  • 现在我们要做的就是把图像和凹凸贴图结合起来,这就是第 11 行所做的。

我的源纹理被选中,这样你可以很容易地看到结果。启动时,光线应该从左向右移动,照亮陆地的边缘,如图 6–15 所示。

Image

图 6–15。 分别在早上、中午和晚上到达北美

看起来很酷,是吧?但是我们能把它应用到一个旋转的球体上吗?试一试,循环利用上一章末尾的太阳系模型。为了使凹凸贴图的细节更容易被看到,太阳被去掉了,代替了地球的一个更大的图像。因此,我们将加载凹凸贴图,将地球移动到场景的中心,调整照明,并添加组合器支持。

在分配主图像的位置下方,添加以下内容:

    if(imageId == true)   {     m_BumpmapID = createTexture(gl, context, imageId, resourceId);   }

加上这个:

    int m_BumpmapID;

现在确保用位于太阳系控制器对象中的init()中的新参数调用这个函数。

使用清单 6–6 作为新的draw()方法,放入Planet.java并从bumpmappingControllerexecutePlanet()例程中调用。这主要是为纹理合并器做准备,并调用清单 6–6 中的multiTextureBumpMap

清单 6–6。 修改后执行为凹凸贴图

`public void draw(GL10 gl)
    {
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            gl.glEnable(GL10.GL_CULL_FACE);
            gl.glCullFace(GL10.GL_BACK);
            gl.glEnable(GL10.GL_LIGHTING);

gl.glFrontFace(GL10.GL_CW);             gl.glEnable(GL10.GL_TEXTURE_2D);
            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, m_VertexData);

gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
            gl.glClientActiveTexture(GL10.GL_TEXTURE0);
            gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, m_textureData);

gl.glClientActiveTexture(GL10.GL_TEXTURE1);
            gl.glTexCoordPointer(2, GL10.GL_FLOAT,0,m_textureData);

gl.glMatrixMode(GL10.GL_MODELVIEW);

gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
            gl.glNormalPointer(GL10.GL_FLOAT, 0, m_NormalData);

gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, m_ColorData);
            multiTextureBumpMap(gl, m_BumpmapID, textures[0]);
            gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, (m_Slices+1)2(m_Stacks-1)+2);

}`

方法multiTextureBumpMap()与前一个相同,除了光矢量计算可以删除(直到第 4 行),所以只需复制到你的行星物体上。

现在转到你在太阳系控制器中初始化灯光的地方,注释掉创建高光材质的调用。凹凸贴图和镜面反射相处得不太好。

清单 6–7 是新的执行例程;控制器也是如此。这使得太阳转储,将地球移动到事物的中心,并将主光线置于左侧。

清单 6–7。 用这个代替旧的execute()套路

`private void execute(GL10 gl) {
        float posFill1[]={-8.0f, 0.0f, 7.0f, 1.0f};
        float cyan[]={0.0f, 1.0f, 1.0f, 1.0f};
        float orbitalIncrement=0.5f;
        float sunPos[]={0.0f, 0.0f, 0.0f, 1.0f};

gl.glLightfv(SS_FILLLIGHT1, GL10.GL_POSITION, makeFloatBuffer(posFill1));

gl.glEnable(GL10.GL_DEPTH_TEST);
            gl.glClearColor(0.0f, 0.25f, 0.35f, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);             gl.glPushMatrix();

gl.glTranslatef(-m_Eyeposition[X_VALUE],-m_Eyeposition[Y_VALUE],-
                m_Eyeposition[Z_VALUE]);
            gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(sunPos));

gl.glEnable(SS_FILLLIGHT1);
            gl.glEnable(SS_FILLLIGHT2);

gl.glPushMatrix();

angle+=orbitalIncrement;
            gl.glRotatef(angle, 0.0f, 1.0f, 0.0f);
            executePlanet(m_Earth, gl);
            gl.glPopMatrix();
            gl.glPopMatrix();
    }`

如果你现在看到类似图 6–16 的东西,你可以正式地拍拍自己的背。

Image

图 6–16颠簸的大地

好,现在做个实验。移动灯光的位置,使它从右边而不是左边进来。图 6–17 是意想不到的结果。这是怎么回事?现在这些山看起来像山谷。

Image

图 6–17嗯?

现在的情况是,我们正在走向一个以前没有合并者去过的地方。通过使用我们自己的照明,由光矢量提供的模拟照明的效果被去除了。我们的灯在左边,它只是碰巧看起来很好,主要是靠运气。如果你的场景的照明是相对静态的,这里的凹凸贴图就可以了。它不喜欢多光源。事实上,通过灯光向量指定的伪照明效果会被忽略,而不是“真实”光源。此外,如果关闭这些光源,灯光向量会完全忽略对象上的任何着色。在这种情况下,你会看到整个星球变亮和变暗,因为这是纹理本身发生的事情,因为它只是一个 2D 表面。如果它的一部分被点亮,所有的都被点亮。那么,一个 GL 呆子该怎么办呢?着色器我的朋友。着色器。这就是 OpenGL ES 2.0 和 Android 扩展的用武之地。

总结

在这一章中,你学习了 OpenGL ES 1 提供的混合功能。混合有自己独特的语言,通过混合函数和组合器来表达。你已经学习了半透明,包括如何和何时应用它。还包括一些巧妙的技巧,通过混合和纹理来制作动画和凹凸贴图。在下一章,我将开始应用其中的一些技巧,并展示其他可以创造更有趣的 3D 世界的技巧。**

七、精心制作的杂集

如果我们知道我们在做什么,那就不叫研究了,对吗?

—阿尔伯特·爱因斯坦

当开始这一章,我试图找到一个合适的报价关于杂集。不幸的是,我所能找到的都是一些杂七杂八的引言。但是阿尔伯特·爱因斯坦写的那本书是一个真正的瑰宝,几乎可以应用,因为亲爱的读者,你正在进行研究——研究如何制作更丰富、更有趣的软件。

在像这样的书中,有时很难对某个特定的主题进行清晰的分类,当它们可能不值得拥有自己的一章时,我们不得不将许多东西放入一章中。因此,在这里我将涵盖一些经典的演示和渲染技巧,无论它们是否可以应用于太阳系项目或,所以在结束时你会惊呼“所以,这就是他们如何做到这一点!”

帧缓冲对象

通常称为 FBOs,您可以将帧缓冲区对象视为简单的渲染表面。到目前为止,您已经使用了一个,并且可能不知道它;你的场景通过GLSurfaceView对象渲染到的 EGL 环境是一个 FBO。你可能不知道的是,你可以同时拥有多个屏幕。像以前一样,我们将从旧标准开始,我们的彩虹色果冻弹跳板,然后看看它能从那里去哪里。

赫德利缓冲对象

这时候你知道该怎么做了:从第五章的中找到练习,用原来的 2D 弹跳纹理方块(图 5-13 ),并以此作为参考。由于大多数代码最终都会更改,我建议从头开始创建一个新项目。活动文件将是标准的默认文件。我们需要为 FBO 支持创建一个单独的对象;把这个叫做FBOController.java。它将涵盖 FBO 的初始化和执行。它应该看起来像清单 7–1,减去几个实用函数,您应该在别处有这些函数以节省空间。这些都在描述中注明了。

清单 7–1。 帧缓冲对象控制器

`public class FBOController
{
            public Context context;
            int[] m_FBO1 = new int[3];
            int[] m_FBOTexture = new int[1];
            public String TAG = "FBO Controller";
            int[] originalFBO = new int[1];
            int[] depthBuffer = new int[1];
            int m_ImageTexture;
            static float m_TransY = 0.0f;
            static float m_RotX = 0.0f;
            static float m_RotZ = 0.0f;
            static float m_Z = -1.5f;
            int[] m_DefaultFBO = new int[1];
            int m_Counter=0;
            boolean m_FullScreen = false;

public int init(GL10 gl, Context contextRegf,int resource, int width,
             int height)
        {
                    GL11ExtensionPack gl11ep = (GL11ExtensionPack) gl;                  //1

//Cache the original FBO, and restore it later.

gl11ep.glGetIntegerv(GL11ExtensionPack.GL_FRAMEBUFFER_BINDING_OES,  //2
                            makeIntBuffer(originalFBO));

gl11ep.glGenRenderbuffersOES(1, makeIntBuffer(depthBuffer));        //3
                    gl11ep.glBindRenderbufferOES(GL11ExtensionPack.GL_RENDERBUFFER_OES,
                            depthBuffer[0]);

gl11ep.glRenderbufferStorageOES(GL11ExtensionPack.GL_RENDERBUFFER_OES,
                    GL11ExtensionPack.GL_DEPTH_COMPONENT16, width, height);

//Make the texture to render to. gl.glGenTextures(1, m_FBOTexture, 0);                               //4
                    gl.glBindTexture(GL10.GL_TEXTURE_2D, m_FBOTexture[0]);

gl.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_RGB, width, height, 0,
                            GL10.GL_RGB, GL10.GL_UNSIGNED_SHORT_5_6_5,null);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
                            GL10.GL_LINEAR);
                    gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
                            GL10.GL_LINEAR);
                    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);

//Now create the actual FBO.

gl11ep.glGenFramebuffersOES(1, m_FBO1,0);                           //5

gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES,
                            m_FBO1[0]);

// Attach the texture to the FBO.                                   //6
                    gl11ep.glFramebufferTexture2DOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES,
                            GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
                            GL10.GL_TEXTURE_2D,
                            m_FBOTexture[0], 0);

// Attach the depth buffer we created earlier to our FBO.           //7
                    gl11ep.glFramebufferRenderbufferOES
                            (GL11ExtensionPack.GL_FRAMEBUFFER_OES,
                            GL11ExtensionPack.GL_DEPTH_ATTACHMENT_OES,
                            GL11ExtensionPack.GL_RENDERBUFFER_OES, depthBuffer[0]);

// Check that our FBO creation was successful.

gl11ep.glCheckFramebufferStatusOES
                            (GL11ExtensionPack.GL_FRAMEBUFFER_OES);

int uStatus =
                    gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);

if(uStatus != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES)
                            return 0;

gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES,   //8
                            originalFBO[0]); m_ImageTexture = createTexture(gl,contextRegf,resource);            //9

return 1;
        }

public int getFBOName()
                    return m_FBO1[0];

public int getTextureName()
                    return m_FBOTexture[0];

public void draw(GL10 gl)
        {
                    GL11ExtensionPack gl11 = (GL11ExtensionPack) gl;

float squareVertices[] =                                                    //10
        {
                -0.5f, -0.5f, 0.0f,
                 0.5f, -0.5f, 0.0f,
                -0.5f,  0.5f, 0.0f,
                 0.5f,  0.5f, 0.0f
         };

float fboVertices[] =
        {
                -0.5f, -0.75f, 0.0f,
                 0.5f, -0.75f, 0.0f,
                -0.5f,  0.75f, 0.0f,
                 0.5f,  0.75f, 0.0f
        };

float textureCoords1[] =

0.0f, 0.0f,
                1.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 1.0f
        };

if((m_Counter%250)==0)                                                      //11
       {
                if(m_FullScreen)
                     m_FullScreen=false;
                else
                     m_FullScreen=true;
       } gl.glDisable(GL10.GL_CULL_FACE);
                 gl.glEnable(GL10.GL_DEPTH_TEST);

if(m_DefaultFBO[0] == 0)                                                    //12
       {
                gl11.glGetIntegerv(GL11ExtensionPack.GL_FRAMEBUFFER_BINDING_OES,
                     makeIntBuffer(m_DefaultFBO));
       }

gl.glDisableClientState(GL10.GL_COLOR_ARRAY | GL10.GL_DEPTH_BUFFER_BIT);

gl.glEnable(GL10.GL_TEXTURE_2D);

//Draw to the off-screen FBO first.

if(!m_FullScreen)                                                           //13
               gl11.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES,
                     m_FBO1[0]);

gl.glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
               gl.glClear(GL10.GL_COLOR_BUFFER_BIT|GL10.GL_DEPTH_BUFFER_BIT);

gl.glPushMatrix();

gl.glMatrixMode(GL10.GL_MODELVIEW);
               gl.glLoadIdentity();

gl.glTranslatef(0.0f, (float)(Math.sin(m_TransY)/2.0f),m_Z);

gl.glRotatef(m_RotZ, 0.0f, 0.0f, 1.0f);

gl.glBindTexture(GL10.GL_TEXTURE_2D,m_ImageTexture);                     //14

gl.glTexCoordPointer(2, GL10.GL_FLOAT,0, makeFloatBuffer(textureCoords1));
               gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, makeFloatBuffer(squareVertices));
               gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

gl.glPopMatrix();

//Now draw the offscreen frame buffer into another framebuffer.

if(!m_FullScreen)                                                           //15
       {
               gl.glPushMatrix();                gl11.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES,
                    m_DefaultFBO[0]);

gl.glMatrixMode(GL10.GL_MODELVIEW);
               gl.glLoadIdentity();

gl.glTranslatef(0.0f, (float)(Math.sin(m_TransY)/2.0f), m_Z);
               gl.glRotatef(m_RotX, 1.0f, 0.0f, 0.0f);

gl.glBindTexture(GL10.GL_TEXTURE_2D, m_FBOTexture[0]);

gl.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
               gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0,
                    makeFloatBuffer(textureCoords1));
               gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0,
                    makeFloatBuffer(fboVertices));
               gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
               gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

gl.glPopMatrix();
       }

m_TransY += 0.025f;
               m_RotX+=1.0f;
               m_RotZ+=1.0f;
               m_Counter++;
  }

//createTexture(), makeFloatBuffer() and makeIntBuffer() removed for clarity.         //16

}`

您应该认识到这里的模式,因为创建 fbo 与许多其他 OpenGL 对象非常相似。您生成一个“名称”,绑定它,然后创建和修改对象。在这种情况下,有大量的创造和修改在进行。所以,让我们来分解一下:

  • 在第一行,我们得到了一个叫做GL11ExtensionPack的东西。扩展包是官方认可的一组额外的 API 调用,对于要被批准的特定版本的 OpenGL ES 不是必需的。这些可以由 GPU 供应商自行添加,但它们仍然必须遵循各种额外功能的规范。这方面的一个例子就是——哒哒!—帧缓冲对象!最初 fbo 是 OpenGL ES 2.0 的一部分,但是它们太有用了,所以决定向 1.1 用户开放。所有的 API 调用和定义都有后缀OES。由于 fbo 是普通 2.0 规范的一部分,这些调用不需要 OES。
  • 第 2 行获得当前 FBO,这很可能是正常屏幕。它被缓存起来,以便以后可以恢复。
  • 由于我们正在创建自己的私人 FBO,我们需要自己处理所有的设置,包括创建和添加一个深度缓冲区到我们的目标。第 3 行和接下来的一行生成一个新的缓冲区名称,绑定它,然后分配存储。
  • 此时,在第 4ff 行中,我们需要分配一个纹理图像,并将其链接到我们的帧缓冲区。这是伪装我们的 FBO 所需要的接口,这样它看起来就像 OpenGL 中的其他纹理一样。在这里,我们也可以为边缘条件设置一些正常的纹理设置,并使用双线性过滤。
  • 到目前为止,我们仅仅创建了深度缓冲和图像接口。在第 5f 行中,我们实际上创建了帧缓冲对象,并将前面的位附加到它上面。
  • 第 6 行首先附加纹理。注意GL_COLOR_ATTACHMENT0_OES的用法。纹理位实际上保存了颜色信息,所以它被称为颜色附件。
  • 在第 7 行,我们使用GL_DEPTH_ATTACHMENT_OES对深度缓冲区做了同样的操作。记住,在 OpenGL ES 中,我们只有三种类型的缓冲附件:深度、颜色和模板。后者做一些事情,比如在屏幕的某一部分阻止渲染,这将在本章后面介绍。OpenGL 成人版增加了第四种,GL_DEPTH_STENCIL_ATTACHMENT
  • 第 8 行恢复了之前的 FBO,第 9 行生成了我们的复活节岛朋友海德利的实际纹理,用于弹跳广场。

下一步是移动到 draw 方法,我们将看到 fbo 如何根据需要换入换出。

  • 在第 10ff 行中,您将立即识别出标准的正方形数据,并添加了 FBO 的顶点。
  • 需要第 11ff 行来允许我们在全屏纹理的 FBO 和正常的原始屏幕之间进行切换。
  • 接下来,我们在第 12f 行再次缓存主屏幕的 FBO,就像在 create 方法中一样。
  • 第 13 行是我们实际上告诉 OpenGL 使用我们的新 FBO 的地方。接下来是管理转换的标准代码,等等,这应该会让您感觉像在家里一样。
  • 在第 14 行,我们绑定 Hedly 图像,然后设置顶点和纹理坐标,接下来是glDrawArray()
  • 现在好戏开始了。在第 15ff 行中,FBO 是现在可以绑定到主屏幕的“新”纹理。首先,原始屏幕的 FBO 被绑定,接着是另一组转换调用,以及另一个glClear()。为了让事情更明显,主屏幕被清除为红色,而 FBO 的背景被清除为蓝色。

所以,这仅仅是创造了一个 FBO。您将看到这是一段相当简洁的代码,使用了 OpenGL ES 1 和 2 中的内置函数。是的,它看起来确实有点过于复杂,但是很容易用一个辅助函数包装起来。

但是我们还没有完全完成,因为我们现在必须重新配置驱动程序来使用两个 fbo。重新配置过程的第一部分是看你的设备是否真的支持帧缓冲对象。为此,我们可以回到第五章中关于使用扩展枚举器的讨论。在这种情况下,下面的代码将会工作,这要感谢 OpenGL ES 工作组对这类事情的标准化。

    private boolean checkIfContextSupportsExtension(GL10 gl, String extension)     {         String extensions = " " + gl.glGetString(GL10.*GL_EXTENSIONS*) + " ";         return extensions.indexOf(" " + extension + " ") >= 0;     }

现在对您的初始化代码所在的位置进行如下调用,例如onSurfaceChanged():

`       m_FBOSupported=checkIfContextSupportsExtension(gl,"GL_OES_framebuffer_object");

if(m_FBOSupported)
{
               int resid = book.BouncyCube1.R.drawable.hedly;

m_FBOController = new FBOController();
               m_FBOController.init(gl, this.context, resid, width, height);
}`

你应该能够运行它,看到它所有的华而不实的荣耀。如果你打算长时间盯着它,你的医生的许可可能是必要的。在 Figure 7–1 中最左边的图像是新的辅助 FBO 成为主要渲染表面的地方,而另一个表面现在嵌套在其中。

随意尝试用不同的图像和颜色做第三个或第四个 FBO。

Image

图 7–1在左边,只有海德利在纺纱。海德里和他的窗户现在都在中间逆时针旋转。在右边,框架首尾相连地旋转着。

太阳缓冲物体

你可以用缓冲对象做很多有趣又诡异的事情,相当于拥有了 3D 超能力。例如,你可以在电视机的小模型上模拟一些动画。您可以在地面上的水坑或汽车后视镜的倒影中显示同一数据的多个视图。更好的是,在我们的太阳系模拟器中放一个动画太阳场景的 OpenGL 帧。不是特别逼真,但是挺酷的。

这次我会把这个问题留给学生,但是我用了第五章的期末项目作为开始。你也可以从网站上下载。

我希望你能得到类似于图 7–2 的东西,其中海德利在太阳上上下跳动。

Image

图 7–2使用一个屏幕外的 FBO 在另一个屏幕上制作纹理动画

镜头眩光

我们都看到了。每当相机对准太阳时,这些幽灵般的、发光的薄纱光就会在电视场景周围飞舞,或者侵入图像。这是因为太阳光在相机的光学系统中愉快地来回反射,产生了大量的二次图像。这些既可以被视为一个明亮的广霾和许多较小的文物。图 7–3(左)用 1971 年阿波罗 14 号登月任务中的一张图片说明了这一点。耀斑遮住了登月舱的大部分。就连 iPhone 也有类似的问题,正如图 7–3 中右图所示。即使在月球上使用的哈苏相机是世界上最好的,我们也不能打败镜头光晕。不幸的是,它已经成为计算机图形中最常见的陈词滥调之一,被用作大喊“嘿!这不是假的电脑图像,因为它有镜头光晕!”然而,透镜耀斑确实有其用途,特别是在空间模拟领域,因为假图像经常看着假太阳。在这种情况下,无论是有意识的还是下意识的,你都会期待一些视觉上的暗示,那就是你在看非常非常非常亮的东西。这也有助于赋予图像额外的深度感。耀斑是在离用户很*的光学系统中产生的,而目标却在十亿英里之外。

Image

图 7–3左边是阿波罗 14 号在月球上的照片,右边是摩托罗拉 Xoom 的照片。

根据特定的光学系统和它们不同的内部涂层,耀斑可以采取许多不同的形式,但它们通常最终只是一堆不同大小和颜色的幽灵多边形。在下一个练习中,我们将创建一个简单的镜头光晕项目,演示如何在 3D 环境中使用 2D 图像。因为设置有很多代码,所以我在这里只强调关键的部分。您需要前往[www.apress.com](http://www.apress.com)获取完整的项目。

从几何学上来说,镜头光晕通常非常简单,因为它们是对称的。它们表现出两个主要特征:所有的镜头光晕都需要非常亮的光源,并且它们会沿着穿过屏幕中心的对角线,如图 Figure 7–4 所示。

Image

图 7–4镜头光晕是由相机镜头内明亮光源的内反射引起的。

既然耀斑图像是 2D,我们如何将它们放入 3D 空间?回到最初的例子,有弹性的正方形也是 2D 物体。但是显示它依赖于一些默认的对象映射到屏幕的方式。这里我们可以更具体一点。

还记得我在第三章中提到的透视和正投影吗?前者是我们感知物体维度的方式;当需要精确的大小和形状时,使用后者,消除透视给场景带来的失真。所以,当你画 2D 物体时,你通常会希望确保它们的视觉尺寸不会被你的世界的其他部分的 3D 所影响。

当涉及到生成镜头光晕时,你将需要一个不同形状的小集合来代表实际镜头的一些机制。六边形或五边形图像是用于改变入射光强度的虹膜图像;参见图 7–5。由于使用了各种涂层来保护镜片或过滤掉不需要的波长,它们也会呈现出不同的色彩。

Image

图 7–5六叶虹膜(戴夫·费希尔拍摄)

生成火炬集需要以下步骤:

  1. 导入各种图像。
  2. 检测源对象在屏幕上的位置。
  3. 创建一个穿过屏幕中心的虚拟向量,以容纳每件艺术品。
  4. 添加十几个或更多的图像,随机大小,颜色和透明度,分散在矢量的上下。
  5. 支持触摸拖动,在所有不同位置进行测试。

我从标准模板开始,添加了对触摸和拖动视觉效果的支持。您会注意到不再有 3D 太阳对象。现在它是一个在用户手指当前位置渲染的闪光 2D 纹理,如清单 7–2 所示。

清单 7–2。 顶级 onDrawFrame()

`    public void onDrawFrame(GL10 gl)
{
        CGPoint centerRelative = new CGPoint();
        CGPoint windowDefault = new CGPoint();
        CGSize         windowSize = new CGSize();
        float cx,cy;
        float aspectRatio

gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

DisplayMetrics display = context.getResources().getDisplayMetrics();        //1
        windowSize.width = display.widthPixels;
        windowSize.height = display.heightPixels;

cx=windowSize.width/2.0f;
        cy=windowSize.height/2.0f;

aspectRatio=cx/cy;

centerRelative.x = m_PointerLocation.x-cx;
        centerRelative.y =(cy-m_PointerLocation.y)/aspectRatio;

CT.renderTextureAt(gl, centerRelative.x, centerRelative.y, windowSize,      //2
               m_FlareSource, 3.0f, 1.0f, 1.0f, 1.0f, 1.0f);

m_LensFlare.execute(gl, windowSize, m_PointerLocation);                     //3
}`

这里有三行需要注意:

  • 第 1ff 行得到屏幕的中心,并用指针(你的手指)创建跟踪耀斑源(太阳)所需的信息。
  • 在第 2 行中,渲染了耀斑的源对象,通常是太阳。
  • 第 3 行调用绘制实际镜头光晕的助手例程。

清单 7–3 中的下一位在屏幕上绘制了一个 2D 纹理。您会发现这非常方便,并且会经常使用它在屏幕上显示文本或类似 HUD 的图形。简而言之,这将绘制一个矩形对象,就像弹性正方形一样。为了使它成为 2D,它在设置投影矩阵时使用了一个名为glOrthof()的新调用。

清单 7–3。 渲染出 2D 纹理

`public void renderTextureAt(GL10 gl, float postionX, float postionY,                //1
        CGSize windowsSize, int textureId, float size, float r, float g, float b,
            float a)
{
        float scaledX, scaledY;
        float zoomBias = .1f;

float scaledSize;

float squareVertices[] =
    {
            -1.0f, -1.0f, 0.0f,
             1.0f, -1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f,
             1.0f, 1.0f, 0.0f
    };

float textureCoords[] =
    {
                0.0f, 0.0f,
                1.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 1.0f
    };

float aspectRatio = windowsSize.height / windowsSize.width;

scaledX = (float) (2.0f * postionX / windowsSize.width);                        // 2
        scaledY = (float) (2.0f * postionY / windowsSize.height);

gl.glDisable(GL10.GL_DEPTH_TEST);                                               // 3
        gl.glDisable(GL10.GL_LIGHTING);

gl.glMatrixMode(GL10.GL_PROJECTION);                                            // 4
        gl.glPushMatrix();
        gl.glLoadIdentity();

gl.glOrthof(-1.0f, 1.0f, -1.0f * aspectRatio, 1.0f * aspectRatio, -1.0f, 1.0f); // 5

gl.glMatrixMode(GL10.GL_MODELVIEW);                                             // 6
        gl.glLoadIdentity();

gl.glTranslatef(scaledX, scaledY, 0);                                           // 7

scaledSize = zoomBias * size;                                                   // 8         gl.glScalef(scaledSize, scaledSize, 1);                                         // 9

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, makeFloatBuffer(squareVertices));
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glEnable(GL10.GL_TEXTURE_2D);                                                // 10
        gl.glEnable(GL10.GL_BLEND);
        gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_COLOR);
        gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);                                // 11
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, makeFloatBuffer(textureCoords));
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glColor4f(r, g, b, a);

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

gl.glMatrixMode(GL10.GL_PROJECTION);                                            // 12
        gl.glPopMatrix();

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glPopMatrix();
        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glEnable(GL10.GL_LIGHTING);
        gl.glDisable(GL10.GL_BLEND);
}`

所以,事情是这样的:

  • 在第 1 行中,位置是相对于纹理中心的纹理原点(以像素为单位),稍后会将其转换为归一化值。大小是相对的,需要把玩才能找到最合适的。最后的参数是颜色和 alpha。如果您不想要任何颜色,请为所有值传递 1.0。沿着这条线,你会认出我们的老朋友,顶点和纹理坐标。
  • 第 2 行根据帧的宽度和高度将像素位置转换为相对值。这些值被缩放 2 倍,因为我们的视口将是 2 个单位宽和 2 个单位高,在每个方向上从-1 到 1。这些是最终传递给glTranslatef() 的值。
  • 接下来,为了安全起见,第 3 行关闭了任意深度测试,同时关闭了照明,因为光晕必须与场景中的实际照明分开计算。
  • 既然我们要使用正投影,让我们将GL_PROJECTION矩阵重置为第 4ff 行中的单位(默认)。请记住,任何时候你想接触一个特定的矩阵,你需要提前指定哪一个。glPushMatrix()让我们修补投影矩阵,而不会弄乱事件链中的任何先前的东西。
  • 第 5 行是这个例程的核心。glOrthof()是一个新的调用,设置了正投影矩阵。实际上,它指定了一个盒子。在这种情况下,框的宽度和深度都从-1 到 1,而高度使用长宽比稍微放大一点,以补偿它是一个非正方形的显示。这就是为什么scaledXscaledY的值被乘以 2。
  • 接下来,在第 6f 行将 modelview 矩阵设置为其标识,然后在第 7 行调用glTranslatef()
  • 第 8 行决定了如何根据我们场景的视野来缩放闪光集合,随后是第 9 行执行实际的缩放。这是相对的,取决于您想要处理的放大范围。现在,缩放还没有实现,所以它保持不变。zoomBias影响所有元素,这使得一次性缩放所有元素变得很容易。
  • 第 10ff 行使用最常见的选项设置混合功能。这使得每个反射以一种非常可信的方式混合,特别是当它们开始在中心堆积时。
  • 现在在第 11ff 行,纹理被着色,装订,最后被绘制。
  • 再次强调,做一个好邻居,弹出矩阵,这样它们就不会影响其他任何东西。重置一堆其他垃圾。

注意,这是一个非常低效的例程。通常情况下,您会以一种避免所有高开销的状态更改的方式来批处理绘制操作。(类似的性能问题将在第九章中讨论。)

我为单独的光斑创建了一个Flare.java,并创建了一个LensFlare父对象来设置矢量,包含每个单独的图像,并在准备好的时候放置它们。在这一点上,清单 7–4 中LensFlare.java的主循环几乎不需要解释。它只计算耀斑向量的起点,然后枚举整个耀斑数组以执行每个实体。

清单 7–4。 对整个镜头执行循环光晕效果来自LensFlare.java

`public void execute(GL10 gl,CGSize size, CGPoint source)
{
        int i;
        float cx,cy;
        float aspectRatio;

cx=(float) (size.width/2.0f);         cy=(float) (size.height/2.0f);

aspectRatio=cx/cy;

startingOffsetFromCenterX = cx-source.x;
        startingOffsetFromCenterY = (source.y-cy)/aspectRatio;

offsetFromCenterX = startingOffsetFromCenterX;
        offsetFromCenterY = startingOffsetFromCenterY;

deltaX = (float) (2.0f * startingOffsetFromCenterX);
        deltaY = (float) (2.0f * startingOffsetFromCenterY);

for (i = 23; i >= 0; i--)
    {
            offsetFromCenterX -= deltaX * myFlares[i].getVectorPosition();
            offsetFromCenterY -= deltaY * myFlares[i].getVectorPosition();

myFlares[i].renderFlareAt(gl, m_Flares[i], offsetFromCenterX,
                    offsetFromCenterY, size, this.context);
  }
        counter++;
}`

最后,每个单独的耀斑图像必须在初始化时加载并添加到NSArray中。下面是几行代码:

`          resid = book.lensflare.R.drawable.hexagonblur;
          m_Flares[0] = myFlares[0].init(gl, context, resid, .5f, .05f-ff, 1.0f, .73f,
              .30f, .4f);

resid = book.lensflare.R.drawable.glow;
          m_Flares[1] = myFlares[1].init(gl, context, resid, 0.5f, .05f-ff, 1.0f, .73f,
              .50f, .4f);`

这个演示有 24 个这样的对象。图 7–6 显示了结果。

Image

图 7–6简单的镜头光晕

不幸的是,在镜头眩光业务中有一个大问题。如果你的光源跟在别的东西后面会怎么样?如果它是一个规则的已知实体,比如场景中心的一个圆球,就很容易识别出来。但如果是随机地点的随机物体,就变得困难多了。那么如果光源只是部分被遮挡会怎么样呢?只有当整个物体被隐藏时,反射才会变暗和闪烁。解决方案暂时留给你。

反射表面

另一个很快变得有点俗套的视觉效果,尽管仍然很酷,是部分或整个场景下面的镜面。例如,Mac-heads 看到每次他们看着 Dock 时,快乐的小图标上下跳着他们的吉格舞,实际上是在说“看这里!看这里!”下面你会看到一个微弱的小倒影。很多第三方 app 也是一样,当然是以苹果自己的设计和例子为首。参见图 7–7。

Image

图 7–7远处太阳的倒影。(没错,就是无偿插。)

谷歌刚刚开始涉足其市场,展示了一些书籍和电影图片,下面还有一些倒影。这将引入下一个主题,它是关于模板和反射的,因为这两者经常被联系在一起。在这种情况下,我们将创建一个反射表面,在我们的对象下面的一个( stage ),做一个对象的镜像,并使用模板沿着舞台的边缘剪切反射。

除了“颜色”缓冲区(即图像缓冲区)和深度缓冲区,OpenGL 还有一个叫做模板缓冲区的东西。

模板格式可以是 8 位或 1 位,通常是后者。

在 Android 中添加一个模板是一件轻而易举的事情,它将我们带回活动文件的onCreate()方法,在这里GLSurfaceView被初始化。OpenGL 表面的默认格式是RGB565,有一个 16 位深度缓冲区,没有模板缓冲区。后者可以通过下面的代码调用setEGLConfigChooser()来解决,最后一个参数指定一个 1 位模板。

glsurface view view =newglsurface view(this);

           view.setEGLConfigChooser(8,8,8,8,16,1);            view.setRenderer(new CubeRenderer(true));

从本质上讲,你可以像对其他任何东西一样对模板缓冲区进行渲染,但是在这种情况下,任何像素及其值都被用来决定如何将未来的图像渲染到屏幕上。最常见的情况是,任何后来绘制到模具区域的图像都将正常呈现,而模具区域之外的任何图像都不会呈现。当然,这些行为是可以修改的,这符合 OpenGL 的理念,即让一切都比绝大多数工程师使用的更加灵活,更不用说理解了。尽管如此,它有时还是非常方便的。我们将暂时使用简单的函数(清单 7–5)。

清单 7–5。 模板像普通屏幕对象一样生成

`    public void renderToStencil(GL10 gl)
{
        gl.glEnable(GL10.GL_STENCIL_TEST);                                     //1
        gl.glStencilFunc(GL10.GL_ALWAYS,1, 0xFFFFFFFF);                        //2
        gl.glStencilOp(GL10.GL_REPLACE, GL10.GL_REPLACE, GL10.GL_REPLACE);     //3

renderStage(gl);                                                       //4

gl.glStencilFunc(GL10.GL_EQUAL, 1, 0xFFFFFFFF);                        //5
        gl.glStencilOp(GL10.GL_KEEP, GL10.GL_KEEP,GL10.GL_KEEP);               //6
}`

所以,你建立你的模具如下:

  • 按照第 1 行中的操作启用模板。
  • 在第 2 行中,我们指定了每当有东西写入模板缓冲区时使用的比较函数。因为我们每次都清除它,所以它将全是零。函数GL_ALWAYS表示每次写入都将通过模板测试,这是我们在构造模板本身时想要的。值 1 被称为参考值,用于执行额外的测试以微调行为,但这超出了本文的范围。最终值是位*面要访问的掩码。既然我们不关心这个,那就把它们都打开吧。
  • 第 3 行指定模板测试成功或失败时要做什么。第一个参数与模板测试失败有关,第二个参数与模板通过但深度测试失败有关,第三个参数与两者都成功有关。由于我们生活在 3D 空间中,将模板测试与深度测试结合在一起可以认识到,可能存在一个测试否决另一个测试的情况。模板缓冲区使用中的一些微妙之处会变得非常复杂。在这种情况下,将三个都设置为GL_REPLACE。表 7–1 显示了所有其他允许值。
  • 第 4 行调用我们的渲染函数,就像你通常调用的那样。在这种情况下,它同时写入模板缓冲区和其中一个颜色通道,因此我们可以在新的闪亮舞台或*台上看到一些闪光。同时,在模板缓冲区中,背景将保持为零,而图像将产生大于 0 的模板像素,因此它允许图像数据稍后写入其中。
  • 第 5 行和第 6 行现在为正常使用准备缓冲区。第 5 行说,如果当前被寻址的模板像素的值是 1,保持它不变,如第 6 行所示。否则,传递片段以进行处理,就好像模板缓冲区不在那里一样(尽管如果深度测试失败,它仍可能被忽略)。因此,对于任何为 0 的模板像素,测试都将失败,传入的片段将被锁定。

Image

正如你所看到的,模板缓冲是一个非常强大的工具,有很多微妙之处。但是任何更奢侈的使用都是为尚未命名的未来书籍保留的。

现在是使用renderStage()方法的时候了,如清单 7–6 所示。

清单 7–6。 只渲染反光区域到模版缓冲区

public void renderStage(GL10 gl) {          float[] flatSquareVertices =     {             -1.0f,  0.0f, -1.0f,              1.0f,  0.0f, -1.0f, `            -1.0f,  0.0f,  1.0f,
             1.0f,  0.0f,  1.0f
    };

FloatBuffer vertexBuffer;

float[] colors=
         {
                 1.0f,   0.0f,  0.0f, 0.5f,
                 1.0f,   0.0f,  0.0f, 1.0f,
                 0.0f,   0.0f,  0.0f, 0.0f,
                 0.5f,   0.0f,  0.0f, 0.5f
         };

FloatBuffer colorBuffer;

gl.glFrontFace(GL10.GL_CW);
             gl.glPushMatrix();
             gl.glTranslatef(0.0f,-2.0f,mOriginZ);
             gl.glScalef(2.5f,1.5f,2.0f);

gl.glVertexPointer(3, GL11.GL_FLOAT,
                 0,makeFloatBuffer(flatSquareVertices));
             gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glColorPointer(4, GL11.GL_FLOAT, 0,makeFloatBuffer(colors));

gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

gl.glDepthMask(false);                                                    //1
             gl.glColorMask(true,false,false, true);                                   //2
             gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP,0, 4);                             //3
             gl.glColorMask(true,true,true,true);                                      //4
             gl.glDepthMask(true);                                                     //5

gl.glPopMatrix();
}`

  • 在第 1 行中,禁止写入深度缓冲区,第 2 行禁止绿色和蓝色通道,因此只使用红色通道。这就是反射区域获得红色小亮点的原因。
  • 现在我们可以将图像绘制到第 3 行的模板缓冲区。
  • 第 4 行和第 5 行重置了掩码。

此时,必须再次修改onDrawFrame()例程。如果你能让你的窥视者睁开眼睛,看看清单 7–7 中残酷而不加掩饰的真相。很抱歉重复了这么多前面代码的,但这比说“……在关于松鼠投石机的那一点之后加上某某行……”要容易得多

清单 7–7。 三国志onDrawFrame()

`public void onDrawFrame(GL10 gl)
{
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT |        //1
        GL10.GL_STENCIL_BUFFER_BIT);
        gl.glClearColor(0.0f,0.0f,0.0f,1.0f);

renderToStencil(gl);                                                    //2

gl.glEnable(GL10.GL_CULL_FACE);
        gl.glCullFace(GL10.GL_BACK);

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

gl.glPushMatrix();

gl.glEnable(GL10.GL_STENCIL_TEST);                                      //3
        gl.glDisable(GL10.GL_DEPTH_TEST);

//Flip the image.

gl.glTranslatef(0.0f,((float)(Math.sin(-mTransY)/2.0f)-2.5f),mOriginZ); //4
        gl.glRotatef(mAngle, 0.0f, 1.0f, 0.0f);

gl.glScalef(1.0f, -1.0f, 1.0f);                                         //5
        gl.glFrontFace(GL10.GL_CW);

gl.glEnable(GL10.GL_BLEND);                                             //6
        gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_COLOR);

mCube.draw(gl);                                                         //7

gl.glDisable(GL10.GL_BLEND);

gl.glPopMatrix();

gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glDisable(GL10.GL_STENCIL_TEST);

//Now the main image.

gl.glPushMatrix();
        gl.glScalef(1.0f, 1.0f, 1.0f);                                          //8         gl.glFrontFace(GL10.GL_CCW);

gl.glTranslatef(0.0f,(float)(1.5f(Math.sin*(mTransY)/2.0f)+2.0f),mOriginZ);

gl.glRotatef(mAngle, 0.0f, 1.0f, 0.0f);

mCube.draw(gl);

gl.glPopMatrix();

mTransY+=.075f;
        mAngle+=.4f;
}`

这是细目分类:

  • 在第 1 行中,GL_STENCIL_BUFFER_BIT被加到glClear()中,这意味着它必须在每一帧中被重建,如清单 7–6 的第 2 行所示。这实际上会创建一个模版区域,戳出一个洞,我们下一步要画出来。
  • 启用第 3 行中的模板测试。
  • 在这里,第 4 行和第 5 行绘制了反射。先向下*移一点,减去 1.5,保证在实物下面。然后就是简单的将 y 轴“缩放”到-1.0,这样就有了上下翻转的效果。在这一点上,你需要改变正面为顺时针方向;否则,您将只能看到背面。
  • 正如我们所期望的,我们希望使下面的图像半透明,而不是全亮度。在第 6f 行中,blend 被启用并使用第六章中GL_ONEGL_ONE_MINUS_SRC_COLOR的最常见混合功能。
  • 在第 7 行我们可以画出我们的对象,在这个例子中是立方体。
  • 因为触摸了 scale 以反转图像,所以在第 8 行中 scale 被重置为默认值。翻译已经用几个其他的小值进行了修改。这就把它向上移动了一点,为倒置的立方体留出了额外的空间。

现在测试:图 7–8 是你应该看到的。

Image

图 7–8使用模板创造倒影

阴影降临

阴影投射在 OpenGL 中一直是一种黑色艺术,在某种程度上仍然如此。然而,随着更快的 CPU 和 GPU,许多以前是研究生论文主题的商业技巧终于可以走出理论,进入现实世界部署的温暖光芒中。阴影投射的严格解决方案仍然是好莱坞采用的非实时渲染的领域,但在有限的条件下,基本阴影可用于全运动渲染。由于各种硬件制造商在其 GPU 中添加了阴影和照明支持,我们的 3D 世界看起来比以往任何时候都更加丰富,因为计算机图形中很少有元素能够比精心管理的阴影更具真实感。(问问任何一个好莱坞电影的灯光导演。)并且不要忘记通过使用 OpenGL ES 2 中的着色器来支持每像素,这可以让程序员微妙地为炸毁一切 3 中每个幽灵城堡的每个角落着色。

有很多方法可以投射阴影,或者至少是看起来像阴影的东西。也许最简单的是有一个预渲染的阴影斑点:一个看起来像地面上的阴影的位图,由你的对象投射。它便宜、快速,但极其有限。另一个极端是成熟的渲染任何你能看到的东西的软件,它把 GPU 当午餐吃掉。在这两者之间,你会发现阴影贴图阴影体积、投影阴影

阴影贴图

曾经,阴影投射最流行的形式之一是通过在游戏中经常使用的阴影贴图。虽然设置起来有点麻烦,更不用说描述了,但是理论非常简单…考虑到。

阴影贴图需要场景的两张快照。一个是从光线的角度,另一个是从相机的角度。根据定义,当从灯光渲染时,图像将看到被自身照亮的一切。颜色信息被忽略,但深度信息被保留,所以我们最终得到了一个可见片段及其相对距离的地图。现在从相机的角度拍一张照片。通过比较这两幅图像,我们可以发现相机看到了光线看不到的部分。这些位在阴影中。

当然,实际上要比这复杂一点。

阴影卷

阴影体积用于决定场景的哪一部分被照亮,哪一部分没有被照亮,这是通过巧妙利用模板缓冲区的某些属性来实现的。这项技术如此强大的原因在于,它允许阴影投射到任意几何形状上,而不是投影阴影(稍后讨论),后者只适用于阴影投射到*面上的简化情况。

当使用阴影体积技术渲染场景时,模板缓冲区将处于这样一种状态,其中被着色的结果图像的任何部分将具有大于零的相应模板像素,而被照亮的任何部分将具有零。参见图 7–9。

Image

图 7–9。 阴影体积显示模板缓冲区中的相应值:0 表示任何被照亮的部分,> 0 表示阴影区域

这分三个阶段完成。第一步是仅使用环境光渲染图像,以便场景的阴影部分仍然可见。接下来是只写入模板缓冲区的过程,最后一个阶段写入全光照的正常图像。然而,只有非阴影像素可以写入照亮的区域,而它们被阻止写入阴影部分,只留下原始的环境像素可见。

回到之前在反射练习中使用的神秘的glStencilOp()函数,我们现在可以利用那些奇怪的GL_INCRGL_DECR操作。GL_INCR可将模板像素的计数增加 1,而GL_DECR将计数减少 1,两种操作都在特定条件下触发。

术语阴影体积来自下面的例子:想象一下这是一个多雾的夜晚。你用一盏明亮的灯,比如你汽车的前灯,照亮薄雾。现在在光束里做些皮影戏。你仍然会看到一部分光束绕过你画得很差的爱荷华州的阴影,飘向远方。我们对那部分不感兴趣。我们想要的是光束变暗的部分,也就是你双手投下的阴影。那是阴影体积。

在你的 OpenGL 场景中,假设你有一个光源和几个遮光器。这些影子投射到身后的任何东西上,不管是球体、圆锥体还是伍德罗·威尔逊的半身像。当你从侧面看时,你会看到有阴影的物体和被照亮的物体。现在从任何一个片段画一个向量,不管有没有被照亮,到你的相机。如果片段被照亮,根据定义,向量必须穿过偶数个阴影体积的墙:一个当它进入阴影体积时,一个当它出来时(当然,忽略场景边缘上的向量可能不需要穿过任何阴影区域的特殊情况)。对于其中一个体积内的碎片,向量必须穿过奇数个壁;让它显得奇怪的那堵额外的墙来自于它自身的居住空间。

现在回到模板。生成的阴影体积看起来像任何其他几何体,但只绘制到模板上,使它们不可见,因为颜色缓冲区都已关闭。使用深度缓冲区,这样体积的壁将只在模板中渲染,如果它比真实的几何体更接*。这个技巧可以让阴影追踪任意物体的轮廓,而不必对球体或复活节岛雕像进行复杂繁琐的相交*面计算。它只是使用深度缓冲区来进行逐个像素的测试,以确定阴影结束的位置。因此,当体积被渲染到模板上时,阴影的每个“圆锥体”的每一侧都会以不同的方式影响模板。面向我们的一边将模板缓冲区中的值增加 1,而另一边将它减少。因此,在体积的另一侧被照亮的任何区域将匹配模板遮罩的一部分,其中所有像素都被设置为零,因为向量必须通过相同数量的进入和出去的面。任何处于阴影中的部分都有一个对应的模板值 1。

这就是为什么本练习从未选择阴影体积的原因。

水滴影

斑点阴影完全是一个骗局。它只是假设没有真正的直射光源,所以物体的阴影只不过是下面的一个斑点,如图 Figure 7–10 所示。如你所见,如果我们的遮光器(投射阴影的东西)是一个巨大的食人玉米煎饼,这就不太好了。

Image

图 7–10放置在所有物体下的斑点阴影纹理

投影阴影

投影阴影是动态阴影算法中“最容易”实现的,但这也意味着它们有许多限制——即投影阴影在大*面上投射阴影时效果最佳,如图 Figure 7–11 所示。此外,阴影不能投射在任意对象上。与其他方法一样,基本过程是从光线的角度和相机的角度拍摄快照。灯光的视图在*面上被压扁,涂上合适的阴影颜色(也就是深色),然后遮光器被渲染在顶部。

Image

图 7–11投影在*面上的阴影,然后被“再投影”出来戳到观众的眼睛

阴影面积通过使用向量和*面的交点计算,向量和*面从光源出发,经过每个顶点。*面上的每个点形成一个“新的”对象,然后可以像*面上的其他任何东西一样进行变换。清单 7–11 展示了这是如何编码的。

让我们再次从基本的 bouncy cube 演示开始(尽管它的大部分内容将因阴影代码而改变,但它仍将作为一个工作模板),但我们将交换不同的控制器代码。清单 7–8 涵盖了您需要首先添加的一些初始化参数。

清单 7–8。 添加到渲染器的初始化素材

`    float mSpinX=-1.0f;
    float mSpinY=0.0f;
    float mSpinZ=0.0f;

float mWorldY=-1.0f;
    float mWorldZ=-20.0f;

float mWorldRotationX=35.0f;
    float mWorldRotationY=0.0f;
    float mLightRadius=2.5f;`

这些只是设置场景的灯光,视角和动画。

清单 7–9 涵盖了onDrawFrame()方法。

清单 7–9。??onDrawFrame()投射阴影的方法

`public void onDrawFrame(GL10 gl)
{
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        gl.glClearColor(0.0f,0.0f,0.0f,1.0f);

gl.glEnable(GL10.GL_DEPTH_TEST);

updateLightPosition(gl);                                                         //1

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

gl.glTranslatef(0.0f,mWorldY,mWorldZ);                                           //2
        gl.glRotatef(mWorldRotationX, 1.0f, 0.0f, 0.0f);
        gl.glRotatef(mWorldRotationY, 0.0f, 1.0f, 0.0f);

renderStage(gl);                                                                 //3

gl.glDisable(GL10.GL_DEPTH_TEST);                                                //4

calculateShadowMatrix();                                                         //5         drawShadow(gl,true);                                                             //6

gl.glShadeModel(GL10.GL_SMOOTH);

gl.glTranslatef(0.0f,(float)(Math.sin(mTransY)/2.0)+mMinY, 0.0f);                //7

gl.glRotatef( mSpinZ, 0.0f, 0.0f, 1.0f );
        gl.glRotatef( mSpinY, 0.0f, 1.0f, 0.0f );
        gl.glRotatef( mSpinX, 1.0f, 0.0f, 0.0f );

gl.glEnable( GL10.GL_DEPTH_TEST);                                                //8
        gl.glFrontFace(GL10.GL_CCW);

mCube.draw(gl);                                                                  //9

gl.glDisable(GL10.GL_BLEND);

mFrameNumber++;

mSpinX+=.4f;                                                                     //10
        mSpinY+=.6f;
        mSpinZ+=.9f;

mTransY+=.075f;
}`

这是怎么回事?

  • 第 1 行将导致光围绕立方体旋转,动态地改变阴影。
  • 2ff 线瞄准您的眼点。
  • 我们在第三行中循环使用上一个练习中的阶段。
  • 我们需要在实际绘制阴影时禁用深度测试(第 4 行);否则,将会有各种 z 争用,产生很酷但无用的闪烁。
  • 第 5 行调用例程来生成阴影的矩阵(稍后详述),接下来是第 6 行,它实际上使用巧妙命名的方法drawShadow()来绘制阴影。
  • 第 7ff 行定位和旋转遮光器,我们的立方体。
  • 第 8f 行安全地再次打开深度测试,之后我们可以安全地在第 9 行绘制立方体。
  • 在最后一位,第 10ff 行,立方体的位置和姿态为下一轮更新。

在进入下一步之前,请查看下面来自renderStage()的描述舞台几何图形的代码片段:

        float[] flatSquareVertices =     {              -1.0f,  -0.01f, -1.0f,               1.0f,  -0.01f, -1.0f,              -1.0f,  -0.01f,  1.0f,               1.0f,  -0.01f,  1.0f     };

注意微小的负 y 值。这是一个快速解决问题的方法,称为 z-fighting ,其中来自共面对象的像素可能会也可能不会共享相同的深度值。结果是两张脸在一瞬间闪烁;A 面是最前面的,接下来,B 面的像素,现在认为自己是最前面的。(注意硬件显示的可能和模拟器不同。这也是总是在硬件上测试的另一个原因。)如果你在几乎任何实时 3D 软件中足够努力地寻找,你很可能会看到一些 z 在背景中战斗。参见图 7–12。

Image

图 7–12站台和影子之间的 Z 战斗

现在我们开始真正有趣的东西,实际上计算和绘制阴影。清单 7–10 展示了矩阵是如何生成的,而清单 7–11 绘制了被挤压的阴影。

清单 7–10。计算影子矩阵

`    public void calculateShadowMatrix()
{
        float[] shadowMat_local =
    {
            mLightPosY,   0.0f,   0.0f,    0.0f,
            -mLightPosX,  0.0f,   -mLightPosZ,            -1.0f,
                          0.0f,   0.0f,    mLightPosY,     0.0f,
                          0.0f,   0.0f,    0.0f,           mLightPosY
    };

for (int i=0;i<16;i++)
     {
                 mShadowMat[i] = shadowMat_local[i];
      }
}`

这实际上是更一般化矩阵的简化版本,如下所示:

Image

dotp是光线矢量与*面法线的点积,l 是光线的位置,p 是*面(我代码中的“舞台”)。由于我们的*台在 x/z *面上,*面方程看起来像 p=[0,1,0,0],否则 p[0]=p[2]=p[3]=0。这意味着矩阵中的大多数项都被归零。一旦生成了矩阵,将其乘以现有的 modelview 矩阵,就可以将这些点与其他所有东西一起映射到您的本地空间。明白了吗?我也不知道,但这似乎很有效。

清单 7–11 为阴影执行所有需要的变换,并通过遮光器本身渲染立方体。

清单 7–11。??drawShadow()套路

`public void drawShadow(GL10 gl,boolean wireframe)
{
        FloatBuffer vertexBuffer;

gl.glPushMatrix();

gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glRotatef(mWorldRotationX, 1.0f, 0.0f, 0.0f);                             //1
        gl.glRotatef(mWorldRotationY, 0.0f, 1.0f, 0.0f);         gl.glMultMatrixf(makeFloatBuffer(mShadowMat));                               //2

//Place the shadows.

gl.glTranslatef(0.0f,(float)(Math.sin(mTransY)/2.0)+mMinY, 0.0f);            //3

gl.glRotatef((float)mSpinZ,0.0f,0.0f,1.0f);
        gl.glRotatef((float)mSpinY,0.0f,1.0f,0.0f);
        gl.glRotatef((float)mSpinX,1.0f,0.0f,0.0f);

//Draw them.

if(mFrameNumber>150)                                                         //4
                mCube.drawShadow(gl,true);
        else
                mCube.drawShadow(gl,false);

gl.glDisable(GL10.GL_BLEND);

gl.glPopMatrix();
}`

  • 首先将所有东西旋转到世界空间,就像我们之前在第一行做的那样。
  • 第 2 行将阴影矩阵与当前模型视图矩阵相乘。
  • 线条 3ff 对阴影执行与实际立方体相同的变换和旋转。
  • 在第 4ff 行,立方体渲染了自己的阴影。这两个调用将导致阴影在实线和线框之间翻转,如图 Figure 7–13 所示。

清单 7–12 涵盖了Cube.java中的drawShadow()方法。

清单 7–12。 画影子

`public void drawShadow(GL10 gl,boolean wireframe)
{
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);                                    //1
        gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);

gl.glEnable(GL10.GL_BLEND);                                                      //2
        gl.glBlendFunc(GL10.GL_ZERO,GL10.GL_ONE_MINUS_SRC_ALPHA);

gl.glColor4f(0.0f,0.0f,0.0f,0.3f);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0,makeFloatBuffer(mVertices));              //3         if(wireframe)
    {
            gl.glLineWidth(3.0f);                                                        //4
            gl.glDrawElements(GL10.GL_LINES, 6 * 3,GL10.GL_UNSIGNED_BYTE, mTfan1);
            gl.glDrawElements(GL10.GL_LINES, 6 * 3,GL10.GL_UNSIGNED_BYTE, mTfan2);
    }
        else
    {
            gl.glDrawElements(GL10.GL_TRIANGLE_FAN,63,GL10.GL_UNSIGNED_BYTE,mTfan1);
            gl.glDrawElements(GL10.GL_TRIANGLE_FAN,6
3,GL10.GL_UNSIGNED_BYTE,mTfan2);
    }
}`

这将绘制一个线框阴影,以显示它是如何组成的,或者更传统的实体模型。

  • 我们首先关闭第一行中的颜色和法线数组,因为这里不需要它们。
  • 混合在 2ff 行中被激活,所以 0.3 的 alpha 值将使阴影不再是纯黑色。
  • 在这里的第三行中,立方体自己的顶点被重用,所以没有必要为阴影使用特殊的几何图形。这意味着,即使是最复杂的模型,你也可以得到非常精确的表示。
  • 第 4 行显示线框代码,该行设置为 3 像素宽,使用GL_LINES类型而不是GL_TRIANGLE_FAN调用glDrawElements()

现在是时候更新灯光的位置了,如清单 7–13 中的所示。

清单 7–13。 更新灯光的位置

`private void updateLightPosition(GL10 gl)
{
        mLightAngle +=1.0f;                //in degrees

mLightPosX   = (float) (mLightRadius * Math.cos(mLightAngle/57.29f));
         mLightPosY   = mLightHight;
         mLightPosZ   = (float) (mLightRadius * Math.sin(mLightAngle/57.29f));

mLightPos[1] = mLightPosY;

mLightPos[0]=mLightPosX;
         mLightPos[2]=mLightPosZ;

gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, makeFloatBuffer(mLightPos)); }`

这将在每次刷新时将灯光的位置更新一度。y 值是固定的,所以光线沿着它在 x/z *面上的小轨道行进。

编译完成后,您是否看到类似于 Figure 7–13 的内容?

Image

图 7–13左侧和中间图像有实影;右边是线框。

又有什么能阻止你拥有多盏灯呢?参见图 7–14,两盏灯并排。

Image

图 7–14多灯魔方

在所有这些图像中,背景都是黑色的。改变背景的颜色并运行。图 7–15 中发生了什么?

Image

图 7–15惊喜!阴影不会被裁剪到*台上。

这里发生的事情是,我们在*台上裁剪阴影时作弊了。由于背景是黑色的,在*台上渲染的那部分阴影是不可见的。但是现在随着背景变亮,可以看到完整的阴影。如果你首先需要一个浅色背景会怎样?简单——只需用模板夹住*台。

总结

在这一章中,我们讲述了一些额外的技巧来增加 OpenGL ES 场景的真实感。首先是帧缓冲对象,它允许你绘制多个 OpenGL 帧并将它们合并在一起。接下来是可以给户外场景增加视觉戏剧的镜头光晕,随后是苹果在包括 CoverFlow 在内的许多 UI 设计中大量使用的反射。我们以使用阴影投影在背景上投射阴影的许多方法中的一种来结束。接下来,这些技巧中的一些将被应用到我们的小太阳系项目中。

八、把这一切放在一起

一个人的一生,即使完全奉献给了天空,也不足以研究如此庞大的课题。

——罗马哲学家塞内加

好了,现在我们已经一路走到了第八章。此时,我们可以将从练习中学到的东西整合到一个更完整的太阳系模型中(尽管它仍然缺少彗星、杀手小行星、中微子和外海王星物体)。然后,我希望你会说,“哇!这有点酷!”

这一章的代码非常多,因为该模型需要大量新的例程和对现有项目的修改。和第七章中的一些清单一样,我不会展示整个代码文件,因为它们很长,为了避免重复,或者只是为了早点睡觉(天哪,现在是凌晨 2:45);因此,我们鼓励您从 Apress 站点获取完整的项目,以及任何必要的数据文件,以确保您拥有完整的功能示例。我总是说,完成一套。

一些新的技巧也将被抛出,比如如何整合标准的 Android 小工具和四元数的使用。请注意,尽管下面的许多代码都是基于前面的练习,但可能需要一些小的调整来将其集成到更大的包中,因此不幸的是,这不会是简单的剪切和粘贴情况。

重访太阳系

如果你想自己填写代码,我建议获取太阳系模型的第五章变体,而不是第七章变体,后者仅用于在 3D 对象上显示动态纹理,在这里不会以那种方式使用。

第一个练习是在节目中添加一些导航元素,这样你就可以在地球上移动视点。

但是首先,我们需要调整模型的大小,以使演示更加真实。就目前而言,地球看起来只有太阳的三分之一大,距离我们只有几千英里。考虑到这是北加利福尼亚的一个宜人的夏日,而且地球一点也不像烧焦的煤渣,我敢打赌这个模型是错误的。好吧,让我们来弥补吧。这将在你们太阳系控制器的initGeometry()方法中完成。当我们这样做的时候,m_Eyeposition的类型将被改变,以将其升级为一个为 3D 操作定制的稍微更加对象化的对象。新的例程在清单 8–1 中。当你在太阳表面的时候,确保添加一个纹理;否则,讨厌的事情可能会发生。

清单 8–1。 为太阳系调整天体大小

`        private void initGeometry(GL10 gl)
    {
            // Let 1.0=1 million miles.
            // The sun's radius=.4.
            // The earth's radius=.04 (10x larger to make it easier to see).

m_Eyeposition[X_VALUE] = 0.0f;
            m_Eyeposition[Y_VALUE] = 0.0f;
            m_Eyeposition[Z_VALUE] = 93.25f;

m_Earth = new Planet(48, 48, .04f, 1.0f, gl, myAppcontext, true,
                            book.SolarSystem.R.drawable.earth);
            m_Earth.setPosition(0.0f, 0.0f, 93.0f);

m_Sun = new Planet(48, 48, 0.4f, 1.0f, gl, myAppcontext, false, 0);
            m_Sun.setPosition(0.0f, 0.0f, 0.0f);
    }`

我们模型的比例设置为 1 单位= 100 万英里(1.7 米公里或 8.3 米弗隆,或 3.52e+9 肘)。太阳的半径为 400,000 英里,用这些单位表示就是 0.4 英里。这意味着地球的半径将会是 0.004,但是我已经把它增加了 10 倍,达到 0.04,使它更容易处理。因为地球的默认位置是沿着+z 轴,让我们把眼睛的位置放在地球的正后方,只有 25 万英里远,在“93.25”在太阳系物体的execute方法中,去掉glRotatef(),这样地球现在将保持固定。这让事情暂时变得简单多了。将视野从 50 度改为 30 度;另外,将setClipping中的zFar设置为 2000(处理未来对象)。您最终应该得到类似于 Figure 8–1 的东西。因为从我们的角度来看,太阳实际上是在地球的后面,所以我调高了SS_FILLLIGHT1的镜面照明。

Image

图 8–1微型屏幕上的我们的家

“一切都好,代码男孩!”你一定在小声嘀咕。“但现在我们被困在太空了!”没错,这意味着下一步是添加导航元素。这意味着(提示戏剧性的音乐)我们将添加四元数

这些四元数到底是什么东西?

1843 年 10 月 16 日,在都柏林,爱尔兰数学家威廉·哈密顿爵士正在皇家运河边散步,突然数学灵感闪现。他一直在研究如何对空间中的两点进行有意义的乘法和除法运算,突然在脑海中看到了四元数的公式:。I2=j2=k2=ijk= 1。印象深刻吧。

他是如此的兴奋,以至于他无法抗拒将它刻在他刚刚来到的 Brougham 桥的石雕上的诱惑(毫无疑问,它位于“Eamon loves Fiona,1839”或“Patrick O'Callahan rulz!”).这种见解直接衍生出看待物理学和几何学的全新方式。例如,电磁理论中的经典麦克斯韦方程完全是通过使用四元数来描述的。随着处理类似情况的新方法的出现,四元数被搁置一旁,直到 20 世纪后期,它们在 3D 计算机图形学、阿波罗飞船到月球的导航以及其他严重依赖空间旋转的领域中发挥了重要作用。由于其紧凑的性质,它们可以描述方向向量,从而比标准的 3×3 矩阵更有效地描述 3D 旋转。不仅如此,它们还提供了一种更好的方法,将一系列旋转串联起来。那么,这意味着什么呢?

在第二章中,我们介绍了使用矩阵的传统 3D 变换数学。如果你想绕 z 轴旋转一个对象 32°,你可以通过命令glRotatef(32,0,0,1)指示 OpenGL ES 执行旋转。对于 x 轴和 y 轴也将执行类似的命令。但是,如果你想要一种时髦的旋转,就像飞机向左倾斜时那样,那该怎么办呢?在glRotatef()格式中是如何描述的?使用更传统的方法,您将为三次旋转生成单独的矩阵,然后按照偏航(绕 y 轴旋转)、俯仰(绕 x 轴旋转)和绕 z 轴滚动的顺序将它们相乘。仅仅针对一个方向就需要大量的数学计算。但是,如果这是一个飞行模拟器,你的倾斜动作将不断更新新的滚动和航向,增量。这意味着你必须每次计算三个矩阵,计算自上一帧以来轨迹的增量,而不是某个起点的绝对值。

在计算机的早期,当浮点计算非常昂贵,并且由于性能原因经常调用快捷方式时,舍入误差是常见的,并且可能会随着时间的推移而增加,导致当前的矩阵“不合适”。然而,四元数被拯救了,因为它们有几个非常引人注目的性质。

首先,四元数可以表示物体在空间中的旋转,大致相当于glRotate()的工作方式,但使用分数轴值。这不是一个直接的一对一的关系,因为你仍然需要做一些数学上的事情来转换四元数和姿态。

第二个也是更重要的性质来源于这样一个事实,即球面上的弧可以用两个四元数来描述,每个端点一个四元数。并且圆弧上它们之间的任何一点也可以用一个四元数来描述,只需使用球面几何插值一个端点到另一个端点的距离,如图 Figure 8–2 所示。也就是说,如果你正在通过一个 60°的圆弧,你可以沿着圆弧的三分之一找到一个中间四元数,比如说,从起点开始 20°。在下一帧中,如果你要跳到 20.1,你只需在你的当前四元数上增加一点点弧度,而不必经历每次生成三个矩阵并将它们相乘的繁琐过程。这个过程叫做 slerping,其中sler RP代表球面线性插补。因为轴/角度对不像使用矩阵时那样依赖于所有先前轴/角度对的累积和,而是依赖于瞬时值,所以前者不会导致误差累积。

Image

图 8–2。 一个中间四元数;球面上的问题 1.5 可以由另外两个 Q1 和 Q2 插值得到

Slerp 用于在从一个点移动到另一个点时提供视点的“相机”的*滑动画。它可以是飞行模拟器、太空模拟器的一部分,也可以是赛车游戏中追逐车的视图。当然,它们也用于实际的飞行导航系统。

现在有了这些背景,我们将使用四元数来帮助移动地球。

在 3D 中移动物体

由于我们目前没有制作地球的动画,我们需要一种方法来移动它,这样我们就可以从各个角度研究它。考虑到这一点,由于地球是我们感兴趣的目标,我们将设置一种情况,在这种情况下,通过挤压和移动手势,视点将有效地悬停在地球上。

第一步是添加手势识别器,这是通过 Android 的onTouchEvent()调用实现的。你需要同时支持挤压和拖动功能。捏是放大和缩小,而*移让你拖动你下面的星球,始终保持它的中心。更复杂的动作,如动量滑动,或“甩”,留给你来实现,不幸的是,这可能会有点乱。

代码的结构略有不同。传统上作为GLSurfaceView.Renderer实现的核心模块现在是一个叫做SolarSystemViewGLSurfaceView子类。渲染器现在是新的SolarSystem对象。前者主要作为收缩和拖动事件的事件接收器,而后者处理主更新循环,并作为任何太阳系类型对象的容器。

在新的SolarSystemView中,我们将只需要捏和*移手势。您使用onTouchEvent()来处理所有触摸事件,初始化一些值,并决定您是在执行挤压还是拖动功能。向视图控制器的onTouchEvent()方法添加清单 8–2。

清单 8–2。 处理捏拖事件

`        public boolean onTouchEvent(MotionEvent ev)
    {
            boolean retval = true;

switch (ev.getAction() & MotionEvent.ACTION_MASK)
        {
                case MotionEvent.ACTION_DOWN:
                    m_Gesture = DRAG;                                                   //1
                    break;

case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_POINTER_UP:
                    m_Gesture = NONE;                                                   //2
                    m_LastTouchPoint.x = ev.getX();
                    m_LastTouchPoint.y = ev.getY();
                    break;

case MotionEvent.ACTION_POINTER_DOWN:
                    m_OldDist = spacing(ev);                                            //3

midPoint(m_MidPoint, ev);
                    m_Gesture = ZOOM;
                    m_LastTouchPoint.x = m_MidPoint.x;
                    m_LastTouchPoint.y = m_MidPoint.y;
                    m_CurrentTouchPoint.x=m_MidPoint.x;
                    m_CurrentTouchPoint.y=m_MidPoint.y;

break;

case MotionEvent.ACTION_MOVE:
                    if (m_Gesture == DRAG)                                              //4
                {    
                        retval = handleDragGesture(ev);
                }
                    else if (m_Gesture == ZOOM)
                {
                        retval = handlePinchGesture(ev);
                }
                    break;
        }

return retval;
    }`

这是细目分类:

  • 第一部分处理触摸显示屏的手指,或多点触摸事件的第一个手指。将运动类型m_Gesture初始化为DRAG
  • 第二部分处理运动何时完成。
  • 第三部分介绍缩放功能。m_MidPoint用于确定放大到屏幕上的哪个点。这里不需要这样做,因为我们将只放大屏幕中央的地球,但这仍然是很好的参考代码。
  • 最后,在第四部分中,调用正确的手势动作。

接下来我们需要添加两个处理程序,handleDragGesture()handlePinchGesture(),如清单 8–3 所示。

清单 8–3。 手势识别器的两个处理程序

`final PointF m_CurrentTouchPoint = new PointF();
        PointF m_MidPoint = new PointF();
        PointF m_LastTouchPoint = new PointF();
        static int m_GestureMode = 0;
        static int DRAG_GESTURE = 1;
        static int PINCH_GESTURE = 2;

public boolean handleDragGesture(MotionEvent ev)
    {
            m_LastTouchPoint.x = m_CurrentTouchPoint.x;            
            m_LastTouchPoint.y = m_CurrentTouchPoint.y;

m_CurrentTouchPoint.x = ev.getX();
            m_CurrentTouchPoint.y = ev.getY();

m_GestureMode = DRAG_GESTURE;
            m_DragFlag = 1;

return true;
    }

public boolean handlePinchGesture(MotionEvent ev)
    {
            float minFOV = 5.0f;
            float maxFOV = 100.0f;
            float newDist = spacing(ev);

m_Scale = m_OldDist/newDist;

if (m_Scale > m_LastScale)
        {
            m_LastScale = m_Scale;
        }
            else if (m_Scale <= m_LastScale)         {
                m_LastScale = m_Scale;
        }

m_CurrentFOV = m_StartFOV * m_Scale;
            m_LastTouchPoint = m_MidPoint;
            m_GestureMode = PINCH_GESTURE;

if (m_CurrentFOV >= minFOV && m_CurrentFOV <= maxFOV)
        {
                mRenderer.setFieldOfView(m_CurrentFOV);
                return true;
        }
            else
                return false;
    }`

这两个都很基本。handleDragGesture()设置跟踪当前和先前的触摸点,在确定拖动操作的速度时使用。两者之间的差值越大,屏幕的动画应该越快。handlePinchGesture()对缩放操作执行相同的操作。m_OldDistnewDist是两个夹指之间以前和新的距离。这种差异决定了视野的变化程度。压缩图形会放大,而展开图形会缩小到最大 100 度。

然后手势在onDrawFrame()方法中被处理,如清单 8–4 所示。

清单 8–4处理新的挤压和拖动状态

`        public void onDrawFrame(GL10 gl)
    {
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
            gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

if (m_GestureMode == PINCH_GESTURE && m_PinchFlag == 1)                   //1
        {
                setClipping(gl, origwidth, origheight);
                m_PinchFlag = 0;
        }
            else if (m_GestureMode == DRAG_GESTURE && m_DragFlag == 1)                //2
        {
                setHoverPosition(gl, 0, m_CurrentTouchPoint, m_LastTouchPoint, m_Earth);

m_DragFlag = 0;
        }

execute(gl);
    }`

在第一部分中,仅当通过m_PinchFlag检测到并标记了新的手势时,才处理挤压,该手势在处理后复位。如果不这样做,缩放将在每次连续调用onDrawFrame()时继续。每次使用m_FieldOfView值通过setClipping()更新视见体,因为该机制实际上决定了视图的放大倍数。第二部分同样适用于拖动手势。在这种情况下,setHoverPosition()是用当前和以前的触摸点调用的。它还有一个通过m_DragFlag的开关,关闭任何进一步的拖动处理,直到检测到新的事件。否则,即使你的手指没有移动,你的观点也会发生偏移。

如果您想立即看到缩放操作,请注释掉前面清单中的行setHoverPosition(),然后编译并运行。

你应该能够放大和缩小地球模型,如图图 8–3 所示。

Image

图 8–3。 使用捏手势放大和缩小

现在我们要做旋转支持,包括那些四元数的东西。这可能是迄今为止最复杂的练习。我们将需要一些辅助程序来瞄准你的视点,并在地球周围的“轨道”上移动它。所以,让我们从顶部开始,一步步往下。清单 8–5 是“悬停模式”的核心

清单 8–5围绕地球设定新的悬停位置

public void setHoverPosition(GL10 gl, int nFlags, PointF location,                PointF prevLocation, Planet m_Planet)     {         double dx; `double dy;
        Quaternion orientation = new Quaternion(0, 0, 0, 1.0);
        Quaternion tempQ;
        Vector3 offset = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 objectLoc = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 vpLoc = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 offsetv = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 temp = new Vector3(0.0f, 0.0f, 0.0f);
        float reference = 300.0f;
        float scale = 2.0f;
        float matrix3[][] = new float[3][3];
        boolean debug = false;

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

orientation = Miniglu.gluGetOrientation(); //1

vpLoc.x = m_Eyeposition[0];                                                    //2
        vpLoc.y = m_Eyeposition[1];
        vpLoc.z = m_Eyeposition[2];

objectLoc.x = m_Planet.m_Pos[0];                                               //3
        objectLoc.y = m_Planet.m_Pos[1];
        objectLoc.z = m_Planet.m_Pos[2];

offset.x = (objectLoc.x - vpLoc.x);                                            //4
        offset.y = (objectLoc.y - vpLoc.y);
        offset.z = (objectLoc.z - vpLoc.z);

offsetv.z = temp.Vector3Distance(objectLoc, vpLoc);                            //5

dx = (double) (location.x - prevLocation.x);
        dy = (double) (location.y - prevLocation.y);

float multiplier;
        multiplier = origwidth / reference;

gl.glMatrixMode(GL10.GL_MODELVIEW);
        // Rotate around the X-axis.

float c, s;                                                                    //6
        float rad = (float) (scale * multiplier * dy / reference)/2.0;

s = (float) Math.sin(rad * .5);                         
        c = (float) Math.cos(rad * .5);         temp.x = s;
        temp.y = 0.0f;
        temp.z = 0.0f;

Quaternion tempQ1 = new Quaternion(temp.x, temp.y, temp.z, c);

tempQ1 = tempQ1.mulThis(orientation);

// Rotate around the Y-axis.

rad = (float) (scale * multiplier * dx / reference);                        //7

s = (float) Math.sin(rad * .5);
        c = (float) Math.cos(rad * .5);

temp.x = 0.0f;
        temp.y = s;
        temp.z = 0.0f;

Quaternion tempQ2 = new Quaternion(temp.x, temp.y, temp.z, c);

tempQ2 = tempQ2.mulThis(tempQ1);

orientation=tempQ2;

matrix3 = orientation.toMatrix();            //8

matrix3 = orientation.tranposeMatrix(matrix3);                              //9
        offsetv = orientation.Matrix3MultiplyVector3(matrix3, offsetv);

m_Eyeposition[0] = (float)(objectLoc.x + offsetv.x);                        //10
        m_Eyeposition[1] =  (float)(objectLoc.y + offsetv.y);
        m_Eyeposition[2] =  (float)(objectLoc.z + offsetv.z);

lookAtTarget(gl, m_Planet);                                                 //11
    }`

我打赌你想知道这里发生了什么?

  • 首先,我们从稍后将创建的新辅助类中获取缓存的四元数。四元数是我们的视点在第 1 行的当前方向,我们需要它和视点在第 2 行太阳系物体的 xyz 位置。

  • 第 3ff 行获取目标的位置。在这种情况下,目标仅仅是地球。有了这些,我们需要找到我们的视点从地球中心的偏移,然后计算这个距离,如第 4ff 行所示。

  • 第 5 行获取了前一次和当前拖动的屏幕坐标,因此我们知道自上次以来我们移动了多少。

  • Lines 6ff create a fractional rotation in radians for each new position of the drag operation around the X-axis. This is then multiplied by the actual orientation quaternion (recovered in line 1) to ensure that the new orientation from each touch position is preserved. The 2.0 divisor scales back the vertical motions; otherwise, they'd be much to fast. This represents the cumulative rotations of the eye point. The three values of scale, multiplier, and reference are all arbitrary. Scale is fixed and was used for some fine-tuning to ensure things moved at just the right speed that ideally will match that of your finger. The multiplier is handy for orientation changes because it is a scaling factor that is based on the screen's current width and a reference value that is also arbitrary.

    另一个封装围绕 Y 轴旋转的四元数以非常相似的方式在第 7ff 行中生成。最后一次旋转时,该值将与前一个值相乘。第 8 行将它转换成传统的矩阵。

  • 第 9f 行使用矩阵对偏移值的转置来到达空间中的新位置,并存储在m_Eyeposition.中,因为我们要从地球的局部坐标到世界坐标,我们进行转置,实际上是反转操作。

  • 即使我们的视点被移动到了一个新的位置,我们仍然需要将它重新对准悬停目标,地球,就像第 11 行中通过lookAtTarget()所做的那样。

现在,我们需要创建一些前面提到的助手例程,它们将有助于把所有的事情都联系在一起。

在普通的 OpenGL 中,我提到过一个叫做 GLUT 的工具库。不幸的是,在撰写本文时,还没有完整的 Android 库,尽管有一些不完整的版本。我已经把它们放到了一个名为Miniglu.java的文件中,可以从这个项目的网站上获得。

注意: Android 在android.opengl.GLU有一个非常小但是官方的 GLU 程序套件,但是它没有我需要的所有东西。

清单 8–6 包含了gluLookAt(),的 Miniglu 版本,这是一个非常有用的工具,正如它所说的:瞄准你的视角。你传递给它你的视点的位置,你想看的东西,和一个向上的向量来指定滚动的角度。自然,直线上升就等于没有滚动。但是你还是需要供应。

清单 8–6。 gluLookAt看东西

`static Quaternion m_Quaternion = new Quaternion(0, 0, 0, 1);

public static void gluLookAt(GL10 gl, float eyex, float eyey, float eyez,
            float centerx, float centery, float centerz, float upx, float upy, float upz)
{
        Vector3 up = new Vector3(0.0f, 0.0f, 0.0f);                           //1
        Vector3 from = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 to = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 lookat = new Vector3(0.0f, 0.0f, 0.0f);
        Vector3 axis = new Vector3(0.0f, 0.0f, 0.0f);
            float angle;

lookat.x = centerx;                                               //2
        lookat.y = centery;
        lookat.z = centerz;

from.x = eyex;
        from.y = eyey;
        from.z = eyez;

to.x = lookat.x;
        to.y = lookat.y;
        to.z = lookat.z;

up.x = upx;
        up.y = upy;
        up.z = upz;

Vector3 temp = new Vector3(0, 0, 0);                                     //3
        temp = temp.Vector3Sub(to, from);
        Vector3 n = temp.normalise(temp);

temp = temp.Vector3CrossProduct(n, up);
        Vector3 v = temp.normalise(temp);

Vector3 u = temp.Vector3CrossProduct(v, n);
        float[][] matrix;

matrix = temp.Matrix3MakeWithRows(v, u, temp.Vector3Negate(n));
        m_Quaternion = m_Quaternion.QuaternionMakeWithMatrix3(matrix);           //4

m_Quaternion.printThis("GluLookat:");

axis = m_Quaternion.QuaternionAxis();
        angle = m_Quaternion.QuaternionAngle();

gl.glRotatef((float) angle * DEGREES_PER_RADIAN, (float) axis.x,
                (float) axis.y, (float) axis.z);                                  //5     }`

事情是这样的:

  • 如前所述,我们需要获取点或向量来完整描述我们和目标在空间中的位置,如第 1ff 行所示。上向量是你的视点的局部向量,它通常只是一个指向 y 轴的单位向量。如果你想做银行卷,你可以修改这个。Vector3对象是与这个项目相关的小型数学库的一部分。然而,存在许多这样的库。
  • 在第 2ff 行中,以离散值形式传递的项被映射到Vector3对象,然后这些对象可以用于矢量数学库。为什么不用矢量呢?官方的 GLUT 库不使用矢量对象,所以这符合现有的标准。
  • 第 3ff 行生成三个新向量,其中两个使用叉积。这确保了一切都是标准化的,并且轴是方形的。
  • gluLookAt()生成矩阵的一些例子。这里,用四元数来代替。在第 4 行中,四元数是由我们的新向量创建的,用于获取glRotatef()喜欢使用的轴/角度参数,如第 5 行所示。注意,生成的四元数是通过一个全局缓存的,如果需要通过gluGetOrientation()获取瞬时姿态,可以稍后获取。很笨拙,但很管用。在现实生活中,你可能不想这样做,因为它假设你的整个世界只有一个单一的观点。实际上,你可能想要不止一个——例如,如果你想要两个同时显示,从两个不同的有利位置显示你的对象。

最后,我们可以看看生成的图像。你现在应该能够随心所欲地旋转我们这个公*的小世界了(见图 8–4)。有时出现的小黄点是太阳。

Image

图 8–4。 悬停模式让你随意旋转地球。

这就是今天练习的第一部分。还记得第七章里那些镜头光晕的东西吗?现在我们可以使用它们了。

添加一些光斑

从第七章的中,从镜头光晕练习中获取三个源文件,并将它们与插图一起添加到您的项目中。这些将是CreateTexture.java助手库,Flare.java用于每个反射,以及LensFlare.java。这也需要对渲染器对象进行一些实质性的调整,主要是在执行例程中。

像镜头光晕效应这样的东西有各种各样的小问题需要解决。也就是说,如果耀斑的源物体,比如太阳,在地球后面,耀斑本身就会消失。此外,请注意,它不会立即消失,但实际上会淡出。在渲染光晕本身之前,需要添加几个新的工具例程。

首先确保在您的onSurfaceCreated()处理程序中初始化 LensFlare 对象:

int resid; resid = book.SolarSystem.R.drawable.gimpsun3; m_FlareSource = CT.createTexture(gl, myAppcontext, true, resid); m_LensFlare.createFlares(gl, myAppcontext);

现在是时候将任何图像工具转储到它们自己的例程中了。它叫做CreateTexture.java。这将有助于支持前面的呼叫。.png文件可以是您想要的任何文件,它将替换当前的 3D 太阳模型。我们希望这样做,这样我们就可以绘制一个太阳的*面位图,在这个位置上,球面模型通常会像过去一样进行渲染。原因是我们可以精细地控制我们的恒星的外观,使它更接*于肉眼可能看到的样子。这个明显的黄色球,虽然在技术上更准确,但看起来并不正确,因为任何光学接收器都会添加各种各样的扭曲、反射,和高光(例如,镜头眩光)。可以使用着色器来对眼睛的光学进行数学建模,但目前对于一个模糊的球状物体来说,这是一个很大的工作量。如果你愿意,你可以从 Apress 网站下载我自己的作品。或者只是复制一些适合自己口味的东西。图 8–5 是我正在使用的。足够有趣的是,这个图像愚弄了我自己的眼睛,足以让我的大脑认为我实际上正在看着一个太亮的东西,因为当我盯着它时,它会导致各种各样的眼睛疲劳。

这使用了一种叫做布告板的技术,这种技术采用了一种*面 2D 纹理,并使其对准观众,无论他们在哪里。它允许复杂和相当随机的有机物体(我想是被称为的东西)在只使用简单纹理的情况下被轻易地描绘出来。随着视点的变化,广告牌对象会旋转以进行补偿。

Image

图 8–5。 太阳图像用来给出看起来更真实的辉光

我称之为LensFlare.java的镜头光晕管理器和单个 Flare.java 物体都需要修改。对于LensFlare.java的执行方法,我添加了两个新参数。execute()现在应该是这个样子:

    public void execute(GL10 gl,CGSize size, CGPoint source, float scale, float alpha)

新的scale参数是一个单一的值,它将增加或减少整个耀斑链的大小,当你放大或缩小场景时需要,而alpha用于在太阳开始滑向地球后面时使整个耀斑变暗。这两个参数同样需要添加到单个 flare 对象的 execute 方法中,然后用于旋转传递给CreateTexture's renderTextureAt()方法的大小和 alpha 参数,如下所示:

    public void renderFlareAt(GL10 gl, int textureID, float x, float y, CGSize size,         Context context, float scale, float alpha)     {         CreateTexture ct = new CreateTexture();         ct.renderTextureAt(gl, x, y, 0f, size, textureID, m_Size*scale,             m_Red*alpha, m_Green*alpha, m_Blue*alpha, m_Alpha);     }

下一个清单,清单 8–7,包含了另外两个 Miniglu 调用。首先是gluGetScreenLocation(),它返回 3D 对象在屏幕上的 2D 坐标。它只不过是gluProject()的前端,它将 3D 点映射或投影到它的视口。尽管这些可能是“固定的”程序,但看看它们是如何工作的仍然是有启发性的。它们在这里被用来获得太阳的位置,以放置 2D 比尔登上艺术品。后来,它们可以用来放置天空中的其他 2D 项目,如星座名称。

清单 8–7。gluProject()``gluGetScreenCoords()

`public static boolean gluProject(float objx, float objy, float objz,
            float[] modelMatrix, float[] projMatrix, int[] viewport,float[] win)
    {
        float[] in = new float[4];
        float[] out = new float[4];

in[0] = objx;                                                      //1 in[1] = objy;
        in[2] = objz;
        in[3] = 1.0f;

gluMultMatrixVector3f (modelMatrix, in, out);                       //2

gluMultMatrixVector3f (projMatrix, out, in);

if (in[3] == 0.0f)
        in[3] = 1.0f;

in[0] /= in[3];
        in[1] /= in[3];
        in[2] /= in[3];

/* Map x, y and z to range 0-1 */

in[0] = in[0] * 0.5f + 0.5f;                                            //3
        in[1] = in[1] * 0.5f + 0.5f;
        in[2] = in[2] * 0.5f + 0.5f;

/* Map x,y to viewport */

win[0] = in[0] * viewport[2] + viewport[0];
        win[1] = in[1] * viewport[3] + viewport[1];
        win[2] = in[3];

return (true);
    }

public static void gluGetScreenLocation(GL10 gl, float xa, float ya, float za,
        float screenRadius, boolean render, float[] screenLoc)
    {
        float[] mvmatrix = new float[16];
        float[] projmatrix = new float[16];
        int[] viewport = new int[4];
        float[] xyz = new float[3];

GL11 gl11 = (GL11) gl;

gl11.glGetIntegerv(GL11.GL_VIEWPORT, viewport, 0);                      // 4
        gl11.glGetFloatv(GL11.GL_MODELVIEW_MATRIX, mvmatrix, 0);
        gl11.glGetFloatv(GL11.GL_PROJECTION_MATRIX, projmatrix, 0);

gluProject(xa, ya, za, mvmatrix, projmatrix, viewport,xyz);

xyz[1]=viewport[3]-xyz[1];                                               //5                 
        screenLoc[0] = xyz[0];
        screenLoc[1] = xyz[1];
        screenLoc[2] = xyz[2];
    }`

让我们更仔细地检查一下代码:

  • 第 1ff 行将对象坐标映射到一个数组,该数组将乘以modelMatrix(作为参数之一提供)。
  • 在第 2ff 行,乘法是通过我添加的另一个 GLUT helper 例程来完成的,因为写要比跟踪快。首先是模型视图矩阵,然后是投影矩阵在我们对象的 xyz 坐标上操作。(记住,列表中的第一个转换是最后执行的。)注意,对 gluMultMatrixVector3f()的第一个调用传递“in”数组,然后是“out”,而第二个调用以相反的顺序传递两个数组。这里没有什么巧妙之处——第二个实例颠倒了两者的使用,只是为了回收现有的数组。
  • 在第 3ff 行中,前面计算的结果值被归一化,然后映射到屏幕的尺寸上,得到最终的值。
  • 我们可能永远不会直接调用gluProject();相反,调用者是gluGetScreenLocation(),它仅仅获取第 4ff 行中所需的矩阵,将它们传递给gluProject(),并检索屏幕坐标。因为 OpenGL ES 会反转 y 轴,所以我们需要在第 5 行取消反转。

SolarSystem renderer中的execute()例程必须进行一些修改,以管理镜头光晕的调用和放置,同时随着增强的executePlanet()增加了一些新参数,以实际识别光晕应该位于屏幕上的什么位置。清单 8–8 中提供了两者。

清单 8–8。 用镜头光晕支持执行

`public void execute(GL10 gl)
    {
        float[] paleYellow = { 1.0f, 1.0f, 0.3f, 1.0f };
        float[] white = { 1.0f, 1.0f, 1.0f, 1.0f };
        float[] black = { 0.0f, 0.0f, 0.0f, 0.0f };
        float[] sunPos = { 0.0f, 0.0f, 0.0f, 1.0f };
        float sunWidth=0.0f;
        float sunScreenLoc[]=new float[4];        //xyz and radius
        float earthScreenLoc[]=new float[4];      //xyz and radius

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glShadeModel(GL10.GL_SMOOTH);

gl.glEnable(GL10.GL_LIGHTING);
        gl.glEnable(GL10.GL_BLEND);
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); gl.glPushMatrix();

gl.glTranslatef(-m_Eyeposition[X_VALUE], -m_Eyeposition[Y_VALUE],          //1
                        -m_Eyeposition[Z_VALUE]);

gl.glLightfv(SS_SUNLIGHT, GL10.GL_POSITION, makeFloatBuffer(sunPos));
        gl.glEnable(SS_SUNLIGHT);

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION,
                        makeFloatBuffer(paleYellow));

executePlanet(m_Sun, gl, false,sunScreenLoc);               //2

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION,makeFloatBuffer(black));

gl.glPopMatrix();

if ((m_LensFlare != null) && (sunScreenLoc[Z_INDEX] > 0.0f))         //3
        {
               CGPoint centerRelative = new CGPoint();
               CGSize     windowSize = new CGSize();
               float sunsBodyWidth=44.0f;            //About the width of the sun's body
                                                     // within the glare in the bitmap,                                                         in pixels.
                float cx,cy;
                float aspectRatio;
                float scale=0f;

DisplayMetrics display =                     myAppcontext.getResources().getDisplayMetrics();
                windowSize.width = display.widthPixels;
                windowSize.height = display.heightPixels;

cx=windowSize.width/2.0f;           
                   cy=windowSize.height/2.0f;

aspectRatio=cx/cy;                                      //4

centerRelative.x = sunScreenLoc[X_INDEX]-cx;
                centerRelative.y =(cy-sunScreenLoc[Y_INDEX])/aspectRatio;

scale=CT.renderTextureAt(gl, centerRelative.x, centerRelative.y, 0f,                     windowSize,
                m_FlareSource,sunScreenLoc[RADIUS_INDEX], 1.0f,1.0f, 1.0f, 1.0f); //5

sunWidth=scalewindowSize.widthsunsBodyWidth/256.0f;           //6
        } gl.glEnable(SS_FILLLIGHT2);

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glPushMatrix();

gl.glTranslatef(-m_Eyeposition[X_VALUE], -m_Eyeposition[Y_VALUE],    //7
        -m_Eyeposition[Z_VALUE]);

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
        makeFloatBuffer(white));

gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR,
            makeFloatBuffer(white));

executePlanet(m_Earth, gl, true,earthScreenLoc);          //8

gl.glPopMatrix();

if ((m_LensFlare != null) && (sunScreenLoc[Z_INDEX] > 0))       //9
    {
        float scale = 1.0f;
        float delX = origwidth / 2.0f - sunScreenLoc[X_INDEX];
        float delY = origheight / 2.0f - sunScreenLoc[Y_INDEX];
        float grazeDist = earthScreenLoc[RADIUS_INDEX] + sunWidth;        
        float percentVisible = 1.0f;
        float vanishDist = earthScreenLoc[RADIUS_INDEX] - sunWidth;

float distanceBetweenBodies = (float) Math.sqrt(delX * delX + delY * delY);

if ((distanceBetweenBodies > vanishDist)&& (distanceBetweenBodies        < grazeDist))                        //10
        {
            percentVisible=(float) ((distanceBetweenBodies - vanishDist) /sunWidth);

if (percentVisible > 1.0)                //11
                percentVisible = 1.0f;
                else if (percentVisible < 0.3)        
                percentVisible = .5f;
            }
            else if (distanceBetweenBodies > grazeDist)
            {
                percentVisible = 1.0f;
            }
            else
            {
                percentVisible = 0.0f;
            }             scale = STANDARD_FOV / m_FieldOfView;                  //12
            CGPoint source = new CGPoint();
            source.x = sunScreenLoc[X_INDEX];
            source.y = sunScreenLoc[Y_INDEX];
            CGSize winsize = new CGSize();
            winsize.width = origwidth;
            winsize.height = origheight;

if (percentVisible > 0.0)
            {
                m_LensFlare.execute(gl, winsize, source, scale,    percentVisible);
            }
        }
    }`

好了,现在开始粉笔对话:

  • 您会注意到发出了两个相同的glTranslatef()调用。第 1 行中的第一个为第 2 行的结果做准备。但是当我们的自定义太阳图像在第 5 行中呈现时,我们需要将它从堆栈中弹出。当地球被绘制到屏幕上时,需要在第 7 行再次调用它。

  • 在第二行,看起来我们正在渲染太阳。但也不尽然。这是为了在主屏幕上提取出太阳实际将要到达的位置。第三个参数render,如果为假,将使例程只返回屏幕位置和预期半径,而不是实际绘制太阳。

  • 第 3 行决定我们是否应该绘制新的太阳和镜头眩光对象,如果太阳基于它的 z 坐标可能是可见的。如果 z 为负,它在我们后面,所以我们可以一起跳过它。

  • 第 4 行的aspectRatio处理非方形视口,这意味着几乎所有的非方形视口。然后,我们根据屏幕的中心计算太阳的预期广告牌图像的位置。

  • 新的renderToTextureAt()调用现在将太阳的广告牌放在屏幕上,如第 5 行的m_FlareSource所示。sunScreenLoc{RADIUS_INDEX]是从executePlanet()获取的值之一,对应于实际 3D 图像的大小。scale的返回值暗示了最终位图的大小,占屏幕的百分比。这在第 6 行中用于计算太阳位图中“热点”的实际宽度,因为太阳身体的中心图像自然会远远小于位图的尺寸。

  • 在第 7 行,我们再次执行翻译,因为前一个在弹出矩阵时丢失了。接下来是第 8 行,它渲染地球,但是在本例中,传递了一个 true 的渲染标志。然而,它仍然获得屏幕位置信息,在这种情况下,仅仅是为了获得图像的尺寸,以便我们知道何时开始消除镜头眩光。

  • 然后我们从第 9ff 行开始,来到实际渲染光晕的地方。这里的大部分代码主要处理一个基本效应:当太阳走到地球后面时会发生什么?自然,耀斑会消失,但它不会立即出现或消失,因为太阳的直径有限。因此, grazeDistvanishDist 等数值告诉我们,当太阳第一次与地球相交时,开始变暗过程,当它最终被完全覆盖时,耀斑完全消失。使用地球屏幕的 x 和 y 值以及太阳的 x 和 y 值,指定一个渐变函数变得很容易。

  • Any value that falls between the vanishDist and grazeDist values specifies what percentage of dimming should be done, as in line 10, while lines 11ff actually calculate the value. Notice the line: else if(percentVisible<0.3)     percentVisible=0.5f

    额外学分:这是做什么的,为什么?

  • Lines 12ff calculate the size of the flare and its corresponding elements. As you zoom in with a decreasing field of view—that is, a higher-power lens—the sun's image will increase and the flare should as well.

    这个练习的最后一点是看一下executePlanet(),如清单 8–9 所示。

清单 8–9。 ExecutePlanet()修改后得到屏幕坐标

`public void executePlanet(Planet planet, GL10 gl, Boolean render,float[] screenLoc)         
    {
        Vector3 planetPos = new Vector3(0, 0, 0);
        float temp;
        float distance;
        float screenRadius;

gl.glPushMatrix();

planetPos.x = planet.m_Pos[0];
        planetPos.y = planet.m_Pos[1];
        planetPos.z = planet.m_Pos[2];

gl.glTranslatef((float) planetPos.x, (float) planetPos.y,(float) planetPos.z);

if (render)
        {
            planet.draw(gl);                           //1
        }

Vector3 eyePosition = new Vector3(0, 0, 0);

eyePosition.x = m_Eyeposition[X_VALUE];
        eyePosition.y = m_Eyeposition[Y_VALUE];         eyePosition.z = m_Eyeposition[Z_VALUE];

distance = (float) planetPos.Vector3Distance(eyePosition, planetPos);

float fieldWidthRadians = (m_FieldOfView /DEGREES_PER_RADIAN) / 2.0f;
        temp = (float) ((0.5f * origwidth) / Math.tan(fieldWidthRadians));

screenRadius = temp * getRadius(planet) / distance;

if(screenLoc!=null)                                                 //2
        {
            Miniglu.gluGetScreenLocation(gl, (float) planetPos.x, (float) -planetPos.y,
                (float) planetPos.z, (float) screenRadius, render,screenLoc);
        }

screenLoc[RADIUS_INDEX]=screenRadius;

gl.glPopMatrix();
        angle += .5f;
    }`

在最后一位,当且仅当渲染标志为真时,第 1 行正常绘制行星。否则,它只是获取屏幕位置和尺寸,如第 2 行所示,这样我们就可以自己绘制了。

应该可以了。我确信你能够编译时没有错误或警告,因为你就是这么好。因为你就是那么优秀,你可能会得到图 8–6 中的图片作为奖励。和我一样,随意使用环境光和镜面光。效果可能不是很逼真,但看起来很不错。

Image

图 8–6。看,马!镜头眩光!

*#### 看星星

下一个练习所需的大部分新代码主要用于加载和管理所有新数据。因为这是一本关于 OpenGL 的书,而不是 XML 或数据结构或如何有效地输入书中的代码,所以我将省去那些你可能已经知道的更乏味的东西。

当然,没有一些漂亮的恒星作为背景,任何太阳系模型都是不完整的。到目前为止,所有的例子都足够小,可以在文本中完整地打印出来,但是现在,当我们在背景中添加一个简单的 star field 时,情况会稍有变化。不同之处很大程度上在于你需要从 Apress 网站获取的数据库,因为它将包含 500 多颗 4.0 星等的恒星,以及一个包含星座轮廓和一些更突出的星座名称的额外数据库。

除了 OpenGL ES 用于创建实体模型的三角形面之外,如果您的 Android 设备支持多像素点表示,您还可以指定将模型的每个顶点渲染为给定大小和大小的点图像。此时,Android 商业模式丑陋的一面开始显露出来。

谷歌对 Android 的做法很简单:试图将它打造成世界上最卓越的移动操作系统。为了做到这一点,谷歌让它免费,并允许制造商随心所欲地修改它。结果,可怕的分裂迅速潜入。从表面上看,消费者不应该担心这一点,因为他们有大量的手机可供选择。但是从开发人员的角度来看,这使得编写在成百上千台设备上运行的软件成为一场噩梦,因为每台设备都有自己的小毛病。从长远来看,这确实会影响消费者,因为开发者可能会选择不支持特定的设备家族,或者如果他们支持,他们可能会遭受发布延迟和成本增加,以确保他们的最新产品能够在所有设备上工作。这种差异在图形支持方面表现得最为明显。

图形处理单元有许多不同的制造商。GC860 的制造商 Vivante 向 Marvell 供应芯片;AMD 把自己的 GPU 送给东芝,PowerVR 卖给苹果和三星。更糟糕的是,每个特定型号的 GPU 都可能比同一制造商的前几代产品拥有更多功能。这意味着您可能不得不通过省略最新设备可能支持的酷功能来编写最低公分母,或者如果您真的需要一个特定的功能来跨所有*台工作,您可能不得不推出自己的功能。或者,作为第三种方法,您可能只需要针对特定的设备进行编码,而将其他设备排除在外。在很大程度上,苹果已经成功地用他们的 iOS 设备在这些水域中航行,而微软(它对旧的 Windows Mobile 手机使用“Android”方法)现在通过确保他们的 Windows Mobile 7 许可证持有者遵守非常严格的规范来避免分裂。因此,选择目标机器至关重要。

现在回到星星上。那么,这有什么特别的呢?很简单。并不是所有的设备都支持 OpenGL 的大于一个像素的渲染。或者那些支持的可能不支持圆形抗锯齿点。前者几乎可以工作,但只适用于上一代低分辨率屏幕,比如 75 DPI 左右。但现在有了更新的高分辨率显示器(如苹果的视网膜显示器),单个像素小到几乎看不见,这使得显示由像素集合组成的星星变得势在必行。这就是这个练习的发展情况,你很快就会看到。但首先,敬明星们。

注意:设备的价格或制造商似乎与其支持的 3D 功能没有多大关系。比如第一代摩托罗拉 Xoom 可以做胖点,但是只能做方点。Kindle Fire 做的是宽线,但只有单像素点,而廉价的无名设备既做粗线又做点。

这第一点的恒星数据库是从我的遥远的太阳数据编译成苹果的 plist XML 文件格式。然后,为了便于演示,对其进行了一点调整,使解析变得更加容易。同样的方法也用于星座数据。加载时,它被绘制成非常像以前的对象,比如球体,但是没有指定GL_TRANGLE_STRIPS,而是使用了GL_POINTS。清单 8–10 显示了恒星的execute()方法。

清单 8–10。 渲染群星

    public void execute(GL10 gl)     {         int len`;
        float[] pointSize = new float[2];                        
        GL11 gl11 = (GL11) gl;

gl.glDisable(GL10.GL_LIGHTING); `                 //1
        gl.glDisable(GL10.GL_TEXTURE_2D);
        gl.glDisable(GL10.GL_DEPTH_TEST);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);             //2

gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_DST_ALPHA);
        gl.glEnable(GL10.GL_BLEND);

gl.glColorPointer(4, GL10.GL_FLOAT, 0, m_ColorData);
        gl.glVertexPointer(3,GL10.GL_FLOAT, 0, m_VertexData);

gl.glEnable(GL10.GL_POINT_SMOOTH);                //3
        gl.glPointSize(5.0f);

gl.glDrawArrays(GL10.GL_POINTS,0,totalElems/4); `           //4

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);    
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);    
        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glEnable(GL10.GL_LIGHTING);
    }`

接下来的故事是:

  • 第 1 行对于渲染任何自发光的对象或者你只想一直可见的对象是必不可少的。在这种情况下,恒星属于前一类,而星座名称和轮廓等各种标识符属于后一类。关灯可以确保无论发生什么都能看到它们。(在之前的练习中,太阳被渲染成一个发射物体,但仍然被照亮,只是为了在表面上获得一个看起来非常好的轻微渐变。)

  • Colors in line 2 are being used to specify the intensity of a star's magnitude. A more sophisticated system would encode both the star's real color and luminosity, but here we're just doing the simple stuff.

    注:恒星的星等是其视亮度;数值越大,恒星越暗。天空中仅次于太阳的最亮的星星当然是天狼星,目视星等为-1.46。肉眼可见的最暗的恒星大约是 6.5 星等。双筒望远镜的最高星等约为 10,而哈勃太空望远镜的最高星等为 31.5。每个整数的实际亮度相差约 2.5 倍,因此,一颗 3 等星的亮度约为 4 等星的 2.5 倍。

  • 硬边星形看起来并不有趣,所以第 3f 行打开了点的反走样,并确保这些点足够大以至于可见。

  • 第 4 行像往常一样绘制数组,但是使用了GL_POINT渲染风格,而不是用于实体的GL_TRIANGLES

让我们回到第 3 行。还记得关于不同设备和 GPU 是否具有不同功能的讨论吗?这里有一个这样的例子。在这个练习的开发中,使用的硬件是摩托罗拉 Droid 的第一代产品。事实证明,它不支持多像素点,所以每颗星星在一个非常高分辨率的屏幕上只是一个像素。解决方案是使用“点精灵”,一种为每个绘制的点分配小位图的方法。OpenGL ES 也可以支持这一点,但是,如前所述,只在某些设备上支持。唉。或者如官方 OpenGL 文档所述:

仅保证支持尺寸 1;其他的就看实现了。

如果支持较大的点,无需任何进一步修改,它们将被绘制为正方形。这里是你要开启GL_POINT_SMOOTHING的地方。如果实现,它将尝试创建圆角。然而,点*滑所允许的大小取决于实现。你可以查一下

通过下面的调用:

float[] pointSize = new float[2]; gl11.glGetFloatv(GL10.GL_SMOOTH_POINT_SIZE_RANGE, makeFloatBuffer(pointSize));

如果点*滑不可用,pointSize将显示 0.0f,0.0f。然而,*滑的点不一定是好看的点。要获得更好的点,请打开混合。那么系统将消除图像的锯齿。然而,这“取决于实施”唉。

图 8–7 显示了三种可能性之间的差异。

Image

图 8–7。 从左到右,一个 8 像素宽的不*滑点的特写,带*滑,带*滑和混合

如果你想获得尽可能多的观众,最终你将不得不处理你自己的点渲染。

看线条

当然,除了恒星和行星,天空中还有更多。有星座。和前面的星星一样,你必须从一个网站获取星座数据库。这包含了 17 个不同星座的数据。该数据包括形成星座轮廓的普通名称和线数据。在这种情况下,设置实际上与前面描述的星形相同,但我们没有绘制点阵列,而是绘制了线阵列:

        gl.glDrawArrays(GL10.*GL_LINE_STRIP*,0,numVertices);

与点一样(取决于实现),线也可以画得比单个像素宽。下面的调用将达到目的:

        gl.glLineWidth(lineWidth);

然而,大于 1 的线宽,现在全部一致,取决于实施

您应该能够通过调用以下代码来检查可用的线宽:

        int[] pointSize = new int[2];         gl11.glGetIntegerv(GL10.*GL_ALIASED_LINE_WIDTH_RANGE*, *makeIntBuffer*(pointSize));

宽度大于 1 的线取决于实现,尽管看起来更多的设备允许宽线而不是宽像素。

线条的 OpenGL ES 实现中的一个问题是,与桌面版本相比,不支持抗锯齿线条(*滑)。这意味着使用标准技术绘制的任何线条在较旧、分辨率较低的显示器上看起来都非常不舒服。但是随着更高的 dpi 变得可用,抗锯齿就不那么必要了。但是如果你还是要考虑的话,常用的一个技巧就是把线条画成真的很细的多边形,使用纹理贴图,可以抗锯齿,可以给图片添加虚线之类的东西。繁琐?没错。作品?相当好。OpenGL ES 宇宙中的另一种方法是使用多纹理抗锯齿。但是,这要看执行情况!

MSAA 创建了两个 OpenGL 渲染表面。一个是我们看到的,另一个是我们看到的两倍大。这两者的混合使整个屏幕中的所有图像变得*滑,看起来非常好,尽管这是以性能和内存使用为代价的。图 8–8 显示了两者的对比。

Image

图 8–8。 左无 MSAA;右侧启用

看到文字

对于 OpenGL 的所有功能,文本支持不在其中。这是一个长期存在的问题,真正正确处理文本的唯一方法是使用带有文本的纹理。毕竟,这就是任何文本的开始:只是一堆小纹理。

如果你有很小的文本需求,最简单的方法是预渲染文本块,然后像导入其他纹理一样导入它们。如果您有很多文本,您可以在需要每个字符串时动态生成它们。如果你想使用各种各样的字体,这是一个不错的方法。总的来说,最好的方法是使用称为纹理贴图集(也称为精灵表)的东西。

当与文本渲染结合使用时,纹理贴图集将获取与您想要的字体相关联的所有字符,并将它们存储在一个位图中,如图 Figure 8–9 所示。为了绘制文本,我们使用之前在镜头光晕中使用的技术来渲染 2D 位图。

Image

图 8–9。 时代新罗马,纹理图册版

取纹理图谱,使用分数纹理映射(见第五章的图 5-6 ),字母可以动态组合成任何需要的字符串。由于 OpenGL 本身不支持任何类型的字体处理,我们要么推出自己的字体管理器,要么求助于第三方。幸运的是,由于这个问题如此普遍,许多善良的灵魂创造了工具和库,并使它们免费可用。图 8–9 是用一个非常好的基于 PC 的工具 CBFG 生成的,可以从[www.codehead.co.uk/cbfg/](http://www.codehead.co.uk/cbfg/)下载。

这包括该工具以及嵌入式 C++ 和 Java 阅读器,在本例中使用了后者。要创建和初始化字体使用,请使用以下代码:

    m_TexFont = new TexFont(context, gl);     m_TexFont.LoadFont("TimesNewRoman.bff", gl);

清单 8–11 展示了如何使用它。摘录来自示例代码中的Outline.java

清单 8–11。 将文本写入 OpenGL 视图

`    public void renderConstName(GL10 gl, String name, int x, int y, float r, float g,
        float b)
    {
        gl.glDisable(GL10.GL_LIGHTING);
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
        gl.glEnable(GL10.GL_BLEND);

if(name!=null)
        {
            m_TexFont.SetPolyColor(r, g, b);
            m_TexFont.PrintAt(gl, name.toUpperCase(), x, y);
        }
    }    `

现在我们有了线、点和文本,我们应该看到什么?啊,问题是我们会看到什么(图 8–10)。毕竟,这将取决于实施

*Image

图 8-10。 左边看不见的单像素恒星;正确的做事方法

查看按钮

当然,任何没有与之交互手段的应用通常都被称为演示程序。但是这里我们的小演示实际上将获得一个简单的用户界面和 HUD 图形。

说到给你的 Android 应用添加 UI 元素,我有一些非常好的消息:不会找到“取决于实现”这句话。添加简单的控制元素非常容易。当然,你通常不会使用 OpenGL 显示作为一个应用的背景,UI 元素通常仍然应该被隔离在它们自己的空间中;这完全取决于你的目标。考虑到这一点,可以添加一个简单的 UI 面板,如SolarSystemActivity中的清单 8–12 所示,看起来应该类似于图 8–11。

清单 8–12。 向天空添加 UI

`public void onCreate(Bundle savedInstanceState)
    {
            super.onCreate(savedInstanceState);        
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);
            mGLSurfaceView = new SolarSystemView(this);
            setContentView(mGLSurfaceView);
            mGLSurfaceView.requestFocus();
            mGLSurfaceView.setFocusableInTouchMode(true);

ll = new LinearLayout(this);
            ll.setOrientation(VERTICAL_ORIENTATION);
            b_name = new Button(this);        
            b_name.setText("names");
            b_name.setBackgroundDrawable(getResources().getDrawable(                 book.SolarSystem.R.drawable.bluebuttonbig));
            ll.addView(b_name);

b_line = new Button(this);        
            b_line.setText("lines");      
            b_line.setBackgroundDrawable(getResources().getDrawable                 (book.SolarSystem.R.drawable.greenbuttonbig));
            ll.addView(b_line, 1);

b_lens_flare = new Button(this);
            b_lens_flare.setText("lens flare");
               b_lens_flare.setBackgroundDrawable(getResources().getDrawable                    (book.SolarSystem.R.drawable.redbuttonbig));
            ll.addView(b_lens_flare);

this.addContentView(ll, new LayoutParams(LayoutParams.WRAP_CONTENT,         LayoutParams.WRAP_CONTENT));

b_name.setOnClickListener(new Button.OnClickListener() {

@Override
                public void onClick(View v) {                    
                    if (name_flag == false)
                        name_flag = true;
                    else if (name_flag == true)
                        name_flag = false;
                    Log.d(TAG, "b_name clicked");
                }                 
            });

b_line.setOnClickListener(new Button.OnClickListener() {                     @Override
                    public void onClick(View v) {                 
                    if (line_flag == false)
                        line_flag = true;
                    else if (line_flag == true)
                        line_flag = false;
                    Log.d(TAG, "b_line clicked");
                }            
            });

b_lens_flare.setOnClickListener(new Button.OnClickListener() {

@Override
                public void onClick(View v) {
                    if (lens_flare_flag == false)
                        lens_flare_flag = true;
                    else if (lens_flare_flag == true)
                        lens_flare_flag = false;        
                    Log.d(TAG, "b_lensflare clicked");
                }            
            });` Image

图 8–11。 将 UI 组件放在 OpenGL 屏幕上

如果你希望有很多 UI 元素,你可能会考虑用 OpenGL 来创建它们。如果您计划支持多个*台,这尤其有用,因为 OpenGL 跳过了所有特定于*台的工具包,尽管这使得最初的工作有些乏味。游戏是最能从这种方法中受益的应用,特别是因为它们通常有高度定制的用户界面。一个编写良好的纯 OpenGL 应用从一个*台移植到另一个*台可能只需要几天,而不是几个月。iPhone 上的《遥远的太阳》使用了两者的混合,主要是看两者如何互补。我在图 8–12 中的小时间设置部件使用了所有的 OpenGL,而其他的都使用了标准的 UI 组件。

Image

图 8–12.遥远的太阳,左侧带有自定义日期轮,同时带有标准 UIKit 工具栏

总结

在这一章中,我们采用了前几章中学到的许多技巧,并根据实现的不同,将它们组合成一个更完整、更吸引人(不那么蹩脚)的太阳系模型。一个单一行星的太阳系并不像现在这样令人印象深刻。所以,亲爱的读者,我把它留给你,添加月亮,添加一些其他的行星,并让地球围绕太阳旋转。

我们添加了第七章中的镜头光晕,以及星星和星座轮廓的点和线对象,将文本插入到 OpenGL 环境中,并在同一个屏幕上混合了 OpenGL 视图和标准 Android 控件。

我还指出了图形子系统是一个在不同设备之间变化最大的子系统,它会引起很多痛苦、烦恼、咬牙切齿和撕裂衣服。在下一章,我们将研究优化技巧,然后是 OpenGL ES 2.0,以及如何应用它来增强我们的地球模型。**

九、性能和材质

一盎司的表现抵得上几磅的承诺。

—西部

我动作太快了,以至于昨晚我在酒店房间里关掉了灯,在房间天黑之前就上床睡觉了。

—穆罕默德·阿里

在处理 3D 世界时,性能几乎总是一个问题,因为即使是简单的场景也需要密集的数学运算。如果你想渲染的只是一个动画旋转的三角形,上面有可爱的机器人,那么不要担心,但是如果你想展示宇宙,那么你会一直关注性能。

到目前为止,练习以一种相当清晰(我希望如此)但不一定有效的方式呈现。不幸的是,高效的代码很少是最清晰易懂的。现在,我们将开始研究稍微复杂一些的东西,看看如何将它集成到您的应用中。

在行业中,这些技巧被称为最佳实践。有些可能是显而易见的,但有些则不然。

顶点缓冲对象

性能增强的两个主要方面是最大限度地减少与图形硬件之间的数据传输,以及最大限度地减少数据本身。顶点缓冲对象(VBOs)是前一个过程的一部分。当你生成了你的几何图形,并愉快地将它发送出去显示时,通常的过程是告诉系统在哪里可以找到每个需要的数据块,允许使用哪些数据(顶点、法线、颜色和纹理坐标),然后绘制它。每次调用glDrawArrays()glDrawElements()时,所有的数据都必须打包并发送到图形处理单元(GPU)。每次数据传输都需要有限的时间,显然,如果一些数据可以缓存在 GPU 上,性能会得到提高。vbo 是在 GPU 上分配常用数据的一种方式,然后可以调用这些数据进行显示,而不必每次都重新提交数据。

创建和使用 VBOs 的过程对你来说应该很熟悉,因为它模仿了用于纹理的过程。首先生成一个“名称”,然后为数据分配空间,然后加载数据,然后在需要使用这些数据时使用glBindBuffer()。我将同时介绍的另一个实践是交错数据,如图 9–1 所示,在提交给 VBO 之前,我将首先做这件事。这可能会也可能不会有太大的区别,但如果驱动程序和 GPU 针对数据局部性进行了优化,交错阵列可能会有所帮助。

Image

图 9–1数据排序。VBO 的例子使用了上面的格式,而下面的例子说明了数据交错。

在我自己的测试中,我发现差异可以忽略不计。但是,仍然要在你的项目中记住交错;为它设计不会有什么坏处,因为未来的硬件可能会更好地利用它。参考列表 9–1 来看看行星的几何形状是如何交错的。

清单 9–1。 创建交错数组

`**    private void** createInterleavedData()
    {
        int i;
        int j;
        float[] interleavedArray;
        int size;

size=NUM_XYZ_ELS+NUM_NXYZ_ELS+NUM_RGBA_ELS+NUM_ST_ELS;

interleavedArray=new float[size*m_NumVertices];

for(i=0;i<m_NumVertices;i++)
        {
            j=i*size;

//Vertex data

interleavedArray[j+0]=m_VertexData.get();
            interleavedArray[j+1]=m_VertexData.get();
            interleavedArray[j+2]=m_VertexData.get();

//Normal data

interleavedArray[j+3]=m_NormalData.get();
            interleavedArray[j+4]=m_NormalData.get();
            interleavedArray[j+5]=m_NormalData.get();

//cColor data

interleavedArray[j+6]=m_ColorData.get();
            interleavedArray[j+7]=m_ColorData.get();
            interleavedArray[j+8]=m_ColorData.get();
            interleavedArray[j+9]=m_ColorData.get();

//Texture coordinates

interleavedArray[j+10]=m_TextureData.get();
            interleavedArray[j+11]=m_TextureData.get();
        }

m_InterleavedData=makeFloatBuffer(interleavedArray);

m_VertexData.position(0);
        m_NormalData.position(0);
        m_ColorData.position(0);
        m_TextureData.position(0);
    }`

这些都是不言自明的,但是请注意最后四行。它们会重置FloatBuffer对象的内部计数器,如果您想在其他地方使用任何单独的数据数组,就需要这样做。

清单 9–2 展示了我如何从行星数据中创建 VBO。由于大多数行星通常都是相同的形状,圆形,所以可以在 CPU 上缓存一个球体模型,并将其用于任何行星或卫星,除了 Demos 或 Hyperion 或 Nix 或 Miranda...或者任何看起来更像发霉土豆的小卫星。听到了吗,火卫一?我在和说话!

清单 9–2。 为行星模型创建一个 VBO

`**    public void** createVBO(GL10 gl)
    {
        int size=NUM_XYZ_ELS+NUM_NXYZ_ELS+NUM_RGBA_ELS+NUM_ST_ELS;

createInterleavedData();

GLES11.glGenBuffers(1,m_VertexVBO,0);                                      //1
        GLES11.glBindBuffer(GL11.GL_ARRAY_BUFFER, m_VertexVBO[0]);                 //2
        GLES11.glBufferData(GLES11.GL_ARRAY_BUFFER,sizeFLOAT_SIZEm_NumVertices,  //3
                  m_InterleavedData,GLES11.GL_STATIC_DRAW);
    }`

简单,嗯?注意 VBOs 直到 OpenGL ES 1.1 才出现,这就是为什么需要GLES11修改器的原因。

  • 第 1 行生成了缓冲区的名称。因为我们只处理一个数据集,所以我们只需要一个。
  • 接下来,我们将它绑定到第 2 行,使其成为当前的 VBO。要解除绑定,可以绑定一个 0。
  • 来自交错阵列的数据现在可以发送到第 3 行中的 GPU。第一个参数是数据的时间,可以是GL_ARRAY_BUFFER也可以是GL_ELEMENT_ARRAY_BUFFER。前者用于传递顶点数据(包括颜色和法线信息),后者用于传递索引连通性数组。但是由于我们使用的是三角形条带,所以不需要索引数据。

那么,我们如何使用 VBOs 呢?非常容易。看一下清单 9–3。

清单 9–3。 使用 VBOs 渲染星球

`**    public void** draw(GL10 gl)
    {
        int startingOffset;
        int i;
        int maxDuplicates=10;                                                                                             //1
        boolean useVBO=true;                                                                                          //2

int stride=(NUM_XYZ_ELS+NUM_NXYZ_ELS+NUM_RGBA_ELS                      //3
+
NUM_ST_ELS
)**FLOAT_SIZE*;

GLES11.glEnable(GLES11.GL_CULL_FACE);
        GLES11.glCullFace(GLES11.GL_BACK);
        GLES11. glDisable ( GLES11.GL_BLEND);
        GLES11.glDisable(GLES11.GL_TEXTURE_2D);

**        if**(useVBO)                                                                                                            //4
        {
            GLES11.glBindBuffer(GL11.GL_ARRAY_BUFFER, m_VertexVBO[0]);  //5

GLES11.glEnableClientState(GL10.GL_VERTEX_ARRAY);                    //6

GLES11.glVertexPointer(NUM_XYZ_ELS,GL11.GL_FLOAT,stride,0);

GLES11.glEnableClientState(GL11.GL_NORMAL_ARRAY);
            GLES11.glNormalPointer(GL11.GL_FLOAT,stride,NUM_XYZ_ELS*FLOAT_SIZE);

GLES11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);

GLES11.glTexCoordPointer(2,GL11.GL_FLOAT,stride,
                     (NUM_XYZ_ELS+NUM_NXYZ_ELS+NUM_RGBA_ELS)**FLOAT_SIZE*);

GLES11.glEnable(GL11.GL_TEXTURE_2D);
            GLES11.glBindTexture(GL11.GL_TEXTURE_2D, m_TextureIDs[0]);
        }
        else
        {
            GLES11.glBindBuffer(GL11.GL_ARRAY_BUFFER,0);                                     //7

m_InterleavedData.position(0);

GLES11.glEnableClientState(GL10.GL_VERTEX_ARRAY);
            GLES11.glVertexPointer(NUM_XYZ_ELS,GL11.GL_FLOAT,stride,m_InterleavedData);

m_InterleavedData.position(NUM_XYZ_ELS);

GLES11.glEnableClientState(GL11.GL_NORMAL_ARRAY);
            GLES11.glNormalPointer(GL11.GL_FLOAT,stride,m_InterleavedData);

m_InterleavedData.position(NUM_XYZ_ELS+NUM_NXYZ_ELS+NUM_RGBA_ELS);

GLES11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
            GLES11.glTexCoordPointer(2,GL11.GL_FLOAT,stride,m_InterleavedData);
            GLES11.glEnable(GL11.GL_TEXTURE_2D);
            GLES11.glBindTexture(GL11.GL_TEXTURE_2D, m_TextureIDs[0]);
        }

for(i=0;i<maxDuplicates;i++)                                                                                 //8
        {
                   GLES11.glTranslatef(0.0f,0.2f,0.0f);
                   GLES11.glDrawArrays(GL11.GL_TRIANGLE_STRIP, 0,
                            (m_Slices+1)2(m_Stacks-1)+2);
        }

GLES11.glDisable(GL11.GL_BLEND);
        GLES11.glDisable(GL11.GL_TEXTURE_2D);
        GLES11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
        GLES11.glBindBuffer(GL11.GL_ARRAY_BUFFER,0);

m_VerticesPerUpdate=maxDuplicates*m_NumVertices;
    }`

渲染 VBOs 非常简单,只有一个简单的“嗯?”在这个过程中。我已经决定在所有的调用前面加上前缀GLES11,只是为了好看。不是所有的套路都需要。

  • 第 1 行和第 2 行让您配置用于测试目的的例程。maxDuplicates是星球被渲染的次数。useVBO可以关闭 VBO 处理,仅使用交错数据进行性能比较。
  • 请记住,第 3 行中的跨距值表示任意数组中从顶点到顶点的字节数。这对于交叉存取数据来说是必不可少的,例如,相同的数组可以用于顶点位置和颜色。跨距只是告诉系统在找到下一个顶点之前要跳过多少字节。
  • 第 4 行将打开实际的 VBO 设置代码。第 5 行以与绑定纹理相同的方式绑定它,使它成为当前使用的对象,直到另一个对象被绑定或者这个对象与glBindBuffer(GL_ARRAY_BUFFER, 0);解除绑定。
  • 线 6ff 启用各种数据缓冲器,如之前的draw()方法所做的那样。一个主要的区别可以从各种glEnable*Pointer()调用的最后一个参数中看出。在正常情况下,你传递一个指针或引用给它们。然而,当使用 VBOs 时,指向数据块的各种“指针”是从第一个元素的偏移,它总是从零“地址”开始,而不是从应用自己的地址空间中的一开始。这意味着在我们的例子中,顶点指针从地址 0 开始,而法线就在顶点之后,颜色是跟随法线的纹理坐标。这些值从数据开始以字节表示。
  • 第 7 行突出显示了另一部分。这里我们只使用交叉存取的数据,并以更传统的方式将其传递给指针例程。这允许您查看从交叉存取数据中获得了什么性能增强(如果有的话)。
  • 第 8 行之后的部分通过maxDuplicates调用循环到glDrawArrays()。值为 10 时效果很好。

当谈到优化代码时,我是一个需要确信某个特定的技巧会起作用的人。否则,我可能会花很多时间做一些事情来增加 0.23%的帧速率。一个游戏程序员可能会觉得这是一种荣誉,但我觉得它通过将注意力转移到可能不会有太大影响的东西上,从我的用户那里偷走了可选的新功能或错误修复。因此,我开发了一个简单的测试程序。

该程序简单地绘制了十个行星地球并旋转它们,如图 Figure 9–2 所示。球体由 100 个堆栈和 100 个切片组成,每个球体有 20,200 个顶点。我使用同一个实例,,所以我只需要在程序启动时加载一次 GPU。如果没有 VBOs,相同的模型将被加载十次。

Image

图 9–2。 巨型计算机生成的母猪虫。或者十个地球互相堆叠在一起。

现在,您可以向您的onDrawFrame()处理程序添加一个帧速率计算器,看看会发生什么。我自己的设置使用的是第一代摩托罗拉 Xoom,下面是一些基本的测试,这些测试应该让我对 Xoom/Java *台与其他类似价格范围的*台相比表现如何有一个大致的了解。

注意:我自己的设置使用的是第一代摩托罗拉 Xoom,它支持 NVIDIA Tegra 2 GPU。截至本文撰写时,苹果的所有 iOS 设备都使用 Imagination Technologies 制造的 PowerVR 系列芯片;三星的 Galaxy Tab 和黑莓的 Playbook 也是如此。去你的 GPU 制造商的开发者部门可能是值得的。Imagination Tech 和 NVIDIA 都有优秀的 notes、SDK 和 demos,可以充分利用各自的硬件。

基线配置打开了纹理,三个灯,具有深度缓冲的 32 位颜色,在 5 个单位之外的视点上混合,并且视场设置为 30 度。结果相当令人惊讶,如表 9–1 所示。

Image

Image

在这种情况下,最大的 CPU 消耗是灯光,因为只要关掉三盏灯中的两盏灯,帧速率就会翻倍。在第十章中,你会看到如何管理引擎盖下的照明,这会更有意义。关闭深度缓冲和纹理对从 32 位降到 16 位颜色几乎没有影响。

另一个令人惊讶的结果是交错数据格式似乎实际上降低了性能!选择为每种数据类型使用单独的离散缓冲区,“老方法”稍微加快了速度。移开视点减少了要处理的像素数量,但几乎没有增加帧速率。这表明 GPU 并没有受到像素的限制,主要的性能问题是实际的顶点计算。很有可能 Java 肯定会在这方面发挥很大的作用。作为语言相关问题的部分解决方案,Android 还提供了一个本地开发包(NDK)。

NDK 旨在让开发人员将他们的性能关键代码放在 Java 层之下,放入 C 或 C++ 中,使用 JNI 在两个世界之间来回移动。(性能关键型可能包括图像处理或大型系统建模。)OpenGL 将针对您使用的任何级别进行优化,因此在纯 OpenGL 比较中,您可能会看到很少的改进。除此之外,即使在网上快速搜索,也会发现许多开发人员创建了比较这两种环境的测试,几乎所有测试都显示,对于数学密集型任务,性能显著提高了 30 倍或更多。但是当然,由于驱动程序、操作系统、GPU 或编译器问题,您的收益可能会有所不同。

配料

尽可能多地批处理依赖于相同状态的操作,因为改变系统状态(通过使用glEnable()glDisable()调用)代价很高。OpenGL 不会在内部检查某个特定的特性是否已经处于您想要的状态。在本书的练习中,我会比我可能需要的更频繁地设置状态,以确保行为是容易预测的。但是对于商业的、性能密集型的应用,在发布版本中尽量去掉多余的调用。

此外,尽可能批处理您的绘图调用。

纹理

一些纹理优化技巧已经被提出,比如小中见大贴图。其他的只是简单的常识:纹理占用大量的内存。使它们尽可能小,并在需要时重复使用。此外,在加载之前设置任何图像参数,因为它们是一个提示,告诉 OpenGL 如何在发送到硬件之前优化信息。

首先绘制不透明的纹理,避免使用半透明的 OpenGL ES 屏幕。

雪碧床单

当介绍在 OpenGL 环境中显示文本时,在第八章中简要提到了精灵表(或纹理图谱)。Figure 9–3 展示了一个 sprite 工作表在屏幕上显示文本时的样子。

Image

图 9–3。24 点铜板用雪碧片

这张特殊的图片是使用一个名为 LabelAtlasCreator 的免费工具为 Mac 制作的,而不是在第八章中使用的 CBFG 工具。除了图像文件之外,它还会生成一个 Apple 的 plist 格式的方便的 XML 文件,其中包含所有易于转换为纹理空间的放置细节。

但是不要停留在字体上,因为精灵表也可以在你有一个志同道合的图像家庭的任何时候使用。从一种纹理切换到另一种纹理会导致大量不必要的开销,而 sprite sheet 充当了一种纹理批处理的形式,节省了大量的 GPU 时间。查看 OS X 兼容的 Zwoptex 工具或 TexturePacker,它用于通用图像。

纹理上传

将纹理复制到 GPU 可能非常昂贵,因为它们必须由芯片重新格式化才能使用。对于较大的纹理,这可能会导致显示不连贯。因此,确保从一开始就用glTexImage2D加载它们。一些 GPU 制造商,如苹果产品所用芯片的制造商 Imagination Technologies,有自己的专有图像格式,针对自己的硬件进行了微调。当然,在日益碎片化的 Android 市场,你将不得不在运行时嗅出你的用户有什么芯片,并在那时处理任何特殊需求。

mipmap

除了 2D 未缩放的图像,一定要使用小中见大贴图。是的,它们确实使用了更多的内存,但是当你的对象在远处时,更小的小贴图可以节省很多周期。建议您使用GL_LINEAR_MIPMAP_NEAREST,因为它比GL_LINEAR_MIPMAP_LINEAR快(参考表 5-3 ),尽管图像保真度稍低。

颜色变少

其他建议可能包括较低分辨率的颜色格式。许多图像在 16 位和 32 位下看起来几乎一样好,特别是如果没有 alpha 蒙版的话。Android 的默认格式是一直流行的 RGB565,这意味着它有 5 位红色,6 位绿色,5 位蓝色。(绿色得到了加强,因为我们的眼睛对它最敏感。)其他 16 位格式包括 RGBA5551 或 RBGA4444。在遥远的太阳上,我的灰度星座作品只有 8 位,减少了 75%的内存使用。如果我想让它与特定的主题相匹配,我会让 OpenGL 来完成这项工作。通过适当的工具和仔细的调整,一些 16 位纹理几乎与 32 位纹理无法区分。

Figure 9–4 展示了 TexturePacker 创建的四种格式,从左到右从高到低排列。图 1 显示了我们一直使用的真彩色纹理,有时称为 RGBA8888。图片 2 使用默认的 RGB565 格式,考虑到其他因素,看起来还是很不错的。图 9–4 中的图像 3 使用 RGBA5551,分配一个 1 位 alpha 通道(注意绿色的额外位与之前的纹理相比有多大区别),图像 4 是最低质量的,使用 RGBA4444。TexturePacker 还支持第五章中引用的 PVRTC 文件类型。

注意:PowerVR 芯片的制造商 Imagination Technologies Ltd .提供了一个替代(免费)工具。它的纹理模式与 TexturePacker 相同,但不像 TexturePacker 那样创建 sprite 表。请注意,它使用 X11 作为 UI,其外观看起来像 Windows NT。前往[www.imgtec.com](http://www.imgtec.com)并在开发者部分下寻找 PowerVR Insider 工具。寻找 PVRTexTool。

压缩后效果最好的图像是那些调色板严重依赖光谱的一个或两个部分的图像。你的颜色越多样,就越难减少人为因素。Hedly 的图像比地球的纹理图更好,因为前者主要是灰色和绿色,而后者是由绿色、棕色、灰色(极地)和蓝色组成的。

Image

图 9–4。 纹理 1,32 位;纹理 2,RGB565 纹理 3,RGBA555 纹理 4,RGBA:4444

其他要记住的提示

以下是一些需要记住的有用提示:

  • 尽管多采样抗锯齿在*滑图像方面非常有用,但它意味着性能部门的突然死亡,将帧速率降低 30%或更多。所以,你一定很需要用它。

  • 避免使用GL_ALPHA_TEST。这是从来没有涵盖,但它也可以杀死性能一样多的 MSAA。

  • 当转到后台时,确保停止动画并删除任何容易重新创建的资源。

  • 任何从系统返回信息的调用(主要是glGet*系列调用)都会查询系统的状态,包括容易被忽略的glGetError()。其中许多命令会强制执行任何以前的命令,然后才能检索状态信息。

  • 使用尽可能少的灯光,通过关闭补光灯和环境光,只使用一个灯光(太阳)。

    不从 CPU 访问帧缓冲区。应该避免像glReadPixels()这样的调用,因为它们会迫使帧缓冲区刷新所有排队的命令。

上述提示仅代表最基本的推荐做法。真正的图形大师们在他们的实用工具中有许多神秘的技巧,简单的谷歌搜索就可能揭示出来。

总结

本章描述了让你的 OpenGL 应用真正运行的基本技巧和最佳实践。VBOs 将通过在 GPU 上保留常用的几何图形来降低总线的饱和度。将状态改变和glGet*调用减少到最少也能显著提高渲染速度。

在第十章中,你会学到一些关于 OpenGL ES 2.0 和那些最*风靡孩子们的神秘着色器的东西。

十、OpenGL ES 2、着色器以及其它

她的天使的脸,像天上的大眼睛一样明亮,在阴暗的地方照出了阳光。

-埃德蒙史宾塞

Android 设备上有两个不同版本的 OpenGL ES 图形库。这本书主要讨论了更高层次的版本,即 OpenGL ES 1,有时也称为 1.1 或 1。 x 。第二个版本是一个相当容易混淆的名字 OpenGL ES 2。第一个是两个中最容易的一个;它附带了各种各样的助手库,为你做大量的 3D 数学和所有的照明、着色和阴影。版本 2 避开了所有这些细节,有时被称为“可编程功能”版本,而不是其他的“固定功能”设计。这通常被真正的像素操纵者嘲笑,他们更喜欢控制他们的图像,通常保留给沉浸式 3D 游戏环境,其中每个小的视觉脚注都被强调。为此,OpenGL ES 2 发布了。

第 1 版相对来说更接*于桌面版的 OpenGL,使得移植应用,尤其是老式的应用,比让獾啃掉你的脸要轻松一些。之所以省略这些内容,是为了在资源有限的设备上保持较小的占用空间,并确保尽可能好的性能。

第二版完全摒弃了兼容性,专注于主要针对娱乐软件的面向性能的特性。被遗漏的东西有glRotate(), glTranslate(),矩阵堆栈操作,等等。但是我们得到的回报是一些令人愉快的小东西,比如通过使用着色器的可编程流水线。幸运的是,Android 自带矩阵和向量库(android.opengl.Matrix),这应该会使任何代码迁移变得更容易一些。

这个主题太大了,无法在一章中涵盖(它通常被归入整本书),但接下来的概述应该会让您对着色器及其使用有一个良好的感觉,以及它们是否是您想要在某个时候解决的问题。

阴影管线

如果你对 OpenGL 或 Direct3D 稍有了解,仅仅提到术语着色器可能会让你不寒而栗。它们似乎是神秘的护身符,属于最神秘的图形圣职圈。

并非如此。

版本 1 的“固定功能”管道是指顶点和片段的照明和着色。例如,您最多可以拥有八个灯光,每个灯光都有不同的属性。灯光照亮表面,每个表面都有自己的属性,称为材质。结合这两者,我们得到了一个相当好的,但有限制的,通用的照明模型。但是如果你想做一些不同的事情呢?如果你想让一个表面根据它的相对照度渐变到透明,那该怎么办呢?如果你想精确地模拟阴影,比如说,土星的光环,投射在云层上,或者日出前你看到的苍白的冷光,那该怎么办?鉴于固定函数模型的局限性,所有这些都几乎是不可能的,尤其是最后一个模型,因为一旦你开始考虑大气中的水分、反向散射等等的影响,照明方程就非常复杂。好吧,一个可编程的管道,让你不需要使用任何技巧,如纹理组合器,就可以模拟那些精确的方程,这正是版本 2 给我们的。

阴暗的三角形

我将从安装 Eclipse 和 Android SDK 时应该已经安装的 Android 示例项目开始。您应该在诸如samples/android-10的目录中找到它们。寻找巨大的 ApiDemo。编译时,它会给你一个冗长的菜单,演示从 NFC 到通知的一切。向下滚动到图形部分,并导航到 OpenGL ES/OpenGL ES2.0 演示。这显示了一个简单的旋转和纹理三角形,如图 Figure 10–1 所示。

Image

图 10–1。Android SDK 的着色器实例

那么,这是怎么做到的呢?

ES 2 的流水线架构允许你在几何处理中有两个不同的访问点,如图 Figure 10–2 所示。第一个将每个顶点以及各种属性(例如,xyz 坐标、颜色、不透明度)信息交给您控制。这被称为顶点着色器。此时,由您来决定这个顶点应该是什么样子,以及它应该如何与所提供的属性一起呈现。完成后,顶点被送回硬件,用你计算的数据栅格化,并作为 2D 位传递给你的片段(或像素)着色器。在这里,您可以根据需要组合纹理,进行任何最终处理,并将其传递回系统,最终在帧缓冲区中进行渲染。

如果这听起来像是对场景中以 60 fps 的速度咆哮的每个对象的每个片段做了大量的工作,那么你是对的。但从根本上说,着色器是实际加载并运行在图形硬件本身上的小程序,因此速度非常快。

Image

图 10–2。OpenGL ES 2 架构概述

着色器结构

顶点和片段着色器在结构上是相似的,看起来有点像一个小的 C 程序。在 C 语言中,入口点总是被称为main(),而语法也非常类似 C 语言。

着色器语言称为 GLSL(不要与它的 Direct3d 对应物 HLSL 混淆),包含一组丰富的内置函数,属于三个主要类别之一:

  • 面向图形处理的数学运算,如矩阵、向量、三角函数、导数和逻辑函数
  • 纹理采样
  • 小型辅助工具,如模、比较和赋值器

值在以下类型的着色器之间来回传递:

  • 制服,是调用程序传过来的值。这些可能包括变换矩阵或投影矩阵。它们在顶点和片段着色器中都可用,并且必须在每个地方声明为相同的类型。
  • 可变变量(是的,是个听起来很哑的名字),是在顶点着色器中定义的变量,传递给片段着色器。

变量既可以定义为通常的数字原语,也可以定义为基于向量和矩阵的面向图形的类型,如 Table 10–1 所示。

除此之外,您还可以提供修饰符来定义基于 int 和基于 float 的类型的精度。这些可以是 highp (24 位)、mediump (16 位)或 lowp (10 位),默认为 highp。所有的变换都必须在 highp 中完成,而颜色只需要在 mediump 中完成。(不过,我不明白为什么 bools 没有精度限定符。)

任何基本类型都可以声明为常量变量,比如const float x=1.0

结构也是允许的,看起来就像它们的 C 对应物。

限制

由于着色器驻留在 GPU 上,它们自然有许多限制,从而限制了它们的复杂性。它们可能受到“指令计数”、允许的统一数量(通常为 128)、临时变量数量以及循环嵌套深度的限制。不幸的是,在 OpenGL ES 上,没有真正的方法从硬件中获取这些限制,所以你只能知道它们的存在,并使你的着色器尽可能小。

顶点着色器和片段着色器之间也存在差异。例如,highp 支持在片段着色器上是可选的,而在顶点着色器上是强制的。呸。

回到旋转的三角形

所以,现在让我们跳回三角形的例子,分解一个基本的 OpenGL ES 2 程序是如何构造的。正如您将看到的,生成着色器的过程与生成大多数其他应用没有什么不同。您已经有了基本的编译、链接和加载序列。清单 10–1 展示了这个过程的第一部分,编译。

注意:清单 10–1 中的代码来自 ApiDemo 包中名为GLES20TriangleRenderer.java的 Android 示例。

清单 10–1编译一个着色器

`    private int createProgram(String vertexSource, String fragmentSource) {     //1
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);   //2
        if (vertexShader == 0) {
            return 0;
        }

int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

int program = GLES20.glCreateProgram()**;                                   //3
        if (program != 0) {
            GLES20.
glAttachShader
(program, vertexShader);                         //4
            GLES20.glAttachShader(program, pixelShader);
            GLES20.glLinkProgram(program);                                        //5
            int[] linkStatus = new int[1];
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); //6
            if (linkStatus[0] != GLES20.GL_TRUE) {
                Log.e(TAG, "Could not link program: ");
                Log.e(TAG, GLES20.glGetProgramInfoLog(program));
                GLES20.glDeleteProgram(program);
                program = 0;
        }
    }
        return program;                                                         //7
    }

private int loadShader(int shaderType, String source) {
        int shader = GLES20.glCreateShader(shaderType);                         //8
        if (shader != 0) {
            GLES20.glShaderSource(shader, source);                              //9
            GLES20.glCompileShader(shader);                                     //10
            int[] compiled = new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);//11
            if (compiled[0] == 0) {
                Log.e(TAG, "Could not compile shader " + shaderType + "😊;
                Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
                GLES20.glDeleteShader(shader);
                shader = 0;
        }
    }
        return shader;                                                          //12
    }`

在前面的例子中,createProgram()是从您的onSurfaceCreated()方法中调用的,这里完成了给定应用的大部分初始化工作。那么,让我们来追溯一下到底发生了什么:

  • createProgram()的参数列表中,如第 1 行所示,来自两个着色器的实际可执行代码的字符串被传递。您可以这样做,或者将它们作为文本文件读入。清单 10–2 有两个着色器,稍后会讨论。
  • 行 2ff 为两个程序调用loadShader()并返回各自的句柄。
  • 第 3 行创建了一个空的程序对象。这托管两个着色器并执行兼容性检查。
  • 第 4f 行将两个着色器都附加到程序。
  • 链接出现在第 5 行。建议检查任何可能的错误,如下所示。不仅仅是一个错误代码,GLSL 还会返回非常好的消息,比如:ERROR: 0:19: Use of undeclared identifier 'normalX'
  • 如果一切正常,我们可以在第 7 行返回程序。
  • 第 8ff 行中定义的loadShader(),执行实际的编译。它首先获取原始源文本,并使用glCreateShader()创建指定类型的着色器(顶点或片段)。这将返回一个空对象,然后通过第 9 行的glShaderSource()将其绑定到源文本。
  • 第 10 行编译实际的着色器,第 11ff 行像以前一样进行错误检查,而第 12 行返回经过验证和编译的对象。

如果你想进一步检查你的着色器代码,你可以用glValidateProgram()来“验证”它。验证是 OpenGL 实现者返回关于代码任何方面的信息的一种方式,比如推荐的改进。您将主要在开发过程中使用它。

着色器现在可以使用了。当在它们和您的调用代码之间来回传递值时,您可以随时指定使用哪一个。这个稍后会讲到。现在,让我们仔细看看这两个演示着色器。这个例子的作者选择将着色器文本定义为一个大的静态字符串。其他人选择从文件中读取它们。但是在这种情况下,我将它们从原来的字符串重新格式化,使它们更具可读性。清单 10–2 涵盖了着色器对的前半部分。

清单 10–2。 顶点着色器

        uniform mat4 uMVPMatrix;                                      //1         attribute vec4 aPosition;                                     //2         attribute vec2 aTextureCoord;         varying vec2 vTextureCoord                                    //3         void main                                                     //4         {             gl_Position = uMVPMatrix * aPosition;                     //5             vTextureCoord = aTextureCoord;                            //6         }

现在仔细看看:

  • 第 1 行定义了从调用程序传入的 4x4 模型/视图/透视矩阵 uniform 。如果你想在一个着色器或者更高的层次上执行实际的变换,这确实是一个风格的问题。并且注意,制服必须在着色器中实际使用,而不仅仅是声明;否则,如果您的调用程序试图引用它,将会失败。
  • 第 2f 行声明了我们也在调用代码中指定的属性。请记住,属性是直接映射到每个顶点的数据数组,仅在顶点着色器中可用。在这种情况下,它们是顶点(被称为术语位置)及其对应的纹理坐标。对每个顶点调用一次着色器。
  • 第 3 行声明了纹理坐标的一个变量
  • 与好的 ol' C 一样,入口点是 a main(),如第 4 行所示。
  • 在第 5 行,位置(顶点)乘以矩阵。您可以在着色器中或调用软件中完成此操作,这是更传统的方法。
  • 最后,第 6 行只是将纹理坐标复制到其变化的副本,这样它就可以被片段着色器拾取。

现在真正的魔术发生在片段着色器中,如清单 10–3 所示。

清单 10–3。 片段着色器

`        precision mediump float;                                                //1
        varying vec2 vTextureCoord;                                             //2

uniform sampler2D sTexture;                                             //3

void main()
        {
            gl_FragColor = texture2D(sTexture, vTextureCoord);                  //4
        }`

  • 如前所述,您可以通过第 1 行指定着色器的精度。
  • 第 2 行声明了变量vTextureCoord。所有变量必须在两个着色器中声明;否则,它将生成一个错误。此外,片段着色器中的变量是只读的,而它们在顶点着色器中是可读/写的。
  • 第 3 行声明了一个sampler2D对象。采样器是内置的制服,用于将纹理信息传递到片段着色器。其他采样器包括sampler1Dsampler3D
  • 第 4 行的gl_FragColor是一个内置变量,用于将片段的最终颜色传回系统进行显示。在这种情况下,我们只是传回由vTextureCoord定义的特定点的纹理颜色。如果你想做更有趣的事,你可以在这里做。例如,您可以去掉蓝色和绿色部分,只留下红色层来显示,添加运动模糊或演示大气反向散射。

在我们可以使用着色器之前,我们需要在创建程序后立即获得着色器内制服和属性的“位置”或句柄,如前所述。这些用于将任何数据从调用方法传递给 GPU。清单 10–4 显示了onSurfaceCreated()中用于旋转三角形的过程。

清单 10–4。 获取制服和属性的句柄

`    public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
    {
        mProgram = createProgram(mVertexShader, mFragmentShader);                  //1
        if (mProgram == 0) {
            return;
        }
        maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");      //2
        if (maPositionHandle == -1) {
            throw new RuntimeException("Could not get attrib location for aPosition");
        }
        maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord");   //3
        if (maTextureHandle == -1) {
            throw new RuntimeException("Could not get attrib location for
                aTextureCoord");
        }

muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");   //4
        if (muMVPMatrixHandle == -1) {
            throw new RuntimeException("Could not get attrib location for uMVPMatrix");
        }
    }`

第 1 行、第 2 行和第 3 行使用着色器内部的实际名称获得属性的句柄。第 4 行获得矩阵的统一句柄。所有四个句柄都被保存起来,供调用程序的主处理循环使用。传入的GL10接口被忽略,代替了GLES20类的静态方法。

注意:您可以获取 OpenGL 定义的对象位置,也可以在链接前自行设置。后一种方法使您可以确保在代码中的整个着色器系列中,类似的统一或属性都利用相同的句柄。

现在剩下的唯一一件事就是执行一个着色器程序,并将任何数据传递给它。清单 10–5 展示了三角形的整个onDrawFrame()方法来演示这一点。

清单 10–5。 调用和使用着色器

`    public void onDrawFrame(GL10 glUnused)                                      //1
    {
        GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f);                            //2
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glUseProgram(mProgram);                                          //3

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);                             //4
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);                 //5
        mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);          //6
        GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, //7
                TRIANGLE_VERTICES_DATA_STRIDE_BYTES,  mTriangleVertices);
        GLES20.glEnableVertexAttribArray(maPositionHandle);                     //8

mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);           //9
        GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false,//10
                TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
        GLES20.glEnableVertexAttribArray(maTextureHandle);                      //11

long time = SystemClock.uptimeMillis() % 4000L;
        float angle = 0.090f * ((int) time);
        Matrix.setRotateM(mMMatrix, 0, angle, 0, 0, 1.0f);                      //12
        Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, mMMatrix, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);

GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);  //13
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);                         //14
    }`

现在,让我们来分解一下:

  • onDrawFrame()的参数是 GL10 对象。但是由于这是一个 OpenGL ES2 应用,GL10 句柄代替静态函数被忽略。
  • 第 2f 行只是清除屏幕的标准内容。
  • 第 3 行是有趣的开始。打开你当时可能想要的任何着色器。你需要多少就有多少,在它们之间自由跳跃。
  • 我们要传递给片段着色器的纹理在第 4 行指定,并由sampler2D对象拾取。此代码代表 GPU 上使用的实际纹理单元。
  • 第 5 行将本地纹理句柄绑定到这个单元。
  • 第 6 行通过将三角形顶点数组对象的内部位置索引设置为实际顶点 xyz 值的起始点,来准备实际的三角形顶点数组对象。
  • 现在我们终于可以把一些东西交给着色器了,如第 7 行所示。在这种情况下,顶点数据通过glVertexAtttribPointer()被发送到着色器,它接受位置属性的句柄maPosition;数据的类型,stride;和指向所述数据的指针。第 8 行允许使用数组。
  • 第 9、10 和 11 行对纹理坐标做了同样的处理。
  • 第 12ff 行使用 Android 自己的矩阵库(android.opengl.Matrix)执行旋转和投影,因为 OpenGL ES 2 没有glRotate / glTranslate / glScale函数。否则,你需要写你自己的数学库。
  • 我们现在可以将之前矩阵操作的结果传递给顶点着色器,使用glUniformMatrix4fv ()和我们之前获得的矩阵句柄。
  • 现在在最后一行,我们称我们的老朋友为glDrawArrays()

所以,你有它。一个“简单的”基于着色器的程序。还不算太糟,是吧?现在我们可以重温我们蹩脚的太阳系模型,并展示如何使用着色器来使它不那么蹩脚。

夜晚的地球

你熟悉用于地球表面的日光图像(图 10–3,左),但你可能也见过类似的地球夜晚图像(图 10–3,右)。如果我们可以在地球的黑暗面显示夜晚纹理,而不仅仅是常规纹理贴图的黑暗版本,那将会是一件好事。

Image

图 10–3白天的地球对夜晚的地球

在 OpenGL 1.1 下,如果不是不可能完成的话,这将是非常棘手的。算法应该相对简单:渲染地球两次,一次用白天的图像,一次用夜晚的图像。然后根据光照改变日光侧地球纹理的日光侧 alpha 通道。当照度达到 0 时,它是完全透明的,夜晚部分显示通过。然而,在 OpenGL ES 2 下,您可以非常容易地对着色器进行编码,以几乎完全匹配算法。(你也可以渲染地球一次,同时提供两种纹理。这项技术将在下一个练习中介绍)。

该程序的结构类似于前面的任何一个程序,有一个“活动”文件、一个渲染器,在本例中还有一个Planet对象。

第一个例子很好,你可能会想,“但是我们实际上如何命令 OpenGL 使用 2。xstuff vs . 1 . x?”清单 10–6 给出了答案。

首先我们需要检测设备是否真的支持 OpenGL ES 2。新的肯定会,但旧的可能不会。iPhone 直到 iOS 3.0 才得到它。这是通过检索配置信息包的getSystemService()方法完成的。如果通过了,只需简单地调用GLSurfaceView() .setEGLContextClientVersion(2)就可以了。

清单 10–6。调用 OpenGL ES 2

`    private boolean detectOpenGLES20()
    {
         ActivityManager am =
            (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);

ConfigurationInfo info = am.getDeviceConfigurationInfo();
         return (info.reqGlEsVersion >= 0x20000);
    }

protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        GLSurfaceView view = new GLSurfaceView(this);

if (detectOpenGLES20())
        {
               view.setEGLContextClientVersion(2);
        view.setEGLConfigChooser(8,8,8,8,16,1);
               view.setRenderer(new SolarSystemRendererES2(this));

setContentView(view);
        }  
    }`

接下来,地球需要以通常的方式生成。这个实例将使用 50 个切片和 50 个堆栈,同时获取两个纹理,如清单 10–7 所示。

清单 10–7。 初始化地球

`    private void initGeometry(GL10 glUnused)
    {
       String extensions = GLES20.glGetString(GL10.GL_EXTENSIONS);

m_DayTexture=createTexture(glUnused, m_Context,
               book.SolarSystem.R.drawable.earth_light);
       m_NightTexture=createTexture(glUnused, m_Context,
               book.SolarSystem.R.drawable.earth_night);

m_Earth = new Planet(50, 50, 1.0f, 1.0f, glUnused, myAppcontext,true,-1);
    }`

onSurfaceCreated()方法在调用initGeometry()的同时加载并初始化两组着色器,如清单 10–8 所示。

清单 10–8。 加载着色器

`    public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
    {
         int program;

m_DaysideProgram=createProgram(m_DaySideVertexShader,m_DaySideFragmentShader);
         m_NightsideProgram=createProgram
            (m_NightSideVertexShader,m_NightSideFragmentShader);

initGeometry(glUnused);

Matrix.setLookAtM(m_WorldMatrix, 0, 0, 5f, -2, 0f, 0f, 0f, 0.0f, 1.0f, 0.0f);    
    }`

与前面的例子没有太大的不同,但是它有更多的制服和属性需要处理。下面,为法线提供了一个新的属性,这样我们可以处理光照。在本例中,我们将特定的标识符绑定到每个属性,以便我们可以确保两组着色器使用相同的值。它有时会让事情变得简单一点。而这个必须在链接前完成。

 GLES20\. glBindAttribLocation(*program, ATTRIB_VERTEX, "aPosition");  GLES20\. glBindAttribLocation(*program, ATTRIB_NORMAL, "aNormal");  GLES20\. glBindAttribLocation(*program, ATTRIB_TEXTURE0_COORDS, "aTextureCoord");

除了用于模型/视图/投影矩阵的制服之外,还有两个新制服需要处理。与前面的例子不同,我们仍然必须在链接后获取位置,所以不能保证它们的位置在程序的其他实例中是在相同的位置,除非程序有相同的变量集。在这里,我将所有统一的句柄缓存到一个数组中,该数组应该适用于两组着色器。新制服是为普通矩阵和光位置准备的。(对于非常简单的模型,你可以在顶点着色器中硬编码灯光的位置。)

m_UniformIDs[*UNIFORM_MVP_MATRIX*]=GLES20.*glGetUniformLocation*(program,                                 "uMVPMatrix") ; m_UniformIDs[*UNIFORM_NORMAL_MATRIX*]=GLES20.*glGetUniformLocation*(program,                                 "uNormalMatrix"); m_UniformIDs[*UNIFORM_LIGHT_POSITION*]=GLES20.*glGetUniformLocation*(program,                                 "uLightPosition");;

因此,添加新制服的流程如下:

  1. 在着色器中声明(即uniform vec3 lightPosition;)。
  2. 使用glGetUniformLocation()获取其“位置”。它只返回该会话的唯一 ID,然后在设置或从着色器获取数据时使用该 ID。(或使用glBindAttribLocation指定具体的位置值。)
  3. 使用众多glUniform*()调用中的一个来动态设置值。

自然,球体生成器也需要做一些修改。利用上一章的交错数据示例,新的draw()方法看起来类似于清单 10–9。

清单 10–9。?? 中兼容 OpenGL ES 2 的绘制方法Planet.java

`    public void draw(GL10 gl,int vertexLocation,int normalLocation,             //1
        int colorLocation, int textureLocation,int textureID)
    {
        //Overrides any default texture that may have been supplied at creation time.

if(textureID>=0)                                                        //2
        {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID);
        }
        else if(m_Texture0>=0)
        {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, m_Texture0);
        }

GLES20.glEnable(GLES20.GL_CULL_FACE);
        GLES20.glCullFace(GLES20.GL_BACK);

m_InterleavedData.position(0);                                          //3

GLES20.glVertexAttribPointer(vertexLocation, 3, GLES20.GL_FLOAT,
        false,m_Stride, m_InterleavedData);
        GLES20.glEnableVertexAttribArray(vertexLocation);

m_InterleavedData.position(NUM_XYZ_ELS);

if(normalLocation>=0)
        {
            GLES20.glVertexAttribPointer(normalLocation, 3, GLES20.GL_FLOAT,
            false,m_Stride, m_InterleavedData);
            GLES20.glEnableVertexAttribArray(normalLocation);
        }

m_InterleavedData.position(NUM_XYZ_ELS+NUM_NXYZ_ELS);

if(colorLocation>=0)
        {
            GLES20.glVertexAttribPointer(colorLocation, 4, GLES20.GL_FLOAT,
            false,m_Stride, m_InterleavedData);
            GLES20.glEnableVertexAttribArray(colorLocation);
        }

m_InterleavedData.position(NUM_XYZ_ELS+NUM_NXYZ_ELS+NUM_RGBA_ELS);

GLES20.glVertexAttribPointer(textureLocation, 2, GLES20.GL_FLOAT,
        false,m_Stride, m_InterleavedData);
        GLES20.glEnableVertexAttribArray(textureLocation);

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, (m_Slices+1)2(m_Stacks-1)+2);
    }`

与其他例程一样,我们忽略传递给它的GL10对象,而是使用GLES20静态调用。

  • 在第 1 行,注意所有的附加参数。这些允许各种属性的句柄或位置在这里传递和使用。
  • 第 2ff 行允许我们使用在对象创建时定义的纹理,或者在运行时交换另一个纹理。
  • 第 3ff 行按照前面演示的标准方式设置属性指针。为特定类型的数据设置每个指针后,交叉索引将前进到下一个数据块的开始处。

接下来我们可以看看实际的着色器,特别是清单 10–10 中的顶点。与前面的一样,为了可读性,这些着色器已被重新格式化。请注意,对于这个示例和下一个示例,白天和夜晚的顶点着色器是相同的。

清单 10–10。 白天和黑夜两边的顶点着色器

`        attribute vec4 aPosition;
        attribute vec3 aNormal;                                                 //1
        attribute vec2 aTextureCoord;
        varying vec2 vTextureCoord;
        varying lowp vec4 colorVarying;
        uniform vec3 uLightPosition;                                            //2
        uniform mat4 uMVPMatrix;
        uniform mat3 uNormalMatrix;                                             //3

void main()
        {
            vTextureCoord = aTextureCoord;
            vec3 normalDirection = normalize(uNormalMatrix * aNormal);//4
            float nDotVP = max(0.0, dot(normalDirection, normalize(uLightPosition)));
            vec4 diffuseColor = vec4(1.0, 1.0, 1.0, 1.0);
            colorVarying = diffuseColor * nDotVP;
            gl_Position = uMVPMatrix * aPosition;                               //5
         }`

这里我们有三个新的参数要担心,更不用说照明了。

  • 线 1 是这个顶点的法线属性,当然是照明解决方案所需要的。

  • 第 2 行通过制服提供灯光的位置。

  • 第 3 行支持法线矩阵。为什么法线应该像顶点一样,却有一个单独的矩阵?在大多数情况下,它们是正常的,但是法线在某些情况下会分解,例如当仅在一个方向上不均匀地缩放几何体时。因此,要将其与这些情况隔离开来,需要一个单独的矩阵。

  • Lines 4ff do the lighting calculations. First the normal is normalized (I always get a kick out of saying that) and when multiplied by the normal's matrix produces the normalized normal direction. Normally.

    之后,我们取法线方向和归一化光线位置的点积。它给出了给定顶点的光强。

    之后,漫射颜色被定义。它被设置为全 1,因为阳光被定义为白色。(提示,设为红色真的看起来很酷。)漫射颜色乘以强度,然后将最终结果传递给片段着色器。

  • 第 5 行通过将原始顶点乘以模型/视图/投影矩阵来处理顶点的最终位置。gl_Position是一个专用的内置变量,不需要声明。

两侧的碎片着色器是不同的,因为黑暗面处理照明的方式不同于日光面。清单 10–11 是白天的片段着色器。

清单 10–11。 地球日光面的片段着色器

    varying lowp vec4 colorVarying;     precision mediump float;     varying vec2 vTextureCoord;     uniform sampler2D sTexture;     void main()     {         gl_FragColor = texture2D(sTexture, vTextureCoord)*colorVarying;     }

这看起来应该和三角形的着色器一样,除了添加了colorVarying。这里,从sTexture得到的输出乘以最终结果的颜色。

然而,夜间的事情会更有趣一些,如清单 10–12 所示。

清单 10–12。 地球黑夜面的碎片着色器

    varying lowp vec4 colorVarying;     precision mediump float;     varying vec2 vTextureCoord;     uniform sampler2D sTexture;     void main()     {         vec4 newColor;         newColor=1.0-colorVarying;         gl_FragColor = texture2D(sTexture, vTextureCoord)*newColor;     }

你会注意到参数与另一个着色器相同,但是我们得到了几行额外的代码来计算夜晚的颜色。因为我们可以根据光照从一个纹理到另一个纹理进行混合,所以夜晚的颜色应该是 1.0 日光色。GLSL 漂亮的向量库使得这样的数学运算变得非常简单。

清单 10–13 显示了完成所有操作的onDrawFrame()

清单 10–13。 把所有的东西放在一起

`    public void onDrawFrame(GL10 glUnused)
    {

GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

m_Angle+=0.20;

Matrix.setRotateM(m_MMatrix, 0,m_Angle, 0, 1.0f, 0.0f);                 //1
        Matrix.multiplyMM(m_MVMatrix, 0, m_WorldMatrix, 0, m_MMatrix, 0);
        Matrix.multiplyMM(m_MVPMatrix, 0, m_ProjMatrix, 0, m_MVMatrix, 0);

m_NormalMatrix[0]=m_MVMatrix[0];                                        //2
        m_NormalMatrix[1]=m_MVMatrix[1];
        m_NormalMatrix[2]=m_MVMatrix[2];
        m_NormalMatrix[3]=m_MVMatrix[4];
        m_NormalMatrix[4]=m_MVMatrix[5];
        m_NormalMatrix[5]=m_MVMatrix[6];
        m_NormalMatrix[6]=m_MVMatrix[8];
        m_NormalMatrix[7]=m_MVMatrix[9];
        m_NormalMatrix[8]=m_MVMatrix[10];

GLES20.glUseProgram(m_NightsideProgram);                                //3
        checkGlError("glUseProgram:nightside");

GLES20.glUniformMatrix4fv(m_UniformIDs[UNIFORM_MVP_MATRIX], 1, false,
            m_MVPMatrix, 0);
        GLES20.glUniformMatrix3fv(m_UniformIDs[UNIFORM_NORMAL_MATRIX], 1, false,
            m_NormalMatrix,0);
        GLES20.glUniform3fv(m_UniformIDs[UNIFORM_LIGHT_POSITION], 1, m_LightPosition,0);

m_Earth.setBlendMode(m_Earth.PLANET_BLEND_MODE_FADE);                   //4
        m_Earth.draw(glUnused,ATTRIB_VERTEX,ATTRIB_NORMAL,-1,
                                ATTRIB_TEXTURE0_COORDS,m_NightTexture);
        checkGlError("glDrawArrays");

GLES20.glUseProgram(m_DaysideProgram);                                  //5
        checkGlError("glUseProgram:dayside");

GLES20.glUniformMatrix4fv(m_UniformIDs[UNIFORM_MVP_MATRIX], 1, false,
            m_MVPMatrix, 0);
        GLES20.glUniformMatrix3fv(m_UniformIDs[UNIFORM_NORMAL_MATRIX], 1, false,
            m_NormalMatrix,0);
        GLES20.glUniform3fv(m_UniformIDs[UNIFORM_LIGHT_POSITION], 1, m_LightPosition,0);

m_Earth.draw(glUnused,ATTRIB_VERTEX,ATTRIB_NORMAL,-1,
            ATTRIB_TEXTURE0_COORDS,m_DayTexture);
        checkGlError("glDrawArrays");
    }`

事情是这样的:

  • 第 1ff 行执行预期的旋转,首先在 Y 轴上,乘以世界矩阵,然后乘以投影矩阵。
  • 第 2ff 行有点欺骗。还记得我之前说过需要一个正规矩阵吗?在简化的情况下,我们可以只使用模型视图矩阵,或者至少是它的一部分。由于法线矩阵只有 9x9(避开了*移分量),我们将它从更大的 4x4 模型视图矩阵的旋转部分中切掉。
  • 现在程序的夜间部分被切换进来,如第 3 行所示。之后,制服被填充。
  • 第 4 行设置了一个类似于 OpenGL ES 1.1 的混合模式。在这种情况下,我们推动系统实际上认识到阿尔法是用来管理半透明。阿尔法值越低,这个碎片越透明。就在那之后,黑暗面被画了出来。
  • 第 5 行现在将我们切换到了日光程序,并且做了许多相同的事情。

图 10–4 应该是结果。你现在可以看到做非常微妙的效果是多么容易,例如满月的光照或太阳在海洋中的反射。

Image

图 10–4。 一点一点照亮黑暗

带来云彩

所以,看起来肯定是缺少了什么。是啊。那些云一样的东西。嗯,我们很幸运,因为着色器也可以很容易地管理这一点。在可下载的项目文件中,我添加了整个地球的云图,如图 Figure 10–5 所示。大块的陆地有点难以看清,但在右下角是澳大利亚,而在左半部你应该能分辨出南美。所以,我们的工作是将它覆盖在彩色风景地图上,去掉所有的暗点。

Image

图 10–5全地球云图案

我们不仅要添加云到我们的模型中,我们还将看到如何使用着色器处理多重纹理,例如,如何告诉一个着色器使用多个纹理?还记得第六章中关于纹理单元的那一课吗?它们现在真的很方便,因为那是纹理存储的地方,为片段着色器拾取它们做好了准备。通常,对于单一纹理,系统默认不需要额外的设置,除了对glBindTexture()的正常调用。但是,如果您想要使用多个,需要进行一些设置。步骤如下:

  1. 在你的主程序中加载新的纹理。
  2. 添加第二个uniform sampler2D到你的片段着色器来支持第二个纹理,并通过glGetUniformLocation()拾取它。
  3. 告诉系统哪个纹理单元使用哪个采样器。
  4. 在主循环中激活所需的纹理并将其绑定到指定的 tu。

现在来看几个细节:你已经知道如何加载纹理;当然,这是显而易见的。因此,对于步骤 2,您将需要向片段着色器添加如下内容,与前两个练习中使用的相同:

    uniform sampler2D sCloudTexture;

并且到createProgram():

`m_UniformIDs[UNIFORM_SAMPLER0] = GLES20.glGetUniformLocation(program, "sTexture);

m_UniformIDs[UNIFORM_SAMPLER1] = GLES20.glGetUniformLocation(program, "sCloudTexture");`

第三步添加到onSurfaceCreated()glUniform1i()调用将片段着色器中统一的位置作为第一个参数,将实际的纹理单元 ID 作为第二个参数。所以在这种情况下,sampler0被绑定到纹理单元 0,而sampler1去纹理单元 1。由于单个纹理总是默认为 TU0 以及第一个采样器,设置代码并不是普遍需要的。

`    GLES20.glUseProgram(m_DaysideProgram);
    GLES20.glUniform1i(m_UniformIDs[UNIFORM_SAMPLER0],0);
    GLES20.glUniform1i(m_UniformIDs[UNIFORM_SAMPLER1],1);

GLES20.glUseProgram(m_NightsideProgram);
    GLES20.glUniform1i(m_UniformIDs[UNIFORM_SAMPLER0],0);
    GLES20.glUniform1i(m_UniformIDs[UNIFORM_SAMPLER1],1);`

当在onDrawFrame()中运行主循环时,在步骤 4 中,你可以执行以下操作来打开两个纹理:

`    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,m_NightTexture);

GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,m_CloudTexture);

GLES20.glUseProgram(m_NightsideProgram);`

glActiveTexture()指定使用什么 TU,然后调用绑定纹理。之后,该程序可以用于预期的效果。

“cloud-lovin”片段现在应该看起来类似于清单 10–14 来执行实际的混合。

清单 10–14。 将第二个纹理和云彩混合在一起

`    varying lowp vec4 colorVarying;
    precision mediump float;
    varying vec2 vTextureCoord;
    uniform sampler2D sTexture;
    uniform sampler2D sCloudTexture;                                 //1

void main()
    {
        vec4 cloudColor;
        vec4 surfaceColor;
        cloudColor=texture2D(sCloudTexture, vTextureCoord );            //2
        surfaceColor=texture2D(sTexture, vTextureCoord );

if(cloudColor[0]>0.2)                                               //3
                {
                    cloudColor[3]=1.0;
                    gl_FragColor=(cloudColor1.3+surfaceColor.4)colorVarying;
                }
            else
            gl_FragColor = texture2D(sTexture, vTextureCoord)
colorVarying;
    }`

下面是正在发生的事情:

  • 第 1 行仅仅声明了新的云纹理。

  • 在第 2 行中,我们从 cloud sampler 对象中获取云的颜色。

  • The new color is filtered and merged with the earth's surface image in lines 3ff. Since the clouds are neutral in color, all we need to do is to analyze one color component, red in this case. If it is brighter than a given value, then blend it with the earth's surface texture. The numbers used are quite arbitrary and can be tweaked based on your taste. Naturally, much of the finer detail will have to be cut out to ensure the colored landmasses show through.

    cloudColor用 1.3 的乘数稍微增加了一点,而下面的表面只使用了 0.4 的乘数,以便更加强调云彩,同时仍然使它们相对不透明。

    低于阈值 0.2,只需发回表面着色。

    由于云是灰度对象,我只需要选取一种颜色进行测试,因为正常的 RGB 值是相同的。所以,我选择处理所有比 0.2 亮的纹理元素。然后,我确保 alpha 通道为 1.0,并将所有三个组件组合在一起。

理想情况下,您会看到类似图 10–6 的内容。这就是我所说的行星!

Image

图 10–6。 给地球增加云层

但是镜面反射呢?

就像任何其他闪亮的东西一样(地球在蓝色部分是闪亮的),你可能会看到太阳在水中的反射。嗯,你是对的。图 10–7 显示了地球的真实图像,正中间是太阳的反射。让我们在自己的地球上试试吧。

Image

图 10–7。 从太空中看到的地球,它反射着太阳

自然,我们将不得不编写自己的镜面反射着色器,或者,在这种情况下,将它添加到现有的日光着色器中。

清单 10–15 是针对日光顶点着色器的。我们只做一面,但是满月可能会对夜晚有类似的影响。在这里,我预先计算了镜面反射信息和正常的漫反射颜色,但是这两者是分开的,直到碎片着色器,因为不是地球的所有部分都是反射的,所以陆地不应该得到镜面反射处理。

清单 10–15。 用于镜面反射的日光顶点着色器

`                attribute vec4 aPosition;
                attribute vec3 aNormal;
                attribute vec2 aTextureCoord;
                varying vec2 vTextureCoord;
                varying lowp vec4 colorVarying;
                varying lowp vec4 specularColorVarying;           //1
                uniform vec3 uLightPosition;
                uniform vec3 uEyePosition;
                uniform mat4 uMVPMatrix;
                uniform mat3 uNormalMatrix;

void main()
            {
                          float shininess=25.0;
                          float balance=.75;
                          float specular=0.0;
                          vTextureCoord = aTextureCoord;
                          vec3 normalDirection = normalize(uNormalMatrix * aNormal);
                          vec3 lightDirection = normalize(uLightPosition);
                          vec3 eyeNormal = normalize(uEyePosition);
                          vec4 diffuseColor = vec4(1.0, 1.0, 1.0, 1.0);
                          float nDotVP = max(0.0, dot(normalDirection, lightDirection));
                                                                                       //2
                          float nDotVPReflection = dot(reflect(-
                lightDirection,normalDirection),eyeNormal);
                          specular = pow(max(0.0,nDotVPReflection),shininess)balance; //3
                          specularColorVarying=vec4(specular,specular,specular,0.0);   //4
                          colorVarying = diffuseColor * nDotVP
1.3;
                          gl_Position = uMVPMatrix * aPosition;
            }`

  • 第 1 行声明了一个可变变量,将镜面照明交给片段着色器。
  • 我们现在需要得到光线反射和法线的点积,以法线方式乘以法线矩阵。二号线。注意reflect()方法的使用,这是着色器语言的另一个优点。reflect()根据负光方向和局部法线生成反射向量。然后点缀着eyeNormal
  • 在第 3 行,前面的点积被用来产生实际的镜面反射分量。你还会看到我们的老朋友 shininess,就像 OpenGS ES 的 1 版一样,数值越高,反射越窄,越“热”。
  • 因为我们可以认为太阳的颜色只是白色,所以第 4 行中的镜面反射颜色可以将其所有组件设置为相同的值。

现在片段着色器可以用来进一步细化,如清单 10–16 所示。

清单 10–16。 处理镜面反射的碎片着色器

`        varying lowp vec4 colorVarying;
        varying lowp vec4 specularColorVarying;                                   //1
        precision mediump float;
        varying vec2 vTextureCoord;
        uniform sampler2D sTexture;
        uniform sampler2D sCloudTexture;

void main()

{
            vec4 finalSpecular=vec4(0,0,0,1);

vec4 cloudColor;
            vec4 surfaceColor;
            float halfBlue;

cloudColor=texture2D(sCloudTexture, vTextureCoord );
            surfaceColor=texture2D(sTexture, vTextureCoord );

halfBlue=0.5*surfaceColor[2];                                          //2

if(halfBlue>1.0)                                                       //3
            halfBlue=1.0;
            if((surfaceColor[0]<halfBlue) && (surfaceColor[1]<halfBlue))           //4
            finalSpecular=specularColorVarying;

if(cloudColor[0]>0.2)
        {
                cloudColor[3]=1.0;
                gl_FragColor=(cloudColor1.3+surfaceColor.4)colorVarying;
        }
            else
            gl_FragColor=(surfaceColor+finalSpecular)
colorVarying;                //5
    }`

这里的主要任务是确定哪些碎片代表海,哪些不代表海。这很简单。蓝色的东西是水(强大的水湿的东西!)而不是的一切都不是。

  • 在第 1 行,我们选择了specularColorVarying变量。
  • 在第 2 行中,我们选取蓝色分量并将其一分为二,将其夹在第 3 行中,因为实际上没有颜色可以超过最大强度。
  • 第 4 行进行过滤。如果红色和绿色的成分都不到蓝色的一半,那么我们可以在水面上绘制镜面反射,而不是像乍得那样的地方。
  • 在第一次与colorVarying相乘后,镜面反射部分现在被添加到最后一行的碎片颜色中,因为这将与其他所有东西一起调制它。

图 10–8 显示了没有云的结果,图 10–9 显示了有云的结果。

Image

图 10–8。 右边地球/水界面的特写

Image

图 10–9。 完工的地球,至少目前来看是

这只是一个简单的例子,使用着色器来增强你渲染的场景的真实感。例如,当涉及到空间主题时,您可能会在一个行星周围生成一个朦胧的大气或 3D 体积纹理来模拟星系或星云。要是我再有十章就好了…

如果你想继续复制整个第八章的项目,用镜头光晕、小部件等等来获得额外的学分,请随意。

总结

在这最后一章中,你学习了一些关于 OpenGL ES 2 的知识,ES 的可编程管道版本,看到了着色器如何和在哪里适合它,并使用它们给地球添加一些额外的细节。然而,对于额外的学分,请参见关于将模拟器的其余部分移植到版本 2。

在这本书里,你已经学习了基本的 3D 理论,包括数学和整体原理。我认为它给了你对这个主题的基本感觉或理解,即使知道这本书可能会大很多倍,考虑到我们几乎没有接触过 3D 图形。

Khronos Group 是官方 OpenGL 的守护者,已经出版了几本关于这个主题的书籍。根据封面的颜色,它们被亲切地称为红皮书(官方编程指南)、蓝皮书(教程和参考)、黄皮书(着色语言)、绿皮书(Mac 上的 Open GL)和略带紫色的书籍(OpenGL ES 2)。还有许多其他第三方书籍,它们比我所能找到的要深入得多。同样,网上有许多致力于 OpenGL 教程的网站;到目前为止是最好的之一,在撰写本文时有* 50 个不同的教程。NVidia 有一系列优秀的大师级书籍可供免费下载,名为 GPU Gems。这些包括从渲染水焦散到起伏的草地。它们确实值得一看。

当你阅读其他作者的作品时,不管是从其他书上还是在网上,只要记住这本书是给你太阳、地球和星星的那本书。没有几个人能这么说。

posted @   绝不原创的飞龙  阅读(90)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示