说到OpenGL动画绘制,我们首先想到的就是读取多张图片并加载为纹理,然后再绘制的时候不断切换纹理图片即可。作为练习,这是可以的,但是,当我们的动画非常丰富的时候,我们就要涉及到大量的图片读取和纹理加载,这是非常低效并且麻烦的。
更为常用的一种方法,就是把所有帧的图片存到一张图里,制作成精灵表,然后在绘制的时候,根据行列索引快速读取到我们需要的精灵图片,这就是我们所称的精灵表。提到精灵表,最经典的就是下面这张爆炸精灵了:
(图片来自网络)
利用索引找到精灵所在位置是一件非常容易的事情,它涉及到的运算量非常小。
在这里,我们主要在void drawRect(GLuint texture,int i,int j)这个函数中做了文章,传入的i,j就是精灵的行列索引(从0开始),我们通过修改dir这个参数来指定纹理映射方式,如果想要完整地映射一张图片到矩形上,dir的取值为
可以看出来它是把纹理图片左下角坐标当作(0,0),右上角当作(1,1),按逆时针顺序读取的。
我们设x为每个精灵的长,y为每个精灵的宽(假设整张图片长宽均为1,对于爆炸精灵而言,长为1/6,宽为1/5),i,j是精灵的行列索引,我们只要对dir参数稍加修改:
以下是我用最近的飞船消散的精灵表做的一个样例:
制作精灵表时,我们可以用matlab对每个帧图像进行拼合,这里,num是指总共的帧数,col是每行帧数。
所有图片命名为1.jpg ~ num.jpg,为了防止图片太大,我对图片进行了隔三行三列的下采样,具体可以根据情况自己修改。
function genSpriteTable( num,col )
for i = 1:num
a = imread([num2str(i),'.jpg']);
m = floor((i-1)/col+1);
n = floor(mod(i,col));
if(n==0)
n=col;
end
if (i==1)
[h,w,~] = size(a);
height = ceil(h/3);
width = ceil(w/3);
f = zeros(height*ceil(num/col),width*col,3);
end
g = a(1:3:h,1:3:w,:);
f((m-1)*height + 1:m*height,(n-1)*width + 1:n*width,:) = g(:,:,:);
end
f = uint8(f);
imwrite(f,'b.bmp','bmp');
test.h
#pragma once
#define GLUT_DISABLE_ATEXIT_HACK
#include "GL/GLUT.H"
void loadTex(int i, char *filename, GLuint* texture);
texture.cpp(加载纹理用,可以当作模板代码)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<windows.h>
#include"test.h"
#define BITMAP_ID 0x4D42
//读纹理图片
static unsigned char *LoadBitmapFile(char *filename, BITMAPINFOHEADER *bitmapInfoHeader)
{
FILE *filePtr; // 文件指针
BITMAPFILEHEADER bitmapFileHeader; // bitmap文件头
unsigned char *bitmapImage; // bitmap图像数据
int imageIdx = 0; // 图像位置索引
unsigned char tempRGB; // 交换变量
// 以“二进制+读”模式打开文件filename
filePtr = fopen(filename, "rb");
if (filePtr == NULL) {
printf("file not open\n");
return NULL;
}
// 读入bitmap文件图
fread(&bitmapFileHeader, sizeof(BITMAPFILEHEADER), 1, filePtr);
// 验证是否为bitmap文件
if (bitmapFileHeader.bfType != BITMAP_ID) {
fprintf(stderr, "Error in LoadBitmapFile: the file is not a bitmap file\n");
return NULL;
}
// 读入bitmap信息头
fread(bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, filePtr);
// 将文件指针移至bitmap数据
fseek(filePtr, bitmapFileHeader.bfOffBits, SEEK_SET);
// 为装载图像数据创建足够的内存
bitmapImage = new unsigned char[bitmapInfoHeader->biSizeImage];
// 验证内存是否创建成功
if (!bitmapImage) {
fprintf(stderr, "Error in LoadBitmapFile: memory error\n");
return NULL;
}
// 读入bitmap图像数据
fread(bitmapImage, 1, bitmapInfoHeader->biSizeImage, filePtr);
// 确认读入成功
if (bitmapImage == NULL) {
fprintf(stderr, "Error in LoadBitmapFile: memory error\n");
return NULL;
}
//由于bitmap中保存的格式是BGR,下面交换R和B的值,得到RGB格式
for (imageIdx = 0; imageIdx < bitmapInfoHeader->biSizeImage; imageIdx += 3) {
tempRGB = bitmapImage[imageIdx];
bitmapImage[imageIdx] = bitmapImage[imageIdx + 2];
bitmapImage[imageIdx + 2] = tempRGB;
}
// 关闭bitmap图像文件
fclose(filePtr);
return bitmapImage;
}
//加载纹理的函数
void loadTex(int i, char *filename, GLuint* texture)
{
BITMAPINFOHEADER bitmapInfoHeader; // bitmap信息头
unsigned char* bitmapData; // 纹理数据
bitmapData = LoadBitmapFile(filename, &bitmapInfoHeader);
glBindTexture(GL_TEXTURE_2D, texture[i]);
// 指定当前纹理的放大/缩小过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D,
0, //mipmap层次(通常为,表示最上层)
GL_RGB, //我们希望该纹理有红、绿、蓝数据
bitmapInfoHeader.biWidth, //纹理宽带,必须是n,若有边框+2
bitmapInfoHeader.biHeight, //纹理高度,必须是n,若有边框+2
0, //边框(0=无边框, 1=有边框)
GL_RGB, //bitmap数据的格式
GL_UNSIGNED_BYTE, //每个颜色数据的类型
bitmapData); //bitmap数据指针
}
main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include<time.h>
#include <stdlib.h>
#include"test.h"
//纹理缓冲区
GLuint texture[1];
//视区
float whRatio;
int wHeight = 0;
int wWidth = 0;
//视点
float center[] = { 0, 0, 0 };
float eye[] = { 0, 0, 5 };
int count = 0;
int times = 0;
//帧动画参数
int num = 33;//一共多少帧
int col = 3;//一行有多少帧
void drawRect(GLuint texture,int i,int j)
{
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture); //选择纹理texture[status]
const GLfloat x1 = -0.5, x2 = 0.5;
const GLfloat y1 = -0.5, y2 = 0.5;
const GLfloat x = 1.0 / col, y = 1.0 / (num/col);
const GLfloat point[4][2] = { { x1,y1 },{ x2,y1 },{ x2,y2 },{ x1,y2 } };
const GLfloat dir[4][2] = { { j*x,1 - (i + 1)*y },{ (j + 1)*x,1 - (i + 1)*y },{ (j + 1)*x ,1 - i*y },{ j*x,1 - i*y } };
glBegin(GL_QUADS);
for (int k = 0; k < 4; k++) {
glTexCoord2fv(dir[k]);
glVertex2fv(point[k]);
}
glEnd();
glDisable(GL_TEXTURE_2D);
}
void drawScene()
{
count++;
if (count >= 100) {
count = 0;
times++;
if (times >= num)times = 0;
}
glPushMatrix();
glScalef(1.0f, 1.0f, 1.0f);
drawRect(texture[0],times/col,times%col);
glPopMatrix();
}
void updateView(int height, int width)
{
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);//设置矩阵模式为投影
glLoadIdentity(); //初始化矩阵为单位矩阵
whRatio = (GLfloat)width / (GLfloat)height; //设置显示比例
glOrtho(-3, 3, -3, 3, -100, 100); //正投影
glMatrixMode(GL_MODELVIEW); //设置矩阵模式为模型
}
void reshape(int width, int height)
{
if (height == 0) //如果高度为0
{
height = 1; //让高度为1(避免出现分母为0的现象)
}
wHeight = height;
wWidth = width;
updateView(wHeight, wWidth); //更新视角
}
void idle()
{
glutPostRedisplay();
}
void init()
{
srand(unsigned(time(NULL)));
glEnable(GL_DEPTH_TEST);//开启深度测试
glEnable(GL_LIGHTING); //开启光照模式
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glGenTextures(1, texture);
loadTex(0, "b.bmp", texture);
}
void redraw()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//清除颜色和深度缓存
glMatrixMode(GL_MODELVIEW);
glLoadIdentity(); //初始化矩阵为单位矩阵
gluLookAt(eye[0], eye[1], eye[2], center[0], center[1], center[2], 0, 1, 0); // 场景(0,0,0)的视点中心 (0,5,50),Y轴向上
glPolygonMode(GL_FRONT, GL_FILL);
glFrontFace(GL_CCW);
glEnable(GL_CULL_FACE);
// 启用光照计算
glEnable(GL_LIGHTING);
// 指定环境光强度(RGBA)
GLfloat ambientLight[] = { 2.0f, 2.0f, 2.0f, 1.0f };
// 设置光照模型,将ambientLight所指定的RGBA强度值应用到环境光
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambientLight);
// 启用颜色追踪
glEnable(GL_COLOR_MATERIAL);
// 设置多边形正面的环境光和散射光材料属性,追踪glColor
glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
drawScene();//绘制场景
glutSwapBuffers();//交换缓冲区
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);//对glut的初始化
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
//初始化显示模式:RGB颜色模型,深度测试,双缓冲
glutInitWindowSize(500, 500);//设置窗口大小
int windowHandle = glutCreateWindow("Simple GLUT App");//设置窗口标题
glutDisplayFunc(redraw); //注册绘制回调函数
glutReshapeFunc(reshape); //注册重绘回调函数
glutIdleFunc(idle);//注册全局回调函数:空闲时调用
init();
glutMainLoop(); // glut事件处理循环
return 0;
}