從零開始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數組中去了。

貼圖

對於紋理貼圖,有這么幾個函數是需要知道的:

  1. glBindTexture(GL_TEXTURE_2D, texName); 綁定紋理:改變OpenGL狀態,使得后續的紋理操作都對texName指向的2D紋理生效
  2. 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:在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行采樣
  3. glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL); 紋理貼圖與材質的混合
    • GL_DECAL
    • GL_REPLACE
    • GL_BLEND
    • GL_MODULATE
    • GL_ADD
  4. 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);

效果如下

img

好吧,它還是很丑 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樣條曲線的繪制也完成了,下一篇將以光線追蹤收尾這一系列的博客。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM