笔记 OpenGL

基本概念

简介

OpenGL(Open Graphics Library):开放图形库或者“开放式图形库”。是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API,只有框架没有实现,就是只有函数声明没有源文件实现,类似于接口和虚函数,所有的实现是显卡生产商提供)。这个接口由近350个不同的函数调用组成,用来绘制从简单的图形到比较复杂的三维景象。

七大功能:1建立3D模型 2图形变换 3颜色模式 4光照、材质的设置 5纹理映射 6图像增强功能和位图显示的扩展功能 7双缓存功能
其中,图像增强功能和位图显示的扩展功能:OpenGL的功能包括像素的读写、复制外,以及一些特殊的图像处理功能:比如,融合、反走样、雾的等等特殊的处理方式。对于图像的重现和处理,可以使得效果更有真实感,逼真。

空间坐标:使用右手系的定义,与屏幕水平方向一致,并方向向右的是x轴;与屏幕垂直方向一致,并方向向上的是y轴;与屏幕垂直方向,并方向向外的是z轴。

函数库:核心库(gl),实用库(glu),辅助库(aux)、实用工具库(glut),窗口库(glx、agl、wgl)和扩展函数库等;freeglut 完全兼容glut,是glut的代替品,开源,功能齐全,但是bug多;glfw(推荐) 是 glut/freegult 的升级和改进

本篇简单实用 glut 库。项目开发可以:glfw + glew 或 glfw + glad。

总参考1

创建窗口

参考

// glut 是一个处理 OpenGL 程式的工具库,负责处理和底层操作系统的呼叫以及 I/O
// 简单的、容易的、小的、古老的
// freeGLUT 是 GLUT 的一个完全开源替代库
#include <GL/glut.h>
#include <math.h>
​
void redraw() {
    glClear(GL_COLOR_BUFFER_BIT);
    glPointSize(5);
    glLineWidth(5.0f);
    glBegin(GL_LINES);
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex2f(-90, -90);
    glVertex2f(90, 90);
    glEnd();
​
    glFlush();
}
void reshape(int width,int height) {
    if (height==0)
        height=1; // 设置为1,防止除0

    glViewport(0,0,width,height); // 设置“视口”,(原点x,原点y,长度,宽度)
    glMatrixMode(GL_PROJECTION); // 设置了矩阵的模式
// 参数:GL_PROJECTION操作投影, GL_MODELVIEW操作模型视景, GL_TEXTURE操作纹理。
    glLoadIdentity(); // 加载单位矩阵。后面的变换矩阵都是和它做乘法
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);
// 参数1表示视角大小,可以简单当成我们眼睛张开多大。2表示宽高比。
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
​
void init() {
    glClearColor(0.0, 0.0, 0.0, 0.0);
    gluOrtho2D(-100, 100, -100, 100);
}

​/*没有窗口事件发生时调用*/
void idle() {
    glutPostRedisplay();
// 加入glutPostRedisplay()来重绘图像,否则图像只有响应鼠标或键盘消息时才会更新图像
}

int main(int argc, char** argv) {
    glutInit(&argc,argv); // 标配,初始化
    glutInitDisplayMode(GLUT_RGBA|GLUT_DOUBLE);
    // GLUT_SINGLE单缓冲,屏幕显示调用glFlush(),将图像在当前显示缓存中直接渲染,
    // 会有图形闪烁的问题
    // GLUT_DOUBLE双缓冲,屏幕显示调用glutSwapBuffers(),将图像先绘制在另外的缓存中,
    // 渲染完毕之后,将其整个缓存贴到当前的窗口,不会闪烁,一般动画用双缓冲.
    glutInitWindowSize(640,480);
    glutCreateWindow("name");

    init();
    glutDisplayFunc(redraw); // 调用绘图函数
    glutReshapeFunc(reshape); // 调整图形比例,保证当窗口大小变化时图形不走样
    glutIdleFunc(idle); // 在没有窗口事件的时候OpenGL干什么
    glutMainLoop(); // 进入GLUT事件处理循环,让所有与事件有关的函数无限循环
// 标配,因为OpenGL是逐帧绘制,要画出完整图像相当于进到了一个循环里,不断地画
    return 0;
}

glutInitDisplayMode 参数值范围:GLUT_SINGLE | GLUT_DOUBLE | GLUT_RGBA | GLUT_RGB | GLUT_INDEX | GLUT_DEPTH | GLUT_STENCIL 等。

glutDisplayFunc 在程序运行时是自动调用的,即程序在以下情况会自动调用display函数重绘窗口:1窗口内容绘制,2窗口大小改变,3窗口重绘。

VAOVBO

用于绘制的顶点数组数据首先保存在 CPU 内存,在调用 glDrawArrays 或者 glDrawElements 等进行绘制时,需要将顶点数组数据从 CPU 内存拷贝到显存。但是很多时候没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以降低内存拷贝的开销。
VBO 和 EBO 的出现就是为了解决这个问题。它们在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据,从而避免每次绘制时的 CPU 与 GPU 之间的内存拷贝,可以改进渲染性能,降低内存带宽和功耗。

VBO(Vertex Buffer Object):顶点缓冲区对象
EBO(Element Buffer Object):图元索引缓冲区对象
(EBO 实际上跟 VBO 一样,只是按照用途的另一种称呼)
VAO(Vertex Array Object):顶点数组对象,主要用于管理 VBO 或 EBO ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。
UBO(Uniform Buffer Object):一个装载 uniform 变量数据的缓冲区对象。当数据加载到 UBO ,那么这些数据将存储在 UBO 上,而不再交给着色器程序,所以它们不会占用着色器程序自身的 uniform 存储空间,UBO 是一种新的从内存到显存的数据传递方式
FBO(Frame Buffer Object):帧缓冲区对象,实际上是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO)。FBO 本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了 3 个附着(Attachment),分别是颜色附着、深度附着和模板附着。
RBO(Render Buffer Object):渲染缓冲区对象,是一个由应用程序分配的 2D 图像缓冲区。渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作 FBO 中的颜色、深度或者模板附着。
TBO(Texture Buffer Object):纹理缓冲区对象,需要配合缓冲区纹理(Buffer Texture)一起使用,Buffer Texture 是一种一维纹理,其存储数据来自纹理缓冲区对象(TBO),用于允许着色器访问由缓冲区对象管理的大型内存表。

管线Pipeline

参考。图形渲染管线被认为是实时图形学中的核心部分,主要包括两个功能:

  1. 将物体3D坐标转变为屏幕空间2D坐标
  2. 给屏幕每个像素点着色

渲染管线的功能是通过给定虚拟相机、3D场景物体以及光源等场景要素来渲染一副2D的图像。其中对象物体的位置形状是由它们的几何形状、环境特性以及在环境中摄像机的位置所共同决定的;对象物体的外观表现则是由材质的属性、光源、纹理贴图、着色模型所影响。

整个处理流程可以被划分为几个阶段,上一个阶段的输出数据作为下一个阶段的输入数据,是一个串行的,面向过程的操作。每一个阶段分别在GPU上运行各自的数据处理程序,这个程序就是着色器(shader)

部分着色器允许我们使用着色语言(OpenGL Shading Language)编写自定义的着色器,这样就可以更为细致的控制图像渲染流程中的特定处理过程了,下图是一个图形渲染管线每一个阶段的抽象表示,蓝色部分代表允许自定义着色器。

  • 1顶点数据(Vertex Data)
  • 是渲染管线的主要数据来源。送入到渲染管线的数据包括顶点坐标、纹理坐标、顶点法线和顶点颜色等顶点属性。为了让OpenGL明白顶点数据构成的是什么图元,我们需要在绘制指令中传递相对应的图元信息,常见的图元包括:点(GL_POINTS)、线(GL_LINES)、线条(GL_LINE_STRIP)、三角面(GL_TRIANGLES)。
  • 2顶点着色器(Vertex Shader)
  • 顶点着色器主要功能是进行坐标变换。将输入的局部坐标变换到世界坐标、观察坐标和裁剪坐标。虽然我们也会在顶点着色器进行光照计算(称作高洛德着色),但这种方法得到的光照比较不自然,所以一般在片段着色器进行光照计算
  • 3图元装配(Shape Assembly)
  • 图元组装将输入的顶点组装成指定的图元,包括点,线段,三角形等,是构成实体模型的基本单位。图元组装阶段会进行裁剪和背面剔除相关的优化,以减少进入光栅化的图元的数量,加速渲染过程。在光栅化之前,还会进行屏幕映射的操作:透视除法和视口变换。
  • 4几何着色器(Geometry Shader)
  • 几何着色器也是渲染管线一个可选的阶段。我们知道,顶点着色器的输入是单个顶点(以及属性), 输出的是经过变换后的顶点。与顶点着色器不同,几何着色器的输入是完整的图元(比如,点),输出可以是一个或多个其他的图元(比如,三角面),或者不输出任何的图元。几何着色器的拿手好戏就是将输入的点或线扩展成多边形。下图展示了几何着色器如何将点扩展成多边形。
  • 5细分着色器(Tesselation shader(s))
  • 曲面细分是利用镶嵌化处理技术对三角面进行细分,以此来增加物体表面的三角面的数量,是渲染管线一个可选的阶段。它由外壳着色器(Hull Shader)、镶嵌器(Tessellator)和域着色器(Domain Shader)构成,其中外壳着色器和域着色器是可编程的,而镶嵌器是有硬件管理的。我们可以借助曲面细分的技术实现细节层次(Level-of-Detail)的机制,使得离摄像机越近的物体具有更加丰富的细节,而远离摄像机的物体具有较少的细节。
  • 6光栅化(Rasterization)
  • 光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程。这是一个将模拟信号转化为离散信号的过程。
  • 光栅化过程产生的是片元,片元中的每一个元素对应于帧缓冲区中的一个像素。光栅化会确定图元所覆盖的片段,利用顶点属性插值得到片段的属性信息,然后送到片段着色器进行颜色计算,我们这里需要注意到片段是像素的候选者,只有通过后续的测试,片段才会成为最终显示的像素点。
  • 光栅化其实是一种将几何图元变为二维图像的过程。该过程包含了两部分的工作。
  • 第一部分工作:决定窗口坐标中的哪些整型栅格区域被基本图元占用;
  • 第二部分工作:分配一个颜色值和一个深度值到各个区域。
  • 7片段着色器(Fragment Shader)
  • 片段着色器用来决定屏幕上像素的最终颜色。在这个阶段会进行光照计算以及阴影处理,是渲染管线高级效果产生的地方。在计算机图形中,颜色被表示为有4个元素的数据,RGBA(RGBA是代表Red(红色)Green(绿色)Blue(蓝色)和Alpha的色彩空间),当在OpenGL或者GLSL中定义一个颜色,我们把颜色每个分量的强度设置在0.0到1.0之间。
  • 8测试与混合(Tests And Blending)
  • 管线的最后一个阶段是测试混合阶段。测试包括裁切测试、Alpha测试、模板测试和深度测试。没有经过测试的片段会被丢弃,不需要进行混合阶段;经过测试的片段会进入混合阶段。Alpha混合可以根据片段的alpha值进行混合,用来产生半透明的效果,这些测试与混合操作决定了屏幕视窗上每个像素点最终的颜色以及透明度。
  • Alpha表示的是物体的不透明度,因此alpha=1表示完全不透明,alpha=0表示完全透明。测试混合阶段虽然不是可编程阶段,但是我们可以通过OpenGL或DirectX提供的接口进行配置,定制混合和测试的方式。

一、简单几何

glBegin(GLenum mode)——glEnd():

  • glVertex2f() 二维顶点坐标;

  • glColor3f() 顶点颜色;

  • glVertex3fv(v) 三维顶点坐标

  • glColor3fv(c) 顶点颜色

  • glPointSize() 大小,线宽,默认1像素;

  • glEnable/glDisable(GL_LINE_STIPPLE) 设置虚线/设置关闭;

  • glNormal*(x, y, z) 设置法向量

  • glEvalCoord*() 产生坐标

  • glCallList(),glCallLists() 执行显示列表

  • glTexCoord*() 设置纹理坐标

  • glEdgeFlag*() 控制边界绘制

  • glMaterial*() 设置材质

  • glIndex*() 设置当前颜色表

注意:glTranslatef(),glScalef(),glRotatef()等此类几何变换接口的作用是对当前模型空间进行几何变换,写在glBegin()和glEnd()之间是无效的。

几何图元类型:

  • GL_POINTS 单个顶点集;

  • GL_LINES 多组双顶点线段;

  • GL_POLYGON 单个简单填充凸多边形(正方形、三角形);

  • GL_TRAINGLES 多组填充三角形;

  • GL_QUADS 多组填充四边形;

  • GL_LINE_STRIP 不闭合折线;

  • GL_LINE_LOOP 闭合折线;

  • GL_TRAINGLE_STRIP 线型连续填充三角形串

  • GL_TRAINGLE_FAN 扇形连续填充三角形串

  • GL_QUAD_STRIP 连续填充四边形串

其他:

gluCylinder()圆柱体: GLUquadricObj* objCylinderz = gluNewQuadric(); gluCylinder(objCylinderz, 0.01f, 0.01f, 1.0f, 5, 5); //参数(指定对象,z=0时半径,z=height时半径,高度height,围绕z轴的细分数,沿z轴的细分数)环;

glutSolidSphere,glutWireSphere--绘制实心球体和线框球体:glutSolidSphere(半径, 面数, 面数); glutsolidCube,glutwireCube--绘制实心立方体和线框立方体;glutsolidCone,glutwireCone--绘制实心圆锥体和线框圆锥体;glutsolidTorus,glutwireTorus--绘制实心圆环和线框圆环; glutSolidDOdeCahedroll,glLltwiFeDOdechedfotl--绘制实心十二面体和线框十二面体; glutSolidOctahedron,glutWireOctahedron--绘制买心八面体和线框八面体;glutsolldTetrahedron,glutwireTetrahedron--绘制实心四面体和线框四面体;glutSollelcosahedron,glutwirelcosahedron--绘制实心二十面体和线框二十面体;glutsolidTeapot,glutwireTeapot--绘制实心茶壶和线框茶壶

glClear(GL_COLOR_BUFFER_BIT);
glPointSize(3);//笔占3个像素
glColor3f(1.0, 1.0, 0.0);//3f指要传三个数进去
glEnable(GL_LINE_SMOOTH);//开启了反走样,可以使用小数
​
glBegin(GL_POLYGON);//矩形
glVertex2f(-0.5f, -0.5f);
glVertex2f(0.0f, -0.5f);
glVertex2f(0.0f, 0.0f);
glVertex2f(-0.5f, 0.0f);
glEnd();
​
glBegin(GL_TRIANGLES);//三角形
glVertex2i(-10,20);
glVertex2i(90,10);
glVertex2i(-71,-62);
glEnd();
​
glBegin(GL_POINTS);//点
glVertex2i(3, -3);//坐标值
glVertex2i(13, 10);//坐标值
glEnd();
​
glEnable(GL_LINE_STIPPLE);
glLineStipple(2, 0x0F0F);//设置虚线
​
glBegin(GL_LINES);//线段
glColor3f(1.0, 0.0, 0.0);
glVertex2i(10, -100);
glVertex2i(10, 100);
glEnd();
​
glBegin(GL_LINE_LOOP);//多边形(闭合折线)
glVertex2i(-99,-100);
glVertex2i(-99,99);
glVertex2i(100,99);
glVertex2i(100,-100);
glEnd();
​
glFlush(); //强制刷新缓冲
const GLfloat R = 15.5f;
const GLfloat Pi = 3.14159265358979f;
​
glBegin(GL_POLYGON);//画圆,x=r*cos,y=r*sin;
glColor3f(1.0, 1.0, 0.0);
for (int i = 1; i < n; i++)
{
    glVertex2f(R * cos(2 * Pi / n * i), R * sin(2 * Pi / n * i));
}
glEnd();

二、几何细节

1.多边形两面

一个多边形具有两个面,每一个面可以设置不同的绘制方式:填充、只绘制边缘轮廓线、只绘制顶点,默认为“填充”。可以为两个面分别设置不同的方式。

//多边形两面可以分别设置
glPolygonMode(GL_FRONT, GL_FILL);  //设置正面为填充模式
glPolygonMode(GL_BACK, GL_LINE);   //设置反面为线性模式
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);  // 设置两面均为顶点模式
​
glFrontFace(GL_CCW);   // 设置CCW逆时针方向为“正面”(默认正面)
//glFrontFace(GL_CW);  // 设置CW顺时针方向为“正面”

2.多边形反转

一般约定“顶点以逆时针顺序出现在屏幕上的面”为“正面”,另一个面即成为“反面”。

一个平面具有正反两面,opengl画点的顺序不同(顺逆时针),出现的面就不同

     glBegin(GL_POLYGON);// 按逆时针绘制,正面模式
         glVertex2f(-0.5f, -0.5f);
         glVertex2f(0.0f, -0.5f);
         glVertex2f(0.0f, 0.0f);
         glVertex2f(-0.5f, 0.0f);
     glEnd();
​
     glBegin(GL_POLYGON);// 按顺时针绘制,反面模式
         glVertex2f(0.0f, 0.0f);
         glVertex2f(0.0f, 0.5f);
         glVertex2f(0.5f, 0.5f);
         glVertex2f(0.5f, 0.0f);
     glEnd();

3.多边形剔除表面

一个多边形有两个面,我们无法看见背面;还有些多边形是正面的,但被其他多边形遮挡。此时,可以将不必要的面剔除来提高效率

*剔除功能只影响多边形,而对点和直线无影响

glEnable(GL_CULL_FACE);//启动剔除功能
​
glCullFace(GL_FRONT);//剔除正面
//(GL_BACK);背面
//(GL_FRONT_AND_BACK);//正反面
​
//glBegin()——body——glEnd();
​
glDisable(GL_CULL_FACE);//关闭

4.多边形镂空

直线可以画成虚线,填充多边形可以镂空;

镂空的部分显示背景色,测试(如ALPHA)不通过的部分不显示

//先设置一个数组mmm表示一个矩形应该如何镂空,就是1&0
static GLubyte Mask[128];
FILE* fp;
fp = fopen("mmm.bmp", "rb");
    //设置图片的宽高均为32,取名为mmm.bmp,保存为“单色位图”
if (!fp)
    exit(0);
if (fseek(fp, -(int)sizeof(Mask), SEEK_END))
    exit(0);
if (!fread(Mask, sizeof(Mask), 1, fp))
    exit(0);
fclose(fp);
​
//开启镂空
glEnable(GL_POLYGON_STIPPLE);
glPolygonStipple(mmm);//设置镂空样式
    //gl_Begin————gl_End;
glDisable(GL_POLYGON_STIPPLE);

三、三维变换

在一个三维的世界——如果要观察一个物体,可以:

  1. 从不同的位置观察它(视点变换

  2. 移动或旋转,放大或缩小它(模型变换

  3. 如果把物体画下来,我们可以选择:是否需要一种“近大远小”的透视效果。是否只要物体的一部分,而不是全部(剪裁)(投影变换

  4. 确定照片的大小,放大照片还是缩小照片。(视口变换

3种矩阵

在OpenGL中,MVP(Model-View-Projection)矩阵是一种用于进行3D图形变换的常见矩阵组合。下面是它们的作用分别:

Model Matrix(模型矩阵):作用: 用于将模型的局部坐标系转换为世界坐标系。模型矩阵包含了平移、旋转和缩放等变换,使得模型可以在世界空间中正确定位和变换。
公式: FinalPosition = ModelMatrix * VertexPosition


View Matrix(视图矩阵):作用: 用于定义观察者的视角和位置,将世界坐标系中的物体变换到相机坐标系。视图矩阵决定了我们从哪个视角观察场景。
公式: CameraSpacePosition = ViewMatrix * WorldSpacePosition


Projection Matrix(投影矩阵):作用: 用于将相机坐标系中的物体变换为裁剪坐标系,进行透视或正交投影。投影矩阵定义了视锥体,将场景投影到裁剪坐标系,最终形成我们看到的屏幕图像。
公式: ClipSpacePosition = ProjectionMatrix * CameraSpacePosition

MVP Matrix(Model-View-Projection 矩阵):作用: 将模型、视图和投影矩阵组合在一起,用于将局部坐标系的模型变换到屏幕空间。MVP矩阵是通过将模型矩阵、视图矩阵和投影矩阵相乘得到的。
公式: ClipSpacePosition = MVPMatrix * VertexPosition

物体坐标 * 模型矩阵 * 视图矩阵 * 投影矩阵 + 裁剪等操作 ——> 屏幕图像

总体而言,MVP矩阵的应用是将3D场景中的对象正确变换和投影到屏幕上,使得它们在屏幕上正确可视化。这些矩阵的组合和使用是3D图形渲染中的基本步骤。

——练习下基本流程:

//自带的球形函数glutSolidSphere()
glEnable(GL_DEPTH_TEST); //启动深度测试
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //清空深度缓冲和颜色缓冲
glMatrixMode(GL_PROJECTION); //操作投影矩阵
glLoadIdentity(); //进行变换前把当前矩阵设置为单位矩阵
gluPerspective(75, 1, 1, 400); 
    //设置可视空间,得到透视效果(可视角,高宽比,最近可视距离,最远可视距离)
glMatrixMode(GL_MODELVIEW); //设置当前操作的矩阵为“模型视图矩阵”
glLoadIdentity();
gluLookAt(0, -200, 200, 0, 0, 0, 0, 0, 1);
    //设定观察点位置(观察点位置,目标位置,观察者上方方向)
​
    //太阳
glColor3f(0.9f, 0.1f, 0.1f);//颜色
glutSolidSphere(70, 20, 20);//半径,面数(精确度)
    //地球
glColor3f(0.1f, 0.3f, 0.7f);
glRotatef(day, 0.0f, 0.0f, -1.0f);//旋转
glTranslatef(150.0f, 0.0f, 0.0f);//位移
glutSolidSphere(15, 15, 15);
    //月亮
glColor3f(0.8f, 0.8f, 0.1f);
glRotatef(day / 30.0 * 360.0 - day, 0.0f, 0.0f, -1.0f);
glTranslatef(38, 0.0f, 0.0f);
glutSolidSphere(5, 10, 10);
​
glutSwapBuffers();

  

——练习正四面体:

//自己用顶点绘制四面体
//提前定义好颜色和顶点
glBegin(GL_TRIANGLES);
// 平面1
ColoredVertex(ColorR, PointA);
ColoredVertex(ColorG, PointB);
ColoredVertex(ColorB, PointC);
// 平面2
ColoredVertex(ColorR, PointA);
ColoredVertex(ColorB, PointC);
ColoredVertex(ColorY, PointD);
// 平面3
ColoredVertex(ColorB, PointC);
ColoredVertex(ColorG, PointB);
ColoredVertex(ColorY, PointD);
// 平面4
ColoredVertex(ColorG, PointB);
ColoredVertex(ColorR, PointA);
ColoredVertex(ColorY, PointD);
glEnd();
​
glEnable(GL_DEPTH_TEST);
glFlush();

正对我们的面,

按逆时针顺序,背对我们的面,则按顺时针顺序。

4种变换

模型变换

  1. glTranslate*,移动,glTranslate(x,y,z);//在三个坐标上的位移值

  2. glRotate*,旋转,glRotate(1,2,3,4); //1是旋转的角度,而2,3,4三个参数组成一个向量,表示物体围绕向量[2,3,4]逆时针旋转

  3. glScale*,把当前矩阵和一个表示缩放物体的矩阵相乘。x,y,z分别表示在该方向上的缩放比例。

  4. glMatrixMode,参数有三个选项:GL_PROJECTION 投影(可接gluPerspective), GL_MODELVIEW 模型视图(接gluLookAt), GL_TEXTURE 纹理.(1)参考

  5. glLoadIdentity,进行变换前把当前矩阵设置为单位矩阵()

  6. gluPerspective,设置可视空间,得到透视效果(可视角,高宽比,最近可视距离,最远可视距离)

(5)参数GL_PROJECTION表示投影,在投影矩阵之前需调用,就是要对投影进行相关操作时,也就是把3维物体投影到2维的平面上时;这样,接下来把矩阵设为单位矩阵glLoadIdentity(),而后调用glFrustum()或gluPerspective(),它们生成的矩阵会与当前的矩阵相乘,生成透视的效果;

(5)参数GL_MODELVIEW是对模型视景的操作,接下来的语句描绘一个以模型为基础的适应,这样来设置参数,接下来用到的就是像gluLookAt()这样的函数; (5)参数GL_TEXTURE,是对纹理相关进行操作; (5)操作大多是基于矩阵的,比如位移,旋转,缩放;这里其实是用glMatrixMode来指定哪个矩阵是当前矩阵,GL_PROJECTION是对投影矩阵操作,GL_MODELVIEW是对模型视景矩阵操作,GL_TEXTURE是对纹理矩阵进行随后的操作

(6)参数1.指定视景体的视野的角度,以度数为单位,y轴的上下方向;2.指定你的视景体的宽高比(x 平面上);3.指定观察者到视景体的最近的裁剪面的距离(必须为正数);4.指定观察者到视景体的最远的裁剪面的距离(必须为正数)

视点变换

gluLookAt,设置观察点位置,(GLdouble eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz);

投影变换

投影变换就是定义一个可视空间,可视空间以外的物体不会被绘制到屏幕上。

透视投影所产生的结果类似于照片,有近大远小的效果

正投影相当于在无限远处观察得到的结果,它只是一种理想状态,对计算机来说速度更快,无论物体距离相机多远,投影后的物体大小尺寸不变

void **glFrustum**(GLdouble left, right, bottom, top, near, far); // 透视投影
void **glOrtho**(GLdouble left, right, bottom, top, near, far); // 正投影

视口变换

将视景体内投影的物体显示在二维的视口平面上。运用相机模拟方式,我们很容易理解视口变换就是类 似于照片的放大与缩小。在计算机图形学中,它的定义是将经过几何变换、投影变换和裁剪变换后的物体显示于屏幕窗口内指定的区域内,这个区域通常为矩形,称为视口。

glViewport,定义绘制到窗口的范围,(GLint x,GLint y,GLsizei width,GLsizei height),x,y 以像素为单位,指定了窗口的左下角位置;width,height表示视口矩形的宽度和高度,根据窗口的实时变化重绘窗口。缺省时,参数值即(0, 0, winWidth, winHeight) 指的是屏幕窗口的实际尺寸大小。所有这些值都是以象素为单位,全为整型数。

6种坐标系

OpenGL中只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系。而模型坐标系、世界坐标系和相机坐标系都是为了方便用户设计而自定义的坐标系

OpenGL总共有6个坐标系,分别是:

  • 模型坐标系:仅适用于该物体的坐标系,以物体某一点为原点,用来简化对物体各部分坐标的描述,也叫当前绘图坐标系,是绘制物体时的坐标系。程序刚初始化时,世界坐标系和当前绘图坐标系是重合的,当用几何变换(glTranslatef,glScalef,glRotatef)对当前绘图坐标系进行平移、伸缩、旋转变换之后,世界坐标系和当前绘图坐标系不再重合;

    • GL_MODELVIEW矩阵是模型变换和视图变换矩阵的组合,使用GL_MODELVIEW矩阵可以使对象直接从对象坐标系转换到眼睛坐标系

  • 世界坐标系:以屏幕中心为原点(0, 0, 0),且始终不变的

  • 相机坐标系:如果相机位置不同,那么观察物体的角度则不同,看到的样子也不同,默认与世界坐标系重合,使用 gluLookAt() 可以指定位置和方向;眼坐标到裁剪坐标是通过投影完成的,投影分为透视投影(进大远小)和正投影,眼坐标乘以GL_PROJECTION矩阵变成了裁剪坐标

  • 裁剪坐标系:执行矩阵变换和投影之后得到,超出裁剪空间的坐标会被丢弃

    • 通过一个透视投影矩阵把顶点从观察空间转换到一个裁剪空间下,转换的过程实际是对x,y,z分量都进行了不同程度的缩放和平移。使x,y,z值满足:直接用w分量作为裁剪的范围值,如果变换后的x,y,z分量都位于[-w,w]这个范围内,就说明该顶点位于裁剪空间内,反之会被剔除。 

  • 规范化设备坐标系(NDC):当完成所有的裁剪工作后,就要进行真正的投影了,即把视锥体投影到屏幕空间中,从而得到真正的二维像素坐标。期间需要进行两步操作,齐次除法映射输出

    • 齐次除法/透视除法:用齐次坐标系的 w 分量去除以x,y,z分量,坐标系由裁剪空间转换到了NDC(归一化的设备坐标,Normalized Device Coordinates)下,此时的x,y,z分量的范围值为[-1,1]。

    • OpenGL 的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。

    • a =(x,y,z,w),当w分量大于零时是世界坐标系下a在视点之前,w分量小于零时是在视点之后。

  • 屏幕坐标系:以左下角为原点,右上角对应屏幕最大像素值;

    • 映射输出:NDC下的x和y的坐标范围是[-1,1],而屏幕空间左下角像素坐标是(0,0),右上角的像素坐标是(pixelWidth,pixelHeight),因此x和y会先除以2再加1/2,映射到[0,1],然后再分别乘以pixelWidth和pixelHeight,得到屏幕坐标

坐标变换:

四、光照

光照系统分为三部分:光源、材质、光照环境(OpenGL不考虑光的折射);

光照环境:值一些额外的参数。光线经过多次反射后,已经无法分清它是由哪个光源发出的,因此指定一个“环境亮度”参数,可以使最后形成的画面更接近真实;

法线:用两个边的向量叉乘得到法线向量;glScale可能导致法线向量的不正确,尽量避免使用;

glEnable:OpenGL默认关闭光照处理,使用glEnable(GL_LIGHTING);打开光照处理功能;

控制光源

——设置光源属性:

void glLightfv(GLenum  light, GLenum  pname, const GLfloat *params);

light参数值:指明是设置哪一个光源的属性。至少支持八个灯光,即from GL_LIGHT0 to GL_LIGHT7。使用glEnable(GL_LIGHT0);可以开启第0号光源,使用glDisable关闭光源;

pname参数值:指明是设置该光源的哪一个属性。GL_AMBIENT 环境光 默认 (0.0, 0.0, 0.0, 1.0) GL_DIFFUSE 漫反射光 默认 (1.0, 1.0, 1.0, 1.0) GL_SEPCULAR 镜面反射光 默认 (1.0, 1.0, 1.0, 1.0) GL_POSITION 光源位置齐次坐标(x,y,z,w) 默认 (0.0, 0.0, 1.0, 0.0) GL_SPOT_DIRECTION 点光源聚光方向矢量(x,y,z) 默认 (0.0, 0.0, -1.0) GL_SPOT_EXPONENT 点光源聚光指数 默认 (0.0) GL_SPOT_CUTOFF 点光源聚光截止角 默认 (180) GL_CONSTANT_ATTENUATION 常量衰减因子 默认 (1.0) GL_LINER_ATTENUATION 线性衰减因子 默认 (0.0) GL_QUADRATIC_ATTENUATION 平方衰减因子 默认 (0.0) // (R, G, B, A)。

params参数值:指明把该属性值设置成多少。是一个指向数组的指针(向量)或数值(非向量)。

void setLight()
  //定义一个蓝绿色的光
{
    static const GLfloat light_position[] = { 1.0f,1.0f,-1.0f,1.0f };
    static const GLfloat light_ambient[] = { 0.0f,0.6f,0.6f,1.0f };
    static const GLfloat light_diffuse[] = { 0.0f,1.0f,1.0f,1.0f };
    static const GLfloat light_specular[] = { 0.0f,1.0f,1.0f,1.0f };
​
    glLightfv(GL_LIGHT0, GL_POSITION, light_position); // 位置
    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); // 环境光
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); // 漫反射光
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); // 镜面光
​
    glEnable(GL_LIGHT0);
    glEnable(GL_LIGHTING);
    glEnable(GL_COLOR_MATERIAL);
    //在光照下希望模型的颜色可以起作用,需要启动颜色材料模式
}
  1. GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR:这三个属性表示了光源所发出的光的反射特性(以及颜色)。每个属性的四个值代表颜色的R, G, B, A值。GL_AMBIENT表示环境光的强度(颜色)。GL_DIFFUSE表示漫反射得到的光的强度(颜色)。GL_SPECULAR表示镜面反射得到的光的强度(颜色)。

  2. GL_SHININESS:镜面指数,取值范围是0到128,值越小表示材质越粗糙。光源照射到上面后,产生较小的亮点。GL_EMISSION属性。由四个值组成,表示一种颜色。材质本身就微微的向外发射光线,因此眼睛感觉到它有这样的颜色,但这光线又比较微弱,因此也不会影响到其它物体的颜色。

  3. GL_POSITION:表示光源所在的位置。参数(X, Y, Z, W),如果第四个值W为零,则表示该光源位于无限远处,前三个值表示了它所在的方向,这种光源称为方向性光源,比如太阳;如果第四个值W不为零,则X/W, Y/W, Z/W表示了光源的位置,这种光源称为位置性光源。设置位置性光源的位置与设置多边形顶点的方式相似,各种矩阵变换函数glTranslateglRotate等在这里也同样有效。方向性光源计算速度比位置性光源更快,因此应尽量使用方向性光源

  4. GL_SPOT_DIRECTION、GL_SPOT_EXPONENT、GL_SPOT_CUTOFF:只对位置性光源有效,表示将光源作为聚光灯使用。大多光源是向四面八方发射光线,但有些光源则是只向某个方向发射,比如手电筒。GL_SPOT_DIRECTION属性有三个值,表示一个向量,即光源发射的方向。GL_SPOT_EXPONENT属性只有一个值,表示聚光的程度,为零时表示光照范围内向各方向发射的光线强度相同,为正数时表示光照向中央集中,正对发射方向的位置受到更多光照,其它位置受到较少光照。数值越大,聚光效果就越明显。GL_SPOT_CUTOFF属性也只有一个值,表示一个角度,它是光源发射光线所覆盖角度的一半,其取值范围在0到90之间,加上180这个特殊值。取值为180表示光源发射光线覆盖360度,即不使用聚光灯,全发射。

  5. GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、GL_QUADRATIC_ATTENUATION:只对位置性光源有效,表示了光源所发出的光线的直线传播特性。现实生活中,光线的强度随着距离的增加而减弱,OpenGL把这个减弱的趋势抽象成函数:衰减因子 = 1 / (k1 + k2 * d + k3 * k3 * d) 其中d表示距离,光线的初始强度乘以衰减因子,就得到对应距离的光线强度。k1, k2, k3分别就是GL_CONSTANT_ATTENUATION,GL_LINEAR_ATTENUATION,GL_QUADRATIC_ATTENUATION。通过设置这三个常数,就可以控制光线在传播过程中的减弱趋势

控制材质

void glMaterial(GLenum face, GLenum pname, TYPE param);

face参数值:指出材质属性将应用于物体的哪面。GL_FRONT、GL_BACK或GL_FRONT_AND_BACK。

pname参数值:指出要设置哪种材质属性。GL_AMBIENT 材质的环境光颜色 默认(0.2, 0.2, 0.2, 1.0) GL_DIFFUSE 材质的漫反射光颜色 默认(0.8, 0.8, 0.8, 1.0) GL_AMBIENT_AND_DIFFUSE 环境光和漫反射光颜色 默认(0.8, 0.8, 0.8, 1.0) GL_SEPCULAR 材质镜面反射光颜色 默认(0.0, 0.0, 0.0, 1.0) GL_SHININESS 镜面指数(高光) 默认(0.0) GL_EMISSION 材质辐射光颜色 默认(0.0, 0.0, 0.0, 1.0) GL_COLOR_INDEXS 材质环境、漫反射和镜面反射颜色 默认(0, 0, 1)。

param参数值:指明把该属性值设置成多少。是一个指向数组的指针(向量)或数值(非向量)。

{
    GLfloat a_mat_ambient[] = { 0.0f,0.0f,0.5f,1.0f };
    GLfloat a_mat_diffuse[] = { 0.0f,0.0f,0.5f,1.0f };
    GLfloat a_mat_specular[] = { 0.0f,0.0f,1.0f,1.0f };
    GLfloat a_mat_emission[] = { 0.0f,0.0f,0.0f,1.0f };
    GLfloat a_mat_shininess = 30.0f;
​
    glMaterialfv(GL_FRONT, GL_AMBIENT,   a_mat_ambient); //环境变量
    glMaterialfv(GL_FRONT, GL_DIFFUSE,   a_mat_diffuse); //散射模式
    glMaterialfv(GL_FRONT, GL_SPECULAR,  a_mat_specular); //镜面反射
    glMaterialfv(GL_FRONT, GL_EMISSION,  a_mat_emission); //幅射光
    glMaterialf( GL_FRONT, GL_SHININESS, a_mat_shininess);
​
    glutSolidSphere(20, 15, 15);//球体
}

指定着色模型

void glShadeModel (GLenum mode);

GL_SMOOTH: 平滑模式,独立的处理图元中各个顶点的颜色。对于线段图元,线段上各点的颜色将根据两个顶点的颜色通过插值得到。对于多边形图元,多边形内部区域的颜色将根据所有顶点的颜色插值得到,RGB模式下看起来有渐变的效果

GL_FLAT: 恒定着色,使用图元中某个顶点的颜色来渲染整个图元。

默认值:GL_SMOOTH。

glBegin(GL_POLYGON);
glColor3f(0.0, 0.0, 1.0);
glVertex2f(0.0f, 0.0f);
for (int i = 0; i <= n; ++i)
{
  //不同颜色显示出渐变的效果
    glColor3f(i & 0x04, i & 0x02, i & 0x01);
    glVertex2f(R * cos(2 * Pi / n * i), R * sin(2 * Pi / n * i));
}
glEnd();
​
glShadeModel(GL_SMOOTH);    // 平滑方式,这也是默认方式
glShadeModel(GL_FLAT);      // 单色方式

颜色混合Blending

只有在RGBA模式下,才可以使用混合功能,颜色索引模式下是无法使用混合功能的。

半透明:首先绘制所有不透明物体,即目标物体;后绘制所有半透明的物体,即源物体。绘制顺序会对结果造成影响,需要注意,必须是先绘制不透明的物体,然后绘制透明的物体。否则,假设背景为蓝色,近处一块红色玻璃,中间一个绿色物体,如果先绘制红色半透明玻璃的话,它先和蓝色背景进行混合,以后绘制中间 的绿色物体时,想单独与红色玻璃混合已经不能实现了。

设源颜色的四个分量(红,绿,蓝,alpha值)是(Rs, Gs, Bs, As),源因子是(Sr, Sg, Sb, Sa);目标颜色的四个分量是(Rd, Gd, Bd, Ad),目标因子是(Dr, Dg, Db, Da)。则混合产生的新颜色为:

(RsSr+RdDr, GsSg+GdDg, BsSb+BdDb, AsSa+AdDa)

可以通过glBlendFunc(a, b)函数设置参数因子,前者表示源因子,后者表示目标因子,如:

  • GL_ZERO:不使用这种颜色

  • GL_ONE:只使用这种颜色

  • GL_SRC_ALPHA:使用源颜色的alpha值来作为因子

  • GL_DST_ALPHA:使用目标颜色的alpha值来作为因子

  • GL_ONE_MINUS_SRC_ALPHA:用1.0减去源颜色的alpha值来作为因子

  • 还有GL_SRC_COLOR(把源颜色的四个分量分别作为因子的四个分量)、GL_DST_COLOR、GL_CONST_COLOR(设定一种常数颜色,将其四个分量分别作为因子的四个分量)等

在进行三维场景的混合时还有一点是需要注意的,那就是深度缓冲。深度缓冲是这样一段数据,它记录了每一个像素距离观察者有多近。在启用深度缓冲测试的情况下,如果要绘制的像素比原来的像素更近,则像素将被绘制。否则,像素就会被忽略掉。这是个很有用的功能,但在需要实现半透明效果时,它会带来一些干扰。

—— 如果是透明的物体(假设为立方体),我们一般会希望透过立方体的前面能看到立方体的后面,透过整个立方体能看到更后面的被它挡住的图形,但由于启用了深度模式GL_DEPTH_TEST,后面的Z值在后,因此不会被画。如果我们能临时关闭GL_DEPTH_TEST,就会让立方体的每个面都进行绘画。glDepthMask 就是解决这个问题的,它相当于在立方体之间调用了glEnable/glDisable(GL_DEPTH_TEST)

glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
​
glEnable(GL_BLEND);//开启混合模式
glBlendFunc(GL_ONE, GL_ZERO);//完全使用源颜色,不使用目标颜色
glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);//根据alpha值来混合
​
glDepthMask(GL_FALSE);
// body具体图形,两个半透明四面体
glDepthMask(GL_TRUE);
​
glDisable(GL_BLEND);//关闭

法向量

平面法向量:平面内两非平行向量的叉积为该平面的法向量。由于 a×b = c;b×a = -c;两向量的相乘顺序需要注意。

顶点法向量:以此点为顶点的所有三角(四角)形的法向量之和即为顶点法向量。

OpenGL为了加快计算速度,会将平面法向量和顶点法向量进行“向量归一化”处理,即向量各分量之和等于1。

五、片段测试

OpenGL提供四种测试,顺序是剪裁测试、Alpha测试、模板测试、深度测试。对每个即将绘制的像素进行以上四种测试,每个像素通过一项测试后才会进入下一项测试,每种测试都可以单独开启,只有通过所有测试的像素才会被绘制。

  • 剪裁测试是指:只有位于指定矩形内部的像素才能通过测试。

  • Alpha测试是指:只有Alpha值与设定值相比较,满足特定条件的像素才能通过测试。

  • 模板测试是指:只有像素模板值与设定值相比较,满足条件的像素才能通过测试。

  • 深度测试是指:只有像素深度值与新的深度值比较,满足条件的像素才能通过测试。

特定条件可以是大于、小于、等于、大于等于、小于等于、不等于、通过、不通过

模板测试需要模板缓冲区GLUT_STENCIL,深度测试需要深度缓冲区GLUT_DEPTH。这些缓冲区都是在初始化OpenGL时指定的。如果使用GLUT工具包,则可以在glutInitDisplayMode函数中指定。

无论是否开启深度测试,OpenGL在像素被绘制时都会尝试修改像素的深度值;而只有开启模板测试时,OpenGL才会尝试修改像素的模板值。

利用这些测试操作可以控制像素被绘制或不被绘制,从而实现一些特殊效果。比如混合功能可以实现半透明或全透明,但这里仅仅是颜色的模拟。OpenGL可以为像素保存颜色、深度值和模板值,利用混合实现透明时,像素颜色不发生变化,但深度值则会可能变化,模板值受glStencilFunc函数中第三个参数影响;利用测试操作实现透明时,像素颜色不发生变化,深度值也不发生变化,模板值受glStencilFunc函数中前两个参数影响。

剪裁测试

用于限制绘制区域。可以提前指定一个剪裁窗口,启用剪裁测试后,只有在这个窗口内的像素才能被绘制,其它像素则会被丢弃。无论怎么绘制,剪裁窗口以外的像素不会被修改

glEnable(GL_SCISSOR_TEST);   // 启用剪裁测试
​
//指定一个左下角位置在(x, y),宽度为width,高度为height的剪裁窗口。
glScissor(x, y, width, height);
​
glDisable(GL_SCISSOR_TEST); // 禁用剪裁测试

还有一种方法可以只绘制到特定的矩形区域内,就是视口变换

区别是:视口变换将所有内容缩放到合适的大小后,放到一个矩形的区域内;而剪裁测试不会进行缩放,超出矩形范围的像素直接忽略

Alpha测试

像素的Alpha值可以用于混合操作,还可以用于Alpha测试。当每个像素即将绘制时,如果启动了Alpha测试,OpenGL会检查像素的Alpha值,只有Alpha值满足条件的像素才有机会进行绘制,不满足条件的则不进行绘制。

如果我们需要绘制一幅图片,而这幅图片的某些部分又是透明的(想象一下,你先绘制一幅相片,然后绘制一个相框,则相框这幅图片有很多地方都是透明的,这样就可以透过相框看到下面的照片),这时可以使用Alpha测试。将图片中所有需要透明的地方的Alpha值设置为0.0,不需要透明的地方Alpha值设置为1.0,然后设置Alpha测试的通过条件为:“大于0.5则通过”,这样便能达到目的。当然也可以设置需要透明的地方Alpha值为1.0,不需要透明的地方Alpha值设置为0.0,然后设置条件为“小于0.5则通过”,根据个人喜好和实际需要进行选择吧。

GL_NEVER 始终不显示;GL_ALWAYS 始终显示

GL_LESS 小于透明层度要求值就显示

GL_GREATER 大于透明层度要求值就显示

GL_EQUAL 等于透明层度要求值就显示

GL_LEQUAL 小于等于;GL_NOTEQUAL 不等于;GL_GEQUAL 大于等于

glEnable(GL_ALPHA_TEST);   // 启用Alpha测试
​
glAlphaFunc(GL_GREATER, 0.5f);
​
glDisable(GL_ALPHA_TEST); // 禁用Alpha测试

模板测试 TODO

当片段着色器处理完片段之后,模板测试(Stencil Test) 就开始执行了,和深度测试一样,它能丢弃一些片段,通过它我们可以实现很多的特效,例如物体轮廓、镜面效果,阴影效果等。

使用剪裁测试可以把绘制区域限制在一个矩形的区域内。但如果需要把绘制区域限制在一个不规则的区域内,则需要使用模板测试。启用模板测试时,OpenGL会在内存中开辟一块空间作为模板缓冲区,里边保存了每个像素的“模板值”,模板测试的过程就是把每一个像素的模板值与一个设定的模板参考值进行比较,符合设定条件的通过测试,不符合条件的则不会绘制。

使用模板缓冲的三要素:

  1. 正确的时间开启和关闭深度缓冲
  2. 模板测试函数
  3. 模板测试函数失败或者成功后的执行的动作   

使用模板缓冲的步骤:

  1. 模板测试需要模板缓冲区,初始化OpenGL时指定:glutInitDisplayMode(GLUT_STENCIL);

  2. 启用/禁用模板测试

    1. glEnable(GL_STENCIL_TEST);  // 启用模板测试
      glDisable(GL_STENCIL_TEST);  // 禁用模板测试

  3. 每次循环,需要清空模板缓冲:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

  4. 模板缓冲区中每个像素有一个“模板值”,当像素需要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的通过测试。 一共有两种函数可供我们使用去配置模板测试:glStencilFuncglStencilOp

    1. void glStencilFunc(GLenum func, GLint ref, GLuint mask);func:设置模板测试操作,可用的选项是:GL_NEVERGL_LEQUALGL_GREATERGL_GEQUALGL_EQUALGL_NOTEQUALGL_ALWAYS。它们的语义和深度缓冲的相似。ref:指定模板测试的引用值。模板缓冲的内容会与这个值对比。mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1。

      1.   例:glStencilFunc(GL_LESS, 3, 0xff);设置模板测试的条件为:“小于3则通过”。

    2.  void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);sfail: 如果模板测试失败将采取的动作。dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。dppass: 如果深度测试和模板测试都通过,将采取的动作。

      1. 参数值: GL_KEEP(不改变,这也是默认值), GL_ZERO(回零), GL_REPLACE(使用测试条件中的设定值来代替当前模板值), GL_INCR(增加1,但如果已经是最大值,则保持不变), GL_INCR_WRAP(增加1,但如果已经是最大值,则从零重新开始), GL_DECR(减少1,但如果已经是零,则保持不变), GL_DECR_WRAP(减少1,但如果已经是零,则重新设置为最大值), GL_INVERT(按位取反);

例1:物体轮廓(Object Outlining)

就像它的名字所描述的那样,它能够给每个(或一个)物体创建一个有颜色的边。在策略游戏中当你打算选择一个单位的时候它特别有用。给物体加上轮廓的步骤如下:

  1. 在绘制物体前,把模板方程设置为GL_ALWAYS,用1更新物体将被渲染的片段。
  2. 渲染物体,写入模板缓冲。
  3. 关闭模板写入和深度测试。
  4. 每个物体放大一点点。
  5. 使用一个不同的片段着色器用来输出一个纯颜色。
  6. 再次绘制物体,但只是当它们的片段的模板值不为1时才进行。
  7. 开启模板写入和深度测试。

例2:假设平面镜所在的平面正好是X轴和Y轴所确定的平面,球体和它在平面镜中的镜像是关于这个平面对称的。我们先绘制球体本身,然后调用glScalef(1.0f, 1.0f, -1.0f),再绘制球体的镜像。 需要注意的是:我们开启了深度测试。站在观察者的位置,球体的镜像其实是在平面镜的“背后”,如果按照常规的方式绘制,平面镜会把镜像覆盖掉。解决办法就是:设置深度缓冲区为只读,绘制平面镜,然后设置深度缓冲区为可写的状态,绘制平面镜“背后”的镜像。 当然,如果在绘制镜像的时候关闭深度测试,那镜像就不会被平面镜遮挡了,但是,如果关闭了深度测试,如果球体“背面”多边形又比“正面”多边形先绘制,就会造成球体的背面反而把正面挡住了,因此在绘制后面的球体时应该开启深度测试。如下:

#include<GL/glut.h>
​
void draw_sphere()
{
    //设置光源
    //绘制一个球体
}
​
void display()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
​
    //设置观察点
    glMatrixMode(GL_PROJECTION);  //采用投影矩阵
    glLoadIdentity();
    gluPerspective(60, 1, 5, 25);  //投影区域,角度,宽高比,近距离,远距离
    glMatrixMode(GL_MODELVIEW);  //采用模型矩阵
    glLoadIdentity();
    gluLookAt(5, 0, 6.5, 0, 0, 0, 0, 1, 0);
​
	glRotatef(angle,0,1,0);
 
    glEnable(GL_DEPTH_TEST);
​
    //绘制球体
    glDisable(GL_STENCIL_TEST);
    draw_sphere();
​
    //绘制一个平面镜。在绘制的同时注意设置模版缓冲
    //为了保证平面镜之后的镜像能够正确绘制
    //在绘制平面镜像时需要将深度缓冲区设置为只读的。
    //在绘制时暂时关闭光照效果
    glClearStencil(0);指定复位后的“模板值”。
    glClear(GL_STENCIL_BUFFER_BIT);//重置stencil buffer里的值为0
    glStencilFunc(GL_ALWAYS, 1, 0xFF);//比较条件,参考值,比较用掩码
    //先用比较用掩码和缓冲区中的值按位与运算,再用参考值与比较条件比较
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    glEnable(GL_STENCIL_TEST);
​
    glDisable(GL_LIGHTING);//关闭光照
    glColor3f(0.5f, 0.5f, 0.5f);
    glDepthMask(GL_FALSE);//关闭深度缓冲区写入
    glRectf(-1.5f, -1.5f, 1.5f, 1.5f);
    glDepthMask(GL_TRUE);
​
    //绘制一个与先前球体关于平面镜对称的球体,注意光源的位置也要发生对称改变
    //因为平面镜是在X轴和Y轴所确定的平面,所以只要Z坐标取反即可实现对称
    //为了保证球体的绘制范围被限制在平面镜内部,使用模版测试
    glStencilFunc(GL_EQUAL, 1, 0xFF);
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    glScalef(1.0f, 1.0f, -1.0f);
    draw_sphere();
​
    //交换缓冲
    glutSwapBuffers();
}
 
void myIdle(void) // 让球旋转
{      
	angle+=0.1f;   
	if( angle >= 360.0f )      
		angle = 0.0f;      
	display();  
}  
​
int main(int argc, char* argv[])
{
    glutInit(&argc, argv); // 初始化
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 显示模式:RGB,双缓冲
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(600, 600);
    glutCreateWindow("模板测试");
    glutDisplayFunc(&display);
	glutIdleFunc(&myIdle); // 在没有窗口事件的时候OpenGL干什么

	// 开始显示
    glutMainLoop();
    return 0;
}

模板测试的设置很复杂,可以实现的功能很多,不止一个“限制像素的绘制范围”。

如果不需要绘制半透明效果,有时候可以用混合功能来代替模板测试。就绘制镜像来说,可以采用下面的步骤: (1) 清除屏幕,在glClearColor中设置合适的值确保清除屏幕后像素的Alpha值为0.0 (2) 关闭混合功能,绘制球体本身,设置合适的颜色(或者光照与材质)以确保所有被绘制的像素的Alpha值为0.0 (3) 绘制平面镜,设置合适的颜色(或者光照与材质)以确保所有被绘制的像素的Alpha值为1.0 (4) 启用混合功能,用GL_DST_ALPHA作为源因子,GL_ONE_MINUS_DST_ALPHA作为目标因子,这样就实现了只有原来Alpha为1.0的像素才能被修改,而原来Alpha为0.0的像素则保持不变。这时再绘制镜像物体,注意确保所有被绘制的像素的Alpha值为1.0。 在有的OpenGL实现中,模板测试是软件实现的,而混合功能是硬件实现的,这时候可以考虑这样的代替方法以提高运行效率。但是并非所有的模板测试都可以用混合功能来代替,并且这样的代替显得不自然,复杂而且容易出错。 另外始终注意:使用混合来模拟时,即使某个像素原来的Alpha值为0.0,以致于在绘制后其颜色不会有任何变化,但是这个像素的深度值有可能会被修改,而如果是使用模板测试,没有通过测试的像素其深度值不会发生任何变化。而且,模板测试和混合功能中,像素模板值的修改方式是不一样的。

与模板测试相比,深度测试的应用要频繁得多,几乎所有的三维场景绘制都使用了深度测试。两种测试在应用上很少有交集,一般不会出现使用一种测试去代替另一种测试的情况。

深度测试

不使用深度测试时,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉。 而如果使用了深度测试:每当一个像素被绘制,OpenGL就记录这个像素的“深度”(可以理解为:该像素距离观察者的距离),如果有新的像素即将覆盖原来的像素时,深度测试会检查新的深度是否会比原来的深度值小。如果是,则覆盖像素,绘制成功;如果不是,则不会覆盖原来的像素,绘制被取消。

深度缓冲区:就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值,深度值(Z值)越大,则离摄像机越远

深度冲突:深度缓冲没有足够的精度来决定两个形状哪个在前面,所以这两个形状不断地在切换前后顺序,这会导致很奇怪的(一般为锯齿状)花纹。

解决方法:不要把多个物体摆得太靠近 or 牺牲一些性能,使用更高精度的深度缓冲。大部分的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲

// 默认情况下深度测试是禁用的,使用GL_DEPTH_TEST选项来启用它
glEnable(GL_DEPTH_TEST);
// 深度测试函数是告诉openGL什么时候应该去执行哪种规则的深度测试
// 参数值:always never less equal lequal greater notequal gequal
glDepthFunc(GL_ALWAYS);
// 如果启用了深度测试,必须在每次重新绘制时使用GL_DEPTH_BUFFER_BIT来清除深度缓冲清理缓冲区
// 否则上一次的深度缓冲就会影响这一次的绘制。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 设置深度缓冲为只读属性。他不是完全意义的只读,而是在本次绘制过程中,
// 只要有值被写入,那么这个片段的深度缓冲就会被设置成只读属性
glDepthMask(GL_FALSE);

六、材料

BMP像素

计算机保存图象的方法通常有两种:一是“矢量图”,一是“像素图”。

“矢量图”放大、缩小时很方便,不会失真,但如果图象复杂,数据量和运算量会很大;“像素图”数据量和运算量都不会增加,但在进行放大、缩小等操作时,会产生失真。 最常见的是256位BMP和24位BMP,可用Windows自带的画图程序绘制BMP Windows所使用的BMP文件有一个文件头,保存了包括文件颜色数、图象大小等信息,24位不压缩的BMP,图象的宽度和高度都是一个32位整数,在文件中的地址分别为0x0012和0x0016,因此很方便读取图片大小信息。

纹理TODO

纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节,我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。

但是载入纹理所需要的系统开销比较大,应该尽可能减少载入纹理的次数,如果程序中只使用一幅纹理,则只在第一次使用前载入,以后不必重新载入;如果程序中要使用多幅纹理,应该将每个纹理都用一个纹理对象来保存,并使用glBindTextures在各个纹理之间进行切换 总参考2

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。

纹理过滤函数:void glTexParameteri(GLenum target, GLenum pname, const GLfloat *params);

target:目标纹理,必须是GL_TEXTURE_1D或GL_TEXTURE_2D。

pname:单个值纹理参数的符号名称。GL_TEXTURE_MIN_FILTER:纹理化的像素映射到大于一个纹理元素的区域时,将使用纹理缩小函数。GL_TEXTURE_MAG_FILTER:纹理化的像素映射到小于或等于一个纹理元素的区域时,将使用纹理放大函数。 GL_TEXTURE_WRAP_S: S方向上的贴图模式。GL_TEXTURE_WRAP_T: T方向上的贴图模式。GL_TEXTURE_BORDER_COLOR:设置边框颜色。GL_TEXTURE_PRIORITY:指定当前绑定纹理的纹理居住优先级。

params:指向存储 pname 值的数组的指针。 params 参数提供一个函数,用于将纹理缩小为以下值之一。GL_NEAREST放大后可能产生颗粒状图案 GL_LINEAR 能产生更平滑的图案GL_LINEAR_MIPMAP_NEAREST等。

——读取一个BMP文件作为纹理:

GLuint load_texture(const char* file_name)
{
    GLint width, height, total_bytes; //存储宽高,字节数
    GLubyte* pixels = 0;//内存中分配字节,存放像素数据
    GLuint last_texture_ID, texture_ID = 0; //纹理名称
​
    //打开文件,如果失败,返回
    FILE* pFile = fopen(file_name, "rb");
    if (pFile == 0)
        return 0;
​
    //读取文件中图像的宽度和高度
    fseek(pFile, 0x0012, SEEK_SET);
    fread(&width, 4, 1, pFile);
    fread(&height, 4, 1, pFile);
    fseek(pFile, BMP_Header_Length, SEEK_SET);
​
    //计算每行像素所占的字节数,并根据此数据计算总像素字节数
    GLint line_bytes = width * 3;
    while (line_bytes % 4 != 0)
        ++line_bytes;
    total_bytes = line_bytes * height;

    //根据总像素字节数分配内存
    pixels = (GLubyte*)malloc(total_bytes);
    if (pixels == 0)
    {
        fclose(pFile);
        return 0;
    }
​
    //读取像素数据
    if (fread(pixels, total_bytes, 1, pFile) <= 0)
    {
        free(pixels);
        fclose(pFile);
        return 0;
    }
​
    //分配一个新的纹理编号
    glGenTextures(1, &texture_ID);
    if (texture_ID == 0)
    {
        free(pixels);
        fclose(pFile);
        return 0;
    }
​
    //绑定新的纹理,载入纹理并设置纹理参数
    //1.在绑定前,先获得原来绑定的纹理编号last_texture_ID,以便在最后进行恢复
    glGetIntegerv(GL_TEXTURE_BINDING_2D, (int*)&last_texture_ID); //返回所选参数的值
    //2.绑定新的纹理
    glBindTexture(GL_TEXTURE_2D, texture_ID);
    //3.设置纹理参数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
        GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);
    //4.恢复last_texture_ID
    glBindTexture(GL_TEXTURE_2D, last_texture_ID); 
​
    //之前为pixels分配的内存可在使用glTexImage2D后释放
    //因为此时像素数据已经被openGL另行保存了一份(可能被保存在专门的图形硬件中)
    free(pixels);
    return texture_ID;
}
void display(){
        //清除屏幕
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
​
    //设置视角
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(75, 1, 1, 21);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(1, 5, 5, 0, 0, 0, 0, 0, 1);
​
    //使用“地”纹理绘制土地
    glBindTexture(GL_TEXTURE_2D, texWall);
    //begin——end
}
​
int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(WindowWidth, WindowHeight);
    glutCreateWindow(WindowTitle);
    glutDisplayFunc(&display);
​
    //一些初始化
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);
    texGround = load_texture("blue.bmp");
​
    //开始显示
    glutMainLoop();
    return 0;
}

多级渐变纹理Mipmap:

距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个,简单地说就是越远解析度分辨率越低。

在创建完一个纹理后调用glGenerateMipmaps函数

七、文字

显示列表显示

要显示字符,就需要通过操作系统,把绘制字符的动作装到显示列表中,然后我们调用显示列表即可绘制字符。

假如我们要显示的文字全部是ASCII字符,则总共只有0到127这128种可能,因此可以预先把所有的字符分别装到对应的显示列表中,然后在需要时调用这些显示列表。 使用wglUseFontBitmaps函数<windows.h>来批量产生显示字符用的显示列表:

void drawString(const char* str) {
    static GLuint lists;
    HDC hDc = wglGetCurrentDC();
  
    if (isFirstCall == 1) {
        isFirstCall = 0;
        // 申请MAX_CHAR个连续的显示列表编号
        lists = glGenLists(MAX_CHAR);//创建显示列表组,成功时返回第一个显示列表的名字
        // 把每个字符的绘制命令都装到对应的显示列表中
        wglUseFontBitmaps(hDc, 0, MAX_CHAR, lists);
    }
  
    for (; *str != '\0'; ++str) {
        glCallList(lists + *str);
    }
}
​
void display(void) {
    glClear(GL_COLOR_BUFFER_BIT);
​
    glColor3f(1.0f, 0.0f, 0.0f);
    glRasterPos2f(0.0f, 0.0f);//指定位置
    drawString("Hello, World!");//绘制字符
​
    glutSwapBuffers();
}

指定字体

在自己的程序里,把selectFont函数抄下来,在调用glutCreateWindow之后、在调用wglUseFontBitmaps之前使用selectFont函数即可指定字体。(Windows GDI)

函数的三个参数分别表示了字体大小、字符集(英文字体可以用ANSI_CHARSET,简体中文字体可以用GB2312_CHARSET,繁体中文字体可以用CHINESEBIG5_CHARSET,对于中文的Windows系统,也可以直接用DEFAULT_CHARSET表示默认字符集)、字体名称。

void selectFont(int size, int charset, const char* face)
  //(抄下来)字体大小、字符集、字体名称
{
    HFONT hFont = CreateFontA(size, 0, 0, 0, FW_MEDIUM, 0, 0, 0,
        charset, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
        DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, face);
    HFONT hOldFont = (HFONT)SelectObject(wglGetCurrentDC(), hFont);
    DeleteObject(hOldFont);
}

显示汉字

英文字母很少,最多只有几百个,可以为每个字母创建一个显示列表。但是汉字有非常多个,如果每个汉字都产生一个显示列表,这是不切实际的。我们不能在初始化时就为每个字符建立一个显示列表,那就只有在每次绘制字符时创建它了。当我们需要绘制一个字符时,创建对应的显示列表,等绘制完毕后,再将它销毁。

这里还经常涉及到中文乱码的问题。通常我们使用的字符串,如果中英文混合的话,例如“this is 中文字符.”,英文字符只占一个字节,而中文字符占用两个字节。使用MultiByteToWideChar函数,可以转化为所有的字符都占两个字节,解决了乱码问题 :)

——转化的代码如下:

void drawCNString(const char* str)
{
    int len, i;
    wchar_t* wstring;
    HDC hDC = wglGetCurrentDC();
    GLuint list = glGenLists(1);
​
    //计算字符个数
    //如果是双字节字符的(比如中文字符),两个字节才算一个字符
    //否则一个字节算一个字符
    len = 0;
    for (i = 0; str[i] != '\0'; ++i)
    {
        if (IsDBCSLeadByte(str[i]))
            ++i;
        ++len;
    }
​
    //将混合字符转化为宽字符
    wstring = (wchar_t*)malloc((len + 1) * sizeof(wchar_t));
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, str, -1, wstring, len);
    wstring[len] = '\0';
​
    //逐个输出字符
    for (i = 0; i < len; ++i)
    {
        wglUseFontBitmapsW(hDC, wstring[i], 1, list);
        glCallList(list);
    }
​
    //回收所有临时资源
    free(wstring);
    glDeleteLists(list, 1);
}

——其它代码如下:

void display()
{
    glClear(GL_COLOR_BUFFER_BIT);
​
    selectFont(48, ANSI_CHARSET, "Comic Sans MS");
    glColor3f(1.0f, 0.0f, 0.0f);
    glRasterPos2f(-0.7f, 0.4f);//指定位置
    drawString("Hello,World!");
​
    selectFont(48, GB2312_CHARSET, "楷体_GB2312");
    glColor3f(1.0f, 1.0f, 0.0f);
    glRasterPos2f(-0.7f, -0.1f);
    drawCNString("你好,汉字");
​
    selectFont(30, GB2312_CHARSET, "宋体");
    glColor3f(1.0f, 1.0f, 0.0f);
    glRasterPos2f(0.7f, 0.7f);
    drawCNString("汉字汉字");
​
    glutSwapBuffers();
}
​
​
int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(512, 512);
    glutCreateWindow("汉字");
​
    glutDisplayFunc(&display);
    glutMainLoop();
    return 0;
}

纹理字体/文字渲染

由于OpenGL本身并没有定义如何渲染文字到屏幕,也没有用于表示文字的基本图形,我们必须自己定义一套全新的方式才能让OpenGL来绘制文字。目前一些技术包括:通过GL_LINES来绘制字形、创建文字的3D网格、将带有文字的纹理渲染到一个2D方块中。

1.经典文字渲染:位图字体。首先将所有用到的文字加载在一张大纹理图中,这张纹理贴图我们把它叫做位图字体(Bitmap Font),它包含了所有我们想要使用的字符。这些字符被称为字形(Glyph)。每个字形根据他们的编号被放到位图字体中的确切位置,在渲染这些字形的时候根据这些排列规则将他们取出并贴到指定的2D方块中。

2.现代文字渲染:FreeType。它是一个能够提供多种字体相关的操作的跨平台软件开发库,往往用来做最简单的文字渲染。TODO

函数

wglUseFontBitmaps函数:

批量产生显示字符用的显示列表,<windows.h>,有四个参数: 1参数是HDC,调用wglGetCurrentDC函数,就可以得到一个HDC了。 2参数表示第一个要产生的字符,比如我们要产生0到127的字符的显示列表,这里填0。 3参数表示要产生字符的总个数,我们要产生0到127总共128个字符,所以这里填128。 4参数表示第一个字符所对应显示列表的编号。假如这里填1000,则第一个字符的绘制命令将被装到第1000号显示列表,第二个字符的绘制命令将被装到第1001号显示列表,依次类推。可以先用glGenLists申请128个连续的显示列表编号,然后把第一个显示列表编号填在这里;

8、算法

判断点P是否在多边形内

射线法:参考

以点P为端点,向左方做射线L,然后沿着L从无穷远处开始向P点移动,当遇到多边形的某一条边时,记为与多边形的第一个交点,表示进入多边形内部,继续移动,当遇到另一个交点时,表示离开多边形内部。由此可知,当L与多边形的交点个数是偶数时,表示P点在多边形外,当L与多边形交点个数是奇数时,表示P点在多边形内部。

矢量:数学中称向量,就是既有大小又有方向的量。

矢量点积内积:P·Q = x1*x2 + y1*y2;如果 P · Q > 0 , 则P和Q的夹角是钝角;如果 P · Q < 0 , 则P和Q的夹角是锐角;如果 P · Q = 0 , 则P和Q的夹角是直角。

矢量叉积外积:P × Q = x1*y2 - x2*y1 = - ( Q × P );如果 P × Q > 0 , 则Q在P的逆时针方向;如果 P × Q < 0 , 则Q在P的顺时针方向;如果 P × Q = 0 , 则Q与P共线(但可能方向是反的);

求任意多边形面积

公式法:

首先已知各定点的坐标分别为(x1,y1),(x2,y2),(x3,y3)。。。,(Xn,Yn)

则该多边形的面积公式为:

s=1/2*[(x1*y2-x2*y1)+(x2*y3-x3*y2)+...... +(Xk*Yk+1-Xk+1*Yk)+...+(Xn*y1-x1*Yn) ]

进一步简化为:

s=1/2*[(x1+x2)*(y1-y2) + (x2+x3)*(y2-y3)+...... +(xn-1+xn)*(yn-1-yn) + (xn+x1)*(yn-y1)]

该定理实质上是将多边形面积,转化为多个小三角形的面积之和。这个面积公式可能算出来是正的,也有可能是负的,所以要加绝对值。

最后的面积为:s=abs(s)。

撒豆子法:

比较古老且繁琐且不准确,在计算机中能得到顶点的情况下,用公式法更合适。

直线生成算法

数值微分法(DDA):y = kx+b

TODO

9、其它

绘制方法改进

// 将立方体的八个顶点保存到一个数组里面
static const GLfloat vertex_list[][3] = {
     -0.5f, -0.5f, -0.5f,
      0.5f, -0.5f, -0.5f,
     -0.5f,   0.5f, -0.5f,
      0.5f,   0.5f, -0.5f,
     -0.5f, -0.5f,   0.5f,
      0.5f, -0.5f,   0.5f,
     -0.5f,   0.5f,   0.5f,
      0.5f,   0.5f,   0.5f,
};
​
// 将要使用的顶点的序号保存到一个数组里面
static const GLint index_list[][4] = {
     0, 2, 3, 1,
     0, 4, 6, 2,
     0, 1, 5, 4,
     4, 5, 7, 6,
     1, 3, 7, 5,
     2, 6, 7, 3,
};
​
int i, j;
​
// 绘制的时候代码很简单
glBegin(GL_QUADS);
for(i=0; i<6; ++i)          // 有六个面,循环六次
    for(j=0; j<4; ++j)      // 每个面有四个顶点,循环四次
         glVertex3fv(vertex_list[index_list[i][j]]);
glEnd();
// 正对我们的面按逆时针顺序,背对我们的面按顺时针顺序,这样就得到了上面那个index_list数组。
// 这样做可以保证无论从哪个角度观察,看到的都是正面。

glFrontFace(GL_CCW);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
​
//则绘制出来的图形就只有正面,并且只显示边线,不进行填充

常用函数

  • gluPerspective(GLdouble fovy, aspect, zNear, zFar);

  • fovy,可以理解为眼睛睁开的角度,如果为0,相当闭上眼睛了,什么也看不见;如果为180,那么可以认为你的视界很广阔 aspect,实际窗口的纵横比,即x/y zNear,表示近处的裁面, zFar,表示远处的裁面,

  • glTexCoord2f(GLfloat x, y);

    一张位图的4个坐标顶点分别为:左下角(0.0,0.0),右下角(1.0,0.0)右上角(1.0,1.0),左上角(0.0,1.0);

    通常配合glVertex3fv使用,glTexCoord2f用来定义纹理坐标,glVertex3fv用来定义几何定点坐标

    考虑性能的话,一般把纹理做成2的n次幂

  • glPushMatrix()和glPopMatrix()的配对使用可以消除上一次的变换对本次变换的影响。使本次变换以世界坐标系的原点为参考点进行

键盘交互

可以操纵图片变化

void myKayBoard(unsigned char key, int x, int y) {
    //key对应键盘上面的一个键
    switch (key)
    {
        case GLUT_KEY_UP: ; break;
        case GLUT_KEY_DOWN: ; break;
    }
    //改变值后我们要使图形重新显示一遍
    glutPostRedisplay();
}
int main() {
    glutKeyboardFunc(myKayBoard);//点击键盘的时候就会调用该方法
}

鼠标交互

void mouseCB(int button, int state, int x, int y)
{
  //其中x, y是鼠标的窗口内坐标
    if (state == GLUT_UP && button == GLUT_LEFT_BUTTON)//左键按下后抬起
    {
        times += 0.01f;
        glutPostRedisplay();
    }
    else if (state == GLUT_UP && button == GLUT_RIGHT_BUTTON)
    {
        times -= 0.01f;
        glutPostRedisplay();
    }
    glutPostRedisplay();
}
​
void mymouse(int x, int y) {
}
​
int main(){
    glutMouseFunc(mouseCB); // 鼠标键按下去后,移动鼠标就会调用该方法
    glutPassiveMotionFunc(mymouse); // 鼠标没有被按下去时,移到鼠标就会调用该方法
}

显示列表

程序多次执行重复的工作,会导致CPU资源浪费和运行速度的下降,因此使用显示列表来解决; 可以将重复的工作编写为函数,在需要的地方调用它。类似的,在编写OpenGL程序时,遇到重复的工作,可以创建一个显示列表,把重复的工作装入其中,并在需要的地方调用这个显示列表。 使用显示列表一般有四个步骤:分配显示列表编号、创建显示列表、调用显示列表、销毁显示列表。

注意:显示列表只能装入OpenGL函数,且并非所有的OpenGL函数都可以装入到显示列表中,例如有返回值的查询函数;

——程序练习:

#define ColoredVertex(c, v) do{ glColor3fv(c); glVertex3fv(v); }while(0)
​
GLfloat angle = 0.0f;
​
void myDisplay(void)
{
     static int list = 0;
     if( list == 0 )
     {
         // 如果显示列表不存在,则创建
         GLfloat
             PointA[] = { 0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointB[] = {-0.5f, -sqrt(6.0f)/12, -sqrt(3.0f)/6},
             PointC[] = { 0.0f, -sqrt(6.0f)/12,  sqrt(3.0f)/3},
             PointD[] = { 0.0f,   sqrt(6.0f)/4,             0};
         GLfloat
             ColorR[] = {1, 0, 0},
             ColorG[] = {0, 1, 0},
             ColorB[] = {0, 0, 1},
             ColorY[] = {1, 1, 0};
​
         list = glGenLists(1);
         glNewList(list, GL_COMPILE);
         glBegin(GL_TRIANGLES);
         // 平面ABC
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorB, PointC);
         // 平面ACD
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorY, PointD);
         // 平面CBD
         ColoredVertex(ColorB, PointC);
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorY, PointD);
         // 平面BAD
         ColoredVertex(ColorG, PointB);
         ColoredVertex(ColorR, PointA);
         ColoredVertex(ColorY, PointD);
         glEnd();
         glEndList();
​
         glEnable(GL_DEPTH_TEST);
     }
     // 已经创建了显示列表,在每次绘制正四面体时将调用它
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glPushMatrix();
     glRotatef(angle, 1, 0.5, 0);
     glCallList(list);
     glPopMatrix();
     glutSwapBuffers();
}
​
void myIdle(void)
{
     ++angle;
     if( angle >= 360.0f )
         angle = 0.0f;
     myDisplay();
}
​
int main(int argc, char* argv[])
{
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}

  • 分配显示列表编号

C语言中不同的函数用不同的名字来区分,而在OpenGL中,不同的显示列表用不同的正整数来区分。

可以自己指定一些各不相同的正整数来表示不同的显示列表。但是可能出现一个显示列表将另一个显示列表覆盖的情况。为了避免可以使用glGenLists函数来自动分配一个没有使用的显示列表编号。 ——glGenLists函数有一个参数i,表示要分配i个连续的未使用的显示列表编号,返回值是一个编号。例如glGenLists(3);如果返回20,则分配了20、21、22这三个连续的编号。如果返回零,则分配失败

  • 创建显示列表

使用glNewList开始装入,使用glEndList结束装入。glNewList有两个参数,第一个参数是一个正整数表示装入到哪个显示列表。第二个参数有两种取值,如果为GL_COMPILE,则表示以下的内容只是装入到显示列表,但现在不执行它们;如果为GL_COMPILE_AND_EXECUTE,表示在装入的同时,把装入的内容执行一遍。

  • 调用显示列表

使用glCallList函数可以调用一个显示列表,该函数有一个参数,表示要调用的显示列表的编号。

使用glCallLists函数可以调用一系列的显示列表。该函数有三个参数,第一个参数表示要调用多少个显示列表。第二个参数表示这些显示列表的编号的储存格式,第三个参数表示了显示列表的编号所在的位置

GLuint lists[] = {1, 3, 4, 8};
glListBase(10);
glCallLists(4, GL_UNSIGNED_INT, lists);
//调用编号为11, 13, 14, 18的四个显示列表
  • 销毁显示列表

使用glDeleteLists来销毁一串编号连续的显示列表。

glDeleteLists(list, range);

其中list是要删除的显示列表序列的第一个,range是要删除的显示列表的数目

2/3

3D转2D

三维空间中,经常需要将3D空间中的点转换到2D(屏幕坐标),或者将2D点转换到3D空间中。OpenGL里可以使用gluProject()和gluUnproject()函数实现这个功能。

基本的思路就是:

1、将输入的顶点,通过模型视图矩阵,变换到模型视图矩阵的坐标系中;

2、将模型视图矩阵中的顶点,再变换到投影矩阵中;

3、将顶点缩放到[0, 1]的映射区间中;

4、通过视口的位置和大小,计算出当前3D顶点中的屏幕坐标(2D坐标)

/* 
* Transform a point (column vector) by a 4x4 matrix
* out = m * in 
* Input: m - the 4x4 matrix 
* in - the 4x1 vector 
* Output: out - the resulting 4x1 vector. 
*/  
static void transform_point(double out[4],const double m[16],const double in[4])  
{  
#define M(row,col) m[col*4+row]  
    out[0] = M(0, 0) * in[0] + M(0, 1) * in[1] + M(0, 2) * in[2] + M(0, 3) * in[3];  
    out[1] = M(1, 0) * in[0] + M(1, 1) * in[1] + M(1, 2) * in[2] + M(1, 3) * in[3];  
    out[2] = M(2, 0) * in[0] + M(2, 1) * in[1] + M(2, 2) * in[2] + M(2, 3) * in[3];  
    out[3] = M(3, 0) * in[0] + M(3, 1) * in[1] + M(3, 2) * in[2] + M(3, 3) * in[3];  
#undef M  //转置后和列向量相乘
}  
​
/*指定对象坐标*3, model指定当前模型视图矩阵, proj指定当前投影矩阵
,viewport指定当前视口, 计算的窗口坐标*3*/
int gluProject(double objx, double objy, double objz  
                , const double model[16], const double proj[16]  
                , const int viewport[4]  
                , double * winx, double * winy, double * winz)  
{  
    /* transformation matrix */  
    double objCoor[4];  
    double objProj[4], objModel[4];  
  
    /* initilise matrix and vector transform */  
    // 4x4 matrix must be multi to a 4 dimension vector( it a 1 x 4 matrix)  
    // so we need to put the original vertex to a 4D vector  
    objCoor[0] = objx;  
    objCoor[1] = objy;  
    objCoor[2] = objz;  
    objCoor[3] = 1.0;  
  
    // 模型矩阵变换:由于原来的向量位于标准基向量(1, 0, 0), (0, 1, 0), (0, 0, 1)中,
    // 所以需要先转换到当前的模型矩阵中  
    transform_point(objModel, model, objCoor);  
  
    // 投影变换:将模型矩阵中的顶点转换到投影矩阵所在坐标系的矩阵中  
    transform_point(objProj, proj, objModel);  
  
    // scale matrix  
    if (objProj[3] == 0.0)  
        return GL_FALSE;  
  
    objProj[0] /= objProj[3];  
    objProj[1] /= objProj[3];  
    objProj[2] /= objProj[3];  
  
    /* in screen coordinates */  
    // 由于投影矩阵投影在[-1, 1]之间,所以需要将转换后的投影坐标放置到[0, 1]之间  
    // 最后再在一个offset 矩形中转换为屏幕坐标就可以了
    //(viewport[4]可以简单的认为一个offset矩形)  
  
#define SCALE_FROM_0_TO_1(_pt)  (((_pt) + 1)/2)  
    objProj[0] = SCALE_FROM_0_TO_1(objProj[0]);  
    objProj[1] = SCALE_FROM_0_TO_1(objProj[1]);  
    objProj[2] = SCALE_FROM_0_TO_1(objProj[2]);  
#undef SCALE_FROM_0_TO_1  
  
    *winx = viewport[0] + objProj[0] * viewport[2];  
    *winy = viewport[1] + objProj[1] * viewport[3];  
  
    /* between 0 and 1 */  
    *winz = objProj[2];  
    return GL_TRUE;  
}  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值