这里讲得纹理合成指的是合成图片纹理,也就是二维纹理合成,并不是指将纹理直接合成到三维模型上面。虽然从本质上面来说,三维纹理合成和二维纹理是一样的。
纹理合成基本上是用的非参数化的采样方法,也就是Li-Yi-Wei的那篇纹理合成的论文所使用的方法。基本思路就是为当前要合成的像素在样图中寻找一个最佳的邻域,至于这个邻域的形状是可以变化的。合成像素选取的顺序也是可以变化的。这些或多或少都会影响合成的最终结果。LiYiWei的论文里面选取的是L形状的邻域,并且使用扫描线的顺序。
在LiYiWei的三维纹理合成论文居然又提到了合成顺序对合成结果影响不大,这个好像没那么正确。因为,合成纹理像素的使用,需要匹配邻域,而且这些邻域最好是已经合成的像素,如果不是就不会得到较好的结果。这个是最关键的一点。所以说,合成顺序对合成结果还是有一定的影响的。
为什么L邻域能够得到较好的结果了,那是因为扫描线顺序下,L邻域里面的像素都是已经合成的像素。所以,如果打算对合成纹理算法进行局部的修改就需要注意邻域和合成顺序的设定,必须尽可能使用已经合成的像素。
为什么说三维纹理的合成和二维纹理的合成,本质是一致的了?因为三维纹理合成需要局部铺平,来构造邻域,然后的匹配和二维的思想是完全一致的。
下面是我自己实现代码的合成结果。纹理合成的代码网上还是能够找到的,虽然不是那么好找,这也只限于二维合成的代码。最好不要看csdn上面那个自以为写了加速的代码。
样本纹理:
合成纹理:

本来这个东西我是用CxImage加载了多种格式的图片,结果今天发现在vs2010的debug模型下有内存溢出,调试了半天才找到是它的原因,所以非常不爽,换了GDI+的实现方法。该控件支持加载多种格式的图片,并且在增加的项内部存储的图片路径。重要的一点是必须在控件的OnDestroy里面释放申请的ItemData,否则退出程序时候还是能检测到内存泄露。这个也是找了半天才找到的。郁闷之。
关于该控件类的具体实现过程,有点复杂。使用到是比较简单,绑定到CListCtrl控件上。用InitCtrl初始化,用AddImage添加。
该类还支持多选和全选,以及删除所有的选择。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#pragma once
// CImageListCtrl

class CImageListCtrl : public CListCtrl
{
DECLARE_DYNAMIC(CImageListCtrl)

public:
CImageListCtrl();
virtual ~CImageListCtrl();
CImageList m_ImageList;
WCHAR m_char16ImgName[MAX_PATH];

protected:
DECLARE_MESSAGE_MAP()

public:
// 添加图片
int AddImage(CString imgPath);
void DelImage(CString imgPath);
void RemoveAllImg(void);
// 获得当前显示所有图片的路径
void GetImgPathList(CStringList strListPath);
BOOL InitCtrl(int nX, int nY);
void DeleteSelItem(void);

private:
int m_nX;
int m_nY;
int m_nNum;

public:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnDestroy();
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// ImageListCtrl.cpp : 实现文件
//

#include "stdafx.h"
#include "CxImage/ximage.h"
#include "ImageListCtrl.h"

// CImageListCtrl

IMPLEMENT_DYNAMIC(CImageListCtrl, CListCtrl)

CImageListCtrl::CImageListCtrl()
{
m_nNum = 0;
}

CImageListCtrl::~CImageListCtrl()
{
m_ImageList.DeleteImageList();
}

BEGIN_MESSAGE_MAP(CImageListCtrl, CListCtrl)
ON_WM_CREATE()
ON_WM_DESTROY()
ON_WM_KEYDOWN()
END_MESSAGE_MAP()

// CImageListCtrl 消息处理程序

int CImageListCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CListCtrl::OnCreate(lpCreateStruct) == -1)
return -1;

return 0;
}

BOOL CImageListCtrl::InitCtrl(int nX, int nY)
{
m_nX = nX;
m_nY = nY;
SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);//LVS_EX_CHECKBOXES
if(!m_ImageList.Create(nX, nY, ILC_COLOR24, 1, 1))
{
return FALSE;
}

DeleteAllItems();
m_ImageList.SetBkColor(RGB(125, 125, 0));
if(SetImageList(m_ImageList, LVSIL_NORMAL) == NULL)
{
return FALSE;
}
return TRUE;
}

// 添加图片
/*
int CImageListCtrl::AddImage(CString imgPath)
{
int nEndLen = imgPath.GetLength() - imgPath.ReverseFind('.') - 1;
CString strEnd = imgPath.Right(nEndLen);
strEnd.MakeLower();
if(strEnd.Compare(_T("jpg")) == 0 || strEnd.Compare(_T("gif")) == 0
|| strEnd.Compare(_T("bmp")) == 0 || strEnd.Compare(_T("png")) == 0
|| strEnd.Compare(_T("tga")) == 0 || strEnd.Compare(_T("tiff")) == 0
|| strEnd.Compare(_T("exif")) == 0 || strEnd.Compare(_T("wmf")) == 0
|| strEnd.Compare(_T("emf")) == 0)
{
CxImage image;
//应用CXImage载入图像,本程序是相对路径
image.Load(imgPath);
if (image.IsValid() == false)
{
return 0;
}

image.Resample(m_nX, m_nY, 2);
CDC *pDC = GetDC();//应用CXImage在内存中生产位图
HBITMAP hBit = image.MakeBitmap(pDC->GetSafeHdc());
CBitmap bmp;
bmp.Attach(hBit);
int nResult = m_ImageList.Add(bmp, RGB(255, 255, 255));
bmp.Detach();
ReleaseDC(pDC);

int nIndex = imgPath.ReverseFind(_T('\\'));
CString strFileName = imgPath.Right(imgPath.GetLength() - (nIndex + 1));
InsertItem(m_nNum, strFileName, nResult);
SetItemData(m_nNum, (DWORD_PTR)strdup(imgPath));

RedrawItems(m_nNum, m_nNum);
m_nNum++;

return 1;
}

return 0;
}
*/

int CImageListCtrl::AddImage(CString imgPath)
{
int nEndLen = imgPath.GetLength() - imgPath.ReverseFind('.') - 1;
CString strEnd = imgPath.Right(nEndLen);
strEnd.MakeLower();
if(strEnd.Compare(_T("jpg")) == 0 || strEnd.Compare(_T("gif")) == 0
|| strEnd.Compare(_T("bmp")) == 0 || strEnd.Compare(_T("png")) == 0
|| strEnd.Compare(_T("tga")) == 0 || strEnd.Compare(_T("tiff")) == 0
|| strEnd.Compare(_T("exif")) == 0 || strEnd.Compare(_T("wmf")) == 0
|| strEnd.Compare(_T("emf")) == 0)
{
int nStrLen = imgPath.GetLength();
char* pCharBuf = imgPath.GetBuffer(0);
imgPath.ReleaseBuffer();
int nLen = MultiByteToWideChar(CP_ACP, 0, pCharBuf, nStrLen, NULL, 0);
MultiByteToWideChar(CP_ACP, 0, pCharBuf, nStrLen, m_char16ImgName, nLen);
m_char16ImgName[nLen] = '\0'; //添加字符串结尾,注意不是len+1

Gdiplus::Bitmap* pImage = new Gdiplus::Bitmap(m_char16ImgName, true);
if (pImage == NULL)
{
return 0;
}
Gdiplus::Bitmap* pThumbnail = (Gdiplus::Bitmap*)pImage->GetThumbnailImage(m_nX, m_nY);
HBITMAP hBmp;
pThumbnail->GetHBITMAP(Gdiplus::Color(255, 255, 255), hBmp);

CBitmap bmp;
bmp.Attach(hBmp);
int nResult = m_ImageList.Add(bmp, RGB(255, 255, 255));
bmp.Detach();
int nIndex = imgPath.ReverseFind(_T('\\'));
CString strFileName = imgPath.Right(imgPath.GetLength() - (nIndex + 1));

InsertItem(m_nNum, strFileName, nResult);
SetItemData(m_nNum, (DWORD_PTR)strdup(imgPath));
RedrawItems(m_nNum, m_nNum);

delete pThumbnail;
delete pImage;
m_nNum++;

return 1;
}

return 0;
}

void CImageListCtrl::DelImage(CString imgPath)
{
int nCount = GetItemCount();
for (int i = 0; i < nCount; i++)
{
char *pData = (char *)GetItemData(i);
if(pData != NULL)
{
CString str;
str.Format(_T("%s"), pData);
str = str.Trim();
if(str.CompareNoCase(imgPath) == 0)
{
free(pData);
DeleteItem(i);
break;
}
}
}
}

void CImageListCtrl::RemoveAllImg(void)
{
int nCount = GetItemCount();
for (int i=0;i<nCount;i++)
{
char *pData = (char *)GetItemData(i);
if(pData!=NULL)
free(pData);
}
DeleteAllItems();
m_nNum = 0;
}

// 获得当前显示所有图片的路径
void CImageListCtrl::GetImgPathList(CStringList strListPath)
{
int nCount = GetItemCount();
strListPath.RemoveAll();
for (int i=0;i<nCount;i++)
{
char *pData = (char *)GetItemData(i);
if(pData!=NULL)
{
CString str;
str.Format(_T("%s"),pData);
str = str.Trim();
strListPath.AddTail(str);
}
}
}

void CImageListCtrl::OnDestroy()
{
int nCnt = GetItemCount();
for (int i = 0; i < nCnt; i++)
{
char *pData = (char *)GetItemData(i);
if (pData)
{
delete [] pData;
}
}

CListCtrl::OnDestroy();
}

//删除所有选中的项
void CImageListCtrl::DeleteSelItem(void)
{
POSITION pos = GetFirstSelectedItemPosition();
while(pos != NULL)
{
int nItem = GetNextSelectedItem(pos);
TRACE1("Item %d was selected!\n", nItem);

char *pData = (char *)GetItemData(nItem);
if(pData != NULL)
{
free(pData);
}

DeleteItem(nItem);
pos = GetFirstSelectedItemPosition();
--m_nNum;
}

Arrange(LVA_ALIGNLEFT);
}

void CImageListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (nChar == VK_DELETE)
{
DeleteSelItem();
}
else if ((nChar == 'a' || nChar == 'A') GetKeyState(VK_CONTROL))
{
for (int i = m_nNum - 1; i >= 0; --i)
{
SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
}
}

CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}

实现效果如图:

关于MFC方面的问题,欢迎加群:87236798交流。

今天在做一个缩略图的控件的时候发现,CXImage库在VS2010的debug模型下有内存泄露,这可是我查错查了几个小时最后得出的结论。瞬间觉得很不爽,就不用CxImage了。觉得加载不同种格式的需求,GDI+应该能够满足我的要求。那么久果断改为GDI+了。GDI+我也没使用过,果断只能搜索其用法了。
后面发现只需要使用Gdiplus::Bitmap就可以处理多种格式的图片。虽然该类叫做位图,实际上可以处理多种格式,据说从Gdiplus::Image继承而来。
nsp; 使用GDI+据说在vs2010下也不需要初始化该库,vc6和vs2008下面需要初始化。初始化也很简单,在App里面包含个成员ULONG_PTR m_gdiplusToken,在InitInstance里面加上Gdiplus::GdiplusStartupInput gdiplusStartupInput;Gdiplus::GdiplusStartup(m_gdiplusToken, gdiplusStartupInput, NULL);在ExitInstance里面加上Gdiplus::GdiplusShutdown(m_gdiplusToken);就行了。
由于GDI+使用的是Unicode,而我一般用的是多字节工程,所以还需要将图片名字转换为Unicode。具体过程是先把图片名字转换为Unicode,再用Gdiplus::Bitmap类的对象bitmap加载图片,然后锁定图片数据,申请一块数据从bitmap内部提取出像素数据,然后释放图片数据,剩下的就是ogl的操作了。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef XPC_YX_TEXTURE_H
#define XPC_YX_TEXTURE_H

class CxImage;

class CTexture
{
public:
CTexture() { m_byData = NULL; m_szTexName[0] = 0; }
CTexture(const char* pcszFileName) { m_byData = 0; LoadFromFile(pcszFileName); }
~CTexture();
int LoadFromFile(const char* pcszFileName);
void Init2DTex();
void Init1DVertTex();

private:
int m_nWidth;
int m_nHeight;
BYTE* m_byData;
char m_szTexName[MAX_PATH];
WCHAR m_char16ImgName[MAX_PATH];
unsigned m_uTexName;
double m_fTexScaleX;
double m_fTexScaleY;
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include "stdafx.h"
#include "Texture.h"
#include "CxImage/ximage.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

//该函数只处理了没有压缩的格式
/*int CTexture::LoadFromFile(const char* pcszFileName)
{
if (pcszFileName != NULL)
{
strcpy(m_szTexName, pcszFileName);
}
CxImage image;

image.Load(pcszFileName);
if (image.IsValid() == false)
{
return 0;
}

m_nWidth = image.GetWidth();
m_nHeight = image.GetHeight();
if (m_byData)
{
delete m_byData;
m_byData = NULL;
}
m_byData = new BYTE[m_nWidth * m_nHeight * 3];
for (int i = 0; i < m_nHeight; ++i)
{
for (int j = 0; j < m_nWidth; ++j)
{
RGBQUAD rgb = image.GetPixelColor(j, i);
m_byData[(i * m_nWidth + j) * 3] = rgb.rgbRed;
m_byData[(i * m_nWidth + j) * 3 + 1] = rgb.rgbGreen;
m_byData[(i * m_nWidth + j) * 3 + 2] = rgb.rgbBlue;
}
}

for (int i = 0; i < m_nHeight; i += 5)
{
for (int j = 0; j < m_nWidth; ++j)
{
m_byData[(i * m_nWidth + j) * 3] = 0;
m_byData[(i * m_nWidth + j) * 3 + 1] = 0;
m_byData[(i * m_nWidth + j) * 3 + 2] = 0;
}
}

return 1;
}*/

int CTexture::LoadFromFile(const char* pcszFileName)
{
if (pcszFileName != NULL)
{
strcpy(m_szTexName, pcszFileName);
}

int nStrLen = strlen(pcszFileName);
int nLen = MultiByteToWideChar(CP_ACP, 0, pcszFileName, nStrLen, NULL, 0);
MultiByteToWideChar(CP_ACP, 0, pcszFileName, nStrLen, m_char16ImgName, nLen);
m_char16ImgName[nLen] = '\0'; //添加字符串结尾,注意不是len+1*/

Gdiplus::Bitmap image(m_char16ImgName);

m_nWidth = image.GetWidth();
m_nHeight = image.GetHeight();
Gdiplus::Rect rect(0, 0, m_nWidth, m_nHeight);
Gdiplus::BitmapData bitmapData;
image.LockBits(rect, Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeWrite,
PixelFormat24bppRGB, bitmapData);

if (m_byData)
{
delete m_byData;
m_byData = NULL;
}
m_byData = new BYTE[m_nWidth * m_nHeight * 3];

BYTE* pByte = (BYTE*)bitmapData.Scan0;
int nOffset = bitmapData.Stride - m_nWidth * 3;
for (int i = 0; i < m_nHeight; ++i)
{
for (int j = 0; j < m_nWidth; ++j)
{
m_byData[(i * m_nWidth + j) * 3] = pByte[0];
m_byData[(i * m_nWidth + j) * 3 + 1] = pByte[1];
m_byData[(i * m_nWidth + j) * 3 + 2] = pByte[2];
pByte += 3;
}
pByte += nOffset;
}
image.UnlockBits(bitmapData);

return 1;
}

void CTexture::Init2DTex()
{
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glGenTextures(1, m_uTexName);
glBindTexture(GL_TEXTURE_2D, m_uTexName);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

int nRet = gluBuild2DMipmaps(GL_TEXTURE_2D, 3, m_nWidth, m_nHeight, GL_RGB, GL_UNSIGNED_BYTE,
m_byData);
if (nRet != 0)
{
AfxMessageBox("加载纹理失败");
}
}

void CTexture::Init1DVertTex()
{
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glGenTextures(1, m_uTexName);
glBindTexture(GL_TEXTURE_1D, m_uTexName);

glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

BYTE* pbyData = new BYTE[m_nHeight * 3];
int nAdd = m_nWidth / 2;
for (int i = 0; i < m_nHeight; ++i)
{
pbyData[i * 3] = m_byData[(i * m_nWidth + nAdd) * 3];
pbyData[i * 3 + 1] = m_byData[(i * m_nWidth + nAdd) * 3 + 1];
pbyData[i * 3 + 2] = m_byData[(i * m_nWidth + nAdd) * 3 + 2];
}

int nRet = gluBuild1DMipmaps(GL_TEXTURE_1D, 3, m_nHeight, GL_RGB, GL_UNSIGNED_BYTE,
pbyData);
if (nRet != 0)
{
AfxMessageBox("加载纹理失败");
}

delete pbyData;
}

CTexture::~CTexture()
{
if (m_byData)
delete m_byData;
}

现在,在vs2010和vs2008下,不需要使用额外的库就能处理多种格式的纹理了。下面是一个贴图效果,用的是一维纹理显示了sdf值在模型顶点上的分布。

处理多种格式本身就是件麻烦的事情。但是使用cximage的话,就可以完美解决这个问题了。cximage可以处理多种常见格式的图片,比如bmp,png,jpg等。
那么如何使用任意尺寸的图片了。glTexImage2D必须使用长宽二次方的图片,使用非二次方的图片,必须用glTexSubImage2D进行处理,具体方法,可以参考 OpenGL使用长宽非二次方纹理 。
那么如何避免使用glTexSubImage2D了。我们可以使用函数gluBuild2DMipmaps来完成以上两个函数完成的工作,还不需要对纹理坐标进行放缩。这是一件非常方便的事情。
下面提供我的纹理类。使用方法很简单。先调用加载图片函数,再调用初始化纹理函数,可以选择二维和一维垂直方向的。然后,在绘制的时候使用纹理就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef XPC_YX_TEXTURE_H
#define XPC_YX_TEXTURE_H

class CTexture
{
public:
CTexture() { m_byData = NULL; m_szTexName[0] = 0; }
CTexture(const char* pcszFileName) { m_byData = 0; LoadFromFile(pcszFileName); }
~CTexture();
int LoadFromFile(const char* pcszFileName);
void Init2DTex();
void Init1DVertTex();

private:
int m_nWidth;
int m_nHeight;
BYTE* m_byData;
char m_szTexName[MAX_PATH];
unsigned m_uTexName;
};

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include "stdafx.h"
#include "Texture.h"
#include "../CxImage/ximage.h"
#include <stdlib.h>
#include <stdio.h>
#pragma comment(lib, "cximage.lib")

int CTexture::LoadFromFile(const char* pcszFileName)
{
if (pcszFileName != NULL)
{
strcpy(m_szTexName, pcszFileName);
}
CxImage image;
image.Load(pcszFileName);
if (image.IsValid() == false)
{
return 0;
}

m_nWidth = image.GetWidth();
m_nHeight = image.GetHeight();
if (m_byData)
{
delete m_byData;
}
m_byData = new BYTE[m_nWidth * m_nHeight * 3];
for (int i = 0; i < m_nHeight; ++i)
{
for (int j = 0; j < m_nWidth; ++j)
{
RGBQUAD rgb = image.GetPixelColor(j, i);
m_byData[(i * m_nWidth + j) * 3] = rgb.rgbRed;
m_byData[(i * m_nWidth + j) * 3 + 1] = rgb.rgbGreen;
m_byData[(i * m_nWidth + j) * 3 + 2] = rgb.rgbBlue;
}
}

return 1;
}

void CTexture::Init2DTex()
{
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glGenTextures(1, m_uTexName);
glBindTexture(GL_TEXTURE_2D, m_uTexName);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

int nRet = gluBuild2DMipmaps(GL_TEXTURE_2D, 3, m_nWidth, m_nHeight, GL_RGB, GL_UNSIGNED_BYTE,
m_byData);
if (nRet != 0)
{
AfxMessageBox("加载纹理失败");
}
}

void CTexture::Init1DVertTex()
{
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glGenTextures(1, m_uTexName);
glBindTexture(GL_TEXTURE_1D, m_uTexName);

glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

BYTE* pbyData = new BYTE[m_nHeight * 3];
int nAdd = m_nWidth / 2;
for (int i = 0; i < m_nHeight; ++i)
{
pbyData[i * 3] = m_byData[(i * m_nWidth + nAdd) * 3];
pbyData[i * 3 + 1] = m_byData[(i * m_nWidth + nAdd) * 3 + 1];
pbyData[i * 3 + 2] = m_byData[(i * m_nWidth + nAdd) * 3 + 2];
}

int nRet = gluBuild1DMipmaps(GL_TEXTURE_1D, 3, m_nHeight, GL_RGB, GL_UNSIGNED_BYTE,
pbyData);
if (nRet != 0)
{
AfxMessageBox("加载纹理失败");
}

delete pbyData;
}

CTexture::~CTexture()
{
if (m_byData)
delete m_byData;
}

要使用该类,还得配置CxImage。下面这张图片是使用一维垂直纹理的效果。

这篇论文讲的是交互式模型分割。用户用画笔在模型上面绘制出一笔,然后获得相关的分割区域。
这篇论文的思路大致分为三步。第一步,对所有顶点或者面计算sdf值。第二步,将笔画投影作为前景,估计一个混合高斯模型,另外随机选取1000个顶点或面估计一个高斯混合模型。第三步,利用sdf值和高斯混合模型,进行graph-cut。
关于sdf值和graph-cut都可以阅读论文引用的文章从而得到知晓。sdf值的计算可以到提出该观点的作者主页上面下载相关软件。但是该软件只能计算面的sdf值,不过可以再映射到顶点上面。graph-cut的应用更为广泛,这个也能到Yuri Y. Boykov主页上下载相关代码,然后使用即可。
更困难的事情是确定graph-cut的参数,这个需要多次调整。
下面是实现效果。

第一步,先把鼠标轨迹点逆投影到三维世界的观察坐标系。
第二步,把第一步获得的三维坐标点集合投射到模型上面,获得笔画,就是模型内部的一些相互连接的边。
第三步,沿着第二步获得那些相互连接的边进行欧拉操作分裂边。可能需要多分割几次才会分割出模型的一部分。
第四步,搜索整个模型,对不同的部分进行标记

下面是实现的一些效果。绿色线是鼠标轨迹,红色线是投影到模型上面的边的连接线。


下面具体解释下以上几个步骤。
步骤一,比较简单,网上代码一堆,不过要自己注意理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
glGetIntegerv(GL_VIEWPORT, m_nViewPort);
glGetDoublev(GL_PROJECTION_MATRIX, m_fProjmatrix);
glGetDoublev(GL_MODELVIEW_MATRIX, m_fMvmatrix);

float fWinX, fWinY, fWinZ;
int nX, nY;
double fX, fY, fZ;

m_pt3ds.clear();
for (int i = 0; i < m_ptStroke.size(); ++i)
{
nX = m_ptStroke[i].x;
nY = m_nViewPort[3] - m_ptStroke[i].y - 1;
glReadBuffer(GL_BACK);
glReadPixels(nX, nY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, fWinZ);
//if (fabs(fWinZ - 1.0) > 1e-8)
{
fWinX = nX;
fWinY = nY;
gluUnProject(fWinX, fWinY, fWinZ, m_fMvmatrix, m_fProjmatrix, m_nViewPort,
fX, fY, fZ);
m_pt3ds.push_back(Point_3(fX, fY, fZ));
}
}
m_ptStroke.clear();

该代码就是把m_ptStroke的点转化为观察坐标系的点m_pt3ds。
步骤二,大致思路如下:第一步,把所有鼠标经过的点,都投影回观察空间。第二步,找到离第一个鼠标点p[0],最近的模型内的顶点pClosed。第三步,循环剩余的鼠标点,如果当前鼠标点离pClosed的相邻顶点更近的话,那么将pClosed更新为上一次找到的pClosed的相邻顶点。所有的这些,pClosed点就是笔画的投影点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
FILE* fpLog = fopen("log.txt", "w");
char szStr[MAX_PATH];
CString str;
str.Format("%d", pt3ds.size());
//AfxMessageBox(str);
sprintf(szStr, "pt3ds.size():%d\n", pt3ds.size());
fprintf(fpLog, "%s", szStr);

if (pt3ds.size() == 0)
{
return;
}

double fX = CGAL::to_double(pt3ds[0].x());
double fY = CGAL::to_double(pt3ds[0].y());
double fZ = CGAL::to_double(pt3ds[0].z());
double fX1, fY1, fZ1;

//获得离第一个投影点最近的halfedge_handle
Halfedge_handle heClosed = facets_begin()->halfedge();
double fDiff = 1e10;
for(Face_iterator pf = facets_begin();
pf != facets_end(); pf++)
{
Halfedge_handle he = pf->halfedge();

do
{
Point_3 p = he->vertex()->point();
fX1 = CGAL::to_double(p.x());
fY1 = CGAL::to_double(p.y());
fZ1 = CGAL::to_double(p.z());
double fTemp = (fX1 - fX) * (fX1 - fX) + (fY1 - fY) * (fY1 - fY)
+ (fZ1 - fZ) * (fZ1 - fZ);
if (fTemp < fDiff)
{
fDiff = fTemp;
heClosed = he;
}
he = he->next();
}while(he!= pf->halfedge());
}

vector<Halfedge_handle> vh;
vh.push_back(heClosed);
for (unsigned i = 1; i < pt3ds.size(); ++i)
{
sprintf(szStr, "处理第:%d个笔画点\n", i);
fprintf(fpLog, szStr);

fX = CGAL::to_double(pt3ds[i].x());
fY = CGAL::to_double(pt3ds[i].y());
fZ = CGAL::to_double(pt3ds[i].z());
sprintf(szStr, "笔画点坐标为:%f,%f,%f\n", fX, fY, fZ);
fprintf(fpLog, szStr);

Halfedge_handle next = heClosed;
Point_3 p = heClosed->vertex()->point();

fX1 = CGAL::to_double(p.x());
fY1 = CGAL::to_double(p.y());
fZ1 = CGAL::to_double(p.z());
sprintf(szStr, "heClosed坐标为:%f,%f,%f\n", fX1, fY1, fZ1);
fprintf(fpLog, szStr);

fDiff = (fX1 - fX) * (fX1 - fX) + (fY1 - fY) * (fY1 - fY)
+ (fZ1 - fZ) * (fZ1 - fZ);

Vertex_handle v = heClosed->vertex();
Halfedge_around_vertex_circulator he, end;
he = end = v->vertex_begin();
CGAL_For_all(he, end)
{
Halfedge_handle heTemp = he->opposite();
p = heTemp->vertex()->point();
fX1 = CGAL::to_double(p.x());
fY1 = CGAL::to_double(p.y());
fZ1 = CGAL::to_double(p.z());

double fTemp = (fX1 - fX) * (fX1 - fX) + (fY1 - fY) * (fY1 - fY)
+ (fZ1 - fZ) * (fZ1 - fZ);

if (fTemp < fDiff heClosed != he)
{
sprintf(szStr, "heTemp坐标为:%f,%f,%f\n", fX1, fY1, fZ1);
fprintf(fpLog, szStr);

sprintf(szStr, "fDiff:%f,fTemp:%f\n", fDiff, fTemp);
fprintf(fpLog, szStr);

if (std::find(vh.begin(), vh.end(), heTemp) != vh.end())
{
continue;
}

//if (vh.size() != 1 heClosed->facet() == heTemp->facet())
{
// continue;
}
fDiff = fTemp;
next = heTemp;
//sprintf(szStr, "next坐标为:%f,%f,%f\n", fX1, fY1, fZ1);
//fprintf(fpLog, szStr);
}
}

//去掉已经出现过的顶点
if (next != heClosed)
{
heClosed = next;
vh.push_back(heClosed);
}
}

glDisable(GL_LIGHTING);
glDisable(GL_DEPTH_TEST);
glColor3f(1.0, 0.0, 0.0);
glLineWidth(3.0);
glBegin(GL_LINE_STRIP);
for (int i = 0; i < vh.size(); ++i)
{
glVertex3d(CGAL::to_double(vh[i]->vertex()->point().x()),
CGAL::to_double(vh[i]->vertex()->point().y()),
CGAL::to_double(vh[i]->vertex()->point().z()));
sprintf(szStr, "i:%d, x:%f,y:%f,z:%f\n", i,
CGAL::to_double(vh[i]->vertex()->point().x()),
CGAL::to_double(vh[i]->vertex()->point().y()),
CGAL::to_double(vh[i]->vertex()->point().z()));
fprintf(fpLog, szStr);
}
glEnd();
glFlush();

步骤三是欧拉操作。我的思路是将经过的边都变成边界边,方便第四步的搜索。首先,split_edge。然后,把沿着边的相邻的两个面沿着原来的边split_facet,再删除多余的面。那么,该经过的边就会变成边界边。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (int i = 1; i < vh.size(); ++i)
{
Halfedge_handle hBegOne = vh[i]->next()->next();
fprintf(fpLog ,"处理i:%d\n", i);
split_edge(vh[i]);

Halfedge_handle hBegTwo = vh[i]->opposite()->next();
Halfedge_handle hEndOne = vh[i];
Halfedge_handle hEndTwo = hBegTwo->next()->next();
split_facet(hBegOne, hEndOne);
split_facet(hBegTwo, hEndTwo);
erase_facet(hEndOne);
erase_facet(hBegTwo);
fprintf(fpLog, "is border:%d\n", hBegOne->next()->is_border_edge());
}

步骤五,对整个模型的面进行dfs即可了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void SetComponent()
{
for (Facet_iterator i = facets_begin(); i != facets_end(); ++i)
{
i->component(-1);
}

int nComponent = 1;
for (Facet_iterator i = facets_begin(); i != facets_end(); ++i)
{
if (i->component() == -1)
{
DfsComponent(i, nComponent);
nComponent++;
}

}

}

void DfsComponent(Facet_iterator i, int nComponent)
{
CString str;
str.Format("处理第:%d个面", i->tag());
//AfxMessageBox(str);
i->component(nComponent);

Halfedge_around_facet_circulator j = i->facet_begin();

do {
Halfedge_handle hfTemp = j;
if (hfTemp->is_border_edge())
{
//AfxMessageBox("找到边界边");
continue;
}
Halfedge_handle hf = j->opposite();
Facet_handle fh = hf->facet();

if (fh->component() == -1)
{
DfsComponent(fh, nComponent);
}
} while ( ++j != i->facet_begin());

}

因为每个面里面都有个代表属于哪个组件的标记,这样就实现了模型的分割。利用该方法就能实现分割出狗的腿和头部等等。

代码分享链接:链接: http://pan.baidu.com/s/1eQ50rf0 密码: 7f6v

关于cgal的使用就不介绍了。安装了也很麻烦,但是可以直接使用库,可以去我的上一篇文章下载,cgal下载
这里只简单的说说实现切割模型的方法。
判断所有的顶点在面的哪一面,如果一个面的所有点都在切割面的正面,那么这个面就是在切割面的positive部分,反之,如果一个面的所有点都在切割面的负面,那么这个面就是在切割面的negative部分,其余的面就是和切割面相交了。根据这个简单的原理,我们就可以用切割面把模型分为三部分。以下是我用cgal实现的效果。


从图片来看,效果基本还行,只是切割处不光滑,但是对于这么简单的算法能实现的效果,已经不错了,性价比很高。
如果想进一步平滑分割处,有什么办法了?我们既然已经把模型内部的面分成三部分了,那么我们只需要计算会和切割面相交的面和切割面的交点,面和面的交点应该是一条直线,那么应该有2个交点,我们再沿着这条相交线,对模型内部的面进行分割就行了。如果分割后出现非三角面,我们还可以进行三角面片化。具体的实现也不会难如上青天。

作为在学习图形学的苦逼,刚开始弄这些库也不简单。即使会弄,也麻烦得要死。装cgal就得去找教程,按照教程安装。
但是如果有安装好的库之后,只需要配置vs的路径就行了。而且库可以带走,不用多次安装了。我还是有一点分享精神的,我就把我的库都分享出来吧。
CGAL是4.1的,Boost是1.47的,OpenGL里面有glew,还有个freeimage,不过没有glm。glm可以去opengl超级宝典的源代码里面找,不过感觉不是很需要glm,即使需要几何计算,还不如找成熟的几何计算的类。

由于这篇文章访问量较大,而且不少同学留言问过我链接失效的事情,因此再次更新百度云盘的分享链接:
点此下载,密码:15j6。

编译一个程序的时候发现glui一直链接不上去,也许是我的glui版本太低了,结果到最后发现作者设置了lib的附加路径。也许glui2.35真的不能在vs2010下使用了。我下载过好几次,都还是发现glui2.36编译不过去,最后把一个模板实例化移到全局命名空间里面才编译过去了。这里打算讲一下怎么编译对应开发平台下的代码库和编译glui2.36。
我下载过cximage,opencv2.2,cgal,glui等开发库,有些开发库可能一下载下来就有头文件和lib,dll了。但是大多的是下载下来一个工程。而且,这个工程的原始环境还可能和你所用的环境不一样。比如,你下载下来的是vs2005或者vs2008的工程,但是你的环境是vs2010。如果,你在vs2010下面用别人用vs2005编译好的lib就可以链接不成功,这个时候你就需要用vs2010打开工程,重新编译vs2010下的lib。一般情况是打开包含所有工程的workspace,然后点击批生成,把所有需要的lib都生成出来。具体步骤就不用细说了。
但是还是可能遇到其它问题,比如说glui2.36就是编译不过去,因为出现了语法问题。那么就需要发挥你的聪明才智了,经验越多,语法了解的越多,就能够编译过去。我这里再上传,我修改过的glui.236的工程,里面已经包含vs2010下面的各种lib和dll了,放心使用吧。工程和lib,dll都在msvc目录下面,头文件在include里面。
glui2.36

如果是用c,c++这当然是相当熟悉的操作了,也不需要做个记录了。但是现在用的是vs2010版本的vb.net。而现在我有一个大对象,里面存储了整形,浮点,字符串,还有图片。我想将数据归档到文件里面,还有从文件中恢复出来。所以,只能使用二进制的读写方法,顺便记录图片的长度。至于要不要记录字符串的长度,只能问VB.NET了。
读取文件的使用BinaryReader,写文件的使用BinaryWriter。这2个对.net的string也支持二进制读写,所以只需要存储图片的长度了。图片本身当做byte[]存储。这2个类在Imports System.IO空间,都需要从FileStream创建。
虽说对VB.NET不熟悉,但是实现之后还是发现这个东西很好用的。以后还可以用来参考下。

存储的对象的类定义如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Imports System.IO

Public Class Project

'建筑,供水,供电,结构,供暖,环境
Public oldValueFactor(0 To 5) As Double
Public oldValue(0 To 5) As Integer
Public newValueFactor(0 To 5) As Double
Public newValue(0 To 5) As Integer

Public idLen As Integer
Public id As String
Public nameLen As Integer
Public name As String
Public placeLen As Integer
Public place As String
Public situationLen As Integer
Public situation As String

Public oldTotal As Integer '总分
Public newTotal As Integer

Public oldLen As Integer
Public oldPic() As Byte
Public newLen As Integer
Public newPic() As Byte '图片

Public Sub New()
oldLen = 0
newLen = 0
Dim j As Integer
For j = 0 To 5
oldValueFactor(j) = New Double()
oldValue(j) = New Double()
newValueFactor(j) = New Double()
newValue(j) = New Double()
Next
End Sub

End Class

对象类的集合类定义如下。该类支持归档到文件,从文件恢复,查找,添加,删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
Imports System.IO

Public Class ProjectSet
Public nNum As Integer
'1000000
Public pros(0 To 100000) As Project

Public Sub New()
nNum = 0
End Sub

Public Sub LoadFromFile(ByVal fileName As String)

Dim Stream As FileStream
Dim BinaryStreamReader As BinaryReader
Try
Stream = New FileStream(fileName, FileMode.Open)
BinaryStreamReader = New BinaryReader(Stream)
nNum = BinaryStreamReader.ReadInt32()
Catch E As Exception
Return
End Try

Dim i As Integer
For i = 0 To nNum - 1
pros(i) = New Project
Dim j As Integer
For j = 0 To 5
pros(i).oldValueFactor(j) = BinaryStreamReader.ReadDouble()
pros(i).oldValue(j) = BinaryStreamReader.ReadInt32()
pros(i).newValueFactor(j) = BinaryStreamReader.ReadDouble()
pros(i).newValue(j) = BinaryStreamReader.ReadInt32()
Next

pros(i).id = BinaryStreamReader.ReadString()
pros(i).name = BinaryStreamReader.ReadString()
pros(i).place = BinaryStreamReader.ReadString()
pros(i).situation = BinaryStreamReader.ReadString()

pros(i).oldTotal = BinaryStreamReader.ReadInt32()
pros(i).newTotal = BinaryStreamReader.ReadInt32()

pros(i).oldLen = BinaryStreamReader.ReadInt32()
pros(i).oldPic = BinaryStreamReader.ReadBytes(pros(i).oldLen)

pros(i).newLen = BinaryStreamReader.ReadInt32()
pros(i).newPic = BinaryStreamReader.ReadBytes(pros(i).newLen)

Next

BinaryStreamReader.Close()
Stream.Close()
End Sub

Public Sub SaveToFile(ByVal fileName As String)

Dim Stream = New FileStream(fileName, FileMode.Create)
Dim BinaryStream As New BinaryWriter(Stream)

BinaryStream.Write(nNum)

Dim i As Integer
For i = 0 To nNum - 1

Dim j As Integer
For j = 0 To 5
BinaryStream.Write(pros(i).oldValueFactor(j))
BinaryStream.Write(pros(i).oldValue(j))
BinaryStream.Write(pros(i).newValueFactor(j))
BinaryStream.Write(pros(i).newValue(j))
Next

BinaryStream.Write(pros(i).id)
BinaryStream.Write(pros(i).name)
BinaryStream.Write(pros(i).place)
BinaryStream.Write(pros(i).situation)

BinaryStream.Write(pros(i).oldTotal)
BinaryStream.Write(pros(i).newTotal)
BinaryStream.Write(pros(i).oldLen)
BinaryStream.Write(pros(i).oldPic)
BinaryStream.Write(pros(i).newLen)
BinaryStream.Write(pros(i).newPic)

Next

BinaryStream.Close()
Stream.Close()

End Sub

Public Sub AddProject(ByVal project As Project)
pros(nNum) = project
nNum += 1
End Sub

Public Sub DeleteProject(ByVal nIndex As Integer)

Dim i As Integer
For i = nIndex To nNum - 2
pros(i) = pros(i + 1)
Next

nNum -= 1

End Sub

Public Function FindProject(ByVal id As String) As Integer

Dim i As Integer
For i = 0 To nNum - 1
If id = pros(i).id Then
Return i
End If
Next
Return -1

End Function

End Class

上面的集合类里面定义了个大数组,是因为我对.NET不熟悉,不知道这样的对象数组定义,产生的是对象集合,还是虚的引用集合。我觉得是引用集合就不用声明数据的大小了吧,结果还是需要声明。而且,读取文件的时候还需要给每个数据项new一个Project。反正不清楚为什么是这样的原因,因为对.NET真的一点不熟悉。