2024-09-29
Learning "Build a Large Language Model (From Scratch)"

《Build a Large Language Model (From Scratch)》


Setup

参考

  • setup/01_optional-python-setup-preferences
  • .setup/02_installing-python-libraries

按照步骤配置环境:

1
2
3
4
5
6
git clone --depth 1 https://github.com/rasbt/LLMs-from-scratch.git
cd LLMs-from-scratch
conda create -n LLMs python=3.10
conda activate LLMs
conda install jupyterlab watermark
pip install -r requirements.txt

在这里遇到了以下问题:

image-20240929162323569

解决方案:

1
hash -r
image-20240929162500733

解释:

  • hash 是一个 Bash 内建命令,用于查找并记住命令的位置。如果你在安装了新的软件之后,想要立即使用它,但是 Bash 仍然使用旧的命令,那么你可以使用 hash -r 命令来刷新 Bash 的命令缓存。

Chapter 01

Read More

2024-09-06
Configure Latex on MacOS

安装MacTex

安装VSCode插件

配置VSCode

设置默认编译器

在VSCode中,打开设置,搜索”latex-workshop.latex.recipes”,将 “args” 部分的内容修改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"name": "latexmk",
"command": "latexmk",
// "args": [
// "-synctex=1",
// "-interaction=nonstopmode",
// "-file-line-error",
// "-pdf",
// "-outdir=%OUTDIR%",
// "%DOC%"
// ],
"args": [
"-xelatex",
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"%DOC%"
],
"env": {}

保存编译

在VSCode中,打开设置,搜索”latex-workshop.latex.autoBuild.run”,将其设置为”onSave”,这样每次保存tex文件时就会自动编译。

使用上次使用的编译器

在VSCode中,打开设置,搜索”latex-workshop.latex.recipe.default”,将其设置为”lastUsed”,这样第一次编译tex文件手动选择选择编译器,后面就会默认使用该选择了。

目前遇到的问题

  • MacOS上编译得到的pdf文件中文字体偏细, 与Windows上的不一样

参考

Read More

2024-08-01
Basic Algorithms

Chapter1 基础算法

快速排序 Quick Sort

785.快速排序

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
#include <iostream>
using namespace std;

const int N = 1e6 + 10;

int n;
int q[N];

void quick_sort(int q[],int l,int r)
{
if(l>=r) return;
int i = l-1;
int j = r+1;
int x = q[(l+r)/2];

while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j) swap(q[i],q[j]);
}

quick_sort(q,l,j);
quick_sort(q,j+1,r);
}


int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&q[i]);

quick_sort(q,0,n-1);

for(int i=0;i<n;i++) printf("%d ",q[i]);

return 0;
}

注意边界问题

quick_sort(q,l,j);
quick_sort(q,j+1,r);//此时x不能取q[r]

可以更改为

quick_sort(q,l,i-1);
quick_sort(q,i,r);//此时x不能取q[l]

归并排序

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
#include<iostream>
using namespace std;

const int N = 1e6+10;
int n;
int q[N],tmp[N];

void merge_sort(int q[],int l,int r)
{
if(l>=r)return;
int mid = l+r >> 1;
merge_sort(q,l,mid);
merge_sort(q,mid+1,r);
int k = 0;
int i = l,j = mid+1;
while(i<=mid && j<=r) {
if (q[i]<=q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
}
while(i<=mid) tmp[k++] = q[i++];
while(j<=r) tmp[k++] = q[j++];
for(i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
}

int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&q[i]);

merge_sort(q,0,n-1);

for(int i=0;i<n;i++) printf("%d ",q[i]);

return 0;
}

二分排序

整数二分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
例题:

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 00 开始计数)。

如果数组中不存在该元素,则返回 -1 -1

输入格式

第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼100001∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式

共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1

数据范围

1≤n≤100000

1≤q≤10000

1≤k≤10000

输入样例

1
2
3
4
5
6 3
1 2 2 3 3 4
3
4
5

输出样例

1
2
3
3 4
5 5
-1 -1

答案

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
#include <iostream>
using namespace std;

const int N = 100010;
int arr[N];

int main()
{
int n,q,k;
scanf("%d%d",&n,&q);
for(int i=0;i<n;i++) scanf("%d",&arr[i]);
for(int i=0;i<q;i++) {
scanf("%d",&k);
int l=0,r=n-1;
while(l<r)
{
int mid = l+r >> 1;
if(arr[mid]>=k) r = mid;
else l = mid+1;
}
if(arr[l]!=k) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l=0,r=n-1;
while(l<r)
{
int mid = l+r+1 >> 1;
if(arr[mid]<=k) l = mid;
else r = mid-1;
}
cout<<r<<endl;
}
}
}

浮点数二分

不考虑边界问题更简单

例题:

给定一个浮点数 n,求它的三次方根。

输入格式

共一行,包含一个浮点数 n。

输出格式

共一行,包含一个浮点数,表示问题的解。
注意,结果保留 66 位小数。

数据范围

−10000≤n≤10000

输入样例

1
1000.00

输出样例

1
10.000000

答案:

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
#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
double n;
cin>>n;
bool isNegtive = false;
if(n<0) {
isNegtive = true;
n = n*-1.0;
}
double l=0,r=max(1.0,n);
while((r-l)>1e-8)
{
double mid = (l+r)/2;
if(mid*mid*mid>=n){
r = mid;
}
else l = mid;
}
if(!isNegtive) printf("%lf\n",l);
else printf("%lf\n",-1*l);

return 0;
}

高精度算法

高精度加法

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
#include <iostream>
#include <vector>

using namespace std;

const int N = 1e6+10;

vector<int> add(vector<int>&A,vector<int>&B)
{
vector<int> C;
int t = 0;
for (int i = 0; i<A.size()||i<B.size(); i ++ ){
if(i<A.size()) t+=A[i];
if(i<B.size()) t+=B[i];
C.push_back(t%10);
t/=10;//上一位的进位
}
if(t) C.push_back(1);
return C;
}

int main()
{
string a,b;
vector<int> A,B;

cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}
for(int i = b.size()-1;i>=0;i--){
B.push_back(b[i]-'0');
}

auto C = add(A,B);

for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
return 0;
}

高精度减法

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
#include <iostream>
#include <vector>

using namespace std;

bool cmp(vector<int>&A,vector<int>&B){
if(A.size()!=B.size()) return A.size()>B.size();
else {
for(int i=A.size()-1;i>=0;i--){
if(A[i]!=B[i]) return A[i]>B[i];
}
}
return true;
}

vector<int> sub(vector<int>&A,vector<int>&B)
{
//要求——A>=B
//if A>=B 直接计算A-B
//else 计算-(B-A)

vector<int> C;
int t = 0;
for (int i = 0; i<A.size(); i ++ ){
t = A[i] - t;
if(i<B.size()) t -= B[i];
C.push_back((t+10)%10);
if(t<0) t = 1;
else t = 0;
}
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}

int main()
{
string a,b;
vector<int> A,B;

cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}
for(int i = b.size()-1;i>=0;i--){
B.push_back(b[i]-'0');
}
if(cmp(A,B)){
auto C = sub(A,B);
for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
}
else{
auto C = sub(B,A);
printf("-");
for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
}

return 0;
}

高精度乘法

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
#include <iostream>
#include <vector>

using namespace std;

vector<int> mul(vector<int> A , int b)
{
vector<int> C;
int t = 0;
for(int i=0;i < A.size() || t ; i++){
if(i<A.size()){
t += A[i] * b;
}
C.push_back(t % 10);
t /= 10;
// C.push_back((A[i] * b + t) % 10);
// t = (A[i] * b + t) / 10;
}
while(C.size()>1 && C.back()==0) C.pop_back();
// 去除前导0
return C;
}

int main()
{
string a;
int b;
vector<int> A;
cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}

auto C = mul(A,b);

for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
}

高精度除法

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
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> div(vector<int> &A, int b ,int &r){
vector<int> C;// 商
r = 0;// 余数
// 从最高位开始算--不同于其他几种计算
for(int i = A.size() - 1;i >= 0 ; i--){
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}

reverse(C.begin() ,C.end());

while(C.size()>1 && C.back()==0) C.pop_back();
// 去除前导0
return C;
}

int main()
{
string a;
int b;
vector<int> A;
cin>>a>>b;
for(int i = a.size()-1;i>=0;i--){
A.push_back(a[i]-'0');
}

int r;
auto C = div(A,b,r);

for(int i = C.size()-1;i>=0;i--){
printf("%d",C[i]);
}
cout<<endl;
cout<<r<<endl;
}

前缀和

一维前缀和

  • 更新 S[i] = S[i-1] + a[i]
  • 计算a[l,r] ans = S[r] - S[l-1]
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
#include <iostream>
using namespace std;

const int N = 100010;
int a[N],S[N];

int main()
{
int n,m;
scanf("%d%d", &n, &m);

for (int i = 1;i<=n;i++) scanf("%d", &a[i]);

S[0] = 0;
for(int i = 1;i<=n;i++){
S[i] = S[i-1] + a[i];
}
for (int i = 0;i < m;i++){
int l,r;
scanf("%d%d", &l, &r);
printf("%d\n",S[r]-S[l-1]);
}

return 0;

}

二维前缀和(子矩阵的和)

  • 更新 S[i][j] =a[i][j] + S[i][j-1] + S[i-1][j] - S[i-1][j-1]
  • 计算(x1,y1)(x2,y2) ans = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]
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
#include <iostream>
#include <cstring>
#include <algorithm>

const int N = 1010;
int a[N][N],S[N][N];

int main()
{
int n,m,q;
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
scanf("%d",&a[i][j]);
}
}
S[0][0] = 0;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
S[i][j] =a[i][j] + S[i][j-1] + S[i-1][j] - S[i-1][j-1];
}
}

int x1,x2,y1,y2;
while(q--){
scanf("%d%d%d%d", &x1, &y1 , &x2, &y2);
int ans = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1];
printf("%d\n",ans);
}

return 0;
}

一维差分

  • 输入一个长度为 n 的整数序列。

    接下来输入 m个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

    请你输出进行完所有操作后的序列。

  • 前缀和的逆运算

  • 构造 等同于 操作(i,i,a[i])

  • 操作等价于

    • b[l] += c;
    • ``b[r+1] -= c;`
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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int a[N],b[N];

void op(int l,int r,int c){
b[l] += c;
b[r+1] -= c;
}

int main()
{
int n,m;
scanf("%d%d", &n, &m);
for(int i=1;i<=n;i++){
scanf("%d", &a[i]);
}
for(int i=1;i<=n;i++){
op(i,i,a[i]);
}

/*接下来输入 m个操作,
* 每个操作包含三个整数 l,r,c
* 表示将序列中 [l,r]之间的每个数加上 c
*/
while (m--) {
int l,r,c;
scanf("%d%d%d", &l, &r, &c);
op(l,r,c);
}

for (int i = 1; i <= n; i++){
b[i] += b[i-1];
}
for (int i = 1; i <= n; i++){
printf("%d ",b[i]);
}

return 0;


}

二维差分

  • 输入一个 n 行 m列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c
    其中 (x1,y1)和 (x2,y2)表示一个子矩阵的左上角坐标和右下角坐标。

    每个操作都要将选中的子矩阵中的每个元素的值加上 c

    请你将进行完所有操作后的矩阵输出。

  • 操作等价于

    • b[x1][y1] += c
    • b[x2+1][y1] -= c
    • b[x1][y2+1] -= c
    • b[x2+1][y2+1] += c
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
#include <iostream>
#include <cstring>
#include <algorithm>

const int N = 1010;
int a[N][N],b[N][N];

void op(int x1,int x2,int y1,int y2,int c){
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;
}

int main()
{
int n,m,q;
scanf("%d%d%d", &n, &m, &q);

for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
scanf("%d",&a[i][j]);
}
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
op(i,i,j,j,a[i][j]);
}
}

int x1,x2,y1,y2,c;
while(q--){
scanf("%d%d%d%d%d", &x1, &y1 , &x2, &y2, &c);
op(x1,x2,y1,y2,c);
}

for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
b[i][j] = b[i][j] + b[i][j-1] + b[i-1][j] - b[i-1][j-1];
}
}

for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
printf("%d ",b[i][j]);
}
puts("");
}


return 0;
}

双指针算法

  • 基本模版

    1
    2
    3
    4
    5
    6
    for(i = 0,j = 0;i < n;i++){
    while(j<i && check(i,j))
    j++;

    // ......
    }
  • 核心思想

    将O(n^2^)算法优化到O(n)

799.最长连续不重复子序列

给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

输入格式

第一行包含整数 n。
第二行包含 n 个整数(均在 0∼1050∼105 范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围

1≤n≤105

code

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
#include <iostream>
#include <cstring>
#include <algorithm>


using namespace std;

const int N = 1e5 + 10;
int n;
int a[N],s[N];


int main()
{
scanf("%d", &n);
for(int i = 0;i < n;i++) {
scanf("%d", &a[i]);
}
int maxLen = 0;
for (int i = 0,j = 0; i < n; i++){
s[a[i]]++;
while(s[a[i]] > 1){
s[a[j]]--;
j++;
}
maxLen = max(maxLen,i - j + 1);
}
cout << maxLen;
return 0;
}

800.数组元素的目标和

给定两个==升序排序==的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据保证有唯一解。

输入格式
第一行包含三个整数 n,m,x ,分别表示 A 的长度,B 的长度以及目标值 x
第二行包含 n 个整数,表示数组 A
第三行包含 m 个整数,表示数组 B

输出格式
共一行,包含两个整数 i 和 j

数据范围
数组长度不超过 10^5^
同一数组内元素各不相同。
1 ≤ 数组元素 ≤ 10^9^

code

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n,m,x;
int A[N],B[N];


int main()
{
scanf("%d%d%d", &n, &m, &x);
for (int i = 0; i < n; i++) {
scanf("%d", &A[i]);
}
for (int i = 0; i < m; i++) {
scanf("%d", &B[i]);
}
int i, j;
for(i = 0, j = m - 1; i < n; i++) {
while (j >= 0 && A[i] + B[j] > x) j--;
if(j >= 0 && A[i] + B[j] == x) {
cout << i << ' ' << j;
return 0;
}
}
return 0;
}

位运算

n的二进制表示中的第k位是几?

1
2
3
4
int n = 10;
for(int k = 3;k >= 0;k--){
cout << (n>>k & 1);
}

lowbit(x) 返回x的最后一位1

x & -x = x & (~x + 1)

eg:

x = 1010……..==1==000……0
x = 0101……..==0==111……1
~x + 1 = 0101……..==1==000……0
x & (
x + 1) = 0000……..==1==000……0

1
2
int x;
cout << (x & -x);

801.二进制中1的个数

给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。

code

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int lowbit(int x) // 返回末尾的1
{
return x & -x;
}

int main()
{
int n;
cin >> n;
while (n--){
int x;
cin>>x;
int res = 0;
while(x){
x -= lowbit(x);
res++;
}
cout<<res<<' ';
}
return 0;
}

(整数)离散化

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}

802.区间和 难☹️

假定有一个无限长的数轴,数轴上每个坐标上的数都是 0
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和

输入格式

第一行包含两个整数 n 和 m
接下来 n 行,每行包含两个整数 x 和
再接下来 m 行,每行包含两个整数 l 和 r

输出格式

共 m 行,每行输出一个询问中所求的区间内数字和。

数据范围

−10^9^≤x≤10^9^,
1≤n,m≤10^5^
−10^9^≤l≤r≤10^9^
−10000≤c≤10000

code

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
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;
int n,m;
int a[N],s[N];

vector<int> alls;
vector<PII> add, query;

int find(int x) // 二分
{
int l = 0, r = alls.size() - 1;
while(l<r){
int mid = l+r >> 1;
if(alls[mid] >= x){
r = mid;
}
else{
l = mid + 1;
}
}
return r + 1;
}


int main()
{
scanf("%d%d", &n, &m);
while (n--) {
int x,c;
scanf("%d%d", &x, &c);
add.push_back({x,c});

alls.push_back(x);
}
while (m--) {
int l,r;
scanf("%d%d", &l, &r);
query.push_back({l,r});
alls.push_back(l);
alls.push_back(r);
}

// 排序 & 去重
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(), alls.end()),alls.end());
// 处理插入
for(auto item : add){
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for(int i = 1;i <= alls.size(); i++){
s[i] = s[i-1] + a[i];
}
// 处理询问
for(auto item : query){
int l = find(item.first);
int r = find(item.second);
cout << s[r] - s[l-1] << endl;
}
return 0;
}

注意:

  1. vector<int> alls 中存的是需要离散化的下标

  2. const int N = 300010; 查询最多10^5^次,插入最多10^5^次,最多需要离散化2*查询+插入 = 3*10^5^次

  3. 实现unique函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    vector<int>::iterator unique(vector<int> &a)
    {
    for(int i = 0;i < a.size(); i++){
    if(!i || a[i]!=a[i-1]){
    a[j++] = a[i];
    }
    }
    return a.begin() + j;
    }

    alls.erase(unique(alls.begin(), alls.end()),alls.end());改为

    alls.erase(unique(alls),alls.end());

区间合并

  1. 按区间左端点排序
  2. 扫描整个区间,把所有可能有交集的区间进行合并

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;

sort(segs.begin(), segs.end());

int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);

if (st != -2e9) res.push_back({st, ed});

segs = res;
}

803. 区间合并

给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。

Eg. [1,3] & [2,6] -> [1,6]

输入格式

第一行包含整数 n。
接下来 n 行,每行包含两个整数 l 和 r。

输出格式

共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围

1 ≤ n ≤ 100000
−10^9^ ≤ li ≤ ri ≤ 10^9^

code

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
typedef pair<int,int> PII;

vector<PII> segs;

void merge(vector<PII> &segs){
vector<PII> res;

sort(segs.begin(),segs.end());

int st = -2e9, ed = -2e9;
for(auto seg : segs){
if(ed < seg.first){
if(st!= -2e9) res.push_back({st,ed});
st = seg.first;
ed = seg.second;
}
else ed = max(ed, seg.second);
}
if(st!= -2e9) res.push_back({st,ed});

segs = res;
}

int main()
{
int n;
scanf("%d", &n);
while (n--) {
int l,r;
scanf("%d%d", &l, &r);
segs.push_back({l,r});
}

merge(segs);

cout << segs.size() << endl;

return 0;

}

Chapter2 数据结构

链表

笔试中一般不采用动态链表

单链表

826. 单链表

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的数;
  3. 在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意: 题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x。
  2. D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
  3. I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 00)。

输出格式

共一行,将整个链表从头到尾输出。

code

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

/* head存储链表头,
e[i]存储节点i的值,
ne[i]存储节点i的next指针,
idx表示当前用到了哪个节点
*/
int head, e[N], ne[N];
int idx;

void init() // 初始化
{
head = -1;
idx = 0;
}

void insert_head(int x) // 插入头节点
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}

void insert(int k, int x) // 在k坐标后插入节点(值为x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}

void remove_head() // 删除头节点head
{
head = ne[head];
}

void remove(int k) // 删除下标k后的点
{
ne[k] = ne[ne[k]];
}

int main()
{
int M;
scanf("%d", &M);
init();
while (M--) {
int k, x;
char op;

cin >> op;
if (op == 'H') {
cin >> x;
insert_head(x);
}
else if (op == 'D') {
cin >> k;
if (k == 0) {
remove_head();
}
remove(k - 1);
}
else {
cin >> k >> x;
insert(k - 1,x);
}
}

for(int i = head; i != -1; i = ne[i]){
cout << e[i] << ' ';
}
cout << endl;

return 0;
}

双链表

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
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
827. 双链表

实现一个双链表,双链表初始为空,支持 55 种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k 个插入的数左侧插入一个数;
  5. 在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x。
  2. R x,表示在链表的最右端插入数 x。
  3. D k,表示将第 k 个插入的数删除。
  4. IL k x,表示在第 k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k 个插入的数右侧插入一个数。

输出格式

共一行,将整个链表从左到右输出。

数据范围

1 ≤ M ≤ 100000
所有操作保证合法。

code

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 <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int m;
int e[N], l[N], r[N], idx;

//初始化
//0号点:head pointer
//1号点:tail pointer
void init()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}

//在k号点右边边插入值为x的点
void insert_right(int k, int x)
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx;
idx++;
}

//在k号点左边插入值为x的点:直接实现
void insert_left(int k, int x)
{
e[idx] = x;
l[idx] = l[k];
r[idx] = k;
r[l[k]] = idx;
l[k] = idx;
idx++;
}

//在k号点左边插入值为x的点:调用法
void insert_left_call(int k, int x)
{
insert_right(l[k], x);
}

//删除第k个点
void delete_k(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}


int main()
{
scanf("%d", &m);
init();
while (m--){
string op;
cin >> op;
if(op == "L"){
int x;
cin >> x;
insert_right(0, x);
}
else if(op == "R"){
int x;
cin >> x;
insert_left(1, x);
}
else if(op == "D"){
int k;
cin >> k;
delete_k(k + 1);
}
else if(op == "IL"){
int k, x;
cin >> k >> x;
insert_left(k + 1, x);
}
else if(op == "IR"){
int k, x;
cin >> k >> x;
insert_right(k + 1, x);
}
}

for(int i = r[0]; i != 1; i = r[i]){
cout << e[i] << " ";
}
cout << endl;

return 0;
}

code

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int stk[N], tt;

int m;

void init()
{
tt = 0;
}

void push(int x)
{
stk[++tt] = x;
}

void pop()
{
tt--;
}

bool isEmpty()
{
if(tt > 0)
return false;
else
return true;
}

int query()
{
return stk[tt];
}

int main()
{
scanf("%d", &m);
init();
while(m--){
string op;
cin >> op;
if(op == "push"){
int x;
cin >> x;
push(x);
}
else if(op == "pop"){
pop();
}
else if(op == "empty"){
string e;
e = isEmpty() ? "YES" : "NO";
cout << e << endl;
}
else if(op == "query"){
cout << query() << endl;
}
}
return 0;
}

队列

队尾插入,队头推出

code

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
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt;

void init(){
tt = -1;
hh = 0;
}

void push(int x){
q[++tt] = x;
}

void pop(){
hh++;
}

bool isEmpty(){
return hh <= tt ? false : true;
}

int query(){
return q[hh];
}

int main()
{
scanf("%d", &m);
init();
while(m--){
string op;
cin >> op;
if(op == "push"){
int x;
cin >> x;
push(x);
}
else if(op == "pop"){
pop();
}
else if(op == "empty"){
string e;
e = isEmpty() ? "YES" : "NO";
cout << e << endl;
}
else if(op == "query"){
cout << query() << endl;
}
}
return 0;
}

单调栈

830. 单调栈

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1 ≤ N ≤ 10^5^
1 ≤ 数列中元素 ≤ 10^9^

样例

3 4 2 7 5
-1 3 -1 2 2

code

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
#include <iostream>

using namespace std;

const int N = 100010;

int n;
int stk[N], tt;

int main()
{
scanf("%d", &n);
while (n--){
int x;
cin >> x;
while(tt && stk[tt] >= x) tt--;
if(tt)
// cout << stk[tt] << ' ';
printf("%d ",stk[tt]);
else
// cout << -1 << ' ';
printf("-1 ");

stk[++tt] = x;
}
}

单调队列

154. 滑动窗口——难

给定一个大小为 n≤10^6^ 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例

1
2
8 3
1 3 -1 -3 5 3 6 7

输出样例

1
2
-1 -3 -3 -3 3 3
3 3 5 5 6 7

code

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
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1000010;

int n, k;
int a[N], q[N], tt, hh;

int main()
{
scanf("%d%d", &n, &k);

for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}

//最小值
hh = 0;
tt = -1;
for (int i = 0; i < n; i++) {

// 判断队头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]){
hh++;
}
while(hh <= tt && a[q[tt]] > a[i]){
tt--;
}
q[++tt] = i;
if(i >= k-1){
printf("%d ", a[q[hh]]);
}
}
printf("\n");
//最大值
hh = 0;
tt = -1;
for (int i = 0; i < n; i++) {

// 判断队头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]){
hh++;
}
while(hh <= tt && a[q[tt]] <= a[i]){
tt--;
}
q[++tt] = i;
if(i >= k-1){
printf("%d ", a[q[hh]]);
}
}
return 0;
}

理解

image-20230721205330280

init() : tt = -1, hh = 0; // 此时hh>tt 队空

窗口应该在(i-k+1)到i范围内

  • 出队判定 : 队列不为空& 队头下标 q[hh] ≥ i - k + 1
  • (i-k+1)到i范围内 ,如果==队尾下标对应值比i下标对应值还要大==就丢弃(此时是以(i-k+1)到i为窗口范围,但实际i还未入队)
  • i下标入队 q[++tt] = i;

Leetcode

剑指 Offer 59 - I. 滑动窗口的最大值

KMP算法
Read More

2023-11-15
CG-Assignment2

本文为计算机图形学作业2报告, 为本人计算机图形学课程作业, 仅供参考, 未经允许不得转载, 抄袭.

1.引言

内容应包含但不局限于项目名称,项目简介。篇幅占整体内容的10%

本次作业在第一次作业的基础上,增加一个bezier曲面,并对场景添加光照和纹理效果。具体要求如下:

  1. 以bezier曲面模拟一面旗帜,曲面至少包含5*5个控制点;

  2. 在场景中使用phong光照模型来得到合理的光照效果;

  3. 对场景中的模型添加纹理贴图, 图片自行选择,不同类型的模型采用不同的贴图。

2. 主体介绍

2.1 程序简介

  1. 编程环境

    MacOS,C++11,IDE: CLion,cmake,使用OpenGL,Glad,GLFW3.3.8,glm0.9.9.1

  2. 文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    (base) alanzeng@AlandeMacBook-Pro CGA2 % tree
    CGA2
    ├── 1.model_loading.fs
    ├── 1.model_loading.vs
    ├── BezierFace.cpp
    ├── CMakeLists.txt
    ├── OBJ_Files
    ├── TJU.jpg
    ├── cmake-build-debug
    ├── glad
    ├── glm-0.9.9.1
    ├── includes
    │   ├── BezierFace.h
    │   ├── camera.h
    │   ├── shader.h
    │   └── stb_image.h
    └── model_loading.cpp

2.2 算法

在这一部分需要描述你的项目实现过程中用了哪些算法,需要你从算法背后的原理入手结合自己的项目进行描述,切忌段落式的粘贴代码,如果是自己写的算法,进行适当的算法复杂度分析。
示例:a) Lambert 漫反射光照模型 b) 正投影相机 c) Phong光照模型 d) 一些图形的绘制举例 e) 碰撞测试特例算法

注:一定要结合自己的项目去写这一部分,落到实处,切忌直接从网上直接粘贴理论知识。

Phong光照模型

Phong光照模型是计算三维计算机图形中光照效果的经典方法之一,它被广泛用于渲染三维场景和物体。Phong光照模型考虑了三种主要的光照成分:环境光、漫反射光和镜面光。

  1. 环境光(Ambient Light): 环境光是物体表面上的均匀光照,不依赖于光源的方向。它用于模拟场景中光源以外的间接光照明。环境光通常是一个常数,用来表示整体光照的强度。

  2. 漫反射光(Diffuse Light): 漫反射光是光线以角度入射到物体表面并均匀反射的光。它依赖于光线入射角和物体表面的法线(法向量),这个成分使物体的表面看起来具有粗糙的外观。漫反射光的强度在表面上不均匀,取决于入射角度和法向量之间的夹角。

  3. 镜面光(Specular Light): 镜面光是光线以特定角度入射到物体表面并以同样的角度反射的光,类似于镜面反射。这个成分使物体的表面看起来光滑,具有高光亮度。镜面光的强度取决于入射光线的方向、视线方向和物体的材质属性。

Phong光照模型可以表示为以下方程:

$$
I = k_a \cdot I_a + k_d \cdot I_d \cdot (N \cdot L) + k_s \cdot I_s \cdot (R \cdot V)^n
$$

其中:

  • $I$ 是最终的光照强度。
  • $k_a$、$k_d$、$k_s$ 分别是环境光、漫反射光和镜面光的系数。
  • $I_a$、$I_d$、$I_s$ 分别是环境光、漫反射光和镜面光的颜色强度。
  • $N$ 是表面法向量。
  • $L$ 是入射光线的方向向量。
  • $R$ 是反射光线的方向向量。
  • $V$ 是视线方向向量。
  • $n$ 是材质的镜面反射指数。

在片段着色器中, 运用Phong光照模型进行计算

片段着色器中定义了Material(材质)和Light(光照)结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};

struct Light {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;

vec3 ambient;
vec3 diffuse;
vec3 specular;

float constant;
float linear;
float quadratic;
};

具体算法实现

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
void main()
{
// ambient
vec3 ambient = light.ambient * texture(material.diffuse, TexCoords).rgb;

// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, TexCoords).rgb;

// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * (spec * material.specular);

// spotlight (soft edges)
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = (light.cutOff - light.outerCutOff);
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
diffuse *= intensity;
specular *= intensity;

// attenuation
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;

vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}

BezierFace

贝塞尔曲面(Bezier Surface)是一种用于三维图形和计算机辅助设计(CAD)中的曲面表示方法。它是由法国工程师皮埃尔·贝塞尔(Pierre Bézier)于20世纪50年代发明的,并在汽车设计领域广泛应用。贝塞尔曲面由多个控制点定义,这些点控制了曲面的形状。它是一种常用的二次和三次曲面建模技术。ß

贝塞尔曲面的定义基于控制点和基函数。控制点是用来确定曲面的形状的点,而基函数是一种数学函数,它们根据控制点的位置来计算曲面上的点。一般来说,贝塞尔曲面是由两个参数u和v来定义的,这些参数的范围通常是从0到1。

一个常见的二次贝塞尔曲面可以用以下公式来表示:

$$
P(u, v) = (1-u)^2 * (1-v) * P0 + 2 * (1-u) * u * (1-v) * P1 + u^2 * (1-v) * P2\ + (1-u)^2 * v * P3 + 2 * (1-u) * u * v * P4 + u^2 * v * P5 +\ (1-u)^2 * (1-v)^2 * P6 + 2 * (1-u) * u * (1-v)^2 * P7 + u^2 * (1-v)^2 * P8
$$

其中,P0到P8是控制点的坐标,(u, v)是曲面上的点坐标。这个公式描述了如何根据控制点的位置以及参数u和v的值来计算曲面上的点。

贝塞尔曲面的优点之一是它在控制点的位置和权重上具有直观性,可以通过移动控制点来调整曲面的形状。此外,贝塞尔曲面是局部控制的,这意味着一个控制点的改变只会影响曲面上的局部区域。

在计算机图形中,贝塞尔曲面通常用于建模、渲染和处理复杂的曲面形状,例如汽车外壳、船体、飞机机身等。

本次实验中,我们要构建一个由$5\times5$控制点构成的贝塞尔曲面, 具体的实现请见2.3.3 BezierFace.h

纹理贴图

纹理贴图是计算机图形学中强大且广泛应用的技术,它可以增强三维场景的真实感和视觉质量。不同类型的纹理和纹理技术可以用来模拟各种不同的视觉效果,从基本的颜色贴图到复杂的环境映射和法线贴图。

在本次实验中我们使用了stb_image库来加载贴图, 通过一下方法来具体实现纹理贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);

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_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

int width, height, nrComponents;
stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
std::cout << "Texture failed to load at path: " << path << std::endl;
}
stbi_image_free(data);
return textureID;

2.3 实现细节

在这一部分,将自己的项目拆解成各个部分进行说明。切忌段落式的粘贴代码。

第二章应占整体篇幅的40%。

2.3.1 shader.h

与上课时给出的shader.h代码相同

使用C++文件流读取着色器内容,储存到几个string对象里

以下实现了对着色器的编译和链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);

定义了

1
void use() const { glUseProgram(ID); }

2.3.2 camera.h

提供ProcessKeyboard, ProcessMouseMovement, ProcessMouseScroll方法,用于实现鼠标键盘控制

Camera 类 定义了位置,接收变化的变量,欧拉角,选项和构造函数,此外还有键盘鼠标操作函数和相机更新函数

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
class Camera
{
public:
// camera Attributes
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// euler Angles
float Yaw;
float Pitch;
// camera options
float MovementSpeed;
float MouseSensitivity;
float Zoom;

// constructor with vectors
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
float yaw = YAW, float pitch = PITCH) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = position;
WorldUp = up;
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
// constructor with scalar values
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = glm::vec3(posX, posY, posZ);
WorldUp = glm::vec3(upX, upY, upZ);
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}

// returns the view matrix calculated using Euler Angles and the LookAt Matrix
glm::mat4 GetViewMatrix()
{
return glm::lookAt(Position, Position + Front, Up);
}
};

ProcessKeyboard处理从任何类似键盘的输入系统接收的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ProcessKeyboard(Camera_Movement direction, float deltaTime)
{
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
if (direction == DOWN)
Position -= Up * velocity;
if (direction == UP)
Position += Up * velocity;
}

ProcessMouseMovement处理从鼠标输入系统接收的输入。期望x和y方向上的偏移值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
{
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;

Yaw += xoffset;
Pitch += yoffset;

// make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch)
{
if (Pitch > 89.0f)
Pitch = 89.0f;
if (Pitch < -89.0f)
Pitch = -89.0f;
}

// update Front, Right and Up Vectors using the updated Euler angles
updateCameraVectors();
}

ProcessMouseScroll处理从鼠标滚轮事件接收的输入。只需要在垂直轮轴上输入

1
2
3
4
5
6
7
8
void ProcessMouseScroll(float yoffset)
{
Zoom -= (float)yoffset;
if (Zoom < 1.0f)
Zoom = 1.0f;
if (Zoom > 45.0f)
Zoom = 45.0f;
}

updateCameraVectors

1
2
3
4
5
6
7
8
9
10
11
12
void updateCameraVectors()
{
// calculate the new Front vector
glm::vec3 front;
front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
front.y = sin(glm::radians(Pitch));
front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
Front = glm::normalize(front);
// also re-calculate the Right and Up vector
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}

2.3.3 BezierFace.h

定义类贝塞尔曲面类, 其中包含顶点个数, 索引个数, 顶点, 法线, 纹理, 索引, 控制点, 曲线阶数等等属性

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
#pragma once
#include <vector>
#include <glm/glm.hpp>
using namespace std;

class BezierFace
{
//顶点个数
int numVertices;
//索引个数
int numIndices;
//顶点
vector<glm::vec3> vertices;
//法线
vector<glm::vec3> normals;
//纹理
vector<glm::vec2> texCoords;
//索引
vector<int> indices;
//计算数据
void init(int prec);
//控制点
float* controlPoints;
vector<glm::vec3> controlPointsVector;
//曲线阶数
int step;
float toRadians(float degrees);
float Bernstein(float u, int index);
public:
BezierFace() = default;
BezierFace(int step, float controlPoints[], int prec);
int getNumVertices();
int getNumIndices();
vector<glm::vec3> getVertices();
vector<glm::vec3> getNormals();
vector<glm::vec2> getTexCoords();
vector<int> getIndices();

};
构造函数
1
2
3
4
5
6
BezierFace::BezierFace(int step, float controlPoints[], int prec)
{
this->step = step;
this->controlPoints = controlPoints;
init(prec);
}
void init(int prec)具体实现(在BezierFace.cpp中)

该函数实现了Bezier曲面初始化

  • 初始化参数

    • prec(precision):这个参数控制曲面的精度,它影响了生成的顶点密度。我们使用 (prec + 1) * (prec + 1) 个顶点和 prec * prec * 6 个索引。
  • 数据初始化

    • 我们首先初始化了用于存储顶点、法线、和纹理坐标的容器 verticesnormalstexCoords,以及存储索引的容器 indices。这些容器将在后续的计算中使用。
  • 控制点

    • 控制点是定义Bezier曲面的关键元素,它们存储在 controlPoints 数组中。我们将这些控制点提取出来并存储在 controlPointsVector 容器中,以便后续计算使用。
  • 计算顶点

    • 在计算Bezier曲面上的顶点时,我们使用了嵌套的循环,遍历 (i, j) 的参数空间,其中 ij 分别表示沿 u 和 v 方向的参数值。对于每个 (i, j) 坐标点,我们使用贝塞尔曲面的公式计算了 x、y 和 z 坐标,并将它们存储在 vertices 中。
  • 计算法线和纹理坐标

    • 在这段代码中,法线的计算似乎与顶点坐标相同。然而,在实际应用中,法线通常需要根据曲面的几何特性来计算。同样,纹理坐标也在这里计算并存储在 texCoords 中。
  • 计算索引

  • 为了定义Bezier曲面的三角形片元,我们使用了嵌套循环来计算索引。这些索引存储在 indices 中,以便在渲染时使用。

这个初始化过程将为Bezier曲面提供一个网格,其中包含离散的顶点、法线和纹理坐标,以及定义曲面几何的索引。在实验的后续部分,我们将使用这些数据来渲染Bezier曲面并展示其效果。

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
void BezierFace::init(int prec)
{
numVertices = (prec + 1) * (prec + 1);
numIndices = prec * prec * 6;
for (int i = 0; i < numVertices; i++)
{
vertices.push_back(glm::vec3());
normals.push_back(glm::vec3());
texCoords.push_back(glm::vec2());
}
for (int i = 0; i < numIndices; i++)
{
indices.push_back(0);
}
for (int i = 0; i < (step + 1) * (step + 1) * 3; i += 3)
{
controlPointsVector.push_back(glm::vec3(controlPoints[i], controlPoints[i + 1], controlPoints[i + 2]));
}
for (int i = 0; i <= prec; i++)
{
for (int j = 0; j <= prec; j++)
{
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float u = (float)i / prec;
float v = (float)j / prec;

for (int ii = 0; ii <= step; ii++)
{
for (int jj = 0; jj <= step; jj++)
{
int index = ii * (step + 1) + jj;
x += controlPointsVector[index].x * Bernstein(u, ii) * Bernstein(v, jj);
y += controlPointsVector[index].y * Bernstein(u, ii) * Bernstein(v, jj);
z += controlPointsVector[index].z * Bernstein(u, ii) * Bernstein(v, jj);
}
}
vertices[i * (prec + 1) + j] = glm::vec3(x, y, z);
normals[i * (prec + 1) + j] = glm::vec3(x, y, z);
texCoords[i * (prec + 1) + j] = glm::vec2((float)j / prec, (float)i / prec);
}
}
//计算索引
for (int i = 0; i < prec; i++) {
for (int j = 0; j < prec; j++) {
indices[6 * (i * prec + j) + 0] = i * (prec + 1) + j;
indices[6 * (i * prec + j) + 1] = i * (prec + 1) + j + 1;
indices[6 * (i * prec + j) + 2] = (i + 1) * (prec + 1) + j;
indices[6 * (i * prec + j) + 3] = i * (prec + 1) + j + 1;
indices[6 * (i * prec + j) + 4] = (i + 1) * (prec + 1) + j + 1;
indices[6 * (i * prec + j) + 5] = (i + 1) * (prec + 1) + j;
}
}
}
Bernstein函数

初始化函数中可以看到其中使用了Bernstein函数, 在Bezier曲线和Bezier曲面的计算中,Bernstein基函数(Bernstein basis functions)起着重要的作用。它们是一组多项式函数,用于确定Bezier曲线和曲面上的点的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
float BezierFace::Bernstein(float t, int index) {
switch (index)
{
default:
case 0:
return pow(1.0 - t, 4);
break;
case 1:
return 4 * t * pow(1.0 - t, 3);
break;
case 2:
return 6 * pow(t, 2) * pow(1.0 - t, 2);
break;
case 3:
return 4 * pow(t, 3) * (1.0 - t);
break;
case 4:
return pow(t, 4);
break;
}
}

2.3.4 Model模型

具体实现和实验一中完全一致, 不再赘述

2.3.5 Mesh

具体实现和实验一中完全一致, 不再赘述

2.3.6 model_loading(Main方法)

主方法

加载Shader,Model,Camera
  • Camera camera(glm::vec3(0.0f, 0.0f, 10.0f));
  • Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
  • Model ourModel("../OBJ_Files/airboat.obj");
鼠标键盘操作基于实验一, 这里不再赘述
  1. 首先引用相关的库, 其中通过#define STB_IMAGE_IMPLEMENTATIONstb_image.h编译为cpp文件使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <stb_image.h>


#include <shader.h>
#include <camera.h>
#include <model.h>
#include <mesh.h>
#include <BezierFace.h>

#include <iostream>
  1. 定义函数, 和一些基本参数

    • 包含鼠标, 键盘控制函数, 对于贝塞尔曲面顶点的加载, 纹理的加载
    1
    2
    3
    4
    5
    6
    7
    void mouse_callback(GLFWwindow* window, double xpos, double ypos);
    void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
    void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
    void processInput(GLFWwindow *window);
    void setUpVertices();
    unsigned int loadTexture(char const* path);
    void framebuffer_size_callback(GLFWwindow* window, int width, int height);
    • 基本参数: 窗口大小, 相机, 光照, 时间以及贝塞尔曲面控制点位置
      • 此处控制点数组存有75个元素, 对应25(5*5)个控制点
    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
    // settings
    const unsigned int SCR_WIDTH = 800;
    const unsigned int SCR_HEIGHT = 600;

    // camera
    Camera camera(glm::vec3(0.5f, 0.5f, 3.5f));
    float lastX = SCR_WIDTH / 2.0f;
    float lastY = SCR_HEIGHT / 2.0f;
    bool firstMouse = true;

    // timing
    float deltaTime = 0.0f;
    float lastFrame = 0.0f;

    // lighting
    glm::vec3 lightPos(0.5f, 0.5f, 2.0f);

    // Control Points
    float controlPoints[] = {
    -1.5, -1.5, 2.0,
    -0.5, -1.5, 2.0,
    0.0, -1.5, 1.0,
    0.5, -1.5, -1.0,
    1.5, -1.5, 2.0,
    -1.5, -0.5, 1.0,
    -0.5, 1.5, 2.0,
    0.0, 0.5, -1.0,
    0.5, 0.5, 1.0,
    1.5, -0.5, -1.0,
    -1.5, 0.0, 0.0,
    -0.5, -1.5, -1.0,
    0.0, 0.0, 0.0,
    0.5, -1.0, 2.0,
    1.5, 1.0, 0.0,
    -1.5, 0.5, 2.0,
    -0.5, 0.5, 1.0,
    0.0, 0.5, 1.0,
    0.5, 0.5, 1.0,
    1.5, -1.5, 1.5,
    -1.5, 1.5, -2.0,
    -0.5, 1.5, -2.0,
    0.0, 1.5, 2.0,
    0.5, 0.5, 1.0,
    1.5, 1.5, -1.0
    };

    BezierFace myBezier = BezierFace(4, controlPoints, 100);
    GLuint VAO, VBO[3];
  2. 初始化, 加载窗口

    这部分代码固定不变, 同时打开了深度检测

    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
    		// glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    #ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "ModelLoading", NULL, NULL);
    if (window == NULL)
    {
    std::cout << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    glfwSetMouseButtonCallback(window,mouse_button_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
    std::cout << "Failed to initialize GLAD" << std::endl;
    return -1;
    }
    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);
  3. 配置shader和model, 以及贝塞尔曲面的顶点和纹理信息

    这里贴图采用了天津大学的校旗图案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // build and compile shaders
    // -------------------------
    Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
    // load models
    Model ourModel("../OBJ_Files/airboat.obj");
    // setup Vertices
    setUpVertices();
    // load Textures
    unsigned int texture = loadTexture("../TJU.jpg");
  4. 在循环中绘制图

    其中为shader配置了参数, 因为单独添加Bezier曲面, Bezier曲面的信息单独在main函数中加载

    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
    while (!glfwWindowShouldClose(window))
    {
    // per-frame time logic
    // --------------------
    float currentFrame = static_cast<float>(glfwGetTime());
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;

    // input
    // -----
    processInput(window);

    // render
    // ------
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // don't forget to enable shader before setting uniforms
    ourShader.use();
    // be sure to activate shader when setting uniforms/drawing objects
    ourShader.use();
    ourShader.setVec3("light.position", camera.Position);
    ourShader.setVec3("light.direction", camera.Front);
    ourShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
    ourShader.setVec3("viewPos", camera.Position);

    // light properties
    ourShader.setVec3("light.ambient", 0.1f, 0.1f, 0.1f);
    // we configure the diffuse intensity slightly higher; the right lighting conditions differ with each lighting method and environment.
    // each environment and lighting type requires some tweaking to get the best out of your environment.
    ourShader.setVec3("light.diffuse", 0.8f, 0.8f, 0.8f);
    ourShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
    ourShader.setFloat("light.constant", 1.0f);
    ourShader.setFloat("light.linear", 0.09f);
    ourShader.setFloat("light.quadratic", 0.032f);

    // material properties
    ourShader.setFloat("material.shininess", 32.0f);
    ourShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f);


    // view/projection transformations
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    glm::mat4 view = camera.GetViewMatrix();
    ourShader.setMat4("projection", projection);
    ourShader.setMat4("view", view);

    // render the loaded model
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); // translate it down so it's at the center of the scene
    model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
    ourShader.setMat4("model", model);




    model = glm::mat4(1.0f);
    model = glm::translate(model, lightPos);
    model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
    ourShader.setMat4("model", model);
    ourShader.setMat4("projection", projection);
    ourShader.setMat4("view", view);
    ourShader.setMat4("transform", glm::mat4(1.0f));


    //--

    glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glEnableVertexAttribArray(2);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);
    glEnable(GL_CULL_FACE);
    glFrontFace(GL_CCW);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, myBezier.getNumIndices());
    glBindVertexArray(0);

    ourModel.Draw(ourShader);


    // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
    // -------------------------------------------------------------------------------
    glfwSwapBuffers(window);
    glfwPollEvents();
    }
  5. 最后终止

    1
    2
    glfwTerminate();
    return 0;
  6. 对这些函数进行实现

    • void mouse_callback(GLFWwindow* window, double xpos, double ypos);

    • void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);

    • void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);

    • void framebuffer_size_callback(GLFWwindow* window, int width, int height);

    • void processInput(GLFWwindow* window);

    • ==void setUpVertices();==

    • ==unsigned int loadTexture(char const* path);==

      前五个函数处理键盘鼠标操作, 在实验一中有具体实现, 在此不再赘述, 此处展示setUpVertices()loadTexture()方法, 用于实现Bezier曲面

      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
      void setUpVertices()
      {
      vector<float> pvalues;
      vector<float> tvalues;
      vector<float> nvalues;
      vector<int> ind = myBezier.getIndices();
      vector<glm::vec3> verts = myBezier.getVertices();
      vector<glm::vec2> tex = myBezier.getTexCoords();
      vector<glm::vec3> norm = myBezier.getNormals();
      for (int i = 0; i < myBezier.getNumIndices(); i++)
      {
      pvalues.push_back(verts[ind[i]].x);
      pvalues.push_back(verts[ind[i]].y);
      pvalues.push_back(verts[ind[i]].z);
      tvalues.push_back(tex[ind[i]].s);
      tvalues.push_back(tex[ind[i]].t);

      nvalues.push_back(norm[ind[i]].x);
      nvalues.push_back(norm[ind[i]].y);
      nvalues.push_back(norm[ind[i]].z);
      }

      glGenVertexArrays(1, &VAO);
      glGenBuffers(3, VBO);
      glBindVertexArray(VAO);
      glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
      glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);

      glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
      glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);

      glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
      glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);

      }

      unsigned int loadTexture(char const* path)
      {
      unsigned int textureID;
      glGenTextures(1, &textureID);
      glBindTexture(GL_TEXTURE_2D, textureID);

      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_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

      int width, height, nrComponents;
      stbi_set_flip_vertically_on_load(true);
      unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);
      if (data) {
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
      glGenerateMipmap(GL_TEXTURE_2D);
      }
      else {
      std::cout << "Texture failed to load at path: " << path << std::endl;
      }
      stbi_image_free(data);
      return textureID;
      }

3. 结果与讨论

3.1 结果

在这一部分,你可以对你的项目结果适当的进行截图展示和说明。

Bezier曲面, 贴图天津大学校旗图案展示

image-20231106195347713

通过调整相机远近, 可以看到完整模型与Bezier曲面

image-20231106194529608

​ 同时可以通过键盘鼠标操作旋转视角

image-20231106195524997

3.2 讨论

在这一部分,你需要对你在开发过程中遇到的一些困难进行描述和你是如何攻克的,在方法上和结果上展开叙述。
第三章占整体篇幅的30%。

在实现Bezier曲面时,我面临了一些挑战。首先,我需要考虑如何加载和处理控制点。Bezier曲面通常由控制点定义,因此我需要找到一种有效的方式来加载这些控制点。我采取了以下措施来解决这些问题:

  • 数据结构选择:我选择了适当的数据结构,使用二维数组来存储控制点,以便能够轻松地访问和操作它们。
  • 数据输入:我考虑了数据的来源,可以是文件、用户输入或通过程序生成。我实现了文件格式的解析,以便从文件加载控制点。
  • Bezier曲面算法:我深入学习了Bezier曲面的计算算法,以确保我能够正确生成曲面。
  • 纹理贴图:在为Bezier曲面应用纹理贴图时,我必须考虑如何映射纹理坐标到曲面上。这可能涉及到在曲面上创建UV映射或使用其他技术,以确保纹理正确贴合曲面。

整合Bezier曲面和实验一的困难:

整合Bezier曲面和实验一是另一个具有挑战性的任务。最初,我发现两者无法同时显示出来,这可能是因为没有正确地绑定和解绑VAO导致的。为了解决这个问题,我采取了以下措施:

  • VAO和渲染状态:我确保了解OpenGL渲染管线的工作方式,以及如何正确地绑定和解绑VAO。通过及时的绑定和解绑VAO,我成功地整合了Bezier曲面和实验一,使它们能够同时显示。

    1
    2
    3
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, myBezier.getNumIndices());
    glBindVertexArray(0);
  • Shader程序:我确保两者之间的着色器程序设置和变量传递是正确的。在调试过程中,我检查了着色器程序是否编译和链接成功,并确保正确传递了变量和纹理。

  • OpenGL状态管理:我了解了OpenGL的状态管理,包括混合、深度测试、清除颜色缓冲等。在整合两者时,我确保了正确管理OpenGL状态,以避免不必要的问题。

  • 坐标转换和变换:我也考虑到了物体的变换,如旋转、平移或缩放,以确保这些变换适用于Bezier曲面,使它们在同一坐标系中正确呈现。

通过逐步调试和测试,我克服了这些挑战,成功地将Bezier曲面整合到实验一中,以实现更复杂的图形应用程序。这个过程不仅增加了我的对OpenGL和图形编程的理解,还提高了我的解决问题和整合不同组件的能力。这一章的详细描述将有助于读者了解我在实验中面对的挑战以及我是如何成功应对它们。

4. 收获和建议

课程收获,项目开发收获,课程建议等各个方面展开来叙述。内容上要有意义,量上要占整篇报告的10%左右。

在整个课程和项目开发过程中,我获得了许多宝贵的经验和知识,这些经验将对我产生深远的影响。以下是我在这个过程中的主要收获和一些建议:

课程收获:

  • 深入理解计算机图形学:这门课程使我更深入地理解了计算机图形学的核心概念,包括OpenGL渲染管线、着色器编程、纹理映射等。我现在更清楚地知道如何创建和渲染三维图形。
  • 编程技能提升:通过实际的项目开发,我不仅提高了C++编程技能,还学会了如何使用OpenGL进行图形编程。这些技能对我的编程能力产生了积极影响,不仅局限于图形学,还可应用于其他领域。

项目开发收获:

  • Bezier曲面实现:在这个项目中,我成功地实现了Bezier曲面的渲染,这是一个非常有挑战性的任务。我深入了解了Bezier曲面的数学原理,以及如何在OpenGL中实现它。

课程建议:

  • 更多实际练习:尽管理论知识很重要,但更多的实际练习和项目可以更好地巩固学习。建议在课程中增加更多的实际编程和项目任务,以加强学生的实际编程技能。

总的来说,这门课程和项目开发经历为我提供了宝贵的机会,让我深入研究计算机图形学和图形编程。我相信我所获得的知识和技能将对我的学术和职业生涯产生长远的影响,同时也期待在未来的学术和职业发展中继续学习和成长。感谢这个精彩的学习经验!

Read More

2023-10-19
CG-Assignment1

本文为计算机图形学作业2报告, 为本人计算机图形学课程作业, 仅供参考, 未经允许不得转载, 抄袭.

1.引言

本次项目完成作业1,实现了对obj文件的加载, 并对模型进行键盘和鼠标控制

具体实现如下:

  1. 实现obj文件的加载
  2. 对模型加键盘控制,通过键盘可以实现模型的缩放、平移和旋转。
    例如,按压右/左方向键,模型可以向右/左移动。具体按键和其对应的功能自行设定,但需要在报告中详细给出文字说明。
  3. 对相机加鼠标控制,主要包含以下两个功能:
    a. 模型本身保持固定,拖动鼠标左键实现视角 (lookat matrix)的改变
    b. 模型本身保持固定,拖动鼠标右键实现视点(viewpoint position)的移动

2. 主体介绍

2.1 程序简介

  1. 编程环境

    MacOS,C++11,IDE: CLion,cmake,使用OpenGL,Glad,GLFW3.3.8,glm0.9.9.1以及Assimp

  2. 文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Computer_Graphics_Assign1

    ├── model_loading.cpp
    ├── 1.model_loading.fs
    ├── 1.model_loading.vs
    ├── CMakeLists.txt
    ├──
    ├── OBJ_Files
    │ ├── airboat.obj
    │ └── bunny.obj

    ├── includes
    │ ├── camera.h
    │ ├── model.h
    │ ├── shader.h
    │ └── mesh.h
    └── ...Glad & Glm & cmake-build-debug

2.2 算法

在这一部分需要描述你的项目实现过程中用了哪些算法,需要你从算法背后的原理入手结合自己的项目进行描述,切忌段落式的粘贴代码,如果是自己写的算法,进行适当的算法复杂度分析。
示例:a) Lambert 漫反射光照模型 b) 正投影相机 c) Phong光照模型 d) 一些图形的绘制举例 e) 碰撞测试特例算法
注:一定要结合自己的项目去写这一部分,落到实处,切忌直接从网上直接粘贴理论知识。

2.2.1 OBJ文件的加载

首先是对于OBJ文件的加载,我使用了一个非常流行的模型导入库——Assimp

Assimp能够导入很多种不同的模型文件格式,它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,就能够从Assimp的数据结构中提取所需的所有数据。

img
  • 所有的场景/模型数据都包含在Scene对象
  • 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
  • 最终仍要将这些数据转换为OpenGL能够理解的格式,才能渲染这个物体
2.2.2 键盘鼠标控制操作

详细思想和代码实现见Camera和Model_loading处

2.3 实现细节

在这一部分,将自己的项目拆解成各个部分进行说明。切忌段落式的粘贴代码。
第二章应占整体篇幅的40%。

2.3.1 Mesh网格

定义一个顶点结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Vertex {
// position
glm::vec3 Position;
// normal 法线
glm::vec3 Normal;
// texCoords
glm::vec2 TexCoords;
// tangent 切线
glm::vec3 Tangent;
// bitangent 双切线
glm::vec3 Bitangent;
//bone indexes which will influence this vertex
int m_BoneIDs[MAX_BONE_INFLUENCE];
//weights from each bone
float m_Weights[MAX_BONE_INFLUENCE];
};

定义网格类

构造函数导入导入数据,调用setupMesh()方法,在setupMesh()函数中初始化缓冲,并最终使用Draw()函数来绘制网格

构造函数

1
2
3
4
5
6
Mesh(vector<Vertex> vertices, vector<unsigned int> indices)
{
this->vertices = vertices;
this->indices = indices;
setupMesh();
}

其中包含setupMesh()

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
void setupMesh()
{
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// vertex tangent
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
// vertex bitangent
glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
// ids
glEnableVertexAttribArray(5);
glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));
// weights
glEnableVertexAttribArray(6);
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));
glBindVertexArray(0);
}

以及提供Draw(),绘制每个mesh

1
2
3
4
5
6
7
8
void Draw(Shader &shader)
{
// draw mesh
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

}

2.3.2 Model模型

定义Model类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Model 
{
public:
vector<Mesh> meshes;
string directory;
// 构造函数
Model(char *path)
{
loadModel(path);
}
void Draw(Shader shader);
private:
void loadModel(string path);
void processNode(aiNode *node, const aiScene *scene);
Mesh processMesh(aiMesh *mesh, const aiScene *scene);
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type,
string typeName);
};

构造函数

其中包含了一个Mesh对象的vector,构造函数包含loadModel()函数和draw()函数

draw()函数遍历了所有网格,并调用它们各自的Draw函数

1
2
3
4
5
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}

loadModel()函数,需要传入一个文件路径path,之后使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中

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
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

// loadModel
void loadModel(string path)
{
// Assimp抽象掉了加载不同文件格式的所有技术细节,只需要一行代码就能完成所有的工作
// 声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
directory = path.substr(0, path.find_last_of('/'));

processNode(scene->mRootNode, scene);
}

// processNode
void processNode(aiNode *node, const aiScene *scene)
{
// 处理节点所有的网格(如果有的话)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 接下来对它的子节点重复这一过程
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}

之后调用processMesh()函数,这样就可以将从obj模型文件中加载出的数据处理到我设置的Mesh类中

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
Mesh processMesh(aiMesh *mesh, const aiScene *scene) {
// data to fill
vector<Vertex> vertices;
vector<unsigned int> indices;

// walk through each of the mesh's vertices
for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
glm::vec3 vector;
// assimp使用自己的vector类,不能直接转换为glm的vec3类
// 所以首先将数据传输到这个占位符glm::vec3。
// positions
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// normals
if (mesh->HasNormals()) {
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
}
vertices.push_back(vertex);
}
// 遍历每个网格的面(面是一个网格的三角形)并检索相应的顶点索引
for (unsigned int i = 0; i < mesh->mNumFaces; i++) {
aiFace face = mesh->mFaces[i];
for (unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}

return Mesh(vertices, indices);
}

2.3.3 shader.h

与上课时给出的shader.h代码相同

使用C++文件流读取着色器内容,储存到几个string对象里

以下实现了对着色器的编译和链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);

定义了

1
void use() const { glUseProgram(ID); }

2.3.4 camera.h

提供ProcessKeyboard, ProcessMouseMovement, ProcessMouseScroll方法,用于实现鼠标键盘控制

Camera 类 定义了位置,接收变化的变量,欧拉角,选项和构造函数,此外还有键盘鼠标操作函数和相机更新函数

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
class Camera
{
public:
// camera Attributes
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// euler Angles
float Yaw;
float Pitch;
// camera options
float MovementSpeed;
float MouseSensitivity;
float Zoom;

// constructor with vectors
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
float yaw = YAW, float pitch = PITCH) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = position;
WorldUp = up;
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
// constructor with scalar values
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) :
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED),
MouseSensitivity(SENSITIVITY),
Zoom(ZOOM)
{
Position = glm::vec3(posX, posY, posZ);
WorldUp = glm::vec3(upX, upY, upZ);
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}

// returns the view matrix calculated using Euler Angles and the LookAt Matrix
glm::mat4 GetViewMatrix()
{
return glm::lookAt(Position, Position + Front, Up);
}
};

ProcessKeyboard处理从任何类似键盘的输入系统接收的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ProcessKeyboard(Camera_Movement direction, float deltaTime)
{
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
if (direction == DOWN)
Position -= Up * velocity;
if (direction == UP)
Position += Up * velocity;
}

ProcessMouseMovement处理从鼠标输入系统接收的输入。期望x和y方向上的偏移值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
{
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;

Yaw += xoffset;
Pitch += yoffset;

// make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch)
{
if (Pitch > 89.0f)
Pitch = 89.0f;
if (Pitch < -89.0f)
Pitch = -89.0f;
}

// update Front, Right and Up Vectors using the updated Euler angles
updateCameraVectors();
}

ProcessMouseScroll处理从鼠标滚轮事件接收的输入。只需要在垂直轮轴上输入

1
2
3
4
5
6
7
8
void ProcessMouseScroll(float yoffset)
{
Zoom -= (float)yoffset;
if (Zoom < 1.0f)
Zoom = 1.0f;
if (Zoom > 45.0f)
Zoom = 45.0f;
}

updateCameraVectors

1
2
3
4
5
6
7
8
9
10
11
12
void updateCameraVectors()
{
// calculate the new Front vector
glm::vec3 front;
front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
front.y = sin(glm::radians(Pitch));
front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
Front = glm::normalize(front);
// also re-calculate the Right and Up vector
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}

2.3.5 model_loading(Main方法)

主方法

加载Shader,Model,Camera
  • Camera camera(glm::vec3(0.0f, 0.0f, 10.0f));
  • Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
  • Model ourModel("../OBJ_Files/airboat.obj");
鼠标键盘操作

processInput (定义了键盘按键对应上下左右前后操作) 见processInput(GLFWwindow *window)

  • W上/ S下/ A左/ D右/ Q前/ E后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);

if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(UP, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(DOWN, deltaTime);
}

mouse_button_callback, mouse_callback, scroll_callback 实现了鼠标的操作

  • mouse_button_callback是检测鼠标点击左键右键时候对相机的更新操作

  • mouse_callback是检测到鼠标移动时对相机的更新操作

  • ==这里定义了两个bool值来判断,只有在按压左键/右键时才对鼠标的移动进行处理==

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
bool leftMouseButtonDown = false;
bool rightMouseButtonDown = false;

void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
if (leftMouseButtonDown) {
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);

if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}

float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

lastX = xpos;
lastY = ypos;

camera.ProcessMouseMovement(xoffset, yoffset);
}

if (rightMouseButtonDown) {
float xpos = static_cast<float>(xposIn);
float ypos = static_cast<float>(yposIn);
float xOffset = xpos - lastX;
float yOffset = lastY - ypos; // 注意y方向翻转

xOffset *= 0.1f;
yOffset *= 0.1f;

// 更新相机位置
camera.Position += camera.Right * xOffset + camera.Up * yOffset;

lastX = xpos;
lastY = ypos;
}

}

void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {
if (button == GLFW_MOUSE_BUTTON_LEFT) {
if(action == GLFW_PRESS){
leftMouseButtonDown = true;
glfwSetCursorPosCallback(window, mouse_callback);
}
else{leftMouseButtonDown = false;}

}
if (button == GLFW_MOUSE_BUTTON_RIGHT) {
if(action == GLFW_PRESS){
rightMouseButtonDown = true;
glfwSetCursorPosCallback(window, mouse_callback);
}
else{leftMouseButtonDown = false;}
}
}
  • 鼠标滚轮

    1
    2
    3
    4
    void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
    {
    camera.ProcessMouseScroll(static_cast<float>(yoffset));
    }
加载窗口,使用之前配置的Model, Mesh, Camera, Shader
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 <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <shader.h>
#include <camera.h>
#include <model.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 10.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

glm::vec3 lightPos(1.2f, 1.0f, 2.0f);


int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "ModelLoading", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

glfwSetMouseButtonCallback(window,mouse_button_callback);
glfwSetScrollCallback(window, scroll_callback);

// tell GLFW to capture our mouse
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}



// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);

// build and compile shaders
// -------------------------
Shader ourShader("../1.model_loading.vs", "../1.model_loading.fs");
Shader lightCubeShader("../configFiles/2.2.light_cube.vs", "../configFiles/2.2.light_cube.fs");

// Shader ourShader("../1.colors.vert", "../1.colors.vert");

// load models
// -----------
Model ourModel("../OBJ_Files/airboat.obj");


// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = static_cast<float>(glfwGetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

// input
// -----
processInput(window);

// render
// ------
// glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// don't forget to enable shader before setting uniforms
ourShader.use();
// be sure to activate shader when setting uniforms/drawing objects
ourShader.setVec3("lightColor", 1.0f, 1.5f, 1.0f);
ourShader.setVec3("lightPos", lightPos);
ourShader.setVec3("viewPos", camera.Position);

// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
ourShader.setMat4("projection", projection);
ourShader.setMat4("view", view);

// render the loaded model
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); // translate it down so it's at the center of the scene
model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
ourShader.setMat4("model", model);
ourModel.Draw(ourShader);
// ourModel2.Draw(ourShader);
// ourModel3.Draw(ourShader);

//--
lightCubeShader.use();
lightCubeShader.setMat4("projection", projection);
lightCubeShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
lightCubeShader.setMat4("model", model);
//--

// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}

// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}

2.3.6 着色器配置文件

由于obj文件没有纹理和材质的信息,因此我加入了光照使得可以看清模型的立体结构

无光照效果如下

image-20231018215433970

加入光照后效果如下

image-20231019165328316
1.model_loading.vs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
1.model_loading.fs
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
#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;

void main()
{
vec3 objectColor = vec3(1.0f, 0.5f, 0.31f);

// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

}

3. 结果与讨论

3.1 结果

初始效果
image-20231019165801231 ##### 向前 image-20231019165841034 ##### 向后 image-20231019165917879 ##### 向上 image-20231019165942015 ##### 旋转 image-20231019170021162

其他具体操作见视频

3.2 讨论

在这一部分,你需要对你在开发过程中遇到的一些困难进行描述和你是如何攻克的,在方法上和结果上展开叙述。
第三章占整体篇幅的30%。

  1. 模型加载和显示问题:最初尝试自己解析和加载OBJ文件时,我遇到了各种问题,包括正确读取文件、解析顶点、法线和纹理坐标数据等。这些问题耗费了很多时间。然而,通过使用Assimp库,加载模型变得更加容易和高效。
  2. 相机控制和光照调试:实现键盘和鼠标控制相机位置和方向时,需要深入理解OpenGL的视图和投影矩阵,以及如何处理用户输入。同时,由于最开始没有材质纹理和光照,显示效果很糟糕,我尝试自己学习添加光照,添加光照效果也需要仔细调整材质、光照强度和颜色等参数,以获得满意的渲染结果。
  3. 着色器编写:编写顶点和片段着色器来实现光照模型是一个有挑战性的任务。理解光照模型的原理,以及如何在着色器中计算环境光、漫反射和镜面反射等部分也很有挑战

4. 收获和建议

课程收获,项目开发收获,课程建议等各个方面展开来叙述。内容上要有意义,量上要占整篇报告的10%左右。

  • 更深入的理解OpenGL:通过项目,我加深了对OpenGL的理解,包括顶点和片段着色器、VBO、VAO等概念。我学会了如何加载和渲染3D模型,以及如何处理用户输入来控制相机位置和方向。
  • 熟悉了Assimp库:使用Assimp库来加载模型是一个非常有用的技能,它使加载不同格式的3D模型变得更加容易。我学到了如何使用Assimp导入模型数据,以及如何在OpenGL中使用这些数据。
Read More