Introduction to shaders
OpenGL ES 2.0中引入的可编程图形管道。
OpenGL ES 2.0是第一个支持可编程图形管道的OpenGL ES版本。 在此之前,OpenGL ES 1.x是一个固定功能的移动图形API。 固定功能图形API允许你指定GPU要处理的各种数据位(位置,颜色等),以及用于使用该数据的严格命令集。
例如,可以让GPU在指定的场景中绘制多边形,可以指定该多边形的材质的反射率,并向场景添加灯光。 然后GPU会开始计算多边形在场景中出现的颜色。
但是,如果想使用与API内置的不同照明方程时将会发生什么? 如果想出一个很棒的新图形技术但API不支持它怎么办? (例如帧缓冲对象,立方体贴图等)。 你非常依赖API的内容; 有一组固定的功能,这些功能的选项有限。 这绝不会涵盖作为开发人员可能想要做的所有事情。 虽然可以扩展API以支持更多选项和更多技术,但依赖于GPU供应商实现这些扩展。
这就是固定功能图形管道不足的地方。 与其创建数百个扩展和功能来支持所有请求的用例,还不如提供创建可编程管道。 可编程管道允许开发人员提供在管道的各个阶段在GPU上运行的自定义代码。 每段代码都称为着色器,因为它们可以包含任意代码,这意味着开发人员可以(几乎)自由地做任何他们喜欢的事情。
当然,所有这些自由都需要付出代价。 由于OpenGL ES 2.0中没有默认着色器,因此必须始终编写自己的着色器。
值得一提的是,OpenGL ES 1.x和OpenGL 2.0以上版本不能混合,OpenGL ES 2.0也不能向后兼容。
Portability
由于着色器是在GPU上运行的类C程序,你可能会想,“如何编译这些着色器?”。 因为每个GPU供应商(有时每个GPU)都有不同的指令集,我们需要一种方法来为每个不同的设备编译着色器。
值得庆幸的是,这可以在OpenGL ES中使用“在线”编译。 在运行时向OpenGL ES API提供着色器的源代码,OpenGL ES驱动程序使用设备本身的编译器来编译设备指令集的代码。
// Create a shader object (we'll see the types of shader you can create later).
glCreateShader(...);
// Add the source code to the shader object.
glShaderSource(...);
// Compile the shader source code.
glCompileShader(...);
复制代码
着色器在程序中协同工作。 这类似于编译普通C代码时发生的情况:将源代码编译为目标文件(.o),然后将它们链接到一个可执行文件中。 在这里,编译着色器,将它们添加到程序中,然后将它们链接在一起。
// Create a program object.
glCreateProgram();
// Attach shader objects to that program.
glAttachShader(...);
// Link the shaders objects inside a program.
glLinkProgram(...);
复制代码
这将提供可在应用程序中使用的程序对象。
// Tell OpenGL ES which program you want to use.
glUseProgram( GLuint program);
复制代码
这里的缺点当然是任何想要的人都能看到你的着色器代码,它将包含你最高机密的超级图形技术。 为了保持着色器源代码的正确性,大多数GPU供应商还提供了一个“离线”编译器,可以为其特定平台生成二进制文件。 然后,这些二进制文件可以随应用程序一起提供,并在运行时使用OpenGL ES API加载。 必须为要运行的每个平台创建和打包二进制文件并选择正确的二进制文件。
可以通过将glShaderSource交换为glShaderBinary来加载二进制文件。
The OpenGL ES Programmable Pipeline
顶点和片段阶段是OpenGL ES 2.0中的可编程阶段。
Vertex Shaders
在顶点着色器中编写的代码将在每个顶点运行一次。 因此,如果告诉OpenGL ES绘制十个未连接的三角形,的顶点着色器将运行30次。
顶点着色器唯一的工作是发出顶点的最终位置。
在一个非常简单的例子中,顶点着色器可以简单地将每个顶点的位置设置为(0,0,0)。 这不需要将任何数据传递到着色器。 这里的#version 100告诉OpenGL ES我们正在使用OpenGL ES着色器语言的版本1。
#version 100
// Trivial vertex shader.
void main()
{
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
复制代码
更常见的是,可以传递要绘制的形状的坐标,并将它们用作位置。 为此,需要为要绘制的每个顶点传递一些数据。 使用vertex属性在OpenGL ES中处理这种类型的顶点数据。
如果要绘制单个三角形,则需要三个顶点。 假设(1.0,0.0,0.0),(0.0,1.0,0.0)和(-1.0,0.0,0.0)。
为了获得这些数据,我们可以将数组[1.0,0.0,0.0,0.0,1.0,0.0,-1.0,0.0,0.0]传递给OpenGL ES并将其与3分量属性(例如vec3)相关联。 当告诉OpenGL ES想要绘制单个三角形时,它将分割数组并将每个3分量位置传递给不同的顶点着色器。
然后我们可以使用此数据在顶点着色器中设置顶点的位置。
// Host code to set data up to be used in the shader.
// Create you C array of vertex positions.
GLfloat [] positions = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0};
// To associate this data with a particular input to the shader, we must get that inputs "location" in the shader. Note, this can only be done after the glProgram has been linked.
GLint position_location = glGetAttribLocation("position");
// Finally, tell OpenGL ES to use the 'positions' array as the input data.
glVertexAttribPointer(position_location, ..., positions);
复制代码
#version 100
// Vertex shader which takes position as a per-vertex input.
// Inputs
attribute vec4 position;
void main()
{
gl_Position = position;
}
复制代码
好的,我们现在可以将数据传递到顶点着色器并设置合理的位置。 好消息是,我们可以使用这些相同的技术传递我们希望每个顶点不同的任何数据。 因此,如果我们想要将颜色与每个顶点相关联,我们也可以这样做。
// Same code as before to set data up to be used in the shader.
GLfloat [] colours = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0};
GLint colour_location = glGetAttribLocation("colour");
glVertexAttribPointer(colour_location, ..., colours);
复制代码
#version 100
// Vertex shader that takes in per-vertex position and colour inputs.
// Inputs
attribute vec4 position;
attribute vec4 colour;
void main()
{
gl_Position = position;
}
复制代码
现在你正在考虑如果我能输出的唯一东西是顶点位置,那么顶点着色器中的colour有什么用处。 好吧,事实证明,还可以从顶点着色器创建任意输出,该着色器将在绘制的形状上进行插值。 这些被称为变化(varyings)。 输出的变化(varyings)必须与指定的变化(varyings)完全匹配,作为片段着色器的输入。
#version 100
// Vertex shader that takes in per-vertex colour data and outputs it as interpolated per-fragment data.
// Inputs
attribute vec4 position;
attribute vec4 colour;
// Outputs
varying vec4 interpolated_colour;
void main()
{
interpolated_colour = colour;
gl_Position = position;
}
复制代码
The Rasterizer
我们的管道图中有两个阶段在顶点和片段阶段之间:原始汇编和光栅化。 由于所有现代显示技术都是基于栅格的(它们只能显示彩色点),我们需要一些方法将点和三角形的概念转换为屏幕上的点。 光栅化步骤为我们完成了这一部分,确定哪些基元覆盖哪些点,然后为这些点生成片段作业。 在生成片段作业时,它使用称为重心坐标的东西计算变换(varyings)的插值。 但在下面的示例中,左侧有一个红色,绿色和蓝色的三角形,指定为三个顶点的颜色属性。 在右侧,颜色已在三角形内插值。
Fragment Shaders
这很好地引导了片段着色器。片段着色器在光栅化器发出的每个片段上运行一次。
片段着色器需要发出的唯一输出是片段的颜色。 因此,为了给出另一个简单的例子,我们可以将每个片段的颜色设置为黑色:(0.0,0.0,0.0,1.0)。
#version 100
// Trivial fragment shader.
void main()
{
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
复制代码
因为我们已经从顶点着色器传递了一些插值颜色,我们可以在这里使用它们。
#version 100
// Fragment shader which takes interpolated colours and uses them to set the final fragment colour.
// Floating point values in fragment shaders must have a precision set.
// This can be done globally (as done here) or per variable.
precision mediump float;
// Inputs
varying vec4 interpolated_colour;
void main()
{
gl_FragColor = interpolated_colour;
}
复制代码
Uniforms
所以回顾到目前为止我们已经涵盖的内容:
- 如何将每个顶点数据放入片段着色器中
- 如何将每个片段数据插入到片段着色器中
常量数据怎么定义? 如果我们有一些所有顶点或片段共有的数据怎么办?
这可以是从时间值到描述场景中光线等级的任何东西。
一个非常常见的用途是使用变换矩阵将顶点定位在场景中。 每个顶点都需要相同的矩阵,以便它们全部转换。
对于这种常量数据,我们有着色器uniforms。 在大多数情况下,声明,设置和使用uniforms与attributes相同,除了只设置了一组值。
#version 100
// Vertex shader which takes a constant input value (a uniform) and uses it to alter all vertex x positions.
// Per-vertex inputs
attribute vec4 position;
attribute vec4 colour;
// Constant inputs
uniform int x_offset;
// Outputs
varying vec4 interpolated_colour;
void main()
{
interpolated_colour = colour;
gl_Position = position + vec4(x_offset, 0.0, 0.0, 0.0);
}
复制代码
Attributes vs. Uniforms
辅助功能:
- Attributes是仅在顶点着色器中可用的值(尽管您可以在片段着色器上传递它们)。
- 可以在片段或顶点着色器中访问Uniforms。
数据类型:
- Attributes是每个顶点不同的值。
- Uniforms是常量值,对于片段和顶点着色器的所有实例都是相同的。
对象模型:
- Attributes附加到位置。
- Uniforms附加到程序对象。
可用性:
- (通常)可用的attributes少于uniforms(尽管两者都是实现定义的)。 可以通过使用适当的参数(在下面的括号中找到)执行glGet(...)来找出平台支持的确切数字。
- 实现必须支持至少8个顶点attributes(GL_MAX_VERTEX_ATTRIBS)。
- 实现必须支持至少128个顶点uniforms(GL_MAX_VERTEX_UNIFORM_VECTORS)。
- 实现必须支持至少16个片段uniforms(GL_MAX_FRAGMENT_UNIFORM_VECTORS)。
- OpenGL ES具有各种其他实现定义的属性,可以使用glGet(...)查询。 这个介绍的一些相关的是:
-
- 实现必须支持至少8个varyings(GL_MAX_VARYING_VECTORS)。
-
- 实现必须支持至少64位varyings的纹理(GL_MAX_TEXTURE_SIZE)。
Textures
我们可能需要最后一种类型的数据,每个片段数据不是从每个顶点数据插入的。 例如,如果我们想在三角形上显示图像,该怎么办? 我们不能使用uniforms,因为我们通常不足以存储图像中的所有像素(更不用说设置和访问数据会很糟糕)。 我们也不能使用attributes和varyings,因为它们是每对顶点然后插值,这对图像没有意义。 对我们来说幸运的是,OpenGL ES也有这个涵盖。 为此我们使用纹理。
纹理允许我们指定可在片段着色器中访问的任意数据数组。
每个片段着色器实例如何知道在纹理中查找的位置? 好问题,但我们已经掌握了解决这个问题所需的全部知识。 如果我们指定每个顶点映射到的图像中的位置(使用attribute),光栅化器将很好地插入坐标,以便为每个片段提供varying。 然后可以使用该坐标在图像中查找正确的点。 这是使用纹理采样器完成的。
#version 100
// Vertex shader which takes a per-vertex texture coordinate and interpolates it for the fragment shader.
attribute vec4 position;
attribute vec2 texture_coordinates;
varying vec2 interpolated_texture_coordinates;
void main()
{
interpolated_texture_coordinates = texture_coordinates;
gl_Position = position;
}
复制代码
// Host side code to set up texture input.
Glint sampler_location = glGetUniformLocation(glProgram, "texture_sampler");
// ...
// Create a Texture (this can be quite involved so we won't go into detail here but it's basically uploading an array of data similar to vertex attributes.)
// ...
// Set the sampler to point to the texture you've just created.
glUniform1i(sampler_location, texture_unit);
复制代码
#version 100
// Fragment shader which looks inside a texture using interpolated texture coordinates.
precision mediump float;
varying vec2 interpolated_texture_coordinates;
uniform sampler2D texture_sampler;
void main()
{
gl_FragColor = texture2D(texture_sampler, interpolated_texture_coordinates);
}
复制代码
Summary
顶点着色器具有以下接口:
- 顶点attributes(相对于每个顶点的数据)和uniforms(常量数据)传入。
- 顶点positions和varyings都会传出。
片段着色器具有以下界面:
- Varyings(每个片段数据的插值),uniforms(常量数据)和纹理都会传入。
- 片段颜色将会传出。
Graphics Setup
Application Flow
- 应用程序将启动,控制将立即传递给Activity onCreate函数。
- 此函数将创建View类的实例,然后将显示设置为此View类。
- 在创建时,View类将调用初始化函数。 此功能也将设置要渲染的曲面。 这涉及设置EGLContextFactory,设置EGLConfiguration,然后设置Renderer。
- Renderer有两个重要功能。 当创建或更改表面尺寸时调用第一个。 每次准备好绘制新帧时,都会调用第二个。 这些函数通常引用在应用程序的本地函数。 它通过使用包含所有本地函数的函数原型的不同类来实现此目的。
- 然后本地函数接管并将渲染场景。
GraphicsSetup Class
在Activity中创建TutorialView,它继承自GLSurfaceView,并将最终显示我们的图形。
public class GraphicsSetup extends Activity
{
private static String LOGTAG = "GraphicsSetup";
protected TutorialView graphicsView;
@Override protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.i(LOGTAG, "Creating New Tutorial View");
graphicsView = new TutorialView(getApplication());
setContentView(graphicsView);
}
@Override protected void onPause()
{
super.onPause();
graphicsView.onPause();
}
@Override protected void onResume()
{
super.onResume();
graphicsView.onResume();
}
}
复制代码
TutorialView Class
我们需要获得我们将要绘制的曲面的配置,我们需要为OpenGL ES 2.0正确设置上下文。
class TutorialView extends GLSurfaceView
{
protected int redSize = 8;
protected int greenSize = 8;
protected int blueSize = 8 ;
protected int alphaSize = 8;
protected int depthSize = 16;
protected int sampleSize = 4;
protected int stencilSize = 0;
protected int[] value = new int [1];
复制代码
在为要运行的所有图形设置曲面时,需要指定一系列参数。 其中一个最重要的是你希望表面处于什么样的格式。我们将选择一个称为RGBA 8888的表面。这意味着我们将使用由组成的完整32位颜色。 8位红色,8位绿色,8位蓝色和8位阿尔法。
我们还将深度缓冲区大小设置为16位。 在以后的教程中将需要这样做,以确保以正确的顺序显示对象。 样本大小与启用抗锯齿功能有关。 模板缓冲区是一个高级概念,因此现在我们通过将其设置为0来禁用它。
public TutorialView(Context context)
{
super(context);
setEGLContextFactory(new ContextFactory());
setEGLConfigChooser(new ConfigChooser());
setRenderer(new Renderer());
}
复制代码
这段代码是我们新的View类的构造函数。 我们要做的第一件事就是再次调用父类构造函数来处理我们不想直接参与的所有设置。接下来的两个调用是与EGL有关,稍后将定义。 EGL是OpenGL ES的一个非常重要的部分,它充当窗口系统和OpenGL ES绘图命令之间的接口。 我们使用它来创建绘图表面并与本地Windows系统进行通信。 我们还使用它来找出所需平台上可用的表面配置。
private static class ContextFactory implements GLSurfaceView.EGLContextFactory
{
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig)
{
final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
return context;
}
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context)
{
egl.eglDestroyContext(display, context);
}
}
复制代码
这实现了一个EGLContextFactory类,它定义为Android的一部分。 它与其他平台上的EGLContext大致相似。 EGLContext是OpenGL内部使用的数据结构。 在大多数情况下,您不必担心它。 事实上,Android隐藏了大部分细节。 您需要做的就是定义这两个函数。
createContext函数只为您创建一个上下文。 它接收一个属性列表,在这个实例中只包含一个属性。 EGL_CONTEXT_CLIENT_VERSION,设置为您正在使用的OpenGL ES版本,在本例中为2。EGL10.EGL_NONE用于表示列表的结尾。 创建上下文后,您只需返回它。 destroyContext函数只删除传递给它的上下文。
protected class ConfigChooser implements GLSurfaceView.EGLConfigChooser
{
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display)
{
final int EGL_OPENGL_ES2_BIT = 4;
int[] configAttributes =
{
EGL10.EGL_RED_SIZE, redSize,
EGL10.EGL_GREEN_SIZE, greenSize,
EGL10.EGL_BLUE_SIZE, blueSize,
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_SAMPLES, sampleSize,
EGL10.EGL_DEPTH_SIZE, depthSize,
EGL10.EGL_STENCIL_SIZE, stencilSize,
EGL10.EGL_NONE
};
int[] num_config = new int[1];
egl.eglChooseConfig(display, configAttributes, null, 0, num_config);
int numConfigs = num_config[0];
EGLConfig[] configs = new EGLConfig[numConfigs];
egl.eglChooseConfig(display, configAttributes, configs, numConfigs, num_config);
return selectConfig(egl, display, configs);
}
复制代码
这是处理我们在视图类开头列出的所有表面参数的类。 这个类需要实现chooseConfig函数。 我们要做的第一件事是创建另一个属性列表,它将保存我们想要的所需配置。 除了EGL_RENDERABLE_TYPE之外,所有列出的属性都在开头讨论过。 我们需要将其设置为我们正在使用的接口。 在这种情况下,它将是OpenGL ES 2.0。
然后我们需要获得支持的配置数量,因此我们将NULL调用eglChooseConfig,以便它只返回可用配置的数量。 然后,我们创建一个新的EGLConfigs数组,以保存符合我们要求的所有配置。 现在不幸的是,即使我们确切地指定了我们想要的表面配置条件,EGL也很乐意为您提供比您要求的更高的配置。 这可能不是所希望的,因为我们可能需要565色空间,并且根据其规则,它将能够返回8888配置。
这意味着我们需要遍历所有配置来选择合适的配置。 这是下一个函数selectConfig的工作。
public EGLConfig selectConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs)
{
for(EGLConfig config : configs)
{
int d = getConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
int s = getConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
int r = getConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE,0);
int g = getConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
int b = getConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
int a = getConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
if (r == redSize && g == greenSize && b == blueSize && a == alphaSize && d >= depthSize && s >= stencilSize)
return config;
}
return null;
}
复制代码
此函数循环遍历所有匹配或超出当前规范的配置,然后确保我们的配置完全匹配。 它通过调用函数getConfigAttrib来执行此操作,该函数返回给定参数的值。 然后,它将它们与所需的值进行比较,只有在找到匹配项时才返回配置。 作为ConfigChooser一部分的最后一个函数是前面描述的getConfigAttrib。
private int getConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config,
int attribute, int defaultValue)
{
if (egl.eglGetConfigAttrib(display, config, attribute, value))
return value[0];
return defaultValue;
}
复制代码
此函数只调用EGL库方法,然后只返回给定的值。 请注意,这里的关键字值来自于类的开头,它是我们定义的参数之一。 这导致我们进入视图结构的最终类Renderer类。
private static class Renderer implements GLSurfaceView.Renderer
{
public void onDrawFrame(GL10 gl)
{
NativeLibrary.step();
}
public void onSurfaceChanged(GL10 gl, int width, int height)
{
NativeLibrary.init(width,height);
}
public void onSurfaceCreated(GL10 gl, EGLConfig config)
{
}
}
复制代码
Renderer类充当基本回调函数。 我们需要实现三种不同的方法。 第一个onDrawFrame是在请求渲染新帧时调用的。 在这种情况下,我们将控制直接发送到应用程序的本地代码。 当曲面发生变化时,会调用onSurfaceChanged。 在创建曲面时也会调用此方法。 如果您熟悉标准OpenGL,您可能熟悉一些回调函数。 当表面被创建时调用onSurfaceChanged,这里我们将不实现onSurfaceCreated。
NativeLibrary Class
我们只需要为step函数添加一个函数原型,每个帧都会调用它,并在init函数中添加两个参数。 这两个参数应该是视图的宽度和高度。
public class NativeLibrary
{
static
{
System.loadLibrary("Native");
}
public static native void init(int width, int height);
public static native void step();
}
复制代码
Native Code
#include
#include
#include
#include
#define LOG_TAG "libNative"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C"
JNIEXPORT void JNICALL
Java_com_zpw_audiovideo_arm_GraphicsSetup_NativeLibrary_init(JNIEnv *env, jclass type, jint width,
jint height) {
LOGI("Hello From the Native Side!!");
}extern "C"
JNIEXPORT void JNICALL
Java_com_zpw_audiovideo_arm_GraphicsSetup_NativeLibrary_step(JNIEnv *env, jclass type) {
sleep(5);
LOGI("New Frame Ready to be Drawn!!!!");
}
复制代码
Simple Triangle
Native Includes
为了让我们不仅仅使用Android日志记录功能,我们已经包含了一些标准的C包括stdio.h和math.h. 由于我们现在也在我们的本地应用程序中执行图形处理,因此我们还包含了一些OpenGL ES 2.0头文件。 这些应包含在所有图形应用程序中。
#include
#include
#include
#include
#include
#include
#include
#define LOG_TAG "libNative"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
复制代码
我们需要在这里做一些事情,我们需要创建一个片段和顶点着色器。 这些可以被认为是一个小程序,每个程序都有一个主要功能,决定你的着色器将做什么。 在顶点着色器中,需要输出顶点的最终位置,并且在fragement着色器中,需要输出最终颜色,该颜色将是给定场景中像素的颜色。
Vertex and Fragment Shader Loading
static const char glVertexShader[] =
"attribute vec4 vPosition;\n"
"void main()\n"
"{\n"
" gl_Position = vPosition;\n"
"}\n";
复制代码
这是顶点着色器的源代码。 第一行将vPosition声明为属性。 这是着色器的输入,必须单独向着色器提供数据。 这将是要绘制到屏幕的每个顶点的位置。 正如所看到的,下一行只是一个标准函数,看起来像在C中创建的任何内容。此函数中唯一的行将名为gl_Position的变量设置为vPosition的值。 如前所述,如果在顶点着色器中没有执行任何其他操作,则需要设置顶点的最终位置。 这总是你设置gl_Position的值。
static const char glFragmentShader[] =
"precision mediump float;\n"
"void main()\n"
"{\n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
复制代码
这是片段着色器的源代码。 第一行是必需的,因为需要设置着色器的精度。 这可以是lowp,mediump或highp。 在此之后,有一个正常的函数定义,就像为顶点着色器创建的函数定义一样。 此着色器中唯一的一行将gl_FragColor设置为一种颜色。 在这种情况下,颜色是红色。 Vec4中的4个数字响应我们之前选择的RGBA色彩空间,可以选择介于0.0和1.0之间的数字。 因此,在这种情况下,红色通道为1.0(最大值)。 绿色和蓝色通道为0.0(最小值)。 Alpha通道设置为1.0。 请注意,这些值可以包含0.0到1.0之间的任何数字。
GLuint loadShader(GLenum shaderType, const char* shaderSource)
{
GLuint shader = glCreateShader(shaderType);
if (shader)
{
glShaderSource(shader, 1, &shaderSource, NULL);
glCompileShader(shader);
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled)
{
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen)
{
char * buf = (char*) malloc(infoLen);
if (buf)
{
glGetShaderInfoLog(shader, infoLen, NULL, buf);
LOGE("Could not Compile Shader %d:\n%s\n", shaderType, buf);
free(buf);
}
glDeleteShader(shader);
shader = 0;
}
}
}
return shader;
}
复制代码
现在是时候创建一个函数来加载每个着色器并将它们编译成可以在GPU上运行的形式。 首先,我们需要使用OpenGL ES API创建着色器。 只要我们设法获得有效的着色器对象,我们就会选择我们将用于此着色器的源。 这通常是我们之前为顶点和片段着色器定义的char数组之一。 然后,我们需要检查编译是否成功,就像我们编写的任何程序一样,可能会出错。
如果由于某种原因编译确实出错了。 我们需要找出原因,看看产生了什么错误,否则我们将在黑暗中拍摄。 因此,我们首先获取信息字符串的长度,然后创建该长度的缓冲区。 然后我们检索字符串本身并再次使用Android日志记录方法将其显示为错误。 然后我们释放缓冲区,因为我们没有其他用途。 如果有错误,我们返回0,否则我们返回着色器。
GLuint createProgram(const char* vertexSource, const char * fragmentSource)
{
GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexSource);
if (!vertexShader)
{
return 0;
}
GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentSource);
if (!fragmentShader)
{
return 0;
}
GLuint program = glCreateProgram();
if (program)
{
glAttachShader(program , vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
GLint linkStatus = GL_FALSE;
glGetProgramiv(program , GL_LINK_STATUS, &linkStatus);
if( linkStatus != GL_TRUE)
{
GLint bufLength = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength);
if (bufLength)
{
char* buf = (char*) malloc(bufLength);
if (buf)
{
glGetProgramInfoLog(program, bufLength, NULL, buf);
LOGE("Could not link program:\n%s\n", buf);
free(buf);
}
}
glDeleteProgram(program);
program = 0;
}
}
return program;
}
复制代码
我们现在需要创建一个程序。 程序可以被认为是保存着色器的东西。 它需要顶点着色器和片段着色器。 首先,我们依次加载每个着色器,并确保每个着色器都有效。 然后我们尝试创建一个程序。 如果这样做,我们将每个着色器附加到程序中。 然后我们链接该程序。 可以像标准链接器一样考虑这个问题。 同样,这可能会像常规链接器那样频繁失败,因此我们需要确保链接状态是成功的。 如果不是,我们再次找到失败信息的长度,然后创建一个缓冲区来显示它。 如果一切都成功,我们返回程序,否则返回0。
Graphics Setup and Rendering
GLuint simpleTriangleProgram;
GLuint vPosition;
bool setupGraphics(int w, int h)
{
simpleTriangleProgram = createProgram(glVertexShader, glFragmentShader);
if (!simpleTriangleProgram)
{
LOGE ("Could not create program");
return false;
}
vPosition = glGetAttribLocation(simpleTriangleProgram, "vPosition");
glViewport(0, 0, w, h);
return true;
}
复制代码
我们现在定义两个全局变量。 首先是program。 这将保存通过调用上述函数创建的程序的值。 第二个将保留GPU期望着色器所需的顶点数据的位置。
这个功能只是最后一点设置。 我们首先使用在开始时定义的两个字符数组创建程序。 我们确保创建程序,然后查询OpenGL ES以获取我们应该放置vPosition数据的位置。 请注意,引号必须与我们在着色器本身中使用的属性名称匹配。 然后我们调用glViewport传递表面的宽度和高度。 这就是设置的全部内容。 我们现在需要在代码中做的就是创建负责绘图的渲染函数和链接到Java层的函数。
const GLfloat triangleVertices[] = {
0.0f, 1.0f,
-1.0f, -1.0f,
1.0f, -1.0f
};
void renderFrame()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear (GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
glUseProgram(simpleTriangleProgram);
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0 ,triangleVertices);
glEnableVertexAttribArray(vPosition);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
复制代码
这部分代码定义的第一部分是我们试图绘制的三角形的顶点。 我们必须绘制从-1.0f到1.0f的空间。 屏幕的左下角是-1.0f -1.0f,屏幕的右上角是1.0f,1.0f。 因为我们只绘制三角形而三角形是2D形状,所以我们不需要担心Z坐标。 因此,如果我们查看指定的坐标,我们将得到0.0f,1.0f,这将是屏幕中间顶部的顶点。 -1.0f,-1.0f,它将是屏幕左下角的坐标,最后是1.0f,-1.0f,它将是屏幕右下角的坐标。
现在我们将创建最令人兴奋的功能:负责绘图的功能。 我们要做的第一件事就是将清除的颜色设置为黑色。 可以将此颜色设置为任何颜色,OpenGL ES会在每个glClear命令上将场景设置为该颜色。 在此之后我们调用glClear,它真正标志着我们场景的开始。 我们将清除深度缓冲区和颜色缓冲区。
然后我们需要选择我们想要使用的程序。 典型的应用程序可能有多个着色器,因此可能有多个程序。 例如,可能希望使用不同的着色器来绘制水,房屋或草。 然后,我们需要将我们在着色器中提到的属性链接到上面定义的实际三角形数据。 我们首先提供GPU期望数据的位置。 然后我们说每个顶点将有2个元素,它们是浮点数。 下一个参数表明我们的数据已经标准化,因此不需要为我们做这些,并且我们的数据之间没有任何跨越。 最后一个参数是指向实际三角形顶点的指针。
然后,我们需要启用数组来表示它已准备好使用。 最后一行是实际绘制三角形的函数 。 我们说我们要绘制一个顶点数组,它们应该绘制为从数组中的元素0开始的三角形,并且要绘制3个顶点。
Function Definitions
extern "C"
JNIEXPORT void JNICALL
Java_com_zpw_audiovideo_arm_GraphicsSetup_NativeLibrary_init(JNIEnv *env, jclass type, jint width,
jint height) {
setupGraphics(width, height);
}extern "C"
JNIEXPORT void JNICALL
Java_com_zpw_audiovideo_arm_GraphicsSetup_NativeLibrary_step(JNIEnv *env, jclass type) {
renderFrame();
}
复制代码
Simple Cube
Discussion of Model, View and Projection Matrices
在我们深入研究如何进行运动的代码之前,我们需要了解发生了什么。 首先,所有变换都由矩阵表示。 矩阵可以被认为是数字网格,通常是正方形。 出于我们的大部分目的,我们将处理一个4 x 4平方的数字,其中包含16个元素。
在图形中,我们每个应用程序使用三个主矩阵。 它们被称为模型矩阵,视图矩阵和投影矩阵。 下面提供了每种方法的简要说明:
- 模型矩阵:你可能还记得在前面的例子中我们绘制了一个坐标为(0.0f,1.0f),( - 1.0f,-1.0f)和(1.0f,-1.0f)的三角形。 然后我们将这些坐标绘制在屏幕中心的位置(0.0f,0.0f,0.0f)。 当你只有一个对象时,这很好,但是如果你想创建另一个对象,它会立即被绘制在前一个对象之上。 模型矩阵是一个解决方案,只是解释了在屏幕上绘制对象的位置。 通常,您将对此矩阵进行移动,缩放和旋转,以确定对象或模型的位置大小和旋转。
- 视图矩阵:玩游戏时,有时不希望移动对象。 例如,游戏中的建筑物移动可能没有意义。 相反,我们希望我们的相机在世界各地移动。 视图矩阵负责摄像机的移动,通常是我们看场景的方式。
- 投影矩阵:现在我们已将对象移动到我们想要的位置。 问题是我们没有深度感。 Z坐标尚未真正考虑在内。 我们需要一个矩阵,使得更靠近相机的物体比远离物体的物体更大。 为此,我们需要使用投影矩阵。
Identity Function
单位函数。 此函数初始化矩阵,以便在使用时不执行平移,旋转或缩放。 实际上,它将确保在使用矩阵时,恰好没有任何反应。 要做到这一点,我们需要将对角线从右上角到左下角全部设置为1。 剩下的就是0。
void matrixIdentityFunction(float* matrix)
{
if(matrix == NULL)
{
return;
}
matrix[0] = 1.0f;
matrix[1] = 0.0f;
matrix[2] = 0.0f;
matrix[3] = 0.0f;
matrix[4] = 0.0f;
matrix[5] = 1.0f;
matrix[6] = 0.0f;
matrix[7] = 0.0f;
matrix[8] = 0.0f;
matrix[9] = 0.0f;
matrix[10] = 1.0f;
matrix[11] = 0.0f;
matrix[12] = 0.0f;
matrix[13] = 0.0f;
matrix[14] = 0.0f;
matrix[15] = 1.0f;
}
复制代码
通常在数学中,你会计算要素。 因此,矩阵的顶行将是元素:0,1,2和3。在OpenGL中,它是相反的方式并且在列中完成,因此第一列将是0,1,2和3。如果你不这样做,你可以按行进行所有转换,然后在结尾处编写转换函数。
Translation
平移是在X,Y和Z轴上移动对象的过程。 为此,只需使用最远的列。 元素12将表示想要在X轴上移动多远。 元素13将表示想要在Y轴上移动多远,而元素14将表示想要在Z轴上移动多远。 还需要将元素15设置为1才能使数学正常工作。
void matrixTranslate(float* matrix, float x, float y, float z)
{
float temporaryMatrix[16];
matrixIdentityFunction(temporaryMatrix);
temporaryMatrix[12] = x;
temporaryMatrix[13] = y;
temporaryMatrix[14] = z;
matrixMultiply(matrix,temporaryMatrix,matrix);
}
复制代码
请注意我们如何首先创建临时矩阵并将其设置为单位矩阵。 我们这样做是为了避免污染已经传递给函数的矩阵中的值。 然后,我们将新的转换值添加到我们的临时矩阵,并使用下面描述的乘法函数将转换应用于当前矩阵。
Matrix Multiplication
矩阵乘法与数字的标准乘法略有不同。 假设您有两个矩阵,一个标记为A,另一个标记为B.要将这两个矩阵的元素相乘,您需要将A列的元素与每个单独元素的B对应元素行相乘。 下图显示了:
所以我们必须创建一个可以进行这种乘法的函数,它在4 * 4矩阵中变得更加复杂。
void matrixMultiply(float* destination, float* operand1, float* operand2)
{
float theResult[16];
int row, column = 0;
int i,j = 0;
for(i = 0; i < 4; i++)
{
for(j = 0; j < 4; j++)
{
theResult[4 * i + j] = operand1[j] * operand2[4 * i] + operand1[4 + j] * operand2[4 * i + 1] +
operand1[8 + j] * operand2[4 * i + 2] + operand1[12 + j] * operand2[4 * i + 3];
}
}
for(int i = 0; i < 16; i++)
{
destination[i] = theResult[i];
}
}
复制代码
注意我们如何创建一个临时矩阵来保存计算结果。 这是因为如果目标和其中一个操作数是相同的指针,则可能会出现意外结果。
Scaling
这是另一个简单的转变。 就像我们之前定义的单位函数一样,我们可以通过改变从左上角到右下角的对角线上的值来实现缩放。 第一个元素是X轴上的缩放,第二个元素将在Y轴上缩放,第三个元素将在Z轴上缩放。
void matrixScale(float* matrix, float x, float y, float z)
{
float tempMatrix[16];
matrixIdentityFunction(tempMatrix);
tempMatrix[0] = x;
tempMatrix[5] = y;
tempMatrix[10] = z;
matrixMultiply(matrix, tempMatrix, matrix);
}
复制代码
再次为了不污染应用于所提供的矩阵的任何变换,我们创建一个临时变换并将它们两者相乘。
Rotation
旋转有点复杂,因为它涉及一些三角学。 想要使用的三角函数取决于想要旋转的轴。
void matrixRotateX(float* matrix, float angle)
{
float tempMatrix[16];
matrixIdentityFunction(tempMatrix);
tempMatrix[5] = cos(matrixDegreesToRadians(angle));
tempMatrix[9] = -sin(matrixDegreesToRadians(angle));
tempMatrix[6] = sin(matrixDegreesToRadians(angle));
tempMatrix[10] = cos(matrixDegreesToRadians(angle));
matrixMultiply(matrix, tempMatrix, matrix);
}
void matrixRotateY(float *matrix, float angle)
{
float tempMatrix[16];
matrixIdentityFunction(tempMatrix);
tempMatrix[0] = cos(matrixDegreesToRadians(angle));
tempMatrix[8] = sin(matrixDegreesToRadians(angle));
tempMatrix[2] = -sin(matrixDegreesToRadians(angle));
tempMatrix[10] = cos(matrixDegreesToRadians(angle));
matrixMultiply(matrix, tempMatrix, matrix);
}
void matrixRotateZ(float *matrix, float angle)
{
float tempMatrix[16];
matrixIdentityFunction(tempMatrix);
tempMatrix[0] = cos(matrixDegreesToRadians(angle));
tempMatrix[4] = -sin(matrixDegreesToRadians(angle));
tempMatrix[1] = sin(matrixDegreesToRadians(angle));
tempMatrix[5] = cos(matrixDegreesToRadians(angle));
matrixMultiply(matrix, tempMatrix, matrix);
}
复制代码
还可以创建一个函数,该函数接收哪个向量作为参数旋转。
Projection
此功能只需要调用一次,结果应存储在投影矩阵中。
我们提供的第一个参数是我们想要进行投影的矩阵。 这就像我们已经描述过的大多数功能一样。 第二个是视野。 这是你应该能够看透的角度。
下一个参数是宽高比。 这通常是屏幕或表面的宽度/高度。 如果所有屏幕比例都相同,则不需要此参数,但遗憾的是它们不是。 这有助于应用程序在各种设备上看起来正确。 考虑到移动领域中可用的大量不同设备,这一点非常重要。
最后两个参数称为zNear和zFar。 zNear表示在物体消失并被剪裁之前,物体与相机的接近程度。 zFar是相反的; 在不再绘制物体之前,物体距离相机有多远。
void matrixPerspective(float* matrix, float fieldOfView, float aspectRatio, float zNear, float zFar)
{
float ymax, xmax;
ymax = zNear * tanf(fieldOfView * M_PI / 360.0);
xmax = ymax * aspectRatio;
matrixFrustum(matrix, -xmax, xmax, -ymax, ymax, zNear, zFar);
}
复制代码
此函数调用matrixFrustum函数,该函数实际上创建了视锥体,这是将看到的场景。
void matrixFrustum(float* matrix, float left, float right, float bottom, float top, float zNear, float zFar)
{
float temp, xDistance, yDistance, zDistance;
temp = 2.0 *zNear;
xDistance = right - left;
yDistance = top - bottom;
zDistance = zFar - zNear;
matrixIdentityFunction(matrix);
matrix[0] = temp / xDistance;
matrix[5] = temp / yDistance;
matrix[8] = (right + left) / xDistance;
matrix[9] = (top + bottom) / yDistance;
matrix[10] = (-zFar - zNear) / zDistance;
matrix[11] = -1.0f;
matrix[14] = (-temp * zFar) / zDistance;
matrix[15] = 0.0f;
}
复制代码
Changes to the Vertex and Fragment Shaders
static const char glVertexShader[] =
"attribute vec4 vertexPosition;\n"
"attribute vec3 vertexColour;\n"
"varying vec3 fragColour;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"void main()\n"
"{\n"
" gl_Position = projection * modelView * vertexPosition;\n"
" fragColour = vertexColour;\n"
"}\n";
复制代码
第一行代表输入顶点。 下一行是另一个输入,表示每个顶点的关联颜色。
下一行介绍了一个新概念; varying。 变量(varying)是可以用来将值传递到片段着色器的东西,因为片段着色器不能采用任何属性。 接下来的两行也引入了一个新概念;uniforms。 uniforms可以被认为有点像全局变量。 它可以在顶点着色器和片段着色器中访问,但它是只读的,不能在任何着色器中编辑。 这是我们上面谈到的矩阵的理想位置。 对于这个简单的例子,我们将模型和视图矩阵组合到了modelView矩阵中。
在main函数中,我们现在像以前一样设置gl_Position,但这次不是直接将它设置为vertexPosition属性,而是将属性乘以投影和模型视图矩阵。 我们这样做的顺序非常重要。
我们着色器中的最后一行只是将变化(varying)设置为我们要在片段着色器中使用的属性。
static const char glFragmentShader[] =
"precision mediump float;\n"
"varying vec3 fragColour;\n"
"void main()\n"
"{\n"
" gl_FragColor = vec4(fragColour, 1.0);\n"
"}\n";
复制代码
以上是我们的新片段着色器。 ]请注意我们现在如何在片段着色器顶部的顶点着色器中进行相同的变化。 我们将glFragColor的前三个元素设置为它。 第四个是alpha,因为我们没有做任何透明度或混合效果,所以我们将其保持为1.0或完全不透明。
Setup Graphics
bool setupGraphics(int width, int height)
{
simpleCubeProgram = createProgram(glVertexShader, glFragmentShader);
if (simpleCubeProgram == 0)
{
LOGE ("Could not create program");
return false;
}
vertexLocation = glGetAttribLocation(simpleCubeProgram, "vertexPosition");
vertexColourLocation = glGetAttribLocation(simpleCubeProgram, "vertexColour");
projectionLocation = glGetUniformLocation(simpleCubeProgram, "projection");
modelViewLocation = glGetUniformLocation(simpleCubeProgram, "modelView");
/* Setup the perspective */
matrixPerspective(projectionMatrix, 45, (float)width / (float)height, 0.1f, 100);
glEnable(GL_DEPTH_TEST);
glViewport(0, 0, width, height);
return true;
}
复制代码
正如看到的一样,我们有一个colourPosition,projectionLocation和modelViewLocation。 colourPosition变量使用与vertexPosition完全相同的函数来链接着色器中的vertexColour,因为它只是另一个属性。 另外两个变量使用一个名为glGetUniformLocation的新函数。 这与glGetAttribLocation完全相同,但仅适用于uniforms而非属性。
在这种情况下,我们的例子非常简单,所以我们根本不会改变场景的投影。 出于这个原因,我们可以在setupGraphics例程中设置一次。 这就是我们在这里调用matrixPerspective的原因。 参数应与之前定义的参数匹配。
我们对此函数进行的最后一项更改是我们已启用GL_DEPTH_TEST。 这告诉OpenGL ES,距离应该是屏幕上显示内容的一个因素。 之前我们并不关心这个,因为在前面的例子中我们只绘制了一个三角形,它是一个2D对象,后面没有任何东西。 现在由于立方体是一个3D对象,我们需要知道哪些面在彼此的前面。 OpenGL ES应该只绘制正面的片段。
Creating the Cube
立方体可以被认为是12个三角形。 立方体的每个面都是两个三角形的正方形。 这意味着我们需要比第一个例子更多的顶点。
GLfloat cubeVertices[] = {-1.0f, 1.0f, -1.0f, /* Back. */
1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, 1.0f, /* Front. */
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, /* Left. */
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f, /* Right. */
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, -1.0f, /* Top. */
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f, /* Bottom. */
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f
};
复制代码
请注意我们如何定义模型空间中的所有内容。 所以一切都在-1.0到1.0之间完成。 我们现在有了新的Z分量,它可以处理特定顶点的近距离或远距离。 如果标记哪些顶点指的是什么,它会使事情更容易阅读。 在大多数项目中,顶点将从Maya或3D Studio Max等第三方工具生成。
接下来,我们需要为刚刚定义的每个顶点定义颜色。 这是通过为每个顶点定义3个通道来完成的。 红色通道,绿色通道和蓝色通道。
GLfloat colour[] = {1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f
};
复制代码
现在你会注意到我们没有定义足够的顶点或颜色来创建一个立方体。 如果一个三角形由3个顶点组成,我们有12个三角形,那么我们应该有36个顶点吗? 相反,我们只有24个。这是因为定义36个顶点占用了大量的额外空间。 所以我们可以定义一张平面上的所有独特点。 所以这将是左上角,左下角,右下角和右上角的一个。 我们可以传递一组索引,这些索引映射哪些顶点涉及哪些三角形。 节省了空间,因为我们使用shorts而不是floats。
GLushort indices[] = {0, 2, 3, 0, 1, 3, 4, 6, 7, 4, 5, 7, 8, 9, 10, 11, 8, 10, 12, 13, 14, 15, 12, 14, 16, 17, 18, 16, 19, 18, 20, 21, 22, 20, 23, 22};
复制代码
Drawing the Scene
void renderFrame()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
matrixIdentityFunction(modelViewMatrix);
matrixRotateX(modelViewMatrix, angle);
matrixRotateY(modelViewMatrix, angle);
matrixTranslate(modelViewMatrix, 0.0f, 0.0f, -10.0f);
glUseProgram(simpleCubeProgram);
glVertexAttribPointer(vertexLocation, 3, GL_FLOAT, GL_FALSE, 0, cubeVertices);
glEnableVertexAttribArray(vertexLocation);
glVertexAttribPointer(vertexColourLocation, 3, GL_FLOAT, GL_FALSE, 0, colour);
glEnableVertexAttribArray(vertexColourLocation);
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, projectionMatrix);
glUniformMatrix4fv(modelViewLocation, 1, GL_FALSE, modelViewMatrix);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, indices);
angle += 1;
if (angle > 360)
{
angle -= 360;
}
}
复制代码
我们首先清除颜色和深度缓冲区。
早期场景渲染的一个新部分是清除modelViewMatrix。 我们直接将其设置为单位方法。 然后我们在x和y轴上以适当的角度旋转。 此时立方体和相机仍处于原点。 如果我们没有移动立方体,我们就会在它周围移动。 为了避免这种情况,我们将10个单位移动到屏幕上。 这样我们就能看到整个旋转立方体。
然后,我们执行选择要用于绘图的程序的标准步骤,然后启用vertexPosition顶点数组和colourPosition顶点数组。 我们还需要将投影矩阵和模型视图矩阵与着色器中的对应物链接起来。
然后我们不能使用glDrawArrays函数,因为它需要定义所有顶点。 正如我们之前提到的,我们只在可能的36中定义了24个。所以我们需要使用一个名为glDrawElements的新函数。 此函数接受我们之前定义的索引,并绘制我们想要的多维数据集的三角形。 这是实际的立方体绘图方法。
最后几行只是为了确保立方体旋转。
Texture Cube
到目前为止,我们在示例中仅使用单一颜色来表示三角形。 这对于简单程序来说很好,但如果你想在一个小区域内使用多种颜色会发生什么。 当然,你可能只有数百万个三角形代表一种颜色,但这是不切实际的。 相反,我们需要使用的是纹理。
可以将纹理视为希望三角形显示的海报或图像。 这样,一个三角形可以代表许多不同的颜色。 这些图像可以是喜欢的任何图像格式,需要的是加载特定图像格式的代码。对于此示例,我们将使用RAW格式,因为它是最简单但自由添加代码以使用任何图像 你想要的格式。
Load Texture Function
我们要看的第一个函数是加载纹理的函数。 这是我们加载图像并将其转换为OpenGL ES理解的格式的地方。 然后我们需要将纹理加载到GPU内存中,以便我们可以在着色器中使用图像。
#include "Texture.h"
#include
#include
#include
GLuint loadSimpleTexture()
{
/* Texture Object Handle. */
GLuint textureId;
/* 3 x 3 Image, R G B A Channels RAW Format. */
GLubyte pixels[9 * 4] =
{
18, 140, 171, 255, /* Some Colour Bottom Left. */
143, 143, 143, 255, /* Some Colour Bottom Middle. */
255, 255, 255, 255, /* Some Colour Bottom Right. */
255, 255, 0, 255, /* Yellow Middle Left. */
0, 255, 255, 255, /* Some Colour Middle. */
255, 0, 255, 255, /* Some Colour Middle Right. */
255, 0, 0, 255, /* Red Top Left. */
0, 255, 0, 255, /* Green Top Middle. */
0, 0, 255, 255, /* Blue Top Right. */
};
复制代码
首先,我们对头文件有一个非常标准的包含。 注意我们的loadSimpleTexture函数如何返回一个GLuint。 这是纹理加载后的ID,以便我们稍后可以在代码中引用它。 函数的第一行定义了一个临时保存该值的变量。
下一个变量是实际图像数据。 如前所述,我们将使用RAW格式,它只是一个接一个的颜色数据。 现在在OpenGL ES中,我们希望图像的开头代表左下角而不是默认的左上角。 这就是我们定义底行,然后是中间,最后是顶部的原因。 每个数字代表RGBA格式的一个通道。 因此,如果我们采用第一行,我们在红色通道中有18个,在绿色通道中有140个,在蓝色通道中有171个,在Alpha通道中有255个。 这些数字不超过255; 0表示缺少颜色,255表示全彩色。 Alpha通道中的255使整个纹理不透明。
/* Use tightly packed data. */
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
/* Generate a texture object. */
glGenTextures(1, &textureId);
/* Activate a texture. */
glActiveTexture(GL_TEXTURE0);
/* Bind the texture object. */
glBindTexture(GL_TEXTURE_2D, textureId);
/* Load the texture. */
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 3, 3, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
/* Set the filtering mode. */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
return textureId;
}
复制代码
我们现在需要将图像数据上传到图形内存中。 首先,我们希望紧密打包数据以节省空间,因此我们使用glPixelStorei函数。 下一行生成一个纹理ID,我们可以将它放在我们之前定义的变量中。 然后我们在函数的末尾返回它,以便程序的其他部分可以使用纹理。 它的参数是要生成的纹理数量和放置它们的位置。
然后,我们需要激活纹理单元,然后绑定特定的单元类型。 您可以拥有的纹理单位数量是硬件特定的,但至少有8对纹理单位类型,我们有两个选择:GL_TEXTURE_2D或GL_TEXTURE_CUBE_MAP。 现在我们总是想选择GL_TEXTURE_2D,因为它是最常见的。 现在,任何后续纹理GL操作都将发生在我们在glGenTextures中创建的纹理中。 从这里我们可以上传实际的纹理数据。
我们使用函数glTexImage2D上传纹理。 第一个参数也是我们想要使用的纹理单元。 下一个参数是MipMap级别编号。现在你总是想把这个值设置为0.接下来是我们应该使用的内部格式。 在OpenGL ES 2.0中,内部格式和图像格式必须相同,因为没有转换方法。 这就是参数7与此匹配的原因。
参数4和5表示图像的宽度和高度。 在这种情况下,它是一个3 x 3的图像,因为为更大的图像手动编写RAW格式需要花费太长时间。 参数6是您想要在图像周围的边框,在OpenGL ES上必须为0。 最后两个参数是我们要使用的数据类型,然后是数据本身。
最后两个函数调用定义了当我们需要拉伸或缩小图像时会发生什么。 例如,我们的每个立方体面都不会是3 x 3像素。 这意味着我们将不得不拉伸图像。 在这种情况下,我们告诉OpenGL ES使用最接近它应该选择的像素。 因此粗略地将立方体分成9个相等的颜色部分。
Changes in the Shaders
static const char glVertexShader[] =
"attribute vec4 vertexPosition;\n"
"attribute vec2 vertexTextureCord;\n"
"varying vec2 textureCord;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"void main()\n"
"{\n"
" gl_Position = projection * modelView * vertexPosition;\n"
" textureCord = vertexTextureCord;\n"
"}\n";
static const char glFragmentShader[] =
"precision mediump float;\n"
"uniform sampler2D texture;\n"
"varying vec2 textureCord;\n"
"void main()\n"
"{\n"
" gl_FragColor = texture2D(texture, textureCord);\n"
"}\n";
复制代码
现在我们需要编辑着色器以使用我们的新纹理。 请注意我们如何删除前一个示例中对color属性的所有引用。 这是因为如果我们要使用纹理,我们不再需要提供每个顶点颜色数据。 但是,我们需要提供每顶点纹理坐标。 纹理坐标表示纹理中此特定顶点应从其中获取颜色的位置。 把它想象成一个映射。 纹理坐标从左下角的0,0到右上角的1,1。 在我们定义每个顶点纹理坐标的部分中有更多相关内容。 现在我们需要知道的是我们在着色器中需要另一个vec2属性,我们需要通过变化将它们传递给片段着色器。
在片段着色器中,我们有一个类型为sampler2D的新uniform。 这用于显示我们想要使用的纹理单元。 在这种情况下,它并不重要,因为我们只有一个纹理。
片段着色器中的代码仍然非常简单。 我们只将最终颜色设置为一个名为texture2D的函数的结果。 此函数包含2个参数:采样器和纹理坐标,它将告诉单元在纹理中的位置。
Changes to Setup Graphics
我们需要做的第一件事是从前一个示例中删除与颜色相关的任何内容,并添加纹理坐标和样本位置的位置。 我们还需要调用加载纹理的函数。 请注意,需要将GLuint textureId添加到Setup Graphics函数上方定义的全局变量列表中。
bool setupGraphics(int width, int height)
{
glProgram = createProgram(glVertexShader, glFragmentShader);
if (!glProgram)
{
LOGE ("Could not create program");
return false;
}
vertexLocation = glGetAttribLocation(glProgram, "vertexPosition");
textureCordLocation = glGetAttribLocation(glProgram, "vertexTextureCord");
projectionLocation = glGetUniformLocation(glProgram, "projection");
modelViewLocation = glGetUniformLocation(glProgram, "modelView");
samplerLocation = glGetUniformLocation(glProgram, "texture");
/* Setup the perspective. */
matrixPerspective(projectionMatrix, 45, (float)width / (float)height, 0.1f, 100);
glEnable(GL_DEPTH_TEST);
glViewport(0, 0, width, height);
/* Load the Texture. */
textureId = loadSimpleTexture();
if(textureId == 0)
{
return false;
}
else
{
return true;
}
}
复制代码
Texture coordinates
我们之前提到过,我们需要为每个顶点设置一个纹理坐标,以便OpenGL ES知道如何将图像映射到我们要绘制的三角形上。 好消息是我们可以完全按照与顶点相同的方式定义纹理坐标。 意思是我们只需要为每个面定义4个。
请记住从0到1,它从左下角开始。 如果采用正面,可以看到左上顶点-1.0,1.0,1.0匹配左上角纹理坐标0.0,1.0。 此主题出现在所有顶点中。
GLfloat cubeVertices[] = {-1.0f, 1.0f, -1.0f, /* Back. */
1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, 1.0f, /* Front. */
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, /* Left. */
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f, /* Right. */
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, /* Top. */
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, - 1.0f, -1.0f, /* Bottom. */
-1.0f, -1.0f, 1.0f,
1.0f, - 1.0f, 1.0f,
1.0f, -1.0f, -1.0f
};
GLfloat textureCords[] = { 1.0f, 1.0f, /* Back. */
0.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
0.0f, 1.0f, /* Front. */
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f, /* Left. */
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
1.0f, 1.0f, /* Right. */
1.0f, 0.0f,
0.0f, 0.0f,
0.0f, 1.0f,
0.0f, 1.0f, /* Top. */
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 0.0f, /* Bottom. */
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f
};
复制代码
Enabling Attributes and Passing Sampler Locations
我们需要做的最后一件事是启用纹理坐标属性并从片段着色器设置采样器。 现在我们只有一个纹理GL_TEXTURE0,我们应该使用那个。
glVertexAttribPointer(textureCordLocation, 2, GL_FLOAT, GL_FALSE, 0, textureCords);
glEnableVertexAttribArray(textureCordLocation);
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE,projectionMatrix);
glUniformMatrix4fv(modelViewLocation, 1, GL_FALSE, modelViewMatrix);
/* Set the sampler texture unit to 0. */
glUniform1i(samplerLocation, 0);
复制代码
- 从系统加载文件并将其转换为OpenGL ES可以理解的格式。
- 生成纹理对象并激活所需的纹理单元。
- 使用glTexImage2D将纹理数据加载到纹理单元中
- 使用glTexParameteri设置采样方法
- 将纹理坐标加载到每个顶点的系统中。
- 将采样器对象添加到片段着色器。
Lighting
到目前为止,我们用简单的颜色渲染了对象。 从照明的角度来看,场景中的所有内容都是完全亮起的,它具有我们指定的颜色。 为了使场景更加逼真,我们必须向场景引入灯光的概念。 在本教程中,我们将介绍顶点法线和漫反射,镜面反射和环境光的概念。
OpenGL ES 2.0以后没有灯光概念。 开发人员可以设计并实现一种在场景中模拟光的方法。 就像计算机图形学中的所有东西一样,灯光是假的 这意味着我们不是试图在整个场景中精确地模拟光子流,而是使用近似值来给出“足够好”的结果。
Preparation
因为传统上正确地点亮立方体非常棘手(它不一定非常好地展示效果),我们将把立方体变成一个非常简单的近似球体。
为此,我们将添加一些额外的几何体。 我们将在每个面的中间添加一个额外的顶点,正好在每个面的中间,但是更多地突出。 顶点数组现在应该如下所示:
GLfloat verticies[] = { 1.0f, 1.0f, -1.0f, /* Back. */
-1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
0.0f, 0.0f, -2.0f,
-1.0f, 1.0f, 1.0f, /* Front. */
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
0.0f, 0.0f, 2.0f,
-1.0f, 1.0f, -1.0f, /* Left. */
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-2.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, /* Right. */
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
2.0f, 0.0f, 0.0f,
-1.0f, -1.0f, 1.0f, /* Bottom. */
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
0.0f, -2.0f, 0.0f,
-1.0f, 1.0f, -1.0f, /* Top. */
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
0.0f, 2.0f, 0.0f
};
复制代码
为了确保顶点正确着色,我们需要在颜色数组中为每个面添加额外的颜色:
GLfloat colour[] = {1.0f, 0.0f, 0.0f, /* Back. */
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, /* Front. */
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f, /* Left. */
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, /* Right. */
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 1.0f, /* Bottom. */
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, /* Top. */
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f
};
复制代码
要使用这些新顶点,我们需要将索引调整到顶点数组中以绘制正确的三角形。 我们现在要在每个面上绘制四个三角形,所有这些都将使用我们在每个面中间添加的顶点。 请注意顶点的缠绕很重要(正面多边形必须具有逆时针缠绕)。
GLushort indices[] = {0, 2, 4, 0, 4, 1, 1, 4, 3, 2, 3, 4, /* Back. */
5, 7, 9, 5, 9, 6, 6, 9, 8, 7, 8, 9, /* Front. */
10, 12, 14, 10, 14, 11, 11, 14, 13, 12, 13, 14, /* Left. */
15, 17, 19, 15, 19, 16, 16, 19, 18, 17, 18, 19, /* Right. */
20, 22, 24, 20, 24, 21, 21, 24, 23, 22, 23, 24, /* Bottom. */
25, 27, 29, 25, 29, 26, 26, 29, 28, 27, 28, 29 /* Top. */
};
复制代码
最后在renderFrame中,我们需要增加绘制调用中绘制的顶点数(从36到72),因为我们绘制了两倍的三角形。 所以它看起来像这样:
glDrawElements(GL_TRIANGLES, 72, GL_UNSIGNED_SHORT, indices);
复制代码
Normals
对于大多数照明模型,重要的是要知道场景中所有多边形面向哪个方向。 例如,如果我们知道光从某个方向指向,为了知道是否应该用该光照亮多边形,我们必须知道它是否面向光。 所以,您可能会想,系统应该知道多边形面向哪个方向,因为我们指定了它。 我们使用正确的绕组指定顶点以显示我们希望它们面向哪个方向,并且我们提供转换以将多边形移动到正确的位置。
我们不使用此信息有两个原因:
- OpenGL ES着色器中没有机制可以获取此信息。
- 单独指定这些信息使我们能够通过不严格准确来做强大而令人兴奋的事情。
我们使用所谓的“表面法线”或仅使用“法线”来编码所需信息。 对于场景中的每个顶点,我们提供一个矢量,该矢量从它所代表的表面垂直指向。
要将此数据发送到GPU,我们使用与向GPU发送颜色数据和位置数据完全相同的机制。 我们在其中定义了每个顶点法线的数组,我们在顶点着色器中获取法线变量的位置,然后使用glVertexAttribPointer和glEnableVertexAttribArray将数据上传到GPU。
1.我们在顶点着色器中为顶点法线添加一个属性:
"attribute vec3 vertexNormal;\n"
复制代码
2.创建一个全局变量来保存顶点法线属性位置:
GLuint vertexNormalLocation;
复制代码
3.在setupGraphics中从主机端获取此属性的位置:
vertexNormalLocation = glGetAttribLocation(lightingProgram, "vertexNormal");
复制代码
4.创建一个包含所有顶点法线的数组(如顶点位置和颜色):
GLfloat normals[] = { 1.0f, 1.0f, -1.0f, /* Back. */
-1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
0.0f, 0.0f, -1.0f,
-1.0f, 1.0f, 1.0f, /* Front. */
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-1.0f, 1.0f, -1.0f, /* Left. */
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, /* Right. */
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 0.0f, 0.0f,
-1.0f, -1.0f, 1.0f, /* Bottom. */
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
0.0f, -1.0f, 0.0f,
-1.0f, 1.0f, -1.0f, /* Top. */
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f
};
复制代码
5.在renderFrame的每一帧上将数据上传到GPU:
glVertexAttribPointer(vertexNormalLocation, 3, GL_FLOAT, GL_FALSE, 0, normals);
glEnableVertexAttribArray(vertexNormalLocation);
复制代码
顶点法线数组通常由建模工具与顶点位置一起生成。 我们在这里手工创建它们以匹配我们指定的顶点位置。 您可能会注意到,在我们的法线数组中,法线并不严格垂直于它们所连接的多边形。 这样我们就可以展示我们在这里呈现所有光技术,特别是Specular Light。
Diffuse Light
如果我们考虑最简单的定向光,我们想知道光线是否会触及多边形的面。 如果我们将光的方向存储为矢量,则会有一个简单的计算,它将为我们提供数据。 两个归一化向量的点积将给出这些向量之间角度的余弦。
因为点积提供了矢量源之间的角度,所以我们在着色器中反转了光线方向。
因此,光方向和表面法线的点积将给出这些矢量之间的角度的余弦。 如果此角度介于-90°(270°)和90°之间,则光线位于多边形的正确一侧。 由于dot的返回值实际上是角度的余弦,我们实际上要检查它是否在cos(90°)和cos(-90°)之间,或者实际上只是大于零(cos(90°)= cos (-90°)= 0)。
下图显示表面正面上任何矢量的点积都是正的。 N是曲面的法线,其他三个矢量代表光(方向相反,点积产生正确的角度)。 对于红色和橙色灯,点积分别返回cos(45°)和cos(295°),它们都是正值。 法线和绿光矢量的点积为cos(155°); 负数。
这将为我们的灯光打开或关闭提供二进制控制,但我们可以做得更好。 如果我们使用角度来直接控制光的强度,当表面更直接面向光时,表面将更亮(例如cos(0°)= 1),当角度增加时,表面将更暗(例如cos(80°) = 0.17)。
再次看图,红光和法线(cos(45°)= 0.71)的点积大于橙色太阳和法线的点积(cos(295°)= 0.42)。
要在我们的着色器中执行此操作,我们需要对顶点着色器进行以下更改,在主函数的顶部添加以下行:
" vec3 transformedVertexNormal = normalize((modelView * vec4(vertexNormal, 0.0)).xyz);"
" vec3 inverseLightDirection = normalize(vec3(0.0, 1.0, 1.0));\n"
" fragColour = vec3(0.0);\n"
复制代码
与我们通过modelViewMatrix转换顶点位置的方式相同,我们还必须转换法线以使它们匹配。 因为法线是向量(而不是位置),所以我们将法线的w分量设置为0.这意味着忽略模型视图矩阵中的任何转换。
然后我们设置了光线方向。 如上所述,数学要求光线方向相反,所以这实际上是我们希望光线指向的方向。 在真实世界的应用程序中,可以将光方向作为属性传递并通过矩阵进行转换,但这里我们保持简单。
最后,我们将片段颜色设置为零,我们将使用不同类型的照明来增加它。
要实际进行漫反射计算,我们需要以下代码:
" vec3 diffuseLightIntensity = vec3(1.0, 1.0, 1.0);\n"
" vec3 vertexDiffuseReflectionConstant = vertexColour;\n"
" float normalDotLight = max(0.0, dot(transformedVertexNormal, inverseLightDirection));\n"
" fragColour += normalDotLight * vertexDiffuseReflectionConstant * diffuseLightIntensity;\n"
复制代码
首先,我们得到了光的漫反射分量的RGB强度。 这可以让你设置灯光的颜色(在我们的例子中它是白光)。
然后我们设置一个RGB常数来描述这个表面应该反射的漫射光的比例。 (介于0和1之间)这允许我们设置表面的颜色。 例如,将比率设置为(1.0,0.0,0.0)意味着100%的红灯将被0%的绿光和蓝光反射,将对象的颜色设置为红色。 在这个例子中,我们可以使用我们已经拥有的比率的顶点颜色,因为它将给我们所需的结果。 然而,在实际应用中,场景中的每种材料类型通常将为每个不同的光分量定义单独的比率。
接下来,我们使用法线方向和光线方向的点积来得到两者之间角度的余弦。 对max的调用可确保忽略负值(当光线在曲面后面时)。
最后,我们将它们全部组合在一起,并将结果添加到片段颜色中。
我们在这里做的一个假设是光线是“平行的”。 对于正常的点光源,光线指向特定的位置,距离该点更远,光线越少; 光线像锥形一样展开。 在我们的模型中,情况并非如此,光线只是一个方向。 然而,这被证明是令人惊讶的有用的近似,因为如果光源足够远(例如太阳),那么光线可以被视为平行而不是锥形。
Ambient Light
光线直接从我们的光源落在物体上是很好的,但是从场景中的物体反射的光怎么样呢? 在现实世界中,即使物体处于阴影中,它们也很少是完全黑色的,光线几乎可以反射所有物体,因此不能直接看到光源的区域仍会被反射所触及。 对这些相互作用进行建模就是所谓的“全局照明”。 以物理上正确的方式执行此操作将非常复杂且计算成本高昂。 一种非常简单的模拟方法是使用Ambient Light。 基本上,我们定义场景中的所有内容都被非常小的均匀光线击中。
为此,我们将其添加到顶点着色器:
" vec3 ambientLightIntensity = vec3(0.1, 0.1, 0.1);\n"
" vec3 vertexAmbientReflectionConstant = vertexColour;\n"
" fragColour += vertexAmbientReflectionConstant * ambientLightIntensity;\n"
复制代码
这将设置RGB环境光强度(在我们的简单全局照明近似的场景中有多少反射光)。
然后设置RGB常数来描述该表面应该反射的环境光的比率(如上面的漫反射常数)。 我们再次使用顶点颜色来简化,但这通常是单独指定的。
最后,它只是将两者相乘,并将其添加到累积片段颜色。 这意味着无论表面法线,光线方向和眼睛方向如何,渲染的每个多边形都会有一些光线。
Specular Light
最后,我们想要处理表面的反射或光泽性质。 例如,即使光的方向,光强度,眼睛位置和表面方向相同,闪亮的金属表面和相同颜色的哑光木质表面也会看起来不同。 到目前为止,只使用漫反射和环境光就意味着表面看起来一样。 我们必须考虑的是从表面直接朝向眼睛反射的光量。
我们可以通过首先找出光从表面反射的方式然后做该向量的点积和逆眼方向来做到这一点。
下图显示如果我们找到一个反射向量R,其中Ω=Θ,我们可以使用它与逆眼方向找到Φ。 我们可以使用Φ来控制从表面反射的镜面反射光的强度,以模拟直接向眼睛反射多少光。
有用的是,OpenGL ES SL包括一个反射函数,它采用入射矢量(我们将使用光线方向)和一个曲面法线(N),并为您提供反射方向(R)。
在顶点着色器中,我们添加:
" vec3 inverseEyeDirection = normalize(vec3(0.0, 0.0, 1.0));\n"
" vec3 specularLightIntensity = vec3(1.0, 1.0, 1.0);\n"
" vec3 vertexSpecularReflectionConstant = vec3(1.0, 1.0, 1.0);\n"
" float shininess = 2.0;\n"
" vec3 lightReflectionDirection = reflect(vec3(0) - inverseLightDirection, transformedVertexNormal);\n"
" float normalDotReflection = max(0.0, dot(inverseEyeDirection, lightReflectionDirection));\n"
" fragColour += pow(normalDotReflection, shininess) * vertexSpecularReflectionConstant * specularLightIntensity;\n"
复制代码
正如我们所说,眼睛方向对计算镜面光照至关重要。 与光方向一样,数学需要反转眼睛方向,因此我们指定反眼睛方向。 也像光线方向一样,可能希望将眼睛方向作为属性传递并通过相机矩阵进行转换,但同样,这里我们保持简单。
与环境光和漫射光一样,我们有RGB光强度和RGB镜面反射常数。 与环境和漫反射(我们使用顶点颜色作为常数)不同,这里我们将值设置为(1.0,1.0,1.0),这意味着当R等于眼睛矢量时,镜面反射颜色将是精确的(1.0,1.0,1.0)。 这给了我们一个很好的白色亮点。
亮度(shininess)参数控制高光的“紧密”程度。 我们稍后使用它作为两个向量的点积的指数。 由于点积总是在0和1之间(由于max函数),指数越高,pow计算的结果就越小。 这意味着当R远离眼睛向量时,镜面反射值将更快地减小。
我们使用reflect函数来查找R的值(使用vec3(0) - inverseLightDirection,因为在这种情况下我们想要实际的光方向)。
然后我们做R和眼睛向量的点积,最后将所有东西相乘以计算镜面反射贡献。
The Shader
在顶点着色器中将它们放在一起看起来像这样:
tatic const char glVertexShader[] =
"attribute vec4 vertexPosition;\n"
"attribute vec3 vertexColour;\n"
/* [Add a vertex normal attribute.] */
"attribute vec3 vertexNormal;\n"
/* [Add a vertex normal attribute.] */
"varying vec3 fragColour;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"void main()\n"
"{\n"
/* [Setup scene vectors.] */
" vec3 transformedVertexNormal = normalize((modelView * vec4(vertexNormal, 0.0)).xyz);"
" vec3 inverseLightDirection = normalize(vec3(0.0, 1.0, 1.0));\n"
" fragColour = vec3(0.0);\n"
/* [Setup scene vectors.] */
"\n"
/* [Calculate the diffuse component.] */
" vec3 diffuseLightIntensity = vec3(1.0, 1.0, 1.0);\n"
" vec3 vertexDiffuseReflectionConstant = vertexColour;\n"
" float normalDotLight = max(0.0, dot(transformedVertexNormal, inverseLightDirection));\n"
" fragColour += normalDotLight * vertexDiffuseReflectionConstant * diffuseLightIntensity;\n"
/* [Calculate the diffuse component.] */
"\n"
/* [Calculate the ambient component.] */
" vec3 ambientLightIntensity = vec3(0.1, 0.1, 0.1);\n"
" vec3 vertexAmbientReflectionConstant = vertexColour;\n"
" fragColour += vertexAmbientReflectionConstant * ambientLightIntensity;\n"
/* [Calculate the ambient component.] */
"\n"
/* [Calculate the specular component.] */
" vec3 inverseEyeDirection = normalize(vec3(0.0, 0.0, 1.0));\n"
" vec3 specularLightIntensity = vec3(1.0, 1.0, 1.0);\n"
" vec3 vertexSpecularReflectionConstant = vec3(1.0, 1.0, 1.0);\n"
" float shininess = 2.0;\n"
" vec3 lightReflectionDirection = reflect(vec3(0) - inverseLightDirection, transformedVertexNormal);\n"
" float normalDotReflection = max(0.0, dot(inverseEyeDirection, lightReflectionDirection));\n"
" fragColour += pow(normalDotReflection, shininess) * vertexSpecularReflectionConstant * specularLightIntensity;\n"
/* [Calculate the specular component.] */
"\n"
" /* Make sure the fragment colour is between 0 and 1. */"
" clamp(fragColour, 0.0, 1.0);\n"
"\n"
" gl_Position = projection * modelView * vertexPosition;\n"
"}\n";
复制代码
请注意,最后我们将片段颜色夹在0和1之间,因为这是最大颜色值。 我们也像以前一样在SimpleCube中计算顶点位置。
这不是最佳方式,它是一个易于理解的简化版本。 如果要将此向前推进,可能希望在着色器外部定义以下内容并将其传递给:
- 光线方向
- 眼睛的方向
- 光强度
- 材料反射组件。
对于多个光源,每个光源都需要多个光源方向和光源强度。 要在场景中使用不同反射属性的不同类型的材质,需要为每个单独的材质设置镜面反射,漫反射和环境反射常数。
Messing with Normals
现在已经了解了法线如何影响场景中的光照,可能会意识到,由于我们可以完全控制整个系统,因此我们可以使用法线实现更多效果。 如果我们的法线严格垂直于场景中的多边形,则多边形将相应地点亮,并且所有多边形将单独可见。 也许这就是我们想要的,但更常见的是我们试图表现出光滑的表面,而多边形只是该表面的近似值。 因此,如果我们使法线垂直于原始表面而不是我们的近似,则照明将表现得好像我们有一个更复杂的表示,并且表面看起来更像我们原来的意图。
我们可以在图中看到这一点,虚线是想要表示的表面,实线是可能与多边形近似的方式。 实心矢量是近似表面的真实法线,即它们垂直于几何形状。 虚线矢量是相同点处真实表面的法线。 因此,如果我们使用具有真实表面法线的近似表面。 我们通过精确的照明获得更便宜(渲染)的几何体。
这实际上就是我们在本教程中所做的,因为我们用我们的形状逼近球体,我们已经将法线指定为曲面。 当物体旋转时,屏幕中心的部分应该点亮,就好像它是一个光滑的曲面。 破坏这种错觉的当然是物体的扁平三角形边缘。 这可以通过添加其他几何体来改进。
如果我们在此示例中使用了精确法线,则镜面反射不会产生相同的效果。 因为多边形的每个顶点都具有相同的法线,所以镜面光照组件对于多边形中的每个片段最终都是相同的。 当眼睛方向接近反射矢量时,这将导致多边形“闪烁”。 如果你在对象中有很多几何形状,那就不那么明显了,但是在我们简单的形状上,它会破坏我们想要达到的效果。
我们可以用法线做更多的事情来实现不同的效果,例如凹凸贴图,法线贴图和视差贴图(这些效果需要将一些光照计算移动到片段着色器中)。
- 更改对象的曲面法线时会发生什么?
- 通过改变对象的材质属性(vertexDiffuseReflectionConstant,vertexSpecularReflectionConstant,vertexAmbientReflectionConstant和shininess)可以实现哪些不同的效果?
- 改变光强度对场景有什么作用(diffuseLightIntensity,specularLightIntensity和ambientLightIntensity)?
Normal Mapping
在进行移动端和计算机图形处理时出现的一个问题是场景复杂性和性能之间的平衡。 为了使模型看起来真实,它往往需要大量的顶点和几何。 不幸的是,这会使应用程序运行得非常慢。 法线贴图是一种允许模型看起来具有比实际更复杂的几何形状的方法。
这通过将简单几何与来自相同几何的更复杂版本的曲面法线组合来工作。 首先,我们创建更复杂的几何体,包括所有表面法线的方向。 然后,我们将这些法线保存在纹理中,并将它们应用于简单几何体,使其看起来更逼真。
File Loading
当项目中需要用到很多纹理时,解决此问题的一种方法是将纹理作为资源包含在apk文件本身中。 这样,纹理将被apk压缩并始终可用。 这比将文件放在Android文件系统某处更受欢迎,因为它包含了用户执行的额外步骤。
将资产放入apk非常简单。 如果您注意到项目目录结构中有一个名为assets的文件夹。 您在此文件夹中放置的所有文件都将在构建时压缩到apk中。
public class NormalMapping extends Activity
{
/* [classMembers] */
private static String LOGTAG = "NormalMapping";
private static String assetDirectory = null;
protected TutorialView graphicsView;
private static android.content.Context applicationContext = null;
/* [classMembers] */
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
graphicsView = new TutorialView(getApplication());
/* [onCreateNew] */
applicationContext = getApplicationContext();
assetDirectory = applicationContext.getFilesDir().getPath() + "/";
extractAsset("normalMap256.raw");
/* [onCreateNew] */
setContentView(graphicsView);
}
@Override protected void onPause()
{
super.onPause();
graphicsView.onPause();
}
@Override protected void onResume()
{
super.onResume();
graphicsView.onResume();
}
/* [extractAssetBeginning] */
private void extractAsset(String assetName)
{
File fileTest = new File(assetDirectory + assetName);
if(fileTest.exists())
{
Log.d(LOGTAG,assetName + " already exists no extraction needed\n");
}
else
{
Log.d(LOGTAG, assetName + " doesn't exist extraction needed \n");
/* [extractAssetBeginning] */
/* [tryCatchExtractAsset] */
try
{
RandomAccessFile out = new RandomAccessFile(assetDirectory + assetName,"rw");
AssetManager am = applicationContext.getResources().getAssets();
/* [tryCatchExtractAsset] */
/* [readWriteFile] */
InputStream inputStream = am.open(assetName);
byte buffer[] = new byte[1024];
int count = inputStream.read(buffer, 0, 1024);
while (count > 0)
{
out.write(buffer, 0, count);
count = inputStream.read(buffer, 0, 1024);
}
out.close();
inputStream.close();
}
catch(Exception e)
{
Log.e(LOGTAG, "Failure in extractAssets(): " + e.toString() + " " + assetDirectory+assetName);
}
if(fileTest.exists())
{
Log.d(LOGTAG,"File Extracted successfully");
/* [readWriteFile] */
}
}
}
}
复制代码
Loading the Texture
现在我们已经从资产管理器中提取了所有文件,现在是时候编辑纹理加载函数来从文件加载纹理而不是硬编码值。 首先,我们需要定义一个我们想要填充数据的指针,而不是定义一个将成为我们纹理的数组。
#define TEXTURE_WIDTH 256
#define TEXTURE_HEIGHT 256
#define CHANNELS_PER_PIXEL 3
GLuint loadTexture()
{
static GLuint textureId;
theTexture = (GLubyte *)malloc(sizeof(GLubyte) * TEXTURE_WIDTH * TEXTURE_HEIGHT * CHANNELS_PER_PIXEL);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
/* Generate a texture object. */
glGenTextures(1, &textureId);
/* Activate a texture. */
glActiveTexture(GL_TEXTURE0);
/* Bind the texture object. */
glBindTexture(GL_TEXTURE_2D, textureId);
FILE * theFile = fopen("/data/data/com.zpw.audiovideo/files/normalMap256.raw", "r");
if(theFile == NULL)
{
LOGE("Failure to load the texture");
return 0;
}
fread(theTexture, TEXTURE_WIDTH * TEXTURE_HEIGHT * CHANNELS_PER_PIXEL, 1, theFile);
/* Load the texture. */
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, TEXTURE_WIDTH, TEXTURE_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, theTexture);
/* Set the filtering mode. */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
free(theTexture);
return textureId;
}
复制代码
函数的开头与前一个纹理加载示例完全相同。 唯一的区别是打开文件。 注意我们现在如何硬编码纹理的路径。 理想情况下,我们会将它作为JString从Java端传入。
一旦我们打开文件,我们就会为它提供一些空间。 当我们打开一个原始文件时,数据的呈现方式与纹理示例完全相同。 红色组件后跟绿色,然后最后是蓝色,每个字节大小。 我们的纹理是256 x 256,所以我们需要将此值乘以3,因为每个像素有3个组件。 如果您使用具有不同尺寸的自己的纹理,则需要编辑此行。
然后我们将纹理读入我们刚刚分配的空间,只读取256 x 256 x 3字节。 然后我们像以前一样将这些信息提供给glTexImage2D,然后我们需要释放我们之前分配的空间,因为纹理将被复制到GPU中。
Normal Maps
如前所述,我们将使用法线贴图增加场景的细节和复杂性。 法线贴图是常规贴图,但不是定义像素在屏幕上的颜色,而是确定每个像素的曲面法线。 纹理中的红色,绿色和蓝色通道分别告诉您法线方向的X,Y和Z分量。
对于我们的示例,我们始终将Z坐标完全打开。 为方便起见,下面提供了纹理图像。
请注意,凹凸贴图技术与此非常相似,唯一的区别是凹凸贴图纹理仅在一个通道中定义。 然后,它会告诉您地图中特定点的高度值。 有了这个,您可以在片段着色器中进行一些计算,以实现一些非常好的效果。
Tangent Space
使用法线贴图的唯一困难是它没有考虑对象的旋转。 在照明示例中,我们通过模型视图矩阵转换了法线,但是如果我们从纹理加载则不能这样做。
该问题的一个解决方案是将眼睛矢量和光矢量移动到称为切线空间的东西中。 可以用三个彼此正交的轴来定义空间。 切线空间是在您尝试渲染的面上有两个轴而第三个从面部出来的空间。 从面部出来的轴被称为我们之前讨论过的法线。 剩下的两个中的任何一个被称为切线,而另一个被称为biNormal。 这是一个复杂的概念,有两种方法可以帮助您理解:
- 首先是用你的手。 握住你的手,然后将拇指指向天空,就像你正在竖起大拇指一样。 将食指伸直,使其与拇指成90度角。 将中指伸展一半,使其垂直于食指。 你的三个手指现在应该代表一组轴。 如果你想象你正在处理你的立方体的正面,那么你的中指将代表法线。 你的拇指或食指是你的切线,另一个是你的biNormal。 现在,如果你旋转整个手来表示你想要处理的面部,你应该得到你需要的一组轴。
- 第二种技术是用纸制作一个立方体。 使用标准多维数据集网并创建多维数据集。 然后在立方体的每个面上放置两个垂直箭头。 然后放下立方体,箭头将在每一侧显示切线和biNormal的位置。 你已经知道了面的法线点。 请注意,这对于计算纹理坐标也非常有效。
现在在我们的例子中,我们分别定义了切线和biNormal。 这样做是为了帮助理解所涉及的概念,很有可能通过使用称为叉乘的东西来计算biNormal。
Defining the Normals, Tangents and BiNormals.
GLfloat cubeVertices[] = {-1.0f, 1.0f, -1.0f, /* Back. */
1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, 1.0f, /* Front. */
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, /* Left. */
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f, /* Right. */
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, /* Top. */
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, - 1.0f, -1.0f, /* Bottom. */
-1.0f, -1.0f, 1.0f,
1.0f, - 1.0f, 1.0f,
1.0f, -1.0f, -1.0f
};
GLfloat normals[] = {0.0f, 0.0f, -1.0f, /* Back */
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, 1.0f, /* Front */
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-1.0f, 0.0, 0.0f, /* Left */
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, /* Right */
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, /* Top */
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, -1.0f, 0.0f, /* Bottom */
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f
};
GLfloat colour[] = {1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f
};
GLfloat tangents[] = {-1.0f, 0.0f, 0.0f, /* Back */
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, /* Front */
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, /* Left */
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, -1.0f, /* Right */
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
1.0f, 0.0f, 0.0f, /* Top */
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, /* Bottom */
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f
};
GLfloat biNormals[] = { 0.0f, 1.0f, 0.0f, /* Back */
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f, /* Front */
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f, /* Left */
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f, /* Right */
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, -1.0f, /* Top */
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, 1.0f, /* Bottom */
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f
};
GLfloat textureCords[] = {1.0f, 1.0f, /* Back. */
0.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
0.0f, 1.0f, /* Front. */
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f, /* Left. */
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
1.0f, 1.0f, /* Right. */
1.0f, 0.0f,
0.0f, 0.0f,
0.0f, 1.0f,
0.0f, 1.0f, /* Top. */
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 0.0f, /* Bottom. */
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f
};
GLushort indicies[] = {0, 3, 2, 0, 1, 3, 4, 6, 7, 4, 7, 5, 8, 9, 10, 8, 11, 10, 12, 13, 14, 15, 12, 14, 16, 17, 18, 16, 19, 18, 20, 21, 22, 20, 23, 22};
复制代码
顶点与Simple Cube或Texture Cube示例中的顶点相同。 每个面定义4个以生成同样的立方体。 法线也被定义为每面4个。 与照明示例不同,它们都是相同的,并且都垂直于每面。 因此,顶面有4个指向正Y方向。 背面有4个指向负Z方向,依此类推。
接下来是颜色; 这些直接取自简单的立方体示例,并为每个面定义纯色。 之后是我们的第一个全新的部分,即每个面的切线。 与法线一样,每个面定义4个,它们都是相同的。 对于正面,这是在正X方向上,如前所述,其位于面的平面中并且垂直于在正Z方向上限定的法线。 记住这一点,背面在负X方向上具有切线,右侧和左侧面分别具有负Z和正Z中的一个。 顶部和底部都比较棘手,并且都被定义为在正X方向上具有切线。
为了完成我们的空间,我们需要定义biNormal。 这必须垂直于法线和切线。 现在对于正面,背面,右面和左面,这可能只是正Y方向。 不幸的是,顶部和底部都不能这样做,因为它们的法线分别是正y轴和负y轴。 这意味着我们需要选择正x方向,而不是以与其他边相同的方向生成相同的轴。
另外需要注意的是,我们定义的纹理坐标与我们定义轴的方式之间存在联系。 如果我们错了,那么纹理可以颠倒显示,或者灯光可能看起来不正确。 对于纹理示例也是如此。 但是,由于我们在该示例中没有使用法线或切线,因此当时没有使用法线或切线。
最后,纹理坐标定义为与TextureCube示例中的纹理坐标相同。
Vertex Shader
static const char glVertexShader[] =
"attribute vec4 vertexPosition;\n"
"attribute vec2 vertexTextureCord;\n"
"attribute vec3 vertexNormal;\n"
"attribute vec3 vertexColor; \n"
"attribute vec3 vertexTangent;\n"
"attribute vec3 vertexBiNormal;\n"
"varying vec2 textureCord;\n"
"varying vec3 varyingColor; \n"
"varying vec3 inverseLightDirection;\n"
"varying vec3 inverseEyeDirection;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"void main()\n"
"{\n"
" vec3 worldSpaceVertex =(modelView * vertexPosition).xyz;"
" vec3 transformedVertexNormal = normalize((modelView * vec4(vertexNormal, 0.0)).xyz);"
" inverseLightDirection = normalize(vec3(0.0, 0.0, 1.0));\n"
" inverseEyeDirection = normalize((vec3(0.0, 0.0, 1.0)- worldSpaceVertex ).xyz);\n"
" gl_Position = projection * modelView * vertexPosition;\n"
" textureCord = vertexTextureCord;\n"
" varyingColor = vertexColor;\n"
" vec3 transformedTangent = normalize((modelView * vec4(vertexTangent, 0.0)).xyz);\n"
" vec3 transformedBinormal = normalize((modelView * vec4(vertexBiNormal, 0.0)).xyz);\n"
" mat3 tangentMatrix = mat3(transformedTangent, transformedBinormal, transformedVertexNormal);\n"
" inverseLightDirection =inverseLightDirection * tangentMatrix;\n"
" inverseEyeDirection = inverseEyeDirection * tangentMatrix;\n"
"}\n";
复制代码
顶点着色器看起来比Lighting教程中的顶点着色器更简单。 这是因为许多计算已移至片段着色器。 main函数中的第一行创建一个临时变量,该变量等于由modelView矩阵转换的当前顶点。 在从当前顶点计算眼睛向量时需要这样做。
下一行转换顶点法线的方式与照明教程中的方法大致相同。 虽然我们将要使用的所有复杂法线都在我们的法线贴图中定义,但我们仍然需要使用“真实”顶点法线来创建将光和眼睛矢量移动到切线空间的矩阵。
接下来的两行定义了inverseLightDirection和inverseEyeDirection。 inverseLightDirection使用与Lighting教程相同的代码。 因为我们仍然只处理定向光,所以我们不需要从顶点到光获得矢量,因为所有光都被视为平行方向矢量。
在此之后的三行对我们来说应该是非常简单的东西。 他们将gl_Position设置为与许多其他教程完全相同,并将纹理坐标和颜色传递给片段着色器而不会被触摸。 在这些行之后,我们定义了transformedTangent和transformedBinormal。 还记得我说我们需要3件事来将我们的眼睛和光线矢量转换成切线空间吗? 好吧,我们已经有了法线,所以接下来我们需要在tangent和biNormal中传递并通过modelview矩阵进行转换。
请注意,如果您想通过使用叉积来计算biNormal,那么您应该在这里进行操作。 所以在transformTangent之后你应该写:
vec3 biNormal = cross(transformedVertexNormal, transformedTangent)
复制代码
然后将其作为参数传递给切线矩阵。 请注意,交叉方法内置于OpenGL ES 2.0中。
顶点着色器的最后3行负责将eyeVector和lightVector转换为切线空间。 首先,我们需要创建一个新的变换矩阵,它由我们之前创建的切线,双法线和法线向量组成。 然后,我们依次通过这个新矩阵将光和眼睛相乘。
Fragment Shader
static const char glFragmentShader[] =
"precision mediump float;\n"
"uniform sampler2D texture;\n"
"varying vec2 textureCord;\n"
"varying vec3 varyingColor;\n"
"varying vec3 inverseLightDirection;\n"
"varying vec3 inverseEyeDirection;\n"
"varying vec3 transformedVertexNormal;\n"
"void main()\n"
"{\n"
" vec3 fragColor = vec3(0.0,0.0,0.0); \n"
" vec3 normal = texture2D(texture, textureCord).xyz;"
" normal = normalize(normal * 2.0 -1.0);"
/* Calculate the diffuse component. */
" vec3 diffuseLightIntensity = vec3(1.0, 1.0, 1.0);\n"
" float normalDotLight = max(0.0, dot(normal, inverseLightDirection));\n"
" fragColor += normalDotLight * varyingColor *diffuseLightIntensity;\n"
/* Calculate the ambient component. */
" vec3 ambientLightIntensity = vec3(0.1, 0.1, 0.1);\n"
" fragColor += ambientLightIntensity * varyingColor;\n"
/* Calculate the specular component. */
" vec3 specularLightIntensity = vec3(1.0, 1.0, 1.0);\n"
" vec3 vertexSpecularReflectionConstant = vec3(1.0, 1.0, 1.0);\n"
" float shininess = 2.0;\n"
" vec3 lightReflectionDirection = reflect(vec3(0) - inverseLightDirection, normal);\n"
" float normalDotReflection = max(0.0, dot(inverseEyeDirection, lightReflectionDirection));\n"
" fragColor += pow(normalDotReflection, shininess) * vertexSpecularReflectionConstant * specularLightIntensity;\n"
" /* Make sure the fragment colour is between 0 and 1. */"
" clamp(fragColor, 0.0, 1.0);\n"
" gl_FragColor = vec4(fragColor,1.0);\n"
"}\n";
复制代码
片段着色器看起来应该非常熟悉,因为很多代码来自Lighting教程的顶点着色器。 主函数的第一行设置起始片段颜色并将其设置为黑色或不存在光和颜色。
接下来的两行只是从法线贴图纹理中获得法线,就像我们从Lighting示例中的常规纹理获得颜色一样。 从那时起,一切都与照明示例足够相似。 漫反射被计算出来并添加到碎片颜色,然后是环境光,最后是镜面反射光。 然后将其钳位在0.0和1.0之间,用于设置gl_FragColor。
Extra Code
我们几乎完成了新的法线贴图示例。 我们只需要将着色器中的新属性和uniforms与主程序中的实际值相关联。
GLuint vertexLocation;
GLuint samplerLocation;
GLuint projectionLocation;
GLuint modelViewLocation;
GLuint textureCordLocation;
GLuint colorLocation;
GLuint textureId;
GLuint vertexNormalLocation;
GLuint tangentLocation;
GLuint biNormalLocation;
复制代码
以上是使本教程有效所需的所有位置变量的列表。 从vertexLocation开始,它将存储顶点属性的位置,并以我们的新tangentLocation结束,该tangentLocation将存储我们的切线位置。 将这些位置设置为正确值的代码如下所示。
vertexLocation = glGetAttribLocation(glProgram, "vertexPosition");
textureCordLocation = glGetAttribLocation(glProgram, "vertexTextureCord");
projectionLocation = glGetUniformLocation(glProgram, "projection");
modelViewLocation = glGetUniformLocation(glProgram, "modelView");
samplerLocation = glGetUniformLocation(glProgram, "texture");
vertexNormalLocation = glGetAttribLocation(glProgram, "vertexNormal");
colorLocation = glGetAttribLocation(glProgram, "vertexColor");
tangentLocation = glGetAttribLocation(glProgram, "vertexTangent");
biNormalLocation = glGetAttribLocation(glProgram, "vertexBiNormal");
复制代码
最后,这里是为上述位置提供相应数据的代码。
glVertexAttribPointer(vertexLocation, 3, GL_FLOAT, GL_FALSE, 0, cubeVertices);
glEnableVertexAttribArray(vertexLocation);
glVertexAttribPointer(textureCordLocation, 2, GL_FLOAT, GL_FALSE, 0, textureCords);
glEnableVertexAttribArray(textureCordLocation);
glVertexAttribPointer(colorLocation, 3, GL_FLOAT, GL_FALSE, 0, colour);
glEnableVertexAttribArray(colorLocation);
glVertexAttribPointer(vertexNormalLocation, 3, GL_FLOAT, GL_FALSE, 0, normals);
glEnableVertexAttribArray(vertexNormalLocation);
glVertexAttribPointer(biNormalLocation, 3, GL_FLOAT, GL_FALSE, 0, biNormals);
glEnableVertexAttribArray(biNormalLocation);
glVertexAttribPointer(tangentLocation, 3, GL_FLOAT, GL_FALSE, 0, tangents);
glEnableVertexAttribArray(tangentLocation);
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE,projectionMatrix);
glUniformMatrix4fv(modelViewLocation, 1, GL_FALSE, modelViewMatrix);
复制代码