本篇内容包括:freetype 汉字显示实现,GLSL使用方法,包括片元着色器使用技巧,纹理混合,透明显示方法,纹理采样方法等等,顶点着色器,常用变换技巧,图片放大,图片缩小,图片旋转等等
接上篇:
一、如何在SDL2中加入汉字
SDL没有文字显示功能,要显示文字就必须要将文字图形化,先将文字转化为图形,再通过图形显示功能将其显示出来,OpenGL也是如此。生成文字图形有很多种方法,SDL本身也提供了专用模块,先将文字转化为SDL图形表面,再进行显示。这就是SDL-ttf,该模块实际调用了另外的一套共享软件freetype,OpenGL也有相类似的应用,我研究了一下, 不知道什么原因,sdl-ttf好像从SDL官网中删除了,按照官网的指引从github上下的源程序也不能用,无奈之下,只好用freetype,好在freetype有例子可以参考,网上相关资料也很多,freetype本身没有显示功能,只能先生成字体缓存结构,导入OpenGL或SDL纹理,再通过纹理将文字渲染到帧缓存中,再通过帧缓存将文字显示到屏幕上。其中将字体缓存加入纹理的过程是相当慢的,所以在程序处理过程中,一般会采取预先将要显示的字体写入纹理进行缓存,再进入渲染循环,需要时使用纹理进行显示。因为纹理使用显存和GPU并发处理,处理速率相当快,我测试过,一个包括近百步骤的渲染,也只不过耗时几毫秒。而同样的过程,如果文字实时处理,用时将是之前的十几倍。
也可以预先将要显示的文字做成一个BMP文件,局部就像这样:
使用时预先将字形图像载入生成纹理,如果该文件为1024*1024大小,就可以存4096个32*32的汉字和其他符号或16*16的汉字16384个,足够日常显示之用。这样做的好处是不需要专门的文字处理过程也能完美的显示文字。当然,这需要事先生成字典。
不过在需要在人机交互过程中,临时输入显示没有的文字时,也可以将输入的字典中没有的字插入字典并逐个复制到纹理中,因为人机交互时,不会要求毫秒级的显示速度。
以下是显示文字的页面
以下是有关代码:
map <WCHAR, int > FontMap;
const int HZSIZE = 32;
const int ASCIISIZE = 32;
const int TextBufSize = 32;
int GL_Render::FontInit(string path, INT isBIG5)
{
//功能初始化字模纹理
//输入,游戏文件所在目录
FT_Library library;
if (FT_Init_FreeType(&library))
return 1;
FT_Face face;
if (FT_New_Face(library, "c:/windows/fonts/simfang.ttf", 0, &face))
{
FT_Done_FreeType(library);
return 1;
}
//以下初始化字库纹理
VOID* gp_TextBuf = NULL;
FILE* fp;
string m_path = path + "wor16.asc";
if ((fopen_s(&fp, m_path.c_str(), "rb")))
return -1;
fseek(fp, 0, SEEK_END);
int nChar = ftell(fp);
nChar /= 2;
fseek(fp, 0, SEEK_SET);
LPWORD charbuf = new WORD[(INT64)nChar + 10];
if (charbuf == nullptr)
exit(1);
fread(charbuf, sizeof(WORD), nChar, fp);
//建立纹理,缓存存放字模
if (TextBufSize == 8)
{
gp_TextBuf = new char[(size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE];
memset(gp_TextBuf, 0, (size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE * sizeof(GLubyte));
}
else
{
gp_TextBuf = new INT32*[(size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE];
memset(gp_TextBuf, 0, (size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE * sizeof(INT32));
}
FT_Set_Pixel_Sizes(face, 0, HZSIZE);
for (int n = 0; n < nChar; n++)
{
char s[3];
WORD w = charbuf[n];
WCHAR wstr[2];
wchar_t rewstr[2];
s[2] = 0;
s[0] = w & 0xff;
s[1] = w >> 8;
if (isBIG5)
{
MultiByteToWideChar(950, 0, s, -1, wstr, 1);
//转成简体字
WORD wLCID = MAKELCID(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED), SORT_CHINESE_BIG5);
LCMapString(wLCID, LCMAP_SIMPLIFIED_CHINESE, wstr, 1, rewstr, 1);
}
else
MultiByteToWideChar(936, 0, s, -1, rewstr, 1);
FontMap.insert(pair<WCHAR, INT>(rewstr[0], n));
rewstr[1] = 0;
size_t cx = (n & 63) * HZSIZE;
size_t cy = (n >> 6) * HZSIZE;
FT_Load_Char(face, rewstr[0], FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL);
FT_GlyphSlot slot = face->glyph;
int xOff = slot->bitmap_left;
int yOff = (face->size->metrics.ascender >> 6) - slot->bitmap_top;
if (TextBufSize == 8)
ConvertSlotToBuf(&slot->bitmap, (GLubyte*)gp_TextBuf, HZSIZE * 64,
cx + xOff, cy + (yOff));
else
ConvertSlotToBuf32(&slot->bitmap, (INT32*)gp_TextBuf, HZSIZE * 64,
cx + xOff, cy + (yOff));
}
//复制到纹理
SDL_Surface* fontsurf;
if (TextBufSize == 8)
fontsurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, HZSIZE * 64,
(nChar / 64 + 1) * HZSIZE, 8,
HZSIZE * 64, SDL_PIXELFORMAT_RGB332);
else
fontsurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, HZSIZE * 64,
(nChar / 64 + 1) * HZSIZE, 32,
HZSIZE * 64 * sizeof(INT32), SDL_PIXELFORMAT_RGBA8888);
//保存字符图
//SDL_SaveBMP(fontsurf, "fontt.bmp");
gpFontTexture = SDL_CreateTextureFromSurface(gpSdlRender,fontsurf);
SDL_FreeSurface(fontsurf);
delete[]gp_TextBuf;
delete [] charbuf;
if (TextBufSize == 8)
{
gp_TextBuf = new char[ASCIISIZE * 60 * ASCIISIZE];
memset(gp_TextBuf, 0, ASCIISIZE * 60 * ASCIISIZE);
}
else
{
gp_TextBuf = new INT32[ASCIISIZE * 60 * ASCIISIZE];
memset(gp_TextBuf, 0, ASCIISIZE * 60 * ASCIISIZE * sizeof(INT32));
}
FT_Set_Pixel_Sizes(face, 0, ASCIISIZE);
//打印ascII字符
for (int n = 0; n < 96; n++)
{
WCHAR s[2];
s[1] = 0;
s[0] = n + 32;//从空格开始到126
size_t cx = n * ASCIISIZE / 2;
//FT_Load_Char(face, s[0], FT_LOAD_RENDER | FT_LOAD_MONOCHROME);
//FT_Load_Char(face, s[0], FT_LOAD_RENDER|FT_LOAD_TARGET_NORMAL| FT_LOAD_FORCE_AUTOHINT);
//FT_Load_Char(face, s[0], FT_LOAD_RENDER| FT_LOAD_TARGET_MONO);
//FT_Load_Char(face, s[0], FT_LOAD_RENDER| FT_LOAD_DEFAULT);
FT_Load_Char(face, s[0], FT_LOAD_RENDER);
FT_GlyphSlot slot = face->glyph;
int xOff = slot->bitmap_left;
int yOff = (face->size->metrics.ascender >> 6) - slot->bitmap_top;
ConvertSlotToBuf32(&slot->bitmap, (INT32*)gp_TextBuf, ASCIISIZE * 48, cx + xOff, (yOff));
}
SDL_Surface* asciiSurf;
if(TextBufSize == 8)
asciiSurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, ASCIISIZE * 48, ASCIISIZE,
8, ASCIISIZE * 48, SDL_PIXELFORMAT_RGB332);
else
asciiSurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, ASCIISIZE * 48, ASCIISIZE,
8, ASCIISIZE * 48 * sizeof(INT32), SDL_PIXELFORMAT_RGBA8888);
//保存字符图
//SDL_SaveBMP(asciiSurf, "ascII.bmp");
gpASCIITexture = SDL_CreateTextureFromSurface(gpSdlRender, asciiSurf);
SDL_FreeSurface(asciiSurf);
//清理
delete[] gp_TextBuf;
fclose(fp);
//
FT_Done_Face(face);
FT_Done_FreeType(library);
return 0;
}
//目标为8byte
//输入:源字形结构,目标缓存,目标一行字节,目标行位移,目标列位移
//返回:0成功,其他失败
int GL_Render::ConvertSlotToBuf(FT_Bitmap* srcSlot, GLubyte* dstBuf,int dstPitch, int xPos, int yPos)
{
if (srcSlot == NULL || dstBuf == NULL)
return -1;
if (xPos + srcSlot->width > dstPitch)
return -1;
for (int y = 0; y < srcSlot->rows; y++)
{
GLubyte* dstP = dstBuf + (static_cast<__int64>(yPos) + y) * dstPitch + xPos;
GLubyte* srcP = srcSlot->buffer + static_cast<__int64>(y) * srcSlot->pitch;
for (int x = 0; x < srcSlot->width; x++)
{
if (srcSlot->pixel_mode == 1)
{
//源为1byte
dstP[x] = ((1 << (7 - (x & 7))) & srcP[(x >> 3)]) ? 255 : 0;
}
else
{
//源为8byte
dstP[x] = srcP[x];
//dstP[x] = srcP[x] ? 255 : 0;
}
}
}
return 0;
}
//目标为32位缓存
//输入:源字形结构,目标缓存,目标一行字节,目标行位移,目标列位移
//返回:0成功,其他失败
int GL_Render::ConvertSlotToBuf32(FT_Bitmap* srcSlot, INT32* dstBuf, int dstPitch, int xPos, int yPos)
{
if (srcSlot == NULL || dstBuf == NULL)
return -1;
if (xPos + srcSlot->width > dstPitch)
return -1;
for (int y = 0; y < srcSlot->rows; y++)
{
INT32 * dstP = dstBuf + (static_cast<__int64>(yPos) + y) * dstPitch + xPos;
GLubyte* srcP = srcSlot->buffer + static_cast<__int64>(y) * srcSlot->pitch;
for (int x = 0; x < srcSlot->width; x++)
{
if (srcSlot->pixel_mode == 1)
{
//源为1byte
dstP[x] = ((1 << (7 - (x & 7))) & srcP[(x >> 3)]) ? 0xffffffff : 0;
}
else
{
//源为8byte
dstP[x] = srcP[x] ? 0xffffff | srcP[x] << 24 : 0;
}
}
}
return 0;
}
SIZE GL_Render::DrawWideText(LPCWSTR lpszTextR, _POS pos, SDL_Color bColor, BOOL fShadow, BOOL fUpdate, int size)
{
//功能:在屏幕上打印宽字符串
//返回:占用屏幕大小
int cx = POS_X(pos) * PictureRatio;
int cy = POS_Y(pos) * PictureRatio;
int len = lstrlenW(lpszTextR);
size *= PictureRatio;
SIZE wsz = { 0,size };
SIZE zsz = { size,size };
SDL_Rect srcRect;
SDL_Rect dstRect;
SDL_Texture* srcTexture;
for (int n = 0; n < len; n++)
{
if (isascii(lpszTextR[n]))
{
if (lpszTextR[n] < 32)
continue;
zsz = { size / 2,size };
srcTexture = gpASCIITexture;
srcRect = { (lpszTextR[n] - 32) * ASCIISIZE / 2,0,ASCIISIZE / 2,ASCIISIZE };
dstRect = { cx,cy,zsz.cx,zsz.cy };
cx += zsz.cx;
wsz.cx += zsz.cx;
}
else
{
zsz = { size,size };
srcTexture = gpFontTexture;
WCHAR s = lpszTextR[n];
map<WCHAR, INT> ::iterator iter;
iter = FontMap.find(s);
if (iter == FontMap.end())
//no find
continue;
int off = iter->second;
srcRect = { (off & 63) * HZSIZE,(off >> 6) * HZSIZE,HZSIZE,HZSIZE };
dstRect = { cx,cy,zsz.cx,zsz.cy };
cx += zsz.cx;
wsz.cx += zsz.cx;
}
if (fShadow)
{
SDL_Color mColor = { 2,2,2,0 };
SDL_Rect mdstRect = dstRect;
mdstRect.x++;
RenderBlendCopy(gpRenderTexture, srcTexture, nullptr, 255, 6,
&mColor, &mdstRect, &srcRect);
mdstRect.y++;
RenderBlendCopy(gpRenderTexture, srcTexture, nullptr, 255, 6,
&mColor, &mdstRect, &srcRect);
}
if (bColor.r == 0 && bColor.g == 0 && bColor.b == 0)
bColor = { 4,4,4,128 };
RenderBlendCopy(gpRenderTexture, srcTexture, nullptr, 255, 6,
&bColor, &dstRect, &srcRect);
}
if (fUpdate)
{
RenderPresent(gpRenderTexture);
}
return wsz;
}
二、GLSL应用
先说片元着色器,也称为片段着色器,片元着色器所执行的是渲染过程中的最后阶段,目标是确定最终计算体现的每个具体像素的颜色,这个过程是并发进行的,也就是说GPU一次要并行计算很多像素,并且,计算像素的顺序是不确定的,所以整个计算过程中,各个原始纹理是只读的。OpenGL最新版本虽然可以支持纹理的修改,但这是有条件的。对纹理的读取(采样)点可能包括多个像素,这决定于采样策略。也可能不是原始的纹理,而是多级渐远而形成的次生纹理,在缺省的情况下,原始纹理像素和目标像素之间不存在一一对应的关系。(显卡本身是应该支持原始纹理读取的,在较新的OpenGL版本中也已经支持原始纹理像素的读取。因为除了图像处理,显卡还支持并行计算,并且总的计算能力是CUP的很多倍。)之所以这样处理,也是实际需求,因为如果对原始纹理一个像素一个像素的读取和处理,给程序的开发带来很大工作量,所以OpenGL对坐标进行了归一化预处理。GLSL着色器程序,每次只处理一个目标像素点,每个目标像素点的颜色,不会影响其他目标像素点颜色。
片元着色器可以对各种信息来源的数据,包括外部输入的,顶点着色器传入,纹理的对应位置信息,以及相关颜色信息,深度信息,这意味着片元着色器可以做许多事,实现很多功能。简单的如纹理混合,颜色过滤,透明,混合、颜色映射反转等等,这些功能在片元着色器中,往往只用几条简单命令就可以实现,这些简单操作都有一个共同的特征,就是只对缺省的固定位置进行纹理采样,这些操作虽然可以用固定管线方式实现,但GLSL实现起来更加简便和易于理解,这也是OpenGL舍弃固定管线操作的原因,除了以上特点之外,GLSL可以对纹理的任意位置进行采样,并进行计算、着色,从而实现特定的渲染目标。如实现屏幕水平分屏操作,
//正常纹理取样操作
//v_tex 纹理 sTexCoord 顶点着色器传递过来的 纹理UV坐标 outColor 输出颜色
outColor = texture(v_tex,sTexCoord);
//水平二分屏
outColor = texture(v_tex,vec2(fract(sTexCoord.x * 2.0) ,sTexCoord.y));
//再以0.4,0.6 为圆心 0.2为半径,0.02宽度 画一个红色的圆
float len = distance(sTexCoord, vec2(0.4,0.6));\n\
if(len <= 0.22 && len >=0.2)\n\
outColor = vec4(1.0,0.0,0.0,1.0);\n\
结果如下:
当然,这个圆有点扁,这是界面的长和宽比例问题,解决起来不难,图片的长宽比为1.6
UV坐标为x,y 距离就是 pow( pow(x - 0.4,2 ) * 1.6+ pow((x - 0.6) ,2),0.5);
当然,也可以将红圈做成半透明的
float len = pow( pow((sTexCoord.x-0.4)*1.6 ,2 ) + pow(sTexCoord.y-0.6,2),0.5);\n\
if(len <= 0.22 && len >=0.2)\n\
outColor = outColor * 0.5 + vec4(1.0,0.0,0.0,1.0)* 0.5;\n\
有关图片透明,可以有很多方法,
一是使用两个纹理,在片元着色器中采样混合,这种方法有一定局限性,除全局混合外,局部混合的实现很麻烦,因为无论纹理大小,OpenGL着色器在处理时要对纹理坐标归一化,如采用采样计算的方法也可以实现局部混合透明化,但必须输入参数,而向GLSL输入数据是一件很麻烦的事,采样点的计算也很麻烦,尽管GLSL着色器程序长度几乎没有限制,但着色器的编制调试还是比c++困难得多,尤其是单步调试和排错手段缺失,关键字补全和检查也没有其他编程语言方便,中间结果检查还须借助专用工具使用CPU模拟实现,而且这类工具通用性又极差,单从编制效率来说,能从外部实现的,还是应该从外部实现。
二是外部实现,实现的方法也很简单,先在帧缓存中生成背景,也就是先渲染背景图片,之后再渲染前景图片,要使前景图片透明,必须满足两个条件,一个是要开启OpenGL的Alpha透明,一个是要设置前景的Alpha通道值。如果前景图片自身格式是RGBA或其他包含Alpha通道的颜色格式,并且已经设置好了,可以直接使用,否则,需要在片元着色器中设置。方法很简单,
//开启OpenGL的Alpha混合
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);//设置融合函数
//渲染,扇型四边型, 从第一个顶点开始 四个顶点
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
//关闭混合
glDisable(GL_BLEND);
//在片元着色器中对输出颜色进行设置 Alpha 透明度,值为(0.0 -- 1.0)
outColor.a = Alpha;\n\
顶点着色器,和片元着色器任务不同,如果片元着色器主要任务是通过采样对颜色进行计算,顶点着色器重点就是对位置进行计算,位置包括大小、移动、旋转等各种拓扑变形,顶点着色器通常还能接收顶点颜色,光照等,使之对最终渲染结果产生影响,但对于这部分我还没有研究透,通常的SDL应用对这方面涉及的也比较少,在这里就不进行介绍了,重点要说的是对位置的变换和计算。提到图形的变换,离不开矩阵运算,使用矩阵为以大大地减轻编程负担,常用变换,往往一个矩阵就能搞定以下就分别介绍
单位矩阵,单位矩阵是对角线为1,其他单元为0的矩阵,在运算上,用同维度的单位矩阵乘同维数组,结果不变。GLSL对位置的描述使用的是4维数组,{x,y,z,w} x, y, z分别代表x ,y , z 轴位置,w 代表观测者距离。在GLSL着色器中,用vec2,vec3,vec4 分别代表 2维,3维,4维数组,用mat2,mat3,mat4 代表2元,3元,4元矩阵。mat2(1),mat3(1),mat4(1),分别代表相应的单位矩阵。vec4 x = mat4(1) * x。矩阵各位置的几何意义比较费解,但只要记住几个特殊位置就够了。
4维 矩阵1 对角线
x,代表x轴宽度 ,y代表y轴宽度,z,代表z 轴宽度 w 代表观察者 对于2D图形,x 增加宽度增加,y会引发高度的变化,z 不起作用,w 加大图形会变远矩形会变小,反之变大。
4维 矩阵2 位移矩阵
对 2D应用 x 代表 x轴移动,y 代表 y轴移动 z 不起作用 w 作用同上
4维 矩阵 观察者位置矩阵
x,y,z分别代表观察者位置分别延相应的轴移动移动 ,w 作用不变
还有旋转矩阵,在这不啰嗦了,网上有大量介绍,讲得比我好,这里主要讲矩阵的用法,还是使用自建结构 SDL_fColor,主要是这个结构和 GLSL中四维数组的大小正好相同,外部引用{r,g,b,a} 与与GLSL中的vec4 相同,
为了控制图片显示的大小引入 SDL_fColor g_zoom = {x,y,z,w}; 其中 x,y 分别控制左右和上下位移,z 无作用,w 控制放大和缩小。
为了控制图片旋转,引入 SDL_fColor g_roll = {x,y,z,w};其中 x,y,z 分别控制 x,y,z 轴的旋转角度。w 控制放大与缩小
将顶点着色器代码修改成
//顶点着色器
const static GLchar* const vertexShader =
"#version 130\n\
in vec2 position;\n\
in vec2 TexCoord;\n\
uniform vec4 v_data[20];\n\
out vec2 sTexCoord;\n\
void main()\n\
{\n\
vec4 zoom = v_data[4];\n\
vec4 roll = v_data[5];\n\
gl_Position = vec4(position, 0.0, 1.0) ;\n\
if(zoom.w > 0.0)//移动缩放\n\
{\n\
mat4 m_zoom = mat4(1.0);\n\
m_zoom[3] = zoom;\n\
gl_Position = m_zoom * gl_Position;\n\
}\n\
if(roll.w >1.0)//旋转\n\
{\n\
float s,c;\n\
mat4 m_roll = mat4(1.0);//X轴\n\
m_roll[3][3] = roll.w;\n\
m_roll[1][1] = m_roll[2][2] = cos(roll.x);\n\
m_roll[1][2] = sin(roll.x);\n\
m_roll[2][1] = -sin(roll.x);\n\
gl_Position = m_roll * gl_Position;\n\
m_roll = mat4(1.0);//Y轴\n\
m_roll[0][0] = m_roll[2][2] = cos(roll.y);\n\
m_roll[0][2] = sin(roll.y);\n\
m_roll[2][0] = -sin(roll.y);\n\
gl_Position = m_roll * gl_Position;\n\
m_roll = mat4(1.0);//Z轴\n\
m_roll[0][0] = m_roll[1][1] = cos(roll.z);\n\
m_roll[0][1] = sin(roll.z);\n\
m_roll[1][0] = -sin(roll.z);\n\
gl_Position = m_roll * gl_Position;\n\
}\n\
sTexCoord = TexCoord ;\n\
}";
其中 v_data[20] 与片元着色器共用一组参数
最后贴上一组效果图片