OpenGL十二讲代码

OpenGL十二讲代码—by yjq

参考资料

OpenGL入门教程

【侵权立删】

第一讲

画一个矩形

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    //glBegin(GL_POINTS);
    glBegin(GL_POLYGON);//单个简单填充多边形
    glVertex2f(0.0f, 0.0f);
    glVertex2f(0.5f, 0.0f);
    glVertex2f(0.5f, 0.5f);
    glVertex2f(0.0f, 0.5f);
    glEnd();
    //glRectf(-0.5f, -0.5f, 0.5f, 0.5f);
    glFlush();
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("几何图形的绘制");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220107194820051

第二讲

image-20220109165521567

画一个五角星

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>

const int n = 1000;
//修改const int n的值,观察当n=3,4,5,8,10,15,20,30,50等不同数值时输出的变化情况
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536;

void myDisplay(void)
{
    //画一个五角星
    GLfloat a = 1 / (2 - 2 * cos(72 * Pi / 180));
    GLfloat bx = a * cos(18 * Pi / 180);
    GLfloat by = a * sin(18 * Pi / 180);
    GLfloat cy = -a * cos(18 * Pi / 180);
    GLfloat
        PointA[2] = { 0, a },
        PointB[2] = { bx, by },
        PointC[2] = { 0.5, cy },
        PointD[2] = { -0.5, cy },
        PointE[2] = { -bx, by };

    glClear(GL_COLOR_BUFFER_BIT);
    // 按照A->C->E->B->D->A的顺序,可以一笔将五角星画出
    glBegin(GL_LINE_LOOP);
    glVertex2fv(PointA);
    glVertex2fv(PointC);
    glVertex2fv(PointE);
    glVertex2fv(PointB);
    glVertex2fv(PointD);
    glEnd();
    glFlush();
}
int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第二讲——画一个五角星");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220112000619749

画一个圆

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>

const int n = 1000;
//修改const int n的值,观察当n=3,4,5,8,10,15,20,30,50等不同数值时输出的变化情况
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536;

void myDisplay(void)
{
    //画一个圆
    int i;
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_POLYGON);//单个简单填充多边形
    for (i = 0; i < n; i++) {
        glVertex2f(R * cos(2 * Pi / n * i), R * sin(2 * Pi / n * i));
    }
    glEnd();
    glFlush();
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第二讲——画一个圆");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220112000453159

画一个正弦函数

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>

const GLfloat factor = 0.1f;
void myDisplay(void)
{
    GLfloat x;
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_LINES);
    glVertex2f(-1.0f, 0.0f);
    glVertex2f(1.0f, 0.0f);        // 以上两个点可以画x轴
    glVertex2f(0.0f, -1.0f);
    glVertex2f(0.0f, 1.0f);        // 以上两个点可以画y轴
    glEnd();
    glBegin(GL_LINE_STRIP);
    for (x = -1.0f / factor; x < 1.0f / factor; x += 0.01f)
    {
        glVertex2f(x * factor, sin(x) * factor);
    }
    glEnd();
    glFlush();
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第二讲——画一个正弦函数");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220112000346482

第三讲

画点

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glPointSize(5.0f);
    glBegin(GL_POINTS);
    glVertex2f(0.0f, 0.0f);
    glVertex2f(0.5f, 0.5f);
    glEnd();
    glFlush();
}


int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第三讲——画两个点");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

画虚线

pattern是由1和0组成的长度为16的序列,从最低位开始看,如果为1,则直线上接下来应该画的factor个点将被画为实的;如果为0,则直线上接下来应该画的factor个点将被画为虚的。
以下是一些例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F4bjJ6WD-1641975777776)(C:/Users/26969/AppData/Local/Temp/msohtmlclip1/01/clip_image001.gif)]

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glEnable(GL_LINE_STIPPLE);
    glLineStipple(2, 0x0F0F);
    glLineWidth(10.0f);
    glBegin(GL_LINES);
    glVertex2f(0.0f, 0.0f);
    glVertex2f(0.5f, 0.5f);
    glEnd();
    glFlush();
}



int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第三讲——画虚线");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220112001221822

多边形

多边形的两面

从三维的角度来看,一个多边形具有两个面。每一个面都可以设置不同的绘制方式:填充、只绘制边缘轮廓线、只绘制顶点,其中“填充”是默认的方式。可以为两个面分别设置不同的方式。

glPolygonMode(GL_FRONT, GL_FILL);      // 设置正面为填充方式
glPolygonMode(GL_BACK, GL_LINE);      // 设置反面为边缘绘制方式
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT); // 设置两面均为顶点绘制方式

前后反转

可以通过glFrontFace函数来交换“正面”和“反面”的概念。

 glFrontFace(GL_CCW); // 设置CCW方向为“正面”,CCW即CounterClockWise,逆时针
 glFrontFace(GL_CW);  // 设置CW方向为“正面”,CW即ClockWise,顺时针
#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glPolygonMode(GL_FRONT, GL_FILL); // 设置正面为填充模式
    glPolygonMode(GL_BACK, GL_LINE);  // 设置反面为线形模式
    glFrontFace(GL_CCW);              // 设置逆时针方向为正面
    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();
    glFlush();
}



int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第三讲——多边形正反面");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220112001608140

剔除多边形表面

使用glEnable(GL_CULL_FACE);来启动剔除功能(使用glDisable(GL_CULL_FACE)可以关闭之)
然后,使用glCullFace来进行剔除。

glCullFace的参数可以是GL_FRONT,GL_BACK或者GL_FRONT_AND_BACK,分别表示剔除正面、剔除反面、剔除正反两面的多边形。

镂空多边形

使用glEnable(GL_POLYGON_STIPPLE);来启动镂空模式(使用glDisable(GL_POLYGON_STIPPLE)可以关闭之)。
然后,使用glPolygonStipple来设置镂空的样式。

 void glPolygonStipple(const GLubyte*mask);

其中的参数mask指向一个长度为128字节的空间,它表示了一个32*32的矩形应该如何镂空。其中:第一个字节表示了最左下方的从左到右(也可以是从右到左,这个可以修改)8个像素是否镂空(1表示不镂空,显示该像素;0表示镂空,显示其后面的颜色),最后一个字节表示了最右上方的8个像素是否镂空。

黑色对应二进制零(镂空),白色对应二进制一(不镂空),编辑完毕后保存。

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
void myDisplay(void)
{
	//镂空效果
	glClear(GL_COLOR_BUFFER_BIT);
	static GLubyte Mask[128];
	FILE* fp;
	fopen_s(&fp,"mask.bmp", "rb");
	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);
	glClear(GL_COLOR_BUFFER_BIT);
	glEnable(GL_POLYGON_STIPPLE);//启动剔除功能
	glPolygonStipple(Mask);
	glRectf(-0.5f, -0.5f, 0.5f, 0.5f);   // 绘制一个有镂空效果的正方形     	 
	glFlush();
}

int main(int argc, char* argv[])
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
	int cx = glutGet(GLUT_SCREEN_WIDTH);
	int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
	glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
	glutInitWindowSize(400, 400);
	glutCreateWindow("第三讲——多边形镂空");
	glutDisplayFunc(&myDisplay);
	glutMainLoop();
	return 0;
}

image-20220112003042201

第四讲

RGBA颜色

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3ub(255,192,203);//粉色
    glRectf(-0.5f, -0.5f, 0.5f, 0.5f);
    glFlush();
}

image-20220107202423099

glColor系列函数,在参数类型不同时,表示“最大”颜色的值也不同。
采用f和d做后缀的函数,以1.0表示最大的使用。
采用b做后缀的函数,以127表示最大的使用。
采用ub做后缀的函数,以255表示最大的使用。
采用s做后缀的函数,以32767表示最大的使用。
采用us做后缀的函数,以65535表示最大的使用。

在默认情况下,OpenGL会计算两点顶点之间的其它点,并为它们填上“合适”的颜色,使相邻的点的颜色值都比较接近。如果使用的是RGB模式,看起来就具有渐变的效果。如果是使用颜色索引模式,则其相邻点的索引值是接近的,如果将颜色表中接近的项设置成接近的颜色,则看起来也是渐变的效果。但如果颜色表中接近的项颜色却差距很大,则看起来可能是很奇怪的效果。

使用glShadeModel函数可以关闭这种计算,如果顶点的颜色不同,则将顶点之间的其它点全部设置为与某一个点相同。(直线以后指定的点的颜色为准,而多边形将以任意顶点的颜色为准,由实现决定。)为了避免这个不确定性,尽量在多边形中使用同一种颜色。

颜色索引

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include <time.h>
#include "tex.h"

#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glaux.lib")
#pragma comment(lib, "legacy_stdio_definitions.lib")


const GLdouble Pi = 3.1415926536;
void myDisplay(void)
{
    int i;
    for (i = 0; i < 8; ++i)
        auxSetOneColor(i, (float)(i & 0x04), (float)(i & 0x02), (float)(i & 0x01));
    glShadeModel(GL_FLAT);
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_TRIANGLE_FAN);
    glVertex2f(0.0f, 0.0f);
    for (i = 0; i <= 8; ++i)
    {
        glIndexi(i);
        glVertex2f(cos(i * Pi / 4), sin(i * Pi / 4));
    }
    glEnd();
    glFlush();
}

int main(void)
{
    auxInitDisplayMode(AUX_SINGLE | AUX_INDEX);
    auxInitPosition(0, 0, 400, 400);
    auxInitWindow(L"");
    myDisplay();
    Sleep(10 * 1000);
    return 0;
}

image-20220112144352102

颜色表

 glShadeModel的使用方法:
 glShadeModel(GL_SMOOTH);  // 平滑方式,这也是默认方式
 glShadeModel(GL_FLAT);   // 单色方式
void myDisplay(void)
{
    int i;
   /* glShadeModel(GL_FLAT);*/
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_TRIANGLE_FAN);
    glColor3f(1.0f, 1.0f, 1.0f);
    glVertex2f(0.0f, 0.0f);
    for (i = 0; i <= 8; ++i)
    {
        glColor3f(i & 0x04, i & 0x02, i & 0x01);
        glVertex2f(cos(i * Pi / 4), sin(i * Pi / 4));
    }
    glEnd();
    glFlush();
}

image-20220107202847534image-20220107202940501

第五讲

从“相对移动”的观点来看,改变观察点的位置与方向和改变物体本身的位置与方向具有等效性。在OpenGL中,实现这两种功能甚至使用的是同样的函数。
由于模型和视图的变换都通过矩阵运算来实现,在进行变换前,应先设置当前操作的矩阵为“模型视图矩阵”。设置的方法是以GL_MODELVIEW为参数调用glMatrixMode函数,像这样:
glMatrixMode(GL_MODELVIEW);
通常,我们需要在进行变换前把当前矩阵设置为单位矩阵。这也只需要一行代码:
glLoadIdentity();

然后,就可以进行模型变换和视图变换了。进行模型和视图变换,主要涉及到三个函数:

glTranslate*,把当前矩阵和一个表示移动物体的矩阵相乘。三个参数分别表示了在三个坐标上的位移值。
glRotate*,把当前矩阵和一个表示旋转物体的矩阵相乘。物体将绕着(0,0,0)到(x,y,z)的直线以逆时针旋转,参数angle表示旋转的角度。
glScale*,把当前矩阵和一个表示缩放物体的矩阵相乘。x,y,z分别表示在该方向上的缩放比例。

“先移动后旋转”和“先旋转后移动”得到的结果很可能不同,初学的时候需要特别注意这一点。

旋转的时候,坐标系统随着物体旋转。移动的时候,坐标系统随着物体移动。如此一来,就不需要考虑代码的顺序反转的问题了。

太阳月亮地球

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>


// 太阳、地球和月亮
// 假设每个月都是30天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359
void myDisplay(void)
{
    glDepthFunc(GL_ALWAYS);//总是绘制
    glEnable(GL_DEPTH_TEST);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(75, 1, 1, 400000000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

    // 绘制红色的“太阳”
    glColor3f(1.0f, 0.0f, 0.0f);
    glutSolidSphere(69600000, 20, 20);
    // 绘制蓝色的“地球”
    glColor3f(0.0f, 0.0f, 1.0f);
    glRotatef(day / 360.0 * 360.0, 0.0f, 0.0f, -1.0f);
    glTranslatef(150000000, 0.0f, 0.0f);
    glutSolidSphere(15945000, 20, 20);
    // 绘制黄色的“月亮”
    glColor3f(1.0f, 1.0f, 0.0f);
    glRotatef(day / 30.0 * 360.0 - day / 360.0 * 360.0, 0.0f, 0.0f, -1.0f);
    glTranslatef(38000000, 0.0f, 0.0f);
    glutSolidSphere(4345000, 20, 20);
    glFlush();
}


int  main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
    int cx = glutGet(GLUT_SCREEN_WIDTH);
    int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
    glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第五讲——太阳月亮地球");
    glutDisplayFunc(myDisplay);
    glutMainLoop();
    return 0;
}

第六讲

太阳月亮地球加旋转

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include<time.h>
static int day = 200; // day的变化:从0到359
double CalFrequency()
{
    static int count;
    static double save;
    static clock_t last, current;
    double timegap;

    ++count;
    if (count <= 50)
        return save;
    count = 0;
    last = current;
    current = clock();
    timegap = (current - last) / (double)CLK_TCK;
    save = 50.0 / timegap;
    return save;
}//统计该函数自身的调用频率

void myDisplay(void)
{
    double FPS = CalFrequency();
    printf("FPS = %f\\n", FPS);
    glDepthFunc(GL_ALWAYS);//总是绘制
    glEnable(GL_DEPTH_TEST);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(75, 1, 1, 400000000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

    // 绘制红色的“太阳”
    glColor3f(1.0f, 0.0f, 0.0f);
    glutSolidSphere(69600000, 20, 20);
    // 绘制蓝色的“地球”
    glColor3f(0.0f, 0.0f, 1.0f);
    glRotatef(day / 360.0 * 360.0, 0.0f, 0.0f, -1.0f);
    glTranslatef(150000000, 0.0f, 0.0f);
    glutSolidSphere(15945000, 20, 20);
    // 绘制黄色的“月亮”
    glColor3f(1.0f, 1.0f, 0.0f);
    glRotatef(day / 30.0 * 360.0 - day / 360.0 * 360.0, 0.0f, 0.0f, -1.0f);
    glTranslatef(38000000, 0.0f, 0.0f);
    glutSolidSphere(4345000, 20, 20);

    glFlush();
    glutSwapBuffers();
}

void myIdle(void)
{
    /* 新的函数,在空闲时调用,作用是把日期往后移动一天并重新绘制,达到动画效果 */
    ++day;
    if (day >= 360)
        day = 0;
    myDisplay();
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了参数为GLUT_DOUBLE
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(400, 400);
    glutCreateWindow("第六讲——太阳,地球和月亮");   // 改了窗口标题
    glutDisplayFunc(&myDisplay);
    glutIdleFunc(&myIdle);               // CPU空闲的时间调用某一函数
    glutMainLoop();
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y5sA4PHu-1641975777778)(C:/Users/26969/Desktop/%E7%AC%AC%E5%85%AD%E8%AE%B2.gif)]

第七讲

太阳月亮加光照

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include<time.h>
#define WIDTH 400
#define HEIGHT 400

static GLfloat angle = 0.0f;

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(1.0, 1.0, 1.0,1.0);
    // 创建透视效果视图
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(90.0f, 1.0f, 1.0f, 20.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(0.0, 5.0, -10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

    // 定义太阳光源,它是一种白色的光源  
    {
        GLfloat sun_light_position[] = { 0.0f, 0.0f, 0.0f, 1.0f };//光源位置
        GLfloat sun_light_ambient[] = { 0.0f, 0.0f, 0.0f, 1.0f }; //多次反射后遗留光
        GLfloat sun_light_diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f };//漫反射光
        GLfloat sun_light_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };//镜面反射光

        glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);
        glLightfv(GL_LIGHT0, GL_AMBIENT, sun_light_ambient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, sun_light_diffuse);
        glLightfv(GL_LIGHT0, GL_SPECULAR, sun_light_specular);

        glEnable(GL_LIGHT0);
        glEnable(GL_LIGHTING);
        glEnable(GL_DEPTH_TEST);
    }

    // 定义太阳的材质并绘制太阳
    {
        GLfloat sun_mat_ambient[] = { 0.0f, 0.0f, 0.0f, 1.0f };
        GLfloat sun_mat_diffuse[] = { 0.0f, 0.0f, 0.0f, 1.0f };
        GLfloat sun_mat_specular[] = { 0.0f, 0.0f, 0.0f, 1.0f };
        GLfloat sun_mat_emission[] = { 0.5f, 0.0f, 0.0f, 1.0f };
        GLfloat sun_mat_shininess = 0.0f;

        glMaterialfv(GL_FRONT, GL_AMBIENT, sun_mat_ambient);
        glMaterialfv(GL_FRONT, GL_DIFFUSE, sun_mat_diffuse);
        glMaterialfv(GL_FRONT, GL_SPECULAR, sun_mat_specular);
        glMaterialfv(GL_FRONT, GL_EMISSION, sun_mat_emission);
        glMaterialf(GL_FRONT, GL_SHININESS, sun_mat_shininess);

        glutSolidSphere(2.0, 40, 32);
    }

    // 定义地球的材质并绘制地球
    {
        GLfloat earth_mat_ambient[] = { 0.0f, 0.0f, 0.5f, 1.0f };
        GLfloat earth_mat_diffuse[] = { 0.0f, 0.0f, 0.5f, 1.0f };//类似于蓝色
        GLfloat earth_mat_specular[] = { 0.0f, 0.0f, 1.0f, 1.0f };
        GLfloat earth_mat_emission[] = { 0.0f, 0.0f, 0.0f, 1.0f };
        GLfloat earth_mat_shininess = 30.0f;

        glMaterialfv(GL_FRONT, GL_AMBIENT, earth_mat_ambient);
        glMaterialfv(GL_FRONT, GL_DIFFUSE, earth_mat_diffuse);
        glMaterialfv(GL_FRONT, GL_SPECULAR, earth_mat_specular);
        glMaterialfv(GL_FRONT, GL_EMISSION, earth_mat_emission);
        glMaterialf(GL_FRONT, GL_SHININESS, earth_mat_shininess);

        glRotatef(angle, 0.0f, -1.0f, 0.0f);
        glTranslatef(5.0f, 0.0f, 0.0f);
        glutSolidSphere(2.0, 40, 32);
    }

    glutSwapBuffers();
}
void myIdle(void)
{
    angle += 1.0f;
    if (angle >= 360.0f)
        angle = 0.0f;
    myDisplay();
}

int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了参数为GLUT_DOUBLE
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(WIDTH,HEIGHT);
    glutCreateWindow("第七讲——太阳,地球和月亮");   // 改了窗口标题
    glutDisplayFunc(&myDisplay);
    glutIdleFunc(&myIdle);               // CPU空闲的时间调用某一函数
    glutMainLoop();
    return 0;
}

第七讲

第八讲

一、分配显示列表编号

OpenGL允许多个显示列表同时存在,就好象C语言允许程序中有多个函数同时存在。C语言中,不同的函数用不同的名字来区分,而在OpenGL中,不同的显示列表用不同的正整数来区分。
你可以自己指定一些各不相同的正整数来表示不同的显示列表。但是如果你不够小心,可能出现一个显示列表将另一个显示列表覆盖的情况。为了避免这一问题,使用glGenLists函数来自动分配一个没有使用的显示列表编号。
glGenLists函数有一个参数i,表示要分配i个连续的未使用的显示列表编号。返回的是分配的若干连续编号中最小的一个。例如,glGenLists(3);如果返回20,则表示分配了20、21、22这三个连续的编号。如果函数返回零,表示分配失败。
可以使用glIsList函数判断一个编号是否已经被用作显示列表。

二、创建显示列表

创建显示列表实际上就是把各种OpenGL函数的调用装入到显示列表中。使用glNewList开始装入,使用glEndList结束装入。glNewList有两个参数,第一个参数是一个正整数表示装入到哪个显示列表。第二个参数有两种取值,如果为GL_COMPILE,则表示以下的内容只是装入到显示列表,但现在不执行它们;如果为GL_COMPILE_AND_EXECUTE,表示在装入的同时,把装入的内容执行一遍。
例如,需要把“设置颜色为红色,并且指定一个坐标为(0, 0)的顶点”这两条命令装入到编号为list的显示列表中,并且在装入的时候不执行,则可以用下面的代码:

 glNewList(list, GL_COMPILE);
 glColor3f(1.0f, 0.0f, 0.0f);
 glVertex2f(0.0f, 0.0f);
 glEnd();

注意:显示列表只能装入OpenGL函数,而不能装入其它内容。例如:

 int i = 3;
 glNewList(list, GL_COMPILE);
 if( i > 20 )
   glColor3f(1.0f, 0.0f, 0.0f);
 glVertex2f(0.0f, 0.0f);
 glEnd();

其中if这个判断就没有被装入到显示列表。以后即使修改i的值,使i>20的条件成立,则glColor3f这个函数也不会被执行。因为它根本就不存在于显示列表中。

另外,并非所有的OpenGL函数都可以装入到显示列表中。例如,各种用于查询的函数,它们无法被装入到显示列表,因为它们都具有返回值,而glCallList和glCallLists函数都不知道如何处理这些返回值。在网络方式下,设置客户端状态的函数也无法被装入到显示列表,这是因为显示列表被保存到服务器端,各种设置客户端状态的函数在发送到服务器端以前就被执行了,而服务器端无法执行这些函数。分配、创建、删除显示列表的动作也无法被装入到另一个显示列表,但调用显示列表的动作则可以被装入到另一个显示列表。

三、调用显示列表

使用glCallList函数可以调用一个显示列表。该函数有一个参数,表示要调用的显示列表的编号。例如,要调用编号为10的显示列表,直接使用glCallList(10);就可以了。

使用glCallLists函数可以调用一系列的显示列表。

该函数有三个参数,第一个参数表示了要调用多少个显示列表。

第二个参数表示了这些显示列表的编号的储存格式,可以是

GL_BYTE(每个编号用一个GLbyte表示),GL_UNSIGNED_BYTE(每个编号

用一个GLubyte表示),GL_SHORT,GL_UNSIGNED_SHORT,GL_INT,GL_UNSIGNED_INT,GL_FLOAT。

第三个参数表示了这些显示列表的编号所在的位置。在使用该函数前,需要用glListBase函数来设置一个偏移量。假设偏移量为k,且glCallLists中要求调用的显示列表编号依次为l1, l2, l3, …,则实际调用的显示列表为l1+k, l2+k, l3+k, …。
例如:

 GLuint lists[] = {1, 3, 4, 8};
 glListBase(10);
 glCallLists(4, GL_UNSIGNED_INT, lists);

则实际上调用的是编号为11, 13, 14, 18的四个显示列表。

注:“调用显示列表”这个动作本身也可以被装在另一个显示列表中。

四、销毁显示列表

销毁显示列表可以回收资源。使用glDeleteLists来销毁一串编号连续的显示列表。
例如,使用glDeleteLists(20, 4);将销毁20,21,22,23这四个显示列表。

使用显示列表将会带来一些开销,例如,把各种动作保存到显示列表中会占用一定数量的内存资源。但如果使用得当,显示列表可以提升程序的性能。这主要表现在以下方面:

1、明显的减少OpenGL函数的调用次数。如果函数调用是通过网络进行的(Linux等操作系统支持这样的方式,即由应用程序在客户端发出OpenGL请求,由网络上的另一台服务器进行实际的绘图操作),将显示列表保存在服务器端,可以大大减少网络负担。
2、保存中间结果,避免一些不必要的计算。例如前面的样例程序中,cos、sin函数的计算结果被直接保存到显示列表中,以后使用时就不必重复计算。
3、便于优化。我们已经知道,使用glTranslate*、glRotate*、glScale*等函数时,实际上是执行矩阵乘法操作,由于这些函数经常被组合在一起使用,通常会出现矩阵的连乘。这时,如果把这些操作保存到显示列表中,则一些复杂的OpenGL版本会尝试先计算出连乘的一部分结果,从而提高程序的运行速度。在其它方面也可能存在类似的例子。

同时,显示列表也为程序的设计带来方便。我们在设置一些属性时,经常把一些相关的函数放在一起调用,(比如,把设置光源的各种属性的函数放到一起)这时,如果把这些设置属性的操作装入到显示列表中,则可以实现属性的成组的切换。
当然了,即使使用显示列表在某些情况下可以提高性能,但这种提高很可能并不明显。毕竟,在硬件配置和大致的软件算法都不变的前提下,性能可提升的空间并不大。

举例

显示列表的内容就是这么多了,下面我们看一个例子。
假设我们需要绘制一个旋转的彩色正四面体,则可以这样考虑:设置一个全局变量angle,然后让它的值不断的增加(到达360后又恢复为0,周而复始)。每次需要绘制图形时,根据angle的值进行旋转,然后绘制正四面体。这里正四面体采用显示列表来实现,即把绘制正四面体的若干OpenGL函数装到一个显示列表中,然后每次需要绘制时,调用这个显示列表即可。

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#define WIDTH 400
#define HEIGHT 400

#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, (GLfloat) (-sqrt(6.0f) / 12) , (GLfloat)(-sqrt(3.0f) / 6) },
            PointB[] = { -0.5f, (GLfloat)(-sqrt(6.0f) / 12) , (GLfloat)(-sqrt(3.0f) / 6) },
            PointC[] = { 0.0f, (GLfloat)(-sqrt(6.0f) / 12) ,   (GLfloat)(sqrt(3.0f) / 3) },
            PointD[] = { 0.0f,   (GLfloat)(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[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(WIDTH, HEIGHT);
    glutCreateWindow("第八讲——显示列表");
    glutDisplayFunc(&myDisplay);
    glutIdleFunc(&myIdle);
    glutMainLoop();
    return 0;
}

第九讲

启动混合

要使用OpenGL的混合功能,只需要调用:glEnable(GL_BLEND);即可。
要关闭OpenGL的混合功能,只需要调用:glDisable(GL_BLEND);即可。

源因子与目标因子

把将要画上去的颜色称为“源颜色”,把原来的颜色称为“目标颜色”。

OpenGL会把源颜色和目标颜色各自取出,并乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),然后相加,这样就得到了新的颜色。
下面用数学公式来表达一下这个运算方式。假设源颜色的四个分量(指红色,绿色,蓝色,alpha值)是(Rs, Gs, Bs, As),目标颜色的四个分量是(Rd, Gd, Bd, Ad),又设源因子为(Sr, Sg, Sb, Sa),目标因子为(Dr, Dg, Db, Da)。则混合产生的新颜色可以表示为:

Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da

当然了,如果颜色的某一分量超过了1.0,则它会被自动截取为1.0,不需要考虑越界的问题。

源因子和目标因子是可以通过glBlendFunc函数来进行设置的。**glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。**这两个参数可以是多种值,下面介绍比较常用的几种。

 GL_ZERO:   表示使用0.0作为因子,实际上相当于不使用这种颜色参与混合运算。
 GL_ONE:   表示使用1.0作为因子,实际上相当于完全的使用了这种颜色参与混合运算。
 GL_SRC_ALPHA:表示使用源颜色的alpha值来作为因子。
 GL_DST_ALPHA:表示使用目标颜色的alpha值来作为因子。
 GL_ONE_MINUS_SRC_ALPHA:表示用1.0减去源颜色的alpha值来作为因子。
 GL_ONE_MINUS_DST_ALPHA:表示用1.0减去目标颜色的alpha值来作为因子。

除此以外,还有GL_SRC_COLOR(把源颜色的四个分量分别作为因子的四个分量)、GL_ONE_MINUS_SRC_COLOR、GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前两个在OpenGL旧版本中只能用于设置目标因子,后两个在OpenGL旧版本中只能用于设置源因子。新版本的OpenGL则没有这个限制,并且支持新的GL_CONST_COLOR(设定一种常数颜色,将其四个分量分别作为因子的四个分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、GL_ONE_MINUS_CONST_ALPHA。另外还有GL_SRC_ALPHA_SATURATE。

🏷 举例来说:
1️⃣ 如果设置了glBlendFunc(GL_ONE, GL_ZERO);,则表示完全使用源颜色,完全不使用目标颜色,因此画面效果和不使用混合的时候一致(当然效率可能会低一点点)。如果没有设置源因子和目标因子,则默认情况就是这样的设置。

2️⃣ 如果设置了glBlendFunc(GL_ZERO, GL_ONE);,表示完全不使用源颜色,因此无论你想画什么,最后都不会被画上去了。(但这并不是说这样设置就没有用,有些时候可能有特殊用途)

3️⃣ 如果设置了glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);,表示源颜色乘以自身的alpha值,目标颜色乘以1.0减去源颜色的alpha值,这样一来,源颜色的alpha值越大,则产生的新颜色中源颜色所占比例就越大,而目标颜色所占比例则减小。这种情况下,我们可以简单的将源颜色的alpha值理解为“ 不透明度”。这也是混合时最常用的方式。

4️⃣ 如果设置了glBlendFunc(GL_ONE, GL_ONE);,则表示完全使用源颜色和目标颜色,最终的颜色实际上就是两种颜色的简单相加。例如红色(1, 0, 0)和绿色(0, 1, 0)相加得到(1, 1, 0),结果为黄色。

⚠️ 注意:
所谓源颜色和目标颜色,是跟绘制的顺序有关的。假如先绘制了一个红色的物体,再在其上绘制绿色的物体。则绿色是源颜色,红色是目标颜色。如果顺序反过来,则红色就是源颜色,绿色才是目标颜色。在绘制时,应该注意顺序,使得绘制的源颜色与设置的源因子对应,目标颜色与设置的目标因子对应。不要被混乱的顺序搞晕了。

二维图形混合举例

下面看一个简单的例子,实现将两种不同的颜色混合在一起。为了便于观察,我们绘制两个矩形:glRectf(-1, -1, 0.5, 0.5);glRectf(-0.5, -0.5, 1, 1);,这两个矩形有一个重叠的区域,便于我们观察混合的效果。
先来看看使用glBlendFunc(GL_ONE, GL_ZERO);的,它的结果与不使用混合时相同。

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#define WIDTH 400
#define HEIGHT 400

void myDisplay(void)
{
    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_BLEND);
    //glBlendFunc(GL_ONE, GL_ZERO);//与不混和没有区别
    //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);//表示使用源颜色的alpha值来作为因子,表示用1.0减去源颜色的alpha值来作为因子。
    glBlendFunc(GL_ONE, GL_ONE);//两种颜色混合
    glColor4f(1, 0, 0, 0.5);
    glRectf(-1, -1, 0.5, 0.5);
    glColor4f(0, 1, 0, 0.5);
    glRectf(-0.5, -0.5, 1, 1);

    glutSwapBuffers();
}


int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(WIDTH, HEIGHT);
    glutCreateWindow("第九讲——颜色混合");
    glutDisplayFunc(&myDisplay);
    glutMainLoop();
    return 0;
}

image-20220112013527056image-20220112013759404image-20220112013909060

三维混合

深度缓冲是这样一段数据,它记录了每一个像素距离观察者有多近。在启用深度缓冲测试的情况下,如果将要绘制的像素比原来的像素更近,则像素将被绘制。否则,像素就会被忽略掉,不进行绘制。这在绘制不透明的物体时非常有用——不管是先绘制近的物体再绘制远的物体,还是先绘制远的物体再绘制近的物体,或者干脆以混乱的顺序进行绘制,最后的显示结果总是近的物体遮住远的物体。

在绘制半透明物体时将深度缓冲区设置为只读,这样一来,虽然半透明物体被绘制上去了,深度缓冲区还保持在原来的状态。如果再有一个物体出现在半透明物体之后,在不透明物体之前,则它也可以被绘制

首先绘制所有不透明的物体。如果两个物体都是不透明的,则谁先谁后都没有关系。然后,将深度缓冲区设置为只读。接下来,绘制所有半透明的物体。如果两个物体都是半透明的,则谁先谁后只需要根据自己的意愿(注意了,先绘制的将成为“目标颜色”,后绘制的将成为“源颜色”,所以绘制的顺序将会对结果造成一些影响)。最后,将深度缓冲区设置为可读可写形式。

glDepthMask(GL_FALSE);可将深度缓冲区设置为只读形式
glDepthMask(GL_TRUE);可将深度缓冲区设置为可读可写形式。

来绘制一些半透明和不透明的球体。假设有三个球体,一个红色不透明的,一个绿色半透明的,一个蓝色半透明的。红色最远,绿色在中间,蓝色最近。根据前面所讲述的内容,**红色不透明球体必须首先绘制,而绿色和蓝色则可以随意修改顺序。**这里为了演示不注意设置深度缓冲的危害,我们故意先绘制最近的蓝色球体,再绘制绿色球体。为了让这些球体有一点立体感,我们使用光照。在(1, 1, -1)处设置一个白色的光源。

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#define WIDTH 400
#define HEIGHT 400

void myDisplay1(void)
{
    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_BLEND);
    //glBlendFunc(GL_ONE, GL_ZERO);//与不混和没有区别
    //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);//表示使用源颜色的alpha值来作为因子,表示用1.0减去源颜色的alpha值来作为因子。
    glBlendFunc(GL_ONE, GL_ONE);//两种颜色混合
    glColor4f(1, 0, 0, 0.5);
    glRectf(-1, -1, 0.5, 0.5);
    glColor4f(0, 1, 0, 0.5);
    glRectf(-0.5, -0.5, 1, 1);

    glutSwapBuffers();
}

void setLight(void)
{
    static const GLfloat light_position[] = { 1.0f, 1.0f, -1.0f, 1.0f };
    static const GLfloat light_ambient[] = { 0.2f, 0.2f, 0.2f, 1.0f };
    static const GLfloat light_diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f };
    static const GLfloat light_specular[] = { 1.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_DEPTH_TEST);
}
//每一个球体颜色不同。所以它们的材质也都不同。这里用一个函数来设置材质。
void setMatirial(const GLfloat mat_diffuse[4], GLfloat mat_shininess)
{
    static const GLfloat mat_specular[] = { 0.0f, 0.0f, 0.0f, 1.0f };
    static const GLfloat mat_emission[] = { 0.0f, 0.0f, 0.0f, 1.0f };

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
    glMaterialfv(GL_FRONT, GL_EMISSION, mat_emission);
    glMaterialf(GL_FRONT, GL_SHININESS, mat_shininess);
}

void myDisplay2(void)
{
    // 定义一些材质颜色
    const static GLfloat red_color[] = { 1.0f, 0.0f, 0.0f, 1.0f };
    const static GLfloat green_color[] = { 0.0f, 1.0f, 0.0f, 0.3333f };
    const static GLfloat blue_color[] = { 0.0f, 0.0f, 1.0f, 0.5f };

    // 清除屏幕
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 启动混合并设置混合因子
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // 设置光源
    setLight();

    // 以(0, 0, 0.5)为中心,绘制一个半径为.3的不透明红色球体(离观察者最远)
    setMatirial(red_color, 30.0);
    glPushMatrix();
    glTranslatef(0.0f, 0.0f, 0.5f);
    glutSolidSphere(0.3, 30, 30);
    glPopMatrix();

    // 下面将绘制半透明物体了,因此将深度缓冲设置为只读
    glDepthMask(GL_FALSE);

    // 以(0.2, 0, -0.5)为中心,绘制一个半径为.2的半透明蓝色球体(离观察者最近)
    setMatirial(blue_color, 30.0);
    glPushMatrix();
    glTranslatef(0.2f, 0.0f, -0.5f);
    glutSolidSphere(0.2, 30, 30);//第一个参数表示球体的半径,后两个参数代表了“面”的数目,简单点说就是球体的精确程度,数值越大越精确,当然代价就是速度越缓慢
    glPopMatrix();

    // 以(0.1, 0, 0)为中心,绘制一个半径为.15的半透明绿色球体(在前两个球体之间)
    setMatirial(green_color, 30.0);
    glPushMatrix();
    glTranslatef(0.1, 0, 0);
    glutSolidSphere(0.15, 30, 30);
    glPopMatrix();

    // 完成半透明物体的绘制,将深度缓冲区恢复为可读可写的形式
    glDepthMask(GL_TRUE);

    glutSwapBuffers();
}


int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(WIDTH, HEIGHT);
    glutCreateWindow("第九讲——颜色混合");
    glutDisplayFunc(&myDisplay2);
    glutMainLoop();
    return 0;
}

image-20220112014532289

在进行三维混合时,不仅要考虑源因子和目标因子,还应该考虑深度缓冲区。必须先绘制所有不透明的物体,再绘制半透明的物体。在绘制半透明物体时前,还需要将深度缓冲区设置为只读形式,否则可能出现画面错误。

第十讲

ReadPixels:读取一些像素。当前可以简单理解为“把已经绘制好的像素(它可能已经被保存到显卡的显存中)读取到内存”。
glDrawPixels:绘制一些像素。当前可以简单理解为“把内存中一些数据作为像素数据,进行绘制”。
glCopyPixels:复制一些像素。当前可以简单理解为“把已经绘制好的像素从一个位置复制到另一个位置”。虽然从功能上看,好象等价于先读取像素再绘制像素,但实际上它不需要把已经绘制的像素(它可能已经被保存到显卡的显存中)转换为内存数据,然后再由内存数据进行重新的绘制,所以要比先读取后绘制快很多。
这三个函数可以完成简单的像素读取、绘制和复制任务,但实际上也可以完成更复杂的任务

image-20220108163818374

Windows所使用的BMP文件,在开始处有一个文件头,大小为54字节。保存了包括文件格式标识、颜色数、图象大小、压缩方式等信息,因为我们仅讨论24位色不压缩的BMP,所以文件头中的信息基本不需要注意,只有“大小”这一项对我们比较有用。

从一个正确的BMP文件中读取前54个字节,修改其中的宽度和高度信息,就可以得到新的文件头了。假设我们先建立一个1*1大小的24位色BMP,文件名为dummy.bmp,又假设新的BMP文件名称为grab.bmp。则可以编写如下代码:

图象的宽度和高度都是一个32位整数,在文件中的地址分别为0x0012和0x0016,

从一个正确的BMP文件中读取前54个字节,修改其中的宽度和高度信息,就可以得到新的文件头了。假设我们先建立一个1*1大小的24位色BMP,文件名为dummy.bmp,又假设新的BMP文件名称为grab.bmp。则可以编写如下代码:

把这段代码复制到以前任何课程的样例程序中,在绘制函数的最后调用grab函数,即可把图象内容保存为BMP文件了。(在我写这个教程的时候,不少地方都用这样的代码进行截图工作,这段代码一旦写好,运行起来是很方便的。)

像素读取

#define WindowWidth   500
#define WindowHeight 500

/* 函数grab
* 抓取窗口中的像素
* 假设窗口宽度为WindowWidth,高度为WindowHeight
*/
#define BMP_Header_Length 54
void grab(void)
{
	glReadBuffer(GL_FRONT);
	FILE* pDummyFile;
	FILE* pWritingFile;
	GLubyte* pPixelData;
	GLubyte   BMP_Header[BMP_Header_Length];
	GLint     i, j;
	GLint     PixelDataLength;

	// 计算像素数据的实际长度
	i = WindowWidth * 3;    // 得到每一行的像素数据长度
	//24位色的BMP文件中,每三个字节表示一个像素的颜色。
	while (i % 4 != 0)       // 补充数据,直到i是4的倍数
		++i;                // 本来还有更快的算法,
						   // 但这里仅追求直观,对速度没有太高要求
	PixelDataLength = i * WindowHeight;

	// 分配内存和打开文件
	pPixelData = (GLubyte*)malloc(PixelDataLength);
	if (pPixelData == 0)
		exit(0);

	fopen_s(&pDummyFile,"dummy.bmp", "rb");
	if (pDummyFile == 0)
		exit(0);

	fopen_s(&pWritingFile,"grab.bmp", "wb");
	if (pWritingFile == 0)
		exit(0);

	// 读取像素
	glPixelStorei(GL_UNPACK_ALIGNMENT, 4);//4对齐
	glReadPixels(0, 0, WindowWidth, WindowHeight,
		GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);//最后一个参数,是像素读取后存放在内存中的位置

	// 把dummy.bmp的文件头复制为新文件的文件头
	fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
	fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
	fseek(pWritingFile, 0x0012, SEEK_SET); //移动到0x0012位置,读取宽度
	i = WindowWidth;
	//读完以后自动到下一个高度
	j = WindowHeight;
	fwrite(&i, sizeof(i), 1, pWritingFile);
	fwrite(&j, sizeof(j), 1, pWritingFile);

	// 写入像素数据
	fseek(pWritingFile, 0, SEEK_END);//当前文件尾部,即头信息后
	//偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))
	fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

	// 释放内存和关闭文件
	fclose(pDummyFile);
	fclose(pWritingFile);
	free(pPixelData);
}
void init()
{
	{

		/*设置光源*/
		GLfloat light_position[] = { 0.5,0.0,1.0,0.0 };//光源位置
		GLfloat light_ambient[] = { 1.0,1.0,1.0,1.0 };   //多次反射后遗留光
		GLfloat light_diffuse[] = { 1.0,1.0,1.0,1.0 };   //漫反射光
		GLfloat light_specular[] = { 1.0,1.0,1.0,1.0 };   //镜面反射光

		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);
	}

	{
		/*设置球的材质*/
		GLfloat sphere_specular[] = { 1.0,1.0,1.0,1.0 };
		GLfloat sphere_diffuse[] = { 1.0,1.0,1.0,1.0 };
		GLfloat sphere_ambient[] = { 0.0,0.0,0.0,1.0 };
		GLfloat sphere_emission[] = { 0.0,0.0,0.0,1.0 };//向外散光
		GLfloat sphere_shininess[] = { 100.0 }; //镜面反射指数

		glMaterialfv(GL_FRONT, GL_AMBIENT, sphere_ambient);
		glMaterialfv(GL_FRONT, GL_DIFFUSE, sphere_diffuse);
		glMaterialfv(GL_FRONT, GL_SPECULAR, sphere_specular);
		glMaterialfv(GL_FRONT, GL_EMISSION, sphere_emission);
		glMaterialfv(GL_FRONT, GL_SPECULAR, sphere_specular);
		glMaterialfv(GL_FRONT, GL_SHININESS, sphere_shininess);
	}

	GLfloat gl_model_ambient[] = { 0.2,0.2,0.2,1.0 };//微弱环境光,使物体可见

	glClearColor(1.0, 0.75, 0.78, 1.0);//背景颜色
	glShadeModel(GL_SMOOTH);//光滑着色

	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gl_model_ambient);
	//指定全局的环境光,物体才能可见
	glEnable(GL_LIGHTING);//启动光照
	glEnable(GL_LIGHT0);	//启动光源
	glEnable(GL_DEPTH_TEST);//启动深度测试
}
void myDisplay()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glutSolidSphere(1.0, 3000, 1000);//半径,南北经线,纬线
	grab();
	glFlush();
}
void reshape(int w, int h)
{
	glViewport(0, 0, (GLsizei)w, (GLsizei)h);//使像素矩阵占据整个新窗口
	glMatrixMode(GL_PROJECTION);//设置当前操作的矩阵为“投影视图矩阵”
	glLoadIdentity();

	float nowRange = 2;
	GLfloat nRange = (GLfloat)nowRange;

	if (w <= h) //改变窗口大小,图形形状不变
		glOrtho(-nRange, nRange, -nRange * h / w, nRange * h / w, -nRange, nRange);
	else
		glOrtho(-nRange * w / h, nRange * w / h, -nRange, nRange, -nRange, nRange);
	//将当前的可视空间设置为正投影空间
	glMatrixMode(GL_MODELVIEW);//设置当前操作的矩阵为“模型视图矩阵”
	glLoadIdentity();

}
int main(int argc, char* argv[])
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_SINGLE);
	int cx = glutGet(GLUT_SCREEN_WIDTH);
	int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
	glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
	glutInitWindowSize(WindowWidth, WindowHeight);
	glutCreateWindow("OpenGL3D编程基础");
	init();//初始化视图设置
	glutDisplayFunc(&myDisplay);
	glutReshapeFunc(reshape);//改变窗口
	glutMainLoop();
	return 0;
}

glPixelStorei(GL_UNPACK_ALIGNMENT,4)控制的是所读取数据的对齐方式,默认4字节对齐,即一行的图像数据字节数必须是4的整数倍,即读取数据时,读取4个字节用来渲染一行,之后读取4字节数据用来渲染第二行。对RGB 3字节像素而言,若一行10个像素,即30个字节,在4字节对齐模式下,OpenGL会读取32个字节的数据,若不加注意,会导致glTextImage中致函数的读取越界,从而全面崩溃。

image-20220112100644584image-20220112100948189

glReadPixels

该函数总共有七个参数。前四个参数可以得到一个矩形,该矩形所包括的像素都会被读取出来。(第一、二个参数表示了矩形的左下角横、纵坐标,坐标以窗口最左下角为零,最右上角为最大值;第三、四个参数表示了矩形的宽度和高度)
第五个参数表示读取的内容,例如:GL_RGB就会依次读取像素的红、绿、蓝三种数据,GL_RGBA则会依次读取像素的红、绿、蓝、alpha四种数据,GL_RED则只读取像素的红色数据(类似的还有GL_GREEN,GL_BLUE,以及GL_ALPHA)。如果采用的不是RGBA颜色模式,而是采用颜色索引模式,则也可以使用GL_COLOR_INDEX来读取像素的颜色索引。目前仅需要知道这些,但实际上还可以读取其它内容,例如深度缓冲区的深度数据等。
第六个参数表示读取的内容保存到内存时所使用的格式,例如:GL_UNSIGNED_BYTE会把各种数据保存为GLubyte,GL_FLOAT会把各种数据保存为GLfloat等。
第七个参数表示一个指针,像素数据被读取后,将被保存到这个指针所表示的地址。

glDrawPixels

glDrawPixels函数与glReadPixels函数相比,参数内容大致相同。它的第一、二、三、四个参数分别对应于glReadPixels函数的第三、四、五、六个参数,依次表示图象宽度、图象高度、像素数据内容、像素数据在内存中的格式。两个函数的最后一个参数也是对应的,glReadPixels中表示像素读取后存放在内存中的位置,glDrawPixels则表示用于绘制的像素数据在内存中的位置。

像素绘制

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include <time.h>


static GLint    ImageWidth;
static GLint    ImageHeight;
static GLint    PixelLength;
static GLubyte* PixelData;


void display(void)
{
    // 清除屏幕并不必要
    // 每次绘制时,画面都覆盖整个屏幕
    // 因此无论是否清除屏幕,结果都一样
    // glClear(GL_COLOR_BUFFER_BIT);

    // 绘制像素
    glDrawPixels(ImageWidth, ImageHeight,
        GL_BGR_EXT, GL_UNSIGNED_BYTE, PixelData);

    // 完成绘制
    glutSwapBuffers();
}

int main(int argc, char* argv[])
{
    // 打开文件
    FILE* pFile;
	fopen_s(&pFile, "grab.bmp", "rb");
    if (pFile == 0)
        exit(0);

    // 读取图象的大小信息
    fseek(pFile, 0x0012, SEEK_SET);
    fread(&ImageWidth, sizeof(ImageWidth), 1, pFile);
    fread(&ImageHeight, sizeof(ImageHeight), 1, pFile);

    // 计算像素数据长度
    PixelLength = ImageWidth * 3;
    while (PixelLength % 4 != 0)
        ++PixelLength;
    PixelLength *= ImageHeight;

    // 读取像素数据
    PixelData = (GLubyte*)malloc(PixelLength);
    if (PixelData == 0)
        exit(0);

    fseek(pFile, 54, SEEK_SET);
    fread(PixelData, PixelLength, 1, pFile);

    // 关闭文件
    fclose(pFile);

    // 初始化GLUT并运行
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(ImageWidth, ImageHeight);
    glutCreateWindow("读取像素");
    glutDisplayFunc(&display);
    glutMainLoop();


    return 0;
}



image-20220112101235333

使用glCopyPixels直接从像素数据复制出新的像素数据,避免了多余的数据的格式转换,并且也可能减少一些数据复制操作(因为数据可能直接由显卡负责复制,不需要经过主内存),因此效率比较高。

glCopyPixels函数也通过glRasterPos*系列函数来设置绘制的位置,因为不需要涉及到主内存,所以不需要指定数据在内存中的格式,也不需要使用任何指针。

glCopyPixels函数有五个参数,第一、二个参数表示复制像素来源的矩形的左下角坐标,第三、四个参数表示复制像素来源的举行的宽度和高度,第五个参数通常使用GL_COLOR,表示复制像素的颜色,但也可以是GL_DEPTH或GL_STENCIL,分别表示复制深度缓冲数据或模板缓冲数据。

像素复制

制一个三角形后,复制像素,并同时进行水平和垂直方向的翻转,然后缩小为原来的一半,并绘制。绘制完毕后,调用前面的grab函数,将屏幕中所有内容保存为grab.bmp。

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include <time.h>

#define WindowWidth   500
#define WindowHeight 500

/* 函数grab
* 抓取窗口中的像素
* 假设窗口宽度为WindowWidth,高度为WindowHeight
*/
#define BMP_Header_Length 54
void grab(void)
{
	glReadBuffer(GL_FRONT);
	FILE* pDummyFile;
	FILE* pWritingFile;
	GLubyte* pPixelData;
	GLubyte   BMP_Header[BMP_Header_Length];
	GLint     i, j;
	GLint     PixelDataLength;

	// 计算像素数据的实际长度
	i = WindowWidth * 3;    // 得到每一行的像素数据长度
	//24位色的BMP文件中,每三个字节表示一个像素的颜色。
	while (i % 4 != 0)       // 补充数据,直到i是4的倍数
		++i;                // 本来还有更快的算法,
						   // 但这里仅追求直观,对速度没有太高要求
	PixelDataLength = i * WindowHeight;

	// 分配内存和打开文件
	pPixelData = (GLubyte*)malloc(PixelDataLength);
	if (pPixelData == 0)
		exit(0);

	fopen_s(&pDummyFile,"dummy.bmp", "rb");
	if (pDummyFile == 0)
		exit(0);

	fopen_s(&pWritingFile,"grab.bmp", "wb");
	if (pWritingFile == 0)
		exit(0);

	// 读取像素
	glPixelStorei(GL_UNPACK_ALIGNMENT, 4);//4对齐
	glReadPixels(0, 0, WindowWidth, WindowHeight,
		GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);//最后一个参数,是像素读取后存放在内存中的位置

	// 把dummy.bmp的文件头复制为新文件的文件头
	fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
	fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
	fseek(pWritingFile, 0x0012, SEEK_SET); //移动到0x0012位置,读取宽度
	i = WindowWidth;
	//读完以后自动到下一个高度
	j = WindowHeight;
	fwrite(&i, sizeof(i), 1, pWritingFile);
	fwrite(&j, sizeof(j), 1, pWritingFile);

	// 写入像素数据
	fseek(pWritingFile, 0, SEEK_END);//当前文件尾部,即头信息后
	//偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))
	fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

	// 释放内存和关闭文件
	fclose(pDummyFile);
	fclose(pWritingFile);
	free(pPixelData);
}

void display(void)
{
	// 清除屏幕
	glClear(GL_COLOR_BUFFER_BIT);

	// 绘制
	glBegin(GL_TRIANGLES);
	glColor3f(1.0f, 0.0f, 0.0f);    glVertex2f(0.0f, 0.0f);
	glColor3f(0.0f, 1.0f, 0.0f);    glVertex2f(1.0f, 0.0f);
	glColor3f(0.0f, 0.0f, 1.0f);    glVertex2f(0.5f, 1.0f);
	glEnd();
	glPixelZoom(-0.5f, -0.5f);
	glRasterPos2i(1, 1);
	glCopyPixels(WindowWidth / 2, WindowHeight / 2,
		WindowWidth / 2, WindowHeight / 2, GL_COLOR);

	// 完成绘制,并抓取图象保存为BMP文件
	glutSwapBuffers();
	grab();
}

int main(int argc, char* argv[])
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	int cx = glutGet(GLUT_SCREEN_WIDTH);
	int cy = glutGet(GLUT_SCREEN_HEIGHT);//为了使窗口居中
	glutInitWindowPosition((cx - 400) / 2, (cy - 400) / 2);
	glutInitWindowSize(WindowWidth, WindowHeight);
	glutCreateWindow("OpenGL3D编程基础");
	glutDisplayFunc(&display);
	glutMainLoop();
	return 0;
}

image-20220112101339315

第十一讲

1、启用纹理和载入纹理

    glEnable(GL_TEXTURE_2D);  // 启用二维纹理
    glDisable(GL_TEXTURE_2D); // 禁用二维纹理

利用glTexImage2D函数可以载入一个二维的纹理

1️⃣ 第一个参数为指定的目标,在我们的入门教材中,这个参数将始终使用GL_TEXTURE_2D。

2️⃣ 第二个参数为“多重细节层次”,现在我们并不考虑多重纹理细节,因此这个参数设置为零。

3️⃣ 第三个参数有两种用法。在OpenGL 1.0,即最初的版本中,使用整数来表示颜色分量数目,例如:像素数据用RGB颜色表示,总共有红、绿、蓝三个值,因此参数设置为3,而如果像素数据是用RGBA颜色表示,总共有红、绿、蓝、alpha四个值,因此参数设置为4。而在后来的版本中,可以直接使用GL_RGB或GL_RGBA来表示以上情况,显得更直观(并带来其它一些好处,这里暂时不提)。注意:虽然我们使用Windows的BMP文件作为纹理时,一般是蓝色的像素在最前,其真实的格式为GL_BGR而不是GL_RGB,在数据的顺序上有所不同,但因为同样是红、绿、蓝三种颜色,因此这里仍然使用GL_RGB。(如果使用GL_BGR,OpenGL将无法识别这个参数,造成错误)

4️⃣ 第四、五个参数是二维纹理像素的宽度和高度。这里有一个很需要注意的地方:OpenGL在以前的很多版本中,限制纹理的大小必须是2的整数次方,即纹理的宽度和高度只能是16, 32, 64, 128, 256等值,直到最近的新版本才取消了这个限制。而且,一些OpenGL实现(例如,某些PC机上板载显卡的驱动程序附带的OpenGL)并没有支持到如此高的OpenGL版本。因此在使用纹理时要特别注意其大小。尽量使用大小为2的整数次方的纹理,当这个要求无法满足时,使用gluScaleImage函数把图象缩放至所指定的大小(在后面的例子中有用到)。另外,无论旧版本还是新版本,都限制了纹理大小的最大值,例如,某OpenGL实现可能要求纹理最大不能超过1024*1024。可以使用如下的代码来获得OpenGL所支持的最大纹理:

GLint max;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);

这样max的值就是当前OpenGL实现中所支持的最大纹理。
在很长一段时间内,很多图形程序都喜欢使用256*256大小的纹理,不仅因为256是2的整数次方,也因为某些硬件可以使用8位的整数来表示纹理坐标,2的8次方正好是256,这一巧妙的组合为处理纹理坐标时的硬件优化创造了一些不错的条件。

5️⃣ 第六个参数是纹理边框的大小,我们没有使用纹理边框,因此这里设置为零

6️⃣ 最后三个参数与glDrawPixels函数的最后三个参数的使用方法相同,其含义可以参考glReadPixels的最后三个参数。大家可以复习一下第10课的相关内容,这里不再重复。
举个例子,如果有一幅大小为width/*height,格式为Windows系统中使用最普遍的24位BGR,保存在pixels中的像素图象。则把这样一幅图象载入为纹理可使用以下代码:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);

注意,载入纹理的过程可能比较慢,原因是纹理数据通常比较大,例如一幅512*512的BGR格式的图象,大小为0.75M。把这些像素数据从主内存传送到专门的图形硬件,这个过程中还可能需要把程序中所指定的像素格式转化为图形硬件所能识别的格式(或最能发挥图形硬件性能的格式),这些操作都需要较多时间。

2、纹理坐标

只要指定每一个顶点在纹理图象中所对应的像素位置,OpenGL就会自动计算顶点以外的其它点在纹理图象中所对应的像素位置。

在绘制一条线段时,我们设置其中一个端点使用“纹理图象中最左下角的颜色”作为它的颜色,另一个端点使用“纹理图象中最右上角的颜色”作为它的颜色,则OpenGL会自动在纹理图象中选择合适位置的颜色,填充到线段的各个像素(例如线段中点,可能就是选择纹理图象中央的那个像素的颜色)。

使用glTexCoord*系列函数来指定纹理坐标。这些函数的用法与使用glVertex*系列函数来指定顶点坐标十分相似。例如:glTexCoord2f(0.0f, 0.0f);指定使用(0, 0)纹理坐标

通常,每个顶点使用不同的纹理,于是下面这样形式的代码是比较常见的。

glBegin( /* ... */ );
    glTexCoord2f( /* ... */ );  glVertex3f( /* ... */ );
    glTexCoord2f( /* ... */ );  glVertex3f( /* ... */ );
    /* ... */
glEnd();

glRotate*,glScale*,glTranslate*等操作矩阵的函数可以用来处理“对纹理坐标进行转换”的工作

3、纹理参数

使用glTexParameter*系列函数来设置纹理参数。通常需要设置下面四个参数:

1️⃣ GL_TEXTURE_MAG_FILTER指当纹理图象被使用到一个大于它的形状上时(即:有可能纹理图象中的一个像素会被应用到实际绘制时的多个像素。例如将一幅256*256的纹理图象应用到一个512*512的正方形),应该如何处理。可选择的设置有GL_NEAREST和GL_LINEAR,前者表示“使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色”,后者表示“使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色”。前者只经过简单比较,需要运算较少,可能速度较快,后者需要经过加权平均计算,其中涉及除法运算,可能速度较慢(但如果有专门的处理硬件,也可能两者速度相同)。从视觉效果上看,前者效果较差,在一些情况下锯齿现象明显,后者效果会较好(但如果纹理图象本身比较大,则两者在视觉效果上就会比较接近)。

2️⃣ GL_TEXTURE_MIN_FILTER:指当纹理图象被使用到一个小于(或等于)它的形状上时(即有可能纹理图象中的多个像素被应用到实际绘制时的一个像素。例如将一幅256*256的纹理图象应用到一个128*128的正方形),应该如何处理。可选择的设置有

GL_NEAREST,GL_LINEAR,GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_NEAREST和GL_LINEAR_MIPMAP_LINEAR。

其中后四个涉及到mipmap,现在暂时不需要了解。前两个选项则和GL_TEXTURE_MAG_FILTER中的类似

3️⃣ GL_TEXTURE_WRAP_S:指当纹理坐标的第一维坐标值大于1.0或小于0.0时,应该如何处理。基本的选项有GL_CLAMP和GL_REPEAT,前者表示“截断”,即超过1.0的按1.0处理,不足0.0的按0.0处理。后者表示“重复”,即对坐标值加上一个合适的整数(可以是正数或负数),得到一个在[0.0, 1.0]范围内的值,然后用这个值作为新的纹理坐标。例如:某二维纹理,在绘制某形状时,一像素需要得到纹理中坐标为(3.5, 0.5)的像素的颜色,其中第一维的坐标值3.5超过了1.0,则在GL_CLAMP方式中将被转化为(1.0, 0.5),在GL_REPEAT方式中将被转化为(0.5, 0.5)。在后来的OpenGL版本中,又增加了新的处理方式,这里不做介绍。如果不指定这个参数,则默认为GL_REPEAT

4️⃣ GL_TEXTURE_WRAP_T:指当纹理坐标的第二维坐标值大于1.0或小于0.0时,应该如何处理。选项与GL_TEXTURE_WRAP_S类似,不再重复。如果不指定这个参数,则默认为GL_REPEAT。

设置参数的代码如下所示:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);

4、纹理对象

我们可以把每一幅纹理(包括纹理的像素数据、纹理大小等信息,也包括了前面所讲的纹理参数)放到一个纹理对象中,通过创建多个纹理对象来达到同时保存多幅纹理的目的。这样一来,在第一次使用纹理前,把所有的纹理都载入,然后在绘制时只需要指明究竟使用哪一个纹理对象就可以了。

使用纹理对象和使用显示列表有相似之处:使用一个正整数来作为纹理对象的编号。在使用前,可以调用glGenTextures来分配纹理对象。该函数有两种比较常见的用法:

GLuint texture_ID;
glGenTextures(1, &texture_ID); // 分配一个纹理对象的编号

或者
    :
GLuint texture_ID_list[5];
glGenTextures(5, texture_ID_list); // 分配5个纹理对象的编号

零是一个特殊的纹理对象编号,表示“默认的纹理对象”,在分配正确的情况下,glGenTextures不会分配这个编号。与glGenTextures对应的是glDeleteTextures,用于销毁一个纹理对象。

在分配了纹理对象编号后,使用glBindTexture函数来指定“当前所使用的纹理对象”。然后就可以使用glTexImage*系列函数来指定纹理像素、使用glTexParameter*系列函数来指定纹理参数、使用glTexCoord*系列函数来指定纹理坐标了。如果不使用glBindTexture函数,那么glTexImage*、glTexParameter*、glTexCoord*系列函数默认在一个编号为0的纹理对象上进行操作。

glBindTexture函数有两个参数,第一个参数是需要使用纹理的目标,因为我们现在只学习二维纹理,所以指定为GL_TEXTURE_2D,第二个参数是所使用的纹理的编号。

使用多个纹理对象,就可以使OpenGL同时保存多个纹理。在使用时只需要调用glBindTexture函数,在不同纹理之间进行切换,而不需要反复载入纹理,因此动画的绘制速度会有非常明显的提升。典型的代码如下所示:

// 在程序开始时:分配好纹理编号,并载入纹理
glGenTextures( /* ... */ );
glBindTexture(GL_TEXTURE_2D, texture_ID_1);
// 载入第一幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_2);
// 载入第二幅纹理

// 在绘制时,切换并使用纹理,不需要再进行载入
glBindTexture(GL_TEXTURE_2D, texture_ID_1); // 指定第一幅纹理
// 使用第一幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_2); // 指定第二幅纹理
// 使用第二幅纹理

5、示例

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include <time.h>

#define WindowWidth   500
#define WindowHeight 500

/* 函数grab
 * 抓取窗口中的像素
 * 假设窗口宽度为WindowWidth,高度为WindowHeight
 */
#define BMP_Header_Length 54
void grab(void)
{
    FILE* pDummyFile;
    FILE* pWritingFile;
    GLubyte* pPixelData;
    GLubyte  BMP_Header[BMP_Header_Length];
    GLint    i, j;
    GLint    PixelDataLength;

    // 计算像素数据的实际长度
    i = WindowWidth * 3;   // 得到每一行的像素数据长度
    while (i % 4 != 0)      // 补充数据,直到i是的倍数
        ++i;               // 本来还有更快的算法,
                           // 但这里仅追求直观,对速度没有太高要求
    PixelDataLength = i * WindowHeight;

    // 分配内存和打开文件
    pPixelData = (GLubyte*)malloc(PixelDataLength);
    if (pPixelData == 0)
        exit(0);

    fopen_s(&pDummyFile, "dummy.bmp", "rb");
    if (pDummyFile == 0)
        exit(0);

    fopen_s(&pWritingFile, "grab.bmp", "wb");
    if (pWritingFile == 0)
        exit(0);

    // 读取像素
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
    glReadPixels(0, 0, WindowWidth, WindowHeight,
        GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

    // 把dummy.bmp的文件头复制为新文件的文件头
    fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
    fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
    fseek(pWritingFile, 0x0012, SEEK_SET);
    i = WindowWidth;
    j = WindowHeight;
    fwrite(&i, sizeof(i), 1, pWritingFile);
    fwrite(&j, sizeof(j), 1, pWritingFile);

    // 写入像素数据
    fseek(pWritingFile, 0, SEEK_END);
    fwrite(pPixelData, PixelDataLength, 1, pWritingFile);

    // 释放内存和关闭文件
    fclose(pDummyFile);
    fclose(pWritingFile);
    free(pPixelData);
}

/* 函数power_of_two
 * 检查一个整数是否为2的整数次方,如果是,返回1,否则返回0
 * 实际上只要查看其二进制位中有多少个,如果正好有1个,返回1,否则返回0
 * 在“查看其二进制位中有多少个”时使用了一个小技巧
 * 使用n &= (n-1)可以使得n中的减少一个(具体原理大家可以自己思考)
 */
int power_of_two(int n)
{
    if (n <= 0)
        return 0;
    return (n & (n - 1)) == 0;
}

/* 函数load_texture
 * 读取一个BMP文件作为纹理
 * 如果失败,返回0,如果成功,返回纹理编号
 */
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_s(&pFile, 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;
    }

    // 在旧版本的OpenGL中
    // 如果图象的宽度和高度不是的整数次方,则需要进行缩放
    // 这里并没有检查OpenGL版本,出于对版本兼容性的考虑,按旧版本处理
    // 另外,无论是旧版本还是新版本,
    // 当图象的宽度和高度超过当前OpenGL实现所支持的最大值时,也要进行缩放
    {
        GLint max;
        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
        if (!power_of_two(width)
            || !power_of_two(height)
            || width > max
            || height > max)
        {
            const GLint new_width = 256;
            const GLint new_height = 256; // 规定缩放后新的大小为边长的正方形
            GLint new_line_bytes, new_total_bytes;
            GLubyte* new_pixels = 0;

            // 计算每行需要的字节数和总字节数
            new_line_bytes = new_width * 3;
            while (new_line_bytes % 4 != 0)
                ++new_line_bytes;
            new_total_bytes = new_line_bytes * new_height;

            // 分配内存
            new_pixels = (GLubyte*)malloc(new_total_bytes);
            if (new_pixels == 0)
            {
                free(pixels);
                fclose(pFile);
                return 0;
            }

            // 进行像素缩放
            gluScaleImage(GL_RGB,
                width, height, GL_UNSIGNED_BYTE, pixels,
                new_width, new_height, GL_UNSIGNED_BYTE, new_pixels);

            // 释放原来的像素数据,把pixels指向新的像素数据,并重新设置width和height
            free(pixels);
            pixels = new_pixels;
            width = new_width;
            height = new_height;
        }
    }

    // 分配一个新的纹理编号
    glGenTextures(1, &texture_ID);
    if (texture_ID == 0)
    {
        free(pixels);
        fclose(pFile);
        return 0;
    }

    // 绑定新的纹理,载入纹理并设置纹理参数
    // 在绑定前,先获得原来绑定的纹理编号,以便在最后进行恢复
    glGetIntegerv(GL_TEXTURE_BINDING_2D, (GLint*)&last_texture_ID);
    glBindTexture(GL_TEXTURE_2D, texture_ID);
    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);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
        GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);//利用glTexImage2D函数可以载入一个二维的纹理
    glBindTexture(GL_TEXTURE_2D, last_texture_ID);//指定“当前所使用的纹理对象”

    // 之前为pixels分配的内存可在使用glTexImage2D以后释放
    // 因为此时像素数据已经被OpenGL另行保存了一份(可能被保存到专门的图形硬件中)
    free(pixels);
    return texture_ID;
}
/* 两个纹理对象的编号
 */
GLuint texGround;
GLuint texWall;

void display(void)
{
    // 清除屏幕
    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, texGround);
    glBegin(GL_QUADS);//QUADS指的是四边形,
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-8.0f, -8.0f, 0.0f);
    glTexCoord2f(0.0f, 5.0f); glVertex3f(-8.0f, 8.0f, 0.0f);
    glTexCoord2f(5.0f, 5.0f); glVertex3f(8.0f, 8.0f, 0.0f);
    glTexCoord2f(5.0f, 0.0f); glVertex3f(8.0f, -8.0f, 0.0f);
    glEnd();
    // 使用“墙”纹理绘制栅栏
    glBindTexture(GL_TEXTURE_2D, texWall);
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
    glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
    glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
    glEnd();

    // 旋转后再绘制一个
    glRotatef(-90, 0, 0, 1);
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
    glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
    glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
    glEnd();

    // 交换缓冲区,并保存像素数据到文件
    grab();//顺序
    glutSwapBuffers();
  
}

int main(int argc, char* argv[])
{
    // GLUT初始化
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(WindowWidth, WindowHeight);
    glutCreateWindow("第十一讲——纹理映射测试");
    glutDisplayFunc(&display);

    // 在这里做一些初始化
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_TEXTURE_2D);
    texGround = load_texture("ground.bmp");
    printf("%d", (int)texGround);
    texWall = load_texture("wall.bmp");
    printf("%d", (int)texWall);
    // 开始显示
    glutMainLoop();

    return 0;
}

image-20220112101831197image-20220112102044755

第十二讲

片断测试其实就是测试每一个像素,只有通过测试的像素才会被绘制,没有通过测试的像素则不进行绘制。OpenGL提供了多种测试操作,利用这些操作可以实现一些特殊的效果。

只要存在深度缓冲区,无论是否启用深度测试,OpenGL在像素被绘制时都会尝试将深度数据写入到缓冲区内,除非调用了glDepthMask(GL_FALSE)来禁止写入。这些深度数据除了用于常规的测试外,还可以有一些有趣的用途,比如绘制阴影等等。

除了深度测试,OpenGL还提供了剪裁测试、Alpha测试和模板测试。

裁剪测试

剪裁测试用于限制绘制区域。我们可以指定一个矩形的剪裁窗口,当启用剪裁测试后,只有在这个窗口之内的像素才能被绘制,其它像素则会被丢弃。换句话说,无论怎么绘制,剪裁窗口以外的像素将不会被修改。

可以通过下面的代码来启用或禁用剪裁测试:

glEnable(GL_SCISSOR_TEST); // 启用剪裁测试
glDisable(GL_SCISSOR_TEST); // 禁用剪裁测试

可以通过下面的代码来指定一个位置在(x, y),宽度为width,高度为height的剪裁窗口。

glScissor(x, y, width, height);

注意,OpenGL窗口坐标是以**左下角为(0, 0),右上角为(width, height)**的,这与Windows系统窗口有所不同。

还有一种方法可以保证像素只绘制到某一个特定的矩形区域内,这就是视口变换(在第五课第3节中有介绍)。但视口变换和剪裁测试是不同的。视口变换是将所有内容缩放到合适的大小后,放到一个矩形的区域内;而剪裁测试不会进行缩放,超出矩形范围的像素直接忽略掉。

Alpha测试

在前面的课程中,我们知道像素的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则通过”。Alpha测试的设置方式往往不只一种,可以根据个人喜好和实际情况需要进行选择。

可以通过下面的代码来启用或禁用Alpha测试:

glEnable(GL_ALPHA_TEST); // 启用Alpha测试
glDisable(GL_ALPHA_TEST); // 禁用Alpha测试

可以通过下面的代码来设置Alpha测试条件为“大于0.5则通过”:

glAlphaFunc(GL_GREATER, 0.5f);

该函数的第二个参数表示设定值,用于进行比较。第一个参数是比较方式,除了GL_LESS(小于则通过)外,还可以选择:

 GL_ALWAYS(始终通过),
 GL_NEVER(始终不通过),
 GL_LESS(小于则通过),
 GL_LEQUAL(小于等于则通过),
 GL_EQUAL(等于则通过),
 GL_GEQUAL(大于等于则通过),
 GL_NOTEQUAL(不等于则通过)。

Alpha测试可以实现的效果几乎都可以通过OpenGL混合功能来实现。那么为什么还需要一个Alpha测试呢?答案就是,这与性能相关。Alpha测试只要简单的比较大小就可以得到最终结果,而混合操作一般需要进行乘法运算,性能有所下降。另外,OpenGL测试的顺序是:剪裁测试、Alpha测试、模板测试、深度测试。如果某项测试不通过,则不会进行下一步,而只有所有测试都通过的情况下才会执行混合操作。因此,在使用Alpha测试的情况下,透明的像素就不需要经过模板测试和深度测试了;而如果使用混合操作,即使透明的像素也需要进行模板测试和深度测试,性能会有所下降。还有一点:对于那些“透明”的像素来说,如果使用Alpha测试,则“透明”的像素不会通过测试,因此像素的深度值不会被修改;而使用混合操作时,虽然像素的颜色没有被修改,但它的深度值则有可能被修改掉了。
因此,如果所有的像素都是“透明”或“不透明”,没有“半透明”时,应该尽量采用Alpha测试而不是采用混合操作。当需要绘制半透明像素时,才采用混合操作。

举例

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include <time.h>
#include "tex.h"
/* 将当前纹理BGR格式转换为BGRA格式
 * 纹理中像素的RGB值如果与指定rgb相差不超过absolute,则将Alpha设置为0.0,否则设置为1.0
 */
void texture_colorkey(GLubyte r, GLubyte g, GLubyte b, GLubyte absolute)
{
    GLint width, height;
    GLubyte* pixels = 0;

    // 获得纹理的大小信息
    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);

    // 分配空间并获得纹理像素
    pixels = (GLubyte*)malloc(width * height * 4);
    if (pixels == 0)
        return;
    glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);

    // 修改像素中的Alpha值
    // 其中pixels[i*4], pixels[i*4+1], pixels[i*4+2], pixels[i*4+3]
    //   分别表示第i个像素的蓝、绿、红、Alpha四种分量,0表示最小,255表示最大
    {
        GLint i;
        GLint count = width * height;
        for (i = 0; i < count; ++i)
        {
            if (abs(pixels[i * 4] - b) <= absolute
                && abs(pixels[i * 4 + 1] - g) <= absolute
                && abs(pixels[i * 4 + 2] - r) <= absolute)
                pixels[i * 4 + 3] = 0;
            else
                pixels[i * 4 + 3] = 255;
        }
    }

    // 将修改后的像素重新设置到纹理中,释放内存
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
        GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);
    free(pixels);
}
void display(void)
{
    static int initialized = 0;
    static GLuint texWindow = 0;
    static GLuint texPicture = 0;

    // 执行初始化操作,包括:读取相片,读取相框,将相框由BGR颜色转换为BGRA,启用二维纹理
    if (!initialized)
    {
        texPicture = load_texture("pic.bmp");
        texWindow = load_texture("window.bmp");
        glBindTexture(GL_TEXTURE_2D, texWindow);
        texture_colorkey(255, 255, 255, 10);

        glEnable(GL_TEXTURE_2D);

        initialized = 1;
    }

    // 清除屏幕
    glClear(GL_COLOR_BUFFER_BIT);

    // 绘制相片,此时不需要进行Alpha测试,所有的像素都进行绘制
    glBindTexture(GL_TEXTURE_2D, texPicture);
    glDisable(GL_ALPHA_TEST);
    glBegin(GL_QUADS);
    glTexCoord2f(0, 0);     glVertex2f(-1.0f, -1.0f);
    glTexCoord2f(0, 1);     glVertex2f(-1.0f, 1.0f);
    glTexCoord2f(1, 1);     glVertex2f(1.0f, 1.0f);
    glTexCoord2f(1, 0);     glVertex2f(1.0f, -1.0f);
    glEnd();

    // 绘制相框,此时进行Alpha测试,只绘制不透明部分的像素
    glBindTexture(GL_TEXTURE_2D, texWindow);
    glEnable(GL_ALPHA_TEST);
    glAlphaFunc(GL_GREATER, 0.5f);
    glBegin(GL_QUADS);
    glTexCoord2f(0, 0);     glVertex2f(-1.0f, -1.0f);
    glTexCoord2f(0, 1);     glVertex2f(-1.0f, 1.0f);
    glTexCoord2f(1, 1);     glVertex2f(1.0f, 1.0f);
    glTexCoord2f(1, 0);     glVertex2f(1.0f, -1.0f);
    glEnd();

    // 交换缓冲
    glutSwapBuffers();
}

int main(int argc, char* argv[])
{
    // GLUT初始化
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(500,500);
    glutCreateWindow("第十二讲——alpha测试");
    glutDisplayFunc(&display);

    glutMainLoop();

    return 0;
}

image-20220112104424788

模板测试

模板测试需要一个模板缓冲区,这个缓冲区是在初始化OpenGL时指定的。如果使用GLUT工具包,可以在调用glutInitDisplayMode函数时在参数中加上GLUT_STENCIL,例如:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_STENCIL);
glEnable(GL_STENCIL_TEST); // 启用模板测试
glDisable(GL_STENCIL_TEST); // 禁用模板测试

OpenGL在模板缓冲区中为每个像素保存了一个“模板值”,当像素需要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的通过测试,不符合条件的则被丢弃,不进行绘制。

条件的设置与Alpha测试中的条件设置相似。但注意Alpha测试中是用浮点数来进行比较,而模板测试则是用整数来进行比较。比较也有八种情况:始终通过、始终不通过、大于则通过、小于则通过、大于等于则通过、小于等于则通过、等于则通过、不等于则通过。

OpenGL在模板缓冲区中为每个像素保存了一个“模板值”,当像素需要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的通过测试,不符合条件的则被丢弃,不进行绘制。
条件的设置与Alpha测试中的条件设置相似。但注意Alpha测试中是用浮点数来进行比较,而模板测试则是用整数来进行比较。比较也有八种情况:始终通过、始终不通过、大于则通过、小于则通过、大于等于则通过、小于等于则通过、等于则通过、不等于则通过。

glStencilFunc(GL_LESS, 3, mask);

这段代码设置模板测试的条件为:“小于3则通过”。glStencilFunc的前两个参数意义与glAlphaFunc的两个参数类似,第三个参数的意义为:如果进行比较,则只比较mask中二进制为1的位。例如,某个像素模板值为5(二进制101),而mask的二进制值为00000011,因为只比较最后两位,5的最后两位为01,其实是小于3的,因此会通过测试。

如何设置像素的“模板值”呢?glClear函数可以将所有像素的模板值复位。代码如下:

glClear(GL_STENCIL_BUFFER_BIT);
 可以同时复位颜色值和模板值:
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
 正如可以使用glClearColor函数来指定清空屏幕后的颜色那样,也可以使用glClearStencil函数来指定复位后的“模板值”。
 每个像素的“模板值”会根据模板测试的结果和深度测试的结果而进行改变。
glStencilOp(fail, zfail, zpass);

该函数指定了三种情况下“模板值”该如何变化。第一个参数表示模板测试未通过时该如何变化;第二个参数表示模板测试通过,但深度测试未通过时该如何变化;第三个参数表示模板测试和深度测试均通过时该如何变化。如果没有起用模板测试,则认为模板测试总是通过;如果没有启用深度测试,则认为深度测试总是通过)
变化可以是:

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

在新版本的OpenGL中,允许为多边形的正面和背面使用不同的模板测试条件和模板值改变方式,于是就有了glStencilFuncSeparate函数和glStencilOpSeparate函数。这两个函数分别与glStencilFunc和glStencilOp类似,只在最前面多了一个参数face,用于指定当前设置的是哪个面。可以选择GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。

注意:模板缓冲区与深度缓冲区有一点不同。无论是否启用深度测试,当有像素被绘制时,总会重新设置该像素的深度值(除非设置glDepthMask(GL_FALSE);)。而模板测试如果不启用,则像素的模板值会保持不变,只有启用模板测试时才有可能修改像素的模板值。

尽量不要使用glStencilFuncSeparate和glStencilOpSeparate函数。

从前面所讲可以知道,使用剪裁测试可以把绘制区域限制在一个矩形的区域内。但如果需要把绘制区域限制在一个不规则的区域内,则需要使用模板测试。

例如:绘制一个湖泊,以及周围的树木,然后绘制树木在湖泊中的倒影。为了保证倒影被正确的限制在湖泊表面,可以使用模板测试。具体的步骤如下:

(1) 关闭模板测试,绘制地面和树木。
(2) 开启模板测试,使用glClear设置所有像素的模板值为0。
(3) 设置glStencilFunc(GL_ALWAYS, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);绘制湖泊水面。这样一来,湖泊水面的像素的“模板值”为1,而其它地方像素的“模板值”为0。
(4) 设置glStencilFunc(GL_EQUAL, 1, 1); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);绘制倒影。这样一来,只有“模板值”为1的像素才会被绘制,因此只有“水面”的像素才有可能被倒影的像素替换,而其它像素则保持不变。

举例

空间中有一个球体,一个平面镜。我们站在某个特殊的观察点,可以看到球体在平面镜中的镜像,并且镜像处于平面镜的边缘,有一部分因为平面镜大小的限制,而无法显示出来。整个场景的效果如下图:

http://blog.programfan.com/upfile/200710/20071007111019.jpg

假设平面镜所在的平面正好是X轴和Y轴所确定的平面,则球体和它在平面镜中的镜像是关于这个平面对称的。我们用一个draw_sphere函数来绘制球体,先调用该函数以绘制球体本身,然后调用glScalef(1.0f, 1.0f, -1.0f); 再调用draw_sphere函数,就可以绘制球体的镜像。
另外需要注意的地方就是:因为是绘制三维的场景,我们**开启了深度测试。**但是站在观察者的位置,球体的镜像其实是在平面镜的“背后”,也就是说,如果按照常规的方式绘制,平面镜会把镜像覆盖掉,这不是我们想要的效果。解决办法就是:设置深度缓冲区为只读,绘制平面镜,然后设置深度缓冲区为可写的状态,绘制平面镜“背后”的镜像。

如果关闭深度测试,而有的“背面”多边形又比“正面”多边形先绘制,就会造成球体的背面反而把正面挡住了,这不是我们想要的效果。为了确保正面可以挡住背面,应该开启深度测试。

#include<stdio.h>
#include<stdlib.h>
#include <math.h>
#include<glut.h>
#include<glaux.h>
#include <time.h>
#include "tex.h"
void draw_sphere()
{
    // 设置光源
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    {
        GLfloat
            pos[] = { 5.0f, 5.0f, 0.0f, 1.0f },
            ambient[] = { 0.0f, 1.0f, 1.0f, 1.0f };
        glLightfv(GL_LIGHT0, GL_POSITION, pos);
        glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
    }

    // 绘制一个球体
    glColor3f(1, 0, 0);
    glPushMatrix();
    glTranslatef(0, 0, 2);
    glutSolidSphere(0.5, 20, 20);
    glPopMatrix();
}

void display(void)
{
    // 清除屏幕
    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);

    glEnable(GL_DEPTH_TEST);

    // 绘制球体
    glDisable(GL_STENCIL_TEST);
    draw_sphere();

    // 绘制一个平面镜。在绘制的同时注意设置模板缓冲。
    // 另外,为了保证平面镜之后的镜像能够正确绘制,在绘制平面镜时需要将深度缓冲区设置为只读的。
    // 在绘制时暂时关闭光照效果
    glClearStencil(0);
    glClear(GL_STENCIL_BUFFER_BIT);
    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();
}
int main(int argc, char* argv[])
{
    // GLUT初始化
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(500, 500);
    glutCreateWindow("第十二讲——模板测试");
    glutDisplayFunc(&display);

    glutMainLoop();

    return 0;
}

image-20220112105744605

深度测试

深度测试需要深度缓冲区,跟模板测试需要模板缓冲区是类似的。如果使用GLUT工具包,可以在调用glutInitDisplayMode函数时在参数中加上GLUT_DEPTH,这样来明确指定要求使用深度缓冲区。

深度测试和模板测试的实现原理很类似,都是在一个缓冲区保存像素的某个值,当需要进行测试时,将保存的值与另一个值进行比较,以确定是否通过测试。两者的区别在于:模板测试是设定一个值,在测试时用这个设定值与像素的“模板值”进行比较,而深度测试是根据顶点的空间坐标计算出深度,用这个深度与像素的“深度值”进行比较。也就是说,模板测试需要指定一个值作为比较参考,而深度测试中,这个比较用的参考值是OpenGL根据空间坐标自动计算的。

通过glEnable/glDisable函数可以启用或禁用深度测试。

 glEnable(GL_DEPTH_TEST); // 启用深度测试 glDisable(GL_DEPTH_TEST); // 禁用深度测试

至于通过测试的条件,同样有八种,与Alpha测试中的条件设置相同。条件设置是通过glDepthFunc函数完成的,默认值是GL_LESS。
glDepthFunc(GL_LESS);

与模板测试相比,深度测试的应用要频繁得多。几乎所有的三维场景绘制都使用了深度测试。正因为这样,几乎所有的OpenGL实现都对深度测试提供了硬件支持,所以虽然两者的实现原理类似,但深度测试很可能会比模板测试快得多。当然了,两种测试在应用上很少有交集,一般不会出现使用一种测试去代替另一种测试的情况。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容