從零開始的openGL——四、紋理貼圖與n次B樣條曲線
前言
在上篇文章中,介紹瞭如何載入繪製模型以及滑鼠互動的實現,並且遺留了個問題,就是沒有模型表面沒有紋理,看起來很醜。這篇文章將介紹如何貼紋理,以及曲線的繪製。
紋理貼圖
紋理載入
既然是貼圖,那首先我們得要有合適的紋理圖片,openGL中支援的圖片為bmp格式。在這裡我還用到了個額外的庫glaux,但當時在找這個庫的時候花了不少時間,這裡為了方便大家就把連結放出來。配置方式與之前glut與glui的配置方式相同。
然後是固定的載入圖片的程式碼
GLuint texture[1]; // 儲存一個紋理---陣列 AUX_RGBImageRec *LoadBMP(CHAR *Filename) { FILE *File = NULL; // 檔案控制代碼 if (!Filename) // 確保檔名已提供 { return NULL; // 如果沒提供,返回 NULL } File = fopen(Filename, "r"); // 嘗試開啟檔案 if (File) // 判斷檔案存在與否 { fclose(File); // 關閉控制代碼 return auxDIBImageLoadA(Filename); // 載入點陣圖並返回指標 } return NULL; // 如果載入失敗,返回 NULL }
載入完圖片後,我們還需要把圖片轉換成紋理
int LoadGLTextures(GLuint *texture, char *bmp_file_name, int texture_id) { int Status = FALSE; // 狀態指示器 // 建立紋理的儲存空間 AUX_RGBImageRec *TextureImage[1]; memset(TextureImage, 0, sizeof(void *) * 1); // 將指標設為 NULL // 載入點陣圖,檢查有無錯誤,如果點陣圖沒找到則退出 if (TextureImage[0] = LoadBMP(bmp_file_name)) { Status = TRUE; // 將 Status 設為 TRUE //生成(generate)紋理 glGenTextures(texture_id, texture); //&texture[0]); //繫結2D紋理物件 glBindTexture(GL_TEXTURE_2D, *texture); //texture[0]); //關聯影象資料與紋理物件 glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); //圖形繪製時所使用的濾波器引數 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 線形濾波 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 線形濾波 } //釋放影象的記憶體,因為已經生成紋理了,沒用了 if (TextureImage[0]) // 紋理是否存在 { if (TextureImage[0]->data) // 紋理影象是否存在 { free(TextureImage[0]->data); // 釋放紋理影象佔用的記憶體 } free(TextureImage[0]); // 釋放影象結構 } else printf("紋理不存在"); return Status; // 返回 Status }
使用方式如下
LoadGLTextures(&texture[0], "4.bmp", 1); //可新增到初始化程式碼中
這樣,我們就把紋理載入到了texture陣列中去了。
貼圖
對於紋理貼圖,有這麼幾個函式是需要知道的:
- glBindTexture(GL_TEXTURE_2D, texName); 繫結紋理:改變OpenGL狀態,使得後續的紋理操作都對texName指向的2D紋理生效
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 確定紋理如何應用到每個畫素上
- GL_REPEAT:預設選擇,重複紋理影象
- GL_MIRRORED_REPEAT:和GL_REPEAT一樣,但每次重複圖片是映象放置的
- GL_CLAMP_TO_EDGE:紋理座標會被約束在0到1之間,超出的部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果
- GL_CLAMP_TO_BORDER:使用者指定邊緣顏色,作為超出的座標的顏色
- GL_NEAREST_MIPMAP_NEAREST:使用最鄰近的多級漸遠紋理來匹配畫素大小,並使用鄰近插值進行紋理取樣
- GL_LINEAR_MIPMAP_NEAREST:使用最鄰近的多級漸遠紋理級別,並使用線性插值進行取樣
- GL_NEAREST_MIPMAP_LINEAR:在兩個最匹配畫素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行取樣
- GL_LINEAR_MIPMAP_LINEAR:在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行取樣
- glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL); 紋理貼圖與材質的混合
- GL_DECAL
- GL_REPLACE
- GL_BLEND
- GL_MODULATE
- GL_ADD
- glTexture2f(x, y); 指定紋理座標進行貼圖
下面就拿之前的模型來做演示
void DrawModel(CObj &model)
{//TODO: 繪製模型
for (int i = 0; i < model.m_faces.size(); i++)
{
glBindTexture(GL_TEXTURE_2D, texture[0]);
glBegin(GL_TRIANGLES);
glNormal3f(model.m_faces[i].normal.fX, model.m_faces[i].normal.fY, model.m_faces[i].normal.fZ);
glTexCoord2f(model.m_pts[model.m_faces[i].pts[0] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[0] - 1].normal.fY);
glVertex3f(model.m_pts[model.m_faces[i].pts[0] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[0] - 1].normal.fY, model.m_pts[model.m_faces[i].pts[0] - 1].normal.fZ);
glTexCoord2f(model.m_pts[model.m_faces[i].pts[1] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[1] - 1].normal.fY);
glVertex3f(model.m_pts[model.m_faces[i].pts[1] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[1] - 1].normal.fY, model.m_pts[model.m_faces[i].pts[1] - 1].normal.fZ);
glTexCoord2f(model.m_pts[model.m_faces[i].pts[2] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[2] - 1].normal.fY);
glVertex3f(model.m_pts[model.m_faces[i].pts[2] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[2] - 1].normal.fY, model.m_pts[model.m_faces[i].pts[2] - 1].normal.fZ);
glEnd();
}
}
// 初始化程式碼中加入
glEnable(GL_TEXTURE_2D);
效果如下
好吧,它還是很醜 orz。。。。
n次B樣條曲線
從前面的學習中,或許已經發現,openGL並不能直接繪製曲線或曲面,那我們要如何繪製呢?對於曲線,一個很簡單的想法就是用直線逼近,但是如何實現呢?如果我們知道這條曲線的引數方程,或許還是比較容易的。
首先先理解一些基本概念:
- 節點(knots):給定集合U,它包含m+1個有理數\(u_0,u_1,u_2,...,u_m\),且滿足\(u_0 ≤ u_1 ≤ u_2 ≤ ... ≤ u_m\) 。
- 節點向量(knot vector):由節點資料組成的向量 \([ u_0,u_1,u_2,...,u_m)\)
- 節點區間(knot span):\([u_i, u_{i+1})\)稱為第i個區間節點
- 控制點:定義n個點,用於控制曲線形狀
- 階數:d(2 ≤ d ≤ n)
- 次數:d - 1
引入曲線表示式$ p(u) = ∑{k=0}^{n}P_kB{k,d}(u) , u_{min} ≤ u ≤ u_{max}, 2 ≤ d ≤ n + 1$
B樣條曲線的混合函式由cox-deBoor遞迴公式定義為
\[
B_{k,1}=\begin{cases}
1,\quad &u_k \leq u \leq u_{k+1}\\
0,\quad &else
\end{cases} \\
B_{k,d}(u) = \frac{u-u_k}{u_{k+d-1}-u_k}B_{k,d-1}(u)+\frac{u_{k+d}-u}{u_{k+d}-u_{k+1}}B_{k+1,d-1}(u)
\]
這裡有個動態過程幫助理解
程式碼實現
先給出基本框架
#ifndef COMMON
#define COMMON
#define VIEW_YES 0x00
#define VIEW_NO 0x01
#define CRTL_LOAD 0x00
#define CRTL_ADD 0x01
#define CRTL_DRAG 0x02
#define CRTL_DENSE 0x03
#define CRTL_WAIT 0x04
#define CRTL_CYLINDER 0x05
#define CRTL_CONE 0x06
#define CRTL_MODEL 0x07
#define CHANGE_DENSE 0x00
#define SHAPE_CUBE 0x01
#define SHAPE_CIRCLE 0x02
#define SHAPE_CYLINDER 0x03
#define SHAPE_TORUS 0x04
#define SHAPE_MODEL 0x05
#define TRANSFORM_ADD 0x51
#define TRANSFORM_DRAG 0x52
#define TRANSFORM_NONE 0x53
#define TRANSFORM_TRANSLATE 0x54
#endif
// 實驗二.cpp : 此檔案包含 "main" 函式。程式執行將在此處開始並結束。
//
#include "pch.h"
#include"common.h"
#include<windows.h>
#include<string.h>
#include <stdlib.h>
#include <math.h>
#include <iostream>
#include <sstream>
#include <algorithm>
#include<gl/glui.h>
#include<gl/glut.h>
#include<vector>
int g_xform_mode = TRANSFORM_NONE;
int g_form_mode = TRANSFORM_NONE;
int g_view_type = VIEW_YES;
int g_control_type = CRTL_LOAD;
int g_dense = 0;
int g_main_window;
int g_index;
double g_windows_width, g_windows_height;
static int g_press_x; //滑鼠按下時的x座標
static int g_press_y; //滑鼠按下時的y座標
struct Point {
double x, y;
};
std::vector<Point> points; // 用於記錄每個控制點的座標
std::vector<float> vecs;
static int controlNum = 0; // 控制點的數量
static int pointNum = 0; // 節點數
static int degree = 0; // B樣條曲線的次數
std::vector<Point> opts;
void createKnots() {
vecs.clear();
int nKnots = controlNum + degree;
for (int i = 0; i <= nKnots; i++) {
if (i < degree) {
vecs.push_back(0);
}
else if (i < nKnots - degree + 1) {
vecs.push_back(vecs[i - 1] + 1);
}
else {
vecs.push_back(vecs[i - 1]);
}
}
}
int find_point(int x, int y) {
}
void add_point(float x, float y) {
}
float Deboor(int k, int d, float t){
}
void bspToPoint() {
}
bool load_Point(const char* pcszFileName)
{
FILE* fpFile = fopen(pcszFileName, "r"); //以只讀方式開啟檔案
if (fpFile == NULL)
{
return false;
}
points.clear();
opts.clear();
vecs.clear();
char strLine[1024];
Point point;
float vec;
fgets(strLine, 1024, fpFile);
std::istringstream sin(strLine);
sin >> degree;
fgets(strLine, 1024, fpFile);
std::istringstream sin1(strLine);
sin1 >> controlNum;
fgets(strLine, 1024, fpFile);
std::istringstream sin2(strLine);
while (sin2 >> vec)
{
vecs.push_back(vec);
}
while (!feof(fpFile))
{
fgets(strLine, 1024, fpFile);
std::istringstream sin3(strLine);
sin3 >> point.x >> point.y;
points.push_back(point);
}
points.pop_back();
fclose(fpFile);
g_dense = controlNum + degree;
bspToPoint();
return true;
}
void displayImage()
{
glClear(GL_COLOR_BUFFER_BIT);
glPointSize(1.0);
glColor3f(1.0, 0.0, 0.0);
//glEnable(GL_LINE_STIPPLE);
glLineStipple(1, 0xF0F0);
if (g_view_type == VIEW_YES) {
glBegin(GL_LINE_STRIP);
//glNormal3f(0.0f, 0.0f, 1.0f);
for (int i = 0; i < controlNum; i++) {
glVertex2f(points[i].x, points[i].y);
}
glEnd();
}
//glDisable(GL_LINE_STIPPLE);
glColor3f(1.0, 1.0, 1.0);
glBegin(GL_LINE_STRIP);
for (int i = 0; i < opts.size(); i++) {
glVertex2f(opts[i].x, opts[i].y);
}
glEnd();
glPointSize(5.0);
glColor3f(1.0, 1.0, 0.0);
glBegin(GL_POINTS);
//glNormal3f(0.0f, 0.0f, 1.0f);
for (int i = 0; i < controlNum; i++) {
glVertex2f(points[i].x, points[i].y);
}
glEnd();
glFlush();
}
void myGlutDisplay() //繪圖函式, 作業系統在必要時刻就會對窗體進行重新繪製操作
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除顏色緩衝以及深度緩衝
displayImage();
glutSwapBuffers(); //雙緩衝
}
void myGlutReshape(int x, int y) //當改變視窗大小時的回撥函式
{
if (y == 0)
{
y = 1;
}
g_windows_width = x;
g_windows_height = y;
double xy_aspect = (float)x / (float)y;
GLUI_Master.auto_set_viewport(); //自動設定視口大小
glMatrixMode(GL_PROJECTION);//當前矩陣為投影矩陣
glLoadIdentity();
gluPerspective(60.0, xy_aspect, 0.01, 1000.0);//視景體
glutPostRedisplay(); //標記當前視窗需要重新繪製
}
void mouse(int button, int state, int x, int y)
{
g_press_x = x;
g_press_y = y;
if (button == GLUT_LEFT_BUTTON) {
if (g_xform_mode + 1 == CRTL_ADD) {
add_point(x, 600 - y);
createKnots();
bspToPoint();
displayImage();
glutPostRedisplay();
}
else if (g_xform_mode + 1 == CRTL_DRAG) {
g_index = find_point(g_press_x, 600 - g_press_y);
g_form_mode = TRANSFORM_DRAG;
}
}
else {
g_form_mode = TRANSFORM_NONE;
}
}
void init()
{
//glClearColor(1.0f, 1.0f, 1.0f, 1.0f);//用白色清屏
glMatrixMode(GL_MODELVIEW); //指定當前矩陣為模型視景矩陣
glLoadIdentity(); //將當前的使用者座標系的原點移到了螢幕中心:類似於一個復位操作
gluOrtho2D(0.0, 800, 0.0, 600);
}
void myGlutMotion(int x, int y) //處理當滑鼠鍵摁下時,滑鼠拖動的事件
{
if (g_form_mode == TRANSFORM_DRAG) //拖拽點
{
float x_offset = (x - g_press_x);
float y_offset = (y - g_press_y);
if (g_index != -1) {
points[g_index].x += x_offset;
points[g_index].y -= y_offset;
}
g_press_x = x;
g_press_y = y;
}
bspToPoint();
// force the redraw function
glutPostRedisplay();
}
void myGlutIdle(void) //空閒回撥函式
{
if (glutGetWindow() != g_main_window)
glutSetWindow(g_main_window);
glutPostRedisplay();
}
void loadFile(void)
{//載入模型
//呼叫系統對話方塊
OPENFILENAME fname;
ZeroMemory(&fname, sizeof(fname));
char strfile[200] = "*.txt";
char szFilter[] = TEXT("TXT Files(*.TXT)\0");
fname.lStructSize = sizeof(OPENFILENAME);
fname.hwndOwner = NULL;
fname.hInstance = NULL;
fname.lpstrFilter = szFilter;
fname.lpstrCustomFilter = NULL;
fname.nFilterIndex = 0;
fname.nMaxCustFilter = 0;
fname.lpstrFile = strfile;
fname.nMaxFile = 200;
fname.lpstrFileTitle = NULL;
fname.nMaxFileTitle = 0;
fname.lpstrTitle = NULL;
fname.Flags = OFN_HIDEREADONLY | OFN_CREATEPROMPT;
fname.nFileOffset = 0;
fname.nFileExtension = 0;
fname.lpstrDefExt = 0;
fname.lCustData = NULL;
fname.lpfnHook = NULL;
fname.lpTemplateName = NULL;
fname.lpstrInitialDir = NULL;
HDC hDC = wglGetCurrentDC();
HGLRC hRC = wglGetCurrentContext();
GetOpenFileName(&fname);
wglMakeCurrent(hDC, hRC);
//printf("讀取檔案\n");
load_Point(fname.lpstrFile); //讀入模型檔案
}
void glui_control(int control) //處理控制元件的返回值
{
switch (control)
{
case CRTL_LOAD://選擇“open”控制元件
loadFile();
break;
case CRTL_DENSE:
bspToPoint();
break;
default:
break;
}
}
void myGlutKeyboard(unsigned char Key, int x, int y)
{//鍵盤時間回撥函式
if (Key = GLUT_KEY_DOWN) {
g_view_type = !g_view_type;
}
}
void myGlui()
{
GLUI_Master.set_glutDisplayFunc(myGlutDisplay); //註冊渲染事件回撥函式, 系統在需要對窗體進行重新繪製操作時呼叫
//GLUI_Master.set_glutReshapeFunc(myGlutReshape); //註冊視窗大小改變事件回撥函式
glutMotionFunc(myGlutMotion);//註冊滑鼠移動事件回撥函式
GLUI_Master.set_glutMouseFunc(mouse);//註冊滑鼠點選事件回撥函式
GLUI_Master.set_glutKeyboardFunc(myGlutKeyboard);//註冊鍵盤輸入事件回撥函式
GLUI_Master.set_glutIdleFunc(myGlutIdle); //為GLUI註冊一個標準的GLUT空閒回撥函式,當系統處於空閒時,就會呼叫該註冊的函式
//GLUI
GLUI *glui = GLUI_Master.create_glui_subwindow(g_main_window, GLUI_SUBWINDOW_RIGHT); //新建子窗體,位於主窗體的右部
new GLUI_StaticText(glui, "GLUI"); //在GLUI下新建一個靜態文字框,輸出內容為“GLUI”
new GLUI_Separator(glui); //新建分隔符
new GLUI_Button(glui, "Open", CRTL_LOAD, glui_control); //新建按鈕控制元件,引數分別為:所屬窗體、名字、ID、回撥函式,當按鈕被觸發時,它會被呼叫.
new GLUI_Button(glui, "Quit", 0, (GLUI_Update_CB)exit);//新建退出按鈕,當按鈕被觸發時,退出程式
GLUI_Panel *type_panel = glui->add_panel("Type");
GLUI_RadioGroup *radio = glui->add_radiogroup_to_panel(type_panel, &g_xform_mode, CRTL_ADD, glui_control);
glui->add_radiobutton_to_group(radio, "add");
glui->add_radiobutton_to_group(radio, "drag");
//glui->add_radiobutton_to_group(radio, "wire");
//glui->add_radiobutton_to_group(radio, "flat");
GLUI_Spinner *spinner = glui->add_spinner("spinner", 2, &g_dense, CRTL_DENSE, glui_control);
spinner->set_int_limits(3, 1000, 1);
glui->set_main_gfx_window(g_main_window); //將子窗體glui與主窗體main_window繫結,當窗體glui中的控制元件的值發生過改變,則該glui視窗被重繪
GLUI_Master.set_glutIdleFunc(myGlutIdle);
}
int main(int argc, char** argv)
{
freopen("log.txt", "w", stdout);//重定位,將輸出放入log.txt檔案中
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB);
glutInitWindowPosition(200, 200); //初始化視窗位置
glutInitWindowSize(800, 600); //初始化視窗大小
g_main_window = glutCreateWindow("lmw");
myGlui();
init();
glutMainLoop();
return 0;
}
遞迴實現基函式
std::vector<Point> points; // 用於記錄每個控制點的座標
std::vector<float> vecs;
static int controlNum = 0; // 控制點的數量
static int pointNum = 0; // 節點數
static int degree = 0; // B樣條曲線的次數
std::vector<Point> opts;
float Deboor(int k, int d, float t){
float Length1 = vecs[k + d - 1] - vecs[k];
float Length2 = vecs[k + d] - vecs[k + 1];
if (d == 1) {
if (t >= vecs[k] && t <= vecs[k + 1]) {
return 1.0;
}
else {
return 0.0;
}
}
else {
float first = 0.0, second = 0.0;
if (Length1 != 0) {
first = (t - vecs[k]) * Deboor(k, d - 1, t) / Length1;
}
if (Length2 != 0) {
second = (vecs[k + d] - t) * Deboor(k + 1, d - 1, t) / Length2;
}
return first + second;
}
}
獲取曲線上點的點
void bspToPoint() {
opts.clear();
float tJump = (vecs[controlNum] - vecs[degree]) / (g_dense); //g_dense 取樣頻率
float t = 0;
for (t = vecs[degree] + 1e-4; t < vecs[controlNum] - 1e-4; t += tJump) {
float tmpx = 0, tmpy = 0;
for (int i = 0; i < points.size(); i++) {
tmpx += points[i].x*Deboor(i, degree + 1, t);
tmpy += points[i].y*Deboor(i, degree + 1, t);
}
Point point = { tmpx, tmpy };
opts.push_back(point);
}
}
執行結果
在這裡,我還加入了控制點的新增與拖動功能
對於控制點的新增,只需使用上篇提到的滑鼠互動的方法即可,這裡我預設加入一個點階數也增加,可以嘗試加入一個控制階數大小的控制條。
void add_point(float x, float y) {
Point point;
point.x = x;
point.y = y;
points.push_back(point);
degree++;
controlNum++;
}
拖動的話,也差不多,只需找到需拖動的點,然後改變座標即可
int find_point(int x, int y) {
for (int i = 0; i < points.size();i++) {
float x_diff = x - points[i].x;
float y_diff = y - points[i].y;
if (x_diff * x_diff + y_diff * y_diff <= 25) {
return i;
}
}
return -1;
}
再執行一遍,好像還行
小節
到這裡,n次B樣條曲線的繪製也完成了,下一篇將以光線追蹤收尾這一系列的部落格